diff --git a/.eslintignore b/.eslintignore index 1eb52b86b10..5535673df8f 100644 --- a/.eslintignore +++ b/.eslintignore @@ -12,6 +12,7 @@ **/CHANGELOG.md !api/release-notes.md !app-shell/build/release-notes.md +**/.yarn-cache/** # components library storybook-static @@ -28,6 +29,9 @@ robot-server/** shared-data/python/** hardware-testing/** +# abr-testing don't format the json protocols +abr-testing/protocols/** + # analyses-snapshot-testing don't format the json protocols analyses-snapshot-testing/files # don't format the snapshots diff --git a/.eslintrc.js b/.eslintrc.js index 7de339ebde5..ec5e3e3536c 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -39,6 +39,7 @@ module.exports = { 'no-case-declarations': 'warn', 'prefer-regex-literals': 'warn', 'react/prop-types': 'warn', + 'react/jsx-curly-brace-presence': 'warn', // Enforce notification hooks 'no-restricted-imports': [ @@ -179,6 +180,8 @@ module.exports = { files: ['./protocol-designer/src/**/*.@(ts|tsx)'], rules: { 'opentrons/no-imports-up-the-tree-of-life': 'warn', + 'opentrons/no-margins-in-css': 'warn', + 'opentrons/no-margins-inline': 'warn', }, }, // apply application structure import requirements to app @@ -186,6 +189,23 @@ module.exports = { files: ['./app/src/**/*.@(ts|tsx)'], rules: { 'opentrons/no-imports-across-applications': 'error', + 'opentrons/no-margins-in-css': 'warn', + 'opentrons/no-margins-inline': 'warn', + }, + }, + { + files: ['./opentrons-ai-client/src/**/*.@(ts|tsx)'], + rules: { + 'opentrons/no-imports-up-the-tree-of-life': 'warn', + 'opentrons/no-margins-in-css': 'warn', + 'opentrons/no-margins-inline': 'warn', + }, + }, + { + files: ['./components/src/**/*.@(ts|tsx)'], + rules: { + 'opentrons/no-margins-in-css': 'warn', + 'opentrons/no-margins-inline': 'warn', }, }, ], diff --git a/.github/actions/.gitattributes b/.github/actions/.gitattributes new file mode 100644 index 00000000000..72f4684a29d --- /dev/null +++ b/.github/actions/.gitattributes @@ -0,0 +1 @@ +odd-resource-analysis/dist/* binary \ No newline at end of file diff --git a/.github/actions/odd-resource-analysis/.gitignore b/.github/actions/odd-resource-analysis/.gitignore new file mode 100644 index 00000000000..368f5c9f9dc --- /dev/null +++ b/.github/actions/odd-resource-analysis/.gitignore @@ -0,0 +1,13 @@ +.DS_Store +.idea +*.log +tmp/ + +*.tern-port +node_modules/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* +*.tsbuildinfo +.npm +.eslintcache diff --git a/.github/actions/odd-resource-analysis/.prettierignore b/.github/actions/odd-resource-analysis/.prettierignore new file mode 100644 index 00000000000..763301fc002 --- /dev/null +++ b/.github/actions/odd-resource-analysis/.prettierignore @@ -0,0 +1,2 @@ +dist/ +node_modules/ \ No newline at end of file diff --git a/.github/actions/odd-resource-analysis/.prettierrc.js b/.github/actions/odd-resource-analysis/.prettierrc.js new file mode 100644 index 00000000000..d9888254456 --- /dev/null +++ b/.github/actions/odd-resource-analysis/.prettierrc.js @@ -0,0 +1,15 @@ +'use strict' + +module.exports = { + printWidth: 80, // default + tabWidth: 2, // default + useTabs: false, // default + semi: false, + singleQuote: true, + jsxSingleQuote: false, // default + trailingComma: 'es5', + bracketSpacing: true, // default + jsxBracketSameLine: false, // default + arrowParens: 'avoid', // default + endOfLine: 'lf', +} diff --git a/.github/actions/odd-resource-analysis/action.yml b/.github/actions/odd-resource-analysis/action.yml new file mode 100644 index 00000000000..f5d6677f6f2 --- /dev/null +++ b/.github/actions/odd-resource-analysis/action.yml @@ -0,0 +1,27 @@ +name: 'ODD Memory Usage Analysis' +description: >- + Analyzes memory usage trends across ODD versions using Mixpanel data. + Note that only processes with positive correlation or explicitly whitelisted processes are shown. + +inputs: + mixpanel-user: + description: 'Mixpanel service account username' + required: true + mixpanel-secret: + description: 'Mixpanel service account password' + required: true + mixpanel-project-id: + description: 'Mixpanel project ID' + required: true + previous-version-count: + description: 'Number of previous versions to analyze' + required: false + default: '2' + +outputs: + analysis-results: + description: 'JSON string containing the complete analysis results' + +runs: + using: 'node16' + main: 'dist/index.js' diff --git a/.github/actions/odd-resource-analysis/action/analyzeMemoryTrends.js b/.github/actions/odd-resource-analysis/action/analyzeMemoryTrends.js new file mode 100644 index 00000000000..139f8719375 --- /dev/null +++ b/.github/actions/odd-resource-analysis/action/analyzeMemoryTrends.js @@ -0,0 +1,295 @@ +const { + parseMixpanelData, + getISODatesForPastMonth, + getMixpanelResourceMonitorDataFor, + downloadAppManifest, + getPrevValidVersions, + latestValidVersionFromManifest, +} = require('./lib/helpers') +const { analyzeCorrelation } = require('./lib/analysis') +const { + AGGREGATED_PROCESSES, + AGGREGATED_PROCESS_NAMES, + BLACKLISTED_PROCESSES, + MINIMUM_VALID_SAMPLE_SIZE, +} = require('./lib/constants') + +const UPTIME_BUCKETS = [ + { min: 0, max: 20, label: '0-20hrs' }, + { min: 20, max: 40, label: '20-40hrs' }, + { min: 40, max: 60, label: '40-60hrs' }, + { min: 60, max: 80, label: '60-80hrs' }, + { min: 80, max: 120, label: '80-120hrs' }, + { min: 120, max: 240, label: '120-240hrs' }, + { min: 240, max: Infinity, label: '240+hrs' }, +] + +/** + * @description Calculate average memory usage for measurements within a specific time range + * @param measurements Array of measurements with uptime and a memory metric + * @param minHours Minimum hours (inclusive) + * @param maxHours Maximum hours (exclusive) + * @param memoryMetric The field to average ('memRssMb' or 'systemAvailMemMb') + * @returns {number | null} Average memory usage or null if no measurements in range + */ +function calculateAverageMemoryForRange( + measurements, + minHours, + maxHours, + memoryMetric = 'memRssMb' +) { + const inRange = measurements.filter( + m => m.uptime >= minHours && m.uptime < maxHours + ) + + if (inRange.length === 0 || inRange.length < MINIMUM_VALID_SAMPLE_SIZE) { + return null + } + + const sum = inRange.reduce((acc, m) => acc + m[memoryMetric], 0) + return sum / inRange.length +} + +/** + * @description Calculate memory usage averages across all defined ranges + * @param measurements Array of measurements with uptime and the memory metric + * @param memoryMetric The field to average ('memRssMb' or 'systemAvailMemMb') + * @returns {Object} Contains averages for each range + */ +function calculateRangeAverages(measurements, memoryMetric = 'memRssMb') { + const averages = {} + UPTIME_BUCKETS.forEach(range => { + const avg = calculateAverageMemoryForRange( + measurements, + range.min, + range.max, + memoryMetric + ) + averages[range.label] = + avg !== null ? avg.toFixed(2) : 'N/A - Not enough data available.' + }) + return averages +} + +/** + * @description Filter the Mixpanel data for the data relevant for memory analysis, aggregating data for certain processes + * and ignoring data for blacklisted processes. + * @param data Mixpanel data. + * @return A tuple of memory data by process and general ODD system memory. + */ +function processMixpanelData(data) { + const processByName = new Map() + const systemMemory = [] + + data.forEach(entry => { + const { + systemUptimeHrs, + systemAvailMemMb, + processesDetails, + } = entry.properties + const uptime = parseFloat(systemUptimeHrs) + + // Validate uptime before adding any measurements + if (isNaN(uptime)) { + return + } + + // Ensure system mem is a valid number before adding it. + const availMemMb = parseFloat(systemAvailMemMb) + if (!isNaN(availMemMb)) { + systemMemory.push({ + uptime, + systemAvailMemMb: availMemMb, + }) + } + + processesDetails.forEach(process => { + const isBlacklisted = BLACKLISTED_PROCESSES.some(pattern => + pattern.test(process.name) + ) + + if (!isBlacklisted) { + let processKey = process.name + // Certain processes are aggregated. + for (const { pattern, key } of AGGREGATED_PROCESSES) { + if (pattern.test(process.name)) { + processKey = key + break + } + } + + const memRssMb = parseFloat(process.memRssMb) + if (!isNaN(memRssMb)) { + if (!processByName.has(processKey)) { + processByName.set(processKey, []) + } + processByName.get(processKey).push({ + memRssMb, + uptime, + }) + } + } + }) + }) + + return [processByName, systemMemory] +} + +/** + * @description Group data by process name and calculate correlation and range averages + * @param data See `analyzeMemoryTrends` + */ +function analyzeProcessMemoryTrends(data) { + const [processByName, systemMemory] = processMixpanelData(data) + + // Filter out any process that has less than the minimum sample size + for (const [processName, measurements] of processByName.entries()) { + if (measurements.length < MINIMUM_VALID_SAMPLE_SIZE) { + processByName.delete(processName) + } + } + + // Calculate correlation coefficient and range averages for each process + const results = new Map() + processByName.forEach((measurements, processName) => { + const analysis = analyzeCorrelation( + measurements.map(m => m.uptime), + measurements.map(m => m.memRssMb) + ) + + results.set(processName, { + correlation: analysis.correlation, + sampleSize: analysis.sampleSize, + interpretation: analysis.interpretation, + averageMemoryMbByUptime: calculateRangeAverages(measurements, 'memRssMb'), + }) + }) + + // Calculate system memory metrics + const systemAnalysis = analyzeCorrelation( + systemMemory.map(m => m.uptime), + systemMemory.map(m => m.systemAvailMemMb) + ) + + results.set('odd-available-memory', { + correlation: systemAnalysis.correlation, + sampleSize: systemAnalysis.sampleSize, + interpretation: systemAnalysis.interpretation, + averageMemoryMbByUptime: calculateRangeAverages( + systemMemory, + 'systemAvailMemMb' + ), + }) + + // Filter out any process with a negative correlation except for a few key ones. + for (const [processName, memResults] of results.entries()) { + if ( + memResults.correlation < 0 && + processName !== 'odd-available-memory' && + ![ + AGGREGATED_PROCESS_NAMES.APP_RENDERER, + AGGREGATED_PROCESS_NAMES.SERVER_UVICORN, + ].includes(processName) + ) { + results.delete(processName) + } + } + + return results +} + +/** + * @description Post-process mixpanel data, returning statistical summaries per process + * @param mixpanelData Each entry is expected to contain a top-level 'properties' field with relevant subfields. + */ +function analyzeMemoryTrends(mixpanelData) { + const parsedData = parseMixpanelData(mixpanelData) + const results = analyzeProcessMemoryTrends(parsedData) + + const analysis = {} + results.forEach((result, processName) => { + analysis[processName] = { + correlation: result.correlation.toFixed(4), + sampleSize: result.sampleSize, + interpretation: result.interpretation, + averageMemoryMbByUptime: result.averageMemoryMbByUptime, + } + }) + + return analysis +} + +/** + * @description The 'where' used as a segmentation expression for Mixpanel data filtering. + */ +function buildWhere(version) { + return `properties["appVersion"]=="${version}" and properties["appMode"]=="ODD"` +} + +/** + * @description Analyze memory trends across multiple versions + * @param {number} previousVersionCount Number of previous versions to analyze + * @param {string} uname Mixpanel service account username. + * @param {string} pwd Mixpanel service account password. + * @param {string} projectId Mixpanel project id. + */ +async function analyzeMemoryTrendsAcrossVersions({ + previousVersionCount, + uname, + pwd, + projectId, +}) { + const manifest = await downloadAppManifest() + const latestValidVersion = latestValidVersionFromManifest(manifest) + const prevValidVersions = getPrevValidVersions( + manifest, + latestValidVersion, + previousVersionCount + ) + const analysisPeriod = getISODatesForPastMonth() + + // Populate backup messaging if there's no data available for a specific version + const noDataAvailableStr = 'N/A - No data available' + const results = { + [latestValidVersion]: noDataAvailableStr, + } + prevValidVersions.forEach(version => { + results[version] = noDataAvailableStr + }) + + // Analyze latest version + const currentVersionData = await getMixpanelResourceMonitorDataFor({ + version: latestValidVersion, + uname, + pwd, + projectId, + fromDate: analysisPeriod.from, + toDate: analysisPeriod.to, + where: buildWhere(latestValidVersion), + }) + + if (currentVersionData) { + results[latestValidVersion] = analyzeMemoryTrends(currentVersionData) + } + + // Analyze previous versions + for (const version of prevValidVersions) { + const versionData = await getMixpanelResourceMonitorDataFor({ + version, + uname, + pwd, + projectId, + fromDate: analysisPeriod.from, + toDate: analysisPeriod.to, + where: buildWhere(version), + }) + + if (versionData) { + results[version] = analyzeMemoryTrends(versionData) + } + } + + return results +} + +module.exports = { analyzeMemoryTrendsAcrossVersions } diff --git a/.github/actions/odd-resource-analysis/action/index.js b/.github/actions/odd-resource-analysis/action/index.js new file mode 100644 index 00000000000..7941e1a2b25 --- /dev/null +++ b/.github/actions/odd-resource-analysis/action/index.js @@ -0,0 +1,50 @@ +const core = require('@actions/core') +const { analyzeMemoryTrendsAcrossVersions } = require('./analyzeMemoryTrends') + +async function run() { + try { + const mixpanelUser = core.getInput('mixpanel-user', { required: true }) + const mixpanelSecret = core.getInput('mixpanel-secret', { required: true }) + const mixpanelProjectId = parseInt( + core.getInput('mixpanel-project-id', { + required: true, + }) + ) + const previousVersionCount = parseInt( + core.getInput('previous-version-count') || '2' + ) + + core.info('Beginning analysis...') + const memoryAnalysis = await analyzeMemoryTrendsAcrossVersions({ + previousVersionCount, + uname: mixpanelUser, + pwd: mixpanelSecret, + projectId: mixpanelProjectId, + }) + + console.log( + 'ODD Available Memory and Processes with Increasing Memory Trend or Selectively Observed by Version (Rolling 1 Month Analysis Window):' + ) + console.log(JSON.stringify(memoryAnalysis, null, 2)) + + const outputText = + 'ODD Available Memory and Processes with Increasing Memory Trend or Selectively Observed by Version (Rolling 1 Month Analysis Window):\n' + + Object.entries(memoryAnalysis) + .map( + ([version, analysis]) => + `\n${version}: ${JSON.stringify(analysis, null, 2)}` + ) + .join('\n') + + core.setOutput('analysis-results', JSON.stringify(memoryAnalysis)) + + await core.summary + .addHeading('ODD Memory Usage Results') + .addCodeBlock(outputText, 'json') + .write() + } catch (error) { + core.setFailed(error.message) + } +} + +run() diff --git a/.github/actions/odd-resource-analysis/action/lib/analysis.js b/.github/actions/odd-resource-analysis/action/lib/analysis.js new file mode 100644 index 00000000000..bc672aace51 --- /dev/null +++ b/.github/actions/odd-resource-analysis/action/lib/analysis.js @@ -0,0 +1,81 @@ +const calculateCorrelation = require('calculate-correlation') +const { MINIMUM_VALID_SAMPLE_SIZE } = require('./constants') + +const P_VALUE_THRESHOLD = 0.05 + +const CORRELATION_THRESHOLDS = { + STRONG: 0.7, + MODERATE: 0.3, +} + +/** + * @description Calculate significance of Pearson correlation coefficient using t-distribution approximation + * @param {number} correlation Pearson correlation coefficient + * @param {number} sampleSize Number of samples + * @returns {number} One-tailed p-value + */ +function calculatePValue(correlation, sampleSize) { + // Convert correlation coefficient to t-statistic + const t = correlation * Math.sqrt((sampleSize - 2) / (1 - correlation ** 2)) + + // Degrees of freedom + const df = sampleSize - 2 + + const x = df / (df + t * t) + return 0.5 * Math.pow(x, df / 2) +} + +/** + * @description Determines correlation strength and direction + * @param {number} correlation Pearson correlation coefficient + * @returns {string} Human readable interpretation + */ +function getCorrelationDescription(correlation) { + const strength = Math.abs(correlation) + const direction = correlation > 0 ? 'positive' : 'negative' + + if (strength > CORRELATION_THRESHOLDS.STRONG) { + return `Strong ${direction} correlation (>${CORRELATION_THRESHOLDS.STRONG})` + } else if (strength > CORRELATION_THRESHOLDS.MODERATE) { + return `Moderate ${direction} correlation (>${CORRELATION_THRESHOLDS.MODERATE} and <${CORRELATION_THRESHOLDS.STRONG})` + } + return `Weak ${direction} correlation (<=${CORRELATION_THRESHOLDS.MODERATE})` +} + +/** + * @description Performs complete correlation analysis including significance testing + * @param {Array} x Array of numbers + * @param {Array} y Array of numbers + * @return {Object} Analysis results including correlation, significance, and interpretation + */ +function analyzeCorrelation(x, y) { + const lowestSampleSize = Math.min(x.length, y.length) + + if (lowestSampleSize < MINIMUM_VALID_SAMPLE_SIZE) { + return { + correlation: 0, + isSignificant: false, + sampleSize: lowestSampleSize, + pValue: 1, + interpretation: 'Not enough samples for analysis', + } + } + + const correlation = calculateCorrelation(x, y, { decimals: 4 }) + const pValue = calculatePValue(correlation, lowestSampleSize) + const isSignificant = pValue < P_VALUE_THRESHOLD + + return { + correlation, + isSignificant, + sampleSize: x.length, + pValue, + interpretation: isSignificant + ? getCorrelationDescription(correlation) + : 'No significant correlation found', + } +} + +module.exports = { + analyzeCorrelation, +} diff --git a/.github/actions/odd-resource-analysis/action/lib/constants.js b/.github/actions/odd-resource-analysis/action/lib/constants.js new file mode 100644 index 00000000000..e6b426422d1 --- /dev/null +++ b/.github/actions/odd-resource-analysis/action/lib/constants.js @@ -0,0 +1,49 @@ +const AGGREGATED_PROCESS_NAMES = { + APP_RENDERER: 'app-renderer-processes', + APP_ZYGOTE: 'app-zygote-processes', + SERVER_UVICORN: 'robot-server-uvicorn-processes', + APP_UTILITY: 'app-utility-processes', +} + +/** + * @description Several processes we care about execute with a lot of unique sub args determined at + * runtime. These processes are aggregated using a regex pattern. + */ +const AGGREGATED_PROCESSES = [ + { + pattern: /^\/opt\/opentrons-app\/opentrons --type=renderer/, + key: AGGREGATED_PROCESS_NAMES.APP_RENDERER, + }, + { + pattern: /^\/opt\/opentrons-app\/opentrons --type=zygote/, + key: AGGREGATED_PROCESS_NAMES.APP_ZYGOTE, + }, + { + pattern: /^python3 -m uvicorn/, + key: AGGREGATED_PROCESS_NAMES.SERVER_UVICORN, + }, + { + pattern: /^\/opt\/opentrons-app\/opentrons --type=utility/, + key: AGGREGATED_PROCESS_NAMES.APP_UTILITY, + }, +] + +/** + * @description Generally don't include any variation of external processes in analysis. + */ +const BLACKLISTED_PROCESSES = [/^nmcli/, /^\/usr\/bin\/python3/] + +/** + * @description For Pearson's, it's generally recommended to use a sample size of at least n=30. + */ +const MINIMUM_VALID_SAMPLE_SIZE = 30 + +const P_VALUE_SIGNIFICANCE_THRESHOLD = 0.05 + +module.exports = { + AGGREGATED_PROCESSES, + AGGREGATED_PROCESS_NAMES, + BLACKLISTED_PROCESSES, + MINIMUM_VALID_SAMPLE_SIZE, + P_VALUE_SIGNIFICANCE_THRESHOLD, +} diff --git a/.github/actions/odd-resource-analysis/action/lib/helpers/date.js b/.github/actions/odd-resource-analysis/action/lib/helpers/date.js new file mode 100644 index 00000000000..fa0bda62d51 --- /dev/null +++ b/.github/actions/odd-resource-analysis/action/lib/helpers/date.js @@ -0,0 +1,22 @@ +/** + * @description Get ISO date strings for the past month from yesterday. + */ +function getISODatesForPastMonth() { + const now = new Date() + // Don't use today's data, because the Mixpanel API seemingly doesn't use UTC timestamps, and + // it's easy to fail a request depending on the time of day it's made. + const yesterday = new Date(now.setDate(now.getDate() - 1)) + const formatDate = date => date.toISOString().split('T')[0] + + const monthAgo = new Date(yesterday) + monthAgo.setMonth(yesterday.getMonth() - 1) + + return { + from: formatDate(monthAgo), + to: formatDate(yesterday), + } +} + +module.exports = { + getISODatesForPastMonth, +} diff --git a/.github/actions/odd-resource-analysis/action/lib/helpers/index.js b/.github/actions/odd-resource-analysis/action/lib/helpers/index.js new file mode 100644 index 00000000000..71991e2d09f --- /dev/null +++ b/.github/actions/odd-resource-analysis/action/lib/helpers/index.js @@ -0,0 +1,5 @@ +module.exports = { + ...require('./date'), + ...require('./manifest'), + ...require('./mixpanel'), +} diff --git a/.github/actions/odd-resource-analysis/action/lib/helpers/manifest.js b/.github/actions/odd-resource-analysis/action/lib/helpers/manifest.js new file mode 100644 index 00000000000..371f5e78cd9 --- /dev/null +++ b/.github/actions/odd-resource-analysis/action/lib/helpers/manifest.js @@ -0,0 +1,52 @@ +const fetch = require('node-fetch') + +const APP_MANIFEST = 'https://builds.opentrons.com/ot3-oe/releases.json' + +async function downloadAppManifest() { + const response = await fetch(APP_MANIFEST) + return await response.json() +} + +/** + * @description Get the most recent app version that is not revoked. + * @param manifest The app manifest + */ +function latestValidVersionFromManifest(manifest) { + const versions = Object.keys(manifest.production) + const latestValidVersion = versions.findLast( + version => !('revoked' in manifest.production[version]) + ) + + if (latestValidVersion != null) { + return latestValidVersion + } else { + throw new Error('No valid versions found') + } +} + +/** + * @description Get `count` latest, previous non revoked versions relative to the latest version. + * @param manifest The app manifest + * @param latestVersion The latest valid version + * @param count Number of previous versions to return + * @returns {string[]} Array of version strings, ordered from newest to oldest + */ +function getPrevValidVersions(manifest, latestVersion, count) { + const versions = Object.keys(manifest.production) + const latestIndex = versions.indexOf(latestVersion) + + if (latestIndex === -1) { + throw new Error('Latest version not found in manifest') + } + + return versions + .slice(0, latestIndex) + .filter(version => !manifest.production[version].revoked) + .slice(-count) + .reverse() +} +module.exports = { + downloadAppManifest, + latestValidVersionFromManifest, + getPrevValidVersions, +} diff --git a/.github/actions/odd-resource-analysis/action/lib/helpers/mixpanel.js b/.github/actions/odd-resource-analysis/action/lib/helpers/mixpanel.js new file mode 100644 index 00000000000..a518d1f4b40 --- /dev/null +++ b/.github/actions/odd-resource-analysis/action/lib/helpers/mixpanel.js @@ -0,0 +1,65 @@ +const fetch = require('node-fetch') + +const MIXPANEL_URL = 'https://data.mixpanel.com/api/2.0/export' + +/** + * @description Base64 encode a username and password in + * @param uname Mixpanel service account username. + * @param pwd Mixpanel service account password. + * @return {string} + */ +function encodeCredentialsForMixpanel(uname, pwd) { + return Buffer.from(`${uname}:${pwd}`).toString('base64') +} + +/** + * @description Cleans up Mixpanel data for post-processing. + * @param data Mixpanel data + */ +function parseMixpanelData(data) { + const lines = data.split('\n').filter(line => line.trim()) + return lines.map(line => JSON.parse(line)) +} + +/** + * @description Make the network request to Mixpanel. + */ +async function getMixpanelResourceMonitorDataFor({ + uname, + pwd, + projectId, + fromDate, + toDate, + where, +}) { + const params = new URLSearchParams({ + project_id: projectId, + from_date: fromDate, + to_date: toDate, + event: '["resourceMonitorReport"]', + where, + }) + + const options = { + method: 'GET', + headers: { + 'Accept-Encoding': 'gzip', + accept: 'text/plain', + authorization: `Basic ${encodeCredentialsForMixpanel(uname, pwd)}`, + }, + } + + const response = await fetch(`${MIXPANEL_URL}?${params}`, options) + const text = await response.text() + if (!response.ok) { + throw new Error( + `Mixpanel request failed: ${response.status}, ${response.statusText}, ${text}` + ) + } + return text +} + +module.exports = { + getMixpanelResourceMonitorDataFor, + parseMixpanelData, +} diff --git a/.github/actions/odd-resource-analysis/dist/101.index.js b/.github/actions/odd-resource-analysis/dist/101.index.js new file mode 100644 index 00000000000..acbcf4681d5 --- /dev/null +++ b/.github/actions/odd-resource-analysis/dist/101.index.js @@ -0,0 +1,469 @@ +'use strict' +exports.id = 101 +exports.ids = [101] +exports.modules = { + /***/ 9101: /***/ ( + __unused_webpack___webpack_module__, + __webpack_exports__, + __webpack_require__ + ) => { + /* harmony export */ __webpack_require__.d(__webpack_exports__, { + /* harmony export */ toFormData: () => /* binding */ toFormData, + /* harmony export */ + }) + /* harmony import */ var fetch_blob_from_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__( + 9802 + ) + /* harmony import */ var formdata_polyfill_esm_min_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__( + 3018 + ) + + let s = 0 + const S = { + START_BOUNDARY: s++, + HEADER_FIELD_START: s++, + HEADER_FIELD: s++, + HEADER_VALUE_START: s++, + HEADER_VALUE: s++, + HEADER_VALUE_ALMOST_DONE: s++, + HEADERS_ALMOST_DONE: s++, + PART_DATA_START: s++, + PART_DATA: s++, + END: s++, + } + + let f = 1 + const F = { + PART_BOUNDARY: f, + LAST_BOUNDARY: (f *= 2), + } + + const LF = 10 + const CR = 13 + const SPACE = 32 + const HYPHEN = 45 + const COLON = 58 + const A = 97 + const Z = 122 + + const lower = c => c | 0x20 + + const noop = () => {} + + class MultipartParser { + /** + * @param {string} boundary + */ + constructor(boundary) { + this.index = 0 + this.flags = 0 + + this.onHeaderEnd = noop + this.onHeaderField = noop + this.onHeadersEnd = noop + this.onHeaderValue = noop + this.onPartBegin = noop + this.onPartData = noop + this.onPartEnd = noop + + this.boundaryChars = {} + + boundary = '\r\n--' + boundary + const ui8a = new Uint8Array(boundary.length) + for (let i = 0; i < boundary.length; i++) { + ui8a[i] = boundary.charCodeAt(i) + this.boundaryChars[ui8a[i]] = true + } + + this.boundary = ui8a + this.lookbehind = new Uint8Array(this.boundary.length + 8) + this.state = S.START_BOUNDARY + } + + /** + * @param {Uint8Array} data + */ + write(data) { + let i = 0 + const length_ = data.length + let previousIndex = this.index + let { lookbehind, boundary, boundaryChars, index, state, flags } = this + const boundaryLength = this.boundary.length + const boundaryEnd = boundaryLength - 1 + const bufferLength = data.length + let c + let cl + + const mark = name => { + this[name + 'Mark'] = i + } + + const clear = name => { + delete this[name + 'Mark'] + } + + const callback = (callbackSymbol, start, end, ui8a) => { + if (start === undefined || start !== end) { + this[callbackSymbol](ui8a && ui8a.subarray(start, end)) + } + } + + const dataCallback = (name, clear) => { + const markSymbol = name + 'Mark' + if (!(markSymbol in this)) { + return + } + + if (clear) { + callback(name, this[markSymbol], i, data) + delete this[markSymbol] + } else { + callback(name, this[markSymbol], data.length, data) + this[markSymbol] = 0 + } + } + + for (i = 0; i < length_; i++) { + c = data[i] + + switch (state) { + case S.START_BOUNDARY: + if (index === boundary.length - 2) { + if (c === HYPHEN) { + flags |= F.LAST_BOUNDARY + } else if (c !== CR) { + return + } + + index++ + break + } else if (index - 1 === boundary.length - 2) { + if (flags & F.LAST_BOUNDARY && c === HYPHEN) { + state = S.END + flags = 0 + } else if (!(flags & F.LAST_BOUNDARY) && c === LF) { + index = 0 + callback('onPartBegin') + state = S.HEADER_FIELD_START + } else { + return + } + + break + } + + if (c !== boundary[index + 2]) { + index = -2 + } + + if (c === boundary[index + 2]) { + index++ + } + + break + case S.HEADER_FIELD_START: + state = S.HEADER_FIELD + mark('onHeaderField') + index = 0 + // falls through + case S.HEADER_FIELD: + if (c === CR) { + clear('onHeaderField') + state = S.HEADERS_ALMOST_DONE + break + } + + index++ + if (c === HYPHEN) { + break + } + + if (c === COLON) { + if (index === 1) { + // empty header field + return + } + + dataCallback('onHeaderField', true) + state = S.HEADER_VALUE_START + break + } + + cl = lower(c) + if (cl < A || cl > Z) { + return + } + + break + case S.HEADER_VALUE_START: + if (c === SPACE) { + break + } + + mark('onHeaderValue') + state = S.HEADER_VALUE + // falls through + case S.HEADER_VALUE: + if (c === CR) { + dataCallback('onHeaderValue', true) + callback('onHeaderEnd') + state = S.HEADER_VALUE_ALMOST_DONE + } + + break + case S.HEADER_VALUE_ALMOST_DONE: + if (c !== LF) { + return + } + + state = S.HEADER_FIELD_START + break + case S.HEADERS_ALMOST_DONE: + if (c !== LF) { + return + } + + callback('onHeadersEnd') + state = S.PART_DATA_START + break + case S.PART_DATA_START: + state = S.PART_DATA + mark('onPartData') + // falls through + case S.PART_DATA: + previousIndex = index + + if (index === 0) { + // boyer-moore derrived algorithm to safely skip non-boundary data + i += boundaryEnd + while (i < bufferLength && !(data[i] in boundaryChars)) { + i += boundaryLength + } + + i -= boundaryEnd + c = data[i] + } + + if (index < boundary.length) { + if (boundary[index] === c) { + if (index === 0) { + dataCallback('onPartData', true) + } + + index++ + } else { + index = 0 + } + } else if (index === boundary.length) { + index++ + if (c === CR) { + // CR = part boundary + flags |= F.PART_BOUNDARY + } else if (c === HYPHEN) { + // HYPHEN = end boundary + flags |= F.LAST_BOUNDARY + } else { + index = 0 + } + } else if (index - 1 === boundary.length) { + if (flags & F.PART_BOUNDARY) { + index = 0 + if (c === LF) { + // unset the PART_BOUNDARY flag + flags &= ~F.PART_BOUNDARY + callback('onPartEnd') + callback('onPartBegin') + state = S.HEADER_FIELD_START + break + } + } else if (flags & F.LAST_BOUNDARY) { + if (c === HYPHEN) { + callback('onPartEnd') + state = S.END + flags = 0 + } else { + index = 0 + } + } else { + index = 0 + } + } + + if (index > 0) { + // when matching a possible boundary, keep a lookbehind reference + // in case it turns out to be a false lead + lookbehind[index - 1] = c + } else if (previousIndex > 0) { + // if our boundary turned out to be rubbish, the captured lookbehind + // belongs to partData + const _lookbehind = new Uint8Array( + lookbehind.buffer, + lookbehind.byteOffset, + lookbehind.byteLength + ) + callback('onPartData', 0, previousIndex, _lookbehind) + previousIndex = 0 + mark('onPartData') + + // reconsider the current character even so it interrupted the sequence + // it could be the beginning of a new sequence + i-- + } + + break + case S.END: + break + default: + throw new Error(`Unexpected state entered: ${state}`) + } + } + + dataCallback('onHeaderField') + dataCallback('onHeaderValue') + dataCallback('onPartData') + + // Update properties for the next call + this.index = index + this.state = state + this.flags = flags + } + + end() { + if ( + (this.state === S.HEADER_FIELD_START && this.index === 0) || + (this.state === S.PART_DATA && this.index === this.boundary.length) + ) { + this.onPartEnd() + } else if (this.state !== S.END) { + throw new Error('MultipartParser.end(): stream ended unexpectedly') + } + } + } + + function _fileName(headerValue) { + // matches either a quoted-string or a token (RFC 2616 section 19.5.1) + const m = headerValue.match( + /\bfilename=("(.*?)"|([^()<>@,;:\\"/[\]?={}\s\t]+))($|;\s)/i + ) + if (!m) { + return + } + + const match = m[2] || m[3] || '' + let filename = match.slice(match.lastIndexOf('\\') + 1) + filename = filename.replace(/%22/g, '"') + filename = filename.replace(/&#(\d{4});/g, (m, code) => { + return String.fromCharCode(code) + }) + return filename + } + + async function toFormData(Body, ct) { + if (!/multipart/i.test(ct)) { + throw new TypeError('Failed to fetch') + } + + const m = ct.match(/boundary=(?:"([^"]+)"|([^;]+))/i) + + if (!m) { + throw new TypeError( + 'no or bad content-type header, no multipart boundary' + ) + } + + const parser = new MultipartParser(m[1] || m[2]) + + let headerField + let headerValue + let entryValue + let entryName + let contentType + let filename + const entryChunks = [] + const formData = new formdata_polyfill_esm_min_js__WEBPACK_IMPORTED_MODULE_1__ /* .FormData */.fS() + + const onPartData = ui8a => { + entryValue += decoder.decode(ui8a, { stream: true }) + } + + const appendToFile = ui8a => { + entryChunks.push(ui8a) + } + + const appendFileToFormData = () => { + const file = new fetch_blob_from_js__WEBPACK_IMPORTED_MODULE_0__ /* .File */.ZH( + entryChunks, + filename, + { type: contentType } + ) + formData.append(entryName, file) + } + + const appendEntryToFormData = () => { + formData.append(entryName, entryValue) + } + + const decoder = new TextDecoder('utf-8') + decoder.decode() + + parser.onPartBegin = function () { + parser.onPartData = onPartData + parser.onPartEnd = appendEntryToFormData + + headerField = '' + headerValue = '' + entryValue = '' + entryName = '' + contentType = '' + filename = null + entryChunks.length = 0 + } + + parser.onHeaderField = function (ui8a) { + headerField += decoder.decode(ui8a, { stream: true }) + } + + parser.onHeaderValue = function (ui8a) { + headerValue += decoder.decode(ui8a, { stream: true }) + } + + parser.onHeaderEnd = function () { + headerValue += decoder.decode() + headerField = headerField.toLowerCase() + + if (headerField === 'content-disposition') { + // matches either a quoted-string or a token (RFC 2616 section 19.5.1) + const m = headerValue.match( + /\bname=("([^"]*)"|([^()<>@,;:\\"/[\]?={}\s\t]+))/i + ) + + if (m) { + entryName = m[2] || m[3] || '' + } + + filename = _fileName(headerValue) + + if (filename) { + parser.onPartData = appendToFile + parser.onPartEnd = appendFileToFormData + } + } else if (headerField === 'content-type') { + contentType = headerValue + } + + headerValue = '' + headerField = '' + } + + for await (const chunk of Body) { + parser.write(chunk) + } + + parser.end() + + return formData + } + + /***/ + }, +} diff --git a/.github/actions/odd-resource-analysis/dist/index.js b/.github/actions/odd-resource-analysis/dist/index.js new file mode 100644 index 00000000000..d1f8bd6cfff --- /dev/null +++ b/.github/actions/odd-resource-analysis/dist/index.js @@ -0,0 +1,35543 @@ +/******/ (() => { // webpackBootstrap +/******/ var __webpack_modules__ = ({ + +/***/ 1548: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +const { + parseMixpanelData, + getISODatesForPastMonth, + getMixpanelResourceMonitorDataFor, + downloadAppManifest, + getPrevValidVersions, + latestValidVersionFromManifest, +} = __nccwpck_require__(3937) +const { analyzeCorrelation } = __nccwpck_require__(725) +const { + AGGREGATED_PROCESSES, + AGGREGATED_PROCESS_NAMES, + BLACKLISTED_PROCESSES, + MINIMUM_VALID_SAMPLE_SIZE, +} = __nccwpck_require__(2038) + +const UPTIME_BUCKETS = [ + { min: 0, max: 20, label: '0-20hrs' }, + { min: 20, max: 40, label: '20-40hrs' }, + { min: 40, max: 60, label: '40-60hrs' }, + { min: 60, max: 80, label: '60-80hrs' }, + { min: 80, max: 120, label: '80-120hrs' }, + { min: 120, max: 240, label: '120-240hrs' }, + { min: 240, max: Infinity, label: '240+hrs' }, +] + +/** + * @description Calculate average memory usage for measurements within a specific time range + * @param measurements Array of measurements with uptime and a memory metric + * @param minHours Minimum hours (inclusive) + * @param maxHours Maximum hours (exclusive) + * @param memoryMetric The field to average ('memRssMb' or 'systemAvailMemMb') + * @returns {number | null} Average memory usage or null if no measurements in range + */ +function calculateAverageMemoryForRange( + measurements, + minHours, + maxHours, + memoryMetric = 'memRssMb' +) { + const inRange = measurements.filter( + m => m.uptime >= minHours && m.uptime < maxHours + ) + + if (inRange.length === 0 || inRange.length < MINIMUM_VALID_SAMPLE_SIZE) { + return null + } + + const sum = inRange.reduce((acc, m) => acc + m[memoryMetric], 0) + return sum / inRange.length +} + +/** + * @description Calculate memory usage averages across all defined ranges + * @param measurements Array of measurements with uptime and the memory metric + * @param memoryMetric The field to average ('memRssMb' or 'systemAvailMemMb') + * @returns {Object} Contains averages for each range + */ +function calculateRangeAverages(measurements, memoryMetric = 'memRssMb') { + const averages = {} + UPTIME_BUCKETS.forEach(range => { + const avg = calculateAverageMemoryForRange( + measurements, + range.min, + range.max, + memoryMetric + ) + averages[range.label] = + avg !== null ? avg.toFixed(2) : 'N/A - Not enough data available.' + }) + return averages +} + +/** + * @description Filter the Mixpanel data for the data relevant for memory analysis, aggregating data for certain processes + * and ignoring data for blacklisted processes. + * @param data Mixpanel data. + * @return A tuple of memory data by process and general ODD system memory. + */ +function processMixpanelData(data) { + const processByName = new Map() + const systemMemory = [] + + data.forEach(entry => { + const { + systemUptimeHrs, + systemAvailMemMb, + processesDetails, + } = entry.properties + const uptime = parseFloat(systemUptimeHrs) + + // Validate uptime before adding any measurements + if (isNaN(uptime)) { + return + } + + // Ensure system mem is a valid number before adding it. + const availMemMb = parseFloat(systemAvailMemMb) + if (!isNaN(availMemMb)) { + systemMemory.push({ + uptime, + systemAvailMemMb: availMemMb, + }) + } + + processesDetails.forEach(process => { + const isBlacklisted = BLACKLISTED_PROCESSES.some(pattern => + pattern.test(process.name) + ) + + if (!isBlacklisted) { + let processKey = process.name + // Certain processes are aggregated. + for (const { pattern, key } of AGGREGATED_PROCESSES) { + if (pattern.test(process.name)) { + processKey = key + break + } + } + + const memRssMb = parseFloat(process.memRssMb) + if (!isNaN(memRssMb)) { + if (!processByName.has(processKey)) { + processByName.set(processKey, []) + } + processByName.get(processKey).push({ + memRssMb, + uptime, + }) + } + } + }) + }) + + return [processByName, systemMemory] +} + +/** + * @description Group data by process name and calculate correlation and range averages + * @param data See `analyzeMemoryTrends` + */ +function analyzeProcessMemoryTrends(data) { + const [processByName, systemMemory] = processMixpanelData(data) + + // Filter out any process that has less than the minimum sample size + for (const [processName, measurements] of processByName.entries()) { + if (measurements.length < MINIMUM_VALID_SAMPLE_SIZE) { + processByName.delete(processName) + } + } + + // Calculate correlation coefficient and range averages for each process + const results = new Map() + processByName.forEach((measurements, processName) => { + const analysis = analyzeCorrelation( + measurements.map(m => m.uptime), + measurements.map(m => m.memRssMb) + ) + + results.set(processName, { + correlation: analysis.correlation, + sampleSize: analysis.sampleSize, + interpretation: analysis.interpretation, + averageMemoryMbByUptime: calculateRangeAverages(measurements, 'memRssMb'), + }) + }) + + // Calculate system memory metrics + const systemAnalysis = analyzeCorrelation( + systemMemory.map(m => m.uptime), + systemMemory.map(m => m.systemAvailMemMb) + ) + + results.set('odd-available-memory', { + correlation: systemAnalysis.correlation, + sampleSize: systemAnalysis.sampleSize, + interpretation: systemAnalysis.interpretation, + averageMemoryMbByUptime: calculateRangeAverages( + systemMemory, + 'systemAvailMemMb' + ), + }) + + // Filter out any process with a negative correlation except for a few key ones. + for (const [processName, memResults] of results.entries()) { + if ( + memResults.correlation < 0 && + processName !== 'odd-available-memory' && + ![ + AGGREGATED_PROCESS_NAMES.APP_RENDERER, + AGGREGATED_PROCESS_NAMES.SERVER_UVICORN, + ].includes(processName) + ) { + results.delete(processName) + } + } + + return results +} + +/** + * @description Post-process mixpanel data, returning statistical summaries per process + * @param mixpanelData Each entry is expected to contain a top-level 'properties' field with relevant subfields. + */ +function analyzeMemoryTrends(mixpanelData) { + const parsedData = parseMixpanelData(mixpanelData) + const results = analyzeProcessMemoryTrends(parsedData) + + const analysis = {} + results.forEach((result, processName) => { + analysis[processName] = { + correlation: result.correlation.toFixed(4), + sampleSize: result.sampleSize, + interpretation: result.interpretation, + averageMemoryMbByUptime: result.averageMemoryMbByUptime, + } + }) + + return analysis +} + +/** + * @description The 'where' used as a segmentation expression for Mixpanel data filtering. + */ +function buildWhere(version) { + return `properties["appVersion"]=="${version}" and properties["appMode"]=="ODD"` +} + +/** + * @description Analyze memory trends across multiple versions + * @param {number} previousVersionCount Number of previous versions to analyze + * @param {string} uname Mixpanel service account username. + * @param {string} pwd Mixpanel service account password. + * @param {string} projectId Mixpanel project id. + */ +async function analyzeMemoryTrendsAcrossVersions({ + previousVersionCount, + uname, + pwd, + projectId, +}) { + const manifest = await downloadAppManifest() + const latestValidVersion = latestValidVersionFromManifest(manifest) + const prevValidVersions = getPrevValidVersions( + manifest, + latestValidVersion, + previousVersionCount + ) + const analysisPeriod = getISODatesForPastMonth() + + // Populate backup messaging if there's no data available for a specific version + const noDataAvailableStr = 'N/A - No data available' + const results = { + [latestValidVersion]: noDataAvailableStr, + } + prevValidVersions.forEach(version => { + results[version] = noDataAvailableStr + }) + + // Analyze latest version + const currentVersionData = await getMixpanelResourceMonitorDataFor({ + version: latestValidVersion, + uname, + pwd, + projectId, + fromDate: analysisPeriod.from, + toDate: analysisPeriod.to, + where: buildWhere(latestValidVersion), + }) + + if (currentVersionData) { + results[latestValidVersion] = analyzeMemoryTrends(currentVersionData) + } + + // Analyze previous versions + for (const version of prevValidVersions) { + const versionData = await getMixpanelResourceMonitorDataFor({ + version, + uname, + pwd, + projectId, + fromDate: analysisPeriod.from, + toDate: analysisPeriod.to, + where: buildWhere(version), + }) + + if (versionData) { + results[version] = analyzeMemoryTrends(versionData) + } + } + + return results +} + +module.exports = { analyzeMemoryTrendsAcrossVersions } + + +/***/ }), + +/***/ 725: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +const calculateCorrelation = __nccwpck_require__(133) +const { MINIMUM_VALID_SAMPLE_SIZE } = __nccwpck_require__(2038) + +const P_VALUE_THRESHOLD = 0.05 + +const CORRELATION_THRESHOLDS = { + STRONG: 0.7, + MODERATE: 0.3, +} + +/** + * @description Calculate significance of Pearson correlation coefficient using t-distribution approximation + * @param {number} correlation Pearson correlation coefficient + * @param {number} sampleSize Number of samples + * @returns {number} One-tailed p-value + */ +function calculatePValue(correlation, sampleSize) { + // Convert correlation coefficient to t-statistic + const t = correlation * Math.sqrt((sampleSize - 2) / (1 - correlation ** 2)) + + // Degrees of freedom + const df = sampleSize - 2 + + const x = df / (df + t * t) + return 0.5 * Math.pow(x, df / 2) +} + +/** + * @description Determines correlation strength and direction + * @param {number} correlation Pearson correlation coefficient + * @returns {string} Human readable interpretation + */ +function getCorrelationDescription(correlation) { + const strength = Math.abs(correlation) + const direction = correlation > 0 ? 'positive' : 'negative' + + if (strength > CORRELATION_THRESHOLDS.STRONG) { + return `Strong ${direction} correlation (>${CORRELATION_THRESHOLDS.STRONG})` + } else if (strength > CORRELATION_THRESHOLDS.MODERATE) { + return `Moderate ${direction} correlation (>${CORRELATION_THRESHOLDS.MODERATE} and <${CORRELATION_THRESHOLDS.STRONG})` + } + return `Weak ${direction} correlation (<=${CORRELATION_THRESHOLDS.MODERATE})` +} + +/** + * @description Performs complete correlation analysis including significance testing + * @param {Array} x Array of numbers + * @param {Array} y Array of numbers + * @return {Object} Analysis results including correlation, significance, and interpretation + */ +function analyzeCorrelation(x, y) { + const lowestSampleSize = Math.min(x.length, y.length) + + if (lowestSampleSize < MINIMUM_VALID_SAMPLE_SIZE) { + return { + correlation: 0, + isSignificant: false, + sampleSize: lowestSampleSize, + pValue: 1, + interpretation: 'Not enough samples for analysis', + } + } + + const correlation = calculateCorrelation(x, y, { decimals: 4 }) + const pValue = calculatePValue(correlation, lowestSampleSize) + const isSignificant = pValue < P_VALUE_THRESHOLD + + return { + correlation, + isSignificant, + sampleSize: x.length, + pValue, + interpretation: isSignificant + ? getCorrelationDescription(correlation) + : 'No significant correlation found', + } +} + +module.exports = { + analyzeCorrelation, +} + + +/***/ }), + +/***/ 2038: +/***/ ((module) => { + +const AGGREGATED_PROCESS_NAMES = { + APP_RENDERER: 'app-renderer-processes', + APP_ZYGOTE: 'app-zygote-processes', + SERVER_UVICORN: 'robot-server-uvicorn-processes', + APP_UTILITY: 'app-utility-processes', +} + +/** + * @description Several processes we care about execute with a lot of unique sub args determined at + * runtime. These processes are aggregated using a regex pattern. + */ +const AGGREGATED_PROCESSES = [ + { + pattern: /^\/opt\/opentrons-app\/opentrons --type=renderer/, + key: AGGREGATED_PROCESS_NAMES.APP_RENDERER, + }, + { + pattern: /^\/opt\/opentrons-app\/opentrons --type=zygote/, + key: AGGREGATED_PROCESS_NAMES.APP_ZYGOTE, + }, + { + pattern: /^python3 -m uvicorn/, + key: AGGREGATED_PROCESS_NAMES.SERVER_UVICORN, + }, + { + pattern: /^\/opt\/opentrons-app\/opentrons --type=utility/, + key: AGGREGATED_PROCESS_NAMES.APP_UTILITY, + }, +] + +/** + * @description Generally don't include any variation of external processes in analysis. + */ +const BLACKLISTED_PROCESSES = [/^nmcli/, /^\/usr\/bin\/python3/] + +/** + * @description For Pearson's, it's generally recommended to use a sample size of at least n=30. + */ +const MINIMUM_VALID_SAMPLE_SIZE = 30 + +const P_VALUE_SIGNIFICANCE_THRESHOLD = 0.05 + +module.exports = { + AGGREGATED_PROCESSES, + AGGREGATED_PROCESS_NAMES, + BLACKLISTED_PROCESSES, + MINIMUM_VALID_SAMPLE_SIZE, + P_VALUE_SIGNIFICANCE_THRESHOLD, +} + + +/***/ }), + +/***/ 9417: +/***/ ((module) => { + +/** + * @description Get ISO date strings for the past month from yesterday. + */ +function getISODatesForPastMonth() { + const now = new Date() + // Don't use today's data, because the Mixpanel API seemingly doesn't use UTC timestamps, and + // it's easy to fail a request depending on the time of day it's made. + const yesterday = new Date(now.setDate(now.getDate() - 1)) + const formatDate = date => date.toISOString().split('T')[0] + + const monthAgo = new Date(yesterday) + monthAgo.setMonth(yesterday.getMonth() - 1) + + return { + from: formatDate(monthAgo), + to: formatDate(yesterday), + } +} + +module.exports = { + getISODatesForPastMonth, +} + + +/***/ }), + +/***/ 3937: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +module.exports = { + ...__nccwpck_require__(9417), + ...__nccwpck_require__(7440), + ...__nccwpck_require__(9437), +} + + +/***/ }), + +/***/ 7440: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +const fetch = __nccwpck_require__(6705) + +const APP_MANIFEST = 'https://builds.opentrons.com/ot3-oe/releases.json' + +async function downloadAppManifest() { + const response = await fetch(APP_MANIFEST) + return await response.json() +} + +/** + * @description Get the most recent app version that is not revoked. + * @param manifest The app manifest + */ +function latestValidVersionFromManifest(manifest) { + const versions = Object.keys(manifest.production) + const latestValidVersion = versions.findLast( + version => !('revoked' in manifest.production[version]) + ) + + if (latestValidVersion != null) { + return latestValidVersion + } else { + throw new Error('No valid versions found') + } +} + +/** + * @description Get `count` latest, previous non revoked versions relative to the latest version. + * @param manifest The app manifest + * @param latestVersion The latest valid version + * @param count Number of previous versions to return + * @returns {string[]} Array of version strings, ordered from newest to oldest + */ +function getPrevValidVersions(manifest, latestVersion, count) { + const versions = Object.keys(manifest.production) + const latestIndex = versions.indexOf(latestVersion) + + if (latestIndex === -1) { + throw new Error('Latest version not found in manifest') + } + + return versions + .slice(0, latestIndex) + .filter(version => !manifest.production[version].revoked) + .slice(-count) + .reverse() +} +module.exports = { + downloadAppManifest, + latestValidVersionFromManifest, + getPrevValidVersions, +} + + +/***/ }), + +/***/ 9437: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +const fetch = __nccwpck_require__(6705) + +const MIXPANEL_URL = 'https://data.mixpanel.com/api/2.0/export' + +/** + * @description Base64 encode a username and password in + * @param uname Mixpanel service account username. + * @param pwd Mixpanel service account password. + * @return {string} + */ +function encodeCredentialsForMixpanel(uname, pwd) { + return Buffer.from(`${uname}:${pwd}`).toString('base64') +} + +/** + * @description Cleans up Mixpanel data for post-processing. + * @param data Mixpanel data + */ +function parseMixpanelData(data) { + const lines = data.split('\n').filter(line => line.trim()) + return lines.map(line => JSON.parse(line)) +} + +/** + * @description Make the network request to Mixpanel. + */ +async function getMixpanelResourceMonitorDataFor({ + uname, + pwd, + projectId, + fromDate, + toDate, + where, +}) { + const params = new URLSearchParams({ + project_id: parseInt(projectId), + from_date: fromDate, + to_date: toDate, + event: '["resourceMonitorReport"]', + where, + }) + + const options = { + method: 'GET', + headers: { + 'Accept-Encoding': 'gzip', + accept: 'text/plain', + authorization: `Basic ${encodeCredentialsForMixpanel(uname, pwd)}`, + }, + } + + const response = await fetch(`${MIXPANEL_URL}?${params}`, options) + const text = await response.text() + if (!response.ok) { + throw new Error( + `Mixpanel request failed: ${response.status}, ${response.statusText}, ${text}` + ) + } + return text +} + +module.exports = { + getMixpanelResourceMonitorDataFor, + parseMixpanelData, +} + + +/***/ }), + +/***/ 4914: +/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { + +"use strict"; + +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.issue = exports.issueCommand = void 0; +const os = __importStar(__nccwpck_require__(857)); +const utils_1 = __nccwpck_require__(302); +/** + * Commands + * + * Command Format: + * ::name key=value,key=value::message + * + * Examples: + * ::warning::This is the message + * ::set-env name=MY_VAR::some value + */ +function issueCommand(command, properties, message) { + const cmd = new Command(command, properties, message); + process.stdout.write(cmd.toString() + os.EOL); +} +exports.issueCommand = issueCommand; +function issue(name, message = '') { + issueCommand(name, {}, message); +} +exports.issue = issue; +const CMD_STRING = '::'; +class Command { + constructor(command, properties, message) { + if (!command) { + command = 'missing.command'; + } + this.command = command; + this.properties = properties; + this.message = message; + } + toString() { + let cmdStr = CMD_STRING + this.command; + if (this.properties && Object.keys(this.properties).length > 0) { + cmdStr += ' '; + let first = true; + for (const key in this.properties) { + if (this.properties.hasOwnProperty(key)) { + const val = this.properties[key]; + if (val) { + if (first) { + first = false; + } + else { + cmdStr += ','; + } + cmdStr += `${key}=${escapeProperty(val)}`; + } + } + } + } + cmdStr += `${CMD_STRING}${escapeData(this.message)}`; + return cmdStr; + } +} +function escapeData(s) { + return (0, utils_1.toCommandValue)(s) + .replace(/%/g, '%25') + .replace(/\r/g, '%0D') + .replace(/\n/g, '%0A'); +} +function escapeProperty(s) { + return (0, utils_1.toCommandValue)(s) + .replace(/%/g, '%25') + .replace(/\r/g, '%0D') + .replace(/\n/g, '%0A') + .replace(/:/g, '%3A') + .replace(/,/g, '%2C'); +} +//# sourceMappingURL=command.js.map + +/***/ }), + +/***/ 7484: +/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { + +"use strict"; + +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.platform = exports.toPlatformPath = exports.toWin32Path = exports.toPosixPath = exports.markdownSummary = exports.summary = exports.getIDToken = exports.getState = exports.saveState = exports.group = exports.endGroup = exports.startGroup = exports.info = exports.notice = exports.warning = exports.error = exports.debug = exports.isDebug = exports.setFailed = exports.setCommandEcho = exports.setOutput = exports.getBooleanInput = exports.getMultilineInput = exports.getInput = exports.addPath = exports.setSecret = exports.exportVariable = exports.ExitCode = void 0; +const command_1 = __nccwpck_require__(4914); +const file_command_1 = __nccwpck_require__(4753); +const utils_1 = __nccwpck_require__(302); +const os = __importStar(__nccwpck_require__(857)); +const path = __importStar(__nccwpck_require__(6928)); +const oidc_utils_1 = __nccwpck_require__(5306); +/** + * The code to exit an action + */ +var ExitCode; +(function (ExitCode) { + /** + * A code indicating that the action was successful + */ + ExitCode[ExitCode["Success"] = 0] = "Success"; + /** + * A code indicating that the action was a failure + */ + ExitCode[ExitCode["Failure"] = 1] = "Failure"; +})(ExitCode || (exports.ExitCode = ExitCode = {})); +//----------------------------------------------------------------------- +// Variables +//----------------------------------------------------------------------- +/** + * Sets env variable for this action and future actions in the job + * @param name the name of the variable to set + * @param val the value of the variable. Non-string values will be converted to a string via JSON.stringify + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function exportVariable(name, val) { + const convertedVal = (0, utils_1.toCommandValue)(val); + process.env[name] = convertedVal; + const filePath = process.env['GITHUB_ENV'] || ''; + if (filePath) { + return (0, file_command_1.issueFileCommand)('ENV', (0, file_command_1.prepareKeyValueMessage)(name, val)); + } + (0, command_1.issueCommand)('set-env', { name }, convertedVal); +} +exports.exportVariable = exportVariable; +/** + * Registers a secret which will get masked from logs + * @param secret value of the secret + */ +function setSecret(secret) { + (0, command_1.issueCommand)('add-mask', {}, secret); +} +exports.setSecret = setSecret; +/** + * Prepends inputPath to the PATH (for this action and future actions) + * @param inputPath + */ +function addPath(inputPath) { + const filePath = process.env['GITHUB_PATH'] || ''; + if (filePath) { + (0, file_command_1.issueFileCommand)('PATH', inputPath); + } + else { + (0, command_1.issueCommand)('add-path', {}, inputPath); + } + process.env['PATH'] = `${inputPath}${path.delimiter}${process.env['PATH']}`; +} +exports.addPath = addPath; +/** + * Gets the value of an input. + * Unless trimWhitespace is set to false in InputOptions, the value is also trimmed. + * Returns an empty string if the value is not defined. + * + * @param name name of the input to get + * @param options optional. See InputOptions. + * @returns string + */ +function getInput(name, options) { + const val = process.env[`INPUT_${name.replace(/ /g, '_').toUpperCase()}`] || ''; + if (options && options.required && !val) { + throw new Error(`Input required and not supplied: ${name}`); + } + if (options && options.trimWhitespace === false) { + return val; + } + return val.trim(); +} +exports.getInput = getInput; +/** + * Gets the values of an multiline input. Each value is also trimmed. + * + * @param name name of the input to get + * @param options optional. See InputOptions. + * @returns string[] + * + */ +function getMultilineInput(name, options) { + const inputs = getInput(name, options) + .split('\n') + .filter(x => x !== ''); + if (options && options.trimWhitespace === false) { + return inputs; + } + return inputs.map(input => input.trim()); +} +exports.getMultilineInput = getMultilineInput; +/** + * Gets the input value of the boolean type in the YAML 1.2 "core schema" specification. + * Support boolean input list: `true | True | TRUE | false | False | FALSE` . + * The return value is also in boolean type. + * ref: https://yaml.org/spec/1.2/spec.html#id2804923 + * + * @param name name of the input to get + * @param options optional. See InputOptions. + * @returns boolean + */ +function getBooleanInput(name, options) { + const trueValue = ['true', 'True', 'TRUE']; + const falseValue = ['false', 'False', 'FALSE']; + const val = getInput(name, options); + if (trueValue.includes(val)) + return true; + if (falseValue.includes(val)) + return false; + throw new TypeError(`Input does not meet YAML 1.2 "Core Schema" specification: ${name}\n` + + `Support boolean input list: \`true | True | TRUE | false | False | FALSE\``); +} +exports.getBooleanInput = getBooleanInput; +/** + * Sets the value of an output. + * + * @param name name of the output to set + * @param value value to store. Non-string values will be converted to a string via JSON.stringify + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function setOutput(name, value) { + const filePath = process.env['GITHUB_OUTPUT'] || ''; + if (filePath) { + return (0, file_command_1.issueFileCommand)('OUTPUT', (0, file_command_1.prepareKeyValueMessage)(name, value)); + } + process.stdout.write(os.EOL); + (0, command_1.issueCommand)('set-output', { name }, (0, utils_1.toCommandValue)(value)); +} +exports.setOutput = setOutput; +/** + * Enables or disables the echoing of commands into stdout for the rest of the step. + * Echoing is disabled by default if ACTIONS_STEP_DEBUG is not set. + * + */ +function setCommandEcho(enabled) { + (0, command_1.issue)('echo', enabled ? 'on' : 'off'); +} +exports.setCommandEcho = setCommandEcho; +//----------------------------------------------------------------------- +// Results +//----------------------------------------------------------------------- +/** + * Sets the action status to failed. + * When the action exits it will be with an exit code of 1 + * @param message add error issue message + */ +function setFailed(message) { + process.exitCode = ExitCode.Failure; + error(message); +} +exports.setFailed = setFailed; +//----------------------------------------------------------------------- +// Logging Commands +//----------------------------------------------------------------------- +/** + * Gets whether Actions Step Debug is on or not + */ +function isDebug() { + return process.env['RUNNER_DEBUG'] === '1'; +} +exports.isDebug = isDebug; +/** + * Writes debug message to user log + * @param message debug message + */ +function debug(message) { + (0, command_1.issueCommand)('debug', {}, message); +} +exports.debug = debug; +/** + * Adds an error issue + * @param message error issue message. Errors will be converted to string via toString() + * @param properties optional properties to add to the annotation. + */ +function error(message, properties = {}) { + (0, command_1.issueCommand)('error', (0, utils_1.toCommandProperties)(properties), message instanceof Error ? message.toString() : message); +} +exports.error = error; +/** + * Adds a warning issue + * @param message warning issue message. Errors will be converted to string via toString() + * @param properties optional properties to add to the annotation. + */ +function warning(message, properties = {}) { + (0, command_1.issueCommand)('warning', (0, utils_1.toCommandProperties)(properties), message instanceof Error ? message.toString() : message); +} +exports.warning = warning; +/** + * Adds a notice issue + * @param message notice issue message. Errors will be converted to string via toString() + * @param properties optional properties to add to the annotation. + */ +function notice(message, properties = {}) { + (0, command_1.issueCommand)('notice', (0, utils_1.toCommandProperties)(properties), message instanceof Error ? message.toString() : message); +} +exports.notice = notice; +/** + * Writes info to log with console.log. + * @param message info message + */ +function info(message) { + process.stdout.write(message + os.EOL); +} +exports.info = info; +/** + * Begin an output group. + * + * Output until the next `groupEnd` will be foldable in this group + * + * @param name The name of the output group + */ +function startGroup(name) { + (0, command_1.issue)('group', name); +} +exports.startGroup = startGroup; +/** + * End an output group. + */ +function endGroup() { + (0, command_1.issue)('endgroup'); +} +exports.endGroup = endGroup; +/** + * Wrap an asynchronous function call in a group. + * + * Returns the same type as the function itself. + * + * @param name The name of the group + * @param fn The function to wrap in the group + */ +function group(name, fn) { + return __awaiter(this, void 0, void 0, function* () { + startGroup(name); + let result; + try { + result = yield fn(); + } + finally { + endGroup(); + } + return result; + }); +} +exports.group = group; +//----------------------------------------------------------------------- +// Wrapper action state +//----------------------------------------------------------------------- +/** + * Saves state for current action, the state can only be retrieved by this action's post job execution. + * + * @param name name of the state to store + * @param value value to store. Non-string values will be converted to a string via JSON.stringify + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function saveState(name, value) { + const filePath = process.env['GITHUB_STATE'] || ''; + if (filePath) { + return (0, file_command_1.issueFileCommand)('STATE', (0, file_command_1.prepareKeyValueMessage)(name, value)); + } + (0, command_1.issueCommand)('save-state', { name }, (0, utils_1.toCommandValue)(value)); +} +exports.saveState = saveState; +/** + * Gets the value of an state set by this action's main execution. + * + * @param name name of the state to get + * @returns string + */ +function getState(name) { + return process.env[`STATE_${name}`] || ''; +} +exports.getState = getState; +function getIDToken(aud) { + return __awaiter(this, void 0, void 0, function* () { + return yield oidc_utils_1.OidcClient.getIDToken(aud); + }); +} +exports.getIDToken = getIDToken; +/** + * Summary exports + */ +var summary_1 = __nccwpck_require__(1847); +Object.defineProperty(exports, "summary", ({ enumerable: true, get: function () { return summary_1.summary; } })); +/** + * @deprecated use core.summary + */ +var summary_2 = __nccwpck_require__(1847); +Object.defineProperty(exports, "markdownSummary", ({ enumerable: true, get: function () { return summary_2.markdownSummary; } })); +/** + * Path exports + */ +var path_utils_1 = __nccwpck_require__(1976); +Object.defineProperty(exports, "toPosixPath", ({ enumerable: true, get: function () { return path_utils_1.toPosixPath; } })); +Object.defineProperty(exports, "toWin32Path", ({ enumerable: true, get: function () { return path_utils_1.toWin32Path; } })); +Object.defineProperty(exports, "toPlatformPath", ({ enumerable: true, get: function () { return path_utils_1.toPlatformPath; } })); +/** + * Platform utilities exports + */ +exports.platform = __importStar(__nccwpck_require__(8968)); +//# sourceMappingURL=core.js.map + +/***/ }), + +/***/ 4753: +/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { + +"use strict"; + +// For internal use, subject to change. +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.prepareKeyValueMessage = exports.issueFileCommand = void 0; +// We use any as a valid input type +/* eslint-disable @typescript-eslint/no-explicit-any */ +const crypto = __importStar(__nccwpck_require__(6982)); +const fs = __importStar(__nccwpck_require__(9896)); +const os = __importStar(__nccwpck_require__(857)); +const utils_1 = __nccwpck_require__(302); +function issueFileCommand(command, message) { + const filePath = process.env[`GITHUB_${command}`]; + if (!filePath) { + throw new Error(`Unable to find environment variable for file command ${command}`); + } + if (!fs.existsSync(filePath)) { + throw new Error(`Missing file at path: ${filePath}`); + } + fs.appendFileSync(filePath, `${(0, utils_1.toCommandValue)(message)}${os.EOL}`, { + encoding: 'utf8' + }); +} +exports.issueFileCommand = issueFileCommand; +function prepareKeyValueMessage(key, value) { + const delimiter = `ghadelimiter_${crypto.randomUUID()}`; + const convertedValue = (0, utils_1.toCommandValue)(value); + // These should realistically never happen, but just in case someone finds a + // way to exploit uuid generation let's not allow keys or values that contain + // the delimiter. + if (key.includes(delimiter)) { + throw new Error(`Unexpected input: name should not contain the delimiter "${delimiter}"`); + } + if (convertedValue.includes(delimiter)) { + throw new Error(`Unexpected input: value should not contain the delimiter "${delimiter}"`); + } + return `${key}<<${delimiter}${os.EOL}${convertedValue}${os.EOL}${delimiter}`; +} +exports.prepareKeyValueMessage = prepareKeyValueMessage; +//# sourceMappingURL=file-command.js.map + +/***/ }), + +/***/ 5306: +/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { + +"use strict"; + +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.OidcClient = void 0; +const http_client_1 = __nccwpck_require__(4844); +const auth_1 = __nccwpck_require__(4552); +const core_1 = __nccwpck_require__(7484); +class OidcClient { + static createHttpClient(allowRetry = true, maxRetry = 10) { + const requestOptions = { + allowRetries: allowRetry, + maxRetries: maxRetry + }; + return new http_client_1.HttpClient('actions/oidc-client', [new auth_1.BearerCredentialHandler(OidcClient.getRequestToken())], requestOptions); + } + static getRequestToken() { + const token = process.env['ACTIONS_ID_TOKEN_REQUEST_TOKEN']; + if (!token) { + throw new Error('Unable to get ACTIONS_ID_TOKEN_REQUEST_TOKEN env variable'); + } + return token; + } + static getIDTokenUrl() { + const runtimeUrl = process.env['ACTIONS_ID_TOKEN_REQUEST_URL']; + if (!runtimeUrl) { + throw new Error('Unable to get ACTIONS_ID_TOKEN_REQUEST_URL env variable'); + } + return runtimeUrl; + } + static getCall(id_token_url) { + var _a; + return __awaiter(this, void 0, void 0, function* () { + const httpclient = OidcClient.createHttpClient(); + const res = yield httpclient + .getJson(id_token_url) + .catch(error => { + throw new Error(`Failed to get ID Token. \n + Error Code : ${error.statusCode}\n + Error Message: ${error.message}`); + }); + const id_token = (_a = res.result) === null || _a === void 0 ? void 0 : _a.value; + if (!id_token) { + throw new Error('Response json body do not have ID Token field'); + } + return id_token; + }); + } + static getIDToken(audience) { + return __awaiter(this, void 0, void 0, function* () { + try { + // New ID Token is requested from action service + let id_token_url = OidcClient.getIDTokenUrl(); + if (audience) { + const encodedAudience = encodeURIComponent(audience); + id_token_url = `${id_token_url}&audience=${encodedAudience}`; + } + (0, core_1.debug)(`ID token url is ${id_token_url}`); + const id_token = yield OidcClient.getCall(id_token_url); + (0, core_1.setSecret)(id_token); + return id_token; + } + catch (error) { + throw new Error(`Error message: ${error.message}`); + } + }); + } +} +exports.OidcClient = OidcClient; +//# sourceMappingURL=oidc-utils.js.map + +/***/ }), + +/***/ 1976: +/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { + +"use strict"; + +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.toPlatformPath = exports.toWin32Path = exports.toPosixPath = void 0; +const path = __importStar(__nccwpck_require__(6928)); +/** + * toPosixPath converts the given path to the posix form. On Windows, \\ will be + * replaced with /. + * + * @param pth. Path to transform. + * @return string Posix path. + */ +function toPosixPath(pth) { + return pth.replace(/[\\]/g, '/'); +} +exports.toPosixPath = toPosixPath; +/** + * toWin32Path converts the given path to the win32 form. On Linux, / will be + * replaced with \\. + * + * @param pth. Path to transform. + * @return string Win32 path. + */ +function toWin32Path(pth) { + return pth.replace(/[/]/g, '\\'); +} +exports.toWin32Path = toWin32Path; +/** + * toPlatformPath converts the given path to a platform-specific path. It does + * this by replacing instances of / and \ with the platform-specific path + * separator. + * + * @param pth The path to platformize. + * @return string The platform-specific path. + */ +function toPlatformPath(pth) { + return pth.replace(/[/\\]/g, path.sep); +} +exports.toPlatformPath = toPlatformPath; +//# sourceMappingURL=path-utils.js.map + +/***/ }), + +/***/ 8968: +/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { + +"use strict"; + +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.getDetails = exports.isLinux = exports.isMacOS = exports.isWindows = exports.arch = exports.platform = void 0; +const os_1 = __importDefault(__nccwpck_require__(857)); +const exec = __importStar(__nccwpck_require__(5236)); +const getWindowsInfo = () => __awaiter(void 0, void 0, void 0, function* () { + const { stdout: version } = yield exec.getExecOutput('powershell -command "(Get-CimInstance -ClassName Win32_OperatingSystem).Version"', undefined, { + silent: true + }); + const { stdout: name } = yield exec.getExecOutput('powershell -command "(Get-CimInstance -ClassName Win32_OperatingSystem).Caption"', undefined, { + silent: true + }); + return { + name: name.trim(), + version: version.trim() + }; +}); +const getMacOsInfo = () => __awaiter(void 0, void 0, void 0, function* () { + var _a, _b, _c, _d; + const { stdout } = yield exec.getExecOutput('sw_vers', undefined, { + silent: true + }); + const version = (_b = (_a = stdout.match(/ProductVersion:\s*(.+)/)) === null || _a === void 0 ? void 0 : _a[1]) !== null && _b !== void 0 ? _b : ''; + const name = (_d = (_c = stdout.match(/ProductName:\s*(.+)/)) === null || _c === void 0 ? void 0 : _c[1]) !== null && _d !== void 0 ? _d : ''; + return { + name, + version + }; +}); +const getLinuxInfo = () => __awaiter(void 0, void 0, void 0, function* () { + const { stdout } = yield exec.getExecOutput('lsb_release', ['-i', '-r', '-s'], { + silent: true + }); + const [name, version] = stdout.trim().split('\n'); + return { + name, + version + }; +}); +exports.platform = os_1.default.platform(); +exports.arch = os_1.default.arch(); +exports.isWindows = exports.platform === 'win32'; +exports.isMacOS = exports.platform === 'darwin'; +exports.isLinux = exports.platform === 'linux'; +function getDetails() { + return __awaiter(this, void 0, void 0, function* () { + return Object.assign(Object.assign({}, (yield (exports.isWindows + ? getWindowsInfo() + : exports.isMacOS + ? getMacOsInfo() + : getLinuxInfo()))), { platform: exports.platform, + arch: exports.arch, + isWindows: exports.isWindows, + isMacOS: exports.isMacOS, + isLinux: exports.isLinux }); + }); +} +exports.getDetails = getDetails; +//# sourceMappingURL=platform.js.map + +/***/ }), + +/***/ 1847: +/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { + +"use strict"; + +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.summary = exports.markdownSummary = exports.SUMMARY_DOCS_URL = exports.SUMMARY_ENV_VAR = void 0; +const os_1 = __nccwpck_require__(857); +const fs_1 = __nccwpck_require__(9896); +const { access, appendFile, writeFile } = fs_1.promises; +exports.SUMMARY_ENV_VAR = 'GITHUB_STEP_SUMMARY'; +exports.SUMMARY_DOCS_URL = 'https://docs.github.com/actions/using-workflows/workflow-commands-for-github-actions#adding-a-job-summary'; +class Summary { + constructor() { + this._buffer = ''; + } + /** + * Finds the summary file path from the environment, rejects if env var is not found or file does not exist + * Also checks r/w permissions. + * + * @returns step summary file path + */ + filePath() { + return __awaiter(this, void 0, void 0, function* () { + if (this._filePath) { + return this._filePath; + } + const pathFromEnv = process.env[exports.SUMMARY_ENV_VAR]; + if (!pathFromEnv) { + throw new Error(`Unable to find environment variable for $${exports.SUMMARY_ENV_VAR}. Check if your runtime environment supports job summaries.`); + } + try { + yield access(pathFromEnv, fs_1.constants.R_OK | fs_1.constants.W_OK); + } + catch (_a) { + throw new Error(`Unable to access summary file: '${pathFromEnv}'. Check if the file has correct read/write permissions.`); + } + this._filePath = pathFromEnv; + return this._filePath; + }); + } + /** + * Wraps content in an HTML tag, adding any HTML attributes + * + * @param {string} tag HTML tag to wrap + * @param {string | null} content content within the tag + * @param {[attribute: string]: string} attrs key-value list of HTML attributes to add + * + * @returns {string} content wrapped in HTML element + */ + wrap(tag, content, attrs = {}) { + const htmlAttrs = Object.entries(attrs) + .map(([key, value]) => ` ${key}="${value}"`) + .join(''); + if (!content) { + return `<${tag}${htmlAttrs}>`; + } + return `<${tag}${htmlAttrs}>${content}`; + } + /** + * Writes text in the buffer to the summary buffer file and empties buffer. Will append by default. + * + * @param {SummaryWriteOptions} [options] (optional) options for write operation + * + * @returns {Promise} summary instance + */ + write(options) { + return __awaiter(this, void 0, void 0, function* () { + const overwrite = !!(options === null || options === void 0 ? void 0 : options.overwrite); + const filePath = yield this.filePath(); + const writeFunc = overwrite ? writeFile : appendFile; + yield writeFunc(filePath, this._buffer, { encoding: 'utf8' }); + return this.emptyBuffer(); + }); + } + /** + * Clears the summary buffer and wipes the summary file + * + * @returns {Summary} summary instance + */ + clear() { + return __awaiter(this, void 0, void 0, function* () { + return this.emptyBuffer().write({ overwrite: true }); + }); + } + /** + * Returns the current summary buffer as a string + * + * @returns {string} string of summary buffer + */ + stringify() { + return this._buffer; + } + /** + * If the summary buffer is empty + * + * @returns {boolen} true if the buffer is empty + */ + isEmptyBuffer() { + return this._buffer.length === 0; + } + /** + * Resets the summary buffer without writing to summary file + * + * @returns {Summary} summary instance + */ + emptyBuffer() { + this._buffer = ''; + return this; + } + /** + * Adds raw text to the summary buffer + * + * @param {string} text content to add + * @param {boolean} [addEOL=false] (optional) append an EOL to the raw text (default: false) + * + * @returns {Summary} summary instance + */ + addRaw(text, addEOL = false) { + this._buffer += text; + return addEOL ? this.addEOL() : this; + } + /** + * Adds the operating system-specific end-of-line marker to the buffer + * + * @returns {Summary} summary instance + */ + addEOL() { + return this.addRaw(os_1.EOL); + } + /** + * Adds an HTML codeblock to the summary buffer + * + * @param {string} code content to render within fenced code block + * @param {string} lang (optional) language to syntax highlight code + * + * @returns {Summary} summary instance + */ + addCodeBlock(code, lang) { + const attrs = Object.assign({}, (lang && { lang })); + const element = this.wrap('pre', this.wrap('code', code), attrs); + return this.addRaw(element).addEOL(); + } + /** + * Adds an HTML list to the summary buffer + * + * @param {string[]} items list of items to render + * @param {boolean} [ordered=false] (optional) if the rendered list should be ordered or not (default: false) + * + * @returns {Summary} summary instance + */ + addList(items, ordered = false) { + const tag = ordered ? 'ol' : 'ul'; + const listItems = items.map(item => this.wrap('li', item)).join(''); + const element = this.wrap(tag, listItems); + return this.addRaw(element).addEOL(); + } + /** + * Adds an HTML table to the summary buffer + * + * @param {SummaryTableCell[]} rows table rows + * + * @returns {Summary} summary instance + */ + addTable(rows) { + const tableBody = rows + .map(row => { + const cells = row + .map(cell => { + if (typeof cell === 'string') { + return this.wrap('td', cell); + } + const { header, data, colspan, rowspan } = cell; + const tag = header ? 'th' : 'td'; + const attrs = Object.assign(Object.assign({}, (colspan && { colspan })), (rowspan && { rowspan })); + return this.wrap(tag, data, attrs); + }) + .join(''); + return this.wrap('tr', cells); + }) + .join(''); + const element = this.wrap('table', tableBody); + return this.addRaw(element).addEOL(); + } + /** + * Adds a collapsable HTML details element to the summary buffer + * + * @param {string} label text for the closed state + * @param {string} content collapsable content + * + * @returns {Summary} summary instance + */ + addDetails(label, content) { + const element = this.wrap('details', this.wrap('summary', label) + content); + return this.addRaw(element).addEOL(); + } + /** + * Adds an HTML image tag to the summary buffer + * + * @param {string} src path to the image you to embed + * @param {string} alt text description of the image + * @param {SummaryImageOptions} options (optional) addition image attributes + * + * @returns {Summary} summary instance + */ + addImage(src, alt, options) { + const { width, height } = options || {}; + const attrs = Object.assign(Object.assign({}, (width && { width })), (height && { height })); + const element = this.wrap('img', null, Object.assign({ src, alt }, attrs)); + return this.addRaw(element).addEOL(); + } + /** + * Adds an HTML section heading element + * + * @param {string} text heading text + * @param {number | string} [level=1] (optional) the heading level, default: 1 + * + * @returns {Summary} summary instance + */ + addHeading(text, level) { + const tag = `h${level}`; + const allowedTag = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(tag) + ? tag + : 'h1'; + const element = this.wrap(allowedTag, text); + return this.addRaw(element).addEOL(); + } + /** + * Adds an HTML thematic break (
) to the summary buffer + * + * @returns {Summary} summary instance + */ + addSeparator() { + const element = this.wrap('hr', null); + return this.addRaw(element).addEOL(); + } + /** + * Adds an HTML line break (
) to the summary buffer + * + * @returns {Summary} summary instance + */ + addBreak() { + const element = this.wrap('br', null); + return this.addRaw(element).addEOL(); + } + /** + * Adds an HTML blockquote to the summary buffer + * + * @param {string} text quote text + * @param {string} cite (optional) citation url + * + * @returns {Summary} summary instance + */ + addQuote(text, cite) { + const attrs = Object.assign({}, (cite && { cite })); + const element = this.wrap('blockquote', text, attrs); + return this.addRaw(element).addEOL(); + } + /** + * Adds an HTML anchor tag to the summary buffer + * + * @param {string} text link text/content + * @param {string} href hyperlink + * + * @returns {Summary} summary instance + */ + addLink(text, href) { + const element = this.wrap('a', text, { href }); + return this.addRaw(element).addEOL(); + } +} +const _summary = new Summary(); +/** + * @deprecated use `core.summary` + */ +exports.markdownSummary = _summary; +exports.summary = _summary; +//# sourceMappingURL=summary.js.map + +/***/ }), + +/***/ 302: +/***/ ((__unused_webpack_module, exports) => { + +"use strict"; + +// We use any as a valid input type +/* eslint-disable @typescript-eslint/no-explicit-any */ +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.toCommandProperties = exports.toCommandValue = void 0; +/** + * Sanitizes an input into a string so it can be passed into issueCommand safely + * @param input input to sanitize into a string + */ +function toCommandValue(input) { + if (input === null || input === undefined) { + return ''; + } + else if (typeof input === 'string' || input instanceof String) { + return input; + } + return JSON.stringify(input); +} +exports.toCommandValue = toCommandValue; +/** + * + * @param annotationProperties + * @returns The command properties to send with the actual annotation command + * See IssueCommandProperties: https://github.com/actions/runner/blob/main/src/Runner.Worker/ActionCommandManager.cs#L646 + */ +function toCommandProperties(annotationProperties) { + if (!Object.keys(annotationProperties).length) { + return {}; + } + return { + title: annotationProperties.title, + file: annotationProperties.file, + line: annotationProperties.startLine, + endLine: annotationProperties.endLine, + col: annotationProperties.startColumn, + endColumn: annotationProperties.endColumn + }; +} +exports.toCommandProperties = toCommandProperties; +//# sourceMappingURL=utils.js.map + +/***/ }), + +/***/ 5236: +/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { + +"use strict"; + +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.getExecOutput = exports.exec = void 0; +const string_decoder_1 = __nccwpck_require__(3193); +const tr = __importStar(__nccwpck_require__(6665)); +/** + * Exec a command. + * Output will be streamed to the live console. + * Returns promise with return code + * + * @param commandLine command to execute (can include additional args). Must be correctly escaped. + * @param args optional arguments for tool. Escaping is handled by the lib. + * @param options optional exec options. See ExecOptions + * @returns Promise exit code + */ +function exec(commandLine, args, options) { + return __awaiter(this, void 0, void 0, function* () { + const commandArgs = tr.argStringToArray(commandLine); + if (commandArgs.length === 0) { + throw new Error(`Parameter 'commandLine' cannot be null or empty.`); + } + // Path to tool to execute should be first arg + const toolPath = commandArgs[0]; + args = commandArgs.slice(1).concat(args || []); + const runner = new tr.ToolRunner(toolPath, args, options); + return runner.exec(); + }); +} +exports.exec = exec; +/** + * Exec a command and get the output. + * Output will be streamed to the live console. + * Returns promise with the exit code and collected stdout and stderr + * + * @param commandLine command to execute (can include additional args). Must be correctly escaped. + * @param args optional arguments for tool. Escaping is handled by the lib. + * @param options optional exec options. See ExecOptions + * @returns Promise exit code, stdout, and stderr + */ +function getExecOutput(commandLine, args, options) { + var _a, _b; + return __awaiter(this, void 0, void 0, function* () { + let stdout = ''; + let stderr = ''; + //Using string decoder covers the case where a mult-byte character is split + const stdoutDecoder = new string_decoder_1.StringDecoder('utf8'); + const stderrDecoder = new string_decoder_1.StringDecoder('utf8'); + const originalStdoutListener = (_a = options === null || options === void 0 ? void 0 : options.listeners) === null || _a === void 0 ? void 0 : _a.stdout; + const originalStdErrListener = (_b = options === null || options === void 0 ? void 0 : options.listeners) === null || _b === void 0 ? void 0 : _b.stderr; + const stdErrListener = (data) => { + stderr += stderrDecoder.write(data); + if (originalStdErrListener) { + originalStdErrListener(data); + } + }; + const stdOutListener = (data) => { + stdout += stdoutDecoder.write(data); + if (originalStdoutListener) { + originalStdoutListener(data); + } + }; + const listeners = Object.assign(Object.assign({}, options === null || options === void 0 ? void 0 : options.listeners), { stdout: stdOutListener, stderr: stdErrListener }); + const exitCode = yield exec(commandLine, args, Object.assign(Object.assign({}, options), { listeners })); + //flush any remaining characters + stdout += stdoutDecoder.end(); + stderr += stderrDecoder.end(); + return { + exitCode, + stdout, + stderr + }; + }); +} +exports.getExecOutput = getExecOutput; +//# sourceMappingURL=exec.js.map + +/***/ }), + +/***/ 6665: +/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { + +"use strict"; + +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.argStringToArray = exports.ToolRunner = void 0; +const os = __importStar(__nccwpck_require__(857)); +const events = __importStar(__nccwpck_require__(4434)); +const child = __importStar(__nccwpck_require__(5317)); +const path = __importStar(__nccwpck_require__(6928)); +const io = __importStar(__nccwpck_require__(4994)); +const ioUtil = __importStar(__nccwpck_require__(5207)); +const timers_1 = __nccwpck_require__(3557); +/* eslint-disable @typescript-eslint/unbound-method */ +const IS_WINDOWS = process.platform === 'win32'; +/* + * Class for running command line tools. Handles quoting and arg parsing in a platform agnostic way. + */ +class ToolRunner extends events.EventEmitter { + constructor(toolPath, args, options) { + super(); + if (!toolPath) { + throw new Error("Parameter 'toolPath' cannot be null or empty."); + } + this.toolPath = toolPath; + this.args = args || []; + this.options = options || {}; + } + _debug(message) { + if (this.options.listeners && this.options.listeners.debug) { + this.options.listeners.debug(message); + } + } + _getCommandString(options, noPrefix) { + const toolPath = this._getSpawnFileName(); + const args = this._getSpawnArgs(options); + let cmd = noPrefix ? '' : '[command]'; // omit prefix when piped to a second tool + if (IS_WINDOWS) { + // Windows + cmd file + if (this._isCmdFile()) { + cmd += toolPath; + for (const a of args) { + cmd += ` ${a}`; + } + } + // Windows + verbatim + else if (options.windowsVerbatimArguments) { + cmd += `"${toolPath}"`; + for (const a of args) { + cmd += ` ${a}`; + } + } + // Windows (regular) + else { + cmd += this._windowsQuoteCmdArg(toolPath); + for (const a of args) { + cmd += ` ${this._windowsQuoteCmdArg(a)}`; + } + } + } + else { + // OSX/Linux - this can likely be improved with some form of quoting. + // creating processes on Unix is fundamentally different than Windows. + // on Unix, execvp() takes an arg array. + cmd += toolPath; + for (const a of args) { + cmd += ` ${a}`; + } + } + return cmd; + } + _processLineBuffer(data, strBuffer, onLine) { + try { + let s = strBuffer + data.toString(); + let n = s.indexOf(os.EOL); + while (n > -1) { + const line = s.substring(0, n); + onLine(line); + // the rest of the string ... + s = s.substring(n + os.EOL.length); + n = s.indexOf(os.EOL); + } + return s; + } + catch (err) { + // streaming lines to console is best effort. Don't fail a build. + this._debug(`error processing line. Failed with error ${err}`); + return ''; + } + } + _getSpawnFileName() { + if (IS_WINDOWS) { + if (this._isCmdFile()) { + return process.env['COMSPEC'] || 'cmd.exe'; + } + } + return this.toolPath; + } + _getSpawnArgs(options) { + if (IS_WINDOWS) { + if (this._isCmdFile()) { + let argline = `/D /S /C "${this._windowsQuoteCmdArg(this.toolPath)}`; + for (const a of this.args) { + argline += ' '; + argline += options.windowsVerbatimArguments + ? a + : this._windowsQuoteCmdArg(a); + } + argline += '"'; + return [argline]; + } + } + return this.args; + } + _endsWith(str, end) { + return str.endsWith(end); + } + _isCmdFile() { + const upperToolPath = this.toolPath.toUpperCase(); + return (this._endsWith(upperToolPath, '.CMD') || + this._endsWith(upperToolPath, '.BAT')); + } + _windowsQuoteCmdArg(arg) { + // for .exe, apply the normal quoting rules that libuv applies + if (!this._isCmdFile()) { + return this._uvQuoteCmdArg(arg); + } + // otherwise apply quoting rules specific to the cmd.exe command line parser. + // the libuv rules are generic and are not designed specifically for cmd.exe + // command line parser. + // + // for a detailed description of the cmd.exe command line parser, refer to + // http://stackoverflow.com/questions/4094699/how-does-the-windows-command-interpreter-cmd-exe-parse-scripts/7970912#7970912 + // need quotes for empty arg + if (!arg) { + return '""'; + } + // determine whether the arg needs to be quoted + const cmdSpecialChars = [ + ' ', + '\t', + '&', + '(', + ')', + '[', + ']', + '{', + '}', + '^', + '=', + ';', + '!', + "'", + '+', + ',', + '`', + '~', + '|', + '<', + '>', + '"' + ]; + let needsQuotes = false; + for (const char of arg) { + if (cmdSpecialChars.some(x => x === char)) { + needsQuotes = true; + break; + } + } + // short-circuit if quotes not needed + if (!needsQuotes) { + return arg; + } + // the following quoting rules are very similar to the rules that by libuv applies. + // + // 1) wrap the string in quotes + // + // 2) double-up quotes - i.e. " => "" + // + // this is different from the libuv quoting rules. libuv replaces " with \", which unfortunately + // doesn't work well with a cmd.exe command line. + // + // note, replacing " with "" also works well if the arg is passed to a downstream .NET console app. + // for example, the command line: + // foo.exe "myarg:""my val""" + // is parsed by a .NET console app into an arg array: + // [ "myarg:\"my val\"" ] + // which is the same end result when applying libuv quoting rules. although the actual + // command line from libuv quoting rules would look like: + // foo.exe "myarg:\"my val\"" + // + // 3) double-up slashes that precede a quote, + // e.g. hello \world => "hello \world" + // hello\"world => "hello\\""world" + // hello\\"world => "hello\\\\""world" + // hello world\ => "hello world\\" + // + // technically this is not required for a cmd.exe command line, or the batch argument parser. + // the reasons for including this as a .cmd quoting rule are: + // + // a) this is optimized for the scenario where the argument is passed from the .cmd file to an + // external program. many programs (e.g. .NET console apps) rely on the slash-doubling rule. + // + // b) it's what we've been doing previously (by deferring to node default behavior) and we + // haven't heard any complaints about that aspect. + // + // note, a weakness of the quoting rules chosen here, is that % is not escaped. in fact, % cannot be + // escaped when used on the command line directly - even though within a .cmd file % can be escaped + // by using %%. + // + // the saving grace is, on the command line, %var% is left as-is if var is not defined. this contrasts + // the line parsing rules within a .cmd file, where if var is not defined it is replaced with nothing. + // + // one option that was explored was replacing % with ^% - i.e. %var% => ^%var^%. this hack would + // often work, since it is unlikely that var^ would exist, and the ^ character is removed when the + // variable is used. the problem, however, is that ^ is not removed when %* is used to pass the args + // to an external program. + // + // an unexplored potential solution for the % escaping problem, is to create a wrapper .cmd file. + // % can be escaped within a .cmd file. + let reverse = '"'; + let quoteHit = true; + for (let i = arg.length; i > 0; i--) { + // walk the string in reverse + reverse += arg[i - 1]; + if (quoteHit && arg[i - 1] === '\\') { + reverse += '\\'; // double the slash + } + else if (arg[i - 1] === '"') { + quoteHit = true; + reverse += '"'; // double the quote + } + else { + quoteHit = false; + } + } + reverse += '"'; + return reverse + .split('') + .reverse() + .join(''); + } + _uvQuoteCmdArg(arg) { + // Tool runner wraps child_process.spawn() and needs to apply the same quoting as + // Node in certain cases where the undocumented spawn option windowsVerbatimArguments + // is used. + // + // Since this function is a port of quote_cmd_arg from Node 4.x (technically, lib UV, + // see https://github.com/nodejs/node/blob/v4.x/deps/uv/src/win/process.c for details), + // pasting copyright notice from Node within this function: + // + // Copyright Joyent, Inc. and other Node contributors. All rights reserved. + // + // Permission is hereby granted, free of charge, to any person obtaining a copy + // of this software and associated documentation files (the "Software"), to + // deal in the Software without restriction, including without limitation the + // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + // sell copies of the Software, and to permit persons to whom the Software is + // furnished to do so, subject to the following conditions: + // + // The above copyright notice and this permission notice shall be included in + // all copies or substantial portions of the Software. + // + // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + // IN THE SOFTWARE. + if (!arg) { + // Need double quotation for empty argument + return '""'; + } + if (!arg.includes(' ') && !arg.includes('\t') && !arg.includes('"')) { + // No quotation needed + return arg; + } + if (!arg.includes('"') && !arg.includes('\\')) { + // No embedded double quotes or backslashes, so I can just wrap + // quote marks around the whole thing. + return `"${arg}"`; + } + // Expected input/output: + // input : hello"world + // output: "hello\"world" + // input : hello""world + // output: "hello\"\"world" + // input : hello\world + // output: hello\world + // input : hello\\world + // output: hello\\world + // input : hello\"world + // output: "hello\\\"world" + // input : hello\\"world + // output: "hello\\\\\"world" + // input : hello world\ + // output: "hello world\\" - note the comment in libuv actually reads "hello world\" + // but it appears the comment is wrong, it should be "hello world\\" + let reverse = '"'; + let quoteHit = true; + for (let i = arg.length; i > 0; i--) { + // walk the string in reverse + reverse += arg[i - 1]; + if (quoteHit && arg[i - 1] === '\\') { + reverse += '\\'; + } + else if (arg[i - 1] === '"') { + quoteHit = true; + reverse += '\\'; + } + else { + quoteHit = false; + } + } + reverse += '"'; + return reverse + .split('') + .reverse() + .join(''); + } + _cloneExecOptions(options) { + options = options || {}; + const result = { + cwd: options.cwd || process.cwd(), + env: options.env || process.env, + silent: options.silent || false, + windowsVerbatimArguments: options.windowsVerbatimArguments || false, + failOnStdErr: options.failOnStdErr || false, + ignoreReturnCode: options.ignoreReturnCode || false, + delay: options.delay || 10000 + }; + result.outStream = options.outStream || process.stdout; + result.errStream = options.errStream || process.stderr; + return result; + } + _getSpawnOptions(options, toolPath) { + options = options || {}; + const result = {}; + result.cwd = options.cwd; + result.env = options.env; + result['windowsVerbatimArguments'] = + options.windowsVerbatimArguments || this._isCmdFile(); + if (options.windowsVerbatimArguments) { + result.argv0 = `"${toolPath}"`; + } + return result; + } + /** + * Exec a tool. + * Output will be streamed to the live console. + * Returns promise with return code + * + * @param tool path to tool to exec + * @param options optional exec options. See ExecOptions + * @returns number + */ + exec() { + return __awaiter(this, void 0, void 0, function* () { + // root the tool path if it is unrooted and contains relative pathing + if (!ioUtil.isRooted(this.toolPath) && + (this.toolPath.includes('/') || + (IS_WINDOWS && this.toolPath.includes('\\')))) { + // prefer options.cwd if it is specified, however options.cwd may also need to be rooted + this.toolPath = path.resolve(process.cwd(), this.options.cwd || process.cwd(), this.toolPath); + } + // if the tool is only a file name, then resolve it from the PATH + // otherwise verify it exists (add extension on Windows if necessary) + this.toolPath = yield io.which(this.toolPath, true); + return new Promise((resolve, reject) => __awaiter(this, void 0, void 0, function* () { + this._debug(`exec tool: ${this.toolPath}`); + this._debug('arguments:'); + for (const arg of this.args) { + this._debug(` ${arg}`); + } + const optionsNonNull = this._cloneExecOptions(this.options); + if (!optionsNonNull.silent && optionsNonNull.outStream) { + optionsNonNull.outStream.write(this._getCommandString(optionsNonNull) + os.EOL); + } + const state = new ExecState(optionsNonNull, this.toolPath); + state.on('debug', (message) => { + this._debug(message); + }); + if (this.options.cwd && !(yield ioUtil.exists(this.options.cwd))) { + return reject(new Error(`The cwd: ${this.options.cwd} does not exist!`)); + } + const fileName = this._getSpawnFileName(); + const cp = child.spawn(fileName, this._getSpawnArgs(optionsNonNull), this._getSpawnOptions(this.options, fileName)); + let stdbuffer = ''; + if (cp.stdout) { + cp.stdout.on('data', (data) => { + if (this.options.listeners && this.options.listeners.stdout) { + this.options.listeners.stdout(data); + } + if (!optionsNonNull.silent && optionsNonNull.outStream) { + optionsNonNull.outStream.write(data); + } + stdbuffer = this._processLineBuffer(data, stdbuffer, (line) => { + if (this.options.listeners && this.options.listeners.stdline) { + this.options.listeners.stdline(line); + } + }); + }); + } + let errbuffer = ''; + if (cp.stderr) { + cp.stderr.on('data', (data) => { + state.processStderr = true; + if (this.options.listeners && this.options.listeners.stderr) { + this.options.listeners.stderr(data); + } + if (!optionsNonNull.silent && + optionsNonNull.errStream && + optionsNonNull.outStream) { + const s = optionsNonNull.failOnStdErr + ? optionsNonNull.errStream + : optionsNonNull.outStream; + s.write(data); + } + errbuffer = this._processLineBuffer(data, errbuffer, (line) => { + if (this.options.listeners && this.options.listeners.errline) { + this.options.listeners.errline(line); + } + }); + }); + } + cp.on('error', (err) => { + state.processError = err.message; + state.processExited = true; + state.processClosed = true; + state.CheckComplete(); + }); + cp.on('exit', (code) => { + state.processExitCode = code; + state.processExited = true; + this._debug(`Exit code ${code} received from tool '${this.toolPath}'`); + state.CheckComplete(); + }); + cp.on('close', (code) => { + state.processExitCode = code; + state.processExited = true; + state.processClosed = true; + this._debug(`STDIO streams have closed for tool '${this.toolPath}'`); + state.CheckComplete(); + }); + state.on('done', (error, exitCode) => { + if (stdbuffer.length > 0) { + this.emit('stdline', stdbuffer); + } + if (errbuffer.length > 0) { + this.emit('errline', errbuffer); + } + cp.removeAllListeners(); + if (error) { + reject(error); + } + else { + resolve(exitCode); + } + }); + if (this.options.input) { + if (!cp.stdin) { + throw new Error('child process missing stdin'); + } + cp.stdin.end(this.options.input); + } + })); + }); + } +} +exports.ToolRunner = ToolRunner; +/** + * Convert an arg string to an array of args. Handles escaping + * + * @param argString string of arguments + * @returns string[] array of arguments + */ +function argStringToArray(argString) { + const args = []; + let inQuotes = false; + let escaped = false; + let arg = ''; + function append(c) { + // we only escape double quotes. + if (escaped && c !== '"') { + arg += '\\'; + } + arg += c; + escaped = false; + } + for (let i = 0; i < argString.length; i++) { + const c = argString.charAt(i); + if (c === '"') { + if (!escaped) { + inQuotes = !inQuotes; + } + else { + append(c); + } + continue; + } + if (c === '\\' && escaped) { + append(c); + continue; + } + if (c === '\\' && inQuotes) { + escaped = true; + continue; + } + if (c === ' ' && !inQuotes) { + if (arg.length > 0) { + args.push(arg); + arg = ''; + } + continue; + } + append(c); + } + if (arg.length > 0) { + args.push(arg.trim()); + } + return args; +} +exports.argStringToArray = argStringToArray; +class ExecState extends events.EventEmitter { + constructor(options, toolPath) { + super(); + this.processClosed = false; // tracks whether the process has exited and stdio is closed + this.processError = ''; + this.processExitCode = 0; + this.processExited = false; // tracks whether the process has exited + this.processStderr = false; // tracks whether stderr was written to + this.delay = 10000; // 10 seconds + this.done = false; + this.timeout = null; + if (!toolPath) { + throw new Error('toolPath must not be empty'); + } + this.options = options; + this.toolPath = toolPath; + if (options.delay) { + this.delay = options.delay; + } + } + CheckComplete() { + if (this.done) { + return; + } + if (this.processClosed) { + this._setResult(); + } + else if (this.processExited) { + this.timeout = timers_1.setTimeout(ExecState.HandleTimeout, this.delay, this); + } + } + _debug(message) { + this.emit('debug', message); + } + _setResult() { + // determine whether there is an error + let error; + if (this.processExited) { + if (this.processError) { + error = new Error(`There was an error when attempting to execute the process '${this.toolPath}'. This may indicate the process failed to start. Error: ${this.processError}`); + } + else if (this.processExitCode !== 0 && !this.options.ignoreReturnCode) { + error = new Error(`The process '${this.toolPath}' failed with exit code ${this.processExitCode}`); + } + else if (this.processStderr && this.options.failOnStdErr) { + error = new Error(`The process '${this.toolPath}' failed because one or more lines were written to the STDERR stream`); + } + } + // clear the timeout + if (this.timeout) { + clearTimeout(this.timeout); + this.timeout = null; + } + this.done = true; + this.emit('done', error, this.processExitCode); + } + static HandleTimeout(state) { + if (state.done) { + return; + } + if (!state.processClosed && state.processExited) { + const message = `The STDIO streams did not close within ${state.delay / + 1000} seconds of the exit event from process '${state.toolPath}'. This may indicate a child process inherited the STDIO streams and has not yet exited.`; + state._debug(message); + } + state._setResult(); + } +} +//# sourceMappingURL=toolrunner.js.map + +/***/ }), + +/***/ 4552: +/***/ (function(__unused_webpack_module, exports) { + +"use strict"; + +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.PersonalAccessTokenCredentialHandler = exports.BearerCredentialHandler = exports.BasicCredentialHandler = void 0; +class BasicCredentialHandler { + constructor(username, password) { + this.username = username; + this.password = password; + } + prepareRequest(options) { + if (!options.headers) { + throw Error('The request has no headers'); + } + options.headers['Authorization'] = `Basic ${Buffer.from(`${this.username}:${this.password}`).toString('base64')}`; + } + // This handler cannot handle 401 + canHandleAuthentication() { + return false; + } + handleAuthentication() { + return __awaiter(this, void 0, void 0, function* () { + throw new Error('not implemented'); + }); + } +} +exports.BasicCredentialHandler = BasicCredentialHandler; +class BearerCredentialHandler { + constructor(token) { + this.token = token; + } + // currently implements pre-authorization + // TODO: support preAuth = false where it hooks on 401 + prepareRequest(options) { + if (!options.headers) { + throw Error('The request has no headers'); + } + options.headers['Authorization'] = `Bearer ${this.token}`; + } + // This handler cannot handle 401 + canHandleAuthentication() { + return false; + } + handleAuthentication() { + return __awaiter(this, void 0, void 0, function* () { + throw new Error('not implemented'); + }); + } +} +exports.BearerCredentialHandler = BearerCredentialHandler; +class PersonalAccessTokenCredentialHandler { + constructor(token) { + this.token = token; + } + // currently implements pre-authorization + // TODO: support preAuth = false where it hooks on 401 + prepareRequest(options) { + if (!options.headers) { + throw Error('The request has no headers'); + } + options.headers['Authorization'] = `Basic ${Buffer.from(`PAT:${this.token}`).toString('base64')}`; + } + // This handler cannot handle 401 + canHandleAuthentication() { + return false; + } + handleAuthentication() { + return __awaiter(this, void 0, void 0, function* () { + throw new Error('not implemented'); + }); + } +} +exports.PersonalAccessTokenCredentialHandler = PersonalAccessTokenCredentialHandler; +//# sourceMappingURL=auth.js.map + +/***/ }), + +/***/ 4844: +/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { + +"use strict"; + +/* eslint-disable @typescript-eslint/no-explicit-any */ +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.HttpClient = exports.isHttps = exports.HttpClientResponse = exports.HttpClientError = exports.getProxyUrl = exports.MediaTypes = exports.Headers = exports.HttpCodes = void 0; +const http = __importStar(__nccwpck_require__(8611)); +const https = __importStar(__nccwpck_require__(5692)); +const pm = __importStar(__nccwpck_require__(4988)); +const tunnel = __importStar(__nccwpck_require__(770)); +const undici_1 = __nccwpck_require__(6752); +var HttpCodes; +(function (HttpCodes) { + HttpCodes[HttpCodes["OK"] = 200] = "OK"; + HttpCodes[HttpCodes["MultipleChoices"] = 300] = "MultipleChoices"; + HttpCodes[HttpCodes["MovedPermanently"] = 301] = "MovedPermanently"; + HttpCodes[HttpCodes["ResourceMoved"] = 302] = "ResourceMoved"; + HttpCodes[HttpCodes["SeeOther"] = 303] = "SeeOther"; + HttpCodes[HttpCodes["NotModified"] = 304] = "NotModified"; + HttpCodes[HttpCodes["UseProxy"] = 305] = "UseProxy"; + HttpCodes[HttpCodes["SwitchProxy"] = 306] = "SwitchProxy"; + HttpCodes[HttpCodes["TemporaryRedirect"] = 307] = "TemporaryRedirect"; + HttpCodes[HttpCodes["PermanentRedirect"] = 308] = "PermanentRedirect"; + HttpCodes[HttpCodes["BadRequest"] = 400] = "BadRequest"; + HttpCodes[HttpCodes["Unauthorized"] = 401] = "Unauthorized"; + HttpCodes[HttpCodes["PaymentRequired"] = 402] = "PaymentRequired"; + HttpCodes[HttpCodes["Forbidden"] = 403] = "Forbidden"; + HttpCodes[HttpCodes["NotFound"] = 404] = "NotFound"; + HttpCodes[HttpCodes["MethodNotAllowed"] = 405] = "MethodNotAllowed"; + HttpCodes[HttpCodes["NotAcceptable"] = 406] = "NotAcceptable"; + HttpCodes[HttpCodes["ProxyAuthenticationRequired"] = 407] = "ProxyAuthenticationRequired"; + HttpCodes[HttpCodes["RequestTimeout"] = 408] = "RequestTimeout"; + HttpCodes[HttpCodes["Conflict"] = 409] = "Conflict"; + HttpCodes[HttpCodes["Gone"] = 410] = "Gone"; + HttpCodes[HttpCodes["TooManyRequests"] = 429] = "TooManyRequests"; + HttpCodes[HttpCodes["InternalServerError"] = 500] = "InternalServerError"; + HttpCodes[HttpCodes["NotImplemented"] = 501] = "NotImplemented"; + HttpCodes[HttpCodes["BadGateway"] = 502] = "BadGateway"; + HttpCodes[HttpCodes["ServiceUnavailable"] = 503] = "ServiceUnavailable"; + HttpCodes[HttpCodes["GatewayTimeout"] = 504] = "GatewayTimeout"; +})(HttpCodes || (exports.HttpCodes = HttpCodes = {})); +var Headers; +(function (Headers) { + Headers["Accept"] = "accept"; + Headers["ContentType"] = "content-type"; +})(Headers || (exports.Headers = Headers = {})); +var MediaTypes; +(function (MediaTypes) { + MediaTypes["ApplicationJson"] = "application/json"; +})(MediaTypes || (exports.MediaTypes = MediaTypes = {})); +/** + * Returns the proxy URL, depending upon the supplied url and proxy environment variables. + * @param serverUrl The server URL where the request will be sent. For example, https://api.github.com + */ +function getProxyUrl(serverUrl) { + const proxyUrl = pm.getProxyUrl(new URL(serverUrl)); + return proxyUrl ? proxyUrl.href : ''; +} +exports.getProxyUrl = getProxyUrl; +const HttpRedirectCodes = [ + HttpCodes.MovedPermanently, + HttpCodes.ResourceMoved, + HttpCodes.SeeOther, + HttpCodes.TemporaryRedirect, + HttpCodes.PermanentRedirect +]; +const HttpResponseRetryCodes = [ + HttpCodes.BadGateway, + HttpCodes.ServiceUnavailable, + HttpCodes.GatewayTimeout +]; +const RetryableHttpVerbs = ['OPTIONS', 'GET', 'DELETE', 'HEAD']; +const ExponentialBackoffCeiling = 10; +const ExponentialBackoffTimeSlice = 5; +class HttpClientError extends Error { + constructor(message, statusCode) { + super(message); + this.name = 'HttpClientError'; + this.statusCode = statusCode; + Object.setPrototypeOf(this, HttpClientError.prototype); + } +} +exports.HttpClientError = HttpClientError; +class HttpClientResponse { + constructor(message) { + this.message = message; + } + readBody() { + return __awaiter(this, void 0, void 0, function* () { + return new Promise((resolve) => __awaiter(this, void 0, void 0, function* () { + let output = Buffer.alloc(0); + this.message.on('data', (chunk) => { + output = Buffer.concat([output, chunk]); + }); + this.message.on('end', () => { + resolve(output.toString()); + }); + })); + }); + } + readBodyBuffer() { + return __awaiter(this, void 0, void 0, function* () { + return new Promise((resolve) => __awaiter(this, void 0, void 0, function* () { + const chunks = []; + this.message.on('data', (chunk) => { + chunks.push(chunk); + }); + this.message.on('end', () => { + resolve(Buffer.concat(chunks)); + }); + })); + }); + } +} +exports.HttpClientResponse = HttpClientResponse; +function isHttps(requestUrl) { + const parsedUrl = new URL(requestUrl); + return parsedUrl.protocol === 'https:'; +} +exports.isHttps = isHttps; +class HttpClient { + constructor(userAgent, handlers, requestOptions) { + this._ignoreSslError = false; + this._allowRedirects = true; + this._allowRedirectDowngrade = false; + this._maxRedirects = 50; + this._allowRetries = false; + this._maxRetries = 1; + this._keepAlive = false; + this._disposed = false; + this.userAgent = userAgent; + this.handlers = handlers || []; + this.requestOptions = requestOptions; + if (requestOptions) { + if (requestOptions.ignoreSslError != null) { + this._ignoreSslError = requestOptions.ignoreSslError; + } + this._socketTimeout = requestOptions.socketTimeout; + if (requestOptions.allowRedirects != null) { + this._allowRedirects = requestOptions.allowRedirects; + } + if (requestOptions.allowRedirectDowngrade != null) { + this._allowRedirectDowngrade = requestOptions.allowRedirectDowngrade; + } + if (requestOptions.maxRedirects != null) { + this._maxRedirects = Math.max(requestOptions.maxRedirects, 0); + } + if (requestOptions.keepAlive != null) { + this._keepAlive = requestOptions.keepAlive; + } + if (requestOptions.allowRetries != null) { + this._allowRetries = requestOptions.allowRetries; + } + if (requestOptions.maxRetries != null) { + this._maxRetries = requestOptions.maxRetries; + } + } + } + options(requestUrl, additionalHeaders) { + return __awaiter(this, void 0, void 0, function* () { + return this.request('OPTIONS', requestUrl, null, additionalHeaders || {}); + }); + } + get(requestUrl, additionalHeaders) { + return __awaiter(this, void 0, void 0, function* () { + return this.request('GET', requestUrl, null, additionalHeaders || {}); + }); + } + del(requestUrl, additionalHeaders) { + return __awaiter(this, void 0, void 0, function* () { + return this.request('DELETE', requestUrl, null, additionalHeaders || {}); + }); + } + post(requestUrl, data, additionalHeaders) { + return __awaiter(this, void 0, void 0, function* () { + return this.request('POST', requestUrl, data, additionalHeaders || {}); + }); + } + patch(requestUrl, data, additionalHeaders) { + return __awaiter(this, void 0, void 0, function* () { + return this.request('PATCH', requestUrl, data, additionalHeaders || {}); + }); + } + put(requestUrl, data, additionalHeaders) { + return __awaiter(this, void 0, void 0, function* () { + return this.request('PUT', requestUrl, data, additionalHeaders || {}); + }); + } + head(requestUrl, additionalHeaders) { + return __awaiter(this, void 0, void 0, function* () { + return this.request('HEAD', requestUrl, null, additionalHeaders || {}); + }); + } + sendStream(verb, requestUrl, stream, additionalHeaders) { + return __awaiter(this, void 0, void 0, function* () { + return this.request(verb, requestUrl, stream, additionalHeaders); + }); + } + /** + * Gets a typed object from an endpoint + * Be aware that not found returns a null. Other errors (4xx, 5xx) reject the promise + */ + getJson(requestUrl, additionalHeaders = {}) { + return __awaiter(this, void 0, void 0, function* () { + additionalHeaders[Headers.Accept] = this._getExistingOrDefaultHeader(additionalHeaders, Headers.Accept, MediaTypes.ApplicationJson); + const res = yield this.get(requestUrl, additionalHeaders); + return this._processResponse(res, this.requestOptions); + }); + } + postJson(requestUrl, obj, additionalHeaders = {}) { + return __awaiter(this, void 0, void 0, function* () { + const data = JSON.stringify(obj, null, 2); + additionalHeaders[Headers.Accept] = this._getExistingOrDefaultHeader(additionalHeaders, Headers.Accept, MediaTypes.ApplicationJson); + additionalHeaders[Headers.ContentType] = this._getExistingOrDefaultHeader(additionalHeaders, Headers.ContentType, MediaTypes.ApplicationJson); + const res = yield this.post(requestUrl, data, additionalHeaders); + return this._processResponse(res, this.requestOptions); + }); + } + putJson(requestUrl, obj, additionalHeaders = {}) { + return __awaiter(this, void 0, void 0, function* () { + const data = JSON.stringify(obj, null, 2); + additionalHeaders[Headers.Accept] = this._getExistingOrDefaultHeader(additionalHeaders, Headers.Accept, MediaTypes.ApplicationJson); + additionalHeaders[Headers.ContentType] = this._getExistingOrDefaultHeader(additionalHeaders, Headers.ContentType, MediaTypes.ApplicationJson); + const res = yield this.put(requestUrl, data, additionalHeaders); + return this._processResponse(res, this.requestOptions); + }); + } + patchJson(requestUrl, obj, additionalHeaders = {}) { + return __awaiter(this, void 0, void 0, function* () { + const data = JSON.stringify(obj, null, 2); + additionalHeaders[Headers.Accept] = this._getExistingOrDefaultHeader(additionalHeaders, Headers.Accept, MediaTypes.ApplicationJson); + additionalHeaders[Headers.ContentType] = this._getExistingOrDefaultHeader(additionalHeaders, Headers.ContentType, MediaTypes.ApplicationJson); + const res = yield this.patch(requestUrl, data, additionalHeaders); + return this._processResponse(res, this.requestOptions); + }); + } + /** + * Makes a raw http request. + * All other methods such as get, post, patch, and request ultimately call this. + * Prefer get, del, post and patch + */ + request(verb, requestUrl, data, headers) { + return __awaiter(this, void 0, void 0, function* () { + if (this._disposed) { + throw new Error('Client has already been disposed.'); + } + const parsedUrl = new URL(requestUrl); + let info = this._prepareRequest(verb, parsedUrl, headers); + // Only perform retries on reads since writes may not be idempotent. + const maxTries = this._allowRetries && RetryableHttpVerbs.includes(verb) + ? this._maxRetries + 1 + : 1; + let numTries = 0; + let response; + do { + response = yield this.requestRaw(info, data); + // Check if it's an authentication challenge + if (response && + response.message && + response.message.statusCode === HttpCodes.Unauthorized) { + let authenticationHandler; + for (const handler of this.handlers) { + if (handler.canHandleAuthentication(response)) { + authenticationHandler = handler; + break; + } + } + if (authenticationHandler) { + return authenticationHandler.handleAuthentication(this, info, data); + } + else { + // We have received an unauthorized response but have no handlers to handle it. + // Let the response return to the caller. + return response; + } + } + let redirectsRemaining = this._maxRedirects; + while (response.message.statusCode && + HttpRedirectCodes.includes(response.message.statusCode) && + this._allowRedirects && + redirectsRemaining > 0) { + const redirectUrl = response.message.headers['location']; + if (!redirectUrl) { + // if there's no location to redirect to, we won't + break; + } + const parsedRedirectUrl = new URL(redirectUrl); + if (parsedUrl.protocol === 'https:' && + parsedUrl.protocol !== parsedRedirectUrl.protocol && + !this._allowRedirectDowngrade) { + throw new Error('Redirect from HTTPS to HTTP protocol. This downgrade is not allowed for security reasons. If you want to allow this behavior, set the allowRedirectDowngrade option to true.'); + } + // we need to finish reading the response before reassigning response + // which will leak the open socket. + yield response.readBody(); + // strip authorization header if redirected to a different hostname + if (parsedRedirectUrl.hostname !== parsedUrl.hostname) { + for (const header in headers) { + // header names are case insensitive + if (header.toLowerCase() === 'authorization') { + delete headers[header]; + } + } + } + // let's make the request with the new redirectUrl + info = this._prepareRequest(verb, parsedRedirectUrl, headers); + response = yield this.requestRaw(info, data); + redirectsRemaining--; + } + if (!response.message.statusCode || + !HttpResponseRetryCodes.includes(response.message.statusCode)) { + // If not a retry code, return immediately instead of retrying + return response; + } + numTries += 1; + if (numTries < maxTries) { + yield response.readBody(); + yield this._performExponentialBackoff(numTries); + } + } while (numTries < maxTries); + return response; + }); + } + /** + * Needs to be called if keepAlive is set to true in request options. + */ + dispose() { + if (this._agent) { + this._agent.destroy(); + } + this._disposed = true; + } + /** + * Raw request. + * @param info + * @param data + */ + requestRaw(info, data) { + return __awaiter(this, void 0, void 0, function* () { + return new Promise((resolve, reject) => { + function callbackForResult(err, res) { + if (err) { + reject(err); + } + else if (!res) { + // If `err` is not passed, then `res` must be passed. + reject(new Error('Unknown error')); + } + else { + resolve(res); + } + } + this.requestRawWithCallback(info, data, callbackForResult); + }); + }); + } + /** + * Raw request with callback. + * @param info + * @param data + * @param onResult + */ + requestRawWithCallback(info, data, onResult) { + if (typeof data === 'string') { + if (!info.options.headers) { + info.options.headers = {}; + } + info.options.headers['Content-Length'] = Buffer.byteLength(data, 'utf8'); + } + let callbackCalled = false; + function handleResult(err, res) { + if (!callbackCalled) { + callbackCalled = true; + onResult(err, res); + } + } + const req = info.httpModule.request(info.options, (msg) => { + const res = new HttpClientResponse(msg); + handleResult(undefined, res); + }); + let socket; + req.on('socket', sock => { + socket = sock; + }); + // If we ever get disconnected, we want the socket to timeout eventually + req.setTimeout(this._socketTimeout || 3 * 60000, () => { + if (socket) { + socket.end(); + } + handleResult(new Error(`Request timeout: ${info.options.path}`)); + }); + req.on('error', function (err) { + // err has statusCode property + // res should have headers + handleResult(err); + }); + if (data && typeof data === 'string') { + req.write(data, 'utf8'); + } + if (data && typeof data !== 'string') { + data.on('close', function () { + req.end(); + }); + data.pipe(req); + } + else { + req.end(); + } + } + /** + * Gets an http agent. This function is useful when you need an http agent that handles + * routing through a proxy server - depending upon the url and proxy environment variables. + * @param serverUrl The server URL where the request will be sent. For example, https://api.github.com + */ + getAgent(serverUrl) { + const parsedUrl = new URL(serverUrl); + return this._getAgent(parsedUrl); + } + getAgentDispatcher(serverUrl) { + const parsedUrl = new URL(serverUrl); + const proxyUrl = pm.getProxyUrl(parsedUrl); + const useProxy = proxyUrl && proxyUrl.hostname; + if (!useProxy) { + return; + } + return this._getProxyAgentDispatcher(parsedUrl, proxyUrl); + } + _prepareRequest(method, requestUrl, headers) { + const info = {}; + info.parsedUrl = requestUrl; + const usingSsl = info.parsedUrl.protocol === 'https:'; + info.httpModule = usingSsl ? https : http; + const defaultPort = usingSsl ? 443 : 80; + info.options = {}; + info.options.host = info.parsedUrl.hostname; + info.options.port = info.parsedUrl.port + ? parseInt(info.parsedUrl.port) + : defaultPort; + info.options.path = + (info.parsedUrl.pathname || '') + (info.parsedUrl.search || ''); + info.options.method = method; + info.options.headers = this._mergeHeaders(headers); + if (this.userAgent != null) { + info.options.headers['user-agent'] = this.userAgent; + } + info.options.agent = this._getAgent(info.parsedUrl); + // gives handlers an opportunity to participate + if (this.handlers) { + for (const handler of this.handlers) { + handler.prepareRequest(info.options); + } + } + return info; + } + _mergeHeaders(headers) { + if (this.requestOptions && this.requestOptions.headers) { + return Object.assign({}, lowercaseKeys(this.requestOptions.headers), lowercaseKeys(headers || {})); + } + return lowercaseKeys(headers || {}); + } + _getExistingOrDefaultHeader(additionalHeaders, header, _default) { + let clientHeader; + if (this.requestOptions && this.requestOptions.headers) { + clientHeader = lowercaseKeys(this.requestOptions.headers)[header]; + } + return additionalHeaders[header] || clientHeader || _default; + } + _getAgent(parsedUrl) { + let agent; + const proxyUrl = pm.getProxyUrl(parsedUrl); + const useProxy = proxyUrl && proxyUrl.hostname; + if (this._keepAlive && useProxy) { + agent = this._proxyAgent; + } + if (!useProxy) { + agent = this._agent; + } + // if agent is already assigned use that agent. + if (agent) { + return agent; + } + const usingSsl = parsedUrl.protocol === 'https:'; + let maxSockets = 100; + if (this.requestOptions) { + maxSockets = this.requestOptions.maxSockets || http.globalAgent.maxSockets; + } + // This is `useProxy` again, but we need to check `proxyURl` directly for TypeScripts's flow analysis. + if (proxyUrl && proxyUrl.hostname) { + const agentOptions = { + maxSockets, + keepAlive: this._keepAlive, + proxy: Object.assign(Object.assign({}, ((proxyUrl.username || proxyUrl.password) && { + proxyAuth: `${proxyUrl.username}:${proxyUrl.password}` + })), { host: proxyUrl.hostname, port: proxyUrl.port }) + }; + let tunnelAgent; + const overHttps = proxyUrl.protocol === 'https:'; + if (usingSsl) { + tunnelAgent = overHttps ? tunnel.httpsOverHttps : tunnel.httpsOverHttp; + } + else { + tunnelAgent = overHttps ? tunnel.httpOverHttps : tunnel.httpOverHttp; + } + agent = tunnelAgent(agentOptions); + this._proxyAgent = agent; + } + // if tunneling agent isn't assigned create a new agent + if (!agent) { + const options = { keepAlive: this._keepAlive, maxSockets }; + agent = usingSsl ? new https.Agent(options) : new http.Agent(options); + this._agent = agent; + } + if (usingSsl && this._ignoreSslError) { + // we don't want to set NODE_TLS_REJECT_UNAUTHORIZED=0 since that will affect request for entire process + // http.RequestOptions doesn't expose a way to modify RequestOptions.agent.options + // we have to cast it to any and change it directly + agent.options = Object.assign(agent.options || {}, { + rejectUnauthorized: false + }); + } + return agent; + } + _getProxyAgentDispatcher(parsedUrl, proxyUrl) { + let proxyAgent; + if (this._keepAlive) { + proxyAgent = this._proxyAgentDispatcher; + } + // if agent is already assigned use that agent. + if (proxyAgent) { + return proxyAgent; + } + const usingSsl = parsedUrl.protocol === 'https:'; + proxyAgent = new undici_1.ProxyAgent(Object.assign({ uri: proxyUrl.href, pipelining: !this._keepAlive ? 0 : 1 }, ((proxyUrl.username || proxyUrl.password) && { + token: `Basic ${Buffer.from(`${proxyUrl.username}:${proxyUrl.password}`).toString('base64')}` + }))); + this._proxyAgentDispatcher = proxyAgent; + if (usingSsl && this._ignoreSslError) { + // we don't want to set NODE_TLS_REJECT_UNAUTHORIZED=0 since that will affect request for entire process + // http.RequestOptions doesn't expose a way to modify RequestOptions.agent.options + // we have to cast it to any and change it directly + proxyAgent.options = Object.assign(proxyAgent.options.requestTls || {}, { + rejectUnauthorized: false + }); + } + return proxyAgent; + } + _performExponentialBackoff(retryNumber) { + return __awaiter(this, void 0, void 0, function* () { + retryNumber = Math.min(ExponentialBackoffCeiling, retryNumber); + const ms = ExponentialBackoffTimeSlice * Math.pow(2, retryNumber); + return new Promise(resolve => setTimeout(() => resolve(), ms)); + }); + } + _processResponse(res, options) { + return __awaiter(this, void 0, void 0, function* () { + return new Promise((resolve, reject) => __awaiter(this, void 0, void 0, function* () { + const statusCode = res.message.statusCode || 0; + const response = { + statusCode, + result: null, + headers: {} + }; + // not found leads to null obj returned + if (statusCode === HttpCodes.NotFound) { + resolve(response); + } + // get the result from the body + function dateTimeDeserializer(key, value) { + if (typeof value === 'string') { + const a = new Date(value); + if (!isNaN(a.valueOf())) { + return a; + } + } + return value; + } + let obj; + let contents; + try { + contents = yield res.readBody(); + if (contents && contents.length > 0) { + if (options && options.deserializeDates) { + obj = JSON.parse(contents, dateTimeDeserializer); + } + else { + obj = JSON.parse(contents); + } + response.result = obj; + } + response.headers = res.message.headers; + } + catch (err) { + // Invalid resource (contents not json); leaving result obj null + } + // note that 3xx redirects are handled by the http layer. + if (statusCode > 299) { + let msg; + // if exception/error in body, attempt to get better error + if (obj && obj.message) { + msg = obj.message; + } + else if (contents && contents.length > 0) { + // it may be the case that the exception is in the body message as string + msg = contents; + } + else { + msg = `Failed request: (${statusCode})`; + } + const err = new HttpClientError(msg, statusCode); + err.result = response.result; + reject(err); + } + else { + resolve(response); + } + })); + }); + } +} +exports.HttpClient = HttpClient; +const lowercaseKeys = (obj) => Object.keys(obj).reduce((c, k) => ((c[k.toLowerCase()] = obj[k]), c), {}); +//# sourceMappingURL=index.js.map + +/***/ }), + +/***/ 4988: +/***/ ((__unused_webpack_module, exports) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.checkBypass = exports.getProxyUrl = void 0; +function getProxyUrl(reqUrl) { + const usingSsl = reqUrl.protocol === 'https:'; + if (checkBypass(reqUrl)) { + return undefined; + } + const proxyVar = (() => { + if (usingSsl) { + return process.env['https_proxy'] || process.env['HTTPS_PROXY']; + } + else { + return process.env['http_proxy'] || process.env['HTTP_PROXY']; + } + })(); + if (proxyVar) { + try { + return new DecodedURL(proxyVar); + } + catch (_a) { + if (!proxyVar.startsWith('http://') && !proxyVar.startsWith('https://')) + return new DecodedURL(`http://${proxyVar}`); + } + } + else { + return undefined; + } +} +exports.getProxyUrl = getProxyUrl; +function checkBypass(reqUrl) { + if (!reqUrl.hostname) { + return false; + } + const reqHost = reqUrl.hostname; + if (isLoopbackAddress(reqHost)) { + return true; + } + const noProxy = process.env['no_proxy'] || process.env['NO_PROXY'] || ''; + if (!noProxy) { + return false; + } + // Determine the request port + let reqPort; + if (reqUrl.port) { + reqPort = Number(reqUrl.port); + } + else if (reqUrl.protocol === 'http:') { + reqPort = 80; + } + else if (reqUrl.protocol === 'https:') { + reqPort = 443; + } + // Format the request hostname and hostname with port + const upperReqHosts = [reqUrl.hostname.toUpperCase()]; + if (typeof reqPort === 'number') { + upperReqHosts.push(`${upperReqHosts[0]}:${reqPort}`); + } + // Compare request host against noproxy + for (const upperNoProxyItem of noProxy + .split(',') + .map(x => x.trim().toUpperCase()) + .filter(x => x)) { + if (upperNoProxyItem === '*' || + upperReqHosts.some(x => x === upperNoProxyItem || + x.endsWith(`.${upperNoProxyItem}`) || + (upperNoProxyItem.startsWith('.') && + x.endsWith(`${upperNoProxyItem}`)))) { + return true; + } + } + return false; +} +exports.checkBypass = checkBypass; +function isLoopbackAddress(host) { + const hostLower = host.toLowerCase(); + return (hostLower === 'localhost' || + hostLower.startsWith('127.') || + hostLower.startsWith('[::1]') || + hostLower.startsWith('[0:0:0:0:0:0:0:1]')); +} +class DecodedURL extends URL { + constructor(url, base) { + super(url, base); + this._decodedUsername = decodeURIComponent(super.username); + this._decodedPassword = decodeURIComponent(super.password); + } + get username() { + return this._decodedUsername; + } + get password() { + return this._decodedPassword; + } +} +//# sourceMappingURL=proxy.js.map + +/***/ }), + +/***/ 5207: +/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { + +"use strict"; + +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var _a; +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.getCmdPath = exports.tryGetExecutablePath = exports.isRooted = exports.isDirectory = exports.exists = exports.READONLY = exports.UV_FS_O_EXLOCK = exports.IS_WINDOWS = exports.unlink = exports.symlink = exports.stat = exports.rmdir = exports.rm = exports.rename = exports.readlink = exports.readdir = exports.open = exports.mkdir = exports.lstat = exports.copyFile = exports.chmod = void 0; +const fs = __importStar(__nccwpck_require__(9896)); +const path = __importStar(__nccwpck_require__(6928)); +_a = fs.promises +// export const {open} = 'fs' +, exports.chmod = _a.chmod, exports.copyFile = _a.copyFile, exports.lstat = _a.lstat, exports.mkdir = _a.mkdir, exports.open = _a.open, exports.readdir = _a.readdir, exports.readlink = _a.readlink, exports.rename = _a.rename, exports.rm = _a.rm, exports.rmdir = _a.rmdir, exports.stat = _a.stat, exports.symlink = _a.symlink, exports.unlink = _a.unlink; +// export const {open} = 'fs' +exports.IS_WINDOWS = process.platform === 'win32'; +// See https://github.com/nodejs/node/blob/d0153aee367422d0858105abec186da4dff0a0c5/deps/uv/include/uv/win.h#L691 +exports.UV_FS_O_EXLOCK = 0x10000000; +exports.READONLY = fs.constants.O_RDONLY; +function exists(fsPath) { + return __awaiter(this, void 0, void 0, function* () { + try { + yield exports.stat(fsPath); + } + catch (err) { + if (err.code === 'ENOENT') { + return false; + } + throw err; + } + return true; + }); +} +exports.exists = exists; +function isDirectory(fsPath, useStat = false) { + return __awaiter(this, void 0, void 0, function* () { + const stats = useStat ? yield exports.stat(fsPath) : yield exports.lstat(fsPath); + return stats.isDirectory(); + }); +} +exports.isDirectory = isDirectory; +/** + * On OSX/Linux, true if path starts with '/'. On Windows, true for paths like: + * \, \hello, \\hello\share, C:, and C:\hello (and corresponding alternate separator cases). + */ +function isRooted(p) { + p = normalizeSeparators(p); + if (!p) { + throw new Error('isRooted() parameter "p" cannot be empty'); + } + if (exports.IS_WINDOWS) { + return (p.startsWith('\\') || /^[A-Z]:/i.test(p) // e.g. \ or \hello or \\hello + ); // e.g. C: or C:\hello + } + return p.startsWith('/'); +} +exports.isRooted = isRooted; +/** + * Best effort attempt to determine whether a file exists and is executable. + * @param filePath file path to check + * @param extensions additional file extensions to try + * @return if file exists and is executable, returns the file path. otherwise empty string. + */ +function tryGetExecutablePath(filePath, extensions) { + return __awaiter(this, void 0, void 0, function* () { + let stats = undefined; + try { + // test file exists + stats = yield exports.stat(filePath); + } + catch (err) { + if (err.code !== 'ENOENT') { + // eslint-disable-next-line no-console + console.log(`Unexpected error attempting to determine if executable file exists '${filePath}': ${err}`); + } + } + if (stats && stats.isFile()) { + if (exports.IS_WINDOWS) { + // on Windows, test for valid extension + const upperExt = path.extname(filePath).toUpperCase(); + if (extensions.some(validExt => validExt.toUpperCase() === upperExt)) { + return filePath; + } + } + else { + if (isUnixExecutable(stats)) { + return filePath; + } + } + } + // try each extension + const originalFilePath = filePath; + for (const extension of extensions) { + filePath = originalFilePath + extension; + stats = undefined; + try { + stats = yield exports.stat(filePath); + } + catch (err) { + if (err.code !== 'ENOENT') { + // eslint-disable-next-line no-console + console.log(`Unexpected error attempting to determine if executable file exists '${filePath}': ${err}`); + } + } + if (stats && stats.isFile()) { + if (exports.IS_WINDOWS) { + // preserve the case of the actual file (since an extension was appended) + try { + const directory = path.dirname(filePath); + const upperName = path.basename(filePath).toUpperCase(); + for (const actualName of yield exports.readdir(directory)) { + if (upperName === actualName.toUpperCase()) { + filePath = path.join(directory, actualName); + break; + } + } + } + catch (err) { + // eslint-disable-next-line no-console + console.log(`Unexpected error attempting to determine the actual case of the file '${filePath}': ${err}`); + } + return filePath; + } + else { + if (isUnixExecutable(stats)) { + return filePath; + } + } + } + } + return ''; + }); +} +exports.tryGetExecutablePath = tryGetExecutablePath; +function normalizeSeparators(p) { + p = p || ''; + if (exports.IS_WINDOWS) { + // convert slashes on Windows + p = p.replace(/\//g, '\\'); + // remove redundant slashes + return p.replace(/\\\\+/g, '\\'); + } + // remove redundant slashes + return p.replace(/\/\/+/g, '/'); +} +// on Mac/Linux, test the execute bit +// R W X R W X R W X +// 256 128 64 32 16 8 4 2 1 +function isUnixExecutable(stats) { + return ((stats.mode & 1) > 0 || + ((stats.mode & 8) > 0 && stats.gid === process.getgid()) || + ((stats.mode & 64) > 0 && stats.uid === process.getuid())); +} +// Get the path of cmd.exe in windows +function getCmdPath() { + var _a; + return (_a = process.env['COMSPEC']) !== null && _a !== void 0 ? _a : `cmd.exe`; +} +exports.getCmdPath = getCmdPath; +//# sourceMappingURL=io-util.js.map + +/***/ }), + +/***/ 4994: +/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { + +"use strict"; + +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.findInPath = exports.which = exports.mkdirP = exports.rmRF = exports.mv = exports.cp = void 0; +const assert_1 = __nccwpck_require__(2613); +const path = __importStar(__nccwpck_require__(6928)); +const ioUtil = __importStar(__nccwpck_require__(5207)); +/** + * Copies a file or folder. + * Based off of shelljs - https://github.com/shelljs/shelljs/blob/9237f66c52e5daa40458f94f9565e18e8132f5a6/src/cp.js + * + * @param source source path + * @param dest destination path + * @param options optional. See CopyOptions. + */ +function cp(source, dest, options = {}) { + return __awaiter(this, void 0, void 0, function* () { + const { force, recursive, copySourceDirectory } = readCopyOptions(options); + const destStat = (yield ioUtil.exists(dest)) ? yield ioUtil.stat(dest) : null; + // Dest is an existing file, but not forcing + if (destStat && destStat.isFile() && !force) { + return; + } + // If dest is an existing directory, should copy inside. + const newDest = destStat && destStat.isDirectory() && copySourceDirectory + ? path.join(dest, path.basename(source)) + : dest; + if (!(yield ioUtil.exists(source))) { + throw new Error(`no such file or directory: ${source}`); + } + const sourceStat = yield ioUtil.stat(source); + if (sourceStat.isDirectory()) { + if (!recursive) { + throw new Error(`Failed to copy. ${source} is a directory, but tried to copy without recursive flag.`); + } + else { + yield cpDirRecursive(source, newDest, 0, force); + } + } + else { + if (path.relative(source, newDest) === '') { + // a file cannot be copied to itself + throw new Error(`'${newDest}' and '${source}' are the same file`); + } + yield copyFile(source, newDest, force); + } + }); +} +exports.cp = cp; +/** + * Moves a path. + * + * @param source source path + * @param dest destination path + * @param options optional. See MoveOptions. + */ +function mv(source, dest, options = {}) { + return __awaiter(this, void 0, void 0, function* () { + if (yield ioUtil.exists(dest)) { + let destExists = true; + if (yield ioUtil.isDirectory(dest)) { + // If dest is directory copy src into dest + dest = path.join(dest, path.basename(source)); + destExists = yield ioUtil.exists(dest); + } + if (destExists) { + if (options.force == null || options.force) { + yield rmRF(dest); + } + else { + throw new Error('Destination already exists'); + } + } + } + yield mkdirP(path.dirname(dest)); + yield ioUtil.rename(source, dest); + }); +} +exports.mv = mv; +/** + * Remove a path recursively with force + * + * @param inputPath path to remove + */ +function rmRF(inputPath) { + return __awaiter(this, void 0, void 0, function* () { + if (ioUtil.IS_WINDOWS) { + // Check for invalid characters + // https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file + if (/[*"<>|]/.test(inputPath)) { + throw new Error('File path must not contain `*`, `"`, `<`, `>` or `|` on Windows'); + } + } + try { + // note if path does not exist, error is silent + yield ioUtil.rm(inputPath, { + force: true, + maxRetries: 3, + recursive: true, + retryDelay: 300 + }); + } + catch (err) { + throw new Error(`File was unable to be removed ${err}`); + } + }); +} +exports.rmRF = rmRF; +/** + * Make a directory. Creates the full path with folders in between + * Will throw if it fails + * + * @param fsPath path to create + * @returns Promise + */ +function mkdirP(fsPath) { + return __awaiter(this, void 0, void 0, function* () { + assert_1.ok(fsPath, 'a path argument must be provided'); + yield ioUtil.mkdir(fsPath, { recursive: true }); + }); +} +exports.mkdirP = mkdirP; +/** + * Returns path of a tool had the tool actually been invoked. Resolves via paths. + * If you check and the tool does not exist, it will throw. + * + * @param tool name of the tool + * @param check whether to check if tool exists + * @returns Promise path to tool + */ +function which(tool, check) { + return __awaiter(this, void 0, void 0, function* () { + if (!tool) { + throw new Error("parameter 'tool' is required"); + } + // recursive when check=true + if (check) { + const result = yield which(tool, false); + if (!result) { + if (ioUtil.IS_WINDOWS) { + throw new Error(`Unable to locate executable file: ${tool}. Please verify either the file path exists or the file can be found within a directory specified by the PATH environment variable. Also verify the file has a valid extension for an executable file.`); + } + else { + throw new Error(`Unable to locate executable file: ${tool}. Please verify either the file path exists or the file can be found within a directory specified by the PATH environment variable. Also check the file mode to verify the file is executable.`); + } + } + return result; + } + const matches = yield findInPath(tool); + if (matches && matches.length > 0) { + return matches[0]; + } + return ''; + }); +} +exports.which = which; +/** + * Returns a list of all occurrences of the given tool on the system path. + * + * @returns Promise the paths of the tool + */ +function findInPath(tool) { + return __awaiter(this, void 0, void 0, function* () { + if (!tool) { + throw new Error("parameter 'tool' is required"); + } + // build the list of extensions to try + const extensions = []; + if (ioUtil.IS_WINDOWS && process.env['PATHEXT']) { + for (const extension of process.env['PATHEXT'].split(path.delimiter)) { + if (extension) { + extensions.push(extension); + } + } + } + // if it's rooted, return it if exists. otherwise return empty. + if (ioUtil.isRooted(tool)) { + const filePath = yield ioUtil.tryGetExecutablePath(tool, extensions); + if (filePath) { + return [filePath]; + } + return []; + } + // if any path separators, return empty + if (tool.includes(path.sep)) { + return []; + } + // build the list of directories + // + // Note, technically "where" checks the current directory on Windows. From a toolkit perspective, + // it feels like we should not do this. Checking the current directory seems like more of a use + // case of a shell, and the which() function exposed by the toolkit should strive for consistency + // across platforms. + const directories = []; + if (process.env.PATH) { + for (const p of process.env.PATH.split(path.delimiter)) { + if (p) { + directories.push(p); + } + } + } + // find all matches + const matches = []; + for (const directory of directories) { + const filePath = yield ioUtil.tryGetExecutablePath(path.join(directory, tool), extensions); + if (filePath) { + matches.push(filePath); + } + } + return matches; + }); +} +exports.findInPath = findInPath; +function readCopyOptions(options) { + const force = options.force == null ? true : options.force; + const recursive = Boolean(options.recursive); + const copySourceDirectory = options.copySourceDirectory == null + ? true + : Boolean(options.copySourceDirectory); + return { force, recursive, copySourceDirectory }; +} +function cpDirRecursive(sourceDir, destDir, currentDepth, force) { + return __awaiter(this, void 0, void 0, function* () { + // Ensure there is not a run away recursive copy + if (currentDepth >= 255) + return; + currentDepth++; + yield mkdirP(destDir); + const files = yield ioUtil.readdir(sourceDir); + for (const fileName of files) { + const srcFile = `${sourceDir}/${fileName}`; + const destFile = `${destDir}/${fileName}`; + const srcFileStat = yield ioUtil.lstat(srcFile); + if (srcFileStat.isDirectory()) { + // Recurse + yield cpDirRecursive(srcFile, destFile, currentDepth, force); + } + else { + yield copyFile(srcFile, destFile, force); + } + } + // Change the mode for the newly created directory + yield ioUtil.chmod(destDir, (yield ioUtil.stat(sourceDir)).mode); + }); +} +// Buffered file copy +function copyFile(srcFile, destFile, force) { + return __awaiter(this, void 0, void 0, function* () { + if ((yield ioUtil.lstat(srcFile)).isSymbolicLink()) { + // unlink/re-link it + try { + yield ioUtil.lstat(destFile); + yield ioUtil.unlink(destFile); + } + catch (e) { + // Try to override file permission + if (e.code === 'EPERM') { + yield ioUtil.chmod(destFile, '0666'); + yield ioUtil.unlink(destFile); + } + // other errors = it doesn't exist, no work to do + } + // Copy over symlink + const symlinkFull = yield ioUtil.readlink(srcFile); + yield ioUtil.symlink(symlinkFull, destFile, ioUtil.IS_WINDOWS ? 'junction' : null); + } + else if (!(yield ioUtil.exists(destFile)) || force) { + yield ioUtil.copyFile(srcFile, destFile); + } + }); +} +//# sourceMappingURL=io.js.map + +/***/ }), + +/***/ 133: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +// export the calculator fn +module.exports = __nccwpck_require__(6737); + +// TODO maybe provide name exports of other functions for avg, std dev, variance, etc... 🤔 + + +/***/ }), + +/***/ 2214: +/***/ ((module) => { + +// will only call it with a _safe_ array of values, so no need to sanitize input here +module.exports = values => + values.reduce((sum, v) => sum + v, 0) / values.length; + + +/***/ }), + +/***/ 6737: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +const calculateAverage = __nccwpck_require__(2214); +const calculateStdDev = __nccwpck_require__(825); +const checkInput = __nccwpck_require__(9039); +const preciseRound = __nccwpck_require__(2940); +const manageInput = __nccwpck_require__(9554); + +module.exports = (...args) => { + const [arrays, options] = manageInput(args); + + const isInputValid = checkInput(arrays); + if (!isInputValid) throw new Error('Input not valid'); + + const [x, y] = arrays; + + const µ = { x: calculateAverage(x), y: calculateAverage(y) }; + const s = { x: calculateStdDev(x), y: calculateStdDev(y) }; + + const addedMultipliedDifferences = x + .map((val, i) => (val - µ.x) * (y[i] - µ.y)) + .reduce((sum, v) => sum + v, 0); + + const dividedByDevs = addedMultipliedDifferences / (s.x * s.y); + + const r = dividedByDevs / (x.length - 1); + + // return string? + if (options.returnString === true) return r.toFixed(options.returnDecimals); + // default return + return preciseRound(r, options.returnDecimals); +}; + + +/***/ }), + +/***/ 825: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +const calculateAverage = __nccwpck_require__(2214); + +// will only call it with a _safe_ array of values +module.exports = values => { + const µ = calculateAverage(values); + const addedSquareDiffs = values + .map(val => val - µ) + .map(diff => diff ** 2) + .reduce((sum, v) => sum + v, 0); + const variance = addedSquareDiffs / (values.length - 1); + return Math.sqrt(variance); +}; + +// TODO maybe export fns to calculate variance and std deviation too from the package? + + +/***/ }), + +/***/ 9039: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +const isNumber = __nccwpck_require__(3230); + +module.exports = args => { + // only two inputs exist + if (args.length !== 2) return false; + const [x, y] = args; + // inputs are not falsy + if (!x || !y) return false; + // they are arrays + if (!Array.isArray(x) || !Array.isArray(y)) return false; + // length is not 0 + if (!x.length || !y.length) return false; + // length is the same + if (x.length !== y.length) return false; + // all the elems in the arrays are numbers + if (x.concat(y).find(el => !isNumber(el))) return false; + // 👌 all good! + return true; +}; + +// TODO maybe return some message about each problem, so it can be thrown in the Error + + +/***/ }), + +/***/ 3230: +/***/ ((module) => { + +// idea from https://codepen.io/grok/pen/LvOQbW?editors=0010 +module.exports = n => + typeof n === 'number' && n === Number(n) && Number.isFinite(n); + + +/***/ }), + +/***/ 5608: +/***/ ((module) => { + +// idea from https://stackoverflow.com/questions/8511281/check-if-a-value-is-an-object-in-javascript/8511350#8511350 +module.exports = obj => + typeof obj === 'object' && obj !== null && !Array.isArray(obj); + + +/***/ }), + +/***/ 9554: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +const isObject = __nccwpck_require__(5608); + +module.exports = input => { + let arrays = input; + let options = {}; + + if (input.length > 2) { + /* eslint-disable-next-line prefer-destructuring */ + if (isObject(input[2])) options = input[2]; + arrays = input.slice(0, 2); + } + + const opts = { + returnString: options.string || false, + returnDecimals: options.decimals || 9, + }; + + return [arrays, opts]; +}; + + +/***/ }), + +/***/ 2940: +/***/ ((module) => { + +// idea from https://www.w3resource.com/javascript-exercises/javascript-math-exercise-14.php +module.exports = (num, dec) => + Math.round(num * 10 ** dec + (num >= 0 ? 1 : -1) * 0.0001) / 10 ** dec; + + +/***/ }), + +/***/ 6705: +/***/ ((module, exports, __nccwpck_require__) => { + +"use strict"; + + +Object.defineProperty(exports, "__esModule", ({ value: true })); + +function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; } + +var Stream = _interopDefault(__nccwpck_require__(2203)); +var http = _interopDefault(__nccwpck_require__(8611)); +var Url = _interopDefault(__nccwpck_require__(7016)); +var whatwgUrl = _interopDefault(__nccwpck_require__(2686)); +var https = _interopDefault(__nccwpck_require__(5692)); +var zlib = _interopDefault(__nccwpck_require__(3106)); + +// Based on https://github.com/tmpvar/jsdom/blob/aa85b2abf07766ff7bf5c1f6daafb3726f2f2db5/lib/jsdom/living/blob.js + +// fix for "Readable" isn't a named export issue +const Readable = Stream.Readable; + +const BUFFER = Symbol('buffer'); +const TYPE = Symbol('type'); + +class Blob { + constructor() { + this[TYPE] = ''; + + const blobParts = arguments[0]; + const options = arguments[1]; + + const buffers = []; + let size = 0; + + if (blobParts) { + const a = blobParts; + const length = Number(a.length); + for (let i = 0; i < length; i++) { + const element = a[i]; + let buffer; + if (element instanceof Buffer) { + buffer = element; + } else if (ArrayBuffer.isView(element)) { + buffer = Buffer.from(element.buffer, element.byteOffset, element.byteLength); + } else if (element instanceof ArrayBuffer) { + buffer = Buffer.from(element); + } else if (element instanceof Blob) { + buffer = element[BUFFER]; + } else { + buffer = Buffer.from(typeof element === 'string' ? element : String(element)); + } + size += buffer.length; + buffers.push(buffer); + } + } + + this[BUFFER] = Buffer.concat(buffers); + + let type = options && options.type !== undefined && String(options.type).toLowerCase(); + if (type && !/[^\u0020-\u007E]/.test(type)) { + this[TYPE] = type; + } + } + get size() { + return this[BUFFER].length; + } + get type() { + return this[TYPE]; + } + text() { + return Promise.resolve(this[BUFFER].toString()); + } + arrayBuffer() { + const buf = this[BUFFER]; + const ab = buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength); + return Promise.resolve(ab); + } + stream() { + const readable = new Readable(); + readable._read = function () {}; + readable.push(this[BUFFER]); + readable.push(null); + return readable; + } + toString() { + return '[object Blob]'; + } + slice() { + const size = this.size; + + const start = arguments[0]; + const end = arguments[1]; + let relativeStart, relativeEnd; + if (start === undefined) { + relativeStart = 0; + } else if (start < 0) { + relativeStart = Math.max(size + start, 0); + } else { + relativeStart = Math.min(start, size); + } + if (end === undefined) { + relativeEnd = size; + } else if (end < 0) { + relativeEnd = Math.max(size + end, 0); + } else { + relativeEnd = Math.min(end, size); + } + const span = Math.max(relativeEnd - relativeStart, 0); + + const buffer = this[BUFFER]; + const slicedBuffer = buffer.slice(relativeStart, relativeStart + span); + const blob = new Blob([], { type: arguments[2] }); + blob[BUFFER] = slicedBuffer; + return blob; + } +} + +Object.defineProperties(Blob.prototype, { + size: { enumerable: true }, + type: { enumerable: true }, + slice: { enumerable: true } +}); + +Object.defineProperty(Blob.prototype, Symbol.toStringTag, { + value: 'Blob', + writable: false, + enumerable: false, + configurable: true +}); + +/** + * fetch-error.js + * + * FetchError interface for operational errors + */ + +/** + * Create FetchError instance + * + * @param String message Error message for human + * @param String type Error type for machine + * @param String systemError For Node.js system error + * @return FetchError + */ +function FetchError(message, type, systemError) { + Error.call(this, message); + + this.message = message; + this.type = type; + + // when err.type is `system`, err.code contains system error code + if (systemError) { + this.code = this.errno = systemError.code; + } + + // hide custom error implementation details from end-users + Error.captureStackTrace(this, this.constructor); +} + +FetchError.prototype = Object.create(Error.prototype); +FetchError.prototype.constructor = FetchError; +FetchError.prototype.name = 'FetchError'; + +let convert; +try { + convert = (__nccwpck_require__(4572)/* .convert */ .C); +} catch (e) {} + +const INTERNALS = Symbol('Body internals'); + +// fix an issue where "PassThrough" isn't a named export for node <10 +const PassThrough = Stream.PassThrough; + +/** + * Body mixin + * + * Ref: https://fetch.spec.whatwg.org/#body + * + * @param Stream body Readable stream + * @param Object opts Response options + * @return Void + */ +function Body(body) { + var _this = this; + + var _ref = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}, + _ref$size = _ref.size; + + let size = _ref$size === undefined ? 0 : _ref$size; + var _ref$timeout = _ref.timeout; + let timeout = _ref$timeout === undefined ? 0 : _ref$timeout; + + if (body == null) { + // body is undefined or null + body = null; + } else if (isURLSearchParams(body)) { + // body is a URLSearchParams + body = Buffer.from(body.toString()); + } else if (isBlob(body)) ; else if (Buffer.isBuffer(body)) ; else if (Object.prototype.toString.call(body) === '[object ArrayBuffer]') { + // body is ArrayBuffer + body = Buffer.from(body); + } else if (ArrayBuffer.isView(body)) { + // body is ArrayBufferView + body = Buffer.from(body.buffer, body.byteOffset, body.byteLength); + } else if (body instanceof Stream) ; else { + // none of the above + // coerce to string then buffer + body = Buffer.from(String(body)); + } + this[INTERNALS] = { + body, + disturbed: false, + error: null + }; + this.size = size; + this.timeout = timeout; + + if (body instanceof Stream) { + body.on('error', function (err) { + const error = err.name === 'AbortError' ? err : new FetchError(`Invalid response body while trying to fetch ${_this.url}: ${err.message}`, 'system', err); + _this[INTERNALS].error = error; + }); + } +} + +Body.prototype = { + get body() { + return this[INTERNALS].body; + }, + + get bodyUsed() { + return this[INTERNALS].disturbed; + }, + + /** + * Decode response as ArrayBuffer + * + * @return Promise + */ + arrayBuffer() { + return consumeBody.call(this).then(function (buf) { + return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength); + }); + }, + + /** + * Return raw response as Blob + * + * @return Promise + */ + blob() { + let ct = this.headers && this.headers.get('content-type') || ''; + return consumeBody.call(this).then(function (buf) { + return Object.assign( + // Prevent copying + new Blob([], { + type: ct.toLowerCase() + }), { + [BUFFER]: buf + }); + }); + }, + + /** + * Decode response as json + * + * @return Promise + */ + json() { + var _this2 = this; + + return consumeBody.call(this).then(function (buffer) { + try { + return JSON.parse(buffer.toString()); + } catch (err) { + return Body.Promise.reject(new FetchError(`invalid json response body at ${_this2.url} reason: ${err.message}`, 'invalid-json')); + } + }); + }, + + /** + * Decode response as text + * + * @return Promise + */ + text() { + return consumeBody.call(this).then(function (buffer) { + return buffer.toString(); + }); + }, + + /** + * Decode response as buffer (non-spec api) + * + * @return Promise + */ + buffer() { + return consumeBody.call(this); + }, + + /** + * Decode response as text, while automatically detecting the encoding and + * trying to decode to UTF-8 (non-spec api) + * + * @return Promise + */ + textConverted() { + var _this3 = this; + + return consumeBody.call(this).then(function (buffer) { + return convertBody(buffer, _this3.headers); + }); + } +}; + +// In browsers, all properties are enumerable. +Object.defineProperties(Body.prototype, { + body: { enumerable: true }, + bodyUsed: { enumerable: true }, + arrayBuffer: { enumerable: true }, + blob: { enumerable: true }, + json: { enumerable: true }, + text: { enumerable: true } +}); + +Body.mixIn = function (proto) { + for (const name of Object.getOwnPropertyNames(Body.prototype)) { + // istanbul ignore else: future proof + if (!(name in proto)) { + const desc = Object.getOwnPropertyDescriptor(Body.prototype, name); + Object.defineProperty(proto, name, desc); + } + } +}; + +/** + * Consume and convert an entire Body to a Buffer. + * + * Ref: https://fetch.spec.whatwg.org/#concept-body-consume-body + * + * @return Promise + */ +function consumeBody() { + var _this4 = this; + + if (this[INTERNALS].disturbed) { + return Body.Promise.reject(new TypeError(`body used already for: ${this.url}`)); + } + + this[INTERNALS].disturbed = true; + + if (this[INTERNALS].error) { + return Body.Promise.reject(this[INTERNALS].error); + } + + let body = this.body; + + // body is null + if (body === null) { + return Body.Promise.resolve(Buffer.alloc(0)); + } + + // body is blob + if (isBlob(body)) { + body = body.stream(); + } + + // body is buffer + if (Buffer.isBuffer(body)) { + return Body.Promise.resolve(body); + } + + // istanbul ignore if: should never happen + if (!(body instanceof Stream)) { + return Body.Promise.resolve(Buffer.alloc(0)); + } + + // body is stream + // get ready to actually consume the body + let accum = []; + let accumBytes = 0; + let abort = false; + + return new Body.Promise(function (resolve, reject) { + let resTimeout; + + // allow timeout on slow response body + if (_this4.timeout) { + resTimeout = setTimeout(function () { + abort = true; + reject(new FetchError(`Response timeout while trying to fetch ${_this4.url} (over ${_this4.timeout}ms)`, 'body-timeout')); + }, _this4.timeout); + } + + // handle stream errors + body.on('error', function (err) { + if (err.name === 'AbortError') { + // if the request was aborted, reject with this Error + abort = true; + reject(err); + } else { + // other errors, such as incorrect content-encoding + reject(new FetchError(`Invalid response body while trying to fetch ${_this4.url}: ${err.message}`, 'system', err)); + } + }); + + body.on('data', function (chunk) { + if (abort || chunk === null) { + return; + } + + if (_this4.size && accumBytes + chunk.length > _this4.size) { + abort = true; + reject(new FetchError(`content size at ${_this4.url} over limit: ${_this4.size}`, 'max-size')); + return; + } + + accumBytes += chunk.length; + accum.push(chunk); + }); + + body.on('end', function () { + if (abort) { + return; + } + + clearTimeout(resTimeout); + + try { + resolve(Buffer.concat(accum, accumBytes)); + } catch (err) { + // handle streams that have accumulated too much data (issue #414) + reject(new FetchError(`Could not create Buffer from response body for ${_this4.url}: ${err.message}`, 'system', err)); + } + }); + }); +} + +/** + * Detect buffer encoding and convert to target encoding + * ref: http://www.w3.org/TR/2011/WD-html5-20110113/parsing.html#determining-the-character-encoding + * + * @param Buffer buffer Incoming buffer + * @param String encoding Target encoding + * @return String + */ +function convertBody(buffer, headers) { + if (typeof convert !== 'function') { + throw new Error('The package `encoding` must be installed to use the textConverted() function'); + } + + const ct = headers.get('content-type'); + let charset = 'utf-8'; + let res, str; + + // header + if (ct) { + res = /charset=([^;]*)/i.exec(ct); + } + + // no charset in content type, peek at response body for at most 1024 bytes + str = buffer.slice(0, 1024).toString(); + + // html5 + if (!res && str) { + res = / 0 && arguments[0] !== undefined ? arguments[0] : undefined; + + this[MAP] = Object.create(null); + + if (init instanceof Headers) { + const rawHeaders = init.raw(); + const headerNames = Object.keys(rawHeaders); + + for (const headerName of headerNames) { + for (const value of rawHeaders[headerName]) { + this.append(headerName, value); + } + } + + return; + } + + // We don't worry about converting prop to ByteString here as append() + // will handle it. + if (init == null) ; else if (typeof init === 'object') { + const method = init[Symbol.iterator]; + if (method != null) { + if (typeof method !== 'function') { + throw new TypeError('Header pairs must be iterable'); + } + + // sequence> + // Note: per spec we have to first exhaust the lists then process them + const pairs = []; + for (const pair of init) { + if (typeof pair !== 'object' || typeof pair[Symbol.iterator] !== 'function') { + throw new TypeError('Each header pair must be iterable'); + } + pairs.push(Array.from(pair)); + } + + for (const pair of pairs) { + if (pair.length !== 2) { + throw new TypeError('Each header pair must be a name/value tuple'); + } + this.append(pair[0], pair[1]); + } + } else { + // record + for (const key of Object.keys(init)) { + const value = init[key]; + this.append(key, value); + } + } + } else { + throw new TypeError('Provided initializer must be an object'); + } + } + + /** + * Return combined header value given name + * + * @param String name Header name + * @return Mixed + */ + get(name) { + name = `${name}`; + validateName(name); + const key = find(this[MAP], name); + if (key === undefined) { + return null; + } + + return this[MAP][key].join(', '); + } + + /** + * Iterate over all headers + * + * @param Function callback Executed for each item with parameters (value, name, thisArg) + * @param Boolean thisArg `this` context for callback function + * @return Void + */ + forEach(callback) { + let thisArg = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : undefined; + + let pairs = getHeaders(this); + let i = 0; + while (i < pairs.length) { + var _pairs$i = pairs[i]; + const name = _pairs$i[0], + value = _pairs$i[1]; + + callback.call(thisArg, value, name, this); + pairs = getHeaders(this); + i++; + } + } + + /** + * Overwrite header values given name + * + * @param String name Header name + * @param String value Header value + * @return Void + */ + set(name, value) { + name = `${name}`; + value = `${value}`; + validateName(name); + validateValue(value); + const key = find(this[MAP], name); + this[MAP][key !== undefined ? key : name] = [value]; + } + + /** + * Append a value onto existing header + * + * @param String name Header name + * @param String value Header value + * @return Void + */ + append(name, value) { + name = `${name}`; + value = `${value}`; + validateName(name); + validateValue(value); + const key = find(this[MAP], name); + if (key !== undefined) { + this[MAP][key].push(value); + } else { + this[MAP][name] = [value]; + } + } + + /** + * Check for header name existence + * + * @param String name Header name + * @return Boolean + */ + has(name) { + name = `${name}`; + validateName(name); + return find(this[MAP], name) !== undefined; + } + + /** + * Delete all header values given name + * + * @param String name Header name + * @return Void + */ + delete(name) { + name = `${name}`; + validateName(name); + const key = find(this[MAP], name); + if (key !== undefined) { + delete this[MAP][key]; + } + } + + /** + * Return raw headers (non-spec api) + * + * @return Object + */ + raw() { + return this[MAP]; + } + + /** + * Get an iterator on keys. + * + * @return Iterator + */ + keys() { + return createHeadersIterator(this, 'key'); + } + + /** + * Get an iterator on values. + * + * @return Iterator + */ + values() { + return createHeadersIterator(this, 'value'); + } + + /** + * Get an iterator on entries. + * + * This is the default iterator of the Headers object. + * + * @return Iterator + */ + [Symbol.iterator]() { + return createHeadersIterator(this, 'key+value'); + } +} +Headers.prototype.entries = Headers.prototype[Symbol.iterator]; + +Object.defineProperty(Headers.prototype, Symbol.toStringTag, { + value: 'Headers', + writable: false, + enumerable: false, + configurable: true +}); + +Object.defineProperties(Headers.prototype, { + get: { enumerable: true }, + forEach: { enumerable: true }, + set: { enumerable: true }, + append: { enumerable: true }, + has: { enumerable: true }, + delete: { enumerable: true }, + keys: { enumerable: true }, + values: { enumerable: true }, + entries: { enumerable: true } +}); + +function getHeaders(headers) { + let kind = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'key+value'; + + const keys = Object.keys(headers[MAP]).sort(); + return keys.map(kind === 'key' ? function (k) { + return k.toLowerCase(); + } : kind === 'value' ? function (k) { + return headers[MAP][k].join(', '); + } : function (k) { + return [k.toLowerCase(), headers[MAP][k].join(', ')]; + }); +} + +const INTERNAL = Symbol('internal'); + +function createHeadersIterator(target, kind) { + const iterator = Object.create(HeadersIteratorPrototype); + iterator[INTERNAL] = { + target, + kind, + index: 0 + }; + return iterator; +} + +const HeadersIteratorPrototype = Object.setPrototypeOf({ + next() { + // istanbul ignore if + if (!this || Object.getPrototypeOf(this) !== HeadersIteratorPrototype) { + throw new TypeError('Value of `this` is not a HeadersIterator'); + } + + var _INTERNAL = this[INTERNAL]; + const target = _INTERNAL.target, + kind = _INTERNAL.kind, + index = _INTERNAL.index; + + const values = getHeaders(target, kind); + const len = values.length; + if (index >= len) { + return { + value: undefined, + done: true + }; + } + + this[INTERNAL].index = index + 1; + + return { + value: values[index], + done: false + }; + } +}, Object.getPrototypeOf(Object.getPrototypeOf([][Symbol.iterator]()))); + +Object.defineProperty(HeadersIteratorPrototype, Symbol.toStringTag, { + value: 'HeadersIterator', + writable: false, + enumerable: false, + configurable: true +}); + +/** + * Export the Headers object in a form that Node.js can consume. + * + * @param Headers headers + * @return Object + */ +function exportNodeCompatibleHeaders(headers) { + const obj = Object.assign({ __proto__: null }, headers[MAP]); + + // http.request() only supports string as Host header. This hack makes + // specifying custom Host header possible. + const hostHeaderKey = find(headers[MAP], 'Host'); + if (hostHeaderKey !== undefined) { + obj[hostHeaderKey] = obj[hostHeaderKey][0]; + } + + return obj; +} + +/** + * Create a Headers object from an object of headers, ignoring those that do + * not conform to HTTP grammar productions. + * + * @param Object obj Object of headers + * @return Headers + */ +function createHeadersLenient(obj) { + const headers = new Headers(); + for (const name of Object.keys(obj)) { + if (invalidTokenRegex.test(name)) { + continue; + } + if (Array.isArray(obj[name])) { + for (const val of obj[name]) { + if (invalidHeaderCharRegex.test(val)) { + continue; + } + if (headers[MAP][name] === undefined) { + headers[MAP][name] = [val]; + } else { + headers[MAP][name].push(val); + } + } + } else if (!invalidHeaderCharRegex.test(obj[name])) { + headers[MAP][name] = [obj[name]]; + } + } + return headers; +} + +const INTERNALS$1 = Symbol('Response internals'); + +// fix an issue where "STATUS_CODES" aren't a named export for node <10 +const STATUS_CODES = http.STATUS_CODES; + +/** + * Response class + * + * @param Stream body Readable stream + * @param Object opts Response options + * @return Void + */ +class Response { + constructor() { + let body = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null; + let opts = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; + + Body.call(this, body, opts); + + const status = opts.status || 200; + const headers = new Headers(opts.headers); + + if (body != null && !headers.has('Content-Type')) { + const contentType = extractContentType(body); + if (contentType) { + headers.append('Content-Type', contentType); + } + } + + this[INTERNALS$1] = { + url: opts.url, + status, + statusText: opts.statusText || STATUS_CODES[status], + headers, + counter: opts.counter + }; + } + + get url() { + return this[INTERNALS$1].url || ''; + } + + get status() { + return this[INTERNALS$1].status; + } + + /** + * Convenience property representing if the request ended normally + */ + get ok() { + return this[INTERNALS$1].status >= 200 && this[INTERNALS$1].status < 300; + } + + get redirected() { + return this[INTERNALS$1].counter > 0; + } + + get statusText() { + return this[INTERNALS$1].statusText; + } + + get headers() { + return this[INTERNALS$1].headers; + } + + /** + * Clone this response + * + * @return Response + */ + clone() { + return new Response(clone(this), { + url: this.url, + status: this.status, + statusText: this.statusText, + headers: this.headers, + ok: this.ok, + redirected: this.redirected + }); + } +} + +Body.mixIn(Response.prototype); + +Object.defineProperties(Response.prototype, { + url: { enumerable: true }, + status: { enumerable: true }, + ok: { enumerable: true }, + redirected: { enumerable: true }, + statusText: { enumerable: true }, + headers: { enumerable: true }, + clone: { enumerable: true } +}); + +Object.defineProperty(Response.prototype, Symbol.toStringTag, { + value: 'Response', + writable: false, + enumerable: false, + configurable: true +}); + +const INTERNALS$2 = Symbol('Request internals'); +const URL = Url.URL || whatwgUrl.URL; + +// fix an issue where "format", "parse" aren't a named export for node <10 +const parse_url = Url.parse; +const format_url = Url.format; + +/** + * Wrapper around `new URL` to handle arbitrary URLs + * + * @param {string} urlStr + * @return {void} + */ +function parseURL(urlStr) { + /* + Check whether the URL is absolute or not + Scheme: https://tools.ietf.org/html/rfc3986#section-3.1 + Absolute URL: https://tools.ietf.org/html/rfc3986#section-4.3 + */ + if (/^[a-zA-Z][a-zA-Z\d+\-.]*:/.exec(urlStr)) { + urlStr = new URL(urlStr).toString(); + } + + // Fallback to old implementation for arbitrary URLs + return parse_url(urlStr); +} + +const streamDestructionSupported = 'destroy' in Stream.Readable.prototype; + +/** + * Check if a value is an instance of Request. + * + * @param Mixed input + * @return Boolean + */ +function isRequest(input) { + return typeof input === 'object' && typeof input[INTERNALS$2] === 'object'; +} + +function isAbortSignal(signal) { + const proto = signal && typeof signal === 'object' && Object.getPrototypeOf(signal); + return !!(proto && proto.constructor.name === 'AbortSignal'); +} + +/** + * Request class + * + * @param Mixed input Url or Request instance + * @param Object init Custom options + * @return Void + */ +class Request { + constructor(input) { + let init = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; + + let parsedURL; + + // normalize input + if (!isRequest(input)) { + if (input && input.href) { + // in order to support Node.js' Url objects; though WHATWG's URL objects + // will fall into this branch also (since their `toString()` will return + // `href` property anyway) + parsedURL = parseURL(input.href); + } else { + // coerce input to a string before attempting to parse + parsedURL = parseURL(`${input}`); + } + input = {}; + } else { + parsedURL = parseURL(input.url); + } + + let method = init.method || input.method || 'GET'; + method = method.toUpperCase(); + + if ((init.body != null || isRequest(input) && input.body !== null) && (method === 'GET' || method === 'HEAD')) { + throw new TypeError('Request with GET/HEAD method cannot have body'); + } + + let inputBody = init.body != null ? init.body : isRequest(input) && input.body !== null ? clone(input) : null; + + Body.call(this, inputBody, { + timeout: init.timeout || input.timeout || 0, + size: init.size || input.size || 0 + }); + + const headers = new Headers(init.headers || input.headers || {}); + + if (inputBody != null && !headers.has('Content-Type')) { + const contentType = extractContentType(inputBody); + if (contentType) { + headers.append('Content-Type', contentType); + } + } + + let signal = isRequest(input) ? input.signal : null; + if ('signal' in init) signal = init.signal; + + if (signal != null && !isAbortSignal(signal)) { + throw new TypeError('Expected signal to be an instanceof AbortSignal'); + } + + this[INTERNALS$2] = { + method, + redirect: init.redirect || input.redirect || 'follow', + headers, + parsedURL, + signal + }; + + // node-fetch-only options + this.follow = init.follow !== undefined ? init.follow : input.follow !== undefined ? input.follow : 20; + this.compress = init.compress !== undefined ? init.compress : input.compress !== undefined ? input.compress : true; + this.counter = init.counter || input.counter || 0; + this.agent = init.agent || input.agent; + } + + get method() { + return this[INTERNALS$2].method; + } + + get url() { + return format_url(this[INTERNALS$2].parsedURL); + } + + get headers() { + return this[INTERNALS$2].headers; + } + + get redirect() { + return this[INTERNALS$2].redirect; + } + + get signal() { + return this[INTERNALS$2].signal; + } + + /** + * Clone this request + * + * @return Request + */ + clone() { + return new Request(this); + } +} + +Body.mixIn(Request.prototype); + +Object.defineProperty(Request.prototype, Symbol.toStringTag, { + value: 'Request', + writable: false, + enumerable: false, + configurable: true +}); + +Object.defineProperties(Request.prototype, { + method: { enumerable: true }, + url: { enumerable: true }, + headers: { enumerable: true }, + redirect: { enumerable: true }, + clone: { enumerable: true }, + signal: { enumerable: true } +}); + +/** + * Convert a Request to Node.js http request options. + * + * @param Request A Request instance + * @return Object The options object to be passed to http.request + */ +function getNodeRequestOptions(request) { + const parsedURL = request[INTERNALS$2].parsedURL; + const headers = new Headers(request[INTERNALS$2].headers); + + // fetch step 1.3 + if (!headers.has('Accept')) { + headers.set('Accept', '*/*'); + } + + // Basic fetch + if (!parsedURL.protocol || !parsedURL.hostname) { + throw new TypeError('Only absolute URLs are supported'); + } + + if (!/^https?:$/.test(parsedURL.protocol)) { + throw new TypeError('Only HTTP(S) protocols are supported'); + } + + if (request.signal && request.body instanceof Stream.Readable && !streamDestructionSupported) { + throw new Error('Cancellation of streamed requests with AbortSignal is not supported in node < 8'); + } + + // HTTP-network-or-cache fetch steps 2.4-2.7 + let contentLengthValue = null; + if (request.body == null && /^(POST|PUT)$/i.test(request.method)) { + contentLengthValue = '0'; + } + if (request.body != null) { + const totalBytes = getTotalBytes(request); + if (typeof totalBytes === 'number') { + contentLengthValue = String(totalBytes); + } + } + if (contentLengthValue) { + headers.set('Content-Length', contentLengthValue); + } + + // HTTP-network-or-cache fetch step 2.11 + if (!headers.has('User-Agent')) { + headers.set('User-Agent', 'node-fetch/1.0 (+https://github.com/bitinn/node-fetch)'); + } + + // HTTP-network-or-cache fetch step 2.15 + if (request.compress && !headers.has('Accept-Encoding')) { + headers.set('Accept-Encoding', 'gzip,deflate'); + } + + let agent = request.agent; + if (typeof agent === 'function') { + agent = agent(parsedURL); + } + + if (!headers.has('Connection') && !agent) { + headers.set('Connection', 'close'); + } + + // HTTP-network fetch step 4.2 + // chunked encoding is handled by Node.js + + return Object.assign({}, parsedURL, { + method: request.method, + headers: exportNodeCompatibleHeaders(headers), + agent + }); +} + +/** + * abort-error.js + * + * AbortError interface for cancelled requests + */ + +/** + * Create AbortError instance + * + * @param String message Error message for human + * @return AbortError + */ +function AbortError(message) { + Error.call(this, message); + + this.type = 'aborted'; + this.message = message; + + // hide custom error implementation details from end-users + Error.captureStackTrace(this, this.constructor); +} + +AbortError.prototype = Object.create(Error.prototype); +AbortError.prototype.constructor = AbortError; +AbortError.prototype.name = 'AbortError'; + +const URL$1 = Url.URL || whatwgUrl.URL; + +// fix an issue where "PassThrough", "resolve" aren't a named export for node <10 +const PassThrough$1 = Stream.PassThrough; + +const isDomainOrSubdomain = function isDomainOrSubdomain(destination, original) { + const orig = new URL$1(original).hostname; + const dest = new URL$1(destination).hostname; + + return orig === dest || orig[orig.length - dest.length - 1] === '.' && orig.endsWith(dest); +}; + +/** + * isSameProtocol reports whether the two provided URLs use the same protocol. + * + * Both domains must already be in canonical form. + * @param {string|URL} original + * @param {string|URL} destination + */ +const isSameProtocol = function isSameProtocol(destination, original) { + const orig = new URL$1(original).protocol; + const dest = new URL$1(destination).protocol; + + return orig === dest; +}; + +/** + * Fetch function + * + * @param Mixed url Absolute url or Request instance + * @param Object opts Fetch options + * @return Promise + */ +function fetch(url, opts) { + + // allow custom promise + if (!fetch.Promise) { + throw new Error('native promise missing, set fetch.Promise to your favorite alternative'); + } + + Body.Promise = fetch.Promise; + + // wrap http.request into fetch + return new fetch.Promise(function (resolve, reject) { + // build request object + const request = new Request(url, opts); + const options = getNodeRequestOptions(request); + + const send = (options.protocol === 'https:' ? https : http).request; + const signal = request.signal; + + let response = null; + + const abort = function abort() { + let error = new AbortError('The user aborted a request.'); + reject(error); + if (request.body && request.body instanceof Stream.Readable) { + destroyStream(request.body, error); + } + if (!response || !response.body) return; + response.body.emit('error', error); + }; + + if (signal && signal.aborted) { + abort(); + return; + } + + const abortAndFinalize = function abortAndFinalize() { + abort(); + finalize(); + }; + + // send request + const req = send(options); + let reqTimeout; + + if (signal) { + signal.addEventListener('abort', abortAndFinalize); + } + + function finalize() { + req.abort(); + if (signal) signal.removeEventListener('abort', abortAndFinalize); + clearTimeout(reqTimeout); + } + + if (request.timeout) { + req.once('socket', function (socket) { + reqTimeout = setTimeout(function () { + reject(new FetchError(`network timeout at: ${request.url}`, 'request-timeout')); + finalize(); + }, request.timeout); + }); + } + + req.on('error', function (err) { + reject(new FetchError(`request to ${request.url} failed, reason: ${err.message}`, 'system', err)); + + if (response && response.body) { + destroyStream(response.body, err); + } + + finalize(); + }); + + fixResponseChunkedTransferBadEnding(req, function (err) { + if (signal && signal.aborted) { + return; + } + + if (response && response.body) { + destroyStream(response.body, err); + } + }); + + /* c8 ignore next 18 */ + if (parseInt(process.version.substring(1)) < 14) { + // Before Node.js 14, pipeline() does not fully support async iterators and does not always + // properly handle when the socket close/end events are out of order. + req.on('socket', function (s) { + s.addListener('close', function (hadError) { + // if a data listener is still present we didn't end cleanly + const hasDataListener = s.listenerCount('data') > 0; + + // if end happened before close but the socket didn't emit an error, do it now + if (response && hasDataListener && !hadError && !(signal && signal.aborted)) { + const err = new Error('Premature close'); + err.code = 'ERR_STREAM_PREMATURE_CLOSE'; + response.body.emit('error', err); + } + }); + }); + } + + req.on('response', function (res) { + clearTimeout(reqTimeout); + + const headers = createHeadersLenient(res.headers); + + // HTTP fetch step 5 + if (fetch.isRedirect(res.statusCode)) { + // HTTP fetch step 5.2 + const location = headers.get('Location'); + + // HTTP fetch step 5.3 + let locationURL = null; + try { + locationURL = location === null ? null : new URL$1(location, request.url).toString(); + } catch (err) { + // error here can only be invalid URL in Location: header + // do not throw when options.redirect == manual + // let the user extract the errorneous redirect URL + if (request.redirect !== 'manual') { + reject(new FetchError(`uri requested responds with an invalid redirect URL: ${location}`, 'invalid-redirect')); + finalize(); + return; + } + } + + // HTTP fetch step 5.5 + switch (request.redirect) { + case 'error': + reject(new FetchError(`uri requested responds with a redirect, redirect mode is set to error: ${request.url}`, 'no-redirect')); + finalize(); + return; + case 'manual': + // node-fetch-specific step: make manual redirect a bit easier to use by setting the Location header value to the resolved URL. + if (locationURL !== null) { + // handle corrupted header + try { + headers.set('Location', locationURL); + } catch (err) { + // istanbul ignore next: nodejs server prevent invalid response headers, we can't test this through normal request + reject(err); + } + } + break; + case 'follow': + // HTTP-redirect fetch step 2 + if (locationURL === null) { + break; + } + + // HTTP-redirect fetch step 5 + if (request.counter >= request.follow) { + reject(new FetchError(`maximum redirect reached at: ${request.url}`, 'max-redirect')); + finalize(); + return; + } + + // HTTP-redirect fetch step 6 (counter increment) + // Create a new Request object. + const requestOpts = { + headers: new Headers(request.headers), + follow: request.follow, + counter: request.counter + 1, + agent: request.agent, + compress: request.compress, + method: request.method, + body: request.body, + signal: request.signal, + timeout: request.timeout, + size: request.size + }; + + if (!isDomainOrSubdomain(request.url, locationURL) || !isSameProtocol(request.url, locationURL)) { + for (const name of ['authorization', 'www-authenticate', 'cookie', 'cookie2']) { + requestOpts.headers.delete(name); + } + } + + // HTTP-redirect fetch step 9 + if (res.statusCode !== 303 && request.body && getTotalBytes(request) === null) { + reject(new FetchError('Cannot follow redirect with body being a readable stream', 'unsupported-redirect')); + finalize(); + return; + } + + // HTTP-redirect fetch step 11 + if (res.statusCode === 303 || (res.statusCode === 301 || res.statusCode === 302) && request.method === 'POST') { + requestOpts.method = 'GET'; + requestOpts.body = undefined; + requestOpts.headers.delete('content-length'); + } + + // HTTP-redirect fetch step 15 + resolve(fetch(new Request(locationURL, requestOpts))); + finalize(); + return; + } + } + + // prepare response + res.once('end', function () { + if (signal) signal.removeEventListener('abort', abortAndFinalize); + }); + let body = res.pipe(new PassThrough$1()); + + const response_options = { + url: request.url, + status: res.statusCode, + statusText: res.statusMessage, + headers: headers, + size: request.size, + timeout: request.timeout, + counter: request.counter + }; + + // HTTP-network fetch step 12.1.1.3 + const codings = headers.get('Content-Encoding'); + + // HTTP-network fetch step 12.1.1.4: handle content codings + + // in following scenarios we ignore compression support + // 1. compression support is disabled + // 2. HEAD request + // 3. no Content-Encoding header + // 4. no content response (204) + // 5. content not modified response (304) + if (!request.compress || request.method === 'HEAD' || codings === null || res.statusCode === 204 || res.statusCode === 304) { + response = new Response(body, response_options); + resolve(response); + return; + } + + // For Node v6+ + // Be less strict when decoding compressed responses, since sometimes + // servers send slightly invalid responses that are still accepted + // by common browsers. + // Always using Z_SYNC_FLUSH is what cURL does. + const zlibOptions = { + flush: zlib.Z_SYNC_FLUSH, + finishFlush: zlib.Z_SYNC_FLUSH + }; + + // for gzip + if (codings == 'gzip' || codings == 'x-gzip') { + body = body.pipe(zlib.createGunzip(zlibOptions)); + response = new Response(body, response_options); + resolve(response); + return; + } + + // for deflate + if (codings == 'deflate' || codings == 'x-deflate') { + // handle the infamous raw deflate response from old servers + // a hack for old IIS and Apache servers + const raw = res.pipe(new PassThrough$1()); + raw.once('data', function (chunk) { + // see http://stackoverflow.com/questions/37519828 + if ((chunk[0] & 0x0F) === 0x08) { + body = body.pipe(zlib.createInflate()); + } else { + body = body.pipe(zlib.createInflateRaw()); + } + response = new Response(body, response_options); + resolve(response); + }); + raw.on('end', function () { + // some old IIS servers return zero-length OK deflate responses, so 'data' is never emitted. + if (!response) { + response = new Response(body, response_options); + resolve(response); + } + }); + return; + } + + // for br + if (codings == 'br' && typeof zlib.createBrotliDecompress === 'function') { + body = body.pipe(zlib.createBrotliDecompress()); + response = new Response(body, response_options); + resolve(response); + return; + } + + // otherwise, use response as-is + response = new Response(body, response_options); + resolve(response); + }); + + writeToStream(req, request); + }); +} +function fixResponseChunkedTransferBadEnding(request, errorCallback) { + let socket; + + request.on('socket', function (s) { + socket = s; + }); + + request.on('response', function (response) { + const headers = response.headers; + + if (headers['transfer-encoding'] === 'chunked' && !headers['content-length']) { + response.once('close', function (hadError) { + // if a data listener is still present we didn't end cleanly + const hasDataListener = socket.listenerCount('data') > 0; + + if (hasDataListener && !hadError) { + const err = new Error('Premature close'); + err.code = 'ERR_STREAM_PREMATURE_CLOSE'; + errorCallback(err); + } + }); + } + }); +} + +function destroyStream(stream, err) { + if (stream.destroy) { + stream.destroy(err); + } else { + // node < 8 + stream.emit('error', err); + stream.end(); + } +} + +/** + * Redirect code matching + * + * @param Number code Status code + * @return Boolean + */ +fetch.isRedirect = function (code) { + return code === 301 || code === 302 || code === 303 || code === 307 || code === 308; +}; + +// expose Promise +fetch.Promise = global.Promise; + +module.exports = exports = fetch; +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports["default"] = exports; +exports.Headers = Headers; +exports.Request = Request; +exports.Response = Response; +exports.FetchError = FetchError; + + +/***/ }), + +/***/ 1552: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +var punycode = __nccwpck_require__(4876); +var mappingTable = __nccwpck_require__(2472); + +var PROCESSING_OPTIONS = { + TRANSITIONAL: 0, + NONTRANSITIONAL: 1 +}; + +function normalize(str) { // fix bug in v8 + return str.split('\u0000').map(function (s) { return s.normalize('NFC'); }).join('\u0000'); +} + +function findStatus(val) { + var start = 0; + var end = mappingTable.length - 1; + + while (start <= end) { + var mid = Math.floor((start + end) / 2); + + var target = mappingTable[mid]; + if (target[0][0] <= val && target[0][1] >= val) { + return target; + } else if (target[0][0] > val) { + end = mid - 1; + } else { + start = mid + 1; + } + } + + return null; +} + +var regexAstralSymbols = /[\uD800-\uDBFF][\uDC00-\uDFFF]/g; + +function countSymbols(string) { + return string + // replace every surrogate pair with a BMP symbol + .replace(regexAstralSymbols, '_') + // then get the length + .length; +} + +function mapChars(domain_name, useSTD3, processing_option) { + var hasError = false; + var processed = ""; + + var len = countSymbols(domain_name); + for (var i = 0; i < len; ++i) { + var codePoint = domain_name.codePointAt(i); + var status = findStatus(codePoint); + + switch (status[1]) { + case "disallowed": + hasError = true; + processed += String.fromCodePoint(codePoint); + break; + case "ignored": + break; + case "mapped": + processed += String.fromCodePoint.apply(String, status[2]); + break; + case "deviation": + if (processing_option === PROCESSING_OPTIONS.TRANSITIONAL) { + processed += String.fromCodePoint.apply(String, status[2]); + } else { + processed += String.fromCodePoint(codePoint); + } + break; + case "valid": + processed += String.fromCodePoint(codePoint); + break; + case "disallowed_STD3_mapped": + if (useSTD3) { + hasError = true; + processed += String.fromCodePoint(codePoint); + } else { + processed += String.fromCodePoint.apply(String, status[2]); + } + break; + case "disallowed_STD3_valid": + if (useSTD3) { + hasError = true; + } + + processed += String.fromCodePoint(codePoint); + break; + } + } + + return { + string: processed, + error: hasError + }; +} + +var combiningMarksRegex = /[\u0300-\u036F\u0483-\u0489\u0591-\u05BD\u05BF\u05C1\u05C2\u05C4\u05C5\u05C7\u0610-\u061A\u064B-\u065F\u0670\u06D6-\u06DC\u06DF-\u06E4\u06E7\u06E8\u06EA-\u06ED\u0711\u0730-\u074A\u07A6-\u07B0\u07EB-\u07F3\u0816-\u0819\u081B-\u0823\u0825-\u0827\u0829-\u082D\u0859-\u085B\u08E4-\u0903\u093A-\u093C\u093E-\u094F\u0951-\u0957\u0962\u0963\u0981-\u0983\u09BC\u09BE-\u09C4\u09C7\u09C8\u09CB-\u09CD\u09D7\u09E2\u09E3\u0A01-\u0A03\u0A3C\u0A3E-\u0A42\u0A47\u0A48\u0A4B-\u0A4D\u0A51\u0A70\u0A71\u0A75\u0A81-\u0A83\u0ABC\u0ABE-\u0AC5\u0AC7-\u0AC9\u0ACB-\u0ACD\u0AE2\u0AE3\u0B01-\u0B03\u0B3C\u0B3E-\u0B44\u0B47\u0B48\u0B4B-\u0B4D\u0B56\u0B57\u0B62\u0B63\u0B82\u0BBE-\u0BC2\u0BC6-\u0BC8\u0BCA-\u0BCD\u0BD7\u0C00-\u0C03\u0C3E-\u0C44\u0C46-\u0C48\u0C4A-\u0C4D\u0C55\u0C56\u0C62\u0C63\u0C81-\u0C83\u0CBC\u0CBE-\u0CC4\u0CC6-\u0CC8\u0CCA-\u0CCD\u0CD5\u0CD6\u0CE2\u0CE3\u0D01-\u0D03\u0D3E-\u0D44\u0D46-\u0D48\u0D4A-\u0D4D\u0D57\u0D62\u0D63\u0D82\u0D83\u0DCA\u0DCF-\u0DD4\u0DD6\u0DD8-\u0DDF\u0DF2\u0DF3\u0E31\u0E34-\u0E3A\u0E47-\u0E4E\u0EB1\u0EB4-\u0EB9\u0EBB\u0EBC\u0EC8-\u0ECD\u0F18\u0F19\u0F35\u0F37\u0F39\u0F3E\u0F3F\u0F71-\u0F84\u0F86\u0F87\u0F8D-\u0F97\u0F99-\u0FBC\u0FC6\u102B-\u103E\u1056-\u1059\u105E-\u1060\u1062-\u1064\u1067-\u106D\u1071-\u1074\u1082-\u108D\u108F\u109A-\u109D\u135D-\u135F\u1712-\u1714\u1732-\u1734\u1752\u1753\u1772\u1773\u17B4-\u17D3\u17DD\u180B-\u180D\u18A9\u1920-\u192B\u1930-\u193B\u19B0-\u19C0\u19C8\u19C9\u1A17-\u1A1B\u1A55-\u1A5E\u1A60-\u1A7C\u1A7F\u1AB0-\u1ABE\u1B00-\u1B04\u1B34-\u1B44\u1B6B-\u1B73\u1B80-\u1B82\u1BA1-\u1BAD\u1BE6-\u1BF3\u1C24-\u1C37\u1CD0-\u1CD2\u1CD4-\u1CE8\u1CED\u1CF2-\u1CF4\u1CF8\u1CF9\u1DC0-\u1DF5\u1DFC-\u1DFF\u20D0-\u20F0\u2CEF-\u2CF1\u2D7F\u2DE0-\u2DFF\u302A-\u302F\u3099\u309A\uA66F-\uA672\uA674-\uA67D\uA69F\uA6F0\uA6F1\uA802\uA806\uA80B\uA823-\uA827\uA880\uA881\uA8B4-\uA8C4\uA8E0-\uA8F1\uA926-\uA92D\uA947-\uA953\uA980-\uA983\uA9B3-\uA9C0\uA9E5\uAA29-\uAA36\uAA43\uAA4C\uAA4D\uAA7B-\uAA7D\uAAB0\uAAB2-\uAAB4\uAAB7\uAAB8\uAABE\uAABF\uAAC1\uAAEB-\uAAEF\uAAF5\uAAF6\uABE3-\uABEA\uABEC\uABED\uFB1E\uFE00-\uFE0F\uFE20-\uFE2D]|\uD800[\uDDFD\uDEE0\uDF76-\uDF7A]|\uD802[\uDE01-\uDE03\uDE05\uDE06\uDE0C-\uDE0F\uDE38-\uDE3A\uDE3F\uDEE5\uDEE6]|\uD804[\uDC00-\uDC02\uDC38-\uDC46\uDC7F-\uDC82\uDCB0-\uDCBA\uDD00-\uDD02\uDD27-\uDD34\uDD73\uDD80-\uDD82\uDDB3-\uDDC0\uDE2C-\uDE37\uDEDF-\uDEEA\uDF01-\uDF03\uDF3C\uDF3E-\uDF44\uDF47\uDF48\uDF4B-\uDF4D\uDF57\uDF62\uDF63\uDF66-\uDF6C\uDF70-\uDF74]|\uD805[\uDCB0-\uDCC3\uDDAF-\uDDB5\uDDB8-\uDDC0\uDE30-\uDE40\uDEAB-\uDEB7]|\uD81A[\uDEF0-\uDEF4\uDF30-\uDF36]|\uD81B[\uDF51-\uDF7E\uDF8F-\uDF92]|\uD82F[\uDC9D\uDC9E]|\uD834[\uDD65-\uDD69\uDD6D-\uDD72\uDD7B-\uDD82\uDD85-\uDD8B\uDDAA-\uDDAD\uDE42-\uDE44]|\uD83A[\uDCD0-\uDCD6]|\uDB40[\uDD00-\uDDEF]/; + +function validateLabel(label, processing_option) { + if (label.substr(0, 4) === "xn--") { + label = punycode.toUnicode(label); + processing_option = PROCESSING_OPTIONS.NONTRANSITIONAL; + } + + var error = false; + + if (normalize(label) !== label || + (label[3] === "-" && label[4] === "-") || + label[0] === "-" || label[label.length - 1] === "-" || + label.indexOf(".") !== -1 || + label.search(combiningMarksRegex) === 0) { + error = true; + } + + var len = countSymbols(label); + for (var i = 0; i < len; ++i) { + var status = findStatus(label.codePointAt(i)); + if ((processing === PROCESSING_OPTIONS.TRANSITIONAL && status[1] !== "valid") || + (processing === PROCESSING_OPTIONS.NONTRANSITIONAL && + status[1] !== "valid" && status[1] !== "deviation")) { + error = true; + break; + } + } + + return { + label: label, + error: error + }; +} + +function processing(domain_name, useSTD3, processing_option) { + var result = mapChars(domain_name, useSTD3, processing_option); + result.string = normalize(result.string); + + var labels = result.string.split("."); + for (var i = 0; i < labels.length; ++i) { + try { + var validation = validateLabel(labels[i]); + labels[i] = validation.label; + result.error = result.error || validation.error; + } catch(e) { + result.error = true; + } + } + + return { + string: labels.join("."), + error: result.error + }; +} + +module.exports.toASCII = function(domain_name, useSTD3, processing_option, verifyDnsLength) { + var result = processing(domain_name, useSTD3, processing_option); + var labels = result.string.split("."); + labels = labels.map(function(l) { + try { + return punycode.toASCII(l); + } catch(e) { + result.error = true; + return l; + } + }); + + if (verifyDnsLength) { + var total = labels.slice(0, labels.length - 1).join(".").length; + if (total.length > 253 || total.length === 0) { + result.error = true; + } + + for (var i=0; i < labels.length; ++i) { + if (labels.length > 63 || labels.length === 0) { + result.error = true; + break; + } + } + } + + if (result.error) return null; + return labels.join("."); +}; + +module.exports.toUnicode = function(domain_name, useSTD3) { + var result = processing(domain_name, useSTD3, PROCESSING_OPTIONS.NONTRANSITIONAL); + + return { + domain: result.string, + error: result.error + }; +}; + +module.exports.PROCESSING_OPTIONS = PROCESSING_OPTIONS; + + +/***/ }), + +/***/ 770: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +module.exports = __nccwpck_require__(218); + + +/***/ }), + +/***/ 218: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + + +var net = __nccwpck_require__(9278); +var tls = __nccwpck_require__(4756); +var http = __nccwpck_require__(8611); +var https = __nccwpck_require__(5692); +var events = __nccwpck_require__(4434); +var assert = __nccwpck_require__(2613); +var util = __nccwpck_require__(9023); + + +exports.httpOverHttp = httpOverHttp; +exports.httpsOverHttp = httpsOverHttp; +exports.httpOverHttps = httpOverHttps; +exports.httpsOverHttps = httpsOverHttps; + + +function httpOverHttp(options) { + var agent = new TunnelingAgent(options); + agent.request = http.request; + return agent; +} + +function httpsOverHttp(options) { + var agent = new TunnelingAgent(options); + agent.request = http.request; + agent.createSocket = createSecureSocket; + agent.defaultPort = 443; + return agent; +} + +function httpOverHttps(options) { + var agent = new TunnelingAgent(options); + agent.request = https.request; + return agent; +} + +function httpsOverHttps(options) { + var agent = new TunnelingAgent(options); + agent.request = https.request; + agent.createSocket = createSecureSocket; + agent.defaultPort = 443; + return agent; +} + + +function TunnelingAgent(options) { + var self = this; + self.options = options || {}; + self.proxyOptions = self.options.proxy || {}; + self.maxSockets = self.options.maxSockets || http.Agent.defaultMaxSockets; + self.requests = []; + self.sockets = []; + + self.on('free', function onFree(socket, host, port, localAddress) { + var options = toOptions(host, port, localAddress); + for (var i = 0, len = self.requests.length; i < len; ++i) { + var pending = self.requests[i]; + if (pending.host === options.host && pending.port === options.port) { + // Detect the request to connect same origin server, + // reuse the connection. + self.requests.splice(i, 1); + pending.request.onSocket(socket); + return; + } + } + socket.destroy(); + self.removeSocket(socket); + }); +} +util.inherits(TunnelingAgent, events.EventEmitter); + +TunnelingAgent.prototype.addRequest = function addRequest(req, host, port, localAddress) { + var self = this; + var options = mergeOptions({request: req}, self.options, toOptions(host, port, localAddress)); + + if (self.sockets.length >= this.maxSockets) { + // We are over limit so we'll add it to the queue. + self.requests.push(options); + return; + } + + // If we are under maxSockets create a new one. + self.createSocket(options, function(socket) { + socket.on('free', onFree); + socket.on('close', onCloseOrRemove); + socket.on('agentRemove', onCloseOrRemove); + req.onSocket(socket); + + function onFree() { + self.emit('free', socket, options); + } + + function onCloseOrRemove(err) { + self.removeSocket(socket); + socket.removeListener('free', onFree); + socket.removeListener('close', onCloseOrRemove); + socket.removeListener('agentRemove', onCloseOrRemove); + } + }); +}; + +TunnelingAgent.prototype.createSocket = function createSocket(options, cb) { + var self = this; + var placeholder = {}; + self.sockets.push(placeholder); + + var connectOptions = mergeOptions({}, self.proxyOptions, { + method: 'CONNECT', + path: options.host + ':' + options.port, + agent: false, + headers: { + host: options.host + ':' + options.port + } + }); + if (options.localAddress) { + connectOptions.localAddress = options.localAddress; + } + if (connectOptions.proxyAuth) { + connectOptions.headers = connectOptions.headers || {}; + connectOptions.headers['Proxy-Authorization'] = 'Basic ' + + new Buffer(connectOptions.proxyAuth).toString('base64'); + } + + debug('making CONNECT request'); + var connectReq = self.request(connectOptions); + connectReq.useChunkedEncodingByDefault = false; // for v0.6 + connectReq.once('response', onResponse); // for v0.6 + connectReq.once('upgrade', onUpgrade); // for v0.6 + connectReq.once('connect', onConnect); // for v0.7 or later + connectReq.once('error', onError); + connectReq.end(); + + function onResponse(res) { + // Very hacky. This is necessary to avoid http-parser leaks. + res.upgrade = true; + } + + function onUpgrade(res, socket, head) { + // Hacky. + process.nextTick(function() { + onConnect(res, socket, head); + }); + } + + function onConnect(res, socket, head) { + connectReq.removeAllListeners(); + socket.removeAllListeners(); + + if (res.statusCode !== 200) { + debug('tunneling socket could not be established, statusCode=%d', + res.statusCode); + socket.destroy(); + var error = new Error('tunneling socket could not be established, ' + + 'statusCode=' + res.statusCode); + error.code = 'ECONNRESET'; + options.request.emit('error', error); + self.removeSocket(placeholder); + return; + } + if (head.length > 0) { + debug('got illegal response body from proxy'); + socket.destroy(); + var error = new Error('got illegal response body from proxy'); + error.code = 'ECONNRESET'; + options.request.emit('error', error); + self.removeSocket(placeholder); + return; + } + debug('tunneling connection has established'); + self.sockets[self.sockets.indexOf(placeholder)] = socket; + return cb(socket); + } + + function onError(cause) { + connectReq.removeAllListeners(); + + debug('tunneling socket could not be established, cause=%s\n', + cause.message, cause.stack); + var error = new Error('tunneling socket could not be established, ' + + 'cause=' + cause.message); + error.code = 'ECONNRESET'; + options.request.emit('error', error); + self.removeSocket(placeholder); + } +}; + +TunnelingAgent.prototype.removeSocket = function removeSocket(socket) { + var pos = this.sockets.indexOf(socket) + if (pos === -1) { + return; + } + this.sockets.splice(pos, 1); + + var pending = this.requests.shift(); + if (pending) { + // If we have pending requests and a socket gets closed a new one + // needs to be created to take over in the pool for the one that closed. + this.createSocket(pending, function(socket) { + pending.request.onSocket(socket); + }); + } +}; + +function createSecureSocket(options, cb) { + var self = this; + TunnelingAgent.prototype.createSocket.call(self, options, function(socket) { + var hostHeader = options.request.getHeader('host'); + var tlsOptions = mergeOptions({}, self.options, { + socket: socket, + servername: hostHeader ? hostHeader.replace(/:.*$/, '') : options.host + }); + + // 0 is dummy port for v0.6 + var secureSocket = tls.connect(0, tlsOptions); + self.sockets[self.sockets.indexOf(socket)] = secureSocket; + cb(secureSocket); + }); +} + + +function toOptions(host, port, localAddress) { + if (typeof host === 'string') { // since v0.10 + return { + host: host, + port: port, + localAddress: localAddress + }; + } + return host; // for v0.11 or later +} + +function mergeOptions(target) { + for (var i = 1, len = arguments.length; i < len; ++i) { + var overrides = arguments[i]; + if (typeof overrides === 'object') { + var keys = Object.keys(overrides); + for (var j = 0, keyLen = keys.length; j < keyLen; ++j) { + var k = keys[j]; + if (overrides[k] !== undefined) { + target[k] = overrides[k]; + } + } + } + } + return target; +} + + +var debug; +if (process.env.NODE_DEBUG && /\btunnel\b/.test(process.env.NODE_DEBUG)) { + debug = function() { + var args = Array.prototype.slice.call(arguments); + if (typeof args[0] === 'string') { + args[0] = 'TUNNEL: ' + args[0]; + } else { + args.unshift('TUNNEL:'); + } + console.error.apply(console, args); + } +} else { + debug = function() {}; +} +exports.debug = debug; // for test + + +/***/ }), + +/***/ 6752: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const Client = __nccwpck_require__(6197) +const Dispatcher = __nccwpck_require__(992) +const errors = __nccwpck_require__(8707) +const Pool = __nccwpck_require__(5076) +const BalancedPool = __nccwpck_require__(1093) +const Agent = __nccwpck_require__(9965) +const util = __nccwpck_require__(3440) +const { InvalidArgumentError } = errors +const api = __nccwpck_require__(6615) +const buildConnector = __nccwpck_require__(9136) +const MockClient = __nccwpck_require__(7365) +const MockAgent = __nccwpck_require__(7501) +const MockPool = __nccwpck_require__(4004) +const mockErrors = __nccwpck_require__(2429) +const ProxyAgent = __nccwpck_require__(2720) +const RetryHandler = __nccwpck_require__(3573) +const { getGlobalDispatcher, setGlobalDispatcher } = __nccwpck_require__(2581) +const DecoratorHandler = __nccwpck_require__(8840) +const RedirectHandler = __nccwpck_require__(8299) +const createRedirectInterceptor = __nccwpck_require__(4415) + +let hasCrypto +try { + __nccwpck_require__(6982) + hasCrypto = true +} catch { + hasCrypto = false +} + +Object.assign(Dispatcher.prototype, api) + +module.exports.Dispatcher = Dispatcher +module.exports.Client = Client +module.exports.Pool = Pool +module.exports.BalancedPool = BalancedPool +module.exports.Agent = Agent +module.exports.ProxyAgent = ProxyAgent +module.exports.RetryHandler = RetryHandler + +module.exports.DecoratorHandler = DecoratorHandler +module.exports.RedirectHandler = RedirectHandler +module.exports.createRedirectInterceptor = createRedirectInterceptor + +module.exports.buildConnector = buildConnector +module.exports.errors = errors + +function makeDispatcher (fn) { + return (url, opts, handler) => { + if (typeof opts === 'function') { + handler = opts + opts = null + } + + if (!url || (typeof url !== 'string' && typeof url !== 'object' && !(url instanceof URL))) { + throw new InvalidArgumentError('invalid url') + } + + if (opts != null && typeof opts !== 'object') { + throw new InvalidArgumentError('invalid opts') + } + + if (opts && opts.path != null) { + if (typeof opts.path !== 'string') { + throw new InvalidArgumentError('invalid opts.path') + } + + let path = opts.path + if (!opts.path.startsWith('/')) { + path = `/${path}` + } + + url = new URL(util.parseOrigin(url).origin + path) + } else { + if (!opts) { + opts = typeof url === 'object' ? url : {} + } + + url = util.parseURL(url) + } + + const { agent, dispatcher = getGlobalDispatcher() } = opts + + if (agent) { + throw new InvalidArgumentError('unsupported opts.agent. Did you mean opts.client?') + } + + return fn.call(dispatcher, { + ...opts, + origin: url.origin, + path: url.search ? `${url.pathname}${url.search}` : url.pathname, + method: opts.method || (opts.body ? 'PUT' : 'GET') + }, handler) + } +} + +module.exports.setGlobalDispatcher = setGlobalDispatcher +module.exports.getGlobalDispatcher = getGlobalDispatcher + +if (util.nodeMajor > 16 || (util.nodeMajor === 16 && util.nodeMinor >= 8)) { + let fetchImpl = null + module.exports.fetch = async function fetch (resource) { + if (!fetchImpl) { + fetchImpl = (__nccwpck_require__(2315).fetch) + } + + try { + return await fetchImpl(...arguments) + } catch (err) { + if (typeof err === 'object') { + Error.captureStackTrace(err, this) + } + + throw err + } + } + module.exports.Headers = __nccwpck_require__(6349).Headers + module.exports.Response = __nccwpck_require__(8676).Response + module.exports.Request = __nccwpck_require__(5194).Request + module.exports.FormData = __nccwpck_require__(3073).FormData + module.exports.File = __nccwpck_require__(3041).File + module.exports.FileReader = __nccwpck_require__(2160).FileReader + + const { setGlobalOrigin, getGlobalOrigin } = __nccwpck_require__(5628) + + module.exports.setGlobalOrigin = setGlobalOrigin + module.exports.getGlobalOrigin = getGlobalOrigin + + const { CacheStorage } = __nccwpck_require__(4738) + const { kConstruct } = __nccwpck_require__(296) + + // Cache & CacheStorage are tightly coupled with fetch. Even if it may run + // in an older version of Node, it doesn't have any use without fetch. + module.exports.caches = new CacheStorage(kConstruct) +} + +if (util.nodeMajor >= 16) { + const { deleteCookie, getCookies, getSetCookies, setCookie } = __nccwpck_require__(3168) + + module.exports.deleteCookie = deleteCookie + module.exports.getCookies = getCookies + module.exports.getSetCookies = getSetCookies + module.exports.setCookie = setCookie + + const { parseMIMEType, serializeAMimeType } = __nccwpck_require__(4322) + + module.exports.parseMIMEType = parseMIMEType + module.exports.serializeAMimeType = serializeAMimeType +} + +if (util.nodeMajor >= 18 && hasCrypto) { + const { WebSocket } = __nccwpck_require__(5171) + + module.exports.WebSocket = WebSocket +} + +module.exports.request = makeDispatcher(api.request) +module.exports.stream = makeDispatcher(api.stream) +module.exports.pipeline = makeDispatcher(api.pipeline) +module.exports.connect = makeDispatcher(api.connect) +module.exports.upgrade = makeDispatcher(api.upgrade) + +module.exports.MockClient = MockClient +module.exports.MockPool = MockPool +module.exports.MockAgent = MockAgent +module.exports.mockErrors = mockErrors + + +/***/ }), + +/***/ 9965: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const { InvalidArgumentError } = __nccwpck_require__(8707) +const { kClients, kRunning, kClose, kDestroy, kDispatch, kInterceptors } = __nccwpck_require__(6443) +const DispatcherBase = __nccwpck_require__(1) +const Pool = __nccwpck_require__(5076) +const Client = __nccwpck_require__(6197) +const util = __nccwpck_require__(3440) +const createRedirectInterceptor = __nccwpck_require__(4415) +const { WeakRef, FinalizationRegistry } = __nccwpck_require__(3194)() + +const kOnConnect = Symbol('onConnect') +const kOnDisconnect = Symbol('onDisconnect') +const kOnConnectionError = Symbol('onConnectionError') +const kMaxRedirections = Symbol('maxRedirections') +const kOnDrain = Symbol('onDrain') +const kFactory = Symbol('factory') +const kFinalizer = Symbol('finalizer') +const kOptions = Symbol('options') + +function defaultFactory (origin, opts) { + return opts && opts.connections === 1 + ? new Client(origin, opts) + : new Pool(origin, opts) +} + +class Agent extends DispatcherBase { + constructor ({ factory = defaultFactory, maxRedirections = 0, connect, ...options } = {}) { + super() + + if (typeof factory !== 'function') { + throw new InvalidArgumentError('factory must be a function.') + } + + if (connect != null && typeof connect !== 'function' && typeof connect !== 'object') { + throw new InvalidArgumentError('connect must be a function or an object') + } + + if (!Number.isInteger(maxRedirections) || maxRedirections < 0) { + throw new InvalidArgumentError('maxRedirections must be a positive number') + } + + if (connect && typeof connect !== 'function') { + connect = { ...connect } + } + + this[kInterceptors] = options.interceptors && options.interceptors.Agent && Array.isArray(options.interceptors.Agent) + ? options.interceptors.Agent + : [createRedirectInterceptor({ maxRedirections })] + + this[kOptions] = { ...util.deepClone(options), connect } + this[kOptions].interceptors = options.interceptors + ? { ...options.interceptors } + : undefined + this[kMaxRedirections] = maxRedirections + this[kFactory] = factory + this[kClients] = new Map() + this[kFinalizer] = new FinalizationRegistry(/* istanbul ignore next: gc is undeterministic */ key => { + const ref = this[kClients].get(key) + if (ref !== undefined && ref.deref() === undefined) { + this[kClients].delete(key) + } + }) + + const agent = this + + this[kOnDrain] = (origin, targets) => { + agent.emit('drain', origin, [agent, ...targets]) + } + + this[kOnConnect] = (origin, targets) => { + agent.emit('connect', origin, [agent, ...targets]) + } + + this[kOnDisconnect] = (origin, targets, err) => { + agent.emit('disconnect', origin, [agent, ...targets], err) + } + + this[kOnConnectionError] = (origin, targets, err) => { + agent.emit('connectionError', origin, [agent, ...targets], err) + } + } + + get [kRunning] () { + let ret = 0 + for (const ref of this[kClients].values()) { + const client = ref.deref() + /* istanbul ignore next: gc is undeterministic */ + if (client) { + ret += client[kRunning] + } + } + return ret + } + + [kDispatch] (opts, handler) { + let key + if (opts.origin && (typeof opts.origin === 'string' || opts.origin instanceof URL)) { + key = String(opts.origin) + } else { + throw new InvalidArgumentError('opts.origin must be a non-empty string or URL.') + } + + const ref = this[kClients].get(key) + + let dispatcher = ref ? ref.deref() : null + if (!dispatcher) { + dispatcher = this[kFactory](opts.origin, this[kOptions]) + .on('drain', this[kOnDrain]) + .on('connect', this[kOnConnect]) + .on('disconnect', this[kOnDisconnect]) + .on('connectionError', this[kOnConnectionError]) + + this[kClients].set(key, new WeakRef(dispatcher)) + this[kFinalizer].register(dispatcher, key) + } + + return dispatcher.dispatch(opts, handler) + } + + async [kClose] () { + const closePromises = [] + for (const ref of this[kClients].values()) { + const client = ref.deref() + /* istanbul ignore else: gc is undeterministic */ + if (client) { + closePromises.push(client.close()) + } + } + + await Promise.all(closePromises) + } + + async [kDestroy] (err) { + const destroyPromises = [] + for (const ref of this[kClients].values()) { + const client = ref.deref() + /* istanbul ignore else: gc is undeterministic */ + if (client) { + destroyPromises.push(client.destroy(err)) + } + } + + await Promise.all(destroyPromises) + } +} + +module.exports = Agent + + +/***/ }), + +/***/ 158: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +const { addAbortListener } = __nccwpck_require__(3440) +const { RequestAbortedError } = __nccwpck_require__(8707) + +const kListener = Symbol('kListener') +const kSignal = Symbol('kSignal') + +function abort (self) { + if (self.abort) { + self.abort() + } else { + self.onError(new RequestAbortedError()) + } +} + +function addSignal (self, signal) { + self[kSignal] = null + self[kListener] = null + + if (!signal) { + return + } + + if (signal.aborted) { + abort(self) + return + } + + self[kSignal] = signal + self[kListener] = () => { + abort(self) + } + + addAbortListener(self[kSignal], self[kListener]) +} + +function removeSignal (self) { + if (!self[kSignal]) { + return + } + + if ('removeEventListener' in self[kSignal]) { + self[kSignal].removeEventListener('abort', self[kListener]) + } else { + self[kSignal].removeListener('abort', self[kListener]) + } + + self[kSignal] = null + self[kListener] = null +} + +module.exports = { + addSignal, + removeSignal +} + + +/***/ }), + +/***/ 4660: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const { AsyncResource } = __nccwpck_require__(290) +const { InvalidArgumentError, RequestAbortedError, SocketError } = __nccwpck_require__(8707) +const util = __nccwpck_require__(3440) +const { addSignal, removeSignal } = __nccwpck_require__(158) + +class ConnectHandler extends AsyncResource { + constructor (opts, callback) { + if (!opts || typeof opts !== 'object') { + throw new InvalidArgumentError('invalid opts') + } + + if (typeof callback !== 'function') { + throw new InvalidArgumentError('invalid callback') + } + + const { signal, opaque, responseHeaders } = opts + + if (signal && typeof signal.on !== 'function' && typeof signal.addEventListener !== 'function') { + throw new InvalidArgumentError('signal must be an EventEmitter or EventTarget') + } + + super('UNDICI_CONNECT') + + this.opaque = opaque || null + this.responseHeaders = responseHeaders || null + this.callback = callback + this.abort = null + + addSignal(this, signal) + } + + onConnect (abort, context) { + if (!this.callback) { + throw new RequestAbortedError() + } + + this.abort = abort + this.context = context + } + + onHeaders () { + throw new SocketError('bad connect', null) + } + + onUpgrade (statusCode, rawHeaders, socket) { + const { callback, opaque, context } = this + + removeSignal(this) + + this.callback = null + + let headers = rawHeaders + // Indicates is an HTTP2Session + if (headers != null) { + headers = this.responseHeaders === 'raw' ? util.parseRawHeaders(rawHeaders) : util.parseHeaders(rawHeaders) + } + + this.runInAsyncScope(callback, null, null, { + statusCode, + headers, + socket, + opaque, + context + }) + } + + onError (err) { + const { callback, opaque } = this + + removeSignal(this) + + if (callback) { + this.callback = null + queueMicrotask(() => { + this.runInAsyncScope(callback, null, err, { opaque }) + }) + } + } +} + +function connect (opts, callback) { + if (callback === undefined) { + return new Promise((resolve, reject) => { + connect.call(this, opts, (err, data) => { + return err ? reject(err) : resolve(data) + }) + }) + } + + try { + const connectHandler = new ConnectHandler(opts, callback) + this.dispatch({ ...opts, method: 'CONNECT' }, connectHandler) + } catch (err) { + if (typeof callback !== 'function') { + throw err + } + const opaque = opts && opts.opaque + queueMicrotask(() => callback(err, { opaque })) + } +} + +module.exports = connect + + +/***/ }), + +/***/ 6862: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const { + Readable, + Duplex, + PassThrough +} = __nccwpck_require__(2203) +const { + InvalidArgumentError, + InvalidReturnValueError, + RequestAbortedError +} = __nccwpck_require__(8707) +const util = __nccwpck_require__(3440) +const { AsyncResource } = __nccwpck_require__(290) +const { addSignal, removeSignal } = __nccwpck_require__(158) +const assert = __nccwpck_require__(2613) + +const kResume = Symbol('resume') + +class PipelineRequest extends Readable { + constructor () { + super({ autoDestroy: true }) + + this[kResume] = null + } + + _read () { + const { [kResume]: resume } = this + + if (resume) { + this[kResume] = null + resume() + } + } + + _destroy (err, callback) { + this._read() + + callback(err) + } +} + +class PipelineResponse extends Readable { + constructor (resume) { + super({ autoDestroy: true }) + this[kResume] = resume + } + + _read () { + this[kResume]() + } + + _destroy (err, callback) { + if (!err && !this._readableState.endEmitted) { + err = new RequestAbortedError() + } + + callback(err) + } +} + +class PipelineHandler extends AsyncResource { + constructor (opts, handler) { + if (!opts || typeof opts !== 'object') { + throw new InvalidArgumentError('invalid opts') + } + + if (typeof handler !== 'function') { + throw new InvalidArgumentError('invalid handler') + } + + const { signal, method, opaque, onInfo, responseHeaders } = opts + + if (signal && typeof signal.on !== 'function' && typeof signal.addEventListener !== 'function') { + throw new InvalidArgumentError('signal must be an EventEmitter or EventTarget') + } + + if (method === 'CONNECT') { + throw new InvalidArgumentError('invalid method') + } + + if (onInfo && typeof onInfo !== 'function') { + throw new InvalidArgumentError('invalid onInfo callback') + } + + super('UNDICI_PIPELINE') + + this.opaque = opaque || null + this.responseHeaders = responseHeaders || null + this.handler = handler + this.abort = null + this.context = null + this.onInfo = onInfo || null + + this.req = new PipelineRequest().on('error', util.nop) + + this.ret = new Duplex({ + readableObjectMode: opts.objectMode, + autoDestroy: true, + read: () => { + const { body } = this + + if (body && body.resume) { + body.resume() + } + }, + write: (chunk, encoding, callback) => { + const { req } = this + + if (req.push(chunk, encoding) || req._readableState.destroyed) { + callback() + } else { + req[kResume] = callback + } + }, + destroy: (err, callback) => { + const { body, req, res, ret, abort } = this + + if (!err && !ret._readableState.endEmitted) { + err = new RequestAbortedError() + } + + if (abort && err) { + abort() + } + + util.destroy(body, err) + util.destroy(req, err) + util.destroy(res, err) + + removeSignal(this) + + callback(err) + } + }).on('prefinish', () => { + const { req } = this + + // Node < 15 does not call _final in same tick. + req.push(null) + }) + + this.res = null + + addSignal(this, signal) + } + + onConnect (abort, context) { + const { ret, res } = this + + assert(!res, 'pipeline cannot be retried') + + if (ret.destroyed) { + throw new RequestAbortedError() + } + + this.abort = abort + this.context = context + } + + onHeaders (statusCode, rawHeaders, resume) { + const { opaque, handler, context } = this + + if (statusCode < 200) { + if (this.onInfo) { + const headers = this.responseHeaders === 'raw' ? util.parseRawHeaders(rawHeaders) : util.parseHeaders(rawHeaders) + this.onInfo({ statusCode, headers }) + } + return + } + + this.res = new PipelineResponse(resume) + + let body + try { + this.handler = null + const headers = this.responseHeaders === 'raw' ? util.parseRawHeaders(rawHeaders) : util.parseHeaders(rawHeaders) + body = this.runInAsyncScope(handler, null, { + statusCode, + headers, + opaque, + body: this.res, + context + }) + } catch (err) { + this.res.on('error', util.nop) + throw err + } + + if (!body || typeof body.on !== 'function') { + throw new InvalidReturnValueError('expected Readable') + } + + body + .on('data', (chunk) => { + const { ret, body } = this + + if (!ret.push(chunk) && body.pause) { + body.pause() + } + }) + .on('error', (err) => { + const { ret } = this + + util.destroy(ret, err) + }) + .on('end', () => { + const { ret } = this + + ret.push(null) + }) + .on('close', () => { + const { ret } = this + + if (!ret._readableState.ended) { + util.destroy(ret, new RequestAbortedError()) + } + }) + + this.body = body + } + + onData (chunk) { + const { res } = this + return res.push(chunk) + } + + onComplete (trailers) { + const { res } = this + res.push(null) + } + + onError (err) { + const { ret } = this + this.handler = null + util.destroy(ret, err) + } +} + +function pipeline (opts, handler) { + try { + const pipelineHandler = new PipelineHandler(opts, handler) + this.dispatch({ ...opts, body: pipelineHandler.req }, pipelineHandler) + return pipelineHandler.ret + } catch (err) { + return new PassThrough().destroy(err) + } +} + +module.exports = pipeline + + +/***/ }), + +/***/ 4043: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const Readable = __nccwpck_require__(9927) +const { + InvalidArgumentError, + RequestAbortedError +} = __nccwpck_require__(8707) +const util = __nccwpck_require__(3440) +const { getResolveErrorBodyCallback } = __nccwpck_require__(7655) +const { AsyncResource } = __nccwpck_require__(290) +const { addSignal, removeSignal } = __nccwpck_require__(158) + +class RequestHandler extends AsyncResource { + constructor (opts, callback) { + if (!opts || typeof opts !== 'object') { + throw new InvalidArgumentError('invalid opts') + } + + const { signal, method, opaque, body, onInfo, responseHeaders, throwOnError, highWaterMark } = opts + + try { + if (typeof callback !== 'function') { + throw new InvalidArgumentError('invalid callback') + } + + if (highWaterMark && (typeof highWaterMark !== 'number' || highWaterMark < 0)) { + throw new InvalidArgumentError('invalid highWaterMark') + } + + if (signal && typeof signal.on !== 'function' && typeof signal.addEventListener !== 'function') { + throw new InvalidArgumentError('signal must be an EventEmitter or EventTarget') + } + + if (method === 'CONNECT') { + throw new InvalidArgumentError('invalid method') + } + + if (onInfo && typeof onInfo !== 'function') { + throw new InvalidArgumentError('invalid onInfo callback') + } + + super('UNDICI_REQUEST') + } catch (err) { + if (util.isStream(body)) { + util.destroy(body.on('error', util.nop), err) + } + throw err + } + + this.responseHeaders = responseHeaders || null + this.opaque = opaque || null + this.callback = callback + this.res = null + this.abort = null + this.body = body + this.trailers = {} + this.context = null + this.onInfo = onInfo || null + this.throwOnError = throwOnError + this.highWaterMark = highWaterMark + + if (util.isStream(body)) { + body.on('error', (err) => { + this.onError(err) + }) + } + + addSignal(this, signal) + } + + onConnect (abort, context) { + if (!this.callback) { + throw new RequestAbortedError() + } + + this.abort = abort + this.context = context + } + + onHeaders (statusCode, rawHeaders, resume, statusMessage) { + const { callback, opaque, abort, context, responseHeaders, highWaterMark } = this + + const headers = responseHeaders === 'raw' ? util.parseRawHeaders(rawHeaders) : util.parseHeaders(rawHeaders) + + if (statusCode < 200) { + if (this.onInfo) { + this.onInfo({ statusCode, headers }) + } + return + } + + const parsedHeaders = responseHeaders === 'raw' ? util.parseHeaders(rawHeaders) : headers + const contentType = parsedHeaders['content-type'] + const body = new Readable({ resume, abort, contentType, highWaterMark }) + + this.callback = null + this.res = body + if (callback !== null) { + if (this.throwOnError && statusCode >= 400) { + this.runInAsyncScope(getResolveErrorBodyCallback, null, + { callback, body, contentType, statusCode, statusMessage, headers } + ) + } else { + this.runInAsyncScope(callback, null, null, { + statusCode, + headers, + trailers: this.trailers, + opaque, + body, + context + }) + } + } + } + + onData (chunk) { + const { res } = this + return res.push(chunk) + } + + onComplete (trailers) { + const { res } = this + + removeSignal(this) + + util.parseHeaders(trailers, this.trailers) + + res.push(null) + } + + onError (err) { + const { res, callback, body, opaque } = this + + removeSignal(this) + + if (callback) { + // TODO: Does this need queueMicrotask? + this.callback = null + queueMicrotask(() => { + this.runInAsyncScope(callback, null, err, { opaque }) + }) + } + + if (res) { + this.res = null + // Ensure all queued handlers are invoked before destroying res. + queueMicrotask(() => { + util.destroy(res, err) + }) + } + + if (body) { + this.body = null + util.destroy(body, err) + } + } +} + +function request (opts, callback) { + if (callback === undefined) { + return new Promise((resolve, reject) => { + request.call(this, opts, (err, data) => { + return err ? reject(err) : resolve(data) + }) + }) + } + + try { + this.dispatch(opts, new RequestHandler(opts, callback)) + } catch (err) { + if (typeof callback !== 'function') { + throw err + } + const opaque = opts && opts.opaque + queueMicrotask(() => callback(err, { opaque })) + } +} + +module.exports = request +module.exports.RequestHandler = RequestHandler + + +/***/ }), + +/***/ 3560: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const { finished, PassThrough } = __nccwpck_require__(2203) +const { + InvalidArgumentError, + InvalidReturnValueError, + RequestAbortedError +} = __nccwpck_require__(8707) +const util = __nccwpck_require__(3440) +const { getResolveErrorBodyCallback } = __nccwpck_require__(7655) +const { AsyncResource } = __nccwpck_require__(290) +const { addSignal, removeSignal } = __nccwpck_require__(158) + +class StreamHandler extends AsyncResource { + constructor (opts, factory, callback) { + if (!opts || typeof opts !== 'object') { + throw new InvalidArgumentError('invalid opts') + } + + const { signal, method, opaque, body, onInfo, responseHeaders, throwOnError } = opts + + try { + if (typeof callback !== 'function') { + throw new InvalidArgumentError('invalid callback') + } + + if (typeof factory !== 'function') { + throw new InvalidArgumentError('invalid factory') + } + + if (signal && typeof signal.on !== 'function' && typeof signal.addEventListener !== 'function') { + throw new InvalidArgumentError('signal must be an EventEmitter or EventTarget') + } + + if (method === 'CONNECT') { + throw new InvalidArgumentError('invalid method') + } + + if (onInfo && typeof onInfo !== 'function') { + throw new InvalidArgumentError('invalid onInfo callback') + } + + super('UNDICI_STREAM') + } catch (err) { + if (util.isStream(body)) { + util.destroy(body.on('error', util.nop), err) + } + throw err + } + + this.responseHeaders = responseHeaders || null + this.opaque = opaque || null + this.factory = factory + this.callback = callback + this.res = null + this.abort = null + this.context = null + this.trailers = null + this.body = body + this.onInfo = onInfo || null + this.throwOnError = throwOnError || false + + if (util.isStream(body)) { + body.on('error', (err) => { + this.onError(err) + }) + } + + addSignal(this, signal) + } + + onConnect (abort, context) { + if (!this.callback) { + throw new RequestAbortedError() + } + + this.abort = abort + this.context = context + } + + onHeaders (statusCode, rawHeaders, resume, statusMessage) { + const { factory, opaque, context, callback, responseHeaders } = this + + const headers = responseHeaders === 'raw' ? util.parseRawHeaders(rawHeaders) : util.parseHeaders(rawHeaders) + + if (statusCode < 200) { + if (this.onInfo) { + this.onInfo({ statusCode, headers }) + } + return + } + + this.factory = null + + let res + + if (this.throwOnError && statusCode >= 400) { + const parsedHeaders = responseHeaders === 'raw' ? util.parseHeaders(rawHeaders) : headers + const contentType = parsedHeaders['content-type'] + res = new PassThrough() + + this.callback = null + this.runInAsyncScope(getResolveErrorBodyCallback, null, + { callback, body: res, contentType, statusCode, statusMessage, headers } + ) + } else { + if (factory === null) { + return + } + + res = this.runInAsyncScope(factory, null, { + statusCode, + headers, + opaque, + context + }) + + if ( + !res || + typeof res.write !== 'function' || + typeof res.end !== 'function' || + typeof res.on !== 'function' + ) { + throw new InvalidReturnValueError('expected Writable') + } + + // TODO: Avoid finished. It registers an unnecessary amount of listeners. + finished(res, { readable: false }, (err) => { + const { callback, res, opaque, trailers, abort } = this + + this.res = null + if (err || !res.readable) { + util.destroy(res, err) + } + + this.callback = null + this.runInAsyncScope(callback, null, err || null, { opaque, trailers }) + + if (err) { + abort() + } + }) + } + + res.on('drain', resume) + + this.res = res + + const needDrain = res.writableNeedDrain !== undefined + ? res.writableNeedDrain + : res._writableState && res._writableState.needDrain + + return needDrain !== true + } + + onData (chunk) { + const { res } = this + + return res ? res.write(chunk) : true + } + + onComplete (trailers) { + const { res } = this + + removeSignal(this) + + if (!res) { + return + } + + this.trailers = util.parseHeaders(trailers) + + res.end() + } + + onError (err) { + const { res, callback, opaque, body } = this + + removeSignal(this) + + this.factory = null + + if (res) { + this.res = null + util.destroy(res, err) + } else if (callback) { + this.callback = null + queueMicrotask(() => { + this.runInAsyncScope(callback, null, err, { opaque }) + }) + } + + if (body) { + this.body = null + util.destroy(body, err) + } + } +} + +function stream (opts, factory, callback) { + if (callback === undefined) { + return new Promise((resolve, reject) => { + stream.call(this, opts, factory, (err, data) => { + return err ? reject(err) : resolve(data) + }) + }) + } + + try { + this.dispatch(opts, new StreamHandler(opts, factory, callback)) + } catch (err) { + if (typeof callback !== 'function') { + throw err + } + const opaque = opts && opts.opaque + queueMicrotask(() => callback(err, { opaque })) + } +} + +module.exports = stream + + +/***/ }), + +/***/ 1882: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const { InvalidArgumentError, RequestAbortedError, SocketError } = __nccwpck_require__(8707) +const { AsyncResource } = __nccwpck_require__(290) +const util = __nccwpck_require__(3440) +const { addSignal, removeSignal } = __nccwpck_require__(158) +const assert = __nccwpck_require__(2613) + +class UpgradeHandler extends AsyncResource { + constructor (opts, callback) { + if (!opts || typeof opts !== 'object') { + throw new InvalidArgumentError('invalid opts') + } + + if (typeof callback !== 'function') { + throw new InvalidArgumentError('invalid callback') + } + + const { signal, opaque, responseHeaders } = opts + + if (signal && typeof signal.on !== 'function' && typeof signal.addEventListener !== 'function') { + throw new InvalidArgumentError('signal must be an EventEmitter or EventTarget') + } + + super('UNDICI_UPGRADE') + + this.responseHeaders = responseHeaders || null + this.opaque = opaque || null + this.callback = callback + this.abort = null + this.context = null + + addSignal(this, signal) + } + + onConnect (abort, context) { + if (!this.callback) { + throw new RequestAbortedError() + } + + this.abort = abort + this.context = null + } + + onHeaders () { + throw new SocketError('bad upgrade', null) + } + + onUpgrade (statusCode, rawHeaders, socket) { + const { callback, opaque, context } = this + + assert.strictEqual(statusCode, 101) + + removeSignal(this) + + this.callback = null + const headers = this.responseHeaders === 'raw' ? util.parseRawHeaders(rawHeaders) : util.parseHeaders(rawHeaders) + this.runInAsyncScope(callback, null, null, { + headers, + socket, + opaque, + context + }) + } + + onError (err) { + const { callback, opaque } = this + + removeSignal(this) + + if (callback) { + this.callback = null + queueMicrotask(() => { + this.runInAsyncScope(callback, null, err, { opaque }) + }) + } + } +} + +function upgrade (opts, callback) { + if (callback === undefined) { + return new Promise((resolve, reject) => { + upgrade.call(this, opts, (err, data) => { + return err ? reject(err) : resolve(data) + }) + }) + } + + try { + const upgradeHandler = new UpgradeHandler(opts, callback) + this.dispatch({ + ...opts, + method: opts.method || 'GET', + upgrade: opts.protocol || 'Websocket' + }, upgradeHandler) + } catch (err) { + if (typeof callback !== 'function') { + throw err + } + const opaque = opts && opts.opaque + queueMicrotask(() => callback(err, { opaque })) + } +} + +module.exports = upgrade + + +/***/ }), + +/***/ 6615: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +module.exports.request = __nccwpck_require__(4043) +module.exports.stream = __nccwpck_require__(3560) +module.exports.pipeline = __nccwpck_require__(6862) +module.exports.upgrade = __nccwpck_require__(1882) +module.exports.connect = __nccwpck_require__(4660) + + +/***/ }), + +/***/ 9927: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; +// Ported from https://github.com/nodejs/undici/pull/907 + + + +const assert = __nccwpck_require__(2613) +const { Readable } = __nccwpck_require__(2203) +const { RequestAbortedError, NotSupportedError, InvalidArgumentError } = __nccwpck_require__(8707) +const util = __nccwpck_require__(3440) +const { ReadableStreamFrom, toUSVString } = __nccwpck_require__(3440) + +let Blob + +const kConsume = Symbol('kConsume') +const kReading = Symbol('kReading') +const kBody = Symbol('kBody') +const kAbort = Symbol('abort') +const kContentType = Symbol('kContentType') + +const noop = () => {} + +module.exports = class BodyReadable extends Readable { + constructor ({ + resume, + abort, + contentType = '', + highWaterMark = 64 * 1024 // Same as nodejs fs streams. + }) { + super({ + autoDestroy: true, + read: resume, + highWaterMark + }) + + this._readableState.dataEmitted = false + + this[kAbort] = abort + this[kConsume] = null + this[kBody] = null + this[kContentType] = contentType + + // Is stream being consumed through Readable API? + // This is an optimization so that we avoid checking + // for 'data' and 'readable' listeners in the hot path + // inside push(). + this[kReading] = false + } + + destroy (err) { + if (this.destroyed) { + // Node < 16 + return this + } + + if (!err && !this._readableState.endEmitted) { + err = new RequestAbortedError() + } + + if (err) { + this[kAbort]() + } + + return super.destroy(err) + } + + emit (ev, ...args) { + if (ev === 'data') { + // Node < 16.7 + this._readableState.dataEmitted = true + } else if (ev === 'error') { + // Node < 16 + this._readableState.errorEmitted = true + } + return super.emit(ev, ...args) + } + + on (ev, ...args) { + if (ev === 'data' || ev === 'readable') { + this[kReading] = true + } + return super.on(ev, ...args) + } + + addListener (ev, ...args) { + return this.on(ev, ...args) + } + + off (ev, ...args) { + const ret = super.off(ev, ...args) + if (ev === 'data' || ev === 'readable') { + this[kReading] = ( + this.listenerCount('data') > 0 || + this.listenerCount('readable') > 0 + ) + } + return ret + } + + removeListener (ev, ...args) { + return this.off(ev, ...args) + } + + push (chunk) { + if (this[kConsume] && chunk !== null && this.readableLength === 0) { + consumePush(this[kConsume], chunk) + return this[kReading] ? super.push(chunk) : true + } + return super.push(chunk) + } + + // https://fetch.spec.whatwg.org/#dom-body-text + async text () { + return consume(this, 'text') + } + + // https://fetch.spec.whatwg.org/#dom-body-json + async json () { + return consume(this, 'json') + } + + // https://fetch.spec.whatwg.org/#dom-body-blob + async blob () { + return consume(this, 'blob') + } + + // https://fetch.spec.whatwg.org/#dom-body-arraybuffer + async arrayBuffer () { + return consume(this, 'arrayBuffer') + } + + // https://fetch.spec.whatwg.org/#dom-body-formdata + async formData () { + // TODO: Implement. + throw new NotSupportedError() + } + + // https://fetch.spec.whatwg.org/#dom-body-bodyused + get bodyUsed () { + return util.isDisturbed(this) + } + + // https://fetch.spec.whatwg.org/#dom-body-body + get body () { + if (!this[kBody]) { + this[kBody] = ReadableStreamFrom(this) + if (this[kConsume]) { + // TODO: Is this the best way to force a lock? + this[kBody].getReader() // Ensure stream is locked. + assert(this[kBody].locked) + } + } + return this[kBody] + } + + dump (opts) { + let limit = opts && Number.isFinite(opts.limit) ? opts.limit : 262144 + const signal = opts && opts.signal + + if (signal) { + try { + if (typeof signal !== 'object' || !('aborted' in signal)) { + throw new InvalidArgumentError('signal must be an AbortSignal') + } + util.throwIfAborted(signal) + } catch (err) { + return Promise.reject(err) + } + } + + if (this.closed) { + return Promise.resolve(null) + } + + return new Promise((resolve, reject) => { + const signalListenerCleanup = signal + ? util.addAbortListener(signal, () => { + this.destroy() + }) + : noop + + this + .on('close', function () { + signalListenerCleanup() + if (signal && signal.aborted) { + reject(signal.reason || Object.assign(new Error('The operation was aborted'), { name: 'AbortError' })) + } else { + resolve(null) + } + }) + .on('error', noop) + .on('data', function (chunk) { + limit -= chunk.length + if (limit <= 0) { + this.destroy() + } + }) + .resume() + }) + } +} + +// https://streams.spec.whatwg.org/#readablestream-locked +function isLocked (self) { + // Consume is an implicit lock. + return (self[kBody] && self[kBody].locked === true) || self[kConsume] +} + +// https://fetch.spec.whatwg.org/#body-unusable +function isUnusable (self) { + return util.isDisturbed(self) || isLocked(self) +} + +async function consume (stream, type) { + if (isUnusable(stream)) { + throw new TypeError('unusable') + } + + assert(!stream[kConsume]) + + return new Promise((resolve, reject) => { + stream[kConsume] = { + type, + stream, + resolve, + reject, + length: 0, + body: [] + } + + stream + .on('error', function (err) { + consumeFinish(this[kConsume], err) + }) + .on('close', function () { + if (this[kConsume].body !== null) { + consumeFinish(this[kConsume], new RequestAbortedError()) + } + }) + + process.nextTick(consumeStart, stream[kConsume]) + }) +} + +function consumeStart (consume) { + if (consume.body === null) { + return + } + + const { _readableState: state } = consume.stream + + for (const chunk of state.buffer) { + consumePush(consume, chunk) + } + + if (state.endEmitted) { + consumeEnd(this[kConsume]) + } else { + consume.stream.on('end', function () { + consumeEnd(this[kConsume]) + }) + } + + consume.stream.resume() + + while (consume.stream.read() != null) { + // Loop + } +} + +function consumeEnd (consume) { + const { type, body, resolve, stream, length } = consume + + try { + if (type === 'text') { + resolve(toUSVString(Buffer.concat(body))) + } else if (type === 'json') { + resolve(JSON.parse(Buffer.concat(body))) + } else if (type === 'arrayBuffer') { + const dst = new Uint8Array(length) + + let pos = 0 + for (const buf of body) { + dst.set(buf, pos) + pos += buf.byteLength + } + + resolve(dst.buffer) + } else if (type === 'blob') { + if (!Blob) { + Blob = (__nccwpck_require__(181).Blob) + } + resolve(new Blob(body, { type: stream[kContentType] })) + } + + consumeFinish(consume) + } catch (err) { + stream.destroy(err) + } +} + +function consumePush (consume, chunk) { + consume.length += chunk.length + consume.body.push(chunk) +} + +function consumeFinish (consume, err) { + if (consume.body === null) { + return + } + + if (err) { + consume.reject(err) + } else { + consume.resolve() + } + + consume.type = null + consume.stream = null + consume.resolve = null + consume.reject = null + consume.length = 0 + consume.body = null +} + + +/***/ }), + +/***/ 7655: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +const assert = __nccwpck_require__(2613) +const { + ResponseStatusCodeError +} = __nccwpck_require__(8707) +const { toUSVString } = __nccwpck_require__(3440) + +async function getResolveErrorBodyCallback ({ callback, body, contentType, statusCode, statusMessage, headers }) { + assert(body) + + let chunks = [] + let limit = 0 + + for await (const chunk of body) { + chunks.push(chunk) + limit += chunk.length + if (limit > 128 * 1024) { + chunks = null + break + } + } + + if (statusCode === 204 || !contentType || !chunks) { + process.nextTick(callback, new ResponseStatusCodeError(`Response status code ${statusCode}${statusMessage ? `: ${statusMessage}` : ''}`, statusCode, headers)) + return + } + + try { + if (contentType.startsWith('application/json')) { + const payload = JSON.parse(toUSVString(Buffer.concat(chunks))) + process.nextTick(callback, new ResponseStatusCodeError(`Response status code ${statusCode}${statusMessage ? `: ${statusMessage}` : ''}`, statusCode, headers, payload)) + return + } + + if (contentType.startsWith('text/')) { + const payload = toUSVString(Buffer.concat(chunks)) + process.nextTick(callback, new ResponseStatusCodeError(`Response status code ${statusCode}${statusMessage ? `: ${statusMessage}` : ''}`, statusCode, headers, payload)) + return + } + } catch (err) { + // Process in a fallback if error + } + + process.nextTick(callback, new ResponseStatusCodeError(`Response status code ${statusCode}${statusMessage ? `: ${statusMessage}` : ''}`, statusCode, headers)) +} + +module.exports = { getResolveErrorBodyCallback } + + +/***/ }), + +/***/ 1093: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const { + BalancedPoolMissingUpstreamError, + InvalidArgumentError +} = __nccwpck_require__(8707) +const { + PoolBase, + kClients, + kNeedDrain, + kAddClient, + kRemoveClient, + kGetDispatcher +} = __nccwpck_require__(8640) +const Pool = __nccwpck_require__(5076) +const { kUrl, kInterceptors } = __nccwpck_require__(6443) +const { parseOrigin } = __nccwpck_require__(3440) +const kFactory = Symbol('factory') + +const kOptions = Symbol('options') +const kGreatestCommonDivisor = Symbol('kGreatestCommonDivisor') +const kCurrentWeight = Symbol('kCurrentWeight') +const kIndex = Symbol('kIndex') +const kWeight = Symbol('kWeight') +const kMaxWeightPerServer = Symbol('kMaxWeightPerServer') +const kErrorPenalty = Symbol('kErrorPenalty') + +function getGreatestCommonDivisor (a, b) { + if (b === 0) return a + return getGreatestCommonDivisor(b, a % b) +} + +function defaultFactory (origin, opts) { + return new Pool(origin, opts) +} + +class BalancedPool extends PoolBase { + constructor (upstreams = [], { factory = defaultFactory, ...opts } = {}) { + super() + + this[kOptions] = opts + this[kIndex] = -1 + this[kCurrentWeight] = 0 + + this[kMaxWeightPerServer] = this[kOptions].maxWeightPerServer || 100 + this[kErrorPenalty] = this[kOptions].errorPenalty || 15 + + if (!Array.isArray(upstreams)) { + upstreams = [upstreams] + } + + if (typeof factory !== 'function') { + throw new InvalidArgumentError('factory must be a function.') + } + + this[kInterceptors] = opts.interceptors && opts.interceptors.BalancedPool && Array.isArray(opts.interceptors.BalancedPool) + ? opts.interceptors.BalancedPool + : [] + this[kFactory] = factory + + for (const upstream of upstreams) { + this.addUpstream(upstream) + } + this._updateBalancedPoolStats() + } + + addUpstream (upstream) { + const upstreamOrigin = parseOrigin(upstream).origin + + if (this[kClients].find((pool) => ( + pool[kUrl].origin === upstreamOrigin && + pool.closed !== true && + pool.destroyed !== true + ))) { + return this + } + const pool = this[kFactory](upstreamOrigin, Object.assign({}, this[kOptions])) + + this[kAddClient](pool) + pool.on('connect', () => { + pool[kWeight] = Math.min(this[kMaxWeightPerServer], pool[kWeight] + this[kErrorPenalty]) + }) + + pool.on('connectionError', () => { + pool[kWeight] = Math.max(1, pool[kWeight] - this[kErrorPenalty]) + this._updateBalancedPoolStats() + }) + + pool.on('disconnect', (...args) => { + const err = args[2] + if (err && err.code === 'UND_ERR_SOCKET') { + // decrease the weight of the pool. + pool[kWeight] = Math.max(1, pool[kWeight] - this[kErrorPenalty]) + this._updateBalancedPoolStats() + } + }) + + for (const client of this[kClients]) { + client[kWeight] = this[kMaxWeightPerServer] + } + + this._updateBalancedPoolStats() + + return this + } + + _updateBalancedPoolStats () { + this[kGreatestCommonDivisor] = this[kClients].map(p => p[kWeight]).reduce(getGreatestCommonDivisor, 0) + } + + removeUpstream (upstream) { + const upstreamOrigin = parseOrigin(upstream).origin + + const pool = this[kClients].find((pool) => ( + pool[kUrl].origin === upstreamOrigin && + pool.closed !== true && + pool.destroyed !== true + )) + + if (pool) { + this[kRemoveClient](pool) + } + + return this + } + + get upstreams () { + return this[kClients] + .filter(dispatcher => dispatcher.closed !== true && dispatcher.destroyed !== true) + .map((p) => p[kUrl].origin) + } + + [kGetDispatcher] () { + // We validate that pools is greater than 0, + // otherwise we would have to wait until an upstream + // is added, which might never happen. + if (this[kClients].length === 0) { + throw new BalancedPoolMissingUpstreamError() + } + + const dispatcher = this[kClients].find(dispatcher => ( + !dispatcher[kNeedDrain] && + dispatcher.closed !== true && + dispatcher.destroyed !== true + )) + + if (!dispatcher) { + return + } + + const allClientsBusy = this[kClients].map(pool => pool[kNeedDrain]).reduce((a, b) => a && b, true) + + if (allClientsBusy) { + return + } + + let counter = 0 + + let maxWeightIndex = this[kClients].findIndex(pool => !pool[kNeedDrain]) + + while (counter++ < this[kClients].length) { + this[kIndex] = (this[kIndex] + 1) % this[kClients].length + const pool = this[kClients][this[kIndex]] + + // find pool index with the largest weight + if (pool[kWeight] > this[kClients][maxWeightIndex][kWeight] && !pool[kNeedDrain]) { + maxWeightIndex = this[kIndex] + } + + // decrease the current weight every `this[kClients].length`. + if (this[kIndex] === 0) { + // Set the current weight to the next lower weight. + this[kCurrentWeight] = this[kCurrentWeight] - this[kGreatestCommonDivisor] + + if (this[kCurrentWeight] <= 0) { + this[kCurrentWeight] = this[kMaxWeightPerServer] + } + } + if (pool[kWeight] >= this[kCurrentWeight] && (!pool[kNeedDrain])) { + return pool + } + } + + this[kCurrentWeight] = this[kClients][maxWeightIndex][kWeight] + this[kIndex] = maxWeightIndex + return this[kClients][maxWeightIndex] + } +} + +module.exports = BalancedPool + + +/***/ }), + +/***/ 479: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const { kConstruct } = __nccwpck_require__(296) +const { urlEquals, fieldValues: getFieldValues } = __nccwpck_require__(3993) +const { kEnumerableProperty, isDisturbed } = __nccwpck_require__(3440) +const { kHeadersList } = __nccwpck_require__(6443) +const { webidl } = __nccwpck_require__(4222) +const { Response, cloneResponse } = __nccwpck_require__(8676) +const { Request } = __nccwpck_require__(5194) +const { kState, kHeaders, kGuard, kRealm } = __nccwpck_require__(9710) +const { fetching } = __nccwpck_require__(2315) +const { urlIsHttpHttpsScheme, createDeferredPromise, readAllBytes } = __nccwpck_require__(5523) +const assert = __nccwpck_require__(2613) +const { getGlobalDispatcher } = __nccwpck_require__(2581) + +/** + * @see https://w3c.github.io/ServiceWorker/#dfn-cache-batch-operation + * @typedef {Object} CacheBatchOperation + * @property {'delete' | 'put'} type + * @property {any} request + * @property {any} response + * @property {import('../../types/cache').CacheQueryOptions} options + */ + +/** + * @see https://w3c.github.io/ServiceWorker/#dfn-request-response-list + * @typedef {[any, any][]} requestResponseList + */ + +class Cache { + /** + * @see https://w3c.github.io/ServiceWorker/#dfn-relevant-request-response-list + * @type {requestResponseList} + */ + #relevantRequestResponseList + + constructor () { + if (arguments[0] !== kConstruct) { + webidl.illegalConstructor() + } + + this.#relevantRequestResponseList = arguments[1] + } + + async match (request, options = {}) { + webidl.brandCheck(this, Cache) + webidl.argumentLengthCheck(arguments, 1, { header: 'Cache.match' }) + + request = webidl.converters.RequestInfo(request) + options = webidl.converters.CacheQueryOptions(options) + + const p = await this.matchAll(request, options) + + if (p.length === 0) { + return + } + + return p[0] + } + + async matchAll (request = undefined, options = {}) { + webidl.brandCheck(this, Cache) + + if (request !== undefined) request = webidl.converters.RequestInfo(request) + options = webidl.converters.CacheQueryOptions(options) + + // 1. + let r = null + + // 2. + if (request !== undefined) { + if (request instanceof Request) { + // 2.1.1 + r = request[kState] + + // 2.1.2 + if (r.method !== 'GET' && !options.ignoreMethod) { + return [] + } + } else if (typeof request === 'string') { + // 2.2.1 + r = new Request(request)[kState] + } + } + + // 5. + // 5.1 + const responses = [] + + // 5.2 + if (request === undefined) { + // 5.2.1 + for (const requestResponse of this.#relevantRequestResponseList) { + responses.push(requestResponse[1]) + } + } else { // 5.3 + // 5.3.1 + const requestResponses = this.#queryCache(r, options) + + // 5.3.2 + for (const requestResponse of requestResponses) { + responses.push(requestResponse[1]) + } + } + + // 5.4 + // We don't implement CORs so we don't need to loop over the responses, yay! + + // 5.5.1 + const responseList = [] + + // 5.5.2 + for (const response of responses) { + // 5.5.2.1 + const responseObject = new Response(response.body?.source ?? null) + const body = responseObject[kState].body + responseObject[kState] = response + responseObject[kState].body = body + responseObject[kHeaders][kHeadersList] = response.headersList + responseObject[kHeaders][kGuard] = 'immutable' + + responseList.push(responseObject) + } + + // 6. + return Object.freeze(responseList) + } + + async add (request) { + webidl.brandCheck(this, Cache) + webidl.argumentLengthCheck(arguments, 1, { header: 'Cache.add' }) + + request = webidl.converters.RequestInfo(request) + + // 1. + const requests = [request] + + // 2. + const responseArrayPromise = this.addAll(requests) + + // 3. + return await responseArrayPromise + } + + async addAll (requests) { + webidl.brandCheck(this, Cache) + webidl.argumentLengthCheck(arguments, 1, { header: 'Cache.addAll' }) + + requests = webidl.converters['sequence'](requests) + + // 1. + const responsePromises = [] + + // 2. + const requestList = [] + + // 3. + for (const request of requests) { + if (typeof request === 'string') { + continue + } + + // 3.1 + const r = request[kState] + + // 3.2 + if (!urlIsHttpHttpsScheme(r.url) || r.method !== 'GET') { + throw webidl.errors.exception({ + header: 'Cache.addAll', + message: 'Expected http/s scheme when method is not GET.' + }) + } + } + + // 4. + /** @type {ReturnType[]} */ + const fetchControllers = [] + + // 5. + for (const request of requests) { + // 5.1 + const r = new Request(request)[kState] + + // 5.2 + if (!urlIsHttpHttpsScheme(r.url)) { + throw webidl.errors.exception({ + header: 'Cache.addAll', + message: 'Expected http/s scheme.' + }) + } + + // 5.4 + r.initiator = 'fetch' + r.destination = 'subresource' + + // 5.5 + requestList.push(r) + + // 5.6 + const responsePromise = createDeferredPromise() + + // 5.7 + fetchControllers.push(fetching({ + request: r, + dispatcher: getGlobalDispatcher(), + processResponse (response) { + // 1. + if (response.type === 'error' || response.status === 206 || response.status < 200 || response.status > 299) { + responsePromise.reject(webidl.errors.exception({ + header: 'Cache.addAll', + message: 'Received an invalid status code or the request failed.' + })) + } else if (response.headersList.contains('vary')) { // 2. + // 2.1 + const fieldValues = getFieldValues(response.headersList.get('vary')) + + // 2.2 + for (const fieldValue of fieldValues) { + // 2.2.1 + if (fieldValue === '*') { + responsePromise.reject(webidl.errors.exception({ + header: 'Cache.addAll', + message: 'invalid vary field value' + })) + + for (const controller of fetchControllers) { + controller.abort() + } + + return + } + } + } + }, + processResponseEndOfBody (response) { + // 1. + if (response.aborted) { + responsePromise.reject(new DOMException('aborted', 'AbortError')) + return + } + + // 2. + responsePromise.resolve(response) + } + })) + + // 5.8 + responsePromises.push(responsePromise.promise) + } + + // 6. + const p = Promise.all(responsePromises) + + // 7. + const responses = await p + + // 7.1 + const operations = [] + + // 7.2 + let index = 0 + + // 7.3 + for (const response of responses) { + // 7.3.1 + /** @type {CacheBatchOperation} */ + const operation = { + type: 'put', // 7.3.2 + request: requestList[index], // 7.3.3 + response // 7.3.4 + } + + operations.push(operation) // 7.3.5 + + index++ // 7.3.6 + } + + // 7.5 + const cacheJobPromise = createDeferredPromise() + + // 7.6.1 + let errorData = null + + // 7.6.2 + try { + this.#batchCacheOperations(operations) + } catch (e) { + errorData = e + } + + // 7.6.3 + queueMicrotask(() => { + // 7.6.3.1 + if (errorData === null) { + cacheJobPromise.resolve(undefined) + } else { + // 7.6.3.2 + cacheJobPromise.reject(errorData) + } + }) + + // 7.7 + return cacheJobPromise.promise + } + + async put (request, response) { + webidl.brandCheck(this, Cache) + webidl.argumentLengthCheck(arguments, 2, { header: 'Cache.put' }) + + request = webidl.converters.RequestInfo(request) + response = webidl.converters.Response(response) + + // 1. + let innerRequest = null + + // 2. + if (request instanceof Request) { + innerRequest = request[kState] + } else { // 3. + innerRequest = new Request(request)[kState] + } + + // 4. + if (!urlIsHttpHttpsScheme(innerRequest.url) || innerRequest.method !== 'GET') { + throw webidl.errors.exception({ + header: 'Cache.put', + message: 'Expected an http/s scheme when method is not GET' + }) + } + + // 5. + const innerResponse = response[kState] + + // 6. + if (innerResponse.status === 206) { + throw webidl.errors.exception({ + header: 'Cache.put', + message: 'Got 206 status' + }) + } + + // 7. + if (innerResponse.headersList.contains('vary')) { + // 7.1. + const fieldValues = getFieldValues(innerResponse.headersList.get('vary')) + + // 7.2. + for (const fieldValue of fieldValues) { + // 7.2.1 + if (fieldValue === '*') { + throw webidl.errors.exception({ + header: 'Cache.put', + message: 'Got * vary field value' + }) + } + } + } + + // 8. + if (innerResponse.body && (isDisturbed(innerResponse.body.stream) || innerResponse.body.stream.locked)) { + throw webidl.errors.exception({ + header: 'Cache.put', + message: 'Response body is locked or disturbed' + }) + } + + // 9. + const clonedResponse = cloneResponse(innerResponse) + + // 10. + const bodyReadPromise = createDeferredPromise() + + // 11. + if (innerResponse.body != null) { + // 11.1 + const stream = innerResponse.body.stream + + // 11.2 + const reader = stream.getReader() + + // 11.3 + readAllBytes(reader).then(bodyReadPromise.resolve, bodyReadPromise.reject) + } else { + bodyReadPromise.resolve(undefined) + } + + // 12. + /** @type {CacheBatchOperation[]} */ + const operations = [] + + // 13. + /** @type {CacheBatchOperation} */ + const operation = { + type: 'put', // 14. + request: innerRequest, // 15. + response: clonedResponse // 16. + } + + // 17. + operations.push(operation) + + // 19. + const bytes = await bodyReadPromise.promise + + if (clonedResponse.body != null) { + clonedResponse.body.source = bytes + } + + // 19.1 + const cacheJobPromise = createDeferredPromise() + + // 19.2.1 + let errorData = null + + // 19.2.2 + try { + this.#batchCacheOperations(operations) + } catch (e) { + errorData = e + } + + // 19.2.3 + queueMicrotask(() => { + // 19.2.3.1 + if (errorData === null) { + cacheJobPromise.resolve() + } else { // 19.2.3.2 + cacheJobPromise.reject(errorData) + } + }) + + return cacheJobPromise.promise + } + + async delete (request, options = {}) { + webidl.brandCheck(this, Cache) + webidl.argumentLengthCheck(arguments, 1, { header: 'Cache.delete' }) + + request = webidl.converters.RequestInfo(request) + options = webidl.converters.CacheQueryOptions(options) + + /** + * @type {Request} + */ + let r = null + + if (request instanceof Request) { + r = request[kState] + + if (r.method !== 'GET' && !options.ignoreMethod) { + return false + } + } else { + assert(typeof request === 'string') + + r = new Request(request)[kState] + } + + /** @type {CacheBatchOperation[]} */ + const operations = [] + + /** @type {CacheBatchOperation} */ + const operation = { + type: 'delete', + request: r, + options + } + + operations.push(operation) + + const cacheJobPromise = createDeferredPromise() + + let errorData = null + let requestResponses + + try { + requestResponses = this.#batchCacheOperations(operations) + } catch (e) { + errorData = e + } + + queueMicrotask(() => { + if (errorData === null) { + cacheJobPromise.resolve(!!requestResponses?.length) + } else { + cacheJobPromise.reject(errorData) + } + }) + + return cacheJobPromise.promise + } + + /** + * @see https://w3c.github.io/ServiceWorker/#dom-cache-keys + * @param {any} request + * @param {import('../../types/cache').CacheQueryOptions} options + * @returns {readonly Request[]} + */ + async keys (request = undefined, options = {}) { + webidl.brandCheck(this, Cache) + + if (request !== undefined) request = webidl.converters.RequestInfo(request) + options = webidl.converters.CacheQueryOptions(options) + + // 1. + let r = null + + // 2. + if (request !== undefined) { + // 2.1 + if (request instanceof Request) { + // 2.1.1 + r = request[kState] + + // 2.1.2 + if (r.method !== 'GET' && !options.ignoreMethod) { + return [] + } + } else if (typeof request === 'string') { // 2.2 + r = new Request(request)[kState] + } + } + + // 4. + const promise = createDeferredPromise() + + // 5. + // 5.1 + const requests = [] + + // 5.2 + if (request === undefined) { + // 5.2.1 + for (const requestResponse of this.#relevantRequestResponseList) { + // 5.2.1.1 + requests.push(requestResponse[0]) + } + } else { // 5.3 + // 5.3.1 + const requestResponses = this.#queryCache(r, options) + + // 5.3.2 + for (const requestResponse of requestResponses) { + // 5.3.2.1 + requests.push(requestResponse[0]) + } + } + + // 5.4 + queueMicrotask(() => { + // 5.4.1 + const requestList = [] + + // 5.4.2 + for (const request of requests) { + const requestObject = new Request('https://a') + requestObject[kState] = request + requestObject[kHeaders][kHeadersList] = request.headersList + requestObject[kHeaders][kGuard] = 'immutable' + requestObject[kRealm] = request.client + + // 5.4.2.1 + requestList.push(requestObject) + } + + // 5.4.3 + promise.resolve(Object.freeze(requestList)) + }) + + return promise.promise + } + + /** + * @see https://w3c.github.io/ServiceWorker/#batch-cache-operations-algorithm + * @param {CacheBatchOperation[]} operations + * @returns {requestResponseList} + */ + #batchCacheOperations (operations) { + // 1. + const cache = this.#relevantRequestResponseList + + // 2. + const backupCache = [...cache] + + // 3. + const addedItems = [] + + // 4.1 + const resultList = [] + + try { + // 4.2 + for (const operation of operations) { + // 4.2.1 + if (operation.type !== 'delete' && operation.type !== 'put') { + throw webidl.errors.exception({ + header: 'Cache.#batchCacheOperations', + message: 'operation type does not match "delete" or "put"' + }) + } + + // 4.2.2 + if (operation.type === 'delete' && operation.response != null) { + throw webidl.errors.exception({ + header: 'Cache.#batchCacheOperations', + message: 'delete operation should not have an associated response' + }) + } + + // 4.2.3 + if (this.#queryCache(operation.request, operation.options, addedItems).length) { + throw new DOMException('???', 'InvalidStateError') + } + + // 4.2.4 + let requestResponses + + // 4.2.5 + if (operation.type === 'delete') { + // 4.2.5.1 + requestResponses = this.#queryCache(operation.request, operation.options) + + // TODO: the spec is wrong, this is needed to pass WPTs + if (requestResponses.length === 0) { + return [] + } + + // 4.2.5.2 + for (const requestResponse of requestResponses) { + const idx = cache.indexOf(requestResponse) + assert(idx !== -1) + + // 4.2.5.2.1 + cache.splice(idx, 1) + } + } else if (operation.type === 'put') { // 4.2.6 + // 4.2.6.1 + if (operation.response == null) { + throw webidl.errors.exception({ + header: 'Cache.#batchCacheOperations', + message: 'put operation should have an associated response' + }) + } + + // 4.2.6.2 + const r = operation.request + + // 4.2.6.3 + if (!urlIsHttpHttpsScheme(r.url)) { + throw webidl.errors.exception({ + header: 'Cache.#batchCacheOperations', + message: 'expected http or https scheme' + }) + } + + // 4.2.6.4 + if (r.method !== 'GET') { + throw webidl.errors.exception({ + header: 'Cache.#batchCacheOperations', + message: 'not get method' + }) + } + + // 4.2.6.5 + if (operation.options != null) { + throw webidl.errors.exception({ + header: 'Cache.#batchCacheOperations', + message: 'options must not be defined' + }) + } + + // 4.2.6.6 + requestResponses = this.#queryCache(operation.request) + + // 4.2.6.7 + for (const requestResponse of requestResponses) { + const idx = cache.indexOf(requestResponse) + assert(idx !== -1) + + // 4.2.6.7.1 + cache.splice(idx, 1) + } + + // 4.2.6.8 + cache.push([operation.request, operation.response]) + + // 4.2.6.10 + addedItems.push([operation.request, operation.response]) + } + + // 4.2.7 + resultList.push([operation.request, operation.response]) + } + + // 4.3 + return resultList + } catch (e) { // 5. + // 5.1 + this.#relevantRequestResponseList.length = 0 + + // 5.2 + this.#relevantRequestResponseList = backupCache + + // 5.3 + throw e + } + } + + /** + * @see https://w3c.github.io/ServiceWorker/#query-cache + * @param {any} requestQuery + * @param {import('../../types/cache').CacheQueryOptions} options + * @param {requestResponseList} targetStorage + * @returns {requestResponseList} + */ + #queryCache (requestQuery, options, targetStorage) { + /** @type {requestResponseList} */ + const resultList = [] + + const storage = targetStorage ?? this.#relevantRequestResponseList + + for (const requestResponse of storage) { + const [cachedRequest, cachedResponse] = requestResponse + if (this.#requestMatchesCachedItem(requestQuery, cachedRequest, cachedResponse, options)) { + resultList.push(requestResponse) + } + } + + return resultList + } + + /** + * @see https://w3c.github.io/ServiceWorker/#request-matches-cached-item-algorithm + * @param {any} requestQuery + * @param {any} request + * @param {any | null} response + * @param {import('../../types/cache').CacheQueryOptions | undefined} options + * @returns {boolean} + */ + #requestMatchesCachedItem (requestQuery, request, response = null, options) { + // if (options?.ignoreMethod === false && request.method === 'GET') { + // return false + // } + + const queryURL = new URL(requestQuery.url) + + const cachedURL = new URL(request.url) + + if (options?.ignoreSearch) { + cachedURL.search = '' + + queryURL.search = '' + } + + if (!urlEquals(queryURL, cachedURL, true)) { + return false + } + + if ( + response == null || + options?.ignoreVary || + !response.headersList.contains('vary') + ) { + return true + } + + const fieldValues = getFieldValues(response.headersList.get('vary')) + + for (const fieldValue of fieldValues) { + if (fieldValue === '*') { + return false + } + + const requestValue = request.headersList.get(fieldValue) + const queryValue = requestQuery.headersList.get(fieldValue) + + // If one has the header and the other doesn't, or one has + // a different value than the other, return false + if (requestValue !== queryValue) { + return false + } + } + + return true + } +} + +Object.defineProperties(Cache.prototype, { + [Symbol.toStringTag]: { + value: 'Cache', + configurable: true + }, + match: kEnumerableProperty, + matchAll: kEnumerableProperty, + add: kEnumerableProperty, + addAll: kEnumerableProperty, + put: kEnumerableProperty, + delete: kEnumerableProperty, + keys: kEnumerableProperty +}) + +const cacheQueryOptionConverters = [ + { + key: 'ignoreSearch', + converter: webidl.converters.boolean, + defaultValue: false + }, + { + key: 'ignoreMethod', + converter: webidl.converters.boolean, + defaultValue: false + }, + { + key: 'ignoreVary', + converter: webidl.converters.boolean, + defaultValue: false + } +] + +webidl.converters.CacheQueryOptions = webidl.dictionaryConverter(cacheQueryOptionConverters) + +webidl.converters.MultiCacheQueryOptions = webidl.dictionaryConverter([ + ...cacheQueryOptionConverters, + { + key: 'cacheName', + converter: webidl.converters.DOMString + } +]) + +webidl.converters.Response = webidl.interfaceConverter(Response) + +webidl.converters['sequence'] = webidl.sequenceConverter( + webidl.converters.RequestInfo +) + +module.exports = { + Cache +} + + +/***/ }), + +/***/ 4738: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const { kConstruct } = __nccwpck_require__(296) +const { Cache } = __nccwpck_require__(479) +const { webidl } = __nccwpck_require__(4222) +const { kEnumerableProperty } = __nccwpck_require__(3440) + +class CacheStorage { + /** + * @see https://w3c.github.io/ServiceWorker/#dfn-relevant-name-to-cache-map + * @type {Map} + */ + async has (cacheName) { + webidl.brandCheck(this, CacheStorage) + webidl.argumentLengthCheck(arguments, 1, { header: 'CacheStorage.has' }) + + cacheName = webidl.converters.DOMString(cacheName) + + // 2.1.1 + // 2.2 + return this.#caches.has(cacheName) + } + + /** + * @see https://w3c.github.io/ServiceWorker/#dom-cachestorage-open + * @param {string} cacheName + * @returns {Promise} + */ + async open (cacheName) { + webidl.brandCheck(this, CacheStorage) + webidl.argumentLengthCheck(arguments, 1, { header: 'CacheStorage.open' }) + + cacheName = webidl.converters.DOMString(cacheName) + + // 2.1 + if (this.#caches.has(cacheName)) { + // await caches.open('v1') !== await caches.open('v1') + + // 2.1.1 + const cache = this.#caches.get(cacheName) + + // 2.1.1.1 + return new Cache(kConstruct, cache) + } + + // 2.2 + const cache = [] + + // 2.3 + this.#caches.set(cacheName, cache) + + // 2.4 + return new Cache(kConstruct, cache) + } + + /** + * @see https://w3c.github.io/ServiceWorker/#cache-storage-delete + * @param {string} cacheName + * @returns {Promise} + */ + async delete (cacheName) { + webidl.brandCheck(this, CacheStorage) + webidl.argumentLengthCheck(arguments, 1, { header: 'CacheStorage.delete' }) + + cacheName = webidl.converters.DOMString(cacheName) + + return this.#caches.delete(cacheName) + } + + /** + * @see https://w3c.github.io/ServiceWorker/#cache-storage-keys + * @returns {string[]} + */ + async keys () { + webidl.brandCheck(this, CacheStorage) + + // 2.1 + const keys = this.#caches.keys() + + // 2.2 + return [...keys] + } +} + +Object.defineProperties(CacheStorage.prototype, { + [Symbol.toStringTag]: { + value: 'CacheStorage', + configurable: true + }, + match: kEnumerableProperty, + has: kEnumerableProperty, + open: kEnumerableProperty, + delete: kEnumerableProperty, + keys: kEnumerableProperty +}) + +module.exports = { + CacheStorage +} + + +/***/ }), + +/***/ 296: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +module.exports = { + kConstruct: (__nccwpck_require__(6443).kConstruct) +} + + +/***/ }), + +/***/ 3993: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const assert = __nccwpck_require__(2613) +const { URLSerializer } = __nccwpck_require__(4322) +const { isValidHeaderName } = __nccwpck_require__(5523) + +/** + * @see https://url.spec.whatwg.org/#concept-url-equals + * @param {URL} A + * @param {URL} B + * @param {boolean | undefined} excludeFragment + * @returns {boolean} + */ +function urlEquals (A, B, excludeFragment = false) { + const serializedA = URLSerializer(A, excludeFragment) + + const serializedB = URLSerializer(B, excludeFragment) + + return serializedA === serializedB +} + +/** + * @see https://github.com/chromium/chromium/blob/694d20d134cb553d8d89e5500b9148012b1ba299/content/browser/cache_storage/cache_storage_cache.cc#L260-L262 + * @param {string} header + */ +function fieldValues (header) { + assert(header !== null) + + const values = [] + + for (let value of header.split(',')) { + value = value.trim() + + if (!value.length) { + continue + } else if (!isValidHeaderName(value)) { + continue + } + + values.push(value) + } + + return values +} + +module.exports = { + urlEquals, + fieldValues +} + + +/***/ }), + +/***/ 6197: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; +// @ts-check + + + +/* global WebAssembly */ + +const assert = __nccwpck_require__(2613) +const net = __nccwpck_require__(9278) +const http = __nccwpck_require__(8611) +const { pipeline } = __nccwpck_require__(2203) +const util = __nccwpck_require__(3440) +const timers = __nccwpck_require__(8804) +const Request = __nccwpck_require__(4655) +const DispatcherBase = __nccwpck_require__(1) +const { + RequestContentLengthMismatchError, + ResponseContentLengthMismatchError, + InvalidArgumentError, + RequestAbortedError, + HeadersTimeoutError, + HeadersOverflowError, + SocketError, + InformationalError, + BodyTimeoutError, + HTTPParserError, + ResponseExceededMaxSizeError, + ClientDestroyedError +} = __nccwpck_require__(8707) +const buildConnector = __nccwpck_require__(9136) +const { + kUrl, + kReset, + kServerName, + kClient, + kBusy, + kParser, + kConnect, + kBlocking, + kResuming, + kRunning, + kPending, + kSize, + kWriting, + kQueue, + kConnected, + kConnecting, + kNeedDrain, + kNoRef, + kKeepAliveDefaultTimeout, + kHostHeader, + kPendingIdx, + kRunningIdx, + kError, + kPipelining, + kSocket, + kKeepAliveTimeoutValue, + kMaxHeadersSize, + kKeepAliveMaxTimeout, + kKeepAliveTimeoutThreshold, + kHeadersTimeout, + kBodyTimeout, + kStrictContentLength, + kConnector, + kMaxRedirections, + kMaxRequests, + kCounter, + kClose, + kDestroy, + kDispatch, + kInterceptors, + kLocalAddress, + kMaxResponseSize, + kHTTPConnVersion, + // HTTP2 + kHost, + kHTTP2Session, + kHTTP2SessionState, + kHTTP2BuildRequest, + kHTTP2CopyHeaders, + kHTTP1BuildRequest +} = __nccwpck_require__(6443) + +/** @type {import('http2')} */ +let http2 +try { + http2 = __nccwpck_require__(5675) +} catch { + // @ts-ignore + http2 = { constants: {} } +} + +const { + constants: { + HTTP2_HEADER_AUTHORITY, + HTTP2_HEADER_METHOD, + HTTP2_HEADER_PATH, + HTTP2_HEADER_SCHEME, + HTTP2_HEADER_CONTENT_LENGTH, + HTTP2_HEADER_EXPECT, + HTTP2_HEADER_STATUS + } +} = http2 + +// Experimental +let h2ExperimentalWarned = false + +const FastBuffer = Buffer[Symbol.species] + +const kClosedResolve = Symbol('kClosedResolve') + +const channels = {} + +try { + const diagnosticsChannel = __nccwpck_require__(1637) + channels.sendHeaders = diagnosticsChannel.channel('undici:client:sendHeaders') + channels.beforeConnect = diagnosticsChannel.channel('undici:client:beforeConnect') + channels.connectError = diagnosticsChannel.channel('undici:client:connectError') + channels.connected = diagnosticsChannel.channel('undici:client:connected') +} catch { + channels.sendHeaders = { hasSubscribers: false } + channels.beforeConnect = { hasSubscribers: false } + channels.connectError = { hasSubscribers: false } + channels.connected = { hasSubscribers: false } +} + +/** + * @type {import('../types/client').default} + */ +class Client extends DispatcherBase { + /** + * + * @param {string|URL} url + * @param {import('../types/client').Client.Options} options + */ + constructor (url, { + interceptors, + maxHeaderSize, + headersTimeout, + socketTimeout, + requestTimeout, + connectTimeout, + bodyTimeout, + idleTimeout, + keepAlive, + keepAliveTimeout, + maxKeepAliveTimeout, + keepAliveMaxTimeout, + keepAliveTimeoutThreshold, + socketPath, + pipelining, + tls, + strictContentLength, + maxCachedSessions, + maxRedirections, + connect, + maxRequestsPerClient, + localAddress, + maxResponseSize, + autoSelectFamily, + autoSelectFamilyAttemptTimeout, + // h2 + allowH2, + maxConcurrentStreams + } = {}) { + super() + + if (keepAlive !== undefined) { + throw new InvalidArgumentError('unsupported keepAlive, use pipelining=0 instead') + } + + if (socketTimeout !== undefined) { + throw new InvalidArgumentError('unsupported socketTimeout, use headersTimeout & bodyTimeout instead') + } + + if (requestTimeout !== undefined) { + throw new InvalidArgumentError('unsupported requestTimeout, use headersTimeout & bodyTimeout instead') + } + + if (idleTimeout !== undefined) { + throw new InvalidArgumentError('unsupported idleTimeout, use keepAliveTimeout instead') + } + + if (maxKeepAliveTimeout !== undefined) { + throw new InvalidArgumentError('unsupported maxKeepAliveTimeout, use keepAliveMaxTimeout instead') + } + + if (maxHeaderSize != null && !Number.isFinite(maxHeaderSize)) { + throw new InvalidArgumentError('invalid maxHeaderSize') + } + + if (socketPath != null && typeof socketPath !== 'string') { + throw new InvalidArgumentError('invalid socketPath') + } + + if (connectTimeout != null && (!Number.isFinite(connectTimeout) || connectTimeout < 0)) { + throw new InvalidArgumentError('invalid connectTimeout') + } + + if (keepAliveTimeout != null && (!Number.isFinite(keepAliveTimeout) || keepAliveTimeout <= 0)) { + throw new InvalidArgumentError('invalid keepAliveTimeout') + } + + if (keepAliveMaxTimeout != null && (!Number.isFinite(keepAliveMaxTimeout) || keepAliveMaxTimeout <= 0)) { + throw new InvalidArgumentError('invalid keepAliveMaxTimeout') + } + + if (keepAliveTimeoutThreshold != null && !Number.isFinite(keepAliveTimeoutThreshold)) { + throw new InvalidArgumentError('invalid keepAliveTimeoutThreshold') + } + + if (headersTimeout != null && (!Number.isInteger(headersTimeout) || headersTimeout < 0)) { + throw new InvalidArgumentError('headersTimeout must be a positive integer or zero') + } + + if (bodyTimeout != null && (!Number.isInteger(bodyTimeout) || bodyTimeout < 0)) { + throw new InvalidArgumentError('bodyTimeout must be a positive integer or zero') + } + + if (connect != null && typeof connect !== 'function' && typeof connect !== 'object') { + throw new InvalidArgumentError('connect must be a function or an object') + } + + if (maxRedirections != null && (!Number.isInteger(maxRedirections) || maxRedirections < 0)) { + throw new InvalidArgumentError('maxRedirections must be a positive number') + } + + if (maxRequestsPerClient != null && (!Number.isInteger(maxRequestsPerClient) || maxRequestsPerClient < 0)) { + throw new InvalidArgumentError('maxRequestsPerClient must be a positive number') + } + + if (localAddress != null && (typeof localAddress !== 'string' || net.isIP(localAddress) === 0)) { + throw new InvalidArgumentError('localAddress must be valid string IP address') + } + + if (maxResponseSize != null && (!Number.isInteger(maxResponseSize) || maxResponseSize < -1)) { + throw new InvalidArgumentError('maxResponseSize must be a positive number') + } + + if ( + autoSelectFamilyAttemptTimeout != null && + (!Number.isInteger(autoSelectFamilyAttemptTimeout) || autoSelectFamilyAttemptTimeout < -1) + ) { + throw new InvalidArgumentError('autoSelectFamilyAttemptTimeout must be a positive number') + } + + // h2 + if (allowH2 != null && typeof allowH2 !== 'boolean') { + throw new InvalidArgumentError('allowH2 must be a valid boolean value') + } + + if (maxConcurrentStreams != null && (typeof maxConcurrentStreams !== 'number' || maxConcurrentStreams < 1)) { + throw new InvalidArgumentError('maxConcurrentStreams must be a possitive integer, greater than 0') + } + + if (typeof connect !== 'function') { + connect = buildConnector({ + ...tls, + maxCachedSessions, + allowH2, + socketPath, + timeout: connectTimeout, + ...(util.nodeHasAutoSelectFamily && autoSelectFamily ? { autoSelectFamily, autoSelectFamilyAttemptTimeout } : undefined), + ...connect + }) + } + + this[kInterceptors] = interceptors && interceptors.Client && Array.isArray(interceptors.Client) + ? interceptors.Client + : [createRedirectInterceptor({ maxRedirections })] + this[kUrl] = util.parseOrigin(url) + this[kConnector] = connect + this[kSocket] = null + this[kPipelining] = pipelining != null ? pipelining : 1 + this[kMaxHeadersSize] = maxHeaderSize || http.maxHeaderSize + this[kKeepAliveDefaultTimeout] = keepAliveTimeout == null ? 4e3 : keepAliveTimeout + this[kKeepAliveMaxTimeout] = keepAliveMaxTimeout == null ? 600e3 : keepAliveMaxTimeout + this[kKeepAliveTimeoutThreshold] = keepAliveTimeoutThreshold == null ? 1e3 : keepAliveTimeoutThreshold + this[kKeepAliveTimeoutValue] = this[kKeepAliveDefaultTimeout] + this[kServerName] = null + this[kLocalAddress] = localAddress != null ? localAddress : null + this[kResuming] = 0 // 0, idle, 1, scheduled, 2 resuming + this[kNeedDrain] = 0 // 0, idle, 1, scheduled, 2 resuming + this[kHostHeader] = `host: ${this[kUrl].hostname}${this[kUrl].port ? `:${this[kUrl].port}` : ''}\r\n` + this[kBodyTimeout] = bodyTimeout != null ? bodyTimeout : 300e3 + this[kHeadersTimeout] = headersTimeout != null ? headersTimeout : 300e3 + this[kStrictContentLength] = strictContentLength == null ? true : strictContentLength + this[kMaxRedirections] = maxRedirections + this[kMaxRequests] = maxRequestsPerClient + this[kClosedResolve] = null + this[kMaxResponseSize] = maxResponseSize > -1 ? maxResponseSize : -1 + this[kHTTPConnVersion] = 'h1' + + // HTTP/2 + this[kHTTP2Session] = null + this[kHTTP2SessionState] = !allowH2 + ? null + : { + // streams: null, // Fixed queue of streams - For future support of `push` + openStreams: 0, // Keep track of them to decide wether or not unref the session + maxConcurrentStreams: maxConcurrentStreams != null ? maxConcurrentStreams : 100 // Max peerConcurrentStreams for a Node h2 server + } + this[kHost] = `${this[kUrl].hostname}${this[kUrl].port ? `:${this[kUrl].port}` : ''}` + + // kQueue is built up of 3 sections separated by + // the kRunningIdx and kPendingIdx indices. + // | complete | running | pending | + // ^ kRunningIdx ^ kPendingIdx ^ kQueue.length + // kRunningIdx points to the first running element. + // kPendingIdx points to the first pending element. + // This implements a fast queue with an amortized + // time of O(1). + + this[kQueue] = [] + this[kRunningIdx] = 0 + this[kPendingIdx] = 0 + } + + get pipelining () { + return this[kPipelining] + } + + set pipelining (value) { + this[kPipelining] = value + resume(this, true) + } + + get [kPending] () { + return this[kQueue].length - this[kPendingIdx] + } + + get [kRunning] () { + return this[kPendingIdx] - this[kRunningIdx] + } + + get [kSize] () { + return this[kQueue].length - this[kRunningIdx] + } + + get [kConnected] () { + return !!this[kSocket] && !this[kConnecting] && !this[kSocket].destroyed + } + + get [kBusy] () { + const socket = this[kSocket] + return ( + (socket && (socket[kReset] || socket[kWriting] || socket[kBlocking])) || + (this[kSize] >= (this[kPipelining] || 1)) || + this[kPending] > 0 + ) + } + + /* istanbul ignore: only used for test */ + [kConnect] (cb) { + connect(this) + this.once('connect', cb) + } + + [kDispatch] (opts, handler) { + const origin = opts.origin || this[kUrl].origin + + const request = this[kHTTPConnVersion] === 'h2' + ? Request[kHTTP2BuildRequest](origin, opts, handler) + : Request[kHTTP1BuildRequest](origin, opts, handler) + + this[kQueue].push(request) + if (this[kResuming]) { + // Do nothing. + } else if (util.bodyLength(request.body) == null && util.isIterable(request.body)) { + // Wait a tick in case stream/iterator is ended in the same tick. + this[kResuming] = 1 + process.nextTick(resume, this) + } else { + resume(this, true) + } + + if (this[kResuming] && this[kNeedDrain] !== 2 && this[kBusy]) { + this[kNeedDrain] = 2 + } + + return this[kNeedDrain] < 2 + } + + async [kClose] () { + // TODO: for H2 we need to gracefully flush the remaining enqueued + // request and close each stream. + return new Promise((resolve) => { + if (!this[kSize]) { + resolve(null) + } else { + this[kClosedResolve] = resolve + } + }) + } + + async [kDestroy] (err) { + return new Promise((resolve) => { + const requests = this[kQueue].splice(this[kPendingIdx]) + for (let i = 0; i < requests.length; i++) { + const request = requests[i] + errorRequest(this, request, err) + } + + const callback = () => { + if (this[kClosedResolve]) { + // TODO (fix): Should we error here with ClientDestroyedError? + this[kClosedResolve]() + this[kClosedResolve] = null + } + resolve() + } + + if (this[kHTTP2Session] != null) { + util.destroy(this[kHTTP2Session], err) + this[kHTTP2Session] = null + this[kHTTP2SessionState] = null + } + + if (!this[kSocket]) { + queueMicrotask(callback) + } else { + util.destroy(this[kSocket].on('close', callback), err) + } + + resume(this) + }) + } +} + +function onHttp2SessionError (err) { + assert(err.code !== 'ERR_TLS_CERT_ALTNAME_INVALID') + + this[kSocket][kError] = err + + onError(this[kClient], err) +} + +function onHttp2FrameError (type, code, id) { + const err = new InformationalError(`HTTP/2: "frameError" received - type ${type}, code ${code}`) + + if (id === 0) { + this[kSocket][kError] = err + onError(this[kClient], err) + } +} + +function onHttp2SessionEnd () { + util.destroy(this, new SocketError('other side closed')) + util.destroy(this[kSocket], new SocketError('other side closed')) +} + +function onHTTP2GoAway (code) { + const client = this[kClient] + const err = new InformationalError(`HTTP/2: "GOAWAY" frame received with code ${code}`) + client[kSocket] = null + client[kHTTP2Session] = null + + if (client.destroyed) { + assert(this[kPending] === 0) + + // Fail entire queue. + const requests = client[kQueue].splice(client[kRunningIdx]) + for (let i = 0; i < requests.length; i++) { + const request = requests[i] + errorRequest(this, request, err) + } + } else if (client[kRunning] > 0) { + // Fail head of pipeline. + const request = client[kQueue][client[kRunningIdx]] + client[kQueue][client[kRunningIdx]++] = null + + errorRequest(client, request, err) + } + + client[kPendingIdx] = client[kRunningIdx] + + assert(client[kRunning] === 0) + + client.emit('disconnect', + client[kUrl], + [client], + err + ) + + resume(client) +} + +const constants = __nccwpck_require__(2824) +const createRedirectInterceptor = __nccwpck_require__(4415) +const EMPTY_BUF = Buffer.alloc(0) + +async function lazyllhttp () { + const llhttpWasmData = process.env.JEST_WORKER_ID ? __nccwpck_require__(3870) : undefined + + let mod + try { + mod = await WebAssembly.compile(Buffer.from(__nccwpck_require__(3434), 'base64')) + } catch (e) { + /* istanbul ignore next */ + + // We could check if the error was caused by the simd option not + // being enabled, but the occurring of this other error + // * https://github.com/emscripten-core/emscripten/issues/11495 + // got me to remove that check to avoid breaking Node 12. + mod = await WebAssembly.compile(Buffer.from(llhttpWasmData || __nccwpck_require__(3870), 'base64')) + } + + return await WebAssembly.instantiate(mod, { + env: { + /* eslint-disable camelcase */ + + wasm_on_url: (p, at, len) => { + /* istanbul ignore next */ + return 0 + }, + wasm_on_status: (p, at, len) => { + assert.strictEqual(currentParser.ptr, p) + const start = at - currentBufferPtr + currentBufferRef.byteOffset + return currentParser.onStatus(new FastBuffer(currentBufferRef.buffer, start, len)) || 0 + }, + wasm_on_message_begin: (p) => { + assert.strictEqual(currentParser.ptr, p) + return currentParser.onMessageBegin() || 0 + }, + wasm_on_header_field: (p, at, len) => { + assert.strictEqual(currentParser.ptr, p) + const start = at - currentBufferPtr + currentBufferRef.byteOffset + return currentParser.onHeaderField(new FastBuffer(currentBufferRef.buffer, start, len)) || 0 + }, + wasm_on_header_value: (p, at, len) => { + assert.strictEqual(currentParser.ptr, p) + const start = at - currentBufferPtr + currentBufferRef.byteOffset + return currentParser.onHeaderValue(new FastBuffer(currentBufferRef.buffer, start, len)) || 0 + }, + wasm_on_headers_complete: (p, statusCode, upgrade, shouldKeepAlive) => { + assert.strictEqual(currentParser.ptr, p) + return currentParser.onHeadersComplete(statusCode, Boolean(upgrade), Boolean(shouldKeepAlive)) || 0 + }, + wasm_on_body: (p, at, len) => { + assert.strictEqual(currentParser.ptr, p) + const start = at - currentBufferPtr + currentBufferRef.byteOffset + return currentParser.onBody(new FastBuffer(currentBufferRef.buffer, start, len)) || 0 + }, + wasm_on_message_complete: (p) => { + assert.strictEqual(currentParser.ptr, p) + return currentParser.onMessageComplete() || 0 + } + + /* eslint-enable camelcase */ + } + }) +} + +let llhttpInstance = null +let llhttpPromise = lazyllhttp() +llhttpPromise.catch() + +let currentParser = null +let currentBufferRef = null +let currentBufferSize = 0 +let currentBufferPtr = null + +const TIMEOUT_HEADERS = 1 +const TIMEOUT_BODY = 2 +const TIMEOUT_IDLE = 3 + +class Parser { + constructor (client, socket, { exports }) { + assert(Number.isFinite(client[kMaxHeadersSize]) && client[kMaxHeadersSize] > 0) + + this.llhttp = exports + this.ptr = this.llhttp.llhttp_alloc(constants.TYPE.RESPONSE) + this.client = client + this.socket = socket + this.timeout = null + this.timeoutValue = null + this.timeoutType = null + this.statusCode = null + this.statusText = '' + this.upgrade = false + this.headers = [] + this.headersSize = 0 + this.headersMaxSize = client[kMaxHeadersSize] + this.shouldKeepAlive = false + this.paused = false + this.resume = this.resume.bind(this) + + this.bytesRead = 0 + + this.keepAlive = '' + this.contentLength = '' + this.connection = '' + this.maxResponseSize = client[kMaxResponseSize] + } + + setTimeout (value, type) { + this.timeoutType = type + if (value !== this.timeoutValue) { + timers.clearTimeout(this.timeout) + if (value) { + this.timeout = timers.setTimeout(onParserTimeout, value, this) + // istanbul ignore else: only for jest + if (this.timeout.unref) { + this.timeout.unref() + } + } else { + this.timeout = null + } + this.timeoutValue = value + } else if (this.timeout) { + // istanbul ignore else: only for jest + if (this.timeout.refresh) { + this.timeout.refresh() + } + } + } + + resume () { + if (this.socket.destroyed || !this.paused) { + return + } + + assert(this.ptr != null) + assert(currentParser == null) + + this.llhttp.llhttp_resume(this.ptr) + + assert(this.timeoutType === TIMEOUT_BODY) + if (this.timeout) { + // istanbul ignore else: only for jest + if (this.timeout.refresh) { + this.timeout.refresh() + } + } + + this.paused = false + this.execute(this.socket.read() || EMPTY_BUF) // Flush parser. + this.readMore() + } + + readMore () { + while (!this.paused && this.ptr) { + const chunk = this.socket.read() + if (chunk === null) { + break + } + this.execute(chunk) + } + } + + execute (data) { + assert(this.ptr != null) + assert(currentParser == null) + assert(!this.paused) + + const { socket, llhttp } = this + + if (data.length > currentBufferSize) { + if (currentBufferPtr) { + llhttp.free(currentBufferPtr) + } + currentBufferSize = Math.ceil(data.length / 4096) * 4096 + currentBufferPtr = llhttp.malloc(currentBufferSize) + } + + new Uint8Array(llhttp.memory.buffer, currentBufferPtr, currentBufferSize).set(data) + + // Call `execute` on the wasm parser. + // We pass the `llhttp_parser` pointer address, the pointer address of buffer view data, + // and finally the length of bytes to parse. + // The return value is an error code or `constants.ERROR.OK`. + try { + let ret + + try { + currentBufferRef = data + currentParser = this + ret = llhttp.llhttp_execute(this.ptr, currentBufferPtr, data.length) + /* eslint-disable-next-line no-useless-catch */ + } catch (err) { + /* istanbul ignore next: difficult to make a test case for */ + throw err + } finally { + currentParser = null + currentBufferRef = null + } + + const offset = llhttp.llhttp_get_error_pos(this.ptr) - currentBufferPtr + + if (ret === constants.ERROR.PAUSED_UPGRADE) { + this.onUpgrade(data.slice(offset)) + } else if (ret === constants.ERROR.PAUSED) { + this.paused = true + socket.unshift(data.slice(offset)) + } else if (ret !== constants.ERROR.OK) { + const ptr = llhttp.llhttp_get_error_reason(this.ptr) + let message = '' + /* istanbul ignore else: difficult to make a test case for */ + if (ptr) { + const len = new Uint8Array(llhttp.memory.buffer, ptr).indexOf(0) + message = + 'Response does not match the HTTP/1.1 protocol (' + + Buffer.from(llhttp.memory.buffer, ptr, len).toString() + + ')' + } + throw new HTTPParserError(message, constants.ERROR[ret], data.slice(offset)) + } + } catch (err) { + util.destroy(socket, err) + } + } + + destroy () { + assert(this.ptr != null) + assert(currentParser == null) + + this.llhttp.llhttp_free(this.ptr) + this.ptr = null + + timers.clearTimeout(this.timeout) + this.timeout = null + this.timeoutValue = null + this.timeoutType = null + + this.paused = false + } + + onStatus (buf) { + this.statusText = buf.toString() + } + + onMessageBegin () { + const { socket, client } = this + + /* istanbul ignore next: difficult to make a test case for */ + if (socket.destroyed) { + return -1 + } + + const request = client[kQueue][client[kRunningIdx]] + if (!request) { + return -1 + } + } + + onHeaderField (buf) { + const len = this.headers.length + + if ((len & 1) === 0) { + this.headers.push(buf) + } else { + this.headers[len - 1] = Buffer.concat([this.headers[len - 1], buf]) + } + + this.trackHeader(buf.length) + } + + onHeaderValue (buf) { + let len = this.headers.length + + if ((len & 1) === 1) { + this.headers.push(buf) + len += 1 + } else { + this.headers[len - 1] = Buffer.concat([this.headers[len - 1], buf]) + } + + const key = this.headers[len - 2] + if (key.length === 10 && key.toString().toLowerCase() === 'keep-alive') { + this.keepAlive += buf.toString() + } else if (key.length === 10 && key.toString().toLowerCase() === 'connection') { + this.connection += buf.toString() + } else if (key.length === 14 && key.toString().toLowerCase() === 'content-length') { + this.contentLength += buf.toString() + } + + this.trackHeader(buf.length) + } + + trackHeader (len) { + this.headersSize += len + if (this.headersSize >= this.headersMaxSize) { + util.destroy(this.socket, new HeadersOverflowError()) + } + } + + onUpgrade (head) { + const { upgrade, client, socket, headers, statusCode } = this + + assert(upgrade) + + const request = client[kQueue][client[kRunningIdx]] + assert(request) + + assert(!socket.destroyed) + assert(socket === client[kSocket]) + assert(!this.paused) + assert(request.upgrade || request.method === 'CONNECT') + + this.statusCode = null + this.statusText = '' + this.shouldKeepAlive = null + + assert(this.headers.length % 2 === 0) + this.headers = [] + this.headersSize = 0 + + socket.unshift(head) + + socket[kParser].destroy() + socket[kParser] = null + + socket[kClient] = null + socket[kError] = null + socket + .removeListener('error', onSocketError) + .removeListener('readable', onSocketReadable) + .removeListener('end', onSocketEnd) + .removeListener('close', onSocketClose) + + client[kSocket] = null + client[kQueue][client[kRunningIdx]++] = null + client.emit('disconnect', client[kUrl], [client], new InformationalError('upgrade')) + + try { + request.onUpgrade(statusCode, headers, socket) + } catch (err) { + util.destroy(socket, err) + } + + resume(client) + } + + onHeadersComplete (statusCode, upgrade, shouldKeepAlive) { + const { client, socket, headers, statusText } = this + + /* istanbul ignore next: difficult to make a test case for */ + if (socket.destroyed) { + return -1 + } + + const request = client[kQueue][client[kRunningIdx]] + + /* istanbul ignore next: difficult to make a test case for */ + if (!request) { + return -1 + } + + assert(!this.upgrade) + assert(this.statusCode < 200) + + if (statusCode === 100) { + util.destroy(socket, new SocketError('bad response', util.getSocketInfo(socket))) + return -1 + } + + /* this can only happen if server is misbehaving */ + if (upgrade && !request.upgrade) { + util.destroy(socket, new SocketError('bad upgrade', util.getSocketInfo(socket))) + return -1 + } + + assert.strictEqual(this.timeoutType, TIMEOUT_HEADERS) + + this.statusCode = statusCode + this.shouldKeepAlive = ( + shouldKeepAlive || + // Override llhttp value which does not allow keepAlive for HEAD. + (request.method === 'HEAD' && !socket[kReset] && this.connection.toLowerCase() === 'keep-alive') + ) + + if (this.statusCode >= 200) { + const bodyTimeout = request.bodyTimeout != null + ? request.bodyTimeout + : client[kBodyTimeout] + this.setTimeout(bodyTimeout, TIMEOUT_BODY) + } else if (this.timeout) { + // istanbul ignore else: only for jest + if (this.timeout.refresh) { + this.timeout.refresh() + } + } + + if (request.method === 'CONNECT') { + assert(client[kRunning] === 1) + this.upgrade = true + return 2 + } + + if (upgrade) { + assert(client[kRunning] === 1) + this.upgrade = true + return 2 + } + + assert(this.headers.length % 2 === 0) + this.headers = [] + this.headersSize = 0 + + if (this.shouldKeepAlive && client[kPipelining]) { + const keepAliveTimeout = this.keepAlive ? util.parseKeepAliveTimeout(this.keepAlive) : null + + if (keepAliveTimeout != null) { + const timeout = Math.min( + keepAliveTimeout - client[kKeepAliveTimeoutThreshold], + client[kKeepAliveMaxTimeout] + ) + if (timeout <= 0) { + socket[kReset] = true + } else { + client[kKeepAliveTimeoutValue] = timeout + } + } else { + client[kKeepAliveTimeoutValue] = client[kKeepAliveDefaultTimeout] + } + } else { + // Stop more requests from being dispatched. + socket[kReset] = true + } + + const pause = request.onHeaders(statusCode, headers, this.resume, statusText) === false + + if (request.aborted) { + return -1 + } + + if (request.method === 'HEAD') { + return 1 + } + + if (statusCode < 200) { + return 1 + } + + if (socket[kBlocking]) { + socket[kBlocking] = false + resume(client) + } + + return pause ? constants.ERROR.PAUSED : 0 + } + + onBody (buf) { + const { client, socket, statusCode, maxResponseSize } = this + + if (socket.destroyed) { + return -1 + } + + const request = client[kQueue][client[kRunningIdx]] + assert(request) + + assert.strictEqual(this.timeoutType, TIMEOUT_BODY) + if (this.timeout) { + // istanbul ignore else: only for jest + if (this.timeout.refresh) { + this.timeout.refresh() + } + } + + assert(statusCode >= 200) + + if (maxResponseSize > -1 && this.bytesRead + buf.length > maxResponseSize) { + util.destroy(socket, new ResponseExceededMaxSizeError()) + return -1 + } + + this.bytesRead += buf.length + + if (request.onData(buf) === false) { + return constants.ERROR.PAUSED + } + } + + onMessageComplete () { + const { client, socket, statusCode, upgrade, headers, contentLength, bytesRead, shouldKeepAlive } = this + + if (socket.destroyed && (!statusCode || shouldKeepAlive)) { + return -1 + } + + if (upgrade) { + return + } + + const request = client[kQueue][client[kRunningIdx]] + assert(request) + + assert(statusCode >= 100) + + this.statusCode = null + this.statusText = '' + this.bytesRead = 0 + this.contentLength = '' + this.keepAlive = '' + this.connection = '' + + assert(this.headers.length % 2 === 0) + this.headers = [] + this.headersSize = 0 + + if (statusCode < 200) { + return + } + + /* istanbul ignore next: should be handled by llhttp? */ + if (request.method !== 'HEAD' && contentLength && bytesRead !== parseInt(contentLength, 10)) { + util.destroy(socket, new ResponseContentLengthMismatchError()) + return -1 + } + + request.onComplete(headers) + + client[kQueue][client[kRunningIdx]++] = null + + if (socket[kWriting]) { + assert.strictEqual(client[kRunning], 0) + // Response completed before request. + util.destroy(socket, new InformationalError('reset')) + return constants.ERROR.PAUSED + } else if (!shouldKeepAlive) { + util.destroy(socket, new InformationalError('reset')) + return constants.ERROR.PAUSED + } else if (socket[kReset] && client[kRunning] === 0) { + // Destroy socket once all requests have completed. + // The request at the tail of the pipeline is the one + // that requested reset and no further requests should + // have been queued since then. + util.destroy(socket, new InformationalError('reset')) + return constants.ERROR.PAUSED + } else if (client[kPipelining] === 1) { + // We must wait a full event loop cycle to reuse this socket to make sure + // that non-spec compliant servers are not closing the connection even if they + // said they won't. + setImmediate(resume, client) + } else { + resume(client) + } + } +} + +function onParserTimeout (parser) { + const { socket, timeoutType, client } = parser + + /* istanbul ignore else */ + if (timeoutType === TIMEOUT_HEADERS) { + if (!socket[kWriting] || socket.writableNeedDrain || client[kRunning] > 1) { + assert(!parser.paused, 'cannot be paused while waiting for headers') + util.destroy(socket, new HeadersTimeoutError()) + } + } else if (timeoutType === TIMEOUT_BODY) { + if (!parser.paused) { + util.destroy(socket, new BodyTimeoutError()) + } + } else if (timeoutType === TIMEOUT_IDLE) { + assert(client[kRunning] === 0 && client[kKeepAliveTimeoutValue]) + util.destroy(socket, new InformationalError('socket idle timeout')) + } +} + +function onSocketReadable () { + const { [kParser]: parser } = this + if (parser) { + parser.readMore() + } +} + +function onSocketError (err) { + const { [kClient]: client, [kParser]: parser } = this + + assert(err.code !== 'ERR_TLS_CERT_ALTNAME_INVALID') + + if (client[kHTTPConnVersion] !== 'h2') { + // On Mac OS, we get an ECONNRESET even if there is a full body to be forwarded + // to the user. + if (err.code === 'ECONNRESET' && parser.statusCode && !parser.shouldKeepAlive) { + // We treat all incoming data so for as a valid response. + parser.onMessageComplete() + return + } + } + + this[kError] = err + + onError(this[kClient], err) +} + +function onError (client, err) { + if ( + client[kRunning] === 0 && + err.code !== 'UND_ERR_INFO' && + err.code !== 'UND_ERR_SOCKET' + ) { + // Error is not caused by running request and not a recoverable + // socket error. + + assert(client[kPendingIdx] === client[kRunningIdx]) + + const requests = client[kQueue].splice(client[kRunningIdx]) + for (let i = 0; i < requests.length; i++) { + const request = requests[i] + errorRequest(client, request, err) + } + assert(client[kSize] === 0) + } +} + +function onSocketEnd () { + const { [kParser]: parser, [kClient]: client } = this + + if (client[kHTTPConnVersion] !== 'h2') { + if (parser.statusCode && !parser.shouldKeepAlive) { + // We treat all incoming data so far as a valid response. + parser.onMessageComplete() + return + } + } + + util.destroy(this, new SocketError('other side closed', util.getSocketInfo(this))) +} + +function onSocketClose () { + const { [kClient]: client, [kParser]: parser } = this + + if (client[kHTTPConnVersion] === 'h1' && parser) { + if (!this[kError] && parser.statusCode && !parser.shouldKeepAlive) { + // We treat all incoming data so far as a valid response. + parser.onMessageComplete() + } + + this[kParser].destroy() + this[kParser] = null + } + + const err = this[kError] || new SocketError('closed', util.getSocketInfo(this)) + + client[kSocket] = null + + if (client.destroyed) { + assert(client[kPending] === 0) + + // Fail entire queue. + const requests = client[kQueue].splice(client[kRunningIdx]) + for (let i = 0; i < requests.length; i++) { + const request = requests[i] + errorRequest(client, request, err) + } + } else if (client[kRunning] > 0 && err.code !== 'UND_ERR_INFO') { + // Fail head of pipeline. + const request = client[kQueue][client[kRunningIdx]] + client[kQueue][client[kRunningIdx]++] = null + + errorRequest(client, request, err) + } + + client[kPendingIdx] = client[kRunningIdx] + + assert(client[kRunning] === 0) + + client.emit('disconnect', client[kUrl], [client], err) + + resume(client) +} + +async function connect (client) { + assert(!client[kConnecting]) + assert(!client[kSocket]) + + let { host, hostname, protocol, port } = client[kUrl] + + // Resolve ipv6 + if (hostname[0] === '[') { + const idx = hostname.indexOf(']') + + assert(idx !== -1) + const ip = hostname.substring(1, idx) + + assert(net.isIP(ip)) + hostname = ip + } + + client[kConnecting] = true + + if (channels.beforeConnect.hasSubscribers) { + channels.beforeConnect.publish({ + connectParams: { + host, + hostname, + protocol, + port, + servername: client[kServerName], + localAddress: client[kLocalAddress] + }, + connector: client[kConnector] + }) + } + + try { + const socket = await new Promise((resolve, reject) => { + client[kConnector]({ + host, + hostname, + protocol, + port, + servername: client[kServerName], + localAddress: client[kLocalAddress] + }, (err, socket) => { + if (err) { + reject(err) + } else { + resolve(socket) + } + }) + }) + + if (client.destroyed) { + util.destroy(socket.on('error', () => {}), new ClientDestroyedError()) + return + } + + client[kConnecting] = false + + assert(socket) + + const isH2 = socket.alpnProtocol === 'h2' + if (isH2) { + if (!h2ExperimentalWarned) { + h2ExperimentalWarned = true + process.emitWarning('H2 support is experimental, expect them to change at any time.', { + code: 'UNDICI-H2' + }) + } + + const session = http2.connect(client[kUrl], { + createConnection: () => socket, + peerMaxConcurrentStreams: client[kHTTP2SessionState].maxConcurrentStreams + }) + + client[kHTTPConnVersion] = 'h2' + session[kClient] = client + session[kSocket] = socket + session.on('error', onHttp2SessionError) + session.on('frameError', onHttp2FrameError) + session.on('end', onHttp2SessionEnd) + session.on('goaway', onHTTP2GoAway) + session.on('close', onSocketClose) + session.unref() + + client[kHTTP2Session] = session + socket[kHTTP2Session] = session + } else { + if (!llhttpInstance) { + llhttpInstance = await llhttpPromise + llhttpPromise = null + } + + socket[kNoRef] = false + socket[kWriting] = false + socket[kReset] = false + socket[kBlocking] = false + socket[kParser] = new Parser(client, socket, llhttpInstance) + } + + socket[kCounter] = 0 + socket[kMaxRequests] = client[kMaxRequests] + socket[kClient] = client + socket[kError] = null + + socket + .on('error', onSocketError) + .on('readable', onSocketReadable) + .on('end', onSocketEnd) + .on('close', onSocketClose) + + client[kSocket] = socket + + if (channels.connected.hasSubscribers) { + channels.connected.publish({ + connectParams: { + host, + hostname, + protocol, + port, + servername: client[kServerName], + localAddress: client[kLocalAddress] + }, + connector: client[kConnector], + socket + }) + } + client.emit('connect', client[kUrl], [client]) + } catch (err) { + if (client.destroyed) { + return + } + + client[kConnecting] = false + + if (channels.connectError.hasSubscribers) { + channels.connectError.publish({ + connectParams: { + host, + hostname, + protocol, + port, + servername: client[kServerName], + localAddress: client[kLocalAddress] + }, + connector: client[kConnector], + error: err + }) + } + + if (err.code === 'ERR_TLS_CERT_ALTNAME_INVALID') { + assert(client[kRunning] === 0) + while (client[kPending] > 0 && client[kQueue][client[kPendingIdx]].servername === client[kServerName]) { + const request = client[kQueue][client[kPendingIdx]++] + errorRequest(client, request, err) + } + } else { + onError(client, err) + } + + client.emit('connectionError', client[kUrl], [client], err) + } + + resume(client) +} + +function emitDrain (client) { + client[kNeedDrain] = 0 + client.emit('drain', client[kUrl], [client]) +} + +function resume (client, sync) { + if (client[kResuming] === 2) { + return + } + + client[kResuming] = 2 + + _resume(client, sync) + client[kResuming] = 0 + + if (client[kRunningIdx] > 256) { + client[kQueue].splice(0, client[kRunningIdx]) + client[kPendingIdx] -= client[kRunningIdx] + client[kRunningIdx] = 0 + } +} + +function _resume (client, sync) { + while (true) { + if (client.destroyed) { + assert(client[kPending] === 0) + return + } + + if (client[kClosedResolve] && !client[kSize]) { + client[kClosedResolve]() + client[kClosedResolve] = null + return + } + + const socket = client[kSocket] + + if (socket && !socket.destroyed && socket.alpnProtocol !== 'h2') { + if (client[kSize] === 0) { + if (!socket[kNoRef] && socket.unref) { + socket.unref() + socket[kNoRef] = true + } + } else if (socket[kNoRef] && socket.ref) { + socket.ref() + socket[kNoRef] = false + } + + if (client[kSize] === 0) { + if (socket[kParser].timeoutType !== TIMEOUT_IDLE) { + socket[kParser].setTimeout(client[kKeepAliveTimeoutValue], TIMEOUT_IDLE) + } + } else if (client[kRunning] > 0 && socket[kParser].statusCode < 200) { + if (socket[kParser].timeoutType !== TIMEOUT_HEADERS) { + const request = client[kQueue][client[kRunningIdx]] + const headersTimeout = request.headersTimeout != null + ? request.headersTimeout + : client[kHeadersTimeout] + socket[kParser].setTimeout(headersTimeout, TIMEOUT_HEADERS) + } + } + } + + if (client[kBusy]) { + client[kNeedDrain] = 2 + } else if (client[kNeedDrain] === 2) { + if (sync) { + client[kNeedDrain] = 1 + process.nextTick(emitDrain, client) + } else { + emitDrain(client) + } + continue + } + + if (client[kPending] === 0) { + return + } + + if (client[kRunning] >= (client[kPipelining] || 1)) { + return + } + + const request = client[kQueue][client[kPendingIdx]] + + if (client[kUrl].protocol === 'https:' && client[kServerName] !== request.servername) { + if (client[kRunning] > 0) { + return + } + + client[kServerName] = request.servername + + if (socket && socket.servername !== request.servername) { + util.destroy(socket, new InformationalError('servername changed')) + return + } + } + + if (client[kConnecting]) { + return + } + + if (!socket && !client[kHTTP2Session]) { + connect(client) + return + } + + if (socket.destroyed || socket[kWriting] || socket[kReset] || socket[kBlocking]) { + return + } + + if (client[kRunning] > 0 && !request.idempotent) { + // Non-idempotent request cannot be retried. + // Ensure that no other requests are inflight and + // could cause failure. + return + } + + if (client[kRunning] > 0 && (request.upgrade || request.method === 'CONNECT')) { + // Don't dispatch an upgrade until all preceding requests have completed. + // A misbehaving server might upgrade the connection before all pipelined + // request has completed. + return + } + + if (client[kRunning] > 0 && util.bodyLength(request.body) !== 0 && + (util.isStream(request.body) || util.isAsyncIterable(request.body))) { + // Request with stream or iterator body can error while other requests + // are inflight and indirectly error those as well. + // Ensure this doesn't happen by waiting for inflight + // to complete before dispatching. + + // Request with stream or iterator body cannot be retried. + // Ensure that no other requests are inflight and + // could cause failure. + return + } + + if (!request.aborted && write(client, request)) { + client[kPendingIdx]++ + } else { + client[kQueue].splice(client[kPendingIdx], 1) + } + } +} + +// https://www.rfc-editor.org/rfc/rfc7230#section-3.3.2 +function shouldSendContentLength (method) { + return method !== 'GET' && method !== 'HEAD' && method !== 'OPTIONS' && method !== 'TRACE' && method !== 'CONNECT' +} + +function write (client, request) { + if (client[kHTTPConnVersion] === 'h2') { + writeH2(client, client[kHTTP2Session], request) + return + } + + const { body, method, path, host, upgrade, headers, blocking, reset } = request + + // https://tools.ietf.org/html/rfc7231#section-4.3.1 + // https://tools.ietf.org/html/rfc7231#section-4.3.2 + // https://tools.ietf.org/html/rfc7231#section-4.3.5 + + // Sending a payload body on a request that does not + // expect it can cause undefined behavior on some + // servers and corrupt connection state. Do not + // re-use the connection for further requests. + + const expectsPayload = ( + method === 'PUT' || + method === 'POST' || + method === 'PATCH' + ) + + if (body && typeof body.read === 'function') { + // Try to read EOF in order to get length. + body.read(0) + } + + const bodyLength = util.bodyLength(body) + + let contentLength = bodyLength + + if (contentLength === null) { + contentLength = request.contentLength + } + + if (contentLength === 0 && !expectsPayload) { + // https://tools.ietf.org/html/rfc7230#section-3.3.2 + // A user agent SHOULD NOT send a Content-Length header field when + // the request message does not contain a payload body and the method + // semantics do not anticipate such a body. + + contentLength = null + } + + // https://github.com/nodejs/undici/issues/2046 + // A user agent may send a Content-Length header with 0 value, this should be allowed. + if (shouldSendContentLength(method) && contentLength > 0 && request.contentLength !== null && request.contentLength !== contentLength) { + if (client[kStrictContentLength]) { + errorRequest(client, request, new RequestContentLengthMismatchError()) + return false + } + + process.emitWarning(new RequestContentLengthMismatchError()) + } + + const socket = client[kSocket] + + try { + request.onConnect((err) => { + if (request.aborted || request.completed) { + return + } + + errorRequest(client, request, err || new RequestAbortedError()) + + util.destroy(socket, new InformationalError('aborted')) + }) + } catch (err) { + errorRequest(client, request, err) + } + + if (request.aborted) { + return false + } + + if (method === 'HEAD') { + // https://github.com/mcollina/undici/issues/258 + // Close after a HEAD request to interop with misbehaving servers + // that may send a body in the response. + + socket[kReset] = true + } + + if (upgrade || method === 'CONNECT') { + // On CONNECT or upgrade, block pipeline from dispatching further + // requests on this connection. + + socket[kReset] = true + } + + if (reset != null) { + socket[kReset] = reset + } + + if (client[kMaxRequests] && socket[kCounter]++ >= client[kMaxRequests]) { + socket[kReset] = true + } + + if (blocking) { + socket[kBlocking] = true + } + + let header = `${method} ${path} HTTP/1.1\r\n` + + if (typeof host === 'string') { + header += `host: ${host}\r\n` + } else { + header += client[kHostHeader] + } + + if (upgrade) { + header += `connection: upgrade\r\nupgrade: ${upgrade}\r\n` + } else if (client[kPipelining] && !socket[kReset]) { + header += 'connection: keep-alive\r\n' + } else { + header += 'connection: close\r\n' + } + + if (headers) { + header += headers + } + + if (channels.sendHeaders.hasSubscribers) { + channels.sendHeaders.publish({ request, headers: header, socket }) + } + + /* istanbul ignore else: assertion */ + if (!body || bodyLength === 0) { + if (contentLength === 0) { + socket.write(`${header}content-length: 0\r\n\r\n`, 'latin1') + } else { + assert(contentLength === null, 'no body must not have content length') + socket.write(`${header}\r\n`, 'latin1') + } + request.onRequestSent() + } else if (util.isBuffer(body)) { + assert(contentLength === body.byteLength, 'buffer body must have content length') + + socket.cork() + socket.write(`${header}content-length: ${contentLength}\r\n\r\n`, 'latin1') + socket.write(body) + socket.uncork() + request.onBodySent(body) + request.onRequestSent() + if (!expectsPayload) { + socket[kReset] = true + } + } else if (util.isBlobLike(body)) { + if (typeof body.stream === 'function') { + writeIterable({ body: body.stream(), client, request, socket, contentLength, header, expectsPayload }) + } else { + writeBlob({ body, client, request, socket, contentLength, header, expectsPayload }) + } + } else if (util.isStream(body)) { + writeStream({ body, client, request, socket, contentLength, header, expectsPayload }) + } else if (util.isIterable(body)) { + writeIterable({ body, client, request, socket, contentLength, header, expectsPayload }) + } else { + assert(false) + } + + return true +} + +function writeH2 (client, session, request) { + const { body, method, path, host, upgrade, expectContinue, signal, headers: reqHeaders } = request + + let headers + if (typeof reqHeaders === 'string') headers = Request[kHTTP2CopyHeaders](reqHeaders.trim()) + else headers = reqHeaders + + if (upgrade) { + errorRequest(client, request, new Error('Upgrade not supported for H2')) + return false + } + + try { + // TODO(HTTP/2): Should we call onConnect immediately or on stream ready event? + request.onConnect((err) => { + if (request.aborted || request.completed) { + return + } + + errorRequest(client, request, err || new RequestAbortedError()) + }) + } catch (err) { + errorRequest(client, request, err) + } + + if (request.aborted) { + return false + } + + /** @type {import('node:http2').ClientHttp2Stream} */ + let stream + const h2State = client[kHTTP2SessionState] + + headers[HTTP2_HEADER_AUTHORITY] = host || client[kHost] + headers[HTTP2_HEADER_METHOD] = method + + if (method === 'CONNECT') { + session.ref() + // we are already connected, streams are pending, first request + // will create a new stream. We trigger a request to create the stream and wait until + // `ready` event is triggered + // We disabled endStream to allow the user to write to the stream + stream = session.request(headers, { endStream: false, signal }) + + if (stream.id && !stream.pending) { + request.onUpgrade(null, null, stream) + ++h2State.openStreams + } else { + stream.once('ready', () => { + request.onUpgrade(null, null, stream) + ++h2State.openStreams + }) + } + + stream.once('close', () => { + h2State.openStreams -= 1 + // TODO(HTTP/2): unref only if current streams count is 0 + if (h2State.openStreams === 0) session.unref() + }) + + return true + } + + // https://tools.ietf.org/html/rfc7540#section-8.3 + // :path and :scheme headers must be omited when sending CONNECT + + headers[HTTP2_HEADER_PATH] = path + headers[HTTP2_HEADER_SCHEME] = 'https' + + // https://tools.ietf.org/html/rfc7231#section-4.3.1 + // https://tools.ietf.org/html/rfc7231#section-4.3.2 + // https://tools.ietf.org/html/rfc7231#section-4.3.5 + + // Sending a payload body on a request that does not + // expect it can cause undefined behavior on some + // servers and corrupt connection state. Do not + // re-use the connection for further requests. + + const expectsPayload = ( + method === 'PUT' || + method === 'POST' || + method === 'PATCH' + ) + + if (body && typeof body.read === 'function') { + // Try to read EOF in order to get length. + body.read(0) + } + + let contentLength = util.bodyLength(body) + + if (contentLength == null) { + contentLength = request.contentLength + } + + if (contentLength === 0 || !expectsPayload) { + // https://tools.ietf.org/html/rfc7230#section-3.3.2 + // A user agent SHOULD NOT send a Content-Length header field when + // the request message does not contain a payload body and the method + // semantics do not anticipate such a body. + + contentLength = null + } + + // https://github.com/nodejs/undici/issues/2046 + // A user agent may send a Content-Length header with 0 value, this should be allowed. + if (shouldSendContentLength(method) && contentLength > 0 && request.contentLength != null && request.contentLength !== contentLength) { + if (client[kStrictContentLength]) { + errorRequest(client, request, new RequestContentLengthMismatchError()) + return false + } + + process.emitWarning(new RequestContentLengthMismatchError()) + } + + if (contentLength != null) { + assert(body, 'no body must not have content length') + headers[HTTP2_HEADER_CONTENT_LENGTH] = `${contentLength}` + } + + session.ref() + + const shouldEndStream = method === 'GET' || method === 'HEAD' + if (expectContinue) { + headers[HTTP2_HEADER_EXPECT] = '100-continue' + stream = session.request(headers, { endStream: shouldEndStream, signal }) + + stream.once('continue', writeBodyH2) + } else { + stream = session.request(headers, { + endStream: shouldEndStream, + signal + }) + writeBodyH2() + } + + // Increment counter as we have new several streams open + ++h2State.openStreams + + stream.once('response', headers => { + const { [HTTP2_HEADER_STATUS]: statusCode, ...realHeaders } = headers + + if (request.onHeaders(Number(statusCode), realHeaders, stream.resume.bind(stream), '') === false) { + stream.pause() + } + }) + + stream.once('end', () => { + request.onComplete([]) + }) + + stream.on('data', (chunk) => { + if (request.onData(chunk) === false) { + stream.pause() + } + }) + + stream.once('close', () => { + h2State.openStreams -= 1 + // TODO(HTTP/2): unref only if current streams count is 0 + if (h2State.openStreams === 0) { + session.unref() + } + }) + + stream.once('error', function (err) { + if (client[kHTTP2Session] && !client[kHTTP2Session].destroyed && !this.closed && !this.destroyed) { + h2State.streams -= 1 + util.destroy(stream, err) + } + }) + + stream.once('frameError', (type, code) => { + const err = new InformationalError(`HTTP/2: "frameError" received - type ${type}, code ${code}`) + errorRequest(client, request, err) + + if (client[kHTTP2Session] && !client[kHTTP2Session].destroyed && !this.closed && !this.destroyed) { + h2State.streams -= 1 + util.destroy(stream, err) + } + }) + + // stream.on('aborted', () => { + // // TODO(HTTP/2): Support aborted + // }) + + // stream.on('timeout', () => { + // // TODO(HTTP/2): Support timeout + // }) + + // stream.on('push', headers => { + // // TODO(HTTP/2): Suppor push + // }) + + // stream.on('trailers', headers => { + // // TODO(HTTP/2): Support trailers + // }) + + return true + + function writeBodyH2 () { + /* istanbul ignore else: assertion */ + if (!body) { + request.onRequestSent() + } else if (util.isBuffer(body)) { + assert(contentLength === body.byteLength, 'buffer body must have content length') + stream.cork() + stream.write(body) + stream.uncork() + stream.end() + request.onBodySent(body) + request.onRequestSent() + } else if (util.isBlobLike(body)) { + if (typeof body.stream === 'function') { + writeIterable({ + client, + request, + contentLength, + h2stream: stream, + expectsPayload, + body: body.stream(), + socket: client[kSocket], + header: '' + }) + } else { + writeBlob({ + body, + client, + request, + contentLength, + expectsPayload, + h2stream: stream, + header: '', + socket: client[kSocket] + }) + } + } else if (util.isStream(body)) { + writeStream({ + body, + client, + request, + contentLength, + expectsPayload, + socket: client[kSocket], + h2stream: stream, + header: '' + }) + } else if (util.isIterable(body)) { + writeIterable({ + body, + client, + request, + contentLength, + expectsPayload, + header: '', + h2stream: stream, + socket: client[kSocket] + }) + } else { + assert(false) + } + } +} + +function writeStream ({ h2stream, body, client, request, socket, contentLength, header, expectsPayload }) { + assert(contentLength !== 0 || client[kRunning] === 0, 'stream body cannot be pipelined') + + if (client[kHTTPConnVersion] === 'h2') { + // For HTTP/2, is enough to pipe the stream + const pipe = pipeline( + body, + h2stream, + (err) => { + if (err) { + util.destroy(body, err) + util.destroy(h2stream, err) + } else { + request.onRequestSent() + } + } + ) + + pipe.on('data', onPipeData) + pipe.once('end', () => { + pipe.removeListener('data', onPipeData) + util.destroy(pipe) + }) + + function onPipeData (chunk) { + request.onBodySent(chunk) + } + + return + } + + let finished = false + + const writer = new AsyncWriter({ socket, request, contentLength, client, expectsPayload, header }) + + const onData = function (chunk) { + if (finished) { + return + } + + try { + if (!writer.write(chunk) && this.pause) { + this.pause() + } + } catch (err) { + util.destroy(this, err) + } + } + const onDrain = function () { + if (finished) { + return + } + + if (body.resume) { + body.resume() + } + } + const onAbort = function () { + if (finished) { + return + } + const err = new RequestAbortedError() + queueMicrotask(() => onFinished(err)) + } + const onFinished = function (err) { + if (finished) { + return + } + + finished = true + + assert(socket.destroyed || (socket[kWriting] && client[kRunning] <= 1)) + + socket + .off('drain', onDrain) + .off('error', onFinished) + + body + .removeListener('data', onData) + .removeListener('end', onFinished) + .removeListener('error', onFinished) + .removeListener('close', onAbort) + + if (!err) { + try { + writer.end() + } catch (er) { + err = er + } + } + + writer.destroy(err) + + if (err && (err.code !== 'UND_ERR_INFO' || err.message !== 'reset')) { + util.destroy(body, err) + } else { + util.destroy(body) + } + } + + body + .on('data', onData) + .on('end', onFinished) + .on('error', onFinished) + .on('close', onAbort) + + if (body.resume) { + body.resume() + } + + socket + .on('drain', onDrain) + .on('error', onFinished) +} + +async function writeBlob ({ h2stream, body, client, request, socket, contentLength, header, expectsPayload }) { + assert(contentLength === body.size, 'blob body must have content length') + + const isH2 = client[kHTTPConnVersion] === 'h2' + try { + if (contentLength != null && contentLength !== body.size) { + throw new RequestContentLengthMismatchError() + } + + const buffer = Buffer.from(await body.arrayBuffer()) + + if (isH2) { + h2stream.cork() + h2stream.write(buffer) + h2stream.uncork() + } else { + socket.cork() + socket.write(`${header}content-length: ${contentLength}\r\n\r\n`, 'latin1') + socket.write(buffer) + socket.uncork() + } + + request.onBodySent(buffer) + request.onRequestSent() + + if (!expectsPayload) { + socket[kReset] = true + } + + resume(client) + } catch (err) { + util.destroy(isH2 ? h2stream : socket, err) + } +} + +async function writeIterable ({ h2stream, body, client, request, socket, contentLength, header, expectsPayload }) { + assert(contentLength !== 0 || client[kRunning] === 0, 'iterator body cannot be pipelined') + + let callback = null + function onDrain () { + if (callback) { + const cb = callback + callback = null + cb() + } + } + + const waitForDrain = () => new Promise((resolve, reject) => { + assert(callback === null) + + if (socket[kError]) { + reject(socket[kError]) + } else { + callback = resolve + } + }) + + if (client[kHTTPConnVersion] === 'h2') { + h2stream + .on('close', onDrain) + .on('drain', onDrain) + + try { + // It's up to the user to somehow abort the async iterable. + for await (const chunk of body) { + if (socket[kError]) { + throw socket[kError] + } + + const res = h2stream.write(chunk) + request.onBodySent(chunk) + if (!res) { + await waitForDrain() + } + } + } catch (err) { + h2stream.destroy(err) + } finally { + request.onRequestSent() + h2stream.end() + h2stream + .off('close', onDrain) + .off('drain', onDrain) + } + + return + } + + socket + .on('close', onDrain) + .on('drain', onDrain) + + const writer = new AsyncWriter({ socket, request, contentLength, client, expectsPayload, header }) + try { + // It's up to the user to somehow abort the async iterable. + for await (const chunk of body) { + if (socket[kError]) { + throw socket[kError] + } + + if (!writer.write(chunk)) { + await waitForDrain() + } + } + + writer.end() + } catch (err) { + writer.destroy(err) + } finally { + socket + .off('close', onDrain) + .off('drain', onDrain) + } +} + +class AsyncWriter { + constructor ({ socket, request, contentLength, client, expectsPayload, header }) { + this.socket = socket + this.request = request + this.contentLength = contentLength + this.client = client + this.bytesWritten = 0 + this.expectsPayload = expectsPayload + this.header = header + + socket[kWriting] = true + } + + write (chunk) { + const { socket, request, contentLength, client, bytesWritten, expectsPayload, header } = this + + if (socket[kError]) { + throw socket[kError] + } + + if (socket.destroyed) { + return false + } + + const len = Buffer.byteLength(chunk) + if (!len) { + return true + } + + // We should defer writing chunks. + if (contentLength !== null && bytesWritten + len > contentLength) { + if (client[kStrictContentLength]) { + throw new RequestContentLengthMismatchError() + } + + process.emitWarning(new RequestContentLengthMismatchError()) + } + + socket.cork() + + if (bytesWritten === 0) { + if (!expectsPayload) { + socket[kReset] = true + } + + if (contentLength === null) { + socket.write(`${header}transfer-encoding: chunked\r\n`, 'latin1') + } else { + socket.write(`${header}content-length: ${contentLength}\r\n\r\n`, 'latin1') + } + } + + if (contentLength === null) { + socket.write(`\r\n${len.toString(16)}\r\n`, 'latin1') + } + + this.bytesWritten += len + + const ret = socket.write(chunk) + + socket.uncork() + + request.onBodySent(chunk) + + if (!ret) { + if (socket[kParser].timeout && socket[kParser].timeoutType === TIMEOUT_HEADERS) { + // istanbul ignore else: only for jest + if (socket[kParser].timeout.refresh) { + socket[kParser].timeout.refresh() + } + } + } + + return ret + } + + end () { + const { socket, contentLength, client, bytesWritten, expectsPayload, header, request } = this + request.onRequestSent() + + socket[kWriting] = false + + if (socket[kError]) { + throw socket[kError] + } + + if (socket.destroyed) { + return + } + + if (bytesWritten === 0) { + if (expectsPayload) { + // https://tools.ietf.org/html/rfc7230#section-3.3.2 + // A user agent SHOULD send a Content-Length in a request message when + // no Transfer-Encoding is sent and the request method defines a meaning + // for an enclosed payload body. + + socket.write(`${header}content-length: 0\r\n\r\n`, 'latin1') + } else { + socket.write(`${header}\r\n`, 'latin1') + } + } else if (contentLength === null) { + socket.write('\r\n0\r\n\r\n', 'latin1') + } + + if (contentLength !== null && bytesWritten !== contentLength) { + if (client[kStrictContentLength]) { + throw new RequestContentLengthMismatchError() + } else { + process.emitWarning(new RequestContentLengthMismatchError()) + } + } + + if (socket[kParser].timeout && socket[kParser].timeoutType === TIMEOUT_HEADERS) { + // istanbul ignore else: only for jest + if (socket[kParser].timeout.refresh) { + socket[kParser].timeout.refresh() + } + } + + resume(client) + } + + destroy (err) { + const { socket, client } = this + + socket[kWriting] = false + + if (err) { + assert(client[kRunning] <= 1, 'pipeline should only contain this request') + util.destroy(socket, err) + } + } +} + +function errorRequest (client, request, err) { + try { + request.onError(err) + assert(request.aborted) + } catch (err) { + client.emit('error', err) + } +} + +module.exports = Client + + +/***/ }), + +/***/ 3194: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +/* istanbul ignore file: only for Node 12 */ + +const { kConnected, kSize } = __nccwpck_require__(6443) + +class CompatWeakRef { + constructor (value) { + this.value = value + } + + deref () { + return this.value[kConnected] === 0 && this.value[kSize] === 0 + ? undefined + : this.value + } +} + +class CompatFinalizer { + constructor (finalizer) { + this.finalizer = finalizer + } + + register (dispatcher, key) { + if (dispatcher.on) { + dispatcher.on('disconnect', () => { + if (dispatcher[kConnected] === 0 && dispatcher[kSize] === 0) { + this.finalizer(key) + } + }) + } + } +} + +module.exports = function () { + // FIXME: remove workaround when the Node bug is fixed + // https://github.com/nodejs/node/issues/49344#issuecomment-1741776308 + if (process.env.NODE_V8_COVERAGE) { + return { + WeakRef: CompatWeakRef, + FinalizationRegistry: CompatFinalizer + } + } + return { + WeakRef: global.WeakRef || CompatWeakRef, + FinalizationRegistry: global.FinalizationRegistry || CompatFinalizer + } +} + + +/***/ }), + +/***/ 9237: +/***/ ((module) => { + +"use strict"; + + +// https://wicg.github.io/cookie-store/#cookie-maximum-attribute-value-size +const maxAttributeValueSize = 1024 + +// https://wicg.github.io/cookie-store/#cookie-maximum-name-value-pair-size +const maxNameValuePairSize = 4096 + +module.exports = { + maxAttributeValueSize, + maxNameValuePairSize +} + + +/***/ }), + +/***/ 3168: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const { parseSetCookie } = __nccwpck_require__(8915) +const { stringify, getHeadersList } = __nccwpck_require__(3834) +const { webidl } = __nccwpck_require__(4222) +const { Headers } = __nccwpck_require__(6349) + +/** + * @typedef {Object} Cookie + * @property {string} name + * @property {string} value + * @property {Date|number|undefined} expires + * @property {number|undefined} maxAge + * @property {string|undefined} domain + * @property {string|undefined} path + * @property {boolean|undefined} secure + * @property {boolean|undefined} httpOnly + * @property {'Strict'|'Lax'|'None'} sameSite + * @property {string[]} unparsed + */ + +/** + * @param {Headers} headers + * @returns {Record} + */ +function getCookies (headers) { + webidl.argumentLengthCheck(arguments, 1, { header: 'getCookies' }) + + webidl.brandCheck(headers, Headers, { strict: false }) + + const cookie = headers.get('cookie') + const out = {} + + if (!cookie) { + return out + } + + for (const piece of cookie.split(';')) { + const [name, ...value] = piece.split('=') + + out[name.trim()] = value.join('=') + } + + return out +} + +/** + * @param {Headers} headers + * @param {string} name + * @param {{ path?: string, domain?: string }|undefined} attributes + * @returns {void} + */ +function deleteCookie (headers, name, attributes) { + webidl.argumentLengthCheck(arguments, 2, { header: 'deleteCookie' }) + + webidl.brandCheck(headers, Headers, { strict: false }) + + name = webidl.converters.DOMString(name) + attributes = webidl.converters.DeleteCookieAttributes(attributes) + + // Matches behavior of + // https://github.com/denoland/deno_std/blob/63827b16330b82489a04614027c33b7904e08be5/http/cookie.ts#L278 + setCookie(headers, { + name, + value: '', + expires: new Date(0), + ...attributes + }) +} + +/** + * @param {Headers} headers + * @returns {Cookie[]} + */ +function getSetCookies (headers) { + webidl.argumentLengthCheck(arguments, 1, { header: 'getSetCookies' }) + + webidl.brandCheck(headers, Headers, { strict: false }) + + const cookies = getHeadersList(headers).cookies + + if (!cookies) { + return [] + } + + // In older versions of undici, cookies is a list of name:value. + return cookies.map((pair) => parseSetCookie(Array.isArray(pair) ? pair[1] : pair)) +} + +/** + * @param {Headers} headers + * @param {Cookie} cookie + * @returns {void} + */ +function setCookie (headers, cookie) { + webidl.argumentLengthCheck(arguments, 2, { header: 'setCookie' }) + + webidl.brandCheck(headers, Headers, { strict: false }) + + cookie = webidl.converters.Cookie(cookie) + + const str = stringify(cookie) + + if (str) { + headers.append('Set-Cookie', stringify(cookie)) + } +} + +webidl.converters.DeleteCookieAttributes = webidl.dictionaryConverter([ + { + converter: webidl.nullableConverter(webidl.converters.DOMString), + key: 'path', + defaultValue: null + }, + { + converter: webidl.nullableConverter(webidl.converters.DOMString), + key: 'domain', + defaultValue: null + } +]) + +webidl.converters.Cookie = webidl.dictionaryConverter([ + { + converter: webidl.converters.DOMString, + key: 'name' + }, + { + converter: webidl.converters.DOMString, + key: 'value' + }, + { + converter: webidl.nullableConverter((value) => { + if (typeof value === 'number') { + return webidl.converters['unsigned long long'](value) + } + + return new Date(value) + }), + key: 'expires', + defaultValue: null + }, + { + converter: webidl.nullableConverter(webidl.converters['long long']), + key: 'maxAge', + defaultValue: null + }, + { + converter: webidl.nullableConverter(webidl.converters.DOMString), + key: 'domain', + defaultValue: null + }, + { + converter: webidl.nullableConverter(webidl.converters.DOMString), + key: 'path', + defaultValue: null + }, + { + converter: webidl.nullableConverter(webidl.converters.boolean), + key: 'secure', + defaultValue: null + }, + { + converter: webidl.nullableConverter(webidl.converters.boolean), + key: 'httpOnly', + defaultValue: null + }, + { + converter: webidl.converters.USVString, + key: 'sameSite', + allowedValues: ['Strict', 'Lax', 'None'] + }, + { + converter: webidl.sequenceConverter(webidl.converters.DOMString), + key: 'unparsed', + defaultValue: [] + } +]) + +module.exports = { + getCookies, + deleteCookie, + getSetCookies, + setCookie +} + + +/***/ }), + +/***/ 8915: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const { maxNameValuePairSize, maxAttributeValueSize } = __nccwpck_require__(9237) +const { isCTLExcludingHtab } = __nccwpck_require__(3834) +const { collectASequenceOfCodePointsFast } = __nccwpck_require__(4322) +const assert = __nccwpck_require__(2613) + +/** + * @description Parses the field-value attributes of a set-cookie header string. + * @see https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#section-5.4 + * @param {string} header + * @returns if the header is invalid, null will be returned + */ +function parseSetCookie (header) { + // 1. If the set-cookie-string contains a %x00-08 / %x0A-1F / %x7F + // character (CTL characters excluding HTAB): Abort these steps and + // ignore the set-cookie-string entirely. + if (isCTLExcludingHtab(header)) { + return null + } + + let nameValuePair = '' + let unparsedAttributes = '' + let name = '' + let value = '' + + // 2. If the set-cookie-string contains a %x3B (";") character: + if (header.includes(';')) { + // 1. The name-value-pair string consists of the characters up to, + // but not including, the first %x3B (";"), and the unparsed- + // attributes consist of the remainder of the set-cookie-string + // (including the %x3B (";") in question). + const position = { position: 0 } + + nameValuePair = collectASequenceOfCodePointsFast(';', header, position) + unparsedAttributes = header.slice(position.position) + } else { + // Otherwise: + + // 1. The name-value-pair string consists of all the characters + // contained in the set-cookie-string, and the unparsed- + // attributes is the empty string. + nameValuePair = header + } + + // 3. If the name-value-pair string lacks a %x3D ("=") character, then + // the name string is empty, and the value string is the value of + // name-value-pair. + if (!nameValuePair.includes('=')) { + value = nameValuePair + } else { + // Otherwise, the name string consists of the characters up to, but + // not including, the first %x3D ("=") character, and the (possibly + // empty) value string consists of the characters after the first + // %x3D ("=") character. + const position = { position: 0 } + name = collectASequenceOfCodePointsFast( + '=', + nameValuePair, + position + ) + value = nameValuePair.slice(position.position + 1) + } + + // 4. Remove any leading or trailing WSP characters from the name + // string and the value string. + name = name.trim() + value = value.trim() + + // 5. If the sum of the lengths of the name string and the value string + // is more than 4096 octets, abort these steps and ignore the set- + // cookie-string entirely. + if (name.length + value.length > maxNameValuePairSize) { + return null + } + + // 6. The cookie-name is the name string, and the cookie-value is the + // value string. + return { + name, value, ...parseUnparsedAttributes(unparsedAttributes) + } +} + +/** + * Parses the remaining attributes of a set-cookie header + * @see https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#section-5.4 + * @param {string} unparsedAttributes + * @param {[Object.]={}} cookieAttributeList + */ +function parseUnparsedAttributes (unparsedAttributes, cookieAttributeList = {}) { + // 1. If the unparsed-attributes string is empty, skip the rest of + // these steps. + if (unparsedAttributes.length === 0) { + return cookieAttributeList + } + + // 2. Discard the first character of the unparsed-attributes (which + // will be a %x3B (";") character). + assert(unparsedAttributes[0] === ';') + unparsedAttributes = unparsedAttributes.slice(1) + + let cookieAv = '' + + // 3. If the remaining unparsed-attributes contains a %x3B (";") + // character: + if (unparsedAttributes.includes(';')) { + // 1. Consume the characters of the unparsed-attributes up to, but + // not including, the first %x3B (";") character. + cookieAv = collectASequenceOfCodePointsFast( + ';', + unparsedAttributes, + { position: 0 } + ) + unparsedAttributes = unparsedAttributes.slice(cookieAv.length) + } else { + // Otherwise: + + // 1. Consume the remainder of the unparsed-attributes. + cookieAv = unparsedAttributes + unparsedAttributes = '' + } + + // Let the cookie-av string be the characters consumed in this step. + + let attributeName = '' + let attributeValue = '' + + // 4. If the cookie-av string contains a %x3D ("=") character: + if (cookieAv.includes('=')) { + // 1. The (possibly empty) attribute-name string consists of the + // characters up to, but not including, the first %x3D ("=") + // character, and the (possibly empty) attribute-value string + // consists of the characters after the first %x3D ("=") + // character. + const position = { position: 0 } + + attributeName = collectASequenceOfCodePointsFast( + '=', + cookieAv, + position + ) + attributeValue = cookieAv.slice(position.position + 1) + } else { + // Otherwise: + + // 1. The attribute-name string consists of the entire cookie-av + // string, and the attribute-value string is empty. + attributeName = cookieAv + } + + // 5. Remove any leading or trailing WSP characters from the attribute- + // name string and the attribute-value string. + attributeName = attributeName.trim() + attributeValue = attributeValue.trim() + + // 6. If the attribute-value is longer than 1024 octets, ignore the + // cookie-av string and return to Step 1 of this algorithm. + if (attributeValue.length > maxAttributeValueSize) { + return parseUnparsedAttributes(unparsedAttributes, cookieAttributeList) + } + + // 7. Process the attribute-name and attribute-value according to the + // requirements in the following subsections. (Notice that + // attributes with unrecognized attribute-names are ignored.) + const attributeNameLowercase = attributeName.toLowerCase() + + // https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#section-5.4.1 + // If the attribute-name case-insensitively matches the string + // "Expires", the user agent MUST process the cookie-av as follows. + if (attributeNameLowercase === 'expires') { + // 1. Let the expiry-time be the result of parsing the attribute-value + // as cookie-date (see Section 5.1.1). + const expiryTime = new Date(attributeValue) + + // 2. If the attribute-value failed to parse as a cookie date, ignore + // the cookie-av. + + cookieAttributeList.expires = expiryTime + } else if (attributeNameLowercase === 'max-age') { + // https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#section-5.4.2 + // If the attribute-name case-insensitively matches the string "Max- + // Age", the user agent MUST process the cookie-av as follows. + + // 1. If the first character of the attribute-value is not a DIGIT or a + // "-" character, ignore the cookie-av. + const charCode = attributeValue.charCodeAt(0) + + if ((charCode < 48 || charCode > 57) && attributeValue[0] !== '-') { + return parseUnparsedAttributes(unparsedAttributes, cookieAttributeList) + } + + // 2. If the remainder of attribute-value contains a non-DIGIT + // character, ignore the cookie-av. + if (!/^\d+$/.test(attributeValue)) { + return parseUnparsedAttributes(unparsedAttributes, cookieAttributeList) + } + + // 3. Let delta-seconds be the attribute-value converted to an integer. + const deltaSeconds = Number(attributeValue) + + // 4. Let cookie-age-limit be the maximum age of the cookie (which + // SHOULD be 400 days or less, see Section 4.1.2.2). + + // 5. Set delta-seconds to the smaller of its present value and cookie- + // age-limit. + // deltaSeconds = Math.min(deltaSeconds * 1000, maxExpiresMs) + + // 6. If delta-seconds is less than or equal to zero (0), let expiry- + // time be the earliest representable date and time. Otherwise, let + // the expiry-time be the current date and time plus delta-seconds + // seconds. + // const expiryTime = deltaSeconds <= 0 ? Date.now() : Date.now() + deltaSeconds + + // 7. Append an attribute to the cookie-attribute-list with an + // attribute-name of Max-Age and an attribute-value of expiry-time. + cookieAttributeList.maxAge = deltaSeconds + } else if (attributeNameLowercase === 'domain') { + // https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#section-5.4.3 + // If the attribute-name case-insensitively matches the string "Domain", + // the user agent MUST process the cookie-av as follows. + + // 1. Let cookie-domain be the attribute-value. + let cookieDomain = attributeValue + + // 2. If cookie-domain starts with %x2E ("."), let cookie-domain be + // cookie-domain without its leading %x2E ("."). + if (cookieDomain[0] === '.') { + cookieDomain = cookieDomain.slice(1) + } + + // 3. Convert the cookie-domain to lower case. + cookieDomain = cookieDomain.toLowerCase() + + // 4. Append an attribute to the cookie-attribute-list with an + // attribute-name of Domain and an attribute-value of cookie-domain. + cookieAttributeList.domain = cookieDomain + } else if (attributeNameLowercase === 'path') { + // https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#section-5.4.4 + // If the attribute-name case-insensitively matches the string "Path", + // the user agent MUST process the cookie-av as follows. + + // 1. If the attribute-value is empty or if the first character of the + // attribute-value is not %x2F ("/"): + let cookiePath = '' + if (attributeValue.length === 0 || attributeValue[0] !== '/') { + // 1. Let cookie-path be the default-path. + cookiePath = '/' + } else { + // Otherwise: + + // 1. Let cookie-path be the attribute-value. + cookiePath = attributeValue + } + + // 2. Append an attribute to the cookie-attribute-list with an + // attribute-name of Path and an attribute-value of cookie-path. + cookieAttributeList.path = cookiePath + } else if (attributeNameLowercase === 'secure') { + // https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#section-5.4.5 + // If the attribute-name case-insensitively matches the string "Secure", + // the user agent MUST append an attribute to the cookie-attribute-list + // with an attribute-name of Secure and an empty attribute-value. + + cookieAttributeList.secure = true + } else if (attributeNameLowercase === 'httponly') { + // https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#section-5.4.6 + // If the attribute-name case-insensitively matches the string + // "HttpOnly", the user agent MUST append an attribute to the cookie- + // attribute-list with an attribute-name of HttpOnly and an empty + // attribute-value. + + cookieAttributeList.httpOnly = true + } else if (attributeNameLowercase === 'samesite') { + // https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#section-5.4.7 + // If the attribute-name case-insensitively matches the string + // "SameSite", the user agent MUST process the cookie-av as follows: + + // 1. Let enforcement be "Default". + let enforcement = 'Default' + + const attributeValueLowercase = attributeValue.toLowerCase() + // 2. If cookie-av's attribute-value is a case-insensitive match for + // "None", set enforcement to "None". + if (attributeValueLowercase.includes('none')) { + enforcement = 'None' + } + + // 3. If cookie-av's attribute-value is a case-insensitive match for + // "Strict", set enforcement to "Strict". + if (attributeValueLowercase.includes('strict')) { + enforcement = 'Strict' + } + + // 4. If cookie-av's attribute-value is a case-insensitive match for + // "Lax", set enforcement to "Lax". + if (attributeValueLowercase.includes('lax')) { + enforcement = 'Lax' + } + + // 5. Append an attribute to the cookie-attribute-list with an + // attribute-name of "SameSite" and an attribute-value of + // enforcement. + cookieAttributeList.sameSite = enforcement + } else { + cookieAttributeList.unparsed ??= [] + + cookieAttributeList.unparsed.push(`${attributeName}=${attributeValue}`) + } + + // 8. Return to Step 1 of this algorithm. + return parseUnparsedAttributes(unparsedAttributes, cookieAttributeList) +} + +module.exports = { + parseSetCookie, + parseUnparsedAttributes +} + + +/***/ }), + +/***/ 3834: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const assert = __nccwpck_require__(2613) +const { kHeadersList } = __nccwpck_require__(6443) + +function isCTLExcludingHtab (value) { + if (value.length === 0) { + return false + } + + for (const char of value) { + const code = char.charCodeAt(0) + + if ( + (code >= 0x00 || code <= 0x08) || + (code >= 0x0A || code <= 0x1F) || + code === 0x7F + ) { + return false + } + } +} + +/** + CHAR = + token = 1* + separators = "(" | ")" | "<" | ">" | "@" + | "," | ";" | ":" | "\" | <"> + | "/" | "[" | "]" | "?" | "=" + | "{" | "}" | SP | HT + * @param {string} name + */ +function validateCookieName (name) { + for (const char of name) { + const code = char.charCodeAt(0) + + if ( + (code <= 0x20 || code > 0x7F) || + char === '(' || + char === ')' || + char === '>' || + char === '<' || + char === '@' || + char === ',' || + char === ';' || + char === ':' || + char === '\\' || + char === '"' || + char === '/' || + char === '[' || + char === ']' || + char === '?' || + char === '=' || + char === '{' || + char === '}' + ) { + throw new Error('Invalid cookie name') + } + } +} + +/** + cookie-value = *cookie-octet / ( DQUOTE *cookie-octet DQUOTE ) + cookie-octet = %x21 / %x23-2B / %x2D-3A / %x3C-5B / %x5D-7E + ; US-ASCII characters excluding CTLs, + ; whitespace DQUOTE, comma, semicolon, + ; and backslash + * @param {string} value + */ +function validateCookieValue (value) { + for (const char of value) { + const code = char.charCodeAt(0) + + if ( + code < 0x21 || // exclude CTLs (0-31) + code === 0x22 || + code === 0x2C || + code === 0x3B || + code === 0x5C || + code > 0x7E // non-ascii + ) { + throw new Error('Invalid header value') + } + } +} + +/** + * path-value = + * @param {string} path + */ +function validateCookiePath (path) { + for (const char of path) { + const code = char.charCodeAt(0) + + if (code < 0x21 || char === ';') { + throw new Error('Invalid cookie path') + } + } +} + +/** + * I have no idea why these values aren't allowed to be honest, + * but Deno tests these. - Khafra + * @param {string} domain + */ +function validateCookieDomain (domain) { + if ( + domain.startsWith('-') || + domain.endsWith('.') || + domain.endsWith('-') + ) { + throw new Error('Invalid cookie domain') + } +} + +/** + * @see https://www.rfc-editor.org/rfc/rfc7231#section-7.1.1.1 + * @param {number|Date} date + IMF-fixdate = day-name "," SP date1 SP time-of-day SP GMT + ; fixed length/zone/capitalization subset of the format + ; see Section 3.3 of [RFC5322] + + day-name = %x4D.6F.6E ; "Mon", case-sensitive + / %x54.75.65 ; "Tue", case-sensitive + / %x57.65.64 ; "Wed", case-sensitive + / %x54.68.75 ; "Thu", case-sensitive + / %x46.72.69 ; "Fri", case-sensitive + / %x53.61.74 ; "Sat", case-sensitive + / %x53.75.6E ; "Sun", case-sensitive + date1 = day SP month SP year + ; e.g., 02 Jun 1982 + + day = 2DIGIT + month = %x4A.61.6E ; "Jan", case-sensitive + / %x46.65.62 ; "Feb", case-sensitive + / %x4D.61.72 ; "Mar", case-sensitive + / %x41.70.72 ; "Apr", case-sensitive + / %x4D.61.79 ; "May", case-sensitive + / %x4A.75.6E ; "Jun", case-sensitive + / %x4A.75.6C ; "Jul", case-sensitive + / %x41.75.67 ; "Aug", case-sensitive + / %x53.65.70 ; "Sep", case-sensitive + / %x4F.63.74 ; "Oct", case-sensitive + / %x4E.6F.76 ; "Nov", case-sensitive + / %x44.65.63 ; "Dec", case-sensitive + year = 4DIGIT + + GMT = %x47.4D.54 ; "GMT", case-sensitive + + time-of-day = hour ":" minute ":" second + ; 00:00:00 - 23:59:60 (leap second) + + hour = 2DIGIT + minute = 2DIGIT + second = 2DIGIT + */ +function toIMFDate (date) { + if (typeof date === 'number') { + date = new Date(date) + } + + const days = [ + 'Sun', 'Mon', 'Tue', 'Wed', + 'Thu', 'Fri', 'Sat' + ] + + const months = [ + 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', + 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec' + ] + + const dayName = days[date.getUTCDay()] + const day = date.getUTCDate().toString().padStart(2, '0') + const month = months[date.getUTCMonth()] + const year = date.getUTCFullYear() + const hour = date.getUTCHours().toString().padStart(2, '0') + const minute = date.getUTCMinutes().toString().padStart(2, '0') + const second = date.getUTCSeconds().toString().padStart(2, '0') + + return `${dayName}, ${day} ${month} ${year} ${hour}:${minute}:${second} GMT` +} + +/** + max-age-av = "Max-Age=" non-zero-digit *DIGIT + ; In practice, both expires-av and max-age-av + ; are limited to dates representable by the + ; user agent. + * @param {number} maxAge + */ +function validateCookieMaxAge (maxAge) { + if (maxAge < 0) { + throw new Error('Invalid cookie max-age') + } +} + +/** + * @see https://www.rfc-editor.org/rfc/rfc6265#section-4.1.1 + * @param {import('./index').Cookie} cookie + */ +function stringify (cookie) { + if (cookie.name.length === 0) { + return null + } + + validateCookieName(cookie.name) + validateCookieValue(cookie.value) + + const out = [`${cookie.name}=${cookie.value}`] + + // https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-cookie-prefixes-00#section-3.1 + // https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-cookie-prefixes-00#section-3.2 + if (cookie.name.startsWith('__Secure-')) { + cookie.secure = true + } + + if (cookie.name.startsWith('__Host-')) { + cookie.secure = true + cookie.domain = null + cookie.path = '/' + } + + if (cookie.secure) { + out.push('Secure') + } + + if (cookie.httpOnly) { + out.push('HttpOnly') + } + + if (typeof cookie.maxAge === 'number') { + validateCookieMaxAge(cookie.maxAge) + out.push(`Max-Age=${cookie.maxAge}`) + } + + if (cookie.domain) { + validateCookieDomain(cookie.domain) + out.push(`Domain=${cookie.domain}`) + } + + if (cookie.path) { + validateCookiePath(cookie.path) + out.push(`Path=${cookie.path}`) + } + + if (cookie.expires && cookie.expires.toString() !== 'Invalid Date') { + out.push(`Expires=${toIMFDate(cookie.expires)}`) + } + + if (cookie.sameSite) { + out.push(`SameSite=${cookie.sameSite}`) + } + + for (const part of cookie.unparsed) { + if (!part.includes('=')) { + throw new Error('Invalid unparsed') + } + + const [key, ...value] = part.split('=') + + out.push(`${key.trim()}=${value.join('=')}`) + } + + return out.join('; ') +} + +let kHeadersListNode + +function getHeadersList (headers) { + if (headers[kHeadersList]) { + return headers[kHeadersList] + } + + if (!kHeadersListNode) { + kHeadersListNode = Object.getOwnPropertySymbols(headers).find( + (symbol) => symbol.description === 'headers list' + ) + + assert(kHeadersListNode, 'Headers cannot be parsed') + } + + const headersList = headers[kHeadersListNode] + assert(headersList) + + return headersList +} + +module.exports = { + isCTLExcludingHtab, + stringify, + getHeadersList +} + + +/***/ }), + +/***/ 9136: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const net = __nccwpck_require__(9278) +const assert = __nccwpck_require__(2613) +const util = __nccwpck_require__(3440) +const { InvalidArgumentError, ConnectTimeoutError } = __nccwpck_require__(8707) + +let tls // include tls conditionally since it is not always available + +// TODO: session re-use does not wait for the first +// connection to resolve the session and might therefore +// resolve the same servername multiple times even when +// re-use is enabled. + +let SessionCache +// FIXME: remove workaround when the Node bug is fixed +// https://github.com/nodejs/node/issues/49344#issuecomment-1741776308 +if (global.FinalizationRegistry && !process.env.NODE_V8_COVERAGE) { + SessionCache = class WeakSessionCache { + constructor (maxCachedSessions) { + this._maxCachedSessions = maxCachedSessions + this._sessionCache = new Map() + this._sessionRegistry = new global.FinalizationRegistry((key) => { + if (this._sessionCache.size < this._maxCachedSessions) { + return + } + + const ref = this._sessionCache.get(key) + if (ref !== undefined && ref.deref() === undefined) { + this._sessionCache.delete(key) + } + }) + } + + get (sessionKey) { + const ref = this._sessionCache.get(sessionKey) + return ref ? ref.deref() : null + } + + set (sessionKey, session) { + if (this._maxCachedSessions === 0) { + return + } + + this._sessionCache.set(sessionKey, new WeakRef(session)) + this._sessionRegistry.register(session, sessionKey) + } + } +} else { + SessionCache = class SimpleSessionCache { + constructor (maxCachedSessions) { + this._maxCachedSessions = maxCachedSessions + this._sessionCache = new Map() + } + + get (sessionKey) { + return this._sessionCache.get(sessionKey) + } + + set (sessionKey, session) { + if (this._maxCachedSessions === 0) { + return + } + + if (this._sessionCache.size >= this._maxCachedSessions) { + // remove the oldest session + const { value: oldestKey } = this._sessionCache.keys().next() + this._sessionCache.delete(oldestKey) + } + + this._sessionCache.set(sessionKey, session) + } + } +} + +function buildConnector ({ allowH2, maxCachedSessions, socketPath, timeout, ...opts }) { + if (maxCachedSessions != null && (!Number.isInteger(maxCachedSessions) || maxCachedSessions < 0)) { + throw new InvalidArgumentError('maxCachedSessions must be a positive integer or zero') + } + + const options = { path: socketPath, ...opts } + const sessionCache = new SessionCache(maxCachedSessions == null ? 100 : maxCachedSessions) + timeout = timeout == null ? 10e3 : timeout + allowH2 = allowH2 != null ? allowH2 : false + return function connect ({ hostname, host, protocol, port, servername, localAddress, httpSocket }, callback) { + let socket + if (protocol === 'https:') { + if (!tls) { + tls = __nccwpck_require__(4756) + } + servername = servername || options.servername || util.getServerName(host) || null + + const sessionKey = servername || hostname + const session = sessionCache.get(sessionKey) || null + + assert(sessionKey) + + socket = tls.connect({ + highWaterMark: 16384, // TLS in node can't have bigger HWM anyway... + ...options, + servername, + session, + localAddress, + // TODO(HTTP/2): Add support for h2c + ALPNProtocols: allowH2 ? ['http/1.1', 'h2'] : ['http/1.1'], + socket: httpSocket, // upgrade socket connection + port: port || 443, + host: hostname + }) + + socket + .on('session', function (session) { + // TODO (fix): Can a session become invalid once established? Don't think so? + sessionCache.set(sessionKey, session) + }) + } else { + assert(!httpSocket, 'httpSocket can only be sent on TLS update') + socket = net.connect({ + highWaterMark: 64 * 1024, // Same as nodejs fs streams. + ...options, + localAddress, + port: port || 80, + host: hostname + }) + } + + // Set TCP keep alive options on the socket here instead of in connect() for the case of assigning the socket + if (options.keepAlive == null || options.keepAlive) { + const keepAliveInitialDelay = options.keepAliveInitialDelay === undefined ? 60e3 : options.keepAliveInitialDelay + socket.setKeepAlive(true, keepAliveInitialDelay) + } + + const cancelTimeout = setupTimeout(() => onConnectTimeout(socket), timeout) + + socket + .setNoDelay(true) + .once(protocol === 'https:' ? 'secureConnect' : 'connect', function () { + cancelTimeout() + + if (callback) { + const cb = callback + callback = null + cb(null, this) + } + }) + .on('error', function (err) { + cancelTimeout() + + if (callback) { + const cb = callback + callback = null + cb(err) + } + }) + + return socket + } +} + +function setupTimeout (onConnectTimeout, timeout) { + if (!timeout) { + return () => {} + } + + let s1 = null + let s2 = null + const timeoutId = setTimeout(() => { + // setImmediate is added to make sure that we priotorise socket error events over timeouts + s1 = setImmediate(() => { + if (process.platform === 'win32') { + // Windows needs an extra setImmediate probably due to implementation differences in the socket logic + s2 = setImmediate(() => onConnectTimeout()) + } else { + onConnectTimeout() + } + }) + }, timeout) + return () => { + clearTimeout(timeoutId) + clearImmediate(s1) + clearImmediate(s2) + } +} + +function onConnectTimeout (socket) { + util.destroy(socket, new ConnectTimeoutError()) +} + +module.exports = buildConnector + + +/***/ }), + +/***/ 735: +/***/ ((module) => { + +"use strict"; + + +/** @type {Record} */ +const headerNameLowerCasedRecord = {} + +// https://developer.mozilla.org/docs/Web/HTTP/Headers +const wellknownHeaderNames = [ + 'Accept', + 'Accept-Encoding', + 'Accept-Language', + 'Accept-Ranges', + 'Access-Control-Allow-Credentials', + 'Access-Control-Allow-Headers', + 'Access-Control-Allow-Methods', + 'Access-Control-Allow-Origin', + 'Access-Control-Expose-Headers', + 'Access-Control-Max-Age', + 'Access-Control-Request-Headers', + 'Access-Control-Request-Method', + 'Age', + 'Allow', + 'Alt-Svc', + 'Alt-Used', + 'Authorization', + 'Cache-Control', + 'Clear-Site-Data', + 'Connection', + 'Content-Disposition', + 'Content-Encoding', + 'Content-Language', + 'Content-Length', + 'Content-Location', + 'Content-Range', + 'Content-Security-Policy', + 'Content-Security-Policy-Report-Only', + 'Content-Type', + 'Cookie', + 'Cross-Origin-Embedder-Policy', + 'Cross-Origin-Opener-Policy', + 'Cross-Origin-Resource-Policy', + 'Date', + 'Device-Memory', + 'Downlink', + 'ECT', + 'ETag', + 'Expect', + 'Expect-CT', + 'Expires', + 'Forwarded', + 'From', + 'Host', + 'If-Match', + 'If-Modified-Since', + 'If-None-Match', + 'If-Range', + 'If-Unmodified-Since', + 'Keep-Alive', + 'Last-Modified', + 'Link', + 'Location', + 'Max-Forwards', + 'Origin', + 'Permissions-Policy', + 'Pragma', + 'Proxy-Authenticate', + 'Proxy-Authorization', + 'RTT', + 'Range', + 'Referer', + 'Referrer-Policy', + 'Refresh', + 'Retry-After', + 'Sec-WebSocket-Accept', + 'Sec-WebSocket-Extensions', + 'Sec-WebSocket-Key', + 'Sec-WebSocket-Protocol', + 'Sec-WebSocket-Version', + 'Server', + 'Server-Timing', + 'Service-Worker-Allowed', + 'Service-Worker-Navigation-Preload', + 'Set-Cookie', + 'SourceMap', + 'Strict-Transport-Security', + 'Supports-Loading-Mode', + 'TE', + 'Timing-Allow-Origin', + 'Trailer', + 'Transfer-Encoding', + 'Upgrade', + 'Upgrade-Insecure-Requests', + 'User-Agent', + 'Vary', + 'Via', + 'WWW-Authenticate', + 'X-Content-Type-Options', + 'X-DNS-Prefetch-Control', + 'X-Frame-Options', + 'X-Permitted-Cross-Domain-Policies', + 'X-Powered-By', + 'X-Requested-With', + 'X-XSS-Protection' +] + +for (let i = 0; i < wellknownHeaderNames.length; ++i) { + const key = wellknownHeaderNames[i] + const lowerCasedKey = key.toLowerCase() + headerNameLowerCasedRecord[key] = headerNameLowerCasedRecord[lowerCasedKey] = + lowerCasedKey +} + +// Note: object prototypes should not be able to be referenced. e.g. `Object#hasOwnProperty`. +Object.setPrototypeOf(headerNameLowerCasedRecord, null) + +module.exports = { + wellknownHeaderNames, + headerNameLowerCasedRecord +} + + +/***/ }), + +/***/ 8707: +/***/ ((module) => { + +"use strict"; + + +class UndiciError extends Error { + constructor (message) { + super(message) + this.name = 'UndiciError' + this.code = 'UND_ERR' + } +} + +class ConnectTimeoutError extends UndiciError { + constructor (message) { + super(message) + Error.captureStackTrace(this, ConnectTimeoutError) + this.name = 'ConnectTimeoutError' + this.message = message || 'Connect Timeout Error' + this.code = 'UND_ERR_CONNECT_TIMEOUT' + } +} + +class HeadersTimeoutError extends UndiciError { + constructor (message) { + super(message) + Error.captureStackTrace(this, HeadersTimeoutError) + this.name = 'HeadersTimeoutError' + this.message = message || 'Headers Timeout Error' + this.code = 'UND_ERR_HEADERS_TIMEOUT' + } +} + +class HeadersOverflowError extends UndiciError { + constructor (message) { + super(message) + Error.captureStackTrace(this, HeadersOverflowError) + this.name = 'HeadersOverflowError' + this.message = message || 'Headers Overflow Error' + this.code = 'UND_ERR_HEADERS_OVERFLOW' + } +} + +class BodyTimeoutError extends UndiciError { + constructor (message) { + super(message) + Error.captureStackTrace(this, BodyTimeoutError) + this.name = 'BodyTimeoutError' + this.message = message || 'Body Timeout Error' + this.code = 'UND_ERR_BODY_TIMEOUT' + } +} + +class ResponseStatusCodeError extends UndiciError { + constructor (message, statusCode, headers, body) { + super(message) + Error.captureStackTrace(this, ResponseStatusCodeError) + this.name = 'ResponseStatusCodeError' + this.message = message || 'Response Status Code Error' + this.code = 'UND_ERR_RESPONSE_STATUS_CODE' + this.body = body + this.status = statusCode + this.statusCode = statusCode + this.headers = headers + } +} + +class InvalidArgumentError extends UndiciError { + constructor (message) { + super(message) + Error.captureStackTrace(this, InvalidArgumentError) + this.name = 'InvalidArgumentError' + this.message = message || 'Invalid Argument Error' + this.code = 'UND_ERR_INVALID_ARG' + } +} + +class InvalidReturnValueError extends UndiciError { + constructor (message) { + super(message) + Error.captureStackTrace(this, InvalidReturnValueError) + this.name = 'InvalidReturnValueError' + this.message = message || 'Invalid Return Value Error' + this.code = 'UND_ERR_INVALID_RETURN_VALUE' + } +} + +class RequestAbortedError extends UndiciError { + constructor (message) { + super(message) + Error.captureStackTrace(this, RequestAbortedError) + this.name = 'AbortError' + this.message = message || 'Request aborted' + this.code = 'UND_ERR_ABORTED' + } +} + +class InformationalError extends UndiciError { + constructor (message) { + super(message) + Error.captureStackTrace(this, InformationalError) + this.name = 'InformationalError' + this.message = message || 'Request information' + this.code = 'UND_ERR_INFO' + } +} + +class RequestContentLengthMismatchError extends UndiciError { + constructor (message) { + super(message) + Error.captureStackTrace(this, RequestContentLengthMismatchError) + this.name = 'RequestContentLengthMismatchError' + this.message = message || 'Request body length does not match content-length header' + this.code = 'UND_ERR_REQ_CONTENT_LENGTH_MISMATCH' + } +} + +class ResponseContentLengthMismatchError extends UndiciError { + constructor (message) { + super(message) + Error.captureStackTrace(this, ResponseContentLengthMismatchError) + this.name = 'ResponseContentLengthMismatchError' + this.message = message || 'Response body length does not match content-length header' + this.code = 'UND_ERR_RES_CONTENT_LENGTH_MISMATCH' + } +} + +class ClientDestroyedError extends UndiciError { + constructor (message) { + super(message) + Error.captureStackTrace(this, ClientDestroyedError) + this.name = 'ClientDestroyedError' + this.message = message || 'The client is destroyed' + this.code = 'UND_ERR_DESTROYED' + } +} + +class ClientClosedError extends UndiciError { + constructor (message) { + super(message) + Error.captureStackTrace(this, ClientClosedError) + this.name = 'ClientClosedError' + this.message = message || 'The client is closed' + this.code = 'UND_ERR_CLOSED' + } +} + +class SocketError extends UndiciError { + constructor (message, socket) { + super(message) + Error.captureStackTrace(this, SocketError) + this.name = 'SocketError' + this.message = message || 'Socket error' + this.code = 'UND_ERR_SOCKET' + this.socket = socket + } +} + +class NotSupportedError extends UndiciError { + constructor (message) { + super(message) + Error.captureStackTrace(this, NotSupportedError) + this.name = 'NotSupportedError' + this.message = message || 'Not supported error' + this.code = 'UND_ERR_NOT_SUPPORTED' + } +} + +class BalancedPoolMissingUpstreamError extends UndiciError { + constructor (message) { + super(message) + Error.captureStackTrace(this, NotSupportedError) + this.name = 'MissingUpstreamError' + this.message = message || 'No upstream has been added to the BalancedPool' + this.code = 'UND_ERR_BPL_MISSING_UPSTREAM' + } +} + +class HTTPParserError extends Error { + constructor (message, code, data) { + super(message) + Error.captureStackTrace(this, HTTPParserError) + this.name = 'HTTPParserError' + this.code = code ? `HPE_${code}` : undefined + this.data = data ? data.toString() : undefined + } +} + +class ResponseExceededMaxSizeError extends UndiciError { + constructor (message) { + super(message) + Error.captureStackTrace(this, ResponseExceededMaxSizeError) + this.name = 'ResponseExceededMaxSizeError' + this.message = message || 'Response content exceeded max size' + this.code = 'UND_ERR_RES_EXCEEDED_MAX_SIZE' + } +} + +class RequestRetryError extends UndiciError { + constructor (message, code, { headers, data }) { + super(message) + Error.captureStackTrace(this, RequestRetryError) + this.name = 'RequestRetryError' + this.message = message || 'Request retry error' + this.code = 'UND_ERR_REQ_RETRY' + this.statusCode = code + this.data = data + this.headers = headers + } +} + +module.exports = { + HTTPParserError, + UndiciError, + HeadersTimeoutError, + HeadersOverflowError, + BodyTimeoutError, + RequestContentLengthMismatchError, + ConnectTimeoutError, + ResponseStatusCodeError, + InvalidArgumentError, + InvalidReturnValueError, + RequestAbortedError, + ClientDestroyedError, + ClientClosedError, + InformationalError, + SocketError, + NotSupportedError, + ResponseContentLengthMismatchError, + BalancedPoolMissingUpstreamError, + ResponseExceededMaxSizeError, + RequestRetryError +} + + +/***/ }), + +/***/ 4655: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const { + InvalidArgumentError, + NotSupportedError +} = __nccwpck_require__(8707) +const assert = __nccwpck_require__(2613) +const { kHTTP2BuildRequest, kHTTP2CopyHeaders, kHTTP1BuildRequest } = __nccwpck_require__(6443) +const util = __nccwpck_require__(3440) + +// tokenRegExp and headerCharRegex have been lifted from +// https://github.com/nodejs/node/blob/main/lib/_http_common.js + +/** + * Verifies that the given val is a valid HTTP token + * per the rules defined in RFC 7230 + * See https://tools.ietf.org/html/rfc7230#section-3.2.6 + */ +const tokenRegExp = /^[\^_`a-zA-Z\-0-9!#$%&'*+.|~]+$/ + +/** + * Matches if val contains an invalid field-vchar + * field-value = *( field-content / obs-fold ) + * field-content = field-vchar [ 1*( SP / HTAB ) field-vchar ] + * field-vchar = VCHAR / obs-text + */ +const headerCharRegex = /[^\t\x20-\x7e\x80-\xff]/ + +// Verifies that a given path is valid does not contain control chars \x00 to \x20 +const invalidPathRegex = /[^\u0021-\u00ff]/ + +const kHandler = Symbol('handler') + +const channels = {} + +let extractBody + +try { + const diagnosticsChannel = __nccwpck_require__(1637) + channels.create = diagnosticsChannel.channel('undici:request:create') + channels.bodySent = diagnosticsChannel.channel('undici:request:bodySent') + channels.headers = diagnosticsChannel.channel('undici:request:headers') + channels.trailers = diagnosticsChannel.channel('undici:request:trailers') + channels.error = diagnosticsChannel.channel('undici:request:error') +} catch { + channels.create = { hasSubscribers: false } + channels.bodySent = { hasSubscribers: false } + channels.headers = { hasSubscribers: false } + channels.trailers = { hasSubscribers: false } + channels.error = { hasSubscribers: false } +} + +class Request { + constructor (origin, { + path, + method, + body, + headers, + query, + idempotent, + blocking, + upgrade, + headersTimeout, + bodyTimeout, + reset, + throwOnError, + expectContinue + }, handler) { + if (typeof path !== 'string') { + throw new InvalidArgumentError('path must be a string') + } else if ( + path[0] !== '/' && + !(path.startsWith('http://') || path.startsWith('https://')) && + method !== 'CONNECT' + ) { + throw new InvalidArgumentError('path must be an absolute URL or start with a slash') + } else if (invalidPathRegex.exec(path) !== null) { + throw new InvalidArgumentError('invalid request path') + } + + if (typeof method !== 'string') { + throw new InvalidArgumentError('method must be a string') + } else if (tokenRegExp.exec(method) === null) { + throw new InvalidArgumentError('invalid request method') + } + + if (upgrade && typeof upgrade !== 'string') { + throw new InvalidArgumentError('upgrade must be a string') + } + + if (headersTimeout != null && (!Number.isFinite(headersTimeout) || headersTimeout < 0)) { + throw new InvalidArgumentError('invalid headersTimeout') + } + + if (bodyTimeout != null && (!Number.isFinite(bodyTimeout) || bodyTimeout < 0)) { + throw new InvalidArgumentError('invalid bodyTimeout') + } + + if (reset != null && typeof reset !== 'boolean') { + throw new InvalidArgumentError('invalid reset') + } + + if (expectContinue != null && typeof expectContinue !== 'boolean') { + throw new InvalidArgumentError('invalid expectContinue') + } + + this.headersTimeout = headersTimeout + + this.bodyTimeout = bodyTimeout + + this.throwOnError = throwOnError === true + + this.method = method + + this.abort = null + + if (body == null) { + this.body = null + } else if (util.isStream(body)) { + this.body = body + + const rState = this.body._readableState + if (!rState || !rState.autoDestroy) { + this.endHandler = function autoDestroy () { + util.destroy(this) + } + this.body.on('end', this.endHandler) + } + + this.errorHandler = err => { + if (this.abort) { + this.abort(err) + } else { + this.error = err + } + } + this.body.on('error', this.errorHandler) + } else if (util.isBuffer(body)) { + this.body = body.byteLength ? body : null + } else if (ArrayBuffer.isView(body)) { + this.body = body.buffer.byteLength ? Buffer.from(body.buffer, body.byteOffset, body.byteLength) : null + } else if (body instanceof ArrayBuffer) { + this.body = body.byteLength ? Buffer.from(body) : null + } else if (typeof body === 'string') { + this.body = body.length ? Buffer.from(body) : null + } else if (util.isFormDataLike(body) || util.isIterable(body) || util.isBlobLike(body)) { + this.body = body + } else { + throw new InvalidArgumentError('body must be a string, a Buffer, a Readable stream, an iterable, or an async iterable') + } + + this.completed = false + + this.aborted = false + + this.upgrade = upgrade || null + + this.path = query ? util.buildURL(path, query) : path + + this.origin = origin + + this.idempotent = idempotent == null + ? method === 'HEAD' || method === 'GET' + : idempotent + + this.blocking = blocking == null ? false : blocking + + this.reset = reset == null ? null : reset + + this.host = null + + this.contentLength = null + + this.contentType = null + + this.headers = '' + + // Only for H2 + this.expectContinue = expectContinue != null ? expectContinue : false + + if (Array.isArray(headers)) { + if (headers.length % 2 !== 0) { + throw new InvalidArgumentError('headers array must be even') + } + for (let i = 0; i < headers.length; i += 2) { + processHeader(this, headers[i], headers[i + 1]) + } + } else if (headers && typeof headers === 'object') { + const keys = Object.keys(headers) + for (let i = 0; i < keys.length; i++) { + const key = keys[i] + processHeader(this, key, headers[key]) + } + } else if (headers != null) { + throw new InvalidArgumentError('headers must be an object or an array') + } + + if (util.isFormDataLike(this.body)) { + if (util.nodeMajor < 16 || (util.nodeMajor === 16 && util.nodeMinor < 8)) { + throw new InvalidArgumentError('Form-Data bodies are only supported in node v16.8 and newer.') + } + + if (!extractBody) { + extractBody = (__nccwpck_require__(8923).extractBody) + } + + const [bodyStream, contentType] = extractBody(body) + if (this.contentType == null) { + this.contentType = contentType + this.headers += `content-type: ${contentType}\r\n` + } + this.body = bodyStream.stream + this.contentLength = bodyStream.length + } else if (util.isBlobLike(body) && this.contentType == null && body.type) { + this.contentType = body.type + this.headers += `content-type: ${body.type}\r\n` + } + + util.validateHandler(handler, method, upgrade) + + this.servername = util.getServerName(this.host) + + this[kHandler] = handler + + if (channels.create.hasSubscribers) { + channels.create.publish({ request: this }) + } + } + + onBodySent (chunk) { + if (this[kHandler].onBodySent) { + try { + return this[kHandler].onBodySent(chunk) + } catch (err) { + this.abort(err) + } + } + } + + onRequestSent () { + if (channels.bodySent.hasSubscribers) { + channels.bodySent.publish({ request: this }) + } + + if (this[kHandler].onRequestSent) { + try { + return this[kHandler].onRequestSent() + } catch (err) { + this.abort(err) + } + } + } + + onConnect (abort) { + assert(!this.aborted) + assert(!this.completed) + + if (this.error) { + abort(this.error) + } else { + this.abort = abort + return this[kHandler].onConnect(abort) + } + } + + onHeaders (statusCode, headers, resume, statusText) { + assert(!this.aborted) + assert(!this.completed) + + if (channels.headers.hasSubscribers) { + channels.headers.publish({ request: this, response: { statusCode, headers, statusText } }) + } + + try { + return this[kHandler].onHeaders(statusCode, headers, resume, statusText) + } catch (err) { + this.abort(err) + } + } + + onData (chunk) { + assert(!this.aborted) + assert(!this.completed) + + try { + return this[kHandler].onData(chunk) + } catch (err) { + this.abort(err) + return false + } + } + + onUpgrade (statusCode, headers, socket) { + assert(!this.aborted) + assert(!this.completed) + + return this[kHandler].onUpgrade(statusCode, headers, socket) + } + + onComplete (trailers) { + this.onFinally() + + assert(!this.aborted) + + this.completed = true + if (channels.trailers.hasSubscribers) { + channels.trailers.publish({ request: this, trailers }) + } + + try { + return this[kHandler].onComplete(trailers) + } catch (err) { + // TODO (fix): This might be a bad idea? + this.onError(err) + } + } + + onError (error) { + this.onFinally() + + if (channels.error.hasSubscribers) { + channels.error.publish({ request: this, error }) + } + + if (this.aborted) { + return + } + this.aborted = true + + return this[kHandler].onError(error) + } + + onFinally () { + if (this.errorHandler) { + this.body.off('error', this.errorHandler) + this.errorHandler = null + } + + if (this.endHandler) { + this.body.off('end', this.endHandler) + this.endHandler = null + } + } + + // TODO: adjust to support H2 + addHeader (key, value) { + processHeader(this, key, value) + return this + } + + static [kHTTP1BuildRequest] (origin, opts, handler) { + // TODO: Migrate header parsing here, to make Requests + // HTTP agnostic + return new Request(origin, opts, handler) + } + + static [kHTTP2BuildRequest] (origin, opts, handler) { + const headers = opts.headers + opts = { ...opts, headers: null } + + const request = new Request(origin, opts, handler) + + request.headers = {} + + if (Array.isArray(headers)) { + if (headers.length % 2 !== 0) { + throw new InvalidArgumentError('headers array must be even') + } + for (let i = 0; i < headers.length; i += 2) { + processHeader(request, headers[i], headers[i + 1], true) + } + } else if (headers && typeof headers === 'object') { + const keys = Object.keys(headers) + for (let i = 0; i < keys.length; i++) { + const key = keys[i] + processHeader(request, key, headers[key], true) + } + } else if (headers != null) { + throw new InvalidArgumentError('headers must be an object or an array') + } + + return request + } + + static [kHTTP2CopyHeaders] (raw) { + const rawHeaders = raw.split('\r\n') + const headers = {} + + for (const header of rawHeaders) { + const [key, value] = header.split(': ') + + if (value == null || value.length === 0) continue + + if (headers[key]) headers[key] += `,${value}` + else headers[key] = value + } + + return headers + } +} + +function processHeaderValue (key, val, skipAppend) { + if (val && typeof val === 'object') { + throw new InvalidArgumentError(`invalid ${key} header`) + } + + val = val != null ? `${val}` : '' + + if (headerCharRegex.exec(val) !== null) { + throw new InvalidArgumentError(`invalid ${key} header`) + } + + return skipAppend ? val : `${key}: ${val}\r\n` +} + +function processHeader (request, key, val, skipAppend = false) { + if (val && (typeof val === 'object' && !Array.isArray(val))) { + throw new InvalidArgumentError(`invalid ${key} header`) + } else if (val === undefined) { + return + } + + if ( + request.host === null && + key.length === 4 && + key.toLowerCase() === 'host' + ) { + if (headerCharRegex.exec(val) !== null) { + throw new InvalidArgumentError(`invalid ${key} header`) + } + // Consumed by Client + request.host = val + } else if ( + request.contentLength === null && + key.length === 14 && + key.toLowerCase() === 'content-length' + ) { + request.contentLength = parseInt(val, 10) + if (!Number.isFinite(request.contentLength)) { + throw new InvalidArgumentError('invalid content-length header') + } + } else if ( + request.contentType === null && + key.length === 12 && + key.toLowerCase() === 'content-type' + ) { + request.contentType = val + if (skipAppend) request.headers[key] = processHeaderValue(key, val, skipAppend) + else request.headers += processHeaderValue(key, val) + } else if ( + key.length === 17 && + key.toLowerCase() === 'transfer-encoding' + ) { + throw new InvalidArgumentError('invalid transfer-encoding header') + } else if ( + key.length === 10 && + key.toLowerCase() === 'connection' + ) { + const value = typeof val === 'string' ? val.toLowerCase() : null + if (value !== 'close' && value !== 'keep-alive') { + throw new InvalidArgumentError('invalid connection header') + } else if (value === 'close') { + request.reset = true + } + } else if ( + key.length === 10 && + key.toLowerCase() === 'keep-alive' + ) { + throw new InvalidArgumentError('invalid keep-alive header') + } else if ( + key.length === 7 && + key.toLowerCase() === 'upgrade' + ) { + throw new InvalidArgumentError('invalid upgrade header') + } else if ( + key.length === 6 && + key.toLowerCase() === 'expect' + ) { + throw new NotSupportedError('expect header not supported') + } else if (tokenRegExp.exec(key) === null) { + throw new InvalidArgumentError('invalid header key') + } else { + if (Array.isArray(val)) { + for (let i = 0; i < val.length; i++) { + if (skipAppend) { + if (request.headers[key]) request.headers[key] += `,${processHeaderValue(key, val[i], skipAppend)}` + else request.headers[key] = processHeaderValue(key, val[i], skipAppend) + } else { + request.headers += processHeaderValue(key, val[i]) + } + } + } else { + if (skipAppend) request.headers[key] = processHeaderValue(key, val, skipAppend) + else request.headers += processHeaderValue(key, val) + } + } +} + +module.exports = Request + + +/***/ }), + +/***/ 6443: +/***/ ((module) => { + +module.exports = { + kClose: Symbol('close'), + kDestroy: Symbol('destroy'), + kDispatch: Symbol('dispatch'), + kUrl: Symbol('url'), + kWriting: Symbol('writing'), + kResuming: Symbol('resuming'), + kQueue: Symbol('queue'), + kConnect: Symbol('connect'), + kConnecting: Symbol('connecting'), + kHeadersList: Symbol('headers list'), + kKeepAliveDefaultTimeout: Symbol('default keep alive timeout'), + kKeepAliveMaxTimeout: Symbol('max keep alive timeout'), + kKeepAliveTimeoutThreshold: Symbol('keep alive timeout threshold'), + kKeepAliveTimeoutValue: Symbol('keep alive timeout'), + kKeepAlive: Symbol('keep alive'), + kHeadersTimeout: Symbol('headers timeout'), + kBodyTimeout: Symbol('body timeout'), + kServerName: Symbol('server name'), + kLocalAddress: Symbol('local address'), + kHost: Symbol('host'), + kNoRef: Symbol('no ref'), + kBodyUsed: Symbol('used'), + kRunning: Symbol('running'), + kBlocking: Symbol('blocking'), + kPending: Symbol('pending'), + kSize: Symbol('size'), + kBusy: Symbol('busy'), + kQueued: Symbol('queued'), + kFree: Symbol('free'), + kConnected: Symbol('connected'), + kClosed: Symbol('closed'), + kNeedDrain: Symbol('need drain'), + kReset: Symbol('reset'), + kDestroyed: Symbol.for('nodejs.stream.destroyed'), + kMaxHeadersSize: Symbol('max headers size'), + kRunningIdx: Symbol('running index'), + kPendingIdx: Symbol('pending index'), + kError: Symbol('error'), + kClients: Symbol('clients'), + kClient: Symbol('client'), + kParser: Symbol('parser'), + kOnDestroyed: Symbol('destroy callbacks'), + kPipelining: Symbol('pipelining'), + kSocket: Symbol('socket'), + kHostHeader: Symbol('host header'), + kConnector: Symbol('connector'), + kStrictContentLength: Symbol('strict content length'), + kMaxRedirections: Symbol('maxRedirections'), + kMaxRequests: Symbol('maxRequestsPerClient'), + kProxy: Symbol('proxy agent options'), + kCounter: Symbol('socket request counter'), + kInterceptors: Symbol('dispatch interceptors'), + kMaxResponseSize: Symbol('max response size'), + kHTTP2Session: Symbol('http2Session'), + kHTTP2SessionState: Symbol('http2Session state'), + kHTTP2BuildRequest: Symbol('http2 build request'), + kHTTP1BuildRequest: Symbol('http1 build request'), + kHTTP2CopyHeaders: Symbol('http2 copy headers'), + kHTTPConnVersion: Symbol('http connection version'), + kRetryHandlerDefaultRetry: Symbol('retry agent default retry'), + kConstruct: Symbol('constructable') +} + + +/***/ }), + +/***/ 3440: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const assert = __nccwpck_require__(2613) +const { kDestroyed, kBodyUsed } = __nccwpck_require__(6443) +const { IncomingMessage } = __nccwpck_require__(8611) +const stream = __nccwpck_require__(2203) +const net = __nccwpck_require__(9278) +const { InvalidArgumentError } = __nccwpck_require__(8707) +const { Blob } = __nccwpck_require__(181) +const nodeUtil = __nccwpck_require__(9023) +const { stringify } = __nccwpck_require__(3480) +const { headerNameLowerCasedRecord } = __nccwpck_require__(735) + +const [nodeMajor, nodeMinor] = process.versions.node.split('.').map(v => Number(v)) + +function nop () {} + +function isStream (obj) { + return obj && typeof obj === 'object' && typeof obj.pipe === 'function' && typeof obj.on === 'function' +} + +// based on https://github.com/node-fetch/fetch-blob/blob/8ab587d34080de94140b54f07168451e7d0b655e/index.js#L229-L241 (MIT License) +function isBlobLike (object) { + return (Blob && object instanceof Blob) || ( + object && + typeof object === 'object' && + (typeof object.stream === 'function' || + typeof object.arrayBuffer === 'function') && + /^(Blob|File)$/.test(object[Symbol.toStringTag]) + ) +} + +function buildURL (url, queryParams) { + if (url.includes('?') || url.includes('#')) { + throw new Error('Query params cannot be passed when url already contains "?" or "#".') + } + + const stringified = stringify(queryParams) + + if (stringified) { + url += '?' + stringified + } + + return url +} + +function parseURL (url) { + if (typeof url === 'string') { + url = new URL(url) + + if (!/^https?:/.test(url.origin || url.protocol)) { + throw new InvalidArgumentError('Invalid URL protocol: the URL must start with `http:` or `https:`.') + } + + return url + } + + if (!url || typeof url !== 'object') { + throw new InvalidArgumentError('Invalid URL: The URL argument must be a non-null object.') + } + + if (!/^https?:/.test(url.origin || url.protocol)) { + throw new InvalidArgumentError('Invalid URL protocol: the URL must start with `http:` or `https:`.') + } + + if (!(url instanceof URL)) { + if (url.port != null && url.port !== '' && !Number.isFinite(parseInt(url.port))) { + throw new InvalidArgumentError('Invalid URL: port must be a valid integer or a string representation of an integer.') + } + + if (url.path != null && typeof url.path !== 'string') { + throw new InvalidArgumentError('Invalid URL path: the path must be a string or null/undefined.') + } + + if (url.pathname != null && typeof url.pathname !== 'string') { + throw new InvalidArgumentError('Invalid URL pathname: the pathname must be a string or null/undefined.') + } + + if (url.hostname != null && typeof url.hostname !== 'string') { + throw new InvalidArgumentError('Invalid URL hostname: the hostname must be a string or null/undefined.') + } + + if (url.origin != null && typeof url.origin !== 'string') { + throw new InvalidArgumentError('Invalid URL origin: the origin must be a string or null/undefined.') + } + + const port = url.port != null + ? url.port + : (url.protocol === 'https:' ? 443 : 80) + let origin = url.origin != null + ? url.origin + : `${url.protocol}//${url.hostname}:${port}` + let path = url.path != null + ? url.path + : `${url.pathname || ''}${url.search || ''}` + + if (origin.endsWith('/')) { + origin = origin.substring(0, origin.length - 1) + } + + if (path && !path.startsWith('/')) { + path = `/${path}` + } + // new URL(path, origin) is unsafe when `path` contains an absolute URL + // From https://developer.mozilla.org/en-US/docs/Web/API/URL/URL: + // If first parameter is a relative URL, second param is required, and will be used as the base URL. + // If first parameter is an absolute URL, a given second param will be ignored. + url = new URL(origin + path) + } + + return url +} + +function parseOrigin (url) { + url = parseURL(url) + + if (url.pathname !== '/' || url.search || url.hash) { + throw new InvalidArgumentError('invalid url') + } + + return url +} + +function getHostname (host) { + if (host[0] === '[') { + const idx = host.indexOf(']') + + assert(idx !== -1) + return host.substring(1, idx) + } + + const idx = host.indexOf(':') + if (idx === -1) return host + + return host.substring(0, idx) +} + +// IP addresses are not valid server names per RFC6066 +// > Currently, the only server names supported are DNS hostnames +function getServerName (host) { + if (!host) { + return null + } + + assert.strictEqual(typeof host, 'string') + + const servername = getHostname(host) + if (net.isIP(servername)) { + return '' + } + + return servername +} + +function deepClone (obj) { + return JSON.parse(JSON.stringify(obj)) +} + +function isAsyncIterable (obj) { + return !!(obj != null && typeof obj[Symbol.asyncIterator] === 'function') +} + +function isIterable (obj) { + return !!(obj != null && (typeof obj[Symbol.iterator] === 'function' || typeof obj[Symbol.asyncIterator] === 'function')) +} + +function bodyLength (body) { + if (body == null) { + return 0 + } else if (isStream(body)) { + const state = body._readableState + return state && state.objectMode === false && state.ended === true && Number.isFinite(state.length) + ? state.length + : null + } else if (isBlobLike(body)) { + return body.size != null ? body.size : null + } else if (isBuffer(body)) { + return body.byteLength + } + + return null +} + +function isDestroyed (stream) { + return !stream || !!(stream.destroyed || stream[kDestroyed]) +} + +function isReadableAborted (stream) { + const state = stream && stream._readableState + return isDestroyed(stream) && state && !state.endEmitted +} + +function destroy (stream, err) { + if (stream == null || !isStream(stream) || isDestroyed(stream)) { + return + } + + if (typeof stream.destroy === 'function') { + if (Object.getPrototypeOf(stream).constructor === IncomingMessage) { + // See: https://github.com/nodejs/node/pull/38505/files + stream.socket = null + } + + stream.destroy(err) + } else if (err) { + process.nextTick((stream, err) => { + stream.emit('error', err) + }, stream, err) + } + + if (stream.destroyed !== true) { + stream[kDestroyed] = true + } +} + +const KEEPALIVE_TIMEOUT_EXPR = /timeout=(\d+)/ +function parseKeepAliveTimeout (val) { + const m = val.toString().match(KEEPALIVE_TIMEOUT_EXPR) + return m ? parseInt(m[1], 10) * 1000 : null +} + +/** + * Retrieves a header name and returns its lowercase value. + * @param {string | Buffer} value Header name + * @returns {string} + */ +function headerNameToString (value) { + return headerNameLowerCasedRecord[value] || value.toLowerCase() +} + +function parseHeaders (headers, obj = {}) { + // For H2 support + if (!Array.isArray(headers)) return headers + + for (let i = 0; i < headers.length; i += 2) { + const key = headers[i].toString().toLowerCase() + let val = obj[key] + + if (!val) { + if (Array.isArray(headers[i + 1])) { + obj[key] = headers[i + 1].map(x => x.toString('utf8')) + } else { + obj[key] = headers[i + 1].toString('utf8') + } + } else { + if (!Array.isArray(val)) { + val = [val] + obj[key] = val + } + val.push(headers[i + 1].toString('utf8')) + } + } + + // See https://github.com/nodejs/node/pull/46528 + if ('content-length' in obj && 'content-disposition' in obj) { + obj['content-disposition'] = Buffer.from(obj['content-disposition']).toString('latin1') + } + + return obj +} + +function parseRawHeaders (headers) { + const ret = [] + let hasContentLength = false + let contentDispositionIdx = -1 + + for (let n = 0; n < headers.length; n += 2) { + const key = headers[n + 0].toString() + const val = headers[n + 1].toString('utf8') + + if (key.length === 14 && (key === 'content-length' || key.toLowerCase() === 'content-length')) { + ret.push(key, val) + hasContentLength = true + } else if (key.length === 19 && (key === 'content-disposition' || key.toLowerCase() === 'content-disposition')) { + contentDispositionIdx = ret.push(key, val) - 1 + } else { + ret.push(key, val) + } + } + + // See https://github.com/nodejs/node/pull/46528 + if (hasContentLength && contentDispositionIdx !== -1) { + ret[contentDispositionIdx] = Buffer.from(ret[contentDispositionIdx]).toString('latin1') + } + + return ret +} + +function isBuffer (buffer) { + // See, https://github.com/mcollina/undici/pull/319 + return buffer instanceof Uint8Array || Buffer.isBuffer(buffer) +} + +function validateHandler (handler, method, upgrade) { + if (!handler || typeof handler !== 'object') { + throw new InvalidArgumentError('handler must be an object') + } + + if (typeof handler.onConnect !== 'function') { + throw new InvalidArgumentError('invalid onConnect method') + } + + if (typeof handler.onError !== 'function') { + throw new InvalidArgumentError('invalid onError method') + } + + if (typeof handler.onBodySent !== 'function' && handler.onBodySent !== undefined) { + throw new InvalidArgumentError('invalid onBodySent method') + } + + if (upgrade || method === 'CONNECT') { + if (typeof handler.onUpgrade !== 'function') { + throw new InvalidArgumentError('invalid onUpgrade method') + } + } else { + if (typeof handler.onHeaders !== 'function') { + throw new InvalidArgumentError('invalid onHeaders method') + } + + if (typeof handler.onData !== 'function') { + throw new InvalidArgumentError('invalid onData method') + } + + if (typeof handler.onComplete !== 'function') { + throw new InvalidArgumentError('invalid onComplete method') + } + } +} + +// A body is disturbed if it has been read from and it cannot +// be re-used without losing state or data. +function isDisturbed (body) { + return !!(body && ( + stream.isDisturbed + ? stream.isDisturbed(body) || body[kBodyUsed] // TODO (fix): Why is body[kBodyUsed] needed? + : body[kBodyUsed] || + body.readableDidRead || + (body._readableState && body._readableState.dataEmitted) || + isReadableAborted(body) + )) +} + +function isErrored (body) { + return !!(body && ( + stream.isErrored + ? stream.isErrored(body) + : /state: 'errored'/.test(nodeUtil.inspect(body) + ))) +} + +function isReadable (body) { + return !!(body && ( + stream.isReadable + ? stream.isReadable(body) + : /state: 'readable'/.test(nodeUtil.inspect(body) + ))) +} + +function getSocketInfo (socket) { + return { + localAddress: socket.localAddress, + localPort: socket.localPort, + remoteAddress: socket.remoteAddress, + remotePort: socket.remotePort, + remoteFamily: socket.remoteFamily, + timeout: socket.timeout, + bytesWritten: socket.bytesWritten, + bytesRead: socket.bytesRead + } +} + +async function * convertIterableToBuffer (iterable) { + for await (const chunk of iterable) { + yield Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk) + } +} + +let ReadableStream +function ReadableStreamFrom (iterable) { + if (!ReadableStream) { + ReadableStream = (__nccwpck_require__(3774).ReadableStream) + } + + if (ReadableStream.from) { + return ReadableStream.from(convertIterableToBuffer(iterable)) + } + + let iterator + return new ReadableStream( + { + async start () { + iterator = iterable[Symbol.asyncIterator]() + }, + async pull (controller) { + const { done, value } = await iterator.next() + if (done) { + queueMicrotask(() => { + controller.close() + }) + } else { + const buf = Buffer.isBuffer(value) ? value : Buffer.from(value) + controller.enqueue(new Uint8Array(buf)) + } + return controller.desiredSize > 0 + }, + async cancel (reason) { + await iterator.return() + } + }, + 0 + ) +} + +// The chunk should be a FormData instance and contains +// all the required methods. +function isFormDataLike (object) { + return ( + object && + typeof object === 'object' && + typeof object.append === 'function' && + typeof object.delete === 'function' && + typeof object.get === 'function' && + typeof object.getAll === 'function' && + typeof object.has === 'function' && + typeof object.set === 'function' && + object[Symbol.toStringTag] === 'FormData' + ) +} + +function throwIfAborted (signal) { + if (!signal) { return } + if (typeof signal.throwIfAborted === 'function') { + signal.throwIfAborted() + } else { + if (signal.aborted) { + // DOMException not available < v17.0.0 + const err = new Error('The operation was aborted') + err.name = 'AbortError' + throw err + } + } +} + +function addAbortListener (signal, listener) { + if ('addEventListener' in signal) { + signal.addEventListener('abort', listener, { once: true }) + return () => signal.removeEventListener('abort', listener) + } + signal.addListener('abort', listener) + return () => signal.removeListener('abort', listener) +} + +const hasToWellFormed = !!String.prototype.toWellFormed + +/** + * @param {string} val + */ +function toUSVString (val) { + if (hasToWellFormed) { + return `${val}`.toWellFormed() + } else if (nodeUtil.toUSVString) { + return nodeUtil.toUSVString(val) + } + + return `${val}` +} + +// Parsed accordingly to RFC 9110 +// https://www.rfc-editor.org/rfc/rfc9110#field.content-range +function parseRangeHeader (range) { + if (range == null || range === '') return { start: 0, end: null, size: null } + + const m = range ? range.match(/^bytes (\d+)-(\d+)\/(\d+)?$/) : null + return m + ? { + start: parseInt(m[1]), + end: m[2] ? parseInt(m[2]) : null, + size: m[3] ? parseInt(m[3]) : null + } + : null +} + +const kEnumerableProperty = Object.create(null) +kEnumerableProperty.enumerable = true + +module.exports = { + kEnumerableProperty, + nop, + isDisturbed, + isErrored, + isReadable, + toUSVString, + isReadableAborted, + isBlobLike, + parseOrigin, + parseURL, + getServerName, + isStream, + isIterable, + isAsyncIterable, + isDestroyed, + headerNameToString, + parseRawHeaders, + parseHeaders, + parseKeepAliveTimeout, + destroy, + bodyLength, + deepClone, + ReadableStreamFrom, + isBuffer, + validateHandler, + getSocketInfo, + isFormDataLike, + buildURL, + throwIfAborted, + addAbortListener, + parseRangeHeader, + nodeMajor, + nodeMinor, + nodeHasAutoSelectFamily: nodeMajor > 18 || (nodeMajor === 18 && nodeMinor >= 13), + safeHTTPMethods: ['GET', 'HEAD', 'OPTIONS', 'TRACE'] +} + + +/***/ }), + +/***/ 1: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const Dispatcher = __nccwpck_require__(992) +const { + ClientDestroyedError, + ClientClosedError, + InvalidArgumentError +} = __nccwpck_require__(8707) +const { kDestroy, kClose, kDispatch, kInterceptors } = __nccwpck_require__(6443) + +const kDestroyed = Symbol('destroyed') +const kClosed = Symbol('closed') +const kOnDestroyed = Symbol('onDestroyed') +const kOnClosed = Symbol('onClosed') +const kInterceptedDispatch = Symbol('Intercepted Dispatch') + +class DispatcherBase extends Dispatcher { + constructor () { + super() + + this[kDestroyed] = false + this[kOnDestroyed] = null + this[kClosed] = false + this[kOnClosed] = [] + } + + get destroyed () { + return this[kDestroyed] + } + + get closed () { + return this[kClosed] + } + + get interceptors () { + return this[kInterceptors] + } + + set interceptors (newInterceptors) { + if (newInterceptors) { + for (let i = newInterceptors.length - 1; i >= 0; i--) { + const interceptor = this[kInterceptors][i] + if (typeof interceptor !== 'function') { + throw new InvalidArgumentError('interceptor must be an function') + } + } + } + + this[kInterceptors] = newInterceptors + } + + close (callback) { + if (callback === undefined) { + return new Promise((resolve, reject) => { + this.close((err, data) => { + return err ? reject(err) : resolve(data) + }) + }) + } + + if (typeof callback !== 'function') { + throw new InvalidArgumentError('invalid callback') + } + + if (this[kDestroyed]) { + queueMicrotask(() => callback(new ClientDestroyedError(), null)) + return + } + + if (this[kClosed]) { + if (this[kOnClosed]) { + this[kOnClosed].push(callback) + } else { + queueMicrotask(() => callback(null, null)) + } + return + } + + this[kClosed] = true + this[kOnClosed].push(callback) + + const onClosed = () => { + const callbacks = this[kOnClosed] + this[kOnClosed] = null + for (let i = 0; i < callbacks.length; i++) { + callbacks[i](null, null) + } + } + + // Should not error. + this[kClose]() + .then(() => this.destroy()) + .then(() => { + queueMicrotask(onClosed) + }) + } + + destroy (err, callback) { + if (typeof err === 'function') { + callback = err + err = null + } + + if (callback === undefined) { + return new Promise((resolve, reject) => { + this.destroy(err, (err, data) => { + return err ? /* istanbul ignore next: should never error */ reject(err) : resolve(data) + }) + }) + } + + if (typeof callback !== 'function') { + throw new InvalidArgumentError('invalid callback') + } + + if (this[kDestroyed]) { + if (this[kOnDestroyed]) { + this[kOnDestroyed].push(callback) + } else { + queueMicrotask(() => callback(null, null)) + } + return + } + + if (!err) { + err = new ClientDestroyedError() + } + + this[kDestroyed] = true + this[kOnDestroyed] = this[kOnDestroyed] || [] + this[kOnDestroyed].push(callback) + + const onDestroyed = () => { + const callbacks = this[kOnDestroyed] + this[kOnDestroyed] = null + for (let i = 0; i < callbacks.length; i++) { + callbacks[i](null, null) + } + } + + // Should not error. + this[kDestroy](err).then(() => { + queueMicrotask(onDestroyed) + }) + } + + [kInterceptedDispatch] (opts, handler) { + if (!this[kInterceptors] || this[kInterceptors].length === 0) { + this[kInterceptedDispatch] = this[kDispatch] + return this[kDispatch](opts, handler) + } + + let dispatch = this[kDispatch].bind(this) + for (let i = this[kInterceptors].length - 1; i >= 0; i--) { + dispatch = this[kInterceptors][i](dispatch) + } + this[kInterceptedDispatch] = dispatch + return dispatch(opts, handler) + } + + dispatch (opts, handler) { + if (!handler || typeof handler !== 'object') { + throw new InvalidArgumentError('handler must be an object') + } + + try { + if (!opts || typeof opts !== 'object') { + throw new InvalidArgumentError('opts must be an object.') + } + + if (this[kDestroyed] || this[kOnDestroyed]) { + throw new ClientDestroyedError() + } + + if (this[kClosed]) { + throw new ClientClosedError() + } + + return this[kInterceptedDispatch](opts, handler) + } catch (err) { + if (typeof handler.onError !== 'function') { + throw new InvalidArgumentError('invalid onError method') + } + + handler.onError(err) + + return false + } + } +} + +module.exports = DispatcherBase + + +/***/ }), + +/***/ 992: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const EventEmitter = __nccwpck_require__(4434) + +class Dispatcher extends EventEmitter { + dispatch () { + throw new Error('not implemented') + } + + close () { + throw new Error('not implemented') + } + + destroy () { + throw new Error('not implemented') + } +} + +module.exports = Dispatcher + + +/***/ }), + +/***/ 8923: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const Busboy = __nccwpck_require__(9581) +const util = __nccwpck_require__(3440) +const { + ReadableStreamFrom, + isBlobLike, + isReadableStreamLike, + readableStreamClose, + createDeferredPromise, + fullyReadBody +} = __nccwpck_require__(5523) +const { FormData } = __nccwpck_require__(3073) +const { kState } = __nccwpck_require__(9710) +const { webidl } = __nccwpck_require__(4222) +const { DOMException, structuredClone } = __nccwpck_require__(7326) +const { Blob, File: NativeFile } = __nccwpck_require__(181) +const { kBodyUsed } = __nccwpck_require__(6443) +const assert = __nccwpck_require__(2613) +const { isErrored } = __nccwpck_require__(3440) +const { isUint8Array, isArrayBuffer } = __nccwpck_require__(8253) +const { File: UndiciFile } = __nccwpck_require__(3041) +const { parseMIMEType, serializeAMimeType } = __nccwpck_require__(4322) + +let ReadableStream = globalThis.ReadableStream + +/** @type {globalThis['File']} */ +const File = NativeFile ?? UndiciFile +const textEncoder = new TextEncoder() +const textDecoder = new TextDecoder() + +// https://fetch.spec.whatwg.org/#concept-bodyinit-extract +function extractBody (object, keepalive = false) { + if (!ReadableStream) { + ReadableStream = (__nccwpck_require__(3774).ReadableStream) + } + + // 1. Let stream be null. + let stream = null + + // 2. If object is a ReadableStream object, then set stream to object. + if (object instanceof ReadableStream) { + stream = object + } else if (isBlobLike(object)) { + // 3. Otherwise, if object is a Blob object, set stream to the + // result of running object’s get stream. + stream = object.stream() + } else { + // 4. Otherwise, set stream to a new ReadableStream object, and set + // up stream. + stream = new ReadableStream({ + async pull (controller) { + controller.enqueue( + typeof source === 'string' ? textEncoder.encode(source) : source + ) + queueMicrotask(() => readableStreamClose(controller)) + }, + start () {}, + type: undefined + }) + } + + // 5. Assert: stream is a ReadableStream object. + assert(isReadableStreamLike(stream)) + + // 6. Let action be null. + let action = null + + // 7. Let source be null. + let source = null + + // 8. Let length be null. + let length = null + + // 9. Let type be null. + let type = null + + // 10. Switch on object: + if (typeof object === 'string') { + // Set source to the UTF-8 encoding of object. + // Note: setting source to a Uint8Array here breaks some mocking assumptions. + source = object + + // Set type to `text/plain;charset=UTF-8`. + type = 'text/plain;charset=UTF-8' + } else if (object instanceof URLSearchParams) { + // URLSearchParams + + // spec says to run application/x-www-form-urlencoded on body.list + // this is implemented in Node.js as apart of an URLSearchParams instance toString method + // See: https://github.com/nodejs/node/blob/e46c680bf2b211bbd52cf959ca17ee98c7f657f5/lib/internal/url.js#L490 + // and https://github.com/nodejs/node/blob/e46c680bf2b211bbd52cf959ca17ee98c7f657f5/lib/internal/url.js#L1100 + + // Set source to the result of running the application/x-www-form-urlencoded serializer with object’s list. + source = object.toString() + + // Set type to `application/x-www-form-urlencoded;charset=UTF-8`. + type = 'application/x-www-form-urlencoded;charset=UTF-8' + } else if (isArrayBuffer(object)) { + // BufferSource/ArrayBuffer + + // Set source to a copy of the bytes held by object. + source = new Uint8Array(object.slice()) + } else if (ArrayBuffer.isView(object)) { + // BufferSource/ArrayBufferView + + // Set source to a copy of the bytes held by object. + source = new Uint8Array(object.buffer.slice(object.byteOffset, object.byteOffset + object.byteLength)) + } else if (util.isFormDataLike(object)) { + const boundary = `----formdata-undici-0${`${Math.floor(Math.random() * 1e11)}`.padStart(11, '0')}` + const prefix = `--${boundary}\r\nContent-Disposition: form-data` + + /*! formdata-polyfill. MIT License. Jimmy Wärting */ + const escape = (str) => + str.replace(/\n/g, '%0A').replace(/\r/g, '%0D').replace(/"/g, '%22') + const normalizeLinefeeds = (value) => value.replace(/\r?\n|\r/g, '\r\n') + + // Set action to this step: run the multipart/form-data + // encoding algorithm, with object’s entry list and UTF-8. + // - This ensures that the body is immutable and can't be changed afterwords + // - That the content-length is calculated in advance. + // - And that all parts are pre-encoded and ready to be sent. + + const blobParts = [] + const rn = new Uint8Array([13, 10]) // '\r\n' + length = 0 + let hasUnknownSizeValue = false + + for (const [name, value] of object) { + if (typeof value === 'string') { + const chunk = textEncoder.encode(prefix + + `; name="${escape(normalizeLinefeeds(name))}"` + + `\r\n\r\n${normalizeLinefeeds(value)}\r\n`) + blobParts.push(chunk) + length += chunk.byteLength + } else { + const chunk = textEncoder.encode(`${prefix}; name="${escape(normalizeLinefeeds(name))}"` + + (value.name ? `; filename="${escape(value.name)}"` : '') + '\r\n' + + `Content-Type: ${ + value.type || 'application/octet-stream' + }\r\n\r\n`) + blobParts.push(chunk, value, rn) + if (typeof value.size === 'number') { + length += chunk.byteLength + value.size + rn.byteLength + } else { + hasUnknownSizeValue = true + } + } + } + + const chunk = textEncoder.encode(`--${boundary}--`) + blobParts.push(chunk) + length += chunk.byteLength + if (hasUnknownSizeValue) { + length = null + } + + // Set source to object. + source = object + + action = async function * () { + for (const part of blobParts) { + if (part.stream) { + yield * part.stream() + } else { + yield part + } + } + } + + // Set type to `multipart/form-data; boundary=`, + // followed by the multipart/form-data boundary string generated + // by the multipart/form-data encoding algorithm. + type = 'multipart/form-data; boundary=' + boundary + } else if (isBlobLike(object)) { + // Blob + + // Set source to object. + source = object + + // Set length to object’s size. + length = object.size + + // If object’s type attribute is not the empty byte sequence, set + // type to its value. + if (object.type) { + type = object.type + } + } else if (typeof object[Symbol.asyncIterator] === 'function') { + // If keepalive is true, then throw a TypeError. + if (keepalive) { + throw new TypeError('keepalive') + } + + // If object is disturbed or locked, then throw a TypeError. + if (util.isDisturbed(object) || object.locked) { + throw new TypeError( + 'Response body object should not be disturbed or locked' + ) + } + + stream = + object instanceof ReadableStream ? object : ReadableStreamFrom(object) + } + + // 11. If source is a byte sequence, then set action to a + // step that returns source and length to source’s length. + if (typeof source === 'string' || util.isBuffer(source)) { + length = Buffer.byteLength(source) + } + + // 12. If action is non-null, then run these steps in in parallel: + if (action != null) { + // Run action. + let iterator + stream = new ReadableStream({ + async start () { + iterator = action(object)[Symbol.asyncIterator]() + }, + async pull (controller) { + const { value, done } = await iterator.next() + if (done) { + // When running action is done, close stream. + queueMicrotask(() => { + controller.close() + }) + } else { + // Whenever one or more bytes are available and stream is not errored, + // enqueue a Uint8Array wrapping an ArrayBuffer containing the available + // bytes into stream. + if (!isErrored(stream)) { + controller.enqueue(new Uint8Array(value)) + } + } + return controller.desiredSize > 0 + }, + async cancel (reason) { + await iterator.return() + }, + type: undefined + }) + } + + // 13. Let body be a body whose stream is stream, source is source, + // and length is length. + const body = { stream, source, length } + + // 14. Return (body, type). + return [body, type] +} + +// https://fetch.spec.whatwg.org/#bodyinit-safely-extract +function safelyExtractBody (object, keepalive = false) { + if (!ReadableStream) { + // istanbul ignore next + ReadableStream = (__nccwpck_require__(3774).ReadableStream) + } + + // To safely extract a body and a `Content-Type` value from + // a byte sequence or BodyInit object object, run these steps: + + // 1. If object is a ReadableStream object, then: + if (object instanceof ReadableStream) { + // Assert: object is neither disturbed nor locked. + // istanbul ignore next + assert(!util.isDisturbed(object), 'The body has already been consumed.') + // istanbul ignore next + assert(!object.locked, 'The stream is locked.') + } + + // 2. Return the results of extracting object. + return extractBody(object, keepalive) +} + +function cloneBody (body) { + // To clone a body body, run these steps: + + // https://fetch.spec.whatwg.org/#concept-body-clone + + // 1. Let « out1, out2 » be the result of teeing body’s stream. + const [out1, out2] = body.stream.tee() + const out2Clone = structuredClone(out2, { transfer: [out2] }) + // This, for whatever reasons, unrefs out2Clone which allows + // the process to exit by itself. + const [, finalClone] = out2Clone.tee() + + // 2. Set body’s stream to out1. + body.stream = out1 + + // 3. Return a body whose stream is out2 and other members are copied from body. + return { + stream: finalClone, + length: body.length, + source: body.source + } +} + +async function * consumeBody (body) { + if (body) { + if (isUint8Array(body)) { + yield body + } else { + const stream = body.stream + + if (util.isDisturbed(stream)) { + throw new TypeError('The body has already been consumed.') + } + + if (stream.locked) { + throw new TypeError('The stream is locked.') + } + + // Compat. + stream[kBodyUsed] = true + + yield * stream + } + } +} + +function throwIfAborted (state) { + if (state.aborted) { + throw new DOMException('The operation was aborted.', 'AbortError') + } +} + +function bodyMixinMethods (instance) { + const methods = { + blob () { + // The blob() method steps are to return the result of + // running consume body with this and the following step + // given a byte sequence bytes: return a Blob whose + // contents are bytes and whose type attribute is this’s + // MIME type. + return specConsumeBody(this, (bytes) => { + let mimeType = bodyMimeType(this) + + if (mimeType === 'failure') { + mimeType = '' + } else if (mimeType) { + mimeType = serializeAMimeType(mimeType) + } + + // Return a Blob whose contents are bytes and type attribute + // is mimeType. + return new Blob([bytes], { type: mimeType }) + }, instance) + }, + + arrayBuffer () { + // The arrayBuffer() method steps are to return the result + // of running consume body with this and the following step + // given a byte sequence bytes: return a new ArrayBuffer + // whose contents are bytes. + return specConsumeBody(this, (bytes) => { + return new Uint8Array(bytes).buffer + }, instance) + }, + + text () { + // The text() method steps are to return the result of running + // consume body with this and UTF-8 decode. + return specConsumeBody(this, utf8DecodeBytes, instance) + }, + + json () { + // The json() method steps are to return the result of running + // consume body with this and parse JSON from bytes. + return specConsumeBody(this, parseJSONFromBytes, instance) + }, + + async formData () { + webidl.brandCheck(this, instance) + + throwIfAborted(this[kState]) + + const contentType = this.headers.get('Content-Type') + + // If mimeType’s essence is "multipart/form-data", then: + if (/multipart\/form-data/.test(contentType)) { + const headers = {} + for (const [key, value] of this.headers) headers[key.toLowerCase()] = value + + const responseFormData = new FormData() + + let busboy + + try { + busboy = new Busboy({ + headers, + preservePath: true + }) + } catch (err) { + throw new DOMException(`${err}`, 'AbortError') + } + + busboy.on('field', (name, value) => { + responseFormData.append(name, value) + }) + busboy.on('file', (name, value, filename, encoding, mimeType) => { + const chunks = [] + + if (encoding === 'base64' || encoding.toLowerCase() === 'base64') { + let base64chunk = '' + + value.on('data', (chunk) => { + base64chunk += chunk.toString().replace(/[\r\n]/gm, '') + + const end = base64chunk.length - base64chunk.length % 4 + chunks.push(Buffer.from(base64chunk.slice(0, end), 'base64')) + + base64chunk = base64chunk.slice(end) + }) + value.on('end', () => { + chunks.push(Buffer.from(base64chunk, 'base64')) + responseFormData.append(name, new File(chunks, filename, { type: mimeType })) + }) + } else { + value.on('data', (chunk) => { + chunks.push(chunk) + }) + value.on('end', () => { + responseFormData.append(name, new File(chunks, filename, { type: mimeType })) + }) + } + }) + + const busboyResolve = new Promise((resolve, reject) => { + busboy.on('finish', resolve) + busboy.on('error', (err) => reject(new TypeError(err))) + }) + + if (this.body !== null) for await (const chunk of consumeBody(this[kState].body)) busboy.write(chunk) + busboy.end() + await busboyResolve + + return responseFormData + } else if (/application\/x-www-form-urlencoded/.test(contentType)) { + // Otherwise, if mimeType’s essence is "application/x-www-form-urlencoded", then: + + // 1. Let entries be the result of parsing bytes. + let entries + try { + let text = '' + // application/x-www-form-urlencoded parser will keep the BOM. + // https://url.spec.whatwg.org/#concept-urlencoded-parser + // Note that streaming decoder is stateful and cannot be reused + const streamingDecoder = new TextDecoder('utf-8', { ignoreBOM: true }) + + for await (const chunk of consumeBody(this[kState].body)) { + if (!isUint8Array(chunk)) { + throw new TypeError('Expected Uint8Array chunk') + } + text += streamingDecoder.decode(chunk, { stream: true }) + } + text += streamingDecoder.decode() + entries = new URLSearchParams(text) + } catch (err) { + // istanbul ignore next: Unclear when new URLSearchParams can fail on a string. + // 2. If entries is failure, then throw a TypeError. + throw Object.assign(new TypeError(), { cause: err }) + } + + // 3. Return a new FormData object whose entries are entries. + const formData = new FormData() + for (const [name, value] of entries) { + formData.append(name, value) + } + return formData + } else { + // Wait a tick before checking if the request has been aborted. + // Otherwise, a TypeError can be thrown when an AbortError should. + await Promise.resolve() + + throwIfAborted(this[kState]) + + // Otherwise, throw a TypeError. + throw webidl.errors.exception({ + header: `${instance.name}.formData`, + message: 'Could not parse content as FormData.' + }) + } + } + } + + return methods +} + +function mixinBody (prototype) { + Object.assign(prototype.prototype, bodyMixinMethods(prototype)) +} + +/** + * @see https://fetch.spec.whatwg.org/#concept-body-consume-body + * @param {Response|Request} object + * @param {(value: unknown) => unknown} convertBytesToJSValue + * @param {Response|Request} instance + */ +async function specConsumeBody (object, convertBytesToJSValue, instance) { + webidl.brandCheck(object, instance) + + throwIfAborted(object[kState]) + + // 1. If object is unusable, then return a promise rejected + // with a TypeError. + if (bodyUnusable(object[kState].body)) { + throw new TypeError('Body is unusable') + } + + // 2. Let promise be a new promise. + const promise = createDeferredPromise() + + // 3. Let errorSteps given error be to reject promise with error. + const errorSteps = (error) => promise.reject(error) + + // 4. Let successSteps given a byte sequence data be to resolve + // promise with the result of running convertBytesToJSValue + // with data. If that threw an exception, then run errorSteps + // with that exception. + const successSteps = (data) => { + try { + promise.resolve(convertBytesToJSValue(data)) + } catch (e) { + errorSteps(e) + } + } + + // 5. If object’s body is null, then run successSteps with an + // empty byte sequence. + if (object[kState].body == null) { + successSteps(new Uint8Array()) + return promise.promise + } + + // 6. Otherwise, fully read object’s body given successSteps, + // errorSteps, and object’s relevant global object. + await fullyReadBody(object[kState].body, successSteps, errorSteps) + + // 7. Return promise. + return promise.promise +} + +// https://fetch.spec.whatwg.org/#body-unusable +function bodyUnusable (body) { + // An object including the Body interface mixin is + // said to be unusable if its body is non-null and + // its body’s stream is disturbed or locked. + return body != null && (body.stream.locked || util.isDisturbed(body.stream)) +} + +/** + * @see https://encoding.spec.whatwg.org/#utf-8-decode + * @param {Buffer} buffer + */ +function utf8DecodeBytes (buffer) { + if (buffer.length === 0) { + return '' + } + + // 1. Let buffer be the result of peeking three bytes from + // ioQueue, converted to a byte sequence. + + // 2. If buffer is 0xEF 0xBB 0xBF, then read three + // bytes from ioQueue. (Do nothing with those bytes.) + if (buffer[0] === 0xEF && buffer[1] === 0xBB && buffer[2] === 0xBF) { + buffer = buffer.subarray(3) + } + + // 3. Process a queue with an instance of UTF-8’s + // decoder, ioQueue, output, and "replacement". + const output = textDecoder.decode(buffer) + + // 4. Return output. + return output +} + +/** + * @see https://infra.spec.whatwg.org/#parse-json-bytes-to-a-javascript-value + * @param {Uint8Array} bytes + */ +function parseJSONFromBytes (bytes) { + return JSON.parse(utf8DecodeBytes(bytes)) +} + +/** + * @see https://fetch.spec.whatwg.org/#concept-body-mime-type + * @param {import('./response').Response|import('./request').Request} object + */ +function bodyMimeType (object) { + const { headersList } = object[kState] + const contentType = headersList.get('content-type') + + if (contentType === null) { + return 'failure' + } + + return parseMIMEType(contentType) +} + +module.exports = { + extractBody, + safelyExtractBody, + cloneBody, + mixinBody +} + + +/***/ }), + +/***/ 7326: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const { MessageChannel, receiveMessageOnPort } = __nccwpck_require__(8167) + +const corsSafeListedMethods = ['GET', 'HEAD', 'POST'] +const corsSafeListedMethodsSet = new Set(corsSafeListedMethods) + +const nullBodyStatus = [101, 204, 205, 304] + +const redirectStatus = [301, 302, 303, 307, 308] +const redirectStatusSet = new Set(redirectStatus) + +// https://fetch.spec.whatwg.org/#block-bad-port +const badPorts = [ + '1', '7', '9', '11', '13', '15', '17', '19', '20', '21', '22', '23', '25', '37', '42', '43', '53', '69', '77', '79', + '87', '95', '101', '102', '103', '104', '109', '110', '111', '113', '115', '117', '119', '123', '135', '137', + '139', '143', '161', '179', '389', '427', '465', '512', '513', '514', '515', '526', '530', '531', '532', + '540', '548', '554', '556', '563', '587', '601', '636', '989', '990', '993', '995', '1719', '1720', '1723', + '2049', '3659', '4045', '5060', '5061', '6000', '6566', '6665', '6666', '6667', '6668', '6669', '6697', + '10080' +] + +const badPortsSet = new Set(badPorts) + +// https://w3c.github.io/webappsec-referrer-policy/#referrer-policies +const referrerPolicy = [ + '', + 'no-referrer', + 'no-referrer-when-downgrade', + 'same-origin', + 'origin', + 'strict-origin', + 'origin-when-cross-origin', + 'strict-origin-when-cross-origin', + 'unsafe-url' +] +const referrerPolicySet = new Set(referrerPolicy) + +const requestRedirect = ['follow', 'manual', 'error'] + +const safeMethods = ['GET', 'HEAD', 'OPTIONS', 'TRACE'] +const safeMethodsSet = new Set(safeMethods) + +const requestMode = ['navigate', 'same-origin', 'no-cors', 'cors'] + +const requestCredentials = ['omit', 'same-origin', 'include'] + +const requestCache = [ + 'default', + 'no-store', + 'reload', + 'no-cache', + 'force-cache', + 'only-if-cached' +] + +// https://fetch.spec.whatwg.org/#request-body-header-name +const requestBodyHeader = [ + 'content-encoding', + 'content-language', + 'content-location', + 'content-type', + // See https://github.com/nodejs/undici/issues/2021 + // 'Content-Length' is a forbidden header name, which is typically + // removed in the Headers implementation. However, undici doesn't + // filter out headers, so we add it here. + 'content-length' +] + +// https://fetch.spec.whatwg.org/#enumdef-requestduplex +const requestDuplex = [ + 'half' +] + +// http://fetch.spec.whatwg.org/#forbidden-method +const forbiddenMethods = ['CONNECT', 'TRACE', 'TRACK'] +const forbiddenMethodsSet = new Set(forbiddenMethods) + +const subresource = [ + 'audio', + 'audioworklet', + 'font', + 'image', + 'manifest', + 'paintworklet', + 'script', + 'style', + 'track', + 'video', + 'xslt', + '' +] +const subresourceSet = new Set(subresource) + +/** @type {globalThis['DOMException']} */ +const DOMException = globalThis.DOMException ?? (() => { + // DOMException was only made a global in Node v17.0.0, + // but fetch supports >= v16.8. + try { + atob('~') + } catch (err) { + return Object.getPrototypeOf(err).constructor + } +})() + +let channel + +/** @type {globalThis['structuredClone']} */ +const structuredClone = + globalThis.structuredClone ?? + // https://github.com/nodejs/node/blob/b27ae24dcc4251bad726d9d84baf678d1f707fed/lib/internal/structured_clone.js + // structuredClone was added in v17.0.0, but fetch supports v16.8 + function structuredClone (value, options = undefined) { + if (arguments.length === 0) { + throw new TypeError('missing argument') + } + + if (!channel) { + channel = new MessageChannel() + } + channel.port1.unref() + channel.port2.unref() + channel.port1.postMessage(value, options?.transfer) + return receiveMessageOnPort(channel.port2).message + } + +module.exports = { + DOMException, + structuredClone, + subresource, + forbiddenMethods, + requestBodyHeader, + referrerPolicy, + requestRedirect, + requestMode, + requestCredentials, + requestCache, + redirectStatus, + corsSafeListedMethods, + nullBodyStatus, + safeMethods, + badPorts, + requestDuplex, + subresourceSet, + badPortsSet, + redirectStatusSet, + corsSafeListedMethodsSet, + safeMethodsSet, + forbiddenMethodsSet, + referrerPolicySet +} + + +/***/ }), + +/***/ 4322: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +const assert = __nccwpck_require__(2613) +const { atob } = __nccwpck_require__(181) +const { isomorphicDecode } = __nccwpck_require__(5523) + +const encoder = new TextEncoder() + +/** + * @see https://mimesniff.spec.whatwg.org/#http-token-code-point + */ +const HTTP_TOKEN_CODEPOINTS = /^[!#$%&'*+-.^_|~A-Za-z0-9]+$/ +const HTTP_WHITESPACE_REGEX = /(\u000A|\u000D|\u0009|\u0020)/ // eslint-disable-line +/** + * @see https://mimesniff.spec.whatwg.org/#http-quoted-string-token-code-point + */ +const HTTP_QUOTED_STRING_TOKENS = /[\u0009|\u0020-\u007E|\u0080-\u00FF]/ // eslint-disable-line + +// https://fetch.spec.whatwg.org/#data-url-processor +/** @param {URL} dataURL */ +function dataURLProcessor (dataURL) { + // 1. Assert: dataURL’s scheme is "data". + assert(dataURL.protocol === 'data:') + + // 2. Let input be the result of running the URL + // serializer on dataURL with exclude fragment + // set to true. + let input = URLSerializer(dataURL, true) + + // 3. Remove the leading "data:" string from input. + input = input.slice(5) + + // 4. Let position point at the start of input. + const position = { position: 0 } + + // 5. Let mimeType be the result of collecting a + // sequence of code points that are not equal + // to U+002C (,), given position. + let mimeType = collectASequenceOfCodePointsFast( + ',', + input, + position + ) + + // 6. Strip leading and trailing ASCII whitespace + // from mimeType. + // Undici implementation note: we need to store the + // length because if the mimetype has spaces removed, + // the wrong amount will be sliced from the input in + // step #9 + const mimeTypeLength = mimeType.length + mimeType = removeASCIIWhitespace(mimeType, true, true) + + // 7. If position is past the end of input, then + // return failure + if (position.position >= input.length) { + return 'failure' + } + + // 8. Advance position by 1. + position.position++ + + // 9. Let encodedBody be the remainder of input. + const encodedBody = input.slice(mimeTypeLength + 1) + + // 10. Let body be the percent-decoding of encodedBody. + let body = stringPercentDecode(encodedBody) + + // 11. If mimeType ends with U+003B (;), followed by + // zero or more U+0020 SPACE, followed by an ASCII + // case-insensitive match for "base64", then: + if (/;(\u0020){0,}base64$/i.test(mimeType)) { + // 1. Let stringBody be the isomorphic decode of body. + const stringBody = isomorphicDecode(body) + + // 2. Set body to the forgiving-base64 decode of + // stringBody. + body = forgivingBase64(stringBody) + + // 3. If body is failure, then return failure. + if (body === 'failure') { + return 'failure' + } + + // 4. Remove the last 6 code points from mimeType. + mimeType = mimeType.slice(0, -6) + + // 5. Remove trailing U+0020 SPACE code points from mimeType, + // if any. + mimeType = mimeType.replace(/(\u0020)+$/, '') + + // 6. Remove the last U+003B (;) code point from mimeType. + mimeType = mimeType.slice(0, -1) + } + + // 12. If mimeType starts with U+003B (;), then prepend + // "text/plain" to mimeType. + if (mimeType.startsWith(';')) { + mimeType = 'text/plain' + mimeType + } + + // 13. Let mimeTypeRecord be the result of parsing + // mimeType. + let mimeTypeRecord = parseMIMEType(mimeType) + + // 14. If mimeTypeRecord is failure, then set + // mimeTypeRecord to text/plain;charset=US-ASCII. + if (mimeTypeRecord === 'failure') { + mimeTypeRecord = parseMIMEType('text/plain;charset=US-ASCII') + } + + // 15. Return a new data: URL struct whose MIME + // type is mimeTypeRecord and body is body. + // https://fetch.spec.whatwg.org/#data-url-struct + return { mimeType: mimeTypeRecord, body } +} + +// https://url.spec.whatwg.org/#concept-url-serializer +/** + * @param {URL} url + * @param {boolean} excludeFragment + */ +function URLSerializer (url, excludeFragment = false) { + if (!excludeFragment) { + return url.href + } + + const href = url.href + const hashLength = url.hash.length + + return hashLength === 0 ? href : href.substring(0, href.length - hashLength) +} + +// https://infra.spec.whatwg.org/#collect-a-sequence-of-code-points +/** + * @param {(char: string) => boolean} condition + * @param {string} input + * @param {{ position: number }} position + */ +function collectASequenceOfCodePoints (condition, input, position) { + // 1. Let result be the empty string. + let result = '' + + // 2. While position doesn’t point past the end of input and the + // code point at position within input meets the condition condition: + while (position.position < input.length && condition(input[position.position])) { + // 1. Append that code point to the end of result. + result += input[position.position] + + // 2. Advance position by 1. + position.position++ + } + + // 3. Return result. + return result +} + +/** + * A faster collectASequenceOfCodePoints that only works when comparing a single character. + * @param {string} char + * @param {string} input + * @param {{ position: number }} position + */ +function collectASequenceOfCodePointsFast (char, input, position) { + const idx = input.indexOf(char, position.position) + const start = position.position + + if (idx === -1) { + position.position = input.length + return input.slice(start) + } + + position.position = idx + return input.slice(start, position.position) +} + +// https://url.spec.whatwg.org/#string-percent-decode +/** @param {string} input */ +function stringPercentDecode (input) { + // 1. Let bytes be the UTF-8 encoding of input. + const bytes = encoder.encode(input) + + // 2. Return the percent-decoding of bytes. + return percentDecode(bytes) +} + +// https://url.spec.whatwg.org/#percent-decode +/** @param {Uint8Array} input */ +function percentDecode (input) { + // 1. Let output be an empty byte sequence. + /** @type {number[]} */ + const output = [] + + // 2. For each byte byte in input: + for (let i = 0; i < input.length; i++) { + const byte = input[i] + + // 1. If byte is not 0x25 (%), then append byte to output. + if (byte !== 0x25) { + output.push(byte) + + // 2. Otherwise, if byte is 0x25 (%) and the next two bytes + // after byte in input are not in the ranges + // 0x30 (0) to 0x39 (9), 0x41 (A) to 0x46 (F), + // and 0x61 (a) to 0x66 (f), all inclusive, append byte + // to output. + } else if ( + byte === 0x25 && + !/^[0-9A-Fa-f]{2}$/i.test(String.fromCharCode(input[i + 1], input[i + 2])) + ) { + output.push(0x25) + + // 3. Otherwise: + } else { + // 1. Let bytePoint be the two bytes after byte in input, + // decoded, and then interpreted as hexadecimal number. + const nextTwoBytes = String.fromCharCode(input[i + 1], input[i + 2]) + const bytePoint = Number.parseInt(nextTwoBytes, 16) + + // 2. Append a byte whose value is bytePoint to output. + output.push(bytePoint) + + // 3. Skip the next two bytes in input. + i += 2 + } + } + + // 3. Return output. + return Uint8Array.from(output) +} + +// https://mimesniff.spec.whatwg.org/#parse-a-mime-type +/** @param {string} input */ +function parseMIMEType (input) { + // 1. Remove any leading and trailing HTTP whitespace + // from input. + input = removeHTTPWhitespace(input, true, true) + + // 2. Let position be a position variable for input, + // initially pointing at the start of input. + const position = { position: 0 } + + // 3. Let type be the result of collecting a sequence + // of code points that are not U+002F (/) from + // input, given position. + const type = collectASequenceOfCodePointsFast( + '/', + input, + position + ) + + // 4. If type is the empty string or does not solely + // contain HTTP token code points, then return failure. + // https://mimesniff.spec.whatwg.org/#http-token-code-point + if (type.length === 0 || !HTTP_TOKEN_CODEPOINTS.test(type)) { + return 'failure' + } + + // 5. If position is past the end of input, then return + // failure + if (position.position > input.length) { + return 'failure' + } + + // 6. Advance position by 1. (This skips past U+002F (/).) + position.position++ + + // 7. Let subtype be the result of collecting a sequence of + // code points that are not U+003B (;) from input, given + // position. + let subtype = collectASequenceOfCodePointsFast( + ';', + input, + position + ) + + // 8. Remove any trailing HTTP whitespace from subtype. + subtype = removeHTTPWhitespace(subtype, false, true) + + // 9. If subtype is the empty string or does not solely + // contain HTTP token code points, then return failure. + if (subtype.length === 0 || !HTTP_TOKEN_CODEPOINTS.test(subtype)) { + return 'failure' + } + + const typeLowercase = type.toLowerCase() + const subtypeLowercase = subtype.toLowerCase() + + // 10. Let mimeType be a new MIME type record whose type + // is type, in ASCII lowercase, and subtype is subtype, + // in ASCII lowercase. + // https://mimesniff.spec.whatwg.org/#mime-type + const mimeType = { + type: typeLowercase, + subtype: subtypeLowercase, + /** @type {Map} */ + parameters: new Map(), + // https://mimesniff.spec.whatwg.org/#mime-type-essence + essence: `${typeLowercase}/${subtypeLowercase}` + } + + // 11. While position is not past the end of input: + while (position.position < input.length) { + // 1. Advance position by 1. (This skips past U+003B (;).) + position.position++ + + // 2. Collect a sequence of code points that are HTTP + // whitespace from input given position. + collectASequenceOfCodePoints( + // https://fetch.spec.whatwg.org/#http-whitespace + char => HTTP_WHITESPACE_REGEX.test(char), + input, + position + ) + + // 3. Let parameterName be the result of collecting a + // sequence of code points that are not U+003B (;) + // or U+003D (=) from input, given position. + let parameterName = collectASequenceOfCodePoints( + (char) => char !== ';' && char !== '=', + input, + position + ) + + // 4. Set parameterName to parameterName, in ASCII + // lowercase. + parameterName = parameterName.toLowerCase() + + // 5. If position is not past the end of input, then: + if (position.position < input.length) { + // 1. If the code point at position within input is + // U+003B (;), then continue. + if (input[position.position] === ';') { + continue + } + + // 2. Advance position by 1. (This skips past U+003D (=).) + position.position++ + } + + // 6. If position is past the end of input, then break. + if (position.position > input.length) { + break + } + + // 7. Let parameterValue be null. + let parameterValue = null + + // 8. If the code point at position within input is + // U+0022 ("), then: + if (input[position.position] === '"') { + // 1. Set parameterValue to the result of collecting + // an HTTP quoted string from input, given position + // and the extract-value flag. + parameterValue = collectAnHTTPQuotedString(input, position, true) + + // 2. Collect a sequence of code points that are not + // U+003B (;) from input, given position. + collectASequenceOfCodePointsFast( + ';', + input, + position + ) + + // 9. Otherwise: + } else { + // 1. Set parameterValue to the result of collecting + // a sequence of code points that are not U+003B (;) + // from input, given position. + parameterValue = collectASequenceOfCodePointsFast( + ';', + input, + position + ) + + // 2. Remove any trailing HTTP whitespace from parameterValue. + parameterValue = removeHTTPWhitespace(parameterValue, false, true) + + // 3. If parameterValue is the empty string, then continue. + if (parameterValue.length === 0) { + continue + } + } + + // 10. If all of the following are true + // - parameterName is not the empty string + // - parameterName solely contains HTTP token code points + // - parameterValue solely contains HTTP quoted-string token code points + // - mimeType’s parameters[parameterName] does not exist + // then set mimeType’s parameters[parameterName] to parameterValue. + if ( + parameterName.length !== 0 && + HTTP_TOKEN_CODEPOINTS.test(parameterName) && + (parameterValue.length === 0 || HTTP_QUOTED_STRING_TOKENS.test(parameterValue)) && + !mimeType.parameters.has(parameterName) + ) { + mimeType.parameters.set(parameterName, parameterValue) + } + } + + // 12. Return mimeType. + return mimeType +} + +// https://infra.spec.whatwg.org/#forgiving-base64-decode +/** @param {string} data */ +function forgivingBase64 (data) { + // 1. Remove all ASCII whitespace from data. + data = data.replace(/[\u0009\u000A\u000C\u000D\u0020]/g, '') // eslint-disable-line + + // 2. If data’s code point length divides by 4 leaving + // no remainder, then: + if (data.length % 4 === 0) { + // 1. If data ends with one or two U+003D (=) code points, + // then remove them from data. + data = data.replace(/=?=$/, '') + } + + // 3. If data’s code point length divides by 4 leaving + // a remainder of 1, then return failure. + if (data.length % 4 === 1) { + return 'failure' + } + + // 4. If data contains a code point that is not one of + // U+002B (+) + // U+002F (/) + // ASCII alphanumeric + // then return failure. + if (/[^+/0-9A-Za-z]/.test(data)) { + return 'failure' + } + + const binary = atob(data) + const bytes = new Uint8Array(binary.length) + + for (let byte = 0; byte < binary.length; byte++) { + bytes[byte] = binary.charCodeAt(byte) + } + + return bytes +} + +// https://fetch.spec.whatwg.org/#collect-an-http-quoted-string +// tests: https://fetch.spec.whatwg.org/#example-http-quoted-string +/** + * @param {string} input + * @param {{ position: number }} position + * @param {boolean?} extractValue + */ +function collectAnHTTPQuotedString (input, position, extractValue) { + // 1. Let positionStart be position. + const positionStart = position.position + + // 2. Let value be the empty string. + let value = '' + + // 3. Assert: the code point at position within input + // is U+0022 ("). + assert(input[position.position] === '"') + + // 4. Advance position by 1. + position.position++ + + // 5. While true: + while (true) { + // 1. Append the result of collecting a sequence of code points + // that are not U+0022 (") or U+005C (\) from input, given + // position, to value. + value += collectASequenceOfCodePoints( + (char) => char !== '"' && char !== '\\', + input, + position + ) + + // 2. If position is past the end of input, then break. + if (position.position >= input.length) { + break + } + + // 3. Let quoteOrBackslash be the code point at position within + // input. + const quoteOrBackslash = input[position.position] + + // 4. Advance position by 1. + position.position++ + + // 5. If quoteOrBackslash is U+005C (\), then: + if (quoteOrBackslash === '\\') { + // 1. If position is past the end of input, then append + // U+005C (\) to value and break. + if (position.position >= input.length) { + value += '\\' + break + } + + // 2. Append the code point at position within input to value. + value += input[position.position] + + // 3. Advance position by 1. + position.position++ + + // 6. Otherwise: + } else { + // 1. Assert: quoteOrBackslash is U+0022 ("). + assert(quoteOrBackslash === '"') + + // 2. Break. + break + } + } + + // 6. If the extract-value flag is set, then return value. + if (extractValue) { + return value + } + + // 7. Return the code points from positionStart to position, + // inclusive, within input. + return input.slice(positionStart, position.position) +} + +/** + * @see https://mimesniff.spec.whatwg.org/#serialize-a-mime-type + */ +function serializeAMimeType (mimeType) { + assert(mimeType !== 'failure') + const { parameters, essence } = mimeType + + // 1. Let serialization be the concatenation of mimeType’s + // type, U+002F (/), and mimeType’s subtype. + let serialization = essence + + // 2. For each name → value of mimeType’s parameters: + for (let [name, value] of parameters.entries()) { + // 1. Append U+003B (;) to serialization. + serialization += ';' + + // 2. Append name to serialization. + serialization += name + + // 3. Append U+003D (=) to serialization. + serialization += '=' + + // 4. If value does not solely contain HTTP token code + // points or value is the empty string, then: + if (!HTTP_TOKEN_CODEPOINTS.test(value)) { + // 1. Precede each occurence of U+0022 (") or + // U+005C (\) in value with U+005C (\). + value = value.replace(/(\\|")/g, '\\$1') + + // 2. Prepend U+0022 (") to value. + value = '"' + value + + // 3. Append U+0022 (") to value. + value += '"' + } + + // 5. Append value to serialization. + serialization += value + } + + // 3. Return serialization. + return serialization +} + +/** + * @see https://fetch.spec.whatwg.org/#http-whitespace + * @param {string} char + */ +function isHTTPWhiteSpace (char) { + return char === '\r' || char === '\n' || char === '\t' || char === ' ' +} + +/** + * @see https://fetch.spec.whatwg.org/#http-whitespace + * @param {string} str + */ +function removeHTTPWhitespace (str, leading = true, trailing = true) { + let lead = 0 + let trail = str.length - 1 + + if (leading) { + for (; lead < str.length && isHTTPWhiteSpace(str[lead]); lead++); + } + + if (trailing) { + for (; trail > 0 && isHTTPWhiteSpace(str[trail]); trail--); + } + + return str.slice(lead, trail + 1) +} + +/** + * @see https://infra.spec.whatwg.org/#ascii-whitespace + * @param {string} char + */ +function isASCIIWhitespace (char) { + return char === '\r' || char === '\n' || char === '\t' || char === '\f' || char === ' ' +} + +/** + * @see https://infra.spec.whatwg.org/#strip-leading-and-trailing-ascii-whitespace + */ +function removeASCIIWhitespace (str, leading = true, trailing = true) { + let lead = 0 + let trail = str.length - 1 + + if (leading) { + for (; lead < str.length && isASCIIWhitespace(str[lead]); lead++); + } + + if (trailing) { + for (; trail > 0 && isASCIIWhitespace(str[trail]); trail--); + } + + return str.slice(lead, trail + 1) +} + +module.exports = { + dataURLProcessor, + URLSerializer, + collectASequenceOfCodePoints, + collectASequenceOfCodePointsFast, + stringPercentDecode, + parseMIMEType, + collectAnHTTPQuotedString, + serializeAMimeType +} + + +/***/ }), + +/***/ 3041: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const { Blob, File: NativeFile } = __nccwpck_require__(181) +const { types } = __nccwpck_require__(9023) +const { kState } = __nccwpck_require__(9710) +const { isBlobLike } = __nccwpck_require__(5523) +const { webidl } = __nccwpck_require__(4222) +const { parseMIMEType, serializeAMimeType } = __nccwpck_require__(4322) +const { kEnumerableProperty } = __nccwpck_require__(3440) +const encoder = new TextEncoder() + +class File extends Blob { + constructor (fileBits, fileName, options = {}) { + // The File constructor is invoked with two or three parameters, depending + // on whether the optional dictionary parameter is used. When the File() + // constructor is invoked, user agents must run the following steps: + webidl.argumentLengthCheck(arguments, 2, { header: 'File constructor' }) + + fileBits = webidl.converters['sequence'](fileBits) + fileName = webidl.converters.USVString(fileName) + options = webidl.converters.FilePropertyBag(options) + + // 1. Let bytes be the result of processing blob parts given fileBits and + // options. + // Note: Blob handles this for us + + // 2. Let n be the fileName argument to the constructor. + const n = fileName + + // 3. Process FilePropertyBag dictionary argument by running the following + // substeps: + + // 1. If the type member is provided and is not the empty string, let t + // be set to the type dictionary member. If t contains any characters + // outside the range U+0020 to U+007E, then set t to the empty string + // and return from these substeps. + // 2. Convert every character in t to ASCII lowercase. + let t = options.type + let d + + // eslint-disable-next-line no-labels + substep: { + if (t) { + t = parseMIMEType(t) + + if (t === 'failure') { + t = '' + // eslint-disable-next-line no-labels + break substep + } + + t = serializeAMimeType(t).toLowerCase() + } + + // 3. If the lastModified member is provided, let d be set to the + // lastModified dictionary member. If it is not provided, set d to the + // current date and time represented as the number of milliseconds since + // the Unix Epoch (which is the equivalent of Date.now() [ECMA-262]). + d = options.lastModified + } + + // 4. Return a new File object F such that: + // F refers to the bytes byte sequence. + // F.size is set to the number of total bytes in bytes. + // F.name is set to n. + // F.type is set to t. + // F.lastModified is set to d. + + super(processBlobParts(fileBits, options), { type: t }) + this[kState] = { + name: n, + lastModified: d, + type: t + } + } + + get name () { + webidl.brandCheck(this, File) + + return this[kState].name + } + + get lastModified () { + webidl.brandCheck(this, File) + + return this[kState].lastModified + } + + get type () { + webidl.brandCheck(this, File) + + return this[kState].type + } +} + +class FileLike { + constructor (blobLike, fileName, options = {}) { + // TODO: argument idl type check + + // The File constructor is invoked with two or three parameters, depending + // on whether the optional dictionary parameter is used. When the File() + // constructor is invoked, user agents must run the following steps: + + // 1. Let bytes be the result of processing blob parts given fileBits and + // options. + + // 2. Let n be the fileName argument to the constructor. + const n = fileName + + // 3. Process FilePropertyBag dictionary argument by running the following + // substeps: + + // 1. If the type member is provided and is not the empty string, let t + // be set to the type dictionary member. If t contains any characters + // outside the range U+0020 to U+007E, then set t to the empty string + // and return from these substeps. + // TODO + const t = options.type + + // 2. Convert every character in t to ASCII lowercase. + // TODO + + // 3. If the lastModified member is provided, let d be set to the + // lastModified dictionary member. If it is not provided, set d to the + // current date and time represented as the number of milliseconds since + // the Unix Epoch (which is the equivalent of Date.now() [ECMA-262]). + const d = options.lastModified ?? Date.now() + + // 4. Return a new File object F such that: + // F refers to the bytes byte sequence. + // F.size is set to the number of total bytes in bytes. + // F.name is set to n. + // F.type is set to t. + // F.lastModified is set to d. + + this[kState] = { + blobLike, + name: n, + type: t, + lastModified: d + } + } + + stream (...args) { + webidl.brandCheck(this, FileLike) + + return this[kState].blobLike.stream(...args) + } + + arrayBuffer (...args) { + webidl.brandCheck(this, FileLike) + + return this[kState].blobLike.arrayBuffer(...args) + } + + slice (...args) { + webidl.brandCheck(this, FileLike) + + return this[kState].blobLike.slice(...args) + } + + text (...args) { + webidl.brandCheck(this, FileLike) + + return this[kState].blobLike.text(...args) + } + + get size () { + webidl.brandCheck(this, FileLike) + + return this[kState].blobLike.size + } + + get type () { + webidl.brandCheck(this, FileLike) + + return this[kState].blobLike.type + } + + get name () { + webidl.brandCheck(this, FileLike) + + return this[kState].name + } + + get lastModified () { + webidl.brandCheck(this, FileLike) + + return this[kState].lastModified + } + + get [Symbol.toStringTag] () { + return 'File' + } +} + +Object.defineProperties(File.prototype, { + [Symbol.toStringTag]: { + value: 'File', + configurable: true + }, + name: kEnumerableProperty, + lastModified: kEnumerableProperty +}) + +webidl.converters.Blob = webidl.interfaceConverter(Blob) + +webidl.converters.BlobPart = function (V, opts) { + if (webidl.util.Type(V) === 'Object') { + if (isBlobLike(V)) { + return webidl.converters.Blob(V, { strict: false }) + } + + if ( + ArrayBuffer.isView(V) || + types.isAnyArrayBuffer(V) + ) { + return webidl.converters.BufferSource(V, opts) + } + } + + return webidl.converters.USVString(V, opts) +} + +webidl.converters['sequence'] = webidl.sequenceConverter( + webidl.converters.BlobPart +) + +// https://www.w3.org/TR/FileAPI/#dfn-FilePropertyBag +webidl.converters.FilePropertyBag = webidl.dictionaryConverter([ + { + key: 'lastModified', + converter: webidl.converters['long long'], + get defaultValue () { + return Date.now() + } + }, + { + key: 'type', + converter: webidl.converters.DOMString, + defaultValue: '' + }, + { + key: 'endings', + converter: (value) => { + value = webidl.converters.DOMString(value) + value = value.toLowerCase() + + if (value !== 'native') { + value = 'transparent' + } + + return value + }, + defaultValue: 'transparent' + } +]) + +/** + * @see https://www.w3.org/TR/FileAPI/#process-blob-parts + * @param {(NodeJS.TypedArray|Blob|string)[]} parts + * @param {{ type: string, endings: string }} options + */ +function processBlobParts (parts, options) { + // 1. Let bytes be an empty sequence of bytes. + /** @type {NodeJS.TypedArray[]} */ + const bytes = [] + + // 2. For each element in parts: + for (const element of parts) { + // 1. If element is a USVString, run the following substeps: + if (typeof element === 'string') { + // 1. Let s be element. + let s = element + + // 2. If the endings member of options is "native", set s + // to the result of converting line endings to native + // of element. + if (options.endings === 'native') { + s = convertLineEndingsNative(s) + } + + // 3. Append the result of UTF-8 encoding s to bytes. + bytes.push(encoder.encode(s)) + } else if ( + types.isAnyArrayBuffer(element) || + types.isTypedArray(element) + ) { + // 2. If element is a BufferSource, get a copy of the + // bytes held by the buffer source, and append those + // bytes to bytes. + if (!element.buffer) { // ArrayBuffer + bytes.push(new Uint8Array(element)) + } else { + bytes.push( + new Uint8Array(element.buffer, element.byteOffset, element.byteLength) + ) + } + } else if (isBlobLike(element)) { + // 3. If element is a Blob, append the bytes it represents + // to bytes. + bytes.push(element) + } + } + + // 3. Return bytes. + return bytes +} + +/** + * @see https://www.w3.org/TR/FileAPI/#convert-line-endings-to-native + * @param {string} s + */ +function convertLineEndingsNative (s) { + // 1. Let native line ending be be the code point U+000A LF. + let nativeLineEnding = '\n' + + // 2. If the underlying platform’s conventions are to + // represent newlines as a carriage return and line feed + // sequence, set native line ending to the code point + // U+000D CR followed by the code point U+000A LF. + if (process.platform === 'win32') { + nativeLineEnding = '\r\n' + } + + return s.replace(/\r?\n/g, nativeLineEnding) +} + +// If this function is moved to ./util.js, some tools (such as +// rollup) will warn about circular dependencies. See: +// https://github.com/nodejs/undici/issues/1629 +function isFileLike (object) { + return ( + (NativeFile && object instanceof NativeFile) || + object instanceof File || ( + object && + (typeof object.stream === 'function' || + typeof object.arrayBuffer === 'function') && + object[Symbol.toStringTag] === 'File' + ) + ) +} + +module.exports = { File, FileLike, isFileLike } + + +/***/ }), + +/***/ 3073: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const { isBlobLike, toUSVString, makeIterator } = __nccwpck_require__(5523) +const { kState } = __nccwpck_require__(9710) +const { File: UndiciFile, FileLike, isFileLike } = __nccwpck_require__(3041) +const { webidl } = __nccwpck_require__(4222) +const { Blob, File: NativeFile } = __nccwpck_require__(181) + +/** @type {globalThis['File']} */ +const File = NativeFile ?? UndiciFile + +// https://xhr.spec.whatwg.org/#formdata +class FormData { + constructor (form) { + if (form !== undefined) { + throw webidl.errors.conversionFailed({ + prefix: 'FormData constructor', + argument: 'Argument 1', + types: ['undefined'] + }) + } + + this[kState] = [] + } + + append (name, value, filename = undefined) { + webidl.brandCheck(this, FormData) + + webidl.argumentLengthCheck(arguments, 2, { header: 'FormData.append' }) + + if (arguments.length === 3 && !isBlobLike(value)) { + throw new TypeError( + "Failed to execute 'append' on 'FormData': parameter 2 is not of type 'Blob'" + ) + } + + // 1. Let value be value if given; otherwise blobValue. + + name = webidl.converters.USVString(name) + value = isBlobLike(value) + ? webidl.converters.Blob(value, { strict: false }) + : webidl.converters.USVString(value) + filename = arguments.length === 3 + ? webidl.converters.USVString(filename) + : undefined + + // 2. Let entry be the result of creating an entry with + // name, value, and filename if given. + const entry = makeEntry(name, value, filename) + + // 3. Append entry to this’s entry list. + this[kState].push(entry) + } + + delete (name) { + webidl.brandCheck(this, FormData) + + webidl.argumentLengthCheck(arguments, 1, { header: 'FormData.delete' }) + + name = webidl.converters.USVString(name) + + // The delete(name) method steps are to remove all entries whose name + // is name from this’s entry list. + this[kState] = this[kState].filter(entry => entry.name !== name) + } + + get (name) { + webidl.brandCheck(this, FormData) + + webidl.argumentLengthCheck(arguments, 1, { header: 'FormData.get' }) + + name = webidl.converters.USVString(name) + + // 1. If there is no entry whose name is name in this’s entry list, + // then return null. + const idx = this[kState].findIndex((entry) => entry.name === name) + if (idx === -1) { + return null + } + + // 2. Return the value of the first entry whose name is name from + // this’s entry list. + return this[kState][idx].value + } + + getAll (name) { + webidl.brandCheck(this, FormData) + + webidl.argumentLengthCheck(arguments, 1, { header: 'FormData.getAll' }) + + name = webidl.converters.USVString(name) + + // 1. If there is no entry whose name is name in this’s entry list, + // then return the empty list. + // 2. Return the values of all entries whose name is name, in order, + // from this’s entry list. + return this[kState] + .filter((entry) => entry.name === name) + .map((entry) => entry.value) + } + + has (name) { + webidl.brandCheck(this, FormData) + + webidl.argumentLengthCheck(arguments, 1, { header: 'FormData.has' }) + + name = webidl.converters.USVString(name) + + // The has(name) method steps are to return true if there is an entry + // whose name is name in this’s entry list; otherwise false. + return this[kState].findIndex((entry) => entry.name === name) !== -1 + } + + set (name, value, filename = undefined) { + webidl.brandCheck(this, FormData) + + webidl.argumentLengthCheck(arguments, 2, { header: 'FormData.set' }) + + if (arguments.length === 3 && !isBlobLike(value)) { + throw new TypeError( + "Failed to execute 'set' on 'FormData': parameter 2 is not of type 'Blob'" + ) + } + + // The set(name, value) and set(name, blobValue, filename) method steps + // are: + + // 1. Let value be value if given; otherwise blobValue. + + name = webidl.converters.USVString(name) + value = isBlobLike(value) + ? webidl.converters.Blob(value, { strict: false }) + : webidl.converters.USVString(value) + filename = arguments.length === 3 + ? toUSVString(filename) + : undefined + + // 2. Let entry be the result of creating an entry with name, value, and + // filename if given. + const entry = makeEntry(name, value, filename) + + // 3. If there are entries in this’s entry list whose name is name, then + // replace the first such entry with entry and remove the others. + const idx = this[kState].findIndex((entry) => entry.name === name) + if (idx !== -1) { + this[kState] = [ + ...this[kState].slice(0, idx), + entry, + ...this[kState].slice(idx + 1).filter((entry) => entry.name !== name) + ] + } else { + // 4. Otherwise, append entry to this’s entry list. + this[kState].push(entry) + } + } + + entries () { + webidl.brandCheck(this, FormData) + + return makeIterator( + () => this[kState].map(pair => [pair.name, pair.value]), + 'FormData', + 'key+value' + ) + } + + keys () { + webidl.brandCheck(this, FormData) + + return makeIterator( + () => this[kState].map(pair => [pair.name, pair.value]), + 'FormData', + 'key' + ) + } + + values () { + webidl.brandCheck(this, FormData) + + return makeIterator( + () => this[kState].map(pair => [pair.name, pair.value]), + 'FormData', + 'value' + ) + } + + /** + * @param {(value: string, key: string, self: FormData) => void} callbackFn + * @param {unknown} thisArg + */ + forEach (callbackFn, thisArg = globalThis) { + webidl.brandCheck(this, FormData) + + webidl.argumentLengthCheck(arguments, 1, { header: 'FormData.forEach' }) + + if (typeof callbackFn !== 'function') { + throw new TypeError( + "Failed to execute 'forEach' on 'FormData': parameter 1 is not of type 'Function'." + ) + } + + for (const [key, value] of this) { + callbackFn.apply(thisArg, [value, key, this]) + } + } +} + +FormData.prototype[Symbol.iterator] = FormData.prototype.entries + +Object.defineProperties(FormData.prototype, { + [Symbol.toStringTag]: { + value: 'FormData', + configurable: true + } +}) + +/** + * @see https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#create-an-entry + * @param {string} name + * @param {string|Blob} value + * @param {?string} filename + * @returns + */ +function makeEntry (name, value, filename) { + // 1. Set name to the result of converting name into a scalar value string. + // "To convert a string into a scalar value string, replace any surrogates + // with U+FFFD." + // see: https://nodejs.org/dist/latest-v18.x/docs/api/buffer.html#buftostringencoding-start-end + name = Buffer.from(name).toString('utf8') + + // 2. If value is a string, then set value to the result of converting + // value into a scalar value string. + if (typeof value === 'string') { + value = Buffer.from(value).toString('utf8') + } else { + // 3. Otherwise: + + // 1. If value is not a File object, then set value to a new File object, + // representing the same bytes, whose name attribute value is "blob" + if (!isFileLike(value)) { + value = value instanceof Blob + ? new File([value], 'blob', { type: value.type }) + : new FileLike(value, 'blob', { type: value.type }) + } + + // 2. If filename is given, then set value to a new File object, + // representing the same bytes, whose name attribute is filename. + if (filename !== undefined) { + /** @type {FilePropertyBag} */ + const options = { + type: value.type, + lastModified: value.lastModified + } + + value = (NativeFile && value instanceof NativeFile) || value instanceof UndiciFile + ? new File([value], filename, options) + : new FileLike(value, filename, options) + } + } + + // 4. Return an entry whose name is name and whose value is value. + return { name, value } +} + +module.exports = { FormData } + + +/***/ }), + +/***/ 5628: +/***/ ((module) => { + +"use strict"; + + +// In case of breaking changes, increase the version +// number to avoid conflicts. +const globalOrigin = Symbol.for('undici.globalOrigin.1') + +function getGlobalOrigin () { + return globalThis[globalOrigin] +} + +function setGlobalOrigin (newOrigin) { + if (newOrigin === undefined) { + Object.defineProperty(globalThis, globalOrigin, { + value: undefined, + writable: true, + enumerable: false, + configurable: false + }) + + return + } + + const parsedURL = new URL(newOrigin) + + if (parsedURL.protocol !== 'http:' && parsedURL.protocol !== 'https:') { + throw new TypeError(`Only http & https urls are allowed, received ${parsedURL.protocol}`) + } + + Object.defineProperty(globalThis, globalOrigin, { + value: parsedURL, + writable: true, + enumerable: false, + configurable: false + }) +} + +module.exports = { + getGlobalOrigin, + setGlobalOrigin +} + + +/***/ }), + +/***/ 6349: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; +// https://github.com/Ethan-Arrowood/undici-fetch + + + +const { kHeadersList, kConstruct } = __nccwpck_require__(6443) +const { kGuard } = __nccwpck_require__(9710) +const { kEnumerableProperty } = __nccwpck_require__(3440) +const { + makeIterator, + isValidHeaderName, + isValidHeaderValue +} = __nccwpck_require__(5523) +const { webidl } = __nccwpck_require__(4222) +const assert = __nccwpck_require__(2613) + +const kHeadersMap = Symbol('headers map') +const kHeadersSortedMap = Symbol('headers map sorted') + +/** + * @param {number} code + */ +function isHTTPWhiteSpaceCharCode (code) { + return code === 0x00a || code === 0x00d || code === 0x009 || code === 0x020 +} + +/** + * @see https://fetch.spec.whatwg.org/#concept-header-value-normalize + * @param {string} potentialValue + */ +function headerValueNormalize (potentialValue) { + // To normalize a byte sequence potentialValue, remove + // any leading and trailing HTTP whitespace bytes from + // potentialValue. + let i = 0; let j = potentialValue.length + + while (j > i && isHTTPWhiteSpaceCharCode(potentialValue.charCodeAt(j - 1))) --j + while (j > i && isHTTPWhiteSpaceCharCode(potentialValue.charCodeAt(i))) ++i + + return i === 0 && j === potentialValue.length ? potentialValue : potentialValue.substring(i, j) +} + +function fill (headers, object) { + // To fill a Headers object headers with a given object object, run these steps: + + // 1. If object is a sequence, then for each header in object: + // Note: webidl conversion to array has already been done. + if (Array.isArray(object)) { + for (let i = 0; i < object.length; ++i) { + const header = object[i] + // 1. If header does not contain exactly two items, then throw a TypeError. + if (header.length !== 2) { + throw webidl.errors.exception({ + header: 'Headers constructor', + message: `expected name/value pair to be length 2, found ${header.length}.` + }) + } + + // 2. Append (header’s first item, header’s second item) to headers. + appendHeader(headers, header[0], header[1]) + } + } else if (typeof object === 'object' && object !== null) { + // Note: null should throw + + // 2. Otherwise, object is a record, then for each key → value in object, + // append (key, value) to headers + const keys = Object.keys(object) + for (let i = 0; i < keys.length; ++i) { + appendHeader(headers, keys[i], object[keys[i]]) + } + } else { + throw webidl.errors.conversionFailed({ + prefix: 'Headers constructor', + argument: 'Argument 1', + types: ['sequence>', 'record'] + }) + } +} + +/** + * @see https://fetch.spec.whatwg.org/#concept-headers-append + */ +function appendHeader (headers, name, value) { + // 1. Normalize value. + value = headerValueNormalize(value) + + // 2. If name is not a header name or value is not a + // header value, then throw a TypeError. + if (!isValidHeaderName(name)) { + throw webidl.errors.invalidArgument({ + prefix: 'Headers.append', + value: name, + type: 'header name' + }) + } else if (!isValidHeaderValue(value)) { + throw webidl.errors.invalidArgument({ + prefix: 'Headers.append', + value, + type: 'header value' + }) + } + + // 3. If headers’s guard is "immutable", then throw a TypeError. + // 4. Otherwise, if headers’s guard is "request" and name is a + // forbidden header name, return. + // Note: undici does not implement forbidden header names + if (headers[kGuard] === 'immutable') { + throw new TypeError('immutable') + } else if (headers[kGuard] === 'request-no-cors') { + // 5. Otherwise, if headers’s guard is "request-no-cors": + // TODO + } + + // 6. Otherwise, if headers’s guard is "response" and name is a + // forbidden response-header name, return. + + // 7. Append (name, value) to headers’s header list. + return headers[kHeadersList].append(name, value) + + // 8. If headers’s guard is "request-no-cors", then remove + // privileged no-CORS request headers from headers +} + +class HeadersList { + /** @type {[string, string][]|null} */ + cookies = null + + constructor (init) { + if (init instanceof HeadersList) { + this[kHeadersMap] = new Map(init[kHeadersMap]) + this[kHeadersSortedMap] = init[kHeadersSortedMap] + this.cookies = init.cookies === null ? null : [...init.cookies] + } else { + this[kHeadersMap] = new Map(init) + this[kHeadersSortedMap] = null + } + } + + // https://fetch.spec.whatwg.org/#header-list-contains + contains (name) { + // A header list list contains a header name name if list + // contains a header whose name is a byte-case-insensitive + // match for name. + name = name.toLowerCase() + + return this[kHeadersMap].has(name) + } + + clear () { + this[kHeadersMap].clear() + this[kHeadersSortedMap] = null + this.cookies = null + } + + // https://fetch.spec.whatwg.org/#concept-header-list-append + append (name, value) { + this[kHeadersSortedMap] = null + + // 1. If list contains name, then set name to the first such + // header’s name. + const lowercaseName = name.toLowerCase() + const exists = this[kHeadersMap].get(lowercaseName) + + // 2. Append (name, value) to list. + if (exists) { + const delimiter = lowercaseName === 'cookie' ? '; ' : ', ' + this[kHeadersMap].set(lowercaseName, { + name: exists.name, + value: `${exists.value}${delimiter}${value}` + }) + } else { + this[kHeadersMap].set(lowercaseName, { name, value }) + } + + if (lowercaseName === 'set-cookie') { + this.cookies ??= [] + this.cookies.push(value) + } + } + + // https://fetch.spec.whatwg.org/#concept-header-list-set + set (name, value) { + this[kHeadersSortedMap] = null + const lowercaseName = name.toLowerCase() + + if (lowercaseName === 'set-cookie') { + this.cookies = [value] + } + + // 1. If list contains name, then set the value of + // the first such header to value and remove the + // others. + // 2. Otherwise, append header (name, value) to list. + this[kHeadersMap].set(lowercaseName, { name, value }) + } + + // https://fetch.spec.whatwg.org/#concept-header-list-delete + delete (name) { + this[kHeadersSortedMap] = null + + name = name.toLowerCase() + + if (name === 'set-cookie') { + this.cookies = null + } + + this[kHeadersMap].delete(name) + } + + // https://fetch.spec.whatwg.org/#concept-header-list-get + get (name) { + const value = this[kHeadersMap].get(name.toLowerCase()) + + // 1. If list does not contain name, then return null. + // 2. Return the values of all headers in list whose name + // is a byte-case-insensitive match for name, + // separated from each other by 0x2C 0x20, in order. + return value === undefined ? null : value.value + } + + * [Symbol.iterator] () { + // use the lowercased name + for (const [name, { value }] of this[kHeadersMap]) { + yield [name, value] + } + } + + get entries () { + const headers = {} + + if (this[kHeadersMap].size) { + for (const { name, value } of this[kHeadersMap].values()) { + headers[name] = value + } + } + + return headers + } +} + +// https://fetch.spec.whatwg.org/#headers-class +class Headers { + constructor (init = undefined) { + if (init === kConstruct) { + return + } + this[kHeadersList] = new HeadersList() + + // The new Headers(init) constructor steps are: + + // 1. Set this’s guard to "none". + this[kGuard] = 'none' + + // 2. If init is given, then fill this with init. + if (init !== undefined) { + init = webidl.converters.HeadersInit(init) + fill(this, init) + } + } + + // https://fetch.spec.whatwg.org/#dom-headers-append + append (name, value) { + webidl.brandCheck(this, Headers) + + webidl.argumentLengthCheck(arguments, 2, { header: 'Headers.append' }) + + name = webidl.converters.ByteString(name) + value = webidl.converters.ByteString(value) + + return appendHeader(this, name, value) + } + + // https://fetch.spec.whatwg.org/#dom-headers-delete + delete (name) { + webidl.brandCheck(this, Headers) + + webidl.argumentLengthCheck(arguments, 1, { header: 'Headers.delete' }) + + name = webidl.converters.ByteString(name) + + // 1. If name is not a header name, then throw a TypeError. + if (!isValidHeaderName(name)) { + throw webidl.errors.invalidArgument({ + prefix: 'Headers.delete', + value: name, + type: 'header name' + }) + } + + // 2. If this’s guard is "immutable", then throw a TypeError. + // 3. Otherwise, if this’s guard is "request" and name is a + // forbidden header name, return. + // 4. Otherwise, if this’s guard is "request-no-cors", name + // is not a no-CORS-safelisted request-header name, and + // name is not a privileged no-CORS request-header name, + // return. + // 5. Otherwise, if this’s guard is "response" and name is + // a forbidden response-header name, return. + // Note: undici does not implement forbidden header names + if (this[kGuard] === 'immutable') { + throw new TypeError('immutable') + } else if (this[kGuard] === 'request-no-cors') { + // TODO + } + + // 6. If this’s header list does not contain name, then + // return. + if (!this[kHeadersList].contains(name)) { + return + } + + // 7. Delete name from this’s header list. + // 8. If this’s guard is "request-no-cors", then remove + // privileged no-CORS request headers from this. + this[kHeadersList].delete(name) + } + + // https://fetch.spec.whatwg.org/#dom-headers-get + get (name) { + webidl.brandCheck(this, Headers) + + webidl.argumentLengthCheck(arguments, 1, { header: 'Headers.get' }) + + name = webidl.converters.ByteString(name) + + // 1. If name is not a header name, then throw a TypeError. + if (!isValidHeaderName(name)) { + throw webidl.errors.invalidArgument({ + prefix: 'Headers.get', + value: name, + type: 'header name' + }) + } + + // 2. Return the result of getting name from this’s header + // list. + return this[kHeadersList].get(name) + } + + // https://fetch.spec.whatwg.org/#dom-headers-has + has (name) { + webidl.brandCheck(this, Headers) + + webidl.argumentLengthCheck(arguments, 1, { header: 'Headers.has' }) + + name = webidl.converters.ByteString(name) + + // 1. If name is not a header name, then throw a TypeError. + if (!isValidHeaderName(name)) { + throw webidl.errors.invalidArgument({ + prefix: 'Headers.has', + value: name, + type: 'header name' + }) + } + + // 2. Return true if this’s header list contains name; + // otherwise false. + return this[kHeadersList].contains(name) + } + + // https://fetch.spec.whatwg.org/#dom-headers-set + set (name, value) { + webidl.brandCheck(this, Headers) + + webidl.argumentLengthCheck(arguments, 2, { header: 'Headers.set' }) + + name = webidl.converters.ByteString(name) + value = webidl.converters.ByteString(value) + + // 1. Normalize value. + value = headerValueNormalize(value) + + // 2. If name is not a header name or value is not a + // header value, then throw a TypeError. + if (!isValidHeaderName(name)) { + throw webidl.errors.invalidArgument({ + prefix: 'Headers.set', + value: name, + type: 'header name' + }) + } else if (!isValidHeaderValue(value)) { + throw webidl.errors.invalidArgument({ + prefix: 'Headers.set', + value, + type: 'header value' + }) + } + + // 3. If this’s guard is "immutable", then throw a TypeError. + // 4. Otherwise, if this’s guard is "request" and name is a + // forbidden header name, return. + // 5. Otherwise, if this’s guard is "request-no-cors" and + // name/value is not a no-CORS-safelisted request-header, + // return. + // 6. Otherwise, if this’s guard is "response" and name is a + // forbidden response-header name, return. + // Note: undici does not implement forbidden header names + if (this[kGuard] === 'immutable') { + throw new TypeError('immutable') + } else if (this[kGuard] === 'request-no-cors') { + // TODO + } + + // 7. Set (name, value) in this’s header list. + // 8. If this’s guard is "request-no-cors", then remove + // privileged no-CORS request headers from this + this[kHeadersList].set(name, value) + } + + // https://fetch.spec.whatwg.org/#dom-headers-getsetcookie + getSetCookie () { + webidl.brandCheck(this, Headers) + + // 1. If this’s header list does not contain `Set-Cookie`, then return « ». + // 2. Return the values of all headers in this’s header list whose name is + // a byte-case-insensitive match for `Set-Cookie`, in order. + + const list = this[kHeadersList].cookies + + if (list) { + return [...list] + } + + return [] + } + + // https://fetch.spec.whatwg.org/#concept-header-list-sort-and-combine + get [kHeadersSortedMap] () { + if (this[kHeadersList][kHeadersSortedMap]) { + return this[kHeadersList][kHeadersSortedMap] + } + + // 1. Let headers be an empty list of headers with the key being the name + // and value the value. + const headers = [] + + // 2. Let names be the result of convert header names to a sorted-lowercase + // set with all the names of the headers in list. + const names = [...this[kHeadersList]].sort((a, b) => a[0] < b[0] ? -1 : 1) + const cookies = this[kHeadersList].cookies + + // 3. For each name of names: + for (let i = 0; i < names.length; ++i) { + const [name, value] = names[i] + // 1. If name is `set-cookie`, then: + if (name === 'set-cookie') { + // 1. Let values be a list of all values of headers in list whose name + // is a byte-case-insensitive match for name, in order. + + // 2. For each value of values: + // 1. Append (name, value) to headers. + for (let j = 0; j < cookies.length; ++j) { + headers.push([name, cookies[j]]) + } + } else { + // 2. Otherwise: + + // 1. Let value be the result of getting name from list. + + // 2. Assert: value is non-null. + assert(value !== null) + + // 3. Append (name, value) to headers. + headers.push([name, value]) + } + } + + this[kHeadersList][kHeadersSortedMap] = headers + + // 4. Return headers. + return headers + } + + keys () { + webidl.brandCheck(this, Headers) + + if (this[kGuard] === 'immutable') { + const value = this[kHeadersSortedMap] + return makeIterator(() => value, 'Headers', + 'key') + } + + return makeIterator( + () => [...this[kHeadersSortedMap].values()], + 'Headers', + 'key' + ) + } + + values () { + webidl.brandCheck(this, Headers) + + if (this[kGuard] === 'immutable') { + const value = this[kHeadersSortedMap] + return makeIterator(() => value, 'Headers', + 'value') + } + + return makeIterator( + () => [...this[kHeadersSortedMap].values()], + 'Headers', + 'value' + ) + } + + entries () { + webidl.brandCheck(this, Headers) + + if (this[kGuard] === 'immutable') { + const value = this[kHeadersSortedMap] + return makeIterator(() => value, 'Headers', + 'key+value') + } + + return makeIterator( + () => [...this[kHeadersSortedMap].values()], + 'Headers', + 'key+value' + ) + } + + /** + * @param {(value: string, key: string, self: Headers) => void} callbackFn + * @param {unknown} thisArg + */ + forEach (callbackFn, thisArg = globalThis) { + webidl.brandCheck(this, Headers) + + webidl.argumentLengthCheck(arguments, 1, { header: 'Headers.forEach' }) + + if (typeof callbackFn !== 'function') { + throw new TypeError( + "Failed to execute 'forEach' on 'Headers': parameter 1 is not of type 'Function'." + ) + } + + for (const [key, value] of this) { + callbackFn.apply(thisArg, [value, key, this]) + } + } + + [Symbol.for('nodejs.util.inspect.custom')] () { + webidl.brandCheck(this, Headers) + + return this[kHeadersList] + } +} + +Headers.prototype[Symbol.iterator] = Headers.prototype.entries + +Object.defineProperties(Headers.prototype, { + append: kEnumerableProperty, + delete: kEnumerableProperty, + get: kEnumerableProperty, + has: kEnumerableProperty, + set: kEnumerableProperty, + getSetCookie: kEnumerableProperty, + keys: kEnumerableProperty, + values: kEnumerableProperty, + entries: kEnumerableProperty, + forEach: kEnumerableProperty, + [Symbol.iterator]: { enumerable: false }, + [Symbol.toStringTag]: { + value: 'Headers', + configurable: true + } +}) + +webidl.converters.HeadersInit = function (V) { + if (webidl.util.Type(V) === 'Object') { + if (V[Symbol.iterator]) { + return webidl.converters['sequence>'](V) + } + + return webidl.converters['record'](V) + } + + throw webidl.errors.conversionFailed({ + prefix: 'Headers constructor', + argument: 'Argument 1', + types: ['sequence>', 'record'] + }) +} + +module.exports = { + fill, + Headers, + HeadersList +} + + +/***/ }), + +/***/ 2315: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; +// https://github.com/Ethan-Arrowood/undici-fetch + + + +const { + Response, + makeNetworkError, + makeAppropriateNetworkError, + filterResponse, + makeResponse +} = __nccwpck_require__(8676) +const { Headers } = __nccwpck_require__(6349) +const { Request, makeRequest } = __nccwpck_require__(5194) +const zlib = __nccwpck_require__(3106) +const { + bytesMatch, + makePolicyContainer, + clonePolicyContainer, + requestBadPort, + TAOCheck, + appendRequestOriginHeader, + responseLocationURL, + requestCurrentURL, + setRequestReferrerPolicyOnRedirect, + tryUpgradeRequestToAPotentiallyTrustworthyURL, + createOpaqueTimingInfo, + appendFetchMetadata, + corsCheck, + crossOriginResourcePolicyCheck, + determineRequestsReferrer, + coarsenedSharedCurrentTime, + createDeferredPromise, + isBlobLike, + sameOrigin, + isCancelled, + isAborted, + isErrorLike, + fullyReadBody, + readableStreamClose, + isomorphicEncode, + urlIsLocal, + urlIsHttpHttpsScheme, + urlHasHttpsScheme +} = __nccwpck_require__(5523) +const { kState, kHeaders, kGuard, kRealm } = __nccwpck_require__(9710) +const assert = __nccwpck_require__(2613) +const { safelyExtractBody } = __nccwpck_require__(8923) +const { + redirectStatusSet, + nullBodyStatus, + safeMethodsSet, + requestBodyHeader, + subresourceSet, + DOMException +} = __nccwpck_require__(7326) +const { kHeadersList } = __nccwpck_require__(6443) +const EE = __nccwpck_require__(4434) +const { Readable, pipeline } = __nccwpck_require__(2203) +const { addAbortListener, isErrored, isReadable, nodeMajor, nodeMinor } = __nccwpck_require__(3440) +const { dataURLProcessor, serializeAMimeType } = __nccwpck_require__(4322) +const { TransformStream } = __nccwpck_require__(3774) +const { getGlobalDispatcher } = __nccwpck_require__(2581) +const { webidl } = __nccwpck_require__(4222) +const { STATUS_CODES } = __nccwpck_require__(8611) +const GET_OR_HEAD = ['GET', 'HEAD'] + +/** @type {import('buffer').resolveObjectURL} */ +let resolveObjectURL +let ReadableStream = globalThis.ReadableStream + +class Fetch extends EE { + constructor (dispatcher) { + super() + + this.dispatcher = dispatcher + this.connection = null + this.dump = false + this.state = 'ongoing' + // 2 terminated listeners get added per request, + // but only 1 gets removed. If there are 20 redirects, + // 21 listeners will be added. + // See https://github.com/nodejs/undici/issues/1711 + // TODO (fix): Find and fix root cause for leaked listener. + this.setMaxListeners(21) + } + + terminate (reason) { + if (this.state !== 'ongoing') { + return + } + + this.state = 'terminated' + this.connection?.destroy(reason) + this.emit('terminated', reason) + } + + // https://fetch.spec.whatwg.org/#fetch-controller-abort + abort (error) { + if (this.state !== 'ongoing') { + return + } + + // 1. Set controller’s state to "aborted". + this.state = 'aborted' + + // 2. Let fallbackError be an "AbortError" DOMException. + // 3. Set error to fallbackError if it is not given. + if (!error) { + error = new DOMException('The operation was aborted.', 'AbortError') + } + + // 4. Let serializedError be StructuredSerialize(error). + // If that threw an exception, catch it, and let + // serializedError be StructuredSerialize(fallbackError). + + // 5. Set controller’s serialized abort reason to serializedError. + this.serializedAbortReason = error + + this.connection?.destroy(error) + this.emit('terminated', error) + } +} + +// https://fetch.spec.whatwg.org/#fetch-method +function fetch (input, init = {}) { + webidl.argumentLengthCheck(arguments, 1, { header: 'globalThis.fetch' }) + + // 1. Let p be a new promise. + const p = createDeferredPromise() + + // 2. Let requestObject be the result of invoking the initial value of + // Request as constructor with input and init as arguments. If this throws + // an exception, reject p with it and return p. + let requestObject + + try { + requestObject = new Request(input, init) + } catch (e) { + p.reject(e) + return p.promise + } + + // 3. Let request be requestObject’s request. + const request = requestObject[kState] + + // 4. If requestObject’s signal’s aborted flag is set, then: + if (requestObject.signal.aborted) { + // 1. Abort the fetch() call with p, request, null, and + // requestObject’s signal’s abort reason. + abortFetch(p, request, null, requestObject.signal.reason) + + // 2. Return p. + return p.promise + } + + // 5. Let globalObject be request’s client’s global object. + const globalObject = request.client.globalObject + + // 6. If globalObject is a ServiceWorkerGlobalScope object, then set + // request’s service-workers mode to "none". + if (globalObject?.constructor?.name === 'ServiceWorkerGlobalScope') { + request.serviceWorkers = 'none' + } + + // 7. Let responseObject be null. + let responseObject = null + + // 8. Let relevantRealm be this’s relevant Realm. + const relevantRealm = null + + // 9. Let locallyAborted be false. + let locallyAborted = false + + // 10. Let controller be null. + let controller = null + + // 11. Add the following abort steps to requestObject’s signal: + addAbortListener( + requestObject.signal, + () => { + // 1. Set locallyAborted to true. + locallyAborted = true + + // 2. Assert: controller is non-null. + assert(controller != null) + + // 3. Abort controller with requestObject’s signal’s abort reason. + controller.abort(requestObject.signal.reason) + + // 4. Abort the fetch() call with p, request, responseObject, + // and requestObject’s signal’s abort reason. + abortFetch(p, request, responseObject, requestObject.signal.reason) + } + ) + + // 12. Let handleFetchDone given response response be to finalize and + // report timing with response, globalObject, and "fetch". + const handleFetchDone = (response) => + finalizeAndReportTiming(response, 'fetch') + + // 13. Set controller to the result of calling fetch given request, + // with processResponseEndOfBody set to handleFetchDone, and processResponse + // given response being these substeps: + + const processResponse = (response) => { + // 1. If locallyAborted is true, terminate these substeps. + if (locallyAborted) { + return Promise.resolve() + } + + // 2. If response’s aborted flag is set, then: + if (response.aborted) { + // 1. Let deserializedError be the result of deserialize a serialized + // abort reason given controller’s serialized abort reason and + // relevantRealm. + + // 2. Abort the fetch() call with p, request, responseObject, and + // deserializedError. + + abortFetch(p, request, responseObject, controller.serializedAbortReason) + return Promise.resolve() + } + + // 3. If response is a network error, then reject p with a TypeError + // and terminate these substeps. + if (response.type === 'error') { + p.reject( + Object.assign(new TypeError('fetch failed'), { cause: response.error }) + ) + return Promise.resolve() + } + + // 4. Set responseObject to the result of creating a Response object, + // given response, "immutable", and relevantRealm. + responseObject = new Response() + responseObject[kState] = response + responseObject[kRealm] = relevantRealm + responseObject[kHeaders][kHeadersList] = response.headersList + responseObject[kHeaders][kGuard] = 'immutable' + responseObject[kHeaders][kRealm] = relevantRealm + + // 5. Resolve p with responseObject. + p.resolve(responseObject) + } + + controller = fetching({ + request, + processResponseEndOfBody: handleFetchDone, + processResponse, + dispatcher: init.dispatcher ?? getGlobalDispatcher() // undici + }) + + // 14. Return p. + return p.promise +} + +// https://fetch.spec.whatwg.org/#finalize-and-report-timing +function finalizeAndReportTiming (response, initiatorType = 'other') { + // 1. If response is an aborted network error, then return. + if (response.type === 'error' && response.aborted) { + return + } + + // 2. If response’s URL list is null or empty, then return. + if (!response.urlList?.length) { + return + } + + // 3. Let originalURL be response’s URL list[0]. + const originalURL = response.urlList[0] + + // 4. Let timingInfo be response’s timing info. + let timingInfo = response.timingInfo + + // 5. Let cacheState be response’s cache state. + let cacheState = response.cacheState + + // 6. If originalURL’s scheme is not an HTTP(S) scheme, then return. + if (!urlIsHttpHttpsScheme(originalURL)) { + return + } + + // 7. If timingInfo is null, then return. + if (timingInfo === null) { + return + } + + // 8. If response’s timing allow passed flag is not set, then: + if (!response.timingAllowPassed) { + // 1. Set timingInfo to a the result of creating an opaque timing info for timingInfo. + timingInfo = createOpaqueTimingInfo({ + startTime: timingInfo.startTime + }) + + // 2. Set cacheState to the empty string. + cacheState = '' + } + + // 9. Set timingInfo’s end time to the coarsened shared current time + // given global’s relevant settings object’s cross-origin isolated + // capability. + // TODO: given global’s relevant settings object’s cross-origin isolated + // capability? + timingInfo.endTime = coarsenedSharedCurrentTime() + + // 10. Set response’s timing info to timingInfo. + response.timingInfo = timingInfo + + // 11. Mark resource timing for timingInfo, originalURL, initiatorType, + // global, and cacheState. + markResourceTiming( + timingInfo, + originalURL, + initiatorType, + globalThis, + cacheState + ) +} + +// https://w3c.github.io/resource-timing/#dfn-mark-resource-timing +function markResourceTiming (timingInfo, originalURL, initiatorType, globalThis, cacheState) { + if (nodeMajor > 18 || (nodeMajor === 18 && nodeMinor >= 2)) { + performance.markResourceTiming(timingInfo, originalURL.href, initiatorType, globalThis, cacheState) + } +} + +// https://fetch.spec.whatwg.org/#abort-fetch +function abortFetch (p, request, responseObject, error) { + // Note: AbortSignal.reason was added in node v17.2.0 + // which would give us an undefined error to reject with. + // Remove this once node v16 is no longer supported. + if (!error) { + error = new DOMException('The operation was aborted.', 'AbortError') + } + + // 1. Reject promise with error. + p.reject(error) + + // 2. If request’s body is not null and is readable, then cancel request’s + // body with error. + if (request.body != null && isReadable(request.body?.stream)) { + request.body.stream.cancel(error).catch((err) => { + if (err.code === 'ERR_INVALID_STATE') { + // Node bug? + return + } + throw err + }) + } + + // 3. If responseObject is null, then return. + if (responseObject == null) { + return + } + + // 4. Let response be responseObject’s response. + const response = responseObject[kState] + + // 5. If response’s body is not null and is readable, then error response’s + // body with error. + if (response.body != null && isReadable(response.body?.stream)) { + response.body.stream.cancel(error).catch((err) => { + if (err.code === 'ERR_INVALID_STATE') { + // Node bug? + return + } + throw err + }) + } +} + +// https://fetch.spec.whatwg.org/#fetching +function fetching ({ + request, + processRequestBodyChunkLength, + processRequestEndOfBody, + processResponse, + processResponseEndOfBody, + processResponseConsumeBody, + useParallelQueue = false, + dispatcher // undici +}) { + // 1. Let taskDestination be null. + let taskDestination = null + + // 2. Let crossOriginIsolatedCapability be false. + let crossOriginIsolatedCapability = false + + // 3. If request’s client is non-null, then: + if (request.client != null) { + // 1. Set taskDestination to request’s client’s global object. + taskDestination = request.client.globalObject + + // 2. Set crossOriginIsolatedCapability to request’s client’s cross-origin + // isolated capability. + crossOriginIsolatedCapability = + request.client.crossOriginIsolatedCapability + } + + // 4. If useParallelQueue is true, then set taskDestination to the result of + // starting a new parallel queue. + // TODO + + // 5. Let timingInfo be a new fetch timing info whose start time and + // post-redirect start time are the coarsened shared current time given + // crossOriginIsolatedCapability. + const currenTime = coarsenedSharedCurrentTime(crossOriginIsolatedCapability) + const timingInfo = createOpaqueTimingInfo({ + startTime: currenTime + }) + + // 6. Let fetchParams be a new fetch params whose + // request is request, + // timing info is timingInfo, + // process request body chunk length is processRequestBodyChunkLength, + // process request end-of-body is processRequestEndOfBody, + // process response is processResponse, + // process response consume body is processResponseConsumeBody, + // process response end-of-body is processResponseEndOfBody, + // task destination is taskDestination, + // and cross-origin isolated capability is crossOriginIsolatedCapability. + const fetchParams = { + controller: new Fetch(dispatcher), + request, + timingInfo, + processRequestBodyChunkLength, + processRequestEndOfBody, + processResponse, + processResponseConsumeBody, + processResponseEndOfBody, + taskDestination, + crossOriginIsolatedCapability + } + + // 7. If request’s body is a byte sequence, then set request’s body to + // request’s body as a body. + // NOTE: Since fetching is only called from fetch, body should already be + // extracted. + assert(!request.body || request.body.stream) + + // 8. If request’s window is "client", then set request’s window to request’s + // client, if request’s client’s global object is a Window object; otherwise + // "no-window". + if (request.window === 'client') { + // TODO: What if request.client is null? + request.window = + request.client?.globalObject?.constructor?.name === 'Window' + ? request.client + : 'no-window' + } + + // 9. If request’s origin is "client", then set request’s origin to request’s + // client’s origin. + if (request.origin === 'client') { + // TODO: What if request.client is null? + request.origin = request.client?.origin + } + + // 10. If all of the following conditions are true: + // TODO + + // 11. If request’s policy container is "client", then: + if (request.policyContainer === 'client') { + // 1. If request’s client is non-null, then set request’s policy + // container to a clone of request’s client’s policy container. [HTML] + if (request.client != null) { + request.policyContainer = clonePolicyContainer( + request.client.policyContainer + ) + } else { + // 2. Otherwise, set request’s policy container to a new policy + // container. + request.policyContainer = makePolicyContainer() + } + } + + // 12. If request’s header list does not contain `Accept`, then: + if (!request.headersList.contains('accept')) { + // 1. Let value be `*/*`. + const value = '*/*' + + // 2. A user agent should set value to the first matching statement, if + // any, switching on request’s destination: + // "document" + // "frame" + // "iframe" + // `text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8` + // "image" + // `image/png,image/svg+xml,image/*;q=0.8,*/*;q=0.5` + // "style" + // `text/css,*/*;q=0.1` + // TODO + + // 3. Append `Accept`/value to request’s header list. + request.headersList.append('accept', value) + } + + // 13. If request’s header list does not contain `Accept-Language`, then + // user agents should append `Accept-Language`/an appropriate value to + // request’s header list. + if (!request.headersList.contains('accept-language')) { + request.headersList.append('accept-language', '*') + } + + // 14. If request’s priority is null, then use request’s initiator and + // destination appropriately in setting request’s priority to a + // user-agent-defined object. + if (request.priority === null) { + // TODO + } + + // 15. If request is a subresource request, then: + if (subresourceSet.has(request.destination)) { + // TODO + } + + // 16. Run main fetch given fetchParams. + mainFetch(fetchParams) + .catch(err => { + fetchParams.controller.terminate(err) + }) + + // 17. Return fetchParam's controller + return fetchParams.controller +} + +// https://fetch.spec.whatwg.org/#concept-main-fetch +async function mainFetch (fetchParams, recursive = false) { + // 1. Let request be fetchParams’s request. + const request = fetchParams.request + + // 2. Let response be null. + let response = null + + // 3. If request’s local-URLs-only flag is set and request’s current URL is + // not local, then set response to a network error. + if (request.localURLsOnly && !urlIsLocal(requestCurrentURL(request))) { + response = makeNetworkError('local URLs only') + } + + // 4. Run report Content Security Policy violations for request. + // TODO + + // 5. Upgrade request to a potentially trustworthy URL, if appropriate. + tryUpgradeRequestToAPotentiallyTrustworthyURL(request) + + // 6. If should request be blocked due to a bad port, should fetching request + // be blocked as mixed content, or should request be blocked by Content + // Security Policy returns blocked, then set response to a network error. + if (requestBadPort(request) === 'blocked') { + response = makeNetworkError('bad port') + } + // TODO: should fetching request be blocked as mixed content? + // TODO: should request be blocked by Content Security Policy? + + // 7. If request’s referrer policy is the empty string, then set request’s + // referrer policy to request’s policy container’s referrer policy. + if (request.referrerPolicy === '') { + request.referrerPolicy = request.policyContainer.referrerPolicy + } + + // 8. If request’s referrer is not "no-referrer", then set request’s + // referrer to the result of invoking determine request’s referrer. + if (request.referrer !== 'no-referrer') { + request.referrer = determineRequestsReferrer(request) + } + + // 9. Set request’s current URL’s scheme to "https" if all of the following + // conditions are true: + // - request’s current URL’s scheme is "http" + // - request’s current URL’s host is a domain + // - Matching request’s current URL’s host per Known HSTS Host Domain Name + // Matching results in either a superdomain match with an asserted + // includeSubDomains directive or a congruent match (with or without an + // asserted includeSubDomains directive). [HSTS] + // TODO + + // 10. If recursive is false, then run the remaining steps in parallel. + // TODO + + // 11. If response is null, then set response to the result of running + // the steps corresponding to the first matching statement: + if (response === null) { + response = await (async () => { + const currentURL = requestCurrentURL(request) + + if ( + // - request’s current URL’s origin is same origin with request’s origin, + // and request’s response tainting is "basic" + (sameOrigin(currentURL, request.url) && request.responseTainting === 'basic') || + // request’s current URL’s scheme is "data" + (currentURL.protocol === 'data:') || + // - request’s mode is "navigate" or "websocket" + (request.mode === 'navigate' || request.mode === 'websocket') + ) { + // 1. Set request’s response tainting to "basic". + request.responseTainting = 'basic' + + // 2. Return the result of running scheme fetch given fetchParams. + return await schemeFetch(fetchParams) + } + + // request’s mode is "same-origin" + if (request.mode === 'same-origin') { + // 1. Return a network error. + return makeNetworkError('request mode cannot be "same-origin"') + } + + // request’s mode is "no-cors" + if (request.mode === 'no-cors') { + // 1. If request’s redirect mode is not "follow", then return a network + // error. + if (request.redirect !== 'follow') { + return makeNetworkError( + 'redirect mode cannot be "follow" for "no-cors" request' + ) + } + + // 2. Set request’s response tainting to "opaque". + request.responseTainting = 'opaque' + + // 3. Return the result of running scheme fetch given fetchParams. + return await schemeFetch(fetchParams) + } + + // request’s current URL’s scheme is not an HTTP(S) scheme + if (!urlIsHttpHttpsScheme(requestCurrentURL(request))) { + // Return a network error. + return makeNetworkError('URL scheme must be a HTTP(S) scheme') + } + + // - request’s use-CORS-preflight flag is set + // - request’s unsafe-request flag is set and either request’s method is + // not a CORS-safelisted method or CORS-unsafe request-header names with + // request’s header list is not empty + // 1. Set request’s response tainting to "cors". + // 2. Let corsWithPreflightResponse be the result of running HTTP fetch + // given fetchParams and true. + // 3. If corsWithPreflightResponse is a network error, then clear cache + // entries using request. + // 4. Return corsWithPreflightResponse. + // TODO + + // Otherwise + // 1. Set request’s response tainting to "cors". + request.responseTainting = 'cors' + + // 2. Return the result of running HTTP fetch given fetchParams. + return await httpFetch(fetchParams) + })() + } + + // 12. If recursive is true, then return response. + if (recursive) { + return response + } + + // 13. If response is not a network error and response is not a filtered + // response, then: + if (response.status !== 0 && !response.internalResponse) { + // If request’s response tainting is "cors", then: + if (request.responseTainting === 'cors') { + // 1. Let headerNames be the result of extracting header list values + // given `Access-Control-Expose-Headers` and response’s header list. + // TODO + // 2. If request’s credentials mode is not "include" and headerNames + // contains `*`, then set response’s CORS-exposed header-name list to + // all unique header names in response’s header list. + // TODO + // 3. Otherwise, if headerNames is not null or failure, then set + // response’s CORS-exposed header-name list to headerNames. + // TODO + } + + // Set response to the following filtered response with response as its + // internal response, depending on request’s response tainting: + if (request.responseTainting === 'basic') { + response = filterResponse(response, 'basic') + } else if (request.responseTainting === 'cors') { + response = filterResponse(response, 'cors') + } else if (request.responseTainting === 'opaque') { + response = filterResponse(response, 'opaque') + } else { + assert(false) + } + } + + // 14. Let internalResponse be response, if response is a network error, + // and response’s internal response otherwise. + let internalResponse = + response.status === 0 ? response : response.internalResponse + + // 15. If internalResponse’s URL list is empty, then set it to a clone of + // request’s URL list. + if (internalResponse.urlList.length === 0) { + internalResponse.urlList.push(...request.urlList) + } + + // 16. If request’s timing allow failed flag is unset, then set + // internalResponse’s timing allow passed flag. + if (!request.timingAllowFailed) { + response.timingAllowPassed = true + } + + // 17. If response is not a network error and any of the following returns + // blocked + // - should internalResponse to request be blocked as mixed content + // - should internalResponse to request be blocked by Content Security Policy + // - should internalResponse to request be blocked due to its MIME type + // - should internalResponse to request be blocked due to nosniff + // TODO + + // 18. If response’s type is "opaque", internalResponse’s status is 206, + // internalResponse’s range-requested flag is set, and request’s header + // list does not contain `Range`, then set response and internalResponse + // to a network error. + if ( + response.type === 'opaque' && + internalResponse.status === 206 && + internalResponse.rangeRequested && + !request.headers.contains('range') + ) { + response = internalResponse = makeNetworkError() + } + + // 19. If response is not a network error and either request’s method is + // `HEAD` or `CONNECT`, or internalResponse’s status is a null body status, + // set internalResponse’s body to null and disregard any enqueuing toward + // it (if any). + if ( + response.status !== 0 && + (request.method === 'HEAD' || + request.method === 'CONNECT' || + nullBodyStatus.includes(internalResponse.status)) + ) { + internalResponse.body = null + fetchParams.controller.dump = true + } + + // 20. If request’s integrity metadata is not the empty string, then: + if (request.integrity) { + // 1. Let processBodyError be this step: run fetch finale given fetchParams + // and a network error. + const processBodyError = (reason) => + fetchFinale(fetchParams, makeNetworkError(reason)) + + // 2. If request’s response tainting is "opaque", or response’s body is null, + // then run processBodyError and abort these steps. + if (request.responseTainting === 'opaque' || response.body == null) { + processBodyError(response.error) + return + } + + // 3. Let processBody given bytes be these steps: + const processBody = (bytes) => { + // 1. If bytes do not match request’s integrity metadata, + // then run processBodyError and abort these steps. [SRI] + if (!bytesMatch(bytes, request.integrity)) { + processBodyError('integrity mismatch') + return + } + + // 2. Set response’s body to bytes as a body. + response.body = safelyExtractBody(bytes)[0] + + // 3. Run fetch finale given fetchParams and response. + fetchFinale(fetchParams, response) + } + + // 4. Fully read response’s body given processBody and processBodyError. + await fullyReadBody(response.body, processBody, processBodyError) + } else { + // 21. Otherwise, run fetch finale given fetchParams and response. + fetchFinale(fetchParams, response) + } +} + +// https://fetch.spec.whatwg.org/#concept-scheme-fetch +// given a fetch params fetchParams +function schemeFetch (fetchParams) { + // Note: since the connection is destroyed on redirect, which sets fetchParams to a + // cancelled state, we do not want this condition to trigger *unless* there have been + // no redirects. See https://github.com/nodejs/undici/issues/1776 + // 1. If fetchParams is canceled, then return the appropriate network error for fetchParams. + if (isCancelled(fetchParams) && fetchParams.request.redirectCount === 0) { + return Promise.resolve(makeAppropriateNetworkError(fetchParams)) + } + + // 2. Let request be fetchParams’s request. + const { request } = fetchParams + + const { protocol: scheme } = requestCurrentURL(request) + + // 3. Switch on request’s current URL’s scheme and run the associated steps: + switch (scheme) { + case 'about:': { + // If request’s current URL’s path is the string "blank", then return a new response + // whose status message is `OK`, header list is « (`Content-Type`, `text/html;charset=utf-8`) », + // and body is the empty byte sequence as a body. + + // Otherwise, return a network error. + return Promise.resolve(makeNetworkError('about scheme is not supported')) + } + case 'blob:': { + if (!resolveObjectURL) { + resolveObjectURL = (__nccwpck_require__(181).resolveObjectURL) + } + + // 1. Let blobURLEntry be request’s current URL’s blob URL entry. + const blobURLEntry = requestCurrentURL(request) + + // https://github.com/web-platform-tests/wpt/blob/7b0ebaccc62b566a1965396e5be7bb2bc06f841f/FileAPI/url/resources/fetch-tests.js#L52-L56 + // Buffer.resolveObjectURL does not ignore URL queries. + if (blobURLEntry.search.length !== 0) { + return Promise.resolve(makeNetworkError('NetworkError when attempting to fetch resource.')) + } + + const blobURLEntryObject = resolveObjectURL(blobURLEntry.toString()) + + // 2. If request’s method is not `GET`, blobURLEntry is null, or blobURLEntry’s + // object is not a Blob object, then return a network error. + if (request.method !== 'GET' || !isBlobLike(blobURLEntryObject)) { + return Promise.resolve(makeNetworkError('invalid method')) + } + + // 3. Let bodyWithType be the result of safely extracting blobURLEntry’s object. + const bodyWithType = safelyExtractBody(blobURLEntryObject) + + // 4. Let body be bodyWithType’s body. + const body = bodyWithType[0] + + // 5. Let length be body’s length, serialized and isomorphic encoded. + const length = isomorphicEncode(`${body.length}`) + + // 6. Let type be bodyWithType’s type if it is non-null; otherwise the empty byte sequence. + const type = bodyWithType[1] ?? '' + + // 7. Return a new response whose status message is `OK`, header list is + // « (`Content-Length`, length), (`Content-Type`, type) », and body is body. + const response = makeResponse({ + statusText: 'OK', + headersList: [ + ['content-length', { name: 'Content-Length', value: length }], + ['content-type', { name: 'Content-Type', value: type }] + ] + }) + + response.body = body + + return Promise.resolve(response) + } + case 'data:': { + // 1. Let dataURLStruct be the result of running the + // data: URL processor on request’s current URL. + const currentURL = requestCurrentURL(request) + const dataURLStruct = dataURLProcessor(currentURL) + + // 2. If dataURLStruct is failure, then return a + // network error. + if (dataURLStruct === 'failure') { + return Promise.resolve(makeNetworkError('failed to fetch the data URL')) + } + + // 3. Let mimeType be dataURLStruct’s MIME type, serialized. + const mimeType = serializeAMimeType(dataURLStruct.mimeType) + + // 4. Return a response whose status message is `OK`, + // header list is « (`Content-Type`, mimeType) », + // and body is dataURLStruct’s body as a body. + return Promise.resolve(makeResponse({ + statusText: 'OK', + headersList: [ + ['content-type', { name: 'Content-Type', value: mimeType }] + ], + body: safelyExtractBody(dataURLStruct.body)[0] + })) + } + case 'file:': { + // For now, unfortunate as it is, file URLs are left as an exercise for the reader. + // When in doubt, return a network error. + return Promise.resolve(makeNetworkError('not implemented... yet...')) + } + case 'http:': + case 'https:': { + // Return the result of running HTTP fetch given fetchParams. + + return httpFetch(fetchParams) + .catch((err) => makeNetworkError(err)) + } + default: { + return Promise.resolve(makeNetworkError('unknown scheme')) + } + } +} + +// https://fetch.spec.whatwg.org/#finalize-response +function finalizeResponse (fetchParams, response) { + // 1. Set fetchParams’s request’s done flag. + fetchParams.request.done = true + + // 2, If fetchParams’s process response done is not null, then queue a fetch + // task to run fetchParams’s process response done given response, with + // fetchParams’s task destination. + if (fetchParams.processResponseDone != null) { + queueMicrotask(() => fetchParams.processResponseDone(response)) + } +} + +// https://fetch.spec.whatwg.org/#fetch-finale +function fetchFinale (fetchParams, response) { + // 1. If response is a network error, then: + if (response.type === 'error') { + // 1. Set response’s URL list to « fetchParams’s request’s URL list[0] ». + response.urlList = [fetchParams.request.urlList[0]] + + // 2. Set response’s timing info to the result of creating an opaque timing + // info for fetchParams’s timing info. + response.timingInfo = createOpaqueTimingInfo({ + startTime: fetchParams.timingInfo.startTime + }) + } + + // 2. Let processResponseEndOfBody be the following steps: + const processResponseEndOfBody = () => { + // 1. Set fetchParams’s request’s done flag. + fetchParams.request.done = true + + // If fetchParams’s process response end-of-body is not null, + // then queue a fetch task to run fetchParams’s process response + // end-of-body given response with fetchParams’s task destination. + if (fetchParams.processResponseEndOfBody != null) { + queueMicrotask(() => fetchParams.processResponseEndOfBody(response)) + } + } + + // 3. If fetchParams’s process response is non-null, then queue a fetch task + // to run fetchParams’s process response given response, with fetchParams’s + // task destination. + if (fetchParams.processResponse != null) { + queueMicrotask(() => fetchParams.processResponse(response)) + } + + // 4. If response’s body is null, then run processResponseEndOfBody. + if (response.body == null) { + processResponseEndOfBody() + } else { + // 5. Otherwise: + + // 1. Let transformStream be a new a TransformStream. + + // 2. Let identityTransformAlgorithm be an algorithm which, given chunk, + // enqueues chunk in transformStream. + const identityTransformAlgorithm = (chunk, controller) => { + controller.enqueue(chunk) + } + + // 3. Set up transformStream with transformAlgorithm set to identityTransformAlgorithm + // and flushAlgorithm set to processResponseEndOfBody. + const transformStream = new TransformStream({ + start () {}, + transform: identityTransformAlgorithm, + flush: processResponseEndOfBody + }, { + size () { + return 1 + } + }, { + size () { + return 1 + } + }) + + // 4. Set response’s body to the result of piping response’s body through transformStream. + response.body = { stream: response.body.stream.pipeThrough(transformStream) } + } + + // 6. If fetchParams’s process response consume body is non-null, then: + if (fetchParams.processResponseConsumeBody != null) { + // 1. Let processBody given nullOrBytes be this step: run fetchParams’s + // process response consume body given response and nullOrBytes. + const processBody = (nullOrBytes) => fetchParams.processResponseConsumeBody(response, nullOrBytes) + + // 2. Let processBodyError be this step: run fetchParams’s process + // response consume body given response and failure. + const processBodyError = (failure) => fetchParams.processResponseConsumeBody(response, failure) + + // 3. If response’s body is null, then queue a fetch task to run processBody + // given null, with fetchParams’s task destination. + if (response.body == null) { + queueMicrotask(() => processBody(null)) + } else { + // 4. Otherwise, fully read response’s body given processBody, processBodyError, + // and fetchParams’s task destination. + return fullyReadBody(response.body, processBody, processBodyError) + } + return Promise.resolve() + } +} + +// https://fetch.spec.whatwg.org/#http-fetch +async function httpFetch (fetchParams) { + // 1. Let request be fetchParams’s request. + const request = fetchParams.request + + // 2. Let response be null. + let response = null + + // 3. Let actualResponse be null. + let actualResponse = null + + // 4. Let timingInfo be fetchParams’s timing info. + const timingInfo = fetchParams.timingInfo + + // 5. If request’s service-workers mode is "all", then: + if (request.serviceWorkers === 'all') { + // TODO + } + + // 6. If response is null, then: + if (response === null) { + // 1. If makeCORSPreflight is true and one of these conditions is true: + // TODO + + // 2. If request’s redirect mode is "follow", then set request’s + // service-workers mode to "none". + if (request.redirect === 'follow') { + request.serviceWorkers = 'none' + } + + // 3. Set response and actualResponse to the result of running + // HTTP-network-or-cache fetch given fetchParams. + actualResponse = response = await httpNetworkOrCacheFetch(fetchParams) + + // 4. If request’s response tainting is "cors" and a CORS check + // for request and response returns failure, then return a network error. + if ( + request.responseTainting === 'cors' && + corsCheck(request, response) === 'failure' + ) { + return makeNetworkError('cors failure') + } + + // 5. If the TAO check for request and response returns failure, then set + // request’s timing allow failed flag. + if (TAOCheck(request, response) === 'failure') { + request.timingAllowFailed = true + } + } + + // 7. If either request’s response tainting or response’s type + // is "opaque", and the cross-origin resource policy check with + // request’s origin, request’s client, request’s destination, + // and actualResponse returns blocked, then return a network error. + if ( + (request.responseTainting === 'opaque' || response.type === 'opaque') && + crossOriginResourcePolicyCheck( + request.origin, + request.client, + request.destination, + actualResponse + ) === 'blocked' + ) { + return makeNetworkError('blocked') + } + + // 8. If actualResponse’s status is a redirect status, then: + if (redirectStatusSet.has(actualResponse.status)) { + // 1. If actualResponse’s status is not 303, request’s body is not null, + // and the connection uses HTTP/2, then user agents may, and are even + // encouraged to, transmit an RST_STREAM frame. + // See, https://github.com/whatwg/fetch/issues/1288 + if (request.redirect !== 'manual') { + fetchParams.controller.connection.destroy() + } + + // 2. Switch on request’s redirect mode: + if (request.redirect === 'error') { + // Set response to a network error. + response = makeNetworkError('unexpected redirect') + } else if (request.redirect === 'manual') { + // Set response to an opaque-redirect filtered response whose internal + // response is actualResponse. + // NOTE(spec): On the web this would return an `opaqueredirect` response, + // but that doesn't make sense server side. + // See https://github.com/nodejs/undici/issues/1193. + response = actualResponse + } else if (request.redirect === 'follow') { + // Set response to the result of running HTTP-redirect fetch given + // fetchParams and response. + response = await httpRedirectFetch(fetchParams, response) + } else { + assert(false) + } + } + + // 9. Set response’s timing info to timingInfo. + response.timingInfo = timingInfo + + // 10. Return response. + return response +} + +// https://fetch.spec.whatwg.org/#http-redirect-fetch +function httpRedirectFetch (fetchParams, response) { + // 1. Let request be fetchParams’s request. + const request = fetchParams.request + + // 2. Let actualResponse be response, if response is not a filtered response, + // and response’s internal response otherwise. + const actualResponse = response.internalResponse + ? response.internalResponse + : response + + // 3. Let locationURL be actualResponse’s location URL given request’s current + // URL’s fragment. + let locationURL + + try { + locationURL = responseLocationURL( + actualResponse, + requestCurrentURL(request).hash + ) + + // 4. If locationURL is null, then return response. + if (locationURL == null) { + return response + } + } catch (err) { + // 5. If locationURL is failure, then return a network error. + return Promise.resolve(makeNetworkError(err)) + } + + // 6. If locationURL’s scheme is not an HTTP(S) scheme, then return a network + // error. + if (!urlIsHttpHttpsScheme(locationURL)) { + return Promise.resolve(makeNetworkError('URL scheme must be a HTTP(S) scheme')) + } + + // 7. If request’s redirect count is 20, then return a network error. + if (request.redirectCount === 20) { + return Promise.resolve(makeNetworkError('redirect count exceeded')) + } + + // 8. Increase request’s redirect count by 1. + request.redirectCount += 1 + + // 9. If request’s mode is "cors", locationURL includes credentials, and + // request’s origin is not same origin with locationURL’s origin, then return + // a network error. + if ( + request.mode === 'cors' && + (locationURL.username || locationURL.password) && + !sameOrigin(request, locationURL) + ) { + return Promise.resolve(makeNetworkError('cross origin not allowed for request mode "cors"')) + } + + // 10. If request’s response tainting is "cors" and locationURL includes + // credentials, then return a network error. + if ( + request.responseTainting === 'cors' && + (locationURL.username || locationURL.password) + ) { + return Promise.resolve(makeNetworkError( + 'URL cannot contain credentials for request mode "cors"' + )) + } + + // 11. If actualResponse’s status is not 303, request’s body is non-null, + // and request’s body’s source is null, then return a network error. + if ( + actualResponse.status !== 303 && + request.body != null && + request.body.source == null + ) { + return Promise.resolve(makeNetworkError()) + } + + // 12. If one of the following is true + // - actualResponse’s status is 301 or 302 and request’s method is `POST` + // - actualResponse’s status is 303 and request’s method is not `GET` or `HEAD` + if ( + ([301, 302].includes(actualResponse.status) && request.method === 'POST') || + (actualResponse.status === 303 && + !GET_OR_HEAD.includes(request.method)) + ) { + // then: + // 1. Set request’s method to `GET` and request’s body to null. + request.method = 'GET' + request.body = null + + // 2. For each headerName of request-body-header name, delete headerName from + // request’s header list. + for (const headerName of requestBodyHeader) { + request.headersList.delete(headerName) + } + } + + // 13. If request’s current URL’s origin is not same origin with locationURL’s + // origin, then for each headerName of CORS non-wildcard request-header name, + // delete headerName from request’s header list. + if (!sameOrigin(requestCurrentURL(request), locationURL)) { + // https://fetch.spec.whatwg.org/#cors-non-wildcard-request-header-name + request.headersList.delete('authorization') + + // https://fetch.spec.whatwg.org/#authentication-entries + request.headersList.delete('proxy-authorization', true) + + // "Cookie" and "Host" are forbidden request-headers, which undici doesn't implement. + request.headersList.delete('cookie') + request.headersList.delete('host') + } + + // 14. If request’s body is non-null, then set request’s body to the first return + // value of safely extracting request’s body’s source. + if (request.body != null) { + assert(request.body.source != null) + request.body = safelyExtractBody(request.body.source)[0] + } + + // 15. Let timingInfo be fetchParams’s timing info. + const timingInfo = fetchParams.timingInfo + + // 16. Set timingInfo’s redirect end time and post-redirect start time to the + // coarsened shared current time given fetchParams’s cross-origin isolated + // capability. + timingInfo.redirectEndTime = timingInfo.postRedirectStartTime = + coarsenedSharedCurrentTime(fetchParams.crossOriginIsolatedCapability) + + // 17. If timingInfo’s redirect start time is 0, then set timingInfo’s + // redirect start time to timingInfo’s start time. + if (timingInfo.redirectStartTime === 0) { + timingInfo.redirectStartTime = timingInfo.startTime + } + + // 18. Append locationURL to request’s URL list. + request.urlList.push(locationURL) + + // 19. Invoke set request’s referrer policy on redirect on request and + // actualResponse. + setRequestReferrerPolicyOnRedirect(request, actualResponse) + + // 20. Return the result of running main fetch given fetchParams and true. + return mainFetch(fetchParams, true) +} + +// https://fetch.spec.whatwg.org/#http-network-or-cache-fetch +async function httpNetworkOrCacheFetch ( + fetchParams, + isAuthenticationFetch = false, + isNewConnectionFetch = false +) { + // 1. Let request be fetchParams’s request. + const request = fetchParams.request + + // 2. Let httpFetchParams be null. + let httpFetchParams = null + + // 3. Let httpRequest be null. + let httpRequest = null + + // 4. Let response be null. + let response = null + + // 5. Let storedResponse be null. + // TODO: cache + + // 6. Let httpCache be null. + const httpCache = null + + // 7. Let the revalidatingFlag be unset. + const revalidatingFlag = false + + // 8. Run these steps, but abort when the ongoing fetch is terminated: + + // 1. If request’s window is "no-window" and request’s redirect mode is + // "error", then set httpFetchParams to fetchParams and httpRequest to + // request. + if (request.window === 'no-window' && request.redirect === 'error') { + httpFetchParams = fetchParams + httpRequest = request + } else { + // Otherwise: + + // 1. Set httpRequest to a clone of request. + httpRequest = makeRequest(request) + + // 2. Set httpFetchParams to a copy of fetchParams. + httpFetchParams = { ...fetchParams } + + // 3. Set httpFetchParams’s request to httpRequest. + httpFetchParams.request = httpRequest + } + + // 3. Let includeCredentials be true if one of + const includeCredentials = + request.credentials === 'include' || + (request.credentials === 'same-origin' && + request.responseTainting === 'basic') + + // 4. Let contentLength be httpRequest’s body’s length, if httpRequest’s + // body is non-null; otherwise null. + const contentLength = httpRequest.body ? httpRequest.body.length : null + + // 5. Let contentLengthHeaderValue be null. + let contentLengthHeaderValue = null + + // 6. If httpRequest’s body is null and httpRequest’s method is `POST` or + // `PUT`, then set contentLengthHeaderValue to `0`. + if ( + httpRequest.body == null && + ['POST', 'PUT'].includes(httpRequest.method) + ) { + contentLengthHeaderValue = '0' + } + + // 7. If contentLength is non-null, then set contentLengthHeaderValue to + // contentLength, serialized and isomorphic encoded. + if (contentLength != null) { + contentLengthHeaderValue = isomorphicEncode(`${contentLength}`) + } + + // 8. If contentLengthHeaderValue is non-null, then append + // `Content-Length`/contentLengthHeaderValue to httpRequest’s header + // list. + if (contentLengthHeaderValue != null) { + httpRequest.headersList.append('content-length', contentLengthHeaderValue) + } + + // 9. If contentLengthHeaderValue is non-null, then append (`Content-Length`, + // contentLengthHeaderValue) to httpRequest’s header list. + + // 10. If contentLength is non-null and httpRequest’s keepalive is true, + // then: + if (contentLength != null && httpRequest.keepalive) { + // NOTE: keepalive is a noop outside of browser context. + } + + // 11. If httpRequest’s referrer is a URL, then append + // `Referer`/httpRequest’s referrer, serialized and isomorphic encoded, + // to httpRequest’s header list. + if (httpRequest.referrer instanceof URL) { + httpRequest.headersList.append('referer', isomorphicEncode(httpRequest.referrer.href)) + } + + // 12. Append a request `Origin` header for httpRequest. + appendRequestOriginHeader(httpRequest) + + // 13. Append the Fetch metadata headers for httpRequest. [FETCH-METADATA] + appendFetchMetadata(httpRequest) + + // 14. If httpRequest’s header list does not contain `User-Agent`, then + // user agents should append `User-Agent`/default `User-Agent` value to + // httpRequest’s header list. + if (!httpRequest.headersList.contains('user-agent')) { + httpRequest.headersList.append('user-agent', typeof esbuildDetection === 'undefined' ? 'undici' : 'node') + } + + // 15. If httpRequest’s cache mode is "default" and httpRequest’s header + // list contains `If-Modified-Since`, `If-None-Match`, + // `If-Unmodified-Since`, `If-Match`, or `If-Range`, then set + // httpRequest’s cache mode to "no-store". + if ( + httpRequest.cache === 'default' && + (httpRequest.headersList.contains('if-modified-since') || + httpRequest.headersList.contains('if-none-match') || + httpRequest.headersList.contains('if-unmodified-since') || + httpRequest.headersList.contains('if-match') || + httpRequest.headersList.contains('if-range')) + ) { + httpRequest.cache = 'no-store' + } + + // 16. If httpRequest’s cache mode is "no-cache", httpRequest’s prevent + // no-cache cache-control header modification flag is unset, and + // httpRequest’s header list does not contain `Cache-Control`, then append + // `Cache-Control`/`max-age=0` to httpRequest’s header list. + if ( + httpRequest.cache === 'no-cache' && + !httpRequest.preventNoCacheCacheControlHeaderModification && + !httpRequest.headersList.contains('cache-control') + ) { + httpRequest.headersList.append('cache-control', 'max-age=0') + } + + // 17. If httpRequest’s cache mode is "no-store" or "reload", then: + if (httpRequest.cache === 'no-store' || httpRequest.cache === 'reload') { + // 1. If httpRequest’s header list does not contain `Pragma`, then append + // `Pragma`/`no-cache` to httpRequest’s header list. + if (!httpRequest.headersList.contains('pragma')) { + httpRequest.headersList.append('pragma', 'no-cache') + } + + // 2. If httpRequest’s header list does not contain `Cache-Control`, + // then append `Cache-Control`/`no-cache` to httpRequest’s header list. + if (!httpRequest.headersList.contains('cache-control')) { + httpRequest.headersList.append('cache-control', 'no-cache') + } + } + + // 18. If httpRequest’s header list contains `Range`, then append + // `Accept-Encoding`/`identity` to httpRequest’s header list. + if (httpRequest.headersList.contains('range')) { + httpRequest.headersList.append('accept-encoding', 'identity') + } + + // 19. Modify httpRequest’s header list per HTTP. Do not append a given + // header if httpRequest’s header list contains that header’s name. + // TODO: https://github.com/whatwg/fetch/issues/1285#issuecomment-896560129 + if (!httpRequest.headersList.contains('accept-encoding')) { + if (urlHasHttpsScheme(requestCurrentURL(httpRequest))) { + httpRequest.headersList.append('accept-encoding', 'br, gzip, deflate') + } else { + httpRequest.headersList.append('accept-encoding', 'gzip, deflate') + } + } + + httpRequest.headersList.delete('host') + + // 20. If includeCredentials is true, then: + if (includeCredentials) { + // 1. If the user agent is not configured to block cookies for httpRequest + // (see section 7 of [COOKIES]), then: + // TODO: credentials + // 2. If httpRequest’s header list does not contain `Authorization`, then: + // TODO: credentials + } + + // 21. If there’s a proxy-authentication entry, use it as appropriate. + // TODO: proxy-authentication + + // 22. Set httpCache to the result of determining the HTTP cache + // partition, given httpRequest. + // TODO: cache + + // 23. If httpCache is null, then set httpRequest’s cache mode to + // "no-store". + if (httpCache == null) { + httpRequest.cache = 'no-store' + } + + // 24. If httpRequest’s cache mode is neither "no-store" nor "reload", + // then: + if (httpRequest.mode !== 'no-store' && httpRequest.mode !== 'reload') { + // TODO: cache + } + + // 9. If aborted, then return the appropriate network error for fetchParams. + // TODO + + // 10. If response is null, then: + if (response == null) { + // 1. If httpRequest’s cache mode is "only-if-cached", then return a + // network error. + if (httpRequest.mode === 'only-if-cached') { + return makeNetworkError('only if cached') + } + + // 2. Let forwardResponse be the result of running HTTP-network fetch + // given httpFetchParams, includeCredentials, and isNewConnectionFetch. + const forwardResponse = await httpNetworkFetch( + httpFetchParams, + includeCredentials, + isNewConnectionFetch + ) + + // 3. If httpRequest’s method is unsafe and forwardResponse’s status is + // in the range 200 to 399, inclusive, invalidate appropriate stored + // responses in httpCache, as per the "Invalidation" chapter of HTTP + // Caching, and set storedResponse to null. [HTTP-CACHING] + if ( + !safeMethodsSet.has(httpRequest.method) && + forwardResponse.status >= 200 && + forwardResponse.status <= 399 + ) { + // TODO: cache + } + + // 4. If the revalidatingFlag is set and forwardResponse’s status is 304, + // then: + if (revalidatingFlag && forwardResponse.status === 304) { + // TODO: cache + } + + // 5. If response is null, then: + if (response == null) { + // 1. Set response to forwardResponse. + response = forwardResponse + + // 2. Store httpRequest and forwardResponse in httpCache, as per the + // "Storing Responses in Caches" chapter of HTTP Caching. [HTTP-CACHING] + // TODO: cache + } + } + + // 11. Set response’s URL list to a clone of httpRequest’s URL list. + response.urlList = [...httpRequest.urlList] + + // 12. If httpRequest’s header list contains `Range`, then set response’s + // range-requested flag. + if (httpRequest.headersList.contains('range')) { + response.rangeRequested = true + } + + // 13. Set response’s request-includes-credentials to includeCredentials. + response.requestIncludesCredentials = includeCredentials + + // 14. If response’s status is 401, httpRequest’s response tainting is not + // "cors", includeCredentials is true, and request’s window is an environment + // settings object, then: + // TODO + + // 15. If response’s status is 407, then: + if (response.status === 407) { + // 1. If request’s window is "no-window", then return a network error. + if (request.window === 'no-window') { + return makeNetworkError() + } + + // 2. ??? + + // 3. If fetchParams is canceled, then return the appropriate network error for fetchParams. + if (isCancelled(fetchParams)) { + return makeAppropriateNetworkError(fetchParams) + } + + // 4. Prompt the end user as appropriate in request’s window and store + // the result as a proxy-authentication entry. [HTTP-AUTH] + // TODO: Invoke some kind of callback? + + // 5. Set response to the result of running HTTP-network-or-cache fetch given + // fetchParams. + // TODO + return makeNetworkError('proxy authentication required') + } + + // 16. If all of the following are true + if ( + // response’s status is 421 + response.status === 421 && + // isNewConnectionFetch is false + !isNewConnectionFetch && + // request’s body is null, or request’s body is non-null and request’s body’s source is non-null + (request.body == null || request.body.source != null) + ) { + // then: + + // 1. If fetchParams is canceled, then return the appropriate network error for fetchParams. + if (isCancelled(fetchParams)) { + return makeAppropriateNetworkError(fetchParams) + } + + // 2. Set response to the result of running HTTP-network-or-cache + // fetch given fetchParams, isAuthenticationFetch, and true. + + // TODO (spec): The spec doesn't specify this but we need to cancel + // the active response before we can start a new one. + // https://github.com/whatwg/fetch/issues/1293 + fetchParams.controller.connection.destroy() + + response = await httpNetworkOrCacheFetch( + fetchParams, + isAuthenticationFetch, + true + ) + } + + // 17. If isAuthenticationFetch is true, then create an authentication entry + if (isAuthenticationFetch) { + // TODO + } + + // 18. Return response. + return response +} + +// https://fetch.spec.whatwg.org/#http-network-fetch +async function httpNetworkFetch ( + fetchParams, + includeCredentials = false, + forceNewConnection = false +) { + assert(!fetchParams.controller.connection || fetchParams.controller.connection.destroyed) + + fetchParams.controller.connection = { + abort: null, + destroyed: false, + destroy (err) { + if (!this.destroyed) { + this.destroyed = true + this.abort?.(err ?? new DOMException('The operation was aborted.', 'AbortError')) + } + } + } + + // 1. Let request be fetchParams’s request. + const request = fetchParams.request + + // 2. Let response be null. + let response = null + + // 3. Let timingInfo be fetchParams’s timing info. + const timingInfo = fetchParams.timingInfo + + // 4. Let httpCache be the result of determining the HTTP cache partition, + // given request. + // TODO: cache + const httpCache = null + + // 5. If httpCache is null, then set request’s cache mode to "no-store". + if (httpCache == null) { + request.cache = 'no-store' + } + + // 6. Let networkPartitionKey be the result of determining the network + // partition key given request. + // TODO + + // 7. Let newConnection be "yes" if forceNewConnection is true; otherwise + // "no". + const newConnection = forceNewConnection ? 'yes' : 'no' // eslint-disable-line no-unused-vars + + // 8. Switch on request’s mode: + if (request.mode === 'websocket') { + // Let connection be the result of obtaining a WebSocket connection, + // given request’s current URL. + // TODO + } else { + // Let connection be the result of obtaining a connection, given + // networkPartitionKey, request’s current URL’s origin, + // includeCredentials, and forceNewConnection. + // TODO + } + + // 9. Run these steps, but abort when the ongoing fetch is terminated: + + // 1. If connection is failure, then return a network error. + + // 2. Set timingInfo’s final connection timing info to the result of + // calling clamp and coarsen connection timing info with connection’s + // timing info, timingInfo’s post-redirect start time, and fetchParams’s + // cross-origin isolated capability. + + // 3. If connection is not an HTTP/2 connection, request’s body is non-null, + // and request’s body’s source is null, then append (`Transfer-Encoding`, + // `chunked`) to request’s header list. + + // 4. Set timingInfo’s final network-request start time to the coarsened + // shared current time given fetchParams’s cross-origin isolated + // capability. + + // 5. Set response to the result of making an HTTP request over connection + // using request with the following caveats: + + // - Follow the relevant requirements from HTTP. [HTTP] [HTTP-SEMANTICS] + // [HTTP-COND] [HTTP-CACHING] [HTTP-AUTH] + + // - If request’s body is non-null, and request’s body’s source is null, + // then the user agent may have a buffer of up to 64 kibibytes and store + // a part of request’s body in that buffer. If the user agent reads from + // request’s body beyond that buffer’s size and the user agent needs to + // resend request, then instead return a network error. + + // - Set timingInfo’s final network-response start time to the coarsened + // shared current time given fetchParams’s cross-origin isolated capability, + // immediately after the user agent’s HTTP parser receives the first byte + // of the response (e.g., frame header bytes for HTTP/2 or response status + // line for HTTP/1.x). + + // - Wait until all the headers are transmitted. + + // - Any responses whose status is in the range 100 to 199, inclusive, + // and is not 101, are to be ignored, except for the purposes of setting + // timingInfo’s final network-response start time above. + + // - If request’s header list contains `Transfer-Encoding`/`chunked` and + // response is transferred via HTTP/1.0 or older, then return a network + // error. + + // - If the HTTP request results in a TLS client certificate dialog, then: + + // 1. If request’s window is an environment settings object, make the + // dialog available in request’s window. + + // 2. Otherwise, return a network error. + + // To transmit request’s body body, run these steps: + let requestBody = null + // 1. If body is null and fetchParams’s process request end-of-body is + // non-null, then queue a fetch task given fetchParams’s process request + // end-of-body and fetchParams’s task destination. + if (request.body == null && fetchParams.processRequestEndOfBody) { + queueMicrotask(() => fetchParams.processRequestEndOfBody()) + } else if (request.body != null) { + // 2. Otherwise, if body is non-null: + + // 1. Let processBodyChunk given bytes be these steps: + const processBodyChunk = async function * (bytes) { + // 1. If the ongoing fetch is terminated, then abort these steps. + if (isCancelled(fetchParams)) { + return + } + + // 2. Run this step in parallel: transmit bytes. + yield bytes + + // 3. If fetchParams’s process request body is non-null, then run + // fetchParams’s process request body given bytes’s length. + fetchParams.processRequestBodyChunkLength?.(bytes.byteLength) + } + + // 2. Let processEndOfBody be these steps: + const processEndOfBody = () => { + // 1. If fetchParams is canceled, then abort these steps. + if (isCancelled(fetchParams)) { + return + } + + // 2. If fetchParams’s process request end-of-body is non-null, + // then run fetchParams’s process request end-of-body. + if (fetchParams.processRequestEndOfBody) { + fetchParams.processRequestEndOfBody() + } + } + + // 3. Let processBodyError given e be these steps: + const processBodyError = (e) => { + // 1. If fetchParams is canceled, then abort these steps. + if (isCancelled(fetchParams)) { + return + } + + // 2. If e is an "AbortError" DOMException, then abort fetchParams’s controller. + if (e.name === 'AbortError') { + fetchParams.controller.abort() + } else { + fetchParams.controller.terminate(e) + } + } + + // 4. Incrementally read request’s body given processBodyChunk, processEndOfBody, + // processBodyError, and fetchParams’s task destination. + requestBody = (async function * () { + try { + for await (const bytes of request.body.stream) { + yield * processBodyChunk(bytes) + } + processEndOfBody() + } catch (err) { + processBodyError(err) + } + })() + } + + try { + // socket is only provided for websockets + const { body, status, statusText, headersList, socket } = await dispatch({ body: requestBody }) + + if (socket) { + response = makeResponse({ status, statusText, headersList, socket }) + } else { + const iterator = body[Symbol.asyncIterator]() + fetchParams.controller.next = () => iterator.next() + + response = makeResponse({ status, statusText, headersList }) + } + } catch (err) { + // 10. If aborted, then: + if (err.name === 'AbortError') { + // 1. If connection uses HTTP/2, then transmit an RST_STREAM frame. + fetchParams.controller.connection.destroy() + + // 2. Return the appropriate network error for fetchParams. + return makeAppropriateNetworkError(fetchParams, err) + } + + return makeNetworkError(err) + } + + // 11. Let pullAlgorithm be an action that resumes the ongoing fetch + // if it is suspended. + const pullAlgorithm = () => { + fetchParams.controller.resume() + } + + // 12. Let cancelAlgorithm be an algorithm that aborts fetchParams’s + // controller with reason, given reason. + const cancelAlgorithm = (reason) => { + fetchParams.controller.abort(reason) + } + + // 13. Let highWaterMark be a non-negative, non-NaN number, chosen by + // the user agent. + // TODO + + // 14. Let sizeAlgorithm be an algorithm that accepts a chunk object + // and returns a non-negative, non-NaN, non-infinite number, chosen by the user agent. + // TODO + + // 15. Let stream be a new ReadableStream. + // 16. Set up stream with pullAlgorithm set to pullAlgorithm, + // cancelAlgorithm set to cancelAlgorithm, highWaterMark set to + // highWaterMark, and sizeAlgorithm set to sizeAlgorithm. + if (!ReadableStream) { + ReadableStream = (__nccwpck_require__(3774).ReadableStream) + } + + const stream = new ReadableStream( + { + async start (controller) { + fetchParams.controller.controller = controller + }, + async pull (controller) { + await pullAlgorithm(controller) + }, + async cancel (reason) { + await cancelAlgorithm(reason) + } + }, + { + highWaterMark: 0, + size () { + return 1 + } + } + ) + + // 17. Run these steps, but abort when the ongoing fetch is terminated: + + // 1. Set response’s body to a new body whose stream is stream. + response.body = { stream } + + // 2. If response is not a network error and request’s cache mode is + // not "no-store", then update response in httpCache for request. + // TODO + + // 3. If includeCredentials is true and the user agent is not configured + // to block cookies for request (see section 7 of [COOKIES]), then run the + // "set-cookie-string" parsing algorithm (see section 5.2 of [COOKIES]) on + // the value of each header whose name is a byte-case-insensitive match for + // `Set-Cookie` in response’s header list, if any, and request’s current URL. + // TODO + + // 18. If aborted, then: + // TODO + + // 19. Run these steps in parallel: + + // 1. Run these steps, but abort when fetchParams is canceled: + fetchParams.controller.on('terminated', onAborted) + fetchParams.controller.resume = async () => { + // 1. While true + while (true) { + // 1-3. See onData... + + // 4. Set bytes to the result of handling content codings given + // codings and bytes. + let bytes + let isFailure + try { + const { done, value } = await fetchParams.controller.next() + + if (isAborted(fetchParams)) { + break + } + + bytes = done ? undefined : value + } catch (err) { + if (fetchParams.controller.ended && !timingInfo.encodedBodySize) { + // zlib doesn't like empty streams. + bytes = undefined + } else { + bytes = err + + // err may be propagated from the result of calling readablestream.cancel, + // which might not be an error. https://github.com/nodejs/undici/issues/2009 + isFailure = true + } + } + + if (bytes === undefined) { + // 2. Otherwise, if the bytes transmission for response’s message + // body is done normally and stream is readable, then close + // stream, finalize response for fetchParams and response, and + // abort these in-parallel steps. + readableStreamClose(fetchParams.controller.controller) + + finalizeResponse(fetchParams, response) + + return + } + + // 5. Increase timingInfo’s decoded body size by bytes’s length. + timingInfo.decodedBodySize += bytes?.byteLength ?? 0 + + // 6. If bytes is failure, then terminate fetchParams’s controller. + if (isFailure) { + fetchParams.controller.terminate(bytes) + return + } + + // 7. Enqueue a Uint8Array wrapping an ArrayBuffer containing bytes + // into stream. + fetchParams.controller.controller.enqueue(new Uint8Array(bytes)) + + // 8. If stream is errored, then terminate the ongoing fetch. + if (isErrored(stream)) { + fetchParams.controller.terminate() + return + } + + // 9. If stream doesn’t need more data ask the user agent to suspend + // the ongoing fetch. + if (!fetchParams.controller.controller.desiredSize) { + return + } + } + } + + // 2. If aborted, then: + function onAborted (reason) { + // 2. If fetchParams is aborted, then: + if (isAborted(fetchParams)) { + // 1. Set response’s aborted flag. + response.aborted = true + + // 2. If stream is readable, then error stream with the result of + // deserialize a serialized abort reason given fetchParams’s + // controller’s serialized abort reason and an + // implementation-defined realm. + if (isReadable(stream)) { + fetchParams.controller.controller.error( + fetchParams.controller.serializedAbortReason + ) + } + } else { + // 3. Otherwise, if stream is readable, error stream with a TypeError. + if (isReadable(stream)) { + fetchParams.controller.controller.error(new TypeError('terminated', { + cause: isErrorLike(reason) ? reason : undefined + })) + } + } + + // 4. If connection uses HTTP/2, then transmit an RST_STREAM frame. + // 5. Otherwise, the user agent should close connection unless it would be bad for performance to do so. + fetchParams.controller.connection.destroy() + } + + // 20. Return response. + return response + + async function dispatch ({ body }) { + const url = requestCurrentURL(request) + /** @type {import('../..').Agent} */ + const agent = fetchParams.controller.dispatcher + + return new Promise((resolve, reject) => agent.dispatch( + { + path: url.pathname + url.search, + origin: url.origin, + method: request.method, + body: fetchParams.controller.dispatcher.isMockActive ? request.body && (request.body.source || request.body.stream) : body, + headers: request.headersList.entries, + maxRedirections: 0, + upgrade: request.mode === 'websocket' ? 'websocket' : undefined + }, + { + body: null, + abort: null, + + onConnect (abort) { + // TODO (fix): Do we need connection here? + const { connection } = fetchParams.controller + + if (connection.destroyed) { + abort(new DOMException('The operation was aborted.', 'AbortError')) + } else { + fetchParams.controller.on('terminated', abort) + this.abort = connection.abort = abort + } + }, + + onHeaders (status, headersList, resume, statusText) { + if (status < 200) { + return + } + + let codings = [] + let location = '' + + const headers = new Headers() + + // For H2, the headers are a plain JS object + // We distinguish between them and iterate accordingly + if (Array.isArray(headersList)) { + for (let n = 0; n < headersList.length; n += 2) { + const key = headersList[n + 0].toString('latin1') + const val = headersList[n + 1].toString('latin1') + if (key.toLowerCase() === 'content-encoding') { + // https://www.rfc-editor.org/rfc/rfc7231#section-3.1.2.1 + // "All content-coding values are case-insensitive..." + codings = val.toLowerCase().split(',').map((x) => x.trim()) + } else if (key.toLowerCase() === 'location') { + location = val + } + + headers[kHeadersList].append(key, val) + } + } else { + const keys = Object.keys(headersList) + for (const key of keys) { + const val = headersList[key] + if (key.toLowerCase() === 'content-encoding') { + // https://www.rfc-editor.org/rfc/rfc7231#section-3.1.2.1 + // "All content-coding values are case-insensitive..." + codings = val.toLowerCase().split(',').map((x) => x.trim()).reverse() + } else if (key.toLowerCase() === 'location') { + location = val + } + + headers[kHeadersList].append(key, val) + } + } + + this.body = new Readable({ read: resume }) + + const decoders = [] + + const willFollow = request.redirect === 'follow' && + location && + redirectStatusSet.has(status) + + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Encoding + if (request.method !== 'HEAD' && request.method !== 'CONNECT' && !nullBodyStatus.includes(status) && !willFollow) { + for (const coding of codings) { + // https://www.rfc-editor.org/rfc/rfc9112.html#section-7.2 + if (coding === 'x-gzip' || coding === 'gzip') { + decoders.push(zlib.createGunzip({ + // Be less strict when decoding compressed responses, since sometimes + // servers send slightly invalid responses that are still accepted + // by common browsers. + // Always using Z_SYNC_FLUSH is what cURL does. + flush: zlib.constants.Z_SYNC_FLUSH, + finishFlush: zlib.constants.Z_SYNC_FLUSH + })) + } else if (coding === 'deflate') { + decoders.push(zlib.createInflate()) + } else if (coding === 'br') { + decoders.push(zlib.createBrotliDecompress()) + } else { + decoders.length = 0 + break + } + } + } + + resolve({ + status, + statusText, + headersList: headers[kHeadersList], + body: decoders.length + ? pipeline(this.body, ...decoders, () => { }) + : this.body.on('error', () => {}) + }) + + return true + }, + + onData (chunk) { + if (fetchParams.controller.dump) { + return + } + + // 1. If one or more bytes have been transmitted from response’s + // message body, then: + + // 1. Let bytes be the transmitted bytes. + const bytes = chunk + + // 2. Let codings be the result of extracting header list values + // given `Content-Encoding` and response’s header list. + // See pullAlgorithm. + + // 3. Increase timingInfo’s encoded body size by bytes’s length. + timingInfo.encodedBodySize += bytes.byteLength + + // 4. See pullAlgorithm... + + return this.body.push(bytes) + }, + + onComplete () { + if (this.abort) { + fetchParams.controller.off('terminated', this.abort) + } + + fetchParams.controller.ended = true + + this.body.push(null) + }, + + onError (error) { + if (this.abort) { + fetchParams.controller.off('terminated', this.abort) + } + + this.body?.destroy(error) + + fetchParams.controller.terminate(error) + + reject(error) + }, + + onUpgrade (status, headersList, socket) { + if (status !== 101) { + return + } + + const headers = new Headers() + + for (let n = 0; n < headersList.length; n += 2) { + const key = headersList[n + 0].toString('latin1') + const val = headersList[n + 1].toString('latin1') + + headers[kHeadersList].append(key, val) + } + + resolve({ + status, + statusText: STATUS_CODES[status], + headersList: headers[kHeadersList], + socket + }) + + return true + } + } + )) + } +} + +module.exports = { + fetch, + Fetch, + fetching, + finalizeAndReportTiming +} + + +/***/ }), + +/***/ 5194: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; +/* globals AbortController */ + + + +const { extractBody, mixinBody, cloneBody } = __nccwpck_require__(8923) +const { Headers, fill: fillHeaders, HeadersList } = __nccwpck_require__(6349) +const { FinalizationRegistry } = __nccwpck_require__(3194)() +const util = __nccwpck_require__(3440) +const { + isValidHTTPToken, + sameOrigin, + normalizeMethod, + makePolicyContainer, + normalizeMethodRecord +} = __nccwpck_require__(5523) +const { + forbiddenMethodsSet, + corsSafeListedMethodsSet, + referrerPolicy, + requestRedirect, + requestMode, + requestCredentials, + requestCache, + requestDuplex +} = __nccwpck_require__(7326) +const { kEnumerableProperty } = util +const { kHeaders, kSignal, kState, kGuard, kRealm } = __nccwpck_require__(9710) +const { webidl } = __nccwpck_require__(4222) +const { getGlobalOrigin } = __nccwpck_require__(5628) +const { URLSerializer } = __nccwpck_require__(4322) +const { kHeadersList, kConstruct } = __nccwpck_require__(6443) +const assert = __nccwpck_require__(2613) +const { getMaxListeners, setMaxListeners, getEventListeners, defaultMaxListeners } = __nccwpck_require__(4434) + +let TransformStream = globalThis.TransformStream + +const kAbortController = Symbol('abortController') + +const requestFinalizer = new FinalizationRegistry(({ signal, abort }) => { + signal.removeEventListener('abort', abort) +}) + +// https://fetch.spec.whatwg.org/#request-class +class Request { + // https://fetch.spec.whatwg.org/#dom-request + constructor (input, init = {}) { + if (input === kConstruct) { + return + } + + webidl.argumentLengthCheck(arguments, 1, { header: 'Request constructor' }) + + input = webidl.converters.RequestInfo(input) + init = webidl.converters.RequestInit(init) + + // https://html.spec.whatwg.org/multipage/webappapis.html#environment-settings-object + this[kRealm] = { + settingsObject: { + baseUrl: getGlobalOrigin(), + get origin () { + return this.baseUrl?.origin + }, + policyContainer: makePolicyContainer() + } + } + + // 1. Let request be null. + let request = null + + // 2. Let fallbackMode be null. + let fallbackMode = null + + // 3. Let baseURL be this’s relevant settings object’s API base URL. + const baseUrl = this[kRealm].settingsObject.baseUrl + + // 4. Let signal be null. + let signal = null + + // 5. If input is a string, then: + if (typeof input === 'string') { + // 1. Let parsedURL be the result of parsing input with baseURL. + // 2. If parsedURL is failure, then throw a TypeError. + let parsedURL + try { + parsedURL = new URL(input, baseUrl) + } catch (err) { + throw new TypeError('Failed to parse URL from ' + input, { cause: err }) + } + + // 3. If parsedURL includes credentials, then throw a TypeError. + if (parsedURL.username || parsedURL.password) { + throw new TypeError( + 'Request cannot be constructed from a URL that includes credentials: ' + + input + ) + } + + // 4. Set request to a new request whose URL is parsedURL. + request = makeRequest({ urlList: [parsedURL] }) + + // 5. Set fallbackMode to "cors". + fallbackMode = 'cors' + } else { + // 6. Otherwise: + + // 7. Assert: input is a Request object. + assert(input instanceof Request) + + // 8. Set request to input’s request. + request = input[kState] + + // 9. Set signal to input’s signal. + signal = input[kSignal] + } + + // 7. Let origin be this’s relevant settings object’s origin. + const origin = this[kRealm].settingsObject.origin + + // 8. Let window be "client". + let window = 'client' + + // 9. If request’s window is an environment settings object and its origin + // is same origin with origin, then set window to request’s window. + if ( + request.window?.constructor?.name === 'EnvironmentSettingsObject' && + sameOrigin(request.window, origin) + ) { + window = request.window + } + + // 10. If init["window"] exists and is non-null, then throw a TypeError. + if (init.window != null) { + throw new TypeError(`'window' option '${window}' must be null`) + } + + // 11. If init["window"] exists, then set window to "no-window". + if ('window' in init) { + window = 'no-window' + } + + // 12. Set request to a new request with the following properties: + request = makeRequest({ + // URL request’s URL. + // undici implementation note: this is set as the first item in request's urlList in makeRequest + // method request’s method. + method: request.method, + // header list A copy of request’s header list. + // undici implementation note: headersList is cloned in makeRequest + headersList: request.headersList, + // unsafe-request flag Set. + unsafeRequest: request.unsafeRequest, + // client This’s relevant settings object. + client: this[kRealm].settingsObject, + // window window. + window, + // priority request’s priority. + priority: request.priority, + // origin request’s origin. The propagation of the origin is only significant for navigation requests + // being handled by a service worker. In this scenario a request can have an origin that is different + // from the current client. + origin: request.origin, + // referrer request’s referrer. + referrer: request.referrer, + // referrer policy request’s referrer policy. + referrerPolicy: request.referrerPolicy, + // mode request’s mode. + mode: request.mode, + // credentials mode request’s credentials mode. + credentials: request.credentials, + // cache mode request’s cache mode. + cache: request.cache, + // redirect mode request’s redirect mode. + redirect: request.redirect, + // integrity metadata request’s integrity metadata. + integrity: request.integrity, + // keepalive request’s keepalive. + keepalive: request.keepalive, + // reload-navigation flag request’s reload-navigation flag. + reloadNavigation: request.reloadNavigation, + // history-navigation flag request’s history-navigation flag. + historyNavigation: request.historyNavigation, + // URL list A clone of request’s URL list. + urlList: [...request.urlList] + }) + + const initHasKey = Object.keys(init).length !== 0 + + // 13. If init is not empty, then: + if (initHasKey) { + // 1. If request’s mode is "navigate", then set it to "same-origin". + if (request.mode === 'navigate') { + request.mode = 'same-origin' + } + + // 2. Unset request’s reload-navigation flag. + request.reloadNavigation = false + + // 3. Unset request’s history-navigation flag. + request.historyNavigation = false + + // 4. Set request’s origin to "client". + request.origin = 'client' + + // 5. Set request’s referrer to "client" + request.referrer = 'client' + + // 6. Set request’s referrer policy to the empty string. + request.referrerPolicy = '' + + // 7. Set request’s URL to request’s current URL. + request.url = request.urlList[request.urlList.length - 1] + + // 8. Set request’s URL list to « request’s URL ». + request.urlList = [request.url] + } + + // 14. If init["referrer"] exists, then: + if (init.referrer !== undefined) { + // 1. Let referrer be init["referrer"]. + const referrer = init.referrer + + // 2. If referrer is the empty string, then set request’s referrer to "no-referrer". + if (referrer === '') { + request.referrer = 'no-referrer' + } else { + // 1. Let parsedReferrer be the result of parsing referrer with + // baseURL. + // 2. If parsedReferrer is failure, then throw a TypeError. + let parsedReferrer + try { + parsedReferrer = new URL(referrer, baseUrl) + } catch (err) { + throw new TypeError(`Referrer "${referrer}" is not a valid URL.`, { cause: err }) + } + + // 3. If one of the following is true + // - parsedReferrer’s scheme is "about" and path is the string "client" + // - parsedReferrer’s origin is not same origin with origin + // then set request’s referrer to "client". + if ( + (parsedReferrer.protocol === 'about:' && parsedReferrer.hostname === 'client') || + (origin && !sameOrigin(parsedReferrer, this[kRealm].settingsObject.baseUrl)) + ) { + request.referrer = 'client' + } else { + // 4. Otherwise, set request’s referrer to parsedReferrer. + request.referrer = parsedReferrer + } + } + } + + // 15. If init["referrerPolicy"] exists, then set request’s referrer policy + // to it. + if (init.referrerPolicy !== undefined) { + request.referrerPolicy = init.referrerPolicy + } + + // 16. Let mode be init["mode"] if it exists, and fallbackMode otherwise. + let mode + if (init.mode !== undefined) { + mode = init.mode + } else { + mode = fallbackMode + } + + // 17. If mode is "navigate", then throw a TypeError. + if (mode === 'navigate') { + throw webidl.errors.exception({ + header: 'Request constructor', + message: 'invalid request mode navigate.' + }) + } + + // 18. If mode is non-null, set request’s mode to mode. + if (mode != null) { + request.mode = mode + } + + // 19. If init["credentials"] exists, then set request’s credentials mode + // to it. + if (init.credentials !== undefined) { + request.credentials = init.credentials + } + + // 18. If init["cache"] exists, then set request’s cache mode to it. + if (init.cache !== undefined) { + request.cache = init.cache + } + + // 21. If request’s cache mode is "only-if-cached" and request’s mode is + // not "same-origin", then throw a TypeError. + if (request.cache === 'only-if-cached' && request.mode !== 'same-origin') { + throw new TypeError( + "'only-if-cached' can be set only with 'same-origin' mode" + ) + } + + // 22. If init["redirect"] exists, then set request’s redirect mode to it. + if (init.redirect !== undefined) { + request.redirect = init.redirect + } + + // 23. If init["integrity"] exists, then set request’s integrity metadata to it. + if (init.integrity != null) { + request.integrity = String(init.integrity) + } + + // 24. If init["keepalive"] exists, then set request’s keepalive to it. + if (init.keepalive !== undefined) { + request.keepalive = Boolean(init.keepalive) + } + + // 25. If init["method"] exists, then: + if (init.method !== undefined) { + // 1. Let method be init["method"]. + let method = init.method + + // 2. If method is not a method or method is a forbidden method, then + // throw a TypeError. + if (!isValidHTTPToken(method)) { + throw new TypeError(`'${method}' is not a valid HTTP method.`) + } + + if (forbiddenMethodsSet.has(method.toUpperCase())) { + throw new TypeError(`'${method}' HTTP method is unsupported.`) + } + + // 3. Normalize method. + method = normalizeMethodRecord[method] ?? normalizeMethod(method) + + // 4. Set request’s method to method. + request.method = method + } + + // 26. If init["signal"] exists, then set signal to it. + if (init.signal !== undefined) { + signal = init.signal + } + + // 27. Set this’s request to request. + this[kState] = request + + // 28. Set this’s signal to a new AbortSignal object with this’s relevant + // Realm. + // TODO: could this be simplified with AbortSignal.any + // (https://dom.spec.whatwg.org/#dom-abortsignal-any) + const ac = new AbortController() + this[kSignal] = ac.signal + this[kSignal][kRealm] = this[kRealm] + + // 29. If signal is not null, then make this’s signal follow signal. + if (signal != null) { + if ( + !signal || + typeof signal.aborted !== 'boolean' || + typeof signal.addEventListener !== 'function' + ) { + throw new TypeError( + "Failed to construct 'Request': member signal is not of type AbortSignal." + ) + } + + if (signal.aborted) { + ac.abort(signal.reason) + } else { + // Keep a strong ref to ac while request object + // is alive. This is needed to prevent AbortController + // from being prematurely garbage collected. + // See, https://github.com/nodejs/undici/issues/1926. + this[kAbortController] = ac + + const acRef = new WeakRef(ac) + const abort = function () { + const ac = acRef.deref() + if (ac !== undefined) { + ac.abort(this.reason) + } + } + + // Third-party AbortControllers may not work with these. + // See, https://github.com/nodejs/undici/pull/1910#issuecomment-1464495619. + try { + // If the max amount of listeners is equal to the default, increase it + // This is only available in node >= v19.9.0 + if (typeof getMaxListeners === 'function' && getMaxListeners(signal) === defaultMaxListeners) { + setMaxListeners(100, signal) + } else if (getEventListeners(signal, 'abort').length >= defaultMaxListeners) { + setMaxListeners(100, signal) + } + } catch {} + + util.addAbortListener(signal, abort) + requestFinalizer.register(ac, { signal, abort }) + } + } + + // 30. Set this’s headers to a new Headers object with this’s relevant + // Realm, whose header list is request’s header list and guard is + // "request". + this[kHeaders] = new Headers(kConstruct) + this[kHeaders][kHeadersList] = request.headersList + this[kHeaders][kGuard] = 'request' + this[kHeaders][kRealm] = this[kRealm] + + // 31. If this’s request’s mode is "no-cors", then: + if (mode === 'no-cors') { + // 1. If this’s request’s method is not a CORS-safelisted method, + // then throw a TypeError. + if (!corsSafeListedMethodsSet.has(request.method)) { + throw new TypeError( + `'${request.method} is unsupported in no-cors mode.` + ) + } + + // 2. Set this’s headers’s guard to "request-no-cors". + this[kHeaders][kGuard] = 'request-no-cors' + } + + // 32. If init is not empty, then: + if (initHasKey) { + /** @type {HeadersList} */ + const headersList = this[kHeaders][kHeadersList] + // 1. Let headers be a copy of this’s headers and its associated header + // list. + // 2. If init["headers"] exists, then set headers to init["headers"]. + const headers = init.headers !== undefined ? init.headers : new HeadersList(headersList) + + // 3. Empty this’s headers’s header list. + headersList.clear() + + // 4. If headers is a Headers object, then for each header in its header + // list, append header’s name/header’s value to this’s headers. + if (headers instanceof HeadersList) { + for (const [key, val] of headers) { + headersList.append(key, val) + } + // Note: Copy the `set-cookie` meta-data. + headersList.cookies = headers.cookies + } else { + // 5. Otherwise, fill this’s headers with headers. + fillHeaders(this[kHeaders], headers) + } + } + + // 33. Let inputBody be input’s request’s body if input is a Request + // object; otherwise null. + const inputBody = input instanceof Request ? input[kState].body : null + + // 34. If either init["body"] exists and is non-null or inputBody is + // non-null, and request’s method is `GET` or `HEAD`, then throw a + // TypeError. + if ( + (init.body != null || inputBody != null) && + (request.method === 'GET' || request.method === 'HEAD') + ) { + throw new TypeError('Request with GET/HEAD method cannot have body.') + } + + // 35. Let initBody be null. + let initBody = null + + // 36. If init["body"] exists and is non-null, then: + if (init.body != null) { + // 1. Let Content-Type be null. + // 2. Set initBody and Content-Type to the result of extracting + // init["body"], with keepalive set to request’s keepalive. + const [extractedBody, contentType] = extractBody( + init.body, + request.keepalive + ) + initBody = extractedBody + + // 3, If Content-Type is non-null and this’s headers’s header list does + // not contain `Content-Type`, then append `Content-Type`/Content-Type to + // this’s headers. + if (contentType && !this[kHeaders][kHeadersList].contains('content-type')) { + this[kHeaders].append('content-type', contentType) + } + } + + // 37. Let inputOrInitBody be initBody if it is non-null; otherwise + // inputBody. + const inputOrInitBody = initBody ?? inputBody + + // 38. If inputOrInitBody is non-null and inputOrInitBody’s source is + // null, then: + if (inputOrInitBody != null && inputOrInitBody.source == null) { + // 1. If initBody is non-null and init["duplex"] does not exist, + // then throw a TypeError. + if (initBody != null && init.duplex == null) { + throw new TypeError('RequestInit: duplex option is required when sending a body.') + } + + // 2. If this’s request’s mode is neither "same-origin" nor "cors", + // then throw a TypeError. + if (request.mode !== 'same-origin' && request.mode !== 'cors') { + throw new TypeError( + 'If request is made from ReadableStream, mode should be "same-origin" or "cors"' + ) + } + + // 3. Set this’s request’s use-CORS-preflight flag. + request.useCORSPreflightFlag = true + } + + // 39. Let finalBody be inputOrInitBody. + let finalBody = inputOrInitBody + + // 40. If initBody is null and inputBody is non-null, then: + if (initBody == null && inputBody != null) { + // 1. If input is unusable, then throw a TypeError. + if (util.isDisturbed(inputBody.stream) || inputBody.stream.locked) { + throw new TypeError( + 'Cannot construct a Request with a Request object that has already been used.' + ) + } + + // 2. Set finalBody to the result of creating a proxy for inputBody. + if (!TransformStream) { + TransformStream = (__nccwpck_require__(3774).TransformStream) + } + + // https://streams.spec.whatwg.org/#readablestream-create-a-proxy + const identityTransform = new TransformStream() + inputBody.stream.pipeThrough(identityTransform) + finalBody = { + source: inputBody.source, + length: inputBody.length, + stream: identityTransform.readable + } + } + + // 41. Set this’s request’s body to finalBody. + this[kState].body = finalBody + } + + // Returns request’s HTTP method, which is "GET" by default. + get method () { + webidl.brandCheck(this, Request) + + // The method getter steps are to return this’s request’s method. + return this[kState].method + } + + // Returns the URL of request as a string. + get url () { + webidl.brandCheck(this, Request) + + // The url getter steps are to return this’s request’s URL, serialized. + return URLSerializer(this[kState].url) + } + + // Returns a Headers object consisting of the headers associated with request. + // Note that headers added in the network layer by the user agent will not + // be accounted for in this object, e.g., the "Host" header. + get headers () { + webidl.brandCheck(this, Request) + + // The headers getter steps are to return this’s headers. + return this[kHeaders] + } + + // Returns the kind of resource requested by request, e.g., "document" + // or "script". + get destination () { + webidl.brandCheck(this, Request) + + // The destination getter are to return this’s request’s destination. + return this[kState].destination + } + + // Returns the referrer of request. Its value can be a same-origin URL if + // explicitly set in init, the empty string to indicate no referrer, and + // "about:client" when defaulting to the global’s default. This is used + // during fetching to determine the value of the `Referer` header of the + // request being made. + get referrer () { + webidl.brandCheck(this, Request) + + // 1. If this’s request’s referrer is "no-referrer", then return the + // empty string. + if (this[kState].referrer === 'no-referrer') { + return '' + } + + // 2. If this’s request’s referrer is "client", then return + // "about:client". + if (this[kState].referrer === 'client') { + return 'about:client' + } + + // Return this’s request’s referrer, serialized. + return this[kState].referrer.toString() + } + + // Returns the referrer policy associated with request. + // This is used during fetching to compute the value of the request’s + // referrer. + get referrerPolicy () { + webidl.brandCheck(this, Request) + + // The referrerPolicy getter steps are to return this’s request’s referrer policy. + return this[kState].referrerPolicy + } + + // Returns the mode associated with request, which is a string indicating + // whether the request will use CORS, or will be restricted to same-origin + // URLs. + get mode () { + webidl.brandCheck(this, Request) + + // The mode getter steps are to return this’s request’s mode. + return this[kState].mode + } + + // Returns the credentials mode associated with request, + // which is a string indicating whether credentials will be sent with the + // request always, never, or only when sent to a same-origin URL. + get credentials () { + // The credentials getter steps are to return this’s request’s credentials mode. + return this[kState].credentials + } + + // Returns the cache mode associated with request, + // which is a string indicating how the request will + // interact with the browser’s cache when fetching. + get cache () { + webidl.brandCheck(this, Request) + + // The cache getter steps are to return this’s request’s cache mode. + return this[kState].cache + } + + // Returns the redirect mode associated with request, + // which is a string indicating how redirects for the + // request will be handled during fetching. A request + // will follow redirects by default. + get redirect () { + webidl.brandCheck(this, Request) + + // The redirect getter steps are to return this’s request’s redirect mode. + return this[kState].redirect + } + + // Returns request’s subresource integrity metadata, which is a + // cryptographic hash of the resource being fetched. Its value + // consists of multiple hashes separated by whitespace. [SRI] + get integrity () { + webidl.brandCheck(this, Request) + + // The integrity getter steps are to return this’s request’s integrity + // metadata. + return this[kState].integrity + } + + // Returns a boolean indicating whether or not request can outlive the + // global in which it was created. + get keepalive () { + webidl.brandCheck(this, Request) + + // The keepalive getter steps are to return this’s request’s keepalive. + return this[kState].keepalive + } + + // Returns a boolean indicating whether or not request is for a reload + // navigation. + get isReloadNavigation () { + webidl.brandCheck(this, Request) + + // The isReloadNavigation getter steps are to return true if this’s + // request’s reload-navigation flag is set; otherwise false. + return this[kState].reloadNavigation + } + + // Returns a boolean indicating whether or not request is for a history + // navigation (a.k.a. back-foward navigation). + get isHistoryNavigation () { + webidl.brandCheck(this, Request) + + // The isHistoryNavigation getter steps are to return true if this’s request’s + // history-navigation flag is set; otherwise false. + return this[kState].historyNavigation + } + + // Returns the signal associated with request, which is an AbortSignal + // object indicating whether or not request has been aborted, and its + // abort event handler. + get signal () { + webidl.brandCheck(this, Request) + + // The signal getter steps are to return this’s signal. + return this[kSignal] + } + + get body () { + webidl.brandCheck(this, Request) + + return this[kState].body ? this[kState].body.stream : null + } + + get bodyUsed () { + webidl.brandCheck(this, Request) + + return !!this[kState].body && util.isDisturbed(this[kState].body.stream) + } + + get duplex () { + webidl.brandCheck(this, Request) + + return 'half' + } + + // Returns a clone of request. + clone () { + webidl.brandCheck(this, Request) + + // 1. If this is unusable, then throw a TypeError. + if (this.bodyUsed || this.body?.locked) { + throw new TypeError('unusable') + } + + // 2. Let clonedRequest be the result of cloning this’s request. + const clonedRequest = cloneRequest(this[kState]) + + // 3. Let clonedRequestObject be the result of creating a Request object, + // given clonedRequest, this’s headers’s guard, and this’s relevant Realm. + const clonedRequestObject = new Request(kConstruct) + clonedRequestObject[kState] = clonedRequest + clonedRequestObject[kRealm] = this[kRealm] + clonedRequestObject[kHeaders] = new Headers(kConstruct) + clonedRequestObject[kHeaders][kHeadersList] = clonedRequest.headersList + clonedRequestObject[kHeaders][kGuard] = this[kHeaders][kGuard] + clonedRequestObject[kHeaders][kRealm] = this[kHeaders][kRealm] + + // 4. Make clonedRequestObject’s signal follow this’s signal. + const ac = new AbortController() + if (this.signal.aborted) { + ac.abort(this.signal.reason) + } else { + util.addAbortListener( + this.signal, + () => { + ac.abort(this.signal.reason) + } + ) + } + clonedRequestObject[kSignal] = ac.signal + + // 4. Return clonedRequestObject. + return clonedRequestObject + } +} + +mixinBody(Request) + +function makeRequest (init) { + // https://fetch.spec.whatwg.org/#requests + const request = { + method: 'GET', + localURLsOnly: false, + unsafeRequest: false, + body: null, + client: null, + reservedClient: null, + replacesClientId: '', + window: 'client', + keepalive: false, + serviceWorkers: 'all', + initiator: '', + destination: '', + priority: null, + origin: 'client', + policyContainer: 'client', + referrer: 'client', + referrerPolicy: '', + mode: 'no-cors', + useCORSPreflightFlag: false, + credentials: 'same-origin', + useCredentials: false, + cache: 'default', + redirect: 'follow', + integrity: '', + cryptoGraphicsNonceMetadata: '', + parserMetadata: '', + reloadNavigation: false, + historyNavigation: false, + userActivation: false, + taintedOrigin: false, + redirectCount: 0, + responseTainting: 'basic', + preventNoCacheCacheControlHeaderModification: false, + done: false, + timingAllowFailed: false, + ...init, + headersList: init.headersList + ? new HeadersList(init.headersList) + : new HeadersList() + } + request.url = request.urlList[0] + return request +} + +// https://fetch.spec.whatwg.org/#concept-request-clone +function cloneRequest (request) { + // To clone a request request, run these steps: + + // 1. Let newRequest be a copy of request, except for its body. + const newRequest = makeRequest({ ...request, body: null }) + + // 2. If request’s body is non-null, set newRequest’s body to the + // result of cloning request’s body. + if (request.body != null) { + newRequest.body = cloneBody(request.body) + } + + // 3. Return newRequest. + return newRequest +} + +Object.defineProperties(Request.prototype, { + method: kEnumerableProperty, + url: kEnumerableProperty, + headers: kEnumerableProperty, + redirect: kEnumerableProperty, + clone: kEnumerableProperty, + signal: kEnumerableProperty, + duplex: kEnumerableProperty, + destination: kEnumerableProperty, + body: kEnumerableProperty, + bodyUsed: kEnumerableProperty, + isHistoryNavigation: kEnumerableProperty, + isReloadNavigation: kEnumerableProperty, + keepalive: kEnumerableProperty, + integrity: kEnumerableProperty, + cache: kEnumerableProperty, + credentials: kEnumerableProperty, + attribute: kEnumerableProperty, + referrerPolicy: kEnumerableProperty, + referrer: kEnumerableProperty, + mode: kEnumerableProperty, + [Symbol.toStringTag]: { + value: 'Request', + configurable: true + } +}) + +webidl.converters.Request = webidl.interfaceConverter( + Request +) + +// https://fetch.spec.whatwg.org/#requestinfo +webidl.converters.RequestInfo = function (V) { + if (typeof V === 'string') { + return webidl.converters.USVString(V) + } + + if (V instanceof Request) { + return webidl.converters.Request(V) + } + + return webidl.converters.USVString(V) +} + +webidl.converters.AbortSignal = webidl.interfaceConverter( + AbortSignal +) + +// https://fetch.spec.whatwg.org/#requestinit +webidl.converters.RequestInit = webidl.dictionaryConverter([ + { + key: 'method', + converter: webidl.converters.ByteString + }, + { + key: 'headers', + converter: webidl.converters.HeadersInit + }, + { + key: 'body', + converter: webidl.nullableConverter( + webidl.converters.BodyInit + ) + }, + { + key: 'referrer', + converter: webidl.converters.USVString + }, + { + key: 'referrerPolicy', + converter: webidl.converters.DOMString, + // https://w3c.github.io/webappsec-referrer-policy/#referrer-policy + allowedValues: referrerPolicy + }, + { + key: 'mode', + converter: webidl.converters.DOMString, + // https://fetch.spec.whatwg.org/#concept-request-mode + allowedValues: requestMode + }, + { + key: 'credentials', + converter: webidl.converters.DOMString, + // https://fetch.spec.whatwg.org/#requestcredentials + allowedValues: requestCredentials + }, + { + key: 'cache', + converter: webidl.converters.DOMString, + // https://fetch.spec.whatwg.org/#requestcache + allowedValues: requestCache + }, + { + key: 'redirect', + converter: webidl.converters.DOMString, + // https://fetch.spec.whatwg.org/#requestredirect + allowedValues: requestRedirect + }, + { + key: 'integrity', + converter: webidl.converters.DOMString + }, + { + key: 'keepalive', + converter: webidl.converters.boolean + }, + { + key: 'signal', + converter: webidl.nullableConverter( + (signal) => webidl.converters.AbortSignal( + signal, + { strict: false } + ) + ) + }, + { + key: 'window', + converter: webidl.converters.any + }, + { + key: 'duplex', + converter: webidl.converters.DOMString, + allowedValues: requestDuplex + } +]) + +module.exports = { Request, makeRequest } + + +/***/ }), + +/***/ 8676: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const { Headers, HeadersList, fill } = __nccwpck_require__(6349) +const { extractBody, cloneBody, mixinBody } = __nccwpck_require__(8923) +const util = __nccwpck_require__(3440) +const { kEnumerableProperty } = util +const { + isValidReasonPhrase, + isCancelled, + isAborted, + isBlobLike, + serializeJavascriptValueToJSONString, + isErrorLike, + isomorphicEncode +} = __nccwpck_require__(5523) +const { + redirectStatusSet, + nullBodyStatus, + DOMException +} = __nccwpck_require__(7326) +const { kState, kHeaders, kGuard, kRealm } = __nccwpck_require__(9710) +const { webidl } = __nccwpck_require__(4222) +const { FormData } = __nccwpck_require__(3073) +const { getGlobalOrigin } = __nccwpck_require__(5628) +const { URLSerializer } = __nccwpck_require__(4322) +const { kHeadersList, kConstruct } = __nccwpck_require__(6443) +const assert = __nccwpck_require__(2613) +const { types } = __nccwpck_require__(9023) + +const ReadableStream = globalThis.ReadableStream || (__nccwpck_require__(3774).ReadableStream) +const textEncoder = new TextEncoder('utf-8') + +// https://fetch.spec.whatwg.org/#response-class +class Response { + // Creates network error Response. + static error () { + // TODO + const relevantRealm = { settingsObject: {} } + + // The static error() method steps are to return the result of creating a + // Response object, given a new network error, "immutable", and this’s + // relevant Realm. + const responseObject = new Response() + responseObject[kState] = makeNetworkError() + responseObject[kRealm] = relevantRealm + responseObject[kHeaders][kHeadersList] = responseObject[kState].headersList + responseObject[kHeaders][kGuard] = 'immutable' + responseObject[kHeaders][kRealm] = relevantRealm + return responseObject + } + + // https://fetch.spec.whatwg.org/#dom-response-json + static json (data, init = {}) { + webidl.argumentLengthCheck(arguments, 1, { header: 'Response.json' }) + + if (init !== null) { + init = webidl.converters.ResponseInit(init) + } + + // 1. Let bytes the result of running serialize a JavaScript value to JSON bytes on data. + const bytes = textEncoder.encode( + serializeJavascriptValueToJSONString(data) + ) + + // 2. Let body be the result of extracting bytes. + const body = extractBody(bytes) + + // 3. Let responseObject be the result of creating a Response object, given a new response, + // "response", and this’s relevant Realm. + const relevantRealm = { settingsObject: {} } + const responseObject = new Response() + responseObject[kRealm] = relevantRealm + responseObject[kHeaders][kGuard] = 'response' + responseObject[kHeaders][kRealm] = relevantRealm + + // 4. Perform initialize a response given responseObject, init, and (body, "application/json"). + initializeResponse(responseObject, init, { body: body[0], type: 'application/json' }) + + // 5. Return responseObject. + return responseObject + } + + // Creates a redirect Response that redirects to url with status status. + static redirect (url, status = 302) { + const relevantRealm = { settingsObject: {} } + + webidl.argumentLengthCheck(arguments, 1, { header: 'Response.redirect' }) + + url = webidl.converters.USVString(url) + status = webidl.converters['unsigned short'](status) + + // 1. Let parsedURL be the result of parsing url with current settings + // object’s API base URL. + // 2. If parsedURL is failure, then throw a TypeError. + // TODO: base-URL? + let parsedURL + try { + parsedURL = new URL(url, getGlobalOrigin()) + } catch (err) { + throw Object.assign(new TypeError('Failed to parse URL from ' + url), { + cause: err + }) + } + + // 3. If status is not a redirect status, then throw a RangeError. + if (!redirectStatusSet.has(status)) { + throw new RangeError('Invalid status code ' + status) + } + + // 4. Let responseObject be the result of creating a Response object, + // given a new response, "immutable", and this’s relevant Realm. + const responseObject = new Response() + responseObject[kRealm] = relevantRealm + responseObject[kHeaders][kGuard] = 'immutable' + responseObject[kHeaders][kRealm] = relevantRealm + + // 5. Set responseObject’s response’s status to status. + responseObject[kState].status = status + + // 6. Let value be parsedURL, serialized and isomorphic encoded. + const value = isomorphicEncode(URLSerializer(parsedURL)) + + // 7. Append `Location`/value to responseObject’s response’s header list. + responseObject[kState].headersList.append('location', value) + + // 8. Return responseObject. + return responseObject + } + + // https://fetch.spec.whatwg.org/#dom-response + constructor (body = null, init = {}) { + if (body !== null) { + body = webidl.converters.BodyInit(body) + } + + init = webidl.converters.ResponseInit(init) + + // TODO + this[kRealm] = { settingsObject: {} } + + // 1. Set this’s response to a new response. + this[kState] = makeResponse({}) + + // 2. Set this’s headers to a new Headers object with this’s relevant + // Realm, whose header list is this’s response’s header list and guard + // is "response". + this[kHeaders] = new Headers(kConstruct) + this[kHeaders][kGuard] = 'response' + this[kHeaders][kHeadersList] = this[kState].headersList + this[kHeaders][kRealm] = this[kRealm] + + // 3. Let bodyWithType be null. + let bodyWithType = null + + // 4. If body is non-null, then set bodyWithType to the result of extracting body. + if (body != null) { + const [extractedBody, type] = extractBody(body) + bodyWithType = { body: extractedBody, type } + } + + // 5. Perform initialize a response given this, init, and bodyWithType. + initializeResponse(this, init, bodyWithType) + } + + // Returns response’s type, e.g., "cors". + get type () { + webidl.brandCheck(this, Response) + + // The type getter steps are to return this’s response’s type. + return this[kState].type + } + + // Returns response’s URL, if it has one; otherwise the empty string. + get url () { + webidl.brandCheck(this, Response) + + const urlList = this[kState].urlList + + // The url getter steps are to return the empty string if this’s + // response’s URL is null; otherwise this’s response’s URL, + // serialized with exclude fragment set to true. + const url = urlList[urlList.length - 1] ?? null + + if (url === null) { + return '' + } + + return URLSerializer(url, true) + } + + // Returns whether response was obtained through a redirect. + get redirected () { + webidl.brandCheck(this, Response) + + // The redirected getter steps are to return true if this’s response’s URL + // list has more than one item; otherwise false. + return this[kState].urlList.length > 1 + } + + // Returns response’s status. + get status () { + webidl.brandCheck(this, Response) + + // The status getter steps are to return this’s response’s status. + return this[kState].status + } + + // Returns whether response’s status is an ok status. + get ok () { + webidl.brandCheck(this, Response) + + // The ok getter steps are to return true if this’s response’s status is an + // ok status; otherwise false. + return this[kState].status >= 200 && this[kState].status <= 299 + } + + // Returns response’s status message. + get statusText () { + webidl.brandCheck(this, Response) + + // The statusText getter steps are to return this’s response’s status + // message. + return this[kState].statusText + } + + // Returns response’s headers as Headers. + get headers () { + webidl.brandCheck(this, Response) + + // The headers getter steps are to return this’s headers. + return this[kHeaders] + } + + get body () { + webidl.brandCheck(this, Response) + + return this[kState].body ? this[kState].body.stream : null + } + + get bodyUsed () { + webidl.brandCheck(this, Response) + + return !!this[kState].body && util.isDisturbed(this[kState].body.stream) + } + + // Returns a clone of response. + clone () { + webidl.brandCheck(this, Response) + + // 1. If this is unusable, then throw a TypeError. + if (this.bodyUsed || (this.body && this.body.locked)) { + throw webidl.errors.exception({ + header: 'Response.clone', + message: 'Body has already been consumed.' + }) + } + + // 2. Let clonedResponse be the result of cloning this’s response. + const clonedResponse = cloneResponse(this[kState]) + + // 3. Return the result of creating a Response object, given + // clonedResponse, this’s headers’s guard, and this’s relevant Realm. + const clonedResponseObject = new Response() + clonedResponseObject[kState] = clonedResponse + clonedResponseObject[kRealm] = this[kRealm] + clonedResponseObject[kHeaders][kHeadersList] = clonedResponse.headersList + clonedResponseObject[kHeaders][kGuard] = this[kHeaders][kGuard] + clonedResponseObject[kHeaders][kRealm] = this[kHeaders][kRealm] + + return clonedResponseObject + } +} + +mixinBody(Response) + +Object.defineProperties(Response.prototype, { + type: kEnumerableProperty, + url: kEnumerableProperty, + status: kEnumerableProperty, + ok: kEnumerableProperty, + redirected: kEnumerableProperty, + statusText: kEnumerableProperty, + headers: kEnumerableProperty, + clone: kEnumerableProperty, + body: kEnumerableProperty, + bodyUsed: kEnumerableProperty, + [Symbol.toStringTag]: { + value: 'Response', + configurable: true + } +}) + +Object.defineProperties(Response, { + json: kEnumerableProperty, + redirect: kEnumerableProperty, + error: kEnumerableProperty +}) + +// https://fetch.spec.whatwg.org/#concept-response-clone +function cloneResponse (response) { + // To clone a response response, run these steps: + + // 1. If response is a filtered response, then return a new identical + // filtered response whose internal response is a clone of response’s + // internal response. + if (response.internalResponse) { + return filterResponse( + cloneResponse(response.internalResponse), + response.type + ) + } + + // 2. Let newResponse be a copy of response, except for its body. + const newResponse = makeResponse({ ...response, body: null }) + + // 3. If response’s body is non-null, then set newResponse’s body to the + // result of cloning response’s body. + if (response.body != null) { + newResponse.body = cloneBody(response.body) + } + + // 4. Return newResponse. + return newResponse +} + +function makeResponse (init) { + return { + aborted: false, + rangeRequested: false, + timingAllowPassed: false, + requestIncludesCredentials: false, + type: 'default', + status: 200, + timingInfo: null, + cacheState: '', + statusText: '', + ...init, + headersList: init.headersList + ? new HeadersList(init.headersList) + : new HeadersList(), + urlList: init.urlList ? [...init.urlList] : [] + } +} + +function makeNetworkError (reason) { + const isError = isErrorLike(reason) + return makeResponse({ + type: 'error', + status: 0, + error: isError + ? reason + : new Error(reason ? String(reason) : reason), + aborted: reason && reason.name === 'AbortError' + }) +} + +function makeFilteredResponse (response, state) { + state = { + internalResponse: response, + ...state + } + + return new Proxy(response, { + get (target, p) { + return p in state ? state[p] : target[p] + }, + set (target, p, value) { + assert(!(p in state)) + target[p] = value + return true + } + }) +} + +// https://fetch.spec.whatwg.org/#concept-filtered-response +function filterResponse (response, type) { + // Set response to the following filtered response with response as its + // internal response, depending on request’s response tainting: + if (type === 'basic') { + // A basic filtered response is a filtered response whose type is "basic" + // and header list excludes any headers in internal response’s header list + // whose name is a forbidden response-header name. + + // Note: undici does not implement forbidden response-header names + return makeFilteredResponse(response, { + type: 'basic', + headersList: response.headersList + }) + } else if (type === 'cors') { + // A CORS filtered response is a filtered response whose type is "cors" + // and header list excludes any headers in internal response’s header + // list whose name is not a CORS-safelisted response-header name, given + // internal response’s CORS-exposed header-name list. + + // Note: undici does not implement CORS-safelisted response-header names + return makeFilteredResponse(response, { + type: 'cors', + headersList: response.headersList + }) + } else if (type === 'opaque') { + // An opaque filtered response is a filtered response whose type is + // "opaque", URL list is the empty list, status is 0, status message + // is the empty byte sequence, header list is empty, and body is null. + + return makeFilteredResponse(response, { + type: 'opaque', + urlList: Object.freeze([]), + status: 0, + statusText: '', + body: null + }) + } else if (type === 'opaqueredirect') { + // An opaque-redirect filtered response is a filtered response whose type + // is "opaqueredirect", status is 0, status message is the empty byte + // sequence, header list is empty, and body is null. + + return makeFilteredResponse(response, { + type: 'opaqueredirect', + status: 0, + statusText: '', + headersList: [], + body: null + }) + } else { + assert(false) + } +} + +// https://fetch.spec.whatwg.org/#appropriate-network-error +function makeAppropriateNetworkError (fetchParams, err = null) { + // 1. Assert: fetchParams is canceled. + assert(isCancelled(fetchParams)) + + // 2. Return an aborted network error if fetchParams is aborted; + // otherwise return a network error. + return isAborted(fetchParams) + ? makeNetworkError(Object.assign(new DOMException('The operation was aborted.', 'AbortError'), { cause: err })) + : makeNetworkError(Object.assign(new DOMException('Request was cancelled.'), { cause: err })) +} + +// https://whatpr.org/fetch/1392.html#initialize-a-response +function initializeResponse (response, init, body) { + // 1. If init["status"] is not in the range 200 to 599, inclusive, then + // throw a RangeError. + if (init.status !== null && (init.status < 200 || init.status > 599)) { + throw new RangeError('init["status"] must be in the range of 200 to 599, inclusive.') + } + + // 2. If init["statusText"] does not match the reason-phrase token production, + // then throw a TypeError. + if ('statusText' in init && init.statusText != null) { + // See, https://datatracker.ietf.org/doc/html/rfc7230#section-3.1.2: + // reason-phrase = *( HTAB / SP / VCHAR / obs-text ) + if (!isValidReasonPhrase(String(init.statusText))) { + throw new TypeError('Invalid statusText') + } + } + + // 3. Set response’s response’s status to init["status"]. + if ('status' in init && init.status != null) { + response[kState].status = init.status + } + + // 4. Set response’s response’s status message to init["statusText"]. + if ('statusText' in init && init.statusText != null) { + response[kState].statusText = init.statusText + } + + // 5. If init["headers"] exists, then fill response’s headers with init["headers"]. + if ('headers' in init && init.headers != null) { + fill(response[kHeaders], init.headers) + } + + // 6. If body was given, then: + if (body) { + // 1. If response's status is a null body status, then throw a TypeError. + if (nullBodyStatus.includes(response.status)) { + throw webidl.errors.exception({ + header: 'Response constructor', + message: 'Invalid response status code ' + response.status + }) + } + + // 2. Set response's body to body's body. + response[kState].body = body.body + + // 3. If body's type is non-null and response's header list does not contain + // `Content-Type`, then append (`Content-Type`, body's type) to response's header list. + if (body.type != null && !response[kState].headersList.contains('Content-Type')) { + response[kState].headersList.append('content-type', body.type) + } + } +} + +webidl.converters.ReadableStream = webidl.interfaceConverter( + ReadableStream +) + +webidl.converters.FormData = webidl.interfaceConverter( + FormData +) + +webidl.converters.URLSearchParams = webidl.interfaceConverter( + URLSearchParams +) + +// https://fetch.spec.whatwg.org/#typedefdef-xmlhttprequestbodyinit +webidl.converters.XMLHttpRequestBodyInit = function (V) { + if (typeof V === 'string') { + return webidl.converters.USVString(V) + } + + if (isBlobLike(V)) { + return webidl.converters.Blob(V, { strict: false }) + } + + if (types.isArrayBuffer(V) || types.isTypedArray(V) || types.isDataView(V)) { + return webidl.converters.BufferSource(V) + } + + if (util.isFormDataLike(V)) { + return webidl.converters.FormData(V, { strict: false }) + } + + if (V instanceof URLSearchParams) { + return webidl.converters.URLSearchParams(V) + } + + return webidl.converters.DOMString(V) +} + +// https://fetch.spec.whatwg.org/#bodyinit +webidl.converters.BodyInit = function (V) { + if (V instanceof ReadableStream) { + return webidl.converters.ReadableStream(V) + } + + // Note: the spec doesn't include async iterables, + // this is an undici extension. + if (V?.[Symbol.asyncIterator]) { + return V + } + + return webidl.converters.XMLHttpRequestBodyInit(V) +} + +webidl.converters.ResponseInit = webidl.dictionaryConverter([ + { + key: 'status', + converter: webidl.converters['unsigned short'], + defaultValue: 200 + }, + { + key: 'statusText', + converter: webidl.converters.ByteString, + defaultValue: '' + }, + { + key: 'headers', + converter: webidl.converters.HeadersInit + } +]) + +module.exports = { + makeNetworkError, + makeResponse, + makeAppropriateNetworkError, + filterResponse, + Response, + cloneResponse +} + + +/***/ }), + +/***/ 9710: +/***/ ((module) => { + +"use strict"; + + +module.exports = { + kUrl: Symbol('url'), + kHeaders: Symbol('headers'), + kSignal: Symbol('signal'), + kState: Symbol('state'), + kGuard: Symbol('guard'), + kRealm: Symbol('realm') +} + + +/***/ }), + +/***/ 5523: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const { redirectStatusSet, referrerPolicySet: referrerPolicyTokens, badPortsSet } = __nccwpck_require__(7326) +const { getGlobalOrigin } = __nccwpck_require__(5628) +const { performance } = __nccwpck_require__(2987) +const { isBlobLike, toUSVString, ReadableStreamFrom } = __nccwpck_require__(3440) +const assert = __nccwpck_require__(2613) +const { isUint8Array } = __nccwpck_require__(8253) + +let supportedHashes = [] + +// https://nodejs.org/api/crypto.html#determining-if-crypto-support-is-unavailable +/** @type {import('crypto')|undefined} */ +let crypto + +try { + crypto = __nccwpck_require__(6982) + const possibleRelevantHashes = ['sha256', 'sha384', 'sha512'] + supportedHashes = crypto.getHashes().filter((hash) => possibleRelevantHashes.includes(hash)) +/* c8 ignore next 3 */ +} catch { +} + +function responseURL (response) { + // https://fetch.spec.whatwg.org/#responses + // A response has an associated URL. It is a pointer to the last URL + // in response’s URL list and null if response’s URL list is empty. + const urlList = response.urlList + const length = urlList.length + return length === 0 ? null : urlList[length - 1].toString() +} + +// https://fetch.spec.whatwg.org/#concept-response-location-url +function responseLocationURL (response, requestFragment) { + // 1. If response’s status is not a redirect status, then return null. + if (!redirectStatusSet.has(response.status)) { + return null + } + + // 2. Let location be the result of extracting header list values given + // `Location` and response’s header list. + let location = response.headersList.get('location') + + // 3. If location is a header value, then set location to the result of + // parsing location with response’s URL. + if (location !== null && isValidHeaderValue(location)) { + location = new URL(location, responseURL(response)) + } + + // 4. If location is a URL whose fragment is null, then set location’s + // fragment to requestFragment. + if (location && !location.hash) { + location.hash = requestFragment + } + + // 5. Return location. + return location +} + +/** @returns {URL} */ +function requestCurrentURL (request) { + return request.urlList[request.urlList.length - 1] +} + +function requestBadPort (request) { + // 1. Let url be request’s current URL. + const url = requestCurrentURL(request) + + // 2. If url’s scheme is an HTTP(S) scheme and url’s port is a bad port, + // then return blocked. + if (urlIsHttpHttpsScheme(url) && badPortsSet.has(url.port)) { + return 'blocked' + } + + // 3. Return allowed. + return 'allowed' +} + +function isErrorLike (object) { + return object instanceof Error || ( + object?.constructor?.name === 'Error' || + object?.constructor?.name === 'DOMException' + ) +} + +// Check whether |statusText| is a ByteString and +// matches the Reason-Phrase token production. +// RFC 2616: https://tools.ietf.org/html/rfc2616 +// RFC 7230: https://tools.ietf.org/html/rfc7230 +// "reason-phrase = *( HTAB / SP / VCHAR / obs-text )" +// https://github.com/chromium/chromium/blob/94.0.4604.1/third_party/blink/renderer/core/fetch/response.cc#L116 +function isValidReasonPhrase (statusText) { + for (let i = 0; i < statusText.length; ++i) { + const c = statusText.charCodeAt(i) + if ( + !( + ( + c === 0x09 || // HTAB + (c >= 0x20 && c <= 0x7e) || // SP / VCHAR + (c >= 0x80 && c <= 0xff) + ) // obs-text + ) + ) { + return false + } + } + return true +} + +/** + * @see https://tools.ietf.org/html/rfc7230#section-3.2.6 + * @param {number} c + */ +function isTokenCharCode (c) { + switch (c) { + case 0x22: + case 0x28: + case 0x29: + case 0x2c: + case 0x2f: + case 0x3a: + case 0x3b: + case 0x3c: + case 0x3d: + case 0x3e: + case 0x3f: + case 0x40: + case 0x5b: + case 0x5c: + case 0x5d: + case 0x7b: + case 0x7d: + // DQUOTE and "(),/:;<=>?@[\]{}" + return false + default: + // VCHAR %x21-7E + return c >= 0x21 && c <= 0x7e + } +} + +/** + * @param {string} characters + */ +function isValidHTTPToken (characters) { + if (characters.length === 0) { + return false + } + for (let i = 0; i < characters.length; ++i) { + if (!isTokenCharCode(characters.charCodeAt(i))) { + return false + } + } + return true +} + +/** + * @see https://fetch.spec.whatwg.org/#header-name + * @param {string} potentialValue + */ +function isValidHeaderName (potentialValue) { + return isValidHTTPToken(potentialValue) +} + +/** + * @see https://fetch.spec.whatwg.org/#header-value + * @param {string} potentialValue + */ +function isValidHeaderValue (potentialValue) { + // - Has no leading or trailing HTTP tab or space bytes. + // - Contains no 0x00 (NUL) or HTTP newline bytes. + if ( + potentialValue.startsWith('\t') || + potentialValue.startsWith(' ') || + potentialValue.endsWith('\t') || + potentialValue.endsWith(' ') + ) { + return false + } + + if ( + potentialValue.includes('\0') || + potentialValue.includes('\r') || + potentialValue.includes('\n') + ) { + return false + } + + return true +} + +// https://w3c.github.io/webappsec-referrer-policy/#set-requests-referrer-policy-on-redirect +function setRequestReferrerPolicyOnRedirect (request, actualResponse) { + // Given a request request and a response actualResponse, this algorithm + // updates request’s referrer policy according to the Referrer-Policy + // header (if any) in actualResponse. + + // 1. Let policy be the result of executing § 8.1 Parse a referrer policy + // from a Referrer-Policy header on actualResponse. + + // 8.1 Parse a referrer policy from a Referrer-Policy header + // 1. Let policy-tokens be the result of extracting header list values given `Referrer-Policy` and response’s header list. + const { headersList } = actualResponse + // 2. Let policy be the empty string. + // 3. For each token in policy-tokens, if token is a referrer policy and token is not the empty string, then set policy to token. + // 4. Return policy. + const policyHeader = (headersList.get('referrer-policy') ?? '').split(',') + + // Note: As the referrer-policy can contain multiple policies + // separated by comma, we need to loop through all of them + // and pick the first valid one. + // Ref: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy#specify_a_fallback_policy + let policy = '' + if (policyHeader.length > 0) { + // The right-most policy takes precedence. + // The left-most policy is the fallback. + for (let i = policyHeader.length; i !== 0; i--) { + const token = policyHeader[i - 1].trim() + if (referrerPolicyTokens.has(token)) { + policy = token + break + } + } + } + + // 2. If policy is not the empty string, then set request’s referrer policy to policy. + if (policy !== '') { + request.referrerPolicy = policy + } +} + +// https://fetch.spec.whatwg.org/#cross-origin-resource-policy-check +function crossOriginResourcePolicyCheck () { + // TODO + return 'allowed' +} + +// https://fetch.spec.whatwg.org/#concept-cors-check +function corsCheck () { + // TODO + return 'success' +} + +// https://fetch.spec.whatwg.org/#concept-tao-check +function TAOCheck () { + // TODO + return 'success' +} + +function appendFetchMetadata (httpRequest) { + // https://w3c.github.io/webappsec-fetch-metadata/#sec-fetch-dest-header + // TODO + + // https://w3c.github.io/webappsec-fetch-metadata/#sec-fetch-mode-header + + // 1. Assert: r’s url is a potentially trustworthy URL. + // TODO + + // 2. Let header be a Structured Header whose value is a token. + let header = null + + // 3. Set header’s value to r’s mode. + header = httpRequest.mode + + // 4. Set a structured field value `Sec-Fetch-Mode`/header in r’s header list. + httpRequest.headersList.set('sec-fetch-mode', header) + + // https://w3c.github.io/webappsec-fetch-metadata/#sec-fetch-site-header + // TODO + + // https://w3c.github.io/webappsec-fetch-metadata/#sec-fetch-user-header + // TODO +} + +// https://fetch.spec.whatwg.org/#append-a-request-origin-header +function appendRequestOriginHeader (request) { + // 1. Let serializedOrigin be the result of byte-serializing a request origin with request. + let serializedOrigin = request.origin + + // 2. If request’s response tainting is "cors" or request’s mode is "websocket", then append (`Origin`, serializedOrigin) to request’s header list. + if (request.responseTainting === 'cors' || request.mode === 'websocket') { + if (serializedOrigin) { + request.headersList.append('origin', serializedOrigin) + } + + // 3. Otherwise, if request’s method is neither `GET` nor `HEAD`, then: + } else if (request.method !== 'GET' && request.method !== 'HEAD') { + // 1. Switch on request’s referrer policy: + switch (request.referrerPolicy) { + case 'no-referrer': + // Set serializedOrigin to `null`. + serializedOrigin = null + break + case 'no-referrer-when-downgrade': + case 'strict-origin': + case 'strict-origin-when-cross-origin': + // If request’s origin is a tuple origin, its scheme is "https", and request’s current URL’s scheme is not "https", then set serializedOrigin to `null`. + if (request.origin && urlHasHttpsScheme(request.origin) && !urlHasHttpsScheme(requestCurrentURL(request))) { + serializedOrigin = null + } + break + case 'same-origin': + // If request’s origin is not same origin with request’s current URL’s origin, then set serializedOrigin to `null`. + if (!sameOrigin(request, requestCurrentURL(request))) { + serializedOrigin = null + } + break + default: + // Do nothing. + } + + if (serializedOrigin) { + // 2. Append (`Origin`, serializedOrigin) to request’s header list. + request.headersList.append('origin', serializedOrigin) + } + } +} + +function coarsenedSharedCurrentTime (crossOriginIsolatedCapability) { + // TODO + return performance.now() +} + +// https://fetch.spec.whatwg.org/#create-an-opaque-timing-info +function createOpaqueTimingInfo (timingInfo) { + return { + startTime: timingInfo.startTime ?? 0, + redirectStartTime: 0, + redirectEndTime: 0, + postRedirectStartTime: timingInfo.startTime ?? 0, + finalServiceWorkerStartTime: 0, + finalNetworkResponseStartTime: 0, + finalNetworkRequestStartTime: 0, + endTime: 0, + encodedBodySize: 0, + decodedBodySize: 0, + finalConnectionTimingInfo: null + } +} + +// https://html.spec.whatwg.org/multipage/origin.html#policy-container +function makePolicyContainer () { + // Note: the fetch spec doesn't make use of embedder policy or CSP list + return { + referrerPolicy: 'strict-origin-when-cross-origin' + } +} + +// https://html.spec.whatwg.org/multipage/origin.html#clone-a-policy-container +function clonePolicyContainer (policyContainer) { + return { + referrerPolicy: policyContainer.referrerPolicy + } +} + +// https://w3c.github.io/webappsec-referrer-policy/#determine-requests-referrer +function determineRequestsReferrer (request) { + // 1. Let policy be request's referrer policy. + const policy = request.referrerPolicy + + // Note: policy cannot (shouldn't) be null or an empty string. + assert(policy) + + // 2. Let environment be request’s client. + + let referrerSource = null + + // 3. Switch on request’s referrer: + if (request.referrer === 'client') { + // Note: node isn't a browser and doesn't implement document/iframes, + // so we bypass this step and replace it with our own. + + const globalOrigin = getGlobalOrigin() + + if (!globalOrigin || globalOrigin.origin === 'null') { + return 'no-referrer' + } + + // note: we need to clone it as it's mutated + referrerSource = new URL(globalOrigin) + } else if (request.referrer instanceof URL) { + // Let referrerSource be request’s referrer. + referrerSource = request.referrer + } + + // 4. Let request’s referrerURL be the result of stripping referrerSource for + // use as a referrer. + let referrerURL = stripURLForReferrer(referrerSource) + + // 5. Let referrerOrigin be the result of stripping referrerSource for use as + // a referrer, with the origin-only flag set to true. + const referrerOrigin = stripURLForReferrer(referrerSource, true) + + // 6. If the result of serializing referrerURL is a string whose length is + // greater than 4096, set referrerURL to referrerOrigin. + if (referrerURL.toString().length > 4096) { + referrerURL = referrerOrigin + } + + const areSameOrigin = sameOrigin(request, referrerURL) + const isNonPotentiallyTrustWorthy = isURLPotentiallyTrustworthy(referrerURL) && + !isURLPotentiallyTrustworthy(request.url) + + // 8. Execute the switch statements corresponding to the value of policy: + switch (policy) { + case 'origin': return referrerOrigin != null ? referrerOrigin : stripURLForReferrer(referrerSource, true) + case 'unsafe-url': return referrerURL + case 'same-origin': + return areSameOrigin ? referrerOrigin : 'no-referrer' + case 'origin-when-cross-origin': + return areSameOrigin ? referrerURL : referrerOrigin + case 'strict-origin-when-cross-origin': { + const currentURL = requestCurrentURL(request) + + // 1. If the origin of referrerURL and the origin of request’s current + // URL are the same, then return referrerURL. + if (sameOrigin(referrerURL, currentURL)) { + return referrerURL + } + + // 2. If referrerURL is a potentially trustworthy URL and request’s + // current URL is not a potentially trustworthy URL, then return no + // referrer. + if (isURLPotentiallyTrustworthy(referrerURL) && !isURLPotentiallyTrustworthy(currentURL)) { + return 'no-referrer' + } + + // 3. Return referrerOrigin. + return referrerOrigin + } + case 'strict-origin': // eslint-disable-line + /** + * 1. If referrerURL is a potentially trustworthy URL and + * request’s current URL is not a potentially trustworthy URL, + * then return no referrer. + * 2. Return referrerOrigin + */ + case 'no-referrer-when-downgrade': // eslint-disable-line + /** + * 1. If referrerURL is a potentially trustworthy URL and + * request’s current URL is not a potentially trustworthy URL, + * then return no referrer. + * 2. Return referrerOrigin + */ + + default: // eslint-disable-line + return isNonPotentiallyTrustWorthy ? 'no-referrer' : referrerOrigin + } +} + +/** + * @see https://w3c.github.io/webappsec-referrer-policy/#strip-url + * @param {URL} url + * @param {boolean|undefined} originOnly + */ +function stripURLForReferrer (url, originOnly) { + // 1. Assert: url is a URL. + assert(url instanceof URL) + + // 2. If url’s scheme is a local scheme, then return no referrer. + if (url.protocol === 'file:' || url.protocol === 'about:' || url.protocol === 'blank:') { + return 'no-referrer' + } + + // 3. Set url’s username to the empty string. + url.username = '' + + // 4. Set url’s password to the empty string. + url.password = '' + + // 5. Set url’s fragment to null. + url.hash = '' + + // 6. If the origin-only flag is true, then: + if (originOnly) { + // 1. Set url’s path to « the empty string ». + url.pathname = '' + + // 2. Set url’s query to null. + url.search = '' + } + + // 7. Return url. + return url +} + +function isURLPotentiallyTrustworthy (url) { + if (!(url instanceof URL)) { + return false + } + + // If child of about, return true + if (url.href === 'about:blank' || url.href === 'about:srcdoc') { + return true + } + + // If scheme is data, return true + if (url.protocol === 'data:') return true + + // If file, return true + if (url.protocol === 'file:') return true + + return isOriginPotentiallyTrustworthy(url.origin) + + function isOriginPotentiallyTrustworthy (origin) { + // If origin is explicitly null, return false + if (origin == null || origin === 'null') return false + + const originAsURL = new URL(origin) + + // If secure, return true + if (originAsURL.protocol === 'https:' || originAsURL.protocol === 'wss:') { + return true + } + + // If localhost or variants, return true + if (/^127(?:\.[0-9]+){0,2}\.[0-9]+$|^\[(?:0*:)*?:?0*1\]$/.test(originAsURL.hostname) || + (originAsURL.hostname === 'localhost' || originAsURL.hostname.includes('localhost.')) || + (originAsURL.hostname.endsWith('.localhost'))) { + return true + } + + // If any other, return false + return false + } +} + +/** + * @see https://w3c.github.io/webappsec-subresource-integrity/#does-response-match-metadatalist + * @param {Uint8Array} bytes + * @param {string} metadataList + */ +function bytesMatch (bytes, metadataList) { + // If node is not built with OpenSSL support, we cannot check + // a request's integrity, so allow it by default (the spec will + // allow requests if an invalid hash is given, as precedence). + /* istanbul ignore if: only if node is built with --without-ssl */ + if (crypto === undefined) { + return true + } + + // 1. Let parsedMetadata be the result of parsing metadataList. + const parsedMetadata = parseMetadata(metadataList) + + // 2. If parsedMetadata is no metadata, return true. + if (parsedMetadata === 'no metadata') { + return true + } + + // 3. If response is not eligible for integrity validation, return false. + // TODO + + // 4. If parsedMetadata is the empty set, return true. + if (parsedMetadata.length === 0) { + return true + } + + // 5. Let metadata be the result of getting the strongest + // metadata from parsedMetadata. + const strongest = getStrongestMetadata(parsedMetadata) + const metadata = filterMetadataListByAlgorithm(parsedMetadata, strongest) + + // 6. For each item in metadata: + for (const item of metadata) { + // 1. Let algorithm be the alg component of item. + const algorithm = item.algo + + // 2. Let expectedValue be the val component of item. + const expectedValue = item.hash + + // See https://github.com/web-platform-tests/wpt/commit/e4c5cc7a5e48093220528dfdd1c4012dc3837a0e + // "be liberal with padding". This is annoying, and it's not even in the spec. + + // 3. Let actualValue be the result of applying algorithm to bytes. + let actualValue = crypto.createHash(algorithm).update(bytes).digest('base64') + + if (actualValue[actualValue.length - 1] === '=') { + if (actualValue[actualValue.length - 2] === '=') { + actualValue = actualValue.slice(0, -2) + } else { + actualValue = actualValue.slice(0, -1) + } + } + + // 4. If actualValue is a case-sensitive match for expectedValue, + // return true. + if (compareBase64Mixed(actualValue, expectedValue)) { + return true + } + } + + // 7. Return false. + return false +} + +// https://w3c.github.io/webappsec-subresource-integrity/#grammardef-hash-with-options +// https://www.w3.org/TR/CSP2/#source-list-syntax +// https://www.rfc-editor.org/rfc/rfc5234#appendix-B.1 +const parseHashWithOptions = /(?sha256|sha384|sha512)-((?[A-Za-z0-9+/]+|[A-Za-z0-9_-]+)={0,2}(?:\s|$)( +[!-~]*)?)?/i + +/** + * @see https://w3c.github.io/webappsec-subresource-integrity/#parse-metadata + * @param {string} metadata + */ +function parseMetadata (metadata) { + // 1. Let result be the empty set. + /** @type {{ algo: string, hash: string }[]} */ + const result = [] + + // 2. Let empty be equal to true. + let empty = true + + // 3. For each token returned by splitting metadata on spaces: + for (const token of metadata.split(' ')) { + // 1. Set empty to false. + empty = false + + // 2. Parse token as a hash-with-options. + const parsedToken = parseHashWithOptions.exec(token) + + // 3. If token does not parse, continue to the next token. + if ( + parsedToken === null || + parsedToken.groups === undefined || + parsedToken.groups.algo === undefined + ) { + // Note: Chromium blocks the request at this point, but Firefox + // gives a warning that an invalid integrity was given. The + // correct behavior is to ignore these, and subsequently not + // check the integrity of the resource. + continue + } + + // 4. Let algorithm be the hash-algo component of token. + const algorithm = parsedToken.groups.algo.toLowerCase() + + // 5. If algorithm is a hash function recognized by the user + // agent, add the parsed token to result. + if (supportedHashes.includes(algorithm)) { + result.push(parsedToken.groups) + } + } + + // 4. Return no metadata if empty is true, otherwise return result. + if (empty === true) { + return 'no metadata' + } + + return result +} + +/** + * @param {{ algo: 'sha256' | 'sha384' | 'sha512' }[]} metadataList + */ +function getStrongestMetadata (metadataList) { + // Let algorithm be the algo component of the first item in metadataList. + // Can be sha256 + let algorithm = metadataList[0].algo + // If the algorithm is sha512, then it is the strongest + // and we can return immediately + if (algorithm[3] === '5') { + return algorithm + } + + for (let i = 1; i < metadataList.length; ++i) { + const metadata = metadataList[i] + // If the algorithm is sha512, then it is the strongest + // and we can break the loop immediately + if (metadata.algo[3] === '5') { + algorithm = 'sha512' + break + // If the algorithm is sha384, then a potential sha256 or sha384 is ignored + } else if (algorithm[3] === '3') { + continue + // algorithm is sha256, check if algorithm is sha384 and if so, set it as + // the strongest + } else if (metadata.algo[3] === '3') { + algorithm = 'sha384' + } + } + return algorithm +} + +function filterMetadataListByAlgorithm (metadataList, algorithm) { + if (metadataList.length === 1) { + return metadataList + } + + let pos = 0 + for (let i = 0; i < metadataList.length; ++i) { + if (metadataList[i].algo === algorithm) { + metadataList[pos++] = metadataList[i] + } + } + + metadataList.length = pos + + return metadataList +} + +/** + * Compares two base64 strings, allowing for base64url + * in the second string. + * +* @param {string} actualValue always base64 + * @param {string} expectedValue base64 or base64url + * @returns {boolean} + */ +function compareBase64Mixed (actualValue, expectedValue) { + if (actualValue.length !== expectedValue.length) { + return false + } + for (let i = 0; i < actualValue.length; ++i) { + if (actualValue[i] !== expectedValue[i]) { + if ( + (actualValue[i] === '+' && expectedValue[i] === '-') || + (actualValue[i] === '/' && expectedValue[i] === '_') + ) { + continue + } + return false + } + } + + return true +} + +// https://w3c.github.io/webappsec-upgrade-insecure-requests/#upgrade-request +function tryUpgradeRequestToAPotentiallyTrustworthyURL (request) { + // TODO +} + +/** + * @link {https://html.spec.whatwg.org/multipage/origin.html#same-origin} + * @param {URL} A + * @param {URL} B + */ +function sameOrigin (A, B) { + // 1. If A and B are the same opaque origin, then return true. + if (A.origin === B.origin && A.origin === 'null') { + return true + } + + // 2. If A and B are both tuple origins and their schemes, + // hosts, and port are identical, then return true. + if (A.protocol === B.protocol && A.hostname === B.hostname && A.port === B.port) { + return true + } + + // 3. Return false. + return false +} + +function createDeferredPromise () { + let res + let rej + const promise = new Promise((resolve, reject) => { + res = resolve + rej = reject + }) + + return { promise, resolve: res, reject: rej } +} + +function isAborted (fetchParams) { + return fetchParams.controller.state === 'aborted' +} + +function isCancelled (fetchParams) { + return fetchParams.controller.state === 'aborted' || + fetchParams.controller.state === 'terminated' +} + +const normalizeMethodRecord = { + delete: 'DELETE', + DELETE: 'DELETE', + get: 'GET', + GET: 'GET', + head: 'HEAD', + HEAD: 'HEAD', + options: 'OPTIONS', + OPTIONS: 'OPTIONS', + post: 'POST', + POST: 'POST', + put: 'PUT', + PUT: 'PUT' +} + +// Note: object prototypes should not be able to be referenced. e.g. `Object#hasOwnProperty`. +Object.setPrototypeOf(normalizeMethodRecord, null) + +/** + * @see https://fetch.spec.whatwg.org/#concept-method-normalize + * @param {string} method + */ +function normalizeMethod (method) { + return normalizeMethodRecord[method.toLowerCase()] ?? method +} + +// https://infra.spec.whatwg.org/#serialize-a-javascript-value-to-a-json-string +function serializeJavascriptValueToJSONString (value) { + // 1. Let result be ? Call(%JSON.stringify%, undefined, « value »). + const result = JSON.stringify(value) + + // 2. If result is undefined, then throw a TypeError. + if (result === undefined) { + throw new TypeError('Value is not JSON serializable') + } + + // 3. Assert: result is a string. + assert(typeof result === 'string') + + // 4. Return result. + return result +} + +// https://tc39.es/ecma262/#sec-%25iteratorprototype%25-object +const esIteratorPrototype = Object.getPrototypeOf(Object.getPrototypeOf([][Symbol.iterator]())) + +/** + * @see https://webidl.spec.whatwg.org/#dfn-iterator-prototype-object + * @param {() => unknown[]} iterator + * @param {string} name name of the instance + * @param {'key'|'value'|'key+value'} kind + */ +function makeIterator (iterator, name, kind) { + const object = { + index: 0, + kind, + target: iterator + } + + const i = { + next () { + // 1. Let interface be the interface for which the iterator prototype object exists. + + // 2. Let thisValue be the this value. + + // 3. Let object be ? ToObject(thisValue). + + // 4. If object is a platform object, then perform a security + // check, passing: + + // 5. If object is not a default iterator object for interface, + // then throw a TypeError. + if (Object.getPrototypeOf(this) !== i) { + throw new TypeError( + `'next' called on an object that does not implement interface ${name} Iterator.` + ) + } + + // 6. Let index be object’s index. + // 7. Let kind be object’s kind. + // 8. Let values be object’s target's value pairs to iterate over. + const { index, kind, target } = object + const values = target() + + // 9. Let len be the length of values. + const len = values.length + + // 10. If index is greater than or equal to len, then return + // CreateIterResultObject(undefined, true). + if (index >= len) { + return { value: undefined, done: true } + } + + // 11. Let pair be the entry in values at index index. + const pair = values[index] + + // 12. Set object’s index to index + 1. + object.index = index + 1 + + // 13. Return the iterator result for pair and kind. + return iteratorResult(pair, kind) + }, + // The class string of an iterator prototype object for a given interface is the + // result of concatenating the identifier of the interface and the string " Iterator". + [Symbol.toStringTag]: `${name} Iterator` + } + + // The [[Prototype]] internal slot of an iterator prototype object must be %IteratorPrototype%. + Object.setPrototypeOf(i, esIteratorPrototype) + // esIteratorPrototype needs to be the prototype of i + // which is the prototype of an empty object. Yes, it's confusing. + return Object.setPrototypeOf({}, i) +} + +// https://webidl.spec.whatwg.org/#iterator-result +function iteratorResult (pair, kind) { + let result + + // 1. Let result be a value determined by the value of kind: + switch (kind) { + case 'key': { + // 1. Let idlKey be pair’s key. + // 2. Let key be the result of converting idlKey to an + // ECMAScript value. + // 3. result is key. + result = pair[0] + break + } + case 'value': { + // 1. Let idlValue be pair’s value. + // 2. Let value be the result of converting idlValue to + // an ECMAScript value. + // 3. result is value. + result = pair[1] + break + } + case 'key+value': { + // 1. Let idlKey be pair’s key. + // 2. Let idlValue be pair’s value. + // 3. Let key be the result of converting idlKey to an + // ECMAScript value. + // 4. Let value be the result of converting idlValue to + // an ECMAScript value. + // 5. Let array be ! ArrayCreate(2). + // 6. Call ! CreateDataProperty(array, "0", key). + // 7. Call ! CreateDataProperty(array, "1", value). + // 8. result is array. + result = pair + break + } + } + + // 2. Return CreateIterResultObject(result, false). + return { value: result, done: false } +} + +/** + * @see https://fetch.spec.whatwg.org/#body-fully-read + */ +async function fullyReadBody (body, processBody, processBodyError) { + // 1. If taskDestination is null, then set taskDestination to + // the result of starting a new parallel queue. + + // 2. Let successSteps given a byte sequence bytes be to queue a + // fetch task to run processBody given bytes, with taskDestination. + const successSteps = processBody + + // 3. Let errorSteps be to queue a fetch task to run processBodyError, + // with taskDestination. + const errorSteps = processBodyError + + // 4. Let reader be the result of getting a reader for body’s stream. + // If that threw an exception, then run errorSteps with that + // exception and return. + let reader + + try { + reader = body.stream.getReader() + } catch (e) { + errorSteps(e) + return + } + + // 5. Read all bytes from reader, given successSteps and errorSteps. + try { + const result = await readAllBytes(reader) + successSteps(result) + } catch (e) { + errorSteps(e) + } +} + +/** @type {ReadableStream} */ +let ReadableStream = globalThis.ReadableStream + +function isReadableStreamLike (stream) { + if (!ReadableStream) { + ReadableStream = (__nccwpck_require__(3774).ReadableStream) + } + + return stream instanceof ReadableStream || ( + stream[Symbol.toStringTag] === 'ReadableStream' && + typeof stream.tee === 'function' + ) +} + +const MAXIMUM_ARGUMENT_LENGTH = 65535 + +/** + * @see https://infra.spec.whatwg.org/#isomorphic-decode + * @param {number[]|Uint8Array} input + */ +function isomorphicDecode (input) { + // 1. To isomorphic decode a byte sequence input, return a string whose code point + // length is equal to input’s length and whose code points have the same values + // as the values of input’s bytes, in the same order. + + if (input.length < MAXIMUM_ARGUMENT_LENGTH) { + return String.fromCharCode(...input) + } + + return input.reduce((previous, current) => previous + String.fromCharCode(current), '') +} + +/** + * @param {ReadableStreamController} controller + */ +function readableStreamClose (controller) { + try { + controller.close() + } catch (err) { + // TODO: add comment explaining why this error occurs. + if (!err.message.includes('Controller is already closed')) { + throw err + } + } +} + +/** + * @see https://infra.spec.whatwg.org/#isomorphic-encode + * @param {string} input + */ +function isomorphicEncode (input) { + // 1. Assert: input contains no code points greater than U+00FF. + for (let i = 0; i < input.length; i++) { + assert(input.charCodeAt(i) <= 0xFF) + } + + // 2. Return a byte sequence whose length is equal to input’s code + // point length and whose bytes have the same values as the + // values of input’s code points, in the same order + return input +} + +/** + * @see https://streams.spec.whatwg.org/#readablestreamdefaultreader-read-all-bytes + * @see https://streams.spec.whatwg.org/#read-loop + * @param {ReadableStreamDefaultReader} reader + */ +async function readAllBytes (reader) { + const bytes = [] + let byteLength = 0 + + while (true) { + const { done, value: chunk } = await reader.read() + + if (done) { + // 1. Call successSteps with bytes. + return Buffer.concat(bytes, byteLength) + } + + // 1. If chunk is not a Uint8Array object, call failureSteps + // with a TypeError and abort these steps. + if (!isUint8Array(chunk)) { + throw new TypeError('Received non-Uint8Array chunk') + } + + // 2. Append the bytes represented by chunk to bytes. + bytes.push(chunk) + byteLength += chunk.length + + // 3. Read-loop given reader, bytes, successSteps, and failureSteps. + } +} + +/** + * @see https://fetch.spec.whatwg.org/#is-local + * @param {URL} url + */ +function urlIsLocal (url) { + assert('protocol' in url) // ensure it's a url object + + const protocol = url.protocol + + return protocol === 'about:' || protocol === 'blob:' || protocol === 'data:' +} + +/** + * @param {string|URL} url + */ +function urlHasHttpsScheme (url) { + if (typeof url === 'string') { + return url.startsWith('https:') + } + + return url.protocol === 'https:' +} + +/** + * @see https://fetch.spec.whatwg.org/#http-scheme + * @param {URL} url + */ +function urlIsHttpHttpsScheme (url) { + assert('protocol' in url) // ensure it's a url object + + const protocol = url.protocol + + return protocol === 'http:' || protocol === 'https:' +} + +/** + * Fetch supports node >= 16.8.0, but Object.hasOwn was added in v16.9.0. + */ +const hasOwn = Object.hasOwn || ((dict, key) => Object.prototype.hasOwnProperty.call(dict, key)) + +module.exports = { + isAborted, + isCancelled, + createDeferredPromise, + ReadableStreamFrom, + toUSVString, + tryUpgradeRequestToAPotentiallyTrustworthyURL, + coarsenedSharedCurrentTime, + determineRequestsReferrer, + makePolicyContainer, + clonePolicyContainer, + appendFetchMetadata, + appendRequestOriginHeader, + TAOCheck, + corsCheck, + crossOriginResourcePolicyCheck, + createOpaqueTimingInfo, + setRequestReferrerPolicyOnRedirect, + isValidHTTPToken, + requestBadPort, + requestCurrentURL, + responseURL, + responseLocationURL, + isBlobLike, + isURLPotentiallyTrustworthy, + isValidReasonPhrase, + sameOrigin, + normalizeMethod, + serializeJavascriptValueToJSONString, + makeIterator, + isValidHeaderName, + isValidHeaderValue, + hasOwn, + isErrorLike, + fullyReadBody, + bytesMatch, + isReadableStreamLike, + readableStreamClose, + isomorphicEncode, + isomorphicDecode, + urlIsLocal, + urlHasHttpsScheme, + urlIsHttpHttpsScheme, + readAllBytes, + normalizeMethodRecord, + parseMetadata +} + + +/***/ }), + +/***/ 4222: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const { types } = __nccwpck_require__(9023) +const { hasOwn, toUSVString } = __nccwpck_require__(5523) + +/** @type {import('../../types/webidl').Webidl} */ +const webidl = {} +webidl.converters = {} +webidl.util = {} +webidl.errors = {} + +webidl.errors.exception = function (message) { + return new TypeError(`${message.header}: ${message.message}`) +} + +webidl.errors.conversionFailed = function (context) { + const plural = context.types.length === 1 ? '' : ' one of' + const message = + `${context.argument} could not be converted to` + + `${plural}: ${context.types.join(', ')}.` + + return webidl.errors.exception({ + header: context.prefix, + message + }) +} + +webidl.errors.invalidArgument = function (context) { + return webidl.errors.exception({ + header: context.prefix, + message: `"${context.value}" is an invalid ${context.type}.` + }) +} + +// https://webidl.spec.whatwg.org/#implements +webidl.brandCheck = function (V, I, opts = undefined) { + if (opts?.strict !== false && !(V instanceof I)) { + throw new TypeError('Illegal invocation') + } else { + return V?.[Symbol.toStringTag] === I.prototype[Symbol.toStringTag] + } +} + +webidl.argumentLengthCheck = function ({ length }, min, ctx) { + if (length < min) { + throw webidl.errors.exception({ + message: `${min} argument${min !== 1 ? 's' : ''} required, ` + + `but${length ? ' only' : ''} ${length} found.`, + ...ctx + }) + } +} + +webidl.illegalConstructor = function () { + throw webidl.errors.exception({ + header: 'TypeError', + message: 'Illegal constructor' + }) +} + +// https://tc39.es/ecma262/#sec-ecmascript-data-types-and-values +webidl.util.Type = function (V) { + switch (typeof V) { + case 'undefined': return 'Undefined' + case 'boolean': return 'Boolean' + case 'string': return 'String' + case 'symbol': return 'Symbol' + case 'number': return 'Number' + case 'bigint': return 'BigInt' + case 'function': + case 'object': { + if (V === null) { + return 'Null' + } + + return 'Object' + } + } +} + +// https://webidl.spec.whatwg.org/#abstract-opdef-converttoint +webidl.util.ConvertToInt = function (V, bitLength, signedness, opts = {}) { + let upperBound + let lowerBound + + // 1. If bitLength is 64, then: + if (bitLength === 64) { + // 1. Let upperBound be 2^53 − 1. + upperBound = Math.pow(2, 53) - 1 + + // 2. If signedness is "unsigned", then let lowerBound be 0. + if (signedness === 'unsigned') { + lowerBound = 0 + } else { + // 3. Otherwise let lowerBound be −2^53 + 1. + lowerBound = Math.pow(-2, 53) + 1 + } + } else if (signedness === 'unsigned') { + // 2. Otherwise, if signedness is "unsigned", then: + + // 1. Let lowerBound be 0. + lowerBound = 0 + + // 2. Let upperBound be 2^bitLength − 1. + upperBound = Math.pow(2, bitLength) - 1 + } else { + // 3. Otherwise: + + // 1. Let lowerBound be -2^bitLength − 1. + lowerBound = Math.pow(-2, bitLength) - 1 + + // 2. Let upperBound be 2^bitLength − 1 − 1. + upperBound = Math.pow(2, bitLength - 1) - 1 + } + + // 4. Let x be ? ToNumber(V). + let x = Number(V) + + // 5. If x is −0, then set x to +0. + if (x === 0) { + x = 0 + } + + // 6. If the conversion is to an IDL type associated + // with the [EnforceRange] extended attribute, then: + if (opts.enforceRange === true) { + // 1. If x is NaN, +∞, or −∞, then throw a TypeError. + if ( + Number.isNaN(x) || + x === Number.POSITIVE_INFINITY || + x === Number.NEGATIVE_INFINITY + ) { + throw webidl.errors.exception({ + header: 'Integer conversion', + message: `Could not convert ${V} to an integer.` + }) + } + + // 2. Set x to IntegerPart(x). + x = webidl.util.IntegerPart(x) + + // 3. If x < lowerBound or x > upperBound, then + // throw a TypeError. + if (x < lowerBound || x > upperBound) { + throw webidl.errors.exception({ + header: 'Integer conversion', + message: `Value must be between ${lowerBound}-${upperBound}, got ${x}.` + }) + } + + // 4. Return x. + return x + } + + // 7. If x is not NaN and the conversion is to an IDL + // type associated with the [Clamp] extended + // attribute, then: + if (!Number.isNaN(x) && opts.clamp === true) { + // 1. Set x to min(max(x, lowerBound), upperBound). + x = Math.min(Math.max(x, lowerBound), upperBound) + + // 2. Round x to the nearest integer, choosing the + // even integer if it lies halfway between two, + // and choosing +0 rather than −0. + if (Math.floor(x) % 2 === 0) { + x = Math.floor(x) + } else { + x = Math.ceil(x) + } + + // 3. Return x. + return x + } + + // 8. If x is NaN, +0, +∞, or −∞, then return +0. + if ( + Number.isNaN(x) || + (x === 0 && Object.is(0, x)) || + x === Number.POSITIVE_INFINITY || + x === Number.NEGATIVE_INFINITY + ) { + return 0 + } + + // 9. Set x to IntegerPart(x). + x = webidl.util.IntegerPart(x) + + // 10. Set x to x modulo 2^bitLength. + x = x % Math.pow(2, bitLength) + + // 11. If signedness is "signed" and x ≥ 2^bitLength − 1, + // then return x − 2^bitLength. + if (signedness === 'signed' && x >= Math.pow(2, bitLength) - 1) { + return x - Math.pow(2, bitLength) + } + + // 12. Otherwise, return x. + return x +} + +// https://webidl.spec.whatwg.org/#abstract-opdef-integerpart +webidl.util.IntegerPart = function (n) { + // 1. Let r be floor(abs(n)). + const r = Math.floor(Math.abs(n)) + + // 2. If n < 0, then return -1 × r. + if (n < 0) { + return -1 * r + } + + // 3. Otherwise, return r. + return r +} + +// https://webidl.spec.whatwg.org/#es-sequence +webidl.sequenceConverter = function (converter) { + return (V) => { + // 1. If Type(V) is not Object, throw a TypeError. + if (webidl.util.Type(V) !== 'Object') { + throw webidl.errors.exception({ + header: 'Sequence', + message: `Value of type ${webidl.util.Type(V)} is not an Object.` + }) + } + + // 2. Let method be ? GetMethod(V, @@iterator). + /** @type {Generator} */ + const method = V?.[Symbol.iterator]?.() + const seq = [] + + // 3. If method is undefined, throw a TypeError. + if ( + method === undefined || + typeof method.next !== 'function' + ) { + throw webidl.errors.exception({ + header: 'Sequence', + message: 'Object is not an iterator.' + }) + } + + // https://webidl.spec.whatwg.org/#create-sequence-from-iterable + while (true) { + const { done, value } = method.next() + + if (done) { + break + } + + seq.push(converter(value)) + } + + return seq + } +} + +// https://webidl.spec.whatwg.org/#es-to-record +webidl.recordConverter = function (keyConverter, valueConverter) { + return (O) => { + // 1. If Type(O) is not Object, throw a TypeError. + if (webidl.util.Type(O) !== 'Object') { + throw webidl.errors.exception({ + header: 'Record', + message: `Value of type ${webidl.util.Type(O)} is not an Object.` + }) + } + + // 2. Let result be a new empty instance of record. + const result = {} + + if (!types.isProxy(O)) { + // Object.keys only returns enumerable properties + const keys = Object.keys(O) + + for (const key of keys) { + // 1. Let typedKey be key converted to an IDL value of type K. + const typedKey = keyConverter(key) + + // 2. Let value be ? Get(O, key). + // 3. Let typedValue be value converted to an IDL value of type V. + const typedValue = valueConverter(O[key]) + + // 4. Set result[typedKey] to typedValue. + result[typedKey] = typedValue + } + + // 5. Return result. + return result + } + + // 3. Let keys be ? O.[[OwnPropertyKeys]](). + const keys = Reflect.ownKeys(O) + + // 4. For each key of keys. + for (const key of keys) { + // 1. Let desc be ? O.[[GetOwnProperty]](key). + const desc = Reflect.getOwnPropertyDescriptor(O, key) + + // 2. If desc is not undefined and desc.[[Enumerable]] is true: + if (desc?.enumerable) { + // 1. Let typedKey be key converted to an IDL value of type K. + const typedKey = keyConverter(key) + + // 2. Let value be ? Get(O, key). + // 3. Let typedValue be value converted to an IDL value of type V. + const typedValue = valueConverter(O[key]) + + // 4. Set result[typedKey] to typedValue. + result[typedKey] = typedValue + } + } + + // 5. Return result. + return result + } +} + +webidl.interfaceConverter = function (i) { + return (V, opts = {}) => { + if (opts.strict !== false && !(V instanceof i)) { + throw webidl.errors.exception({ + header: i.name, + message: `Expected ${V} to be an instance of ${i.name}.` + }) + } + + return V + } +} + +webidl.dictionaryConverter = function (converters) { + return (dictionary) => { + const type = webidl.util.Type(dictionary) + const dict = {} + + if (type === 'Null' || type === 'Undefined') { + return dict + } else if (type !== 'Object') { + throw webidl.errors.exception({ + header: 'Dictionary', + message: `Expected ${dictionary} to be one of: Null, Undefined, Object.` + }) + } + + for (const options of converters) { + const { key, defaultValue, required, converter } = options + + if (required === true) { + if (!hasOwn(dictionary, key)) { + throw webidl.errors.exception({ + header: 'Dictionary', + message: `Missing required key "${key}".` + }) + } + } + + let value = dictionary[key] + const hasDefault = hasOwn(options, 'defaultValue') + + // Only use defaultValue if value is undefined and + // a defaultValue options was provided. + if (hasDefault && value !== null) { + value = value ?? defaultValue + } + + // A key can be optional and have no default value. + // When this happens, do not perform a conversion, + // and do not assign the key a value. + if (required || hasDefault || value !== undefined) { + value = converter(value) + + if ( + options.allowedValues && + !options.allowedValues.includes(value) + ) { + throw webidl.errors.exception({ + header: 'Dictionary', + message: `${value} is not an accepted type. Expected one of ${options.allowedValues.join(', ')}.` + }) + } + + dict[key] = value + } + } + + return dict + } +} + +webidl.nullableConverter = function (converter) { + return (V) => { + if (V === null) { + return V + } + + return converter(V) + } +} + +// https://webidl.spec.whatwg.org/#es-DOMString +webidl.converters.DOMString = function (V, opts = {}) { + // 1. If V is null and the conversion is to an IDL type + // associated with the [LegacyNullToEmptyString] + // extended attribute, then return the DOMString value + // that represents the empty string. + if (V === null && opts.legacyNullToEmptyString) { + return '' + } + + // 2. Let x be ? ToString(V). + if (typeof V === 'symbol') { + throw new TypeError('Could not convert argument of type symbol to string.') + } + + // 3. Return the IDL DOMString value that represents the + // same sequence of code units as the one the + // ECMAScript String value x represents. + return String(V) +} + +// https://webidl.spec.whatwg.org/#es-ByteString +webidl.converters.ByteString = function (V) { + // 1. Let x be ? ToString(V). + // Note: DOMString converter perform ? ToString(V) + const x = webidl.converters.DOMString(V) + + // 2. If the value of any element of x is greater than + // 255, then throw a TypeError. + for (let index = 0; index < x.length; index++) { + if (x.charCodeAt(index) > 255) { + throw new TypeError( + 'Cannot convert argument to a ByteString because the character at ' + + `index ${index} has a value of ${x.charCodeAt(index)} which is greater than 255.` + ) + } + } + + // 3. Return an IDL ByteString value whose length is the + // length of x, and where the value of each element is + // the value of the corresponding element of x. + return x +} + +// https://webidl.spec.whatwg.org/#es-USVString +webidl.converters.USVString = toUSVString + +// https://webidl.spec.whatwg.org/#es-boolean +webidl.converters.boolean = function (V) { + // 1. Let x be the result of computing ToBoolean(V). + const x = Boolean(V) + + // 2. Return the IDL boolean value that is the one that represents + // the same truth value as the ECMAScript Boolean value x. + return x +} + +// https://webidl.spec.whatwg.org/#es-any +webidl.converters.any = function (V) { + return V +} + +// https://webidl.spec.whatwg.org/#es-long-long +webidl.converters['long long'] = function (V) { + // 1. Let x be ? ConvertToInt(V, 64, "signed"). + const x = webidl.util.ConvertToInt(V, 64, 'signed') + + // 2. Return the IDL long long value that represents + // the same numeric value as x. + return x +} + +// https://webidl.spec.whatwg.org/#es-unsigned-long-long +webidl.converters['unsigned long long'] = function (V) { + // 1. Let x be ? ConvertToInt(V, 64, "unsigned"). + const x = webidl.util.ConvertToInt(V, 64, 'unsigned') + + // 2. Return the IDL unsigned long long value that + // represents the same numeric value as x. + return x +} + +// https://webidl.spec.whatwg.org/#es-unsigned-long +webidl.converters['unsigned long'] = function (V) { + // 1. Let x be ? ConvertToInt(V, 32, "unsigned"). + const x = webidl.util.ConvertToInt(V, 32, 'unsigned') + + // 2. Return the IDL unsigned long value that + // represents the same numeric value as x. + return x +} + +// https://webidl.spec.whatwg.org/#es-unsigned-short +webidl.converters['unsigned short'] = function (V, opts) { + // 1. Let x be ? ConvertToInt(V, 16, "unsigned"). + const x = webidl.util.ConvertToInt(V, 16, 'unsigned', opts) + + // 2. Return the IDL unsigned short value that represents + // the same numeric value as x. + return x +} + +// https://webidl.spec.whatwg.org/#idl-ArrayBuffer +webidl.converters.ArrayBuffer = function (V, opts = {}) { + // 1. If Type(V) is not Object, or V does not have an + // [[ArrayBufferData]] internal slot, then throw a + // TypeError. + // see: https://tc39.es/ecma262/#sec-properties-of-the-arraybuffer-instances + // see: https://tc39.es/ecma262/#sec-properties-of-the-sharedarraybuffer-instances + if ( + webidl.util.Type(V) !== 'Object' || + !types.isAnyArrayBuffer(V) + ) { + throw webidl.errors.conversionFailed({ + prefix: `${V}`, + argument: `${V}`, + types: ['ArrayBuffer'] + }) + } + + // 2. If the conversion is not to an IDL type associated + // with the [AllowShared] extended attribute, and + // IsSharedArrayBuffer(V) is true, then throw a + // TypeError. + if (opts.allowShared === false && types.isSharedArrayBuffer(V)) { + throw webidl.errors.exception({ + header: 'ArrayBuffer', + message: 'SharedArrayBuffer is not allowed.' + }) + } + + // 3. If the conversion is not to an IDL type associated + // with the [AllowResizable] extended attribute, and + // IsResizableArrayBuffer(V) is true, then throw a + // TypeError. + // Note: resizable ArrayBuffers are currently a proposal. + + // 4. Return the IDL ArrayBuffer value that is a + // reference to the same object as V. + return V +} + +webidl.converters.TypedArray = function (V, T, opts = {}) { + // 1. Let T be the IDL type V is being converted to. + + // 2. If Type(V) is not Object, or V does not have a + // [[TypedArrayName]] internal slot with a value + // equal to T’s name, then throw a TypeError. + if ( + webidl.util.Type(V) !== 'Object' || + !types.isTypedArray(V) || + V.constructor.name !== T.name + ) { + throw webidl.errors.conversionFailed({ + prefix: `${T.name}`, + argument: `${V}`, + types: [T.name] + }) + } + + // 3. If the conversion is not to an IDL type associated + // with the [AllowShared] extended attribute, and + // IsSharedArrayBuffer(V.[[ViewedArrayBuffer]]) is + // true, then throw a TypeError. + if (opts.allowShared === false && types.isSharedArrayBuffer(V.buffer)) { + throw webidl.errors.exception({ + header: 'ArrayBuffer', + message: 'SharedArrayBuffer is not allowed.' + }) + } + + // 4. If the conversion is not to an IDL type associated + // with the [AllowResizable] extended attribute, and + // IsResizableArrayBuffer(V.[[ViewedArrayBuffer]]) is + // true, then throw a TypeError. + // Note: resizable array buffers are currently a proposal + + // 5. Return the IDL value of type T that is a reference + // to the same object as V. + return V +} + +webidl.converters.DataView = function (V, opts = {}) { + // 1. If Type(V) is not Object, or V does not have a + // [[DataView]] internal slot, then throw a TypeError. + if (webidl.util.Type(V) !== 'Object' || !types.isDataView(V)) { + throw webidl.errors.exception({ + header: 'DataView', + message: 'Object is not a DataView.' + }) + } + + // 2. If the conversion is not to an IDL type associated + // with the [AllowShared] extended attribute, and + // IsSharedArrayBuffer(V.[[ViewedArrayBuffer]]) is true, + // then throw a TypeError. + if (opts.allowShared === false && types.isSharedArrayBuffer(V.buffer)) { + throw webidl.errors.exception({ + header: 'ArrayBuffer', + message: 'SharedArrayBuffer is not allowed.' + }) + } + + // 3. If the conversion is not to an IDL type associated + // with the [AllowResizable] extended attribute, and + // IsResizableArrayBuffer(V.[[ViewedArrayBuffer]]) is + // true, then throw a TypeError. + // Note: resizable ArrayBuffers are currently a proposal + + // 4. Return the IDL DataView value that is a reference + // to the same object as V. + return V +} + +// https://webidl.spec.whatwg.org/#BufferSource +webidl.converters.BufferSource = function (V, opts = {}) { + if (types.isAnyArrayBuffer(V)) { + return webidl.converters.ArrayBuffer(V, opts) + } + + if (types.isTypedArray(V)) { + return webidl.converters.TypedArray(V, V.constructor) + } + + if (types.isDataView(V)) { + return webidl.converters.DataView(V, opts) + } + + throw new TypeError(`Could not convert ${V} to a BufferSource.`) +} + +webidl.converters['sequence'] = webidl.sequenceConverter( + webidl.converters.ByteString +) + +webidl.converters['sequence>'] = webidl.sequenceConverter( + webidl.converters['sequence'] +) + +webidl.converters['record'] = webidl.recordConverter( + webidl.converters.ByteString, + webidl.converters.ByteString +) + +module.exports = { + webidl +} + + +/***/ }), + +/***/ 396: +/***/ ((module) => { + +"use strict"; + + +/** + * @see https://encoding.spec.whatwg.org/#concept-encoding-get + * @param {string|undefined} label + */ +function getEncoding (label) { + if (!label) { + return 'failure' + } + + // 1. Remove any leading and trailing ASCII whitespace from label. + // 2. If label is an ASCII case-insensitive match for any of the + // labels listed in the table below, then return the + // corresponding encoding; otherwise return failure. + switch (label.trim().toLowerCase()) { + case 'unicode-1-1-utf-8': + case 'unicode11utf8': + case 'unicode20utf8': + case 'utf-8': + case 'utf8': + case 'x-unicode20utf8': + return 'UTF-8' + case '866': + case 'cp866': + case 'csibm866': + case 'ibm866': + return 'IBM866' + case 'csisolatin2': + case 'iso-8859-2': + case 'iso-ir-101': + case 'iso8859-2': + case 'iso88592': + case 'iso_8859-2': + case 'iso_8859-2:1987': + case 'l2': + case 'latin2': + return 'ISO-8859-2' + case 'csisolatin3': + case 'iso-8859-3': + case 'iso-ir-109': + case 'iso8859-3': + case 'iso88593': + case 'iso_8859-3': + case 'iso_8859-3:1988': + case 'l3': + case 'latin3': + return 'ISO-8859-3' + case 'csisolatin4': + case 'iso-8859-4': + case 'iso-ir-110': + case 'iso8859-4': + case 'iso88594': + case 'iso_8859-4': + case 'iso_8859-4:1988': + case 'l4': + case 'latin4': + return 'ISO-8859-4' + case 'csisolatincyrillic': + case 'cyrillic': + case 'iso-8859-5': + case 'iso-ir-144': + case 'iso8859-5': + case 'iso88595': + case 'iso_8859-5': + case 'iso_8859-5:1988': + return 'ISO-8859-5' + case 'arabic': + case 'asmo-708': + case 'csiso88596e': + case 'csiso88596i': + case 'csisolatinarabic': + case 'ecma-114': + case 'iso-8859-6': + case 'iso-8859-6-e': + case 'iso-8859-6-i': + case 'iso-ir-127': + case 'iso8859-6': + case 'iso88596': + case 'iso_8859-6': + case 'iso_8859-6:1987': + return 'ISO-8859-6' + case 'csisolatingreek': + case 'ecma-118': + case 'elot_928': + case 'greek': + case 'greek8': + case 'iso-8859-7': + case 'iso-ir-126': + case 'iso8859-7': + case 'iso88597': + case 'iso_8859-7': + case 'iso_8859-7:1987': + case 'sun_eu_greek': + return 'ISO-8859-7' + case 'csiso88598e': + case 'csisolatinhebrew': + case 'hebrew': + case 'iso-8859-8': + case 'iso-8859-8-e': + case 'iso-ir-138': + case 'iso8859-8': + case 'iso88598': + case 'iso_8859-8': + case 'iso_8859-8:1988': + case 'visual': + return 'ISO-8859-8' + case 'csiso88598i': + case 'iso-8859-8-i': + case 'logical': + return 'ISO-8859-8-I' + case 'csisolatin6': + case 'iso-8859-10': + case 'iso-ir-157': + case 'iso8859-10': + case 'iso885910': + case 'l6': + case 'latin6': + return 'ISO-8859-10' + case 'iso-8859-13': + case 'iso8859-13': + case 'iso885913': + return 'ISO-8859-13' + case 'iso-8859-14': + case 'iso8859-14': + case 'iso885914': + return 'ISO-8859-14' + case 'csisolatin9': + case 'iso-8859-15': + case 'iso8859-15': + case 'iso885915': + case 'iso_8859-15': + case 'l9': + return 'ISO-8859-15' + case 'iso-8859-16': + return 'ISO-8859-16' + case 'cskoi8r': + case 'koi': + case 'koi8': + case 'koi8-r': + case 'koi8_r': + return 'KOI8-R' + case 'koi8-ru': + case 'koi8-u': + return 'KOI8-U' + case 'csmacintosh': + case 'mac': + case 'macintosh': + case 'x-mac-roman': + return 'macintosh' + case 'iso-8859-11': + case 'iso8859-11': + case 'iso885911': + case 'tis-620': + case 'windows-874': + return 'windows-874' + case 'cp1250': + case 'windows-1250': + case 'x-cp1250': + return 'windows-1250' + case 'cp1251': + case 'windows-1251': + case 'x-cp1251': + return 'windows-1251' + case 'ansi_x3.4-1968': + case 'ascii': + case 'cp1252': + case 'cp819': + case 'csisolatin1': + case 'ibm819': + case 'iso-8859-1': + case 'iso-ir-100': + case 'iso8859-1': + case 'iso88591': + case 'iso_8859-1': + case 'iso_8859-1:1987': + case 'l1': + case 'latin1': + case 'us-ascii': + case 'windows-1252': + case 'x-cp1252': + return 'windows-1252' + case 'cp1253': + case 'windows-1253': + case 'x-cp1253': + return 'windows-1253' + case 'cp1254': + case 'csisolatin5': + case 'iso-8859-9': + case 'iso-ir-148': + case 'iso8859-9': + case 'iso88599': + case 'iso_8859-9': + case 'iso_8859-9:1989': + case 'l5': + case 'latin5': + case 'windows-1254': + case 'x-cp1254': + return 'windows-1254' + case 'cp1255': + case 'windows-1255': + case 'x-cp1255': + return 'windows-1255' + case 'cp1256': + case 'windows-1256': + case 'x-cp1256': + return 'windows-1256' + case 'cp1257': + case 'windows-1257': + case 'x-cp1257': + return 'windows-1257' + case 'cp1258': + case 'windows-1258': + case 'x-cp1258': + return 'windows-1258' + case 'x-mac-cyrillic': + case 'x-mac-ukrainian': + return 'x-mac-cyrillic' + case 'chinese': + case 'csgb2312': + case 'csiso58gb231280': + case 'gb2312': + case 'gb_2312': + case 'gb_2312-80': + case 'gbk': + case 'iso-ir-58': + case 'x-gbk': + return 'GBK' + case 'gb18030': + return 'gb18030' + case 'big5': + case 'big5-hkscs': + case 'cn-big5': + case 'csbig5': + case 'x-x-big5': + return 'Big5' + case 'cseucpkdfmtjapanese': + case 'euc-jp': + case 'x-euc-jp': + return 'EUC-JP' + case 'csiso2022jp': + case 'iso-2022-jp': + return 'ISO-2022-JP' + case 'csshiftjis': + case 'ms932': + case 'ms_kanji': + case 'shift-jis': + case 'shift_jis': + case 'sjis': + case 'windows-31j': + case 'x-sjis': + return 'Shift_JIS' + case 'cseuckr': + case 'csksc56011987': + case 'euc-kr': + case 'iso-ir-149': + case 'korean': + case 'ks_c_5601-1987': + case 'ks_c_5601-1989': + case 'ksc5601': + case 'ksc_5601': + case 'windows-949': + return 'EUC-KR' + case 'csiso2022kr': + case 'hz-gb-2312': + case 'iso-2022-cn': + case 'iso-2022-cn-ext': + case 'iso-2022-kr': + case 'replacement': + return 'replacement' + case 'unicodefffe': + case 'utf-16be': + return 'UTF-16BE' + case 'csunicode': + case 'iso-10646-ucs-2': + case 'ucs-2': + case 'unicode': + case 'unicodefeff': + case 'utf-16': + case 'utf-16le': + return 'UTF-16LE' + case 'x-user-defined': + return 'x-user-defined' + default: return 'failure' + } +} + +module.exports = { + getEncoding +} + + +/***/ }), + +/***/ 2160: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const { + staticPropertyDescriptors, + readOperation, + fireAProgressEvent +} = __nccwpck_require__(165) +const { + kState, + kError, + kResult, + kEvents, + kAborted +} = __nccwpck_require__(6812) +const { webidl } = __nccwpck_require__(4222) +const { kEnumerableProperty } = __nccwpck_require__(3440) + +class FileReader extends EventTarget { + constructor () { + super() + + this[kState] = 'empty' + this[kResult] = null + this[kError] = null + this[kEvents] = { + loadend: null, + error: null, + abort: null, + load: null, + progress: null, + loadstart: null + } + } + + /** + * @see https://w3c.github.io/FileAPI/#dfn-readAsArrayBuffer + * @param {import('buffer').Blob} blob + */ + readAsArrayBuffer (blob) { + webidl.brandCheck(this, FileReader) + + webidl.argumentLengthCheck(arguments, 1, { header: 'FileReader.readAsArrayBuffer' }) + + blob = webidl.converters.Blob(blob, { strict: false }) + + // The readAsArrayBuffer(blob) method, when invoked, + // must initiate a read operation for blob with ArrayBuffer. + readOperation(this, blob, 'ArrayBuffer') + } + + /** + * @see https://w3c.github.io/FileAPI/#readAsBinaryString + * @param {import('buffer').Blob} blob + */ + readAsBinaryString (blob) { + webidl.brandCheck(this, FileReader) + + webidl.argumentLengthCheck(arguments, 1, { header: 'FileReader.readAsBinaryString' }) + + blob = webidl.converters.Blob(blob, { strict: false }) + + // The readAsBinaryString(blob) method, when invoked, + // must initiate a read operation for blob with BinaryString. + readOperation(this, blob, 'BinaryString') + } + + /** + * @see https://w3c.github.io/FileAPI/#readAsDataText + * @param {import('buffer').Blob} blob + * @param {string?} encoding + */ + readAsText (blob, encoding = undefined) { + webidl.brandCheck(this, FileReader) + + webidl.argumentLengthCheck(arguments, 1, { header: 'FileReader.readAsText' }) + + blob = webidl.converters.Blob(blob, { strict: false }) + + if (encoding !== undefined) { + encoding = webidl.converters.DOMString(encoding) + } + + // The readAsText(blob, encoding) method, when invoked, + // must initiate a read operation for blob with Text and encoding. + readOperation(this, blob, 'Text', encoding) + } + + /** + * @see https://w3c.github.io/FileAPI/#dfn-readAsDataURL + * @param {import('buffer').Blob} blob + */ + readAsDataURL (blob) { + webidl.brandCheck(this, FileReader) + + webidl.argumentLengthCheck(arguments, 1, { header: 'FileReader.readAsDataURL' }) + + blob = webidl.converters.Blob(blob, { strict: false }) + + // The readAsDataURL(blob) method, when invoked, must + // initiate a read operation for blob with DataURL. + readOperation(this, blob, 'DataURL') + } + + /** + * @see https://w3c.github.io/FileAPI/#dfn-abort + */ + abort () { + // 1. If this's state is "empty" or if this's state is + // "done" set this's result to null and terminate + // this algorithm. + if (this[kState] === 'empty' || this[kState] === 'done') { + this[kResult] = null + return + } + + // 2. If this's state is "loading" set this's state to + // "done" and set this's result to null. + if (this[kState] === 'loading') { + this[kState] = 'done' + this[kResult] = null + } + + // 3. If there are any tasks from this on the file reading + // task source in an affiliated task queue, then remove + // those tasks from that task queue. + this[kAborted] = true + + // 4. Terminate the algorithm for the read method being processed. + // TODO + + // 5. Fire a progress event called abort at this. + fireAProgressEvent('abort', this) + + // 6. If this's state is not "loading", fire a progress + // event called loadend at this. + if (this[kState] !== 'loading') { + fireAProgressEvent('loadend', this) + } + } + + /** + * @see https://w3c.github.io/FileAPI/#dom-filereader-readystate + */ + get readyState () { + webidl.brandCheck(this, FileReader) + + switch (this[kState]) { + case 'empty': return this.EMPTY + case 'loading': return this.LOADING + case 'done': return this.DONE + } + } + + /** + * @see https://w3c.github.io/FileAPI/#dom-filereader-result + */ + get result () { + webidl.brandCheck(this, FileReader) + + // The result attribute’s getter, when invoked, must return + // this's result. + return this[kResult] + } + + /** + * @see https://w3c.github.io/FileAPI/#dom-filereader-error + */ + get error () { + webidl.brandCheck(this, FileReader) + + // The error attribute’s getter, when invoked, must return + // this's error. + return this[kError] + } + + get onloadend () { + webidl.brandCheck(this, FileReader) + + return this[kEvents].loadend + } + + set onloadend (fn) { + webidl.brandCheck(this, FileReader) + + if (this[kEvents].loadend) { + this.removeEventListener('loadend', this[kEvents].loadend) + } + + if (typeof fn === 'function') { + this[kEvents].loadend = fn + this.addEventListener('loadend', fn) + } else { + this[kEvents].loadend = null + } + } + + get onerror () { + webidl.brandCheck(this, FileReader) + + return this[kEvents].error + } + + set onerror (fn) { + webidl.brandCheck(this, FileReader) + + if (this[kEvents].error) { + this.removeEventListener('error', this[kEvents].error) + } + + if (typeof fn === 'function') { + this[kEvents].error = fn + this.addEventListener('error', fn) + } else { + this[kEvents].error = null + } + } + + get onloadstart () { + webidl.brandCheck(this, FileReader) + + return this[kEvents].loadstart + } + + set onloadstart (fn) { + webidl.brandCheck(this, FileReader) + + if (this[kEvents].loadstart) { + this.removeEventListener('loadstart', this[kEvents].loadstart) + } + + if (typeof fn === 'function') { + this[kEvents].loadstart = fn + this.addEventListener('loadstart', fn) + } else { + this[kEvents].loadstart = null + } + } + + get onprogress () { + webidl.brandCheck(this, FileReader) + + return this[kEvents].progress + } + + set onprogress (fn) { + webidl.brandCheck(this, FileReader) + + if (this[kEvents].progress) { + this.removeEventListener('progress', this[kEvents].progress) + } + + if (typeof fn === 'function') { + this[kEvents].progress = fn + this.addEventListener('progress', fn) + } else { + this[kEvents].progress = null + } + } + + get onload () { + webidl.brandCheck(this, FileReader) + + return this[kEvents].load + } + + set onload (fn) { + webidl.brandCheck(this, FileReader) + + if (this[kEvents].load) { + this.removeEventListener('load', this[kEvents].load) + } + + if (typeof fn === 'function') { + this[kEvents].load = fn + this.addEventListener('load', fn) + } else { + this[kEvents].load = null + } + } + + get onabort () { + webidl.brandCheck(this, FileReader) + + return this[kEvents].abort + } + + set onabort (fn) { + webidl.brandCheck(this, FileReader) + + if (this[kEvents].abort) { + this.removeEventListener('abort', this[kEvents].abort) + } + + if (typeof fn === 'function') { + this[kEvents].abort = fn + this.addEventListener('abort', fn) + } else { + this[kEvents].abort = null + } + } +} + +// https://w3c.github.io/FileAPI/#dom-filereader-empty +FileReader.EMPTY = FileReader.prototype.EMPTY = 0 +// https://w3c.github.io/FileAPI/#dom-filereader-loading +FileReader.LOADING = FileReader.prototype.LOADING = 1 +// https://w3c.github.io/FileAPI/#dom-filereader-done +FileReader.DONE = FileReader.prototype.DONE = 2 + +Object.defineProperties(FileReader.prototype, { + EMPTY: staticPropertyDescriptors, + LOADING: staticPropertyDescriptors, + DONE: staticPropertyDescriptors, + readAsArrayBuffer: kEnumerableProperty, + readAsBinaryString: kEnumerableProperty, + readAsText: kEnumerableProperty, + readAsDataURL: kEnumerableProperty, + abort: kEnumerableProperty, + readyState: kEnumerableProperty, + result: kEnumerableProperty, + error: kEnumerableProperty, + onloadstart: kEnumerableProperty, + onprogress: kEnumerableProperty, + onload: kEnumerableProperty, + onabort: kEnumerableProperty, + onerror: kEnumerableProperty, + onloadend: kEnumerableProperty, + [Symbol.toStringTag]: { + value: 'FileReader', + writable: false, + enumerable: false, + configurable: true + } +}) + +Object.defineProperties(FileReader, { + EMPTY: staticPropertyDescriptors, + LOADING: staticPropertyDescriptors, + DONE: staticPropertyDescriptors +}) + +module.exports = { + FileReader +} + + +/***/ }), + +/***/ 5976: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const { webidl } = __nccwpck_require__(4222) + +const kState = Symbol('ProgressEvent state') + +/** + * @see https://xhr.spec.whatwg.org/#progressevent + */ +class ProgressEvent extends Event { + constructor (type, eventInitDict = {}) { + type = webidl.converters.DOMString(type) + eventInitDict = webidl.converters.ProgressEventInit(eventInitDict ?? {}) + + super(type, eventInitDict) + + this[kState] = { + lengthComputable: eventInitDict.lengthComputable, + loaded: eventInitDict.loaded, + total: eventInitDict.total + } + } + + get lengthComputable () { + webidl.brandCheck(this, ProgressEvent) + + return this[kState].lengthComputable + } + + get loaded () { + webidl.brandCheck(this, ProgressEvent) + + return this[kState].loaded + } + + get total () { + webidl.brandCheck(this, ProgressEvent) + + return this[kState].total + } +} + +webidl.converters.ProgressEventInit = webidl.dictionaryConverter([ + { + key: 'lengthComputable', + converter: webidl.converters.boolean, + defaultValue: false + }, + { + key: 'loaded', + converter: webidl.converters['unsigned long long'], + defaultValue: 0 + }, + { + key: 'total', + converter: webidl.converters['unsigned long long'], + defaultValue: 0 + }, + { + key: 'bubbles', + converter: webidl.converters.boolean, + defaultValue: false + }, + { + key: 'cancelable', + converter: webidl.converters.boolean, + defaultValue: false + }, + { + key: 'composed', + converter: webidl.converters.boolean, + defaultValue: false + } +]) + +module.exports = { + ProgressEvent +} + + +/***/ }), + +/***/ 6812: +/***/ ((module) => { + +"use strict"; + + +module.exports = { + kState: Symbol('FileReader state'), + kResult: Symbol('FileReader result'), + kError: Symbol('FileReader error'), + kLastProgressEventFired: Symbol('FileReader last progress event fired timestamp'), + kEvents: Symbol('FileReader events'), + kAborted: Symbol('FileReader aborted') +} + + +/***/ }), + +/***/ 165: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const { + kState, + kError, + kResult, + kAborted, + kLastProgressEventFired +} = __nccwpck_require__(6812) +const { ProgressEvent } = __nccwpck_require__(5976) +const { getEncoding } = __nccwpck_require__(396) +const { DOMException } = __nccwpck_require__(7326) +const { serializeAMimeType, parseMIMEType } = __nccwpck_require__(4322) +const { types } = __nccwpck_require__(9023) +const { StringDecoder } = __nccwpck_require__(3193) +const { btoa } = __nccwpck_require__(181) + +/** @type {PropertyDescriptor} */ +const staticPropertyDescriptors = { + enumerable: true, + writable: false, + configurable: false +} + +/** + * @see https://w3c.github.io/FileAPI/#readOperation + * @param {import('./filereader').FileReader} fr + * @param {import('buffer').Blob} blob + * @param {string} type + * @param {string?} encodingName + */ +function readOperation (fr, blob, type, encodingName) { + // 1. If fr’s state is "loading", throw an InvalidStateError + // DOMException. + if (fr[kState] === 'loading') { + throw new DOMException('Invalid state', 'InvalidStateError') + } + + // 2. Set fr’s state to "loading". + fr[kState] = 'loading' + + // 3. Set fr’s result to null. + fr[kResult] = null + + // 4. Set fr’s error to null. + fr[kError] = null + + // 5. Let stream be the result of calling get stream on blob. + /** @type {import('stream/web').ReadableStream} */ + const stream = blob.stream() + + // 6. Let reader be the result of getting a reader from stream. + const reader = stream.getReader() + + // 7. Let bytes be an empty byte sequence. + /** @type {Uint8Array[]} */ + const bytes = [] + + // 8. Let chunkPromise be the result of reading a chunk from + // stream with reader. + let chunkPromise = reader.read() + + // 9. Let isFirstChunk be true. + let isFirstChunk = true + + // 10. In parallel, while true: + // Note: "In parallel" just means non-blocking + // Note 2: readOperation itself cannot be async as double + // reading the body would then reject the promise, instead + // of throwing an error. + ;(async () => { + while (!fr[kAborted]) { + // 1. Wait for chunkPromise to be fulfilled or rejected. + try { + const { done, value } = await chunkPromise + + // 2. If chunkPromise is fulfilled, and isFirstChunk is + // true, queue a task to fire a progress event called + // loadstart at fr. + if (isFirstChunk && !fr[kAborted]) { + queueMicrotask(() => { + fireAProgressEvent('loadstart', fr) + }) + } + + // 3. Set isFirstChunk to false. + isFirstChunk = false + + // 4. If chunkPromise is fulfilled with an object whose + // done property is false and whose value property is + // a Uint8Array object, run these steps: + if (!done && types.isUint8Array(value)) { + // 1. Let bs be the byte sequence represented by the + // Uint8Array object. + + // 2. Append bs to bytes. + bytes.push(value) + + // 3. If roughly 50ms have passed since these steps + // were last invoked, queue a task to fire a + // progress event called progress at fr. + if ( + ( + fr[kLastProgressEventFired] === undefined || + Date.now() - fr[kLastProgressEventFired] >= 50 + ) && + !fr[kAborted] + ) { + fr[kLastProgressEventFired] = Date.now() + queueMicrotask(() => { + fireAProgressEvent('progress', fr) + }) + } + + // 4. Set chunkPromise to the result of reading a + // chunk from stream with reader. + chunkPromise = reader.read() + } else if (done) { + // 5. Otherwise, if chunkPromise is fulfilled with an + // object whose done property is true, queue a task + // to run the following steps and abort this algorithm: + queueMicrotask(() => { + // 1. Set fr’s state to "done". + fr[kState] = 'done' + + // 2. Let result be the result of package data given + // bytes, type, blob’s type, and encodingName. + try { + const result = packageData(bytes, type, blob.type, encodingName) + + // 4. Else: + + if (fr[kAborted]) { + return + } + + // 1. Set fr’s result to result. + fr[kResult] = result + + // 2. Fire a progress event called load at the fr. + fireAProgressEvent('load', fr) + } catch (error) { + // 3. If package data threw an exception error: + + // 1. Set fr’s error to error. + fr[kError] = error + + // 2. Fire a progress event called error at fr. + fireAProgressEvent('error', fr) + } + + // 5. If fr’s state is not "loading", fire a progress + // event called loadend at the fr. + if (fr[kState] !== 'loading') { + fireAProgressEvent('loadend', fr) + } + }) + + break + } + } catch (error) { + if (fr[kAborted]) { + return + } + + // 6. Otherwise, if chunkPromise is rejected with an + // error error, queue a task to run the following + // steps and abort this algorithm: + queueMicrotask(() => { + // 1. Set fr’s state to "done". + fr[kState] = 'done' + + // 2. Set fr’s error to error. + fr[kError] = error + + // 3. Fire a progress event called error at fr. + fireAProgressEvent('error', fr) + + // 4. If fr’s state is not "loading", fire a progress + // event called loadend at fr. + if (fr[kState] !== 'loading') { + fireAProgressEvent('loadend', fr) + } + }) + + break + } + } + })() +} + +/** + * @see https://w3c.github.io/FileAPI/#fire-a-progress-event + * @see https://dom.spec.whatwg.org/#concept-event-fire + * @param {string} e The name of the event + * @param {import('./filereader').FileReader} reader + */ +function fireAProgressEvent (e, reader) { + // The progress event e does not bubble. e.bubbles must be false + // The progress event e is NOT cancelable. e.cancelable must be false + const event = new ProgressEvent(e, { + bubbles: false, + cancelable: false + }) + + reader.dispatchEvent(event) +} + +/** + * @see https://w3c.github.io/FileAPI/#blob-package-data + * @param {Uint8Array[]} bytes + * @param {string} type + * @param {string?} mimeType + * @param {string?} encodingName + */ +function packageData (bytes, type, mimeType, encodingName) { + // 1. A Blob has an associated package data algorithm, given + // bytes, a type, a optional mimeType, and a optional + // encodingName, which switches on type and runs the + // associated steps: + + switch (type) { + case 'DataURL': { + // 1. Return bytes as a DataURL [RFC2397] subject to + // the considerations below: + // * Use mimeType as part of the Data URL if it is + // available in keeping with the Data URL + // specification [RFC2397]. + // * If mimeType is not available return a Data URL + // without a media-type. [RFC2397]. + + // https://datatracker.ietf.org/doc/html/rfc2397#section-3 + // dataurl := "data:" [ mediatype ] [ ";base64" ] "," data + // mediatype := [ type "/" subtype ] *( ";" parameter ) + // data := *urlchar + // parameter := attribute "=" value + let dataURL = 'data:' + + const parsed = parseMIMEType(mimeType || 'application/octet-stream') + + if (parsed !== 'failure') { + dataURL += serializeAMimeType(parsed) + } + + dataURL += ';base64,' + + const decoder = new StringDecoder('latin1') + + for (const chunk of bytes) { + dataURL += btoa(decoder.write(chunk)) + } + + dataURL += btoa(decoder.end()) + + return dataURL + } + case 'Text': { + // 1. Let encoding be failure + let encoding = 'failure' + + // 2. If the encodingName is present, set encoding to the + // result of getting an encoding from encodingName. + if (encodingName) { + encoding = getEncoding(encodingName) + } + + // 3. If encoding is failure, and mimeType is present: + if (encoding === 'failure' && mimeType) { + // 1. Let type be the result of parse a MIME type + // given mimeType. + const type = parseMIMEType(mimeType) + + // 2. If type is not failure, set encoding to the result + // of getting an encoding from type’s parameters["charset"]. + if (type !== 'failure') { + encoding = getEncoding(type.parameters.get('charset')) + } + } + + // 4. If encoding is failure, then set encoding to UTF-8. + if (encoding === 'failure') { + encoding = 'UTF-8' + } + + // 5. Decode bytes using fallback encoding encoding, and + // return the result. + return decode(bytes, encoding) + } + case 'ArrayBuffer': { + // Return a new ArrayBuffer whose contents are bytes. + const sequence = combineByteSequences(bytes) + + return sequence.buffer + } + case 'BinaryString': { + // Return bytes as a binary string, in which every byte + // is represented by a code unit of equal value [0..255]. + let binaryString = '' + + const decoder = new StringDecoder('latin1') + + for (const chunk of bytes) { + binaryString += decoder.write(chunk) + } + + binaryString += decoder.end() + + return binaryString + } + } +} + +/** + * @see https://encoding.spec.whatwg.org/#decode + * @param {Uint8Array[]} ioQueue + * @param {string} encoding + */ +function decode (ioQueue, encoding) { + const bytes = combineByteSequences(ioQueue) + + // 1. Let BOMEncoding be the result of BOM sniffing ioQueue. + const BOMEncoding = BOMSniffing(bytes) + + let slice = 0 + + // 2. If BOMEncoding is non-null: + if (BOMEncoding !== null) { + // 1. Set encoding to BOMEncoding. + encoding = BOMEncoding + + // 2. Read three bytes from ioQueue, if BOMEncoding is + // UTF-8; otherwise read two bytes. + // (Do nothing with those bytes.) + slice = BOMEncoding === 'UTF-8' ? 3 : 2 + } + + // 3. Process a queue with an instance of encoding’s + // decoder, ioQueue, output, and "replacement". + + // 4. Return output. + + const sliced = bytes.slice(slice) + return new TextDecoder(encoding).decode(sliced) +} + +/** + * @see https://encoding.spec.whatwg.org/#bom-sniff + * @param {Uint8Array} ioQueue + */ +function BOMSniffing (ioQueue) { + // 1. Let BOM be the result of peeking 3 bytes from ioQueue, + // converted to a byte sequence. + const [a, b, c] = ioQueue + + // 2. For each of the rows in the table below, starting with + // the first one and going down, if BOM starts with the + // bytes given in the first column, then return the + // encoding given in the cell in the second column of that + // row. Otherwise, return null. + if (a === 0xEF && b === 0xBB && c === 0xBF) { + return 'UTF-8' + } else if (a === 0xFE && b === 0xFF) { + return 'UTF-16BE' + } else if (a === 0xFF && b === 0xFE) { + return 'UTF-16LE' + } + + return null +} + +/** + * @param {Uint8Array[]} sequences + */ +function combineByteSequences (sequences) { + const size = sequences.reduce((a, b) => { + return a + b.byteLength + }, 0) + + let offset = 0 + + return sequences.reduce((a, b) => { + a.set(b, offset) + offset += b.byteLength + return a + }, new Uint8Array(size)) +} + +module.exports = { + staticPropertyDescriptors, + readOperation, + fireAProgressEvent +} + + +/***/ }), + +/***/ 2581: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +// We include a version number for the Dispatcher API. In case of breaking changes, +// this version number must be increased to avoid conflicts. +const globalDispatcher = Symbol.for('undici.globalDispatcher.1') +const { InvalidArgumentError } = __nccwpck_require__(8707) +const Agent = __nccwpck_require__(9965) + +if (getGlobalDispatcher() === undefined) { + setGlobalDispatcher(new Agent()) +} + +function setGlobalDispatcher (agent) { + if (!agent || typeof agent.dispatch !== 'function') { + throw new InvalidArgumentError('Argument agent must implement Agent') + } + Object.defineProperty(globalThis, globalDispatcher, { + value: agent, + writable: true, + enumerable: false, + configurable: false + }) +} + +function getGlobalDispatcher () { + return globalThis[globalDispatcher] +} + +module.exports = { + setGlobalDispatcher, + getGlobalDispatcher +} + + +/***/ }), + +/***/ 8840: +/***/ ((module) => { + +"use strict"; + + +module.exports = class DecoratorHandler { + constructor (handler) { + this.handler = handler + } + + onConnect (...args) { + return this.handler.onConnect(...args) + } + + onError (...args) { + return this.handler.onError(...args) + } + + onUpgrade (...args) { + return this.handler.onUpgrade(...args) + } + + onHeaders (...args) { + return this.handler.onHeaders(...args) + } + + onData (...args) { + return this.handler.onData(...args) + } + + onComplete (...args) { + return this.handler.onComplete(...args) + } + + onBodySent (...args) { + return this.handler.onBodySent(...args) + } +} + + +/***/ }), + +/***/ 8299: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const util = __nccwpck_require__(3440) +const { kBodyUsed } = __nccwpck_require__(6443) +const assert = __nccwpck_require__(2613) +const { InvalidArgumentError } = __nccwpck_require__(8707) +const EE = __nccwpck_require__(4434) + +const redirectableStatusCodes = [300, 301, 302, 303, 307, 308] + +const kBody = Symbol('body') + +class BodyAsyncIterable { + constructor (body) { + this[kBody] = body + this[kBodyUsed] = false + } + + async * [Symbol.asyncIterator] () { + assert(!this[kBodyUsed], 'disturbed') + this[kBodyUsed] = true + yield * this[kBody] + } +} + +class RedirectHandler { + constructor (dispatch, maxRedirections, opts, handler) { + if (maxRedirections != null && (!Number.isInteger(maxRedirections) || maxRedirections < 0)) { + throw new InvalidArgumentError('maxRedirections must be a positive number') + } + + util.validateHandler(handler, opts.method, opts.upgrade) + + this.dispatch = dispatch + this.location = null + this.abort = null + this.opts = { ...opts, maxRedirections: 0 } // opts must be a copy + this.maxRedirections = maxRedirections + this.handler = handler + this.history = [] + + if (util.isStream(this.opts.body)) { + // TODO (fix): Provide some way for the user to cache the file to e.g. /tmp + // so that it can be dispatched again? + // TODO (fix): Do we need 100-expect support to provide a way to do this properly? + if (util.bodyLength(this.opts.body) === 0) { + this.opts.body + .on('data', function () { + assert(false) + }) + } + + if (typeof this.opts.body.readableDidRead !== 'boolean') { + this.opts.body[kBodyUsed] = false + EE.prototype.on.call(this.opts.body, 'data', function () { + this[kBodyUsed] = true + }) + } + } else if (this.opts.body && typeof this.opts.body.pipeTo === 'function') { + // TODO (fix): We can't access ReadableStream internal state + // to determine whether or not it has been disturbed. This is just + // a workaround. + this.opts.body = new BodyAsyncIterable(this.opts.body) + } else if ( + this.opts.body && + typeof this.opts.body !== 'string' && + !ArrayBuffer.isView(this.opts.body) && + util.isIterable(this.opts.body) + ) { + // TODO: Should we allow re-using iterable if !this.opts.idempotent + // or through some other flag? + this.opts.body = new BodyAsyncIterable(this.opts.body) + } + } + + onConnect (abort) { + this.abort = abort + this.handler.onConnect(abort, { history: this.history }) + } + + onUpgrade (statusCode, headers, socket) { + this.handler.onUpgrade(statusCode, headers, socket) + } + + onError (error) { + this.handler.onError(error) + } + + onHeaders (statusCode, headers, resume, statusText) { + this.location = this.history.length >= this.maxRedirections || util.isDisturbed(this.opts.body) + ? null + : parseLocation(statusCode, headers) + + if (this.opts.origin) { + this.history.push(new URL(this.opts.path, this.opts.origin)) + } + + if (!this.location) { + return this.handler.onHeaders(statusCode, headers, resume, statusText) + } + + const { origin, pathname, search } = util.parseURL(new URL(this.location, this.opts.origin && new URL(this.opts.path, this.opts.origin))) + const path = search ? `${pathname}${search}` : pathname + + // Remove headers referring to the original URL. + // By default it is Host only, unless it's a 303 (see below), which removes also all Content-* headers. + // https://tools.ietf.org/html/rfc7231#section-6.4 + this.opts.headers = cleanRequestHeaders(this.opts.headers, statusCode === 303, this.opts.origin !== origin) + this.opts.path = path + this.opts.origin = origin + this.opts.maxRedirections = 0 + this.opts.query = null + + // https://tools.ietf.org/html/rfc7231#section-6.4.4 + // In case of HTTP 303, always replace method to be either HEAD or GET + if (statusCode === 303 && this.opts.method !== 'HEAD') { + this.opts.method = 'GET' + this.opts.body = null + } + } + + onData (chunk) { + if (this.location) { + /* + https://tools.ietf.org/html/rfc7231#section-6.4 + + TLDR: undici always ignores 3xx response bodies. + + Redirection is used to serve the requested resource from another URL, so it is assumes that + no body is generated (and thus can be ignored). Even though generating a body is not prohibited. + + For status 301, 302, 303, 307 and 308 (the latter from RFC 7238), the specs mention that the body usually + (which means it's optional and not mandated) contain just an hyperlink to the value of + the Location response header, so the body can be ignored safely. + + For status 300, which is "Multiple Choices", the spec mentions both generating a Location + response header AND a response body with the other possible location to follow. + Since the spec explicitily chooses not to specify a format for such body and leave it to + servers and browsers implementors, we ignore the body as there is no specified way to eventually parse it. + */ + } else { + return this.handler.onData(chunk) + } + } + + onComplete (trailers) { + if (this.location) { + /* + https://tools.ietf.org/html/rfc7231#section-6.4 + + TLDR: undici always ignores 3xx response trailers as they are not expected in case of redirections + and neither are useful if present. + + See comment on onData method above for more detailed informations. + */ + + this.location = null + this.abort = null + + this.dispatch(this.opts, this) + } else { + this.handler.onComplete(trailers) + } + } + + onBodySent (chunk) { + if (this.handler.onBodySent) { + this.handler.onBodySent(chunk) + } + } +} + +function parseLocation (statusCode, headers) { + if (redirectableStatusCodes.indexOf(statusCode) === -1) { + return null + } + + for (let i = 0; i < headers.length; i += 2) { + if (headers[i].toString().toLowerCase() === 'location') { + return headers[i + 1] + } + } +} + +// https://tools.ietf.org/html/rfc7231#section-6.4.4 +function shouldRemoveHeader (header, removeContent, unknownOrigin) { + if (header.length === 4) { + return util.headerNameToString(header) === 'host' + } + if (removeContent && util.headerNameToString(header).startsWith('content-')) { + return true + } + if (unknownOrigin && (header.length === 13 || header.length === 6 || header.length === 19)) { + const name = util.headerNameToString(header) + return name === 'authorization' || name === 'cookie' || name === 'proxy-authorization' + } + return false +} + +// https://tools.ietf.org/html/rfc7231#section-6.4 +function cleanRequestHeaders (headers, removeContent, unknownOrigin) { + const ret = [] + if (Array.isArray(headers)) { + for (let i = 0; i < headers.length; i += 2) { + if (!shouldRemoveHeader(headers[i], removeContent, unknownOrigin)) { + ret.push(headers[i], headers[i + 1]) + } + } + } else if (headers && typeof headers === 'object') { + for (const key of Object.keys(headers)) { + if (!shouldRemoveHeader(key, removeContent, unknownOrigin)) { + ret.push(key, headers[key]) + } + } + } else { + assert(headers == null, 'headers must be an object or an array') + } + return ret +} + +module.exports = RedirectHandler + + +/***/ }), + +/***/ 3573: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +const assert = __nccwpck_require__(2613) + +const { kRetryHandlerDefaultRetry } = __nccwpck_require__(6443) +const { RequestRetryError } = __nccwpck_require__(8707) +const { isDisturbed, parseHeaders, parseRangeHeader } = __nccwpck_require__(3440) + +function calculateRetryAfterHeader (retryAfter) { + const current = Date.now() + const diff = new Date(retryAfter).getTime() - current + + return diff +} + +class RetryHandler { + constructor (opts, handlers) { + const { retryOptions, ...dispatchOpts } = opts + const { + // Retry scoped + retry: retryFn, + maxRetries, + maxTimeout, + minTimeout, + timeoutFactor, + // Response scoped + methods, + errorCodes, + retryAfter, + statusCodes + } = retryOptions ?? {} + + this.dispatch = handlers.dispatch + this.handler = handlers.handler + this.opts = dispatchOpts + this.abort = null + this.aborted = false + this.retryOpts = { + retry: retryFn ?? RetryHandler[kRetryHandlerDefaultRetry], + retryAfter: retryAfter ?? true, + maxTimeout: maxTimeout ?? 30 * 1000, // 30s, + timeout: minTimeout ?? 500, // .5s + timeoutFactor: timeoutFactor ?? 2, + maxRetries: maxRetries ?? 5, + // What errors we should retry + methods: methods ?? ['GET', 'HEAD', 'OPTIONS', 'PUT', 'DELETE', 'TRACE'], + // Indicates which errors to retry + statusCodes: statusCodes ?? [500, 502, 503, 504, 429], + // List of errors to retry + errorCodes: errorCodes ?? [ + 'ECONNRESET', + 'ECONNREFUSED', + 'ENOTFOUND', + 'ENETDOWN', + 'ENETUNREACH', + 'EHOSTDOWN', + 'EHOSTUNREACH', + 'EPIPE' + ] + } + + this.retryCount = 0 + this.start = 0 + this.end = null + this.etag = null + this.resume = null + + // Handle possible onConnect duplication + this.handler.onConnect(reason => { + this.aborted = true + if (this.abort) { + this.abort(reason) + } else { + this.reason = reason + } + }) + } + + onRequestSent () { + if (this.handler.onRequestSent) { + this.handler.onRequestSent() + } + } + + onUpgrade (statusCode, headers, socket) { + if (this.handler.onUpgrade) { + this.handler.onUpgrade(statusCode, headers, socket) + } + } + + onConnect (abort) { + if (this.aborted) { + abort(this.reason) + } else { + this.abort = abort + } + } + + onBodySent (chunk) { + if (this.handler.onBodySent) return this.handler.onBodySent(chunk) + } + + static [kRetryHandlerDefaultRetry] (err, { state, opts }, cb) { + const { statusCode, code, headers } = err + const { method, retryOptions } = opts + const { + maxRetries, + timeout, + maxTimeout, + timeoutFactor, + statusCodes, + errorCodes, + methods + } = retryOptions + let { counter, currentTimeout } = state + + currentTimeout = + currentTimeout != null && currentTimeout > 0 ? currentTimeout : timeout + + // Any code that is not a Undici's originated and allowed to retry + if ( + code && + code !== 'UND_ERR_REQ_RETRY' && + code !== 'UND_ERR_SOCKET' && + !errorCodes.includes(code) + ) { + cb(err) + return + } + + // If a set of method are provided and the current method is not in the list + if (Array.isArray(methods) && !methods.includes(method)) { + cb(err) + return + } + + // If a set of status code are provided and the current status code is not in the list + if ( + statusCode != null && + Array.isArray(statusCodes) && + !statusCodes.includes(statusCode) + ) { + cb(err) + return + } + + // If we reached the max number of retries + if (counter > maxRetries) { + cb(err) + return + } + + let retryAfterHeader = headers != null && headers['retry-after'] + if (retryAfterHeader) { + retryAfterHeader = Number(retryAfterHeader) + retryAfterHeader = isNaN(retryAfterHeader) + ? calculateRetryAfterHeader(retryAfterHeader) + : retryAfterHeader * 1e3 // Retry-After is in seconds + } + + const retryTimeout = + retryAfterHeader > 0 + ? Math.min(retryAfterHeader, maxTimeout) + : Math.min(currentTimeout * timeoutFactor ** counter, maxTimeout) + + state.currentTimeout = retryTimeout + + setTimeout(() => cb(null), retryTimeout) + } + + onHeaders (statusCode, rawHeaders, resume, statusMessage) { + const headers = parseHeaders(rawHeaders) + + this.retryCount += 1 + + if (statusCode >= 300) { + this.abort( + new RequestRetryError('Request failed', statusCode, { + headers, + count: this.retryCount + }) + ) + return false + } + + // Checkpoint for resume from where we left it + if (this.resume != null) { + this.resume = null + + if (statusCode !== 206) { + return true + } + + const contentRange = parseRangeHeader(headers['content-range']) + // If no content range + if (!contentRange) { + this.abort( + new RequestRetryError('Content-Range mismatch', statusCode, { + headers, + count: this.retryCount + }) + ) + return false + } + + // Let's start with a weak etag check + if (this.etag != null && this.etag !== headers.etag) { + this.abort( + new RequestRetryError('ETag mismatch', statusCode, { + headers, + count: this.retryCount + }) + ) + return false + } + + const { start, size, end = size } = contentRange + + assert(this.start === start, 'content-range mismatch') + assert(this.end == null || this.end === end, 'content-range mismatch') + + this.resume = resume + return true + } + + if (this.end == null) { + if (statusCode === 206) { + // First time we receive 206 + const range = parseRangeHeader(headers['content-range']) + + if (range == null) { + return this.handler.onHeaders( + statusCode, + rawHeaders, + resume, + statusMessage + ) + } + + const { start, size, end = size } = range + + assert( + start != null && Number.isFinite(start) && this.start !== start, + 'content-range mismatch' + ) + assert(Number.isFinite(start)) + assert( + end != null && Number.isFinite(end) && this.end !== end, + 'invalid content-length' + ) + + this.start = start + this.end = end + } + + // We make our best to checkpoint the body for further range headers + if (this.end == null) { + const contentLength = headers['content-length'] + this.end = contentLength != null ? Number(contentLength) : null + } + + assert(Number.isFinite(this.start)) + assert( + this.end == null || Number.isFinite(this.end), + 'invalid content-length' + ) + + this.resume = resume + this.etag = headers.etag != null ? headers.etag : null + + return this.handler.onHeaders( + statusCode, + rawHeaders, + resume, + statusMessage + ) + } + + const err = new RequestRetryError('Request failed', statusCode, { + headers, + count: this.retryCount + }) + + this.abort(err) + + return false + } + + onData (chunk) { + this.start += chunk.length + + return this.handler.onData(chunk) + } + + onComplete (rawTrailers) { + this.retryCount = 0 + return this.handler.onComplete(rawTrailers) + } + + onError (err) { + if (this.aborted || isDisturbed(this.opts.body)) { + return this.handler.onError(err) + } + + this.retryOpts.retry( + err, + { + state: { counter: this.retryCount++, currentTimeout: this.retryAfter }, + opts: { retryOptions: this.retryOpts, ...this.opts } + }, + onRetry.bind(this) + ) + + function onRetry (err) { + if (err != null || this.aborted || isDisturbed(this.opts.body)) { + return this.handler.onError(err) + } + + if (this.start !== 0) { + this.opts = { + ...this.opts, + headers: { + ...this.opts.headers, + range: `bytes=${this.start}-${this.end ?? ''}` + } + } + } + + try { + this.dispatch(this.opts, this) + } catch (err) { + this.handler.onError(err) + } + } + } +} + +module.exports = RetryHandler + + +/***/ }), + +/***/ 4415: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const RedirectHandler = __nccwpck_require__(8299) + +function createRedirectInterceptor ({ maxRedirections: defaultMaxRedirections }) { + return (dispatch) => { + return function Intercept (opts, handler) { + const { maxRedirections = defaultMaxRedirections } = opts + + if (!maxRedirections) { + return dispatch(opts, handler) + } + + const redirectHandler = new RedirectHandler(dispatch, maxRedirections, opts, handler) + opts = { ...opts, maxRedirections: 0 } // Stop sub dispatcher from also redirecting. + return dispatch(opts, redirectHandler) + } + } +} + +module.exports = createRedirectInterceptor + + +/***/ }), + +/***/ 2824: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.SPECIAL_HEADERS = exports.HEADER_STATE = exports.MINOR = exports.MAJOR = exports.CONNECTION_TOKEN_CHARS = exports.HEADER_CHARS = exports.TOKEN = exports.STRICT_TOKEN = exports.HEX = exports.URL_CHAR = exports.STRICT_URL_CHAR = exports.USERINFO_CHARS = exports.MARK = exports.ALPHANUM = exports.NUM = exports.HEX_MAP = exports.NUM_MAP = exports.ALPHA = exports.FINISH = exports.H_METHOD_MAP = exports.METHOD_MAP = exports.METHODS_RTSP = exports.METHODS_ICE = exports.METHODS_HTTP = exports.METHODS = exports.LENIENT_FLAGS = exports.FLAGS = exports.TYPE = exports.ERROR = void 0; +const utils_1 = __nccwpck_require__(172); +// C headers +var ERROR; +(function (ERROR) { + ERROR[ERROR["OK"] = 0] = "OK"; + ERROR[ERROR["INTERNAL"] = 1] = "INTERNAL"; + ERROR[ERROR["STRICT"] = 2] = "STRICT"; + ERROR[ERROR["LF_EXPECTED"] = 3] = "LF_EXPECTED"; + ERROR[ERROR["UNEXPECTED_CONTENT_LENGTH"] = 4] = "UNEXPECTED_CONTENT_LENGTH"; + ERROR[ERROR["CLOSED_CONNECTION"] = 5] = "CLOSED_CONNECTION"; + ERROR[ERROR["INVALID_METHOD"] = 6] = "INVALID_METHOD"; + ERROR[ERROR["INVALID_URL"] = 7] = "INVALID_URL"; + ERROR[ERROR["INVALID_CONSTANT"] = 8] = "INVALID_CONSTANT"; + ERROR[ERROR["INVALID_VERSION"] = 9] = "INVALID_VERSION"; + ERROR[ERROR["INVALID_HEADER_TOKEN"] = 10] = "INVALID_HEADER_TOKEN"; + ERROR[ERROR["INVALID_CONTENT_LENGTH"] = 11] = "INVALID_CONTENT_LENGTH"; + ERROR[ERROR["INVALID_CHUNK_SIZE"] = 12] = "INVALID_CHUNK_SIZE"; + ERROR[ERROR["INVALID_STATUS"] = 13] = "INVALID_STATUS"; + ERROR[ERROR["INVALID_EOF_STATE"] = 14] = "INVALID_EOF_STATE"; + ERROR[ERROR["INVALID_TRANSFER_ENCODING"] = 15] = "INVALID_TRANSFER_ENCODING"; + ERROR[ERROR["CB_MESSAGE_BEGIN"] = 16] = "CB_MESSAGE_BEGIN"; + ERROR[ERROR["CB_HEADERS_COMPLETE"] = 17] = "CB_HEADERS_COMPLETE"; + ERROR[ERROR["CB_MESSAGE_COMPLETE"] = 18] = "CB_MESSAGE_COMPLETE"; + ERROR[ERROR["CB_CHUNK_HEADER"] = 19] = "CB_CHUNK_HEADER"; + ERROR[ERROR["CB_CHUNK_COMPLETE"] = 20] = "CB_CHUNK_COMPLETE"; + ERROR[ERROR["PAUSED"] = 21] = "PAUSED"; + ERROR[ERROR["PAUSED_UPGRADE"] = 22] = "PAUSED_UPGRADE"; + ERROR[ERROR["PAUSED_H2_UPGRADE"] = 23] = "PAUSED_H2_UPGRADE"; + ERROR[ERROR["USER"] = 24] = "USER"; +})(ERROR = exports.ERROR || (exports.ERROR = {})); +var TYPE; +(function (TYPE) { + TYPE[TYPE["BOTH"] = 0] = "BOTH"; + TYPE[TYPE["REQUEST"] = 1] = "REQUEST"; + TYPE[TYPE["RESPONSE"] = 2] = "RESPONSE"; +})(TYPE = exports.TYPE || (exports.TYPE = {})); +var FLAGS; +(function (FLAGS) { + FLAGS[FLAGS["CONNECTION_KEEP_ALIVE"] = 1] = "CONNECTION_KEEP_ALIVE"; + FLAGS[FLAGS["CONNECTION_CLOSE"] = 2] = "CONNECTION_CLOSE"; + FLAGS[FLAGS["CONNECTION_UPGRADE"] = 4] = "CONNECTION_UPGRADE"; + FLAGS[FLAGS["CHUNKED"] = 8] = "CHUNKED"; + FLAGS[FLAGS["UPGRADE"] = 16] = "UPGRADE"; + FLAGS[FLAGS["CONTENT_LENGTH"] = 32] = "CONTENT_LENGTH"; + FLAGS[FLAGS["SKIPBODY"] = 64] = "SKIPBODY"; + FLAGS[FLAGS["TRAILING"] = 128] = "TRAILING"; + // 1 << 8 is unused + FLAGS[FLAGS["TRANSFER_ENCODING"] = 512] = "TRANSFER_ENCODING"; +})(FLAGS = exports.FLAGS || (exports.FLAGS = {})); +var LENIENT_FLAGS; +(function (LENIENT_FLAGS) { + LENIENT_FLAGS[LENIENT_FLAGS["HEADERS"] = 1] = "HEADERS"; + LENIENT_FLAGS[LENIENT_FLAGS["CHUNKED_LENGTH"] = 2] = "CHUNKED_LENGTH"; + LENIENT_FLAGS[LENIENT_FLAGS["KEEP_ALIVE"] = 4] = "KEEP_ALIVE"; +})(LENIENT_FLAGS = exports.LENIENT_FLAGS || (exports.LENIENT_FLAGS = {})); +var METHODS; +(function (METHODS) { + METHODS[METHODS["DELETE"] = 0] = "DELETE"; + METHODS[METHODS["GET"] = 1] = "GET"; + METHODS[METHODS["HEAD"] = 2] = "HEAD"; + METHODS[METHODS["POST"] = 3] = "POST"; + METHODS[METHODS["PUT"] = 4] = "PUT"; + /* pathological */ + METHODS[METHODS["CONNECT"] = 5] = "CONNECT"; + METHODS[METHODS["OPTIONS"] = 6] = "OPTIONS"; + METHODS[METHODS["TRACE"] = 7] = "TRACE"; + /* WebDAV */ + METHODS[METHODS["COPY"] = 8] = "COPY"; + METHODS[METHODS["LOCK"] = 9] = "LOCK"; + METHODS[METHODS["MKCOL"] = 10] = "MKCOL"; + METHODS[METHODS["MOVE"] = 11] = "MOVE"; + METHODS[METHODS["PROPFIND"] = 12] = "PROPFIND"; + METHODS[METHODS["PROPPATCH"] = 13] = "PROPPATCH"; + METHODS[METHODS["SEARCH"] = 14] = "SEARCH"; + METHODS[METHODS["UNLOCK"] = 15] = "UNLOCK"; + METHODS[METHODS["BIND"] = 16] = "BIND"; + METHODS[METHODS["REBIND"] = 17] = "REBIND"; + METHODS[METHODS["UNBIND"] = 18] = "UNBIND"; + METHODS[METHODS["ACL"] = 19] = "ACL"; + /* subversion */ + METHODS[METHODS["REPORT"] = 20] = "REPORT"; + METHODS[METHODS["MKACTIVITY"] = 21] = "MKACTIVITY"; + METHODS[METHODS["CHECKOUT"] = 22] = "CHECKOUT"; + METHODS[METHODS["MERGE"] = 23] = "MERGE"; + /* upnp */ + METHODS[METHODS["M-SEARCH"] = 24] = "M-SEARCH"; + METHODS[METHODS["NOTIFY"] = 25] = "NOTIFY"; + METHODS[METHODS["SUBSCRIBE"] = 26] = "SUBSCRIBE"; + METHODS[METHODS["UNSUBSCRIBE"] = 27] = "UNSUBSCRIBE"; + /* RFC-5789 */ + METHODS[METHODS["PATCH"] = 28] = "PATCH"; + METHODS[METHODS["PURGE"] = 29] = "PURGE"; + /* CalDAV */ + METHODS[METHODS["MKCALENDAR"] = 30] = "MKCALENDAR"; + /* RFC-2068, section 19.6.1.2 */ + METHODS[METHODS["LINK"] = 31] = "LINK"; + METHODS[METHODS["UNLINK"] = 32] = "UNLINK"; + /* icecast */ + METHODS[METHODS["SOURCE"] = 33] = "SOURCE"; + /* RFC-7540, section 11.6 */ + METHODS[METHODS["PRI"] = 34] = "PRI"; + /* RFC-2326 RTSP */ + METHODS[METHODS["DESCRIBE"] = 35] = "DESCRIBE"; + METHODS[METHODS["ANNOUNCE"] = 36] = "ANNOUNCE"; + METHODS[METHODS["SETUP"] = 37] = "SETUP"; + METHODS[METHODS["PLAY"] = 38] = "PLAY"; + METHODS[METHODS["PAUSE"] = 39] = "PAUSE"; + METHODS[METHODS["TEARDOWN"] = 40] = "TEARDOWN"; + METHODS[METHODS["GET_PARAMETER"] = 41] = "GET_PARAMETER"; + METHODS[METHODS["SET_PARAMETER"] = 42] = "SET_PARAMETER"; + METHODS[METHODS["REDIRECT"] = 43] = "REDIRECT"; + METHODS[METHODS["RECORD"] = 44] = "RECORD"; + /* RAOP */ + METHODS[METHODS["FLUSH"] = 45] = "FLUSH"; +})(METHODS = exports.METHODS || (exports.METHODS = {})); +exports.METHODS_HTTP = [ + METHODS.DELETE, + METHODS.GET, + METHODS.HEAD, + METHODS.POST, + METHODS.PUT, + METHODS.CONNECT, + METHODS.OPTIONS, + METHODS.TRACE, + METHODS.COPY, + METHODS.LOCK, + METHODS.MKCOL, + METHODS.MOVE, + METHODS.PROPFIND, + METHODS.PROPPATCH, + METHODS.SEARCH, + METHODS.UNLOCK, + METHODS.BIND, + METHODS.REBIND, + METHODS.UNBIND, + METHODS.ACL, + METHODS.REPORT, + METHODS.MKACTIVITY, + METHODS.CHECKOUT, + METHODS.MERGE, + METHODS['M-SEARCH'], + METHODS.NOTIFY, + METHODS.SUBSCRIBE, + METHODS.UNSUBSCRIBE, + METHODS.PATCH, + METHODS.PURGE, + METHODS.MKCALENDAR, + METHODS.LINK, + METHODS.UNLINK, + METHODS.PRI, + // TODO(indutny): should we allow it with HTTP? + METHODS.SOURCE, +]; +exports.METHODS_ICE = [ + METHODS.SOURCE, +]; +exports.METHODS_RTSP = [ + METHODS.OPTIONS, + METHODS.DESCRIBE, + METHODS.ANNOUNCE, + METHODS.SETUP, + METHODS.PLAY, + METHODS.PAUSE, + METHODS.TEARDOWN, + METHODS.GET_PARAMETER, + METHODS.SET_PARAMETER, + METHODS.REDIRECT, + METHODS.RECORD, + METHODS.FLUSH, + // For AirPlay + METHODS.GET, + METHODS.POST, +]; +exports.METHOD_MAP = utils_1.enumToMap(METHODS); +exports.H_METHOD_MAP = {}; +Object.keys(exports.METHOD_MAP).forEach((key) => { + if (/^H/.test(key)) { + exports.H_METHOD_MAP[key] = exports.METHOD_MAP[key]; + } +}); +var FINISH; +(function (FINISH) { + FINISH[FINISH["SAFE"] = 0] = "SAFE"; + FINISH[FINISH["SAFE_WITH_CB"] = 1] = "SAFE_WITH_CB"; + FINISH[FINISH["UNSAFE"] = 2] = "UNSAFE"; +})(FINISH = exports.FINISH || (exports.FINISH = {})); +exports.ALPHA = []; +for (let i = 'A'.charCodeAt(0); i <= 'Z'.charCodeAt(0); i++) { + // Upper case + exports.ALPHA.push(String.fromCharCode(i)); + // Lower case + exports.ALPHA.push(String.fromCharCode(i + 0x20)); +} +exports.NUM_MAP = { + 0: 0, 1: 1, 2: 2, 3: 3, 4: 4, + 5: 5, 6: 6, 7: 7, 8: 8, 9: 9, +}; +exports.HEX_MAP = { + 0: 0, 1: 1, 2: 2, 3: 3, 4: 4, + 5: 5, 6: 6, 7: 7, 8: 8, 9: 9, + A: 0XA, B: 0XB, C: 0XC, D: 0XD, E: 0XE, F: 0XF, + a: 0xa, b: 0xb, c: 0xc, d: 0xd, e: 0xe, f: 0xf, +}; +exports.NUM = [ + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', +]; +exports.ALPHANUM = exports.ALPHA.concat(exports.NUM); +exports.MARK = ['-', '_', '.', '!', '~', '*', '\'', '(', ')']; +exports.USERINFO_CHARS = exports.ALPHANUM + .concat(exports.MARK) + .concat(['%', ';', ':', '&', '=', '+', '$', ',']); +// TODO(indutny): use RFC +exports.STRICT_URL_CHAR = [ + '!', '"', '$', '%', '&', '\'', + '(', ')', '*', '+', ',', '-', '.', '/', + ':', ';', '<', '=', '>', + '@', '[', '\\', ']', '^', '_', + '`', + '{', '|', '}', '~', +].concat(exports.ALPHANUM); +exports.URL_CHAR = exports.STRICT_URL_CHAR + .concat(['\t', '\f']); +// All characters with 0x80 bit set to 1 +for (let i = 0x80; i <= 0xff; i++) { + exports.URL_CHAR.push(i); +} +exports.HEX = exports.NUM.concat(['a', 'b', 'c', 'd', 'e', 'f', 'A', 'B', 'C', 'D', 'E', 'F']); +/* Tokens as defined by rfc 2616. Also lowercases them. + * token = 1* + * separators = "(" | ")" | "<" | ">" | "@" + * | "," | ";" | ":" | "\" | <"> + * | "/" | "[" | "]" | "?" | "=" + * | "{" | "}" | SP | HT + */ +exports.STRICT_TOKEN = [ + '!', '#', '$', '%', '&', '\'', + '*', '+', '-', '.', + '^', '_', '`', + '|', '~', +].concat(exports.ALPHANUM); +exports.TOKEN = exports.STRICT_TOKEN.concat([' ']); +/* + * Verify that a char is a valid visible (printable) US-ASCII + * character or %x80-FF + */ +exports.HEADER_CHARS = ['\t']; +for (let i = 32; i <= 255; i++) { + if (i !== 127) { + exports.HEADER_CHARS.push(i); + } +} +// ',' = \x44 +exports.CONNECTION_TOKEN_CHARS = exports.HEADER_CHARS.filter((c) => c !== 44); +exports.MAJOR = exports.NUM_MAP; +exports.MINOR = exports.MAJOR; +var HEADER_STATE; +(function (HEADER_STATE) { + HEADER_STATE[HEADER_STATE["GENERAL"] = 0] = "GENERAL"; + HEADER_STATE[HEADER_STATE["CONNECTION"] = 1] = "CONNECTION"; + HEADER_STATE[HEADER_STATE["CONTENT_LENGTH"] = 2] = "CONTENT_LENGTH"; + HEADER_STATE[HEADER_STATE["TRANSFER_ENCODING"] = 3] = "TRANSFER_ENCODING"; + HEADER_STATE[HEADER_STATE["UPGRADE"] = 4] = "UPGRADE"; + HEADER_STATE[HEADER_STATE["CONNECTION_KEEP_ALIVE"] = 5] = "CONNECTION_KEEP_ALIVE"; + HEADER_STATE[HEADER_STATE["CONNECTION_CLOSE"] = 6] = "CONNECTION_CLOSE"; + HEADER_STATE[HEADER_STATE["CONNECTION_UPGRADE"] = 7] = "CONNECTION_UPGRADE"; + HEADER_STATE[HEADER_STATE["TRANSFER_ENCODING_CHUNKED"] = 8] = "TRANSFER_ENCODING_CHUNKED"; +})(HEADER_STATE = exports.HEADER_STATE || (exports.HEADER_STATE = {})); +exports.SPECIAL_HEADERS = { + 'connection': HEADER_STATE.CONNECTION, + 'content-length': HEADER_STATE.CONTENT_LENGTH, + 'proxy-connection': HEADER_STATE.CONNECTION, + 'transfer-encoding': HEADER_STATE.TRANSFER_ENCODING, + 'upgrade': HEADER_STATE.UPGRADE, +}; +//# sourceMappingURL=constants.js.map + +/***/ }), + +/***/ 3870: +/***/ ((module) => { + +module.exports = 'AGFzbQEAAAABMAhgAX8Bf2ADf39/AX9gBH9/f38Bf2AAAGADf39/AGABfwBgAn9/AGAGf39/f39/AALLAQgDZW52GHdhc21fb25faGVhZGVyc19jb21wbGV0ZQACA2VudhV3YXNtX29uX21lc3NhZ2VfYmVnaW4AAANlbnYLd2FzbV9vbl91cmwAAQNlbnYOd2FzbV9vbl9zdGF0dXMAAQNlbnYUd2FzbV9vbl9oZWFkZXJfZmllbGQAAQNlbnYUd2FzbV9vbl9oZWFkZXJfdmFsdWUAAQNlbnYMd2FzbV9vbl9ib2R5AAEDZW52GHdhc21fb25fbWVzc2FnZV9jb21wbGV0ZQAAA0ZFAwMEAAAFAAAAAAAABQEFAAUFBQAABgAAAAAGBgYGAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQABAAABAQcAAAUFAwABBAUBcAESEgUDAQACBggBfwFBgNQECwfRBSIGbWVtb3J5AgALX2luaXRpYWxpemUACRlfX2luZGlyZWN0X2Z1bmN0aW9uX3RhYmxlAQALbGxodHRwX2luaXQAChhsbGh0dHBfc2hvdWxkX2tlZXBfYWxpdmUAQQxsbGh0dHBfYWxsb2MADAZtYWxsb2MARgtsbGh0dHBfZnJlZQANBGZyZWUASA9sbGh0dHBfZ2V0X3R5cGUADhVsbGh0dHBfZ2V0X2h0dHBfbWFqb3IADxVsbGh0dHBfZ2V0X2h0dHBfbWlub3IAEBFsbGh0dHBfZ2V0X21ldGhvZAARFmxsaHR0cF9nZXRfc3RhdHVzX2NvZGUAEhJsbGh0dHBfZ2V0X3VwZ3JhZGUAEwxsbGh0dHBfcmVzZXQAFA5sbGh0dHBfZXhlY3V0ZQAVFGxsaHR0cF9zZXR0aW5nc19pbml0ABYNbGxodHRwX2ZpbmlzaAAXDGxsaHR0cF9wYXVzZQAYDWxsaHR0cF9yZXN1bWUAGRtsbGh0dHBfcmVzdW1lX2FmdGVyX3VwZ3JhZGUAGhBsbGh0dHBfZ2V0X2Vycm5vABsXbGxodHRwX2dldF9lcnJvcl9yZWFzb24AHBdsbGh0dHBfc2V0X2Vycm9yX3JlYXNvbgAdFGxsaHR0cF9nZXRfZXJyb3JfcG9zAB4RbGxodHRwX2Vycm5vX25hbWUAHxJsbGh0dHBfbWV0aG9kX25hbWUAIBJsbGh0dHBfc3RhdHVzX25hbWUAIRpsbGh0dHBfc2V0X2xlbmllbnRfaGVhZGVycwAiIWxsaHR0cF9zZXRfbGVuaWVudF9jaHVua2VkX2xlbmd0aAAjHWxsaHR0cF9zZXRfbGVuaWVudF9rZWVwX2FsaXZlACQkbGxodHRwX3NldF9sZW5pZW50X3RyYW5zZmVyX2VuY29kaW5nACUYbGxodHRwX21lc3NhZ2VfbmVlZHNfZW9mAD8JFwEAQQELEQECAwQFCwYHNTk3MS8tJyspCsLgAkUCAAsIABCIgICAAAsZACAAEMKAgIAAGiAAIAI2AjggACABOgAoCxwAIAAgAC8BMiAALQAuIAAQwYCAgAAQgICAgAALKgEBf0HAABDGgICAACIBEMKAgIAAGiABQYCIgIAANgI4IAEgADoAKCABCwoAIAAQyICAgAALBwAgAC0AKAsHACAALQAqCwcAIAAtACsLBwAgAC0AKQsHACAALwEyCwcAIAAtAC4LRQEEfyAAKAIYIQEgAC0ALSECIAAtACghAyAAKAI4IQQgABDCgICAABogACAENgI4IAAgAzoAKCAAIAI6AC0gACABNgIYCxEAIAAgASABIAJqEMOAgIAACxAAIABBAEHcABDMgICAABoLZwEBf0EAIQECQCAAKAIMDQACQAJAAkACQCAALQAvDgMBAAMCCyAAKAI4IgFFDQAgASgCLCIBRQ0AIAAgARGAgICAAAAiAQ0DC0EADwsQyoCAgAAACyAAQcOWgIAANgIQQQ4hAQsgAQseAAJAIAAoAgwNACAAQdGbgIAANgIQIABBFTYCDAsLFgACQCAAKAIMQRVHDQAgAEEANgIMCwsWAAJAIAAoAgxBFkcNACAAQQA2AgwLCwcAIAAoAgwLBwAgACgCEAsJACAAIAE2AhALBwAgACgCFAsiAAJAIABBJEkNABDKgICAAAALIABBAnRBoLOAgABqKAIACyIAAkAgAEEuSQ0AEMqAgIAAAAsgAEECdEGwtICAAGooAgAL7gsBAX9B66iAgAAhAQJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAIABBnH9qDvQDY2IAAWFhYWFhYQIDBAVhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhBgcICQoLDA0OD2FhYWFhEGFhYWFhYWFhYWFhEWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYRITFBUWFxgZGhthYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhHB0eHyAhIiMkJSYnKCkqKywtLi8wMTIzNDU2YTc4OTphYWFhYWFhYTthYWE8YWFhYT0+P2FhYWFhYWFhQGFhQWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYUJDREVGR0hJSktMTU5PUFFSU2FhYWFhYWFhVFVWV1hZWlthXF1hYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFeYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhX2BhC0Hhp4CAAA8LQaShgIAADwtBy6yAgAAPC0H+sYCAAA8LQcCkgIAADwtBq6SAgAAPC0GNqICAAA8LQeKmgIAADwtBgLCAgAAPC0G5r4CAAA8LQdekgIAADwtB75+AgAAPC0Hhn4CAAA8LQfqfgIAADwtB8qCAgAAPC0Gor4CAAA8LQa6ygIAADwtBiLCAgAAPC0Hsp4CAAA8LQYKigIAADwtBjp2AgAAPC0HQroCAAA8LQcqjgIAADwtBxbKAgAAPC0HfnICAAA8LQdKcgIAADwtBxKCAgAAPC0HXoICAAA8LQaKfgIAADwtB7a6AgAAPC0GrsICAAA8LQdSlgIAADwtBzK6AgAAPC0H6roCAAA8LQfyrgIAADwtB0rCAgAAPC0HxnYCAAA8LQbuggIAADwtB96uAgAAPC0GQsYCAAA8LQdexgIAADwtBoq2AgAAPC0HUp4CAAA8LQeCrgIAADwtBn6yAgAAPC0HrsYCAAA8LQdWfgIAADwtByrGAgAAPC0HepYCAAA8LQdSegIAADwtB9JyAgAAPC0GnsoCAAA8LQbGdgIAADwtBoJ2AgAAPC0G5sYCAAA8LQbywgIAADwtBkqGAgAAPC0GzpoCAAA8LQemsgIAADwtBrJ6AgAAPC0HUq4CAAA8LQfemgIAADwtBgKaAgAAPC0GwoYCAAA8LQf6egIAADwtBjaOAgAAPC0GJrYCAAA8LQfeigIAADwtBoLGAgAAPC0Gun4CAAA8LQcalgIAADwtB6J6AgAAPC0GTooCAAA8LQcKvgIAADwtBw52AgAAPC0GLrICAAA8LQeGdgIAADwtBja+AgAAPC0HqoYCAAA8LQbStgIAADwtB0q+AgAAPC0HfsoCAAA8LQdKygIAADwtB8LCAgAAPC0GpooCAAA8LQfmjgIAADwtBmZ6AgAAPC0G1rICAAA8LQZuwgIAADwtBkrKAgAAPC0G2q4CAAA8LQcKigIAADwtB+LKAgAAPC0GepYCAAA8LQdCigIAADwtBup6AgAAPC0GBnoCAAA8LEMqAgIAAAAtB1qGAgAAhAQsgAQsWACAAIAAtAC1B/gFxIAFBAEdyOgAtCxkAIAAgAC0ALUH9AXEgAUEAR0EBdHI6AC0LGQAgACAALQAtQfsBcSABQQBHQQJ0cjoALQsZACAAIAAtAC1B9wFxIAFBAEdBA3RyOgAtCy4BAn9BACEDAkAgACgCOCIERQ0AIAQoAgAiBEUNACAAIAQRgICAgAAAIQMLIAMLSQECf0EAIQMCQCAAKAI4IgRFDQAgBCgCBCIERQ0AIAAgASACIAFrIAQRgYCAgAAAIgNBf0cNACAAQcaRgIAANgIQQRghAwsgAwsuAQJ/QQAhAwJAIAAoAjgiBEUNACAEKAIwIgRFDQAgACAEEYCAgIAAACEDCyADC0kBAn9BACEDAkAgACgCOCIERQ0AIAQoAggiBEUNACAAIAEgAiABayAEEYGAgIAAACIDQX9HDQAgAEH2ioCAADYCEEEYIQMLIAMLLgECf0EAIQMCQCAAKAI4IgRFDQAgBCgCNCIERQ0AIAAgBBGAgICAAAAhAwsgAwtJAQJ/QQAhAwJAIAAoAjgiBEUNACAEKAIMIgRFDQAgACABIAIgAWsgBBGBgICAAAAiA0F/Rw0AIABB7ZqAgAA2AhBBGCEDCyADCy4BAn9BACEDAkAgACgCOCIERQ0AIAQoAjgiBEUNACAAIAQRgICAgAAAIQMLIAMLSQECf0EAIQMCQCAAKAI4IgRFDQAgBCgCECIERQ0AIAAgASACIAFrIAQRgYCAgAAAIgNBf0cNACAAQZWQgIAANgIQQRghAwsgAwsuAQJ/QQAhAwJAIAAoAjgiBEUNACAEKAI8IgRFDQAgACAEEYCAgIAAACEDCyADC0kBAn9BACEDAkAgACgCOCIERQ0AIAQoAhQiBEUNACAAIAEgAiABayAEEYGAgIAAACIDQX9HDQAgAEGqm4CAADYCEEEYIQMLIAMLLgECf0EAIQMCQCAAKAI4IgRFDQAgBCgCQCIERQ0AIAAgBBGAgICAAAAhAwsgAwtJAQJ/QQAhAwJAIAAoAjgiBEUNACAEKAIYIgRFDQAgACABIAIgAWsgBBGBgICAAAAiA0F/Rw0AIABB7ZOAgAA2AhBBGCEDCyADCy4BAn9BACEDAkAgACgCOCIERQ0AIAQoAkQiBEUNACAAIAQRgICAgAAAIQMLIAMLLgECf0EAIQMCQCAAKAI4IgRFDQAgBCgCJCIERQ0AIAAgBBGAgICAAAAhAwsgAwsuAQJ/QQAhAwJAIAAoAjgiBEUNACAEKAIsIgRFDQAgACAEEYCAgIAAACEDCyADC0kBAn9BACEDAkAgACgCOCIERQ0AIAQoAigiBEUNACAAIAEgAiABayAEEYGAgIAAACIDQX9HDQAgAEH2iICAADYCEEEYIQMLIAMLLgECf0EAIQMCQCAAKAI4IgRFDQAgBCgCUCIERQ0AIAAgBBGAgICAAAAhAwsgAwtJAQJ/QQAhAwJAIAAoAjgiBEUNACAEKAIcIgRFDQAgACABIAIgAWsgBBGBgICAAAAiA0F/Rw0AIABBwpmAgAA2AhBBGCEDCyADCy4BAn9BACEDAkAgACgCOCIERQ0AIAQoAkgiBEUNACAAIAQRgICAgAAAIQMLIAMLSQECf0EAIQMCQCAAKAI4IgRFDQAgBCgCICIERQ0AIAAgASACIAFrIAQRgYCAgAAAIgNBf0cNACAAQZSUgIAANgIQQRghAwsgAwsuAQJ/QQAhAwJAIAAoAjgiBEUNACAEKAJMIgRFDQAgACAEEYCAgIAAACEDCyADCy4BAn9BACEDAkAgACgCOCIERQ0AIAQoAlQiBEUNACAAIAQRgICAgAAAIQMLIAMLLgECf0EAIQMCQCAAKAI4IgRFDQAgBCgCWCIERQ0AIAAgBBGAgICAAAAhAwsgAwtFAQF/AkACQCAALwEwQRRxQRRHDQBBASEDIAAtAChBAUYNASAALwEyQeUARiEDDAELIAAtAClBBUYhAwsgACADOgAuQQAL/gEBA39BASEDAkAgAC8BMCIEQQhxDQAgACkDIEIAUiEDCwJAAkAgAC0ALkUNAEEBIQUgAC0AKUEFRg0BQQEhBSAEQcAAcUUgA3FBAUcNAQtBACEFIARBwABxDQBBAiEFIARB//8DcSIDQQhxDQACQCADQYAEcUUNAAJAIAAtAChBAUcNACAALQAtQQpxDQBBBQ8LQQQPCwJAIANBIHENAAJAIAAtAChBAUYNACAALwEyQf//A3EiAEGcf2pB5ABJDQAgAEHMAUYNACAAQbACRg0AQQQhBSAEQShxRQ0CIANBiARxQYAERg0CC0EADwtBAEEDIAApAyBQGyEFCyAFC2IBAn9BACEBAkAgAC0AKEEBRg0AIAAvATJB//8DcSICQZx/akHkAEkNACACQcwBRg0AIAJBsAJGDQAgAC8BMCIAQcAAcQ0AQQEhASAAQYgEcUGABEYNACAAQShxRSEBCyABC6cBAQN/AkACQAJAIAAtACpFDQAgAC0AK0UNAEEAIQMgAC8BMCIEQQJxRQ0BDAILQQAhAyAALwEwIgRBAXFFDQELQQEhAyAALQAoQQFGDQAgAC8BMkH//wNxIgVBnH9qQeQASQ0AIAVBzAFGDQAgBUGwAkYNACAEQcAAcQ0AQQAhAyAEQYgEcUGABEYNACAEQShxQQBHIQMLIABBADsBMCAAQQA6AC8gAwuZAQECfwJAAkACQCAALQAqRQ0AIAAtACtFDQBBACEBIAAvATAiAkECcUUNAQwCC0EAIQEgAC8BMCICQQFxRQ0BC0EBIQEgAC0AKEEBRg0AIAAvATJB//8DcSIAQZx/akHkAEkNACAAQcwBRg0AIABBsAJGDQAgAkHAAHENAEEAIQEgAkGIBHFBgARGDQAgAkEocUEARyEBCyABC1kAIABBGGpCADcDACAAQgA3AwAgAEE4akIANwMAIABBMGpCADcDACAAQShqQgA3AwAgAEEgakIANwMAIABBEGpCADcDACAAQQhqQgA3AwAgAEHdATYCHEEAC3sBAX8CQCAAKAIMIgMNAAJAIAAoAgRFDQAgACABNgIECwJAIAAgASACEMSAgIAAIgMNACAAKAIMDwsgACADNgIcQQAhAyAAKAIEIgFFDQAgACABIAIgACgCCBGBgICAAAAiAUUNACAAIAI2AhQgACABNgIMIAEhAwsgAwvk8wEDDn8DfgR/I4CAgIAAQRBrIgMkgICAgAAgASEEIAEhBSABIQYgASEHIAEhCCABIQkgASEKIAEhCyABIQwgASENIAEhDiABIQ8CQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkAgACgCHCIQQX9qDt0B2gEB2QECAwQFBgcICQoLDA0O2AEPENcBERLWARMUFRYXGBkaG+AB3wEcHR7VAR8gISIjJCXUASYnKCkqKyzTAdIBLS7RAdABLzAxMjM0NTY3ODk6Ozw9Pj9AQUJDREVG2wFHSElKzwHOAUvNAUzMAU1OT1BRUlNUVVZXWFlaW1xdXl9gYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXp7fH1+f4ABgQGCAYMBhAGFAYYBhwGIAYkBigGLAYwBjQGOAY8BkAGRAZIBkwGUAZUBlgGXAZgBmQGaAZsBnAGdAZ4BnwGgAaEBogGjAaQBpQGmAacBqAGpAaoBqwGsAa0BrgGvAbABsQGyAbMBtAG1AbYBtwHLAcoBuAHJAbkByAG6AbsBvAG9Ab4BvwHAAcEBwgHDAcQBxQHGAQDcAQtBACEQDMYBC0EOIRAMxQELQQ0hEAzEAQtBDyEQDMMBC0EQIRAMwgELQRMhEAzBAQtBFCEQDMABC0EVIRAMvwELQRYhEAy+AQtBFyEQDL0BC0EYIRAMvAELQRkhEAy7AQtBGiEQDLoBC0EbIRAMuQELQRwhEAy4AQtBCCEQDLcBC0EdIRAMtgELQSAhEAy1AQtBHyEQDLQBC0EHIRAMswELQSEhEAyyAQtBIiEQDLEBC0EeIRAMsAELQSMhEAyvAQtBEiEQDK4BC0ERIRAMrQELQSQhEAysAQtBJSEQDKsBC0EmIRAMqgELQSchEAypAQtBwwEhEAyoAQtBKSEQDKcBC0ErIRAMpgELQSwhEAylAQtBLSEQDKQBC0EuIRAMowELQS8hEAyiAQtBxAEhEAyhAQtBMCEQDKABC0E0IRAMnwELQQwhEAyeAQtBMSEQDJ0BC0EyIRAMnAELQTMhEAybAQtBOSEQDJoBC0E1IRAMmQELQcUBIRAMmAELQQshEAyXAQtBOiEQDJYBC0E2IRAMlQELQQohEAyUAQtBNyEQDJMBC0E4IRAMkgELQTwhEAyRAQtBOyEQDJABC0E9IRAMjwELQQkhEAyOAQtBKCEQDI0BC0E+IRAMjAELQT8hEAyLAQtBwAAhEAyKAQtBwQAhEAyJAQtBwgAhEAyIAQtBwwAhEAyHAQtBxAAhEAyGAQtBxQAhEAyFAQtBxgAhEAyEAQtBKiEQDIMBC0HHACEQDIIBC0HIACEQDIEBC0HJACEQDIABC0HKACEQDH8LQcsAIRAMfgtBzQAhEAx9C0HMACEQDHwLQc4AIRAMewtBzwAhEAx6C0HQACEQDHkLQdEAIRAMeAtB0gAhEAx3C0HTACEQDHYLQdQAIRAMdQtB1gAhEAx0C0HVACEQDHMLQQYhEAxyC0HXACEQDHELQQUhEAxwC0HYACEQDG8LQQQhEAxuC0HZACEQDG0LQdoAIRAMbAtB2wAhEAxrC0HcACEQDGoLQQMhEAxpC0HdACEQDGgLQd4AIRAMZwtB3wAhEAxmC0HhACEQDGULQeAAIRAMZAtB4gAhEAxjC0HjACEQDGILQQIhEAxhC0HkACEQDGALQeUAIRAMXwtB5gAhEAxeC0HnACEQDF0LQegAIRAMXAtB6QAhEAxbC0HqACEQDFoLQesAIRAMWQtB7AAhEAxYC0HtACEQDFcLQe4AIRAMVgtB7wAhEAxVC0HwACEQDFQLQfEAIRAMUwtB8gAhEAxSC0HzACEQDFELQfQAIRAMUAtB9QAhEAxPC0H2ACEQDE4LQfcAIRAMTQtB+AAhEAxMC0H5ACEQDEsLQfoAIRAMSgtB+wAhEAxJC0H8ACEQDEgLQf0AIRAMRwtB/gAhEAxGC0H/ACEQDEULQYABIRAMRAtBgQEhEAxDC0GCASEQDEILQYMBIRAMQQtBhAEhEAxAC0GFASEQDD8LQYYBIRAMPgtBhwEhEAw9C0GIASEQDDwLQYkBIRAMOwtBigEhEAw6C0GLASEQDDkLQYwBIRAMOAtBjQEhEAw3C0GOASEQDDYLQY8BIRAMNQtBkAEhEAw0C0GRASEQDDMLQZIBIRAMMgtBkwEhEAwxC0GUASEQDDALQZUBIRAMLwtBlgEhEAwuC0GXASEQDC0LQZgBIRAMLAtBmQEhEAwrC0GaASEQDCoLQZsBIRAMKQtBnAEhEAwoC0GdASEQDCcLQZ4BIRAMJgtBnwEhEAwlC0GgASEQDCQLQaEBIRAMIwtBogEhEAwiC0GjASEQDCELQaQBIRAMIAtBpQEhEAwfC0GmASEQDB4LQacBIRAMHQtBqAEhEAwcC0GpASEQDBsLQaoBIRAMGgtBqwEhEAwZC0GsASEQDBgLQa0BIRAMFwtBrgEhEAwWC0EBIRAMFQtBrwEhEAwUC0GwASEQDBMLQbEBIRAMEgtBswEhEAwRC0GyASEQDBALQbQBIRAMDwtBtQEhEAwOC0G2ASEQDA0LQbcBIRAMDAtBuAEhEAwLC0G5ASEQDAoLQboBIRAMCQtBuwEhEAwIC0HGASEQDAcLQbwBIRAMBgtBvQEhEAwFC0G+ASEQDAQLQb8BIRAMAwtBwAEhEAwCC0HCASEQDAELQcEBIRALA0ACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQCAQDscBAAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxweHyAhIyUoP0BBREVGR0hJSktMTU9QUVJT3gNXWVtcXWBiZWZnaGlqa2xtb3BxcnN0dXZ3eHl6e3x9foABggGFAYYBhwGJAYsBjAGNAY4BjwGQAZEBlAGVAZYBlwGYAZkBmgGbAZwBnQGeAZ8BoAGhAaIBowGkAaUBpgGnAagBqQGqAasBrAGtAa4BrwGwAbEBsgGzAbQBtQG2AbcBuAG5AboBuwG8Ab0BvgG/AcABwQHCAcMBxAHFAcYBxwHIAckBygHLAcwBzQHOAc8B0AHRAdIB0wHUAdUB1gHXAdgB2QHaAdsB3AHdAd4B4AHhAeIB4wHkAeUB5gHnAegB6QHqAesB7AHtAe4B7wHwAfEB8gHzAZkCpAKwAv4C/gILIAEiBCACRw3zAUHdASEQDP8DCyABIhAgAkcN3QFBwwEhEAz+AwsgASIBIAJHDZABQfcAIRAM/QMLIAEiASACRw2GAUHvACEQDPwDCyABIgEgAkcNf0HqACEQDPsDCyABIgEgAkcNe0HoACEQDPoDCyABIgEgAkcNeEHmACEQDPkDCyABIgEgAkcNGkEYIRAM+AMLIAEiASACRw0UQRIhEAz3AwsgASIBIAJHDVlBxQAhEAz2AwsgASIBIAJHDUpBPyEQDPUDCyABIgEgAkcNSEE8IRAM9AMLIAEiASACRw1BQTEhEAzzAwsgAC0ALkEBRg3rAwyHAgsgACABIgEgAhDAgICAAEEBRw3mASAAQgA3AyAM5wELIAAgASIBIAIQtICAgAAiEA3nASABIQEM9QILAkAgASIBIAJHDQBBBiEQDPADCyAAIAFBAWoiASACELuAgIAAIhAN6AEgASEBDDELIABCADcDIEESIRAM1QMLIAEiECACRw0rQR0hEAztAwsCQCABIgEgAkYNACABQQFqIQFBECEQDNQDC0EHIRAM7AMLIABCACAAKQMgIhEgAiABIhBrrSISfSITIBMgEVYbNwMgIBEgElYiFEUN5QFBCCEQDOsDCwJAIAEiASACRg0AIABBiYCAgAA2AgggACABNgIEIAEhAUEUIRAM0gMLQQkhEAzqAwsgASEBIAApAyBQDeQBIAEhAQzyAgsCQCABIgEgAkcNAEELIRAM6QMLIAAgAUEBaiIBIAIQtoCAgAAiEA3lASABIQEM8gILIAAgASIBIAIQuICAgAAiEA3lASABIQEM8gILIAAgASIBIAIQuICAgAAiEA3mASABIQEMDQsgACABIgEgAhC6gICAACIQDecBIAEhAQzwAgsCQCABIgEgAkcNAEEPIRAM5QMLIAEtAAAiEEE7Rg0IIBBBDUcN6AEgAUEBaiEBDO8CCyAAIAEiASACELqAgIAAIhAN6AEgASEBDPICCwNAAkAgAS0AAEHwtYCAAGotAAAiEEEBRg0AIBBBAkcN6wEgACgCBCEQIABBADYCBCAAIBAgAUEBaiIBELmAgIAAIhAN6gEgASEBDPQCCyABQQFqIgEgAkcNAAtBEiEQDOIDCyAAIAEiASACELqAgIAAIhAN6QEgASEBDAoLIAEiASACRw0GQRshEAzgAwsCQCABIgEgAkcNAEEWIRAM4AMLIABBioCAgAA2AgggACABNgIEIAAgASACELiAgIAAIhAN6gEgASEBQSAhEAzGAwsCQCABIgEgAkYNAANAAkAgAS0AAEHwt4CAAGotAAAiEEECRg0AAkAgEEF/ag4E5QHsAQDrAewBCyABQQFqIQFBCCEQDMgDCyABQQFqIgEgAkcNAAtBFSEQDN8DC0EVIRAM3gMLA0ACQCABLQAAQfC5gIAAai0AACIQQQJGDQAgEEF/ag4E3gHsAeAB6wHsAQsgAUEBaiIBIAJHDQALQRghEAzdAwsCQCABIgEgAkYNACAAQYuAgIAANgIIIAAgATYCBCABIQFBByEQDMQDC0EZIRAM3AMLIAFBAWohAQwCCwJAIAEiFCACRw0AQRohEAzbAwsgFCEBAkAgFC0AAEFzag4U3QLuAu4C7gLuAu4C7gLuAu4C7gLuAu4C7gLuAu4C7gLuAu4C7gIA7gILQQAhECAAQQA2AhwgAEGvi4CAADYCECAAQQI2AgwgACAUQQFqNgIUDNoDCwJAIAEtAAAiEEE7Rg0AIBBBDUcN6AEgAUEBaiEBDOUCCyABQQFqIQELQSIhEAy/AwsCQCABIhAgAkcNAEEcIRAM2AMLQgAhESAQIQEgEC0AAEFQag435wHmAQECAwQFBgcIAAAAAAAAAAkKCwwNDgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADxAREhMUAAtBHiEQDL0DC0ICIREM5QELQgMhEQzkAQtCBCERDOMBC0IFIREM4gELQgYhEQzhAQtCByERDOABC0IIIREM3wELQgkhEQzeAQtCCiERDN0BC0ILIREM3AELQgwhEQzbAQtCDSERDNoBC0IOIREM2QELQg8hEQzYAQtCCiERDNcBC0ILIREM1gELQgwhEQzVAQtCDSERDNQBC0IOIREM0wELQg8hEQzSAQtCACERAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQCAQLQAAQVBqDjflAeQBAAECAwQFBgfmAeYB5gHmAeYB5gHmAQgJCgsMDeYB5gHmAeYB5gHmAeYB5gHmAeYB5gHmAeYB5gHmAeYB5gHmAeYB5gHmAeYB5gHmAeYB5gEODxAREhPmAQtCAiERDOQBC0IDIREM4wELQgQhEQziAQtCBSERDOEBC0IGIREM4AELQgchEQzfAQtCCCERDN4BC0IJIREM3QELQgohEQzcAQtCCyERDNsBC0IMIREM2gELQg0hEQzZAQtCDiERDNgBC0IPIREM1wELQgohEQzWAQtCCyERDNUBC0IMIREM1AELQg0hEQzTAQtCDiERDNIBC0IPIREM0QELIABCACAAKQMgIhEgAiABIhBrrSISfSITIBMgEVYbNwMgIBEgElYiFEUN0gFBHyEQDMADCwJAIAEiASACRg0AIABBiYCAgAA2AgggACABNgIEIAEhAUEkIRAMpwMLQSAhEAy/AwsgACABIhAgAhC+gICAAEF/ag4FtgEAxQIB0QHSAQtBESEQDKQDCyAAQQE6AC8gECEBDLsDCyABIgEgAkcN0gFBJCEQDLsDCyABIg0gAkcNHkHGACEQDLoDCyAAIAEiASACELKAgIAAIhAN1AEgASEBDLUBCyABIhAgAkcNJkHQACEQDLgDCwJAIAEiASACRw0AQSghEAy4AwsgAEEANgIEIABBjICAgAA2AgggACABIAEQsYCAgAAiEA3TASABIQEM2AELAkAgASIQIAJHDQBBKSEQDLcDCyAQLQAAIgFBIEYNFCABQQlHDdMBIBBBAWohAQwVCwJAIAEiASACRg0AIAFBAWohAQwXC0EqIRAMtQMLAkAgASIQIAJHDQBBKyEQDLUDCwJAIBAtAAAiAUEJRg0AIAFBIEcN1QELIAAtACxBCEYN0wEgECEBDJEDCwJAIAEiASACRw0AQSwhEAy0AwsgAS0AAEEKRw3VASABQQFqIQEMyQILIAEiDiACRw3VAUEvIRAMsgMLA0ACQCABLQAAIhBBIEYNAAJAIBBBdmoOBADcAdwBANoBCyABIQEM4AELIAFBAWoiASACRw0AC0ExIRAMsQMLQTIhECABIhQgAkYNsAMgAiAUayAAKAIAIgFqIRUgFCABa0EDaiEWAkADQCAULQAAIhdBIHIgFyAXQb9/akH/AXFBGkkbQf8BcSABQfC7gIAAai0AAEcNAQJAIAFBA0cNAEEGIQEMlgMLIAFBAWohASAUQQFqIhQgAkcNAAsgACAVNgIADLEDCyAAQQA2AgAgFCEBDNkBC0EzIRAgASIUIAJGDa8DIAIgFGsgACgCACIBaiEVIBQgAWtBCGohFgJAA0AgFC0AACIXQSByIBcgF0G/f2pB/wFxQRpJG0H/AXEgAUH0u4CAAGotAABHDQECQCABQQhHDQBBBSEBDJUDCyABQQFqIQEgFEEBaiIUIAJHDQALIAAgFTYCAAywAwsgAEEANgIAIBQhAQzYAQtBNCEQIAEiFCACRg2uAyACIBRrIAAoAgAiAWohFSAUIAFrQQVqIRYCQANAIBQtAAAiF0EgciAXIBdBv39qQf8BcUEaSRtB/wFxIAFB0MKAgABqLQAARw0BAkAgAUEFRw0AQQchAQyUAwsgAUEBaiEBIBRBAWoiFCACRw0ACyAAIBU2AgAMrwMLIABBADYCACAUIQEM1wELAkAgASIBIAJGDQADQAJAIAEtAABBgL6AgABqLQAAIhBBAUYNACAQQQJGDQogASEBDN0BCyABQQFqIgEgAkcNAAtBMCEQDK4DC0EwIRAMrQMLAkAgASIBIAJGDQADQAJAIAEtAAAiEEEgRg0AIBBBdmoOBNkB2gHaAdkB2gELIAFBAWoiASACRw0AC0E4IRAMrQMLQTghEAysAwsDQAJAIAEtAAAiEEEgRg0AIBBBCUcNAwsgAUEBaiIBIAJHDQALQTwhEAyrAwsDQAJAIAEtAAAiEEEgRg0AAkACQCAQQXZqDgTaAQEB2gEACyAQQSxGDdsBCyABIQEMBAsgAUEBaiIBIAJHDQALQT8hEAyqAwsgASEBDNsBC0HAACEQIAEiFCACRg2oAyACIBRrIAAoAgAiAWohFiAUIAFrQQZqIRcCQANAIBQtAABBIHIgAUGAwICAAGotAABHDQEgAUEGRg2OAyABQQFqIQEgFEEBaiIUIAJHDQALIAAgFjYCAAypAwsgAEEANgIAIBQhAQtBNiEQDI4DCwJAIAEiDyACRw0AQcEAIRAMpwMLIABBjICAgAA2AgggACAPNgIEIA8hASAALQAsQX9qDgTNAdUB1wHZAYcDCyABQQFqIQEMzAELAkAgASIBIAJGDQADQAJAIAEtAAAiEEEgciAQIBBBv39qQf8BcUEaSRtB/wFxIhBBCUYNACAQQSBGDQACQAJAAkACQCAQQZ1/ag4TAAMDAwMDAwMBAwMDAwMDAwMDAgMLIAFBAWohAUExIRAMkQMLIAFBAWohAUEyIRAMkAMLIAFBAWohAUEzIRAMjwMLIAEhAQzQAQsgAUEBaiIBIAJHDQALQTUhEAylAwtBNSEQDKQDCwJAIAEiASACRg0AA0ACQCABLQAAQYC8gIAAai0AAEEBRg0AIAEhAQzTAQsgAUEBaiIBIAJHDQALQT0hEAykAwtBPSEQDKMDCyAAIAEiASACELCAgIAAIhAN1gEgASEBDAELIBBBAWohAQtBPCEQDIcDCwJAIAEiASACRw0AQcIAIRAMoAMLAkADQAJAIAEtAABBd2oOGAAC/gL+AoQD/gL+Av4C/gL+Av4C/gL+Av4C/gL+Av4C/gL+Av4C/gL+Av4CAP4CCyABQQFqIgEgAkcNAAtBwgAhEAygAwsgAUEBaiEBIAAtAC1BAXFFDb0BIAEhAQtBLCEQDIUDCyABIgEgAkcN0wFBxAAhEAydAwsDQAJAIAEtAABBkMCAgABqLQAAQQFGDQAgASEBDLcCCyABQQFqIgEgAkcNAAtBxQAhEAycAwsgDS0AACIQQSBGDbMBIBBBOkcNgQMgACgCBCEBIABBADYCBCAAIAEgDRCvgICAACIBDdABIA1BAWohAQyzAgtBxwAhECABIg0gAkYNmgMgAiANayAAKAIAIgFqIRYgDSABa0EFaiEXA0AgDS0AACIUQSByIBQgFEG/f2pB/wFxQRpJG0H/AXEgAUGQwoCAAGotAABHDYADIAFBBUYN9AIgAUEBaiEBIA1BAWoiDSACRw0ACyAAIBY2AgAMmgMLQcgAIRAgASINIAJGDZkDIAIgDWsgACgCACIBaiEWIA0gAWtBCWohFwNAIA0tAAAiFEEgciAUIBRBv39qQf8BcUEaSRtB/wFxIAFBlsKAgABqLQAARw3/AgJAIAFBCUcNAEECIQEM9QILIAFBAWohASANQQFqIg0gAkcNAAsgACAWNgIADJkDCwJAIAEiDSACRw0AQckAIRAMmQMLAkACQCANLQAAIgFBIHIgASABQb9/akH/AXFBGkkbQf8BcUGSf2oOBwCAA4ADgAOAA4ADAYADCyANQQFqIQFBPiEQDIADCyANQQFqIQFBPyEQDP8CC0HKACEQIAEiDSACRg2XAyACIA1rIAAoAgAiAWohFiANIAFrQQFqIRcDQCANLQAAIhRBIHIgFCAUQb9/akH/AXFBGkkbQf8BcSABQaDCgIAAai0AAEcN/QIgAUEBRg3wAiABQQFqIQEgDUEBaiINIAJHDQALIAAgFjYCAAyXAwtBywAhECABIg0gAkYNlgMgAiANayAAKAIAIgFqIRYgDSABa0EOaiEXA0AgDS0AACIUQSByIBQgFEG/f2pB/wFxQRpJG0H/AXEgAUGiwoCAAGotAABHDfwCIAFBDkYN8AIgAUEBaiEBIA1BAWoiDSACRw0ACyAAIBY2AgAMlgMLQcwAIRAgASINIAJGDZUDIAIgDWsgACgCACIBaiEWIA0gAWtBD2ohFwNAIA0tAAAiFEEgciAUIBRBv39qQf8BcUEaSRtB/wFxIAFBwMKAgABqLQAARw37AgJAIAFBD0cNAEEDIQEM8QILIAFBAWohASANQQFqIg0gAkcNAAsgACAWNgIADJUDC0HNACEQIAEiDSACRg2UAyACIA1rIAAoAgAiAWohFiANIAFrQQVqIRcDQCANLQAAIhRBIHIgFCAUQb9/akH/AXFBGkkbQf8BcSABQdDCgIAAai0AAEcN+gICQCABQQVHDQBBBCEBDPACCyABQQFqIQEgDUEBaiINIAJHDQALIAAgFjYCAAyUAwsCQCABIg0gAkcNAEHOACEQDJQDCwJAAkACQAJAIA0tAAAiAUEgciABIAFBv39qQf8BcUEaSRtB/wFxQZ1/ag4TAP0C/QL9Av0C/QL9Av0C/QL9Av0C/QL9AgH9Av0C/QICA/0CCyANQQFqIQFBwQAhEAz9AgsgDUEBaiEBQcIAIRAM/AILIA1BAWohAUHDACEQDPsCCyANQQFqIQFBxAAhEAz6AgsCQCABIgEgAkYNACAAQY2AgIAANgIIIAAgATYCBCABIQFBxQAhEAz6AgtBzwAhEAySAwsgECEBAkACQCAQLQAAQXZqDgQBqAKoAgCoAgsgEEEBaiEBC0EnIRAM+AILAkAgASIBIAJHDQBB0QAhEAyRAwsCQCABLQAAQSBGDQAgASEBDI0BCyABQQFqIQEgAC0ALUEBcUUNxwEgASEBDIwBCyABIhcgAkcNyAFB0gAhEAyPAwtB0wAhECABIhQgAkYNjgMgAiAUayAAKAIAIgFqIRYgFCABa0EBaiEXA0AgFC0AACABQdbCgIAAai0AAEcNzAEgAUEBRg3HASABQQFqIQEgFEEBaiIUIAJHDQALIAAgFjYCAAyOAwsCQCABIgEgAkcNAEHVACEQDI4DCyABLQAAQQpHDcwBIAFBAWohAQzHAQsCQCABIgEgAkcNAEHWACEQDI0DCwJAAkAgAS0AAEF2ag4EAM0BzQEBzQELIAFBAWohAQzHAQsgAUEBaiEBQcoAIRAM8wILIAAgASIBIAIQroCAgAAiEA3LASABIQFBzQAhEAzyAgsgAC0AKUEiRg2FAwymAgsCQCABIgEgAkcNAEHbACEQDIoDC0EAIRRBASEXQQEhFkEAIRACQAJAAkACQAJAAkACQAJAAkAgAS0AAEFQag4K1AHTAQABAgMEBQYI1QELQQIhEAwGC0EDIRAMBQtBBCEQDAQLQQUhEAwDC0EGIRAMAgtBByEQDAELQQghEAtBACEXQQAhFkEAIRQMzAELQQkhEEEBIRRBACEXQQAhFgzLAQsCQCABIgEgAkcNAEHdACEQDIkDCyABLQAAQS5HDcwBIAFBAWohAQymAgsgASIBIAJHDcwBQd8AIRAMhwMLAkAgASIBIAJGDQAgAEGOgICAADYCCCAAIAE2AgQgASEBQdAAIRAM7gILQeAAIRAMhgMLQeEAIRAgASIBIAJGDYUDIAIgAWsgACgCACIUaiEWIAEgFGtBA2ohFwNAIAEtAAAgFEHiwoCAAGotAABHDc0BIBRBA0YNzAEgFEEBaiEUIAFBAWoiASACRw0ACyAAIBY2AgAMhQMLQeIAIRAgASIBIAJGDYQDIAIgAWsgACgCACIUaiEWIAEgFGtBAmohFwNAIAEtAAAgFEHmwoCAAGotAABHDcwBIBRBAkYNzgEgFEEBaiEUIAFBAWoiASACRw0ACyAAIBY2AgAMhAMLQeMAIRAgASIBIAJGDYMDIAIgAWsgACgCACIUaiEWIAEgFGtBA2ohFwNAIAEtAAAgFEHpwoCAAGotAABHDcsBIBRBA0YNzgEgFEEBaiEUIAFBAWoiASACRw0ACyAAIBY2AgAMgwMLAkAgASIBIAJHDQBB5QAhEAyDAwsgACABQQFqIgEgAhCogICAACIQDc0BIAEhAUHWACEQDOkCCwJAIAEiASACRg0AA0ACQCABLQAAIhBBIEYNAAJAAkACQCAQQbh/ag4LAAHPAc8BzwHPAc8BzwHPAc8BAs8BCyABQQFqIQFB0gAhEAztAgsgAUEBaiEBQdMAIRAM7AILIAFBAWohAUHUACEQDOsCCyABQQFqIgEgAkcNAAtB5AAhEAyCAwtB5AAhEAyBAwsDQAJAIAEtAABB8MKAgABqLQAAIhBBAUYNACAQQX5qDgPPAdAB0QHSAQsgAUEBaiIBIAJHDQALQeYAIRAMgAMLAkAgASIBIAJGDQAgAUEBaiEBDAMLQecAIRAM/wILA0ACQCABLQAAQfDEgIAAai0AACIQQQFGDQACQCAQQX5qDgTSAdMB1AEA1QELIAEhAUHXACEQDOcCCyABQQFqIgEgAkcNAAtB6AAhEAz+AgsCQCABIgEgAkcNAEHpACEQDP4CCwJAIAEtAAAiEEF2ag4augHVAdUBvAHVAdUB1QHVAdUB1QHVAdUB1QHVAdUB1QHVAdUB1QHVAdUB1QHKAdUB1QEA0wELIAFBAWohAQtBBiEQDOMCCwNAAkAgAS0AAEHwxoCAAGotAABBAUYNACABIQEMngILIAFBAWoiASACRw0AC0HqACEQDPsCCwJAIAEiASACRg0AIAFBAWohAQwDC0HrACEQDPoCCwJAIAEiASACRw0AQewAIRAM+gILIAFBAWohAQwBCwJAIAEiASACRw0AQe0AIRAM+QILIAFBAWohAQtBBCEQDN4CCwJAIAEiFCACRw0AQe4AIRAM9wILIBQhAQJAAkACQCAULQAAQfDIgIAAai0AAEF/ag4H1AHVAdYBAJwCAQLXAQsgFEEBaiEBDAoLIBRBAWohAQzNAQtBACEQIABBADYCHCAAQZuSgIAANgIQIABBBzYCDCAAIBRBAWo2AhQM9gILAkADQAJAIAEtAABB8MiAgABqLQAAIhBBBEYNAAJAAkAgEEF/ag4H0gHTAdQB2QEABAHZAQsgASEBQdoAIRAM4AILIAFBAWohAUHcACEQDN8CCyABQQFqIgEgAkcNAAtB7wAhEAz2AgsgAUEBaiEBDMsBCwJAIAEiFCACRw0AQfAAIRAM9QILIBQtAABBL0cN1AEgFEEBaiEBDAYLAkAgASIUIAJHDQBB8QAhEAz0AgsCQCAULQAAIgFBL0cNACAUQQFqIQFB3QAhEAzbAgsgAUF2aiIEQRZLDdMBQQEgBHRBiYCAAnFFDdMBDMoCCwJAIAEiASACRg0AIAFBAWohAUHeACEQDNoCC0HyACEQDPICCwJAIAEiFCACRw0AQfQAIRAM8gILIBQhAQJAIBQtAABB8MyAgABqLQAAQX9qDgPJApQCANQBC0HhACEQDNgCCwJAIAEiFCACRg0AA0ACQCAULQAAQfDKgIAAai0AACIBQQNGDQACQCABQX9qDgLLAgDVAQsgFCEBQd8AIRAM2gILIBRBAWoiFCACRw0AC0HzACEQDPECC0HzACEQDPACCwJAIAEiASACRg0AIABBj4CAgAA2AgggACABNgIEIAEhAUHgACEQDNcCC0H1ACEQDO8CCwJAIAEiASACRw0AQfYAIRAM7wILIABBj4CAgAA2AgggACABNgIEIAEhAQtBAyEQDNQCCwNAIAEtAABBIEcNwwIgAUEBaiIBIAJHDQALQfcAIRAM7AILAkAgASIBIAJHDQBB+AAhEAzsAgsgAS0AAEEgRw3OASABQQFqIQEM7wELIAAgASIBIAIQrICAgAAiEA3OASABIQEMjgILAkAgASIEIAJHDQBB+gAhEAzqAgsgBC0AAEHMAEcN0QEgBEEBaiEBQRMhEAzPAQsCQCABIgQgAkcNAEH7ACEQDOkCCyACIARrIAAoAgAiAWohFCAEIAFrQQVqIRADQCAELQAAIAFB8M6AgABqLQAARw3QASABQQVGDc4BIAFBAWohASAEQQFqIgQgAkcNAAsgACAUNgIAQfsAIRAM6AILAkAgASIEIAJHDQBB/AAhEAzoAgsCQAJAIAQtAABBvX9qDgwA0QHRAdEB0QHRAdEB0QHRAdEB0QEB0QELIARBAWohAUHmACEQDM8CCyAEQQFqIQFB5wAhEAzOAgsCQCABIgQgAkcNAEH9ACEQDOcCCyACIARrIAAoAgAiAWohFCAEIAFrQQJqIRACQANAIAQtAAAgAUHtz4CAAGotAABHDc8BIAFBAkYNASABQQFqIQEgBEEBaiIEIAJHDQALIAAgFDYCAEH9ACEQDOcCCyAAQQA2AgAgEEEBaiEBQRAhEAzMAQsCQCABIgQgAkcNAEH+ACEQDOYCCyACIARrIAAoAgAiAWohFCAEIAFrQQVqIRACQANAIAQtAAAgAUH2zoCAAGotAABHDc4BIAFBBUYNASABQQFqIQEgBEEBaiIEIAJHDQALIAAgFDYCAEH+ACEQDOYCCyAAQQA2AgAgEEEBaiEBQRYhEAzLAQsCQCABIgQgAkcNAEH/ACEQDOUCCyACIARrIAAoAgAiAWohFCAEIAFrQQNqIRACQANAIAQtAAAgAUH8zoCAAGotAABHDc0BIAFBA0YNASABQQFqIQEgBEEBaiIEIAJHDQALIAAgFDYCAEH/ACEQDOUCCyAAQQA2AgAgEEEBaiEBQQUhEAzKAQsCQCABIgQgAkcNAEGAASEQDOQCCyAELQAAQdkARw3LASAEQQFqIQFBCCEQDMkBCwJAIAEiBCACRw0AQYEBIRAM4wILAkACQCAELQAAQbJ/ag4DAMwBAcwBCyAEQQFqIQFB6wAhEAzKAgsgBEEBaiEBQewAIRAMyQILAkAgASIEIAJHDQBBggEhEAziAgsCQAJAIAQtAABBuH9qDggAywHLAcsBywHLAcsBAcsBCyAEQQFqIQFB6gAhEAzJAgsgBEEBaiEBQe0AIRAMyAILAkAgASIEIAJHDQBBgwEhEAzhAgsgAiAEayAAKAIAIgFqIRAgBCABa0ECaiEUAkADQCAELQAAIAFBgM+AgABqLQAARw3JASABQQJGDQEgAUEBaiEBIARBAWoiBCACRw0ACyAAIBA2AgBBgwEhEAzhAgtBACEQIABBADYCACAUQQFqIQEMxgELAkAgASIEIAJHDQBBhAEhEAzgAgsgAiAEayAAKAIAIgFqIRQgBCABa0EEaiEQAkADQCAELQAAIAFBg8+AgABqLQAARw3IASABQQRGDQEgAUEBaiEBIARBAWoiBCACRw0ACyAAIBQ2AgBBhAEhEAzgAgsgAEEANgIAIBBBAWohAUEjIRAMxQELAkAgASIEIAJHDQBBhQEhEAzfAgsCQAJAIAQtAABBtH9qDggAyAHIAcgByAHIAcgBAcgBCyAEQQFqIQFB7wAhEAzGAgsgBEEBaiEBQfAAIRAMxQILAkAgASIEIAJHDQBBhgEhEAzeAgsgBC0AAEHFAEcNxQEgBEEBaiEBDIMCCwJAIAEiBCACRw0AQYcBIRAM3QILIAIgBGsgACgCACIBaiEUIAQgAWtBA2ohEAJAA0AgBC0AACABQYjPgIAAai0AAEcNxQEgAUEDRg0BIAFBAWohASAEQQFqIgQgAkcNAAsgACAUNgIAQYcBIRAM3QILIABBADYCACAQQQFqIQFBLSEQDMIBCwJAIAEiBCACRw0AQYgBIRAM3AILIAIgBGsgACgCACIBaiEUIAQgAWtBCGohEAJAA0AgBC0AACABQdDPgIAAai0AAEcNxAEgAUEIRg0BIAFBAWohASAEQQFqIgQgAkcNAAsgACAUNgIAQYgBIRAM3AILIABBADYCACAQQQFqIQFBKSEQDMEBCwJAIAEiASACRw0AQYkBIRAM2wILQQEhECABLQAAQd8ARw3AASABQQFqIQEMgQILAkAgASIEIAJHDQBBigEhEAzaAgsgAiAEayAAKAIAIgFqIRQgBCABa0EBaiEQA0AgBC0AACABQYzPgIAAai0AAEcNwQEgAUEBRg2vAiABQQFqIQEgBEEBaiIEIAJHDQALIAAgFDYCAEGKASEQDNkCCwJAIAEiBCACRw0AQYsBIRAM2QILIAIgBGsgACgCACIBaiEUIAQgAWtBAmohEAJAA0AgBC0AACABQY7PgIAAai0AAEcNwQEgAUECRg0BIAFBAWohASAEQQFqIgQgAkcNAAsgACAUNgIAQYsBIRAM2QILIABBADYCACAQQQFqIQFBAiEQDL4BCwJAIAEiBCACRw0AQYwBIRAM2AILIAIgBGsgACgCACIBaiEUIAQgAWtBAWohEAJAA0AgBC0AACABQfDPgIAAai0AAEcNwAEgAUEBRg0BIAFBAWohASAEQQFqIgQgAkcNAAsgACAUNgIAQYwBIRAM2AILIABBADYCACAQQQFqIQFBHyEQDL0BCwJAIAEiBCACRw0AQY0BIRAM1wILIAIgBGsgACgCACIBaiEUIAQgAWtBAWohEAJAA0AgBC0AACABQfLPgIAAai0AAEcNvwEgAUEBRg0BIAFBAWohASAEQQFqIgQgAkcNAAsgACAUNgIAQY0BIRAM1wILIABBADYCACAQQQFqIQFBCSEQDLwBCwJAIAEiBCACRw0AQY4BIRAM1gILAkACQCAELQAAQbd/ag4HAL8BvwG/Ab8BvwEBvwELIARBAWohAUH4ACEQDL0CCyAEQQFqIQFB+QAhEAy8AgsCQCABIgQgAkcNAEGPASEQDNUCCyACIARrIAAoAgAiAWohFCAEIAFrQQVqIRACQANAIAQtAAAgAUGRz4CAAGotAABHDb0BIAFBBUYNASABQQFqIQEgBEEBaiIEIAJHDQALIAAgFDYCAEGPASEQDNUCCyAAQQA2AgAgEEEBaiEBQRghEAy6AQsCQCABIgQgAkcNAEGQASEQDNQCCyACIARrIAAoAgAiAWohFCAEIAFrQQJqIRACQANAIAQtAAAgAUGXz4CAAGotAABHDbwBIAFBAkYNASABQQFqIQEgBEEBaiIEIAJHDQALIAAgFDYCAEGQASEQDNQCCyAAQQA2AgAgEEEBaiEBQRchEAy5AQsCQCABIgQgAkcNAEGRASEQDNMCCyACIARrIAAoAgAiAWohFCAEIAFrQQZqIRACQANAIAQtAAAgAUGaz4CAAGotAABHDbsBIAFBBkYNASABQQFqIQEgBEEBaiIEIAJHDQALIAAgFDYCAEGRASEQDNMCCyAAQQA2AgAgEEEBaiEBQRUhEAy4AQsCQCABIgQgAkcNAEGSASEQDNICCyACIARrIAAoAgAiAWohFCAEIAFrQQVqIRACQANAIAQtAAAgAUGhz4CAAGotAABHDboBIAFBBUYNASABQQFqIQEgBEEBaiIEIAJHDQALIAAgFDYCAEGSASEQDNICCyAAQQA2AgAgEEEBaiEBQR4hEAy3AQsCQCABIgQgAkcNAEGTASEQDNECCyAELQAAQcwARw24ASAEQQFqIQFBCiEQDLYBCwJAIAQgAkcNAEGUASEQDNACCwJAAkAgBC0AAEG/f2oODwC5AbkBuQG5AbkBuQG5AbkBuQG5AbkBuQG5AQG5AQsgBEEBaiEBQf4AIRAMtwILIARBAWohAUH/ACEQDLYCCwJAIAQgAkcNAEGVASEQDM8CCwJAAkAgBC0AAEG/f2oOAwC4AQG4AQsgBEEBaiEBQf0AIRAMtgILIARBAWohBEGAASEQDLUCCwJAIAQgAkcNAEGWASEQDM4CCyACIARrIAAoAgAiAWohFCAEIAFrQQFqIRACQANAIAQtAAAgAUGnz4CAAGotAABHDbYBIAFBAUYNASABQQFqIQEgBEEBaiIEIAJHDQALIAAgFDYCAEGWASEQDM4CCyAAQQA2AgAgEEEBaiEBQQshEAyzAQsCQCAEIAJHDQBBlwEhEAzNAgsCQAJAAkACQCAELQAAQVNqDiMAuAG4AbgBuAG4AbgBuAG4AbgBuAG4AbgBuAG4AbgBuAG4AbgBuAG4AbgBuAG4AQG4AbgBuAG4AbgBArgBuAG4AQO4AQsgBEEBaiEBQfsAIRAMtgILIARBAWohAUH8ACEQDLUCCyAEQQFqIQRBgQEhEAy0AgsgBEEBaiEEQYIBIRAMswILAkAgBCACRw0AQZgBIRAMzAILIAIgBGsgACgCACIBaiEUIAQgAWtBBGohEAJAA0AgBC0AACABQanPgIAAai0AAEcNtAEgAUEERg0BIAFBAWohASAEQQFqIgQgAkcNAAsgACAUNgIAQZgBIRAMzAILIABBADYCACAQQQFqIQFBGSEQDLEBCwJAIAQgAkcNAEGZASEQDMsCCyACIARrIAAoAgAiAWohFCAEIAFrQQVqIRACQANAIAQtAAAgAUGuz4CAAGotAABHDbMBIAFBBUYNASABQQFqIQEgBEEBaiIEIAJHDQALIAAgFDYCAEGZASEQDMsCCyAAQQA2AgAgEEEBaiEBQQYhEAywAQsCQCAEIAJHDQBBmgEhEAzKAgsgAiAEayAAKAIAIgFqIRQgBCABa0EBaiEQAkADQCAELQAAIAFBtM+AgABqLQAARw2yASABQQFGDQEgAUEBaiEBIARBAWoiBCACRw0ACyAAIBQ2AgBBmgEhEAzKAgsgAEEANgIAIBBBAWohAUEcIRAMrwELAkAgBCACRw0AQZsBIRAMyQILIAIgBGsgACgCACIBaiEUIAQgAWtBAWohEAJAA0AgBC0AACABQbbPgIAAai0AAEcNsQEgAUEBRg0BIAFBAWohASAEQQFqIgQgAkcNAAsgACAUNgIAQZsBIRAMyQILIABBADYCACAQQQFqIQFBJyEQDK4BCwJAIAQgAkcNAEGcASEQDMgCCwJAAkAgBC0AAEGsf2oOAgABsQELIARBAWohBEGGASEQDK8CCyAEQQFqIQRBhwEhEAyuAgsCQCAEIAJHDQBBnQEhEAzHAgsgAiAEayAAKAIAIgFqIRQgBCABa0EBaiEQAkADQCAELQAAIAFBuM+AgABqLQAARw2vASABQQFGDQEgAUEBaiEBIARBAWoiBCACRw0ACyAAIBQ2AgBBnQEhEAzHAgsgAEEANgIAIBBBAWohAUEmIRAMrAELAkAgBCACRw0AQZ4BIRAMxgILIAIgBGsgACgCACIBaiEUIAQgAWtBAWohEAJAA0AgBC0AACABQbrPgIAAai0AAEcNrgEgAUEBRg0BIAFBAWohASAEQQFqIgQgAkcNAAsgACAUNgIAQZ4BIRAMxgILIABBADYCACAQQQFqIQFBAyEQDKsBCwJAIAQgAkcNAEGfASEQDMUCCyACIARrIAAoAgAiAWohFCAEIAFrQQJqIRACQANAIAQtAAAgAUHtz4CAAGotAABHDa0BIAFBAkYNASABQQFqIQEgBEEBaiIEIAJHDQALIAAgFDYCAEGfASEQDMUCCyAAQQA2AgAgEEEBaiEBQQwhEAyqAQsCQCAEIAJHDQBBoAEhEAzEAgsgAiAEayAAKAIAIgFqIRQgBCABa0EDaiEQAkADQCAELQAAIAFBvM+AgABqLQAARw2sASABQQNGDQEgAUEBaiEBIARBAWoiBCACRw0ACyAAIBQ2AgBBoAEhEAzEAgsgAEEANgIAIBBBAWohAUENIRAMqQELAkAgBCACRw0AQaEBIRAMwwILAkACQCAELQAAQbp/ag4LAKwBrAGsAawBrAGsAawBrAGsAQGsAQsgBEEBaiEEQYsBIRAMqgILIARBAWohBEGMASEQDKkCCwJAIAQgAkcNAEGiASEQDMICCyAELQAAQdAARw2pASAEQQFqIQQM6QELAkAgBCACRw0AQaMBIRAMwQILAkACQCAELQAAQbd/ag4HAaoBqgGqAaoBqgEAqgELIARBAWohBEGOASEQDKgCCyAEQQFqIQFBIiEQDKYBCwJAIAQgAkcNAEGkASEQDMACCyACIARrIAAoAgAiAWohFCAEIAFrQQFqIRACQANAIAQtAAAgAUHAz4CAAGotAABHDagBIAFBAUYNASABQQFqIQEgBEEBaiIEIAJHDQALIAAgFDYCAEGkASEQDMACCyAAQQA2AgAgEEEBaiEBQR0hEAylAQsCQCAEIAJHDQBBpQEhEAy/AgsCQAJAIAQtAABBrn9qDgMAqAEBqAELIARBAWohBEGQASEQDKYCCyAEQQFqIQFBBCEQDKQBCwJAIAQgAkcNAEGmASEQDL4CCwJAAkACQAJAAkAgBC0AAEG/f2oOFQCqAaoBqgGqAaoBqgGqAaoBqgGqAQGqAaoBAqoBqgEDqgGqAQSqAQsgBEEBaiEEQYgBIRAMqAILIARBAWohBEGJASEQDKcCCyAEQQFqIQRBigEhEAymAgsgBEEBaiEEQY8BIRAMpQILIARBAWohBEGRASEQDKQCCwJAIAQgAkcNAEGnASEQDL0CCyACIARrIAAoAgAiAWohFCAEIAFrQQJqIRACQANAIAQtAAAgAUHtz4CAAGotAABHDaUBIAFBAkYNASABQQFqIQEgBEEBaiIEIAJHDQALIAAgFDYCAEGnASEQDL0CCyAAQQA2AgAgEEEBaiEBQREhEAyiAQsCQCAEIAJHDQBBqAEhEAy8AgsgAiAEayAAKAIAIgFqIRQgBCABa0ECaiEQAkADQCAELQAAIAFBws+AgABqLQAARw2kASABQQJGDQEgAUEBaiEBIARBAWoiBCACRw0ACyAAIBQ2AgBBqAEhEAy8AgsgAEEANgIAIBBBAWohAUEsIRAMoQELAkAgBCACRw0AQakBIRAMuwILIAIgBGsgACgCACIBaiEUIAQgAWtBBGohEAJAA0AgBC0AACABQcXPgIAAai0AAEcNowEgAUEERg0BIAFBAWohASAEQQFqIgQgAkcNAAsgACAUNgIAQakBIRAMuwILIABBADYCACAQQQFqIQFBKyEQDKABCwJAIAQgAkcNAEGqASEQDLoCCyACIARrIAAoAgAiAWohFCAEIAFrQQJqIRACQANAIAQtAAAgAUHKz4CAAGotAABHDaIBIAFBAkYNASABQQFqIQEgBEEBaiIEIAJHDQALIAAgFDYCAEGqASEQDLoCCyAAQQA2AgAgEEEBaiEBQRQhEAyfAQsCQCAEIAJHDQBBqwEhEAy5AgsCQAJAAkACQCAELQAAQb5/ag4PAAECpAGkAaQBpAGkAaQBpAGkAaQBpAGkAQOkAQsgBEEBaiEEQZMBIRAMogILIARBAWohBEGUASEQDKECCyAEQQFqIQRBlQEhEAygAgsgBEEBaiEEQZYBIRAMnwILAkAgBCACRw0AQawBIRAMuAILIAQtAABBxQBHDZ8BIARBAWohBAzgAQsCQCAEIAJHDQBBrQEhEAy3AgsgAiAEayAAKAIAIgFqIRQgBCABa0ECaiEQAkADQCAELQAAIAFBzc+AgABqLQAARw2fASABQQJGDQEgAUEBaiEBIARBAWoiBCACRw0ACyAAIBQ2AgBBrQEhEAy3AgsgAEEANgIAIBBBAWohAUEOIRAMnAELAkAgBCACRw0AQa4BIRAMtgILIAQtAABB0ABHDZ0BIARBAWohAUElIRAMmwELAkAgBCACRw0AQa8BIRAMtQILIAIgBGsgACgCACIBaiEUIAQgAWtBCGohEAJAA0AgBC0AACABQdDPgIAAai0AAEcNnQEgAUEIRg0BIAFBAWohASAEQQFqIgQgAkcNAAsgACAUNgIAQa8BIRAMtQILIABBADYCACAQQQFqIQFBKiEQDJoBCwJAIAQgAkcNAEGwASEQDLQCCwJAAkAgBC0AAEGrf2oOCwCdAZ0BnQGdAZ0BnQGdAZ0BnQEBnQELIARBAWohBEGaASEQDJsCCyAEQQFqIQRBmwEhEAyaAgsCQCAEIAJHDQBBsQEhEAyzAgsCQAJAIAQtAABBv39qDhQAnAGcAZwBnAGcAZwBnAGcAZwBnAGcAZwBnAGcAZwBnAGcAZwBAZwBCyAEQQFqIQRBmQEhEAyaAgsgBEEBaiEEQZwBIRAMmQILAkAgBCACRw0AQbIBIRAMsgILIAIgBGsgACgCACIBaiEUIAQgAWtBA2ohEAJAA0AgBC0AACABQdnPgIAAai0AAEcNmgEgAUEDRg0BIAFBAWohASAEQQFqIgQgAkcNAAsgACAUNgIAQbIBIRAMsgILIABBADYCACAQQQFqIQFBISEQDJcBCwJAIAQgAkcNAEGzASEQDLECCyACIARrIAAoAgAiAWohFCAEIAFrQQZqIRACQANAIAQtAAAgAUHdz4CAAGotAABHDZkBIAFBBkYNASABQQFqIQEgBEEBaiIEIAJHDQALIAAgFDYCAEGzASEQDLECCyAAQQA2AgAgEEEBaiEBQRohEAyWAQsCQCAEIAJHDQBBtAEhEAywAgsCQAJAAkAgBC0AAEG7f2oOEQCaAZoBmgGaAZoBmgGaAZoBmgEBmgGaAZoBmgGaAQKaAQsgBEEBaiEEQZ0BIRAMmAILIARBAWohBEGeASEQDJcCCyAEQQFqIQRBnwEhEAyWAgsCQCAEIAJHDQBBtQEhEAyvAgsgAiAEayAAKAIAIgFqIRQgBCABa0EFaiEQAkADQCAELQAAIAFB5M+AgABqLQAARw2XASABQQVGDQEgAUEBaiEBIARBAWoiBCACRw0ACyAAIBQ2AgBBtQEhEAyvAgsgAEEANgIAIBBBAWohAUEoIRAMlAELAkAgBCACRw0AQbYBIRAMrgILIAIgBGsgACgCACIBaiEUIAQgAWtBAmohEAJAA0AgBC0AACABQerPgIAAai0AAEcNlgEgAUECRg0BIAFBAWohASAEQQFqIgQgAkcNAAsgACAUNgIAQbYBIRAMrgILIABBADYCACAQQQFqIQFBByEQDJMBCwJAIAQgAkcNAEG3ASEQDK0CCwJAAkAgBC0AAEG7f2oODgCWAZYBlgGWAZYBlgGWAZYBlgGWAZYBlgEBlgELIARBAWohBEGhASEQDJQCCyAEQQFqIQRBogEhEAyTAgsCQCAEIAJHDQBBuAEhEAysAgsgAiAEayAAKAIAIgFqIRQgBCABa0ECaiEQAkADQCAELQAAIAFB7c+AgABqLQAARw2UASABQQJGDQEgAUEBaiEBIARBAWoiBCACRw0ACyAAIBQ2AgBBuAEhEAysAgsgAEEANgIAIBBBAWohAUESIRAMkQELAkAgBCACRw0AQbkBIRAMqwILIAIgBGsgACgCACIBaiEUIAQgAWtBAWohEAJAA0AgBC0AACABQfDPgIAAai0AAEcNkwEgAUEBRg0BIAFBAWohASAEQQFqIgQgAkcNAAsgACAUNgIAQbkBIRAMqwILIABBADYCACAQQQFqIQFBICEQDJABCwJAIAQgAkcNAEG6ASEQDKoCCyACIARrIAAoAgAiAWohFCAEIAFrQQFqIRACQANAIAQtAAAgAUHyz4CAAGotAABHDZIBIAFBAUYNASABQQFqIQEgBEEBaiIEIAJHDQALIAAgFDYCAEG6ASEQDKoCCyAAQQA2AgAgEEEBaiEBQQ8hEAyPAQsCQCAEIAJHDQBBuwEhEAypAgsCQAJAIAQtAABBt39qDgcAkgGSAZIBkgGSAQGSAQsgBEEBaiEEQaUBIRAMkAILIARBAWohBEGmASEQDI8CCwJAIAQgAkcNAEG8ASEQDKgCCyACIARrIAAoAgAiAWohFCAEIAFrQQdqIRACQANAIAQtAAAgAUH0z4CAAGotAABHDZABIAFBB0YNASABQQFqIQEgBEEBaiIEIAJHDQALIAAgFDYCAEG8ASEQDKgCCyAAQQA2AgAgEEEBaiEBQRshEAyNAQsCQCAEIAJHDQBBvQEhEAynAgsCQAJAAkAgBC0AAEG+f2oOEgCRAZEBkQGRAZEBkQGRAZEBkQEBkQGRAZEBkQGRAZEBApEBCyAEQQFqIQRBpAEhEAyPAgsgBEEBaiEEQacBIRAMjgILIARBAWohBEGoASEQDI0CCwJAIAQgAkcNAEG+ASEQDKYCCyAELQAAQc4ARw2NASAEQQFqIQQMzwELAkAgBCACRw0AQb8BIRAMpQILAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkAgBC0AAEG/f2oOFQABAgOcAQQFBpwBnAGcAQcICQoLnAEMDQ4PnAELIARBAWohAUHoACEQDJoCCyAEQQFqIQFB6QAhEAyZAgsgBEEBaiEBQe4AIRAMmAILIARBAWohAUHyACEQDJcCCyAEQQFqIQFB8wAhEAyWAgsgBEEBaiEBQfYAIRAMlQILIARBAWohAUH3ACEQDJQCCyAEQQFqIQFB+gAhEAyTAgsgBEEBaiEEQYMBIRAMkgILIARBAWohBEGEASEQDJECCyAEQQFqIQRBhQEhEAyQAgsgBEEBaiEEQZIBIRAMjwILIARBAWohBEGYASEQDI4CCyAEQQFqIQRBoAEhEAyNAgsgBEEBaiEEQaMBIRAMjAILIARBAWohBEGqASEQDIsCCwJAIAQgAkYNACAAQZCAgIAANgIIIAAgBDYCBEGrASEQDIsCC0HAASEQDKMCCyAAIAUgAhCqgICAACIBDYsBIAUhAQxcCwJAIAYgAkYNACAGQQFqIQUMjQELQcIBIRAMoQILA0ACQCAQLQAAQXZqDgSMAQAAjwEACyAQQQFqIhAgAkcNAAtBwwEhEAygAgsCQCAHIAJGDQAgAEGRgICAADYCCCAAIAc2AgQgByEBQQEhEAyHAgtBxAEhEAyfAgsCQCAHIAJHDQBBxQEhEAyfAgsCQAJAIActAABBdmoOBAHOAc4BAM4BCyAHQQFqIQYMjQELIAdBAWohBQyJAQsCQCAHIAJHDQBBxgEhEAyeAgsCQAJAIActAABBdmoOFwGPAY8BAY8BjwGPAY8BjwGPAY8BjwGPAY8BjwGPAY8BjwGPAY8BjwGPAQCPAQsgB0EBaiEHC0GwASEQDIQCCwJAIAggAkcNAEHIASEQDJ0CCyAILQAAQSBHDY0BIABBADsBMiAIQQFqIQFBswEhEAyDAgsgASEXAkADQCAXIgcgAkYNASAHLQAAQVBqQf8BcSIQQQpPDcwBAkAgAC8BMiIUQZkzSw0AIAAgFEEKbCIUOwEyIBBB//8DcyAUQf7/A3FJDQAgB0EBaiEXIAAgFCAQaiIQOwEyIBBB//8DcUHoB0kNAQsLQQAhECAAQQA2AhwgAEHBiYCAADYCECAAQQ02AgwgACAHQQFqNgIUDJwCC0HHASEQDJsCCyAAIAggAhCugICAACIQRQ3KASAQQRVHDYwBIABByAE2AhwgACAINgIUIABByZeAgAA2AhAgAEEVNgIMQQAhEAyaAgsCQCAJIAJHDQBBzAEhEAyaAgtBACEUQQEhF0EBIRZBACEQAkACQAJAAkACQAJAAkACQAJAIAktAABBUGoOCpYBlQEAAQIDBAUGCJcBC0ECIRAMBgtBAyEQDAULQQQhEAwEC0EFIRAMAwtBBiEQDAILQQchEAwBC0EIIRALQQAhF0EAIRZBACEUDI4BC0EJIRBBASEUQQAhF0EAIRYMjQELAkAgCiACRw0AQc4BIRAMmQILIAotAABBLkcNjgEgCkEBaiEJDMoBCyALIAJHDY4BQdABIRAMlwILAkAgCyACRg0AIABBjoCAgAA2AgggACALNgIEQbcBIRAM/gELQdEBIRAMlgILAkAgBCACRw0AQdIBIRAMlgILIAIgBGsgACgCACIQaiEUIAQgEGtBBGohCwNAIAQtAAAgEEH8z4CAAGotAABHDY4BIBBBBEYN6QEgEEEBaiEQIARBAWoiBCACRw0ACyAAIBQ2AgBB0gEhEAyVAgsgACAMIAIQrICAgAAiAQ2NASAMIQEMuAELAkAgBCACRw0AQdQBIRAMlAILIAIgBGsgACgCACIQaiEUIAQgEGtBAWohDANAIAQtAAAgEEGB0ICAAGotAABHDY8BIBBBAUYNjgEgEEEBaiEQIARBAWoiBCACRw0ACyAAIBQ2AgBB1AEhEAyTAgsCQCAEIAJHDQBB1gEhEAyTAgsgAiAEayAAKAIAIhBqIRQgBCAQa0ECaiELA0AgBC0AACAQQYPQgIAAai0AAEcNjgEgEEECRg2QASAQQQFqIRAgBEEBaiIEIAJHDQALIAAgFDYCAEHWASEQDJICCwJAIAQgAkcNAEHXASEQDJICCwJAAkAgBC0AAEG7f2oOEACPAY8BjwGPAY8BjwGPAY8BjwGPAY8BjwGPAY8BAY8BCyAEQQFqIQRBuwEhEAz5AQsgBEEBaiEEQbwBIRAM+AELAkAgBCACRw0AQdgBIRAMkQILIAQtAABByABHDYwBIARBAWohBAzEAQsCQCAEIAJGDQAgAEGQgICAADYCCCAAIAQ2AgRBvgEhEAz3AQtB2QEhEAyPAgsCQCAEIAJHDQBB2gEhEAyPAgsgBC0AAEHIAEYNwwEgAEEBOgAoDLkBCyAAQQI6AC8gACAEIAIQpoCAgAAiEA2NAUHCASEQDPQBCyAALQAoQX9qDgK3AbkBuAELA0ACQCAELQAAQXZqDgQAjgGOAQCOAQsgBEEBaiIEIAJHDQALQd0BIRAMiwILIABBADoALyAALQAtQQRxRQ2EAgsgAEEAOgAvIABBAToANCABIQEMjAELIBBBFUYN2gEgAEEANgIcIAAgATYCFCAAQaeOgIAANgIQIABBEjYCDEEAIRAMiAILAkAgACAQIAIQtICAgAAiBA0AIBAhAQyBAgsCQCAEQRVHDQAgAEEDNgIcIAAgEDYCFCAAQbCYgIAANgIQIABBFTYCDEEAIRAMiAILIABBADYCHCAAIBA2AhQgAEGnjoCAADYCECAAQRI2AgxBACEQDIcCCyAQQRVGDdYBIABBADYCHCAAIAE2AhQgAEHajYCAADYCECAAQRQ2AgxBACEQDIYCCyAAKAIEIRcgAEEANgIEIBAgEadqIhYhASAAIBcgECAWIBQbIhAQtYCAgAAiFEUNjQEgAEEHNgIcIAAgEDYCFCAAIBQ2AgxBACEQDIUCCyAAIAAvATBBgAFyOwEwIAEhAQtBKiEQDOoBCyAQQRVGDdEBIABBADYCHCAAIAE2AhQgAEGDjICAADYCECAAQRM2AgxBACEQDIICCyAQQRVGDc8BIABBADYCHCAAIAE2AhQgAEGaj4CAADYCECAAQSI2AgxBACEQDIECCyAAKAIEIRAgAEEANgIEAkAgACAQIAEQt4CAgAAiEA0AIAFBAWohAQyNAQsgAEEMNgIcIAAgEDYCDCAAIAFBAWo2AhRBACEQDIACCyAQQRVGDcwBIABBADYCHCAAIAE2AhQgAEGaj4CAADYCECAAQSI2AgxBACEQDP8BCyAAKAIEIRAgAEEANgIEAkAgACAQIAEQt4CAgAAiEA0AIAFBAWohAQyMAQsgAEENNgIcIAAgEDYCDCAAIAFBAWo2AhRBACEQDP4BCyAQQRVGDckBIABBADYCHCAAIAE2AhQgAEHGjICAADYCECAAQSM2AgxBACEQDP0BCyAAKAIEIRAgAEEANgIEAkAgACAQIAEQuYCAgAAiEA0AIAFBAWohAQyLAQsgAEEONgIcIAAgEDYCDCAAIAFBAWo2AhRBACEQDPwBCyAAQQA2AhwgACABNgIUIABBwJWAgAA2AhAgAEECNgIMQQAhEAz7AQsgEEEVRg3FASAAQQA2AhwgACABNgIUIABBxoyAgAA2AhAgAEEjNgIMQQAhEAz6AQsgAEEQNgIcIAAgATYCFCAAIBA2AgxBACEQDPkBCyAAKAIEIQQgAEEANgIEAkAgACAEIAEQuYCAgAAiBA0AIAFBAWohAQzxAQsgAEERNgIcIAAgBDYCDCAAIAFBAWo2AhRBACEQDPgBCyAQQRVGDcEBIABBADYCHCAAIAE2AhQgAEHGjICAADYCECAAQSM2AgxBACEQDPcBCyAAKAIEIRAgAEEANgIEAkAgACAQIAEQuYCAgAAiEA0AIAFBAWohAQyIAQsgAEETNgIcIAAgEDYCDCAAIAFBAWo2AhRBACEQDPYBCyAAKAIEIQQgAEEANgIEAkAgACAEIAEQuYCAgAAiBA0AIAFBAWohAQztAQsgAEEUNgIcIAAgBDYCDCAAIAFBAWo2AhRBACEQDPUBCyAQQRVGDb0BIABBADYCHCAAIAE2AhQgAEGaj4CAADYCECAAQSI2AgxBACEQDPQBCyAAKAIEIRAgAEEANgIEAkAgACAQIAEQt4CAgAAiEA0AIAFBAWohAQyGAQsgAEEWNgIcIAAgEDYCDCAAIAFBAWo2AhRBACEQDPMBCyAAKAIEIQQgAEEANgIEAkAgACAEIAEQt4CAgAAiBA0AIAFBAWohAQzpAQsgAEEXNgIcIAAgBDYCDCAAIAFBAWo2AhRBACEQDPIBCyAAQQA2AhwgACABNgIUIABBzZOAgAA2AhAgAEEMNgIMQQAhEAzxAQtCASERCyAQQQFqIQECQCAAKQMgIhJC//////////8PVg0AIAAgEkIEhiARhDcDICABIQEMhAELIABBADYCHCAAIAE2AhQgAEGtiYCAADYCECAAQQw2AgxBACEQDO8BCyAAQQA2AhwgACAQNgIUIABBzZOAgAA2AhAgAEEMNgIMQQAhEAzuAQsgACgCBCEXIABBADYCBCAQIBGnaiIWIQEgACAXIBAgFiAUGyIQELWAgIAAIhRFDXMgAEEFNgIcIAAgEDYCFCAAIBQ2AgxBACEQDO0BCyAAQQA2AhwgACAQNgIUIABBqpyAgAA2AhAgAEEPNgIMQQAhEAzsAQsgACAQIAIQtICAgAAiAQ0BIBAhAQtBDiEQDNEBCwJAIAFBFUcNACAAQQI2AhwgACAQNgIUIABBsJiAgAA2AhAgAEEVNgIMQQAhEAzqAQsgAEEANgIcIAAgEDYCFCAAQaeOgIAANgIQIABBEjYCDEEAIRAM6QELIAFBAWohEAJAIAAvATAiAUGAAXFFDQACQCAAIBAgAhC7gICAACIBDQAgECEBDHALIAFBFUcNugEgAEEFNgIcIAAgEDYCFCAAQfmXgIAANgIQIABBFTYCDEEAIRAM6QELAkAgAUGgBHFBoARHDQAgAC0ALUECcQ0AIABBADYCHCAAIBA2AhQgAEGWk4CAADYCECAAQQQ2AgxBACEQDOkBCyAAIBAgAhC9gICAABogECEBAkACQAJAAkACQCAAIBAgAhCzgICAAA4WAgEABAQEBAQEBAQEBAQEBAQEBAQEAwQLIABBAToALgsgACAALwEwQcAAcjsBMCAQIQELQSYhEAzRAQsgAEEjNgIcIAAgEDYCFCAAQaWWgIAANgIQIABBFTYCDEEAIRAM6QELIABBADYCHCAAIBA2AhQgAEHVi4CAADYCECAAQRE2AgxBACEQDOgBCyAALQAtQQFxRQ0BQcMBIRAMzgELAkAgDSACRg0AA0ACQCANLQAAQSBGDQAgDSEBDMQBCyANQQFqIg0gAkcNAAtBJSEQDOcBC0ElIRAM5gELIAAoAgQhBCAAQQA2AgQgACAEIA0Qr4CAgAAiBEUNrQEgAEEmNgIcIAAgBDYCDCAAIA1BAWo2AhRBACEQDOUBCyAQQRVGDasBIABBADYCHCAAIAE2AhQgAEH9jYCAADYCECAAQR02AgxBACEQDOQBCyAAQSc2AhwgACABNgIUIAAgEDYCDEEAIRAM4wELIBAhAUEBIRQCQAJAAkACQAJAAkACQCAALQAsQX5qDgcGBQUDAQIABQsgACAALwEwQQhyOwEwDAMLQQIhFAwBC0EEIRQLIABBAToALCAAIAAvATAgFHI7ATALIBAhAQtBKyEQDMoBCyAAQQA2AhwgACAQNgIUIABBq5KAgAA2AhAgAEELNgIMQQAhEAziAQsgAEEANgIcIAAgATYCFCAAQeGPgIAANgIQIABBCjYCDEEAIRAM4QELIABBADoALCAQIQEMvQELIBAhAUEBIRQCQAJAAkACQAJAIAAtACxBe2oOBAMBAgAFCyAAIAAvATBBCHI7ATAMAwtBAiEUDAELQQQhFAsgAEEBOgAsIAAgAC8BMCAUcjsBMAsgECEBC0EpIRAMxQELIABBADYCHCAAIAE2AhQgAEHwlICAADYCECAAQQM2AgxBACEQDN0BCwJAIA4tAABBDUcNACAAKAIEIQEgAEEANgIEAkAgACABIA4QsYCAgAAiAQ0AIA5BAWohAQx1CyAAQSw2AhwgACABNgIMIAAgDkEBajYCFEEAIRAM3QELIAAtAC1BAXFFDQFBxAEhEAzDAQsCQCAOIAJHDQBBLSEQDNwBCwJAAkADQAJAIA4tAABBdmoOBAIAAAMACyAOQQFqIg4gAkcNAAtBLSEQDN0BCyAAKAIEIQEgAEEANgIEAkAgACABIA4QsYCAgAAiAQ0AIA4hAQx0CyAAQSw2AhwgACAONgIUIAAgATYCDEEAIRAM3AELIAAoAgQhASAAQQA2AgQCQCAAIAEgDhCxgICAACIBDQAgDkEBaiEBDHMLIABBLDYCHCAAIAE2AgwgACAOQQFqNgIUQQAhEAzbAQsgACgCBCEEIABBADYCBCAAIAQgDhCxgICAACIEDaABIA4hAQzOAQsgEEEsRw0BIAFBAWohEEEBIQECQAJAAkACQAJAIAAtACxBe2oOBAMBAgQACyAQIQEMBAtBAiEBDAELQQQhAQsgAEEBOgAsIAAgAC8BMCABcjsBMCAQIQEMAQsgACAALwEwQQhyOwEwIBAhAQtBOSEQDL8BCyAAQQA6ACwgASEBC0E0IRAMvQELIAAgAC8BMEEgcjsBMCABIQEMAgsgACgCBCEEIABBADYCBAJAIAAgBCABELGAgIAAIgQNACABIQEMxwELIABBNzYCHCAAIAE2AhQgACAENgIMQQAhEAzUAQsgAEEIOgAsIAEhAQtBMCEQDLkBCwJAIAAtAChBAUYNACABIQEMBAsgAC0ALUEIcUUNkwEgASEBDAMLIAAtADBBIHENlAFBxQEhEAy3AQsCQCAPIAJGDQACQANAAkAgDy0AAEFQaiIBQf8BcUEKSQ0AIA8hAUE1IRAMugELIAApAyAiEUKZs+bMmbPmzBlWDQEgACARQgp+IhE3AyAgESABrUL/AYMiEkJ/hVYNASAAIBEgEnw3AyAgD0EBaiIPIAJHDQALQTkhEAzRAQsgACgCBCECIABBADYCBCAAIAIgD0EBaiIEELGAgIAAIgINlQEgBCEBDMMBC0E5IRAMzwELAkAgAC8BMCIBQQhxRQ0AIAAtAChBAUcNACAALQAtQQhxRQ2QAQsgACABQff7A3FBgARyOwEwIA8hAQtBNyEQDLQBCyAAIAAvATBBEHI7ATAMqwELIBBBFUYNiwEgAEEANgIcIAAgATYCFCAAQfCOgIAANgIQIABBHDYCDEEAIRAMywELIABBwwA2AhwgACABNgIMIAAgDUEBajYCFEEAIRAMygELAkAgAS0AAEE6Rw0AIAAoAgQhECAAQQA2AgQCQCAAIBAgARCvgICAACIQDQAgAUEBaiEBDGMLIABBwwA2AhwgACAQNgIMIAAgAUEBajYCFEEAIRAMygELIABBADYCHCAAIAE2AhQgAEGxkYCAADYCECAAQQo2AgxBACEQDMkBCyAAQQA2AhwgACABNgIUIABBoJmAgAA2AhAgAEEeNgIMQQAhEAzIAQsgAEEANgIACyAAQYASOwEqIAAgF0EBaiIBIAIQqICAgAAiEA0BIAEhAQtBxwAhEAysAQsgEEEVRw2DASAAQdEANgIcIAAgATYCFCAAQeOXgIAANgIQIABBFTYCDEEAIRAMxAELIAAoAgQhECAAQQA2AgQCQCAAIBAgARCngICAACIQDQAgASEBDF4LIABB0gA2AhwgACABNgIUIAAgEDYCDEEAIRAMwwELIABBADYCHCAAIBQ2AhQgAEHBqICAADYCECAAQQc2AgwgAEEANgIAQQAhEAzCAQsgACgCBCEQIABBADYCBAJAIAAgECABEKeAgIAAIhANACABIQEMXQsgAEHTADYCHCAAIAE2AhQgACAQNgIMQQAhEAzBAQtBACEQIABBADYCHCAAIAE2AhQgAEGAkYCAADYCECAAQQk2AgwMwAELIBBBFUYNfSAAQQA2AhwgACABNgIUIABBlI2AgAA2AhAgAEEhNgIMQQAhEAy/AQtBASEWQQAhF0EAIRRBASEQCyAAIBA6ACsgAUEBaiEBAkACQCAALQAtQRBxDQACQAJAAkAgAC0AKg4DAQACBAsgFkUNAwwCCyAUDQEMAgsgF0UNAQsgACgCBCEQIABBADYCBAJAIAAgECABEK2AgIAAIhANACABIQEMXAsgAEHYADYCHCAAIAE2AhQgACAQNgIMQQAhEAy+AQsgACgCBCEEIABBADYCBAJAIAAgBCABEK2AgIAAIgQNACABIQEMrQELIABB2QA2AhwgACABNgIUIAAgBDYCDEEAIRAMvQELIAAoAgQhBCAAQQA2AgQCQCAAIAQgARCtgICAACIEDQAgASEBDKsBCyAAQdoANgIcIAAgATYCFCAAIAQ2AgxBACEQDLwBCyAAKAIEIQQgAEEANgIEAkAgACAEIAEQrYCAgAAiBA0AIAEhAQypAQsgAEHcADYCHCAAIAE2AhQgACAENgIMQQAhEAy7AQsCQCABLQAAQVBqIhBB/wFxQQpPDQAgACAQOgAqIAFBAWohAUHPACEQDKIBCyAAKAIEIQQgAEEANgIEAkAgACAEIAEQrYCAgAAiBA0AIAEhAQynAQsgAEHeADYCHCAAIAE2AhQgACAENgIMQQAhEAy6AQsgAEEANgIAIBdBAWohAQJAIAAtAClBI08NACABIQEMWQsgAEEANgIcIAAgATYCFCAAQdOJgIAANgIQIABBCDYCDEEAIRAMuQELIABBADYCAAtBACEQIABBADYCHCAAIAE2AhQgAEGQs4CAADYCECAAQQg2AgwMtwELIABBADYCACAXQQFqIQECQCAALQApQSFHDQAgASEBDFYLIABBADYCHCAAIAE2AhQgAEGbioCAADYCECAAQQg2AgxBACEQDLYBCyAAQQA2AgAgF0EBaiEBAkAgAC0AKSIQQV1qQQtPDQAgASEBDFULAkAgEEEGSw0AQQEgEHRBygBxRQ0AIAEhAQxVC0EAIRAgAEEANgIcIAAgATYCFCAAQfeJgIAANgIQIABBCDYCDAy1AQsgEEEVRg1xIABBADYCHCAAIAE2AhQgAEG5jYCAADYCECAAQRo2AgxBACEQDLQBCyAAKAIEIRAgAEEANgIEAkAgACAQIAEQp4CAgAAiEA0AIAEhAQxUCyAAQeUANgIcIAAgATYCFCAAIBA2AgxBACEQDLMBCyAAKAIEIRAgAEEANgIEAkAgACAQIAEQp4CAgAAiEA0AIAEhAQxNCyAAQdIANgIcIAAgATYCFCAAIBA2AgxBACEQDLIBCyAAKAIEIRAgAEEANgIEAkAgACAQIAEQp4CAgAAiEA0AIAEhAQxNCyAAQdMANgIcIAAgATYCFCAAIBA2AgxBACEQDLEBCyAAKAIEIRAgAEEANgIEAkAgACAQIAEQp4CAgAAiEA0AIAEhAQxRCyAAQeUANgIcIAAgATYCFCAAIBA2AgxBACEQDLABCyAAQQA2AhwgACABNgIUIABBxoqAgAA2AhAgAEEHNgIMQQAhEAyvAQsgACgCBCEQIABBADYCBAJAIAAgECABEKeAgIAAIhANACABIQEMSQsgAEHSADYCHCAAIAE2AhQgACAQNgIMQQAhEAyuAQsgACgCBCEQIABBADYCBAJAIAAgECABEKeAgIAAIhANACABIQEMSQsgAEHTADYCHCAAIAE2AhQgACAQNgIMQQAhEAytAQsgACgCBCEQIABBADYCBAJAIAAgECABEKeAgIAAIhANACABIQEMTQsgAEHlADYCHCAAIAE2AhQgACAQNgIMQQAhEAysAQsgAEEANgIcIAAgATYCFCAAQdyIgIAANgIQIABBBzYCDEEAIRAMqwELIBBBP0cNASABQQFqIQELQQUhEAyQAQtBACEQIABBADYCHCAAIAE2AhQgAEH9koCAADYCECAAQQc2AgwMqAELIAAoAgQhECAAQQA2AgQCQCAAIBAgARCngICAACIQDQAgASEBDEILIABB0gA2AhwgACABNgIUIAAgEDYCDEEAIRAMpwELIAAoAgQhECAAQQA2AgQCQCAAIBAgARCngICAACIQDQAgASEBDEILIABB0wA2AhwgACABNgIUIAAgEDYCDEEAIRAMpgELIAAoAgQhECAAQQA2AgQCQCAAIBAgARCngICAACIQDQAgASEBDEYLIABB5QA2AhwgACABNgIUIAAgEDYCDEEAIRAMpQELIAAoAgQhASAAQQA2AgQCQCAAIAEgFBCngICAACIBDQAgFCEBDD8LIABB0gA2AhwgACAUNgIUIAAgATYCDEEAIRAMpAELIAAoAgQhASAAQQA2AgQCQCAAIAEgFBCngICAACIBDQAgFCEBDD8LIABB0wA2AhwgACAUNgIUIAAgATYCDEEAIRAMowELIAAoAgQhASAAQQA2AgQCQCAAIAEgFBCngICAACIBDQAgFCEBDEMLIABB5QA2AhwgACAUNgIUIAAgATYCDEEAIRAMogELIABBADYCHCAAIBQ2AhQgAEHDj4CAADYCECAAQQc2AgxBACEQDKEBCyAAQQA2AhwgACABNgIUIABBw4+AgAA2AhAgAEEHNgIMQQAhEAygAQtBACEQIABBADYCHCAAIBQ2AhQgAEGMnICAADYCECAAQQc2AgwMnwELIABBADYCHCAAIBQ2AhQgAEGMnICAADYCECAAQQc2AgxBACEQDJ4BCyAAQQA2AhwgACAUNgIUIABB/pGAgAA2AhAgAEEHNgIMQQAhEAydAQsgAEEANgIcIAAgATYCFCAAQY6bgIAANgIQIABBBjYCDEEAIRAMnAELIBBBFUYNVyAAQQA2AhwgACABNgIUIABBzI6AgAA2AhAgAEEgNgIMQQAhEAybAQsgAEEANgIAIBBBAWohAUEkIRALIAAgEDoAKSAAKAIEIRAgAEEANgIEIAAgECABEKuAgIAAIhANVCABIQEMPgsgAEEANgIAC0EAIRAgAEEANgIcIAAgBDYCFCAAQfGbgIAANgIQIABBBjYCDAyXAQsgAUEVRg1QIABBADYCHCAAIAU2AhQgAEHwjICAADYCECAAQRs2AgxBACEQDJYBCyAAKAIEIQUgAEEANgIEIAAgBSAQEKmAgIAAIgUNASAQQQFqIQULQa0BIRAMewsgAEHBATYCHCAAIAU2AgwgACAQQQFqNgIUQQAhEAyTAQsgACgCBCEGIABBADYCBCAAIAYgEBCpgICAACIGDQEgEEEBaiEGC0GuASEQDHgLIABBwgE2AhwgACAGNgIMIAAgEEEBajYCFEEAIRAMkAELIABBADYCHCAAIAc2AhQgAEGXi4CAADYCECAAQQ02AgxBACEQDI8BCyAAQQA2AhwgACAINgIUIABB45CAgAA2AhAgAEEJNgIMQQAhEAyOAQsgAEEANgIcIAAgCDYCFCAAQZSNgIAANgIQIABBITYCDEEAIRAMjQELQQEhFkEAIRdBACEUQQEhEAsgACAQOgArIAlBAWohCAJAAkAgAC0ALUEQcQ0AAkACQAJAIAAtACoOAwEAAgQLIBZFDQMMAgsgFA0BDAILIBdFDQELIAAoAgQhECAAQQA2AgQgACAQIAgQrYCAgAAiEEUNPSAAQckBNgIcIAAgCDYCFCAAIBA2AgxBACEQDIwBCyAAKAIEIQQgAEEANgIEIAAgBCAIEK2AgIAAIgRFDXYgAEHKATYCHCAAIAg2AhQgACAENgIMQQAhEAyLAQsgACgCBCEEIABBADYCBCAAIAQgCRCtgICAACIERQ10IABBywE2AhwgACAJNgIUIAAgBDYCDEEAIRAMigELIAAoAgQhBCAAQQA2AgQgACAEIAoQrYCAgAAiBEUNciAAQc0BNgIcIAAgCjYCFCAAIAQ2AgxBACEQDIkBCwJAIAstAABBUGoiEEH/AXFBCk8NACAAIBA6ACogC0EBaiEKQbYBIRAMcAsgACgCBCEEIABBADYCBCAAIAQgCxCtgICAACIERQ1wIABBzwE2AhwgACALNgIUIAAgBDYCDEEAIRAMiAELIABBADYCHCAAIAQ2AhQgAEGQs4CAADYCECAAQQg2AgwgAEEANgIAQQAhEAyHAQsgAUEVRg0/IABBADYCHCAAIAw2AhQgAEHMjoCAADYCECAAQSA2AgxBACEQDIYBCyAAQYEEOwEoIAAoAgQhECAAQgA3AwAgACAQIAxBAWoiDBCrgICAACIQRQ04IABB0wE2AhwgACAMNgIUIAAgEDYCDEEAIRAMhQELIABBADYCAAtBACEQIABBADYCHCAAIAQ2AhQgAEHYm4CAADYCECAAQQg2AgwMgwELIAAoAgQhECAAQgA3AwAgACAQIAtBAWoiCxCrgICAACIQDQFBxgEhEAxpCyAAQQI6ACgMVQsgAEHVATYCHCAAIAs2AhQgACAQNgIMQQAhEAyAAQsgEEEVRg03IABBADYCHCAAIAQ2AhQgAEGkjICAADYCECAAQRA2AgxBACEQDH8LIAAtADRBAUcNNCAAIAQgAhC8gICAACIQRQ00IBBBFUcNNSAAQdwBNgIcIAAgBDYCFCAAQdWWgIAANgIQIABBFTYCDEEAIRAMfgtBACEQIABBADYCHCAAQa+LgIAANgIQIABBAjYCDCAAIBRBAWo2AhQMfQtBACEQDGMLQQIhEAxiC0ENIRAMYQtBDyEQDGALQSUhEAxfC0ETIRAMXgtBFSEQDF0LQRYhEAxcC0EXIRAMWwtBGCEQDFoLQRkhEAxZC0EaIRAMWAtBGyEQDFcLQRwhEAxWC0EdIRAMVQtBHyEQDFQLQSEhEAxTC0EjIRAMUgtBxgAhEAxRC0EuIRAMUAtBLyEQDE8LQTshEAxOC0E9IRAMTQtByAAhEAxMC0HJACEQDEsLQcsAIRAMSgtBzAAhEAxJC0HOACEQDEgLQdEAIRAMRwtB1QAhEAxGC0HYACEQDEULQdkAIRAMRAtB2wAhEAxDC0HkACEQDEILQeUAIRAMQQtB8QAhEAxAC0H0ACEQDD8LQY0BIRAMPgtBlwEhEAw9C0GpASEQDDwLQawBIRAMOwtBwAEhEAw6C0G5ASEQDDkLQa8BIRAMOAtBsQEhEAw3C0GyASEQDDYLQbQBIRAMNQtBtQEhEAw0C0G6ASEQDDMLQb0BIRAMMgtBvwEhEAwxC0HBASEQDDALIABBADYCHCAAIAQ2AhQgAEHpi4CAADYCECAAQR82AgxBACEQDEgLIABB2wE2AhwgACAENgIUIABB+paAgAA2AhAgAEEVNgIMQQAhEAxHCyAAQfgANgIcIAAgDDYCFCAAQcqYgIAANgIQIABBFTYCDEEAIRAMRgsgAEHRADYCHCAAIAU2AhQgAEGwl4CAADYCECAAQRU2AgxBACEQDEULIABB+QA2AhwgACABNgIUIAAgEDYCDEEAIRAMRAsgAEH4ADYCHCAAIAE2AhQgAEHKmICAADYCECAAQRU2AgxBACEQDEMLIABB5AA2AhwgACABNgIUIABB45eAgAA2AhAgAEEVNgIMQQAhEAxCCyAAQdcANgIcIAAgATYCFCAAQcmXgIAANgIQIABBFTYCDEEAIRAMQQsgAEEANgIcIAAgATYCFCAAQbmNgIAANgIQIABBGjYCDEEAIRAMQAsgAEHCADYCHCAAIAE2AhQgAEHjmICAADYCECAAQRU2AgxBACEQDD8LIABBADYCBCAAIA8gDxCxgICAACIERQ0BIABBOjYCHCAAIAQ2AgwgACAPQQFqNgIUQQAhEAw+CyAAKAIEIQQgAEEANgIEAkAgACAEIAEQsYCAgAAiBEUNACAAQTs2AhwgACAENgIMIAAgAUEBajYCFEEAIRAMPgsgAUEBaiEBDC0LIA9BAWohAQwtCyAAQQA2AhwgACAPNgIUIABB5JKAgAA2AhAgAEEENgIMQQAhEAw7CyAAQTY2AhwgACAENgIUIAAgAjYCDEEAIRAMOgsgAEEuNgIcIAAgDjYCFCAAIAQ2AgxBACEQDDkLIABB0AA2AhwgACABNgIUIABBkZiAgAA2AhAgAEEVNgIMQQAhEAw4CyANQQFqIQEMLAsgAEEVNgIcIAAgATYCFCAAQYKZgIAANgIQIABBFTYCDEEAIRAMNgsgAEEbNgIcIAAgATYCFCAAQZGXgIAANgIQIABBFTYCDEEAIRAMNQsgAEEPNgIcIAAgATYCFCAAQZGXgIAANgIQIABBFTYCDEEAIRAMNAsgAEELNgIcIAAgATYCFCAAQZGXgIAANgIQIABBFTYCDEEAIRAMMwsgAEEaNgIcIAAgATYCFCAAQYKZgIAANgIQIABBFTYCDEEAIRAMMgsgAEELNgIcIAAgATYCFCAAQYKZgIAANgIQIABBFTYCDEEAIRAMMQsgAEEKNgIcIAAgATYCFCAAQeSWgIAANgIQIABBFTYCDEEAIRAMMAsgAEEeNgIcIAAgATYCFCAAQfmXgIAANgIQIABBFTYCDEEAIRAMLwsgAEEANgIcIAAgEDYCFCAAQdqNgIAANgIQIABBFDYCDEEAIRAMLgsgAEEENgIcIAAgATYCFCAAQbCYgIAANgIQIABBFTYCDEEAIRAMLQsgAEEANgIAIAtBAWohCwtBuAEhEAwSCyAAQQA2AgAgEEEBaiEBQfUAIRAMEQsgASEBAkAgAC0AKUEFRw0AQeMAIRAMEQtB4gAhEAwQC0EAIRAgAEEANgIcIABB5JGAgAA2AhAgAEEHNgIMIAAgFEEBajYCFAwoCyAAQQA2AgAgF0EBaiEBQcAAIRAMDgtBASEBCyAAIAE6ACwgAEEANgIAIBdBAWohAQtBKCEQDAsLIAEhAQtBOCEQDAkLAkAgASIPIAJGDQADQAJAIA8tAABBgL6AgABqLQAAIgFBAUYNACABQQJHDQMgD0EBaiEBDAQLIA9BAWoiDyACRw0AC0E+IRAMIgtBPiEQDCELIABBADoALCAPIQEMAQtBCyEQDAYLQTohEAwFCyABQQFqIQFBLSEQDAQLIAAgAToALCAAQQA2AgAgFkEBaiEBQQwhEAwDCyAAQQA2AgAgF0EBaiEBQQohEAwCCyAAQQA2AgALIABBADoALCANIQFBCSEQDAALC0EAIRAgAEEANgIcIAAgCzYCFCAAQc2QgIAANgIQIABBCTYCDAwXC0EAIRAgAEEANgIcIAAgCjYCFCAAQemKgIAANgIQIABBCTYCDAwWC0EAIRAgAEEANgIcIAAgCTYCFCAAQbeQgIAANgIQIABBCTYCDAwVC0EAIRAgAEEANgIcIAAgCDYCFCAAQZyRgIAANgIQIABBCTYCDAwUC0EAIRAgAEEANgIcIAAgATYCFCAAQc2QgIAANgIQIABBCTYCDAwTC0EAIRAgAEEANgIcIAAgATYCFCAAQemKgIAANgIQIABBCTYCDAwSC0EAIRAgAEEANgIcIAAgATYCFCAAQbeQgIAANgIQIABBCTYCDAwRC0EAIRAgAEEANgIcIAAgATYCFCAAQZyRgIAANgIQIABBCTYCDAwQC0EAIRAgAEEANgIcIAAgATYCFCAAQZeVgIAANgIQIABBDzYCDAwPC0EAIRAgAEEANgIcIAAgATYCFCAAQZeVgIAANgIQIABBDzYCDAwOC0EAIRAgAEEANgIcIAAgATYCFCAAQcCSgIAANgIQIABBCzYCDAwNC0EAIRAgAEEANgIcIAAgATYCFCAAQZWJgIAANgIQIABBCzYCDAwMC0EAIRAgAEEANgIcIAAgATYCFCAAQeGPgIAANgIQIABBCjYCDAwLC0EAIRAgAEEANgIcIAAgATYCFCAAQfuPgIAANgIQIABBCjYCDAwKC0EAIRAgAEEANgIcIAAgATYCFCAAQfGZgIAANgIQIABBAjYCDAwJC0EAIRAgAEEANgIcIAAgATYCFCAAQcSUgIAANgIQIABBAjYCDAwIC0EAIRAgAEEANgIcIAAgATYCFCAAQfKVgIAANgIQIABBAjYCDAwHCyAAQQI2AhwgACABNgIUIABBnJqAgAA2AhAgAEEWNgIMQQAhEAwGC0EBIRAMBQtB1AAhECABIgQgAkYNBCADQQhqIAAgBCACQdjCgIAAQQoQxYCAgAAgAygCDCEEIAMoAggOAwEEAgALEMqAgIAAAAsgAEEANgIcIABBtZqAgAA2AhAgAEEXNgIMIAAgBEEBajYCFEEAIRAMAgsgAEEANgIcIAAgBDYCFCAAQcqagIAANgIQIABBCTYCDEEAIRAMAQsCQCABIgQgAkcNAEEiIRAMAQsgAEGJgICAADYCCCAAIAQ2AgRBISEQCyADQRBqJICAgIAAIBALrwEBAn8gASgCACEGAkACQCACIANGDQAgBCAGaiEEIAYgA2ogAmshByACIAZBf3MgBWoiBmohBQNAAkAgAi0AACAELQAARg0AQQIhBAwDCwJAIAYNAEEAIQQgBSECDAMLIAZBf2ohBiAEQQFqIQQgAkEBaiICIANHDQALIAchBiADIQILIABBATYCACABIAY2AgAgACACNgIEDwsgAUEANgIAIAAgBDYCACAAIAI2AgQLCgAgABDHgICAAAvyNgELfyOAgICAAEEQayIBJICAgIAAAkBBACgCoNCAgAANAEEAEMuAgIAAQYDUhIAAayICQdkASQ0AQQAhAwJAQQAoAuDTgIAAIgQNAEEAQn83AuzTgIAAQQBCgICEgICAwAA3AuTTgIAAQQAgAUEIakFwcUHYqtWqBXMiBDYC4NOAgABBAEEANgL004CAAEEAQQA2AsTTgIAAC0EAIAI2AszTgIAAQQBBgNSEgAA2AsjTgIAAQQBBgNSEgAA2ApjQgIAAQQAgBDYCrNCAgABBAEF/NgKo0ICAAANAIANBxNCAgABqIANBuNCAgABqIgQ2AgAgBCADQbDQgIAAaiIFNgIAIANBvNCAgABqIAU2AgAgA0HM0ICAAGogA0HA0ICAAGoiBTYCACAFIAQ2AgAgA0HU0ICAAGogA0HI0ICAAGoiBDYCACAEIAU2AgAgA0HQ0ICAAGogBDYCACADQSBqIgNBgAJHDQALQYDUhIAAQXhBgNSEgABrQQ9xQQBBgNSEgABBCGpBD3EbIgNqIgRBBGogAkFIaiIFIANrIgNBAXI2AgBBAEEAKALw04CAADYCpNCAgABBACADNgKU0ICAAEEAIAQ2AqDQgIAAQYDUhIAAIAVqQTg2AgQLAkACQAJAAkACQAJAAkACQAJAAkACQAJAIABB7AFLDQACQEEAKAKI0ICAACIGQRAgAEETakFwcSAAQQtJGyICQQN2IgR2IgNBA3FFDQACQAJAIANBAXEgBHJBAXMiBUEDdCIEQbDQgIAAaiIDIARBuNCAgABqKAIAIgQoAggiAkcNAEEAIAZBfiAFd3E2AojQgIAADAELIAMgAjYCCCACIAM2AgwLIARBCGohAyAEIAVBA3QiBUEDcjYCBCAEIAVqIgQgBCgCBEEBcjYCBAwMCyACQQAoApDQgIAAIgdNDQECQCADRQ0AAkACQCADIAR0QQIgBHQiA0EAIANrcnEiA0EAIANrcUF/aiIDIANBDHZBEHEiA3YiBEEFdkEIcSIFIANyIAQgBXYiA0ECdkEEcSIEciADIAR2IgNBAXZBAnEiBHIgAyAEdiIDQQF2QQFxIgRyIAMgBHZqIgRBA3QiA0Gw0ICAAGoiBSADQbjQgIAAaigCACIDKAIIIgBHDQBBACAGQX4gBHdxIgY2AojQgIAADAELIAUgADYCCCAAIAU2AgwLIAMgAkEDcjYCBCADIARBA3QiBGogBCACayIFNgIAIAMgAmoiACAFQQFyNgIEAkAgB0UNACAHQXhxQbDQgIAAaiECQQAoApzQgIAAIQQCQAJAIAZBASAHQQN2dCIIcQ0AQQAgBiAIcjYCiNCAgAAgAiEIDAELIAIoAgghCAsgCCAENgIMIAIgBDYCCCAEIAI2AgwgBCAINgIICyADQQhqIQNBACAANgKc0ICAAEEAIAU2ApDQgIAADAwLQQAoAozQgIAAIglFDQEgCUEAIAlrcUF/aiIDIANBDHZBEHEiA3YiBEEFdkEIcSIFIANyIAQgBXYiA0ECdkEEcSIEciADIAR2IgNBAXZBAnEiBHIgAyAEdiIDQQF2QQFxIgRyIAMgBHZqQQJ0QbjSgIAAaigCACIAKAIEQXhxIAJrIQQgACEFAkADQAJAIAUoAhAiAw0AIAVBFGooAgAiA0UNAgsgAygCBEF4cSACayIFIAQgBSAESSIFGyEEIAMgACAFGyEAIAMhBQwACwsgACgCGCEKAkAgACgCDCIIIABGDQAgACgCCCIDQQAoApjQgIAASRogCCADNgIIIAMgCDYCDAwLCwJAIABBFGoiBSgCACIDDQAgACgCECIDRQ0DIABBEGohBQsDQCAFIQsgAyIIQRRqIgUoAgAiAw0AIAhBEGohBSAIKAIQIgMNAAsgC0EANgIADAoLQX8hAiAAQb9/Sw0AIABBE2oiA0FwcSECQQAoAozQgIAAIgdFDQBBACELAkAgAkGAAkkNAEEfIQsgAkH///8HSw0AIANBCHYiAyADQYD+P2pBEHZBCHEiA3QiBCAEQYDgH2pBEHZBBHEiBHQiBSAFQYCAD2pBEHZBAnEiBXRBD3YgAyAEciAFcmsiA0EBdCACIANBFWp2QQFxckEcaiELC0EAIAJrIQQCQAJAAkACQCALQQJ0QbjSgIAAaigCACIFDQBBACEDQQAhCAwBC0EAIQMgAkEAQRkgC0EBdmsgC0EfRht0IQBBACEIA0ACQCAFKAIEQXhxIAJrIgYgBE8NACAGIQQgBSEIIAYNAEEAIQQgBSEIIAUhAwwDCyADIAVBFGooAgAiBiAGIAUgAEEddkEEcWpBEGooAgAiBUYbIAMgBhshAyAAQQF0IQAgBQ0ACwsCQCADIAhyDQBBACEIQQIgC3QiA0EAIANrciAHcSIDRQ0DIANBACADa3FBf2oiAyADQQx2QRBxIgN2IgVBBXZBCHEiACADciAFIAB2IgNBAnZBBHEiBXIgAyAFdiIDQQF2QQJxIgVyIAMgBXYiA0EBdkEBcSIFciADIAV2akECdEG40oCAAGooAgAhAwsgA0UNAQsDQCADKAIEQXhxIAJrIgYgBEkhAAJAIAMoAhAiBQ0AIANBFGooAgAhBQsgBiAEIAAbIQQgAyAIIAAbIQggBSEDIAUNAAsLIAhFDQAgBEEAKAKQ0ICAACACa08NACAIKAIYIQsCQCAIKAIMIgAgCEYNACAIKAIIIgNBACgCmNCAgABJGiAAIAM2AgggAyAANgIMDAkLAkAgCEEUaiIFKAIAIgMNACAIKAIQIgNFDQMgCEEQaiEFCwNAIAUhBiADIgBBFGoiBSgCACIDDQAgAEEQaiEFIAAoAhAiAw0ACyAGQQA2AgAMCAsCQEEAKAKQ0ICAACIDIAJJDQBBACgCnNCAgAAhBAJAAkAgAyACayIFQRBJDQAgBCACaiIAIAVBAXI2AgRBACAFNgKQ0ICAAEEAIAA2ApzQgIAAIAQgA2ogBTYCACAEIAJBA3I2AgQMAQsgBCADQQNyNgIEIAQgA2oiAyADKAIEQQFyNgIEQQBBADYCnNCAgABBAEEANgKQ0ICAAAsgBEEIaiEDDAoLAkBBACgClNCAgAAiACACTQ0AQQAoAqDQgIAAIgMgAmoiBCAAIAJrIgVBAXI2AgRBACAFNgKU0ICAAEEAIAQ2AqDQgIAAIAMgAkEDcjYCBCADQQhqIQMMCgsCQAJAQQAoAuDTgIAARQ0AQQAoAujTgIAAIQQMAQtBAEJ/NwLs04CAAEEAQoCAhICAgMAANwLk04CAAEEAIAFBDGpBcHFB2KrVqgVzNgLg04CAAEEAQQA2AvTTgIAAQQBBADYCxNOAgABBgIAEIQQLQQAhAwJAIAQgAkHHAGoiB2oiBkEAIARrIgtxIgggAksNAEEAQTA2AvjTgIAADAoLAkBBACgCwNOAgAAiA0UNAAJAQQAoArjTgIAAIgQgCGoiBSAETQ0AIAUgA00NAQtBACEDQQBBMDYC+NOAgAAMCgtBAC0AxNOAgABBBHENBAJAAkACQEEAKAKg0ICAACIERQ0AQcjTgIAAIQMDQAJAIAMoAgAiBSAESw0AIAUgAygCBGogBEsNAwsgAygCCCIDDQALC0EAEMuAgIAAIgBBf0YNBSAIIQYCQEEAKALk04CAACIDQX9qIgQgAHFFDQAgCCAAayAEIABqQQAgA2txaiEGCyAGIAJNDQUgBkH+////B0sNBQJAQQAoAsDTgIAAIgNFDQBBACgCuNOAgAAiBCAGaiIFIARNDQYgBSADSw0GCyAGEMuAgIAAIgMgAEcNAQwHCyAGIABrIAtxIgZB/v///wdLDQQgBhDLgICAACIAIAMoAgAgAygCBGpGDQMgACEDCwJAIANBf0YNACACQcgAaiAGTQ0AAkAgByAGa0EAKALo04CAACIEakEAIARrcSIEQf7///8HTQ0AIAMhAAwHCwJAIAQQy4CAgABBf0YNACAEIAZqIQYgAyEADAcLQQAgBmsQy4CAgAAaDAQLIAMhACADQX9HDQUMAwtBACEIDAcLQQAhAAwFCyAAQX9HDQILQQBBACgCxNOAgABBBHI2AsTTgIAACyAIQf7///8HSw0BIAgQy4CAgAAhAEEAEMuAgIAAIQMgAEF/Rg0BIANBf0YNASAAIANPDQEgAyAAayIGIAJBOGpNDQELQQBBACgCuNOAgAAgBmoiAzYCuNOAgAACQCADQQAoArzTgIAATQ0AQQAgAzYCvNOAgAALAkACQAJAAkBBACgCoNCAgAAiBEUNAEHI04CAACEDA0AgACADKAIAIgUgAygCBCIIakYNAiADKAIIIgMNAAwDCwsCQAJAQQAoApjQgIAAIgNFDQAgACADTw0BC0EAIAA2ApjQgIAAC0EAIQNBACAGNgLM04CAAEEAIAA2AsjTgIAAQQBBfzYCqNCAgABBAEEAKALg04CAADYCrNCAgABBAEEANgLU04CAAANAIANBxNCAgABqIANBuNCAgABqIgQ2AgAgBCADQbDQgIAAaiIFNgIAIANBvNCAgABqIAU2AgAgA0HM0ICAAGogA0HA0ICAAGoiBTYCACAFIAQ2AgAgA0HU0ICAAGogA0HI0ICAAGoiBDYCACAEIAU2AgAgA0HQ0ICAAGogBDYCACADQSBqIgNBgAJHDQALIABBeCAAa0EPcUEAIABBCGpBD3EbIgNqIgQgBkFIaiIFIANrIgNBAXI2AgRBAEEAKALw04CAADYCpNCAgABBACADNgKU0ICAAEEAIAQ2AqDQgIAAIAAgBWpBODYCBAwCCyADLQAMQQhxDQAgBCAFSQ0AIAQgAE8NACAEQXggBGtBD3FBACAEQQhqQQ9xGyIFaiIAQQAoApTQgIAAIAZqIgsgBWsiBUEBcjYCBCADIAggBmo2AgRBAEEAKALw04CAADYCpNCAgABBACAFNgKU0ICAAEEAIAA2AqDQgIAAIAQgC2pBODYCBAwBCwJAIABBACgCmNCAgAAiCE8NAEEAIAA2ApjQgIAAIAAhCAsgACAGaiEFQcjTgIAAIQMCQAJAAkACQAJAAkACQANAIAMoAgAgBUYNASADKAIIIgMNAAwCCwsgAy0ADEEIcUUNAQtByNOAgAAhAwNAAkAgAygCACIFIARLDQAgBSADKAIEaiIFIARLDQMLIAMoAgghAwwACwsgAyAANgIAIAMgAygCBCAGajYCBCAAQXggAGtBD3FBACAAQQhqQQ9xG2oiCyACQQNyNgIEIAVBeCAFa0EPcUEAIAVBCGpBD3EbaiIGIAsgAmoiAmshAwJAIAYgBEcNAEEAIAI2AqDQgIAAQQBBACgClNCAgAAgA2oiAzYClNCAgAAgAiADQQFyNgIEDAMLAkAgBkEAKAKc0ICAAEcNAEEAIAI2ApzQgIAAQQBBACgCkNCAgAAgA2oiAzYCkNCAgAAgAiADQQFyNgIEIAIgA2ogAzYCAAwDCwJAIAYoAgQiBEEDcUEBRw0AIARBeHEhBwJAAkAgBEH/AUsNACAGKAIIIgUgBEEDdiIIQQN0QbDQgIAAaiIARhoCQCAGKAIMIgQgBUcNAEEAQQAoAojQgIAAQX4gCHdxNgKI0ICAAAwCCyAEIABGGiAEIAU2AgggBSAENgIMDAELIAYoAhghCQJAAkAgBigCDCIAIAZGDQAgBigCCCIEIAhJGiAAIAQ2AgggBCAANgIMDAELAkAgBkEUaiIEKAIAIgUNACAGQRBqIgQoAgAiBQ0AQQAhAAwBCwNAIAQhCCAFIgBBFGoiBCgCACIFDQAgAEEQaiEEIAAoAhAiBQ0ACyAIQQA2AgALIAlFDQACQAJAIAYgBigCHCIFQQJ0QbjSgIAAaiIEKAIARw0AIAQgADYCACAADQFBAEEAKAKM0ICAAEF+IAV3cTYCjNCAgAAMAgsgCUEQQRQgCSgCECAGRhtqIAA2AgAgAEUNAQsgACAJNgIYAkAgBigCECIERQ0AIAAgBDYCECAEIAA2AhgLIAYoAhQiBEUNACAAQRRqIAQ2AgAgBCAANgIYCyAHIANqIQMgBiAHaiIGKAIEIQQLIAYgBEF+cTYCBCACIANqIAM2AgAgAiADQQFyNgIEAkAgA0H/AUsNACADQXhxQbDQgIAAaiEEAkACQEEAKAKI0ICAACIFQQEgA0EDdnQiA3ENAEEAIAUgA3I2AojQgIAAIAQhAwwBCyAEKAIIIQMLIAMgAjYCDCAEIAI2AgggAiAENgIMIAIgAzYCCAwDC0EfIQQCQCADQf///wdLDQAgA0EIdiIEIARBgP4/akEQdkEIcSIEdCIFIAVBgOAfakEQdkEEcSIFdCIAIABBgIAPakEQdkECcSIAdEEPdiAEIAVyIAByayIEQQF0IAMgBEEVanZBAXFyQRxqIQQLIAIgBDYCHCACQgA3AhAgBEECdEG40oCAAGohBQJAQQAoAozQgIAAIgBBASAEdCIIcQ0AIAUgAjYCAEEAIAAgCHI2AozQgIAAIAIgBTYCGCACIAI2AgggAiACNgIMDAMLIANBAEEZIARBAXZrIARBH0YbdCEEIAUoAgAhAANAIAAiBSgCBEF4cSADRg0CIARBHXYhACAEQQF0IQQgBSAAQQRxakEQaiIIKAIAIgANAAsgCCACNgIAIAIgBTYCGCACIAI2AgwgAiACNgIIDAILIABBeCAAa0EPcUEAIABBCGpBD3EbIgNqIgsgBkFIaiIIIANrIgNBAXI2AgQgACAIakE4NgIEIAQgBUE3IAVrQQ9xQQAgBUFJakEPcRtqQUFqIgggCCAEQRBqSRsiCEEjNgIEQQBBACgC8NOAgAA2AqTQgIAAQQAgAzYClNCAgABBACALNgKg0ICAACAIQRBqQQApAtDTgIAANwIAIAhBACkCyNOAgAA3AghBACAIQQhqNgLQ04CAAEEAIAY2AszTgIAAQQAgADYCyNOAgABBAEEANgLU04CAACAIQSRqIQMDQCADQQc2AgAgA0EEaiIDIAVJDQALIAggBEYNAyAIIAgoAgRBfnE2AgQgCCAIIARrIgA2AgAgBCAAQQFyNgIEAkAgAEH/AUsNACAAQXhxQbDQgIAAaiEDAkACQEEAKAKI0ICAACIFQQEgAEEDdnQiAHENAEEAIAUgAHI2AojQgIAAIAMhBQwBCyADKAIIIQULIAUgBDYCDCADIAQ2AgggBCADNgIMIAQgBTYCCAwEC0EfIQMCQCAAQf///wdLDQAgAEEIdiIDIANBgP4/akEQdkEIcSIDdCIFIAVBgOAfakEQdkEEcSIFdCIIIAhBgIAPakEQdkECcSIIdEEPdiADIAVyIAhyayIDQQF0IAAgA0EVanZBAXFyQRxqIQMLIAQgAzYCHCAEQgA3AhAgA0ECdEG40oCAAGohBQJAQQAoAozQgIAAIghBASADdCIGcQ0AIAUgBDYCAEEAIAggBnI2AozQgIAAIAQgBTYCGCAEIAQ2AgggBCAENgIMDAQLIABBAEEZIANBAXZrIANBH0YbdCEDIAUoAgAhCANAIAgiBSgCBEF4cSAARg0DIANBHXYhCCADQQF0IQMgBSAIQQRxakEQaiIGKAIAIggNAAsgBiAENgIAIAQgBTYCGCAEIAQ2AgwgBCAENgIIDAMLIAUoAggiAyACNgIMIAUgAjYCCCACQQA2AhggAiAFNgIMIAIgAzYCCAsgC0EIaiEDDAULIAUoAggiAyAENgIMIAUgBDYCCCAEQQA2AhggBCAFNgIMIAQgAzYCCAtBACgClNCAgAAiAyACTQ0AQQAoAqDQgIAAIgQgAmoiBSADIAJrIgNBAXI2AgRBACADNgKU0ICAAEEAIAU2AqDQgIAAIAQgAkEDcjYCBCAEQQhqIQMMAwtBACEDQQBBMDYC+NOAgAAMAgsCQCALRQ0AAkACQCAIIAgoAhwiBUECdEG40oCAAGoiAygCAEcNACADIAA2AgAgAA0BQQAgB0F+IAV3cSIHNgKM0ICAAAwCCyALQRBBFCALKAIQIAhGG2ogADYCACAARQ0BCyAAIAs2AhgCQCAIKAIQIgNFDQAgACADNgIQIAMgADYCGAsgCEEUaigCACIDRQ0AIABBFGogAzYCACADIAA2AhgLAkACQCAEQQ9LDQAgCCAEIAJqIgNBA3I2AgQgCCADaiIDIAMoAgRBAXI2AgQMAQsgCCACaiIAIARBAXI2AgQgCCACQQNyNgIEIAAgBGogBDYCAAJAIARB/wFLDQAgBEF4cUGw0ICAAGohAwJAAkBBACgCiNCAgAAiBUEBIARBA3Z0IgRxDQBBACAFIARyNgKI0ICAACADIQQMAQsgAygCCCEECyAEIAA2AgwgAyAANgIIIAAgAzYCDCAAIAQ2AggMAQtBHyEDAkAgBEH///8HSw0AIARBCHYiAyADQYD+P2pBEHZBCHEiA3QiBSAFQYDgH2pBEHZBBHEiBXQiAiACQYCAD2pBEHZBAnEiAnRBD3YgAyAFciACcmsiA0EBdCAEIANBFWp2QQFxckEcaiEDCyAAIAM2AhwgAEIANwIQIANBAnRBuNKAgABqIQUCQCAHQQEgA3QiAnENACAFIAA2AgBBACAHIAJyNgKM0ICAACAAIAU2AhggACAANgIIIAAgADYCDAwBCyAEQQBBGSADQQF2ayADQR9GG3QhAyAFKAIAIQICQANAIAIiBSgCBEF4cSAERg0BIANBHXYhAiADQQF0IQMgBSACQQRxakEQaiIGKAIAIgINAAsgBiAANgIAIAAgBTYCGCAAIAA2AgwgACAANgIIDAELIAUoAggiAyAANgIMIAUgADYCCCAAQQA2AhggACAFNgIMIAAgAzYCCAsgCEEIaiEDDAELAkAgCkUNAAJAAkAgACAAKAIcIgVBAnRBuNKAgABqIgMoAgBHDQAgAyAINgIAIAgNAUEAIAlBfiAFd3E2AozQgIAADAILIApBEEEUIAooAhAgAEYbaiAINgIAIAhFDQELIAggCjYCGAJAIAAoAhAiA0UNACAIIAM2AhAgAyAINgIYCyAAQRRqKAIAIgNFDQAgCEEUaiADNgIAIAMgCDYCGAsCQAJAIARBD0sNACAAIAQgAmoiA0EDcjYCBCAAIANqIgMgAygCBEEBcjYCBAwBCyAAIAJqIgUgBEEBcjYCBCAAIAJBA3I2AgQgBSAEaiAENgIAAkAgB0UNACAHQXhxQbDQgIAAaiECQQAoApzQgIAAIQMCQAJAQQEgB0EDdnQiCCAGcQ0AQQAgCCAGcjYCiNCAgAAgAiEIDAELIAIoAgghCAsgCCADNgIMIAIgAzYCCCADIAI2AgwgAyAINgIIC0EAIAU2ApzQgIAAQQAgBDYCkNCAgAALIABBCGohAwsgAUEQaiSAgICAACADCwoAIAAQyYCAgAAL4g0BB38CQCAARQ0AIABBeGoiASAAQXxqKAIAIgJBeHEiAGohAwJAIAJBAXENACACQQNxRQ0BIAEgASgCACICayIBQQAoApjQgIAAIgRJDQEgAiAAaiEAAkAgAUEAKAKc0ICAAEYNAAJAIAJB/wFLDQAgASgCCCIEIAJBA3YiBUEDdEGw0ICAAGoiBkYaAkAgASgCDCICIARHDQBBAEEAKAKI0ICAAEF+IAV3cTYCiNCAgAAMAwsgAiAGRhogAiAENgIIIAQgAjYCDAwCCyABKAIYIQcCQAJAIAEoAgwiBiABRg0AIAEoAggiAiAESRogBiACNgIIIAIgBjYCDAwBCwJAIAFBFGoiAigCACIEDQAgAUEQaiICKAIAIgQNAEEAIQYMAQsDQCACIQUgBCIGQRRqIgIoAgAiBA0AIAZBEGohAiAGKAIQIgQNAAsgBUEANgIACyAHRQ0BAkACQCABIAEoAhwiBEECdEG40oCAAGoiAigCAEcNACACIAY2AgAgBg0BQQBBACgCjNCAgABBfiAEd3E2AozQgIAADAMLIAdBEEEUIAcoAhAgAUYbaiAGNgIAIAZFDQILIAYgBzYCGAJAIAEoAhAiAkUNACAGIAI2AhAgAiAGNgIYCyABKAIUIgJFDQEgBkEUaiACNgIAIAIgBjYCGAwBCyADKAIEIgJBA3FBA0cNACADIAJBfnE2AgRBACAANgKQ0ICAACABIABqIAA2AgAgASAAQQFyNgIEDwsgASADTw0AIAMoAgQiAkEBcUUNAAJAAkAgAkECcQ0AAkAgA0EAKAKg0ICAAEcNAEEAIAE2AqDQgIAAQQBBACgClNCAgAAgAGoiADYClNCAgAAgASAAQQFyNgIEIAFBACgCnNCAgABHDQNBAEEANgKQ0ICAAEEAQQA2ApzQgIAADwsCQCADQQAoApzQgIAARw0AQQAgATYCnNCAgABBAEEAKAKQ0ICAACAAaiIANgKQ0ICAACABIABBAXI2AgQgASAAaiAANgIADwsgAkF4cSAAaiEAAkACQCACQf8BSw0AIAMoAggiBCACQQN2IgVBA3RBsNCAgABqIgZGGgJAIAMoAgwiAiAERw0AQQBBACgCiNCAgABBfiAFd3E2AojQgIAADAILIAIgBkYaIAIgBDYCCCAEIAI2AgwMAQsgAygCGCEHAkACQCADKAIMIgYgA0YNACADKAIIIgJBACgCmNCAgABJGiAGIAI2AgggAiAGNgIMDAELAkAgA0EUaiICKAIAIgQNACADQRBqIgIoAgAiBA0AQQAhBgwBCwNAIAIhBSAEIgZBFGoiAigCACIEDQAgBkEQaiECIAYoAhAiBA0ACyAFQQA2AgALIAdFDQACQAJAIAMgAygCHCIEQQJ0QbjSgIAAaiICKAIARw0AIAIgBjYCACAGDQFBAEEAKAKM0ICAAEF+IAR3cTYCjNCAgAAMAgsgB0EQQRQgBygCECADRhtqIAY2AgAgBkUNAQsgBiAHNgIYAkAgAygCECICRQ0AIAYgAjYCECACIAY2AhgLIAMoAhQiAkUNACAGQRRqIAI2AgAgAiAGNgIYCyABIABqIAA2AgAgASAAQQFyNgIEIAFBACgCnNCAgABHDQFBACAANgKQ0ICAAA8LIAMgAkF+cTYCBCABIABqIAA2AgAgASAAQQFyNgIECwJAIABB/wFLDQAgAEF4cUGw0ICAAGohAgJAAkBBACgCiNCAgAAiBEEBIABBA3Z0IgBxDQBBACAEIAByNgKI0ICAACACIQAMAQsgAigCCCEACyAAIAE2AgwgAiABNgIIIAEgAjYCDCABIAA2AggPC0EfIQICQCAAQf///wdLDQAgAEEIdiICIAJBgP4/akEQdkEIcSICdCIEIARBgOAfakEQdkEEcSIEdCIGIAZBgIAPakEQdkECcSIGdEEPdiACIARyIAZyayICQQF0IAAgAkEVanZBAXFyQRxqIQILIAEgAjYCHCABQgA3AhAgAkECdEG40oCAAGohBAJAAkBBACgCjNCAgAAiBkEBIAJ0IgNxDQAgBCABNgIAQQAgBiADcjYCjNCAgAAgASAENgIYIAEgATYCCCABIAE2AgwMAQsgAEEAQRkgAkEBdmsgAkEfRht0IQIgBCgCACEGAkADQCAGIgQoAgRBeHEgAEYNASACQR12IQYgAkEBdCECIAQgBkEEcWpBEGoiAygCACIGDQALIAMgATYCACABIAQ2AhggASABNgIMIAEgATYCCAwBCyAEKAIIIgAgATYCDCAEIAE2AgggAUEANgIYIAEgBDYCDCABIAA2AggLQQBBACgCqNCAgABBf2oiAUF/IAEbNgKo0ICAAAsLBAAAAAtOAAJAIAANAD8AQRB0DwsCQCAAQf//A3ENACAAQX9MDQACQCAAQRB2QAAiAEF/Rw0AQQBBMDYC+NOAgABBfw8LIABBEHQPCxDKgICAAAAL8gICA38BfgJAIAJFDQAgACABOgAAIAIgAGoiA0F/aiABOgAAIAJBA0kNACAAIAE6AAIgACABOgABIANBfWogAToAACADQX5qIAE6AAAgAkEHSQ0AIAAgAToAAyADQXxqIAE6AAAgAkEJSQ0AIABBACAAa0EDcSIEaiIDIAFB/wFxQYGChAhsIgE2AgAgAyACIARrQXxxIgRqIgJBfGogATYCACAEQQlJDQAgAyABNgIIIAMgATYCBCACQXhqIAE2AgAgAkF0aiABNgIAIARBGUkNACADIAE2AhggAyABNgIUIAMgATYCECADIAE2AgwgAkFwaiABNgIAIAJBbGogATYCACACQWhqIAE2AgAgAkFkaiABNgIAIAQgA0EEcUEYciIFayICQSBJDQAgAa1CgYCAgBB+IQYgAyAFaiEBA0AgASAGNwMYIAEgBjcDECABIAY3AwggASAGNwMAIAFBIGohASACQWBqIgJBH0sNAAsLIAALC45IAQBBgAgLhkgBAAAAAgAAAAMAAAAAAAAAAAAAAAQAAAAFAAAAAAAAAAAAAAAGAAAABwAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEludmFsaWQgY2hhciBpbiB1cmwgcXVlcnkAU3BhbiBjYWxsYmFjayBlcnJvciBpbiBvbl9ib2R5AENvbnRlbnQtTGVuZ3RoIG92ZXJmbG93AENodW5rIHNpemUgb3ZlcmZsb3cAUmVzcG9uc2Ugb3ZlcmZsb3cASW52YWxpZCBtZXRob2QgZm9yIEhUVFAveC54IHJlcXVlc3QASW52YWxpZCBtZXRob2QgZm9yIFJUU1AveC54IHJlcXVlc3QARXhwZWN0ZWQgU09VUkNFIG1ldGhvZCBmb3IgSUNFL3gueCByZXF1ZXN0AEludmFsaWQgY2hhciBpbiB1cmwgZnJhZ21lbnQgc3RhcnQARXhwZWN0ZWQgZG90AFNwYW4gY2FsbGJhY2sgZXJyb3IgaW4gb25fc3RhdHVzAEludmFsaWQgcmVzcG9uc2Ugc3RhdHVzAEludmFsaWQgY2hhcmFjdGVyIGluIGNodW5rIGV4dGVuc2lvbnMAVXNlciBjYWxsYmFjayBlcnJvcgBgb25fcmVzZXRgIGNhbGxiYWNrIGVycm9yAGBvbl9jaHVua19oZWFkZXJgIGNhbGxiYWNrIGVycm9yAGBvbl9tZXNzYWdlX2JlZ2luYCBjYWxsYmFjayBlcnJvcgBgb25fY2h1bmtfZXh0ZW5zaW9uX3ZhbHVlYCBjYWxsYmFjayBlcnJvcgBgb25fc3RhdHVzX2NvbXBsZXRlYCBjYWxsYmFjayBlcnJvcgBgb25fdmVyc2lvbl9jb21wbGV0ZWAgY2FsbGJhY2sgZXJyb3IAYG9uX3VybF9jb21wbGV0ZWAgY2FsbGJhY2sgZXJyb3IAYG9uX2NodW5rX2NvbXBsZXRlYCBjYWxsYmFjayBlcnJvcgBgb25faGVhZGVyX3ZhbHVlX2NvbXBsZXRlYCBjYWxsYmFjayBlcnJvcgBgb25fbWVzc2FnZV9jb21wbGV0ZWAgY2FsbGJhY2sgZXJyb3IAYG9uX21ldGhvZF9jb21wbGV0ZWAgY2FsbGJhY2sgZXJyb3IAYG9uX2hlYWRlcl9maWVsZF9jb21wbGV0ZWAgY2FsbGJhY2sgZXJyb3IAYG9uX2NodW5rX2V4dGVuc2lvbl9uYW1lYCBjYWxsYmFjayBlcnJvcgBVbmV4cGVjdGVkIGNoYXIgaW4gdXJsIHNlcnZlcgBJbnZhbGlkIGhlYWRlciB2YWx1ZSBjaGFyAEludmFsaWQgaGVhZGVyIGZpZWxkIGNoYXIAU3BhbiBjYWxsYmFjayBlcnJvciBpbiBvbl92ZXJzaW9uAEludmFsaWQgbWlub3IgdmVyc2lvbgBJbnZhbGlkIG1ham9yIHZlcnNpb24ARXhwZWN0ZWQgc3BhY2UgYWZ0ZXIgdmVyc2lvbgBFeHBlY3RlZCBDUkxGIGFmdGVyIHZlcnNpb24ASW52YWxpZCBIVFRQIHZlcnNpb24ASW52YWxpZCBoZWFkZXIgdG9rZW4AU3BhbiBjYWxsYmFjayBlcnJvciBpbiBvbl91cmwASW52YWxpZCBjaGFyYWN0ZXJzIGluIHVybABVbmV4cGVjdGVkIHN0YXJ0IGNoYXIgaW4gdXJsAERvdWJsZSBAIGluIHVybABFbXB0eSBDb250ZW50LUxlbmd0aABJbnZhbGlkIGNoYXJhY3RlciBpbiBDb250ZW50LUxlbmd0aABEdXBsaWNhdGUgQ29udGVudC1MZW5ndGgASW52YWxpZCBjaGFyIGluIHVybCBwYXRoAENvbnRlbnQtTGVuZ3RoIGNhbid0IGJlIHByZXNlbnQgd2l0aCBUcmFuc2Zlci1FbmNvZGluZwBJbnZhbGlkIGNoYXJhY3RlciBpbiBjaHVuayBzaXplAFNwYW4gY2FsbGJhY2sgZXJyb3IgaW4gb25faGVhZGVyX3ZhbHVlAFNwYW4gY2FsbGJhY2sgZXJyb3IgaW4gb25fY2h1bmtfZXh0ZW5zaW9uX3ZhbHVlAEludmFsaWQgY2hhcmFjdGVyIGluIGNodW5rIGV4dGVuc2lvbnMgdmFsdWUATWlzc2luZyBleHBlY3RlZCBMRiBhZnRlciBoZWFkZXIgdmFsdWUASW52YWxpZCBgVHJhbnNmZXItRW5jb2RpbmdgIGhlYWRlciB2YWx1ZQBJbnZhbGlkIGNoYXJhY3RlciBpbiBjaHVuayBleHRlbnNpb25zIHF1b3RlIHZhbHVlAEludmFsaWQgY2hhcmFjdGVyIGluIGNodW5rIGV4dGVuc2lvbnMgcXVvdGVkIHZhbHVlAFBhdXNlZCBieSBvbl9oZWFkZXJzX2NvbXBsZXRlAEludmFsaWQgRU9GIHN0YXRlAG9uX3Jlc2V0IHBhdXNlAG9uX2NodW5rX2hlYWRlciBwYXVzZQBvbl9tZXNzYWdlX2JlZ2luIHBhdXNlAG9uX2NodW5rX2V4dGVuc2lvbl92YWx1ZSBwYXVzZQBvbl9zdGF0dXNfY29tcGxldGUgcGF1c2UAb25fdmVyc2lvbl9jb21wbGV0ZSBwYXVzZQBvbl91cmxfY29tcGxldGUgcGF1c2UAb25fY2h1bmtfY29tcGxldGUgcGF1c2UAb25faGVhZGVyX3ZhbHVlX2NvbXBsZXRlIHBhdXNlAG9uX21lc3NhZ2VfY29tcGxldGUgcGF1c2UAb25fbWV0aG9kX2NvbXBsZXRlIHBhdXNlAG9uX2hlYWRlcl9maWVsZF9jb21wbGV0ZSBwYXVzZQBvbl9jaHVua19leHRlbnNpb25fbmFtZSBwYXVzZQBVbmV4cGVjdGVkIHNwYWNlIGFmdGVyIHN0YXJ0IGxpbmUAU3BhbiBjYWxsYmFjayBlcnJvciBpbiBvbl9jaHVua19leHRlbnNpb25fbmFtZQBJbnZhbGlkIGNoYXJhY3RlciBpbiBjaHVuayBleHRlbnNpb25zIG5hbWUAUGF1c2Ugb24gQ09OTkVDVC9VcGdyYWRlAFBhdXNlIG9uIFBSSS9VcGdyYWRlAEV4cGVjdGVkIEhUVFAvMiBDb25uZWN0aW9uIFByZWZhY2UAU3BhbiBjYWxsYmFjayBlcnJvciBpbiBvbl9tZXRob2QARXhwZWN0ZWQgc3BhY2UgYWZ0ZXIgbWV0aG9kAFNwYW4gY2FsbGJhY2sgZXJyb3IgaW4gb25faGVhZGVyX2ZpZWxkAFBhdXNlZABJbnZhbGlkIHdvcmQgZW5jb3VudGVyZWQASW52YWxpZCBtZXRob2QgZW5jb3VudGVyZWQAVW5leHBlY3RlZCBjaGFyIGluIHVybCBzY2hlbWEAUmVxdWVzdCBoYXMgaW52YWxpZCBgVHJhbnNmZXItRW5jb2RpbmdgAFNXSVRDSF9QUk9YWQBVU0VfUFJPWFkATUtBQ1RJVklUWQBVTlBST0NFU1NBQkxFX0VOVElUWQBDT1BZAE1PVkVEX1BFUk1BTkVOVExZAFRPT19FQVJMWQBOT1RJRlkARkFJTEVEX0RFUEVOREVOQ1kAQkFEX0dBVEVXQVkAUExBWQBQVVQAQ0hFQ0tPVVQAR0FURVdBWV9USU1FT1VUAFJFUVVFU1RfVElNRU9VVABORVRXT1JLX0NPTk5FQ1RfVElNRU9VVABDT05ORUNUSU9OX1RJTUVPVVQATE9HSU5fVElNRU9VVABORVRXT1JLX1JFQURfVElNRU9VVABQT1NUAE1JU0RJUkVDVEVEX1JFUVVFU1QAQ0xJRU5UX0NMT1NFRF9SRVFVRVNUAENMSUVOVF9DTE9TRURfTE9BRF9CQUxBTkNFRF9SRVFVRVNUAEJBRF9SRVFVRVNUAEhUVFBfUkVRVUVTVF9TRU5UX1RPX0hUVFBTX1BPUlQAUkVQT1JUAElNX0FfVEVBUE9UAFJFU0VUX0NPTlRFTlQATk9fQ09OVEVOVABQQVJUSUFMX0NPTlRFTlQASFBFX0lOVkFMSURfQ09OU1RBTlQASFBFX0NCX1JFU0VUAEdFVABIUEVfU1RSSUNUAENPTkZMSUNUAFRFTVBPUkFSWV9SRURJUkVDVABQRVJNQU5FTlRfUkVESVJFQ1QAQ09OTkVDVABNVUxUSV9TVEFUVVMASFBFX0lOVkFMSURfU1RBVFVTAFRPT19NQU5ZX1JFUVVFU1RTAEVBUkxZX0hJTlRTAFVOQVZBSUxBQkxFX0ZPUl9MRUdBTF9SRUFTT05TAE9QVElPTlMAU1dJVENISU5HX1BST1RPQ09MUwBWQVJJQU5UX0FMU09fTkVHT1RJQVRFUwBNVUxUSVBMRV9DSE9JQ0VTAElOVEVSTkFMX1NFUlZFUl9FUlJPUgBXRUJfU0VSVkVSX1VOS05PV05fRVJST1IAUkFJTEdVTl9FUlJPUgBJREVOVElUWV9QUk9WSURFUl9BVVRIRU5USUNBVElPTl9FUlJPUgBTU0xfQ0VSVElGSUNBVEVfRVJST1IASU5WQUxJRF9YX0ZPUldBUkRFRF9GT1IAU0VUX1BBUkFNRVRFUgBHRVRfUEFSQU1FVEVSAEhQRV9VU0VSAFNFRV9PVEhFUgBIUEVfQ0JfQ0hVTktfSEVBREVSAE1LQ0FMRU5EQVIAU0VUVVAAV0VCX1NFUlZFUl9JU19ET1dOAFRFQVJET1dOAEhQRV9DTE9TRURfQ09OTkVDVElPTgBIRVVSSVNUSUNfRVhQSVJBVElPTgBESVNDT05ORUNURURfT1BFUkFUSU9OAE5PTl9BVVRIT1JJVEFUSVZFX0lORk9STUFUSU9OAEhQRV9JTlZBTElEX1ZFUlNJT04ASFBFX0NCX01FU1NBR0VfQkVHSU4AU0lURV9JU19GUk9aRU4ASFBFX0lOVkFMSURfSEVBREVSX1RPS0VOAElOVkFMSURfVE9LRU4ARk9SQklEREVOAEVOSEFOQ0VfWU9VUl9DQUxNAEhQRV9JTlZBTElEX1VSTABCTE9DS0VEX0JZX1BBUkVOVEFMX0NPTlRST0wATUtDT0wAQUNMAEhQRV9JTlRFUk5BTABSRVFVRVNUX0hFQURFUl9GSUVMRFNfVE9PX0xBUkdFX1VOT0ZGSUNJQUwASFBFX09LAFVOTElOSwBVTkxPQ0sAUFJJAFJFVFJZX1dJVEgASFBFX0lOVkFMSURfQ09OVEVOVF9MRU5HVEgASFBFX1VORVhQRUNURURfQ09OVEVOVF9MRU5HVEgARkxVU0gAUFJPUFBBVENIAE0tU0VBUkNIAFVSSV9UT09fTE9ORwBQUk9DRVNTSU5HAE1JU0NFTExBTkVPVVNfUEVSU0lTVEVOVF9XQVJOSU5HAE1JU0NFTExBTkVPVVNfV0FSTklORwBIUEVfSU5WQUxJRF9UUkFOU0ZFUl9FTkNPRElORwBFeHBlY3RlZCBDUkxGAEhQRV9JTlZBTElEX0NIVU5LX1NJWkUATU9WRQBDT05USU5VRQBIUEVfQ0JfU1RBVFVTX0NPTVBMRVRFAEhQRV9DQl9IRUFERVJTX0NPTVBMRVRFAEhQRV9DQl9WRVJTSU9OX0NPTVBMRVRFAEhQRV9DQl9VUkxfQ09NUExFVEUASFBFX0NCX0NIVU5LX0NPTVBMRVRFAEhQRV9DQl9IRUFERVJfVkFMVUVfQ09NUExFVEUASFBFX0NCX0NIVU5LX0VYVEVOU0lPTl9WQUxVRV9DT01QTEVURQBIUEVfQ0JfQ0hVTktfRVhURU5TSU9OX05BTUVfQ09NUExFVEUASFBFX0NCX01FU1NBR0VfQ09NUExFVEUASFBFX0NCX01FVEhPRF9DT01QTEVURQBIUEVfQ0JfSEVBREVSX0ZJRUxEX0NPTVBMRVRFAERFTEVURQBIUEVfSU5WQUxJRF9FT0ZfU1RBVEUASU5WQUxJRF9TU0xfQ0VSVElGSUNBVEUAUEFVU0UATk9fUkVTUE9OU0UAVU5TVVBQT1JURURfTUVESUFfVFlQRQBHT05FAE5PVF9BQ0NFUFRBQkxFAFNFUlZJQ0VfVU5BVkFJTEFCTEUAUkFOR0VfTk9UX1NBVElTRklBQkxFAE9SSUdJTl9JU19VTlJFQUNIQUJMRQBSRVNQT05TRV9JU19TVEFMRQBQVVJHRQBNRVJHRQBSRVFVRVNUX0hFQURFUl9GSUVMRFNfVE9PX0xBUkdFAFJFUVVFU1RfSEVBREVSX1RPT19MQVJHRQBQQVlMT0FEX1RPT19MQVJHRQBJTlNVRkZJQ0lFTlRfU1RPUkFHRQBIUEVfUEFVU0VEX1VQR1JBREUASFBFX1BBVVNFRF9IMl9VUEdSQURFAFNPVVJDRQBBTk5PVU5DRQBUUkFDRQBIUEVfVU5FWFBFQ1RFRF9TUEFDRQBERVNDUklCRQBVTlNVQlNDUklCRQBSRUNPUkQASFBFX0lOVkFMSURfTUVUSE9EAE5PVF9GT1VORABQUk9QRklORABVTkJJTkQAUkVCSU5EAFVOQVVUSE9SSVpFRABNRVRIT0RfTk9UX0FMTE9XRUQASFRUUF9WRVJTSU9OX05PVF9TVVBQT1JURUQAQUxSRUFEWV9SRVBPUlRFRABBQ0NFUFRFRABOT1RfSU1QTEVNRU5URUQATE9PUF9ERVRFQ1RFRABIUEVfQ1JfRVhQRUNURUQASFBFX0xGX0VYUEVDVEVEAENSRUFURUQASU1fVVNFRABIUEVfUEFVU0VEAFRJTUVPVVRfT0NDVVJFRABQQVlNRU5UX1JFUVVJUkVEAFBSRUNPTkRJVElPTl9SRVFVSVJFRABQUk9YWV9BVVRIRU5USUNBVElPTl9SRVFVSVJFRABORVRXT1JLX0FVVEhFTlRJQ0FUSU9OX1JFUVVJUkVEAExFTkdUSF9SRVFVSVJFRABTU0xfQ0VSVElGSUNBVEVfUkVRVUlSRUQAVVBHUkFERV9SRVFVSVJFRABQQUdFX0VYUElSRUQAUFJFQ09ORElUSU9OX0ZBSUxFRABFWFBFQ1RBVElPTl9GQUlMRUQAUkVWQUxJREFUSU9OX0ZBSUxFRABTU0xfSEFORFNIQUtFX0ZBSUxFRABMT0NLRUQAVFJBTlNGT1JNQVRJT05fQVBQTElFRABOT1RfTU9ESUZJRUQATk9UX0VYVEVOREVEAEJBTkRXSURUSF9MSU1JVF9FWENFRURFRABTSVRFX0lTX09WRVJMT0FERUQASEVBRABFeHBlY3RlZCBIVFRQLwAAXhMAACYTAAAwEAAA8BcAAJ0TAAAVEgAAORcAAPASAAAKEAAAdRIAAK0SAACCEwAATxQAAH8QAACgFQAAIxQAAIkSAACLFAAATRUAANQRAADPFAAAEBgAAMkWAADcFgAAwREAAOAXAAC7FAAAdBQAAHwVAADlFAAACBcAAB8QAABlFQAAoxQAACgVAAACFQAAmRUAACwQAACLGQAATw8AANQOAABqEAAAzhAAAAIXAACJDgAAbhMAABwTAABmFAAAVhcAAMETAADNEwAAbBMAAGgXAABmFwAAXxcAACITAADODwAAaQ4AANgOAABjFgAAyxMAAKoOAAAoFwAAJhcAAMUTAABdFgAA6BEAAGcTAABlEwAA8hYAAHMTAAAdFwAA+RYAAPMRAADPDgAAzhUAAAwSAACzEQAApREAAGEQAAAyFwAAuxMAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQIBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAIDAgICAgIAAAICAAICAAICAgICAgICAgIABAAAAAAAAgICAgICAgICAgICAgICAgICAgICAgICAgIAAAACAgICAgICAgICAgICAgICAgICAgICAgICAgICAgACAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAACAAICAgICAAACAgACAgACAgICAgICAgICAAMABAAAAAICAgICAgICAgICAgICAgICAgICAgICAgICAAAAAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAAgACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAbG9zZWVlcC1hbGl2ZQAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQIBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBY2h1bmtlZAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEAAQEBAQEAAAEBAAEBAAEBAQEBAQEBAQEAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQABAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABlY3Rpb25lbnQtbGVuZ3Rob25yb3h5LWNvbm5lY3Rpb24AAAAAAAAAAAAAAAAAAAByYW5zZmVyLWVuY29kaW5ncGdyYWRlDQoNCg0KU00NCg0KVFRQL0NFL1RTUC8AAAAAAAAAAAAAAAABAgABAwAAAAAAAAAAAAAAAAAAAAAAAAQBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAQIAAQMAAAAAAAAAAAAAAAAAAAAAAAAEAQEFAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAEAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAQAAAgAAAAAAAAAAAAAAAAAAAAAAAAMEAAAEBAQEBAQEBAQEBAUEBAQEBAQEBAQEBAQABAAGBwQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEAAQABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAEAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAADAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwAAAAAAAAMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAABAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAIAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMAAAAAAAADAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABOT1VOQ0VFQ0tPVVRORUNURVRFQ1JJQkVMVVNIRVRFQURTRUFSQ0hSR0VDVElWSVRZTEVOREFSVkVPVElGWVBUSU9OU0NIU0VBWVNUQVRDSEdFT1JESVJFQ1RPUlRSQ0hQQVJBTUVURVJVUkNFQlNDUklCRUFSRE9XTkFDRUlORE5LQ0tVQlNDUklCRUhUVFAvQURUUC8=' + + +/***/ }), + +/***/ 3434: +/***/ ((module) => { + +module.exports = 'AGFzbQEAAAABMAhgAX8Bf2ADf39/AX9gBH9/f38Bf2AAAGADf39/AGABfwBgAn9/AGAGf39/f39/AALLAQgDZW52GHdhc21fb25faGVhZGVyc19jb21wbGV0ZQACA2VudhV3YXNtX29uX21lc3NhZ2VfYmVnaW4AAANlbnYLd2FzbV9vbl91cmwAAQNlbnYOd2FzbV9vbl9zdGF0dXMAAQNlbnYUd2FzbV9vbl9oZWFkZXJfZmllbGQAAQNlbnYUd2FzbV9vbl9oZWFkZXJfdmFsdWUAAQNlbnYMd2FzbV9vbl9ib2R5AAEDZW52GHdhc21fb25fbWVzc2FnZV9jb21wbGV0ZQAAA0ZFAwMEAAAFAAAAAAAABQEFAAUFBQAABgAAAAAGBgYGAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQABAAABAQcAAAUFAwABBAUBcAESEgUDAQACBggBfwFBgNQECwfRBSIGbWVtb3J5AgALX2luaXRpYWxpemUACRlfX2luZGlyZWN0X2Z1bmN0aW9uX3RhYmxlAQALbGxodHRwX2luaXQAChhsbGh0dHBfc2hvdWxkX2tlZXBfYWxpdmUAQQxsbGh0dHBfYWxsb2MADAZtYWxsb2MARgtsbGh0dHBfZnJlZQANBGZyZWUASA9sbGh0dHBfZ2V0X3R5cGUADhVsbGh0dHBfZ2V0X2h0dHBfbWFqb3IADxVsbGh0dHBfZ2V0X2h0dHBfbWlub3IAEBFsbGh0dHBfZ2V0X21ldGhvZAARFmxsaHR0cF9nZXRfc3RhdHVzX2NvZGUAEhJsbGh0dHBfZ2V0X3VwZ3JhZGUAEwxsbGh0dHBfcmVzZXQAFA5sbGh0dHBfZXhlY3V0ZQAVFGxsaHR0cF9zZXR0aW5nc19pbml0ABYNbGxodHRwX2ZpbmlzaAAXDGxsaHR0cF9wYXVzZQAYDWxsaHR0cF9yZXN1bWUAGRtsbGh0dHBfcmVzdW1lX2FmdGVyX3VwZ3JhZGUAGhBsbGh0dHBfZ2V0X2Vycm5vABsXbGxodHRwX2dldF9lcnJvcl9yZWFzb24AHBdsbGh0dHBfc2V0X2Vycm9yX3JlYXNvbgAdFGxsaHR0cF9nZXRfZXJyb3JfcG9zAB4RbGxodHRwX2Vycm5vX25hbWUAHxJsbGh0dHBfbWV0aG9kX25hbWUAIBJsbGh0dHBfc3RhdHVzX25hbWUAIRpsbGh0dHBfc2V0X2xlbmllbnRfaGVhZGVycwAiIWxsaHR0cF9zZXRfbGVuaWVudF9jaHVua2VkX2xlbmd0aAAjHWxsaHR0cF9zZXRfbGVuaWVudF9rZWVwX2FsaXZlACQkbGxodHRwX3NldF9sZW5pZW50X3RyYW5zZmVyX2VuY29kaW5nACUYbGxodHRwX21lc3NhZ2VfbmVlZHNfZW9mAD8JFwEAQQELEQECAwQFCwYHNTk3MS8tJyspCrLgAkUCAAsIABCIgICAAAsZACAAEMKAgIAAGiAAIAI2AjggACABOgAoCxwAIAAgAC8BMiAALQAuIAAQwYCAgAAQgICAgAALKgEBf0HAABDGgICAACIBEMKAgIAAGiABQYCIgIAANgI4IAEgADoAKCABCwoAIAAQyICAgAALBwAgAC0AKAsHACAALQAqCwcAIAAtACsLBwAgAC0AKQsHACAALwEyCwcAIAAtAC4LRQEEfyAAKAIYIQEgAC0ALSECIAAtACghAyAAKAI4IQQgABDCgICAABogACAENgI4IAAgAzoAKCAAIAI6AC0gACABNgIYCxEAIAAgASABIAJqEMOAgIAACxAAIABBAEHcABDMgICAABoLZwEBf0EAIQECQCAAKAIMDQACQAJAAkACQCAALQAvDgMBAAMCCyAAKAI4IgFFDQAgASgCLCIBRQ0AIAAgARGAgICAAAAiAQ0DC0EADwsQyoCAgAAACyAAQcOWgIAANgIQQQ4hAQsgAQseAAJAIAAoAgwNACAAQdGbgIAANgIQIABBFTYCDAsLFgACQCAAKAIMQRVHDQAgAEEANgIMCwsWAAJAIAAoAgxBFkcNACAAQQA2AgwLCwcAIAAoAgwLBwAgACgCEAsJACAAIAE2AhALBwAgACgCFAsiAAJAIABBJEkNABDKgICAAAALIABBAnRBoLOAgABqKAIACyIAAkAgAEEuSQ0AEMqAgIAAAAsgAEECdEGwtICAAGooAgAL7gsBAX9B66iAgAAhAQJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAIABBnH9qDvQDY2IAAWFhYWFhYQIDBAVhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhBgcICQoLDA0OD2FhYWFhEGFhYWFhYWFhYWFhEWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYRITFBUWFxgZGhthYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhHB0eHyAhIiMkJSYnKCkqKywtLi8wMTIzNDU2YTc4OTphYWFhYWFhYTthYWE8YWFhYT0+P2FhYWFhYWFhQGFhQWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYUJDREVGR0hJSktMTU5PUFFSU2FhYWFhYWFhVFVWV1hZWlthXF1hYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFeYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhX2BhC0Hhp4CAAA8LQaShgIAADwtBy6yAgAAPC0H+sYCAAA8LQcCkgIAADwtBq6SAgAAPC0GNqICAAA8LQeKmgIAADwtBgLCAgAAPC0G5r4CAAA8LQdekgIAADwtB75+AgAAPC0Hhn4CAAA8LQfqfgIAADwtB8qCAgAAPC0Gor4CAAA8LQa6ygIAADwtBiLCAgAAPC0Hsp4CAAA8LQYKigIAADwtBjp2AgAAPC0HQroCAAA8LQcqjgIAADwtBxbKAgAAPC0HfnICAAA8LQdKcgIAADwtBxKCAgAAPC0HXoICAAA8LQaKfgIAADwtB7a6AgAAPC0GrsICAAA8LQdSlgIAADwtBzK6AgAAPC0H6roCAAA8LQfyrgIAADwtB0rCAgAAPC0HxnYCAAA8LQbuggIAADwtB96uAgAAPC0GQsYCAAA8LQdexgIAADwtBoq2AgAAPC0HUp4CAAA8LQeCrgIAADwtBn6yAgAAPC0HrsYCAAA8LQdWfgIAADwtByrGAgAAPC0HepYCAAA8LQdSegIAADwtB9JyAgAAPC0GnsoCAAA8LQbGdgIAADwtBoJ2AgAAPC0G5sYCAAA8LQbywgIAADwtBkqGAgAAPC0GzpoCAAA8LQemsgIAADwtBrJ6AgAAPC0HUq4CAAA8LQfemgIAADwtBgKaAgAAPC0GwoYCAAA8LQf6egIAADwtBjaOAgAAPC0GJrYCAAA8LQfeigIAADwtBoLGAgAAPC0Gun4CAAA8LQcalgIAADwtB6J6AgAAPC0GTooCAAA8LQcKvgIAADwtBw52AgAAPC0GLrICAAA8LQeGdgIAADwtBja+AgAAPC0HqoYCAAA8LQbStgIAADwtB0q+AgAAPC0HfsoCAAA8LQdKygIAADwtB8LCAgAAPC0GpooCAAA8LQfmjgIAADwtBmZ6AgAAPC0G1rICAAA8LQZuwgIAADwtBkrKAgAAPC0G2q4CAAA8LQcKigIAADwtB+LKAgAAPC0GepYCAAA8LQdCigIAADwtBup6AgAAPC0GBnoCAAA8LEMqAgIAAAAtB1qGAgAAhAQsgAQsWACAAIAAtAC1B/gFxIAFBAEdyOgAtCxkAIAAgAC0ALUH9AXEgAUEAR0EBdHI6AC0LGQAgACAALQAtQfsBcSABQQBHQQJ0cjoALQsZACAAIAAtAC1B9wFxIAFBAEdBA3RyOgAtCy4BAn9BACEDAkAgACgCOCIERQ0AIAQoAgAiBEUNACAAIAQRgICAgAAAIQMLIAMLSQECf0EAIQMCQCAAKAI4IgRFDQAgBCgCBCIERQ0AIAAgASACIAFrIAQRgYCAgAAAIgNBf0cNACAAQcaRgIAANgIQQRghAwsgAwsuAQJ/QQAhAwJAIAAoAjgiBEUNACAEKAIwIgRFDQAgACAEEYCAgIAAACEDCyADC0kBAn9BACEDAkAgACgCOCIERQ0AIAQoAggiBEUNACAAIAEgAiABayAEEYGAgIAAACIDQX9HDQAgAEH2ioCAADYCEEEYIQMLIAMLLgECf0EAIQMCQCAAKAI4IgRFDQAgBCgCNCIERQ0AIAAgBBGAgICAAAAhAwsgAwtJAQJ/QQAhAwJAIAAoAjgiBEUNACAEKAIMIgRFDQAgACABIAIgAWsgBBGBgICAAAAiA0F/Rw0AIABB7ZqAgAA2AhBBGCEDCyADCy4BAn9BACEDAkAgACgCOCIERQ0AIAQoAjgiBEUNACAAIAQRgICAgAAAIQMLIAMLSQECf0EAIQMCQCAAKAI4IgRFDQAgBCgCECIERQ0AIAAgASACIAFrIAQRgYCAgAAAIgNBf0cNACAAQZWQgIAANgIQQRghAwsgAwsuAQJ/QQAhAwJAIAAoAjgiBEUNACAEKAI8IgRFDQAgACAEEYCAgIAAACEDCyADC0kBAn9BACEDAkAgACgCOCIERQ0AIAQoAhQiBEUNACAAIAEgAiABayAEEYGAgIAAACIDQX9HDQAgAEGqm4CAADYCEEEYIQMLIAMLLgECf0EAIQMCQCAAKAI4IgRFDQAgBCgCQCIERQ0AIAAgBBGAgICAAAAhAwsgAwtJAQJ/QQAhAwJAIAAoAjgiBEUNACAEKAIYIgRFDQAgACABIAIgAWsgBBGBgICAAAAiA0F/Rw0AIABB7ZOAgAA2AhBBGCEDCyADCy4BAn9BACEDAkAgACgCOCIERQ0AIAQoAkQiBEUNACAAIAQRgICAgAAAIQMLIAMLLgECf0EAIQMCQCAAKAI4IgRFDQAgBCgCJCIERQ0AIAAgBBGAgICAAAAhAwsgAwsuAQJ/QQAhAwJAIAAoAjgiBEUNACAEKAIsIgRFDQAgACAEEYCAgIAAACEDCyADC0kBAn9BACEDAkAgACgCOCIERQ0AIAQoAigiBEUNACAAIAEgAiABayAEEYGAgIAAACIDQX9HDQAgAEH2iICAADYCEEEYIQMLIAMLLgECf0EAIQMCQCAAKAI4IgRFDQAgBCgCUCIERQ0AIAAgBBGAgICAAAAhAwsgAwtJAQJ/QQAhAwJAIAAoAjgiBEUNACAEKAIcIgRFDQAgACABIAIgAWsgBBGBgICAAAAiA0F/Rw0AIABBwpmAgAA2AhBBGCEDCyADCy4BAn9BACEDAkAgACgCOCIERQ0AIAQoAkgiBEUNACAAIAQRgICAgAAAIQMLIAMLSQECf0EAIQMCQCAAKAI4IgRFDQAgBCgCICIERQ0AIAAgASACIAFrIAQRgYCAgAAAIgNBf0cNACAAQZSUgIAANgIQQRghAwsgAwsuAQJ/QQAhAwJAIAAoAjgiBEUNACAEKAJMIgRFDQAgACAEEYCAgIAAACEDCyADCy4BAn9BACEDAkAgACgCOCIERQ0AIAQoAlQiBEUNACAAIAQRgICAgAAAIQMLIAMLLgECf0EAIQMCQCAAKAI4IgRFDQAgBCgCWCIERQ0AIAAgBBGAgICAAAAhAwsgAwtFAQF/AkACQCAALwEwQRRxQRRHDQBBASEDIAAtAChBAUYNASAALwEyQeUARiEDDAELIAAtAClBBUYhAwsgACADOgAuQQAL/gEBA39BASEDAkAgAC8BMCIEQQhxDQAgACkDIEIAUiEDCwJAAkAgAC0ALkUNAEEBIQUgAC0AKUEFRg0BQQEhBSAEQcAAcUUgA3FBAUcNAQtBACEFIARBwABxDQBBAiEFIARB//8DcSIDQQhxDQACQCADQYAEcUUNAAJAIAAtAChBAUcNACAALQAtQQpxDQBBBQ8LQQQPCwJAIANBIHENAAJAIAAtAChBAUYNACAALwEyQf//A3EiAEGcf2pB5ABJDQAgAEHMAUYNACAAQbACRg0AQQQhBSAEQShxRQ0CIANBiARxQYAERg0CC0EADwtBAEEDIAApAyBQGyEFCyAFC2IBAn9BACEBAkAgAC0AKEEBRg0AIAAvATJB//8DcSICQZx/akHkAEkNACACQcwBRg0AIAJBsAJGDQAgAC8BMCIAQcAAcQ0AQQEhASAAQYgEcUGABEYNACAAQShxRSEBCyABC6cBAQN/AkACQAJAIAAtACpFDQAgAC0AK0UNAEEAIQMgAC8BMCIEQQJxRQ0BDAILQQAhAyAALwEwIgRBAXFFDQELQQEhAyAALQAoQQFGDQAgAC8BMkH//wNxIgVBnH9qQeQASQ0AIAVBzAFGDQAgBUGwAkYNACAEQcAAcQ0AQQAhAyAEQYgEcUGABEYNACAEQShxQQBHIQMLIABBADsBMCAAQQA6AC8gAwuZAQECfwJAAkACQCAALQAqRQ0AIAAtACtFDQBBACEBIAAvATAiAkECcUUNAQwCC0EAIQEgAC8BMCICQQFxRQ0BC0EBIQEgAC0AKEEBRg0AIAAvATJB//8DcSIAQZx/akHkAEkNACAAQcwBRg0AIABBsAJGDQAgAkHAAHENAEEAIQEgAkGIBHFBgARGDQAgAkEocUEARyEBCyABC0kBAXsgAEEQav0MAAAAAAAAAAAAAAAAAAAAACIB/QsDACAAIAH9CwMAIABBMGogAf0LAwAgAEEgaiAB/QsDACAAQd0BNgIcQQALewEBfwJAIAAoAgwiAw0AAkAgACgCBEUNACAAIAE2AgQLAkAgACABIAIQxICAgAAiAw0AIAAoAgwPCyAAIAM2AhxBACEDIAAoAgQiAUUNACAAIAEgAiAAKAIIEYGAgIAAACIBRQ0AIAAgAjYCFCAAIAE2AgwgASEDCyADC+TzAQMOfwN+BH8jgICAgABBEGsiAySAgICAACABIQQgASEFIAEhBiABIQcgASEIIAEhCSABIQogASELIAEhDCABIQ0gASEOIAEhDwJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQCAAKAIcIhBBf2oO3QHaAQHZAQIDBAUGBwgJCgsMDQ7YAQ8Q1wEREtYBExQVFhcYGRob4AHfARwdHtUBHyAhIiMkJdQBJicoKSorLNMB0gEtLtEB0AEvMDEyMzQ1Njc4OTo7PD0+P0BBQkNERUbbAUdISUrPAc4BS80BTMwBTU5PUFFSU1RVVldYWVpbXF1eX2BhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ent8fX5/gAGBAYIBgwGEAYUBhgGHAYgBiQGKAYsBjAGNAY4BjwGQAZEBkgGTAZQBlQGWAZcBmAGZAZoBmwGcAZ0BngGfAaABoQGiAaMBpAGlAaYBpwGoAakBqgGrAawBrQGuAa8BsAGxAbIBswG0AbUBtgG3AcsBygG4AckBuQHIAboBuwG8Ab0BvgG/AcABwQHCAcMBxAHFAcYBANwBC0EAIRAMxgELQQ4hEAzFAQtBDSEQDMQBC0EPIRAMwwELQRAhEAzCAQtBEyEQDMEBC0EUIRAMwAELQRUhEAy/AQtBFiEQDL4BC0EXIRAMvQELQRghEAy8AQtBGSEQDLsBC0EaIRAMugELQRshEAy5AQtBHCEQDLgBC0EIIRAMtwELQR0hEAy2AQtBICEQDLUBC0EfIRAMtAELQQchEAyzAQtBISEQDLIBC0EiIRAMsQELQR4hEAywAQtBIyEQDK8BC0ESIRAMrgELQREhEAytAQtBJCEQDKwBC0ElIRAMqwELQSYhEAyqAQtBJyEQDKkBC0HDASEQDKgBC0EpIRAMpwELQSshEAymAQtBLCEQDKUBC0EtIRAMpAELQS4hEAyjAQtBLyEQDKIBC0HEASEQDKEBC0EwIRAMoAELQTQhEAyfAQtBDCEQDJ4BC0ExIRAMnQELQTIhEAycAQtBMyEQDJsBC0E5IRAMmgELQTUhEAyZAQtBxQEhEAyYAQtBCyEQDJcBC0E6IRAMlgELQTYhEAyVAQtBCiEQDJQBC0E3IRAMkwELQTghEAySAQtBPCEQDJEBC0E7IRAMkAELQT0hEAyPAQtBCSEQDI4BC0EoIRAMjQELQT4hEAyMAQtBPyEQDIsBC0HAACEQDIoBC0HBACEQDIkBC0HCACEQDIgBC0HDACEQDIcBC0HEACEQDIYBC0HFACEQDIUBC0HGACEQDIQBC0EqIRAMgwELQccAIRAMggELQcgAIRAMgQELQckAIRAMgAELQcoAIRAMfwtBywAhEAx+C0HNACEQDH0LQcwAIRAMfAtBzgAhEAx7C0HPACEQDHoLQdAAIRAMeQtB0QAhEAx4C0HSACEQDHcLQdMAIRAMdgtB1AAhEAx1C0HWACEQDHQLQdUAIRAMcwtBBiEQDHILQdcAIRAMcQtBBSEQDHALQdgAIRAMbwtBBCEQDG4LQdkAIRAMbQtB2gAhEAxsC0HbACEQDGsLQdwAIRAMagtBAyEQDGkLQd0AIRAMaAtB3gAhEAxnC0HfACEQDGYLQeEAIRAMZQtB4AAhEAxkC0HiACEQDGMLQeMAIRAMYgtBAiEQDGELQeQAIRAMYAtB5QAhEAxfC0HmACEQDF4LQecAIRAMXQtB6AAhEAxcC0HpACEQDFsLQeoAIRAMWgtB6wAhEAxZC0HsACEQDFgLQe0AIRAMVwtB7gAhEAxWC0HvACEQDFULQfAAIRAMVAtB8QAhEAxTC0HyACEQDFILQfMAIRAMUQtB9AAhEAxQC0H1ACEQDE8LQfYAIRAMTgtB9wAhEAxNC0H4ACEQDEwLQfkAIRAMSwtB+gAhEAxKC0H7ACEQDEkLQfwAIRAMSAtB/QAhEAxHC0H+ACEQDEYLQf8AIRAMRQtBgAEhEAxEC0GBASEQDEMLQYIBIRAMQgtBgwEhEAxBC0GEASEQDEALQYUBIRAMPwtBhgEhEAw+C0GHASEQDD0LQYgBIRAMPAtBiQEhEAw7C0GKASEQDDoLQYsBIRAMOQtBjAEhEAw4C0GNASEQDDcLQY4BIRAMNgtBjwEhEAw1C0GQASEQDDQLQZEBIRAMMwtBkgEhEAwyC0GTASEQDDELQZQBIRAMMAtBlQEhEAwvC0GWASEQDC4LQZcBIRAMLQtBmAEhEAwsC0GZASEQDCsLQZoBIRAMKgtBmwEhEAwpC0GcASEQDCgLQZ0BIRAMJwtBngEhEAwmC0GfASEQDCULQaABIRAMJAtBoQEhEAwjC0GiASEQDCILQaMBIRAMIQtBpAEhEAwgC0GlASEQDB8LQaYBIRAMHgtBpwEhEAwdC0GoASEQDBwLQakBIRAMGwtBqgEhEAwaC0GrASEQDBkLQawBIRAMGAtBrQEhEAwXC0GuASEQDBYLQQEhEAwVC0GvASEQDBQLQbABIRAMEwtBsQEhEAwSC0GzASEQDBELQbIBIRAMEAtBtAEhEAwPC0G1ASEQDA4LQbYBIRAMDQtBtwEhEAwMC0G4ASEQDAsLQbkBIRAMCgtBugEhEAwJC0G7ASEQDAgLQcYBIRAMBwtBvAEhEAwGC0G9ASEQDAULQb4BIRAMBAtBvwEhEAwDC0HAASEQDAILQcIBIRAMAQtBwQEhEAsDQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAIBAOxwEAAQIDBAUGBwgJCgsMDQ4PEBESExQVFhcYGRobHB4fICEjJSg/QEFERUZHSElKS0xNT1BRUlPeA1dZW1xdYGJlZmdoaWprbG1vcHFyc3R1dnd4eXp7fH1+gAGCAYUBhgGHAYkBiwGMAY0BjgGPAZABkQGUAZUBlgGXAZgBmQGaAZsBnAGdAZ4BnwGgAaEBogGjAaQBpQGmAacBqAGpAaoBqwGsAa0BrgGvAbABsQGyAbMBtAG1AbYBtwG4AbkBugG7AbwBvQG+Ab8BwAHBAcIBwwHEAcUBxgHHAcgByQHKAcsBzAHNAc4BzwHQAdEB0gHTAdQB1QHWAdcB2AHZAdoB2wHcAd0B3gHgAeEB4gHjAeQB5QHmAecB6AHpAeoB6wHsAe0B7gHvAfAB8QHyAfMBmQKkArAC/gL+AgsgASIEIAJHDfMBQd0BIRAM/wMLIAEiECACRw3dAUHDASEQDP4DCyABIgEgAkcNkAFB9wAhEAz9AwsgASIBIAJHDYYBQe8AIRAM/AMLIAEiASACRw1/QeoAIRAM+wMLIAEiASACRw17QegAIRAM+gMLIAEiASACRw14QeYAIRAM+QMLIAEiASACRw0aQRghEAz4AwsgASIBIAJHDRRBEiEQDPcDCyABIgEgAkcNWUHFACEQDPYDCyABIgEgAkcNSkE/IRAM9QMLIAEiASACRw1IQTwhEAz0AwsgASIBIAJHDUFBMSEQDPMDCyAALQAuQQFGDesDDIcCCyAAIAEiASACEMCAgIAAQQFHDeYBIABCADcDIAznAQsgACABIgEgAhC0gICAACIQDecBIAEhAQz1AgsCQCABIgEgAkcNAEEGIRAM8AMLIAAgAUEBaiIBIAIQu4CAgAAiEA3oASABIQEMMQsgAEIANwMgQRIhEAzVAwsgASIQIAJHDStBHSEQDO0DCwJAIAEiASACRg0AIAFBAWohAUEQIRAM1AMLQQchEAzsAwsgAEIAIAApAyAiESACIAEiEGutIhJ9IhMgEyARVhs3AyAgESASViIURQ3lAUEIIRAM6wMLAkAgASIBIAJGDQAgAEGJgICAADYCCCAAIAE2AgQgASEBQRQhEAzSAwtBCSEQDOoDCyABIQEgACkDIFAN5AEgASEBDPICCwJAIAEiASACRw0AQQshEAzpAwsgACABQQFqIgEgAhC2gICAACIQDeUBIAEhAQzyAgsgACABIgEgAhC4gICAACIQDeUBIAEhAQzyAgsgACABIgEgAhC4gICAACIQDeYBIAEhAQwNCyAAIAEiASACELqAgIAAIhAN5wEgASEBDPACCwJAIAEiASACRw0AQQ8hEAzlAwsgAS0AACIQQTtGDQggEEENRw3oASABQQFqIQEM7wILIAAgASIBIAIQuoCAgAAiEA3oASABIQEM8gILA0ACQCABLQAAQfC1gIAAai0AACIQQQFGDQAgEEECRw3rASAAKAIEIRAgAEEANgIEIAAgECABQQFqIgEQuYCAgAAiEA3qASABIQEM9AILIAFBAWoiASACRw0AC0ESIRAM4gMLIAAgASIBIAIQuoCAgAAiEA3pASABIQEMCgsgASIBIAJHDQZBGyEQDOADCwJAIAEiASACRw0AQRYhEAzgAwsgAEGKgICAADYCCCAAIAE2AgQgACABIAIQuICAgAAiEA3qASABIQFBICEQDMYDCwJAIAEiASACRg0AA0ACQCABLQAAQfC3gIAAai0AACIQQQJGDQACQCAQQX9qDgTlAewBAOsB7AELIAFBAWohAUEIIRAMyAMLIAFBAWoiASACRw0AC0EVIRAM3wMLQRUhEAzeAwsDQAJAIAEtAABB8LmAgABqLQAAIhBBAkYNACAQQX9qDgTeAewB4AHrAewBCyABQQFqIgEgAkcNAAtBGCEQDN0DCwJAIAEiASACRg0AIABBi4CAgAA2AgggACABNgIEIAEhAUEHIRAMxAMLQRkhEAzcAwsgAUEBaiEBDAILAkAgASIUIAJHDQBBGiEQDNsDCyAUIQECQCAULQAAQXNqDhTdAu4C7gLuAu4C7gLuAu4C7gLuAu4C7gLuAu4C7gLuAu4C7gLuAgDuAgtBACEQIABBADYCHCAAQa+LgIAANgIQIABBAjYCDCAAIBRBAWo2AhQM2gMLAkAgAS0AACIQQTtGDQAgEEENRw3oASABQQFqIQEM5QILIAFBAWohAQtBIiEQDL8DCwJAIAEiECACRw0AQRwhEAzYAwtCACERIBAhASAQLQAAQVBqDjfnAeYBAQIDBAUGBwgAAAAAAAAACQoLDA0OAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPEBESExQAC0EeIRAMvQMLQgIhEQzlAQtCAyERDOQBC0IEIREM4wELQgUhEQziAQtCBiERDOEBC0IHIREM4AELQgghEQzfAQtCCSERDN4BC0IKIREM3QELQgshEQzcAQtCDCERDNsBC0INIREM2gELQg4hEQzZAQtCDyERDNgBC0IKIREM1wELQgshEQzWAQtCDCERDNUBC0INIREM1AELQg4hEQzTAQtCDyERDNIBC0IAIRECQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAIBAtAABBUGoON+UB5AEAAQIDBAUGB+YB5gHmAeYB5gHmAeYBCAkKCwwN5gHmAeYB5gHmAeYB5gHmAeYB5gHmAeYB5gHmAeYB5gHmAeYB5gHmAeYB5gHmAeYB5gHmAQ4PEBESE+YBC0ICIREM5AELQgMhEQzjAQtCBCERDOIBC0IFIREM4QELQgYhEQzgAQtCByERDN8BC0IIIREM3gELQgkhEQzdAQtCCiERDNwBC0ILIREM2wELQgwhEQzaAQtCDSERDNkBC0IOIREM2AELQg8hEQzXAQtCCiERDNYBC0ILIREM1QELQgwhEQzUAQtCDSERDNMBC0IOIREM0gELQg8hEQzRAQsgAEIAIAApAyAiESACIAEiEGutIhJ9IhMgEyARVhs3AyAgESASViIURQ3SAUEfIRAMwAMLAkAgASIBIAJGDQAgAEGJgICAADYCCCAAIAE2AgQgASEBQSQhEAynAwtBICEQDL8DCyAAIAEiECACEL6AgIAAQX9qDgW2AQDFAgHRAdIBC0ERIRAMpAMLIABBAToALyAQIQEMuwMLIAEiASACRw3SAUEkIRAMuwMLIAEiDSACRw0eQcYAIRAMugMLIAAgASIBIAIQsoCAgAAiEA3UASABIQEMtQELIAEiECACRw0mQdAAIRAMuAMLAkAgASIBIAJHDQBBKCEQDLgDCyAAQQA2AgQgAEGMgICAADYCCCAAIAEgARCxgICAACIQDdMBIAEhAQzYAQsCQCABIhAgAkcNAEEpIRAMtwMLIBAtAAAiAUEgRg0UIAFBCUcN0wEgEEEBaiEBDBULAkAgASIBIAJGDQAgAUEBaiEBDBcLQSohEAy1AwsCQCABIhAgAkcNAEErIRAMtQMLAkAgEC0AACIBQQlGDQAgAUEgRw3VAQsgAC0ALEEIRg3TASAQIQEMkQMLAkAgASIBIAJHDQBBLCEQDLQDCyABLQAAQQpHDdUBIAFBAWohAQzJAgsgASIOIAJHDdUBQS8hEAyyAwsDQAJAIAEtAAAiEEEgRg0AAkAgEEF2ag4EANwB3AEA2gELIAEhAQzgAQsgAUEBaiIBIAJHDQALQTEhEAyxAwtBMiEQIAEiFCACRg2wAyACIBRrIAAoAgAiAWohFSAUIAFrQQNqIRYCQANAIBQtAAAiF0EgciAXIBdBv39qQf8BcUEaSRtB/wFxIAFB8LuAgABqLQAARw0BAkAgAUEDRw0AQQYhAQyWAwsgAUEBaiEBIBRBAWoiFCACRw0ACyAAIBU2AgAMsQMLIABBADYCACAUIQEM2QELQTMhECABIhQgAkYNrwMgAiAUayAAKAIAIgFqIRUgFCABa0EIaiEWAkADQCAULQAAIhdBIHIgFyAXQb9/akH/AXFBGkkbQf8BcSABQfS7gIAAai0AAEcNAQJAIAFBCEcNAEEFIQEMlQMLIAFBAWohASAUQQFqIhQgAkcNAAsgACAVNgIADLADCyAAQQA2AgAgFCEBDNgBC0E0IRAgASIUIAJGDa4DIAIgFGsgACgCACIBaiEVIBQgAWtBBWohFgJAA0AgFC0AACIXQSByIBcgF0G/f2pB/wFxQRpJG0H/AXEgAUHQwoCAAGotAABHDQECQCABQQVHDQBBByEBDJQDCyABQQFqIQEgFEEBaiIUIAJHDQALIAAgFTYCAAyvAwsgAEEANgIAIBQhAQzXAQsCQCABIgEgAkYNAANAAkAgAS0AAEGAvoCAAGotAAAiEEEBRg0AIBBBAkYNCiABIQEM3QELIAFBAWoiASACRw0AC0EwIRAMrgMLQTAhEAytAwsCQCABIgEgAkYNAANAAkAgAS0AACIQQSBGDQAgEEF2ag4E2QHaAdoB2QHaAQsgAUEBaiIBIAJHDQALQTghEAytAwtBOCEQDKwDCwNAAkAgAS0AACIQQSBGDQAgEEEJRw0DCyABQQFqIgEgAkcNAAtBPCEQDKsDCwNAAkAgAS0AACIQQSBGDQACQAJAIBBBdmoOBNoBAQHaAQALIBBBLEYN2wELIAEhAQwECyABQQFqIgEgAkcNAAtBPyEQDKoDCyABIQEM2wELQcAAIRAgASIUIAJGDagDIAIgFGsgACgCACIBaiEWIBQgAWtBBmohFwJAA0AgFC0AAEEgciABQYDAgIAAai0AAEcNASABQQZGDY4DIAFBAWohASAUQQFqIhQgAkcNAAsgACAWNgIADKkDCyAAQQA2AgAgFCEBC0E2IRAMjgMLAkAgASIPIAJHDQBBwQAhEAynAwsgAEGMgICAADYCCCAAIA82AgQgDyEBIAAtACxBf2oOBM0B1QHXAdkBhwMLIAFBAWohAQzMAQsCQCABIgEgAkYNAANAAkAgAS0AACIQQSByIBAgEEG/f2pB/wFxQRpJG0H/AXEiEEEJRg0AIBBBIEYNAAJAAkACQAJAIBBBnX9qDhMAAwMDAwMDAwEDAwMDAwMDAwMCAwsgAUEBaiEBQTEhEAyRAwsgAUEBaiEBQTIhEAyQAwsgAUEBaiEBQTMhEAyPAwsgASEBDNABCyABQQFqIgEgAkcNAAtBNSEQDKUDC0E1IRAMpAMLAkAgASIBIAJGDQADQAJAIAEtAABBgLyAgABqLQAAQQFGDQAgASEBDNMBCyABQQFqIgEgAkcNAAtBPSEQDKQDC0E9IRAMowMLIAAgASIBIAIQsICAgAAiEA3WASABIQEMAQsgEEEBaiEBC0E8IRAMhwMLAkAgASIBIAJHDQBBwgAhEAygAwsCQANAAkAgAS0AAEF3ag4YAAL+Av4ChAP+Av4C/gL+Av4C/gL+Av4C/gL+Av4C/gL+Av4C/gL+Av4C/gIA/gILIAFBAWoiASACRw0AC0HCACEQDKADCyABQQFqIQEgAC0ALUEBcUUNvQEgASEBC0EsIRAMhQMLIAEiASACRw3TAUHEACEQDJ0DCwNAAkAgAS0AAEGQwICAAGotAABBAUYNACABIQEMtwILIAFBAWoiASACRw0AC0HFACEQDJwDCyANLQAAIhBBIEYNswEgEEE6Rw2BAyAAKAIEIQEgAEEANgIEIAAgASANEK+AgIAAIgEN0AEgDUEBaiEBDLMCC0HHACEQIAEiDSACRg2aAyACIA1rIAAoAgAiAWohFiANIAFrQQVqIRcDQCANLQAAIhRBIHIgFCAUQb9/akH/AXFBGkkbQf8BcSABQZDCgIAAai0AAEcNgAMgAUEFRg30AiABQQFqIQEgDUEBaiINIAJHDQALIAAgFjYCAAyaAwtByAAhECABIg0gAkYNmQMgAiANayAAKAIAIgFqIRYgDSABa0EJaiEXA0AgDS0AACIUQSByIBQgFEG/f2pB/wFxQRpJG0H/AXEgAUGWwoCAAGotAABHDf8CAkAgAUEJRw0AQQIhAQz1AgsgAUEBaiEBIA1BAWoiDSACRw0ACyAAIBY2AgAMmQMLAkAgASINIAJHDQBByQAhEAyZAwsCQAJAIA0tAAAiAUEgciABIAFBv39qQf8BcUEaSRtB/wFxQZJ/ag4HAIADgAOAA4ADgAMBgAMLIA1BAWohAUE+IRAMgAMLIA1BAWohAUE/IRAM/wILQcoAIRAgASINIAJGDZcDIAIgDWsgACgCACIBaiEWIA0gAWtBAWohFwNAIA0tAAAiFEEgciAUIBRBv39qQf8BcUEaSRtB/wFxIAFBoMKAgABqLQAARw39AiABQQFGDfACIAFBAWohASANQQFqIg0gAkcNAAsgACAWNgIADJcDC0HLACEQIAEiDSACRg2WAyACIA1rIAAoAgAiAWohFiANIAFrQQ5qIRcDQCANLQAAIhRBIHIgFCAUQb9/akH/AXFBGkkbQf8BcSABQaLCgIAAai0AAEcN/AIgAUEORg3wAiABQQFqIQEgDUEBaiINIAJHDQALIAAgFjYCAAyWAwtBzAAhECABIg0gAkYNlQMgAiANayAAKAIAIgFqIRYgDSABa0EPaiEXA0AgDS0AACIUQSByIBQgFEG/f2pB/wFxQRpJG0H/AXEgAUHAwoCAAGotAABHDfsCAkAgAUEPRw0AQQMhAQzxAgsgAUEBaiEBIA1BAWoiDSACRw0ACyAAIBY2AgAMlQMLQc0AIRAgASINIAJGDZQDIAIgDWsgACgCACIBaiEWIA0gAWtBBWohFwNAIA0tAAAiFEEgciAUIBRBv39qQf8BcUEaSRtB/wFxIAFB0MKAgABqLQAARw36AgJAIAFBBUcNAEEEIQEM8AILIAFBAWohASANQQFqIg0gAkcNAAsgACAWNgIADJQDCwJAIAEiDSACRw0AQc4AIRAMlAMLAkACQAJAAkAgDS0AACIBQSByIAEgAUG/f2pB/wFxQRpJG0H/AXFBnX9qDhMA/QL9Av0C/QL9Av0C/QL9Av0C/QL9Av0CAf0C/QL9AgID/QILIA1BAWohAUHBACEQDP0CCyANQQFqIQFBwgAhEAz8AgsgDUEBaiEBQcMAIRAM+wILIA1BAWohAUHEACEQDPoCCwJAIAEiASACRg0AIABBjYCAgAA2AgggACABNgIEIAEhAUHFACEQDPoCC0HPACEQDJIDCyAQIQECQAJAIBAtAABBdmoOBAGoAqgCAKgCCyAQQQFqIQELQSchEAz4AgsCQCABIgEgAkcNAEHRACEQDJEDCwJAIAEtAABBIEYNACABIQEMjQELIAFBAWohASAALQAtQQFxRQ3HASABIQEMjAELIAEiFyACRw3IAUHSACEQDI8DC0HTACEQIAEiFCACRg2OAyACIBRrIAAoAgAiAWohFiAUIAFrQQFqIRcDQCAULQAAIAFB1sKAgABqLQAARw3MASABQQFGDccBIAFBAWohASAUQQFqIhQgAkcNAAsgACAWNgIADI4DCwJAIAEiASACRw0AQdUAIRAMjgMLIAEtAABBCkcNzAEgAUEBaiEBDMcBCwJAIAEiASACRw0AQdYAIRAMjQMLAkACQCABLQAAQXZqDgQAzQHNAQHNAQsgAUEBaiEBDMcBCyABQQFqIQFBygAhEAzzAgsgACABIgEgAhCugICAACIQDcsBIAEhAUHNACEQDPICCyAALQApQSJGDYUDDKYCCwJAIAEiASACRw0AQdsAIRAMigMLQQAhFEEBIRdBASEWQQAhEAJAAkACQAJAAkACQAJAAkACQCABLQAAQVBqDgrUAdMBAAECAwQFBgjVAQtBAiEQDAYLQQMhEAwFC0EEIRAMBAtBBSEQDAMLQQYhEAwCC0EHIRAMAQtBCCEQC0EAIRdBACEWQQAhFAzMAQtBCSEQQQEhFEEAIRdBACEWDMsBCwJAIAEiASACRw0AQd0AIRAMiQMLIAEtAABBLkcNzAEgAUEBaiEBDKYCCyABIgEgAkcNzAFB3wAhEAyHAwsCQCABIgEgAkYNACAAQY6AgIAANgIIIAAgATYCBCABIQFB0AAhEAzuAgtB4AAhEAyGAwtB4QAhECABIgEgAkYNhQMgAiABayAAKAIAIhRqIRYgASAUa0EDaiEXA0AgAS0AACAUQeLCgIAAai0AAEcNzQEgFEEDRg3MASAUQQFqIRQgAUEBaiIBIAJHDQALIAAgFjYCAAyFAwtB4gAhECABIgEgAkYNhAMgAiABayAAKAIAIhRqIRYgASAUa0ECaiEXA0AgAS0AACAUQebCgIAAai0AAEcNzAEgFEECRg3OASAUQQFqIRQgAUEBaiIBIAJHDQALIAAgFjYCAAyEAwtB4wAhECABIgEgAkYNgwMgAiABayAAKAIAIhRqIRYgASAUa0EDaiEXA0AgAS0AACAUQenCgIAAai0AAEcNywEgFEEDRg3OASAUQQFqIRQgAUEBaiIBIAJHDQALIAAgFjYCAAyDAwsCQCABIgEgAkcNAEHlACEQDIMDCyAAIAFBAWoiASACEKiAgIAAIhANzQEgASEBQdYAIRAM6QILAkAgASIBIAJGDQADQAJAIAEtAAAiEEEgRg0AAkACQAJAIBBBuH9qDgsAAc8BzwHPAc8BzwHPAc8BzwECzwELIAFBAWohAUHSACEQDO0CCyABQQFqIQFB0wAhEAzsAgsgAUEBaiEBQdQAIRAM6wILIAFBAWoiASACRw0AC0HkACEQDIIDC0HkACEQDIEDCwNAAkAgAS0AAEHwwoCAAGotAAAiEEEBRg0AIBBBfmoOA88B0AHRAdIBCyABQQFqIgEgAkcNAAtB5gAhEAyAAwsCQCABIgEgAkYNACABQQFqIQEMAwtB5wAhEAz/AgsDQAJAIAEtAABB8MSAgABqLQAAIhBBAUYNAAJAIBBBfmoOBNIB0wHUAQDVAQsgASEBQdcAIRAM5wILIAFBAWoiASACRw0AC0HoACEQDP4CCwJAIAEiASACRw0AQekAIRAM/gILAkAgAS0AACIQQXZqDhq6AdUB1QG8AdUB1QHVAdUB1QHVAdUB1QHVAdUB1QHVAdUB1QHVAdUB1QHVAcoB1QHVAQDTAQsgAUEBaiEBC0EGIRAM4wILA0ACQCABLQAAQfDGgIAAai0AAEEBRg0AIAEhAQyeAgsgAUEBaiIBIAJHDQALQeoAIRAM+wILAkAgASIBIAJGDQAgAUEBaiEBDAMLQesAIRAM+gILAkAgASIBIAJHDQBB7AAhEAz6AgsgAUEBaiEBDAELAkAgASIBIAJHDQBB7QAhEAz5AgsgAUEBaiEBC0EEIRAM3gILAkAgASIUIAJHDQBB7gAhEAz3AgsgFCEBAkACQAJAIBQtAABB8MiAgABqLQAAQX9qDgfUAdUB1gEAnAIBAtcBCyAUQQFqIQEMCgsgFEEBaiEBDM0BC0EAIRAgAEEANgIcIABBm5KAgAA2AhAgAEEHNgIMIAAgFEEBajYCFAz2AgsCQANAAkAgAS0AAEHwyICAAGotAAAiEEEERg0AAkACQCAQQX9qDgfSAdMB1AHZAQAEAdkBCyABIQFB2gAhEAzgAgsgAUEBaiEBQdwAIRAM3wILIAFBAWoiASACRw0AC0HvACEQDPYCCyABQQFqIQEMywELAkAgASIUIAJHDQBB8AAhEAz1AgsgFC0AAEEvRw3UASAUQQFqIQEMBgsCQCABIhQgAkcNAEHxACEQDPQCCwJAIBQtAAAiAUEvRw0AIBRBAWohAUHdACEQDNsCCyABQXZqIgRBFksN0wFBASAEdEGJgIACcUUN0wEMygILAkAgASIBIAJGDQAgAUEBaiEBQd4AIRAM2gILQfIAIRAM8gILAkAgASIUIAJHDQBB9AAhEAzyAgsgFCEBAkAgFC0AAEHwzICAAGotAABBf2oOA8kClAIA1AELQeEAIRAM2AILAkAgASIUIAJGDQADQAJAIBQtAABB8MqAgABqLQAAIgFBA0YNAAJAIAFBf2oOAssCANUBCyAUIQFB3wAhEAzaAgsgFEEBaiIUIAJHDQALQfMAIRAM8QILQfMAIRAM8AILAkAgASIBIAJGDQAgAEGPgICAADYCCCAAIAE2AgQgASEBQeAAIRAM1wILQfUAIRAM7wILAkAgASIBIAJHDQBB9gAhEAzvAgsgAEGPgICAADYCCCAAIAE2AgQgASEBC0EDIRAM1AILA0AgAS0AAEEgRw3DAiABQQFqIgEgAkcNAAtB9wAhEAzsAgsCQCABIgEgAkcNAEH4ACEQDOwCCyABLQAAQSBHDc4BIAFBAWohAQzvAQsgACABIgEgAhCsgICAACIQDc4BIAEhAQyOAgsCQCABIgQgAkcNAEH6ACEQDOoCCyAELQAAQcwARw3RASAEQQFqIQFBEyEQDM8BCwJAIAEiBCACRw0AQfsAIRAM6QILIAIgBGsgACgCACIBaiEUIAQgAWtBBWohEANAIAQtAAAgAUHwzoCAAGotAABHDdABIAFBBUYNzgEgAUEBaiEBIARBAWoiBCACRw0ACyAAIBQ2AgBB+wAhEAzoAgsCQCABIgQgAkcNAEH8ACEQDOgCCwJAAkAgBC0AAEG9f2oODADRAdEB0QHRAdEB0QHRAdEB0QHRAQHRAQsgBEEBaiEBQeYAIRAMzwILIARBAWohAUHnACEQDM4CCwJAIAEiBCACRw0AQf0AIRAM5wILIAIgBGsgACgCACIBaiEUIAQgAWtBAmohEAJAA0AgBC0AACABQe3PgIAAai0AAEcNzwEgAUECRg0BIAFBAWohASAEQQFqIgQgAkcNAAsgACAUNgIAQf0AIRAM5wILIABBADYCACAQQQFqIQFBECEQDMwBCwJAIAEiBCACRw0AQf4AIRAM5gILIAIgBGsgACgCACIBaiEUIAQgAWtBBWohEAJAA0AgBC0AACABQfbOgIAAai0AAEcNzgEgAUEFRg0BIAFBAWohASAEQQFqIgQgAkcNAAsgACAUNgIAQf4AIRAM5gILIABBADYCACAQQQFqIQFBFiEQDMsBCwJAIAEiBCACRw0AQf8AIRAM5QILIAIgBGsgACgCACIBaiEUIAQgAWtBA2ohEAJAA0AgBC0AACABQfzOgIAAai0AAEcNzQEgAUEDRg0BIAFBAWohASAEQQFqIgQgAkcNAAsgACAUNgIAQf8AIRAM5QILIABBADYCACAQQQFqIQFBBSEQDMoBCwJAIAEiBCACRw0AQYABIRAM5AILIAQtAABB2QBHDcsBIARBAWohAUEIIRAMyQELAkAgASIEIAJHDQBBgQEhEAzjAgsCQAJAIAQtAABBsn9qDgMAzAEBzAELIARBAWohAUHrACEQDMoCCyAEQQFqIQFB7AAhEAzJAgsCQCABIgQgAkcNAEGCASEQDOICCwJAAkAgBC0AAEG4f2oOCADLAcsBywHLAcsBywEBywELIARBAWohAUHqACEQDMkCCyAEQQFqIQFB7QAhEAzIAgsCQCABIgQgAkcNAEGDASEQDOECCyACIARrIAAoAgAiAWohECAEIAFrQQJqIRQCQANAIAQtAAAgAUGAz4CAAGotAABHDckBIAFBAkYNASABQQFqIQEgBEEBaiIEIAJHDQALIAAgEDYCAEGDASEQDOECC0EAIRAgAEEANgIAIBRBAWohAQzGAQsCQCABIgQgAkcNAEGEASEQDOACCyACIARrIAAoAgAiAWohFCAEIAFrQQRqIRACQANAIAQtAAAgAUGDz4CAAGotAABHDcgBIAFBBEYNASABQQFqIQEgBEEBaiIEIAJHDQALIAAgFDYCAEGEASEQDOACCyAAQQA2AgAgEEEBaiEBQSMhEAzFAQsCQCABIgQgAkcNAEGFASEQDN8CCwJAAkAgBC0AAEG0f2oOCADIAcgByAHIAcgByAEByAELIARBAWohAUHvACEQDMYCCyAEQQFqIQFB8AAhEAzFAgsCQCABIgQgAkcNAEGGASEQDN4CCyAELQAAQcUARw3FASAEQQFqIQEMgwILAkAgASIEIAJHDQBBhwEhEAzdAgsgAiAEayAAKAIAIgFqIRQgBCABa0EDaiEQAkADQCAELQAAIAFBiM+AgABqLQAARw3FASABQQNGDQEgAUEBaiEBIARBAWoiBCACRw0ACyAAIBQ2AgBBhwEhEAzdAgsgAEEANgIAIBBBAWohAUEtIRAMwgELAkAgASIEIAJHDQBBiAEhEAzcAgsgAiAEayAAKAIAIgFqIRQgBCABa0EIaiEQAkADQCAELQAAIAFB0M+AgABqLQAARw3EASABQQhGDQEgAUEBaiEBIARBAWoiBCACRw0ACyAAIBQ2AgBBiAEhEAzcAgsgAEEANgIAIBBBAWohAUEpIRAMwQELAkAgASIBIAJHDQBBiQEhEAzbAgtBASEQIAEtAABB3wBHDcABIAFBAWohAQyBAgsCQCABIgQgAkcNAEGKASEQDNoCCyACIARrIAAoAgAiAWohFCAEIAFrQQFqIRADQCAELQAAIAFBjM+AgABqLQAARw3BASABQQFGDa8CIAFBAWohASAEQQFqIgQgAkcNAAsgACAUNgIAQYoBIRAM2QILAkAgASIEIAJHDQBBiwEhEAzZAgsgAiAEayAAKAIAIgFqIRQgBCABa0ECaiEQAkADQCAELQAAIAFBjs+AgABqLQAARw3BASABQQJGDQEgAUEBaiEBIARBAWoiBCACRw0ACyAAIBQ2AgBBiwEhEAzZAgsgAEEANgIAIBBBAWohAUECIRAMvgELAkAgASIEIAJHDQBBjAEhEAzYAgsgAiAEayAAKAIAIgFqIRQgBCABa0EBaiEQAkADQCAELQAAIAFB8M+AgABqLQAARw3AASABQQFGDQEgAUEBaiEBIARBAWoiBCACRw0ACyAAIBQ2AgBBjAEhEAzYAgsgAEEANgIAIBBBAWohAUEfIRAMvQELAkAgASIEIAJHDQBBjQEhEAzXAgsgAiAEayAAKAIAIgFqIRQgBCABa0EBaiEQAkADQCAELQAAIAFB8s+AgABqLQAARw2/ASABQQFGDQEgAUEBaiEBIARBAWoiBCACRw0ACyAAIBQ2AgBBjQEhEAzXAgsgAEEANgIAIBBBAWohAUEJIRAMvAELAkAgASIEIAJHDQBBjgEhEAzWAgsCQAJAIAQtAABBt39qDgcAvwG/Ab8BvwG/AQG/AQsgBEEBaiEBQfgAIRAMvQILIARBAWohAUH5ACEQDLwCCwJAIAEiBCACRw0AQY8BIRAM1QILIAIgBGsgACgCACIBaiEUIAQgAWtBBWohEAJAA0AgBC0AACABQZHPgIAAai0AAEcNvQEgAUEFRg0BIAFBAWohASAEQQFqIgQgAkcNAAsgACAUNgIAQY8BIRAM1QILIABBADYCACAQQQFqIQFBGCEQDLoBCwJAIAEiBCACRw0AQZABIRAM1AILIAIgBGsgACgCACIBaiEUIAQgAWtBAmohEAJAA0AgBC0AACABQZfPgIAAai0AAEcNvAEgAUECRg0BIAFBAWohASAEQQFqIgQgAkcNAAsgACAUNgIAQZABIRAM1AILIABBADYCACAQQQFqIQFBFyEQDLkBCwJAIAEiBCACRw0AQZEBIRAM0wILIAIgBGsgACgCACIBaiEUIAQgAWtBBmohEAJAA0AgBC0AACABQZrPgIAAai0AAEcNuwEgAUEGRg0BIAFBAWohASAEQQFqIgQgAkcNAAsgACAUNgIAQZEBIRAM0wILIABBADYCACAQQQFqIQFBFSEQDLgBCwJAIAEiBCACRw0AQZIBIRAM0gILIAIgBGsgACgCACIBaiEUIAQgAWtBBWohEAJAA0AgBC0AACABQaHPgIAAai0AAEcNugEgAUEFRg0BIAFBAWohASAEQQFqIgQgAkcNAAsgACAUNgIAQZIBIRAM0gILIABBADYCACAQQQFqIQFBHiEQDLcBCwJAIAEiBCACRw0AQZMBIRAM0QILIAQtAABBzABHDbgBIARBAWohAUEKIRAMtgELAkAgBCACRw0AQZQBIRAM0AILAkACQCAELQAAQb9/ag4PALkBuQG5AbkBuQG5AbkBuQG5AbkBuQG5AbkBAbkBCyAEQQFqIQFB/gAhEAy3AgsgBEEBaiEBQf8AIRAMtgILAkAgBCACRw0AQZUBIRAMzwILAkACQCAELQAAQb9/ag4DALgBAbgBCyAEQQFqIQFB/QAhEAy2AgsgBEEBaiEEQYABIRAMtQILAkAgBCACRw0AQZYBIRAMzgILIAIgBGsgACgCACIBaiEUIAQgAWtBAWohEAJAA0AgBC0AACABQafPgIAAai0AAEcNtgEgAUEBRg0BIAFBAWohASAEQQFqIgQgAkcNAAsgACAUNgIAQZYBIRAMzgILIABBADYCACAQQQFqIQFBCyEQDLMBCwJAIAQgAkcNAEGXASEQDM0CCwJAAkACQAJAIAQtAABBU2oOIwC4AbgBuAG4AbgBuAG4AbgBuAG4AbgBuAG4AbgBuAG4AbgBuAG4AbgBuAG4AbgBAbgBuAG4AbgBuAECuAG4AbgBA7gBCyAEQQFqIQFB+wAhEAy2AgsgBEEBaiEBQfwAIRAMtQILIARBAWohBEGBASEQDLQCCyAEQQFqIQRBggEhEAyzAgsCQCAEIAJHDQBBmAEhEAzMAgsgAiAEayAAKAIAIgFqIRQgBCABa0EEaiEQAkADQCAELQAAIAFBqc+AgABqLQAARw20ASABQQRGDQEgAUEBaiEBIARBAWoiBCACRw0ACyAAIBQ2AgBBmAEhEAzMAgsgAEEANgIAIBBBAWohAUEZIRAMsQELAkAgBCACRw0AQZkBIRAMywILIAIgBGsgACgCACIBaiEUIAQgAWtBBWohEAJAA0AgBC0AACABQa7PgIAAai0AAEcNswEgAUEFRg0BIAFBAWohASAEQQFqIgQgAkcNAAsgACAUNgIAQZkBIRAMywILIABBADYCACAQQQFqIQFBBiEQDLABCwJAIAQgAkcNAEGaASEQDMoCCyACIARrIAAoAgAiAWohFCAEIAFrQQFqIRACQANAIAQtAAAgAUG0z4CAAGotAABHDbIBIAFBAUYNASABQQFqIQEgBEEBaiIEIAJHDQALIAAgFDYCAEGaASEQDMoCCyAAQQA2AgAgEEEBaiEBQRwhEAyvAQsCQCAEIAJHDQBBmwEhEAzJAgsgAiAEayAAKAIAIgFqIRQgBCABa0EBaiEQAkADQCAELQAAIAFBts+AgABqLQAARw2xASABQQFGDQEgAUEBaiEBIARBAWoiBCACRw0ACyAAIBQ2AgBBmwEhEAzJAgsgAEEANgIAIBBBAWohAUEnIRAMrgELAkAgBCACRw0AQZwBIRAMyAILAkACQCAELQAAQax/ag4CAAGxAQsgBEEBaiEEQYYBIRAMrwILIARBAWohBEGHASEQDK4CCwJAIAQgAkcNAEGdASEQDMcCCyACIARrIAAoAgAiAWohFCAEIAFrQQFqIRACQANAIAQtAAAgAUG4z4CAAGotAABHDa8BIAFBAUYNASABQQFqIQEgBEEBaiIEIAJHDQALIAAgFDYCAEGdASEQDMcCCyAAQQA2AgAgEEEBaiEBQSYhEAysAQsCQCAEIAJHDQBBngEhEAzGAgsgAiAEayAAKAIAIgFqIRQgBCABa0EBaiEQAkADQCAELQAAIAFBus+AgABqLQAARw2uASABQQFGDQEgAUEBaiEBIARBAWoiBCACRw0ACyAAIBQ2AgBBngEhEAzGAgsgAEEANgIAIBBBAWohAUEDIRAMqwELAkAgBCACRw0AQZ8BIRAMxQILIAIgBGsgACgCACIBaiEUIAQgAWtBAmohEAJAA0AgBC0AACABQe3PgIAAai0AAEcNrQEgAUECRg0BIAFBAWohASAEQQFqIgQgAkcNAAsgACAUNgIAQZ8BIRAMxQILIABBADYCACAQQQFqIQFBDCEQDKoBCwJAIAQgAkcNAEGgASEQDMQCCyACIARrIAAoAgAiAWohFCAEIAFrQQNqIRACQANAIAQtAAAgAUG8z4CAAGotAABHDawBIAFBA0YNASABQQFqIQEgBEEBaiIEIAJHDQALIAAgFDYCAEGgASEQDMQCCyAAQQA2AgAgEEEBaiEBQQ0hEAypAQsCQCAEIAJHDQBBoQEhEAzDAgsCQAJAIAQtAABBun9qDgsArAGsAawBrAGsAawBrAGsAawBAawBCyAEQQFqIQRBiwEhEAyqAgsgBEEBaiEEQYwBIRAMqQILAkAgBCACRw0AQaIBIRAMwgILIAQtAABB0ABHDakBIARBAWohBAzpAQsCQCAEIAJHDQBBowEhEAzBAgsCQAJAIAQtAABBt39qDgcBqgGqAaoBqgGqAQCqAQsgBEEBaiEEQY4BIRAMqAILIARBAWohAUEiIRAMpgELAkAgBCACRw0AQaQBIRAMwAILIAIgBGsgACgCACIBaiEUIAQgAWtBAWohEAJAA0AgBC0AACABQcDPgIAAai0AAEcNqAEgAUEBRg0BIAFBAWohASAEQQFqIgQgAkcNAAsgACAUNgIAQaQBIRAMwAILIABBADYCACAQQQFqIQFBHSEQDKUBCwJAIAQgAkcNAEGlASEQDL8CCwJAAkAgBC0AAEGuf2oOAwCoAQGoAQsgBEEBaiEEQZABIRAMpgILIARBAWohAUEEIRAMpAELAkAgBCACRw0AQaYBIRAMvgILAkACQAJAAkACQCAELQAAQb9/ag4VAKoBqgGqAaoBqgGqAaoBqgGqAaoBAaoBqgECqgGqAQOqAaoBBKoBCyAEQQFqIQRBiAEhEAyoAgsgBEEBaiEEQYkBIRAMpwILIARBAWohBEGKASEQDKYCCyAEQQFqIQRBjwEhEAylAgsgBEEBaiEEQZEBIRAMpAILAkAgBCACRw0AQacBIRAMvQILIAIgBGsgACgCACIBaiEUIAQgAWtBAmohEAJAA0AgBC0AACABQe3PgIAAai0AAEcNpQEgAUECRg0BIAFBAWohASAEQQFqIgQgAkcNAAsgACAUNgIAQacBIRAMvQILIABBADYCACAQQQFqIQFBESEQDKIBCwJAIAQgAkcNAEGoASEQDLwCCyACIARrIAAoAgAiAWohFCAEIAFrQQJqIRACQANAIAQtAAAgAUHCz4CAAGotAABHDaQBIAFBAkYNASABQQFqIQEgBEEBaiIEIAJHDQALIAAgFDYCAEGoASEQDLwCCyAAQQA2AgAgEEEBaiEBQSwhEAyhAQsCQCAEIAJHDQBBqQEhEAy7AgsgAiAEayAAKAIAIgFqIRQgBCABa0EEaiEQAkADQCAELQAAIAFBxc+AgABqLQAARw2jASABQQRGDQEgAUEBaiEBIARBAWoiBCACRw0ACyAAIBQ2AgBBqQEhEAy7AgsgAEEANgIAIBBBAWohAUErIRAMoAELAkAgBCACRw0AQaoBIRAMugILIAIgBGsgACgCACIBaiEUIAQgAWtBAmohEAJAA0AgBC0AACABQcrPgIAAai0AAEcNogEgAUECRg0BIAFBAWohASAEQQFqIgQgAkcNAAsgACAUNgIAQaoBIRAMugILIABBADYCACAQQQFqIQFBFCEQDJ8BCwJAIAQgAkcNAEGrASEQDLkCCwJAAkACQAJAIAQtAABBvn9qDg8AAQKkAaQBpAGkAaQBpAGkAaQBpAGkAaQBA6QBCyAEQQFqIQRBkwEhEAyiAgsgBEEBaiEEQZQBIRAMoQILIARBAWohBEGVASEQDKACCyAEQQFqIQRBlgEhEAyfAgsCQCAEIAJHDQBBrAEhEAy4AgsgBC0AAEHFAEcNnwEgBEEBaiEEDOABCwJAIAQgAkcNAEGtASEQDLcCCyACIARrIAAoAgAiAWohFCAEIAFrQQJqIRACQANAIAQtAAAgAUHNz4CAAGotAABHDZ8BIAFBAkYNASABQQFqIQEgBEEBaiIEIAJHDQALIAAgFDYCAEGtASEQDLcCCyAAQQA2AgAgEEEBaiEBQQ4hEAycAQsCQCAEIAJHDQBBrgEhEAy2AgsgBC0AAEHQAEcNnQEgBEEBaiEBQSUhEAybAQsCQCAEIAJHDQBBrwEhEAy1AgsgAiAEayAAKAIAIgFqIRQgBCABa0EIaiEQAkADQCAELQAAIAFB0M+AgABqLQAARw2dASABQQhGDQEgAUEBaiEBIARBAWoiBCACRw0ACyAAIBQ2AgBBrwEhEAy1AgsgAEEANgIAIBBBAWohAUEqIRAMmgELAkAgBCACRw0AQbABIRAMtAILAkACQCAELQAAQat/ag4LAJ0BnQGdAZ0BnQGdAZ0BnQGdAQGdAQsgBEEBaiEEQZoBIRAMmwILIARBAWohBEGbASEQDJoCCwJAIAQgAkcNAEGxASEQDLMCCwJAAkAgBC0AAEG/f2oOFACcAZwBnAGcAZwBnAGcAZwBnAGcAZwBnAGcAZwBnAGcAZwBnAEBnAELIARBAWohBEGZASEQDJoCCyAEQQFqIQRBnAEhEAyZAgsCQCAEIAJHDQBBsgEhEAyyAgsgAiAEayAAKAIAIgFqIRQgBCABa0EDaiEQAkADQCAELQAAIAFB2c+AgABqLQAARw2aASABQQNGDQEgAUEBaiEBIARBAWoiBCACRw0ACyAAIBQ2AgBBsgEhEAyyAgsgAEEANgIAIBBBAWohAUEhIRAMlwELAkAgBCACRw0AQbMBIRAMsQILIAIgBGsgACgCACIBaiEUIAQgAWtBBmohEAJAA0AgBC0AACABQd3PgIAAai0AAEcNmQEgAUEGRg0BIAFBAWohASAEQQFqIgQgAkcNAAsgACAUNgIAQbMBIRAMsQILIABBADYCACAQQQFqIQFBGiEQDJYBCwJAIAQgAkcNAEG0ASEQDLACCwJAAkACQCAELQAAQbt/ag4RAJoBmgGaAZoBmgGaAZoBmgGaAQGaAZoBmgGaAZoBApoBCyAEQQFqIQRBnQEhEAyYAgsgBEEBaiEEQZ4BIRAMlwILIARBAWohBEGfASEQDJYCCwJAIAQgAkcNAEG1ASEQDK8CCyACIARrIAAoAgAiAWohFCAEIAFrQQVqIRACQANAIAQtAAAgAUHkz4CAAGotAABHDZcBIAFBBUYNASABQQFqIQEgBEEBaiIEIAJHDQALIAAgFDYCAEG1ASEQDK8CCyAAQQA2AgAgEEEBaiEBQSghEAyUAQsCQCAEIAJHDQBBtgEhEAyuAgsgAiAEayAAKAIAIgFqIRQgBCABa0ECaiEQAkADQCAELQAAIAFB6s+AgABqLQAARw2WASABQQJGDQEgAUEBaiEBIARBAWoiBCACRw0ACyAAIBQ2AgBBtgEhEAyuAgsgAEEANgIAIBBBAWohAUEHIRAMkwELAkAgBCACRw0AQbcBIRAMrQILAkACQCAELQAAQbt/ag4OAJYBlgGWAZYBlgGWAZYBlgGWAZYBlgGWAQGWAQsgBEEBaiEEQaEBIRAMlAILIARBAWohBEGiASEQDJMCCwJAIAQgAkcNAEG4ASEQDKwCCyACIARrIAAoAgAiAWohFCAEIAFrQQJqIRACQANAIAQtAAAgAUHtz4CAAGotAABHDZQBIAFBAkYNASABQQFqIQEgBEEBaiIEIAJHDQALIAAgFDYCAEG4ASEQDKwCCyAAQQA2AgAgEEEBaiEBQRIhEAyRAQsCQCAEIAJHDQBBuQEhEAyrAgsgAiAEayAAKAIAIgFqIRQgBCABa0EBaiEQAkADQCAELQAAIAFB8M+AgABqLQAARw2TASABQQFGDQEgAUEBaiEBIARBAWoiBCACRw0ACyAAIBQ2AgBBuQEhEAyrAgsgAEEANgIAIBBBAWohAUEgIRAMkAELAkAgBCACRw0AQboBIRAMqgILIAIgBGsgACgCACIBaiEUIAQgAWtBAWohEAJAA0AgBC0AACABQfLPgIAAai0AAEcNkgEgAUEBRg0BIAFBAWohASAEQQFqIgQgAkcNAAsgACAUNgIAQboBIRAMqgILIABBADYCACAQQQFqIQFBDyEQDI8BCwJAIAQgAkcNAEG7ASEQDKkCCwJAAkAgBC0AAEG3f2oOBwCSAZIBkgGSAZIBAZIBCyAEQQFqIQRBpQEhEAyQAgsgBEEBaiEEQaYBIRAMjwILAkAgBCACRw0AQbwBIRAMqAILIAIgBGsgACgCACIBaiEUIAQgAWtBB2ohEAJAA0AgBC0AACABQfTPgIAAai0AAEcNkAEgAUEHRg0BIAFBAWohASAEQQFqIgQgAkcNAAsgACAUNgIAQbwBIRAMqAILIABBADYCACAQQQFqIQFBGyEQDI0BCwJAIAQgAkcNAEG9ASEQDKcCCwJAAkACQCAELQAAQb5/ag4SAJEBkQGRAZEBkQGRAZEBkQGRAQGRAZEBkQGRAZEBkQECkQELIARBAWohBEGkASEQDI8CCyAEQQFqIQRBpwEhEAyOAgsgBEEBaiEEQagBIRAMjQILAkAgBCACRw0AQb4BIRAMpgILIAQtAABBzgBHDY0BIARBAWohBAzPAQsCQCAEIAJHDQBBvwEhEAylAgsCQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQCAELQAAQb9/ag4VAAECA5wBBAUGnAGcAZwBBwgJCgucAQwNDg+cAQsgBEEBaiEBQegAIRAMmgILIARBAWohAUHpACEQDJkCCyAEQQFqIQFB7gAhEAyYAgsgBEEBaiEBQfIAIRAMlwILIARBAWohAUHzACEQDJYCCyAEQQFqIQFB9gAhEAyVAgsgBEEBaiEBQfcAIRAMlAILIARBAWohAUH6ACEQDJMCCyAEQQFqIQRBgwEhEAySAgsgBEEBaiEEQYQBIRAMkQILIARBAWohBEGFASEQDJACCyAEQQFqIQRBkgEhEAyPAgsgBEEBaiEEQZgBIRAMjgILIARBAWohBEGgASEQDI0CCyAEQQFqIQRBowEhEAyMAgsgBEEBaiEEQaoBIRAMiwILAkAgBCACRg0AIABBkICAgAA2AgggACAENgIEQasBIRAMiwILQcABIRAMowILIAAgBSACEKqAgIAAIgENiwEgBSEBDFwLAkAgBiACRg0AIAZBAWohBQyNAQtBwgEhEAyhAgsDQAJAIBAtAABBdmoOBIwBAACPAQALIBBBAWoiECACRw0AC0HDASEQDKACCwJAIAcgAkYNACAAQZGAgIAANgIIIAAgBzYCBCAHIQFBASEQDIcCC0HEASEQDJ8CCwJAIAcgAkcNAEHFASEQDJ8CCwJAAkAgBy0AAEF2ag4EAc4BzgEAzgELIAdBAWohBgyNAQsgB0EBaiEFDIkBCwJAIAcgAkcNAEHGASEQDJ4CCwJAAkAgBy0AAEF2ag4XAY8BjwEBjwGPAY8BjwGPAY8BjwGPAY8BjwGPAY8BjwGPAY8BjwGPAY8BAI8BCyAHQQFqIQcLQbABIRAMhAILAkAgCCACRw0AQcgBIRAMnQILIAgtAABBIEcNjQEgAEEAOwEyIAhBAWohAUGzASEQDIMCCyABIRcCQANAIBciByACRg0BIActAABBUGpB/wFxIhBBCk8NzAECQCAALwEyIhRBmTNLDQAgACAUQQpsIhQ7ATIgEEH//wNzIBRB/v8DcUkNACAHQQFqIRcgACAUIBBqIhA7ATIgEEH//wNxQegHSQ0BCwtBACEQIABBADYCHCAAQcGJgIAANgIQIABBDTYCDCAAIAdBAWo2AhQMnAILQccBIRAMmwILIAAgCCACEK6AgIAAIhBFDcoBIBBBFUcNjAEgAEHIATYCHCAAIAg2AhQgAEHJl4CAADYCECAAQRU2AgxBACEQDJoCCwJAIAkgAkcNAEHMASEQDJoCC0EAIRRBASEXQQEhFkEAIRACQAJAAkACQAJAAkACQAJAAkAgCS0AAEFQag4KlgGVAQABAgMEBQYIlwELQQIhEAwGC0EDIRAMBQtBBCEQDAQLQQUhEAwDC0EGIRAMAgtBByEQDAELQQghEAtBACEXQQAhFkEAIRQMjgELQQkhEEEBIRRBACEXQQAhFgyNAQsCQCAKIAJHDQBBzgEhEAyZAgsgCi0AAEEuRw2OASAKQQFqIQkMygELIAsgAkcNjgFB0AEhEAyXAgsCQCALIAJGDQAgAEGOgICAADYCCCAAIAs2AgRBtwEhEAz+AQtB0QEhEAyWAgsCQCAEIAJHDQBB0gEhEAyWAgsgAiAEayAAKAIAIhBqIRQgBCAQa0EEaiELA0AgBC0AACAQQfzPgIAAai0AAEcNjgEgEEEERg3pASAQQQFqIRAgBEEBaiIEIAJHDQALIAAgFDYCAEHSASEQDJUCCyAAIAwgAhCsgICAACIBDY0BIAwhAQy4AQsCQCAEIAJHDQBB1AEhEAyUAgsgAiAEayAAKAIAIhBqIRQgBCAQa0EBaiEMA0AgBC0AACAQQYHQgIAAai0AAEcNjwEgEEEBRg2OASAQQQFqIRAgBEEBaiIEIAJHDQALIAAgFDYCAEHUASEQDJMCCwJAIAQgAkcNAEHWASEQDJMCCyACIARrIAAoAgAiEGohFCAEIBBrQQJqIQsDQCAELQAAIBBBg9CAgABqLQAARw2OASAQQQJGDZABIBBBAWohECAEQQFqIgQgAkcNAAsgACAUNgIAQdYBIRAMkgILAkAgBCACRw0AQdcBIRAMkgILAkACQCAELQAAQbt/ag4QAI8BjwGPAY8BjwGPAY8BjwGPAY8BjwGPAY8BjwEBjwELIARBAWohBEG7ASEQDPkBCyAEQQFqIQRBvAEhEAz4AQsCQCAEIAJHDQBB2AEhEAyRAgsgBC0AAEHIAEcNjAEgBEEBaiEEDMQBCwJAIAQgAkYNACAAQZCAgIAANgIIIAAgBDYCBEG+ASEQDPcBC0HZASEQDI8CCwJAIAQgAkcNAEHaASEQDI8CCyAELQAAQcgARg3DASAAQQE6ACgMuQELIABBAjoALyAAIAQgAhCmgICAACIQDY0BQcIBIRAM9AELIAAtAChBf2oOArcBuQG4AQsDQAJAIAQtAABBdmoOBACOAY4BAI4BCyAEQQFqIgQgAkcNAAtB3QEhEAyLAgsgAEEAOgAvIAAtAC1BBHFFDYQCCyAAQQA6AC8gAEEBOgA0IAEhAQyMAQsgEEEVRg3aASAAQQA2AhwgACABNgIUIABBp46AgAA2AhAgAEESNgIMQQAhEAyIAgsCQCAAIBAgAhC0gICAACIEDQAgECEBDIECCwJAIARBFUcNACAAQQM2AhwgACAQNgIUIABBsJiAgAA2AhAgAEEVNgIMQQAhEAyIAgsgAEEANgIcIAAgEDYCFCAAQaeOgIAANgIQIABBEjYCDEEAIRAMhwILIBBBFUYN1gEgAEEANgIcIAAgATYCFCAAQdqNgIAANgIQIABBFDYCDEEAIRAMhgILIAAoAgQhFyAAQQA2AgQgECARp2oiFiEBIAAgFyAQIBYgFBsiEBC1gICAACIURQ2NASAAQQc2AhwgACAQNgIUIAAgFDYCDEEAIRAMhQILIAAgAC8BMEGAAXI7ATAgASEBC0EqIRAM6gELIBBBFUYN0QEgAEEANgIcIAAgATYCFCAAQYOMgIAANgIQIABBEzYCDEEAIRAMggILIBBBFUYNzwEgAEEANgIcIAAgATYCFCAAQZqPgIAANgIQIABBIjYCDEEAIRAMgQILIAAoAgQhECAAQQA2AgQCQCAAIBAgARC3gICAACIQDQAgAUEBaiEBDI0BCyAAQQw2AhwgACAQNgIMIAAgAUEBajYCFEEAIRAMgAILIBBBFUYNzAEgAEEANgIcIAAgATYCFCAAQZqPgIAANgIQIABBIjYCDEEAIRAM/wELIAAoAgQhECAAQQA2AgQCQCAAIBAgARC3gICAACIQDQAgAUEBaiEBDIwBCyAAQQ02AhwgACAQNgIMIAAgAUEBajYCFEEAIRAM/gELIBBBFUYNyQEgAEEANgIcIAAgATYCFCAAQcaMgIAANgIQIABBIzYCDEEAIRAM/QELIAAoAgQhECAAQQA2AgQCQCAAIBAgARC5gICAACIQDQAgAUEBaiEBDIsBCyAAQQ42AhwgACAQNgIMIAAgAUEBajYCFEEAIRAM/AELIABBADYCHCAAIAE2AhQgAEHAlYCAADYCECAAQQI2AgxBACEQDPsBCyAQQRVGDcUBIABBADYCHCAAIAE2AhQgAEHGjICAADYCECAAQSM2AgxBACEQDPoBCyAAQRA2AhwgACABNgIUIAAgEDYCDEEAIRAM+QELIAAoAgQhBCAAQQA2AgQCQCAAIAQgARC5gICAACIEDQAgAUEBaiEBDPEBCyAAQRE2AhwgACAENgIMIAAgAUEBajYCFEEAIRAM+AELIBBBFUYNwQEgAEEANgIcIAAgATYCFCAAQcaMgIAANgIQIABBIzYCDEEAIRAM9wELIAAoAgQhECAAQQA2AgQCQCAAIBAgARC5gICAACIQDQAgAUEBaiEBDIgBCyAAQRM2AhwgACAQNgIMIAAgAUEBajYCFEEAIRAM9gELIAAoAgQhBCAAQQA2AgQCQCAAIAQgARC5gICAACIEDQAgAUEBaiEBDO0BCyAAQRQ2AhwgACAENgIMIAAgAUEBajYCFEEAIRAM9QELIBBBFUYNvQEgAEEANgIcIAAgATYCFCAAQZqPgIAANgIQIABBIjYCDEEAIRAM9AELIAAoAgQhECAAQQA2AgQCQCAAIBAgARC3gICAACIQDQAgAUEBaiEBDIYBCyAAQRY2AhwgACAQNgIMIAAgAUEBajYCFEEAIRAM8wELIAAoAgQhBCAAQQA2AgQCQCAAIAQgARC3gICAACIEDQAgAUEBaiEBDOkBCyAAQRc2AhwgACAENgIMIAAgAUEBajYCFEEAIRAM8gELIABBADYCHCAAIAE2AhQgAEHNk4CAADYCECAAQQw2AgxBACEQDPEBC0IBIRELIBBBAWohAQJAIAApAyAiEkL//////////w9WDQAgACASQgSGIBGENwMgIAEhAQyEAQsgAEEANgIcIAAgATYCFCAAQa2JgIAANgIQIABBDDYCDEEAIRAM7wELIABBADYCHCAAIBA2AhQgAEHNk4CAADYCECAAQQw2AgxBACEQDO4BCyAAKAIEIRcgAEEANgIEIBAgEadqIhYhASAAIBcgECAWIBQbIhAQtYCAgAAiFEUNcyAAQQU2AhwgACAQNgIUIAAgFDYCDEEAIRAM7QELIABBADYCHCAAIBA2AhQgAEGqnICAADYCECAAQQ82AgxBACEQDOwBCyAAIBAgAhC0gICAACIBDQEgECEBC0EOIRAM0QELAkAgAUEVRw0AIABBAjYCHCAAIBA2AhQgAEGwmICAADYCECAAQRU2AgxBACEQDOoBCyAAQQA2AhwgACAQNgIUIABBp46AgAA2AhAgAEESNgIMQQAhEAzpAQsgAUEBaiEQAkAgAC8BMCIBQYABcUUNAAJAIAAgECACELuAgIAAIgENACAQIQEMcAsgAUEVRw26ASAAQQU2AhwgACAQNgIUIABB+ZeAgAA2AhAgAEEVNgIMQQAhEAzpAQsCQCABQaAEcUGgBEcNACAALQAtQQJxDQAgAEEANgIcIAAgEDYCFCAAQZaTgIAANgIQIABBBDYCDEEAIRAM6QELIAAgECACEL2AgIAAGiAQIQECQAJAAkACQAJAIAAgECACELOAgIAADhYCAQAEBAQEBAQEBAQEBAQEBAQEBAQDBAsgAEEBOgAuCyAAIAAvATBBwAByOwEwIBAhAQtBJiEQDNEBCyAAQSM2AhwgACAQNgIUIABBpZaAgAA2AhAgAEEVNgIMQQAhEAzpAQsgAEEANgIcIAAgEDYCFCAAQdWLgIAANgIQIABBETYCDEEAIRAM6AELIAAtAC1BAXFFDQFBwwEhEAzOAQsCQCANIAJGDQADQAJAIA0tAABBIEYNACANIQEMxAELIA1BAWoiDSACRw0AC0ElIRAM5wELQSUhEAzmAQsgACgCBCEEIABBADYCBCAAIAQgDRCvgICAACIERQ2tASAAQSY2AhwgACAENgIMIAAgDUEBajYCFEEAIRAM5QELIBBBFUYNqwEgAEEANgIcIAAgATYCFCAAQf2NgIAANgIQIABBHTYCDEEAIRAM5AELIABBJzYCHCAAIAE2AhQgACAQNgIMQQAhEAzjAQsgECEBQQEhFAJAAkACQAJAAkACQAJAIAAtACxBfmoOBwYFBQMBAgAFCyAAIAAvATBBCHI7ATAMAwtBAiEUDAELQQQhFAsgAEEBOgAsIAAgAC8BMCAUcjsBMAsgECEBC0ErIRAMygELIABBADYCHCAAIBA2AhQgAEGrkoCAADYCECAAQQs2AgxBACEQDOIBCyAAQQA2AhwgACABNgIUIABB4Y+AgAA2AhAgAEEKNgIMQQAhEAzhAQsgAEEAOgAsIBAhAQy9AQsgECEBQQEhFAJAAkACQAJAAkAgAC0ALEF7ag4EAwECAAULIAAgAC8BMEEIcjsBMAwDC0ECIRQMAQtBBCEUCyAAQQE6ACwgACAALwEwIBRyOwEwCyAQIQELQSkhEAzFAQsgAEEANgIcIAAgATYCFCAAQfCUgIAANgIQIABBAzYCDEEAIRAM3QELAkAgDi0AAEENRw0AIAAoAgQhASAAQQA2AgQCQCAAIAEgDhCxgICAACIBDQAgDkEBaiEBDHULIABBLDYCHCAAIAE2AgwgACAOQQFqNgIUQQAhEAzdAQsgAC0ALUEBcUUNAUHEASEQDMMBCwJAIA4gAkcNAEEtIRAM3AELAkACQANAAkAgDi0AAEF2ag4EAgAAAwALIA5BAWoiDiACRw0AC0EtIRAM3QELIAAoAgQhASAAQQA2AgQCQCAAIAEgDhCxgICAACIBDQAgDiEBDHQLIABBLDYCHCAAIA42AhQgACABNgIMQQAhEAzcAQsgACgCBCEBIABBADYCBAJAIAAgASAOELGAgIAAIgENACAOQQFqIQEMcwsgAEEsNgIcIAAgATYCDCAAIA5BAWo2AhRBACEQDNsBCyAAKAIEIQQgAEEANgIEIAAgBCAOELGAgIAAIgQNoAEgDiEBDM4BCyAQQSxHDQEgAUEBaiEQQQEhAQJAAkACQAJAAkAgAC0ALEF7ag4EAwECBAALIBAhAQwEC0ECIQEMAQtBBCEBCyAAQQE6ACwgACAALwEwIAFyOwEwIBAhAQwBCyAAIAAvATBBCHI7ATAgECEBC0E5IRAMvwELIABBADoALCABIQELQTQhEAy9AQsgACAALwEwQSByOwEwIAEhAQwCCyAAKAIEIQQgAEEANgIEAkAgACAEIAEQsYCAgAAiBA0AIAEhAQzHAQsgAEE3NgIcIAAgATYCFCAAIAQ2AgxBACEQDNQBCyAAQQg6ACwgASEBC0EwIRAMuQELAkAgAC0AKEEBRg0AIAEhAQwECyAALQAtQQhxRQ2TASABIQEMAwsgAC0AMEEgcQ2UAUHFASEQDLcBCwJAIA8gAkYNAAJAA0ACQCAPLQAAQVBqIgFB/wFxQQpJDQAgDyEBQTUhEAy6AQsgACkDICIRQpmz5syZs+bMGVYNASAAIBFCCn4iETcDICARIAGtQv8BgyISQn+FVg0BIAAgESASfDcDICAPQQFqIg8gAkcNAAtBOSEQDNEBCyAAKAIEIQIgAEEANgIEIAAgAiAPQQFqIgQQsYCAgAAiAg2VASAEIQEMwwELQTkhEAzPAQsCQCAALwEwIgFBCHFFDQAgAC0AKEEBRw0AIAAtAC1BCHFFDZABCyAAIAFB9/sDcUGABHI7ATAgDyEBC0E3IRAMtAELIAAgAC8BMEEQcjsBMAyrAQsgEEEVRg2LASAAQQA2AhwgACABNgIUIABB8I6AgAA2AhAgAEEcNgIMQQAhEAzLAQsgAEHDADYCHCAAIAE2AgwgACANQQFqNgIUQQAhEAzKAQsCQCABLQAAQTpHDQAgACgCBCEQIABBADYCBAJAIAAgECABEK+AgIAAIhANACABQQFqIQEMYwsgAEHDADYCHCAAIBA2AgwgACABQQFqNgIUQQAhEAzKAQsgAEEANgIcIAAgATYCFCAAQbGRgIAANgIQIABBCjYCDEEAIRAMyQELIABBADYCHCAAIAE2AhQgAEGgmYCAADYCECAAQR42AgxBACEQDMgBCyAAQQA2AgALIABBgBI7ASogACAXQQFqIgEgAhCogICAACIQDQEgASEBC0HHACEQDKwBCyAQQRVHDYMBIABB0QA2AhwgACABNgIUIABB45eAgAA2AhAgAEEVNgIMQQAhEAzEAQsgACgCBCEQIABBADYCBAJAIAAgECABEKeAgIAAIhANACABIQEMXgsgAEHSADYCHCAAIAE2AhQgACAQNgIMQQAhEAzDAQsgAEEANgIcIAAgFDYCFCAAQcGogIAANgIQIABBBzYCDCAAQQA2AgBBACEQDMIBCyAAKAIEIRAgAEEANgIEAkAgACAQIAEQp4CAgAAiEA0AIAEhAQxdCyAAQdMANgIcIAAgATYCFCAAIBA2AgxBACEQDMEBC0EAIRAgAEEANgIcIAAgATYCFCAAQYCRgIAANgIQIABBCTYCDAzAAQsgEEEVRg19IABBADYCHCAAIAE2AhQgAEGUjYCAADYCECAAQSE2AgxBACEQDL8BC0EBIRZBACEXQQAhFEEBIRALIAAgEDoAKyABQQFqIQECQAJAIAAtAC1BEHENAAJAAkACQCAALQAqDgMBAAIECyAWRQ0DDAILIBQNAQwCCyAXRQ0BCyAAKAIEIRAgAEEANgIEAkAgACAQIAEQrYCAgAAiEA0AIAEhAQxcCyAAQdgANgIcIAAgATYCFCAAIBA2AgxBACEQDL4BCyAAKAIEIQQgAEEANgIEAkAgACAEIAEQrYCAgAAiBA0AIAEhAQytAQsgAEHZADYCHCAAIAE2AhQgACAENgIMQQAhEAy9AQsgACgCBCEEIABBADYCBAJAIAAgBCABEK2AgIAAIgQNACABIQEMqwELIABB2gA2AhwgACABNgIUIAAgBDYCDEEAIRAMvAELIAAoAgQhBCAAQQA2AgQCQCAAIAQgARCtgICAACIEDQAgASEBDKkBCyAAQdwANgIcIAAgATYCFCAAIAQ2AgxBACEQDLsBCwJAIAEtAABBUGoiEEH/AXFBCk8NACAAIBA6ACogAUEBaiEBQc8AIRAMogELIAAoAgQhBCAAQQA2AgQCQCAAIAQgARCtgICAACIEDQAgASEBDKcBCyAAQd4ANgIcIAAgATYCFCAAIAQ2AgxBACEQDLoBCyAAQQA2AgAgF0EBaiEBAkAgAC0AKUEjTw0AIAEhAQxZCyAAQQA2AhwgACABNgIUIABB04mAgAA2AhAgAEEINgIMQQAhEAy5AQsgAEEANgIAC0EAIRAgAEEANgIcIAAgATYCFCAAQZCzgIAANgIQIABBCDYCDAy3AQsgAEEANgIAIBdBAWohAQJAIAAtAClBIUcNACABIQEMVgsgAEEANgIcIAAgATYCFCAAQZuKgIAANgIQIABBCDYCDEEAIRAMtgELIABBADYCACAXQQFqIQECQCAALQApIhBBXWpBC08NACABIQEMVQsCQCAQQQZLDQBBASAQdEHKAHFFDQAgASEBDFULQQAhECAAQQA2AhwgACABNgIUIABB94mAgAA2AhAgAEEINgIMDLUBCyAQQRVGDXEgAEEANgIcIAAgATYCFCAAQbmNgIAANgIQIABBGjYCDEEAIRAMtAELIAAoAgQhECAAQQA2AgQCQCAAIBAgARCngICAACIQDQAgASEBDFQLIABB5QA2AhwgACABNgIUIAAgEDYCDEEAIRAMswELIAAoAgQhECAAQQA2AgQCQCAAIBAgARCngICAACIQDQAgASEBDE0LIABB0gA2AhwgACABNgIUIAAgEDYCDEEAIRAMsgELIAAoAgQhECAAQQA2AgQCQCAAIBAgARCngICAACIQDQAgASEBDE0LIABB0wA2AhwgACABNgIUIAAgEDYCDEEAIRAMsQELIAAoAgQhECAAQQA2AgQCQCAAIBAgARCngICAACIQDQAgASEBDFELIABB5QA2AhwgACABNgIUIAAgEDYCDEEAIRAMsAELIABBADYCHCAAIAE2AhQgAEHGioCAADYCECAAQQc2AgxBACEQDK8BCyAAKAIEIRAgAEEANgIEAkAgACAQIAEQp4CAgAAiEA0AIAEhAQxJCyAAQdIANgIcIAAgATYCFCAAIBA2AgxBACEQDK4BCyAAKAIEIRAgAEEANgIEAkAgACAQIAEQp4CAgAAiEA0AIAEhAQxJCyAAQdMANgIcIAAgATYCFCAAIBA2AgxBACEQDK0BCyAAKAIEIRAgAEEANgIEAkAgACAQIAEQp4CAgAAiEA0AIAEhAQxNCyAAQeUANgIcIAAgATYCFCAAIBA2AgxBACEQDKwBCyAAQQA2AhwgACABNgIUIABB3IiAgAA2AhAgAEEHNgIMQQAhEAyrAQsgEEE/Rw0BIAFBAWohAQtBBSEQDJABC0EAIRAgAEEANgIcIAAgATYCFCAAQf2SgIAANgIQIABBBzYCDAyoAQsgACgCBCEQIABBADYCBAJAIAAgECABEKeAgIAAIhANACABIQEMQgsgAEHSADYCHCAAIAE2AhQgACAQNgIMQQAhEAynAQsgACgCBCEQIABBADYCBAJAIAAgECABEKeAgIAAIhANACABIQEMQgsgAEHTADYCHCAAIAE2AhQgACAQNgIMQQAhEAymAQsgACgCBCEQIABBADYCBAJAIAAgECABEKeAgIAAIhANACABIQEMRgsgAEHlADYCHCAAIAE2AhQgACAQNgIMQQAhEAylAQsgACgCBCEBIABBADYCBAJAIAAgASAUEKeAgIAAIgENACAUIQEMPwsgAEHSADYCHCAAIBQ2AhQgACABNgIMQQAhEAykAQsgACgCBCEBIABBADYCBAJAIAAgASAUEKeAgIAAIgENACAUIQEMPwsgAEHTADYCHCAAIBQ2AhQgACABNgIMQQAhEAyjAQsgACgCBCEBIABBADYCBAJAIAAgASAUEKeAgIAAIgENACAUIQEMQwsgAEHlADYCHCAAIBQ2AhQgACABNgIMQQAhEAyiAQsgAEEANgIcIAAgFDYCFCAAQcOPgIAANgIQIABBBzYCDEEAIRAMoQELIABBADYCHCAAIAE2AhQgAEHDj4CAADYCECAAQQc2AgxBACEQDKABC0EAIRAgAEEANgIcIAAgFDYCFCAAQYycgIAANgIQIABBBzYCDAyfAQsgAEEANgIcIAAgFDYCFCAAQYycgIAANgIQIABBBzYCDEEAIRAMngELIABBADYCHCAAIBQ2AhQgAEH+kYCAADYCECAAQQc2AgxBACEQDJ0BCyAAQQA2AhwgACABNgIUIABBjpuAgAA2AhAgAEEGNgIMQQAhEAycAQsgEEEVRg1XIABBADYCHCAAIAE2AhQgAEHMjoCAADYCECAAQSA2AgxBACEQDJsBCyAAQQA2AgAgEEEBaiEBQSQhEAsgACAQOgApIAAoAgQhECAAQQA2AgQgACAQIAEQq4CAgAAiEA1UIAEhAQw+CyAAQQA2AgALQQAhECAAQQA2AhwgACAENgIUIABB8ZuAgAA2AhAgAEEGNgIMDJcBCyABQRVGDVAgAEEANgIcIAAgBTYCFCAAQfCMgIAANgIQIABBGzYCDEEAIRAMlgELIAAoAgQhBSAAQQA2AgQgACAFIBAQqYCAgAAiBQ0BIBBBAWohBQtBrQEhEAx7CyAAQcEBNgIcIAAgBTYCDCAAIBBBAWo2AhRBACEQDJMBCyAAKAIEIQYgAEEANgIEIAAgBiAQEKmAgIAAIgYNASAQQQFqIQYLQa4BIRAMeAsgAEHCATYCHCAAIAY2AgwgACAQQQFqNgIUQQAhEAyQAQsgAEEANgIcIAAgBzYCFCAAQZeLgIAANgIQIABBDTYCDEEAIRAMjwELIABBADYCHCAAIAg2AhQgAEHjkICAADYCECAAQQk2AgxBACEQDI4BCyAAQQA2AhwgACAINgIUIABBlI2AgAA2AhAgAEEhNgIMQQAhEAyNAQtBASEWQQAhF0EAIRRBASEQCyAAIBA6ACsgCUEBaiEIAkACQCAALQAtQRBxDQACQAJAAkAgAC0AKg4DAQACBAsgFkUNAwwCCyAUDQEMAgsgF0UNAQsgACgCBCEQIABBADYCBCAAIBAgCBCtgICAACIQRQ09IABByQE2AhwgACAINgIUIAAgEDYCDEEAIRAMjAELIAAoAgQhBCAAQQA2AgQgACAEIAgQrYCAgAAiBEUNdiAAQcoBNgIcIAAgCDYCFCAAIAQ2AgxBACEQDIsBCyAAKAIEIQQgAEEANgIEIAAgBCAJEK2AgIAAIgRFDXQgAEHLATYCHCAAIAk2AhQgACAENgIMQQAhEAyKAQsgACgCBCEEIABBADYCBCAAIAQgChCtgICAACIERQ1yIABBzQE2AhwgACAKNgIUIAAgBDYCDEEAIRAMiQELAkAgCy0AAEFQaiIQQf8BcUEKTw0AIAAgEDoAKiALQQFqIQpBtgEhEAxwCyAAKAIEIQQgAEEANgIEIAAgBCALEK2AgIAAIgRFDXAgAEHPATYCHCAAIAs2AhQgACAENgIMQQAhEAyIAQsgAEEANgIcIAAgBDYCFCAAQZCzgIAANgIQIABBCDYCDCAAQQA2AgBBACEQDIcBCyABQRVGDT8gAEEANgIcIAAgDDYCFCAAQcyOgIAANgIQIABBIDYCDEEAIRAMhgELIABBgQQ7ASggACgCBCEQIABCADcDACAAIBAgDEEBaiIMEKuAgIAAIhBFDTggAEHTATYCHCAAIAw2AhQgACAQNgIMQQAhEAyFAQsgAEEANgIAC0EAIRAgAEEANgIcIAAgBDYCFCAAQdibgIAANgIQIABBCDYCDAyDAQsgACgCBCEQIABCADcDACAAIBAgC0EBaiILEKuAgIAAIhANAUHGASEQDGkLIABBAjoAKAxVCyAAQdUBNgIcIAAgCzYCFCAAIBA2AgxBACEQDIABCyAQQRVGDTcgAEEANgIcIAAgBDYCFCAAQaSMgIAANgIQIABBEDYCDEEAIRAMfwsgAC0ANEEBRw00IAAgBCACELyAgIAAIhBFDTQgEEEVRw01IABB3AE2AhwgACAENgIUIABB1ZaAgAA2AhAgAEEVNgIMQQAhEAx+C0EAIRAgAEEANgIcIABBr4uAgAA2AhAgAEECNgIMIAAgFEEBajYCFAx9C0EAIRAMYwtBAiEQDGILQQ0hEAxhC0EPIRAMYAtBJSEQDF8LQRMhEAxeC0EVIRAMXQtBFiEQDFwLQRchEAxbC0EYIRAMWgtBGSEQDFkLQRohEAxYC0EbIRAMVwtBHCEQDFYLQR0hEAxVC0EfIRAMVAtBISEQDFMLQSMhEAxSC0HGACEQDFELQS4hEAxQC0EvIRAMTwtBOyEQDE4LQT0hEAxNC0HIACEQDEwLQckAIRAMSwtBywAhEAxKC0HMACEQDEkLQc4AIRAMSAtB0QAhEAxHC0HVACEQDEYLQdgAIRAMRQtB2QAhEAxEC0HbACEQDEMLQeQAIRAMQgtB5QAhEAxBC0HxACEQDEALQfQAIRAMPwtBjQEhEAw+C0GXASEQDD0LQakBIRAMPAtBrAEhEAw7C0HAASEQDDoLQbkBIRAMOQtBrwEhEAw4C0GxASEQDDcLQbIBIRAMNgtBtAEhEAw1C0G1ASEQDDQLQboBIRAMMwtBvQEhEAwyC0G/ASEQDDELQcEBIRAMMAsgAEEANgIcIAAgBDYCFCAAQemLgIAANgIQIABBHzYCDEEAIRAMSAsgAEHbATYCHCAAIAQ2AhQgAEH6loCAADYCECAAQRU2AgxBACEQDEcLIABB+AA2AhwgACAMNgIUIABBypiAgAA2AhAgAEEVNgIMQQAhEAxGCyAAQdEANgIcIAAgBTYCFCAAQbCXgIAANgIQIABBFTYCDEEAIRAMRQsgAEH5ADYCHCAAIAE2AhQgACAQNgIMQQAhEAxECyAAQfgANgIcIAAgATYCFCAAQcqYgIAANgIQIABBFTYCDEEAIRAMQwsgAEHkADYCHCAAIAE2AhQgAEHjl4CAADYCECAAQRU2AgxBACEQDEILIABB1wA2AhwgACABNgIUIABByZeAgAA2AhAgAEEVNgIMQQAhEAxBCyAAQQA2AhwgACABNgIUIABBuY2AgAA2AhAgAEEaNgIMQQAhEAxACyAAQcIANgIcIAAgATYCFCAAQeOYgIAANgIQIABBFTYCDEEAIRAMPwsgAEEANgIEIAAgDyAPELGAgIAAIgRFDQEgAEE6NgIcIAAgBDYCDCAAIA9BAWo2AhRBACEQDD4LIAAoAgQhBCAAQQA2AgQCQCAAIAQgARCxgICAACIERQ0AIABBOzYCHCAAIAQ2AgwgACABQQFqNgIUQQAhEAw+CyABQQFqIQEMLQsgD0EBaiEBDC0LIABBADYCHCAAIA82AhQgAEHkkoCAADYCECAAQQQ2AgxBACEQDDsLIABBNjYCHCAAIAQ2AhQgACACNgIMQQAhEAw6CyAAQS42AhwgACAONgIUIAAgBDYCDEEAIRAMOQsgAEHQADYCHCAAIAE2AhQgAEGRmICAADYCECAAQRU2AgxBACEQDDgLIA1BAWohAQwsCyAAQRU2AhwgACABNgIUIABBgpmAgAA2AhAgAEEVNgIMQQAhEAw2CyAAQRs2AhwgACABNgIUIABBkZeAgAA2AhAgAEEVNgIMQQAhEAw1CyAAQQ82AhwgACABNgIUIABBkZeAgAA2AhAgAEEVNgIMQQAhEAw0CyAAQQs2AhwgACABNgIUIABBkZeAgAA2AhAgAEEVNgIMQQAhEAwzCyAAQRo2AhwgACABNgIUIABBgpmAgAA2AhAgAEEVNgIMQQAhEAwyCyAAQQs2AhwgACABNgIUIABBgpmAgAA2AhAgAEEVNgIMQQAhEAwxCyAAQQo2AhwgACABNgIUIABB5JaAgAA2AhAgAEEVNgIMQQAhEAwwCyAAQR42AhwgACABNgIUIABB+ZeAgAA2AhAgAEEVNgIMQQAhEAwvCyAAQQA2AhwgACAQNgIUIABB2o2AgAA2AhAgAEEUNgIMQQAhEAwuCyAAQQQ2AhwgACABNgIUIABBsJiAgAA2AhAgAEEVNgIMQQAhEAwtCyAAQQA2AgAgC0EBaiELC0G4ASEQDBILIABBADYCACAQQQFqIQFB9QAhEAwRCyABIQECQCAALQApQQVHDQBB4wAhEAwRC0HiACEQDBALQQAhECAAQQA2AhwgAEHkkYCAADYCECAAQQc2AgwgACAUQQFqNgIUDCgLIABBADYCACAXQQFqIQFBwAAhEAwOC0EBIQELIAAgAToALCAAQQA2AgAgF0EBaiEBC0EoIRAMCwsgASEBC0E4IRAMCQsCQCABIg8gAkYNAANAAkAgDy0AAEGAvoCAAGotAAAiAUEBRg0AIAFBAkcNAyAPQQFqIQEMBAsgD0EBaiIPIAJHDQALQT4hEAwiC0E+IRAMIQsgAEEAOgAsIA8hAQwBC0ELIRAMBgtBOiEQDAULIAFBAWohAUEtIRAMBAsgACABOgAsIABBADYCACAWQQFqIQFBDCEQDAMLIABBADYCACAXQQFqIQFBCiEQDAILIABBADYCAAsgAEEAOgAsIA0hAUEJIRAMAAsLQQAhECAAQQA2AhwgACALNgIUIABBzZCAgAA2AhAgAEEJNgIMDBcLQQAhECAAQQA2AhwgACAKNgIUIABB6YqAgAA2AhAgAEEJNgIMDBYLQQAhECAAQQA2AhwgACAJNgIUIABBt5CAgAA2AhAgAEEJNgIMDBULQQAhECAAQQA2AhwgACAINgIUIABBnJGAgAA2AhAgAEEJNgIMDBQLQQAhECAAQQA2AhwgACABNgIUIABBzZCAgAA2AhAgAEEJNgIMDBMLQQAhECAAQQA2AhwgACABNgIUIABB6YqAgAA2AhAgAEEJNgIMDBILQQAhECAAQQA2AhwgACABNgIUIABBt5CAgAA2AhAgAEEJNgIMDBELQQAhECAAQQA2AhwgACABNgIUIABBnJGAgAA2AhAgAEEJNgIMDBALQQAhECAAQQA2AhwgACABNgIUIABBl5WAgAA2AhAgAEEPNgIMDA8LQQAhECAAQQA2AhwgACABNgIUIABBl5WAgAA2AhAgAEEPNgIMDA4LQQAhECAAQQA2AhwgACABNgIUIABBwJKAgAA2AhAgAEELNgIMDA0LQQAhECAAQQA2AhwgACABNgIUIABBlYmAgAA2AhAgAEELNgIMDAwLQQAhECAAQQA2AhwgACABNgIUIABB4Y+AgAA2AhAgAEEKNgIMDAsLQQAhECAAQQA2AhwgACABNgIUIABB+4+AgAA2AhAgAEEKNgIMDAoLQQAhECAAQQA2AhwgACABNgIUIABB8ZmAgAA2AhAgAEECNgIMDAkLQQAhECAAQQA2AhwgACABNgIUIABBxJSAgAA2AhAgAEECNgIMDAgLQQAhECAAQQA2AhwgACABNgIUIABB8pWAgAA2AhAgAEECNgIMDAcLIABBAjYCHCAAIAE2AhQgAEGcmoCAADYCECAAQRY2AgxBACEQDAYLQQEhEAwFC0HUACEQIAEiBCACRg0EIANBCGogACAEIAJB2MKAgABBChDFgICAACADKAIMIQQgAygCCA4DAQQCAAsQyoCAgAAACyAAQQA2AhwgAEG1moCAADYCECAAQRc2AgwgACAEQQFqNgIUQQAhEAwCCyAAQQA2AhwgACAENgIUIABBypqAgAA2AhAgAEEJNgIMQQAhEAwBCwJAIAEiBCACRw0AQSIhEAwBCyAAQYmAgIAANgIIIAAgBDYCBEEhIRALIANBEGokgICAgAAgEAuvAQECfyABKAIAIQYCQAJAIAIgA0YNACAEIAZqIQQgBiADaiACayEHIAIgBkF/cyAFaiIGaiEFA0ACQCACLQAAIAQtAABGDQBBAiEEDAMLAkAgBg0AQQAhBCAFIQIMAwsgBkF/aiEGIARBAWohBCACQQFqIgIgA0cNAAsgByEGIAMhAgsgAEEBNgIAIAEgBjYCACAAIAI2AgQPCyABQQA2AgAgACAENgIAIAAgAjYCBAsKACAAEMeAgIAAC/I2AQt/I4CAgIAAQRBrIgEkgICAgAACQEEAKAKg0ICAAA0AQQAQy4CAgABBgNSEgABrIgJB2QBJDQBBACEDAkBBACgC4NOAgAAiBA0AQQBCfzcC7NOAgABBAEKAgISAgIDAADcC5NOAgABBACABQQhqQXBxQdiq1aoFcyIENgLg04CAAEEAQQA2AvTTgIAAQQBBADYCxNOAgAALQQAgAjYCzNOAgABBAEGA1ISAADYCyNOAgABBAEGA1ISAADYCmNCAgABBACAENgKs0ICAAEEAQX82AqjQgIAAA0AgA0HE0ICAAGogA0G40ICAAGoiBDYCACAEIANBsNCAgABqIgU2AgAgA0G80ICAAGogBTYCACADQczQgIAAaiADQcDQgIAAaiIFNgIAIAUgBDYCACADQdTQgIAAaiADQcjQgIAAaiIENgIAIAQgBTYCACADQdDQgIAAaiAENgIAIANBIGoiA0GAAkcNAAtBgNSEgABBeEGA1ISAAGtBD3FBAEGA1ISAAEEIakEPcRsiA2oiBEEEaiACQUhqIgUgA2siA0EBcjYCAEEAQQAoAvDTgIAANgKk0ICAAEEAIAM2ApTQgIAAQQAgBDYCoNCAgABBgNSEgAAgBWpBODYCBAsCQAJAAkACQAJAAkACQAJAAkACQAJAAkAgAEHsAUsNAAJAQQAoAojQgIAAIgZBECAAQRNqQXBxIABBC0kbIgJBA3YiBHYiA0EDcUUNAAJAAkAgA0EBcSAEckEBcyIFQQN0IgRBsNCAgABqIgMgBEG40ICAAGooAgAiBCgCCCICRw0AQQAgBkF+IAV3cTYCiNCAgAAMAQsgAyACNgIIIAIgAzYCDAsgBEEIaiEDIAQgBUEDdCIFQQNyNgIEIAQgBWoiBCAEKAIEQQFyNgIEDAwLIAJBACgCkNCAgAAiB00NAQJAIANFDQACQAJAIAMgBHRBAiAEdCIDQQAgA2tycSIDQQAgA2txQX9qIgMgA0EMdkEQcSIDdiIEQQV2QQhxIgUgA3IgBCAFdiIDQQJ2QQRxIgRyIAMgBHYiA0EBdkECcSIEciADIAR2IgNBAXZBAXEiBHIgAyAEdmoiBEEDdCIDQbDQgIAAaiIFIANBuNCAgABqKAIAIgMoAggiAEcNAEEAIAZBfiAEd3EiBjYCiNCAgAAMAQsgBSAANgIIIAAgBTYCDAsgAyACQQNyNgIEIAMgBEEDdCIEaiAEIAJrIgU2AgAgAyACaiIAIAVBAXI2AgQCQCAHRQ0AIAdBeHFBsNCAgABqIQJBACgCnNCAgAAhBAJAAkAgBkEBIAdBA3Z0IghxDQBBACAGIAhyNgKI0ICAACACIQgMAQsgAigCCCEICyAIIAQ2AgwgAiAENgIIIAQgAjYCDCAEIAg2AggLIANBCGohA0EAIAA2ApzQgIAAQQAgBTYCkNCAgAAMDAtBACgCjNCAgAAiCUUNASAJQQAgCWtxQX9qIgMgA0EMdkEQcSIDdiIEQQV2QQhxIgUgA3IgBCAFdiIDQQJ2QQRxIgRyIAMgBHYiA0EBdkECcSIEciADIAR2IgNBAXZBAXEiBHIgAyAEdmpBAnRBuNKAgABqKAIAIgAoAgRBeHEgAmshBCAAIQUCQANAAkAgBSgCECIDDQAgBUEUaigCACIDRQ0CCyADKAIEQXhxIAJrIgUgBCAFIARJIgUbIQQgAyAAIAUbIQAgAyEFDAALCyAAKAIYIQoCQCAAKAIMIgggAEYNACAAKAIIIgNBACgCmNCAgABJGiAIIAM2AgggAyAINgIMDAsLAkAgAEEUaiIFKAIAIgMNACAAKAIQIgNFDQMgAEEQaiEFCwNAIAUhCyADIghBFGoiBSgCACIDDQAgCEEQaiEFIAgoAhAiAw0ACyALQQA2AgAMCgtBfyECIABBv39LDQAgAEETaiIDQXBxIQJBACgCjNCAgAAiB0UNAEEAIQsCQCACQYACSQ0AQR8hCyACQf///wdLDQAgA0EIdiIDIANBgP4/akEQdkEIcSIDdCIEIARBgOAfakEQdkEEcSIEdCIFIAVBgIAPakEQdkECcSIFdEEPdiADIARyIAVyayIDQQF0IAIgA0EVanZBAXFyQRxqIQsLQQAgAmshBAJAAkACQAJAIAtBAnRBuNKAgABqKAIAIgUNAEEAIQNBACEIDAELQQAhAyACQQBBGSALQQF2ayALQR9GG3QhAEEAIQgDQAJAIAUoAgRBeHEgAmsiBiAETw0AIAYhBCAFIQggBg0AQQAhBCAFIQggBSEDDAMLIAMgBUEUaigCACIGIAYgBSAAQR12QQRxakEQaigCACIFRhsgAyAGGyEDIABBAXQhACAFDQALCwJAIAMgCHINAEEAIQhBAiALdCIDQQAgA2tyIAdxIgNFDQMgA0EAIANrcUF/aiIDIANBDHZBEHEiA3YiBUEFdkEIcSIAIANyIAUgAHYiA0ECdkEEcSIFciADIAV2IgNBAXZBAnEiBXIgAyAFdiIDQQF2QQFxIgVyIAMgBXZqQQJ0QbjSgIAAaigCACEDCyADRQ0BCwNAIAMoAgRBeHEgAmsiBiAESSEAAkAgAygCECIFDQAgA0EUaigCACEFCyAGIAQgABshBCADIAggABshCCAFIQMgBQ0ACwsgCEUNACAEQQAoApDQgIAAIAJrTw0AIAgoAhghCwJAIAgoAgwiACAIRg0AIAgoAggiA0EAKAKY0ICAAEkaIAAgAzYCCCADIAA2AgwMCQsCQCAIQRRqIgUoAgAiAw0AIAgoAhAiA0UNAyAIQRBqIQULA0AgBSEGIAMiAEEUaiIFKAIAIgMNACAAQRBqIQUgACgCECIDDQALIAZBADYCAAwICwJAQQAoApDQgIAAIgMgAkkNAEEAKAKc0ICAACEEAkACQCADIAJrIgVBEEkNACAEIAJqIgAgBUEBcjYCBEEAIAU2ApDQgIAAQQAgADYCnNCAgAAgBCADaiAFNgIAIAQgAkEDcjYCBAwBCyAEIANBA3I2AgQgBCADaiIDIAMoAgRBAXI2AgRBAEEANgKc0ICAAEEAQQA2ApDQgIAACyAEQQhqIQMMCgsCQEEAKAKU0ICAACIAIAJNDQBBACgCoNCAgAAiAyACaiIEIAAgAmsiBUEBcjYCBEEAIAU2ApTQgIAAQQAgBDYCoNCAgAAgAyACQQNyNgIEIANBCGohAwwKCwJAAkBBACgC4NOAgABFDQBBACgC6NOAgAAhBAwBC0EAQn83AuzTgIAAQQBCgICEgICAwAA3AuTTgIAAQQAgAUEMakFwcUHYqtWqBXM2AuDTgIAAQQBBADYC9NOAgABBAEEANgLE04CAAEGAgAQhBAtBACEDAkAgBCACQccAaiIHaiIGQQAgBGsiC3EiCCACSw0AQQBBMDYC+NOAgAAMCgsCQEEAKALA04CAACIDRQ0AAkBBACgCuNOAgAAiBCAIaiIFIARNDQAgBSADTQ0BC0EAIQNBAEEwNgL404CAAAwKC0EALQDE04CAAEEEcQ0EAkACQAJAQQAoAqDQgIAAIgRFDQBByNOAgAAhAwNAAkAgAygCACIFIARLDQAgBSADKAIEaiAESw0DCyADKAIIIgMNAAsLQQAQy4CAgAAiAEF/Rg0FIAghBgJAQQAoAuTTgIAAIgNBf2oiBCAAcUUNACAIIABrIAQgAGpBACADa3FqIQYLIAYgAk0NBSAGQf7///8HSw0FAkBBACgCwNOAgAAiA0UNAEEAKAK404CAACIEIAZqIgUgBE0NBiAFIANLDQYLIAYQy4CAgAAiAyAARw0BDAcLIAYgAGsgC3EiBkH+////B0sNBCAGEMuAgIAAIgAgAygCACADKAIEakYNAyAAIQMLAkAgA0F/Rg0AIAJByABqIAZNDQACQCAHIAZrQQAoAujTgIAAIgRqQQAgBGtxIgRB/v///wdNDQAgAyEADAcLAkAgBBDLgICAAEF/Rg0AIAQgBmohBiADIQAMBwtBACAGaxDLgICAABoMBAsgAyEAIANBf0cNBQwDC0EAIQgMBwtBACEADAULIABBf0cNAgtBAEEAKALE04CAAEEEcjYCxNOAgAALIAhB/v///wdLDQEgCBDLgICAACEAQQAQy4CAgAAhAyAAQX9GDQEgA0F/Rg0BIAAgA08NASADIABrIgYgAkE4ak0NAQtBAEEAKAK404CAACAGaiIDNgK404CAAAJAIANBACgCvNOAgABNDQBBACADNgK804CAAAsCQAJAAkACQEEAKAKg0ICAACIERQ0AQcjTgIAAIQMDQCAAIAMoAgAiBSADKAIEIghqRg0CIAMoAggiAw0ADAMLCwJAAkBBACgCmNCAgAAiA0UNACAAIANPDQELQQAgADYCmNCAgAALQQAhA0EAIAY2AszTgIAAQQAgADYCyNOAgABBAEF/NgKo0ICAAEEAQQAoAuDTgIAANgKs0ICAAEEAQQA2AtTTgIAAA0AgA0HE0ICAAGogA0G40ICAAGoiBDYCACAEIANBsNCAgABqIgU2AgAgA0G80ICAAGogBTYCACADQczQgIAAaiADQcDQgIAAaiIFNgIAIAUgBDYCACADQdTQgIAAaiADQcjQgIAAaiIENgIAIAQgBTYCACADQdDQgIAAaiAENgIAIANBIGoiA0GAAkcNAAsgAEF4IABrQQ9xQQAgAEEIakEPcRsiA2oiBCAGQUhqIgUgA2siA0EBcjYCBEEAQQAoAvDTgIAANgKk0ICAAEEAIAM2ApTQgIAAQQAgBDYCoNCAgAAgACAFakE4NgIEDAILIAMtAAxBCHENACAEIAVJDQAgBCAATw0AIARBeCAEa0EPcUEAIARBCGpBD3EbIgVqIgBBACgClNCAgAAgBmoiCyAFayIFQQFyNgIEIAMgCCAGajYCBEEAQQAoAvDTgIAANgKk0ICAAEEAIAU2ApTQgIAAQQAgADYCoNCAgAAgBCALakE4NgIEDAELAkAgAEEAKAKY0ICAACIITw0AQQAgADYCmNCAgAAgACEICyAAIAZqIQVByNOAgAAhAwJAAkACQAJAAkACQAJAA0AgAygCACAFRg0BIAMoAggiAw0ADAILCyADLQAMQQhxRQ0BC0HI04CAACEDA0ACQCADKAIAIgUgBEsNACAFIAMoAgRqIgUgBEsNAwsgAygCCCEDDAALCyADIAA2AgAgAyADKAIEIAZqNgIEIABBeCAAa0EPcUEAIABBCGpBD3EbaiILIAJBA3I2AgQgBUF4IAVrQQ9xQQAgBUEIakEPcRtqIgYgCyACaiICayEDAkAgBiAERw0AQQAgAjYCoNCAgABBAEEAKAKU0ICAACADaiIDNgKU0ICAACACIANBAXI2AgQMAwsCQCAGQQAoApzQgIAARw0AQQAgAjYCnNCAgABBAEEAKAKQ0ICAACADaiIDNgKQ0ICAACACIANBAXI2AgQgAiADaiADNgIADAMLAkAgBigCBCIEQQNxQQFHDQAgBEF4cSEHAkACQCAEQf8BSw0AIAYoAggiBSAEQQN2IghBA3RBsNCAgABqIgBGGgJAIAYoAgwiBCAFRw0AQQBBACgCiNCAgABBfiAId3E2AojQgIAADAILIAQgAEYaIAQgBTYCCCAFIAQ2AgwMAQsgBigCGCEJAkACQCAGKAIMIgAgBkYNACAGKAIIIgQgCEkaIAAgBDYCCCAEIAA2AgwMAQsCQCAGQRRqIgQoAgAiBQ0AIAZBEGoiBCgCACIFDQBBACEADAELA0AgBCEIIAUiAEEUaiIEKAIAIgUNACAAQRBqIQQgACgCECIFDQALIAhBADYCAAsgCUUNAAJAAkAgBiAGKAIcIgVBAnRBuNKAgABqIgQoAgBHDQAgBCAANgIAIAANAUEAQQAoAozQgIAAQX4gBXdxNgKM0ICAAAwCCyAJQRBBFCAJKAIQIAZGG2ogADYCACAARQ0BCyAAIAk2AhgCQCAGKAIQIgRFDQAgACAENgIQIAQgADYCGAsgBigCFCIERQ0AIABBFGogBDYCACAEIAA2AhgLIAcgA2ohAyAGIAdqIgYoAgQhBAsgBiAEQX5xNgIEIAIgA2ogAzYCACACIANBAXI2AgQCQCADQf8BSw0AIANBeHFBsNCAgABqIQQCQAJAQQAoAojQgIAAIgVBASADQQN2dCIDcQ0AQQAgBSADcjYCiNCAgAAgBCEDDAELIAQoAgghAwsgAyACNgIMIAQgAjYCCCACIAQ2AgwgAiADNgIIDAMLQR8hBAJAIANB////B0sNACADQQh2IgQgBEGA/j9qQRB2QQhxIgR0IgUgBUGA4B9qQRB2QQRxIgV0IgAgAEGAgA9qQRB2QQJxIgB0QQ92IAQgBXIgAHJrIgRBAXQgAyAEQRVqdkEBcXJBHGohBAsgAiAENgIcIAJCADcCECAEQQJ0QbjSgIAAaiEFAkBBACgCjNCAgAAiAEEBIAR0IghxDQAgBSACNgIAQQAgACAIcjYCjNCAgAAgAiAFNgIYIAIgAjYCCCACIAI2AgwMAwsgA0EAQRkgBEEBdmsgBEEfRht0IQQgBSgCACEAA0AgACIFKAIEQXhxIANGDQIgBEEddiEAIARBAXQhBCAFIABBBHFqQRBqIggoAgAiAA0ACyAIIAI2AgAgAiAFNgIYIAIgAjYCDCACIAI2AggMAgsgAEF4IABrQQ9xQQAgAEEIakEPcRsiA2oiCyAGQUhqIgggA2siA0EBcjYCBCAAIAhqQTg2AgQgBCAFQTcgBWtBD3FBACAFQUlqQQ9xG2pBQWoiCCAIIARBEGpJGyIIQSM2AgRBAEEAKALw04CAADYCpNCAgABBACADNgKU0ICAAEEAIAs2AqDQgIAAIAhBEGpBACkC0NOAgAA3AgAgCEEAKQLI04CAADcCCEEAIAhBCGo2AtDTgIAAQQAgBjYCzNOAgABBACAANgLI04CAAEEAQQA2AtTTgIAAIAhBJGohAwNAIANBBzYCACADQQRqIgMgBUkNAAsgCCAERg0DIAggCCgCBEF+cTYCBCAIIAggBGsiADYCACAEIABBAXI2AgQCQCAAQf8BSw0AIABBeHFBsNCAgABqIQMCQAJAQQAoAojQgIAAIgVBASAAQQN2dCIAcQ0AQQAgBSAAcjYCiNCAgAAgAyEFDAELIAMoAgghBQsgBSAENgIMIAMgBDYCCCAEIAM2AgwgBCAFNgIIDAQLQR8hAwJAIABB////B0sNACAAQQh2IgMgA0GA/j9qQRB2QQhxIgN0IgUgBUGA4B9qQRB2QQRxIgV0IgggCEGAgA9qQRB2QQJxIgh0QQ92IAMgBXIgCHJrIgNBAXQgACADQRVqdkEBcXJBHGohAwsgBCADNgIcIARCADcCECADQQJ0QbjSgIAAaiEFAkBBACgCjNCAgAAiCEEBIAN0IgZxDQAgBSAENgIAQQAgCCAGcjYCjNCAgAAgBCAFNgIYIAQgBDYCCCAEIAQ2AgwMBAsgAEEAQRkgA0EBdmsgA0EfRht0IQMgBSgCACEIA0AgCCIFKAIEQXhxIABGDQMgA0EddiEIIANBAXQhAyAFIAhBBHFqQRBqIgYoAgAiCA0ACyAGIAQ2AgAgBCAFNgIYIAQgBDYCDCAEIAQ2AggMAwsgBSgCCCIDIAI2AgwgBSACNgIIIAJBADYCGCACIAU2AgwgAiADNgIICyALQQhqIQMMBQsgBSgCCCIDIAQ2AgwgBSAENgIIIARBADYCGCAEIAU2AgwgBCADNgIIC0EAKAKU0ICAACIDIAJNDQBBACgCoNCAgAAiBCACaiIFIAMgAmsiA0EBcjYCBEEAIAM2ApTQgIAAQQAgBTYCoNCAgAAgBCACQQNyNgIEIARBCGohAwwDC0EAIQNBAEEwNgL404CAAAwCCwJAIAtFDQACQAJAIAggCCgCHCIFQQJ0QbjSgIAAaiIDKAIARw0AIAMgADYCACAADQFBACAHQX4gBXdxIgc2AozQgIAADAILIAtBEEEUIAsoAhAgCEYbaiAANgIAIABFDQELIAAgCzYCGAJAIAgoAhAiA0UNACAAIAM2AhAgAyAANgIYCyAIQRRqKAIAIgNFDQAgAEEUaiADNgIAIAMgADYCGAsCQAJAIARBD0sNACAIIAQgAmoiA0EDcjYCBCAIIANqIgMgAygCBEEBcjYCBAwBCyAIIAJqIgAgBEEBcjYCBCAIIAJBA3I2AgQgACAEaiAENgIAAkAgBEH/AUsNACAEQXhxQbDQgIAAaiEDAkACQEEAKAKI0ICAACIFQQEgBEEDdnQiBHENAEEAIAUgBHI2AojQgIAAIAMhBAwBCyADKAIIIQQLIAQgADYCDCADIAA2AgggACADNgIMIAAgBDYCCAwBC0EfIQMCQCAEQf///wdLDQAgBEEIdiIDIANBgP4/akEQdkEIcSIDdCIFIAVBgOAfakEQdkEEcSIFdCICIAJBgIAPakEQdkECcSICdEEPdiADIAVyIAJyayIDQQF0IAQgA0EVanZBAXFyQRxqIQMLIAAgAzYCHCAAQgA3AhAgA0ECdEG40oCAAGohBQJAIAdBASADdCICcQ0AIAUgADYCAEEAIAcgAnI2AozQgIAAIAAgBTYCGCAAIAA2AgggACAANgIMDAELIARBAEEZIANBAXZrIANBH0YbdCEDIAUoAgAhAgJAA0AgAiIFKAIEQXhxIARGDQEgA0EddiECIANBAXQhAyAFIAJBBHFqQRBqIgYoAgAiAg0ACyAGIAA2AgAgACAFNgIYIAAgADYCDCAAIAA2AggMAQsgBSgCCCIDIAA2AgwgBSAANgIIIABBADYCGCAAIAU2AgwgACADNgIICyAIQQhqIQMMAQsCQCAKRQ0AAkACQCAAIAAoAhwiBUECdEG40oCAAGoiAygCAEcNACADIAg2AgAgCA0BQQAgCUF+IAV3cTYCjNCAgAAMAgsgCkEQQRQgCigCECAARhtqIAg2AgAgCEUNAQsgCCAKNgIYAkAgACgCECIDRQ0AIAggAzYCECADIAg2AhgLIABBFGooAgAiA0UNACAIQRRqIAM2AgAgAyAINgIYCwJAAkAgBEEPSw0AIAAgBCACaiIDQQNyNgIEIAAgA2oiAyADKAIEQQFyNgIEDAELIAAgAmoiBSAEQQFyNgIEIAAgAkEDcjYCBCAFIARqIAQ2AgACQCAHRQ0AIAdBeHFBsNCAgABqIQJBACgCnNCAgAAhAwJAAkBBASAHQQN2dCIIIAZxDQBBACAIIAZyNgKI0ICAACACIQgMAQsgAigCCCEICyAIIAM2AgwgAiADNgIIIAMgAjYCDCADIAg2AggLQQAgBTYCnNCAgABBACAENgKQ0ICAAAsgAEEIaiEDCyABQRBqJICAgIAAIAMLCgAgABDJgICAAAviDQEHfwJAIABFDQAgAEF4aiIBIABBfGooAgAiAkF4cSIAaiEDAkAgAkEBcQ0AIAJBA3FFDQEgASABKAIAIgJrIgFBACgCmNCAgAAiBEkNASACIABqIQACQCABQQAoApzQgIAARg0AAkAgAkH/AUsNACABKAIIIgQgAkEDdiIFQQN0QbDQgIAAaiIGRhoCQCABKAIMIgIgBEcNAEEAQQAoAojQgIAAQX4gBXdxNgKI0ICAAAwDCyACIAZGGiACIAQ2AgggBCACNgIMDAILIAEoAhghBwJAAkAgASgCDCIGIAFGDQAgASgCCCICIARJGiAGIAI2AgggAiAGNgIMDAELAkAgAUEUaiICKAIAIgQNACABQRBqIgIoAgAiBA0AQQAhBgwBCwNAIAIhBSAEIgZBFGoiAigCACIEDQAgBkEQaiECIAYoAhAiBA0ACyAFQQA2AgALIAdFDQECQAJAIAEgASgCHCIEQQJ0QbjSgIAAaiICKAIARw0AIAIgBjYCACAGDQFBAEEAKAKM0ICAAEF+IAR3cTYCjNCAgAAMAwsgB0EQQRQgBygCECABRhtqIAY2AgAgBkUNAgsgBiAHNgIYAkAgASgCECICRQ0AIAYgAjYCECACIAY2AhgLIAEoAhQiAkUNASAGQRRqIAI2AgAgAiAGNgIYDAELIAMoAgQiAkEDcUEDRw0AIAMgAkF+cTYCBEEAIAA2ApDQgIAAIAEgAGogADYCACABIABBAXI2AgQPCyABIANPDQAgAygCBCICQQFxRQ0AAkACQCACQQJxDQACQCADQQAoAqDQgIAARw0AQQAgATYCoNCAgABBAEEAKAKU0ICAACAAaiIANgKU0ICAACABIABBAXI2AgQgAUEAKAKc0ICAAEcNA0EAQQA2ApDQgIAAQQBBADYCnNCAgAAPCwJAIANBACgCnNCAgABHDQBBACABNgKc0ICAAEEAQQAoApDQgIAAIABqIgA2ApDQgIAAIAEgAEEBcjYCBCABIABqIAA2AgAPCyACQXhxIABqIQACQAJAIAJB/wFLDQAgAygCCCIEIAJBA3YiBUEDdEGw0ICAAGoiBkYaAkAgAygCDCICIARHDQBBAEEAKAKI0ICAAEF+IAV3cTYCiNCAgAAMAgsgAiAGRhogAiAENgIIIAQgAjYCDAwBCyADKAIYIQcCQAJAIAMoAgwiBiADRg0AIAMoAggiAkEAKAKY0ICAAEkaIAYgAjYCCCACIAY2AgwMAQsCQCADQRRqIgIoAgAiBA0AIANBEGoiAigCACIEDQBBACEGDAELA0AgAiEFIAQiBkEUaiICKAIAIgQNACAGQRBqIQIgBigCECIEDQALIAVBADYCAAsgB0UNAAJAAkAgAyADKAIcIgRBAnRBuNKAgABqIgIoAgBHDQAgAiAGNgIAIAYNAUEAQQAoAozQgIAAQX4gBHdxNgKM0ICAAAwCCyAHQRBBFCAHKAIQIANGG2ogBjYCACAGRQ0BCyAGIAc2AhgCQCADKAIQIgJFDQAgBiACNgIQIAIgBjYCGAsgAygCFCICRQ0AIAZBFGogAjYCACACIAY2AhgLIAEgAGogADYCACABIABBAXI2AgQgAUEAKAKc0ICAAEcNAUEAIAA2ApDQgIAADwsgAyACQX5xNgIEIAEgAGogADYCACABIABBAXI2AgQLAkAgAEH/AUsNACAAQXhxQbDQgIAAaiECAkACQEEAKAKI0ICAACIEQQEgAEEDdnQiAHENAEEAIAQgAHI2AojQgIAAIAIhAAwBCyACKAIIIQALIAAgATYCDCACIAE2AgggASACNgIMIAEgADYCCA8LQR8hAgJAIABB////B0sNACAAQQh2IgIgAkGA/j9qQRB2QQhxIgJ0IgQgBEGA4B9qQRB2QQRxIgR0IgYgBkGAgA9qQRB2QQJxIgZ0QQ92IAIgBHIgBnJrIgJBAXQgACACQRVqdkEBcXJBHGohAgsgASACNgIcIAFCADcCECACQQJ0QbjSgIAAaiEEAkACQEEAKAKM0ICAACIGQQEgAnQiA3ENACAEIAE2AgBBACAGIANyNgKM0ICAACABIAQ2AhggASABNgIIIAEgATYCDAwBCyAAQQBBGSACQQF2ayACQR9GG3QhAiAEKAIAIQYCQANAIAYiBCgCBEF4cSAARg0BIAJBHXYhBiACQQF0IQIgBCAGQQRxakEQaiIDKAIAIgYNAAsgAyABNgIAIAEgBDYCGCABIAE2AgwgASABNgIIDAELIAQoAggiACABNgIMIAQgATYCCCABQQA2AhggASAENgIMIAEgADYCCAtBAEEAKAKo0ICAAEF/aiIBQX8gARs2AqjQgIAACwsEAAAAC04AAkAgAA0APwBBEHQPCwJAIABB//8DcQ0AIABBf0wNAAJAIABBEHZAACIAQX9HDQBBAEEwNgL404CAAEF/DwsgAEEQdA8LEMqAgIAAAAvyAgIDfwF+AkAgAkUNACAAIAE6AAAgAiAAaiIDQX9qIAE6AAAgAkEDSQ0AIAAgAToAAiAAIAE6AAEgA0F9aiABOgAAIANBfmogAToAACACQQdJDQAgACABOgADIANBfGogAToAACACQQlJDQAgAEEAIABrQQNxIgRqIgMgAUH/AXFBgYKECGwiATYCACADIAIgBGtBfHEiBGoiAkF8aiABNgIAIARBCUkNACADIAE2AgggAyABNgIEIAJBeGogATYCACACQXRqIAE2AgAgBEEZSQ0AIAMgATYCGCADIAE2AhQgAyABNgIQIAMgATYCDCACQXBqIAE2AgAgAkFsaiABNgIAIAJBaGogATYCACACQWRqIAE2AgAgBCADQQRxQRhyIgVrIgJBIEkNACABrUKBgICAEH4hBiADIAVqIQEDQCABIAY3AxggASAGNwMQIAEgBjcDCCABIAY3AwAgAUEgaiEBIAJBYGoiAkEfSw0ACwsgAAsLjkgBAEGACAuGSAEAAAACAAAAAwAAAAAAAAAAAAAABAAAAAUAAAAAAAAAAAAAAAYAAAAHAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAASW52YWxpZCBjaGFyIGluIHVybCBxdWVyeQBTcGFuIGNhbGxiYWNrIGVycm9yIGluIG9uX2JvZHkAQ29udGVudC1MZW5ndGggb3ZlcmZsb3cAQ2h1bmsgc2l6ZSBvdmVyZmxvdwBSZXNwb25zZSBvdmVyZmxvdwBJbnZhbGlkIG1ldGhvZCBmb3IgSFRUUC94LnggcmVxdWVzdABJbnZhbGlkIG1ldGhvZCBmb3IgUlRTUC94LnggcmVxdWVzdABFeHBlY3RlZCBTT1VSQ0UgbWV0aG9kIGZvciBJQ0UveC54IHJlcXVlc3QASW52YWxpZCBjaGFyIGluIHVybCBmcmFnbWVudCBzdGFydABFeHBlY3RlZCBkb3QAU3BhbiBjYWxsYmFjayBlcnJvciBpbiBvbl9zdGF0dXMASW52YWxpZCByZXNwb25zZSBzdGF0dXMASW52YWxpZCBjaGFyYWN0ZXIgaW4gY2h1bmsgZXh0ZW5zaW9ucwBVc2VyIGNhbGxiYWNrIGVycm9yAGBvbl9yZXNldGAgY2FsbGJhY2sgZXJyb3IAYG9uX2NodW5rX2hlYWRlcmAgY2FsbGJhY2sgZXJyb3IAYG9uX21lc3NhZ2VfYmVnaW5gIGNhbGxiYWNrIGVycm9yAGBvbl9jaHVua19leHRlbnNpb25fdmFsdWVgIGNhbGxiYWNrIGVycm9yAGBvbl9zdGF0dXNfY29tcGxldGVgIGNhbGxiYWNrIGVycm9yAGBvbl92ZXJzaW9uX2NvbXBsZXRlYCBjYWxsYmFjayBlcnJvcgBgb25fdXJsX2NvbXBsZXRlYCBjYWxsYmFjayBlcnJvcgBgb25fY2h1bmtfY29tcGxldGVgIGNhbGxiYWNrIGVycm9yAGBvbl9oZWFkZXJfdmFsdWVfY29tcGxldGVgIGNhbGxiYWNrIGVycm9yAGBvbl9tZXNzYWdlX2NvbXBsZXRlYCBjYWxsYmFjayBlcnJvcgBgb25fbWV0aG9kX2NvbXBsZXRlYCBjYWxsYmFjayBlcnJvcgBgb25faGVhZGVyX2ZpZWxkX2NvbXBsZXRlYCBjYWxsYmFjayBlcnJvcgBgb25fY2h1bmtfZXh0ZW5zaW9uX25hbWVgIGNhbGxiYWNrIGVycm9yAFVuZXhwZWN0ZWQgY2hhciBpbiB1cmwgc2VydmVyAEludmFsaWQgaGVhZGVyIHZhbHVlIGNoYXIASW52YWxpZCBoZWFkZXIgZmllbGQgY2hhcgBTcGFuIGNhbGxiYWNrIGVycm9yIGluIG9uX3ZlcnNpb24ASW52YWxpZCBtaW5vciB2ZXJzaW9uAEludmFsaWQgbWFqb3IgdmVyc2lvbgBFeHBlY3RlZCBzcGFjZSBhZnRlciB2ZXJzaW9uAEV4cGVjdGVkIENSTEYgYWZ0ZXIgdmVyc2lvbgBJbnZhbGlkIEhUVFAgdmVyc2lvbgBJbnZhbGlkIGhlYWRlciB0b2tlbgBTcGFuIGNhbGxiYWNrIGVycm9yIGluIG9uX3VybABJbnZhbGlkIGNoYXJhY3RlcnMgaW4gdXJsAFVuZXhwZWN0ZWQgc3RhcnQgY2hhciBpbiB1cmwARG91YmxlIEAgaW4gdXJsAEVtcHR5IENvbnRlbnQtTGVuZ3RoAEludmFsaWQgY2hhcmFjdGVyIGluIENvbnRlbnQtTGVuZ3RoAER1cGxpY2F0ZSBDb250ZW50LUxlbmd0aABJbnZhbGlkIGNoYXIgaW4gdXJsIHBhdGgAQ29udGVudC1MZW5ndGggY2FuJ3QgYmUgcHJlc2VudCB3aXRoIFRyYW5zZmVyLUVuY29kaW5nAEludmFsaWQgY2hhcmFjdGVyIGluIGNodW5rIHNpemUAU3BhbiBjYWxsYmFjayBlcnJvciBpbiBvbl9oZWFkZXJfdmFsdWUAU3BhbiBjYWxsYmFjayBlcnJvciBpbiBvbl9jaHVua19leHRlbnNpb25fdmFsdWUASW52YWxpZCBjaGFyYWN0ZXIgaW4gY2h1bmsgZXh0ZW5zaW9ucyB2YWx1ZQBNaXNzaW5nIGV4cGVjdGVkIExGIGFmdGVyIGhlYWRlciB2YWx1ZQBJbnZhbGlkIGBUcmFuc2Zlci1FbmNvZGluZ2AgaGVhZGVyIHZhbHVlAEludmFsaWQgY2hhcmFjdGVyIGluIGNodW5rIGV4dGVuc2lvbnMgcXVvdGUgdmFsdWUASW52YWxpZCBjaGFyYWN0ZXIgaW4gY2h1bmsgZXh0ZW5zaW9ucyBxdW90ZWQgdmFsdWUAUGF1c2VkIGJ5IG9uX2hlYWRlcnNfY29tcGxldGUASW52YWxpZCBFT0Ygc3RhdGUAb25fcmVzZXQgcGF1c2UAb25fY2h1bmtfaGVhZGVyIHBhdXNlAG9uX21lc3NhZ2VfYmVnaW4gcGF1c2UAb25fY2h1bmtfZXh0ZW5zaW9uX3ZhbHVlIHBhdXNlAG9uX3N0YXR1c19jb21wbGV0ZSBwYXVzZQBvbl92ZXJzaW9uX2NvbXBsZXRlIHBhdXNlAG9uX3VybF9jb21wbGV0ZSBwYXVzZQBvbl9jaHVua19jb21wbGV0ZSBwYXVzZQBvbl9oZWFkZXJfdmFsdWVfY29tcGxldGUgcGF1c2UAb25fbWVzc2FnZV9jb21wbGV0ZSBwYXVzZQBvbl9tZXRob2RfY29tcGxldGUgcGF1c2UAb25faGVhZGVyX2ZpZWxkX2NvbXBsZXRlIHBhdXNlAG9uX2NodW5rX2V4dGVuc2lvbl9uYW1lIHBhdXNlAFVuZXhwZWN0ZWQgc3BhY2UgYWZ0ZXIgc3RhcnQgbGluZQBTcGFuIGNhbGxiYWNrIGVycm9yIGluIG9uX2NodW5rX2V4dGVuc2lvbl9uYW1lAEludmFsaWQgY2hhcmFjdGVyIGluIGNodW5rIGV4dGVuc2lvbnMgbmFtZQBQYXVzZSBvbiBDT05ORUNUL1VwZ3JhZGUAUGF1c2Ugb24gUFJJL1VwZ3JhZGUARXhwZWN0ZWQgSFRUUC8yIENvbm5lY3Rpb24gUHJlZmFjZQBTcGFuIGNhbGxiYWNrIGVycm9yIGluIG9uX21ldGhvZABFeHBlY3RlZCBzcGFjZSBhZnRlciBtZXRob2QAU3BhbiBjYWxsYmFjayBlcnJvciBpbiBvbl9oZWFkZXJfZmllbGQAUGF1c2VkAEludmFsaWQgd29yZCBlbmNvdW50ZXJlZABJbnZhbGlkIG1ldGhvZCBlbmNvdW50ZXJlZABVbmV4cGVjdGVkIGNoYXIgaW4gdXJsIHNjaGVtYQBSZXF1ZXN0IGhhcyBpbnZhbGlkIGBUcmFuc2Zlci1FbmNvZGluZ2AAU1dJVENIX1BST1hZAFVTRV9QUk9YWQBNS0FDVElWSVRZAFVOUFJPQ0VTU0FCTEVfRU5USVRZAENPUFkATU9WRURfUEVSTUFORU5UTFkAVE9PX0VBUkxZAE5PVElGWQBGQUlMRURfREVQRU5ERU5DWQBCQURfR0FURVdBWQBQTEFZAFBVVABDSEVDS09VVABHQVRFV0FZX1RJTUVPVVQAUkVRVUVTVF9USU1FT1VUAE5FVFdPUktfQ09OTkVDVF9USU1FT1VUAENPTk5FQ1RJT05fVElNRU9VVABMT0dJTl9USU1FT1VUAE5FVFdPUktfUkVBRF9USU1FT1VUAFBPU1QATUlTRElSRUNURURfUkVRVUVTVABDTElFTlRfQ0xPU0VEX1JFUVVFU1QAQ0xJRU5UX0NMT1NFRF9MT0FEX0JBTEFOQ0VEX1JFUVVFU1QAQkFEX1JFUVVFU1QASFRUUF9SRVFVRVNUX1NFTlRfVE9fSFRUUFNfUE9SVABSRVBPUlQASU1fQV9URUFQT1QAUkVTRVRfQ09OVEVOVABOT19DT05URU5UAFBBUlRJQUxfQ09OVEVOVABIUEVfSU5WQUxJRF9DT05TVEFOVABIUEVfQ0JfUkVTRVQAR0VUAEhQRV9TVFJJQ1QAQ09ORkxJQ1QAVEVNUE9SQVJZX1JFRElSRUNUAFBFUk1BTkVOVF9SRURJUkVDVABDT05ORUNUAE1VTFRJX1NUQVRVUwBIUEVfSU5WQUxJRF9TVEFUVVMAVE9PX01BTllfUkVRVUVTVFMARUFSTFlfSElOVFMAVU5BVkFJTEFCTEVfRk9SX0xFR0FMX1JFQVNPTlMAT1BUSU9OUwBTV0lUQ0hJTkdfUFJPVE9DT0xTAFZBUklBTlRfQUxTT19ORUdPVElBVEVTAE1VTFRJUExFX0NIT0lDRVMASU5URVJOQUxfU0VSVkVSX0VSUk9SAFdFQl9TRVJWRVJfVU5LTk9XTl9FUlJPUgBSQUlMR1VOX0VSUk9SAElERU5USVRZX1BST1ZJREVSX0FVVEhFTlRJQ0FUSU9OX0VSUk9SAFNTTF9DRVJUSUZJQ0FURV9FUlJPUgBJTlZBTElEX1hfRk9SV0FSREVEX0ZPUgBTRVRfUEFSQU1FVEVSAEdFVF9QQVJBTUVURVIASFBFX1VTRVIAU0VFX09USEVSAEhQRV9DQl9DSFVOS19IRUFERVIATUtDQUxFTkRBUgBTRVRVUABXRUJfU0VSVkVSX0lTX0RPV04AVEVBUkRPV04ASFBFX0NMT1NFRF9DT05ORUNUSU9OAEhFVVJJU1RJQ19FWFBJUkFUSU9OAERJU0NPTk5FQ1RFRF9PUEVSQVRJT04ATk9OX0FVVEhPUklUQVRJVkVfSU5GT1JNQVRJT04ASFBFX0lOVkFMSURfVkVSU0lPTgBIUEVfQ0JfTUVTU0FHRV9CRUdJTgBTSVRFX0lTX0ZST1pFTgBIUEVfSU5WQUxJRF9IRUFERVJfVE9LRU4ASU5WQUxJRF9UT0tFTgBGT1JCSURERU4ARU5IQU5DRV9ZT1VSX0NBTE0ASFBFX0lOVkFMSURfVVJMAEJMT0NLRURfQllfUEFSRU5UQUxfQ09OVFJPTABNS0NPTABBQ0wASFBFX0lOVEVSTkFMAFJFUVVFU1RfSEVBREVSX0ZJRUxEU19UT09fTEFSR0VfVU5PRkZJQ0lBTABIUEVfT0sAVU5MSU5LAFVOTE9DSwBQUkkAUkVUUllfV0lUSABIUEVfSU5WQUxJRF9DT05URU5UX0xFTkdUSABIUEVfVU5FWFBFQ1RFRF9DT05URU5UX0xFTkdUSABGTFVTSABQUk9QUEFUQ0gATS1TRUFSQ0gAVVJJX1RPT19MT05HAFBST0NFU1NJTkcATUlTQ0VMTEFORU9VU19QRVJTSVNURU5UX1dBUk5JTkcATUlTQ0VMTEFORU9VU19XQVJOSU5HAEhQRV9JTlZBTElEX1RSQU5TRkVSX0VOQ09ESU5HAEV4cGVjdGVkIENSTEYASFBFX0lOVkFMSURfQ0hVTktfU0laRQBNT1ZFAENPTlRJTlVFAEhQRV9DQl9TVEFUVVNfQ09NUExFVEUASFBFX0NCX0hFQURFUlNfQ09NUExFVEUASFBFX0NCX1ZFUlNJT05fQ09NUExFVEUASFBFX0NCX1VSTF9DT01QTEVURQBIUEVfQ0JfQ0hVTktfQ09NUExFVEUASFBFX0NCX0hFQURFUl9WQUxVRV9DT01QTEVURQBIUEVfQ0JfQ0hVTktfRVhURU5TSU9OX1ZBTFVFX0NPTVBMRVRFAEhQRV9DQl9DSFVOS19FWFRFTlNJT05fTkFNRV9DT01QTEVURQBIUEVfQ0JfTUVTU0FHRV9DT01QTEVURQBIUEVfQ0JfTUVUSE9EX0NPTVBMRVRFAEhQRV9DQl9IRUFERVJfRklFTERfQ09NUExFVEUAREVMRVRFAEhQRV9JTlZBTElEX0VPRl9TVEFURQBJTlZBTElEX1NTTF9DRVJUSUZJQ0FURQBQQVVTRQBOT19SRVNQT05TRQBVTlNVUFBPUlRFRF9NRURJQV9UWVBFAEdPTkUATk9UX0FDQ0VQVEFCTEUAU0VSVklDRV9VTkFWQUlMQUJMRQBSQU5HRV9OT1RfU0FUSVNGSUFCTEUAT1JJR0lOX0lTX1VOUkVBQ0hBQkxFAFJFU1BPTlNFX0lTX1NUQUxFAFBVUkdFAE1FUkdFAFJFUVVFU1RfSEVBREVSX0ZJRUxEU19UT09fTEFSR0UAUkVRVUVTVF9IRUFERVJfVE9PX0xBUkdFAFBBWUxPQURfVE9PX0xBUkdFAElOU1VGRklDSUVOVF9TVE9SQUdFAEhQRV9QQVVTRURfVVBHUkFERQBIUEVfUEFVU0VEX0gyX1VQR1JBREUAU09VUkNFAEFOTk9VTkNFAFRSQUNFAEhQRV9VTkVYUEVDVEVEX1NQQUNFAERFU0NSSUJFAFVOU1VCU0NSSUJFAFJFQ09SRABIUEVfSU5WQUxJRF9NRVRIT0QATk9UX0ZPVU5EAFBST1BGSU5EAFVOQklORABSRUJJTkQAVU5BVVRIT1JJWkVEAE1FVEhPRF9OT1RfQUxMT1dFRABIVFRQX1ZFUlNJT05fTk9UX1NVUFBPUlRFRABBTFJFQURZX1JFUE9SVEVEAEFDQ0VQVEVEAE5PVF9JTVBMRU1FTlRFRABMT09QX0RFVEVDVEVEAEhQRV9DUl9FWFBFQ1RFRABIUEVfTEZfRVhQRUNURUQAQ1JFQVRFRABJTV9VU0VEAEhQRV9QQVVTRUQAVElNRU9VVF9PQ0NVUkVEAFBBWU1FTlRfUkVRVUlSRUQAUFJFQ09ORElUSU9OX1JFUVVJUkVEAFBST1hZX0FVVEhFTlRJQ0FUSU9OX1JFUVVJUkVEAE5FVFdPUktfQVVUSEVOVElDQVRJT05fUkVRVUlSRUQATEVOR1RIX1JFUVVJUkVEAFNTTF9DRVJUSUZJQ0FURV9SRVFVSVJFRABVUEdSQURFX1JFUVVJUkVEAFBBR0VfRVhQSVJFRABQUkVDT05ESVRJT05fRkFJTEVEAEVYUEVDVEFUSU9OX0ZBSUxFRABSRVZBTElEQVRJT05fRkFJTEVEAFNTTF9IQU5EU0hBS0VfRkFJTEVEAExPQ0tFRABUUkFOU0ZPUk1BVElPTl9BUFBMSUVEAE5PVF9NT0RJRklFRABOT1RfRVhURU5ERUQAQkFORFdJRFRIX0xJTUlUX0VYQ0VFREVEAFNJVEVfSVNfT1ZFUkxPQURFRABIRUFEAEV4cGVjdGVkIEhUVFAvAABeEwAAJhMAADAQAADwFwAAnRMAABUSAAA5FwAA8BIAAAoQAAB1EgAArRIAAIITAABPFAAAfxAAAKAVAAAjFAAAiRIAAIsUAABNFQAA1BEAAM8UAAAQGAAAyRYAANwWAADBEQAA4BcAALsUAAB0FAAAfBUAAOUUAAAIFwAAHxAAAGUVAACjFAAAKBUAAAIVAACZFQAALBAAAIsZAABPDwAA1A4AAGoQAADOEAAAAhcAAIkOAABuEwAAHBMAAGYUAABWFwAAwRMAAM0TAABsEwAAaBcAAGYXAABfFwAAIhMAAM4PAABpDgAA2A4AAGMWAADLEwAAqg4AACgXAAAmFwAAxRMAAF0WAADoEQAAZxMAAGUTAADyFgAAcxMAAB0XAAD5FgAA8xEAAM8OAADOFQAADBIAALMRAAClEQAAYRAAADIXAAC7EwAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAgEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAgMCAgICAgAAAgIAAgIAAgICAgICAgICAgAEAAAAAAACAgICAgICAgICAgICAgICAgICAgICAgICAgAAAAICAgICAgICAgICAgICAgICAgICAgICAgICAgICAAIAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAIAAgICAgIAAAICAAICAAICAgICAgICAgIAAwAEAAAAAgICAgICAgICAgICAgICAgICAgICAgICAgIAAAACAgICAgICAgICAgICAgICAgICAgICAgICAgICAgACAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABsb3NlZWVwLWFsaXZlAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAgEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQFjaHVua2VkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQABAQEBAQAAAQEAAQEAAQEBAQEBAQEBAQAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGVjdGlvbmVudC1sZW5ndGhvbnJveHktY29ubmVjdGlvbgAAAAAAAAAAAAAAAAAAAHJhbnNmZXItZW5jb2RpbmdwZ3JhZGUNCg0KDQpTTQ0KDQpUVFAvQ0UvVFNQLwAAAAAAAAAAAAAAAAECAAEDAAAAAAAAAAAAAAAAAAAAAAAABAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAABAgABAwAAAAAAAAAAAAAAAAAAAAAAAAQBAQUBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAQAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQEAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAABAAACAAAAAAAAAAAAAAAAAAAAAAAAAwQAAAQEBAQEBAQEBAQEBQQEBAQEBAQEBAQEBAAEAAYHBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAQABAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAQAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAAAAAAAAAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAEAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAgAAAAACAAAAAAAAAAAAAAAAAAAAAAADAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwAAAAAAAAMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAE5PVU5DRUVDS09VVE5FQ1RFVEVDUklCRUxVU0hFVEVBRFNFQVJDSFJHRUNUSVZJVFlMRU5EQVJWRU9USUZZUFRJT05TQ0hTRUFZU1RBVENIR0VPUkRJUkVDVE9SVFJDSFBBUkFNRVRFUlVSQ0VCU0NSSUJFQVJET1dOQUNFSU5ETktDS1VCU0NSSUJFSFRUUC9BRFRQLw==' + + +/***/ }), + +/***/ 172: +/***/ ((__unused_webpack_module, exports) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.enumToMap = void 0; +function enumToMap(obj) { + const res = {}; + Object.keys(obj).forEach((key) => { + const value = obj[key]; + if (typeof value === 'number') { + res[key] = value; + } + }); + return res; +} +exports.enumToMap = enumToMap; +//# sourceMappingURL=utils.js.map + +/***/ }), + +/***/ 7501: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const { kClients } = __nccwpck_require__(6443) +const Agent = __nccwpck_require__(9965) +const { + kAgent, + kMockAgentSet, + kMockAgentGet, + kDispatches, + kIsMockActive, + kNetConnect, + kGetNetConnect, + kOptions, + kFactory +} = __nccwpck_require__(1117) +const MockClient = __nccwpck_require__(7365) +const MockPool = __nccwpck_require__(4004) +const { matchValue, buildMockOptions } = __nccwpck_require__(3397) +const { InvalidArgumentError, UndiciError } = __nccwpck_require__(8707) +const Dispatcher = __nccwpck_require__(992) +const Pluralizer = __nccwpck_require__(1529) +const PendingInterceptorsFormatter = __nccwpck_require__(6142) + +class FakeWeakRef { + constructor (value) { + this.value = value + } + + deref () { + return this.value + } +} + +class MockAgent extends Dispatcher { + constructor (opts) { + super(opts) + + this[kNetConnect] = true + this[kIsMockActive] = true + + // Instantiate Agent and encapsulate + if ((opts && opts.agent && typeof opts.agent.dispatch !== 'function')) { + throw new InvalidArgumentError('Argument opts.agent must implement Agent') + } + const agent = opts && opts.agent ? opts.agent : new Agent(opts) + this[kAgent] = agent + + this[kClients] = agent[kClients] + this[kOptions] = buildMockOptions(opts) + } + + get (origin) { + let dispatcher = this[kMockAgentGet](origin) + + if (!dispatcher) { + dispatcher = this[kFactory](origin) + this[kMockAgentSet](origin, dispatcher) + } + return dispatcher + } + + dispatch (opts, handler) { + // Call MockAgent.get to perform additional setup before dispatching as normal + this.get(opts.origin) + return this[kAgent].dispatch(opts, handler) + } + + async close () { + await this[kAgent].close() + this[kClients].clear() + } + + deactivate () { + this[kIsMockActive] = false + } + + activate () { + this[kIsMockActive] = true + } + + enableNetConnect (matcher) { + if (typeof matcher === 'string' || typeof matcher === 'function' || matcher instanceof RegExp) { + if (Array.isArray(this[kNetConnect])) { + this[kNetConnect].push(matcher) + } else { + this[kNetConnect] = [matcher] + } + } else if (typeof matcher === 'undefined') { + this[kNetConnect] = true + } else { + throw new InvalidArgumentError('Unsupported matcher. Must be one of String|Function|RegExp.') + } + } + + disableNetConnect () { + this[kNetConnect] = false + } + + // This is required to bypass issues caused by using global symbols - see: + // https://github.com/nodejs/undici/issues/1447 + get isMockActive () { + return this[kIsMockActive] + } + + [kMockAgentSet] (origin, dispatcher) { + this[kClients].set(origin, new FakeWeakRef(dispatcher)) + } + + [kFactory] (origin) { + const mockOptions = Object.assign({ agent: this }, this[kOptions]) + return this[kOptions] && this[kOptions].connections === 1 + ? new MockClient(origin, mockOptions) + : new MockPool(origin, mockOptions) + } + + [kMockAgentGet] (origin) { + // First check if we can immediately find it + const ref = this[kClients].get(origin) + if (ref) { + return ref.deref() + } + + // If the origin is not a string create a dummy parent pool and return to user + if (typeof origin !== 'string') { + const dispatcher = this[kFactory]('http://localhost:9999') + this[kMockAgentSet](origin, dispatcher) + return dispatcher + } + + // If we match, create a pool and assign the same dispatches + for (const [keyMatcher, nonExplicitRef] of Array.from(this[kClients])) { + const nonExplicitDispatcher = nonExplicitRef.deref() + if (nonExplicitDispatcher && typeof keyMatcher !== 'string' && matchValue(keyMatcher, origin)) { + const dispatcher = this[kFactory](origin) + this[kMockAgentSet](origin, dispatcher) + dispatcher[kDispatches] = nonExplicitDispatcher[kDispatches] + return dispatcher + } + } + } + + [kGetNetConnect] () { + return this[kNetConnect] + } + + pendingInterceptors () { + const mockAgentClients = this[kClients] + + return Array.from(mockAgentClients.entries()) + .flatMap(([origin, scope]) => scope.deref()[kDispatches].map(dispatch => ({ ...dispatch, origin }))) + .filter(({ pending }) => pending) + } + + assertNoPendingInterceptors ({ pendingInterceptorsFormatter = new PendingInterceptorsFormatter() } = {}) { + const pending = this.pendingInterceptors() + + if (pending.length === 0) { + return + } + + const pluralizer = new Pluralizer('interceptor', 'interceptors').pluralize(pending.length) + + throw new UndiciError(` +${pluralizer.count} ${pluralizer.noun} ${pluralizer.is} pending: + +${pendingInterceptorsFormatter.format(pending)} +`.trim()) + } +} + +module.exports = MockAgent + + +/***/ }), + +/***/ 7365: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const { promisify } = __nccwpck_require__(9023) +const Client = __nccwpck_require__(6197) +const { buildMockDispatch } = __nccwpck_require__(3397) +const { + kDispatches, + kMockAgent, + kClose, + kOriginalClose, + kOrigin, + kOriginalDispatch, + kConnected +} = __nccwpck_require__(1117) +const { MockInterceptor } = __nccwpck_require__(1511) +const Symbols = __nccwpck_require__(6443) +const { InvalidArgumentError } = __nccwpck_require__(8707) + +/** + * MockClient provides an API that extends the Client to influence the mockDispatches. + */ +class MockClient extends Client { + constructor (origin, opts) { + super(origin, opts) + + if (!opts || !opts.agent || typeof opts.agent.dispatch !== 'function') { + throw new InvalidArgumentError('Argument opts.agent must implement Agent') + } + + this[kMockAgent] = opts.agent + this[kOrigin] = origin + this[kDispatches] = [] + this[kConnected] = 1 + this[kOriginalDispatch] = this.dispatch + this[kOriginalClose] = this.close.bind(this) + + this.dispatch = buildMockDispatch.call(this) + this.close = this[kClose] + } + + get [Symbols.kConnected] () { + return this[kConnected] + } + + /** + * Sets up the base interceptor for mocking replies from undici. + */ + intercept (opts) { + return new MockInterceptor(opts, this[kDispatches]) + } + + async [kClose] () { + await promisify(this[kOriginalClose])() + this[kConnected] = 0 + this[kMockAgent][Symbols.kClients].delete(this[kOrigin]) + } +} + +module.exports = MockClient + + +/***/ }), + +/***/ 2429: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const { UndiciError } = __nccwpck_require__(8707) + +class MockNotMatchedError extends UndiciError { + constructor (message) { + super(message) + Error.captureStackTrace(this, MockNotMatchedError) + this.name = 'MockNotMatchedError' + this.message = message || 'The request does not match any registered mock dispatches' + this.code = 'UND_MOCK_ERR_MOCK_NOT_MATCHED' + } +} + +module.exports = { + MockNotMatchedError +} + + +/***/ }), + +/***/ 1511: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const { getResponseData, buildKey, addMockDispatch } = __nccwpck_require__(3397) +const { + kDispatches, + kDispatchKey, + kDefaultHeaders, + kDefaultTrailers, + kContentLength, + kMockDispatch +} = __nccwpck_require__(1117) +const { InvalidArgumentError } = __nccwpck_require__(8707) +const { buildURL } = __nccwpck_require__(3440) + +/** + * Defines the scope API for an interceptor reply + */ +class MockScope { + constructor (mockDispatch) { + this[kMockDispatch] = mockDispatch + } + + /** + * Delay a reply by a set amount in ms. + */ + delay (waitInMs) { + if (typeof waitInMs !== 'number' || !Number.isInteger(waitInMs) || waitInMs <= 0) { + throw new InvalidArgumentError('waitInMs must be a valid integer > 0') + } + + this[kMockDispatch].delay = waitInMs + return this + } + + /** + * For a defined reply, never mark as consumed. + */ + persist () { + this[kMockDispatch].persist = true + return this + } + + /** + * Allow one to define a reply for a set amount of matching requests. + */ + times (repeatTimes) { + if (typeof repeatTimes !== 'number' || !Number.isInteger(repeatTimes) || repeatTimes <= 0) { + throw new InvalidArgumentError('repeatTimes must be a valid integer > 0') + } + + this[kMockDispatch].times = repeatTimes + return this + } +} + +/** + * Defines an interceptor for a Mock + */ +class MockInterceptor { + constructor (opts, mockDispatches) { + if (typeof opts !== 'object') { + throw new InvalidArgumentError('opts must be an object') + } + if (typeof opts.path === 'undefined') { + throw new InvalidArgumentError('opts.path must be defined') + } + if (typeof opts.method === 'undefined') { + opts.method = 'GET' + } + // See https://github.com/nodejs/undici/issues/1245 + // As per RFC 3986, clients are not supposed to send URI + // fragments to servers when they retrieve a document, + if (typeof opts.path === 'string') { + if (opts.query) { + opts.path = buildURL(opts.path, opts.query) + } else { + // Matches https://github.com/nodejs/undici/blob/main/lib/fetch/index.js#L1811 + const parsedURL = new URL(opts.path, 'data://') + opts.path = parsedURL.pathname + parsedURL.search + } + } + if (typeof opts.method === 'string') { + opts.method = opts.method.toUpperCase() + } + + this[kDispatchKey] = buildKey(opts) + this[kDispatches] = mockDispatches + this[kDefaultHeaders] = {} + this[kDefaultTrailers] = {} + this[kContentLength] = false + } + + createMockScopeDispatchData (statusCode, data, responseOptions = {}) { + const responseData = getResponseData(data) + const contentLength = this[kContentLength] ? { 'content-length': responseData.length } : {} + const headers = { ...this[kDefaultHeaders], ...contentLength, ...responseOptions.headers } + const trailers = { ...this[kDefaultTrailers], ...responseOptions.trailers } + + return { statusCode, data, headers, trailers } + } + + validateReplyParameters (statusCode, data, responseOptions) { + if (typeof statusCode === 'undefined') { + throw new InvalidArgumentError('statusCode must be defined') + } + if (typeof data === 'undefined') { + throw new InvalidArgumentError('data must be defined') + } + if (typeof responseOptions !== 'object') { + throw new InvalidArgumentError('responseOptions must be an object') + } + } + + /** + * Mock an undici request with a defined reply. + */ + reply (replyData) { + // Values of reply aren't available right now as they + // can only be available when the reply callback is invoked. + if (typeof replyData === 'function') { + // We'll first wrap the provided callback in another function, + // this function will properly resolve the data from the callback + // when invoked. + const wrappedDefaultsCallback = (opts) => { + // Our reply options callback contains the parameter for statusCode, data and options. + const resolvedData = replyData(opts) + + // Check if it is in the right format + if (typeof resolvedData !== 'object') { + throw new InvalidArgumentError('reply options callback must return an object') + } + + const { statusCode, data = '', responseOptions = {} } = resolvedData + this.validateReplyParameters(statusCode, data, responseOptions) + // Since the values can be obtained immediately we return them + // from this higher order function that will be resolved later. + return { + ...this.createMockScopeDispatchData(statusCode, data, responseOptions) + } + } + + // Add usual dispatch data, but this time set the data parameter to function that will eventually provide data. + const newMockDispatch = addMockDispatch(this[kDispatches], this[kDispatchKey], wrappedDefaultsCallback) + return new MockScope(newMockDispatch) + } + + // We can have either one or three parameters, if we get here, + // we should have 1-3 parameters. So we spread the arguments of + // this function to obtain the parameters, since replyData will always + // just be the statusCode. + const [statusCode, data = '', responseOptions = {}] = [...arguments] + this.validateReplyParameters(statusCode, data, responseOptions) + + // Send in-already provided data like usual + const dispatchData = this.createMockScopeDispatchData(statusCode, data, responseOptions) + const newMockDispatch = addMockDispatch(this[kDispatches], this[kDispatchKey], dispatchData) + return new MockScope(newMockDispatch) + } + + /** + * Mock an undici request with a defined error. + */ + replyWithError (error) { + if (typeof error === 'undefined') { + throw new InvalidArgumentError('error must be defined') + } + + const newMockDispatch = addMockDispatch(this[kDispatches], this[kDispatchKey], { error }) + return new MockScope(newMockDispatch) + } + + /** + * Set default reply headers on the interceptor for subsequent replies + */ + defaultReplyHeaders (headers) { + if (typeof headers === 'undefined') { + throw new InvalidArgumentError('headers must be defined') + } + + this[kDefaultHeaders] = headers + return this + } + + /** + * Set default reply trailers on the interceptor for subsequent replies + */ + defaultReplyTrailers (trailers) { + if (typeof trailers === 'undefined') { + throw new InvalidArgumentError('trailers must be defined') + } + + this[kDefaultTrailers] = trailers + return this + } + + /** + * Set reply content length header for replies on the interceptor + */ + replyContentLength () { + this[kContentLength] = true + return this + } +} + +module.exports.MockInterceptor = MockInterceptor +module.exports.MockScope = MockScope + + +/***/ }), + +/***/ 4004: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const { promisify } = __nccwpck_require__(9023) +const Pool = __nccwpck_require__(5076) +const { buildMockDispatch } = __nccwpck_require__(3397) +const { + kDispatches, + kMockAgent, + kClose, + kOriginalClose, + kOrigin, + kOriginalDispatch, + kConnected +} = __nccwpck_require__(1117) +const { MockInterceptor } = __nccwpck_require__(1511) +const Symbols = __nccwpck_require__(6443) +const { InvalidArgumentError } = __nccwpck_require__(8707) + +/** + * MockPool provides an API that extends the Pool to influence the mockDispatches. + */ +class MockPool extends Pool { + constructor (origin, opts) { + super(origin, opts) + + if (!opts || !opts.agent || typeof opts.agent.dispatch !== 'function') { + throw new InvalidArgumentError('Argument opts.agent must implement Agent') + } + + this[kMockAgent] = opts.agent + this[kOrigin] = origin + this[kDispatches] = [] + this[kConnected] = 1 + this[kOriginalDispatch] = this.dispatch + this[kOriginalClose] = this.close.bind(this) + + this.dispatch = buildMockDispatch.call(this) + this.close = this[kClose] + } + + get [Symbols.kConnected] () { + return this[kConnected] + } + + /** + * Sets up the base interceptor for mocking replies from undici. + */ + intercept (opts) { + return new MockInterceptor(opts, this[kDispatches]) + } + + async [kClose] () { + await promisify(this[kOriginalClose])() + this[kConnected] = 0 + this[kMockAgent][Symbols.kClients].delete(this[kOrigin]) + } +} + +module.exports = MockPool + + +/***/ }), + +/***/ 1117: +/***/ ((module) => { + +"use strict"; + + +module.exports = { + kAgent: Symbol('agent'), + kOptions: Symbol('options'), + kFactory: Symbol('factory'), + kDispatches: Symbol('dispatches'), + kDispatchKey: Symbol('dispatch key'), + kDefaultHeaders: Symbol('default headers'), + kDefaultTrailers: Symbol('default trailers'), + kContentLength: Symbol('content length'), + kMockAgent: Symbol('mock agent'), + kMockAgentSet: Symbol('mock agent set'), + kMockAgentGet: Symbol('mock agent get'), + kMockDispatch: Symbol('mock dispatch'), + kClose: Symbol('close'), + kOriginalClose: Symbol('original agent close'), + kOrigin: Symbol('origin'), + kIsMockActive: Symbol('is mock active'), + kNetConnect: Symbol('net connect'), + kGetNetConnect: Symbol('get net connect'), + kConnected: Symbol('connected') +} + + +/***/ }), + +/***/ 3397: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const { MockNotMatchedError } = __nccwpck_require__(2429) +const { + kDispatches, + kMockAgent, + kOriginalDispatch, + kOrigin, + kGetNetConnect +} = __nccwpck_require__(1117) +const { buildURL, nop } = __nccwpck_require__(3440) +const { STATUS_CODES } = __nccwpck_require__(8611) +const { + types: { + isPromise + } +} = __nccwpck_require__(9023) + +function matchValue (match, value) { + if (typeof match === 'string') { + return match === value + } + if (match instanceof RegExp) { + return match.test(value) + } + if (typeof match === 'function') { + return match(value) === true + } + return false +} + +function lowerCaseEntries (headers) { + return Object.fromEntries( + Object.entries(headers).map(([headerName, headerValue]) => { + return [headerName.toLocaleLowerCase(), headerValue] + }) + ) +} + +/** + * @param {import('../../index').Headers|string[]|Record} headers + * @param {string} key + */ +function getHeaderByName (headers, key) { + if (Array.isArray(headers)) { + for (let i = 0; i < headers.length; i += 2) { + if (headers[i].toLocaleLowerCase() === key.toLocaleLowerCase()) { + return headers[i + 1] + } + } + + return undefined + } else if (typeof headers.get === 'function') { + return headers.get(key) + } else { + return lowerCaseEntries(headers)[key.toLocaleLowerCase()] + } +} + +/** @param {string[]} headers */ +function buildHeadersFromArray (headers) { // fetch HeadersList + const clone = headers.slice() + const entries = [] + for (let index = 0; index < clone.length; index += 2) { + entries.push([clone[index], clone[index + 1]]) + } + return Object.fromEntries(entries) +} + +function matchHeaders (mockDispatch, headers) { + if (typeof mockDispatch.headers === 'function') { + if (Array.isArray(headers)) { // fetch HeadersList + headers = buildHeadersFromArray(headers) + } + return mockDispatch.headers(headers ? lowerCaseEntries(headers) : {}) + } + if (typeof mockDispatch.headers === 'undefined') { + return true + } + if (typeof headers !== 'object' || typeof mockDispatch.headers !== 'object') { + return false + } + + for (const [matchHeaderName, matchHeaderValue] of Object.entries(mockDispatch.headers)) { + const headerValue = getHeaderByName(headers, matchHeaderName) + + if (!matchValue(matchHeaderValue, headerValue)) { + return false + } + } + return true +} + +function safeUrl (path) { + if (typeof path !== 'string') { + return path + } + + const pathSegments = path.split('?') + + if (pathSegments.length !== 2) { + return path + } + + const qp = new URLSearchParams(pathSegments.pop()) + qp.sort() + return [...pathSegments, qp.toString()].join('?') +} + +function matchKey (mockDispatch, { path, method, body, headers }) { + const pathMatch = matchValue(mockDispatch.path, path) + const methodMatch = matchValue(mockDispatch.method, method) + const bodyMatch = typeof mockDispatch.body !== 'undefined' ? matchValue(mockDispatch.body, body) : true + const headersMatch = matchHeaders(mockDispatch, headers) + return pathMatch && methodMatch && bodyMatch && headersMatch +} + +function getResponseData (data) { + if (Buffer.isBuffer(data)) { + return data + } else if (typeof data === 'object') { + return JSON.stringify(data) + } else { + return data.toString() + } +} + +function getMockDispatch (mockDispatches, key) { + const basePath = key.query ? buildURL(key.path, key.query) : key.path + const resolvedPath = typeof basePath === 'string' ? safeUrl(basePath) : basePath + + // Match path + let matchedMockDispatches = mockDispatches.filter(({ consumed }) => !consumed).filter(({ path }) => matchValue(safeUrl(path), resolvedPath)) + if (matchedMockDispatches.length === 0) { + throw new MockNotMatchedError(`Mock dispatch not matched for path '${resolvedPath}'`) + } + + // Match method + matchedMockDispatches = matchedMockDispatches.filter(({ method }) => matchValue(method, key.method)) + if (matchedMockDispatches.length === 0) { + throw new MockNotMatchedError(`Mock dispatch not matched for method '${key.method}'`) + } + + // Match body + matchedMockDispatches = matchedMockDispatches.filter(({ body }) => typeof body !== 'undefined' ? matchValue(body, key.body) : true) + if (matchedMockDispatches.length === 0) { + throw new MockNotMatchedError(`Mock dispatch not matched for body '${key.body}'`) + } + + // Match headers + matchedMockDispatches = matchedMockDispatches.filter((mockDispatch) => matchHeaders(mockDispatch, key.headers)) + if (matchedMockDispatches.length === 0) { + throw new MockNotMatchedError(`Mock dispatch not matched for headers '${typeof key.headers === 'object' ? JSON.stringify(key.headers) : key.headers}'`) + } + + return matchedMockDispatches[0] +} + +function addMockDispatch (mockDispatches, key, data) { + const baseData = { timesInvoked: 0, times: 1, persist: false, consumed: false } + const replyData = typeof data === 'function' ? { callback: data } : { ...data } + const newMockDispatch = { ...baseData, ...key, pending: true, data: { error: null, ...replyData } } + mockDispatches.push(newMockDispatch) + return newMockDispatch +} + +function deleteMockDispatch (mockDispatches, key) { + const index = mockDispatches.findIndex(dispatch => { + if (!dispatch.consumed) { + return false + } + return matchKey(dispatch, key) + }) + if (index !== -1) { + mockDispatches.splice(index, 1) + } +} + +function buildKey (opts) { + const { path, method, body, headers, query } = opts + return { + path, + method, + body, + headers, + query + } +} + +function generateKeyValues (data) { + return Object.entries(data).reduce((keyValuePairs, [key, value]) => [ + ...keyValuePairs, + Buffer.from(`${key}`), + Array.isArray(value) ? value.map(x => Buffer.from(`${x}`)) : Buffer.from(`${value}`) + ], []) +} + +/** + * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Status + * @param {number} statusCode + */ +function getStatusText (statusCode) { + return STATUS_CODES[statusCode] || 'unknown' +} + +async function getResponse (body) { + const buffers = [] + for await (const data of body) { + buffers.push(data) + } + return Buffer.concat(buffers).toString('utf8') +} + +/** + * Mock dispatch function used to simulate undici dispatches + */ +function mockDispatch (opts, handler) { + // Get mock dispatch from built key + const key = buildKey(opts) + const mockDispatch = getMockDispatch(this[kDispatches], key) + + mockDispatch.timesInvoked++ + + // Here's where we resolve a callback if a callback is present for the dispatch data. + if (mockDispatch.data.callback) { + mockDispatch.data = { ...mockDispatch.data, ...mockDispatch.data.callback(opts) } + } + + // Parse mockDispatch data + const { data: { statusCode, data, headers, trailers, error }, delay, persist } = mockDispatch + const { timesInvoked, times } = mockDispatch + + // If it's used up and not persistent, mark as consumed + mockDispatch.consumed = !persist && timesInvoked >= times + mockDispatch.pending = timesInvoked < times + + // If specified, trigger dispatch error + if (error !== null) { + deleteMockDispatch(this[kDispatches], key) + handler.onError(error) + return true + } + + // Handle the request with a delay if necessary + if (typeof delay === 'number' && delay > 0) { + setTimeout(() => { + handleReply(this[kDispatches]) + }, delay) + } else { + handleReply(this[kDispatches]) + } + + function handleReply (mockDispatches, _data = data) { + // fetch's HeadersList is a 1D string array + const optsHeaders = Array.isArray(opts.headers) + ? buildHeadersFromArray(opts.headers) + : opts.headers + const body = typeof _data === 'function' + ? _data({ ...opts, headers: optsHeaders }) + : _data + + // util.types.isPromise is likely needed for jest. + if (isPromise(body)) { + // If handleReply is asynchronous, throwing an error + // in the callback will reject the promise, rather than + // synchronously throw the error, which breaks some tests. + // Rather, we wait for the callback to resolve if it is a + // promise, and then re-run handleReply with the new body. + body.then((newData) => handleReply(mockDispatches, newData)) + return + } + + const responseData = getResponseData(body) + const responseHeaders = generateKeyValues(headers) + const responseTrailers = generateKeyValues(trailers) + + handler.abort = nop + handler.onHeaders(statusCode, responseHeaders, resume, getStatusText(statusCode)) + handler.onData(Buffer.from(responseData)) + handler.onComplete(responseTrailers) + deleteMockDispatch(mockDispatches, key) + } + + function resume () {} + + return true +} + +function buildMockDispatch () { + const agent = this[kMockAgent] + const origin = this[kOrigin] + const originalDispatch = this[kOriginalDispatch] + + return function dispatch (opts, handler) { + if (agent.isMockActive) { + try { + mockDispatch.call(this, opts, handler) + } catch (error) { + if (error instanceof MockNotMatchedError) { + const netConnect = agent[kGetNetConnect]() + if (netConnect === false) { + throw new MockNotMatchedError(`${error.message}: subsequent request to origin ${origin} was not allowed (net.connect disabled)`) + } + if (checkNetConnect(netConnect, origin)) { + originalDispatch.call(this, opts, handler) + } else { + throw new MockNotMatchedError(`${error.message}: subsequent request to origin ${origin} was not allowed (net.connect is not enabled for this origin)`) + } + } else { + throw error + } + } + } else { + originalDispatch.call(this, opts, handler) + } + } +} + +function checkNetConnect (netConnect, origin) { + const url = new URL(origin) + if (netConnect === true) { + return true + } else if (Array.isArray(netConnect) && netConnect.some((matcher) => matchValue(matcher, url.host))) { + return true + } + return false +} + +function buildMockOptions (opts) { + if (opts) { + const { agent, ...mockOptions } = opts + return mockOptions + } +} + +module.exports = { + getResponseData, + getMockDispatch, + addMockDispatch, + deleteMockDispatch, + buildKey, + generateKeyValues, + matchValue, + getResponse, + getStatusText, + mockDispatch, + buildMockDispatch, + checkNetConnect, + buildMockOptions, + getHeaderByName +} + + +/***/ }), + +/***/ 6142: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const { Transform } = __nccwpck_require__(2203) +const { Console } = __nccwpck_require__(4236) + +/** + * Gets the output of `console.table(…)` as a string. + */ +module.exports = class PendingInterceptorsFormatter { + constructor ({ disableColors } = {}) { + this.transform = new Transform({ + transform (chunk, _enc, cb) { + cb(null, chunk) + } + }) + + this.logger = new Console({ + stdout: this.transform, + inspectOptions: { + colors: !disableColors && !process.env.CI + } + }) + } + + format (pendingInterceptors) { + const withPrettyHeaders = pendingInterceptors.map( + ({ method, path, data: { statusCode }, persist, times, timesInvoked, origin }) => ({ + Method: method, + Origin: origin, + Path: path, + 'Status code': statusCode, + Persistent: persist ? '✅' : '❌', + Invocations: timesInvoked, + Remaining: persist ? Infinity : times - timesInvoked + })) + + this.logger.table(withPrettyHeaders) + return this.transform.read().toString() + } +} + + +/***/ }), + +/***/ 1529: +/***/ ((module) => { + +"use strict"; + + +const singulars = { + pronoun: 'it', + is: 'is', + was: 'was', + this: 'this' +} + +const plurals = { + pronoun: 'they', + is: 'are', + was: 'were', + this: 'these' +} + +module.exports = class Pluralizer { + constructor (singular, plural) { + this.singular = singular + this.plural = plural + } + + pluralize (count) { + const one = count === 1 + const keys = one ? singulars : plurals + const noun = one ? this.singular : this.plural + return { ...keys, count, noun } + } +} + + +/***/ }), + +/***/ 4869: +/***/ ((module) => { + +"use strict"; +/* eslint-disable */ + + + +// Extracted from node/lib/internal/fixed_queue.js + +// Currently optimal queue size, tested on V8 6.0 - 6.6. Must be power of two. +const kSize = 2048; +const kMask = kSize - 1; + +// The FixedQueue is implemented as a singly-linked list of fixed-size +// circular buffers. It looks something like this: +// +// head tail +// | | +// v v +// +-----------+ <-----\ +-----------+ <------\ +-----------+ +// | [null] | \----- | next | \------- | next | +// +-----------+ +-----------+ +-----------+ +// | item | <-- bottom | item | <-- bottom | [empty] | +// | item | | item | | [empty] | +// | item | | item | | [empty] | +// | item | | item | | [empty] | +// | item | | item | bottom --> | item | +// | item | | item | | item | +// | ... | | ... | | ... | +// | item | | item | | item | +// | item | | item | | item | +// | [empty] | <-- top | item | | item | +// | [empty] | | item | | item | +// | [empty] | | [empty] | <-- top top --> | [empty] | +// +-----------+ +-----------+ +-----------+ +// +// Or, if there is only one circular buffer, it looks something +// like either of these: +// +// head tail head tail +// | | | | +// v v v v +// +-----------+ +-----------+ +// | [null] | | [null] | +// +-----------+ +-----------+ +// | [empty] | | item | +// | [empty] | | item | +// | item | <-- bottom top --> | [empty] | +// | item | | [empty] | +// | [empty] | <-- top bottom --> | item | +// | [empty] | | item | +// +-----------+ +-----------+ +// +// Adding a value means moving `top` forward by one, removing means +// moving `bottom` forward by one. After reaching the end, the queue +// wraps around. +// +// When `top === bottom` the current queue is empty and when +// `top + 1 === bottom` it's full. This wastes a single space of storage +// but allows much quicker checks. + +class FixedCircularBuffer { + constructor() { + this.bottom = 0; + this.top = 0; + this.list = new Array(kSize); + this.next = null; + } + + isEmpty() { + return this.top === this.bottom; + } + + isFull() { + return ((this.top + 1) & kMask) === this.bottom; + } + + push(data) { + this.list[this.top] = data; + this.top = (this.top + 1) & kMask; + } + + shift() { + const nextItem = this.list[this.bottom]; + if (nextItem === undefined) + return null; + this.list[this.bottom] = undefined; + this.bottom = (this.bottom + 1) & kMask; + return nextItem; + } +} + +module.exports = class FixedQueue { + constructor() { + this.head = this.tail = new FixedCircularBuffer(); + } + + isEmpty() { + return this.head.isEmpty(); + } + + push(data) { + if (this.head.isFull()) { + // Head is full: Creates a new queue, sets the old queue's `.next` to it, + // and sets it as the new main queue. + this.head = this.head.next = new FixedCircularBuffer(); + } + this.head.push(data); + } + + shift() { + const tail = this.tail; + const next = tail.shift(); + if (tail.isEmpty() && tail.next !== null) { + // If there is another queue, it forms the new tail. + this.tail = tail.next; + } + return next; + } +}; + + +/***/ }), + +/***/ 8640: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const DispatcherBase = __nccwpck_require__(1) +const FixedQueue = __nccwpck_require__(4869) +const { kConnected, kSize, kRunning, kPending, kQueued, kBusy, kFree, kUrl, kClose, kDestroy, kDispatch } = __nccwpck_require__(6443) +const PoolStats = __nccwpck_require__(4622) + +const kClients = Symbol('clients') +const kNeedDrain = Symbol('needDrain') +const kQueue = Symbol('queue') +const kClosedResolve = Symbol('closed resolve') +const kOnDrain = Symbol('onDrain') +const kOnConnect = Symbol('onConnect') +const kOnDisconnect = Symbol('onDisconnect') +const kOnConnectionError = Symbol('onConnectionError') +const kGetDispatcher = Symbol('get dispatcher') +const kAddClient = Symbol('add client') +const kRemoveClient = Symbol('remove client') +const kStats = Symbol('stats') + +class PoolBase extends DispatcherBase { + constructor () { + super() + + this[kQueue] = new FixedQueue() + this[kClients] = [] + this[kQueued] = 0 + + const pool = this + + this[kOnDrain] = function onDrain (origin, targets) { + const queue = pool[kQueue] + + let needDrain = false + + while (!needDrain) { + const item = queue.shift() + if (!item) { + break + } + pool[kQueued]-- + needDrain = !this.dispatch(item.opts, item.handler) + } + + this[kNeedDrain] = needDrain + + if (!this[kNeedDrain] && pool[kNeedDrain]) { + pool[kNeedDrain] = false + pool.emit('drain', origin, [pool, ...targets]) + } + + if (pool[kClosedResolve] && queue.isEmpty()) { + Promise + .all(pool[kClients].map(c => c.close())) + .then(pool[kClosedResolve]) + } + } + + this[kOnConnect] = (origin, targets) => { + pool.emit('connect', origin, [pool, ...targets]) + } + + this[kOnDisconnect] = (origin, targets, err) => { + pool.emit('disconnect', origin, [pool, ...targets], err) + } + + this[kOnConnectionError] = (origin, targets, err) => { + pool.emit('connectionError', origin, [pool, ...targets], err) + } + + this[kStats] = new PoolStats(this) + } + + get [kBusy] () { + return this[kNeedDrain] + } + + get [kConnected] () { + return this[kClients].filter(client => client[kConnected]).length + } + + get [kFree] () { + return this[kClients].filter(client => client[kConnected] && !client[kNeedDrain]).length + } + + get [kPending] () { + let ret = this[kQueued] + for (const { [kPending]: pending } of this[kClients]) { + ret += pending + } + return ret + } + + get [kRunning] () { + let ret = 0 + for (const { [kRunning]: running } of this[kClients]) { + ret += running + } + return ret + } + + get [kSize] () { + let ret = this[kQueued] + for (const { [kSize]: size } of this[kClients]) { + ret += size + } + return ret + } + + get stats () { + return this[kStats] + } + + async [kClose] () { + if (this[kQueue].isEmpty()) { + return Promise.all(this[kClients].map(c => c.close())) + } else { + return new Promise((resolve) => { + this[kClosedResolve] = resolve + }) + } + } + + async [kDestroy] (err) { + while (true) { + const item = this[kQueue].shift() + if (!item) { + break + } + item.handler.onError(err) + } + + return Promise.all(this[kClients].map(c => c.destroy(err))) + } + + [kDispatch] (opts, handler) { + const dispatcher = this[kGetDispatcher]() + + if (!dispatcher) { + this[kNeedDrain] = true + this[kQueue].push({ opts, handler }) + this[kQueued]++ + } else if (!dispatcher.dispatch(opts, handler)) { + dispatcher[kNeedDrain] = true + this[kNeedDrain] = !this[kGetDispatcher]() + } + + return !this[kNeedDrain] + } + + [kAddClient] (client) { + client + .on('drain', this[kOnDrain]) + .on('connect', this[kOnConnect]) + .on('disconnect', this[kOnDisconnect]) + .on('connectionError', this[kOnConnectionError]) + + this[kClients].push(client) + + if (this[kNeedDrain]) { + process.nextTick(() => { + if (this[kNeedDrain]) { + this[kOnDrain](client[kUrl], [this, client]) + } + }) + } + + return this + } + + [kRemoveClient] (client) { + client.close(() => { + const idx = this[kClients].indexOf(client) + if (idx !== -1) { + this[kClients].splice(idx, 1) + } + }) + + this[kNeedDrain] = this[kClients].some(dispatcher => ( + !dispatcher[kNeedDrain] && + dispatcher.closed !== true && + dispatcher.destroyed !== true + )) + } +} + +module.exports = { + PoolBase, + kClients, + kNeedDrain, + kAddClient, + kRemoveClient, + kGetDispatcher +} + + +/***/ }), + +/***/ 4622: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +const { kFree, kConnected, kPending, kQueued, kRunning, kSize } = __nccwpck_require__(6443) +const kPool = Symbol('pool') + +class PoolStats { + constructor (pool) { + this[kPool] = pool + } + + get connected () { + return this[kPool][kConnected] + } + + get free () { + return this[kPool][kFree] + } + + get pending () { + return this[kPool][kPending] + } + + get queued () { + return this[kPool][kQueued] + } + + get running () { + return this[kPool][kRunning] + } + + get size () { + return this[kPool][kSize] + } +} + +module.exports = PoolStats + + +/***/ }), + +/***/ 5076: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const { + PoolBase, + kClients, + kNeedDrain, + kAddClient, + kGetDispatcher +} = __nccwpck_require__(8640) +const Client = __nccwpck_require__(6197) +const { + InvalidArgumentError +} = __nccwpck_require__(8707) +const util = __nccwpck_require__(3440) +const { kUrl, kInterceptors } = __nccwpck_require__(6443) +const buildConnector = __nccwpck_require__(9136) + +const kOptions = Symbol('options') +const kConnections = Symbol('connections') +const kFactory = Symbol('factory') + +function defaultFactory (origin, opts) { + return new Client(origin, opts) +} + +class Pool extends PoolBase { + constructor (origin, { + connections, + factory = defaultFactory, + connect, + connectTimeout, + tls, + maxCachedSessions, + socketPath, + autoSelectFamily, + autoSelectFamilyAttemptTimeout, + allowH2, + ...options + } = {}) { + super() + + if (connections != null && (!Number.isFinite(connections) || connections < 0)) { + throw new InvalidArgumentError('invalid connections') + } + + if (typeof factory !== 'function') { + throw new InvalidArgumentError('factory must be a function.') + } + + if (connect != null && typeof connect !== 'function' && typeof connect !== 'object') { + throw new InvalidArgumentError('connect must be a function or an object') + } + + if (typeof connect !== 'function') { + connect = buildConnector({ + ...tls, + maxCachedSessions, + allowH2, + socketPath, + timeout: connectTimeout, + ...(util.nodeHasAutoSelectFamily && autoSelectFamily ? { autoSelectFamily, autoSelectFamilyAttemptTimeout } : undefined), + ...connect + }) + } + + this[kInterceptors] = options.interceptors && options.interceptors.Pool && Array.isArray(options.interceptors.Pool) + ? options.interceptors.Pool + : [] + this[kConnections] = connections || null + this[kUrl] = util.parseOrigin(origin) + this[kOptions] = { ...util.deepClone(options), connect, allowH2 } + this[kOptions].interceptors = options.interceptors + ? { ...options.interceptors } + : undefined + this[kFactory] = factory + } + + [kGetDispatcher] () { + let dispatcher = this[kClients].find(dispatcher => !dispatcher[kNeedDrain]) + + if (dispatcher) { + return dispatcher + } + + if (!this[kConnections] || this[kClients].length < this[kConnections]) { + dispatcher = this[kFactory](this[kUrl], this[kOptions]) + this[kAddClient](dispatcher) + } + + return dispatcher + } +} + +module.exports = Pool + + +/***/ }), + +/***/ 2720: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const { kProxy, kClose, kDestroy, kInterceptors } = __nccwpck_require__(6443) +const { URL } = __nccwpck_require__(7016) +const Agent = __nccwpck_require__(9965) +const Pool = __nccwpck_require__(5076) +const DispatcherBase = __nccwpck_require__(1) +const { InvalidArgumentError, RequestAbortedError } = __nccwpck_require__(8707) +const buildConnector = __nccwpck_require__(9136) + +const kAgent = Symbol('proxy agent') +const kClient = Symbol('proxy client') +const kProxyHeaders = Symbol('proxy headers') +const kRequestTls = Symbol('request tls settings') +const kProxyTls = Symbol('proxy tls settings') +const kConnectEndpoint = Symbol('connect endpoint function') + +function defaultProtocolPort (protocol) { + return protocol === 'https:' ? 443 : 80 +} + +function buildProxyOptions (opts) { + if (typeof opts === 'string') { + opts = { uri: opts } + } + + if (!opts || !opts.uri) { + throw new InvalidArgumentError('Proxy opts.uri is mandatory') + } + + return { + uri: opts.uri, + protocol: opts.protocol || 'https' + } +} + +function defaultFactory (origin, opts) { + return new Pool(origin, opts) +} + +class ProxyAgent extends DispatcherBase { + constructor (opts) { + super(opts) + this[kProxy] = buildProxyOptions(opts) + this[kAgent] = new Agent(opts) + this[kInterceptors] = opts.interceptors && opts.interceptors.ProxyAgent && Array.isArray(opts.interceptors.ProxyAgent) + ? opts.interceptors.ProxyAgent + : [] + + if (typeof opts === 'string') { + opts = { uri: opts } + } + + if (!opts || !opts.uri) { + throw new InvalidArgumentError('Proxy opts.uri is mandatory') + } + + const { clientFactory = defaultFactory } = opts + + if (typeof clientFactory !== 'function') { + throw new InvalidArgumentError('Proxy opts.clientFactory must be a function.') + } + + this[kRequestTls] = opts.requestTls + this[kProxyTls] = opts.proxyTls + this[kProxyHeaders] = opts.headers || {} + + const resolvedUrl = new URL(opts.uri) + const { origin, port, host, username, password } = resolvedUrl + + if (opts.auth && opts.token) { + throw new InvalidArgumentError('opts.auth cannot be used in combination with opts.token') + } else if (opts.auth) { + /* @deprecated in favour of opts.token */ + this[kProxyHeaders]['proxy-authorization'] = `Basic ${opts.auth}` + } else if (opts.token) { + this[kProxyHeaders]['proxy-authorization'] = opts.token + } else if (username && password) { + this[kProxyHeaders]['proxy-authorization'] = `Basic ${Buffer.from(`${decodeURIComponent(username)}:${decodeURIComponent(password)}`).toString('base64')}` + } + + const connect = buildConnector({ ...opts.proxyTls }) + this[kConnectEndpoint] = buildConnector({ ...opts.requestTls }) + this[kClient] = clientFactory(resolvedUrl, { connect }) + this[kAgent] = new Agent({ + ...opts, + connect: async (opts, callback) => { + let requestedHost = opts.host + if (!opts.port) { + requestedHost += `:${defaultProtocolPort(opts.protocol)}` + } + try { + const { socket, statusCode } = await this[kClient].connect({ + origin, + port, + path: requestedHost, + signal: opts.signal, + headers: { + ...this[kProxyHeaders], + host + } + }) + if (statusCode !== 200) { + socket.on('error', () => {}).destroy() + callback(new RequestAbortedError(`Proxy response (${statusCode}) !== 200 when HTTP Tunneling`)) + } + if (opts.protocol !== 'https:') { + callback(null, socket) + return + } + let servername + if (this[kRequestTls]) { + servername = this[kRequestTls].servername + } else { + servername = opts.servername + } + this[kConnectEndpoint]({ ...opts, servername, httpSocket: socket }, callback) + } catch (err) { + callback(err) + } + } + }) + } + + dispatch (opts, handler) { + const { host } = new URL(opts.origin) + const headers = buildHeaders(opts.headers) + throwIfProxyAuthIsSent(headers) + return this[kAgent].dispatch( + { + ...opts, + headers: { + ...headers, + host + } + }, + handler + ) + } + + async [kClose] () { + await this[kAgent].close() + await this[kClient].close() + } + + async [kDestroy] () { + await this[kAgent].destroy() + await this[kClient].destroy() + } +} + +/** + * @param {string[] | Record} headers + * @returns {Record} + */ +function buildHeaders (headers) { + // When using undici.fetch, the headers list is stored + // as an array. + if (Array.isArray(headers)) { + /** @type {Record} */ + const headersPair = {} + + for (let i = 0; i < headers.length; i += 2) { + headersPair[headers[i]] = headers[i + 1] + } + + return headersPair + } + + return headers +} + +/** + * @param {Record} headers + * + * Previous versions of ProxyAgent suggests the Proxy-Authorization in request headers + * Nevertheless, it was changed and to avoid a security vulnerability by end users + * this check was created. + * It should be removed in the next major version for performance reasons + */ +function throwIfProxyAuthIsSent (headers) { + const existProxyAuth = headers && Object.keys(headers) + .find((key) => key.toLowerCase() === 'proxy-authorization') + if (existProxyAuth) { + throw new InvalidArgumentError('Proxy-Authorization should be sent in ProxyAgent constructor') + } +} + +module.exports = ProxyAgent + + +/***/ }), + +/***/ 8804: +/***/ ((module) => { + +"use strict"; + + +let fastNow = Date.now() +let fastNowTimeout + +const fastTimers = [] + +function onTimeout () { + fastNow = Date.now() + + let len = fastTimers.length + let idx = 0 + while (idx < len) { + const timer = fastTimers[idx] + + if (timer.state === 0) { + timer.state = fastNow + timer.delay + } else if (timer.state > 0 && fastNow >= timer.state) { + timer.state = -1 + timer.callback(timer.opaque) + } + + if (timer.state === -1) { + timer.state = -2 + if (idx !== len - 1) { + fastTimers[idx] = fastTimers.pop() + } else { + fastTimers.pop() + } + len -= 1 + } else { + idx += 1 + } + } + + if (fastTimers.length > 0) { + refreshTimeout() + } +} + +function refreshTimeout () { + if (fastNowTimeout && fastNowTimeout.refresh) { + fastNowTimeout.refresh() + } else { + clearTimeout(fastNowTimeout) + fastNowTimeout = setTimeout(onTimeout, 1e3) + if (fastNowTimeout.unref) { + fastNowTimeout.unref() + } + } +} + +class Timeout { + constructor (callback, delay, opaque) { + this.callback = callback + this.delay = delay + this.opaque = opaque + + // -2 not in timer list + // -1 in timer list but inactive + // 0 in timer list waiting for time + // > 0 in timer list waiting for time to expire + this.state = -2 + + this.refresh() + } + + refresh () { + if (this.state === -2) { + fastTimers.push(this) + if (!fastNowTimeout || fastTimers.length === 1) { + refreshTimeout() + } + } + + this.state = 0 + } + + clear () { + this.state = -1 + } +} + +module.exports = { + setTimeout (callback, delay, opaque) { + return delay < 1e3 + ? setTimeout(callback, delay, opaque) + : new Timeout(callback, delay, opaque) + }, + clearTimeout (timeout) { + if (timeout instanceof Timeout) { + timeout.clear() + } else { + clearTimeout(timeout) + } + } +} + + +/***/ }), + +/***/ 8550: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const diagnosticsChannel = __nccwpck_require__(1637) +const { uid, states } = __nccwpck_require__(5913) +const { + kReadyState, + kSentClose, + kByteParser, + kReceivedClose +} = __nccwpck_require__(2933) +const { fireEvent, failWebsocketConnection } = __nccwpck_require__(3574) +const { CloseEvent } = __nccwpck_require__(6255) +const { makeRequest } = __nccwpck_require__(5194) +const { fetching } = __nccwpck_require__(2315) +const { Headers } = __nccwpck_require__(6349) +const { getGlobalDispatcher } = __nccwpck_require__(2581) +const { kHeadersList } = __nccwpck_require__(6443) + +const channels = {} +channels.open = diagnosticsChannel.channel('undici:websocket:open') +channels.close = diagnosticsChannel.channel('undici:websocket:close') +channels.socketError = diagnosticsChannel.channel('undici:websocket:socket_error') + +/** @type {import('crypto')} */ +let crypto +try { + crypto = __nccwpck_require__(6982) +} catch { + +} + +/** + * @see https://websockets.spec.whatwg.org/#concept-websocket-establish + * @param {URL} url + * @param {string|string[]} protocols + * @param {import('./websocket').WebSocket} ws + * @param {(response: any) => void} onEstablish + * @param {Partial} options + */ +function establishWebSocketConnection (url, protocols, ws, onEstablish, options) { + // 1. Let requestURL be a copy of url, with its scheme set to "http", if url’s + // scheme is "ws", and to "https" otherwise. + const requestURL = url + + requestURL.protocol = url.protocol === 'ws:' ? 'http:' : 'https:' + + // 2. Let request be a new request, whose URL is requestURL, client is client, + // service-workers mode is "none", referrer is "no-referrer", mode is + // "websocket", credentials mode is "include", cache mode is "no-store" , + // and redirect mode is "error". + const request = makeRequest({ + urlList: [requestURL], + serviceWorkers: 'none', + referrer: 'no-referrer', + mode: 'websocket', + credentials: 'include', + cache: 'no-store', + redirect: 'error' + }) + + // Note: undici extension, allow setting custom headers. + if (options.headers) { + const headersList = new Headers(options.headers)[kHeadersList] + + request.headersList = headersList + } + + // 3. Append (`Upgrade`, `websocket`) to request’s header list. + // 4. Append (`Connection`, `Upgrade`) to request’s header list. + // Note: both of these are handled by undici currently. + // https://github.com/nodejs/undici/blob/68c269c4144c446f3f1220951338daef4a6b5ec4/lib/client.js#L1397 + + // 5. Let keyValue be a nonce consisting of a randomly selected + // 16-byte value that has been forgiving-base64-encoded and + // isomorphic encoded. + const keyValue = crypto.randomBytes(16).toString('base64') + + // 6. Append (`Sec-WebSocket-Key`, keyValue) to request’s + // header list. + request.headersList.append('sec-websocket-key', keyValue) + + // 7. Append (`Sec-WebSocket-Version`, `13`) to request’s + // header list. + request.headersList.append('sec-websocket-version', '13') + + // 8. For each protocol in protocols, combine + // (`Sec-WebSocket-Protocol`, protocol) in request’s header + // list. + for (const protocol of protocols) { + request.headersList.append('sec-websocket-protocol', protocol) + } + + // 9. Let permessageDeflate be a user-agent defined + // "permessage-deflate" extension header value. + // https://github.com/mozilla/gecko-dev/blob/ce78234f5e653a5d3916813ff990f053510227bc/netwerk/protocol/websocket/WebSocketChannel.cpp#L2673 + // TODO: enable once permessage-deflate is supported + const permessageDeflate = '' // 'permessage-deflate; 15' + + // 10. Append (`Sec-WebSocket-Extensions`, permessageDeflate) to + // request’s header list. + // request.headersList.append('sec-websocket-extensions', permessageDeflate) + + // 11. Fetch request with useParallelQueue set to true, and + // processResponse given response being these steps: + const controller = fetching({ + request, + useParallelQueue: true, + dispatcher: options.dispatcher ?? getGlobalDispatcher(), + processResponse (response) { + // 1. If response is a network error or its status is not 101, + // fail the WebSocket connection. + if (response.type === 'error' || response.status !== 101) { + failWebsocketConnection(ws, 'Received network error or non-101 status code.') + return + } + + // 2. If protocols is not the empty list and extracting header + // list values given `Sec-WebSocket-Protocol` and response’s + // header list results in null, failure, or the empty byte + // sequence, then fail the WebSocket connection. + if (protocols.length !== 0 && !response.headersList.get('Sec-WebSocket-Protocol')) { + failWebsocketConnection(ws, 'Server did not respond with sent protocols.') + return + } + + // 3. Follow the requirements stated step 2 to step 6, inclusive, + // of the last set of steps in section 4.1 of The WebSocket + // Protocol to validate response. This either results in fail + // the WebSocket connection or the WebSocket connection is + // established. + + // 2. If the response lacks an |Upgrade| header field or the |Upgrade| + // header field contains a value that is not an ASCII case- + // insensitive match for the value "websocket", the client MUST + // _Fail the WebSocket Connection_. + if (response.headersList.get('Upgrade')?.toLowerCase() !== 'websocket') { + failWebsocketConnection(ws, 'Server did not set Upgrade header to "websocket".') + return + } + + // 3. If the response lacks a |Connection| header field or the + // |Connection| header field doesn't contain a token that is an + // ASCII case-insensitive match for the value "Upgrade", the client + // MUST _Fail the WebSocket Connection_. + if (response.headersList.get('Connection')?.toLowerCase() !== 'upgrade') { + failWebsocketConnection(ws, 'Server did not set Connection header to "upgrade".') + return + } + + // 4. If the response lacks a |Sec-WebSocket-Accept| header field or + // the |Sec-WebSocket-Accept| contains a value other than the + // base64-encoded SHA-1 of the concatenation of the |Sec-WebSocket- + // Key| (as a string, not base64-decoded) with the string "258EAFA5- + // E914-47DA-95CA-C5AB0DC85B11" but ignoring any leading and + // trailing whitespace, the client MUST _Fail the WebSocket + // Connection_. + const secWSAccept = response.headersList.get('Sec-WebSocket-Accept') + const digest = crypto.createHash('sha1').update(keyValue + uid).digest('base64') + if (secWSAccept !== digest) { + failWebsocketConnection(ws, 'Incorrect hash received in Sec-WebSocket-Accept header.') + return + } + + // 5. If the response includes a |Sec-WebSocket-Extensions| header + // field and this header field indicates the use of an extension + // that was not present in the client's handshake (the server has + // indicated an extension not requested by the client), the client + // MUST _Fail the WebSocket Connection_. (The parsing of this + // header field to determine which extensions are requested is + // discussed in Section 9.1.) + const secExtension = response.headersList.get('Sec-WebSocket-Extensions') + + if (secExtension !== null && secExtension !== permessageDeflate) { + failWebsocketConnection(ws, 'Received different permessage-deflate than the one set.') + return + } + + // 6. If the response includes a |Sec-WebSocket-Protocol| header field + // and this header field indicates the use of a subprotocol that was + // not present in the client's handshake (the server has indicated a + // subprotocol not requested by the client), the client MUST _Fail + // the WebSocket Connection_. + const secProtocol = response.headersList.get('Sec-WebSocket-Protocol') + + if (secProtocol !== null && secProtocol !== request.headersList.get('Sec-WebSocket-Protocol')) { + failWebsocketConnection(ws, 'Protocol was not set in the opening handshake.') + return + } + + response.socket.on('data', onSocketData) + response.socket.on('close', onSocketClose) + response.socket.on('error', onSocketError) + + if (channels.open.hasSubscribers) { + channels.open.publish({ + address: response.socket.address(), + protocol: secProtocol, + extensions: secExtension + }) + } + + onEstablish(response) + } + }) + + return controller +} + +/** + * @param {Buffer} chunk + */ +function onSocketData (chunk) { + if (!this.ws[kByteParser].write(chunk)) { + this.pause() + } +} + +/** + * @see https://websockets.spec.whatwg.org/#feedback-from-the-protocol + * @see https://datatracker.ietf.org/doc/html/rfc6455#section-7.1.4 + */ +function onSocketClose () { + const { ws } = this + + // If the TCP connection was closed after the + // WebSocket closing handshake was completed, the WebSocket connection + // is said to have been closed _cleanly_. + const wasClean = ws[kSentClose] && ws[kReceivedClose] + + let code = 1005 + let reason = '' + + const result = ws[kByteParser].closingInfo + + if (result) { + code = result.code ?? 1005 + reason = result.reason + } else if (!ws[kSentClose]) { + // If _The WebSocket + // Connection is Closed_ and no Close control frame was received by the + // endpoint (such as could occur if the underlying transport connection + // is lost), _The WebSocket Connection Close Code_ is considered to be + // 1006. + code = 1006 + } + + // 1. Change the ready state to CLOSED (3). + ws[kReadyState] = states.CLOSED + + // 2. If the user agent was required to fail the WebSocket + // connection, or if the WebSocket connection was closed + // after being flagged as full, fire an event named error + // at the WebSocket object. + // TODO + + // 3. Fire an event named close at the WebSocket object, + // using CloseEvent, with the wasClean attribute + // initialized to true if the connection closed cleanly + // and false otherwise, the code attribute initialized to + // the WebSocket connection close code, and the reason + // attribute initialized to the result of applying UTF-8 + // decode without BOM to the WebSocket connection close + // reason. + fireEvent('close', ws, CloseEvent, { + wasClean, code, reason + }) + + if (channels.close.hasSubscribers) { + channels.close.publish({ + websocket: ws, + code, + reason + }) + } +} + +function onSocketError (error) { + const { ws } = this + + ws[kReadyState] = states.CLOSING + + if (channels.socketError.hasSubscribers) { + channels.socketError.publish(error) + } + + this.destroy() +} + +module.exports = { + establishWebSocketConnection +} + + +/***/ }), + +/***/ 5913: +/***/ ((module) => { + +"use strict"; + + +// This is a Globally Unique Identifier unique used +// to validate that the endpoint accepts websocket +// connections. +// See https://www.rfc-editor.org/rfc/rfc6455.html#section-1.3 +const uid = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11' + +/** @type {PropertyDescriptor} */ +const staticPropertyDescriptors = { + enumerable: true, + writable: false, + configurable: false +} + +const states = { + CONNECTING: 0, + OPEN: 1, + CLOSING: 2, + CLOSED: 3 +} + +const opcodes = { + CONTINUATION: 0x0, + TEXT: 0x1, + BINARY: 0x2, + CLOSE: 0x8, + PING: 0x9, + PONG: 0xA +} + +const maxUnsigned16Bit = 2 ** 16 - 1 // 65535 + +const parserStates = { + INFO: 0, + PAYLOADLENGTH_16: 2, + PAYLOADLENGTH_64: 3, + READ_DATA: 4 +} + +const emptyBuffer = Buffer.allocUnsafe(0) + +module.exports = { + uid, + staticPropertyDescriptors, + states, + opcodes, + maxUnsigned16Bit, + parserStates, + emptyBuffer +} + + +/***/ }), + +/***/ 6255: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const { webidl } = __nccwpck_require__(4222) +const { kEnumerableProperty } = __nccwpck_require__(3440) +const { MessagePort } = __nccwpck_require__(8167) + +/** + * @see https://html.spec.whatwg.org/multipage/comms.html#messageevent + */ +class MessageEvent extends Event { + #eventInit + + constructor (type, eventInitDict = {}) { + webidl.argumentLengthCheck(arguments, 1, { header: 'MessageEvent constructor' }) + + type = webidl.converters.DOMString(type) + eventInitDict = webidl.converters.MessageEventInit(eventInitDict) + + super(type, eventInitDict) + + this.#eventInit = eventInitDict + } + + get data () { + webidl.brandCheck(this, MessageEvent) + + return this.#eventInit.data + } + + get origin () { + webidl.brandCheck(this, MessageEvent) + + return this.#eventInit.origin + } + + get lastEventId () { + webidl.brandCheck(this, MessageEvent) + + return this.#eventInit.lastEventId + } + + get source () { + webidl.brandCheck(this, MessageEvent) + + return this.#eventInit.source + } + + get ports () { + webidl.brandCheck(this, MessageEvent) + + if (!Object.isFrozen(this.#eventInit.ports)) { + Object.freeze(this.#eventInit.ports) + } + + return this.#eventInit.ports + } + + initMessageEvent ( + type, + bubbles = false, + cancelable = false, + data = null, + origin = '', + lastEventId = '', + source = null, + ports = [] + ) { + webidl.brandCheck(this, MessageEvent) + + webidl.argumentLengthCheck(arguments, 1, { header: 'MessageEvent.initMessageEvent' }) + + return new MessageEvent(type, { + bubbles, cancelable, data, origin, lastEventId, source, ports + }) + } +} + +/** + * @see https://websockets.spec.whatwg.org/#the-closeevent-interface + */ +class CloseEvent extends Event { + #eventInit + + constructor (type, eventInitDict = {}) { + webidl.argumentLengthCheck(arguments, 1, { header: 'CloseEvent constructor' }) + + type = webidl.converters.DOMString(type) + eventInitDict = webidl.converters.CloseEventInit(eventInitDict) + + super(type, eventInitDict) + + this.#eventInit = eventInitDict + } + + get wasClean () { + webidl.brandCheck(this, CloseEvent) + + return this.#eventInit.wasClean + } + + get code () { + webidl.brandCheck(this, CloseEvent) + + return this.#eventInit.code + } + + get reason () { + webidl.brandCheck(this, CloseEvent) + + return this.#eventInit.reason + } +} + +// https://html.spec.whatwg.org/multipage/webappapis.html#the-errorevent-interface +class ErrorEvent extends Event { + #eventInit + + constructor (type, eventInitDict) { + webidl.argumentLengthCheck(arguments, 1, { header: 'ErrorEvent constructor' }) + + super(type, eventInitDict) + + type = webidl.converters.DOMString(type) + eventInitDict = webidl.converters.ErrorEventInit(eventInitDict ?? {}) + + this.#eventInit = eventInitDict + } + + get message () { + webidl.brandCheck(this, ErrorEvent) + + return this.#eventInit.message + } + + get filename () { + webidl.brandCheck(this, ErrorEvent) + + return this.#eventInit.filename + } + + get lineno () { + webidl.brandCheck(this, ErrorEvent) + + return this.#eventInit.lineno + } + + get colno () { + webidl.brandCheck(this, ErrorEvent) + + return this.#eventInit.colno + } + + get error () { + webidl.brandCheck(this, ErrorEvent) + + return this.#eventInit.error + } +} + +Object.defineProperties(MessageEvent.prototype, { + [Symbol.toStringTag]: { + value: 'MessageEvent', + configurable: true + }, + data: kEnumerableProperty, + origin: kEnumerableProperty, + lastEventId: kEnumerableProperty, + source: kEnumerableProperty, + ports: kEnumerableProperty, + initMessageEvent: kEnumerableProperty +}) + +Object.defineProperties(CloseEvent.prototype, { + [Symbol.toStringTag]: { + value: 'CloseEvent', + configurable: true + }, + reason: kEnumerableProperty, + code: kEnumerableProperty, + wasClean: kEnumerableProperty +}) + +Object.defineProperties(ErrorEvent.prototype, { + [Symbol.toStringTag]: { + value: 'ErrorEvent', + configurable: true + }, + message: kEnumerableProperty, + filename: kEnumerableProperty, + lineno: kEnumerableProperty, + colno: kEnumerableProperty, + error: kEnumerableProperty +}) + +webidl.converters.MessagePort = webidl.interfaceConverter(MessagePort) + +webidl.converters['sequence'] = webidl.sequenceConverter( + webidl.converters.MessagePort +) + +const eventInit = [ + { + key: 'bubbles', + converter: webidl.converters.boolean, + defaultValue: false + }, + { + key: 'cancelable', + converter: webidl.converters.boolean, + defaultValue: false + }, + { + key: 'composed', + converter: webidl.converters.boolean, + defaultValue: false + } +] + +webidl.converters.MessageEventInit = webidl.dictionaryConverter([ + ...eventInit, + { + key: 'data', + converter: webidl.converters.any, + defaultValue: null + }, + { + key: 'origin', + converter: webidl.converters.USVString, + defaultValue: '' + }, + { + key: 'lastEventId', + converter: webidl.converters.DOMString, + defaultValue: '' + }, + { + key: 'source', + // Node doesn't implement WindowProxy or ServiceWorker, so the only + // valid value for source is a MessagePort. + converter: webidl.nullableConverter(webidl.converters.MessagePort), + defaultValue: null + }, + { + key: 'ports', + converter: webidl.converters['sequence'], + get defaultValue () { + return [] + } + } +]) + +webidl.converters.CloseEventInit = webidl.dictionaryConverter([ + ...eventInit, + { + key: 'wasClean', + converter: webidl.converters.boolean, + defaultValue: false + }, + { + key: 'code', + converter: webidl.converters['unsigned short'], + defaultValue: 0 + }, + { + key: 'reason', + converter: webidl.converters.USVString, + defaultValue: '' + } +]) + +webidl.converters.ErrorEventInit = webidl.dictionaryConverter([ + ...eventInit, + { + key: 'message', + converter: webidl.converters.DOMString, + defaultValue: '' + }, + { + key: 'filename', + converter: webidl.converters.USVString, + defaultValue: '' + }, + { + key: 'lineno', + converter: webidl.converters['unsigned long'], + defaultValue: 0 + }, + { + key: 'colno', + converter: webidl.converters['unsigned long'], + defaultValue: 0 + }, + { + key: 'error', + converter: webidl.converters.any + } +]) + +module.exports = { + MessageEvent, + CloseEvent, + ErrorEvent +} + + +/***/ }), + +/***/ 1237: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const { maxUnsigned16Bit } = __nccwpck_require__(5913) + +/** @type {import('crypto')} */ +let crypto +try { + crypto = __nccwpck_require__(6982) +} catch { + +} + +class WebsocketFrameSend { + /** + * @param {Buffer|undefined} data + */ + constructor (data) { + this.frameData = data + this.maskKey = crypto.randomBytes(4) + } + + createFrame (opcode) { + const bodyLength = this.frameData?.byteLength ?? 0 + + /** @type {number} */ + let payloadLength = bodyLength // 0-125 + let offset = 6 + + if (bodyLength > maxUnsigned16Bit) { + offset += 8 // payload length is next 8 bytes + payloadLength = 127 + } else if (bodyLength > 125) { + offset += 2 // payload length is next 2 bytes + payloadLength = 126 + } + + const buffer = Buffer.allocUnsafe(bodyLength + offset) + + // Clear first 2 bytes, everything else is overwritten + buffer[0] = buffer[1] = 0 + buffer[0] |= 0x80 // FIN + buffer[0] = (buffer[0] & 0xF0) + opcode // opcode + + /*! ws. MIT License. Einar Otto Stangvik */ + buffer[offset - 4] = this.maskKey[0] + buffer[offset - 3] = this.maskKey[1] + buffer[offset - 2] = this.maskKey[2] + buffer[offset - 1] = this.maskKey[3] + + buffer[1] = payloadLength + + if (payloadLength === 126) { + buffer.writeUInt16BE(bodyLength, 2) + } else if (payloadLength === 127) { + // Clear extended payload length + buffer[2] = buffer[3] = 0 + buffer.writeUIntBE(bodyLength, 4, 6) + } + + buffer[1] |= 0x80 // MASK + + // mask body + for (let i = 0; i < bodyLength; i++) { + buffer[offset + i] = this.frameData[i] ^ this.maskKey[i % 4] + } + + return buffer + } +} + +module.exports = { + WebsocketFrameSend +} + + +/***/ }), + +/***/ 3171: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const { Writable } = __nccwpck_require__(2203) +const diagnosticsChannel = __nccwpck_require__(1637) +const { parserStates, opcodes, states, emptyBuffer } = __nccwpck_require__(5913) +const { kReadyState, kSentClose, kResponse, kReceivedClose } = __nccwpck_require__(2933) +const { isValidStatusCode, failWebsocketConnection, websocketMessageReceived } = __nccwpck_require__(3574) +const { WebsocketFrameSend } = __nccwpck_require__(1237) + +// This code was influenced by ws released under the MIT license. +// Copyright (c) 2011 Einar Otto Stangvik +// Copyright (c) 2013 Arnout Kazemier and contributors +// Copyright (c) 2016 Luigi Pinca and contributors + +const channels = {} +channels.ping = diagnosticsChannel.channel('undici:websocket:ping') +channels.pong = diagnosticsChannel.channel('undici:websocket:pong') + +class ByteParser extends Writable { + #buffers = [] + #byteOffset = 0 + + #state = parserStates.INFO + + #info = {} + #fragments = [] + + constructor (ws) { + super() + + this.ws = ws + } + + /** + * @param {Buffer} chunk + * @param {() => void} callback + */ + _write (chunk, _, callback) { + this.#buffers.push(chunk) + this.#byteOffset += chunk.length + + this.run(callback) + } + + /** + * Runs whenever a new chunk is received. + * Callback is called whenever there are no more chunks buffering, + * or not enough bytes are buffered to parse. + */ + run (callback) { + while (true) { + if (this.#state === parserStates.INFO) { + // If there aren't enough bytes to parse the payload length, etc. + if (this.#byteOffset < 2) { + return callback() + } + + const buffer = this.consume(2) + + this.#info.fin = (buffer[0] & 0x80) !== 0 + this.#info.opcode = buffer[0] & 0x0F + + // If we receive a fragmented message, we use the type of the first + // frame to parse the full message as binary/text, when it's terminated + this.#info.originalOpcode ??= this.#info.opcode + + this.#info.fragmented = !this.#info.fin && this.#info.opcode !== opcodes.CONTINUATION + + if (this.#info.fragmented && this.#info.opcode !== opcodes.BINARY && this.#info.opcode !== opcodes.TEXT) { + // Only text and binary frames can be fragmented + failWebsocketConnection(this.ws, 'Invalid frame type was fragmented.') + return + } + + const payloadLength = buffer[1] & 0x7F + + if (payloadLength <= 125) { + this.#info.payloadLength = payloadLength + this.#state = parserStates.READ_DATA + } else if (payloadLength === 126) { + this.#state = parserStates.PAYLOADLENGTH_16 + } else if (payloadLength === 127) { + this.#state = parserStates.PAYLOADLENGTH_64 + } + + if (this.#info.fragmented && payloadLength > 125) { + // A fragmented frame can't be fragmented itself + failWebsocketConnection(this.ws, 'Fragmented frame exceeded 125 bytes.') + return + } else if ( + (this.#info.opcode === opcodes.PING || + this.#info.opcode === opcodes.PONG || + this.#info.opcode === opcodes.CLOSE) && + payloadLength > 125 + ) { + // Control frames can have a payload length of 125 bytes MAX + failWebsocketConnection(this.ws, 'Payload length for control frame exceeded 125 bytes.') + return + } else if (this.#info.opcode === opcodes.CLOSE) { + if (payloadLength === 1) { + failWebsocketConnection(this.ws, 'Received close frame with a 1-byte body.') + return + } + + const body = this.consume(payloadLength) + + this.#info.closeInfo = this.parseCloseBody(false, body) + + if (!this.ws[kSentClose]) { + // If an endpoint receives a Close frame and did not previously send a + // Close frame, the endpoint MUST send a Close frame in response. (When + // sending a Close frame in response, the endpoint typically echos the + // status code it received.) + const body = Buffer.allocUnsafe(2) + body.writeUInt16BE(this.#info.closeInfo.code, 0) + const closeFrame = new WebsocketFrameSend(body) + + this.ws[kResponse].socket.write( + closeFrame.createFrame(opcodes.CLOSE), + (err) => { + if (!err) { + this.ws[kSentClose] = true + } + } + ) + } + + // Upon either sending or receiving a Close control frame, it is said + // that _The WebSocket Closing Handshake is Started_ and that the + // WebSocket connection is in the CLOSING state. + this.ws[kReadyState] = states.CLOSING + this.ws[kReceivedClose] = true + + this.end() + + return + } else if (this.#info.opcode === opcodes.PING) { + // Upon receipt of a Ping frame, an endpoint MUST send a Pong frame in + // response, unless it already received a Close frame. + // A Pong frame sent in response to a Ping frame must have identical + // "Application data" + + const body = this.consume(payloadLength) + + if (!this.ws[kReceivedClose]) { + const frame = new WebsocketFrameSend(body) + + this.ws[kResponse].socket.write(frame.createFrame(opcodes.PONG)) + + if (channels.ping.hasSubscribers) { + channels.ping.publish({ + payload: body + }) + } + } + + this.#state = parserStates.INFO + + if (this.#byteOffset > 0) { + continue + } else { + callback() + return + } + } else if (this.#info.opcode === opcodes.PONG) { + // A Pong frame MAY be sent unsolicited. This serves as a + // unidirectional heartbeat. A response to an unsolicited Pong frame is + // not expected. + + const body = this.consume(payloadLength) + + if (channels.pong.hasSubscribers) { + channels.pong.publish({ + payload: body + }) + } + + if (this.#byteOffset > 0) { + continue + } else { + callback() + return + } + } + } else if (this.#state === parserStates.PAYLOADLENGTH_16) { + if (this.#byteOffset < 2) { + return callback() + } + + const buffer = this.consume(2) + + this.#info.payloadLength = buffer.readUInt16BE(0) + this.#state = parserStates.READ_DATA + } else if (this.#state === parserStates.PAYLOADLENGTH_64) { + if (this.#byteOffset < 8) { + return callback() + } + + const buffer = this.consume(8) + const upper = buffer.readUInt32BE(0) + + // 2^31 is the maxinimum bytes an arraybuffer can contain + // on 32-bit systems. Although, on 64-bit systems, this is + // 2^53-1 bytes. + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/Invalid_array_length + // https://source.chromium.org/chromium/chromium/src/+/main:v8/src/common/globals.h;drc=1946212ac0100668f14eb9e2843bdd846e510a1e;bpv=1;bpt=1;l=1275 + // https://source.chromium.org/chromium/chromium/src/+/main:v8/src/objects/js-array-buffer.h;l=34;drc=1946212ac0100668f14eb9e2843bdd846e510a1e + if (upper > 2 ** 31 - 1) { + failWebsocketConnection(this.ws, 'Received payload length > 2^31 bytes.') + return + } + + const lower = buffer.readUInt32BE(4) + + this.#info.payloadLength = (upper << 8) + lower + this.#state = parserStates.READ_DATA + } else if (this.#state === parserStates.READ_DATA) { + if (this.#byteOffset < this.#info.payloadLength) { + // If there is still more data in this chunk that needs to be read + return callback() + } else if (this.#byteOffset >= this.#info.payloadLength) { + // If the server sent multiple frames in a single chunk + + const body = this.consume(this.#info.payloadLength) + + this.#fragments.push(body) + + // If the frame is unfragmented, or a fragmented frame was terminated, + // a message was received + if (!this.#info.fragmented || (this.#info.fin && this.#info.opcode === opcodes.CONTINUATION)) { + const fullMessage = Buffer.concat(this.#fragments) + + websocketMessageReceived(this.ws, this.#info.originalOpcode, fullMessage) + + this.#info = {} + this.#fragments.length = 0 + } + + this.#state = parserStates.INFO + } + } + + if (this.#byteOffset > 0) { + continue + } else { + callback() + break + } + } + } + + /** + * Take n bytes from the buffered Buffers + * @param {number} n + * @returns {Buffer|null} + */ + consume (n) { + if (n > this.#byteOffset) { + return null + } else if (n === 0) { + return emptyBuffer + } + + if (this.#buffers[0].length === n) { + this.#byteOffset -= this.#buffers[0].length + return this.#buffers.shift() + } + + const buffer = Buffer.allocUnsafe(n) + let offset = 0 + + while (offset !== n) { + const next = this.#buffers[0] + const { length } = next + + if (length + offset === n) { + buffer.set(this.#buffers.shift(), offset) + break + } else if (length + offset > n) { + buffer.set(next.subarray(0, n - offset), offset) + this.#buffers[0] = next.subarray(n - offset) + break + } else { + buffer.set(this.#buffers.shift(), offset) + offset += next.length + } + } + + this.#byteOffset -= n + + return buffer + } + + parseCloseBody (onlyCode, data) { + // https://datatracker.ietf.org/doc/html/rfc6455#section-7.1.5 + /** @type {number|undefined} */ + let code + + if (data.length >= 2) { + // _The WebSocket Connection Close Code_ is + // defined as the status code (Section 7.4) contained in the first Close + // control frame received by the application + code = data.readUInt16BE(0) + } + + if (onlyCode) { + if (!isValidStatusCode(code)) { + return null + } + + return { code } + } + + // https://datatracker.ietf.org/doc/html/rfc6455#section-7.1.6 + /** @type {Buffer} */ + let reason = data.subarray(2) + + // Remove BOM + if (reason[0] === 0xEF && reason[1] === 0xBB && reason[2] === 0xBF) { + reason = reason.subarray(3) + } + + if (code !== undefined && !isValidStatusCode(code)) { + return null + } + + try { + // TODO: optimize this + reason = new TextDecoder('utf-8', { fatal: true }).decode(reason) + } catch { + return null + } + + return { code, reason } + } + + get closingInfo () { + return this.#info.closeInfo + } +} + +module.exports = { + ByteParser +} + + +/***/ }), + +/***/ 2933: +/***/ ((module) => { + +"use strict"; + + +module.exports = { + kWebSocketURL: Symbol('url'), + kReadyState: Symbol('ready state'), + kController: Symbol('controller'), + kResponse: Symbol('response'), + kBinaryType: Symbol('binary type'), + kSentClose: Symbol('sent close'), + kReceivedClose: Symbol('received close'), + kByteParser: Symbol('byte parser') +} + + +/***/ }), + +/***/ 3574: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const { kReadyState, kController, kResponse, kBinaryType, kWebSocketURL } = __nccwpck_require__(2933) +const { states, opcodes } = __nccwpck_require__(5913) +const { MessageEvent, ErrorEvent } = __nccwpck_require__(6255) + +/* globals Blob */ + +/** + * @param {import('./websocket').WebSocket} ws + */ +function isEstablished (ws) { + // If the server's response is validated as provided for above, it is + // said that _The WebSocket Connection is Established_ and that the + // WebSocket Connection is in the OPEN state. + return ws[kReadyState] === states.OPEN +} + +/** + * @param {import('./websocket').WebSocket} ws + */ +function isClosing (ws) { + // Upon either sending or receiving a Close control frame, it is said + // that _The WebSocket Closing Handshake is Started_ and that the + // WebSocket connection is in the CLOSING state. + return ws[kReadyState] === states.CLOSING +} + +/** + * @param {import('./websocket').WebSocket} ws + */ +function isClosed (ws) { + return ws[kReadyState] === states.CLOSED +} + +/** + * @see https://dom.spec.whatwg.org/#concept-event-fire + * @param {string} e + * @param {EventTarget} target + * @param {EventInit | undefined} eventInitDict + */ +function fireEvent (e, target, eventConstructor = Event, eventInitDict) { + // 1. If eventConstructor is not given, then let eventConstructor be Event. + + // 2. Let event be the result of creating an event given eventConstructor, + // in the relevant realm of target. + // 3. Initialize event’s type attribute to e. + const event = new eventConstructor(e, eventInitDict) // eslint-disable-line new-cap + + // 4. Initialize any other IDL attributes of event as described in the + // invocation of this algorithm. + + // 5. Return the result of dispatching event at target, with legacy target + // override flag set if set. + target.dispatchEvent(event) +} + +/** + * @see https://websockets.spec.whatwg.org/#feedback-from-the-protocol + * @param {import('./websocket').WebSocket} ws + * @param {number} type Opcode + * @param {Buffer} data application data + */ +function websocketMessageReceived (ws, type, data) { + // 1. If ready state is not OPEN (1), then return. + if (ws[kReadyState] !== states.OPEN) { + return + } + + // 2. Let dataForEvent be determined by switching on type and binary type: + let dataForEvent + + if (type === opcodes.TEXT) { + // -> type indicates that the data is Text + // a new DOMString containing data + try { + dataForEvent = new TextDecoder('utf-8', { fatal: true }).decode(data) + } catch { + failWebsocketConnection(ws, 'Received invalid UTF-8 in text frame.') + return + } + } else if (type === opcodes.BINARY) { + if (ws[kBinaryType] === 'blob') { + // -> type indicates that the data is Binary and binary type is "blob" + // a new Blob object, created in the relevant Realm of the WebSocket + // object, that represents data as its raw data + dataForEvent = new Blob([data]) + } else { + // -> type indicates that the data is Binary and binary type is "arraybuffer" + // a new ArrayBuffer object, created in the relevant Realm of the + // WebSocket object, whose contents are data + dataForEvent = new Uint8Array(data).buffer + } + } + + // 3. Fire an event named message at the WebSocket object, using MessageEvent, + // with the origin attribute initialized to the serialization of the WebSocket + // object’s url's origin, and the data attribute initialized to dataForEvent. + fireEvent('message', ws, MessageEvent, { + origin: ws[kWebSocketURL].origin, + data: dataForEvent + }) +} + +/** + * @see https://datatracker.ietf.org/doc/html/rfc6455 + * @see https://datatracker.ietf.org/doc/html/rfc2616 + * @see https://bugs.chromium.org/p/chromium/issues/detail?id=398407 + * @param {string} protocol + */ +function isValidSubprotocol (protocol) { + // If present, this value indicates one + // or more comma-separated subprotocol the client wishes to speak, + // ordered by preference. The elements that comprise this value + // MUST be non-empty strings with characters in the range U+0021 to + // U+007E not including separator characters as defined in + // [RFC2616] and MUST all be unique strings. + if (protocol.length === 0) { + return false + } + + for (const char of protocol) { + const code = char.charCodeAt(0) + + if ( + code < 0x21 || + code > 0x7E || + char === '(' || + char === ')' || + char === '<' || + char === '>' || + char === '@' || + char === ',' || + char === ';' || + char === ':' || + char === '\\' || + char === '"' || + char === '/' || + char === '[' || + char === ']' || + char === '?' || + char === '=' || + char === '{' || + char === '}' || + code === 32 || // SP + code === 9 // HT + ) { + return false + } + } + + return true +} + +/** + * @see https://datatracker.ietf.org/doc/html/rfc6455#section-7-4 + * @param {number} code + */ +function isValidStatusCode (code) { + if (code >= 1000 && code < 1015) { + return ( + code !== 1004 && // reserved + code !== 1005 && // "MUST NOT be set as a status code" + code !== 1006 // "MUST NOT be set as a status code" + ) + } + + return code >= 3000 && code <= 4999 +} + +/** + * @param {import('./websocket').WebSocket} ws + * @param {string|undefined} reason + */ +function failWebsocketConnection (ws, reason) { + const { [kController]: controller, [kResponse]: response } = ws + + controller.abort() + + if (response?.socket && !response.socket.destroyed) { + response.socket.destroy() + } + + if (reason) { + fireEvent('error', ws, ErrorEvent, { + error: new Error(reason) + }) + } +} + +module.exports = { + isEstablished, + isClosing, + isClosed, + fireEvent, + isValidSubprotocol, + isValidStatusCode, + failWebsocketConnection, + websocketMessageReceived +} + + +/***/ }), + +/***/ 5171: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const { webidl } = __nccwpck_require__(4222) +const { DOMException } = __nccwpck_require__(7326) +const { URLSerializer } = __nccwpck_require__(4322) +const { getGlobalOrigin } = __nccwpck_require__(5628) +const { staticPropertyDescriptors, states, opcodes, emptyBuffer } = __nccwpck_require__(5913) +const { + kWebSocketURL, + kReadyState, + kController, + kBinaryType, + kResponse, + kSentClose, + kByteParser +} = __nccwpck_require__(2933) +const { isEstablished, isClosing, isValidSubprotocol, failWebsocketConnection, fireEvent } = __nccwpck_require__(3574) +const { establishWebSocketConnection } = __nccwpck_require__(8550) +const { WebsocketFrameSend } = __nccwpck_require__(1237) +const { ByteParser } = __nccwpck_require__(3171) +const { kEnumerableProperty, isBlobLike } = __nccwpck_require__(3440) +const { getGlobalDispatcher } = __nccwpck_require__(2581) +const { types } = __nccwpck_require__(9023) + +let experimentalWarned = false + +// https://websockets.spec.whatwg.org/#interface-definition +class WebSocket extends EventTarget { + #events = { + open: null, + error: null, + close: null, + message: null + } + + #bufferedAmount = 0 + #protocol = '' + #extensions = '' + + /** + * @param {string} url + * @param {string|string[]} protocols + */ + constructor (url, protocols = []) { + super() + + webidl.argumentLengthCheck(arguments, 1, { header: 'WebSocket constructor' }) + + if (!experimentalWarned) { + experimentalWarned = true + process.emitWarning('WebSockets are experimental, expect them to change at any time.', { + code: 'UNDICI-WS' + }) + } + + const options = webidl.converters['DOMString or sequence or WebSocketInit'](protocols) + + url = webidl.converters.USVString(url) + protocols = options.protocols + + // 1. Let baseURL be this's relevant settings object's API base URL. + const baseURL = getGlobalOrigin() + + // 1. Let urlRecord be the result of applying the URL parser to url with baseURL. + let urlRecord + + try { + urlRecord = new URL(url, baseURL) + } catch (e) { + // 3. If urlRecord is failure, then throw a "SyntaxError" DOMException. + throw new DOMException(e, 'SyntaxError') + } + + // 4. If urlRecord’s scheme is "http", then set urlRecord’s scheme to "ws". + if (urlRecord.protocol === 'http:') { + urlRecord.protocol = 'ws:' + } else if (urlRecord.protocol === 'https:') { + // 5. Otherwise, if urlRecord’s scheme is "https", set urlRecord’s scheme to "wss". + urlRecord.protocol = 'wss:' + } + + // 6. If urlRecord’s scheme is not "ws" or "wss", then throw a "SyntaxError" DOMException. + if (urlRecord.protocol !== 'ws:' && urlRecord.protocol !== 'wss:') { + throw new DOMException( + `Expected a ws: or wss: protocol, got ${urlRecord.protocol}`, + 'SyntaxError' + ) + } + + // 7. If urlRecord’s fragment is non-null, then throw a "SyntaxError" + // DOMException. + if (urlRecord.hash || urlRecord.href.endsWith('#')) { + throw new DOMException('Got fragment', 'SyntaxError') + } + + // 8. If protocols is a string, set protocols to a sequence consisting + // of just that string. + if (typeof protocols === 'string') { + protocols = [protocols] + } + + // 9. If any of the values in protocols occur more than once or otherwise + // fail to match the requirements for elements that comprise the value + // of `Sec-WebSocket-Protocol` fields as defined by The WebSocket + // protocol, then throw a "SyntaxError" DOMException. + if (protocols.length !== new Set(protocols.map(p => p.toLowerCase())).size) { + throw new DOMException('Invalid Sec-WebSocket-Protocol value', 'SyntaxError') + } + + if (protocols.length > 0 && !protocols.every(p => isValidSubprotocol(p))) { + throw new DOMException('Invalid Sec-WebSocket-Protocol value', 'SyntaxError') + } + + // 10. Set this's url to urlRecord. + this[kWebSocketURL] = new URL(urlRecord.href) + + // 11. Let client be this's relevant settings object. + + // 12. Run this step in parallel: + + // 1. Establish a WebSocket connection given urlRecord, protocols, + // and client. + this[kController] = establishWebSocketConnection( + urlRecord, + protocols, + this, + (response) => this.#onConnectionEstablished(response), + options + ) + + // Each WebSocket object has an associated ready state, which is a + // number representing the state of the connection. Initially it must + // be CONNECTING (0). + this[kReadyState] = WebSocket.CONNECTING + + // The extensions attribute must initially return the empty string. + + // The protocol attribute must initially return the empty string. + + // Each WebSocket object has an associated binary type, which is a + // BinaryType. Initially it must be "blob". + this[kBinaryType] = 'blob' + } + + /** + * @see https://websockets.spec.whatwg.org/#dom-websocket-close + * @param {number|undefined} code + * @param {string|undefined} reason + */ + close (code = undefined, reason = undefined) { + webidl.brandCheck(this, WebSocket) + + if (code !== undefined) { + code = webidl.converters['unsigned short'](code, { clamp: true }) + } + + if (reason !== undefined) { + reason = webidl.converters.USVString(reason) + } + + // 1. If code is present, but is neither an integer equal to 1000 nor an + // integer in the range 3000 to 4999, inclusive, throw an + // "InvalidAccessError" DOMException. + if (code !== undefined) { + if (code !== 1000 && (code < 3000 || code > 4999)) { + throw new DOMException('invalid code', 'InvalidAccessError') + } + } + + let reasonByteLength = 0 + + // 2. If reason is present, then run these substeps: + if (reason !== undefined) { + // 1. Let reasonBytes be the result of encoding reason. + // 2. If reasonBytes is longer than 123 bytes, then throw a + // "SyntaxError" DOMException. + reasonByteLength = Buffer.byteLength(reason) + + if (reasonByteLength > 123) { + throw new DOMException( + `Reason must be less than 123 bytes; received ${reasonByteLength}`, + 'SyntaxError' + ) + } + } + + // 3. Run the first matching steps from the following list: + if (this[kReadyState] === WebSocket.CLOSING || this[kReadyState] === WebSocket.CLOSED) { + // If this's ready state is CLOSING (2) or CLOSED (3) + // Do nothing. + } else if (!isEstablished(this)) { + // If the WebSocket connection is not yet established + // Fail the WebSocket connection and set this's ready state + // to CLOSING (2). + failWebsocketConnection(this, 'Connection was closed before it was established.') + this[kReadyState] = WebSocket.CLOSING + } else if (!isClosing(this)) { + // If the WebSocket closing handshake has not yet been started + // Start the WebSocket closing handshake and set this's ready + // state to CLOSING (2). + // - If neither code nor reason is present, the WebSocket Close + // message must not have a body. + // - If code is present, then the status code to use in the + // WebSocket Close message must be the integer given by code. + // - If reason is also present, then reasonBytes must be + // provided in the Close message after the status code. + + const frame = new WebsocketFrameSend() + + // If neither code nor reason is present, the WebSocket Close + // message must not have a body. + + // If code is present, then the status code to use in the + // WebSocket Close message must be the integer given by code. + if (code !== undefined && reason === undefined) { + frame.frameData = Buffer.allocUnsafe(2) + frame.frameData.writeUInt16BE(code, 0) + } else if (code !== undefined && reason !== undefined) { + // If reason is also present, then reasonBytes must be + // provided in the Close message after the status code. + frame.frameData = Buffer.allocUnsafe(2 + reasonByteLength) + frame.frameData.writeUInt16BE(code, 0) + // the body MAY contain UTF-8-encoded data with value /reason/ + frame.frameData.write(reason, 2, 'utf-8') + } else { + frame.frameData = emptyBuffer + } + + /** @type {import('stream').Duplex} */ + const socket = this[kResponse].socket + + socket.write(frame.createFrame(opcodes.CLOSE), (err) => { + if (!err) { + this[kSentClose] = true + } + }) + + // Upon either sending or receiving a Close control frame, it is said + // that _The WebSocket Closing Handshake is Started_ and that the + // WebSocket connection is in the CLOSING state. + this[kReadyState] = states.CLOSING + } else { + // Otherwise + // Set this's ready state to CLOSING (2). + this[kReadyState] = WebSocket.CLOSING + } + } + + /** + * @see https://websockets.spec.whatwg.org/#dom-websocket-send + * @param {NodeJS.TypedArray|ArrayBuffer|Blob|string} data + */ + send (data) { + webidl.brandCheck(this, WebSocket) + + webidl.argumentLengthCheck(arguments, 1, { header: 'WebSocket.send' }) + + data = webidl.converters.WebSocketSendData(data) + + // 1. If this's ready state is CONNECTING, then throw an + // "InvalidStateError" DOMException. + if (this[kReadyState] === WebSocket.CONNECTING) { + throw new DOMException('Sent before connected.', 'InvalidStateError') + } + + // 2. Run the appropriate set of steps from the following list: + // https://datatracker.ietf.org/doc/html/rfc6455#section-6.1 + // https://datatracker.ietf.org/doc/html/rfc6455#section-5.2 + + if (!isEstablished(this) || isClosing(this)) { + return + } + + /** @type {import('stream').Duplex} */ + const socket = this[kResponse].socket + + // If data is a string + if (typeof data === 'string') { + // If the WebSocket connection is established and the WebSocket + // closing handshake has not yet started, then the user agent + // must send a WebSocket Message comprised of the data argument + // using a text frame opcode; if the data cannot be sent, e.g. + // because it would need to be buffered but the buffer is full, + // the user agent must flag the WebSocket as full and then close + // the WebSocket connection. Any invocation of this method with a + // string argument that does not throw an exception must increase + // the bufferedAmount attribute by the number of bytes needed to + // express the argument as UTF-8. + + const value = Buffer.from(data) + const frame = new WebsocketFrameSend(value) + const buffer = frame.createFrame(opcodes.TEXT) + + this.#bufferedAmount += value.byteLength + socket.write(buffer, () => { + this.#bufferedAmount -= value.byteLength + }) + } else if (types.isArrayBuffer(data)) { + // If the WebSocket connection is established, and the WebSocket + // closing handshake has not yet started, then the user agent must + // send a WebSocket Message comprised of data using a binary frame + // opcode; if the data cannot be sent, e.g. because it would need + // to be buffered but the buffer is full, the user agent must flag + // the WebSocket as full and then close the WebSocket connection. + // The data to be sent is the data stored in the buffer described + // by the ArrayBuffer object. Any invocation of this method with an + // ArrayBuffer argument that does not throw an exception must + // increase the bufferedAmount attribute by the length of the + // ArrayBuffer in bytes. + + const value = Buffer.from(data) + const frame = new WebsocketFrameSend(value) + const buffer = frame.createFrame(opcodes.BINARY) + + this.#bufferedAmount += value.byteLength + socket.write(buffer, () => { + this.#bufferedAmount -= value.byteLength + }) + } else if (ArrayBuffer.isView(data)) { + // If the WebSocket connection is established, and the WebSocket + // closing handshake has not yet started, then the user agent must + // send a WebSocket Message comprised of data using a binary frame + // opcode; if the data cannot be sent, e.g. because it would need to + // be buffered but the buffer is full, the user agent must flag the + // WebSocket as full and then close the WebSocket connection. The + // data to be sent is the data stored in the section of the buffer + // described by the ArrayBuffer object that data references. Any + // invocation of this method with this kind of argument that does + // not throw an exception must increase the bufferedAmount attribute + // by the length of data’s buffer in bytes. + + const ab = Buffer.from(data, data.byteOffset, data.byteLength) + + const frame = new WebsocketFrameSend(ab) + const buffer = frame.createFrame(opcodes.BINARY) + + this.#bufferedAmount += ab.byteLength + socket.write(buffer, () => { + this.#bufferedAmount -= ab.byteLength + }) + } else if (isBlobLike(data)) { + // If the WebSocket connection is established, and the WebSocket + // closing handshake has not yet started, then the user agent must + // send a WebSocket Message comprised of data using a binary frame + // opcode; if the data cannot be sent, e.g. because it would need to + // be buffered but the buffer is full, the user agent must flag the + // WebSocket as full and then close the WebSocket connection. The data + // to be sent is the raw data represented by the Blob object. Any + // invocation of this method with a Blob argument that does not throw + // an exception must increase the bufferedAmount attribute by the size + // of the Blob object’s raw data, in bytes. + + const frame = new WebsocketFrameSend() + + data.arrayBuffer().then((ab) => { + const value = Buffer.from(ab) + frame.frameData = value + const buffer = frame.createFrame(opcodes.BINARY) + + this.#bufferedAmount += value.byteLength + socket.write(buffer, () => { + this.#bufferedAmount -= value.byteLength + }) + }) + } + } + + get readyState () { + webidl.brandCheck(this, WebSocket) + + // The readyState getter steps are to return this's ready state. + return this[kReadyState] + } + + get bufferedAmount () { + webidl.brandCheck(this, WebSocket) + + return this.#bufferedAmount + } + + get url () { + webidl.brandCheck(this, WebSocket) + + // The url getter steps are to return this's url, serialized. + return URLSerializer(this[kWebSocketURL]) + } + + get extensions () { + webidl.brandCheck(this, WebSocket) + + return this.#extensions + } + + get protocol () { + webidl.brandCheck(this, WebSocket) + + return this.#protocol + } + + get onopen () { + webidl.brandCheck(this, WebSocket) + + return this.#events.open + } + + set onopen (fn) { + webidl.brandCheck(this, WebSocket) + + if (this.#events.open) { + this.removeEventListener('open', this.#events.open) + } + + if (typeof fn === 'function') { + this.#events.open = fn + this.addEventListener('open', fn) + } else { + this.#events.open = null + } + } + + get onerror () { + webidl.brandCheck(this, WebSocket) + + return this.#events.error + } + + set onerror (fn) { + webidl.brandCheck(this, WebSocket) + + if (this.#events.error) { + this.removeEventListener('error', this.#events.error) + } + + if (typeof fn === 'function') { + this.#events.error = fn + this.addEventListener('error', fn) + } else { + this.#events.error = null + } + } + + get onclose () { + webidl.brandCheck(this, WebSocket) + + return this.#events.close + } + + set onclose (fn) { + webidl.brandCheck(this, WebSocket) + + if (this.#events.close) { + this.removeEventListener('close', this.#events.close) + } + + if (typeof fn === 'function') { + this.#events.close = fn + this.addEventListener('close', fn) + } else { + this.#events.close = null + } + } + + get onmessage () { + webidl.brandCheck(this, WebSocket) + + return this.#events.message + } + + set onmessage (fn) { + webidl.brandCheck(this, WebSocket) + + if (this.#events.message) { + this.removeEventListener('message', this.#events.message) + } + + if (typeof fn === 'function') { + this.#events.message = fn + this.addEventListener('message', fn) + } else { + this.#events.message = null + } + } + + get binaryType () { + webidl.brandCheck(this, WebSocket) + + return this[kBinaryType] + } + + set binaryType (type) { + webidl.brandCheck(this, WebSocket) + + if (type !== 'blob' && type !== 'arraybuffer') { + this[kBinaryType] = 'blob' + } else { + this[kBinaryType] = type + } + } + + /** + * @see https://websockets.spec.whatwg.org/#feedback-from-the-protocol + */ + #onConnectionEstablished (response) { + // processResponse is called when the "response’s header list has been received and initialized." + // once this happens, the connection is open + this[kResponse] = response + + const parser = new ByteParser(this) + parser.on('drain', function onParserDrain () { + this.ws[kResponse].socket.resume() + }) + + response.socket.ws = this + this[kByteParser] = parser + + // 1. Change the ready state to OPEN (1). + this[kReadyState] = states.OPEN + + // 2. Change the extensions attribute’s value to the extensions in use, if + // it is not the null value. + // https://datatracker.ietf.org/doc/html/rfc6455#section-9.1 + const extensions = response.headersList.get('sec-websocket-extensions') + + if (extensions !== null) { + this.#extensions = extensions + } + + // 3. Change the protocol attribute’s value to the subprotocol in use, if + // it is not the null value. + // https://datatracker.ietf.org/doc/html/rfc6455#section-1.9 + const protocol = response.headersList.get('sec-websocket-protocol') + + if (protocol !== null) { + this.#protocol = protocol + } + + // 4. Fire an event named open at the WebSocket object. + fireEvent('open', this) + } +} + +// https://websockets.spec.whatwg.org/#dom-websocket-connecting +WebSocket.CONNECTING = WebSocket.prototype.CONNECTING = states.CONNECTING +// https://websockets.spec.whatwg.org/#dom-websocket-open +WebSocket.OPEN = WebSocket.prototype.OPEN = states.OPEN +// https://websockets.spec.whatwg.org/#dom-websocket-closing +WebSocket.CLOSING = WebSocket.prototype.CLOSING = states.CLOSING +// https://websockets.spec.whatwg.org/#dom-websocket-closed +WebSocket.CLOSED = WebSocket.prototype.CLOSED = states.CLOSED + +Object.defineProperties(WebSocket.prototype, { + CONNECTING: staticPropertyDescriptors, + OPEN: staticPropertyDescriptors, + CLOSING: staticPropertyDescriptors, + CLOSED: staticPropertyDescriptors, + url: kEnumerableProperty, + readyState: kEnumerableProperty, + bufferedAmount: kEnumerableProperty, + onopen: kEnumerableProperty, + onerror: kEnumerableProperty, + onclose: kEnumerableProperty, + close: kEnumerableProperty, + onmessage: kEnumerableProperty, + binaryType: kEnumerableProperty, + send: kEnumerableProperty, + extensions: kEnumerableProperty, + protocol: kEnumerableProperty, + [Symbol.toStringTag]: { + value: 'WebSocket', + writable: false, + enumerable: false, + configurable: true + } +}) + +Object.defineProperties(WebSocket, { + CONNECTING: staticPropertyDescriptors, + OPEN: staticPropertyDescriptors, + CLOSING: staticPropertyDescriptors, + CLOSED: staticPropertyDescriptors +}) + +webidl.converters['sequence'] = webidl.sequenceConverter( + webidl.converters.DOMString +) + +webidl.converters['DOMString or sequence'] = function (V) { + if (webidl.util.Type(V) === 'Object' && Symbol.iterator in V) { + return webidl.converters['sequence'](V) + } + + return webidl.converters.DOMString(V) +} + +// This implements the propsal made in https://github.com/whatwg/websockets/issues/42 +webidl.converters.WebSocketInit = webidl.dictionaryConverter([ + { + key: 'protocols', + converter: webidl.converters['DOMString or sequence'], + get defaultValue () { + return [] + } + }, + { + key: 'dispatcher', + converter: (V) => V, + get defaultValue () { + return getGlobalDispatcher() + } + }, + { + key: 'headers', + converter: webidl.nullableConverter(webidl.converters.HeadersInit) + } +]) + +webidl.converters['DOMString or sequence or WebSocketInit'] = function (V) { + if (webidl.util.Type(V) === 'Object' && !(Symbol.iterator in V)) { + return webidl.converters.WebSocketInit(V) + } + + return { protocols: webidl.converters['DOMString or sequence'](V) } +} + +webidl.converters.WebSocketSendData = function (V) { + if (webidl.util.Type(V) === 'Object') { + if (isBlobLike(V)) { + return webidl.converters.Blob(V, { strict: false }) + } + + if (ArrayBuffer.isView(V) || types.isAnyArrayBuffer(V)) { + return webidl.converters.BufferSource(V) + } + } + + return webidl.converters.USVString(V) +} + +module.exports = { + WebSocket +} + + +/***/ }), + +/***/ 7125: +/***/ ((module) => { + +"use strict"; + + +var conversions = {}; +module.exports = conversions; + +function sign(x) { + return x < 0 ? -1 : 1; +} + +function evenRound(x) { + // Round x to the nearest integer, choosing the even integer if it lies halfway between two. + if ((x % 1) === 0.5 && (x & 1) === 0) { // [even number].5; round down (i.e. floor) + return Math.floor(x); + } else { + return Math.round(x); + } +} + +function createNumberConversion(bitLength, typeOpts) { + if (!typeOpts.unsigned) { + --bitLength; + } + const lowerBound = typeOpts.unsigned ? 0 : -Math.pow(2, bitLength); + const upperBound = Math.pow(2, bitLength) - 1; + + const moduloVal = typeOpts.moduloBitLength ? Math.pow(2, typeOpts.moduloBitLength) : Math.pow(2, bitLength); + const moduloBound = typeOpts.moduloBitLength ? Math.pow(2, typeOpts.moduloBitLength - 1) : Math.pow(2, bitLength - 1); + + return function(V, opts) { + if (!opts) opts = {}; + + let x = +V; + + if (opts.enforceRange) { + if (!Number.isFinite(x)) { + throw new TypeError("Argument is not a finite number"); + } + + x = sign(x) * Math.floor(Math.abs(x)); + if (x < lowerBound || x > upperBound) { + throw new TypeError("Argument is not in byte range"); + } + + return x; + } + + if (!isNaN(x) && opts.clamp) { + x = evenRound(x); + + if (x < lowerBound) x = lowerBound; + if (x > upperBound) x = upperBound; + return x; + } + + if (!Number.isFinite(x) || x === 0) { + return 0; + } + + x = sign(x) * Math.floor(Math.abs(x)); + x = x % moduloVal; + + if (!typeOpts.unsigned && x >= moduloBound) { + return x - moduloVal; + } else if (typeOpts.unsigned) { + if (x < 0) { + x += moduloVal; + } else if (x === -0) { // don't return negative zero + return 0; + } + } + + return x; + } +} + +conversions["void"] = function () { + return undefined; +}; + +conversions["boolean"] = function (val) { + return !!val; +}; + +conversions["byte"] = createNumberConversion(8, { unsigned: false }); +conversions["octet"] = createNumberConversion(8, { unsigned: true }); + +conversions["short"] = createNumberConversion(16, { unsigned: false }); +conversions["unsigned short"] = createNumberConversion(16, { unsigned: true }); + +conversions["long"] = createNumberConversion(32, { unsigned: false }); +conversions["unsigned long"] = createNumberConversion(32, { unsigned: true }); + +conversions["long long"] = createNumberConversion(32, { unsigned: false, moduloBitLength: 64 }); +conversions["unsigned long long"] = createNumberConversion(32, { unsigned: true, moduloBitLength: 64 }); + +conversions["double"] = function (V) { + const x = +V; + + if (!Number.isFinite(x)) { + throw new TypeError("Argument is not a finite floating-point value"); + } + + return x; +}; + +conversions["unrestricted double"] = function (V) { + const x = +V; + + if (isNaN(x)) { + throw new TypeError("Argument is NaN"); + } + + return x; +}; + +// not quite valid, but good enough for JS +conversions["float"] = conversions["double"]; +conversions["unrestricted float"] = conversions["unrestricted double"]; + +conversions["DOMString"] = function (V, opts) { + if (!opts) opts = {}; + + if (opts.treatNullAsEmptyString && V === null) { + return ""; + } + + return String(V); +}; + +conversions["ByteString"] = function (V, opts) { + const x = String(V); + let c = undefined; + for (let i = 0; (c = x.codePointAt(i)) !== undefined; ++i) { + if (c > 255) { + throw new TypeError("Argument is not a valid bytestring"); + } + } + + return x; +}; + +conversions["USVString"] = function (V) { + const S = String(V); + const n = S.length; + const U = []; + for (let i = 0; i < n; ++i) { + const c = S.charCodeAt(i); + if (c < 0xD800 || c > 0xDFFF) { + U.push(String.fromCodePoint(c)); + } else if (0xDC00 <= c && c <= 0xDFFF) { + U.push(String.fromCodePoint(0xFFFD)); + } else { + if (i === n - 1) { + U.push(String.fromCodePoint(0xFFFD)); + } else { + const d = S.charCodeAt(i + 1); + if (0xDC00 <= d && d <= 0xDFFF) { + const a = c & 0x3FF; + const b = d & 0x3FF; + U.push(String.fromCodePoint((2 << 15) + (2 << 9) * a + b)); + ++i; + } else { + U.push(String.fromCodePoint(0xFFFD)); + } + } + } + } + + return U.join(''); +}; + +conversions["Date"] = function (V, opts) { + if (!(V instanceof Date)) { + throw new TypeError("Argument is not a Date object"); + } + if (isNaN(V)) { + return undefined; + } + + return V; +}; + +conversions["RegExp"] = function (V, opts) { + if (!(V instanceof RegExp)) { + V = new RegExp(V); + } + + return V; +}; + + +/***/ }), + +/***/ 3184: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +const usm = __nccwpck_require__(905); + +exports.implementation = class URLImpl { + constructor(constructorArgs) { + const url = constructorArgs[0]; + const base = constructorArgs[1]; + + let parsedBase = null; + if (base !== undefined) { + parsedBase = usm.basicURLParse(base); + if (parsedBase === "failure") { + throw new TypeError("Invalid base URL"); + } + } + + const parsedURL = usm.basicURLParse(url, { baseURL: parsedBase }); + if (parsedURL === "failure") { + throw new TypeError("Invalid URL"); + } + + this._url = parsedURL; + + // TODO: query stuff + } + + get href() { + return usm.serializeURL(this._url); + } + + set href(v) { + const parsedURL = usm.basicURLParse(v); + if (parsedURL === "failure") { + throw new TypeError("Invalid URL"); + } + + this._url = parsedURL; + } + + get origin() { + return usm.serializeURLOrigin(this._url); + } + + get protocol() { + return this._url.scheme + ":"; + } + + set protocol(v) { + usm.basicURLParse(v + ":", { url: this._url, stateOverride: "scheme start" }); + } + + get username() { + return this._url.username; + } + + set username(v) { + if (usm.cannotHaveAUsernamePasswordPort(this._url)) { + return; + } + + usm.setTheUsername(this._url, v); + } + + get password() { + return this._url.password; + } + + set password(v) { + if (usm.cannotHaveAUsernamePasswordPort(this._url)) { + return; + } + + usm.setThePassword(this._url, v); + } + + get host() { + const url = this._url; + + if (url.host === null) { + return ""; + } + + if (url.port === null) { + return usm.serializeHost(url.host); + } + + return usm.serializeHost(url.host) + ":" + usm.serializeInteger(url.port); + } + + set host(v) { + if (this._url.cannotBeABaseURL) { + return; + } + + usm.basicURLParse(v, { url: this._url, stateOverride: "host" }); + } + + get hostname() { + if (this._url.host === null) { + return ""; + } + + return usm.serializeHost(this._url.host); + } + + set hostname(v) { + if (this._url.cannotBeABaseURL) { + return; + } + + usm.basicURLParse(v, { url: this._url, stateOverride: "hostname" }); + } + + get port() { + if (this._url.port === null) { + return ""; + } + + return usm.serializeInteger(this._url.port); + } + + set port(v) { + if (usm.cannotHaveAUsernamePasswordPort(this._url)) { + return; + } + + if (v === "") { + this._url.port = null; + } else { + usm.basicURLParse(v, { url: this._url, stateOverride: "port" }); + } + } + + get pathname() { + if (this._url.cannotBeABaseURL) { + return this._url.path[0]; + } + + if (this._url.path.length === 0) { + return ""; + } + + return "/" + this._url.path.join("/"); + } + + set pathname(v) { + if (this._url.cannotBeABaseURL) { + return; + } + + this._url.path = []; + usm.basicURLParse(v, { url: this._url, stateOverride: "path start" }); + } + + get search() { + if (this._url.query === null || this._url.query === "") { + return ""; + } + + return "?" + this._url.query; + } + + set search(v) { + // TODO: query stuff + + const url = this._url; + + if (v === "") { + url.query = null; + return; + } + + const input = v[0] === "?" ? v.substring(1) : v; + url.query = ""; + usm.basicURLParse(input, { url, stateOverride: "query" }); + } + + get hash() { + if (this._url.fragment === null || this._url.fragment === "") { + return ""; + } + + return "#" + this._url.fragment; + } + + set hash(v) { + if (v === "") { + this._url.fragment = null; + return; + } + + const input = v[0] === "#" ? v.substring(1) : v; + this._url.fragment = ""; + usm.basicURLParse(input, { url: this._url, stateOverride: "fragment" }); + } + + toJSON() { + return this.href; + } +}; + + +/***/ }), + +/***/ 6633: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const conversions = __nccwpck_require__(7125); +const utils = __nccwpck_require__(9857); +const Impl = __nccwpck_require__(3184); + +const impl = utils.implSymbol; + +function URL(url) { + if (!this || this[impl] || !(this instanceof URL)) { + throw new TypeError("Failed to construct 'URL': Please use the 'new' operator, this DOM object constructor cannot be called as a function."); + } + if (arguments.length < 1) { + throw new TypeError("Failed to construct 'URL': 1 argument required, but only " + arguments.length + " present."); + } + const args = []; + for (let i = 0; i < arguments.length && i < 2; ++i) { + args[i] = arguments[i]; + } + args[0] = conversions["USVString"](args[0]); + if (args[1] !== undefined) { + args[1] = conversions["USVString"](args[1]); + } + + module.exports.setup(this, args); +} + +URL.prototype.toJSON = function toJSON() { + if (!this || !module.exports.is(this)) { + throw new TypeError("Illegal invocation"); + } + const args = []; + for (let i = 0; i < arguments.length && i < 0; ++i) { + args[i] = arguments[i]; + } + return this[impl].toJSON.apply(this[impl], args); +}; +Object.defineProperty(URL.prototype, "href", { + get() { + return this[impl].href; + }, + set(V) { + V = conversions["USVString"](V); + this[impl].href = V; + }, + enumerable: true, + configurable: true +}); + +URL.prototype.toString = function () { + if (!this || !module.exports.is(this)) { + throw new TypeError("Illegal invocation"); + } + return this.href; +}; + +Object.defineProperty(URL.prototype, "origin", { + get() { + return this[impl].origin; + }, + enumerable: true, + configurable: true +}); + +Object.defineProperty(URL.prototype, "protocol", { + get() { + return this[impl].protocol; + }, + set(V) { + V = conversions["USVString"](V); + this[impl].protocol = V; + }, + enumerable: true, + configurable: true +}); + +Object.defineProperty(URL.prototype, "username", { + get() { + return this[impl].username; + }, + set(V) { + V = conversions["USVString"](V); + this[impl].username = V; + }, + enumerable: true, + configurable: true +}); + +Object.defineProperty(URL.prototype, "password", { + get() { + return this[impl].password; + }, + set(V) { + V = conversions["USVString"](V); + this[impl].password = V; + }, + enumerable: true, + configurable: true +}); + +Object.defineProperty(URL.prototype, "host", { + get() { + return this[impl].host; + }, + set(V) { + V = conversions["USVString"](V); + this[impl].host = V; + }, + enumerable: true, + configurable: true +}); + +Object.defineProperty(URL.prototype, "hostname", { + get() { + return this[impl].hostname; + }, + set(V) { + V = conversions["USVString"](V); + this[impl].hostname = V; + }, + enumerable: true, + configurable: true +}); + +Object.defineProperty(URL.prototype, "port", { + get() { + return this[impl].port; + }, + set(V) { + V = conversions["USVString"](V); + this[impl].port = V; + }, + enumerable: true, + configurable: true +}); + +Object.defineProperty(URL.prototype, "pathname", { + get() { + return this[impl].pathname; + }, + set(V) { + V = conversions["USVString"](V); + this[impl].pathname = V; + }, + enumerable: true, + configurable: true +}); + +Object.defineProperty(URL.prototype, "search", { + get() { + return this[impl].search; + }, + set(V) { + V = conversions["USVString"](V); + this[impl].search = V; + }, + enumerable: true, + configurable: true +}); + +Object.defineProperty(URL.prototype, "hash", { + get() { + return this[impl].hash; + }, + set(V) { + V = conversions["USVString"](V); + this[impl].hash = V; + }, + enumerable: true, + configurable: true +}); + + +module.exports = { + is(obj) { + return !!obj && obj[impl] instanceof Impl.implementation; + }, + create(constructorArgs, privateData) { + let obj = Object.create(URL.prototype); + this.setup(obj, constructorArgs, privateData); + return obj; + }, + setup(obj, constructorArgs, privateData) { + if (!privateData) privateData = {}; + privateData.wrapper = obj; + + obj[impl] = new Impl.implementation(constructorArgs, privateData); + obj[impl][utils.wrapperSymbol] = obj; + }, + interface: URL, + expose: { + Window: { URL: URL }, + Worker: { URL: URL } + } +}; + + + +/***/ }), + +/***/ 2686: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + + +exports.URL = __nccwpck_require__(6633)["interface"]; +exports.serializeURL = __nccwpck_require__(905).serializeURL; +exports.serializeURLOrigin = __nccwpck_require__(905).serializeURLOrigin; +exports.basicURLParse = __nccwpck_require__(905).basicURLParse; +exports.setTheUsername = __nccwpck_require__(905).setTheUsername; +exports.setThePassword = __nccwpck_require__(905).setThePassword; +exports.serializeHost = __nccwpck_require__(905).serializeHost; +exports.serializeInteger = __nccwpck_require__(905).serializeInteger; +exports.parseURL = __nccwpck_require__(905).parseURL; + + +/***/ }), + +/***/ 905: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + +const punycode = __nccwpck_require__(4876); +const tr46 = __nccwpck_require__(1552); + +const specialSchemes = { + ftp: 21, + file: null, + gopher: 70, + http: 80, + https: 443, + ws: 80, + wss: 443 +}; + +const failure = Symbol("failure"); + +function countSymbols(str) { + return punycode.ucs2.decode(str).length; +} + +function at(input, idx) { + const c = input[idx]; + return isNaN(c) ? undefined : String.fromCodePoint(c); +} + +function isASCIIDigit(c) { + return c >= 0x30 && c <= 0x39; +} + +function isASCIIAlpha(c) { + return (c >= 0x41 && c <= 0x5A) || (c >= 0x61 && c <= 0x7A); +} + +function isASCIIAlphanumeric(c) { + return isASCIIAlpha(c) || isASCIIDigit(c); +} + +function isASCIIHex(c) { + return isASCIIDigit(c) || (c >= 0x41 && c <= 0x46) || (c >= 0x61 && c <= 0x66); +} + +function isSingleDot(buffer) { + return buffer === "." || buffer.toLowerCase() === "%2e"; +} + +function isDoubleDot(buffer) { + buffer = buffer.toLowerCase(); + return buffer === ".." || buffer === "%2e." || buffer === ".%2e" || buffer === "%2e%2e"; +} + +function isWindowsDriveLetterCodePoints(cp1, cp2) { + return isASCIIAlpha(cp1) && (cp2 === 58 || cp2 === 124); +} + +function isWindowsDriveLetterString(string) { + return string.length === 2 && isASCIIAlpha(string.codePointAt(0)) && (string[1] === ":" || string[1] === "|"); +} + +function isNormalizedWindowsDriveLetterString(string) { + return string.length === 2 && isASCIIAlpha(string.codePointAt(0)) && string[1] === ":"; +} + +function containsForbiddenHostCodePoint(string) { + return string.search(/\u0000|\u0009|\u000A|\u000D|\u0020|#|%|\/|:|\?|@|\[|\\|\]/) !== -1; +} + +function containsForbiddenHostCodePointExcludingPercent(string) { + return string.search(/\u0000|\u0009|\u000A|\u000D|\u0020|#|\/|:|\?|@|\[|\\|\]/) !== -1; +} + +function isSpecialScheme(scheme) { + return specialSchemes[scheme] !== undefined; +} + +function isSpecial(url) { + return isSpecialScheme(url.scheme); +} + +function defaultPort(scheme) { + return specialSchemes[scheme]; +} + +function percentEncode(c) { + let hex = c.toString(16).toUpperCase(); + if (hex.length === 1) { + hex = "0" + hex; + } + + return "%" + hex; +} + +function utf8PercentEncode(c) { + const buf = new Buffer(c); + + let str = ""; + + for (let i = 0; i < buf.length; ++i) { + str += percentEncode(buf[i]); + } + + return str; +} + +function utf8PercentDecode(str) { + const input = new Buffer(str); + const output = []; + for (let i = 0; i < input.length; ++i) { + if (input[i] !== 37) { + output.push(input[i]); + } else if (input[i] === 37 && isASCIIHex(input[i + 1]) && isASCIIHex(input[i + 2])) { + output.push(parseInt(input.slice(i + 1, i + 3).toString(), 16)); + i += 2; + } else { + output.push(input[i]); + } + } + return new Buffer(output).toString(); +} + +function isC0ControlPercentEncode(c) { + return c <= 0x1F || c > 0x7E; +} + +const extraPathPercentEncodeSet = new Set([32, 34, 35, 60, 62, 63, 96, 123, 125]); +function isPathPercentEncode(c) { + return isC0ControlPercentEncode(c) || extraPathPercentEncodeSet.has(c); +} + +const extraUserinfoPercentEncodeSet = + new Set([47, 58, 59, 61, 64, 91, 92, 93, 94, 124]); +function isUserinfoPercentEncode(c) { + return isPathPercentEncode(c) || extraUserinfoPercentEncodeSet.has(c); +} + +function percentEncodeChar(c, encodeSetPredicate) { + const cStr = String.fromCodePoint(c); + + if (encodeSetPredicate(c)) { + return utf8PercentEncode(cStr); + } + + return cStr; +} + +function parseIPv4Number(input) { + let R = 10; + + if (input.length >= 2 && input.charAt(0) === "0" && input.charAt(1).toLowerCase() === "x") { + input = input.substring(2); + R = 16; + } else if (input.length >= 2 && input.charAt(0) === "0") { + input = input.substring(1); + R = 8; + } + + if (input === "") { + return 0; + } + + const regex = R === 10 ? /[^0-9]/ : (R === 16 ? /[^0-9A-Fa-f]/ : /[^0-7]/); + if (regex.test(input)) { + return failure; + } + + return parseInt(input, R); +} + +function parseIPv4(input) { + const parts = input.split("."); + if (parts[parts.length - 1] === "") { + if (parts.length > 1) { + parts.pop(); + } + } + + if (parts.length > 4) { + return input; + } + + const numbers = []; + for (const part of parts) { + if (part === "") { + return input; + } + const n = parseIPv4Number(part); + if (n === failure) { + return input; + } + + numbers.push(n); + } + + for (let i = 0; i < numbers.length - 1; ++i) { + if (numbers[i] > 255) { + return failure; + } + } + if (numbers[numbers.length - 1] >= Math.pow(256, 5 - numbers.length)) { + return failure; + } + + let ipv4 = numbers.pop(); + let counter = 0; + + for (const n of numbers) { + ipv4 += n * Math.pow(256, 3 - counter); + ++counter; + } + + return ipv4; +} + +function serializeIPv4(address) { + let output = ""; + let n = address; + + for (let i = 1; i <= 4; ++i) { + output = String(n % 256) + output; + if (i !== 4) { + output = "." + output; + } + n = Math.floor(n / 256); + } + + return output; +} + +function parseIPv6(input) { + const address = [0, 0, 0, 0, 0, 0, 0, 0]; + let pieceIndex = 0; + let compress = null; + let pointer = 0; + + input = punycode.ucs2.decode(input); + + if (input[pointer] === 58) { + if (input[pointer + 1] !== 58) { + return failure; + } + + pointer += 2; + ++pieceIndex; + compress = pieceIndex; + } + + while (pointer < input.length) { + if (pieceIndex === 8) { + return failure; + } + + if (input[pointer] === 58) { + if (compress !== null) { + return failure; + } + ++pointer; + ++pieceIndex; + compress = pieceIndex; + continue; + } + + let value = 0; + let length = 0; + + while (length < 4 && isASCIIHex(input[pointer])) { + value = value * 0x10 + parseInt(at(input, pointer), 16); + ++pointer; + ++length; + } + + if (input[pointer] === 46) { + if (length === 0) { + return failure; + } + + pointer -= length; + + if (pieceIndex > 6) { + return failure; + } + + let numbersSeen = 0; + + while (input[pointer] !== undefined) { + let ipv4Piece = null; + + if (numbersSeen > 0) { + if (input[pointer] === 46 && numbersSeen < 4) { + ++pointer; + } else { + return failure; + } + } + + if (!isASCIIDigit(input[pointer])) { + return failure; + } + + while (isASCIIDigit(input[pointer])) { + const number = parseInt(at(input, pointer)); + if (ipv4Piece === null) { + ipv4Piece = number; + } else if (ipv4Piece === 0) { + return failure; + } else { + ipv4Piece = ipv4Piece * 10 + number; + } + if (ipv4Piece > 255) { + return failure; + } + ++pointer; + } + + address[pieceIndex] = address[pieceIndex] * 0x100 + ipv4Piece; + + ++numbersSeen; + + if (numbersSeen === 2 || numbersSeen === 4) { + ++pieceIndex; + } + } + + if (numbersSeen !== 4) { + return failure; + } + + break; + } else if (input[pointer] === 58) { + ++pointer; + if (input[pointer] === undefined) { + return failure; + } + } else if (input[pointer] !== undefined) { + return failure; + } + + address[pieceIndex] = value; + ++pieceIndex; + } + + if (compress !== null) { + let swaps = pieceIndex - compress; + pieceIndex = 7; + while (pieceIndex !== 0 && swaps > 0) { + const temp = address[compress + swaps - 1]; + address[compress + swaps - 1] = address[pieceIndex]; + address[pieceIndex] = temp; + --pieceIndex; + --swaps; + } + } else if (compress === null && pieceIndex !== 8) { + return failure; + } + + return address; +} + +function serializeIPv6(address) { + let output = ""; + const seqResult = findLongestZeroSequence(address); + const compress = seqResult.idx; + let ignore0 = false; + + for (let pieceIndex = 0; pieceIndex <= 7; ++pieceIndex) { + if (ignore0 && address[pieceIndex] === 0) { + continue; + } else if (ignore0) { + ignore0 = false; + } + + if (compress === pieceIndex) { + const separator = pieceIndex === 0 ? "::" : ":"; + output += separator; + ignore0 = true; + continue; + } + + output += address[pieceIndex].toString(16); + + if (pieceIndex !== 7) { + output += ":"; + } + } + + return output; +} + +function parseHost(input, isSpecialArg) { + if (input[0] === "[") { + if (input[input.length - 1] !== "]") { + return failure; + } + + return parseIPv6(input.substring(1, input.length - 1)); + } + + if (!isSpecialArg) { + return parseOpaqueHost(input); + } + + const domain = utf8PercentDecode(input); + const asciiDomain = tr46.toASCII(domain, false, tr46.PROCESSING_OPTIONS.NONTRANSITIONAL, false); + if (asciiDomain === null) { + return failure; + } + + if (containsForbiddenHostCodePoint(asciiDomain)) { + return failure; + } + + const ipv4Host = parseIPv4(asciiDomain); + if (typeof ipv4Host === "number" || ipv4Host === failure) { + return ipv4Host; + } + + return asciiDomain; +} + +function parseOpaqueHost(input) { + if (containsForbiddenHostCodePointExcludingPercent(input)) { + return failure; + } + + let output = ""; + const decoded = punycode.ucs2.decode(input); + for (let i = 0; i < decoded.length; ++i) { + output += percentEncodeChar(decoded[i], isC0ControlPercentEncode); + } + return output; +} + +function findLongestZeroSequence(arr) { + let maxIdx = null; + let maxLen = 1; // only find elements > 1 + let currStart = null; + let currLen = 0; + + for (let i = 0; i < arr.length; ++i) { + if (arr[i] !== 0) { + if (currLen > maxLen) { + maxIdx = currStart; + maxLen = currLen; + } + + currStart = null; + currLen = 0; + } else { + if (currStart === null) { + currStart = i; + } + ++currLen; + } + } + + // if trailing zeros + if (currLen > maxLen) { + maxIdx = currStart; + maxLen = currLen; + } + + return { + idx: maxIdx, + len: maxLen + }; +} + +function serializeHost(host) { + if (typeof host === "number") { + return serializeIPv4(host); + } + + // IPv6 serializer + if (host instanceof Array) { + return "[" + serializeIPv6(host) + "]"; + } + + return host; +} + +function trimControlChars(url) { + return url.replace(/^[\u0000-\u001F\u0020]+|[\u0000-\u001F\u0020]+$/g, ""); +} + +function trimTabAndNewline(url) { + return url.replace(/\u0009|\u000A|\u000D/g, ""); +} + +function shortenPath(url) { + const path = url.path; + if (path.length === 0) { + return; + } + if (url.scheme === "file" && path.length === 1 && isNormalizedWindowsDriveLetter(path[0])) { + return; + } + + path.pop(); +} + +function includesCredentials(url) { + return url.username !== "" || url.password !== ""; +} + +function cannotHaveAUsernamePasswordPort(url) { + return url.host === null || url.host === "" || url.cannotBeABaseURL || url.scheme === "file"; +} + +function isNormalizedWindowsDriveLetter(string) { + return /^[A-Za-z]:$/.test(string); +} + +function URLStateMachine(input, base, encodingOverride, url, stateOverride) { + this.pointer = 0; + this.input = input; + this.base = base || null; + this.encodingOverride = encodingOverride || "utf-8"; + this.stateOverride = stateOverride; + this.url = url; + this.failure = false; + this.parseError = false; + + if (!this.url) { + this.url = { + scheme: "", + username: "", + password: "", + host: null, + port: null, + path: [], + query: null, + fragment: null, + + cannotBeABaseURL: false + }; + + const res = trimControlChars(this.input); + if (res !== this.input) { + this.parseError = true; + } + this.input = res; + } + + const res = trimTabAndNewline(this.input); + if (res !== this.input) { + this.parseError = true; + } + this.input = res; + + this.state = stateOverride || "scheme start"; + + this.buffer = ""; + this.atFlag = false; + this.arrFlag = false; + this.passwordTokenSeenFlag = false; + + this.input = punycode.ucs2.decode(this.input); + + for (; this.pointer <= this.input.length; ++this.pointer) { + const c = this.input[this.pointer]; + const cStr = isNaN(c) ? undefined : String.fromCodePoint(c); + + // exec state machine + const ret = this["parse " + this.state](c, cStr); + if (!ret) { + break; // terminate algorithm + } else if (ret === failure) { + this.failure = true; + break; + } + } +} + +URLStateMachine.prototype["parse scheme start"] = function parseSchemeStart(c, cStr) { + if (isASCIIAlpha(c)) { + this.buffer += cStr.toLowerCase(); + this.state = "scheme"; + } else if (!this.stateOverride) { + this.state = "no scheme"; + --this.pointer; + } else { + this.parseError = true; + return failure; + } + + return true; +}; + +URLStateMachine.prototype["parse scheme"] = function parseScheme(c, cStr) { + if (isASCIIAlphanumeric(c) || c === 43 || c === 45 || c === 46) { + this.buffer += cStr.toLowerCase(); + } else if (c === 58) { + if (this.stateOverride) { + if (isSpecial(this.url) && !isSpecialScheme(this.buffer)) { + return false; + } + + if (!isSpecial(this.url) && isSpecialScheme(this.buffer)) { + return false; + } + + if ((includesCredentials(this.url) || this.url.port !== null) && this.buffer === "file") { + return false; + } + + if (this.url.scheme === "file" && (this.url.host === "" || this.url.host === null)) { + return false; + } + } + this.url.scheme = this.buffer; + this.buffer = ""; + if (this.stateOverride) { + return false; + } + if (this.url.scheme === "file") { + if (this.input[this.pointer + 1] !== 47 || this.input[this.pointer + 2] !== 47) { + this.parseError = true; + } + this.state = "file"; + } else if (isSpecial(this.url) && this.base !== null && this.base.scheme === this.url.scheme) { + this.state = "special relative or authority"; + } else if (isSpecial(this.url)) { + this.state = "special authority slashes"; + } else if (this.input[this.pointer + 1] === 47) { + this.state = "path or authority"; + ++this.pointer; + } else { + this.url.cannotBeABaseURL = true; + this.url.path.push(""); + this.state = "cannot-be-a-base-URL path"; + } + } else if (!this.stateOverride) { + this.buffer = ""; + this.state = "no scheme"; + this.pointer = -1; + } else { + this.parseError = true; + return failure; + } + + return true; +}; + +URLStateMachine.prototype["parse no scheme"] = function parseNoScheme(c) { + if (this.base === null || (this.base.cannotBeABaseURL && c !== 35)) { + return failure; + } else if (this.base.cannotBeABaseURL && c === 35) { + this.url.scheme = this.base.scheme; + this.url.path = this.base.path.slice(); + this.url.query = this.base.query; + this.url.fragment = ""; + this.url.cannotBeABaseURL = true; + this.state = "fragment"; + } else if (this.base.scheme === "file") { + this.state = "file"; + --this.pointer; + } else { + this.state = "relative"; + --this.pointer; + } + + return true; +}; + +URLStateMachine.prototype["parse special relative or authority"] = function parseSpecialRelativeOrAuthority(c) { + if (c === 47 && this.input[this.pointer + 1] === 47) { + this.state = "special authority ignore slashes"; + ++this.pointer; + } else { + this.parseError = true; + this.state = "relative"; + --this.pointer; + } + + return true; +}; + +URLStateMachine.prototype["parse path or authority"] = function parsePathOrAuthority(c) { + if (c === 47) { + this.state = "authority"; + } else { + this.state = "path"; + --this.pointer; + } + + return true; +}; + +URLStateMachine.prototype["parse relative"] = function parseRelative(c) { + this.url.scheme = this.base.scheme; + if (isNaN(c)) { + this.url.username = this.base.username; + this.url.password = this.base.password; + this.url.host = this.base.host; + this.url.port = this.base.port; + this.url.path = this.base.path.slice(); + this.url.query = this.base.query; + } else if (c === 47) { + this.state = "relative slash"; + } else if (c === 63) { + this.url.username = this.base.username; + this.url.password = this.base.password; + this.url.host = this.base.host; + this.url.port = this.base.port; + this.url.path = this.base.path.slice(); + this.url.query = ""; + this.state = "query"; + } else if (c === 35) { + this.url.username = this.base.username; + this.url.password = this.base.password; + this.url.host = this.base.host; + this.url.port = this.base.port; + this.url.path = this.base.path.slice(); + this.url.query = this.base.query; + this.url.fragment = ""; + this.state = "fragment"; + } else if (isSpecial(this.url) && c === 92) { + this.parseError = true; + this.state = "relative slash"; + } else { + this.url.username = this.base.username; + this.url.password = this.base.password; + this.url.host = this.base.host; + this.url.port = this.base.port; + this.url.path = this.base.path.slice(0, this.base.path.length - 1); + + this.state = "path"; + --this.pointer; + } + + return true; +}; + +URLStateMachine.prototype["parse relative slash"] = function parseRelativeSlash(c) { + if (isSpecial(this.url) && (c === 47 || c === 92)) { + if (c === 92) { + this.parseError = true; + } + this.state = "special authority ignore slashes"; + } else if (c === 47) { + this.state = "authority"; + } else { + this.url.username = this.base.username; + this.url.password = this.base.password; + this.url.host = this.base.host; + this.url.port = this.base.port; + this.state = "path"; + --this.pointer; + } + + return true; +}; + +URLStateMachine.prototype["parse special authority slashes"] = function parseSpecialAuthoritySlashes(c) { + if (c === 47 && this.input[this.pointer + 1] === 47) { + this.state = "special authority ignore slashes"; + ++this.pointer; + } else { + this.parseError = true; + this.state = "special authority ignore slashes"; + --this.pointer; + } + + return true; +}; + +URLStateMachine.prototype["parse special authority ignore slashes"] = function parseSpecialAuthorityIgnoreSlashes(c) { + if (c !== 47 && c !== 92) { + this.state = "authority"; + --this.pointer; + } else { + this.parseError = true; + } + + return true; +}; + +URLStateMachine.prototype["parse authority"] = function parseAuthority(c, cStr) { + if (c === 64) { + this.parseError = true; + if (this.atFlag) { + this.buffer = "%40" + this.buffer; + } + this.atFlag = true; + + // careful, this is based on buffer and has its own pointer (this.pointer != pointer) and inner chars + const len = countSymbols(this.buffer); + for (let pointer = 0; pointer < len; ++pointer) { + const codePoint = this.buffer.codePointAt(pointer); + + if (codePoint === 58 && !this.passwordTokenSeenFlag) { + this.passwordTokenSeenFlag = true; + continue; + } + const encodedCodePoints = percentEncodeChar(codePoint, isUserinfoPercentEncode); + if (this.passwordTokenSeenFlag) { + this.url.password += encodedCodePoints; + } else { + this.url.username += encodedCodePoints; + } + } + this.buffer = ""; + } else if (isNaN(c) || c === 47 || c === 63 || c === 35 || + (isSpecial(this.url) && c === 92)) { + if (this.atFlag && this.buffer === "") { + this.parseError = true; + return failure; + } + this.pointer -= countSymbols(this.buffer) + 1; + this.buffer = ""; + this.state = "host"; + } else { + this.buffer += cStr; + } + + return true; +}; + +URLStateMachine.prototype["parse hostname"] = +URLStateMachine.prototype["parse host"] = function parseHostName(c, cStr) { + if (this.stateOverride && this.url.scheme === "file") { + --this.pointer; + this.state = "file host"; + } else if (c === 58 && !this.arrFlag) { + if (this.buffer === "") { + this.parseError = true; + return failure; + } + + const host = parseHost(this.buffer, isSpecial(this.url)); + if (host === failure) { + return failure; + } + + this.url.host = host; + this.buffer = ""; + this.state = "port"; + if (this.stateOverride === "hostname") { + return false; + } + } else if (isNaN(c) || c === 47 || c === 63 || c === 35 || + (isSpecial(this.url) && c === 92)) { + --this.pointer; + if (isSpecial(this.url) && this.buffer === "") { + this.parseError = true; + return failure; + } else if (this.stateOverride && this.buffer === "" && + (includesCredentials(this.url) || this.url.port !== null)) { + this.parseError = true; + return false; + } + + const host = parseHost(this.buffer, isSpecial(this.url)); + if (host === failure) { + return failure; + } + + this.url.host = host; + this.buffer = ""; + this.state = "path start"; + if (this.stateOverride) { + return false; + } + } else { + if (c === 91) { + this.arrFlag = true; + } else if (c === 93) { + this.arrFlag = false; + } + this.buffer += cStr; + } + + return true; +}; + +URLStateMachine.prototype["parse port"] = function parsePort(c, cStr) { + if (isASCIIDigit(c)) { + this.buffer += cStr; + } else if (isNaN(c) || c === 47 || c === 63 || c === 35 || + (isSpecial(this.url) && c === 92) || + this.stateOverride) { + if (this.buffer !== "") { + const port = parseInt(this.buffer); + if (port > Math.pow(2, 16) - 1) { + this.parseError = true; + return failure; + } + this.url.port = port === defaultPort(this.url.scheme) ? null : port; + this.buffer = ""; + } + if (this.stateOverride) { + return false; + } + this.state = "path start"; + --this.pointer; + } else { + this.parseError = true; + return failure; + } + + return true; +}; + +const fileOtherwiseCodePoints = new Set([47, 92, 63, 35]); + +URLStateMachine.prototype["parse file"] = function parseFile(c) { + this.url.scheme = "file"; + + if (c === 47 || c === 92) { + if (c === 92) { + this.parseError = true; + } + this.state = "file slash"; + } else if (this.base !== null && this.base.scheme === "file") { + if (isNaN(c)) { + this.url.host = this.base.host; + this.url.path = this.base.path.slice(); + this.url.query = this.base.query; + } else if (c === 63) { + this.url.host = this.base.host; + this.url.path = this.base.path.slice(); + this.url.query = ""; + this.state = "query"; + } else if (c === 35) { + this.url.host = this.base.host; + this.url.path = this.base.path.slice(); + this.url.query = this.base.query; + this.url.fragment = ""; + this.state = "fragment"; + } else { + if (this.input.length - this.pointer - 1 === 0 || // remaining consists of 0 code points + !isWindowsDriveLetterCodePoints(c, this.input[this.pointer + 1]) || + (this.input.length - this.pointer - 1 >= 2 && // remaining has at least 2 code points + !fileOtherwiseCodePoints.has(this.input[this.pointer + 2]))) { + this.url.host = this.base.host; + this.url.path = this.base.path.slice(); + shortenPath(this.url); + } else { + this.parseError = true; + } + + this.state = "path"; + --this.pointer; + } + } else { + this.state = "path"; + --this.pointer; + } + + return true; +}; + +URLStateMachine.prototype["parse file slash"] = function parseFileSlash(c) { + if (c === 47 || c === 92) { + if (c === 92) { + this.parseError = true; + } + this.state = "file host"; + } else { + if (this.base !== null && this.base.scheme === "file") { + if (isNormalizedWindowsDriveLetterString(this.base.path[0])) { + this.url.path.push(this.base.path[0]); + } else { + this.url.host = this.base.host; + } + } + this.state = "path"; + --this.pointer; + } + + return true; +}; + +URLStateMachine.prototype["parse file host"] = function parseFileHost(c, cStr) { + if (isNaN(c) || c === 47 || c === 92 || c === 63 || c === 35) { + --this.pointer; + if (!this.stateOverride && isWindowsDriveLetterString(this.buffer)) { + this.parseError = true; + this.state = "path"; + } else if (this.buffer === "") { + this.url.host = ""; + if (this.stateOverride) { + return false; + } + this.state = "path start"; + } else { + let host = parseHost(this.buffer, isSpecial(this.url)); + if (host === failure) { + return failure; + } + if (host === "localhost") { + host = ""; + } + this.url.host = host; + + if (this.stateOverride) { + return false; + } + + this.buffer = ""; + this.state = "path start"; + } + } else { + this.buffer += cStr; + } + + return true; +}; + +URLStateMachine.prototype["parse path start"] = function parsePathStart(c) { + if (isSpecial(this.url)) { + if (c === 92) { + this.parseError = true; + } + this.state = "path"; + + if (c !== 47 && c !== 92) { + --this.pointer; + } + } else if (!this.stateOverride && c === 63) { + this.url.query = ""; + this.state = "query"; + } else if (!this.stateOverride && c === 35) { + this.url.fragment = ""; + this.state = "fragment"; + } else if (c !== undefined) { + this.state = "path"; + if (c !== 47) { + --this.pointer; + } + } + + return true; +}; + +URLStateMachine.prototype["parse path"] = function parsePath(c) { + if (isNaN(c) || c === 47 || (isSpecial(this.url) && c === 92) || + (!this.stateOverride && (c === 63 || c === 35))) { + if (isSpecial(this.url) && c === 92) { + this.parseError = true; + } + + if (isDoubleDot(this.buffer)) { + shortenPath(this.url); + if (c !== 47 && !(isSpecial(this.url) && c === 92)) { + this.url.path.push(""); + } + } else if (isSingleDot(this.buffer) && c !== 47 && + !(isSpecial(this.url) && c === 92)) { + this.url.path.push(""); + } else if (!isSingleDot(this.buffer)) { + if (this.url.scheme === "file" && this.url.path.length === 0 && isWindowsDriveLetterString(this.buffer)) { + if (this.url.host !== "" && this.url.host !== null) { + this.parseError = true; + this.url.host = ""; + } + this.buffer = this.buffer[0] + ":"; + } + this.url.path.push(this.buffer); + } + this.buffer = ""; + if (this.url.scheme === "file" && (c === undefined || c === 63 || c === 35)) { + while (this.url.path.length > 1 && this.url.path[0] === "") { + this.parseError = true; + this.url.path.shift(); + } + } + if (c === 63) { + this.url.query = ""; + this.state = "query"; + } + if (c === 35) { + this.url.fragment = ""; + this.state = "fragment"; + } + } else { + // TODO: If c is not a URL code point and not "%", parse error. + + if (c === 37 && + (!isASCIIHex(this.input[this.pointer + 1]) || + !isASCIIHex(this.input[this.pointer + 2]))) { + this.parseError = true; + } + + this.buffer += percentEncodeChar(c, isPathPercentEncode); + } + + return true; +}; + +URLStateMachine.prototype["parse cannot-be-a-base-URL path"] = function parseCannotBeABaseURLPath(c) { + if (c === 63) { + this.url.query = ""; + this.state = "query"; + } else if (c === 35) { + this.url.fragment = ""; + this.state = "fragment"; + } else { + // TODO: Add: not a URL code point + if (!isNaN(c) && c !== 37) { + this.parseError = true; + } + + if (c === 37 && + (!isASCIIHex(this.input[this.pointer + 1]) || + !isASCIIHex(this.input[this.pointer + 2]))) { + this.parseError = true; + } + + if (!isNaN(c)) { + this.url.path[0] = this.url.path[0] + percentEncodeChar(c, isC0ControlPercentEncode); + } + } + + return true; +}; + +URLStateMachine.prototype["parse query"] = function parseQuery(c, cStr) { + if (isNaN(c) || (!this.stateOverride && c === 35)) { + if (!isSpecial(this.url) || this.url.scheme === "ws" || this.url.scheme === "wss") { + this.encodingOverride = "utf-8"; + } + + const buffer = new Buffer(this.buffer); // TODO: Use encoding override instead + for (let i = 0; i < buffer.length; ++i) { + if (buffer[i] < 0x21 || buffer[i] > 0x7E || buffer[i] === 0x22 || buffer[i] === 0x23 || + buffer[i] === 0x3C || buffer[i] === 0x3E) { + this.url.query += percentEncode(buffer[i]); + } else { + this.url.query += String.fromCodePoint(buffer[i]); + } + } + + this.buffer = ""; + if (c === 35) { + this.url.fragment = ""; + this.state = "fragment"; + } + } else { + // TODO: If c is not a URL code point and not "%", parse error. + if (c === 37 && + (!isASCIIHex(this.input[this.pointer + 1]) || + !isASCIIHex(this.input[this.pointer + 2]))) { + this.parseError = true; + } + + this.buffer += cStr; + } + + return true; +}; + +URLStateMachine.prototype["parse fragment"] = function parseFragment(c) { + if (isNaN(c)) { // do nothing + } else if (c === 0x0) { + this.parseError = true; + } else { + // TODO: If c is not a URL code point and not "%", parse error. + if (c === 37 && + (!isASCIIHex(this.input[this.pointer + 1]) || + !isASCIIHex(this.input[this.pointer + 2]))) { + this.parseError = true; + } + + this.url.fragment += percentEncodeChar(c, isC0ControlPercentEncode); + } + + return true; +}; + +function serializeURL(url, excludeFragment) { + let output = url.scheme + ":"; + if (url.host !== null) { + output += "//"; + + if (url.username !== "" || url.password !== "") { + output += url.username; + if (url.password !== "") { + output += ":" + url.password; + } + output += "@"; + } + + output += serializeHost(url.host); + + if (url.port !== null) { + output += ":" + url.port; + } + } else if (url.host === null && url.scheme === "file") { + output += "//"; + } + + if (url.cannotBeABaseURL) { + output += url.path[0]; + } else { + for (const string of url.path) { + output += "/" + string; + } + } + + if (url.query !== null) { + output += "?" + url.query; + } + + if (!excludeFragment && url.fragment !== null) { + output += "#" + url.fragment; + } + + return output; +} + +function serializeOrigin(tuple) { + let result = tuple.scheme + "://"; + result += serializeHost(tuple.host); + + if (tuple.port !== null) { + result += ":" + tuple.port; + } + + return result; +} + +module.exports.serializeURL = serializeURL; + +module.exports.serializeURLOrigin = function (url) { + // https://url.spec.whatwg.org/#concept-url-origin + switch (url.scheme) { + case "blob": + try { + return module.exports.serializeURLOrigin(module.exports.parseURL(url.path[0])); + } catch (e) { + // serializing an opaque origin returns "null" + return "null"; + } + case "ftp": + case "gopher": + case "http": + case "https": + case "ws": + case "wss": + return serializeOrigin({ + scheme: url.scheme, + host: url.host, + port: url.port + }); + case "file": + // spec says "exercise to the reader", chrome says "file://" + return "file://"; + default: + // serializing an opaque origin returns "null" + return "null"; + } +}; + +module.exports.basicURLParse = function (input, options) { + if (options === undefined) { + options = {}; + } + + const usm = new URLStateMachine(input, options.baseURL, options.encodingOverride, options.url, options.stateOverride); + if (usm.failure) { + return "failure"; + } + + return usm.url; +}; + +module.exports.setTheUsername = function (url, username) { + url.username = ""; + const decoded = punycode.ucs2.decode(username); + for (let i = 0; i < decoded.length; ++i) { + url.username += percentEncodeChar(decoded[i], isUserinfoPercentEncode); + } +}; + +module.exports.setThePassword = function (url, password) { + url.password = ""; + const decoded = punycode.ucs2.decode(password); + for (let i = 0; i < decoded.length; ++i) { + url.password += percentEncodeChar(decoded[i], isUserinfoPercentEncode); + } +}; + +module.exports.serializeHost = serializeHost; + +module.exports.cannotHaveAUsernamePasswordPort = cannotHaveAUsernamePasswordPort; + +module.exports.serializeInteger = function (integer) { + return String(integer); +}; + +module.exports.parseURL = function (input, options) { + if (options === undefined) { + options = {}; + } + + // We don't handle blobs, so this just delegates: + return module.exports.basicURLParse(input, { baseURL: options.baseURL, encodingOverride: options.encodingOverride }); +}; + + +/***/ }), + +/***/ 9857: +/***/ ((module) => { + +"use strict"; + + +module.exports.mixin = function mixin(target, source) { + const keys = Object.getOwnPropertyNames(source); + for (let i = 0; i < keys.length; ++i) { + Object.defineProperty(target, keys[i], Object.getOwnPropertyDescriptor(source, keys[i])); + } +}; + +module.exports.wrapperSymbol = Symbol("wrapper"); +module.exports.implSymbol = Symbol("impl"); + +module.exports.wrapperForImpl = function (impl) { + return impl[module.exports.wrapperSymbol]; +}; + +module.exports.implForWrapper = function (wrapper) { + return wrapper[module.exports.implSymbol]; +}; + + + +/***/ }), + +/***/ 4572: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +var iconvLite = __nccwpck_require__(6249); + +// Expose to the world +module.exports.C = convert; + +/** + * Convert encoding of an UTF-8 string or a buffer + * + * @param {String|Buffer} str String to be converted + * @param {String} to Encoding to be converted to + * @param {String} [from='UTF-8'] Encoding to be converted from + * @return {Buffer} Encoded string + */ +function convert(str, to, from) { + from = checkEncoding(from || 'UTF-8'); + to = checkEncoding(to || 'UTF-8'); + str = str || ''; + + var result; + + if (from !== 'UTF-8' && typeof str === 'string') { + str = Buffer.from(str, 'binary'); + } + + if (from === to) { + if (typeof str === 'string') { + result = Buffer.from(str); + } else { + result = str; + } + } else { + try { + result = convertIconvLite(str, to, from); + } catch (E) { + console.error(E); + result = str; + } + } + + if (typeof result === 'string') { + result = Buffer.from(result, 'utf-8'); + } + + return result; +} + +/** + * Convert encoding of astring with iconv-lite + * + * @param {String|Buffer} str String to be converted + * @param {String} to Encoding to be converted to + * @param {String} [from='UTF-8'] Encoding to be converted from + * @return {Buffer} Encoded string + */ +function convertIconvLite(str, to, from) { + if (to === 'UTF-8') { + return iconvLite.decode(str, from); + } else if (from === 'UTF-8') { + return iconvLite.encode(str, to); + } else { + return iconvLite.encode(iconvLite.decode(str, from), to); + } +} + +/** + * Converts charset name if needed + * + * @param {String} name Character set + * @return {String} Character set name + */ +function checkEncoding(name) { + return (name || '') + .toString() + .trim() + .replace(/^latin[\-_]?(\d+)$/i, 'ISO-8859-$1') + .replace(/^win(?:dows)?[\-_]?(\d+)$/i, 'WINDOWS-$1') + .replace(/^utf[\-_]?(\d+)$/i, 'UTF-$1') + .replace(/^ks_c_5601\-1987$/i, 'CP949') + .replace(/^us[\-_]?ascii$/i, 'ASCII') + .toUpperCase(); +} + + +/***/ }), + +/***/ 2417: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +var Buffer = (__nccwpck_require__(4199).Buffer); + +// Multibyte codec. In this scheme, a character is represented by 1 or more bytes. +// Our codec supports UTF-16 surrogates, extensions for GB18030 and unicode sequences. +// To save memory and loading time, we read table files only when requested. + +exports._dbcs = DBCSCodec; + +var UNASSIGNED = -1, + GB18030_CODE = -2, + SEQ_START = -10, + NODE_START = -1000, + UNASSIGNED_NODE = new Array(0x100), + DEF_CHAR = -1; + +for (var i = 0; i < 0x100; i++) + UNASSIGNED_NODE[i] = UNASSIGNED; + + +// Class DBCSCodec reads and initializes mapping tables. +function DBCSCodec(codecOptions, iconv) { + this.encodingName = codecOptions.encodingName; + if (!codecOptions) + throw new Error("DBCS codec is called without the data.") + if (!codecOptions.table) + throw new Error("Encoding '" + this.encodingName + "' has no data."); + + // Load tables. + var mappingTable = codecOptions.table(); + + + // Decode tables: MBCS -> Unicode. + + // decodeTables is a trie, encoded as an array of arrays of integers. Internal arrays are trie nodes and all have len = 256. + // Trie root is decodeTables[0]. + // Values: >= 0 -> unicode character code. can be > 0xFFFF + // == UNASSIGNED -> unknown/unassigned sequence. + // == GB18030_CODE -> this is the end of a GB18030 4-byte sequence. + // <= NODE_START -> index of the next node in our trie to process next byte. + // <= SEQ_START -> index of the start of a character code sequence, in decodeTableSeq. + this.decodeTables = []; + this.decodeTables[0] = UNASSIGNED_NODE.slice(0); // Create root node. + + // Sometimes a MBCS char corresponds to a sequence of unicode chars. We store them as arrays of integers here. + this.decodeTableSeq = []; + + // Actual mapping tables consist of chunks. Use them to fill up decode tables. + for (var i = 0; i < mappingTable.length; i++) + this._addDecodeChunk(mappingTable[i]); + + // Load & create GB18030 tables when needed. + if (typeof codecOptions.gb18030 === 'function') { + this.gb18030 = codecOptions.gb18030(); // Load GB18030 ranges. + + // Add GB18030 common decode nodes. + var commonThirdByteNodeIdx = this.decodeTables.length; + this.decodeTables.push(UNASSIGNED_NODE.slice(0)); + + var commonFourthByteNodeIdx = this.decodeTables.length; + this.decodeTables.push(UNASSIGNED_NODE.slice(0)); + + // Fill out the tree + var firstByteNode = this.decodeTables[0]; + for (var i = 0x81; i <= 0xFE; i++) { + var secondByteNode = this.decodeTables[NODE_START - firstByteNode[i]]; + for (var j = 0x30; j <= 0x39; j++) { + if (secondByteNode[j] === UNASSIGNED) { + secondByteNode[j] = NODE_START - commonThirdByteNodeIdx; + } else if (secondByteNode[j] > NODE_START) { + throw new Error("gb18030 decode tables conflict at byte 2"); + } + + var thirdByteNode = this.decodeTables[NODE_START - secondByteNode[j]]; + for (var k = 0x81; k <= 0xFE; k++) { + if (thirdByteNode[k] === UNASSIGNED) { + thirdByteNode[k] = NODE_START - commonFourthByteNodeIdx; + } else if (thirdByteNode[k] === NODE_START - commonFourthByteNodeIdx) { + continue; + } else if (thirdByteNode[k] > NODE_START) { + throw new Error("gb18030 decode tables conflict at byte 3"); + } + + var fourthByteNode = this.decodeTables[NODE_START - thirdByteNode[k]]; + for (var l = 0x30; l <= 0x39; l++) { + if (fourthByteNode[l] === UNASSIGNED) + fourthByteNode[l] = GB18030_CODE; + } + } + } + } + } + + this.defaultCharUnicode = iconv.defaultCharUnicode; + + + // Encode tables: Unicode -> DBCS. + + // `encodeTable` is array mapping from unicode char to encoded char. All its values are integers for performance. + // Because it can be sparse, it is represented as array of buckets by 256 chars each. Bucket can be null. + // Values: >= 0 -> it is a normal char. Write the value (if <=256 then 1 byte, if <=65536 then 2 bytes, etc.). + // == UNASSIGNED -> no conversion found. Output a default char. + // <= SEQ_START -> it's an index in encodeTableSeq, see below. The character starts a sequence. + this.encodeTable = []; + + // `encodeTableSeq` is used when a sequence of unicode characters is encoded as a single code. We use a tree of + // objects where keys correspond to characters in sequence and leafs are the encoded dbcs values. A special DEF_CHAR key + // means end of sequence (needed when one sequence is a strict subsequence of another). + // Objects are kept separately from encodeTable to increase performance. + this.encodeTableSeq = []; + + // Some chars can be decoded, but need not be encoded. + var skipEncodeChars = {}; + if (codecOptions.encodeSkipVals) + for (var i = 0; i < codecOptions.encodeSkipVals.length; i++) { + var val = codecOptions.encodeSkipVals[i]; + if (typeof val === 'number') + skipEncodeChars[val] = true; + else + for (var j = val.from; j <= val.to; j++) + skipEncodeChars[j] = true; + } + + // Use decode trie to recursively fill out encode tables. + this._fillEncodeTable(0, 0, skipEncodeChars); + + // Add more encoding pairs when needed. + if (codecOptions.encodeAdd) { + for (var uChar in codecOptions.encodeAdd) + if (Object.prototype.hasOwnProperty.call(codecOptions.encodeAdd, uChar)) + this._setEncodeChar(uChar.charCodeAt(0), codecOptions.encodeAdd[uChar]); + } + + this.defCharSB = this.encodeTable[0][iconv.defaultCharSingleByte.charCodeAt(0)]; + if (this.defCharSB === UNASSIGNED) this.defCharSB = this.encodeTable[0]['?']; + if (this.defCharSB === UNASSIGNED) this.defCharSB = "?".charCodeAt(0); +} + +DBCSCodec.prototype.encoder = DBCSEncoder; +DBCSCodec.prototype.decoder = DBCSDecoder; + +// Decoder helpers +DBCSCodec.prototype._getDecodeTrieNode = function(addr) { + var bytes = []; + for (; addr > 0; addr >>>= 8) + bytes.push(addr & 0xFF); + if (bytes.length == 0) + bytes.push(0); + + var node = this.decodeTables[0]; + for (var i = bytes.length-1; i > 0; i--) { // Traverse nodes deeper into the trie. + var val = node[bytes[i]]; + + if (val == UNASSIGNED) { // Create new node. + node[bytes[i]] = NODE_START - this.decodeTables.length; + this.decodeTables.push(node = UNASSIGNED_NODE.slice(0)); + } + else if (val <= NODE_START) { // Existing node. + node = this.decodeTables[NODE_START - val]; + } + else + throw new Error("Overwrite byte in " + this.encodingName + ", addr: " + addr.toString(16)); + } + return node; +} + + +DBCSCodec.prototype._addDecodeChunk = function(chunk) { + // First element of chunk is the hex mbcs code where we start. + var curAddr = parseInt(chunk[0], 16); + + // Choose the decoding node where we'll write our chars. + var writeTable = this._getDecodeTrieNode(curAddr); + curAddr = curAddr & 0xFF; + + // Write all other elements of the chunk to the table. + for (var k = 1; k < chunk.length; k++) { + var part = chunk[k]; + if (typeof part === "string") { // String, write as-is. + for (var l = 0; l < part.length;) { + var code = part.charCodeAt(l++); + if (0xD800 <= code && code < 0xDC00) { // Decode surrogate + var codeTrail = part.charCodeAt(l++); + if (0xDC00 <= codeTrail && codeTrail < 0xE000) + writeTable[curAddr++] = 0x10000 + (code - 0xD800) * 0x400 + (codeTrail - 0xDC00); + else + throw new Error("Incorrect surrogate pair in " + this.encodingName + " at chunk " + chunk[0]); + } + else if (0x0FF0 < code && code <= 0x0FFF) { // Character sequence (our own encoding used) + var len = 0xFFF - code + 2; + var seq = []; + for (var m = 0; m < len; m++) + seq.push(part.charCodeAt(l++)); // Simple variation: don't support surrogates or subsequences in seq. + + writeTable[curAddr++] = SEQ_START - this.decodeTableSeq.length; + this.decodeTableSeq.push(seq); + } + else + writeTable[curAddr++] = code; // Basic char + } + } + else if (typeof part === "number") { // Integer, meaning increasing sequence starting with prev character. + var charCode = writeTable[curAddr - 1] + 1; + for (var l = 0; l < part; l++) + writeTable[curAddr++] = charCode++; + } + else + throw new Error("Incorrect type '" + typeof part + "' given in " + this.encodingName + " at chunk " + chunk[0]); + } + if (curAddr > 0xFF) + throw new Error("Incorrect chunk in " + this.encodingName + " at addr " + chunk[0] + ": too long" + curAddr); +} + +// Encoder helpers +DBCSCodec.prototype._getEncodeBucket = function(uCode) { + var high = uCode >> 8; // This could be > 0xFF because of astral characters. + if (this.encodeTable[high] === undefined) + this.encodeTable[high] = UNASSIGNED_NODE.slice(0); // Create bucket on demand. + return this.encodeTable[high]; +} + +DBCSCodec.prototype._setEncodeChar = function(uCode, dbcsCode) { + var bucket = this._getEncodeBucket(uCode); + var low = uCode & 0xFF; + if (bucket[low] <= SEQ_START) + this.encodeTableSeq[SEQ_START-bucket[low]][DEF_CHAR] = dbcsCode; // There's already a sequence, set a single-char subsequence of it. + else if (bucket[low] == UNASSIGNED) + bucket[low] = dbcsCode; +} + +DBCSCodec.prototype._setEncodeSequence = function(seq, dbcsCode) { + + // Get the root of character tree according to first character of the sequence. + var uCode = seq[0]; + var bucket = this._getEncodeBucket(uCode); + var low = uCode & 0xFF; + + var node; + if (bucket[low] <= SEQ_START) { + // There's already a sequence with - use it. + node = this.encodeTableSeq[SEQ_START-bucket[low]]; + } + else { + // There was no sequence object - allocate a new one. + node = {}; + if (bucket[low] !== UNASSIGNED) node[DEF_CHAR] = bucket[low]; // If a char was set before - make it a single-char subsequence. + bucket[low] = SEQ_START - this.encodeTableSeq.length; + this.encodeTableSeq.push(node); + } + + // Traverse the character tree, allocating new nodes as needed. + for (var j = 1; j < seq.length-1; j++) { + var oldVal = node[uCode]; + if (typeof oldVal === 'object') + node = oldVal; + else { + node = node[uCode] = {} + if (oldVal !== undefined) + node[DEF_CHAR] = oldVal + } + } + + // Set the leaf to given dbcsCode. + uCode = seq[seq.length-1]; + node[uCode] = dbcsCode; +} + +DBCSCodec.prototype._fillEncodeTable = function(nodeIdx, prefix, skipEncodeChars) { + var node = this.decodeTables[nodeIdx]; + var hasValues = false; + var subNodeEmpty = {}; + for (var i = 0; i < 0x100; i++) { + var uCode = node[i]; + var mbCode = prefix + i; + if (skipEncodeChars[mbCode]) + continue; + + if (uCode >= 0) { + this._setEncodeChar(uCode, mbCode); + hasValues = true; + } else if (uCode <= NODE_START) { + var subNodeIdx = NODE_START - uCode; + if (!subNodeEmpty[subNodeIdx]) { // Skip empty subtrees (they are too large in gb18030). + var newPrefix = (mbCode << 8) >>> 0; // NOTE: '>>> 0' keeps 32-bit num positive. + if (this._fillEncodeTable(subNodeIdx, newPrefix, skipEncodeChars)) + hasValues = true; + else + subNodeEmpty[subNodeIdx] = true; + } + } else if (uCode <= SEQ_START) { + this._setEncodeSequence(this.decodeTableSeq[SEQ_START - uCode], mbCode); + hasValues = true; + } + } + return hasValues; +} + + + +// == Encoder ================================================================== + +function DBCSEncoder(options, codec) { + // Encoder state + this.leadSurrogate = -1; + this.seqObj = undefined; + + // Static data + this.encodeTable = codec.encodeTable; + this.encodeTableSeq = codec.encodeTableSeq; + this.defaultCharSingleByte = codec.defCharSB; + this.gb18030 = codec.gb18030; +} + +DBCSEncoder.prototype.write = function(str) { + var newBuf = Buffer.alloc(str.length * (this.gb18030 ? 4 : 3)), + leadSurrogate = this.leadSurrogate, + seqObj = this.seqObj, nextChar = -1, + i = 0, j = 0; + + while (true) { + // 0. Get next character. + if (nextChar === -1) { + if (i == str.length) break; + var uCode = str.charCodeAt(i++); + } + else { + var uCode = nextChar; + nextChar = -1; + } + + // 1. Handle surrogates. + if (0xD800 <= uCode && uCode < 0xE000) { // Char is one of surrogates. + if (uCode < 0xDC00) { // We've got lead surrogate. + if (leadSurrogate === -1) { + leadSurrogate = uCode; + continue; + } else { + leadSurrogate = uCode; + // Double lead surrogate found. + uCode = UNASSIGNED; + } + } else { // We've got trail surrogate. + if (leadSurrogate !== -1) { + uCode = 0x10000 + (leadSurrogate - 0xD800) * 0x400 + (uCode - 0xDC00); + leadSurrogate = -1; + } else { + // Incomplete surrogate pair - only trail surrogate found. + uCode = UNASSIGNED; + } + + } + } + else if (leadSurrogate !== -1) { + // Incomplete surrogate pair - only lead surrogate found. + nextChar = uCode; uCode = UNASSIGNED; // Write an error, then current char. + leadSurrogate = -1; + } + + // 2. Convert uCode character. + var dbcsCode = UNASSIGNED; + if (seqObj !== undefined && uCode != UNASSIGNED) { // We are in the middle of the sequence + var resCode = seqObj[uCode]; + if (typeof resCode === 'object') { // Sequence continues. + seqObj = resCode; + continue; + + } else if (typeof resCode == 'number') { // Sequence finished. Write it. + dbcsCode = resCode; + + } else if (resCode == undefined) { // Current character is not part of the sequence. + + // Try default character for this sequence + resCode = seqObj[DEF_CHAR]; + if (resCode !== undefined) { + dbcsCode = resCode; // Found. Write it. + nextChar = uCode; // Current character will be written too in the next iteration. + + } else { + // TODO: What if we have no default? (resCode == undefined) + // Then, we should write first char of the sequence as-is and try the rest recursively. + // Didn't do it for now because no encoding has this situation yet. + // Currently, just skip the sequence and write current char. + } + } + seqObj = undefined; + } + else if (uCode >= 0) { // Regular character + var subtable = this.encodeTable[uCode >> 8]; + if (subtable !== undefined) + dbcsCode = subtable[uCode & 0xFF]; + + if (dbcsCode <= SEQ_START) { // Sequence start + seqObj = this.encodeTableSeq[SEQ_START-dbcsCode]; + continue; + } + + if (dbcsCode == UNASSIGNED && this.gb18030) { + // Use GB18030 algorithm to find character(s) to write. + var idx = findIdx(this.gb18030.uChars, uCode); + if (idx != -1) { + var dbcsCode = this.gb18030.gbChars[idx] + (uCode - this.gb18030.uChars[idx]); + newBuf[j++] = 0x81 + Math.floor(dbcsCode / 12600); dbcsCode = dbcsCode % 12600; + newBuf[j++] = 0x30 + Math.floor(dbcsCode / 1260); dbcsCode = dbcsCode % 1260; + newBuf[j++] = 0x81 + Math.floor(dbcsCode / 10); dbcsCode = dbcsCode % 10; + newBuf[j++] = 0x30 + dbcsCode; + continue; + } + } + } + + // 3. Write dbcsCode character. + if (dbcsCode === UNASSIGNED) + dbcsCode = this.defaultCharSingleByte; + + if (dbcsCode < 0x100) { + newBuf[j++] = dbcsCode; + } + else if (dbcsCode < 0x10000) { + newBuf[j++] = dbcsCode >> 8; // high byte + newBuf[j++] = dbcsCode & 0xFF; // low byte + } + else if (dbcsCode < 0x1000000) { + newBuf[j++] = dbcsCode >> 16; + newBuf[j++] = (dbcsCode >> 8) & 0xFF; + newBuf[j++] = dbcsCode & 0xFF; + } else { + newBuf[j++] = dbcsCode >>> 24; + newBuf[j++] = (dbcsCode >>> 16) & 0xFF; + newBuf[j++] = (dbcsCode >>> 8) & 0xFF; + newBuf[j++] = dbcsCode & 0xFF; + } + } + + this.seqObj = seqObj; + this.leadSurrogate = leadSurrogate; + return newBuf.slice(0, j); +} + +DBCSEncoder.prototype.end = function() { + if (this.leadSurrogate === -1 && this.seqObj === undefined) + return; // All clean. Most often case. + + var newBuf = Buffer.alloc(10), j = 0; + + if (this.seqObj) { // We're in the sequence. + var dbcsCode = this.seqObj[DEF_CHAR]; + if (dbcsCode !== undefined) { // Write beginning of the sequence. + if (dbcsCode < 0x100) { + newBuf[j++] = dbcsCode; + } + else { + newBuf[j++] = dbcsCode >> 8; // high byte + newBuf[j++] = dbcsCode & 0xFF; // low byte + } + } else { + // See todo above. + } + this.seqObj = undefined; + } + + if (this.leadSurrogate !== -1) { + // Incomplete surrogate pair - only lead surrogate found. + newBuf[j++] = this.defaultCharSingleByte; + this.leadSurrogate = -1; + } + + return newBuf.slice(0, j); +} + +// Export for testing +DBCSEncoder.prototype.findIdx = findIdx; + + +// == Decoder ================================================================== + +function DBCSDecoder(options, codec) { + // Decoder state + this.nodeIdx = 0; + this.prevBytes = []; + + // Static data + this.decodeTables = codec.decodeTables; + this.decodeTableSeq = codec.decodeTableSeq; + this.defaultCharUnicode = codec.defaultCharUnicode; + this.gb18030 = codec.gb18030; +} + +DBCSDecoder.prototype.write = function(buf) { + var newBuf = Buffer.alloc(buf.length*2), + nodeIdx = this.nodeIdx, + prevBytes = this.prevBytes, prevOffset = this.prevBytes.length, + seqStart = -this.prevBytes.length, // idx of the start of current parsed sequence. + uCode; + + for (var i = 0, j = 0; i < buf.length; i++) { + var curByte = (i >= 0) ? buf[i] : prevBytes[i + prevOffset]; + + // Lookup in current trie node. + var uCode = this.decodeTables[nodeIdx][curByte]; + + if (uCode >= 0) { + // Normal character, just use it. + } + else if (uCode === UNASSIGNED) { // Unknown char. + // TODO: Callback with seq. + uCode = this.defaultCharUnicode.charCodeAt(0); + i = seqStart; // Skip one byte ('i' will be incremented by the for loop) and try to parse again. + } + else if (uCode === GB18030_CODE) { + if (i >= 3) { + var ptr = (buf[i-3]-0x81)*12600 + (buf[i-2]-0x30)*1260 + (buf[i-1]-0x81)*10 + (curByte-0x30); + } else { + var ptr = (prevBytes[i-3+prevOffset]-0x81)*12600 + + (((i-2 >= 0) ? buf[i-2] : prevBytes[i-2+prevOffset])-0x30)*1260 + + (((i-1 >= 0) ? buf[i-1] : prevBytes[i-1+prevOffset])-0x81)*10 + + (curByte-0x30); + } + var idx = findIdx(this.gb18030.gbChars, ptr); + uCode = this.gb18030.uChars[idx] + ptr - this.gb18030.gbChars[idx]; + } + else if (uCode <= NODE_START) { // Go to next trie node. + nodeIdx = NODE_START - uCode; + continue; + } + else if (uCode <= SEQ_START) { // Output a sequence of chars. + var seq = this.decodeTableSeq[SEQ_START - uCode]; + for (var k = 0; k < seq.length - 1; k++) { + uCode = seq[k]; + newBuf[j++] = uCode & 0xFF; + newBuf[j++] = uCode >> 8; + } + uCode = seq[seq.length-1]; + } + else + throw new Error("iconv-lite internal error: invalid decoding table value " + uCode + " at " + nodeIdx + "/" + curByte); + + // Write the character to buffer, handling higher planes using surrogate pair. + if (uCode >= 0x10000) { + uCode -= 0x10000; + var uCodeLead = 0xD800 | (uCode >> 10); + newBuf[j++] = uCodeLead & 0xFF; + newBuf[j++] = uCodeLead >> 8; + + uCode = 0xDC00 | (uCode & 0x3FF); + } + newBuf[j++] = uCode & 0xFF; + newBuf[j++] = uCode >> 8; + + // Reset trie node. + nodeIdx = 0; seqStart = i+1; + } + + this.nodeIdx = nodeIdx; + this.prevBytes = (seqStart >= 0) + ? Array.prototype.slice.call(buf, seqStart) + : prevBytes.slice(seqStart + prevOffset).concat(Array.prototype.slice.call(buf)); + + return newBuf.slice(0, j).toString('ucs2'); +} + +DBCSDecoder.prototype.end = function() { + var ret = ''; + + // Try to parse all remaining chars. + while (this.prevBytes.length > 0) { + // Skip 1 character in the buffer. + ret += this.defaultCharUnicode; + var bytesArr = this.prevBytes.slice(1); + + // Parse remaining as usual. + this.prevBytes = []; + this.nodeIdx = 0; + if (bytesArr.length > 0) + ret += this.write(bytesArr); + } + + this.prevBytes = []; + this.nodeIdx = 0; + return ret; +} + +// Binary search for GB18030. Returns largest i such that table[i] <= val. +function findIdx(table, val) { + if (table[0] > val) + return -1; + + var l = 0, r = table.length; + while (l < r-1) { // always table[l] <= val < table[r] + var mid = l + ((r-l+1) >> 1); + if (table[mid] <= val) + l = mid; + else + r = mid; + } + return l; +} + + + +/***/ }), + +/***/ 2435: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +// Description of supported double byte encodings and aliases. +// Tables are not require()-d until they are needed to speed up library load. +// require()-s are direct to support Browserify. + +module.exports = { + + // == Japanese/ShiftJIS ==================================================== + // All japanese encodings are based on JIS X set of standards: + // JIS X 0201 - Single-byte encoding of ASCII + ¥ + Kana chars at 0xA1-0xDF. + // JIS X 0208 - Main set of 6879 characters, placed in 94x94 plane, to be encoded by 2 bytes. + // Has several variations in 1978, 1983, 1990 and 1997. + // JIS X 0212 - Supplementary plane of 6067 chars in 94x94 plane. 1990. Effectively dead. + // JIS X 0213 - Extension and modern replacement of 0208 and 0212. Total chars: 11233. + // 2 planes, first is superset of 0208, second - revised 0212. + // Introduced in 2000, revised 2004. Some characters are in Unicode Plane 2 (0x2xxxx) + + // Byte encodings are: + // * Shift_JIS: Compatible with 0201, uses not defined chars in top half as lead bytes for double-byte + // encoding of 0208. Lead byte ranges: 0x81-0x9F, 0xE0-0xEF; Trail byte ranges: 0x40-0x7E, 0x80-0x9E, 0x9F-0xFC. + // Windows CP932 is a superset of Shift_JIS. Some companies added more chars, notably KDDI. + // * EUC-JP: Up to 3 bytes per character. Used mostly on *nixes. + // 0x00-0x7F - lower part of 0201 + // 0x8E, 0xA1-0xDF - upper part of 0201 + // (0xA1-0xFE)x2 - 0208 plane (94x94). + // 0x8F, (0xA1-0xFE)x2 - 0212 plane (94x94). + // * JIS X 208: 7-bit, direct encoding of 0208. Byte ranges: 0x21-0x7E (94 values). Uncommon. + // Used as-is in ISO2022 family. + // * ISO2022-JP: Stateful encoding, with escape sequences to switch between ASCII, + // 0201-1976 Roman, 0208-1978, 0208-1983. + // * ISO2022-JP-1: Adds esc seq for 0212-1990. + // * ISO2022-JP-2: Adds esc seq for GB2313-1980, KSX1001-1992, ISO8859-1, ISO8859-7. + // * ISO2022-JP-3: Adds esc seq for 0201-1976 Kana set, 0213-2000 Planes 1, 2. + // * ISO2022-JP-2004: Adds 0213-2004 Plane 1. + // + // After JIS X 0213 appeared, Shift_JIS-2004, EUC-JISX0213 and ISO2022-JP-2004 followed, with just changing the planes. + // + // Overall, it seems that it's a mess :( http://www8.plala.or.jp/tkubota1/unicode-symbols-map2.html + + 'shiftjis': { + type: '_dbcs', + table: function() { return __nccwpck_require__(7572) }, + encodeAdd: {'\u00a5': 0x5C, '\u203E': 0x7E}, + encodeSkipVals: [{from: 0xED40, to: 0xF940}], + }, + 'csshiftjis': 'shiftjis', + 'mskanji': 'shiftjis', + 'sjis': 'shiftjis', + 'windows31j': 'shiftjis', + 'ms31j': 'shiftjis', + 'xsjis': 'shiftjis', + 'windows932': 'shiftjis', + 'ms932': 'shiftjis', + '932': 'shiftjis', + 'cp932': 'shiftjis', + + 'eucjp': { + type: '_dbcs', + table: function() { return __nccwpck_require__(4231) }, + encodeAdd: {'\u00a5': 0x5C, '\u203E': 0x7E}, + }, + + // TODO: KDDI extension to Shift_JIS + // TODO: IBM CCSID 942 = CP932, but F0-F9 custom chars and other char changes. + // TODO: IBM CCSID 943 = Shift_JIS = CP932 with original Shift_JIS lower 128 chars. + + + // == Chinese/GBK ========================================================== + // http://en.wikipedia.org/wiki/GBK + // We mostly implement W3C recommendation: https://www.w3.org/TR/encoding/#gbk-encoder + + // Oldest GB2312 (1981, ~7600 chars) is a subset of CP936 + 'gb2312': 'cp936', + 'gb231280': 'cp936', + 'gb23121980': 'cp936', + 'csgb2312': 'cp936', + 'csiso58gb231280': 'cp936', + 'euccn': 'cp936', + + // Microsoft's CP936 is a subset and approximation of GBK. + 'windows936': 'cp936', + 'ms936': 'cp936', + '936': 'cp936', + 'cp936': { + type: '_dbcs', + table: function() { return __nccwpck_require__(8949) }, + }, + + // GBK (~22000 chars) is an extension of CP936 that added user-mapped chars and some other. + 'gbk': { + type: '_dbcs', + table: function() { return (__nccwpck_require__(8949).concat)(__nccwpck_require__(9603)) }, + }, + 'xgbk': 'gbk', + 'isoir58': 'gbk', + + // GB18030 is an algorithmic extension of GBK. + // Main source: https://www.w3.org/TR/encoding/#gbk-encoder + // http://icu-project.org/docs/papers/gb18030.html + // http://source.icu-project.org/repos/icu/data/trunk/charset/data/xml/gb-18030-2000.xml + // http://www.khngai.com/chinese/charmap/tblgbk.php?page=0 + 'gb18030': { + type: '_dbcs', + table: function() { return (__nccwpck_require__(8949).concat)(__nccwpck_require__(9603)) }, + gb18030: function() { return __nccwpck_require__(7302) }, + encodeSkipVals: [0x80], + encodeAdd: {'€': 0xA2E3}, + }, + + 'chinese': 'gb18030', + + + // == Korean =============================================================== + // EUC-KR, KS_C_5601 and KS X 1001 are exactly the same. + 'windows949': 'cp949', + 'ms949': 'cp949', + '949': 'cp949', + 'cp949': { + type: '_dbcs', + table: function() { return __nccwpck_require__(5923) }, + }, + + 'cseuckr': 'cp949', + 'csksc56011987': 'cp949', + 'euckr': 'cp949', + 'isoir149': 'cp949', + 'korean': 'cp949', + 'ksc56011987': 'cp949', + 'ksc56011989': 'cp949', + 'ksc5601': 'cp949', + + + // == Big5/Taiwan/Hong Kong ================================================ + // There are lots of tables for Big5 and cp950. Please see the following links for history: + // http://moztw.org/docs/big5/ http://www.haible.de/bruno/charsets/conversion-tables/Big5.html + // Variations, in roughly number of defined chars: + // * Windows CP 950: Microsoft variant of Big5. Canonical: http://www.unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WINDOWS/CP950.TXT + // * Windows CP 951: Microsoft variant of Big5-HKSCS-2001. Seems to be never public. http://me.abelcheung.org/articles/research/what-is-cp951/ + // * Big5-2003 (Taiwan standard) almost superset of cp950. + // * Unicode-at-on (UAO) / Mozilla 1.8. Falling out of use on the Web. Not supported by other browsers. + // * Big5-HKSCS (-2001, -2004, -2008). Hong Kong standard. + // many unicode code points moved from PUA to Supplementary plane (U+2XXXX) over the years. + // Plus, it has 4 combining sequences. + // Seems that Mozilla refused to support it for 10 yrs. https://bugzilla.mozilla.org/show_bug.cgi?id=162431 https://bugzilla.mozilla.org/show_bug.cgi?id=310299 + // because big5-hkscs is the only encoding to include astral characters in non-algorithmic way. + // Implementations are not consistent within browsers; sometimes labeled as just big5. + // MS Internet Explorer switches from big5 to big5-hkscs when a patch applied. + // Great discussion & recap of what's going on https://bugzilla.mozilla.org/show_bug.cgi?id=912470#c31 + // In the encoder, it might make sense to support encoding old PUA mappings to Big5 bytes seq-s. + // Official spec: http://www.ogcio.gov.hk/en/business/tech_promotion/ccli/terms/doc/2003cmp_2008.txt + // http://www.ogcio.gov.hk/tc/business/tech_promotion/ccli/terms/doc/hkscs-2008-big5-iso.txt + // + // Current understanding of how to deal with Big5(-HKSCS) is in the Encoding Standard, http://encoding.spec.whatwg.org/#big5-encoder + // Unicode mapping (http://www.unicode.org/Public/MAPPINGS/OBSOLETE/EASTASIA/OTHER/BIG5.TXT) is said to be wrong. + + 'windows950': 'cp950', + 'ms950': 'cp950', + '950': 'cp950', + 'cp950': { + type: '_dbcs', + table: function() { return __nccwpck_require__(6517) }, + }, + + // Big5 has many variations and is an extension of cp950. We use Encoding Standard's as a consensus. + 'big5': 'big5hkscs', + 'big5hkscs': { + type: '_dbcs', + table: function() { return (__nccwpck_require__(6517).concat)(__nccwpck_require__(4244)) }, + encodeSkipVals: [ + // Although Encoding Standard says we should avoid encoding to HKSCS area (See Step 1 of + // https://encoding.spec.whatwg.org/#index-big5-pointer), we still do it to increase compatibility with ICU. + // But if a single unicode point can be encoded both as HKSCS and regular Big5, we prefer the latter. + 0x8e69, 0x8e6f, 0x8e7e, 0x8eab, 0x8eb4, 0x8ecd, 0x8ed0, 0x8f57, 0x8f69, 0x8f6e, 0x8fcb, 0x8ffe, + 0x906d, 0x907a, 0x90c4, 0x90dc, 0x90f1, 0x91bf, 0x92af, 0x92b0, 0x92b1, 0x92b2, 0x92d1, 0x9447, 0x94ca, + 0x95d9, 0x96fc, 0x9975, 0x9b76, 0x9b78, 0x9b7b, 0x9bc6, 0x9bde, 0x9bec, 0x9bf6, 0x9c42, 0x9c53, 0x9c62, + 0x9c68, 0x9c6b, 0x9c77, 0x9cbc, 0x9cbd, 0x9cd0, 0x9d57, 0x9d5a, 0x9dc4, 0x9def, 0x9dfb, 0x9ea9, 0x9eef, + 0x9efd, 0x9f60, 0x9fcb, 0xa077, 0xa0dc, 0xa0df, 0x8fcc, 0x92c8, 0x9644, 0x96ed, + + // Step 2 of https://encoding.spec.whatwg.org/#index-big5-pointer: Use last pointer for U+2550, U+255E, U+2561, U+256A, U+5341, or U+5345 + 0xa2a4, 0xa2a5, 0xa2a7, 0xa2a6, 0xa2cc, 0xa2ce, + ], + }, + + 'cnbig5': 'big5hkscs', + 'csbig5': 'big5hkscs', + 'xxbig5': 'big5hkscs', +}; + + +/***/ }), + +/***/ 5424: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + + +// Update this array if you add/rename/remove files in this directory. +// We support Browserify by skipping automatic module discovery and requiring modules directly. +var modules = [ + __nccwpck_require__(7799), + __nccwpck_require__(8015), + __nccwpck_require__(2402), + __nccwpck_require__(3152), + __nccwpck_require__(6146), + __nccwpck_require__(4818), + __nccwpck_require__(1566), + __nccwpck_require__(2417), + __nccwpck_require__(2435), +]; + +// Put all encoding/alias/codec definitions to single object and export it. +for (var i = 0; i < modules.length; i++) { + var module = modules[i]; + for (var enc in module) + if (Object.prototype.hasOwnProperty.call(module, enc)) + exports[enc] = module[enc]; +} + + +/***/ }), + +/***/ 7799: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + +var Buffer = (__nccwpck_require__(4199).Buffer); + +// Export Node.js internal encodings. + +module.exports = { + // Encodings + utf8: { type: "_internal", bomAware: true}, + cesu8: { type: "_internal", bomAware: true}, + unicode11utf8: "utf8", + + ucs2: { type: "_internal", bomAware: true}, + utf16le: "ucs2", + + binary: { type: "_internal" }, + base64: { type: "_internal" }, + hex: { type: "_internal" }, + + // Codec. + _internal: InternalCodec, +}; + +//------------------------------------------------------------------------------ + +function InternalCodec(codecOptions, iconv) { + this.enc = codecOptions.encodingName; + this.bomAware = codecOptions.bomAware; + + if (this.enc === "base64") + this.encoder = InternalEncoderBase64; + else if (this.enc === "cesu8") { + this.enc = "utf8"; // Use utf8 for decoding. + this.encoder = InternalEncoderCesu8; + + // Add decoder for versions of Node not supporting CESU-8 + if (Buffer.from('eda0bdedb2a9', 'hex').toString() !== '💩') { + this.decoder = InternalDecoderCesu8; + this.defaultCharUnicode = iconv.defaultCharUnicode; + } + } +} + +InternalCodec.prototype.encoder = InternalEncoder; +InternalCodec.prototype.decoder = InternalDecoder; + +//------------------------------------------------------------------------------ + +// We use node.js internal decoder. Its signature is the same as ours. +var StringDecoder = (__nccwpck_require__(3193).StringDecoder); + +if (!StringDecoder.prototype.end) // Node v0.8 doesn't have this method. + StringDecoder.prototype.end = function() {}; + + +function InternalDecoder(options, codec) { + this.decoder = new StringDecoder(codec.enc); +} + +InternalDecoder.prototype.write = function(buf) { + if (!Buffer.isBuffer(buf)) { + buf = Buffer.from(buf); + } + + return this.decoder.write(buf); +} + +InternalDecoder.prototype.end = function() { + return this.decoder.end(); +} + + +//------------------------------------------------------------------------------ +// Encoder is mostly trivial + +function InternalEncoder(options, codec) { + this.enc = codec.enc; +} + +InternalEncoder.prototype.write = function(str) { + return Buffer.from(str, this.enc); +} + +InternalEncoder.prototype.end = function() { +} + + +//------------------------------------------------------------------------------ +// Except base64 encoder, which must keep its state. + +function InternalEncoderBase64(options, codec) { + this.prevStr = ''; +} + +InternalEncoderBase64.prototype.write = function(str) { + str = this.prevStr + str; + var completeQuads = str.length - (str.length % 4); + this.prevStr = str.slice(completeQuads); + str = str.slice(0, completeQuads); + + return Buffer.from(str, "base64"); +} + +InternalEncoderBase64.prototype.end = function() { + return Buffer.from(this.prevStr, "base64"); +} + + +//------------------------------------------------------------------------------ +// CESU-8 encoder is also special. + +function InternalEncoderCesu8(options, codec) { +} + +InternalEncoderCesu8.prototype.write = function(str) { + var buf = Buffer.alloc(str.length * 3), bufIdx = 0; + for (var i = 0; i < str.length; i++) { + var charCode = str.charCodeAt(i); + // Naive implementation, but it works because CESU-8 is especially easy + // to convert from UTF-16 (which all JS strings are encoded in). + if (charCode < 0x80) + buf[bufIdx++] = charCode; + else if (charCode < 0x800) { + buf[bufIdx++] = 0xC0 + (charCode >>> 6); + buf[bufIdx++] = 0x80 + (charCode & 0x3f); + } + else { // charCode will always be < 0x10000 in javascript. + buf[bufIdx++] = 0xE0 + (charCode >>> 12); + buf[bufIdx++] = 0x80 + ((charCode >>> 6) & 0x3f); + buf[bufIdx++] = 0x80 + (charCode & 0x3f); + } + } + return buf.slice(0, bufIdx); +} + +InternalEncoderCesu8.prototype.end = function() { +} + +//------------------------------------------------------------------------------ +// CESU-8 decoder is not implemented in Node v4.0+ + +function InternalDecoderCesu8(options, codec) { + this.acc = 0; + this.contBytes = 0; + this.accBytes = 0; + this.defaultCharUnicode = codec.defaultCharUnicode; +} + +InternalDecoderCesu8.prototype.write = function(buf) { + var acc = this.acc, contBytes = this.contBytes, accBytes = this.accBytes, + res = ''; + for (var i = 0; i < buf.length; i++) { + var curByte = buf[i]; + if ((curByte & 0xC0) !== 0x80) { // Leading byte + if (contBytes > 0) { // Previous code is invalid + res += this.defaultCharUnicode; + contBytes = 0; + } + + if (curByte < 0x80) { // Single-byte code + res += String.fromCharCode(curByte); + } else if (curByte < 0xE0) { // Two-byte code + acc = curByte & 0x1F; + contBytes = 1; accBytes = 1; + } else if (curByte < 0xF0) { // Three-byte code + acc = curByte & 0x0F; + contBytes = 2; accBytes = 1; + } else { // Four or more are not supported for CESU-8. + res += this.defaultCharUnicode; + } + } else { // Continuation byte + if (contBytes > 0) { // We're waiting for it. + acc = (acc << 6) | (curByte & 0x3f); + contBytes--; accBytes++; + if (contBytes === 0) { + // Check for overlong encoding, but support Modified UTF-8 (encoding NULL as C0 80) + if (accBytes === 2 && acc < 0x80 && acc > 0) + res += this.defaultCharUnicode; + else if (accBytes === 3 && acc < 0x800) + res += this.defaultCharUnicode; + else + // Actually add character. + res += String.fromCharCode(acc); + } + } else { // Unexpected continuation byte + res += this.defaultCharUnicode; + } + } + } + this.acc = acc; this.contBytes = contBytes; this.accBytes = accBytes; + return res; +} + +InternalDecoderCesu8.prototype.end = function() { + var res = 0; + if (this.contBytes > 0) + res += this.defaultCharUnicode; + return res; +} + + +/***/ }), + +/***/ 6146: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +var Buffer = (__nccwpck_require__(4199).Buffer); + +// Single-byte codec. Needs a 'chars' string parameter that contains 256 or 128 chars that +// correspond to encoded bytes (if 128 - then lower half is ASCII). + +exports._sbcs = SBCSCodec; +function SBCSCodec(codecOptions, iconv) { + if (!codecOptions) + throw new Error("SBCS codec is called without the data.") + + // Prepare char buffer for decoding. + if (!codecOptions.chars || (codecOptions.chars.length !== 128 && codecOptions.chars.length !== 256)) + throw new Error("Encoding '"+codecOptions.type+"' has incorrect 'chars' (must be of len 128 or 256)"); + + if (codecOptions.chars.length === 128) { + var asciiString = ""; + for (var i = 0; i < 128; i++) + asciiString += String.fromCharCode(i); + codecOptions.chars = asciiString + codecOptions.chars; + } + + this.decodeBuf = Buffer.from(codecOptions.chars, 'ucs2'); + + // Encoding buffer. + var encodeBuf = Buffer.alloc(65536, iconv.defaultCharSingleByte.charCodeAt(0)); + + for (var i = 0; i < codecOptions.chars.length; i++) + encodeBuf[codecOptions.chars.charCodeAt(i)] = i; + + this.encodeBuf = encodeBuf; +} + +SBCSCodec.prototype.encoder = SBCSEncoder; +SBCSCodec.prototype.decoder = SBCSDecoder; + + +function SBCSEncoder(options, codec) { + this.encodeBuf = codec.encodeBuf; +} + +SBCSEncoder.prototype.write = function(str) { + var buf = Buffer.alloc(str.length); + for (var i = 0; i < str.length; i++) + buf[i] = this.encodeBuf[str.charCodeAt(i)]; + + return buf; +} + +SBCSEncoder.prototype.end = function() { +} + + +function SBCSDecoder(options, codec) { + this.decodeBuf = codec.decodeBuf; +} + +SBCSDecoder.prototype.write = function(buf) { + // Strings are immutable in JS -> we use ucs2 buffer to speed up computations. + var decodeBuf = this.decodeBuf; + var newBuf = Buffer.alloc(buf.length*2); + var idx1 = 0, idx2 = 0; + for (var i = 0; i < buf.length; i++) { + idx1 = buf[i]*2; idx2 = i*2; + newBuf[idx2] = decodeBuf[idx1]; + newBuf[idx2+1] = decodeBuf[idx1+1]; + } + return newBuf.toString('ucs2'); +} + +SBCSDecoder.prototype.end = function() { +} + + +/***/ }), + +/***/ 1566: +/***/ ((module) => { + +"use strict"; + + +// Generated data for sbcs codec. Don't edit manually. Regenerate using generation/gen-sbcs.js script. +module.exports = { + "437": "cp437", + "737": "cp737", + "775": "cp775", + "850": "cp850", + "852": "cp852", + "855": "cp855", + "856": "cp856", + "857": "cp857", + "858": "cp858", + "860": "cp860", + "861": "cp861", + "862": "cp862", + "863": "cp863", + "864": "cp864", + "865": "cp865", + "866": "cp866", + "869": "cp869", + "874": "windows874", + "922": "cp922", + "1046": "cp1046", + "1124": "cp1124", + "1125": "cp1125", + "1129": "cp1129", + "1133": "cp1133", + "1161": "cp1161", + "1162": "cp1162", + "1163": "cp1163", + "1250": "windows1250", + "1251": "windows1251", + "1252": "windows1252", + "1253": "windows1253", + "1254": "windows1254", + "1255": "windows1255", + "1256": "windows1256", + "1257": "windows1257", + "1258": "windows1258", + "28591": "iso88591", + "28592": "iso88592", + "28593": "iso88593", + "28594": "iso88594", + "28595": "iso88595", + "28596": "iso88596", + "28597": "iso88597", + "28598": "iso88598", + "28599": "iso88599", + "28600": "iso885910", + "28601": "iso885911", + "28603": "iso885913", + "28604": "iso885914", + "28605": "iso885915", + "28606": "iso885916", + "windows874": { + "type": "_sbcs", + "chars": "€����…�����������‘’“”•–—�������� กขฃคฅฆงจฉชซฌญฎฏฐฑฒณดตถทธนบปผฝพฟภมยรฤลฦวศษสหฬอฮฯะัาำิีึืฺุู����฿เแโใไๅๆ็่้๊๋์ํ๎๏๐๑๒๓๔๕๖๗๘๙๚๛����" + }, + "win874": "windows874", + "cp874": "windows874", + "windows1250": { + "type": "_sbcs", + "chars": "€�‚�„…†‡�‰Š‹ŚŤŽŹ�‘’“”•–—�™š›śťžź ˇ˘Ł¤Ą¦§¨©Ş«¬­®Ż°±˛ł´µ¶·¸ąş»Ľ˝ľżŔÁÂĂÄĹĆÇČÉĘËĚÍÎĎĐŃŇÓÔŐÖ×ŘŮÚŰÜÝŢßŕáâăäĺćçčéęëěíîďđńňóôőö÷řůúűüýţ˙" + }, + "win1250": "windows1250", + "cp1250": "windows1250", + "windows1251": { + "type": "_sbcs", + "chars": "ЂЃ‚ѓ„…†‡€‰Љ‹ЊЌЋЏђ‘’“”•–—�™љ›њќћџ ЎўЈ¤Ґ¦§Ё©Є«¬­®Ї°±Ііґµ¶·ё№є»јЅѕїАБВГДЕЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯабвгдежзийклмнопрстуфхцчшщъыьэюя" + }, + "win1251": "windows1251", + "cp1251": "windows1251", + "windows1252": { + "type": "_sbcs", + "chars": "€�‚ƒ„…†‡ˆ‰Š‹Œ�Ž��‘’“”•–—˜™š›œ�žŸ ¡¢£¤¥¦§¨©ª«¬­®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖ×ØÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ" + }, + "win1252": "windows1252", + "cp1252": "windows1252", + "windows1253": { + "type": "_sbcs", + "chars": "€�‚ƒ„…†‡�‰�‹�����‘’“”•–—�™�›���� ΅Ά£¤¥¦§¨©�«¬­®―°±²³΄µ¶·ΈΉΊ»Ό½ΎΏΐΑΒΓΔΕΖΗΘΙΚΛΜΝΞΟΠΡ�ΣΤΥΦΧΨΩΪΫάέήίΰαβγδεζηθικλμνξοπρςστυφχψωϊϋόύώ�" + }, + "win1253": "windows1253", + "cp1253": "windows1253", + "windows1254": { + "type": "_sbcs", + "chars": "€�‚ƒ„…†‡ˆ‰Š‹Œ����‘’“”•–—˜™š›œ��Ÿ ¡¢£¤¥¦§¨©ª«¬­®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏĞÑÒÓÔÕÖ×ØÙÚÛÜİŞßàáâãäåæçèéêëìíîïğñòóôõö÷øùúûüışÿ" + }, + "win1254": "windows1254", + "cp1254": "windows1254", + "windows1255": { + "type": "_sbcs", + "chars": "€�‚ƒ„…†‡ˆ‰�‹�����‘’“”•–—˜™�›���� ¡¢£₪¥¦§¨©×«¬­®¯°±²³´µ¶·¸¹÷»¼½¾¿ְֱֲֳִֵֶַָֹֺֻּֽ־ֿ׀ׁׂ׃װױײ׳״�������אבגדהוזחטיךכלםמןנסעףפץצקרשת��‎‏�" + }, + "win1255": "windows1255", + "cp1255": "windows1255", + "windows1256": { + "type": "_sbcs", + "chars": "€پ‚ƒ„…†‡ˆ‰ٹ‹Œچژڈگ‘’“”•–—ک™ڑ›œ‌‍ں ،¢£¤¥¦§¨©ھ«¬­®¯°±²³´µ¶·¸¹؛»¼½¾؟ہءآأؤإئابةتثجحخدذرزسشصض×طظعغـفقكàلâمنهوçèéêëىيîïًٌٍَôُِ÷ّùْûü‎‏ے" + }, + "win1256": "windows1256", + "cp1256": "windows1256", + "windows1257": { + "type": "_sbcs", + "chars": "€�‚�„…†‡�‰�‹�¨ˇ¸�‘’“”•–—�™�›�¯˛� �¢£¤�¦§Ø©Ŗ«¬­®Æ°±²³´µ¶·ø¹ŗ»¼½¾æĄĮĀĆÄÅĘĒČÉŹĖĢĶĪĻŠŃŅÓŌÕÖ×ŲŁŚŪÜŻŽßąįāćäåęēčéźėģķīļšńņóōõö÷ųłśūüżž˙" + }, + "win1257": "windows1257", + "cp1257": "windows1257", + "windows1258": { + "type": "_sbcs", + "chars": "€�‚ƒ„…†‡ˆ‰�‹Œ����‘’“”•–—˜™�›œ��Ÿ ¡¢£¤¥¦§¨©ª«¬­®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂĂÄÅÆÇÈÉÊË̀ÍÎÏĐÑ̉ÓÔƠÖ×ØÙÚÛÜỮßàáâăäåæçèéêë́íîïđṇ̃óôơö÷øùúûüư₫ÿ" + }, + "win1258": "windows1258", + "cp1258": "windows1258", + "iso88591": { + "type": "_sbcs", + "chars": "€‚ƒ„…†‡ˆ‰Š‹ŒŽ‘’“”•–—˜™š›œžŸ ¡¢£¤¥¦§¨©ª«¬­®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖ×ØÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ" + }, + "cp28591": "iso88591", + "iso88592": { + "type": "_sbcs", + "chars": "€‚ƒ„…†‡ˆ‰Š‹ŒŽ‘’“”•–—˜™š›œžŸ Ą˘Ł¤ĽŚ§¨ŠŞŤŹ­ŽŻ°ą˛ł´ľśˇ¸šşťź˝žżŔÁÂĂÄĹĆÇČÉĘËĚÍÎĎĐŃŇÓÔŐÖ×ŘŮÚŰÜÝŢßŕáâăäĺćçčéęëěíîďđńňóôőö÷řůúűüýţ˙" + }, + "cp28592": "iso88592", + "iso88593": { + "type": "_sbcs", + "chars": "€‚ƒ„…†‡ˆ‰Š‹ŒŽ‘’“”•–—˜™š›œžŸ Ħ˘£¤�Ĥ§¨İŞĞĴ­�Ż°ħ²³´µĥ·¸ışğĵ½�żÀÁÂ�ÄĊĈÇÈÉÊËÌÍÎÏ�ÑÒÓÔĠÖ×ĜÙÚÛÜŬŜßàáâ�äċĉçèéêëìíîï�ñòóôġö÷ĝùúûüŭŝ˙" + }, + "cp28593": "iso88593", + "iso88594": { + "type": "_sbcs", + "chars": "€‚ƒ„…†‡ˆ‰Š‹ŒŽ‘’“”•–—˜™š›œžŸ ĄĸŖ¤ĨĻ§¨ŠĒĢŦ­Ž¯°ą˛ŗ´ĩļˇ¸šēģŧŊžŋĀÁÂÃÄÅÆĮČÉĘËĖÍÎĪĐŅŌĶÔÕÖ×ØŲÚÛÜŨŪßāáâãäåæįčéęëėíîīđņōķôõö÷øųúûüũū˙" + }, + "cp28594": "iso88594", + "iso88595": { + "type": "_sbcs", + "chars": "€‚ƒ„…†‡ˆ‰Š‹ŒŽ‘’“”•–—˜™š›œžŸ ЁЂЃЄЅІЇЈЉЊЋЌ­ЎЏАБВГДЕЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯабвгдежзийклмнопрстуфхцчшщъыьэюя№ёђѓєѕіїјљњћќ§ўџ" + }, + "cp28595": "iso88595", + "iso88596": { + "type": "_sbcs", + "chars": "€‚ƒ„…†‡ˆ‰Š‹ŒŽ‘’“”•–—˜™š›œžŸ ���¤�������،­�������������؛���؟�ءآأؤإئابةتثجحخدذرزسشصضطظعغ�����ـفقكلمنهوىيًٌٍَُِّْ�������������" + }, + "cp28596": "iso88596", + "iso88597": { + "type": "_sbcs", + "chars": "€‚ƒ„…†‡ˆ‰Š‹ŒŽ‘’“”•–—˜™š›œžŸ ‘’£€₯¦§¨©ͺ«¬­�―°±²³΄΅Ά·ΈΉΊ»Ό½ΎΏΐΑΒΓΔΕΖΗΘΙΚΛΜΝΞΟΠΡ�ΣΤΥΦΧΨΩΪΫάέήίΰαβγδεζηθικλμνξοπρςστυφχψωϊϋόύώ�" + }, + "cp28597": "iso88597", + "iso88598": { + "type": "_sbcs", + "chars": "€‚ƒ„…†‡ˆ‰Š‹ŒŽ‘’“”•–—˜™š›œžŸ �¢£¤¥¦§¨©×«¬­®¯°±²³´µ¶·¸¹÷»¼½¾��������������������������������‗אבגדהוזחטיךכלםמןנסעףפץצקרשת��‎‏�" + }, + "cp28598": "iso88598", + "iso88599": { + "type": "_sbcs", + "chars": "€‚ƒ„…†‡ˆ‰Š‹ŒŽ‘’“”•–—˜™š›œžŸ ¡¢£¤¥¦§¨©ª«¬­®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏĞÑÒÓÔÕÖ×ØÙÚÛÜİŞßàáâãäåæçèéêëìíîïğñòóôõö÷øùúûüışÿ" + }, + "cp28599": "iso88599", + "iso885910": { + "type": "_sbcs", + "chars": "€‚ƒ„…†‡ˆ‰Š‹ŒŽ‘’“”•–—˜™š›œžŸ ĄĒĢĪĨĶ§ĻĐŠŦŽ­ŪŊ°ąēģīĩķ·ļđšŧž―ūŋĀÁÂÃÄÅÆĮČÉĘËĖÍÎÏÐŅŌÓÔÕÖŨØŲÚÛÜÝÞßāáâãäåæįčéęëėíîïðņōóôõöũøųúûüýþĸ" + }, + "cp28600": "iso885910", + "iso885911": { + "type": "_sbcs", + "chars": "€‚ƒ„…†‡ˆ‰Š‹ŒŽ‘’“”•–—˜™š›œžŸ กขฃคฅฆงจฉชซฌญฎฏฐฑฒณดตถทธนบปผฝพฟภมยรฤลฦวศษสหฬอฮฯะัาำิีึืฺุู����฿เแโใไๅๆ็่้๊๋์ํ๎๏๐๑๒๓๔๕๖๗๘๙๚๛����" + }, + "cp28601": "iso885911", + "iso885913": { + "type": "_sbcs", + "chars": "€‚ƒ„…†‡ˆ‰Š‹ŒŽ‘’“”•–—˜™š›œžŸ ”¢£¤„¦§Ø©Ŗ«¬­®Æ°±²³“µ¶·ø¹ŗ»¼½¾æĄĮĀĆÄÅĘĒČÉŹĖĢĶĪĻŠŃŅÓŌÕÖ×ŲŁŚŪÜŻŽßąįāćäåęēčéźėģķīļšńņóōõö÷ųłśūüżž’" + }, + "cp28603": "iso885913", + "iso885914": { + "type": "_sbcs", + "chars": "€‚ƒ„…†‡ˆ‰Š‹ŒŽ‘’“”•–—˜™š›œžŸ Ḃḃ£ĊċḊ§Ẁ©ẂḋỲ­®ŸḞḟĠġṀṁ¶ṖẁṗẃṠỳẄẅṡÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏŴÑÒÓÔÕÖṪØÙÚÛÜÝŶßàáâãäåæçèéêëìíîïŵñòóôõöṫøùúûüýŷÿ" + }, + "cp28604": "iso885914", + "iso885915": { + "type": "_sbcs", + "chars": "€‚ƒ„…†‡ˆ‰Š‹ŒŽ‘’“”•–—˜™š›œžŸ ¡¢£€¥Š§š©ª«¬­®¯°±²³Žµ¶·ž¹º»ŒœŸ¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖ×ØÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ" + }, + "cp28605": "iso885915", + "iso885916": { + "type": "_sbcs", + "chars": "€‚ƒ„…†‡ˆ‰Š‹ŒŽ‘’“”•–—˜™š›œžŸ ĄąŁ€„Š§š©Ș«Ź­źŻ°±ČłŽ”¶·žčș»ŒœŸżÀÁÂĂÄĆÆÇÈÉÊËÌÍÎÏĐŃÒÓÔŐÖŚŰÙÚÛÜĘȚßàáâăäćæçèéêëìíîïđńòóôőöśűùúûüęțÿ" + }, + "cp28606": "iso885916", + "cp437": { + "type": "_sbcs", + "chars": "ÇüéâäàåçêëèïîìÄÅÉæÆôöòûùÿÖÜ¢£¥₧ƒáíóúñѪº¿⌐¬½¼¡«»░▒▓│┤╡╢╖╕╣║╗╝╜╛┐└┴┬├─┼╞╟╚╔╩╦╠═╬╧╨╤╥╙╘╒╓╫╪┘┌█▄▌▐▀αßΓπΣσµτΦΘΩδ∞φε∩≡±≥≤⌠⌡÷≈°∙·√ⁿ²■ " + }, + "ibm437": "cp437", + "csibm437": "cp437", + "cp737": { + "type": "_sbcs", + "chars": "ΑΒΓΔΕΖΗΘΙΚΛΜΝΞΟΠΡΣΤΥΦΧΨΩαβγδεζηθικλμνξοπρσςτυφχψ░▒▓│┤╡╢╖╕╣║╗╝╜╛┐└┴┬├─┼╞╟╚╔╩╦╠═╬╧╨╤╥╙╘╒╓╫╪┘┌█▄▌▐▀ωάέήϊίόύϋώΆΈΉΊΌΎΏ±≥≤ΪΫ÷≈°∙·√ⁿ²■ " + }, + "ibm737": "cp737", + "csibm737": "cp737", + "cp775": { + "type": "_sbcs", + "chars": "ĆüéāäģåćłēŖŗīŹÄÅÉæÆōöĢ¢ŚśÖÜø£ØפĀĪóŻżź”¦©®¬½¼Ł«»░▒▓│┤ĄČĘĖ╣║╗╝ĮŠ┐└┴┬├─┼ŲŪ╚╔╩╦╠═╬Žąčęėįšųūž┘┌█▄▌▐▀ÓßŌŃõÕµńĶķĻļņĒŅ’­±“¾¶§÷„°∙·¹³²■ " + }, + "ibm775": "cp775", + "csibm775": "cp775", + "cp850": { + "type": "_sbcs", + "chars": "ÇüéâäàåçêëèïîìÄÅÉæÆôöòûùÿÖÜø£Ø׃áíóúñѪº¿®¬½¼¡«»░▒▓│┤ÁÂÀ©╣║╗╝¢¥┐└┴┬├─┼ãÃ╚╔╩╦╠═╬¤ðÐÊËÈıÍÎÏ┘┌█▄¦Ì▀ÓßÔÒõÕµþÞÚÛÙýݯ´­±‗¾¶§÷¸°¨·¹³²■ " + }, + "ibm850": "cp850", + "csibm850": "cp850", + "cp852": { + "type": "_sbcs", + "chars": "ÇüéâäůćçłëŐőîŹÄĆÉĹĺôöĽľŚśÖÜŤťŁ×čáíóúĄąŽžĘ꬟Ⱥ«»░▒▓│┤ÁÂĚŞ╣║╗╝Żż┐└┴┬├─┼Ăă╚╔╩╦╠═╬¤đĐĎËďŇÍÎě┘┌█▄ŢŮ▀ÓßÔŃńňŠšŔÚŕŰýÝţ´­˝˛ˇ˘§÷¸°¨˙űŘř■ " + }, + "ibm852": "cp852", + "csibm852": "cp852", + "cp855": { + "type": "_sbcs", + "chars": "ђЂѓЃёЁєЄѕЅіІїЇјЈљЉњЊћЋќЌўЎџЏюЮъЪаАбБцЦдДеЕфФгГ«»░▒▓│┤хХиИ╣║╗╝йЙ┐└┴┬├─┼кК╚╔╩╦╠═╬¤лЛмМнНоОп┘┌█▄Пя▀ЯрРсСтТуУжЖвВьЬ№­ыЫзЗшШэЭщЩчЧ§■ " + }, + "ibm855": "cp855", + "csibm855": "cp855", + "cp856": { + "type": "_sbcs", + "chars": "אבגדהוזחטיךכלםמןנסעףפץצקרשת�£�×����������®¬½¼�«»░▒▓│┤���©╣║╗╝¢¥┐└┴┬├─┼��╚╔╩╦╠═╬¤���������┘┌█▄¦�▀������µ�������¯´­±‗¾¶§÷¸°¨·¹³²■ " + }, + "ibm856": "cp856", + "csibm856": "cp856", + "cp857": { + "type": "_sbcs", + "chars": "ÇüéâäàåçêëèïîıÄÅÉæÆôöòûùİÖÜø£ØŞşáíóúñÑĞ𿮬½¼¡«»░▒▓│┤ÁÂÀ©╣║╗╝¢¥┐└┴┬├─┼ãÃ╚╔╩╦╠═╬¤ºªÊËÈ�ÍÎÏ┘┌█▄¦Ì▀ÓßÔÒõÕµ�×ÚÛÙìÿ¯´­±�¾¶§÷¸°¨·¹³²■ " + }, + "ibm857": "cp857", + "csibm857": "cp857", + "cp858": { + "type": "_sbcs", + "chars": "ÇüéâäàåçêëèïîìÄÅÉæÆôöòûùÿÖÜø£Ø׃áíóúñѪº¿®¬½¼¡«»░▒▓│┤ÁÂÀ©╣║╗╝¢¥┐└┴┬├─┼ãÃ╚╔╩╦╠═╬¤ðÐÊËÈ€ÍÎÏ┘┌█▄¦Ì▀ÓßÔÒõÕµþÞÚÛÙýݯ´­±‗¾¶§÷¸°¨·¹³²■ " + }, + "ibm858": "cp858", + "csibm858": "cp858", + "cp860": { + "type": "_sbcs", + "chars": "ÇüéâãàÁçêÊèÍÔìÃÂÉÀÈôõòÚùÌÕÜ¢£Ù₧ÓáíóúñѪº¿Ò¬½¼¡«»░▒▓│┤╡╢╖╕╣║╗╝╜╛┐└┴┬├─┼╞╟╚╔╩╦╠═╬╧╨╤╥╙╘╒╓╫╪┘┌█▄▌▐▀αßΓπΣσµτΦΘΩδ∞φε∩≡±≥≤⌠⌡÷≈°∙·√ⁿ²■ " + }, + "ibm860": "cp860", + "csibm860": "cp860", + "cp861": { + "type": "_sbcs", + "chars": "ÇüéâäàåçêëèÐðÞÄÅÉæÆôöþûÝýÖÜø£Ø₧ƒáíóúÁÍÓÚ¿⌐¬½¼¡«»░▒▓│┤╡╢╖╕╣║╗╝╜╛┐└┴┬├─┼╞╟╚╔╩╦╠═╬╧╨╤╥╙╘╒╓╫╪┘┌█▄▌▐▀αßΓπΣσµτΦΘΩδ∞φε∩≡±≥≤⌠⌡÷≈°∙·√ⁿ²■ " + }, + "ibm861": "cp861", + "csibm861": "cp861", + "cp862": { + "type": "_sbcs", + "chars": "אבגדהוזחטיךכלםמןנסעףפץצקרשת¢£¥₧ƒáíóúñѪº¿⌐¬½¼¡«»░▒▓│┤╡╢╖╕╣║╗╝╜╛┐└┴┬├─┼╞╟╚╔╩╦╠═╬╧╨╤╥╙╘╒╓╫╪┘┌█▄▌▐▀αßΓπΣσµτΦΘΩδ∞φε∩≡±≥≤⌠⌡÷≈°∙·√ⁿ²■ " + }, + "ibm862": "cp862", + "csibm862": "cp862", + "cp863": { + "type": "_sbcs", + "chars": "ÇüéâÂà¶çêëèïî‗À§ÉÈÊôËÏûù¤ÔÜ¢£ÙÛƒ¦´óú¨¸³¯Î⌐¬½¼¾«»░▒▓│┤╡╢╖╕╣║╗╝╜╛┐└┴┬├─┼╞╟╚╔╩╦╠═╬╧╨╤╥╙╘╒╓╫╪┘┌█▄▌▐▀αßΓπΣσµτΦΘΩδ∞φε∩≡±≥≤⌠⌡÷≈°∙·√ⁿ²■ " + }, + "ibm863": "cp863", + "csibm863": "cp863", + "cp864": { + "type": "_sbcs", + "chars": "\u0000\u0001\u0002\u0003\u0004\u0005\u0006\u0007\b\t\n\u000b\f\r\u000e\u000f\u0010\u0011\u0012\u0013\u0014\u0015\u0016\u0017\u0018\u0019\u001a\u001b\u001c\u001d\u001e\u001f !\"#$٪&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~°·∙√▒─│┼┤┬├┴┐┌└┘β∞φ±½¼≈«»ﻷﻸ��ﻻﻼ� ­ﺂ£¤ﺄ��ﺎﺏﺕﺙ،ﺝﺡﺥ٠١٢٣٤٥٦٧٨٩ﻑ؛ﺱﺵﺹ؟¢ﺀﺁﺃﺅﻊﺋﺍﺑﺓﺗﺛﺟﺣﺧﺩﺫﺭﺯﺳﺷﺻﺿﻁﻅﻋﻏ¦¬÷×ﻉـﻓﻗﻛﻟﻣﻧﻫﻭﻯﻳﺽﻌﻎﻍﻡﹽّﻥﻩﻬﻰﻲﻐﻕﻵﻶﻝﻙﻱ■�" + }, + "ibm864": "cp864", + "csibm864": "cp864", + "cp865": { + "type": "_sbcs", + "chars": "ÇüéâäàåçêëèïîìÄÅÉæÆôöòûùÿÖÜø£Ø₧ƒáíóúñѪº¿⌐¬½¼¡«¤░▒▓│┤╡╢╖╕╣║╗╝╜╛┐└┴┬├─┼╞╟╚╔╩╦╠═╬╧╨╤╥╙╘╒╓╫╪┘┌█▄▌▐▀αßΓπΣσµτΦΘΩδ∞φε∩≡±≥≤⌠⌡÷≈°∙·√ⁿ²■ " + }, + "ibm865": "cp865", + "csibm865": "cp865", + "cp866": { + "type": "_sbcs", + "chars": "АБВГДЕЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯабвгдежзийклмноп░▒▓│┤╡╢╖╕╣║╗╝╜╛┐└┴┬├─┼╞╟╚╔╩╦╠═╬╧╨╤╥╙╘╒╓╫╪┘┌█▄▌▐▀рстуфхцчшщъыьэюяЁёЄєЇїЎў°∙·√№¤■ " + }, + "ibm866": "cp866", + "csibm866": "cp866", + "cp869": { + "type": "_sbcs", + "chars": "������Ά�·¬¦‘’Έ―ΉΊΪΌ��ΎΫ©Ώ²³ά£έήίϊΐόύΑΒΓΔΕΖΗ½ΘΙ«»░▒▓│┤ΚΛΜΝ╣║╗╝ΞΟ┐└┴┬├─┼ΠΡ╚╔╩╦╠═╬ΣΤΥΦΧΨΩαβγ┘┌█▄δε▀ζηθικλμνξοπρσςτ΄­±υφχ§ψ΅°¨ωϋΰώ■ " + }, + "ibm869": "cp869", + "csibm869": "cp869", + "cp922": { + "type": "_sbcs", + "chars": "€‚ƒ„…†‡ˆ‰Š‹ŒŽ‘’“”•–—˜™š›œžŸ ¡¢£¤¥¦§¨©ª«¬­®‾°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏŠÑÒÓÔÕÖ×ØÙÚÛÜÝŽßàáâãäåæçèéêëìíîïšñòóôõö÷øùúûüýžÿ" + }, + "ibm922": "cp922", + "csibm922": "cp922", + "cp1046": { + "type": "_sbcs", + "chars": "ﺈ×÷ﹱˆ■│─┐┌└┘ﹹﹻﹽﹿﹷﺊﻰﻳﻲﻎﻏﻐﻶﻸﻺﻼ ¤ﺋﺑﺗﺛﺟﺣ،­ﺧﺳ٠١٢٣٤٥٦٧٨٩ﺷ؛ﺻﺿﻊ؟ﻋءآأؤإئابةتثجحخدذرزسشصضطﻇعغﻌﺂﺄﺎﻓـفقكلمنهوىيًٌٍَُِّْﻗﻛﻟﻵﻷﻹﻻﻣﻧﻬﻩ�" + }, + "ibm1046": "cp1046", + "csibm1046": "cp1046", + "cp1124": { + "type": "_sbcs", + "chars": "€‚ƒ„…†‡ˆ‰Š‹ŒŽ‘’“”•–—˜™š›œžŸ ЁЂҐЄЅІЇЈЉЊЋЌ­ЎЏАБВГДЕЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯабвгдежзийклмнопрстуфхцчшщъыьэюя№ёђґєѕіїјљњћќ§ўџ" + }, + "ibm1124": "cp1124", + "csibm1124": "cp1124", + "cp1125": { + "type": "_sbcs", + "chars": "АБВГДЕЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯабвгдежзийклмноп░▒▓│┤╡╢╖╕╣║╗╝╜╛┐└┴┬├─┼╞╟╚╔╩╦╠═╬╧╨╤╥╙╘╒╓╫╪┘┌█▄▌▐▀рстуфхцчшщъыьэюяЁёҐґЄєІіЇї·√№¤■ " + }, + "ibm1125": "cp1125", + "csibm1125": "cp1125", + "cp1129": { + "type": "_sbcs", + "chars": "€‚ƒ„…†‡ˆ‰Š‹ŒŽ‘’“”•–—˜™š›œžŸ ¡¢£¤¥¦§œ©ª«¬­®¯°±²³Ÿµ¶·Œ¹º»¼½¾¿ÀÁÂĂÄÅÆÇÈÉÊË̀ÍÎÏĐÑ̉ÓÔƠÖ×ØÙÚÛÜỮßàáâăäåæçèéêë́íîïđṇ̃óôơö÷øùúûüư₫ÿ" + }, + "ibm1129": "cp1129", + "csibm1129": "cp1129", + "cp1133": { + "type": "_sbcs", + "chars": "€‚ƒ„…†‡ˆ‰Š‹ŒŽ‘’“”•–—˜™š›œžŸ ກຂຄງຈສຊຍດຕຖທນບປຜຝພຟມຢຣລວຫອຮ���ຯະາຳິີຶືຸູຼັົຽ���ເແໂໃໄ່້໊໋໌ໍໆ�ໜໝ₭����������������໐໑໒໓໔໕໖໗໘໙��¢¬¦�" + }, + "ibm1133": "cp1133", + "csibm1133": "cp1133", + "cp1161": { + "type": "_sbcs", + "chars": "��������������������������������่กขฃคฅฆงจฉชซฌญฎฏฐฑฒณดตถทธนบปผฝพฟภมยรฤลฦวศษสหฬอฮฯะัาำิีึืฺุู้๊๋€฿เแโใไๅๆ็่้๊๋์ํ๎๏๐๑๒๓๔๕๖๗๘๙๚๛¢¬¦ " + }, + "ibm1161": "cp1161", + "csibm1161": "cp1161", + "cp1162": { + "type": "_sbcs", + "chars": "€‚ƒ„…†‡ˆ‰Š‹ŒŽ‘’“”•–—˜™š›œžŸ กขฃคฅฆงจฉชซฌญฎฏฐฑฒณดตถทธนบปผฝพฟภมยรฤลฦวศษสหฬอฮฯะัาำิีึืฺุู����฿เแโใไๅๆ็่้๊๋์ํ๎๏๐๑๒๓๔๕๖๗๘๙๚๛����" + }, + "ibm1162": "cp1162", + "csibm1162": "cp1162", + "cp1163": { + "type": "_sbcs", + "chars": "€‚ƒ„…†‡ˆ‰Š‹ŒŽ‘’“”•–—˜™š›œžŸ ¡¢£€¥¦§œ©ª«¬­®¯°±²³Ÿµ¶·Œ¹º»¼½¾¿ÀÁÂĂÄÅÆÇÈÉÊË̀ÍÎÏĐÑ̉ÓÔƠÖ×ØÙÚÛÜỮßàáâăäåæçèéêë́íîïđṇ̃óôơö÷øùúûüư₫ÿ" + }, + "ibm1163": "cp1163", + "csibm1163": "cp1163", + "maccroatian": { + "type": "_sbcs", + "chars": "ÄÅÇÉÑÖÜáàâäãåçéèêëíìîïñóòôöõúùûü†°¢£§•¶ß®Š™´¨≠ŽØ∞±≤≥∆µ∂∑∏š∫ªºΩžø¿¡¬√ƒ≈Ć«Č… ÀÃÕŒœĐ—“”‘’÷◊�©⁄¤‹›Æ»–·‚„‰ÂćÁčÈÍÎÏÌÓÔđÒÚÛÙıˆ˜¯πË˚¸Êæˇ" + }, + "maccyrillic": { + "type": "_sbcs", + "chars": "АБВГДЕЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯ†°¢£§•¶І®©™Ђђ≠Ѓѓ∞±≤≥іµ∂ЈЄєЇїЉљЊњјЅ¬√ƒ≈∆«»… ЋћЌќѕ–—“”‘’÷„ЎўЏџ№Ёёяабвгдежзийклмнопрстуфхцчшщъыьэю¤" + }, + "macgreek": { + "type": "_sbcs", + "chars": "Ĺ²É³ÖÜ΅àâä΄¨çéèê룙î‰ôö¦­ùûü†ΓΔΘΛΞΠß®©ΣΪ§≠°·Α±≤≥¥ΒΕΖΗΙΚΜΦΫΨΩάΝ¬ΟΡ≈Τ«»… ΥΧΆΈœ–―“”‘’÷ΉΊΌΎέήίόΏύαβψδεφγηιξκλμνοπώρστθωςχυζϊϋΐΰ�" + }, + "maciceland": { + "type": "_sbcs", + "chars": "ÄÅÇÉÑÖÜáàâäãåçéèêëíìîïñóòôöõúùûüÝ°¢£§•¶ß®©™´¨≠ÆØ∞±≤≥¥µ∂∑∏π∫ªºΩæø¿¡¬√ƒ≈∆«»… ÀÃÕŒœ–—“”‘’÷◊ÿŸ⁄¤ÐðÞþý·‚„‰ÂÊÁËÈÍÎÏÌÓÔ�ÒÚÛÙıˆ˜¯˘˙˚¸˝˛ˇ" + }, + "macroman": { + "type": "_sbcs", + "chars": "ÄÅÇÉÑÖÜáàâäãåçéèêëíìîïñóòôöõúùûü†°¢£§•¶ß®©™´¨≠ÆØ∞±≤≥¥µ∂∑∏π∫ªºΩæø¿¡¬√ƒ≈∆«»… ÀÃÕŒœ–—“”‘’÷◊ÿŸ⁄¤‹›fifl‡·‚„‰ÂÊÁËÈÍÎÏÌÓÔ�ÒÚÛÙıˆ˜¯˘˙˚¸˝˛ˇ" + }, + "macromania": { + "type": "_sbcs", + "chars": "ÄÅÇÉÑÖÜáàâäãåçéèêëíìîïñóòôöõúùûü†°¢£§•¶ß®©™´¨≠ĂŞ∞±≤≥¥µ∂∑∏π∫ªºΩăş¿¡¬√ƒ≈∆«»… ÀÃÕŒœ–—“”‘’÷◊ÿŸ⁄¤‹›Ţţ‡·‚„‰ÂÊÁËÈÍÎÏÌÓÔ�ÒÚÛÙıˆ˜¯˘˙˚¸˝˛ˇ" + }, + "macthai": { + "type": "_sbcs", + "chars": "«»…“”�•‘’� กขฃคฅฆงจฉชซฌญฎฏฐฑฒณดตถทธนบปผฝพฟภมยรฤลฦวศษสหฬอฮฯะัาำิีึืฺุู​–—฿เแโใไๅๆ็่้๊๋์ํ™๏๐๑๒๓๔๕๖๗๘๙®©����" + }, + "macturkish": { + "type": "_sbcs", + "chars": "ÄÅÇÉÑÖÜáàâäãåçéèêëíìîïñóòôöõúùûü†°¢£§•¶ß®©™´¨≠ÆØ∞±≤≥¥µ∂∑∏π∫ªºΩæø¿¡¬√ƒ≈∆«»… ÀÃÕŒœ–—“”‘’÷◊ÿŸĞğİıŞş‡·‚„‰ÂÊÁËÈÍÎÏÌÓÔ�ÒÚÛÙ�ˆ˜¯˘˙˚¸˝˛ˇ" + }, + "macukraine": { + "type": "_sbcs", + "chars": "АБВГДЕЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯ†°Ґ£§•¶І®©™Ђђ≠Ѓѓ∞±≤≥іµґЈЄєЇїЉљЊњјЅ¬√ƒ≈∆«»… ЋћЌќѕ–—“”‘’÷„ЎўЏџ№Ёёяабвгдежзийклмнопрстуфхцчшщъыьэю¤" + }, + "koi8r": { + "type": "_sbcs", + "chars": "─│┌┐└┘├┤┬┴┼▀▄█▌▐░▒▓⌠■∙√≈≤≥ ⌡°²·÷═║╒ё╓╔╕╖╗╘╙╚╛╜╝╞╟╠╡Ё╢╣╤╥╦╧╨╩╪╫╬©юабцдефгхийклмнопярстужвьызшэщчъЮАБЦДЕФГХИЙКЛМНОПЯРСТУЖВЬЫЗШЭЩЧЪ" + }, + "koi8u": { + "type": "_sbcs", + "chars": "─│┌┐└┘├┤┬┴┼▀▄█▌▐░▒▓⌠■∙√≈≤≥ ⌡°²·÷═║╒ёє╔ії╗╘╙╚╛ґ╝╞╟╠╡ЁЄ╣ІЇ╦╧╨╩╪Ґ╬©юабцдефгхийклмнопярстужвьызшэщчъЮАБЦДЕФГХИЙКЛМНОПЯРСТУЖВЬЫЗШЭЩЧЪ" + }, + "koi8ru": { + "type": "_sbcs", + "chars": "─│┌┐└┘├┤┬┴┼▀▄█▌▐░▒▓⌠■∙√≈≤≥ ⌡°²·÷═║╒ёє╔ії╗╘╙╚╛ґў╞╟╠╡ЁЄ╣ІЇ╦╧╨╩╪ҐЎ©юабцдефгхийклмнопярстужвьызшэщчъЮАБЦДЕФГХИЙКЛМНОПЯРСТУЖВЬЫЗШЭЩЧЪ" + }, + "koi8t": { + "type": "_sbcs", + "chars": "қғ‚Ғ„…†‡�‰ҳ‹ҲҷҶ�Қ‘’“”•–—�™�›�����ӯӮё¤ӣ¦§���«¬­®�°±²Ё�Ӣ¶·�№�»���©юабцдефгхийклмнопярстужвьызшэщчъЮАБЦДЕФГХИЙКЛМНОПЯРСТУЖВЬЫЗШЭЩЧЪ" + }, + "armscii8": { + "type": "_sbcs", + "chars": "€‚ƒ„…†‡ˆ‰Š‹ŒŽ‘’“”•–—˜™š›œžŸ �և։)(»«—.՝,-֊…՜՛՞ԱաԲբԳգԴդԵեԶզԷէԸըԹթԺժԻիԼլԽխԾծԿկՀհՁձՂղՃճՄմՅյՆնՇշՈոՉչՊպՋջՌռՍսՎվՏտՐրՑցՒւՓփՔքՕօՖֆ՚�" + }, + "rk1048": { + "type": "_sbcs", + "chars": "ЂЃ‚ѓ„…†‡€‰Љ‹ЊҚҺЏђ‘’“”•–—�™љ›њқһџ ҰұӘ¤Ө¦§Ё©Ғ«¬­®Ү°±Ііөµ¶·ё№ғ»әҢңүАБВГДЕЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯабвгдежзийклмнопрстуфхцчшщъыьэюя" + }, + "tcvn": { + "type": "_sbcs", + "chars": "\u0000ÚỤ\u0003ỪỬỮ\u0007\b\t\n\u000b\f\r\u000e\u000f\u0010ỨỰỲỶỸÝỴ\u0018\u0019\u001a\u001b\u001c\u001d\u001e\u001f !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~ÀẢÃÁẠẶẬÈẺẼÉẸỆÌỈĨÍỊÒỎÕÓỌỘỜỞỠỚỢÙỦŨ ĂÂÊÔƠƯĐăâêôơưđẶ̀̀̉̃́àảãáạẲằẳẵắẴẮẦẨẪẤỀặầẩẫấậèỂẻẽéẹềểễếệìỉỄẾỒĩíịòỔỏõóọồổỗốộờởỡớợùỖủũúụừửữứựỳỷỹýỵỐ" + }, + "georgianacademy": { + "type": "_sbcs", + "chars": "€‚ƒ„…†‡ˆ‰Š‹ŒŽ‘’“”•–—˜™š›œžŸ ¡¢£¤¥¦§¨©ª«¬­®¯°±²³´µ¶·¸¹º»¼½¾¿აბგდევზთიკლმნოპჟრსტუფქღყშჩცძწჭხჯჰჱჲჳჴჵჶçèéêëìíîïðñòóôõö÷øùúûüýþÿ" + }, + "georgianps": { + "type": "_sbcs", + "chars": "€‚ƒ„…†‡ˆ‰Š‹ŒŽ‘’“”•–—˜™š›œžŸ ¡¢£¤¥¦§¨©ª«¬­®¯°±²³´µ¶·¸¹º»¼½¾¿აბგდევზჱთიკლმნჲოპჟრსტჳუფქღყშჩცძწჭხჴჯჰჵæçèéêëìíîïðñòóôõö÷øùúûüýþÿ" + }, + "pt154": { + "type": "_sbcs", + "chars": "ҖҒӮғ„…ҶҮҲүҠӢҢҚҺҸҗ‘’“”•–—ҳҷҡӣңқһҹ ЎўЈӨҘҰ§Ё©Ә«¬ӯ®Ҝ°ұІіҙө¶·ё№ә»јҪҫҝАБВГДЕЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯабвгдежзийклмнопрстуфхцчшщъыьэюя" + }, + "viscii": { + "type": "_sbcs", + "chars": "\u0000\u0001Ẳ\u0003\u0004ẴẪ\u0007\b\t\n\u000b\f\r\u000e\u000f\u0010\u0011\u0012\u0013Ỷ\u0015\u0016\u0017\u0018Ỹ\u001a\u001b\u001c\u001dỴ\u001f !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~ẠẮẰẶẤẦẨẬẼẸẾỀỂỄỆỐỒỔỖỘỢỚỜỞỊỎỌỈỦŨỤỲÕắằặấầẩậẽẹếềểễệốồổỗỠƠộờởịỰỨỪỬơớƯÀÁÂÃẢĂẳẵÈÉÊẺÌÍĨỳĐứÒÓÔạỷừửÙÚỹỵÝỡưàáâãảăữẫèéêẻìíĩỉđựòóôõỏọụùúũủýợỮ" + }, + "iso646cn": { + "type": "_sbcs", + "chars": "\u0000\u0001\u0002\u0003\u0004\u0005\u0006\u0007\b\t\n\u000b\f\r\u000e\u000f\u0010\u0011\u0012\u0013\u0014\u0015\u0016\u0017\u0018\u0019\u001a\u001b\u001c\u001d\u001e\u001f !\"#¥%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}‾��������������������������������������������������������������������������������������������������������������������������������" + }, + "iso646jp": { + "type": "_sbcs", + "chars": "\u0000\u0001\u0002\u0003\u0004\u0005\u0006\u0007\b\t\n\u000b\f\r\u000e\u000f\u0010\u0011\u0012\u0013\u0014\u0015\u0016\u0017\u0018\u0019\u001a\u001b\u001c\u001d\u001e\u001f !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[¥]^_`abcdefghijklmnopqrstuvwxyz{|}‾��������������������������������������������������������������������������������������������������������������������������������" + }, + "hproman8": { + "type": "_sbcs", + "chars": "€‚ƒ„…†‡ˆ‰Š‹ŒŽ‘’“”•–—˜™š›œžŸ ÀÂÈÊËÎÏ´ˋˆ¨˜ÙÛ₤¯Ýý°ÇçÑñ¡¿¤£¥§ƒ¢âêôûáéóúàèòùäëöüÅîØÆåíøæÄìÖÜÉïßÔÁÃãÐðÍÌÓÒÕõŠšÚŸÿÞþ·µ¶¾—¼½ªº«■»±�" + }, + "macintosh": { + "type": "_sbcs", + "chars": "ÄÅÇÉÑÖÜáàâäãåçéèêëíìîïñóòôöõúùûü†°¢£§•¶ß®©™´¨≠ÆØ∞±≤≥¥µ∂∑∏π∫ªºΩæø¿¡¬√ƒ≈∆«»… ÀÃÕŒœ–—“”‘’÷◊ÿŸ⁄¤‹›fifl‡·‚„‰ÂÊÁËÈÍÎÏÌÓÔ�ÒÚÛÙıˆ˜¯˘˙˚¸˝˛ˇ" + }, + "ascii": { + "type": "_sbcs", + "chars": "��������������������������������������������������������������������������������������������������������������������������������" + }, + "tis620": { + "type": "_sbcs", + "chars": "���������������������������������กขฃคฅฆงจฉชซฌญฎฏฐฑฒณดตถทธนบปผฝพฟภมยรฤลฦวศษสหฬอฮฯะัาำิีึืฺุู����฿เแโใไๅๆ็่้๊๋์ํ๎๏๐๑๒๓๔๕๖๗๘๙๚๛����" + } +} + +/***/ }), + +/***/ 4818: +/***/ ((module) => { + +"use strict"; + + +// Manually added data to be used by sbcs codec in addition to generated one. + +module.exports = { + // Not supported by iconv, not sure why. + "10029": "maccenteuro", + "maccenteuro": { + "type": "_sbcs", + "chars": "ÄĀāÉĄÖÜáąČäčĆć鏟ĎíďĒēĖóėôöõúĚěü†°Ę£§•¶ß®©™ę¨≠ģĮįĪ≤≥īĶ∂∑łĻļĽľĹĺŅņѬ√ńŇ∆«»… ňŐÕőŌ–—“”‘’÷◊ōŔŕŘ‹›řŖŗŠ‚„šŚśÁŤťÍŽžŪÓÔūŮÚůŰűŲųÝýķŻŁżĢˇ" + }, + + "808": "cp808", + "ibm808": "cp808", + "cp808": { + "type": "_sbcs", + "chars": "АБВГДЕЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯабвгдежзийклмноп░▒▓│┤╡╢╖╕╣║╗╝╜╛┐└┴┬├─┼╞╟╚╔╩╦╠═╬╧╨╤╥╙╘╒╓╫╪┘┌█▄▌▐▀рстуфхцчшщъыьэюяЁёЄєЇїЎў°∙·√№€■ " + }, + + "mik": { + "type": "_sbcs", + "chars": "АБВГДЕЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯабвгдежзийклмнопрстуфхцчшщъыьэюя└┴┬├─┼╣║╚╔╩╦╠═╬┐░▒▓│┤№§╗╝┘┌█▄▌▐▀αßΓπΣσµτΦΘΩδ∞φε∩≡±≥≤⌠⌡÷≈°∙·√ⁿ²■ " + }, + + "cp720": { + "type": "_sbcs", + "chars": "\x80\x81éâ\x84à\x86çêëèïî\x8d\x8e\x8f\x90\u0651\u0652ô¤ـûùءآأؤ£إئابةتثجحخدذرزسشص«»░▒▓│┤╡╢╖╕╣║╗╝╜╛┐└┴┬├─┼╞╟╚╔╩╦╠═╬╧╨╤╥╙╘╒╓╫╪┘┌█▄▌▐▀ضطظعغفµقكلمنهوىي≡\u064b\u064c\u064d\u064e\u064f\u0650≈°∙·√ⁿ²■\u00a0" + }, + + // Aliases of generated encodings. + "ascii8bit": "ascii", + "usascii": "ascii", + "ansix34": "ascii", + "ansix341968": "ascii", + "ansix341986": "ascii", + "csascii": "ascii", + "cp367": "ascii", + "ibm367": "ascii", + "isoir6": "ascii", + "iso646us": "ascii", + "iso646irv": "ascii", + "us": "ascii", + + "latin1": "iso88591", + "latin2": "iso88592", + "latin3": "iso88593", + "latin4": "iso88594", + "latin5": "iso88599", + "latin6": "iso885910", + "latin7": "iso885913", + "latin8": "iso885914", + "latin9": "iso885915", + "latin10": "iso885916", + + "csisolatin1": "iso88591", + "csisolatin2": "iso88592", + "csisolatin3": "iso88593", + "csisolatin4": "iso88594", + "csisolatincyrillic": "iso88595", + "csisolatinarabic": "iso88596", + "csisolatingreek" : "iso88597", + "csisolatinhebrew": "iso88598", + "csisolatin5": "iso88599", + "csisolatin6": "iso885910", + + "l1": "iso88591", + "l2": "iso88592", + "l3": "iso88593", + "l4": "iso88594", + "l5": "iso88599", + "l6": "iso885910", + "l7": "iso885913", + "l8": "iso885914", + "l9": "iso885915", + "l10": "iso885916", + + "isoir14": "iso646jp", + "isoir57": "iso646cn", + "isoir100": "iso88591", + "isoir101": "iso88592", + "isoir109": "iso88593", + "isoir110": "iso88594", + "isoir144": "iso88595", + "isoir127": "iso88596", + "isoir126": "iso88597", + "isoir138": "iso88598", + "isoir148": "iso88599", + "isoir157": "iso885910", + "isoir166": "tis620", + "isoir179": "iso885913", + "isoir199": "iso885914", + "isoir203": "iso885915", + "isoir226": "iso885916", + + "cp819": "iso88591", + "ibm819": "iso88591", + + "cyrillic": "iso88595", + + "arabic": "iso88596", + "arabic8": "iso88596", + "ecma114": "iso88596", + "asmo708": "iso88596", + + "greek" : "iso88597", + "greek8" : "iso88597", + "ecma118" : "iso88597", + "elot928" : "iso88597", + + "hebrew": "iso88598", + "hebrew8": "iso88598", + + "turkish": "iso88599", + "turkish8": "iso88599", + + "thai": "iso885911", + "thai8": "iso885911", + + "celtic": "iso885914", + "celtic8": "iso885914", + "isoceltic": "iso885914", + + "tis6200": "tis620", + "tis62025291": "tis620", + "tis62025330": "tis620", + + "10000": "macroman", + "10006": "macgreek", + "10007": "maccyrillic", + "10079": "maciceland", + "10081": "macturkish", + + "cspc8codepage437": "cp437", + "cspc775baltic": "cp775", + "cspc850multilingual": "cp850", + "cspcp852": "cp852", + "cspc862latinhebrew": "cp862", + "cpgr": "cp869", + + "msee": "cp1250", + "mscyrl": "cp1251", + "msansi": "cp1252", + "msgreek": "cp1253", + "msturk": "cp1254", + "mshebr": "cp1255", + "msarab": "cp1256", + "winbaltrim": "cp1257", + + "cp20866": "koi8r", + "20866": "koi8r", + "ibm878": "koi8r", + "cskoi8r": "koi8r", + + "cp21866": "koi8u", + "21866": "koi8u", + "ibm1168": "koi8u", + + "strk10482002": "rk1048", + + "tcvn5712": "tcvn", + "tcvn57121": "tcvn", + + "gb198880": "iso646cn", + "cn": "iso646cn", + + "csiso14jisc6220ro": "iso646jp", + "jisc62201969ro": "iso646jp", + "jp": "iso646jp", + + "cshproman8": "hproman8", + "r8": "hproman8", + "roman8": "hproman8", + "xroman8": "hproman8", + "ibm1051": "hproman8", + + "mac": "macintosh", + "csmacintosh": "macintosh", +}; + + + +/***/ }), + +/***/ 2402: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +var Buffer = (__nccwpck_require__(4199).Buffer); + +// Note: UTF16-LE (or UCS2) codec is Node.js native. See encodings/internal.js + +// == UTF16-BE codec. ========================================================== + +exports.utf16be = Utf16BECodec; +function Utf16BECodec() { +} + +Utf16BECodec.prototype.encoder = Utf16BEEncoder; +Utf16BECodec.prototype.decoder = Utf16BEDecoder; +Utf16BECodec.prototype.bomAware = true; + + +// -- Encoding + +function Utf16BEEncoder() { +} + +Utf16BEEncoder.prototype.write = function(str) { + var buf = Buffer.from(str, 'ucs2'); + for (var i = 0; i < buf.length; i += 2) { + var tmp = buf[i]; buf[i] = buf[i+1]; buf[i+1] = tmp; + } + return buf; +} + +Utf16BEEncoder.prototype.end = function() { +} + + +// -- Decoding + +function Utf16BEDecoder() { + this.overflowByte = -1; +} + +Utf16BEDecoder.prototype.write = function(buf) { + if (buf.length == 0) + return ''; + + var buf2 = Buffer.alloc(buf.length + 1), + i = 0, j = 0; + + if (this.overflowByte !== -1) { + buf2[0] = buf[0]; + buf2[1] = this.overflowByte; + i = 1; j = 2; + } + + for (; i < buf.length-1; i += 2, j+= 2) { + buf2[j] = buf[i+1]; + buf2[j+1] = buf[i]; + } + + this.overflowByte = (i == buf.length-1) ? buf[buf.length-1] : -1; + + return buf2.slice(0, j).toString('ucs2'); +} + +Utf16BEDecoder.prototype.end = function() { + this.overflowByte = -1; +} + + +// == UTF-16 codec ============================================================= +// Decoder chooses automatically from UTF-16LE and UTF-16BE using BOM and space-based heuristic. +// Defaults to UTF-16LE, as it's prevalent and default in Node. +// http://en.wikipedia.org/wiki/UTF-16 and http://encoding.spec.whatwg.org/#utf-16le +// Decoder default can be changed: iconv.decode(buf, 'utf16', {defaultEncoding: 'utf-16be'}); + +// Encoder uses UTF-16LE and prepends BOM (which can be overridden with addBOM: false). + +exports.utf16 = Utf16Codec; +function Utf16Codec(codecOptions, iconv) { + this.iconv = iconv; +} + +Utf16Codec.prototype.encoder = Utf16Encoder; +Utf16Codec.prototype.decoder = Utf16Decoder; + + +// -- Encoding (pass-through) + +function Utf16Encoder(options, codec) { + options = options || {}; + if (options.addBOM === undefined) + options.addBOM = true; + this.encoder = codec.iconv.getEncoder('utf-16le', options); +} + +Utf16Encoder.prototype.write = function(str) { + return this.encoder.write(str); +} + +Utf16Encoder.prototype.end = function() { + return this.encoder.end(); +} + + +// -- Decoding + +function Utf16Decoder(options, codec) { + this.decoder = null; + this.initialBufs = []; + this.initialBufsLen = 0; + + this.options = options || {}; + this.iconv = codec.iconv; +} + +Utf16Decoder.prototype.write = function(buf) { + if (!this.decoder) { + // Codec is not chosen yet. Accumulate initial bytes. + this.initialBufs.push(buf); + this.initialBufsLen += buf.length; + + if (this.initialBufsLen < 16) // We need more bytes to use space heuristic (see below) + return ''; + + // We have enough bytes -> detect endianness. + var encoding = detectEncoding(this.initialBufs, this.options.defaultEncoding); + this.decoder = this.iconv.getDecoder(encoding, this.options); + + var resStr = ''; + for (var i = 0; i < this.initialBufs.length; i++) + resStr += this.decoder.write(this.initialBufs[i]); + + this.initialBufs.length = this.initialBufsLen = 0; + return resStr; + } + + return this.decoder.write(buf); +} + +Utf16Decoder.prototype.end = function() { + if (!this.decoder) { + var encoding = detectEncoding(this.initialBufs, this.options.defaultEncoding); + this.decoder = this.iconv.getDecoder(encoding, this.options); + + var resStr = ''; + for (var i = 0; i < this.initialBufs.length; i++) + resStr += this.decoder.write(this.initialBufs[i]); + + var trail = this.decoder.end(); + if (trail) + resStr += trail; + + this.initialBufs.length = this.initialBufsLen = 0; + return resStr; + } + return this.decoder.end(); +} + +function detectEncoding(bufs, defaultEncoding) { + var b = []; + var charsProcessed = 0; + var asciiCharsLE = 0, asciiCharsBE = 0; // Number of ASCII chars when decoded as LE or BE. + + outer_loop: + for (var i = 0; i < bufs.length; i++) { + var buf = bufs[i]; + for (var j = 0; j < buf.length; j++) { + b.push(buf[j]); + if (b.length === 2) { + if (charsProcessed === 0) { + // Check BOM first. + if (b[0] === 0xFF && b[1] === 0xFE) return 'utf-16le'; + if (b[0] === 0xFE && b[1] === 0xFF) return 'utf-16be'; + } + + if (b[0] === 0 && b[1] !== 0) asciiCharsBE++; + if (b[0] !== 0 && b[1] === 0) asciiCharsLE++; + + b.length = 0; + charsProcessed++; + + if (charsProcessed >= 100) { + break outer_loop; + } + } + } + } + + // Make decisions. + // Most of the time, the content has ASCII chars (U+00**), but the opposite (U+**00) is uncommon. + // So, we count ASCII as if it was LE or BE, and decide from that. + if (asciiCharsBE > asciiCharsLE) return 'utf-16be'; + if (asciiCharsBE < asciiCharsLE) return 'utf-16le'; + + // Couldn't decide (likely all zeros or not enough data). + return defaultEncoding || 'utf-16le'; +} + + + + +/***/ }), + +/***/ 8015: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + + +var Buffer = (__nccwpck_require__(4199).Buffer); + +// == UTF32-LE/BE codec. ========================================================== + +exports._utf32 = Utf32Codec; + +function Utf32Codec(codecOptions, iconv) { + this.iconv = iconv; + this.bomAware = true; + this.isLE = codecOptions.isLE; +} + +exports.utf32le = { type: '_utf32', isLE: true }; +exports.utf32be = { type: '_utf32', isLE: false }; + +// Aliases +exports.ucs4le = 'utf32le'; +exports.ucs4be = 'utf32be'; + +Utf32Codec.prototype.encoder = Utf32Encoder; +Utf32Codec.prototype.decoder = Utf32Decoder; + +// -- Encoding + +function Utf32Encoder(options, codec) { + this.isLE = codec.isLE; + this.highSurrogate = 0; +} + +Utf32Encoder.prototype.write = function(str) { + var src = Buffer.from(str, 'ucs2'); + var dst = Buffer.alloc(src.length * 2); + var write32 = this.isLE ? dst.writeUInt32LE : dst.writeUInt32BE; + var offset = 0; + + for (var i = 0; i < src.length; i += 2) { + var code = src.readUInt16LE(i); + var isHighSurrogate = (0xD800 <= code && code < 0xDC00); + var isLowSurrogate = (0xDC00 <= code && code < 0xE000); + + if (this.highSurrogate) { + if (isHighSurrogate || !isLowSurrogate) { + // There shouldn't be two high surrogates in a row, nor a high surrogate which isn't followed by a low + // surrogate. If this happens, keep the pending high surrogate as a stand-alone semi-invalid character + // (technically wrong, but expected by some applications, like Windows file names). + write32.call(dst, this.highSurrogate, offset); + offset += 4; + } + else { + // Create 32-bit value from high and low surrogates; + var codepoint = (((this.highSurrogate - 0xD800) << 10) | (code - 0xDC00)) + 0x10000; + + write32.call(dst, codepoint, offset); + offset += 4; + this.highSurrogate = 0; + + continue; + } + } + + if (isHighSurrogate) + this.highSurrogate = code; + else { + // Even if the current character is a low surrogate, with no previous high surrogate, we'll + // encode it as a semi-invalid stand-alone character for the same reasons expressed above for + // unpaired high surrogates. + write32.call(dst, code, offset); + offset += 4; + this.highSurrogate = 0; + } + } + + if (offset < dst.length) + dst = dst.slice(0, offset); + + return dst; +}; + +Utf32Encoder.prototype.end = function() { + // Treat any leftover high surrogate as a semi-valid independent character. + if (!this.highSurrogate) + return; + + var buf = Buffer.alloc(4); + + if (this.isLE) + buf.writeUInt32LE(this.highSurrogate, 0); + else + buf.writeUInt32BE(this.highSurrogate, 0); + + this.highSurrogate = 0; + + return buf; +}; + +// -- Decoding + +function Utf32Decoder(options, codec) { + this.isLE = codec.isLE; + this.badChar = codec.iconv.defaultCharUnicode.charCodeAt(0); + this.overflow = []; +} + +Utf32Decoder.prototype.write = function(src) { + if (src.length === 0) + return ''; + + var i = 0; + var codepoint = 0; + var dst = Buffer.alloc(src.length + 4); + var offset = 0; + var isLE = this.isLE; + var overflow = this.overflow; + var badChar = this.badChar; + + if (overflow.length > 0) { + for (; i < src.length && overflow.length < 4; i++) + overflow.push(src[i]); + + if (overflow.length === 4) { + // NOTE: codepoint is a signed int32 and can be negative. + // NOTE: We copied this block from below to help V8 optimize it (it works with array, not buffer). + if (isLE) { + codepoint = overflow[i] | (overflow[i+1] << 8) | (overflow[i+2] << 16) | (overflow[i+3] << 24); + } else { + codepoint = overflow[i+3] | (overflow[i+2] << 8) | (overflow[i+1] << 16) | (overflow[i] << 24); + } + overflow.length = 0; + + offset = _writeCodepoint(dst, offset, codepoint, badChar); + } + } + + // Main loop. Should be as optimized as possible. + for (; i < src.length - 3; i += 4) { + // NOTE: codepoint is a signed int32 and can be negative. + if (isLE) { + codepoint = src[i] | (src[i+1] << 8) | (src[i+2] << 16) | (src[i+3] << 24); + } else { + codepoint = src[i+3] | (src[i+2] << 8) | (src[i+1] << 16) | (src[i] << 24); + } + offset = _writeCodepoint(dst, offset, codepoint, badChar); + } + + // Keep overflowing bytes. + for (; i < src.length; i++) { + overflow.push(src[i]); + } + + return dst.slice(0, offset).toString('ucs2'); +}; + +function _writeCodepoint(dst, offset, codepoint, badChar) { + // NOTE: codepoint is signed int32 and can be negative. We keep it that way to help V8 with optimizations. + if (codepoint < 0 || codepoint > 0x10FFFF) { + // Not a valid Unicode codepoint + codepoint = badChar; + } + + // Ephemeral Planes: Write high surrogate. + if (codepoint >= 0x10000) { + codepoint -= 0x10000; + + var high = 0xD800 | (codepoint >> 10); + dst[offset++] = high & 0xff; + dst[offset++] = high >> 8; + + // Low surrogate is written below. + var codepoint = 0xDC00 | (codepoint & 0x3FF); + } + + // Write BMP char or low surrogate. + dst[offset++] = codepoint & 0xff; + dst[offset++] = codepoint >> 8; + + return offset; +}; + +Utf32Decoder.prototype.end = function() { + this.overflow.length = 0; +}; + +// == UTF-32 Auto codec ============================================================= +// Decoder chooses automatically from UTF-32LE and UTF-32BE using BOM and space-based heuristic. +// Defaults to UTF-32LE. http://en.wikipedia.org/wiki/UTF-32 +// Encoder/decoder default can be changed: iconv.decode(buf, 'utf32', {defaultEncoding: 'utf-32be'}); + +// Encoder prepends BOM (which can be overridden with (addBOM: false}). + +exports.utf32 = Utf32AutoCodec; +exports.ucs4 = 'utf32'; + +function Utf32AutoCodec(options, iconv) { + this.iconv = iconv; +} + +Utf32AutoCodec.prototype.encoder = Utf32AutoEncoder; +Utf32AutoCodec.prototype.decoder = Utf32AutoDecoder; + +// -- Encoding + +function Utf32AutoEncoder(options, codec) { + options = options || {}; + + if (options.addBOM === undefined) + options.addBOM = true; + + this.encoder = codec.iconv.getEncoder(options.defaultEncoding || 'utf-32le', options); +} + +Utf32AutoEncoder.prototype.write = function(str) { + return this.encoder.write(str); +}; + +Utf32AutoEncoder.prototype.end = function() { + return this.encoder.end(); +}; + +// -- Decoding + +function Utf32AutoDecoder(options, codec) { + this.decoder = null; + this.initialBufs = []; + this.initialBufsLen = 0; + this.options = options || {}; + this.iconv = codec.iconv; +} + +Utf32AutoDecoder.prototype.write = function(buf) { + if (!this.decoder) { + // Codec is not chosen yet. Accumulate initial bytes. + this.initialBufs.push(buf); + this.initialBufsLen += buf.length; + + if (this.initialBufsLen < 32) // We need more bytes to use space heuristic (see below) + return ''; + + // We have enough bytes -> detect endianness. + var encoding = detectEncoding(this.initialBufs, this.options.defaultEncoding); + this.decoder = this.iconv.getDecoder(encoding, this.options); + + var resStr = ''; + for (var i = 0; i < this.initialBufs.length; i++) + resStr += this.decoder.write(this.initialBufs[i]); + + this.initialBufs.length = this.initialBufsLen = 0; + return resStr; + } + + return this.decoder.write(buf); +}; + +Utf32AutoDecoder.prototype.end = function() { + if (!this.decoder) { + var encoding = detectEncoding(this.initialBufs, this.options.defaultEncoding); + this.decoder = this.iconv.getDecoder(encoding, this.options); + + var resStr = ''; + for (var i = 0; i < this.initialBufs.length; i++) + resStr += this.decoder.write(this.initialBufs[i]); + + var trail = this.decoder.end(); + if (trail) + resStr += trail; + + this.initialBufs.length = this.initialBufsLen = 0; + return resStr; + } + + return this.decoder.end(); +}; + +function detectEncoding(bufs, defaultEncoding) { + var b = []; + var charsProcessed = 0; + var invalidLE = 0, invalidBE = 0; // Number of invalid chars when decoded as LE or BE. + var bmpCharsLE = 0, bmpCharsBE = 0; // Number of BMP chars when decoded as LE or BE. + + outer_loop: + for (var i = 0; i < bufs.length; i++) { + var buf = bufs[i]; + for (var j = 0; j < buf.length; j++) { + b.push(buf[j]); + if (b.length === 4) { + if (charsProcessed === 0) { + // Check BOM first. + if (b[0] === 0xFF && b[1] === 0xFE && b[2] === 0 && b[3] === 0) { + return 'utf-32le'; + } + if (b[0] === 0 && b[1] === 0 && b[2] === 0xFE && b[3] === 0xFF) { + return 'utf-32be'; + } + } + + if (b[0] !== 0 || b[1] > 0x10) invalidBE++; + if (b[3] !== 0 || b[2] > 0x10) invalidLE++; + + if (b[0] === 0 && b[1] === 0 && (b[2] !== 0 || b[3] !== 0)) bmpCharsBE++; + if ((b[0] !== 0 || b[1] !== 0) && b[2] === 0 && b[3] === 0) bmpCharsLE++; + + b.length = 0; + charsProcessed++; + + if (charsProcessed >= 100) { + break outer_loop; + } + } + } + } + + // Make decisions. + if (bmpCharsBE - invalidBE > bmpCharsLE - invalidLE) return 'utf-32be'; + if (bmpCharsBE - invalidBE < bmpCharsLE - invalidLE) return 'utf-32le'; + + // Couldn't decide (likely all zeros or not enough data). + return defaultEncoding || 'utf-32le'; +} + + +/***/ }), + +/***/ 3152: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +var Buffer = (__nccwpck_require__(4199).Buffer); + +// UTF-7 codec, according to https://tools.ietf.org/html/rfc2152 +// See also below a UTF-7-IMAP codec, according to http://tools.ietf.org/html/rfc3501#section-5.1.3 + +exports.utf7 = Utf7Codec; +exports.unicode11utf7 = 'utf7'; // Alias UNICODE-1-1-UTF-7 +function Utf7Codec(codecOptions, iconv) { + this.iconv = iconv; +}; + +Utf7Codec.prototype.encoder = Utf7Encoder; +Utf7Codec.prototype.decoder = Utf7Decoder; +Utf7Codec.prototype.bomAware = true; + + +// -- Encoding + +var nonDirectChars = /[^A-Za-z0-9'\(\),-\.\/:\? \n\r\t]+/g; + +function Utf7Encoder(options, codec) { + this.iconv = codec.iconv; +} + +Utf7Encoder.prototype.write = function(str) { + // Naive implementation. + // Non-direct chars are encoded as "+-"; single "+" char is encoded as "+-". + return Buffer.from(str.replace(nonDirectChars, function(chunk) { + return "+" + (chunk === '+' ? '' : + this.iconv.encode(chunk, 'utf16-be').toString('base64').replace(/=+$/, '')) + + "-"; + }.bind(this))); +} + +Utf7Encoder.prototype.end = function() { +} + + +// -- Decoding + +function Utf7Decoder(options, codec) { + this.iconv = codec.iconv; + this.inBase64 = false; + this.base64Accum = ''; +} + +var base64Regex = /[A-Za-z0-9\/+]/; +var base64Chars = []; +for (var i = 0; i < 256; i++) + base64Chars[i] = base64Regex.test(String.fromCharCode(i)); + +var plusChar = '+'.charCodeAt(0), + minusChar = '-'.charCodeAt(0), + andChar = '&'.charCodeAt(0); + +Utf7Decoder.prototype.write = function(buf) { + var res = "", lastI = 0, + inBase64 = this.inBase64, + base64Accum = this.base64Accum; + + // The decoder is more involved as we must handle chunks in stream. + + for (var i = 0; i < buf.length; i++) { + if (!inBase64) { // We're in direct mode. + // Write direct chars until '+' + if (buf[i] == plusChar) { + res += this.iconv.decode(buf.slice(lastI, i), "ascii"); // Write direct chars. + lastI = i+1; + inBase64 = true; + } + } else { // We decode base64. + if (!base64Chars[buf[i]]) { // Base64 ended. + if (i == lastI && buf[i] == minusChar) {// "+-" -> "+" + res += "+"; + } else { + var b64str = base64Accum + this.iconv.decode(buf.slice(lastI, i), "ascii"); + res += this.iconv.decode(Buffer.from(b64str, 'base64'), "utf16-be"); + } + + if (buf[i] != minusChar) // Minus is absorbed after base64. + i--; + + lastI = i+1; + inBase64 = false; + base64Accum = ''; + } + } + } + + if (!inBase64) { + res += this.iconv.decode(buf.slice(lastI), "ascii"); // Write direct chars. + } else { + var b64str = base64Accum + this.iconv.decode(buf.slice(lastI), "ascii"); + + var canBeDecoded = b64str.length - (b64str.length % 8); // Minimal chunk: 2 quads -> 2x3 bytes -> 3 chars. + base64Accum = b64str.slice(canBeDecoded); // The rest will be decoded in future. + b64str = b64str.slice(0, canBeDecoded); + + res += this.iconv.decode(Buffer.from(b64str, 'base64'), "utf16-be"); + } + + this.inBase64 = inBase64; + this.base64Accum = base64Accum; + + return res; +} + +Utf7Decoder.prototype.end = function() { + var res = ""; + if (this.inBase64 && this.base64Accum.length > 0) + res = this.iconv.decode(Buffer.from(this.base64Accum, 'base64'), "utf16-be"); + + this.inBase64 = false; + this.base64Accum = ''; + return res; +} + + +// UTF-7-IMAP codec. +// RFC3501 Sec. 5.1.3 Modified UTF-7 (http://tools.ietf.org/html/rfc3501#section-5.1.3) +// Differences: +// * Base64 part is started by "&" instead of "+" +// * Direct characters are 0x20-0x7E, except "&" (0x26) +// * In Base64, "," is used instead of "/" +// * Base64 must not be used to represent direct characters. +// * No implicit shift back from Base64 (should always end with '-') +// * String must end in non-shifted position. +// * "-&" while in base64 is not allowed. + + +exports.utf7imap = Utf7IMAPCodec; +function Utf7IMAPCodec(codecOptions, iconv) { + this.iconv = iconv; +}; + +Utf7IMAPCodec.prototype.encoder = Utf7IMAPEncoder; +Utf7IMAPCodec.prototype.decoder = Utf7IMAPDecoder; +Utf7IMAPCodec.prototype.bomAware = true; + + +// -- Encoding + +function Utf7IMAPEncoder(options, codec) { + this.iconv = codec.iconv; + this.inBase64 = false; + this.base64Accum = Buffer.alloc(6); + this.base64AccumIdx = 0; +} + +Utf7IMAPEncoder.prototype.write = function(str) { + var inBase64 = this.inBase64, + base64Accum = this.base64Accum, + base64AccumIdx = this.base64AccumIdx, + buf = Buffer.alloc(str.length*5 + 10), bufIdx = 0; + + for (var i = 0; i < str.length; i++) { + var uChar = str.charCodeAt(i); + if (0x20 <= uChar && uChar <= 0x7E) { // Direct character or '&'. + if (inBase64) { + if (base64AccumIdx > 0) { + bufIdx += buf.write(base64Accum.slice(0, base64AccumIdx).toString('base64').replace(/\//g, ',').replace(/=+$/, ''), bufIdx); + base64AccumIdx = 0; + } + + buf[bufIdx++] = minusChar; // Write '-', then go to direct mode. + inBase64 = false; + } + + if (!inBase64) { + buf[bufIdx++] = uChar; // Write direct character + + if (uChar === andChar) // Ampersand -> '&-' + buf[bufIdx++] = minusChar; + } + + } else { // Non-direct character + if (!inBase64) { + buf[bufIdx++] = andChar; // Write '&', then go to base64 mode. + inBase64 = true; + } + if (inBase64) { + base64Accum[base64AccumIdx++] = uChar >> 8; + base64Accum[base64AccumIdx++] = uChar & 0xFF; + + if (base64AccumIdx == base64Accum.length) { + bufIdx += buf.write(base64Accum.toString('base64').replace(/\//g, ','), bufIdx); + base64AccumIdx = 0; + } + } + } + } + + this.inBase64 = inBase64; + this.base64AccumIdx = base64AccumIdx; + + return buf.slice(0, bufIdx); +} + +Utf7IMAPEncoder.prototype.end = function() { + var buf = Buffer.alloc(10), bufIdx = 0; + if (this.inBase64) { + if (this.base64AccumIdx > 0) { + bufIdx += buf.write(this.base64Accum.slice(0, this.base64AccumIdx).toString('base64').replace(/\//g, ',').replace(/=+$/, ''), bufIdx); + this.base64AccumIdx = 0; + } + + buf[bufIdx++] = minusChar; // Write '-', then go to direct mode. + this.inBase64 = false; + } + + return buf.slice(0, bufIdx); +} + + +// -- Decoding + +function Utf7IMAPDecoder(options, codec) { + this.iconv = codec.iconv; + this.inBase64 = false; + this.base64Accum = ''; +} + +var base64IMAPChars = base64Chars.slice(); +base64IMAPChars[','.charCodeAt(0)] = true; + +Utf7IMAPDecoder.prototype.write = function(buf) { + var res = "", lastI = 0, + inBase64 = this.inBase64, + base64Accum = this.base64Accum; + + // The decoder is more involved as we must handle chunks in stream. + // It is forgiving, closer to standard UTF-7 (for example, '-' is optional at the end). + + for (var i = 0; i < buf.length; i++) { + if (!inBase64) { // We're in direct mode. + // Write direct chars until '&' + if (buf[i] == andChar) { + res += this.iconv.decode(buf.slice(lastI, i), "ascii"); // Write direct chars. + lastI = i+1; + inBase64 = true; + } + } else { // We decode base64. + if (!base64IMAPChars[buf[i]]) { // Base64 ended. + if (i == lastI && buf[i] == minusChar) { // "&-" -> "&" + res += "&"; + } else { + var b64str = base64Accum + this.iconv.decode(buf.slice(lastI, i), "ascii").replace(/,/g, '/'); + res += this.iconv.decode(Buffer.from(b64str, 'base64'), "utf16-be"); + } + + if (buf[i] != minusChar) // Minus may be absorbed after base64. + i--; + + lastI = i+1; + inBase64 = false; + base64Accum = ''; + } + } + } + + if (!inBase64) { + res += this.iconv.decode(buf.slice(lastI), "ascii"); // Write direct chars. + } else { + var b64str = base64Accum + this.iconv.decode(buf.slice(lastI), "ascii").replace(/,/g, '/'); + + var canBeDecoded = b64str.length - (b64str.length % 8); // Minimal chunk: 2 quads -> 2x3 bytes -> 3 chars. + base64Accum = b64str.slice(canBeDecoded); // The rest will be decoded in future. + b64str = b64str.slice(0, canBeDecoded); + + res += this.iconv.decode(Buffer.from(b64str, 'base64'), "utf16-be"); + } + + this.inBase64 = inBase64; + this.base64Accum = base64Accum; + + return res; +} + +Utf7IMAPDecoder.prototype.end = function() { + var res = ""; + if (this.inBase64 && this.base64Accum.length > 0) + res = this.iconv.decode(Buffer.from(this.base64Accum, 'base64'), "utf16-be"); + + this.inBase64 = false; + this.base64Accum = ''; + return res; +} + + + + +/***/ }), + +/***/ 4277: +/***/ ((__unused_webpack_module, exports) => { + +"use strict"; + + +var BOMChar = '\uFEFF'; + +exports.PrependBOM = PrependBOMWrapper +function PrependBOMWrapper(encoder, options) { + this.encoder = encoder; + this.addBOM = true; +} + +PrependBOMWrapper.prototype.write = function(str) { + if (this.addBOM) { + str = BOMChar + str; + this.addBOM = false; + } + + return this.encoder.write(str); +} + +PrependBOMWrapper.prototype.end = function() { + return this.encoder.end(); +} + + +//------------------------------------------------------------------------------ + +exports.StripBOM = StripBOMWrapper; +function StripBOMWrapper(decoder, options) { + this.decoder = decoder; + this.pass = false; + this.options = options || {}; +} + +StripBOMWrapper.prototype.write = function(buf) { + var res = this.decoder.write(buf); + if (this.pass || !res) + return res; + + if (res[0] === BOMChar) { + res = res.slice(1); + if (typeof this.options.stripBOM === 'function') + this.options.stripBOM(); + } + + this.pass = true; + return res; +} + +StripBOMWrapper.prototype.end = function() { + return this.decoder.end(); +} + + + +/***/ }), + +/***/ 6249: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +var Buffer = (__nccwpck_require__(4199).Buffer); + +var bomHandling = __nccwpck_require__(4277), + iconv = module.exports; + +// All codecs and aliases are kept here, keyed by encoding name/alias. +// They are lazy loaded in `iconv.getCodec` from `encodings/index.js`. +iconv.encodings = null; + +// Characters emitted in case of error. +iconv.defaultCharUnicode = '�'; +iconv.defaultCharSingleByte = '?'; + +// Public API. +iconv.encode = function encode(str, encoding, options) { + str = "" + (str || ""); // Ensure string. + + var encoder = iconv.getEncoder(encoding, options); + + var res = encoder.write(str); + var trail = encoder.end(); + + return (trail && trail.length > 0) ? Buffer.concat([res, trail]) : res; +} + +iconv.decode = function decode(buf, encoding, options) { + if (typeof buf === 'string') { + if (!iconv.skipDecodeWarning) { + console.error('Iconv-lite warning: decode()-ing strings is deprecated. Refer to https://github.com/ashtuchkin/iconv-lite/wiki/Use-Buffers-when-decoding'); + iconv.skipDecodeWarning = true; + } + + buf = Buffer.from("" + (buf || ""), "binary"); // Ensure buffer. + } + + var decoder = iconv.getDecoder(encoding, options); + + var res = decoder.write(buf); + var trail = decoder.end(); + + return trail ? (res + trail) : res; +} + +iconv.encodingExists = function encodingExists(enc) { + try { + iconv.getCodec(enc); + return true; + } catch (e) { + return false; + } +} + +// Legacy aliases to convert functions +iconv.toEncoding = iconv.encode; +iconv.fromEncoding = iconv.decode; + +// Search for a codec in iconv.encodings. Cache codec data in iconv._codecDataCache. +iconv._codecDataCache = {}; +iconv.getCodec = function getCodec(encoding) { + if (!iconv.encodings) + iconv.encodings = __nccwpck_require__(5424); // Lazy load all encoding definitions. + + // Canonicalize encoding name: strip all non-alphanumeric chars and appended year. + var enc = iconv._canonicalizeEncoding(encoding); + + // Traverse iconv.encodings to find actual codec. + var codecOptions = {}; + while (true) { + var codec = iconv._codecDataCache[enc]; + if (codec) + return codec; + + var codecDef = iconv.encodings[enc]; + + switch (typeof codecDef) { + case "string": // Direct alias to other encoding. + enc = codecDef; + break; + + case "object": // Alias with options. Can be layered. + for (var key in codecDef) + codecOptions[key] = codecDef[key]; + + if (!codecOptions.encodingName) + codecOptions.encodingName = enc; + + enc = codecDef.type; + break; + + case "function": // Codec itself. + if (!codecOptions.encodingName) + codecOptions.encodingName = enc; + + // The codec function must load all tables and return object with .encoder and .decoder methods. + // It'll be called only once (for each different options object). + codec = new codecDef(codecOptions, iconv); + + iconv._codecDataCache[codecOptions.encodingName] = codec; // Save it to be reused later. + return codec; + + default: + throw new Error("Encoding not recognized: '" + encoding + "' (searched as: '"+enc+"')"); + } + } +} + +iconv._canonicalizeEncoding = function(encoding) { + // Canonicalize encoding name: strip all non-alphanumeric chars and appended year. + return (''+encoding).toLowerCase().replace(/:\d{4}$|[^0-9a-z]/g, ""); +} + +iconv.getEncoder = function getEncoder(encoding, options) { + var codec = iconv.getCodec(encoding), + encoder = new codec.encoder(options, codec); + + if (codec.bomAware && options && options.addBOM) + encoder = new bomHandling.PrependBOM(encoder, options); + + return encoder; +} + +iconv.getDecoder = function getDecoder(encoding, options) { + var codec = iconv.getCodec(encoding), + decoder = new codec.decoder(options, codec); + + if (codec.bomAware && !(options && options.stripBOM === false)) + decoder = new bomHandling.StripBOM(decoder, options); + + return decoder; +} + +// Streaming API +// NOTE: Streaming API naturally depends on 'stream' module from Node.js. Unfortunately in browser environments this module can add +// up to 100Kb to the output bundle. To avoid unnecessary code bloat, we don't enable Streaming API in browser by default. +// If you would like to enable it explicitly, please add the following code to your app: +// > iconv.enableStreamingAPI(require('stream')); +iconv.enableStreamingAPI = function enableStreamingAPI(stream_module) { + if (iconv.supportsStreams) + return; + + // Dependency-inject stream module to create IconvLite stream classes. + var streams = __nccwpck_require__(3208)(stream_module); + + // Not public API yet, but expose the stream classes. + iconv.IconvLiteEncoderStream = streams.IconvLiteEncoderStream; + iconv.IconvLiteDecoderStream = streams.IconvLiteDecoderStream; + + // Streaming API. + iconv.encodeStream = function encodeStream(encoding, options) { + return new iconv.IconvLiteEncoderStream(iconv.getEncoder(encoding, options), options); + } + + iconv.decodeStream = function decodeStream(encoding, options) { + return new iconv.IconvLiteDecoderStream(iconv.getDecoder(encoding, options), options); + } + + iconv.supportsStreams = true; +} + +// Enable Streaming API automatically if 'stream' module is available and non-empty (the majority of environments). +var stream_module; +try { + stream_module = __nccwpck_require__(2203); +} catch (e) {} + +if (stream_module && stream_module.Transform) { + iconv.enableStreamingAPI(stream_module); + +} else { + // In rare cases where 'stream' module is not available by default, throw a helpful exception. + iconv.encodeStream = iconv.decodeStream = function() { + throw new Error("iconv-lite Streaming API is not enabled. Use iconv.enableStreamingAPI(require('stream')); to enable it."); + }; +} + +if (false) {} + + +/***/ }), + +/***/ 3208: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +var Buffer = (__nccwpck_require__(4199).Buffer); + +// NOTE: Due to 'stream' module being pretty large (~100Kb, significant in browser environments), +// we opt to dependency-inject it instead of creating a hard dependency. +module.exports = function(stream_module) { + var Transform = stream_module.Transform; + + // == Encoder stream ======================================================= + + function IconvLiteEncoderStream(conv, options) { + this.conv = conv; + options = options || {}; + options.decodeStrings = false; // We accept only strings, so we don't need to decode them. + Transform.call(this, options); + } + + IconvLiteEncoderStream.prototype = Object.create(Transform.prototype, { + constructor: { value: IconvLiteEncoderStream } + }); + + IconvLiteEncoderStream.prototype._transform = function(chunk, encoding, done) { + if (typeof chunk != 'string') + return done(new Error("Iconv encoding stream needs strings as its input.")); + try { + var res = this.conv.write(chunk); + if (res && res.length) this.push(res); + done(); + } + catch (e) { + done(e); + } + } + + IconvLiteEncoderStream.prototype._flush = function(done) { + try { + var res = this.conv.end(); + if (res && res.length) this.push(res); + done(); + } + catch (e) { + done(e); + } + } + + IconvLiteEncoderStream.prototype.collect = function(cb) { + var chunks = []; + this.on('error', cb); + this.on('data', function(chunk) { chunks.push(chunk); }); + this.on('end', function() { + cb(null, Buffer.concat(chunks)); + }); + return this; + } + + + // == Decoder stream ======================================================= + + function IconvLiteDecoderStream(conv, options) { + this.conv = conv; + options = options || {}; + options.encoding = this.encoding = 'utf8'; // We output strings. + Transform.call(this, options); + } + + IconvLiteDecoderStream.prototype = Object.create(Transform.prototype, { + constructor: { value: IconvLiteDecoderStream } + }); + + IconvLiteDecoderStream.prototype._transform = function(chunk, encoding, done) { + if (!Buffer.isBuffer(chunk) && !(chunk instanceof Uint8Array)) + return done(new Error("Iconv decoding stream needs buffers as its input.")); + try { + var res = this.conv.write(chunk); + if (res && res.length) this.push(res, this.encoding); + done(); + } + catch (e) { + done(e); + } + } + + IconvLiteDecoderStream.prototype._flush = function(done) { + try { + var res = this.conv.end(); + if (res && res.length) this.push(res, this.encoding); + done(); + } + catch (e) { + done(e); + } + } + + IconvLiteDecoderStream.prototype.collect = function(cb) { + var res = ''; + this.on('error', cb); + this.on('data', function(chunk) { res += chunk; }); + this.on('end', function() { + cb(null, res); + }); + return this; + } + + return { + IconvLiteEncoderStream: IconvLiteEncoderStream, + IconvLiteDecoderStream: IconvLiteDecoderStream, + }; +}; + + +/***/ }), + +/***/ 4199: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; +/* eslint-disable node/no-deprecated-api */ + + + +var buffer = __nccwpck_require__(181) +var Buffer = buffer.Buffer + +var safer = {} + +var key + +for (key in buffer) { + if (!buffer.hasOwnProperty(key)) continue + if (key === 'SlowBuffer' || key === 'Buffer') continue + safer[key] = buffer[key] +} + +var Safer = safer.Buffer = {} +for (key in Buffer) { + if (!Buffer.hasOwnProperty(key)) continue + if (key === 'allocUnsafe' || key === 'allocUnsafeSlow') continue + Safer[key] = Buffer[key] +} + +safer.Buffer.prototype = Buffer.prototype + +if (!Safer.from || Safer.from === Uint8Array.from) { + Safer.from = function (value, encodingOrOffset, length) { + if (typeof value === 'number') { + throw new TypeError('The "value" argument must not be of type number. Received type ' + typeof value) + } + if (value && typeof value.length === 'undefined') { + throw new TypeError('The first argument must be one of type string, Buffer, ArrayBuffer, Array, or Array-like Object. Received type ' + typeof value) + } + return Buffer(value, encodingOrOffset, length) + } +} + +if (!Safer.alloc) { + Safer.alloc = function (size, fill, encoding) { + if (typeof size !== 'number') { + throw new TypeError('The "size" argument must be of type number. Received type ' + typeof size) + } + if (size < 0 || size >= 2 * (1 << 30)) { + throw new RangeError('The value "' + size + '" is invalid for option "size"') + } + var buf = Buffer(size) + if (!fill || fill.length === 0) { + buf.fill(0) + } else if (typeof encoding === 'string') { + buf.fill(fill, encoding) + } else { + buf.fill(fill) + } + return buf + } +} + +if (!safer.kStringMaxLength) { + try { + safer.kStringMaxLength = process.binding('buffer').kStringMaxLength + } catch (e) { + // we can't determine kStringMaxLength in environments where process.binding + // is unsupported, so let's not set it + } +} + +if (!safer.constants) { + safer.constants = { + MAX_LENGTH: safer.kMaxLength + } + if (safer.kStringMaxLength) { + safer.constants.MAX_STRING_LENGTH = safer.kStringMaxLength + } +} + +module.exports = safer + + +/***/ }), + +/***/ 2613: +/***/ ((module) => { + +"use strict"; +module.exports = require("assert"); + +/***/ }), + +/***/ 290: +/***/ ((module) => { + +"use strict"; +module.exports = require("async_hooks"); + +/***/ }), + +/***/ 181: +/***/ ((module) => { + +"use strict"; +module.exports = require("buffer"); + +/***/ }), + +/***/ 5317: +/***/ ((module) => { + +"use strict"; +module.exports = require("child_process"); + +/***/ }), + +/***/ 4236: +/***/ ((module) => { + +"use strict"; +module.exports = require("console"); + +/***/ }), + +/***/ 6982: +/***/ ((module) => { + +"use strict"; +module.exports = require("crypto"); + +/***/ }), + +/***/ 1637: +/***/ ((module) => { + +"use strict"; +module.exports = require("diagnostics_channel"); + +/***/ }), + +/***/ 4434: +/***/ ((module) => { + +"use strict"; +module.exports = require("events"); + +/***/ }), + +/***/ 9896: +/***/ ((module) => { + +"use strict"; +module.exports = require("fs"); + +/***/ }), + +/***/ 8611: +/***/ ((module) => { + +"use strict"; +module.exports = require("http"); + +/***/ }), + +/***/ 5675: +/***/ ((module) => { + +"use strict"; +module.exports = require("http2"); + +/***/ }), + +/***/ 5692: +/***/ ((module) => { + +"use strict"; +module.exports = require("https"); + +/***/ }), + +/***/ 9278: +/***/ ((module) => { + +"use strict"; +module.exports = require("net"); + +/***/ }), + +/***/ 8474: +/***/ ((module) => { + +"use strict"; +module.exports = require("node:events"); + +/***/ }), + +/***/ 7075: +/***/ ((module) => { + +"use strict"; +module.exports = require("node:stream"); + +/***/ }), + +/***/ 7975: +/***/ ((module) => { + +"use strict"; +module.exports = require("node:util"); + +/***/ }), + +/***/ 857: +/***/ ((module) => { + +"use strict"; +module.exports = require("os"); + +/***/ }), + +/***/ 6928: +/***/ ((module) => { + +"use strict"; +module.exports = require("path"); + +/***/ }), + +/***/ 2987: +/***/ ((module) => { + +"use strict"; +module.exports = require("perf_hooks"); + +/***/ }), + +/***/ 4876: +/***/ ((module) => { + +"use strict"; +module.exports = require("punycode"); + +/***/ }), + +/***/ 3480: +/***/ ((module) => { + +"use strict"; +module.exports = require("querystring"); + +/***/ }), + +/***/ 2203: +/***/ ((module) => { + +"use strict"; +module.exports = require("stream"); + +/***/ }), + +/***/ 3774: +/***/ ((module) => { + +"use strict"; +module.exports = require("stream/web"); + +/***/ }), + +/***/ 3193: +/***/ ((module) => { + +"use strict"; +module.exports = require("string_decoder"); + +/***/ }), + +/***/ 3557: +/***/ ((module) => { + +"use strict"; +module.exports = require("timers"); + +/***/ }), + +/***/ 4756: +/***/ ((module) => { + +"use strict"; +module.exports = require("tls"); + +/***/ }), + +/***/ 7016: +/***/ ((module) => { + +"use strict"; +module.exports = require("url"); + +/***/ }), + +/***/ 9023: +/***/ ((module) => { + +"use strict"; +module.exports = require("util"); + +/***/ }), + +/***/ 8253: +/***/ ((module) => { + +"use strict"; +module.exports = require("util/types"); + +/***/ }), + +/***/ 8167: +/***/ ((module) => { + +"use strict"; +module.exports = require("worker_threads"); + +/***/ }), + +/***/ 3106: +/***/ ((module) => { + +"use strict"; +module.exports = require("zlib"); + +/***/ }), + +/***/ 7182: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const WritableStream = (__nccwpck_require__(7075).Writable) +const inherits = (__nccwpck_require__(7975).inherits) + +const StreamSearch = __nccwpck_require__(4136) + +const PartStream = __nccwpck_require__(612) +const HeaderParser = __nccwpck_require__(2271) + +const DASH = 45 +const B_ONEDASH = Buffer.from('-') +const B_CRLF = Buffer.from('\r\n') +const EMPTY_FN = function () {} + +function Dicer (cfg) { + if (!(this instanceof Dicer)) { return new Dicer(cfg) } + WritableStream.call(this, cfg) + + if (!cfg || (!cfg.headerFirst && typeof cfg.boundary !== 'string')) { throw new TypeError('Boundary required') } + + if (typeof cfg.boundary === 'string') { this.setBoundary(cfg.boundary) } else { this._bparser = undefined } + + this._headerFirst = cfg.headerFirst + + this._dashes = 0 + this._parts = 0 + this._finished = false + this._realFinish = false + this._isPreamble = true + this._justMatched = false + this._firstWrite = true + this._inHeader = true + this._part = undefined + this._cb = undefined + this._ignoreData = false + this._partOpts = { highWaterMark: cfg.partHwm } + this._pause = false + + const self = this + this._hparser = new HeaderParser(cfg) + this._hparser.on('header', function (header) { + self._inHeader = false + self._part.emit('header', header) + }) +} +inherits(Dicer, WritableStream) + +Dicer.prototype.emit = function (ev) { + if (ev === 'finish' && !this._realFinish) { + if (!this._finished) { + const self = this + process.nextTick(function () { + self.emit('error', new Error('Unexpected end of multipart data')) + if (self._part && !self._ignoreData) { + const type = (self._isPreamble ? 'Preamble' : 'Part') + self._part.emit('error', new Error(type + ' terminated early due to unexpected end of multipart data')) + self._part.push(null) + process.nextTick(function () { + self._realFinish = true + self.emit('finish') + self._realFinish = false + }) + return + } + self._realFinish = true + self.emit('finish') + self._realFinish = false + }) + } + } else { WritableStream.prototype.emit.apply(this, arguments) } +} + +Dicer.prototype._write = function (data, encoding, cb) { + // ignore unexpected data (e.g. extra trailer data after finished) + if (!this._hparser && !this._bparser) { return cb() } + + if (this._headerFirst && this._isPreamble) { + if (!this._part) { + this._part = new PartStream(this._partOpts) + if (this.listenerCount('preamble') !== 0) { this.emit('preamble', this._part) } else { this._ignore() } + } + const r = this._hparser.push(data) + if (!this._inHeader && r !== undefined && r < data.length) { data = data.slice(r) } else { return cb() } + } + + // allows for "easier" testing + if (this._firstWrite) { + this._bparser.push(B_CRLF) + this._firstWrite = false + } + + this._bparser.push(data) + + if (this._pause) { this._cb = cb } else { cb() } +} + +Dicer.prototype.reset = function () { + this._part = undefined + this._bparser = undefined + this._hparser = undefined +} + +Dicer.prototype.setBoundary = function (boundary) { + const self = this + this._bparser = new StreamSearch('\r\n--' + boundary) + this._bparser.on('info', function (isMatch, data, start, end) { + self._oninfo(isMatch, data, start, end) + }) +} + +Dicer.prototype._ignore = function () { + if (this._part && !this._ignoreData) { + this._ignoreData = true + this._part.on('error', EMPTY_FN) + // we must perform some kind of read on the stream even though we are + // ignoring the data, otherwise node's Readable stream will not emit 'end' + // after pushing null to the stream + this._part.resume() + } +} + +Dicer.prototype._oninfo = function (isMatch, data, start, end) { + let buf; const self = this; let i = 0; let r; let shouldWriteMore = true + + if (!this._part && this._justMatched && data) { + while (this._dashes < 2 && (start + i) < end) { + if (data[start + i] === DASH) { + ++i + ++this._dashes + } else { + if (this._dashes) { buf = B_ONEDASH } + this._dashes = 0 + break + } + } + if (this._dashes === 2) { + if ((start + i) < end && this.listenerCount('trailer') !== 0) { this.emit('trailer', data.slice(start + i, end)) } + this.reset() + this._finished = true + // no more parts will be added + if (self._parts === 0) { + self._realFinish = true + self.emit('finish') + self._realFinish = false + } + } + if (this._dashes) { return } + } + if (this._justMatched) { this._justMatched = false } + if (!this._part) { + this._part = new PartStream(this._partOpts) + this._part._read = function (n) { + self._unpause() + } + if (this._isPreamble && this.listenerCount('preamble') !== 0) { + this.emit('preamble', this._part) + } else if (this._isPreamble !== true && this.listenerCount('part') !== 0) { + this.emit('part', this._part) + } else { + this._ignore() + } + if (!this._isPreamble) { this._inHeader = true } + } + if (data && start < end && !this._ignoreData) { + if (this._isPreamble || !this._inHeader) { + if (buf) { shouldWriteMore = this._part.push(buf) } + shouldWriteMore = this._part.push(data.slice(start, end)) + if (!shouldWriteMore) { this._pause = true } + } else if (!this._isPreamble && this._inHeader) { + if (buf) { this._hparser.push(buf) } + r = this._hparser.push(data.slice(start, end)) + if (!this._inHeader && r !== undefined && r < end) { this._oninfo(false, data, start + r, end) } + } + } + if (isMatch) { + this._hparser.reset() + if (this._isPreamble) { this._isPreamble = false } else { + if (start !== end) { + ++this._parts + this._part.on('end', function () { + if (--self._parts === 0) { + if (self._finished) { + self._realFinish = true + self.emit('finish') + self._realFinish = false + } else { + self._unpause() + } + } + }) + } + } + this._part.push(null) + this._part = undefined + this._ignoreData = false + this._justMatched = true + this._dashes = 0 + } +} + +Dicer.prototype._unpause = function () { + if (!this._pause) { return } + + this._pause = false + if (this._cb) { + const cb = this._cb + this._cb = undefined + cb() + } +} + +module.exports = Dicer + + +/***/ }), + +/***/ 2271: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const EventEmitter = (__nccwpck_require__(8474).EventEmitter) +const inherits = (__nccwpck_require__(7975).inherits) +const getLimit = __nccwpck_require__(2393) + +const StreamSearch = __nccwpck_require__(4136) + +const B_DCRLF = Buffer.from('\r\n\r\n') +const RE_CRLF = /\r\n/g +const RE_HDR = /^([^:]+):[ \t]?([\x00-\xFF]+)?$/ // eslint-disable-line no-control-regex + +function HeaderParser (cfg) { + EventEmitter.call(this) + + cfg = cfg || {} + const self = this + this.nread = 0 + this.maxed = false + this.npairs = 0 + this.maxHeaderPairs = getLimit(cfg, 'maxHeaderPairs', 2000) + this.maxHeaderSize = getLimit(cfg, 'maxHeaderSize', 80 * 1024) + this.buffer = '' + this.header = {} + this.finished = false + this.ss = new StreamSearch(B_DCRLF) + this.ss.on('info', function (isMatch, data, start, end) { + if (data && !self.maxed) { + if (self.nread + end - start >= self.maxHeaderSize) { + end = self.maxHeaderSize - self.nread + start + self.nread = self.maxHeaderSize + self.maxed = true + } else { self.nread += (end - start) } + + self.buffer += data.toString('binary', start, end) + } + if (isMatch) { self._finish() } + }) +} +inherits(HeaderParser, EventEmitter) + +HeaderParser.prototype.push = function (data) { + const r = this.ss.push(data) + if (this.finished) { return r } +} + +HeaderParser.prototype.reset = function () { + this.finished = false + this.buffer = '' + this.header = {} + this.ss.reset() +} + +HeaderParser.prototype._finish = function () { + if (this.buffer) { this._parseHeader() } + this.ss.matches = this.ss.maxMatches + const header = this.header + this.header = {} + this.buffer = '' + this.finished = true + this.nread = this.npairs = 0 + this.maxed = false + this.emit('header', header) +} + +HeaderParser.prototype._parseHeader = function () { + if (this.npairs === this.maxHeaderPairs) { return } + + const lines = this.buffer.split(RE_CRLF) + const len = lines.length + let m, h + + for (var i = 0; i < len; ++i) { // eslint-disable-line no-var + if (lines[i].length === 0) { continue } + if (lines[i][0] === '\t' || lines[i][0] === ' ') { + // folded header content + // RFC2822 says to just remove the CRLF and not the whitespace following + // it, so we follow the RFC and include the leading whitespace ... + if (h) { + this.header[h][this.header[h].length - 1] += lines[i] + continue + } + } + + const posColon = lines[i].indexOf(':') + if ( + posColon === -1 || + posColon === 0 + ) { + return + } + m = RE_HDR.exec(lines[i]) + h = m[1].toLowerCase() + this.header[h] = this.header[h] || [] + this.header[h].push((m[2] || '')) + if (++this.npairs === this.maxHeaderPairs) { break } + } +} + +module.exports = HeaderParser + + +/***/ }), + +/***/ 612: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const inherits = (__nccwpck_require__(7975).inherits) +const ReadableStream = (__nccwpck_require__(7075).Readable) + +function PartStream (opts) { + ReadableStream.call(this, opts) +} +inherits(PartStream, ReadableStream) + +PartStream.prototype._read = function (n) {} + +module.exports = PartStream + + +/***/ }), + +/***/ 4136: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +/** + * Copyright Brian White. All rights reserved. + * + * @see https://github.com/mscdex/streamsearch + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + * + * Based heavily on the Streaming Boyer-Moore-Horspool C++ implementation + * by Hongli Lai at: https://github.com/FooBarWidget/boyer-moore-horspool + */ +const EventEmitter = (__nccwpck_require__(8474).EventEmitter) +const inherits = (__nccwpck_require__(7975).inherits) + +function SBMH (needle) { + if (typeof needle === 'string') { + needle = Buffer.from(needle) + } + + if (!Buffer.isBuffer(needle)) { + throw new TypeError('The needle has to be a String or a Buffer.') + } + + const needleLength = needle.length + + if (needleLength === 0) { + throw new Error('The needle cannot be an empty String/Buffer.') + } + + if (needleLength > 256) { + throw new Error('The needle cannot have a length bigger than 256.') + } + + this.maxMatches = Infinity + this.matches = 0 + + this._occ = new Array(256) + .fill(needleLength) // Initialize occurrence table. + this._lookbehind_size = 0 + this._needle = needle + this._bufpos = 0 + + this._lookbehind = Buffer.alloc(needleLength) + + // Populate occurrence table with analysis of the needle, + // ignoring last letter. + for (var i = 0; i < needleLength - 1; ++i) { // eslint-disable-line no-var + this._occ[needle[i]] = needleLength - 1 - i + } +} +inherits(SBMH, EventEmitter) + +SBMH.prototype.reset = function () { + this._lookbehind_size = 0 + this.matches = 0 + this._bufpos = 0 +} + +SBMH.prototype.push = function (chunk, pos) { + if (!Buffer.isBuffer(chunk)) { + chunk = Buffer.from(chunk, 'binary') + } + const chlen = chunk.length + this._bufpos = pos || 0 + let r + while (r !== chlen && this.matches < this.maxMatches) { r = this._sbmh_feed(chunk) } + return r +} + +SBMH.prototype._sbmh_feed = function (data) { + const len = data.length + const needle = this._needle + const needleLength = needle.length + const lastNeedleChar = needle[needleLength - 1] + + // Positive: points to a position in `data` + // pos == 3 points to data[3] + // Negative: points to a position in the lookbehind buffer + // pos == -2 points to lookbehind[lookbehind_size - 2] + let pos = -this._lookbehind_size + let ch + + if (pos < 0) { + // Lookbehind buffer is not empty. Perform Boyer-Moore-Horspool + // search with character lookup code that considers both the + // lookbehind buffer and the current round's haystack data. + // + // Loop until + // there is a match. + // or until + // we've moved past the position that requires the + // lookbehind buffer. In this case we switch to the + // optimized loop. + // or until + // the character to look at lies outside the haystack. + while (pos < 0 && pos <= len - needleLength) { + ch = this._sbmh_lookup_char(data, pos + needleLength - 1) + + if ( + ch === lastNeedleChar && + this._sbmh_memcmp(data, pos, needleLength - 1) + ) { + this._lookbehind_size = 0 + ++this.matches + this.emit('info', true) + + return (this._bufpos = pos + needleLength) + } + pos += this._occ[ch] + } + + // No match. + + if (pos < 0) { + // There's too few data for Boyer-Moore-Horspool to run, + // so let's use a different algorithm to skip as much as + // we can. + // Forward pos until + // the trailing part of lookbehind + data + // looks like the beginning of the needle + // or until + // pos == 0 + while (pos < 0 && !this._sbmh_memcmp(data, pos, len - pos)) { ++pos } + } + + if (pos >= 0) { + // Discard lookbehind buffer. + this.emit('info', false, this._lookbehind, 0, this._lookbehind_size) + this._lookbehind_size = 0 + } else { + // Cut off part of the lookbehind buffer that has + // been processed and append the entire haystack + // into it. + const bytesToCutOff = this._lookbehind_size + pos + if (bytesToCutOff > 0) { + // The cut off data is guaranteed not to contain the needle. + this.emit('info', false, this._lookbehind, 0, bytesToCutOff) + } + + this._lookbehind.copy(this._lookbehind, 0, bytesToCutOff, + this._lookbehind_size - bytesToCutOff) + this._lookbehind_size -= bytesToCutOff + + data.copy(this._lookbehind, this._lookbehind_size) + this._lookbehind_size += len + + this._bufpos = len + return len + } + } + + pos += (pos >= 0) * this._bufpos + + // Lookbehind buffer is now empty. We only need to check if the + // needle is in the haystack. + if (data.indexOf(needle, pos) !== -1) { + pos = data.indexOf(needle, pos) + ++this.matches + if (pos > 0) { this.emit('info', true, data, this._bufpos, pos) } else { this.emit('info', true) } + + return (this._bufpos = pos + needleLength) + } else { + pos = len - needleLength + } + + // There was no match. If there's trailing haystack data that we cannot + // match yet using the Boyer-Moore-Horspool algorithm (because the trailing + // data is less than the needle size) then match using a modified + // algorithm that starts matching from the beginning instead of the end. + // Whatever trailing data is left after running this algorithm is added to + // the lookbehind buffer. + while ( + pos < len && + ( + data[pos] !== needle[0] || + ( + (Buffer.compare( + data.subarray(pos, pos + len - pos), + needle.subarray(0, len - pos) + ) !== 0) + ) + ) + ) { + ++pos + } + if (pos < len) { + data.copy(this._lookbehind, 0, pos, pos + (len - pos)) + this._lookbehind_size = len - pos + } + + // Everything until pos is guaranteed not to contain needle data. + if (pos > 0) { this.emit('info', false, data, this._bufpos, pos < len ? pos : len) } + + this._bufpos = len + return len +} + +SBMH.prototype._sbmh_lookup_char = function (data, pos) { + return (pos < 0) + ? this._lookbehind[this._lookbehind_size + pos] + : data[pos] +} + +SBMH.prototype._sbmh_memcmp = function (data, pos, len) { + for (var i = 0; i < len; ++i) { // eslint-disable-line no-var + if (this._sbmh_lookup_char(data, pos + i) !== this._needle[i]) { return false } + } + return true +} + +module.exports = SBMH + + +/***/ }), + +/***/ 9581: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const WritableStream = (__nccwpck_require__(7075).Writable) +const { inherits } = __nccwpck_require__(7975) +const Dicer = __nccwpck_require__(7182) + +const MultipartParser = __nccwpck_require__(1192) +const UrlencodedParser = __nccwpck_require__(855) +const parseParams = __nccwpck_require__(8929) + +function Busboy (opts) { + if (!(this instanceof Busboy)) { return new Busboy(opts) } + + if (typeof opts !== 'object') { + throw new TypeError('Busboy expected an options-Object.') + } + if (typeof opts.headers !== 'object') { + throw new TypeError('Busboy expected an options-Object with headers-attribute.') + } + if (typeof opts.headers['content-type'] !== 'string') { + throw new TypeError('Missing Content-Type-header.') + } + + const { + headers, + ...streamOptions + } = opts + + this.opts = { + autoDestroy: false, + ...streamOptions + } + WritableStream.call(this, this.opts) + + this._done = false + this._parser = this.getParserByHeaders(headers) + this._finished = false +} +inherits(Busboy, WritableStream) + +Busboy.prototype.emit = function (ev) { + if (ev === 'finish') { + if (!this._done) { + this._parser?.end() + return + } else if (this._finished) { + return + } + this._finished = true + } + WritableStream.prototype.emit.apply(this, arguments) +} + +Busboy.prototype.getParserByHeaders = function (headers) { + const parsed = parseParams(headers['content-type']) + + const cfg = { + defCharset: this.opts.defCharset, + fileHwm: this.opts.fileHwm, + headers, + highWaterMark: this.opts.highWaterMark, + isPartAFile: this.opts.isPartAFile, + limits: this.opts.limits, + parsedConType: parsed, + preservePath: this.opts.preservePath + } + + if (MultipartParser.detect.test(parsed[0])) { + return new MultipartParser(this, cfg) + } + if (UrlencodedParser.detect.test(parsed[0])) { + return new UrlencodedParser(this, cfg) + } + throw new Error('Unsupported Content-Type.') +} + +Busboy.prototype._write = function (chunk, encoding, cb) { + this._parser.write(chunk, cb) +} + +module.exports = Busboy +module.exports["default"] = Busboy +module.exports.Busboy = Busboy + +module.exports.Dicer = Dicer + + +/***/ }), + +/***/ 1192: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +// TODO: +// * support 1 nested multipart level +// (see second multipart example here: +// http://www.w3.org/TR/html401/interact/forms.html#didx-multipartform-data) +// * support limits.fieldNameSize +// -- this will require modifications to utils.parseParams + +const { Readable } = __nccwpck_require__(7075) +const { inherits } = __nccwpck_require__(7975) + +const Dicer = __nccwpck_require__(7182) + +const parseParams = __nccwpck_require__(8929) +const decodeText = __nccwpck_require__(2747) +const basename = __nccwpck_require__(692) +const getLimit = __nccwpck_require__(2393) + +const RE_BOUNDARY = /^boundary$/i +const RE_FIELD = /^form-data$/i +const RE_CHARSET = /^charset$/i +const RE_FILENAME = /^filename$/i +const RE_NAME = /^name$/i + +Multipart.detect = /^multipart\/form-data/i +function Multipart (boy, cfg) { + let i + let len + const self = this + let boundary + const limits = cfg.limits + const isPartAFile = cfg.isPartAFile || ((fieldName, contentType, fileName) => (contentType === 'application/octet-stream' || fileName !== undefined)) + const parsedConType = cfg.parsedConType || [] + const defCharset = cfg.defCharset || 'utf8' + const preservePath = cfg.preservePath + const fileOpts = { highWaterMark: cfg.fileHwm } + + for (i = 0, len = parsedConType.length; i < len; ++i) { + if (Array.isArray(parsedConType[i]) && + RE_BOUNDARY.test(parsedConType[i][0])) { + boundary = parsedConType[i][1] + break + } + } + + function checkFinished () { + if (nends === 0 && finished && !boy._done) { + finished = false + self.end() + } + } + + if (typeof boundary !== 'string') { throw new Error('Multipart: Boundary not found') } + + const fieldSizeLimit = getLimit(limits, 'fieldSize', 1 * 1024 * 1024) + const fileSizeLimit = getLimit(limits, 'fileSize', Infinity) + const filesLimit = getLimit(limits, 'files', Infinity) + const fieldsLimit = getLimit(limits, 'fields', Infinity) + const partsLimit = getLimit(limits, 'parts', Infinity) + const headerPairsLimit = getLimit(limits, 'headerPairs', 2000) + const headerSizeLimit = getLimit(limits, 'headerSize', 80 * 1024) + + let nfiles = 0 + let nfields = 0 + let nends = 0 + let curFile + let curField + let finished = false + + this._needDrain = false + this._pause = false + this._cb = undefined + this._nparts = 0 + this._boy = boy + + const parserCfg = { + boundary, + maxHeaderPairs: headerPairsLimit, + maxHeaderSize: headerSizeLimit, + partHwm: fileOpts.highWaterMark, + highWaterMark: cfg.highWaterMark + } + + this.parser = new Dicer(parserCfg) + this.parser.on('drain', function () { + self._needDrain = false + if (self._cb && !self._pause) { + const cb = self._cb + self._cb = undefined + cb() + } + }).on('part', function onPart (part) { + if (++self._nparts > partsLimit) { + self.parser.removeListener('part', onPart) + self.parser.on('part', skipPart) + boy.hitPartsLimit = true + boy.emit('partsLimit') + return skipPart(part) + } + + // hack because streams2 _always_ doesn't emit 'end' until nextTick, so let + // us emit 'end' early since we know the part has ended if we are already + // seeing the next part + if (curField) { + const field = curField + field.emit('end') + field.removeAllListeners('end') + } + + part.on('header', function (header) { + let contype + let fieldname + let parsed + let charset + let encoding + let filename + let nsize = 0 + + if (header['content-type']) { + parsed = parseParams(header['content-type'][0]) + if (parsed[0]) { + contype = parsed[0].toLowerCase() + for (i = 0, len = parsed.length; i < len; ++i) { + if (RE_CHARSET.test(parsed[i][0])) { + charset = parsed[i][1].toLowerCase() + break + } + } + } + } + + if (contype === undefined) { contype = 'text/plain' } + if (charset === undefined) { charset = defCharset } + + if (header['content-disposition']) { + parsed = parseParams(header['content-disposition'][0]) + if (!RE_FIELD.test(parsed[0])) { return skipPart(part) } + for (i = 0, len = parsed.length; i < len; ++i) { + if (RE_NAME.test(parsed[i][0])) { + fieldname = parsed[i][1] + } else if (RE_FILENAME.test(parsed[i][0])) { + filename = parsed[i][1] + if (!preservePath) { filename = basename(filename) } + } + } + } else { return skipPart(part) } + + if (header['content-transfer-encoding']) { encoding = header['content-transfer-encoding'][0].toLowerCase() } else { encoding = '7bit' } + + let onData, + onEnd + + if (isPartAFile(fieldname, contype, filename)) { + // file/binary field + if (nfiles === filesLimit) { + if (!boy.hitFilesLimit) { + boy.hitFilesLimit = true + boy.emit('filesLimit') + } + return skipPart(part) + } + + ++nfiles + + if (boy.listenerCount('file') === 0) { + self.parser._ignore() + return + } + + ++nends + const file = new FileStream(fileOpts) + curFile = file + file.on('end', function () { + --nends + self._pause = false + checkFinished() + if (self._cb && !self._needDrain) { + const cb = self._cb + self._cb = undefined + cb() + } + }) + file._read = function (n) { + if (!self._pause) { return } + self._pause = false + if (self._cb && !self._needDrain) { + const cb = self._cb + self._cb = undefined + cb() + } + } + boy.emit('file', fieldname, file, filename, encoding, contype) + + onData = function (data) { + if ((nsize += data.length) > fileSizeLimit) { + const extralen = fileSizeLimit - nsize + data.length + if (extralen > 0) { file.push(data.slice(0, extralen)) } + file.truncated = true + file.bytesRead = fileSizeLimit + part.removeAllListeners('data') + file.emit('limit') + return + } else if (!file.push(data)) { self._pause = true } + + file.bytesRead = nsize + } + + onEnd = function () { + curFile = undefined + file.push(null) + } + } else { + // non-file field + if (nfields === fieldsLimit) { + if (!boy.hitFieldsLimit) { + boy.hitFieldsLimit = true + boy.emit('fieldsLimit') + } + return skipPart(part) + } + + ++nfields + ++nends + let buffer = '' + let truncated = false + curField = part + + onData = function (data) { + if ((nsize += data.length) > fieldSizeLimit) { + const extralen = (fieldSizeLimit - (nsize - data.length)) + buffer += data.toString('binary', 0, extralen) + truncated = true + part.removeAllListeners('data') + } else { buffer += data.toString('binary') } + } + + onEnd = function () { + curField = undefined + if (buffer.length) { buffer = decodeText(buffer, 'binary', charset) } + boy.emit('field', fieldname, buffer, false, truncated, encoding, contype) + --nends + checkFinished() + } + } + + /* As of node@2efe4ab761666 (v0.10.29+/v0.11.14+), busboy had become + broken. Streams2/streams3 is a huge black box of confusion, but + somehow overriding the sync state seems to fix things again (and still + seems to work for previous node versions). + */ + part._readableState.sync = false + + part.on('data', onData) + part.on('end', onEnd) + }).on('error', function (err) { + if (curFile) { curFile.emit('error', err) } + }) + }).on('error', function (err) { + boy.emit('error', err) + }).on('finish', function () { + finished = true + checkFinished() + }) +} + +Multipart.prototype.write = function (chunk, cb) { + const r = this.parser.write(chunk) + if (r && !this._pause) { + cb() + } else { + this._needDrain = !r + this._cb = cb + } +} + +Multipart.prototype.end = function () { + const self = this + + if (self.parser.writable) { + self.parser.end() + } else if (!self._boy._done) { + process.nextTick(function () { + self._boy._done = true + self._boy.emit('finish') + }) + } +} + +function skipPart (part) { + part.resume() +} + +function FileStream (opts) { + Readable.call(this, opts) + + this.bytesRead = 0 + + this.truncated = false +} + +inherits(FileStream, Readable) + +FileStream.prototype._read = function (n) {} + +module.exports = Multipart + + +/***/ }), + +/***/ 855: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const Decoder = __nccwpck_require__(1496) +const decodeText = __nccwpck_require__(2747) +const getLimit = __nccwpck_require__(2393) + +const RE_CHARSET = /^charset$/i + +UrlEncoded.detect = /^application\/x-www-form-urlencoded/i +function UrlEncoded (boy, cfg) { + const limits = cfg.limits + const parsedConType = cfg.parsedConType + this.boy = boy + + this.fieldSizeLimit = getLimit(limits, 'fieldSize', 1 * 1024 * 1024) + this.fieldNameSizeLimit = getLimit(limits, 'fieldNameSize', 100) + this.fieldsLimit = getLimit(limits, 'fields', Infinity) + + let charset + for (var i = 0, len = parsedConType.length; i < len; ++i) { // eslint-disable-line no-var + if (Array.isArray(parsedConType[i]) && + RE_CHARSET.test(parsedConType[i][0])) { + charset = parsedConType[i][1].toLowerCase() + break + } + } + + if (charset === undefined) { charset = cfg.defCharset || 'utf8' } + + this.decoder = new Decoder() + this.charset = charset + this._fields = 0 + this._state = 'key' + this._checkingBytes = true + this._bytesKey = 0 + this._bytesVal = 0 + this._key = '' + this._val = '' + this._keyTrunc = false + this._valTrunc = false + this._hitLimit = false +} + +UrlEncoded.prototype.write = function (data, cb) { + if (this._fields === this.fieldsLimit) { + if (!this.boy.hitFieldsLimit) { + this.boy.hitFieldsLimit = true + this.boy.emit('fieldsLimit') + } + return cb() + } + + let idxeq; let idxamp; let i; let p = 0; const len = data.length + + while (p < len) { + if (this._state === 'key') { + idxeq = idxamp = undefined + for (i = p; i < len; ++i) { + if (!this._checkingBytes) { ++p } + if (data[i] === 0x3D/* = */) { + idxeq = i + break + } else if (data[i] === 0x26/* & */) { + idxamp = i + break + } + if (this._checkingBytes && this._bytesKey === this.fieldNameSizeLimit) { + this._hitLimit = true + break + } else if (this._checkingBytes) { ++this._bytesKey } + } + + if (idxeq !== undefined) { + // key with assignment + if (idxeq > p) { this._key += this.decoder.write(data.toString('binary', p, idxeq)) } + this._state = 'val' + + this._hitLimit = false + this._checkingBytes = true + this._val = '' + this._bytesVal = 0 + this._valTrunc = false + this.decoder.reset() + + p = idxeq + 1 + } else if (idxamp !== undefined) { + // key with no assignment + ++this._fields + let key; const keyTrunc = this._keyTrunc + if (idxamp > p) { key = (this._key += this.decoder.write(data.toString('binary', p, idxamp))) } else { key = this._key } + + this._hitLimit = false + this._checkingBytes = true + this._key = '' + this._bytesKey = 0 + this._keyTrunc = false + this.decoder.reset() + + if (key.length) { + this.boy.emit('field', decodeText(key, 'binary', this.charset), + '', + keyTrunc, + false) + } + + p = idxamp + 1 + if (this._fields === this.fieldsLimit) { return cb() } + } else if (this._hitLimit) { + // we may not have hit the actual limit if there are encoded bytes... + if (i > p) { this._key += this.decoder.write(data.toString('binary', p, i)) } + p = i + if ((this._bytesKey = this._key.length) === this.fieldNameSizeLimit) { + // yep, we actually did hit the limit + this._checkingBytes = false + this._keyTrunc = true + } + } else { + if (p < len) { this._key += this.decoder.write(data.toString('binary', p)) } + p = len + } + } else { + idxamp = undefined + for (i = p; i < len; ++i) { + if (!this._checkingBytes) { ++p } + if (data[i] === 0x26/* & */) { + idxamp = i + break + } + if (this._checkingBytes && this._bytesVal === this.fieldSizeLimit) { + this._hitLimit = true + break + } else if (this._checkingBytes) { ++this._bytesVal } + } + + if (idxamp !== undefined) { + ++this._fields + if (idxamp > p) { this._val += this.decoder.write(data.toString('binary', p, idxamp)) } + this.boy.emit('field', decodeText(this._key, 'binary', this.charset), + decodeText(this._val, 'binary', this.charset), + this._keyTrunc, + this._valTrunc) + this._state = 'key' + + this._hitLimit = false + this._checkingBytes = true + this._key = '' + this._bytesKey = 0 + this._keyTrunc = false + this.decoder.reset() + + p = idxamp + 1 + if (this._fields === this.fieldsLimit) { return cb() } + } else if (this._hitLimit) { + // we may not have hit the actual limit if there are encoded bytes... + if (i > p) { this._val += this.decoder.write(data.toString('binary', p, i)) } + p = i + if ((this._val === '' && this.fieldSizeLimit === 0) || + (this._bytesVal = this._val.length) === this.fieldSizeLimit) { + // yep, we actually did hit the limit + this._checkingBytes = false + this._valTrunc = true + } + } else { + if (p < len) { this._val += this.decoder.write(data.toString('binary', p)) } + p = len + } + } + } + cb() +} + +UrlEncoded.prototype.end = function () { + if (this.boy._done) { return } + + if (this._state === 'key' && this._key.length > 0) { + this.boy.emit('field', decodeText(this._key, 'binary', this.charset), + '', + this._keyTrunc, + false) + } else if (this._state === 'val') { + this.boy.emit('field', decodeText(this._key, 'binary', this.charset), + decodeText(this._val, 'binary', this.charset), + this._keyTrunc, + this._valTrunc) + } + this.boy._done = true + this.boy.emit('finish') +} + +module.exports = UrlEncoded + + +/***/ }), + +/***/ 1496: +/***/ ((module) => { + +"use strict"; + + +const RE_PLUS = /\+/g + +const HEX = [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, + 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 +] + +function Decoder () { + this.buffer = undefined +} +Decoder.prototype.write = function (str) { + // Replace '+' with ' ' before decoding + str = str.replace(RE_PLUS, ' ') + let res = '' + let i = 0; let p = 0; const len = str.length + for (; i < len; ++i) { + if (this.buffer !== undefined) { + if (!HEX[str.charCodeAt(i)]) { + res += '%' + this.buffer + this.buffer = undefined + --i // retry character + } else { + this.buffer += str[i] + ++p + if (this.buffer.length === 2) { + res += String.fromCharCode(parseInt(this.buffer, 16)) + this.buffer = undefined + } + } + } else if (str[i] === '%') { + if (i > p) { + res += str.substring(p, i) + p = i + } + this.buffer = '' + ++p + } + } + if (p < len && this.buffer === undefined) { res += str.substring(p) } + return res +} +Decoder.prototype.reset = function () { + this.buffer = undefined +} + +module.exports = Decoder + + +/***/ }), + +/***/ 692: +/***/ ((module) => { + +"use strict"; + + +module.exports = function basename (path) { + if (typeof path !== 'string') { return '' } + for (var i = path.length - 1; i >= 0; --i) { // eslint-disable-line no-var + switch (path.charCodeAt(i)) { + case 0x2F: // '/' + case 0x5C: // '\' + path = path.slice(i + 1) + return (path === '..' || path === '.' ? '' : path) + } + } + return (path === '..' || path === '.' ? '' : path) +} + + +/***/ }), + +/***/ 2747: +/***/ (function(module) { + +"use strict"; + + +// Node has always utf-8 +const utf8Decoder = new TextDecoder('utf-8') +const textDecoders = new Map([ + ['utf-8', utf8Decoder], + ['utf8', utf8Decoder] +]) + +function getDecoder (charset) { + let lc + while (true) { + switch (charset) { + case 'utf-8': + case 'utf8': + return decoders.utf8 + case 'latin1': + case 'ascii': // TODO: Make these a separate, strict decoder? + case 'us-ascii': + case 'iso-8859-1': + case 'iso8859-1': + case 'iso88591': + case 'iso_8859-1': + case 'windows-1252': + case 'iso_8859-1:1987': + case 'cp1252': + case 'x-cp1252': + return decoders.latin1 + case 'utf16le': + case 'utf-16le': + case 'ucs2': + case 'ucs-2': + return decoders.utf16le + case 'base64': + return decoders.base64 + default: + if (lc === undefined) { + lc = true + charset = charset.toLowerCase() + continue + } + return decoders.other.bind(charset) + } + } +} + +const decoders = { + utf8: (data, sourceEncoding) => { + if (data.length === 0) { + return '' + } + if (typeof data === 'string') { + data = Buffer.from(data, sourceEncoding) + } + return data.utf8Slice(0, data.length) + }, + + latin1: (data, sourceEncoding) => { + if (data.length === 0) { + return '' + } + if (typeof data === 'string') { + return data + } + return data.latin1Slice(0, data.length) + }, + + utf16le: (data, sourceEncoding) => { + if (data.length === 0) { + return '' + } + if (typeof data === 'string') { + data = Buffer.from(data, sourceEncoding) + } + return data.ucs2Slice(0, data.length) + }, + + base64: (data, sourceEncoding) => { + if (data.length === 0) { + return '' + } + if (typeof data === 'string') { + data = Buffer.from(data, sourceEncoding) + } + return data.base64Slice(0, data.length) + }, + + other: (data, sourceEncoding) => { + if (data.length === 0) { + return '' + } + if (typeof data === 'string') { + data = Buffer.from(data, sourceEncoding) + } + + if (textDecoders.has(this.toString())) { + try { + return textDecoders.get(this).decode(data) + } catch {} + } + return typeof data === 'string' + ? data + : data.toString() + } +} + +function decodeText (text, sourceEncoding, destEncoding) { + if (text) { + return getDecoder(destEncoding)(text, sourceEncoding) + } + return text +} + +module.exports = decodeText + + +/***/ }), + +/***/ 2393: +/***/ ((module) => { + +"use strict"; + + +module.exports = function getLimit (limits, name, defaultLimit) { + if ( + !limits || + limits[name] === undefined || + limits[name] === null + ) { return defaultLimit } + + if ( + typeof limits[name] !== 'number' || + isNaN(limits[name]) + ) { throw new TypeError('Limit ' + name + ' is not a valid number') } + + return limits[name] +} + + +/***/ }), + +/***/ 8929: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; +/* eslint-disable object-property-newline */ + + +const decodeText = __nccwpck_require__(2747) + +const RE_ENCODED = /%[a-fA-F0-9][a-fA-F0-9]/g + +const EncodedLookup = { + '%00': '\x00', '%01': '\x01', '%02': '\x02', '%03': '\x03', '%04': '\x04', + '%05': '\x05', '%06': '\x06', '%07': '\x07', '%08': '\x08', '%09': '\x09', + '%0a': '\x0a', '%0A': '\x0a', '%0b': '\x0b', '%0B': '\x0b', '%0c': '\x0c', + '%0C': '\x0c', '%0d': '\x0d', '%0D': '\x0d', '%0e': '\x0e', '%0E': '\x0e', + '%0f': '\x0f', '%0F': '\x0f', '%10': '\x10', '%11': '\x11', '%12': '\x12', + '%13': '\x13', '%14': '\x14', '%15': '\x15', '%16': '\x16', '%17': '\x17', + '%18': '\x18', '%19': '\x19', '%1a': '\x1a', '%1A': '\x1a', '%1b': '\x1b', + '%1B': '\x1b', '%1c': '\x1c', '%1C': '\x1c', '%1d': '\x1d', '%1D': '\x1d', + '%1e': '\x1e', '%1E': '\x1e', '%1f': '\x1f', '%1F': '\x1f', '%20': '\x20', + '%21': '\x21', '%22': '\x22', '%23': '\x23', '%24': '\x24', '%25': '\x25', + '%26': '\x26', '%27': '\x27', '%28': '\x28', '%29': '\x29', '%2a': '\x2a', + '%2A': '\x2a', '%2b': '\x2b', '%2B': '\x2b', '%2c': '\x2c', '%2C': '\x2c', + '%2d': '\x2d', '%2D': '\x2d', '%2e': '\x2e', '%2E': '\x2e', '%2f': '\x2f', + '%2F': '\x2f', '%30': '\x30', '%31': '\x31', '%32': '\x32', '%33': '\x33', + '%34': '\x34', '%35': '\x35', '%36': '\x36', '%37': '\x37', '%38': '\x38', + '%39': '\x39', '%3a': '\x3a', '%3A': '\x3a', '%3b': '\x3b', '%3B': '\x3b', + '%3c': '\x3c', '%3C': '\x3c', '%3d': '\x3d', '%3D': '\x3d', '%3e': '\x3e', + '%3E': '\x3e', '%3f': '\x3f', '%3F': '\x3f', '%40': '\x40', '%41': '\x41', + '%42': '\x42', '%43': '\x43', '%44': '\x44', '%45': '\x45', '%46': '\x46', + '%47': '\x47', '%48': '\x48', '%49': '\x49', '%4a': '\x4a', '%4A': '\x4a', + '%4b': '\x4b', '%4B': '\x4b', '%4c': '\x4c', '%4C': '\x4c', '%4d': '\x4d', + '%4D': '\x4d', '%4e': '\x4e', '%4E': '\x4e', '%4f': '\x4f', '%4F': '\x4f', + '%50': '\x50', '%51': '\x51', '%52': '\x52', '%53': '\x53', '%54': '\x54', + '%55': '\x55', '%56': '\x56', '%57': '\x57', '%58': '\x58', '%59': '\x59', + '%5a': '\x5a', '%5A': '\x5a', '%5b': '\x5b', '%5B': '\x5b', '%5c': '\x5c', + '%5C': '\x5c', '%5d': '\x5d', '%5D': '\x5d', '%5e': '\x5e', '%5E': '\x5e', + '%5f': '\x5f', '%5F': '\x5f', '%60': '\x60', '%61': '\x61', '%62': '\x62', + '%63': '\x63', '%64': '\x64', '%65': '\x65', '%66': '\x66', '%67': '\x67', + '%68': '\x68', '%69': '\x69', '%6a': '\x6a', '%6A': '\x6a', '%6b': '\x6b', + '%6B': '\x6b', '%6c': '\x6c', '%6C': '\x6c', '%6d': '\x6d', '%6D': '\x6d', + '%6e': '\x6e', '%6E': '\x6e', '%6f': '\x6f', '%6F': '\x6f', '%70': '\x70', + '%71': '\x71', '%72': '\x72', '%73': '\x73', '%74': '\x74', '%75': '\x75', + '%76': '\x76', '%77': '\x77', '%78': '\x78', '%79': '\x79', '%7a': '\x7a', + '%7A': '\x7a', '%7b': '\x7b', '%7B': '\x7b', '%7c': '\x7c', '%7C': '\x7c', + '%7d': '\x7d', '%7D': '\x7d', '%7e': '\x7e', '%7E': '\x7e', '%7f': '\x7f', + '%7F': '\x7f', '%80': '\x80', '%81': '\x81', '%82': '\x82', '%83': '\x83', + '%84': '\x84', '%85': '\x85', '%86': '\x86', '%87': '\x87', '%88': '\x88', + '%89': '\x89', '%8a': '\x8a', '%8A': '\x8a', '%8b': '\x8b', '%8B': '\x8b', + '%8c': '\x8c', '%8C': '\x8c', '%8d': '\x8d', '%8D': '\x8d', '%8e': '\x8e', + '%8E': '\x8e', '%8f': '\x8f', '%8F': '\x8f', '%90': '\x90', '%91': '\x91', + '%92': '\x92', '%93': '\x93', '%94': '\x94', '%95': '\x95', '%96': '\x96', + '%97': '\x97', '%98': '\x98', '%99': '\x99', '%9a': '\x9a', '%9A': '\x9a', + '%9b': '\x9b', '%9B': '\x9b', '%9c': '\x9c', '%9C': '\x9c', '%9d': '\x9d', + '%9D': '\x9d', '%9e': '\x9e', '%9E': '\x9e', '%9f': '\x9f', '%9F': '\x9f', + '%a0': '\xa0', '%A0': '\xa0', '%a1': '\xa1', '%A1': '\xa1', '%a2': '\xa2', + '%A2': '\xa2', '%a3': '\xa3', '%A3': '\xa3', '%a4': '\xa4', '%A4': '\xa4', + '%a5': '\xa5', '%A5': '\xa5', '%a6': '\xa6', '%A6': '\xa6', '%a7': '\xa7', + '%A7': '\xa7', '%a8': '\xa8', '%A8': '\xa8', '%a9': '\xa9', '%A9': '\xa9', + '%aa': '\xaa', '%Aa': '\xaa', '%aA': '\xaa', '%AA': '\xaa', '%ab': '\xab', + '%Ab': '\xab', '%aB': '\xab', '%AB': '\xab', '%ac': '\xac', '%Ac': '\xac', + '%aC': '\xac', '%AC': '\xac', '%ad': '\xad', '%Ad': '\xad', '%aD': '\xad', + '%AD': '\xad', '%ae': '\xae', '%Ae': '\xae', '%aE': '\xae', '%AE': '\xae', + '%af': '\xaf', '%Af': '\xaf', '%aF': '\xaf', '%AF': '\xaf', '%b0': '\xb0', + '%B0': '\xb0', '%b1': '\xb1', '%B1': '\xb1', '%b2': '\xb2', '%B2': '\xb2', + '%b3': '\xb3', '%B3': '\xb3', '%b4': '\xb4', '%B4': '\xb4', '%b5': '\xb5', + '%B5': '\xb5', '%b6': '\xb6', '%B6': '\xb6', '%b7': '\xb7', '%B7': '\xb7', + '%b8': '\xb8', '%B8': '\xb8', '%b9': '\xb9', '%B9': '\xb9', '%ba': '\xba', + '%Ba': '\xba', '%bA': '\xba', '%BA': '\xba', '%bb': '\xbb', '%Bb': '\xbb', + '%bB': '\xbb', '%BB': '\xbb', '%bc': '\xbc', '%Bc': '\xbc', '%bC': '\xbc', + '%BC': '\xbc', '%bd': '\xbd', '%Bd': '\xbd', '%bD': '\xbd', '%BD': '\xbd', + '%be': '\xbe', '%Be': '\xbe', '%bE': '\xbe', '%BE': '\xbe', '%bf': '\xbf', + '%Bf': '\xbf', '%bF': '\xbf', '%BF': '\xbf', '%c0': '\xc0', '%C0': '\xc0', + '%c1': '\xc1', '%C1': '\xc1', '%c2': '\xc2', '%C2': '\xc2', '%c3': '\xc3', + '%C3': '\xc3', '%c4': '\xc4', '%C4': '\xc4', '%c5': '\xc5', '%C5': '\xc5', + '%c6': '\xc6', '%C6': '\xc6', '%c7': '\xc7', '%C7': '\xc7', '%c8': '\xc8', + '%C8': '\xc8', '%c9': '\xc9', '%C9': '\xc9', '%ca': '\xca', '%Ca': '\xca', + '%cA': '\xca', '%CA': '\xca', '%cb': '\xcb', '%Cb': '\xcb', '%cB': '\xcb', + '%CB': '\xcb', '%cc': '\xcc', '%Cc': '\xcc', '%cC': '\xcc', '%CC': '\xcc', + '%cd': '\xcd', '%Cd': '\xcd', '%cD': '\xcd', '%CD': '\xcd', '%ce': '\xce', + '%Ce': '\xce', '%cE': '\xce', '%CE': '\xce', '%cf': '\xcf', '%Cf': '\xcf', + '%cF': '\xcf', '%CF': '\xcf', '%d0': '\xd0', '%D0': '\xd0', '%d1': '\xd1', + '%D1': '\xd1', '%d2': '\xd2', '%D2': '\xd2', '%d3': '\xd3', '%D3': '\xd3', + '%d4': '\xd4', '%D4': '\xd4', '%d5': '\xd5', '%D5': '\xd5', '%d6': '\xd6', + '%D6': '\xd6', '%d7': '\xd7', '%D7': '\xd7', '%d8': '\xd8', '%D8': '\xd8', + '%d9': '\xd9', '%D9': '\xd9', '%da': '\xda', '%Da': '\xda', '%dA': '\xda', + '%DA': '\xda', '%db': '\xdb', '%Db': '\xdb', '%dB': '\xdb', '%DB': '\xdb', + '%dc': '\xdc', '%Dc': '\xdc', '%dC': '\xdc', '%DC': '\xdc', '%dd': '\xdd', + '%Dd': '\xdd', '%dD': '\xdd', '%DD': '\xdd', '%de': '\xde', '%De': '\xde', + '%dE': '\xde', '%DE': '\xde', '%df': '\xdf', '%Df': '\xdf', '%dF': '\xdf', + '%DF': '\xdf', '%e0': '\xe0', '%E0': '\xe0', '%e1': '\xe1', '%E1': '\xe1', + '%e2': '\xe2', '%E2': '\xe2', '%e3': '\xe3', '%E3': '\xe3', '%e4': '\xe4', + '%E4': '\xe4', '%e5': '\xe5', '%E5': '\xe5', '%e6': '\xe6', '%E6': '\xe6', + '%e7': '\xe7', '%E7': '\xe7', '%e8': '\xe8', '%E8': '\xe8', '%e9': '\xe9', + '%E9': '\xe9', '%ea': '\xea', '%Ea': '\xea', '%eA': '\xea', '%EA': '\xea', + '%eb': '\xeb', '%Eb': '\xeb', '%eB': '\xeb', '%EB': '\xeb', '%ec': '\xec', + '%Ec': '\xec', '%eC': '\xec', '%EC': '\xec', '%ed': '\xed', '%Ed': '\xed', + '%eD': '\xed', '%ED': '\xed', '%ee': '\xee', '%Ee': '\xee', '%eE': '\xee', + '%EE': '\xee', '%ef': '\xef', '%Ef': '\xef', '%eF': '\xef', '%EF': '\xef', + '%f0': '\xf0', '%F0': '\xf0', '%f1': '\xf1', '%F1': '\xf1', '%f2': '\xf2', + '%F2': '\xf2', '%f3': '\xf3', '%F3': '\xf3', '%f4': '\xf4', '%F4': '\xf4', + '%f5': '\xf5', '%F5': '\xf5', '%f6': '\xf6', '%F6': '\xf6', '%f7': '\xf7', + '%F7': '\xf7', '%f8': '\xf8', '%F8': '\xf8', '%f9': '\xf9', '%F9': '\xf9', + '%fa': '\xfa', '%Fa': '\xfa', '%fA': '\xfa', '%FA': '\xfa', '%fb': '\xfb', + '%Fb': '\xfb', '%fB': '\xfb', '%FB': '\xfb', '%fc': '\xfc', '%Fc': '\xfc', + '%fC': '\xfc', '%FC': '\xfc', '%fd': '\xfd', '%Fd': '\xfd', '%fD': '\xfd', + '%FD': '\xfd', '%fe': '\xfe', '%Fe': '\xfe', '%fE': '\xfe', '%FE': '\xfe', + '%ff': '\xff', '%Ff': '\xff', '%fF': '\xff', '%FF': '\xff' +} + +function encodedReplacer (match) { + return EncodedLookup[match] +} + +const STATE_KEY = 0 +const STATE_VALUE = 1 +const STATE_CHARSET = 2 +const STATE_LANG = 3 + +function parseParams (str) { + const res = [] + let state = STATE_KEY + let charset = '' + let inquote = false + let escaping = false + let p = 0 + let tmp = '' + const len = str.length + + for (var i = 0; i < len; ++i) { // eslint-disable-line no-var + const char = str[i] + if (char === '\\' && inquote) { + if (escaping) { escaping = false } else { + escaping = true + continue + } + } else if (char === '"') { + if (!escaping) { + if (inquote) { + inquote = false + state = STATE_KEY + } else { inquote = true } + continue + } else { escaping = false } + } else { + if (escaping && inquote) { tmp += '\\' } + escaping = false + if ((state === STATE_CHARSET || state === STATE_LANG) && char === "'") { + if (state === STATE_CHARSET) { + state = STATE_LANG + charset = tmp.substring(1) + } else { state = STATE_VALUE } + tmp = '' + continue + } else if (state === STATE_KEY && + (char === '*' || char === '=') && + res.length) { + state = char === '*' + ? STATE_CHARSET + : STATE_VALUE + res[p] = [tmp, undefined] + tmp = '' + continue + } else if (!inquote && char === ';') { + state = STATE_KEY + if (charset) { + if (tmp.length) { + tmp = decodeText(tmp.replace(RE_ENCODED, encodedReplacer), + 'binary', + charset) + } + charset = '' + } else if (tmp.length) { + tmp = decodeText(tmp, 'binary', 'utf8') + } + if (res[p] === undefined) { res[p] = tmp } else { res[p][1] = tmp } + tmp = '' + ++p + continue + } else if (!inquote && (char === ' ' || char === '\t')) { continue } + } + tmp += char + } + if (charset && tmp.length) { + tmp = decodeText(tmp.replace(RE_ENCODED, encodedReplacer), + 'binary', + charset) + } else if (tmp) { + tmp = decodeText(tmp, 'binary', 'utf8') + } + + if (res[p] === undefined) { + if (tmp) { res[p] = tmp } + } else { res[p][1] = tmp } + + return res +} + +module.exports = parseParams + + +/***/ }), + +/***/ 2472: +/***/ ((module) => { + +"use strict"; +module.exports = /*#__PURE__*/JSON.parse('[[[0,44],"disallowed_STD3_valid"],[[45,46],"valid"],[[47,47],"disallowed_STD3_valid"],[[48,57],"valid"],[[58,64],"disallowed_STD3_valid"],[[65,65],"mapped",[97]],[[66,66],"mapped",[98]],[[67,67],"mapped",[99]],[[68,68],"mapped",[100]],[[69,69],"mapped",[101]],[[70,70],"mapped",[102]],[[71,71],"mapped",[103]],[[72,72],"mapped",[104]],[[73,73],"mapped",[105]],[[74,74],"mapped",[106]],[[75,75],"mapped",[107]],[[76,76],"mapped",[108]],[[77,77],"mapped",[109]],[[78,78],"mapped",[110]],[[79,79],"mapped",[111]],[[80,80],"mapped",[112]],[[81,81],"mapped",[113]],[[82,82],"mapped",[114]],[[83,83],"mapped",[115]],[[84,84],"mapped",[116]],[[85,85],"mapped",[117]],[[86,86],"mapped",[118]],[[87,87],"mapped",[119]],[[88,88],"mapped",[120]],[[89,89],"mapped",[121]],[[90,90],"mapped",[122]],[[91,96],"disallowed_STD3_valid"],[[97,122],"valid"],[[123,127],"disallowed_STD3_valid"],[[128,159],"disallowed"],[[160,160],"disallowed_STD3_mapped",[32]],[[161,167],"valid",[],"NV8"],[[168,168],"disallowed_STD3_mapped",[32,776]],[[169,169],"valid",[],"NV8"],[[170,170],"mapped",[97]],[[171,172],"valid",[],"NV8"],[[173,173],"ignored"],[[174,174],"valid",[],"NV8"],[[175,175],"disallowed_STD3_mapped",[32,772]],[[176,177],"valid",[],"NV8"],[[178,178],"mapped",[50]],[[179,179],"mapped",[51]],[[180,180],"disallowed_STD3_mapped",[32,769]],[[181,181],"mapped",[956]],[[182,182],"valid",[],"NV8"],[[183,183],"valid"],[[184,184],"disallowed_STD3_mapped",[32,807]],[[185,185],"mapped",[49]],[[186,186],"mapped",[111]],[[187,187],"valid",[],"NV8"],[[188,188],"mapped",[49,8260,52]],[[189,189],"mapped",[49,8260,50]],[[190,190],"mapped",[51,8260,52]],[[191,191],"valid",[],"NV8"],[[192,192],"mapped",[224]],[[193,193],"mapped",[225]],[[194,194],"mapped",[226]],[[195,195],"mapped",[227]],[[196,196],"mapped",[228]],[[197,197],"mapped",[229]],[[198,198],"mapped",[230]],[[199,199],"mapped",[231]],[[200,200],"mapped",[232]],[[201,201],"mapped",[233]],[[202,202],"mapped",[234]],[[203,203],"mapped",[235]],[[204,204],"mapped",[236]],[[205,205],"mapped",[237]],[[206,206],"mapped",[238]],[[207,207],"mapped",[239]],[[208,208],"mapped",[240]],[[209,209],"mapped",[241]],[[210,210],"mapped",[242]],[[211,211],"mapped",[243]],[[212,212],"mapped",[244]],[[213,213],"mapped",[245]],[[214,214],"mapped",[246]],[[215,215],"valid",[],"NV8"],[[216,216],"mapped",[248]],[[217,217],"mapped",[249]],[[218,218],"mapped",[250]],[[219,219],"mapped",[251]],[[220,220],"mapped",[252]],[[221,221],"mapped",[253]],[[222,222],"mapped",[254]],[[223,223],"deviation",[115,115]],[[224,246],"valid"],[[247,247],"valid",[],"NV8"],[[248,255],"valid"],[[256,256],"mapped",[257]],[[257,257],"valid"],[[258,258],"mapped",[259]],[[259,259],"valid"],[[260,260],"mapped",[261]],[[261,261],"valid"],[[262,262],"mapped",[263]],[[263,263],"valid"],[[264,264],"mapped",[265]],[[265,265],"valid"],[[266,266],"mapped",[267]],[[267,267],"valid"],[[268,268],"mapped",[269]],[[269,269],"valid"],[[270,270],"mapped",[271]],[[271,271],"valid"],[[272,272],"mapped",[273]],[[273,273],"valid"],[[274,274],"mapped",[275]],[[275,275],"valid"],[[276,276],"mapped",[277]],[[277,277],"valid"],[[278,278],"mapped",[279]],[[279,279],"valid"],[[280,280],"mapped",[281]],[[281,281],"valid"],[[282,282],"mapped",[283]],[[283,283],"valid"],[[284,284],"mapped",[285]],[[285,285],"valid"],[[286,286],"mapped",[287]],[[287,287],"valid"],[[288,288],"mapped",[289]],[[289,289],"valid"],[[290,290],"mapped",[291]],[[291,291],"valid"],[[292,292],"mapped",[293]],[[293,293],"valid"],[[294,294],"mapped",[295]],[[295,295],"valid"],[[296,296],"mapped",[297]],[[297,297],"valid"],[[298,298],"mapped",[299]],[[299,299],"valid"],[[300,300],"mapped",[301]],[[301,301],"valid"],[[302,302],"mapped",[303]],[[303,303],"valid"],[[304,304],"mapped",[105,775]],[[305,305],"valid"],[[306,307],"mapped",[105,106]],[[308,308],"mapped",[309]],[[309,309],"valid"],[[310,310],"mapped",[311]],[[311,312],"valid"],[[313,313],"mapped",[314]],[[314,314],"valid"],[[315,315],"mapped",[316]],[[316,316],"valid"],[[317,317],"mapped",[318]],[[318,318],"valid"],[[319,320],"mapped",[108,183]],[[321,321],"mapped",[322]],[[322,322],"valid"],[[323,323],"mapped",[324]],[[324,324],"valid"],[[325,325],"mapped",[326]],[[326,326],"valid"],[[327,327],"mapped",[328]],[[328,328],"valid"],[[329,329],"mapped",[700,110]],[[330,330],"mapped",[331]],[[331,331],"valid"],[[332,332],"mapped",[333]],[[333,333],"valid"],[[334,334],"mapped",[335]],[[335,335],"valid"],[[336,336],"mapped",[337]],[[337,337],"valid"],[[338,338],"mapped",[339]],[[339,339],"valid"],[[340,340],"mapped",[341]],[[341,341],"valid"],[[342,342],"mapped",[343]],[[343,343],"valid"],[[344,344],"mapped",[345]],[[345,345],"valid"],[[346,346],"mapped",[347]],[[347,347],"valid"],[[348,348],"mapped",[349]],[[349,349],"valid"],[[350,350],"mapped",[351]],[[351,351],"valid"],[[352,352],"mapped",[353]],[[353,353],"valid"],[[354,354],"mapped",[355]],[[355,355],"valid"],[[356,356],"mapped",[357]],[[357,357],"valid"],[[358,358],"mapped",[359]],[[359,359],"valid"],[[360,360],"mapped",[361]],[[361,361],"valid"],[[362,362],"mapped",[363]],[[363,363],"valid"],[[364,364],"mapped",[365]],[[365,365],"valid"],[[366,366],"mapped",[367]],[[367,367],"valid"],[[368,368],"mapped",[369]],[[369,369],"valid"],[[370,370],"mapped",[371]],[[371,371],"valid"],[[372,372],"mapped",[373]],[[373,373],"valid"],[[374,374],"mapped",[375]],[[375,375],"valid"],[[376,376],"mapped",[255]],[[377,377],"mapped",[378]],[[378,378],"valid"],[[379,379],"mapped",[380]],[[380,380],"valid"],[[381,381],"mapped",[382]],[[382,382],"valid"],[[383,383],"mapped",[115]],[[384,384],"valid"],[[385,385],"mapped",[595]],[[386,386],"mapped",[387]],[[387,387],"valid"],[[388,388],"mapped",[389]],[[389,389],"valid"],[[390,390],"mapped",[596]],[[391,391],"mapped",[392]],[[392,392],"valid"],[[393,393],"mapped",[598]],[[394,394],"mapped",[599]],[[395,395],"mapped",[396]],[[396,397],"valid"],[[398,398],"mapped",[477]],[[399,399],"mapped",[601]],[[400,400],"mapped",[603]],[[401,401],"mapped",[402]],[[402,402],"valid"],[[403,403],"mapped",[608]],[[404,404],"mapped",[611]],[[405,405],"valid"],[[406,406],"mapped",[617]],[[407,407],"mapped",[616]],[[408,408],"mapped",[409]],[[409,411],"valid"],[[412,412],"mapped",[623]],[[413,413],"mapped",[626]],[[414,414],"valid"],[[415,415],"mapped",[629]],[[416,416],"mapped",[417]],[[417,417],"valid"],[[418,418],"mapped",[419]],[[419,419],"valid"],[[420,420],"mapped",[421]],[[421,421],"valid"],[[422,422],"mapped",[640]],[[423,423],"mapped",[424]],[[424,424],"valid"],[[425,425],"mapped",[643]],[[426,427],"valid"],[[428,428],"mapped",[429]],[[429,429],"valid"],[[430,430],"mapped",[648]],[[431,431],"mapped",[432]],[[432,432],"valid"],[[433,433],"mapped",[650]],[[434,434],"mapped",[651]],[[435,435],"mapped",[436]],[[436,436],"valid"],[[437,437],"mapped",[438]],[[438,438],"valid"],[[439,439],"mapped",[658]],[[440,440],"mapped",[441]],[[441,443],"valid"],[[444,444],"mapped",[445]],[[445,451],"valid"],[[452,454],"mapped",[100,382]],[[455,457],"mapped",[108,106]],[[458,460],"mapped",[110,106]],[[461,461],"mapped",[462]],[[462,462],"valid"],[[463,463],"mapped",[464]],[[464,464],"valid"],[[465,465],"mapped",[466]],[[466,466],"valid"],[[467,467],"mapped",[468]],[[468,468],"valid"],[[469,469],"mapped",[470]],[[470,470],"valid"],[[471,471],"mapped",[472]],[[472,472],"valid"],[[473,473],"mapped",[474]],[[474,474],"valid"],[[475,475],"mapped",[476]],[[476,477],"valid"],[[478,478],"mapped",[479]],[[479,479],"valid"],[[480,480],"mapped",[481]],[[481,481],"valid"],[[482,482],"mapped",[483]],[[483,483],"valid"],[[484,484],"mapped",[485]],[[485,485],"valid"],[[486,486],"mapped",[487]],[[487,487],"valid"],[[488,488],"mapped",[489]],[[489,489],"valid"],[[490,490],"mapped",[491]],[[491,491],"valid"],[[492,492],"mapped",[493]],[[493,493],"valid"],[[494,494],"mapped",[495]],[[495,496],"valid"],[[497,499],"mapped",[100,122]],[[500,500],"mapped",[501]],[[501,501],"valid"],[[502,502],"mapped",[405]],[[503,503],"mapped",[447]],[[504,504],"mapped",[505]],[[505,505],"valid"],[[506,506],"mapped",[507]],[[507,507],"valid"],[[508,508],"mapped",[509]],[[509,509],"valid"],[[510,510],"mapped",[511]],[[511,511],"valid"],[[512,512],"mapped",[513]],[[513,513],"valid"],[[514,514],"mapped",[515]],[[515,515],"valid"],[[516,516],"mapped",[517]],[[517,517],"valid"],[[518,518],"mapped",[519]],[[519,519],"valid"],[[520,520],"mapped",[521]],[[521,521],"valid"],[[522,522],"mapped",[523]],[[523,523],"valid"],[[524,524],"mapped",[525]],[[525,525],"valid"],[[526,526],"mapped",[527]],[[527,527],"valid"],[[528,528],"mapped",[529]],[[529,529],"valid"],[[530,530],"mapped",[531]],[[531,531],"valid"],[[532,532],"mapped",[533]],[[533,533],"valid"],[[534,534],"mapped",[535]],[[535,535],"valid"],[[536,536],"mapped",[537]],[[537,537],"valid"],[[538,538],"mapped",[539]],[[539,539],"valid"],[[540,540],"mapped",[541]],[[541,541],"valid"],[[542,542],"mapped",[543]],[[543,543],"valid"],[[544,544],"mapped",[414]],[[545,545],"valid"],[[546,546],"mapped",[547]],[[547,547],"valid"],[[548,548],"mapped",[549]],[[549,549],"valid"],[[550,550],"mapped",[551]],[[551,551],"valid"],[[552,552],"mapped",[553]],[[553,553],"valid"],[[554,554],"mapped",[555]],[[555,555],"valid"],[[556,556],"mapped",[557]],[[557,557],"valid"],[[558,558],"mapped",[559]],[[559,559],"valid"],[[560,560],"mapped",[561]],[[561,561],"valid"],[[562,562],"mapped",[563]],[[563,563],"valid"],[[564,566],"valid"],[[567,569],"valid"],[[570,570],"mapped",[11365]],[[571,571],"mapped",[572]],[[572,572],"valid"],[[573,573],"mapped",[410]],[[574,574],"mapped",[11366]],[[575,576],"valid"],[[577,577],"mapped",[578]],[[578,578],"valid"],[[579,579],"mapped",[384]],[[580,580],"mapped",[649]],[[581,581],"mapped",[652]],[[582,582],"mapped",[583]],[[583,583],"valid"],[[584,584],"mapped",[585]],[[585,585],"valid"],[[586,586],"mapped",[587]],[[587,587],"valid"],[[588,588],"mapped",[589]],[[589,589],"valid"],[[590,590],"mapped",[591]],[[591,591],"valid"],[[592,680],"valid"],[[681,685],"valid"],[[686,687],"valid"],[[688,688],"mapped",[104]],[[689,689],"mapped",[614]],[[690,690],"mapped",[106]],[[691,691],"mapped",[114]],[[692,692],"mapped",[633]],[[693,693],"mapped",[635]],[[694,694],"mapped",[641]],[[695,695],"mapped",[119]],[[696,696],"mapped",[121]],[[697,705],"valid"],[[706,709],"valid",[],"NV8"],[[710,721],"valid"],[[722,727],"valid",[],"NV8"],[[728,728],"disallowed_STD3_mapped",[32,774]],[[729,729],"disallowed_STD3_mapped",[32,775]],[[730,730],"disallowed_STD3_mapped",[32,778]],[[731,731],"disallowed_STD3_mapped",[32,808]],[[732,732],"disallowed_STD3_mapped",[32,771]],[[733,733],"disallowed_STD3_mapped",[32,779]],[[734,734],"valid",[],"NV8"],[[735,735],"valid",[],"NV8"],[[736,736],"mapped",[611]],[[737,737],"mapped",[108]],[[738,738],"mapped",[115]],[[739,739],"mapped",[120]],[[740,740],"mapped",[661]],[[741,745],"valid",[],"NV8"],[[746,747],"valid",[],"NV8"],[[748,748],"valid"],[[749,749],"valid",[],"NV8"],[[750,750],"valid"],[[751,767],"valid",[],"NV8"],[[768,831],"valid"],[[832,832],"mapped",[768]],[[833,833],"mapped",[769]],[[834,834],"valid"],[[835,835],"mapped",[787]],[[836,836],"mapped",[776,769]],[[837,837],"mapped",[953]],[[838,846],"valid"],[[847,847],"ignored"],[[848,855],"valid"],[[856,860],"valid"],[[861,863],"valid"],[[864,865],"valid"],[[866,866],"valid"],[[867,879],"valid"],[[880,880],"mapped",[881]],[[881,881],"valid"],[[882,882],"mapped",[883]],[[883,883],"valid"],[[884,884],"mapped",[697]],[[885,885],"valid"],[[886,886],"mapped",[887]],[[887,887],"valid"],[[888,889],"disallowed"],[[890,890],"disallowed_STD3_mapped",[32,953]],[[891,893],"valid"],[[894,894],"disallowed_STD3_mapped",[59]],[[895,895],"mapped",[1011]],[[896,899],"disallowed"],[[900,900],"disallowed_STD3_mapped",[32,769]],[[901,901],"disallowed_STD3_mapped",[32,776,769]],[[902,902],"mapped",[940]],[[903,903],"mapped",[183]],[[904,904],"mapped",[941]],[[905,905],"mapped",[942]],[[906,906],"mapped",[943]],[[907,907],"disallowed"],[[908,908],"mapped",[972]],[[909,909],"disallowed"],[[910,910],"mapped",[973]],[[911,911],"mapped",[974]],[[912,912],"valid"],[[913,913],"mapped",[945]],[[914,914],"mapped",[946]],[[915,915],"mapped",[947]],[[916,916],"mapped",[948]],[[917,917],"mapped",[949]],[[918,918],"mapped",[950]],[[919,919],"mapped",[951]],[[920,920],"mapped",[952]],[[921,921],"mapped",[953]],[[922,922],"mapped",[954]],[[923,923],"mapped",[955]],[[924,924],"mapped",[956]],[[925,925],"mapped",[957]],[[926,926],"mapped",[958]],[[927,927],"mapped",[959]],[[928,928],"mapped",[960]],[[929,929],"mapped",[961]],[[930,930],"disallowed"],[[931,931],"mapped",[963]],[[932,932],"mapped",[964]],[[933,933],"mapped",[965]],[[934,934],"mapped",[966]],[[935,935],"mapped",[967]],[[936,936],"mapped",[968]],[[937,937],"mapped",[969]],[[938,938],"mapped",[970]],[[939,939],"mapped",[971]],[[940,961],"valid"],[[962,962],"deviation",[963]],[[963,974],"valid"],[[975,975],"mapped",[983]],[[976,976],"mapped",[946]],[[977,977],"mapped",[952]],[[978,978],"mapped",[965]],[[979,979],"mapped",[973]],[[980,980],"mapped",[971]],[[981,981],"mapped",[966]],[[982,982],"mapped",[960]],[[983,983],"valid"],[[984,984],"mapped",[985]],[[985,985],"valid"],[[986,986],"mapped",[987]],[[987,987],"valid"],[[988,988],"mapped",[989]],[[989,989],"valid"],[[990,990],"mapped",[991]],[[991,991],"valid"],[[992,992],"mapped",[993]],[[993,993],"valid"],[[994,994],"mapped",[995]],[[995,995],"valid"],[[996,996],"mapped",[997]],[[997,997],"valid"],[[998,998],"mapped",[999]],[[999,999],"valid"],[[1000,1000],"mapped",[1001]],[[1001,1001],"valid"],[[1002,1002],"mapped",[1003]],[[1003,1003],"valid"],[[1004,1004],"mapped",[1005]],[[1005,1005],"valid"],[[1006,1006],"mapped",[1007]],[[1007,1007],"valid"],[[1008,1008],"mapped",[954]],[[1009,1009],"mapped",[961]],[[1010,1010],"mapped",[963]],[[1011,1011],"valid"],[[1012,1012],"mapped",[952]],[[1013,1013],"mapped",[949]],[[1014,1014],"valid",[],"NV8"],[[1015,1015],"mapped",[1016]],[[1016,1016],"valid"],[[1017,1017],"mapped",[963]],[[1018,1018],"mapped",[1019]],[[1019,1019],"valid"],[[1020,1020],"valid"],[[1021,1021],"mapped",[891]],[[1022,1022],"mapped",[892]],[[1023,1023],"mapped",[893]],[[1024,1024],"mapped",[1104]],[[1025,1025],"mapped",[1105]],[[1026,1026],"mapped",[1106]],[[1027,1027],"mapped",[1107]],[[1028,1028],"mapped",[1108]],[[1029,1029],"mapped",[1109]],[[1030,1030],"mapped",[1110]],[[1031,1031],"mapped",[1111]],[[1032,1032],"mapped",[1112]],[[1033,1033],"mapped",[1113]],[[1034,1034],"mapped",[1114]],[[1035,1035],"mapped",[1115]],[[1036,1036],"mapped",[1116]],[[1037,1037],"mapped",[1117]],[[1038,1038],"mapped",[1118]],[[1039,1039],"mapped",[1119]],[[1040,1040],"mapped",[1072]],[[1041,1041],"mapped",[1073]],[[1042,1042],"mapped",[1074]],[[1043,1043],"mapped",[1075]],[[1044,1044],"mapped",[1076]],[[1045,1045],"mapped",[1077]],[[1046,1046],"mapped",[1078]],[[1047,1047],"mapped",[1079]],[[1048,1048],"mapped",[1080]],[[1049,1049],"mapped",[1081]],[[1050,1050],"mapped",[1082]],[[1051,1051],"mapped",[1083]],[[1052,1052],"mapped",[1084]],[[1053,1053],"mapped",[1085]],[[1054,1054],"mapped",[1086]],[[1055,1055],"mapped",[1087]],[[1056,1056],"mapped",[1088]],[[1057,1057],"mapped",[1089]],[[1058,1058],"mapped",[1090]],[[1059,1059],"mapped",[1091]],[[1060,1060],"mapped",[1092]],[[1061,1061],"mapped",[1093]],[[1062,1062],"mapped",[1094]],[[1063,1063],"mapped",[1095]],[[1064,1064],"mapped",[1096]],[[1065,1065],"mapped",[1097]],[[1066,1066],"mapped",[1098]],[[1067,1067],"mapped",[1099]],[[1068,1068],"mapped",[1100]],[[1069,1069],"mapped",[1101]],[[1070,1070],"mapped",[1102]],[[1071,1071],"mapped",[1103]],[[1072,1103],"valid"],[[1104,1104],"valid"],[[1105,1116],"valid"],[[1117,1117],"valid"],[[1118,1119],"valid"],[[1120,1120],"mapped",[1121]],[[1121,1121],"valid"],[[1122,1122],"mapped",[1123]],[[1123,1123],"valid"],[[1124,1124],"mapped",[1125]],[[1125,1125],"valid"],[[1126,1126],"mapped",[1127]],[[1127,1127],"valid"],[[1128,1128],"mapped",[1129]],[[1129,1129],"valid"],[[1130,1130],"mapped",[1131]],[[1131,1131],"valid"],[[1132,1132],"mapped",[1133]],[[1133,1133],"valid"],[[1134,1134],"mapped",[1135]],[[1135,1135],"valid"],[[1136,1136],"mapped",[1137]],[[1137,1137],"valid"],[[1138,1138],"mapped",[1139]],[[1139,1139],"valid"],[[1140,1140],"mapped",[1141]],[[1141,1141],"valid"],[[1142,1142],"mapped",[1143]],[[1143,1143],"valid"],[[1144,1144],"mapped",[1145]],[[1145,1145],"valid"],[[1146,1146],"mapped",[1147]],[[1147,1147],"valid"],[[1148,1148],"mapped",[1149]],[[1149,1149],"valid"],[[1150,1150],"mapped",[1151]],[[1151,1151],"valid"],[[1152,1152],"mapped",[1153]],[[1153,1153],"valid"],[[1154,1154],"valid",[],"NV8"],[[1155,1158],"valid"],[[1159,1159],"valid"],[[1160,1161],"valid",[],"NV8"],[[1162,1162],"mapped",[1163]],[[1163,1163],"valid"],[[1164,1164],"mapped",[1165]],[[1165,1165],"valid"],[[1166,1166],"mapped",[1167]],[[1167,1167],"valid"],[[1168,1168],"mapped",[1169]],[[1169,1169],"valid"],[[1170,1170],"mapped",[1171]],[[1171,1171],"valid"],[[1172,1172],"mapped",[1173]],[[1173,1173],"valid"],[[1174,1174],"mapped",[1175]],[[1175,1175],"valid"],[[1176,1176],"mapped",[1177]],[[1177,1177],"valid"],[[1178,1178],"mapped",[1179]],[[1179,1179],"valid"],[[1180,1180],"mapped",[1181]],[[1181,1181],"valid"],[[1182,1182],"mapped",[1183]],[[1183,1183],"valid"],[[1184,1184],"mapped",[1185]],[[1185,1185],"valid"],[[1186,1186],"mapped",[1187]],[[1187,1187],"valid"],[[1188,1188],"mapped",[1189]],[[1189,1189],"valid"],[[1190,1190],"mapped",[1191]],[[1191,1191],"valid"],[[1192,1192],"mapped",[1193]],[[1193,1193],"valid"],[[1194,1194],"mapped",[1195]],[[1195,1195],"valid"],[[1196,1196],"mapped",[1197]],[[1197,1197],"valid"],[[1198,1198],"mapped",[1199]],[[1199,1199],"valid"],[[1200,1200],"mapped",[1201]],[[1201,1201],"valid"],[[1202,1202],"mapped",[1203]],[[1203,1203],"valid"],[[1204,1204],"mapped",[1205]],[[1205,1205],"valid"],[[1206,1206],"mapped",[1207]],[[1207,1207],"valid"],[[1208,1208],"mapped",[1209]],[[1209,1209],"valid"],[[1210,1210],"mapped",[1211]],[[1211,1211],"valid"],[[1212,1212],"mapped",[1213]],[[1213,1213],"valid"],[[1214,1214],"mapped",[1215]],[[1215,1215],"valid"],[[1216,1216],"disallowed"],[[1217,1217],"mapped",[1218]],[[1218,1218],"valid"],[[1219,1219],"mapped",[1220]],[[1220,1220],"valid"],[[1221,1221],"mapped",[1222]],[[1222,1222],"valid"],[[1223,1223],"mapped",[1224]],[[1224,1224],"valid"],[[1225,1225],"mapped",[1226]],[[1226,1226],"valid"],[[1227,1227],"mapped",[1228]],[[1228,1228],"valid"],[[1229,1229],"mapped",[1230]],[[1230,1230],"valid"],[[1231,1231],"valid"],[[1232,1232],"mapped",[1233]],[[1233,1233],"valid"],[[1234,1234],"mapped",[1235]],[[1235,1235],"valid"],[[1236,1236],"mapped",[1237]],[[1237,1237],"valid"],[[1238,1238],"mapped",[1239]],[[1239,1239],"valid"],[[1240,1240],"mapped",[1241]],[[1241,1241],"valid"],[[1242,1242],"mapped",[1243]],[[1243,1243],"valid"],[[1244,1244],"mapped",[1245]],[[1245,1245],"valid"],[[1246,1246],"mapped",[1247]],[[1247,1247],"valid"],[[1248,1248],"mapped",[1249]],[[1249,1249],"valid"],[[1250,1250],"mapped",[1251]],[[1251,1251],"valid"],[[1252,1252],"mapped",[1253]],[[1253,1253],"valid"],[[1254,1254],"mapped",[1255]],[[1255,1255],"valid"],[[1256,1256],"mapped",[1257]],[[1257,1257],"valid"],[[1258,1258],"mapped",[1259]],[[1259,1259],"valid"],[[1260,1260],"mapped",[1261]],[[1261,1261],"valid"],[[1262,1262],"mapped",[1263]],[[1263,1263],"valid"],[[1264,1264],"mapped",[1265]],[[1265,1265],"valid"],[[1266,1266],"mapped",[1267]],[[1267,1267],"valid"],[[1268,1268],"mapped",[1269]],[[1269,1269],"valid"],[[1270,1270],"mapped",[1271]],[[1271,1271],"valid"],[[1272,1272],"mapped",[1273]],[[1273,1273],"valid"],[[1274,1274],"mapped",[1275]],[[1275,1275],"valid"],[[1276,1276],"mapped",[1277]],[[1277,1277],"valid"],[[1278,1278],"mapped",[1279]],[[1279,1279],"valid"],[[1280,1280],"mapped",[1281]],[[1281,1281],"valid"],[[1282,1282],"mapped",[1283]],[[1283,1283],"valid"],[[1284,1284],"mapped",[1285]],[[1285,1285],"valid"],[[1286,1286],"mapped",[1287]],[[1287,1287],"valid"],[[1288,1288],"mapped",[1289]],[[1289,1289],"valid"],[[1290,1290],"mapped",[1291]],[[1291,1291],"valid"],[[1292,1292],"mapped",[1293]],[[1293,1293],"valid"],[[1294,1294],"mapped",[1295]],[[1295,1295],"valid"],[[1296,1296],"mapped",[1297]],[[1297,1297],"valid"],[[1298,1298],"mapped",[1299]],[[1299,1299],"valid"],[[1300,1300],"mapped",[1301]],[[1301,1301],"valid"],[[1302,1302],"mapped",[1303]],[[1303,1303],"valid"],[[1304,1304],"mapped",[1305]],[[1305,1305],"valid"],[[1306,1306],"mapped",[1307]],[[1307,1307],"valid"],[[1308,1308],"mapped",[1309]],[[1309,1309],"valid"],[[1310,1310],"mapped",[1311]],[[1311,1311],"valid"],[[1312,1312],"mapped",[1313]],[[1313,1313],"valid"],[[1314,1314],"mapped",[1315]],[[1315,1315],"valid"],[[1316,1316],"mapped",[1317]],[[1317,1317],"valid"],[[1318,1318],"mapped",[1319]],[[1319,1319],"valid"],[[1320,1320],"mapped",[1321]],[[1321,1321],"valid"],[[1322,1322],"mapped",[1323]],[[1323,1323],"valid"],[[1324,1324],"mapped",[1325]],[[1325,1325],"valid"],[[1326,1326],"mapped",[1327]],[[1327,1327],"valid"],[[1328,1328],"disallowed"],[[1329,1329],"mapped",[1377]],[[1330,1330],"mapped",[1378]],[[1331,1331],"mapped",[1379]],[[1332,1332],"mapped",[1380]],[[1333,1333],"mapped",[1381]],[[1334,1334],"mapped",[1382]],[[1335,1335],"mapped",[1383]],[[1336,1336],"mapped",[1384]],[[1337,1337],"mapped",[1385]],[[1338,1338],"mapped",[1386]],[[1339,1339],"mapped",[1387]],[[1340,1340],"mapped",[1388]],[[1341,1341],"mapped",[1389]],[[1342,1342],"mapped",[1390]],[[1343,1343],"mapped",[1391]],[[1344,1344],"mapped",[1392]],[[1345,1345],"mapped",[1393]],[[1346,1346],"mapped",[1394]],[[1347,1347],"mapped",[1395]],[[1348,1348],"mapped",[1396]],[[1349,1349],"mapped",[1397]],[[1350,1350],"mapped",[1398]],[[1351,1351],"mapped",[1399]],[[1352,1352],"mapped",[1400]],[[1353,1353],"mapped",[1401]],[[1354,1354],"mapped",[1402]],[[1355,1355],"mapped",[1403]],[[1356,1356],"mapped",[1404]],[[1357,1357],"mapped",[1405]],[[1358,1358],"mapped",[1406]],[[1359,1359],"mapped",[1407]],[[1360,1360],"mapped",[1408]],[[1361,1361],"mapped",[1409]],[[1362,1362],"mapped",[1410]],[[1363,1363],"mapped",[1411]],[[1364,1364],"mapped",[1412]],[[1365,1365],"mapped",[1413]],[[1366,1366],"mapped",[1414]],[[1367,1368],"disallowed"],[[1369,1369],"valid"],[[1370,1375],"valid",[],"NV8"],[[1376,1376],"disallowed"],[[1377,1414],"valid"],[[1415,1415],"mapped",[1381,1410]],[[1416,1416],"disallowed"],[[1417,1417],"valid",[],"NV8"],[[1418,1418],"valid",[],"NV8"],[[1419,1420],"disallowed"],[[1421,1422],"valid",[],"NV8"],[[1423,1423],"valid",[],"NV8"],[[1424,1424],"disallowed"],[[1425,1441],"valid"],[[1442,1442],"valid"],[[1443,1455],"valid"],[[1456,1465],"valid"],[[1466,1466],"valid"],[[1467,1469],"valid"],[[1470,1470],"valid",[],"NV8"],[[1471,1471],"valid"],[[1472,1472],"valid",[],"NV8"],[[1473,1474],"valid"],[[1475,1475],"valid",[],"NV8"],[[1476,1476],"valid"],[[1477,1477],"valid"],[[1478,1478],"valid",[],"NV8"],[[1479,1479],"valid"],[[1480,1487],"disallowed"],[[1488,1514],"valid"],[[1515,1519],"disallowed"],[[1520,1524],"valid"],[[1525,1535],"disallowed"],[[1536,1539],"disallowed"],[[1540,1540],"disallowed"],[[1541,1541],"disallowed"],[[1542,1546],"valid",[],"NV8"],[[1547,1547],"valid",[],"NV8"],[[1548,1548],"valid",[],"NV8"],[[1549,1551],"valid",[],"NV8"],[[1552,1557],"valid"],[[1558,1562],"valid"],[[1563,1563],"valid",[],"NV8"],[[1564,1564],"disallowed"],[[1565,1565],"disallowed"],[[1566,1566],"valid",[],"NV8"],[[1567,1567],"valid",[],"NV8"],[[1568,1568],"valid"],[[1569,1594],"valid"],[[1595,1599],"valid"],[[1600,1600],"valid",[],"NV8"],[[1601,1618],"valid"],[[1619,1621],"valid"],[[1622,1624],"valid"],[[1625,1630],"valid"],[[1631,1631],"valid"],[[1632,1641],"valid"],[[1642,1645],"valid",[],"NV8"],[[1646,1647],"valid"],[[1648,1652],"valid"],[[1653,1653],"mapped",[1575,1652]],[[1654,1654],"mapped",[1608,1652]],[[1655,1655],"mapped",[1735,1652]],[[1656,1656],"mapped",[1610,1652]],[[1657,1719],"valid"],[[1720,1721],"valid"],[[1722,1726],"valid"],[[1727,1727],"valid"],[[1728,1742],"valid"],[[1743,1743],"valid"],[[1744,1747],"valid"],[[1748,1748],"valid",[],"NV8"],[[1749,1756],"valid"],[[1757,1757],"disallowed"],[[1758,1758],"valid",[],"NV8"],[[1759,1768],"valid"],[[1769,1769],"valid",[],"NV8"],[[1770,1773],"valid"],[[1774,1775],"valid"],[[1776,1785],"valid"],[[1786,1790],"valid"],[[1791,1791],"valid"],[[1792,1805],"valid",[],"NV8"],[[1806,1806],"disallowed"],[[1807,1807],"disallowed"],[[1808,1836],"valid"],[[1837,1839],"valid"],[[1840,1866],"valid"],[[1867,1868],"disallowed"],[[1869,1871],"valid"],[[1872,1901],"valid"],[[1902,1919],"valid"],[[1920,1968],"valid"],[[1969,1969],"valid"],[[1970,1983],"disallowed"],[[1984,2037],"valid"],[[2038,2042],"valid",[],"NV8"],[[2043,2047],"disallowed"],[[2048,2093],"valid"],[[2094,2095],"disallowed"],[[2096,2110],"valid",[],"NV8"],[[2111,2111],"disallowed"],[[2112,2139],"valid"],[[2140,2141],"disallowed"],[[2142,2142],"valid",[],"NV8"],[[2143,2207],"disallowed"],[[2208,2208],"valid"],[[2209,2209],"valid"],[[2210,2220],"valid"],[[2221,2226],"valid"],[[2227,2228],"valid"],[[2229,2274],"disallowed"],[[2275,2275],"valid"],[[2276,2302],"valid"],[[2303,2303],"valid"],[[2304,2304],"valid"],[[2305,2307],"valid"],[[2308,2308],"valid"],[[2309,2361],"valid"],[[2362,2363],"valid"],[[2364,2381],"valid"],[[2382,2382],"valid"],[[2383,2383],"valid"],[[2384,2388],"valid"],[[2389,2389],"valid"],[[2390,2391],"valid"],[[2392,2392],"mapped",[2325,2364]],[[2393,2393],"mapped",[2326,2364]],[[2394,2394],"mapped",[2327,2364]],[[2395,2395],"mapped",[2332,2364]],[[2396,2396],"mapped",[2337,2364]],[[2397,2397],"mapped",[2338,2364]],[[2398,2398],"mapped",[2347,2364]],[[2399,2399],"mapped",[2351,2364]],[[2400,2403],"valid"],[[2404,2405],"valid",[],"NV8"],[[2406,2415],"valid"],[[2416,2416],"valid",[],"NV8"],[[2417,2418],"valid"],[[2419,2423],"valid"],[[2424,2424],"valid"],[[2425,2426],"valid"],[[2427,2428],"valid"],[[2429,2429],"valid"],[[2430,2431],"valid"],[[2432,2432],"valid"],[[2433,2435],"valid"],[[2436,2436],"disallowed"],[[2437,2444],"valid"],[[2445,2446],"disallowed"],[[2447,2448],"valid"],[[2449,2450],"disallowed"],[[2451,2472],"valid"],[[2473,2473],"disallowed"],[[2474,2480],"valid"],[[2481,2481],"disallowed"],[[2482,2482],"valid"],[[2483,2485],"disallowed"],[[2486,2489],"valid"],[[2490,2491],"disallowed"],[[2492,2492],"valid"],[[2493,2493],"valid"],[[2494,2500],"valid"],[[2501,2502],"disallowed"],[[2503,2504],"valid"],[[2505,2506],"disallowed"],[[2507,2509],"valid"],[[2510,2510],"valid"],[[2511,2518],"disallowed"],[[2519,2519],"valid"],[[2520,2523],"disallowed"],[[2524,2524],"mapped",[2465,2492]],[[2525,2525],"mapped",[2466,2492]],[[2526,2526],"disallowed"],[[2527,2527],"mapped",[2479,2492]],[[2528,2531],"valid"],[[2532,2533],"disallowed"],[[2534,2545],"valid"],[[2546,2554],"valid",[],"NV8"],[[2555,2555],"valid",[],"NV8"],[[2556,2560],"disallowed"],[[2561,2561],"valid"],[[2562,2562],"valid"],[[2563,2563],"valid"],[[2564,2564],"disallowed"],[[2565,2570],"valid"],[[2571,2574],"disallowed"],[[2575,2576],"valid"],[[2577,2578],"disallowed"],[[2579,2600],"valid"],[[2601,2601],"disallowed"],[[2602,2608],"valid"],[[2609,2609],"disallowed"],[[2610,2610],"valid"],[[2611,2611],"mapped",[2610,2620]],[[2612,2612],"disallowed"],[[2613,2613],"valid"],[[2614,2614],"mapped",[2616,2620]],[[2615,2615],"disallowed"],[[2616,2617],"valid"],[[2618,2619],"disallowed"],[[2620,2620],"valid"],[[2621,2621],"disallowed"],[[2622,2626],"valid"],[[2627,2630],"disallowed"],[[2631,2632],"valid"],[[2633,2634],"disallowed"],[[2635,2637],"valid"],[[2638,2640],"disallowed"],[[2641,2641],"valid"],[[2642,2648],"disallowed"],[[2649,2649],"mapped",[2582,2620]],[[2650,2650],"mapped",[2583,2620]],[[2651,2651],"mapped",[2588,2620]],[[2652,2652],"valid"],[[2653,2653],"disallowed"],[[2654,2654],"mapped",[2603,2620]],[[2655,2661],"disallowed"],[[2662,2676],"valid"],[[2677,2677],"valid"],[[2678,2688],"disallowed"],[[2689,2691],"valid"],[[2692,2692],"disallowed"],[[2693,2699],"valid"],[[2700,2700],"valid"],[[2701,2701],"valid"],[[2702,2702],"disallowed"],[[2703,2705],"valid"],[[2706,2706],"disallowed"],[[2707,2728],"valid"],[[2729,2729],"disallowed"],[[2730,2736],"valid"],[[2737,2737],"disallowed"],[[2738,2739],"valid"],[[2740,2740],"disallowed"],[[2741,2745],"valid"],[[2746,2747],"disallowed"],[[2748,2757],"valid"],[[2758,2758],"disallowed"],[[2759,2761],"valid"],[[2762,2762],"disallowed"],[[2763,2765],"valid"],[[2766,2767],"disallowed"],[[2768,2768],"valid"],[[2769,2783],"disallowed"],[[2784,2784],"valid"],[[2785,2787],"valid"],[[2788,2789],"disallowed"],[[2790,2799],"valid"],[[2800,2800],"valid",[],"NV8"],[[2801,2801],"valid",[],"NV8"],[[2802,2808],"disallowed"],[[2809,2809],"valid"],[[2810,2816],"disallowed"],[[2817,2819],"valid"],[[2820,2820],"disallowed"],[[2821,2828],"valid"],[[2829,2830],"disallowed"],[[2831,2832],"valid"],[[2833,2834],"disallowed"],[[2835,2856],"valid"],[[2857,2857],"disallowed"],[[2858,2864],"valid"],[[2865,2865],"disallowed"],[[2866,2867],"valid"],[[2868,2868],"disallowed"],[[2869,2869],"valid"],[[2870,2873],"valid"],[[2874,2875],"disallowed"],[[2876,2883],"valid"],[[2884,2884],"valid"],[[2885,2886],"disallowed"],[[2887,2888],"valid"],[[2889,2890],"disallowed"],[[2891,2893],"valid"],[[2894,2901],"disallowed"],[[2902,2903],"valid"],[[2904,2907],"disallowed"],[[2908,2908],"mapped",[2849,2876]],[[2909,2909],"mapped",[2850,2876]],[[2910,2910],"disallowed"],[[2911,2913],"valid"],[[2914,2915],"valid"],[[2916,2917],"disallowed"],[[2918,2927],"valid"],[[2928,2928],"valid",[],"NV8"],[[2929,2929],"valid"],[[2930,2935],"valid",[],"NV8"],[[2936,2945],"disallowed"],[[2946,2947],"valid"],[[2948,2948],"disallowed"],[[2949,2954],"valid"],[[2955,2957],"disallowed"],[[2958,2960],"valid"],[[2961,2961],"disallowed"],[[2962,2965],"valid"],[[2966,2968],"disallowed"],[[2969,2970],"valid"],[[2971,2971],"disallowed"],[[2972,2972],"valid"],[[2973,2973],"disallowed"],[[2974,2975],"valid"],[[2976,2978],"disallowed"],[[2979,2980],"valid"],[[2981,2983],"disallowed"],[[2984,2986],"valid"],[[2987,2989],"disallowed"],[[2990,2997],"valid"],[[2998,2998],"valid"],[[2999,3001],"valid"],[[3002,3005],"disallowed"],[[3006,3010],"valid"],[[3011,3013],"disallowed"],[[3014,3016],"valid"],[[3017,3017],"disallowed"],[[3018,3021],"valid"],[[3022,3023],"disallowed"],[[3024,3024],"valid"],[[3025,3030],"disallowed"],[[3031,3031],"valid"],[[3032,3045],"disallowed"],[[3046,3046],"valid"],[[3047,3055],"valid"],[[3056,3058],"valid",[],"NV8"],[[3059,3066],"valid",[],"NV8"],[[3067,3071],"disallowed"],[[3072,3072],"valid"],[[3073,3075],"valid"],[[3076,3076],"disallowed"],[[3077,3084],"valid"],[[3085,3085],"disallowed"],[[3086,3088],"valid"],[[3089,3089],"disallowed"],[[3090,3112],"valid"],[[3113,3113],"disallowed"],[[3114,3123],"valid"],[[3124,3124],"valid"],[[3125,3129],"valid"],[[3130,3132],"disallowed"],[[3133,3133],"valid"],[[3134,3140],"valid"],[[3141,3141],"disallowed"],[[3142,3144],"valid"],[[3145,3145],"disallowed"],[[3146,3149],"valid"],[[3150,3156],"disallowed"],[[3157,3158],"valid"],[[3159,3159],"disallowed"],[[3160,3161],"valid"],[[3162,3162],"valid"],[[3163,3167],"disallowed"],[[3168,3169],"valid"],[[3170,3171],"valid"],[[3172,3173],"disallowed"],[[3174,3183],"valid"],[[3184,3191],"disallowed"],[[3192,3199],"valid",[],"NV8"],[[3200,3200],"disallowed"],[[3201,3201],"valid"],[[3202,3203],"valid"],[[3204,3204],"disallowed"],[[3205,3212],"valid"],[[3213,3213],"disallowed"],[[3214,3216],"valid"],[[3217,3217],"disallowed"],[[3218,3240],"valid"],[[3241,3241],"disallowed"],[[3242,3251],"valid"],[[3252,3252],"disallowed"],[[3253,3257],"valid"],[[3258,3259],"disallowed"],[[3260,3261],"valid"],[[3262,3268],"valid"],[[3269,3269],"disallowed"],[[3270,3272],"valid"],[[3273,3273],"disallowed"],[[3274,3277],"valid"],[[3278,3284],"disallowed"],[[3285,3286],"valid"],[[3287,3293],"disallowed"],[[3294,3294],"valid"],[[3295,3295],"disallowed"],[[3296,3297],"valid"],[[3298,3299],"valid"],[[3300,3301],"disallowed"],[[3302,3311],"valid"],[[3312,3312],"disallowed"],[[3313,3314],"valid"],[[3315,3328],"disallowed"],[[3329,3329],"valid"],[[3330,3331],"valid"],[[3332,3332],"disallowed"],[[3333,3340],"valid"],[[3341,3341],"disallowed"],[[3342,3344],"valid"],[[3345,3345],"disallowed"],[[3346,3368],"valid"],[[3369,3369],"valid"],[[3370,3385],"valid"],[[3386,3386],"valid"],[[3387,3388],"disallowed"],[[3389,3389],"valid"],[[3390,3395],"valid"],[[3396,3396],"valid"],[[3397,3397],"disallowed"],[[3398,3400],"valid"],[[3401,3401],"disallowed"],[[3402,3405],"valid"],[[3406,3406],"valid"],[[3407,3414],"disallowed"],[[3415,3415],"valid"],[[3416,3422],"disallowed"],[[3423,3423],"valid"],[[3424,3425],"valid"],[[3426,3427],"valid"],[[3428,3429],"disallowed"],[[3430,3439],"valid"],[[3440,3445],"valid",[],"NV8"],[[3446,3448],"disallowed"],[[3449,3449],"valid",[],"NV8"],[[3450,3455],"valid"],[[3456,3457],"disallowed"],[[3458,3459],"valid"],[[3460,3460],"disallowed"],[[3461,3478],"valid"],[[3479,3481],"disallowed"],[[3482,3505],"valid"],[[3506,3506],"disallowed"],[[3507,3515],"valid"],[[3516,3516],"disallowed"],[[3517,3517],"valid"],[[3518,3519],"disallowed"],[[3520,3526],"valid"],[[3527,3529],"disallowed"],[[3530,3530],"valid"],[[3531,3534],"disallowed"],[[3535,3540],"valid"],[[3541,3541],"disallowed"],[[3542,3542],"valid"],[[3543,3543],"disallowed"],[[3544,3551],"valid"],[[3552,3557],"disallowed"],[[3558,3567],"valid"],[[3568,3569],"disallowed"],[[3570,3571],"valid"],[[3572,3572],"valid",[],"NV8"],[[3573,3584],"disallowed"],[[3585,3634],"valid"],[[3635,3635],"mapped",[3661,3634]],[[3636,3642],"valid"],[[3643,3646],"disallowed"],[[3647,3647],"valid",[],"NV8"],[[3648,3662],"valid"],[[3663,3663],"valid",[],"NV8"],[[3664,3673],"valid"],[[3674,3675],"valid",[],"NV8"],[[3676,3712],"disallowed"],[[3713,3714],"valid"],[[3715,3715],"disallowed"],[[3716,3716],"valid"],[[3717,3718],"disallowed"],[[3719,3720],"valid"],[[3721,3721],"disallowed"],[[3722,3722],"valid"],[[3723,3724],"disallowed"],[[3725,3725],"valid"],[[3726,3731],"disallowed"],[[3732,3735],"valid"],[[3736,3736],"disallowed"],[[3737,3743],"valid"],[[3744,3744],"disallowed"],[[3745,3747],"valid"],[[3748,3748],"disallowed"],[[3749,3749],"valid"],[[3750,3750],"disallowed"],[[3751,3751],"valid"],[[3752,3753],"disallowed"],[[3754,3755],"valid"],[[3756,3756],"disallowed"],[[3757,3762],"valid"],[[3763,3763],"mapped",[3789,3762]],[[3764,3769],"valid"],[[3770,3770],"disallowed"],[[3771,3773],"valid"],[[3774,3775],"disallowed"],[[3776,3780],"valid"],[[3781,3781],"disallowed"],[[3782,3782],"valid"],[[3783,3783],"disallowed"],[[3784,3789],"valid"],[[3790,3791],"disallowed"],[[3792,3801],"valid"],[[3802,3803],"disallowed"],[[3804,3804],"mapped",[3755,3737]],[[3805,3805],"mapped",[3755,3745]],[[3806,3807],"valid"],[[3808,3839],"disallowed"],[[3840,3840],"valid"],[[3841,3850],"valid",[],"NV8"],[[3851,3851],"valid"],[[3852,3852],"mapped",[3851]],[[3853,3863],"valid",[],"NV8"],[[3864,3865],"valid"],[[3866,3871],"valid",[],"NV8"],[[3872,3881],"valid"],[[3882,3892],"valid",[],"NV8"],[[3893,3893],"valid"],[[3894,3894],"valid",[],"NV8"],[[3895,3895],"valid"],[[3896,3896],"valid",[],"NV8"],[[3897,3897],"valid"],[[3898,3901],"valid",[],"NV8"],[[3902,3906],"valid"],[[3907,3907],"mapped",[3906,4023]],[[3908,3911],"valid"],[[3912,3912],"disallowed"],[[3913,3916],"valid"],[[3917,3917],"mapped",[3916,4023]],[[3918,3921],"valid"],[[3922,3922],"mapped",[3921,4023]],[[3923,3926],"valid"],[[3927,3927],"mapped",[3926,4023]],[[3928,3931],"valid"],[[3932,3932],"mapped",[3931,4023]],[[3933,3944],"valid"],[[3945,3945],"mapped",[3904,4021]],[[3946,3946],"valid"],[[3947,3948],"valid"],[[3949,3952],"disallowed"],[[3953,3954],"valid"],[[3955,3955],"mapped",[3953,3954]],[[3956,3956],"valid"],[[3957,3957],"mapped",[3953,3956]],[[3958,3958],"mapped",[4018,3968]],[[3959,3959],"mapped",[4018,3953,3968]],[[3960,3960],"mapped",[4019,3968]],[[3961,3961],"mapped",[4019,3953,3968]],[[3962,3968],"valid"],[[3969,3969],"mapped",[3953,3968]],[[3970,3972],"valid"],[[3973,3973],"valid",[],"NV8"],[[3974,3979],"valid"],[[3980,3983],"valid"],[[3984,3986],"valid"],[[3987,3987],"mapped",[3986,4023]],[[3988,3989],"valid"],[[3990,3990],"valid"],[[3991,3991],"valid"],[[3992,3992],"disallowed"],[[3993,3996],"valid"],[[3997,3997],"mapped",[3996,4023]],[[3998,4001],"valid"],[[4002,4002],"mapped",[4001,4023]],[[4003,4006],"valid"],[[4007,4007],"mapped",[4006,4023]],[[4008,4011],"valid"],[[4012,4012],"mapped",[4011,4023]],[[4013,4013],"valid"],[[4014,4016],"valid"],[[4017,4023],"valid"],[[4024,4024],"valid"],[[4025,4025],"mapped",[3984,4021]],[[4026,4028],"valid"],[[4029,4029],"disallowed"],[[4030,4037],"valid",[],"NV8"],[[4038,4038],"valid"],[[4039,4044],"valid",[],"NV8"],[[4045,4045],"disallowed"],[[4046,4046],"valid",[],"NV8"],[[4047,4047],"valid",[],"NV8"],[[4048,4049],"valid",[],"NV8"],[[4050,4052],"valid",[],"NV8"],[[4053,4056],"valid",[],"NV8"],[[4057,4058],"valid",[],"NV8"],[[4059,4095],"disallowed"],[[4096,4129],"valid"],[[4130,4130],"valid"],[[4131,4135],"valid"],[[4136,4136],"valid"],[[4137,4138],"valid"],[[4139,4139],"valid"],[[4140,4146],"valid"],[[4147,4149],"valid"],[[4150,4153],"valid"],[[4154,4159],"valid"],[[4160,4169],"valid"],[[4170,4175],"valid",[],"NV8"],[[4176,4185],"valid"],[[4186,4249],"valid"],[[4250,4253],"valid"],[[4254,4255],"valid",[],"NV8"],[[4256,4293],"disallowed"],[[4294,4294],"disallowed"],[[4295,4295],"mapped",[11559]],[[4296,4300],"disallowed"],[[4301,4301],"mapped",[11565]],[[4302,4303],"disallowed"],[[4304,4342],"valid"],[[4343,4344],"valid"],[[4345,4346],"valid"],[[4347,4347],"valid",[],"NV8"],[[4348,4348],"mapped",[4316]],[[4349,4351],"valid"],[[4352,4441],"valid",[],"NV8"],[[4442,4446],"valid",[],"NV8"],[[4447,4448],"disallowed"],[[4449,4514],"valid",[],"NV8"],[[4515,4519],"valid",[],"NV8"],[[4520,4601],"valid",[],"NV8"],[[4602,4607],"valid",[],"NV8"],[[4608,4614],"valid"],[[4615,4615],"valid"],[[4616,4678],"valid"],[[4679,4679],"valid"],[[4680,4680],"valid"],[[4681,4681],"disallowed"],[[4682,4685],"valid"],[[4686,4687],"disallowed"],[[4688,4694],"valid"],[[4695,4695],"disallowed"],[[4696,4696],"valid"],[[4697,4697],"disallowed"],[[4698,4701],"valid"],[[4702,4703],"disallowed"],[[4704,4742],"valid"],[[4743,4743],"valid"],[[4744,4744],"valid"],[[4745,4745],"disallowed"],[[4746,4749],"valid"],[[4750,4751],"disallowed"],[[4752,4782],"valid"],[[4783,4783],"valid"],[[4784,4784],"valid"],[[4785,4785],"disallowed"],[[4786,4789],"valid"],[[4790,4791],"disallowed"],[[4792,4798],"valid"],[[4799,4799],"disallowed"],[[4800,4800],"valid"],[[4801,4801],"disallowed"],[[4802,4805],"valid"],[[4806,4807],"disallowed"],[[4808,4814],"valid"],[[4815,4815],"valid"],[[4816,4822],"valid"],[[4823,4823],"disallowed"],[[4824,4846],"valid"],[[4847,4847],"valid"],[[4848,4878],"valid"],[[4879,4879],"valid"],[[4880,4880],"valid"],[[4881,4881],"disallowed"],[[4882,4885],"valid"],[[4886,4887],"disallowed"],[[4888,4894],"valid"],[[4895,4895],"valid"],[[4896,4934],"valid"],[[4935,4935],"valid"],[[4936,4954],"valid"],[[4955,4956],"disallowed"],[[4957,4958],"valid"],[[4959,4959],"valid"],[[4960,4960],"valid",[],"NV8"],[[4961,4988],"valid",[],"NV8"],[[4989,4991],"disallowed"],[[4992,5007],"valid"],[[5008,5017],"valid",[],"NV8"],[[5018,5023],"disallowed"],[[5024,5108],"valid"],[[5109,5109],"valid"],[[5110,5111],"disallowed"],[[5112,5112],"mapped",[5104]],[[5113,5113],"mapped",[5105]],[[5114,5114],"mapped",[5106]],[[5115,5115],"mapped",[5107]],[[5116,5116],"mapped",[5108]],[[5117,5117],"mapped",[5109]],[[5118,5119],"disallowed"],[[5120,5120],"valid",[],"NV8"],[[5121,5740],"valid"],[[5741,5742],"valid",[],"NV8"],[[5743,5750],"valid"],[[5751,5759],"valid"],[[5760,5760],"disallowed"],[[5761,5786],"valid"],[[5787,5788],"valid",[],"NV8"],[[5789,5791],"disallowed"],[[5792,5866],"valid"],[[5867,5872],"valid",[],"NV8"],[[5873,5880],"valid"],[[5881,5887],"disallowed"],[[5888,5900],"valid"],[[5901,5901],"disallowed"],[[5902,5908],"valid"],[[5909,5919],"disallowed"],[[5920,5940],"valid"],[[5941,5942],"valid",[],"NV8"],[[5943,5951],"disallowed"],[[5952,5971],"valid"],[[5972,5983],"disallowed"],[[5984,5996],"valid"],[[5997,5997],"disallowed"],[[5998,6000],"valid"],[[6001,6001],"disallowed"],[[6002,6003],"valid"],[[6004,6015],"disallowed"],[[6016,6067],"valid"],[[6068,6069],"disallowed"],[[6070,6099],"valid"],[[6100,6102],"valid",[],"NV8"],[[6103,6103],"valid"],[[6104,6107],"valid",[],"NV8"],[[6108,6108],"valid"],[[6109,6109],"valid"],[[6110,6111],"disallowed"],[[6112,6121],"valid"],[[6122,6127],"disallowed"],[[6128,6137],"valid",[],"NV8"],[[6138,6143],"disallowed"],[[6144,6149],"valid",[],"NV8"],[[6150,6150],"disallowed"],[[6151,6154],"valid",[],"NV8"],[[6155,6157],"ignored"],[[6158,6158],"disallowed"],[[6159,6159],"disallowed"],[[6160,6169],"valid"],[[6170,6175],"disallowed"],[[6176,6263],"valid"],[[6264,6271],"disallowed"],[[6272,6313],"valid"],[[6314,6314],"valid"],[[6315,6319],"disallowed"],[[6320,6389],"valid"],[[6390,6399],"disallowed"],[[6400,6428],"valid"],[[6429,6430],"valid"],[[6431,6431],"disallowed"],[[6432,6443],"valid"],[[6444,6447],"disallowed"],[[6448,6459],"valid"],[[6460,6463],"disallowed"],[[6464,6464],"valid",[],"NV8"],[[6465,6467],"disallowed"],[[6468,6469],"valid",[],"NV8"],[[6470,6509],"valid"],[[6510,6511],"disallowed"],[[6512,6516],"valid"],[[6517,6527],"disallowed"],[[6528,6569],"valid"],[[6570,6571],"valid"],[[6572,6575],"disallowed"],[[6576,6601],"valid"],[[6602,6607],"disallowed"],[[6608,6617],"valid"],[[6618,6618],"valid",[],"XV8"],[[6619,6621],"disallowed"],[[6622,6623],"valid",[],"NV8"],[[6624,6655],"valid",[],"NV8"],[[6656,6683],"valid"],[[6684,6685],"disallowed"],[[6686,6687],"valid",[],"NV8"],[[6688,6750],"valid"],[[6751,6751],"disallowed"],[[6752,6780],"valid"],[[6781,6782],"disallowed"],[[6783,6793],"valid"],[[6794,6799],"disallowed"],[[6800,6809],"valid"],[[6810,6815],"disallowed"],[[6816,6822],"valid",[],"NV8"],[[6823,6823],"valid"],[[6824,6829],"valid",[],"NV8"],[[6830,6831],"disallowed"],[[6832,6845],"valid"],[[6846,6846],"valid",[],"NV8"],[[6847,6911],"disallowed"],[[6912,6987],"valid"],[[6988,6991],"disallowed"],[[6992,7001],"valid"],[[7002,7018],"valid",[],"NV8"],[[7019,7027],"valid"],[[7028,7036],"valid",[],"NV8"],[[7037,7039],"disallowed"],[[7040,7082],"valid"],[[7083,7085],"valid"],[[7086,7097],"valid"],[[7098,7103],"valid"],[[7104,7155],"valid"],[[7156,7163],"disallowed"],[[7164,7167],"valid",[],"NV8"],[[7168,7223],"valid"],[[7224,7226],"disallowed"],[[7227,7231],"valid",[],"NV8"],[[7232,7241],"valid"],[[7242,7244],"disallowed"],[[7245,7293],"valid"],[[7294,7295],"valid",[],"NV8"],[[7296,7359],"disallowed"],[[7360,7367],"valid",[],"NV8"],[[7368,7375],"disallowed"],[[7376,7378],"valid"],[[7379,7379],"valid",[],"NV8"],[[7380,7410],"valid"],[[7411,7414],"valid"],[[7415,7415],"disallowed"],[[7416,7417],"valid"],[[7418,7423],"disallowed"],[[7424,7467],"valid"],[[7468,7468],"mapped",[97]],[[7469,7469],"mapped",[230]],[[7470,7470],"mapped",[98]],[[7471,7471],"valid"],[[7472,7472],"mapped",[100]],[[7473,7473],"mapped",[101]],[[7474,7474],"mapped",[477]],[[7475,7475],"mapped",[103]],[[7476,7476],"mapped",[104]],[[7477,7477],"mapped",[105]],[[7478,7478],"mapped",[106]],[[7479,7479],"mapped",[107]],[[7480,7480],"mapped",[108]],[[7481,7481],"mapped",[109]],[[7482,7482],"mapped",[110]],[[7483,7483],"valid"],[[7484,7484],"mapped",[111]],[[7485,7485],"mapped",[547]],[[7486,7486],"mapped",[112]],[[7487,7487],"mapped",[114]],[[7488,7488],"mapped",[116]],[[7489,7489],"mapped",[117]],[[7490,7490],"mapped",[119]],[[7491,7491],"mapped",[97]],[[7492,7492],"mapped",[592]],[[7493,7493],"mapped",[593]],[[7494,7494],"mapped",[7426]],[[7495,7495],"mapped",[98]],[[7496,7496],"mapped",[100]],[[7497,7497],"mapped",[101]],[[7498,7498],"mapped",[601]],[[7499,7499],"mapped",[603]],[[7500,7500],"mapped",[604]],[[7501,7501],"mapped",[103]],[[7502,7502],"valid"],[[7503,7503],"mapped",[107]],[[7504,7504],"mapped",[109]],[[7505,7505],"mapped",[331]],[[7506,7506],"mapped",[111]],[[7507,7507],"mapped",[596]],[[7508,7508],"mapped",[7446]],[[7509,7509],"mapped",[7447]],[[7510,7510],"mapped",[112]],[[7511,7511],"mapped",[116]],[[7512,7512],"mapped",[117]],[[7513,7513],"mapped",[7453]],[[7514,7514],"mapped",[623]],[[7515,7515],"mapped",[118]],[[7516,7516],"mapped",[7461]],[[7517,7517],"mapped",[946]],[[7518,7518],"mapped",[947]],[[7519,7519],"mapped",[948]],[[7520,7520],"mapped",[966]],[[7521,7521],"mapped",[967]],[[7522,7522],"mapped",[105]],[[7523,7523],"mapped",[114]],[[7524,7524],"mapped",[117]],[[7525,7525],"mapped",[118]],[[7526,7526],"mapped",[946]],[[7527,7527],"mapped",[947]],[[7528,7528],"mapped",[961]],[[7529,7529],"mapped",[966]],[[7530,7530],"mapped",[967]],[[7531,7531],"valid"],[[7532,7543],"valid"],[[7544,7544],"mapped",[1085]],[[7545,7578],"valid"],[[7579,7579],"mapped",[594]],[[7580,7580],"mapped",[99]],[[7581,7581],"mapped",[597]],[[7582,7582],"mapped",[240]],[[7583,7583],"mapped",[604]],[[7584,7584],"mapped",[102]],[[7585,7585],"mapped",[607]],[[7586,7586],"mapped",[609]],[[7587,7587],"mapped",[613]],[[7588,7588],"mapped",[616]],[[7589,7589],"mapped",[617]],[[7590,7590],"mapped",[618]],[[7591,7591],"mapped",[7547]],[[7592,7592],"mapped",[669]],[[7593,7593],"mapped",[621]],[[7594,7594],"mapped",[7557]],[[7595,7595],"mapped",[671]],[[7596,7596],"mapped",[625]],[[7597,7597],"mapped",[624]],[[7598,7598],"mapped",[626]],[[7599,7599],"mapped",[627]],[[7600,7600],"mapped",[628]],[[7601,7601],"mapped",[629]],[[7602,7602],"mapped",[632]],[[7603,7603],"mapped",[642]],[[7604,7604],"mapped",[643]],[[7605,7605],"mapped",[427]],[[7606,7606],"mapped",[649]],[[7607,7607],"mapped",[650]],[[7608,7608],"mapped",[7452]],[[7609,7609],"mapped",[651]],[[7610,7610],"mapped",[652]],[[7611,7611],"mapped",[122]],[[7612,7612],"mapped",[656]],[[7613,7613],"mapped",[657]],[[7614,7614],"mapped",[658]],[[7615,7615],"mapped",[952]],[[7616,7619],"valid"],[[7620,7626],"valid"],[[7627,7654],"valid"],[[7655,7669],"valid"],[[7670,7675],"disallowed"],[[7676,7676],"valid"],[[7677,7677],"valid"],[[7678,7679],"valid"],[[7680,7680],"mapped",[7681]],[[7681,7681],"valid"],[[7682,7682],"mapped",[7683]],[[7683,7683],"valid"],[[7684,7684],"mapped",[7685]],[[7685,7685],"valid"],[[7686,7686],"mapped",[7687]],[[7687,7687],"valid"],[[7688,7688],"mapped",[7689]],[[7689,7689],"valid"],[[7690,7690],"mapped",[7691]],[[7691,7691],"valid"],[[7692,7692],"mapped",[7693]],[[7693,7693],"valid"],[[7694,7694],"mapped",[7695]],[[7695,7695],"valid"],[[7696,7696],"mapped",[7697]],[[7697,7697],"valid"],[[7698,7698],"mapped",[7699]],[[7699,7699],"valid"],[[7700,7700],"mapped",[7701]],[[7701,7701],"valid"],[[7702,7702],"mapped",[7703]],[[7703,7703],"valid"],[[7704,7704],"mapped",[7705]],[[7705,7705],"valid"],[[7706,7706],"mapped",[7707]],[[7707,7707],"valid"],[[7708,7708],"mapped",[7709]],[[7709,7709],"valid"],[[7710,7710],"mapped",[7711]],[[7711,7711],"valid"],[[7712,7712],"mapped",[7713]],[[7713,7713],"valid"],[[7714,7714],"mapped",[7715]],[[7715,7715],"valid"],[[7716,7716],"mapped",[7717]],[[7717,7717],"valid"],[[7718,7718],"mapped",[7719]],[[7719,7719],"valid"],[[7720,7720],"mapped",[7721]],[[7721,7721],"valid"],[[7722,7722],"mapped",[7723]],[[7723,7723],"valid"],[[7724,7724],"mapped",[7725]],[[7725,7725],"valid"],[[7726,7726],"mapped",[7727]],[[7727,7727],"valid"],[[7728,7728],"mapped",[7729]],[[7729,7729],"valid"],[[7730,7730],"mapped",[7731]],[[7731,7731],"valid"],[[7732,7732],"mapped",[7733]],[[7733,7733],"valid"],[[7734,7734],"mapped",[7735]],[[7735,7735],"valid"],[[7736,7736],"mapped",[7737]],[[7737,7737],"valid"],[[7738,7738],"mapped",[7739]],[[7739,7739],"valid"],[[7740,7740],"mapped",[7741]],[[7741,7741],"valid"],[[7742,7742],"mapped",[7743]],[[7743,7743],"valid"],[[7744,7744],"mapped",[7745]],[[7745,7745],"valid"],[[7746,7746],"mapped",[7747]],[[7747,7747],"valid"],[[7748,7748],"mapped",[7749]],[[7749,7749],"valid"],[[7750,7750],"mapped",[7751]],[[7751,7751],"valid"],[[7752,7752],"mapped",[7753]],[[7753,7753],"valid"],[[7754,7754],"mapped",[7755]],[[7755,7755],"valid"],[[7756,7756],"mapped",[7757]],[[7757,7757],"valid"],[[7758,7758],"mapped",[7759]],[[7759,7759],"valid"],[[7760,7760],"mapped",[7761]],[[7761,7761],"valid"],[[7762,7762],"mapped",[7763]],[[7763,7763],"valid"],[[7764,7764],"mapped",[7765]],[[7765,7765],"valid"],[[7766,7766],"mapped",[7767]],[[7767,7767],"valid"],[[7768,7768],"mapped",[7769]],[[7769,7769],"valid"],[[7770,7770],"mapped",[7771]],[[7771,7771],"valid"],[[7772,7772],"mapped",[7773]],[[7773,7773],"valid"],[[7774,7774],"mapped",[7775]],[[7775,7775],"valid"],[[7776,7776],"mapped",[7777]],[[7777,7777],"valid"],[[7778,7778],"mapped",[7779]],[[7779,7779],"valid"],[[7780,7780],"mapped",[7781]],[[7781,7781],"valid"],[[7782,7782],"mapped",[7783]],[[7783,7783],"valid"],[[7784,7784],"mapped",[7785]],[[7785,7785],"valid"],[[7786,7786],"mapped",[7787]],[[7787,7787],"valid"],[[7788,7788],"mapped",[7789]],[[7789,7789],"valid"],[[7790,7790],"mapped",[7791]],[[7791,7791],"valid"],[[7792,7792],"mapped",[7793]],[[7793,7793],"valid"],[[7794,7794],"mapped",[7795]],[[7795,7795],"valid"],[[7796,7796],"mapped",[7797]],[[7797,7797],"valid"],[[7798,7798],"mapped",[7799]],[[7799,7799],"valid"],[[7800,7800],"mapped",[7801]],[[7801,7801],"valid"],[[7802,7802],"mapped",[7803]],[[7803,7803],"valid"],[[7804,7804],"mapped",[7805]],[[7805,7805],"valid"],[[7806,7806],"mapped",[7807]],[[7807,7807],"valid"],[[7808,7808],"mapped",[7809]],[[7809,7809],"valid"],[[7810,7810],"mapped",[7811]],[[7811,7811],"valid"],[[7812,7812],"mapped",[7813]],[[7813,7813],"valid"],[[7814,7814],"mapped",[7815]],[[7815,7815],"valid"],[[7816,7816],"mapped",[7817]],[[7817,7817],"valid"],[[7818,7818],"mapped",[7819]],[[7819,7819],"valid"],[[7820,7820],"mapped",[7821]],[[7821,7821],"valid"],[[7822,7822],"mapped",[7823]],[[7823,7823],"valid"],[[7824,7824],"mapped",[7825]],[[7825,7825],"valid"],[[7826,7826],"mapped",[7827]],[[7827,7827],"valid"],[[7828,7828],"mapped",[7829]],[[7829,7833],"valid"],[[7834,7834],"mapped",[97,702]],[[7835,7835],"mapped",[7777]],[[7836,7837],"valid"],[[7838,7838],"mapped",[115,115]],[[7839,7839],"valid"],[[7840,7840],"mapped",[7841]],[[7841,7841],"valid"],[[7842,7842],"mapped",[7843]],[[7843,7843],"valid"],[[7844,7844],"mapped",[7845]],[[7845,7845],"valid"],[[7846,7846],"mapped",[7847]],[[7847,7847],"valid"],[[7848,7848],"mapped",[7849]],[[7849,7849],"valid"],[[7850,7850],"mapped",[7851]],[[7851,7851],"valid"],[[7852,7852],"mapped",[7853]],[[7853,7853],"valid"],[[7854,7854],"mapped",[7855]],[[7855,7855],"valid"],[[7856,7856],"mapped",[7857]],[[7857,7857],"valid"],[[7858,7858],"mapped",[7859]],[[7859,7859],"valid"],[[7860,7860],"mapped",[7861]],[[7861,7861],"valid"],[[7862,7862],"mapped",[7863]],[[7863,7863],"valid"],[[7864,7864],"mapped",[7865]],[[7865,7865],"valid"],[[7866,7866],"mapped",[7867]],[[7867,7867],"valid"],[[7868,7868],"mapped",[7869]],[[7869,7869],"valid"],[[7870,7870],"mapped",[7871]],[[7871,7871],"valid"],[[7872,7872],"mapped",[7873]],[[7873,7873],"valid"],[[7874,7874],"mapped",[7875]],[[7875,7875],"valid"],[[7876,7876],"mapped",[7877]],[[7877,7877],"valid"],[[7878,7878],"mapped",[7879]],[[7879,7879],"valid"],[[7880,7880],"mapped",[7881]],[[7881,7881],"valid"],[[7882,7882],"mapped",[7883]],[[7883,7883],"valid"],[[7884,7884],"mapped",[7885]],[[7885,7885],"valid"],[[7886,7886],"mapped",[7887]],[[7887,7887],"valid"],[[7888,7888],"mapped",[7889]],[[7889,7889],"valid"],[[7890,7890],"mapped",[7891]],[[7891,7891],"valid"],[[7892,7892],"mapped",[7893]],[[7893,7893],"valid"],[[7894,7894],"mapped",[7895]],[[7895,7895],"valid"],[[7896,7896],"mapped",[7897]],[[7897,7897],"valid"],[[7898,7898],"mapped",[7899]],[[7899,7899],"valid"],[[7900,7900],"mapped",[7901]],[[7901,7901],"valid"],[[7902,7902],"mapped",[7903]],[[7903,7903],"valid"],[[7904,7904],"mapped",[7905]],[[7905,7905],"valid"],[[7906,7906],"mapped",[7907]],[[7907,7907],"valid"],[[7908,7908],"mapped",[7909]],[[7909,7909],"valid"],[[7910,7910],"mapped",[7911]],[[7911,7911],"valid"],[[7912,7912],"mapped",[7913]],[[7913,7913],"valid"],[[7914,7914],"mapped",[7915]],[[7915,7915],"valid"],[[7916,7916],"mapped",[7917]],[[7917,7917],"valid"],[[7918,7918],"mapped",[7919]],[[7919,7919],"valid"],[[7920,7920],"mapped",[7921]],[[7921,7921],"valid"],[[7922,7922],"mapped",[7923]],[[7923,7923],"valid"],[[7924,7924],"mapped",[7925]],[[7925,7925],"valid"],[[7926,7926],"mapped",[7927]],[[7927,7927],"valid"],[[7928,7928],"mapped",[7929]],[[7929,7929],"valid"],[[7930,7930],"mapped",[7931]],[[7931,7931],"valid"],[[7932,7932],"mapped",[7933]],[[7933,7933],"valid"],[[7934,7934],"mapped",[7935]],[[7935,7935],"valid"],[[7936,7943],"valid"],[[7944,7944],"mapped",[7936]],[[7945,7945],"mapped",[7937]],[[7946,7946],"mapped",[7938]],[[7947,7947],"mapped",[7939]],[[7948,7948],"mapped",[7940]],[[7949,7949],"mapped",[7941]],[[7950,7950],"mapped",[7942]],[[7951,7951],"mapped",[7943]],[[7952,7957],"valid"],[[7958,7959],"disallowed"],[[7960,7960],"mapped",[7952]],[[7961,7961],"mapped",[7953]],[[7962,7962],"mapped",[7954]],[[7963,7963],"mapped",[7955]],[[7964,7964],"mapped",[7956]],[[7965,7965],"mapped",[7957]],[[7966,7967],"disallowed"],[[7968,7975],"valid"],[[7976,7976],"mapped",[7968]],[[7977,7977],"mapped",[7969]],[[7978,7978],"mapped",[7970]],[[7979,7979],"mapped",[7971]],[[7980,7980],"mapped",[7972]],[[7981,7981],"mapped",[7973]],[[7982,7982],"mapped",[7974]],[[7983,7983],"mapped",[7975]],[[7984,7991],"valid"],[[7992,7992],"mapped",[7984]],[[7993,7993],"mapped",[7985]],[[7994,7994],"mapped",[7986]],[[7995,7995],"mapped",[7987]],[[7996,7996],"mapped",[7988]],[[7997,7997],"mapped",[7989]],[[7998,7998],"mapped",[7990]],[[7999,7999],"mapped",[7991]],[[8000,8005],"valid"],[[8006,8007],"disallowed"],[[8008,8008],"mapped",[8000]],[[8009,8009],"mapped",[8001]],[[8010,8010],"mapped",[8002]],[[8011,8011],"mapped",[8003]],[[8012,8012],"mapped",[8004]],[[8013,8013],"mapped",[8005]],[[8014,8015],"disallowed"],[[8016,8023],"valid"],[[8024,8024],"disallowed"],[[8025,8025],"mapped",[8017]],[[8026,8026],"disallowed"],[[8027,8027],"mapped",[8019]],[[8028,8028],"disallowed"],[[8029,8029],"mapped",[8021]],[[8030,8030],"disallowed"],[[8031,8031],"mapped",[8023]],[[8032,8039],"valid"],[[8040,8040],"mapped",[8032]],[[8041,8041],"mapped",[8033]],[[8042,8042],"mapped",[8034]],[[8043,8043],"mapped",[8035]],[[8044,8044],"mapped",[8036]],[[8045,8045],"mapped",[8037]],[[8046,8046],"mapped",[8038]],[[8047,8047],"mapped",[8039]],[[8048,8048],"valid"],[[8049,8049],"mapped",[940]],[[8050,8050],"valid"],[[8051,8051],"mapped",[941]],[[8052,8052],"valid"],[[8053,8053],"mapped",[942]],[[8054,8054],"valid"],[[8055,8055],"mapped",[943]],[[8056,8056],"valid"],[[8057,8057],"mapped",[972]],[[8058,8058],"valid"],[[8059,8059],"mapped",[973]],[[8060,8060],"valid"],[[8061,8061],"mapped",[974]],[[8062,8063],"disallowed"],[[8064,8064],"mapped",[7936,953]],[[8065,8065],"mapped",[7937,953]],[[8066,8066],"mapped",[7938,953]],[[8067,8067],"mapped",[7939,953]],[[8068,8068],"mapped",[7940,953]],[[8069,8069],"mapped",[7941,953]],[[8070,8070],"mapped",[7942,953]],[[8071,8071],"mapped",[7943,953]],[[8072,8072],"mapped",[7936,953]],[[8073,8073],"mapped",[7937,953]],[[8074,8074],"mapped",[7938,953]],[[8075,8075],"mapped",[7939,953]],[[8076,8076],"mapped",[7940,953]],[[8077,8077],"mapped",[7941,953]],[[8078,8078],"mapped",[7942,953]],[[8079,8079],"mapped",[7943,953]],[[8080,8080],"mapped",[7968,953]],[[8081,8081],"mapped",[7969,953]],[[8082,8082],"mapped",[7970,953]],[[8083,8083],"mapped",[7971,953]],[[8084,8084],"mapped",[7972,953]],[[8085,8085],"mapped",[7973,953]],[[8086,8086],"mapped",[7974,953]],[[8087,8087],"mapped",[7975,953]],[[8088,8088],"mapped",[7968,953]],[[8089,8089],"mapped",[7969,953]],[[8090,8090],"mapped",[7970,953]],[[8091,8091],"mapped",[7971,953]],[[8092,8092],"mapped",[7972,953]],[[8093,8093],"mapped",[7973,953]],[[8094,8094],"mapped",[7974,953]],[[8095,8095],"mapped",[7975,953]],[[8096,8096],"mapped",[8032,953]],[[8097,8097],"mapped",[8033,953]],[[8098,8098],"mapped",[8034,953]],[[8099,8099],"mapped",[8035,953]],[[8100,8100],"mapped",[8036,953]],[[8101,8101],"mapped",[8037,953]],[[8102,8102],"mapped",[8038,953]],[[8103,8103],"mapped",[8039,953]],[[8104,8104],"mapped",[8032,953]],[[8105,8105],"mapped",[8033,953]],[[8106,8106],"mapped",[8034,953]],[[8107,8107],"mapped",[8035,953]],[[8108,8108],"mapped",[8036,953]],[[8109,8109],"mapped",[8037,953]],[[8110,8110],"mapped",[8038,953]],[[8111,8111],"mapped",[8039,953]],[[8112,8113],"valid"],[[8114,8114],"mapped",[8048,953]],[[8115,8115],"mapped",[945,953]],[[8116,8116],"mapped",[940,953]],[[8117,8117],"disallowed"],[[8118,8118],"valid"],[[8119,8119],"mapped",[8118,953]],[[8120,8120],"mapped",[8112]],[[8121,8121],"mapped",[8113]],[[8122,8122],"mapped",[8048]],[[8123,8123],"mapped",[940]],[[8124,8124],"mapped",[945,953]],[[8125,8125],"disallowed_STD3_mapped",[32,787]],[[8126,8126],"mapped",[953]],[[8127,8127],"disallowed_STD3_mapped",[32,787]],[[8128,8128],"disallowed_STD3_mapped",[32,834]],[[8129,8129],"disallowed_STD3_mapped",[32,776,834]],[[8130,8130],"mapped",[8052,953]],[[8131,8131],"mapped",[951,953]],[[8132,8132],"mapped",[942,953]],[[8133,8133],"disallowed"],[[8134,8134],"valid"],[[8135,8135],"mapped",[8134,953]],[[8136,8136],"mapped",[8050]],[[8137,8137],"mapped",[941]],[[8138,8138],"mapped",[8052]],[[8139,8139],"mapped",[942]],[[8140,8140],"mapped",[951,953]],[[8141,8141],"disallowed_STD3_mapped",[32,787,768]],[[8142,8142],"disallowed_STD3_mapped",[32,787,769]],[[8143,8143],"disallowed_STD3_mapped",[32,787,834]],[[8144,8146],"valid"],[[8147,8147],"mapped",[912]],[[8148,8149],"disallowed"],[[8150,8151],"valid"],[[8152,8152],"mapped",[8144]],[[8153,8153],"mapped",[8145]],[[8154,8154],"mapped",[8054]],[[8155,8155],"mapped",[943]],[[8156,8156],"disallowed"],[[8157,8157],"disallowed_STD3_mapped",[32,788,768]],[[8158,8158],"disallowed_STD3_mapped",[32,788,769]],[[8159,8159],"disallowed_STD3_mapped",[32,788,834]],[[8160,8162],"valid"],[[8163,8163],"mapped",[944]],[[8164,8167],"valid"],[[8168,8168],"mapped",[8160]],[[8169,8169],"mapped",[8161]],[[8170,8170],"mapped",[8058]],[[8171,8171],"mapped",[973]],[[8172,8172],"mapped",[8165]],[[8173,8173],"disallowed_STD3_mapped",[32,776,768]],[[8174,8174],"disallowed_STD3_mapped",[32,776,769]],[[8175,8175],"disallowed_STD3_mapped",[96]],[[8176,8177],"disallowed"],[[8178,8178],"mapped",[8060,953]],[[8179,8179],"mapped",[969,953]],[[8180,8180],"mapped",[974,953]],[[8181,8181],"disallowed"],[[8182,8182],"valid"],[[8183,8183],"mapped",[8182,953]],[[8184,8184],"mapped",[8056]],[[8185,8185],"mapped",[972]],[[8186,8186],"mapped",[8060]],[[8187,8187],"mapped",[974]],[[8188,8188],"mapped",[969,953]],[[8189,8189],"disallowed_STD3_mapped",[32,769]],[[8190,8190],"disallowed_STD3_mapped",[32,788]],[[8191,8191],"disallowed"],[[8192,8202],"disallowed_STD3_mapped",[32]],[[8203,8203],"ignored"],[[8204,8205],"deviation",[]],[[8206,8207],"disallowed"],[[8208,8208],"valid",[],"NV8"],[[8209,8209],"mapped",[8208]],[[8210,8214],"valid",[],"NV8"],[[8215,8215],"disallowed_STD3_mapped",[32,819]],[[8216,8227],"valid",[],"NV8"],[[8228,8230],"disallowed"],[[8231,8231],"valid",[],"NV8"],[[8232,8238],"disallowed"],[[8239,8239],"disallowed_STD3_mapped",[32]],[[8240,8242],"valid",[],"NV8"],[[8243,8243],"mapped",[8242,8242]],[[8244,8244],"mapped",[8242,8242,8242]],[[8245,8245],"valid",[],"NV8"],[[8246,8246],"mapped",[8245,8245]],[[8247,8247],"mapped",[8245,8245,8245]],[[8248,8251],"valid",[],"NV8"],[[8252,8252],"disallowed_STD3_mapped",[33,33]],[[8253,8253],"valid",[],"NV8"],[[8254,8254],"disallowed_STD3_mapped",[32,773]],[[8255,8262],"valid",[],"NV8"],[[8263,8263],"disallowed_STD3_mapped",[63,63]],[[8264,8264],"disallowed_STD3_mapped",[63,33]],[[8265,8265],"disallowed_STD3_mapped",[33,63]],[[8266,8269],"valid",[],"NV8"],[[8270,8274],"valid",[],"NV8"],[[8275,8276],"valid",[],"NV8"],[[8277,8278],"valid",[],"NV8"],[[8279,8279],"mapped",[8242,8242,8242,8242]],[[8280,8286],"valid",[],"NV8"],[[8287,8287],"disallowed_STD3_mapped",[32]],[[8288,8288],"ignored"],[[8289,8291],"disallowed"],[[8292,8292],"ignored"],[[8293,8293],"disallowed"],[[8294,8297],"disallowed"],[[8298,8303],"disallowed"],[[8304,8304],"mapped",[48]],[[8305,8305],"mapped",[105]],[[8306,8307],"disallowed"],[[8308,8308],"mapped",[52]],[[8309,8309],"mapped",[53]],[[8310,8310],"mapped",[54]],[[8311,8311],"mapped",[55]],[[8312,8312],"mapped",[56]],[[8313,8313],"mapped",[57]],[[8314,8314],"disallowed_STD3_mapped",[43]],[[8315,8315],"mapped",[8722]],[[8316,8316],"disallowed_STD3_mapped",[61]],[[8317,8317],"disallowed_STD3_mapped",[40]],[[8318,8318],"disallowed_STD3_mapped",[41]],[[8319,8319],"mapped",[110]],[[8320,8320],"mapped",[48]],[[8321,8321],"mapped",[49]],[[8322,8322],"mapped",[50]],[[8323,8323],"mapped",[51]],[[8324,8324],"mapped",[52]],[[8325,8325],"mapped",[53]],[[8326,8326],"mapped",[54]],[[8327,8327],"mapped",[55]],[[8328,8328],"mapped",[56]],[[8329,8329],"mapped",[57]],[[8330,8330],"disallowed_STD3_mapped",[43]],[[8331,8331],"mapped",[8722]],[[8332,8332],"disallowed_STD3_mapped",[61]],[[8333,8333],"disallowed_STD3_mapped",[40]],[[8334,8334],"disallowed_STD3_mapped",[41]],[[8335,8335],"disallowed"],[[8336,8336],"mapped",[97]],[[8337,8337],"mapped",[101]],[[8338,8338],"mapped",[111]],[[8339,8339],"mapped",[120]],[[8340,8340],"mapped",[601]],[[8341,8341],"mapped",[104]],[[8342,8342],"mapped",[107]],[[8343,8343],"mapped",[108]],[[8344,8344],"mapped",[109]],[[8345,8345],"mapped",[110]],[[8346,8346],"mapped",[112]],[[8347,8347],"mapped",[115]],[[8348,8348],"mapped",[116]],[[8349,8351],"disallowed"],[[8352,8359],"valid",[],"NV8"],[[8360,8360],"mapped",[114,115]],[[8361,8362],"valid",[],"NV8"],[[8363,8363],"valid",[],"NV8"],[[8364,8364],"valid",[],"NV8"],[[8365,8367],"valid",[],"NV8"],[[8368,8369],"valid",[],"NV8"],[[8370,8373],"valid",[],"NV8"],[[8374,8376],"valid",[],"NV8"],[[8377,8377],"valid",[],"NV8"],[[8378,8378],"valid",[],"NV8"],[[8379,8381],"valid",[],"NV8"],[[8382,8382],"valid",[],"NV8"],[[8383,8399],"disallowed"],[[8400,8417],"valid",[],"NV8"],[[8418,8419],"valid",[],"NV8"],[[8420,8426],"valid",[],"NV8"],[[8427,8427],"valid",[],"NV8"],[[8428,8431],"valid",[],"NV8"],[[8432,8432],"valid",[],"NV8"],[[8433,8447],"disallowed"],[[8448,8448],"disallowed_STD3_mapped",[97,47,99]],[[8449,8449],"disallowed_STD3_mapped",[97,47,115]],[[8450,8450],"mapped",[99]],[[8451,8451],"mapped",[176,99]],[[8452,8452],"valid",[],"NV8"],[[8453,8453],"disallowed_STD3_mapped",[99,47,111]],[[8454,8454],"disallowed_STD3_mapped",[99,47,117]],[[8455,8455],"mapped",[603]],[[8456,8456],"valid",[],"NV8"],[[8457,8457],"mapped",[176,102]],[[8458,8458],"mapped",[103]],[[8459,8462],"mapped",[104]],[[8463,8463],"mapped",[295]],[[8464,8465],"mapped",[105]],[[8466,8467],"mapped",[108]],[[8468,8468],"valid",[],"NV8"],[[8469,8469],"mapped",[110]],[[8470,8470],"mapped",[110,111]],[[8471,8472],"valid",[],"NV8"],[[8473,8473],"mapped",[112]],[[8474,8474],"mapped",[113]],[[8475,8477],"mapped",[114]],[[8478,8479],"valid",[],"NV8"],[[8480,8480],"mapped",[115,109]],[[8481,8481],"mapped",[116,101,108]],[[8482,8482],"mapped",[116,109]],[[8483,8483],"valid",[],"NV8"],[[8484,8484],"mapped",[122]],[[8485,8485],"valid",[],"NV8"],[[8486,8486],"mapped",[969]],[[8487,8487],"valid",[],"NV8"],[[8488,8488],"mapped",[122]],[[8489,8489],"valid",[],"NV8"],[[8490,8490],"mapped",[107]],[[8491,8491],"mapped",[229]],[[8492,8492],"mapped",[98]],[[8493,8493],"mapped",[99]],[[8494,8494],"valid",[],"NV8"],[[8495,8496],"mapped",[101]],[[8497,8497],"mapped",[102]],[[8498,8498],"disallowed"],[[8499,8499],"mapped",[109]],[[8500,8500],"mapped",[111]],[[8501,8501],"mapped",[1488]],[[8502,8502],"mapped",[1489]],[[8503,8503],"mapped",[1490]],[[8504,8504],"mapped",[1491]],[[8505,8505],"mapped",[105]],[[8506,8506],"valid",[],"NV8"],[[8507,8507],"mapped",[102,97,120]],[[8508,8508],"mapped",[960]],[[8509,8510],"mapped",[947]],[[8511,8511],"mapped",[960]],[[8512,8512],"mapped",[8721]],[[8513,8516],"valid",[],"NV8"],[[8517,8518],"mapped",[100]],[[8519,8519],"mapped",[101]],[[8520,8520],"mapped",[105]],[[8521,8521],"mapped",[106]],[[8522,8523],"valid",[],"NV8"],[[8524,8524],"valid",[],"NV8"],[[8525,8525],"valid",[],"NV8"],[[8526,8526],"valid"],[[8527,8527],"valid",[],"NV8"],[[8528,8528],"mapped",[49,8260,55]],[[8529,8529],"mapped",[49,8260,57]],[[8530,8530],"mapped",[49,8260,49,48]],[[8531,8531],"mapped",[49,8260,51]],[[8532,8532],"mapped",[50,8260,51]],[[8533,8533],"mapped",[49,8260,53]],[[8534,8534],"mapped",[50,8260,53]],[[8535,8535],"mapped",[51,8260,53]],[[8536,8536],"mapped",[52,8260,53]],[[8537,8537],"mapped",[49,8260,54]],[[8538,8538],"mapped",[53,8260,54]],[[8539,8539],"mapped",[49,8260,56]],[[8540,8540],"mapped",[51,8260,56]],[[8541,8541],"mapped",[53,8260,56]],[[8542,8542],"mapped",[55,8260,56]],[[8543,8543],"mapped",[49,8260]],[[8544,8544],"mapped",[105]],[[8545,8545],"mapped",[105,105]],[[8546,8546],"mapped",[105,105,105]],[[8547,8547],"mapped",[105,118]],[[8548,8548],"mapped",[118]],[[8549,8549],"mapped",[118,105]],[[8550,8550],"mapped",[118,105,105]],[[8551,8551],"mapped",[118,105,105,105]],[[8552,8552],"mapped",[105,120]],[[8553,8553],"mapped",[120]],[[8554,8554],"mapped",[120,105]],[[8555,8555],"mapped",[120,105,105]],[[8556,8556],"mapped",[108]],[[8557,8557],"mapped",[99]],[[8558,8558],"mapped",[100]],[[8559,8559],"mapped",[109]],[[8560,8560],"mapped",[105]],[[8561,8561],"mapped",[105,105]],[[8562,8562],"mapped",[105,105,105]],[[8563,8563],"mapped",[105,118]],[[8564,8564],"mapped",[118]],[[8565,8565],"mapped",[118,105]],[[8566,8566],"mapped",[118,105,105]],[[8567,8567],"mapped",[118,105,105,105]],[[8568,8568],"mapped",[105,120]],[[8569,8569],"mapped",[120]],[[8570,8570],"mapped",[120,105]],[[8571,8571],"mapped",[120,105,105]],[[8572,8572],"mapped",[108]],[[8573,8573],"mapped",[99]],[[8574,8574],"mapped",[100]],[[8575,8575],"mapped",[109]],[[8576,8578],"valid",[],"NV8"],[[8579,8579],"disallowed"],[[8580,8580],"valid"],[[8581,8584],"valid",[],"NV8"],[[8585,8585],"mapped",[48,8260,51]],[[8586,8587],"valid",[],"NV8"],[[8588,8591],"disallowed"],[[8592,8682],"valid",[],"NV8"],[[8683,8691],"valid",[],"NV8"],[[8692,8703],"valid",[],"NV8"],[[8704,8747],"valid",[],"NV8"],[[8748,8748],"mapped",[8747,8747]],[[8749,8749],"mapped",[8747,8747,8747]],[[8750,8750],"valid",[],"NV8"],[[8751,8751],"mapped",[8750,8750]],[[8752,8752],"mapped",[8750,8750,8750]],[[8753,8799],"valid",[],"NV8"],[[8800,8800],"disallowed_STD3_valid"],[[8801,8813],"valid",[],"NV8"],[[8814,8815],"disallowed_STD3_valid"],[[8816,8945],"valid",[],"NV8"],[[8946,8959],"valid",[],"NV8"],[[8960,8960],"valid",[],"NV8"],[[8961,8961],"valid",[],"NV8"],[[8962,9000],"valid",[],"NV8"],[[9001,9001],"mapped",[12296]],[[9002,9002],"mapped",[12297]],[[9003,9082],"valid",[],"NV8"],[[9083,9083],"valid",[],"NV8"],[[9084,9084],"valid",[],"NV8"],[[9085,9114],"valid",[],"NV8"],[[9115,9166],"valid",[],"NV8"],[[9167,9168],"valid",[],"NV8"],[[9169,9179],"valid",[],"NV8"],[[9180,9191],"valid",[],"NV8"],[[9192,9192],"valid",[],"NV8"],[[9193,9203],"valid",[],"NV8"],[[9204,9210],"valid",[],"NV8"],[[9211,9215],"disallowed"],[[9216,9252],"valid",[],"NV8"],[[9253,9254],"valid",[],"NV8"],[[9255,9279],"disallowed"],[[9280,9290],"valid",[],"NV8"],[[9291,9311],"disallowed"],[[9312,9312],"mapped",[49]],[[9313,9313],"mapped",[50]],[[9314,9314],"mapped",[51]],[[9315,9315],"mapped",[52]],[[9316,9316],"mapped",[53]],[[9317,9317],"mapped",[54]],[[9318,9318],"mapped",[55]],[[9319,9319],"mapped",[56]],[[9320,9320],"mapped",[57]],[[9321,9321],"mapped",[49,48]],[[9322,9322],"mapped",[49,49]],[[9323,9323],"mapped",[49,50]],[[9324,9324],"mapped",[49,51]],[[9325,9325],"mapped",[49,52]],[[9326,9326],"mapped",[49,53]],[[9327,9327],"mapped",[49,54]],[[9328,9328],"mapped",[49,55]],[[9329,9329],"mapped",[49,56]],[[9330,9330],"mapped",[49,57]],[[9331,9331],"mapped",[50,48]],[[9332,9332],"disallowed_STD3_mapped",[40,49,41]],[[9333,9333],"disallowed_STD3_mapped",[40,50,41]],[[9334,9334],"disallowed_STD3_mapped",[40,51,41]],[[9335,9335],"disallowed_STD3_mapped",[40,52,41]],[[9336,9336],"disallowed_STD3_mapped",[40,53,41]],[[9337,9337],"disallowed_STD3_mapped",[40,54,41]],[[9338,9338],"disallowed_STD3_mapped",[40,55,41]],[[9339,9339],"disallowed_STD3_mapped",[40,56,41]],[[9340,9340],"disallowed_STD3_mapped",[40,57,41]],[[9341,9341],"disallowed_STD3_mapped",[40,49,48,41]],[[9342,9342],"disallowed_STD3_mapped",[40,49,49,41]],[[9343,9343],"disallowed_STD3_mapped",[40,49,50,41]],[[9344,9344],"disallowed_STD3_mapped",[40,49,51,41]],[[9345,9345],"disallowed_STD3_mapped",[40,49,52,41]],[[9346,9346],"disallowed_STD3_mapped",[40,49,53,41]],[[9347,9347],"disallowed_STD3_mapped",[40,49,54,41]],[[9348,9348],"disallowed_STD3_mapped",[40,49,55,41]],[[9349,9349],"disallowed_STD3_mapped",[40,49,56,41]],[[9350,9350],"disallowed_STD3_mapped",[40,49,57,41]],[[9351,9351],"disallowed_STD3_mapped",[40,50,48,41]],[[9352,9371],"disallowed"],[[9372,9372],"disallowed_STD3_mapped",[40,97,41]],[[9373,9373],"disallowed_STD3_mapped",[40,98,41]],[[9374,9374],"disallowed_STD3_mapped",[40,99,41]],[[9375,9375],"disallowed_STD3_mapped",[40,100,41]],[[9376,9376],"disallowed_STD3_mapped",[40,101,41]],[[9377,9377],"disallowed_STD3_mapped",[40,102,41]],[[9378,9378],"disallowed_STD3_mapped",[40,103,41]],[[9379,9379],"disallowed_STD3_mapped",[40,104,41]],[[9380,9380],"disallowed_STD3_mapped",[40,105,41]],[[9381,9381],"disallowed_STD3_mapped",[40,106,41]],[[9382,9382],"disallowed_STD3_mapped",[40,107,41]],[[9383,9383],"disallowed_STD3_mapped",[40,108,41]],[[9384,9384],"disallowed_STD3_mapped",[40,109,41]],[[9385,9385],"disallowed_STD3_mapped",[40,110,41]],[[9386,9386],"disallowed_STD3_mapped",[40,111,41]],[[9387,9387],"disallowed_STD3_mapped",[40,112,41]],[[9388,9388],"disallowed_STD3_mapped",[40,113,41]],[[9389,9389],"disallowed_STD3_mapped",[40,114,41]],[[9390,9390],"disallowed_STD3_mapped",[40,115,41]],[[9391,9391],"disallowed_STD3_mapped",[40,116,41]],[[9392,9392],"disallowed_STD3_mapped",[40,117,41]],[[9393,9393],"disallowed_STD3_mapped",[40,118,41]],[[9394,9394],"disallowed_STD3_mapped",[40,119,41]],[[9395,9395],"disallowed_STD3_mapped",[40,120,41]],[[9396,9396],"disallowed_STD3_mapped",[40,121,41]],[[9397,9397],"disallowed_STD3_mapped",[40,122,41]],[[9398,9398],"mapped",[97]],[[9399,9399],"mapped",[98]],[[9400,9400],"mapped",[99]],[[9401,9401],"mapped",[100]],[[9402,9402],"mapped",[101]],[[9403,9403],"mapped",[102]],[[9404,9404],"mapped",[103]],[[9405,9405],"mapped",[104]],[[9406,9406],"mapped",[105]],[[9407,9407],"mapped",[106]],[[9408,9408],"mapped",[107]],[[9409,9409],"mapped",[108]],[[9410,9410],"mapped",[109]],[[9411,9411],"mapped",[110]],[[9412,9412],"mapped",[111]],[[9413,9413],"mapped",[112]],[[9414,9414],"mapped",[113]],[[9415,9415],"mapped",[114]],[[9416,9416],"mapped",[115]],[[9417,9417],"mapped",[116]],[[9418,9418],"mapped",[117]],[[9419,9419],"mapped",[118]],[[9420,9420],"mapped",[119]],[[9421,9421],"mapped",[120]],[[9422,9422],"mapped",[121]],[[9423,9423],"mapped",[122]],[[9424,9424],"mapped",[97]],[[9425,9425],"mapped",[98]],[[9426,9426],"mapped",[99]],[[9427,9427],"mapped",[100]],[[9428,9428],"mapped",[101]],[[9429,9429],"mapped",[102]],[[9430,9430],"mapped",[103]],[[9431,9431],"mapped",[104]],[[9432,9432],"mapped",[105]],[[9433,9433],"mapped",[106]],[[9434,9434],"mapped",[107]],[[9435,9435],"mapped",[108]],[[9436,9436],"mapped",[109]],[[9437,9437],"mapped",[110]],[[9438,9438],"mapped",[111]],[[9439,9439],"mapped",[112]],[[9440,9440],"mapped",[113]],[[9441,9441],"mapped",[114]],[[9442,9442],"mapped",[115]],[[9443,9443],"mapped",[116]],[[9444,9444],"mapped",[117]],[[9445,9445],"mapped",[118]],[[9446,9446],"mapped",[119]],[[9447,9447],"mapped",[120]],[[9448,9448],"mapped",[121]],[[9449,9449],"mapped",[122]],[[9450,9450],"mapped",[48]],[[9451,9470],"valid",[],"NV8"],[[9471,9471],"valid",[],"NV8"],[[9472,9621],"valid",[],"NV8"],[[9622,9631],"valid",[],"NV8"],[[9632,9711],"valid",[],"NV8"],[[9712,9719],"valid",[],"NV8"],[[9720,9727],"valid",[],"NV8"],[[9728,9747],"valid",[],"NV8"],[[9748,9749],"valid",[],"NV8"],[[9750,9751],"valid",[],"NV8"],[[9752,9752],"valid",[],"NV8"],[[9753,9753],"valid",[],"NV8"],[[9754,9839],"valid",[],"NV8"],[[9840,9841],"valid",[],"NV8"],[[9842,9853],"valid",[],"NV8"],[[9854,9855],"valid",[],"NV8"],[[9856,9865],"valid",[],"NV8"],[[9866,9873],"valid",[],"NV8"],[[9874,9884],"valid",[],"NV8"],[[9885,9885],"valid",[],"NV8"],[[9886,9887],"valid",[],"NV8"],[[9888,9889],"valid",[],"NV8"],[[9890,9905],"valid",[],"NV8"],[[9906,9906],"valid",[],"NV8"],[[9907,9916],"valid",[],"NV8"],[[9917,9919],"valid",[],"NV8"],[[9920,9923],"valid",[],"NV8"],[[9924,9933],"valid",[],"NV8"],[[9934,9934],"valid",[],"NV8"],[[9935,9953],"valid",[],"NV8"],[[9954,9954],"valid",[],"NV8"],[[9955,9955],"valid",[],"NV8"],[[9956,9959],"valid",[],"NV8"],[[9960,9983],"valid",[],"NV8"],[[9984,9984],"valid",[],"NV8"],[[9985,9988],"valid",[],"NV8"],[[9989,9989],"valid",[],"NV8"],[[9990,9993],"valid",[],"NV8"],[[9994,9995],"valid",[],"NV8"],[[9996,10023],"valid",[],"NV8"],[[10024,10024],"valid",[],"NV8"],[[10025,10059],"valid",[],"NV8"],[[10060,10060],"valid",[],"NV8"],[[10061,10061],"valid",[],"NV8"],[[10062,10062],"valid",[],"NV8"],[[10063,10066],"valid",[],"NV8"],[[10067,10069],"valid",[],"NV8"],[[10070,10070],"valid",[],"NV8"],[[10071,10071],"valid",[],"NV8"],[[10072,10078],"valid",[],"NV8"],[[10079,10080],"valid",[],"NV8"],[[10081,10087],"valid",[],"NV8"],[[10088,10101],"valid",[],"NV8"],[[10102,10132],"valid",[],"NV8"],[[10133,10135],"valid",[],"NV8"],[[10136,10159],"valid",[],"NV8"],[[10160,10160],"valid",[],"NV8"],[[10161,10174],"valid",[],"NV8"],[[10175,10175],"valid",[],"NV8"],[[10176,10182],"valid",[],"NV8"],[[10183,10186],"valid",[],"NV8"],[[10187,10187],"valid",[],"NV8"],[[10188,10188],"valid",[],"NV8"],[[10189,10189],"valid",[],"NV8"],[[10190,10191],"valid",[],"NV8"],[[10192,10219],"valid",[],"NV8"],[[10220,10223],"valid",[],"NV8"],[[10224,10239],"valid",[],"NV8"],[[10240,10495],"valid",[],"NV8"],[[10496,10763],"valid",[],"NV8"],[[10764,10764],"mapped",[8747,8747,8747,8747]],[[10765,10867],"valid",[],"NV8"],[[10868,10868],"disallowed_STD3_mapped",[58,58,61]],[[10869,10869],"disallowed_STD3_mapped",[61,61]],[[10870,10870],"disallowed_STD3_mapped",[61,61,61]],[[10871,10971],"valid",[],"NV8"],[[10972,10972],"mapped",[10973,824]],[[10973,11007],"valid",[],"NV8"],[[11008,11021],"valid",[],"NV8"],[[11022,11027],"valid",[],"NV8"],[[11028,11034],"valid",[],"NV8"],[[11035,11039],"valid",[],"NV8"],[[11040,11043],"valid",[],"NV8"],[[11044,11084],"valid",[],"NV8"],[[11085,11087],"valid",[],"NV8"],[[11088,11092],"valid",[],"NV8"],[[11093,11097],"valid",[],"NV8"],[[11098,11123],"valid",[],"NV8"],[[11124,11125],"disallowed"],[[11126,11157],"valid",[],"NV8"],[[11158,11159],"disallowed"],[[11160,11193],"valid",[],"NV8"],[[11194,11196],"disallowed"],[[11197,11208],"valid",[],"NV8"],[[11209,11209],"disallowed"],[[11210,11217],"valid",[],"NV8"],[[11218,11243],"disallowed"],[[11244,11247],"valid",[],"NV8"],[[11248,11263],"disallowed"],[[11264,11264],"mapped",[11312]],[[11265,11265],"mapped",[11313]],[[11266,11266],"mapped",[11314]],[[11267,11267],"mapped",[11315]],[[11268,11268],"mapped",[11316]],[[11269,11269],"mapped",[11317]],[[11270,11270],"mapped",[11318]],[[11271,11271],"mapped",[11319]],[[11272,11272],"mapped",[11320]],[[11273,11273],"mapped",[11321]],[[11274,11274],"mapped",[11322]],[[11275,11275],"mapped",[11323]],[[11276,11276],"mapped",[11324]],[[11277,11277],"mapped",[11325]],[[11278,11278],"mapped",[11326]],[[11279,11279],"mapped",[11327]],[[11280,11280],"mapped",[11328]],[[11281,11281],"mapped",[11329]],[[11282,11282],"mapped",[11330]],[[11283,11283],"mapped",[11331]],[[11284,11284],"mapped",[11332]],[[11285,11285],"mapped",[11333]],[[11286,11286],"mapped",[11334]],[[11287,11287],"mapped",[11335]],[[11288,11288],"mapped",[11336]],[[11289,11289],"mapped",[11337]],[[11290,11290],"mapped",[11338]],[[11291,11291],"mapped",[11339]],[[11292,11292],"mapped",[11340]],[[11293,11293],"mapped",[11341]],[[11294,11294],"mapped",[11342]],[[11295,11295],"mapped",[11343]],[[11296,11296],"mapped",[11344]],[[11297,11297],"mapped",[11345]],[[11298,11298],"mapped",[11346]],[[11299,11299],"mapped",[11347]],[[11300,11300],"mapped",[11348]],[[11301,11301],"mapped",[11349]],[[11302,11302],"mapped",[11350]],[[11303,11303],"mapped",[11351]],[[11304,11304],"mapped",[11352]],[[11305,11305],"mapped",[11353]],[[11306,11306],"mapped",[11354]],[[11307,11307],"mapped",[11355]],[[11308,11308],"mapped",[11356]],[[11309,11309],"mapped",[11357]],[[11310,11310],"mapped",[11358]],[[11311,11311],"disallowed"],[[11312,11358],"valid"],[[11359,11359],"disallowed"],[[11360,11360],"mapped",[11361]],[[11361,11361],"valid"],[[11362,11362],"mapped",[619]],[[11363,11363],"mapped",[7549]],[[11364,11364],"mapped",[637]],[[11365,11366],"valid"],[[11367,11367],"mapped",[11368]],[[11368,11368],"valid"],[[11369,11369],"mapped",[11370]],[[11370,11370],"valid"],[[11371,11371],"mapped",[11372]],[[11372,11372],"valid"],[[11373,11373],"mapped",[593]],[[11374,11374],"mapped",[625]],[[11375,11375],"mapped",[592]],[[11376,11376],"mapped",[594]],[[11377,11377],"valid"],[[11378,11378],"mapped",[11379]],[[11379,11379],"valid"],[[11380,11380],"valid"],[[11381,11381],"mapped",[11382]],[[11382,11383],"valid"],[[11384,11387],"valid"],[[11388,11388],"mapped",[106]],[[11389,11389],"mapped",[118]],[[11390,11390],"mapped",[575]],[[11391,11391],"mapped",[576]],[[11392,11392],"mapped",[11393]],[[11393,11393],"valid"],[[11394,11394],"mapped",[11395]],[[11395,11395],"valid"],[[11396,11396],"mapped",[11397]],[[11397,11397],"valid"],[[11398,11398],"mapped",[11399]],[[11399,11399],"valid"],[[11400,11400],"mapped",[11401]],[[11401,11401],"valid"],[[11402,11402],"mapped",[11403]],[[11403,11403],"valid"],[[11404,11404],"mapped",[11405]],[[11405,11405],"valid"],[[11406,11406],"mapped",[11407]],[[11407,11407],"valid"],[[11408,11408],"mapped",[11409]],[[11409,11409],"valid"],[[11410,11410],"mapped",[11411]],[[11411,11411],"valid"],[[11412,11412],"mapped",[11413]],[[11413,11413],"valid"],[[11414,11414],"mapped",[11415]],[[11415,11415],"valid"],[[11416,11416],"mapped",[11417]],[[11417,11417],"valid"],[[11418,11418],"mapped",[11419]],[[11419,11419],"valid"],[[11420,11420],"mapped",[11421]],[[11421,11421],"valid"],[[11422,11422],"mapped",[11423]],[[11423,11423],"valid"],[[11424,11424],"mapped",[11425]],[[11425,11425],"valid"],[[11426,11426],"mapped",[11427]],[[11427,11427],"valid"],[[11428,11428],"mapped",[11429]],[[11429,11429],"valid"],[[11430,11430],"mapped",[11431]],[[11431,11431],"valid"],[[11432,11432],"mapped",[11433]],[[11433,11433],"valid"],[[11434,11434],"mapped",[11435]],[[11435,11435],"valid"],[[11436,11436],"mapped",[11437]],[[11437,11437],"valid"],[[11438,11438],"mapped",[11439]],[[11439,11439],"valid"],[[11440,11440],"mapped",[11441]],[[11441,11441],"valid"],[[11442,11442],"mapped",[11443]],[[11443,11443],"valid"],[[11444,11444],"mapped",[11445]],[[11445,11445],"valid"],[[11446,11446],"mapped",[11447]],[[11447,11447],"valid"],[[11448,11448],"mapped",[11449]],[[11449,11449],"valid"],[[11450,11450],"mapped",[11451]],[[11451,11451],"valid"],[[11452,11452],"mapped",[11453]],[[11453,11453],"valid"],[[11454,11454],"mapped",[11455]],[[11455,11455],"valid"],[[11456,11456],"mapped",[11457]],[[11457,11457],"valid"],[[11458,11458],"mapped",[11459]],[[11459,11459],"valid"],[[11460,11460],"mapped",[11461]],[[11461,11461],"valid"],[[11462,11462],"mapped",[11463]],[[11463,11463],"valid"],[[11464,11464],"mapped",[11465]],[[11465,11465],"valid"],[[11466,11466],"mapped",[11467]],[[11467,11467],"valid"],[[11468,11468],"mapped",[11469]],[[11469,11469],"valid"],[[11470,11470],"mapped",[11471]],[[11471,11471],"valid"],[[11472,11472],"mapped",[11473]],[[11473,11473],"valid"],[[11474,11474],"mapped",[11475]],[[11475,11475],"valid"],[[11476,11476],"mapped",[11477]],[[11477,11477],"valid"],[[11478,11478],"mapped",[11479]],[[11479,11479],"valid"],[[11480,11480],"mapped",[11481]],[[11481,11481],"valid"],[[11482,11482],"mapped",[11483]],[[11483,11483],"valid"],[[11484,11484],"mapped",[11485]],[[11485,11485],"valid"],[[11486,11486],"mapped",[11487]],[[11487,11487],"valid"],[[11488,11488],"mapped",[11489]],[[11489,11489],"valid"],[[11490,11490],"mapped",[11491]],[[11491,11492],"valid"],[[11493,11498],"valid",[],"NV8"],[[11499,11499],"mapped",[11500]],[[11500,11500],"valid"],[[11501,11501],"mapped",[11502]],[[11502,11505],"valid"],[[11506,11506],"mapped",[11507]],[[11507,11507],"valid"],[[11508,11512],"disallowed"],[[11513,11519],"valid",[],"NV8"],[[11520,11557],"valid"],[[11558,11558],"disallowed"],[[11559,11559],"valid"],[[11560,11564],"disallowed"],[[11565,11565],"valid"],[[11566,11567],"disallowed"],[[11568,11621],"valid"],[[11622,11623],"valid"],[[11624,11630],"disallowed"],[[11631,11631],"mapped",[11617]],[[11632,11632],"valid",[],"NV8"],[[11633,11646],"disallowed"],[[11647,11647],"valid"],[[11648,11670],"valid"],[[11671,11679],"disallowed"],[[11680,11686],"valid"],[[11687,11687],"disallowed"],[[11688,11694],"valid"],[[11695,11695],"disallowed"],[[11696,11702],"valid"],[[11703,11703],"disallowed"],[[11704,11710],"valid"],[[11711,11711],"disallowed"],[[11712,11718],"valid"],[[11719,11719],"disallowed"],[[11720,11726],"valid"],[[11727,11727],"disallowed"],[[11728,11734],"valid"],[[11735,11735],"disallowed"],[[11736,11742],"valid"],[[11743,11743],"disallowed"],[[11744,11775],"valid"],[[11776,11799],"valid",[],"NV8"],[[11800,11803],"valid",[],"NV8"],[[11804,11805],"valid",[],"NV8"],[[11806,11822],"valid",[],"NV8"],[[11823,11823],"valid"],[[11824,11824],"valid",[],"NV8"],[[11825,11825],"valid",[],"NV8"],[[11826,11835],"valid",[],"NV8"],[[11836,11842],"valid",[],"NV8"],[[11843,11903],"disallowed"],[[11904,11929],"valid",[],"NV8"],[[11930,11930],"disallowed"],[[11931,11934],"valid",[],"NV8"],[[11935,11935],"mapped",[27597]],[[11936,12018],"valid",[],"NV8"],[[12019,12019],"mapped",[40863]],[[12020,12031],"disallowed"],[[12032,12032],"mapped",[19968]],[[12033,12033],"mapped",[20008]],[[12034,12034],"mapped",[20022]],[[12035,12035],"mapped",[20031]],[[12036,12036],"mapped",[20057]],[[12037,12037],"mapped",[20101]],[[12038,12038],"mapped",[20108]],[[12039,12039],"mapped",[20128]],[[12040,12040],"mapped",[20154]],[[12041,12041],"mapped",[20799]],[[12042,12042],"mapped",[20837]],[[12043,12043],"mapped",[20843]],[[12044,12044],"mapped",[20866]],[[12045,12045],"mapped",[20886]],[[12046,12046],"mapped",[20907]],[[12047,12047],"mapped",[20960]],[[12048,12048],"mapped",[20981]],[[12049,12049],"mapped",[20992]],[[12050,12050],"mapped",[21147]],[[12051,12051],"mapped",[21241]],[[12052,12052],"mapped",[21269]],[[12053,12053],"mapped",[21274]],[[12054,12054],"mapped",[21304]],[[12055,12055],"mapped",[21313]],[[12056,12056],"mapped",[21340]],[[12057,12057],"mapped",[21353]],[[12058,12058],"mapped",[21378]],[[12059,12059],"mapped",[21430]],[[12060,12060],"mapped",[21448]],[[12061,12061],"mapped",[21475]],[[12062,12062],"mapped",[22231]],[[12063,12063],"mapped",[22303]],[[12064,12064],"mapped",[22763]],[[12065,12065],"mapped",[22786]],[[12066,12066],"mapped",[22794]],[[12067,12067],"mapped",[22805]],[[12068,12068],"mapped",[22823]],[[12069,12069],"mapped",[22899]],[[12070,12070],"mapped",[23376]],[[12071,12071],"mapped",[23424]],[[12072,12072],"mapped",[23544]],[[12073,12073],"mapped",[23567]],[[12074,12074],"mapped",[23586]],[[12075,12075],"mapped",[23608]],[[12076,12076],"mapped",[23662]],[[12077,12077],"mapped",[23665]],[[12078,12078],"mapped",[24027]],[[12079,12079],"mapped",[24037]],[[12080,12080],"mapped",[24049]],[[12081,12081],"mapped",[24062]],[[12082,12082],"mapped",[24178]],[[12083,12083],"mapped",[24186]],[[12084,12084],"mapped",[24191]],[[12085,12085],"mapped",[24308]],[[12086,12086],"mapped",[24318]],[[12087,12087],"mapped",[24331]],[[12088,12088],"mapped",[24339]],[[12089,12089],"mapped",[24400]],[[12090,12090],"mapped",[24417]],[[12091,12091],"mapped",[24435]],[[12092,12092],"mapped",[24515]],[[12093,12093],"mapped",[25096]],[[12094,12094],"mapped",[25142]],[[12095,12095],"mapped",[25163]],[[12096,12096],"mapped",[25903]],[[12097,12097],"mapped",[25908]],[[12098,12098],"mapped",[25991]],[[12099,12099],"mapped",[26007]],[[12100,12100],"mapped",[26020]],[[12101,12101],"mapped",[26041]],[[12102,12102],"mapped",[26080]],[[12103,12103],"mapped",[26085]],[[12104,12104],"mapped",[26352]],[[12105,12105],"mapped",[26376]],[[12106,12106],"mapped",[26408]],[[12107,12107],"mapped",[27424]],[[12108,12108],"mapped",[27490]],[[12109,12109],"mapped",[27513]],[[12110,12110],"mapped",[27571]],[[12111,12111],"mapped",[27595]],[[12112,12112],"mapped",[27604]],[[12113,12113],"mapped",[27611]],[[12114,12114],"mapped",[27663]],[[12115,12115],"mapped",[27668]],[[12116,12116],"mapped",[27700]],[[12117,12117],"mapped",[28779]],[[12118,12118],"mapped",[29226]],[[12119,12119],"mapped",[29238]],[[12120,12120],"mapped",[29243]],[[12121,12121],"mapped",[29247]],[[12122,12122],"mapped",[29255]],[[12123,12123],"mapped",[29273]],[[12124,12124],"mapped",[29275]],[[12125,12125],"mapped",[29356]],[[12126,12126],"mapped",[29572]],[[12127,12127],"mapped",[29577]],[[12128,12128],"mapped",[29916]],[[12129,12129],"mapped",[29926]],[[12130,12130],"mapped",[29976]],[[12131,12131],"mapped",[29983]],[[12132,12132],"mapped",[29992]],[[12133,12133],"mapped",[30000]],[[12134,12134],"mapped",[30091]],[[12135,12135],"mapped",[30098]],[[12136,12136],"mapped",[30326]],[[12137,12137],"mapped",[30333]],[[12138,12138],"mapped",[30382]],[[12139,12139],"mapped",[30399]],[[12140,12140],"mapped",[30446]],[[12141,12141],"mapped",[30683]],[[12142,12142],"mapped",[30690]],[[12143,12143],"mapped",[30707]],[[12144,12144],"mapped",[31034]],[[12145,12145],"mapped",[31160]],[[12146,12146],"mapped",[31166]],[[12147,12147],"mapped",[31348]],[[12148,12148],"mapped",[31435]],[[12149,12149],"mapped",[31481]],[[12150,12150],"mapped",[31859]],[[12151,12151],"mapped",[31992]],[[12152,12152],"mapped",[32566]],[[12153,12153],"mapped",[32593]],[[12154,12154],"mapped",[32650]],[[12155,12155],"mapped",[32701]],[[12156,12156],"mapped",[32769]],[[12157,12157],"mapped",[32780]],[[12158,12158],"mapped",[32786]],[[12159,12159],"mapped",[32819]],[[12160,12160],"mapped",[32895]],[[12161,12161],"mapped",[32905]],[[12162,12162],"mapped",[33251]],[[12163,12163],"mapped",[33258]],[[12164,12164],"mapped",[33267]],[[12165,12165],"mapped",[33276]],[[12166,12166],"mapped",[33292]],[[12167,12167],"mapped",[33307]],[[12168,12168],"mapped",[33311]],[[12169,12169],"mapped",[33390]],[[12170,12170],"mapped",[33394]],[[12171,12171],"mapped",[33400]],[[12172,12172],"mapped",[34381]],[[12173,12173],"mapped",[34411]],[[12174,12174],"mapped",[34880]],[[12175,12175],"mapped",[34892]],[[12176,12176],"mapped",[34915]],[[12177,12177],"mapped",[35198]],[[12178,12178],"mapped",[35211]],[[12179,12179],"mapped",[35282]],[[12180,12180],"mapped",[35328]],[[12181,12181],"mapped",[35895]],[[12182,12182],"mapped",[35910]],[[12183,12183],"mapped",[35925]],[[12184,12184],"mapped",[35960]],[[12185,12185],"mapped",[35997]],[[12186,12186],"mapped",[36196]],[[12187,12187],"mapped",[36208]],[[12188,12188],"mapped",[36275]],[[12189,12189],"mapped",[36523]],[[12190,12190],"mapped",[36554]],[[12191,12191],"mapped",[36763]],[[12192,12192],"mapped",[36784]],[[12193,12193],"mapped",[36789]],[[12194,12194],"mapped",[37009]],[[12195,12195],"mapped",[37193]],[[12196,12196],"mapped",[37318]],[[12197,12197],"mapped",[37324]],[[12198,12198],"mapped",[37329]],[[12199,12199],"mapped",[38263]],[[12200,12200],"mapped",[38272]],[[12201,12201],"mapped",[38428]],[[12202,12202],"mapped",[38582]],[[12203,12203],"mapped",[38585]],[[12204,12204],"mapped",[38632]],[[12205,12205],"mapped",[38737]],[[12206,12206],"mapped",[38750]],[[12207,12207],"mapped",[38754]],[[12208,12208],"mapped",[38761]],[[12209,12209],"mapped",[38859]],[[12210,12210],"mapped",[38893]],[[12211,12211],"mapped",[38899]],[[12212,12212],"mapped",[38913]],[[12213,12213],"mapped",[39080]],[[12214,12214],"mapped",[39131]],[[12215,12215],"mapped",[39135]],[[12216,12216],"mapped",[39318]],[[12217,12217],"mapped",[39321]],[[12218,12218],"mapped",[39340]],[[12219,12219],"mapped",[39592]],[[12220,12220],"mapped",[39640]],[[12221,12221],"mapped",[39647]],[[12222,12222],"mapped",[39717]],[[12223,12223],"mapped",[39727]],[[12224,12224],"mapped",[39730]],[[12225,12225],"mapped",[39740]],[[12226,12226],"mapped",[39770]],[[12227,12227],"mapped",[40165]],[[12228,12228],"mapped",[40565]],[[12229,12229],"mapped",[40575]],[[12230,12230],"mapped",[40613]],[[12231,12231],"mapped",[40635]],[[12232,12232],"mapped",[40643]],[[12233,12233],"mapped",[40653]],[[12234,12234],"mapped",[40657]],[[12235,12235],"mapped",[40697]],[[12236,12236],"mapped",[40701]],[[12237,12237],"mapped",[40718]],[[12238,12238],"mapped",[40723]],[[12239,12239],"mapped",[40736]],[[12240,12240],"mapped",[40763]],[[12241,12241],"mapped",[40778]],[[12242,12242],"mapped",[40786]],[[12243,12243],"mapped",[40845]],[[12244,12244],"mapped",[40860]],[[12245,12245],"mapped",[40864]],[[12246,12271],"disallowed"],[[12272,12283],"disallowed"],[[12284,12287],"disallowed"],[[12288,12288],"disallowed_STD3_mapped",[32]],[[12289,12289],"valid",[],"NV8"],[[12290,12290],"mapped",[46]],[[12291,12292],"valid",[],"NV8"],[[12293,12295],"valid"],[[12296,12329],"valid",[],"NV8"],[[12330,12333],"valid"],[[12334,12341],"valid",[],"NV8"],[[12342,12342],"mapped",[12306]],[[12343,12343],"valid",[],"NV8"],[[12344,12344],"mapped",[21313]],[[12345,12345],"mapped",[21316]],[[12346,12346],"mapped",[21317]],[[12347,12347],"valid",[],"NV8"],[[12348,12348],"valid"],[[12349,12349],"valid",[],"NV8"],[[12350,12350],"valid",[],"NV8"],[[12351,12351],"valid",[],"NV8"],[[12352,12352],"disallowed"],[[12353,12436],"valid"],[[12437,12438],"valid"],[[12439,12440],"disallowed"],[[12441,12442],"valid"],[[12443,12443],"disallowed_STD3_mapped",[32,12441]],[[12444,12444],"disallowed_STD3_mapped",[32,12442]],[[12445,12446],"valid"],[[12447,12447],"mapped",[12424,12426]],[[12448,12448],"valid",[],"NV8"],[[12449,12542],"valid"],[[12543,12543],"mapped",[12467,12488]],[[12544,12548],"disallowed"],[[12549,12588],"valid"],[[12589,12589],"valid"],[[12590,12592],"disallowed"],[[12593,12593],"mapped",[4352]],[[12594,12594],"mapped",[4353]],[[12595,12595],"mapped",[4522]],[[12596,12596],"mapped",[4354]],[[12597,12597],"mapped",[4524]],[[12598,12598],"mapped",[4525]],[[12599,12599],"mapped",[4355]],[[12600,12600],"mapped",[4356]],[[12601,12601],"mapped",[4357]],[[12602,12602],"mapped",[4528]],[[12603,12603],"mapped",[4529]],[[12604,12604],"mapped",[4530]],[[12605,12605],"mapped",[4531]],[[12606,12606],"mapped",[4532]],[[12607,12607],"mapped",[4533]],[[12608,12608],"mapped",[4378]],[[12609,12609],"mapped",[4358]],[[12610,12610],"mapped",[4359]],[[12611,12611],"mapped",[4360]],[[12612,12612],"mapped",[4385]],[[12613,12613],"mapped",[4361]],[[12614,12614],"mapped",[4362]],[[12615,12615],"mapped",[4363]],[[12616,12616],"mapped",[4364]],[[12617,12617],"mapped",[4365]],[[12618,12618],"mapped",[4366]],[[12619,12619],"mapped",[4367]],[[12620,12620],"mapped",[4368]],[[12621,12621],"mapped",[4369]],[[12622,12622],"mapped",[4370]],[[12623,12623],"mapped",[4449]],[[12624,12624],"mapped",[4450]],[[12625,12625],"mapped",[4451]],[[12626,12626],"mapped",[4452]],[[12627,12627],"mapped",[4453]],[[12628,12628],"mapped",[4454]],[[12629,12629],"mapped",[4455]],[[12630,12630],"mapped",[4456]],[[12631,12631],"mapped",[4457]],[[12632,12632],"mapped",[4458]],[[12633,12633],"mapped",[4459]],[[12634,12634],"mapped",[4460]],[[12635,12635],"mapped",[4461]],[[12636,12636],"mapped",[4462]],[[12637,12637],"mapped",[4463]],[[12638,12638],"mapped",[4464]],[[12639,12639],"mapped",[4465]],[[12640,12640],"mapped",[4466]],[[12641,12641],"mapped",[4467]],[[12642,12642],"mapped",[4468]],[[12643,12643],"mapped",[4469]],[[12644,12644],"disallowed"],[[12645,12645],"mapped",[4372]],[[12646,12646],"mapped",[4373]],[[12647,12647],"mapped",[4551]],[[12648,12648],"mapped",[4552]],[[12649,12649],"mapped",[4556]],[[12650,12650],"mapped",[4558]],[[12651,12651],"mapped",[4563]],[[12652,12652],"mapped",[4567]],[[12653,12653],"mapped",[4569]],[[12654,12654],"mapped",[4380]],[[12655,12655],"mapped",[4573]],[[12656,12656],"mapped",[4575]],[[12657,12657],"mapped",[4381]],[[12658,12658],"mapped",[4382]],[[12659,12659],"mapped",[4384]],[[12660,12660],"mapped",[4386]],[[12661,12661],"mapped",[4387]],[[12662,12662],"mapped",[4391]],[[12663,12663],"mapped",[4393]],[[12664,12664],"mapped",[4395]],[[12665,12665],"mapped",[4396]],[[12666,12666],"mapped",[4397]],[[12667,12667],"mapped",[4398]],[[12668,12668],"mapped",[4399]],[[12669,12669],"mapped",[4402]],[[12670,12670],"mapped",[4406]],[[12671,12671],"mapped",[4416]],[[12672,12672],"mapped",[4423]],[[12673,12673],"mapped",[4428]],[[12674,12674],"mapped",[4593]],[[12675,12675],"mapped",[4594]],[[12676,12676],"mapped",[4439]],[[12677,12677],"mapped",[4440]],[[12678,12678],"mapped",[4441]],[[12679,12679],"mapped",[4484]],[[12680,12680],"mapped",[4485]],[[12681,12681],"mapped",[4488]],[[12682,12682],"mapped",[4497]],[[12683,12683],"mapped",[4498]],[[12684,12684],"mapped",[4500]],[[12685,12685],"mapped",[4510]],[[12686,12686],"mapped",[4513]],[[12687,12687],"disallowed"],[[12688,12689],"valid",[],"NV8"],[[12690,12690],"mapped",[19968]],[[12691,12691],"mapped",[20108]],[[12692,12692],"mapped",[19977]],[[12693,12693],"mapped",[22235]],[[12694,12694],"mapped",[19978]],[[12695,12695],"mapped",[20013]],[[12696,12696],"mapped",[19979]],[[12697,12697],"mapped",[30002]],[[12698,12698],"mapped",[20057]],[[12699,12699],"mapped",[19993]],[[12700,12700],"mapped",[19969]],[[12701,12701],"mapped",[22825]],[[12702,12702],"mapped",[22320]],[[12703,12703],"mapped",[20154]],[[12704,12727],"valid"],[[12728,12730],"valid"],[[12731,12735],"disallowed"],[[12736,12751],"valid",[],"NV8"],[[12752,12771],"valid",[],"NV8"],[[12772,12783],"disallowed"],[[12784,12799],"valid"],[[12800,12800],"disallowed_STD3_mapped",[40,4352,41]],[[12801,12801],"disallowed_STD3_mapped",[40,4354,41]],[[12802,12802],"disallowed_STD3_mapped",[40,4355,41]],[[12803,12803],"disallowed_STD3_mapped",[40,4357,41]],[[12804,12804],"disallowed_STD3_mapped",[40,4358,41]],[[12805,12805],"disallowed_STD3_mapped",[40,4359,41]],[[12806,12806],"disallowed_STD3_mapped",[40,4361,41]],[[12807,12807],"disallowed_STD3_mapped",[40,4363,41]],[[12808,12808],"disallowed_STD3_mapped",[40,4364,41]],[[12809,12809],"disallowed_STD3_mapped",[40,4366,41]],[[12810,12810],"disallowed_STD3_mapped",[40,4367,41]],[[12811,12811],"disallowed_STD3_mapped",[40,4368,41]],[[12812,12812],"disallowed_STD3_mapped",[40,4369,41]],[[12813,12813],"disallowed_STD3_mapped",[40,4370,41]],[[12814,12814],"disallowed_STD3_mapped",[40,44032,41]],[[12815,12815],"disallowed_STD3_mapped",[40,45208,41]],[[12816,12816],"disallowed_STD3_mapped",[40,45796,41]],[[12817,12817],"disallowed_STD3_mapped",[40,46972,41]],[[12818,12818],"disallowed_STD3_mapped",[40,47560,41]],[[12819,12819],"disallowed_STD3_mapped",[40,48148,41]],[[12820,12820],"disallowed_STD3_mapped",[40,49324,41]],[[12821,12821],"disallowed_STD3_mapped",[40,50500,41]],[[12822,12822],"disallowed_STD3_mapped",[40,51088,41]],[[12823,12823],"disallowed_STD3_mapped",[40,52264,41]],[[12824,12824],"disallowed_STD3_mapped",[40,52852,41]],[[12825,12825],"disallowed_STD3_mapped",[40,53440,41]],[[12826,12826],"disallowed_STD3_mapped",[40,54028,41]],[[12827,12827],"disallowed_STD3_mapped",[40,54616,41]],[[12828,12828],"disallowed_STD3_mapped",[40,51452,41]],[[12829,12829],"disallowed_STD3_mapped",[40,50724,51204,41]],[[12830,12830],"disallowed_STD3_mapped",[40,50724,54980,41]],[[12831,12831],"disallowed"],[[12832,12832],"disallowed_STD3_mapped",[40,19968,41]],[[12833,12833],"disallowed_STD3_mapped",[40,20108,41]],[[12834,12834],"disallowed_STD3_mapped",[40,19977,41]],[[12835,12835],"disallowed_STD3_mapped",[40,22235,41]],[[12836,12836],"disallowed_STD3_mapped",[40,20116,41]],[[12837,12837],"disallowed_STD3_mapped",[40,20845,41]],[[12838,12838],"disallowed_STD3_mapped",[40,19971,41]],[[12839,12839],"disallowed_STD3_mapped",[40,20843,41]],[[12840,12840],"disallowed_STD3_mapped",[40,20061,41]],[[12841,12841],"disallowed_STD3_mapped",[40,21313,41]],[[12842,12842],"disallowed_STD3_mapped",[40,26376,41]],[[12843,12843],"disallowed_STD3_mapped",[40,28779,41]],[[12844,12844],"disallowed_STD3_mapped",[40,27700,41]],[[12845,12845],"disallowed_STD3_mapped",[40,26408,41]],[[12846,12846],"disallowed_STD3_mapped",[40,37329,41]],[[12847,12847],"disallowed_STD3_mapped",[40,22303,41]],[[12848,12848],"disallowed_STD3_mapped",[40,26085,41]],[[12849,12849],"disallowed_STD3_mapped",[40,26666,41]],[[12850,12850],"disallowed_STD3_mapped",[40,26377,41]],[[12851,12851],"disallowed_STD3_mapped",[40,31038,41]],[[12852,12852],"disallowed_STD3_mapped",[40,21517,41]],[[12853,12853],"disallowed_STD3_mapped",[40,29305,41]],[[12854,12854],"disallowed_STD3_mapped",[40,36001,41]],[[12855,12855],"disallowed_STD3_mapped",[40,31069,41]],[[12856,12856],"disallowed_STD3_mapped",[40,21172,41]],[[12857,12857],"disallowed_STD3_mapped",[40,20195,41]],[[12858,12858],"disallowed_STD3_mapped",[40,21628,41]],[[12859,12859],"disallowed_STD3_mapped",[40,23398,41]],[[12860,12860],"disallowed_STD3_mapped",[40,30435,41]],[[12861,12861],"disallowed_STD3_mapped",[40,20225,41]],[[12862,12862],"disallowed_STD3_mapped",[40,36039,41]],[[12863,12863],"disallowed_STD3_mapped",[40,21332,41]],[[12864,12864],"disallowed_STD3_mapped",[40,31085,41]],[[12865,12865],"disallowed_STD3_mapped",[40,20241,41]],[[12866,12866],"disallowed_STD3_mapped",[40,33258,41]],[[12867,12867],"disallowed_STD3_mapped",[40,33267,41]],[[12868,12868],"mapped",[21839]],[[12869,12869],"mapped",[24188]],[[12870,12870],"mapped",[25991]],[[12871,12871],"mapped",[31631]],[[12872,12879],"valid",[],"NV8"],[[12880,12880],"mapped",[112,116,101]],[[12881,12881],"mapped",[50,49]],[[12882,12882],"mapped",[50,50]],[[12883,12883],"mapped",[50,51]],[[12884,12884],"mapped",[50,52]],[[12885,12885],"mapped",[50,53]],[[12886,12886],"mapped",[50,54]],[[12887,12887],"mapped",[50,55]],[[12888,12888],"mapped",[50,56]],[[12889,12889],"mapped",[50,57]],[[12890,12890],"mapped",[51,48]],[[12891,12891],"mapped",[51,49]],[[12892,12892],"mapped",[51,50]],[[12893,12893],"mapped",[51,51]],[[12894,12894],"mapped",[51,52]],[[12895,12895],"mapped",[51,53]],[[12896,12896],"mapped",[4352]],[[12897,12897],"mapped",[4354]],[[12898,12898],"mapped",[4355]],[[12899,12899],"mapped",[4357]],[[12900,12900],"mapped",[4358]],[[12901,12901],"mapped",[4359]],[[12902,12902],"mapped",[4361]],[[12903,12903],"mapped",[4363]],[[12904,12904],"mapped",[4364]],[[12905,12905],"mapped",[4366]],[[12906,12906],"mapped",[4367]],[[12907,12907],"mapped",[4368]],[[12908,12908],"mapped",[4369]],[[12909,12909],"mapped",[4370]],[[12910,12910],"mapped",[44032]],[[12911,12911],"mapped",[45208]],[[12912,12912],"mapped",[45796]],[[12913,12913],"mapped",[46972]],[[12914,12914],"mapped",[47560]],[[12915,12915],"mapped",[48148]],[[12916,12916],"mapped",[49324]],[[12917,12917],"mapped",[50500]],[[12918,12918],"mapped",[51088]],[[12919,12919],"mapped",[52264]],[[12920,12920],"mapped",[52852]],[[12921,12921],"mapped",[53440]],[[12922,12922],"mapped",[54028]],[[12923,12923],"mapped",[54616]],[[12924,12924],"mapped",[52280,44256]],[[12925,12925],"mapped",[51452,51032]],[[12926,12926],"mapped",[50864]],[[12927,12927],"valid",[],"NV8"],[[12928,12928],"mapped",[19968]],[[12929,12929],"mapped",[20108]],[[12930,12930],"mapped",[19977]],[[12931,12931],"mapped",[22235]],[[12932,12932],"mapped",[20116]],[[12933,12933],"mapped",[20845]],[[12934,12934],"mapped",[19971]],[[12935,12935],"mapped",[20843]],[[12936,12936],"mapped",[20061]],[[12937,12937],"mapped",[21313]],[[12938,12938],"mapped",[26376]],[[12939,12939],"mapped",[28779]],[[12940,12940],"mapped",[27700]],[[12941,12941],"mapped",[26408]],[[12942,12942],"mapped",[37329]],[[12943,12943],"mapped",[22303]],[[12944,12944],"mapped",[26085]],[[12945,12945],"mapped",[26666]],[[12946,12946],"mapped",[26377]],[[12947,12947],"mapped",[31038]],[[12948,12948],"mapped",[21517]],[[12949,12949],"mapped",[29305]],[[12950,12950],"mapped",[36001]],[[12951,12951],"mapped",[31069]],[[12952,12952],"mapped",[21172]],[[12953,12953],"mapped",[31192]],[[12954,12954],"mapped",[30007]],[[12955,12955],"mapped",[22899]],[[12956,12956],"mapped",[36969]],[[12957,12957],"mapped",[20778]],[[12958,12958],"mapped",[21360]],[[12959,12959],"mapped",[27880]],[[12960,12960],"mapped",[38917]],[[12961,12961],"mapped",[20241]],[[12962,12962],"mapped",[20889]],[[12963,12963],"mapped",[27491]],[[12964,12964],"mapped",[19978]],[[12965,12965],"mapped",[20013]],[[12966,12966],"mapped",[19979]],[[12967,12967],"mapped",[24038]],[[12968,12968],"mapped",[21491]],[[12969,12969],"mapped",[21307]],[[12970,12970],"mapped",[23447]],[[12971,12971],"mapped",[23398]],[[12972,12972],"mapped",[30435]],[[12973,12973],"mapped",[20225]],[[12974,12974],"mapped",[36039]],[[12975,12975],"mapped",[21332]],[[12976,12976],"mapped",[22812]],[[12977,12977],"mapped",[51,54]],[[12978,12978],"mapped",[51,55]],[[12979,12979],"mapped",[51,56]],[[12980,12980],"mapped",[51,57]],[[12981,12981],"mapped",[52,48]],[[12982,12982],"mapped",[52,49]],[[12983,12983],"mapped",[52,50]],[[12984,12984],"mapped",[52,51]],[[12985,12985],"mapped",[52,52]],[[12986,12986],"mapped",[52,53]],[[12987,12987],"mapped",[52,54]],[[12988,12988],"mapped",[52,55]],[[12989,12989],"mapped",[52,56]],[[12990,12990],"mapped",[52,57]],[[12991,12991],"mapped",[53,48]],[[12992,12992],"mapped",[49,26376]],[[12993,12993],"mapped",[50,26376]],[[12994,12994],"mapped",[51,26376]],[[12995,12995],"mapped",[52,26376]],[[12996,12996],"mapped",[53,26376]],[[12997,12997],"mapped",[54,26376]],[[12998,12998],"mapped",[55,26376]],[[12999,12999],"mapped",[56,26376]],[[13000,13000],"mapped",[57,26376]],[[13001,13001],"mapped",[49,48,26376]],[[13002,13002],"mapped",[49,49,26376]],[[13003,13003],"mapped",[49,50,26376]],[[13004,13004],"mapped",[104,103]],[[13005,13005],"mapped",[101,114,103]],[[13006,13006],"mapped",[101,118]],[[13007,13007],"mapped",[108,116,100]],[[13008,13008],"mapped",[12450]],[[13009,13009],"mapped",[12452]],[[13010,13010],"mapped",[12454]],[[13011,13011],"mapped",[12456]],[[13012,13012],"mapped",[12458]],[[13013,13013],"mapped",[12459]],[[13014,13014],"mapped",[12461]],[[13015,13015],"mapped",[12463]],[[13016,13016],"mapped",[12465]],[[13017,13017],"mapped",[12467]],[[13018,13018],"mapped",[12469]],[[13019,13019],"mapped",[12471]],[[13020,13020],"mapped",[12473]],[[13021,13021],"mapped",[12475]],[[13022,13022],"mapped",[12477]],[[13023,13023],"mapped",[12479]],[[13024,13024],"mapped",[12481]],[[13025,13025],"mapped",[12484]],[[13026,13026],"mapped",[12486]],[[13027,13027],"mapped",[12488]],[[13028,13028],"mapped",[12490]],[[13029,13029],"mapped",[12491]],[[13030,13030],"mapped",[12492]],[[13031,13031],"mapped",[12493]],[[13032,13032],"mapped",[12494]],[[13033,13033],"mapped",[12495]],[[13034,13034],"mapped",[12498]],[[13035,13035],"mapped",[12501]],[[13036,13036],"mapped",[12504]],[[13037,13037],"mapped",[12507]],[[13038,13038],"mapped",[12510]],[[13039,13039],"mapped",[12511]],[[13040,13040],"mapped",[12512]],[[13041,13041],"mapped",[12513]],[[13042,13042],"mapped",[12514]],[[13043,13043],"mapped",[12516]],[[13044,13044],"mapped",[12518]],[[13045,13045],"mapped",[12520]],[[13046,13046],"mapped",[12521]],[[13047,13047],"mapped",[12522]],[[13048,13048],"mapped",[12523]],[[13049,13049],"mapped",[12524]],[[13050,13050],"mapped",[12525]],[[13051,13051],"mapped",[12527]],[[13052,13052],"mapped",[12528]],[[13053,13053],"mapped",[12529]],[[13054,13054],"mapped",[12530]],[[13055,13055],"disallowed"],[[13056,13056],"mapped",[12450,12497,12540,12488]],[[13057,13057],"mapped",[12450,12523,12501,12449]],[[13058,13058],"mapped",[12450,12531,12506,12450]],[[13059,13059],"mapped",[12450,12540,12523]],[[13060,13060],"mapped",[12452,12491,12531,12464]],[[13061,13061],"mapped",[12452,12531,12481]],[[13062,13062],"mapped",[12454,12457,12531]],[[13063,13063],"mapped",[12456,12473,12463,12540,12489]],[[13064,13064],"mapped",[12456,12540,12459,12540]],[[13065,13065],"mapped",[12458,12531,12473]],[[13066,13066],"mapped",[12458,12540,12512]],[[13067,13067],"mapped",[12459,12452,12522]],[[13068,13068],"mapped",[12459,12521,12483,12488]],[[13069,13069],"mapped",[12459,12525,12522,12540]],[[13070,13070],"mapped",[12460,12525,12531]],[[13071,13071],"mapped",[12460,12531,12510]],[[13072,13072],"mapped",[12462,12460]],[[13073,13073],"mapped",[12462,12491,12540]],[[13074,13074],"mapped",[12461,12517,12522,12540]],[[13075,13075],"mapped",[12462,12523,12480,12540]],[[13076,13076],"mapped",[12461,12525]],[[13077,13077],"mapped",[12461,12525,12464,12521,12512]],[[13078,13078],"mapped",[12461,12525,12513,12540,12488,12523]],[[13079,13079],"mapped",[12461,12525,12527,12483,12488]],[[13080,13080],"mapped",[12464,12521,12512]],[[13081,13081],"mapped",[12464,12521,12512,12488,12531]],[[13082,13082],"mapped",[12463,12523,12476,12452,12525]],[[13083,13083],"mapped",[12463,12525,12540,12493]],[[13084,13084],"mapped",[12465,12540,12473]],[[13085,13085],"mapped",[12467,12523,12490]],[[13086,13086],"mapped",[12467,12540,12509]],[[13087,13087],"mapped",[12469,12452,12463,12523]],[[13088,13088],"mapped",[12469,12531,12481,12540,12512]],[[13089,13089],"mapped",[12471,12522,12531,12464]],[[13090,13090],"mapped",[12475,12531,12481]],[[13091,13091],"mapped",[12475,12531,12488]],[[13092,13092],"mapped",[12480,12540,12473]],[[13093,13093],"mapped",[12487,12471]],[[13094,13094],"mapped",[12489,12523]],[[13095,13095],"mapped",[12488,12531]],[[13096,13096],"mapped",[12490,12494]],[[13097,13097],"mapped",[12494,12483,12488]],[[13098,13098],"mapped",[12495,12452,12484]],[[13099,13099],"mapped",[12497,12540,12475,12531,12488]],[[13100,13100],"mapped",[12497,12540,12484]],[[13101,13101],"mapped",[12496,12540,12524,12523]],[[13102,13102],"mapped",[12500,12450,12473,12488,12523]],[[13103,13103],"mapped",[12500,12463,12523]],[[13104,13104],"mapped",[12500,12467]],[[13105,13105],"mapped",[12499,12523]],[[13106,13106],"mapped",[12501,12449,12521,12483,12489]],[[13107,13107],"mapped",[12501,12451,12540,12488]],[[13108,13108],"mapped",[12502,12483,12471,12455,12523]],[[13109,13109],"mapped",[12501,12521,12531]],[[13110,13110],"mapped",[12504,12463,12479,12540,12523]],[[13111,13111],"mapped",[12506,12477]],[[13112,13112],"mapped",[12506,12491,12498]],[[13113,13113],"mapped",[12504,12523,12484]],[[13114,13114],"mapped",[12506,12531,12473]],[[13115,13115],"mapped",[12506,12540,12472]],[[13116,13116],"mapped",[12505,12540,12479]],[[13117,13117],"mapped",[12509,12452,12531,12488]],[[13118,13118],"mapped",[12508,12523,12488]],[[13119,13119],"mapped",[12507,12531]],[[13120,13120],"mapped",[12509,12531,12489]],[[13121,13121],"mapped",[12507,12540,12523]],[[13122,13122],"mapped",[12507,12540,12531]],[[13123,13123],"mapped",[12510,12452,12463,12525]],[[13124,13124],"mapped",[12510,12452,12523]],[[13125,13125],"mapped",[12510,12483,12495]],[[13126,13126],"mapped",[12510,12523,12463]],[[13127,13127],"mapped",[12510,12531,12471,12519,12531]],[[13128,13128],"mapped",[12511,12463,12525,12531]],[[13129,13129],"mapped",[12511,12522]],[[13130,13130],"mapped",[12511,12522,12496,12540,12523]],[[13131,13131],"mapped",[12513,12460]],[[13132,13132],"mapped",[12513,12460,12488,12531]],[[13133,13133],"mapped",[12513,12540,12488,12523]],[[13134,13134],"mapped",[12516,12540,12489]],[[13135,13135],"mapped",[12516,12540,12523]],[[13136,13136],"mapped",[12518,12450,12531]],[[13137,13137],"mapped",[12522,12483,12488,12523]],[[13138,13138],"mapped",[12522,12521]],[[13139,13139],"mapped",[12523,12500,12540]],[[13140,13140],"mapped",[12523,12540,12502,12523]],[[13141,13141],"mapped",[12524,12512]],[[13142,13142],"mapped",[12524,12531,12488,12466,12531]],[[13143,13143],"mapped",[12527,12483,12488]],[[13144,13144],"mapped",[48,28857]],[[13145,13145],"mapped",[49,28857]],[[13146,13146],"mapped",[50,28857]],[[13147,13147],"mapped",[51,28857]],[[13148,13148],"mapped",[52,28857]],[[13149,13149],"mapped",[53,28857]],[[13150,13150],"mapped",[54,28857]],[[13151,13151],"mapped",[55,28857]],[[13152,13152],"mapped",[56,28857]],[[13153,13153],"mapped",[57,28857]],[[13154,13154],"mapped",[49,48,28857]],[[13155,13155],"mapped",[49,49,28857]],[[13156,13156],"mapped",[49,50,28857]],[[13157,13157],"mapped",[49,51,28857]],[[13158,13158],"mapped",[49,52,28857]],[[13159,13159],"mapped",[49,53,28857]],[[13160,13160],"mapped",[49,54,28857]],[[13161,13161],"mapped",[49,55,28857]],[[13162,13162],"mapped",[49,56,28857]],[[13163,13163],"mapped",[49,57,28857]],[[13164,13164],"mapped",[50,48,28857]],[[13165,13165],"mapped",[50,49,28857]],[[13166,13166],"mapped",[50,50,28857]],[[13167,13167],"mapped",[50,51,28857]],[[13168,13168],"mapped",[50,52,28857]],[[13169,13169],"mapped",[104,112,97]],[[13170,13170],"mapped",[100,97]],[[13171,13171],"mapped",[97,117]],[[13172,13172],"mapped",[98,97,114]],[[13173,13173],"mapped",[111,118]],[[13174,13174],"mapped",[112,99]],[[13175,13175],"mapped",[100,109]],[[13176,13176],"mapped",[100,109,50]],[[13177,13177],"mapped",[100,109,51]],[[13178,13178],"mapped",[105,117]],[[13179,13179],"mapped",[24179,25104]],[[13180,13180],"mapped",[26157,21644]],[[13181,13181],"mapped",[22823,27491]],[[13182,13182],"mapped",[26126,27835]],[[13183,13183],"mapped",[26666,24335,20250,31038]],[[13184,13184],"mapped",[112,97]],[[13185,13185],"mapped",[110,97]],[[13186,13186],"mapped",[956,97]],[[13187,13187],"mapped",[109,97]],[[13188,13188],"mapped",[107,97]],[[13189,13189],"mapped",[107,98]],[[13190,13190],"mapped",[109,98]],[[13191,13191],"mapped",[103,98]],[[13192,13192],"mapped",[99,97,108]],[[13193,13193],"mapped",[107,99,97,108]],[[13194,13194],"mapped",[112,102]],[[13195,13195],"mapped",[110,102]],[[13196,13196],"mapped",[956,102]],[[13197,13197],"mapped",[956,103]],[[13198,13198],"mapped",[109,103]],[[13199,13199],"mapped",[107,103]],[[13200,13200],"mapped",[104,122]],[[13201,13201],"mapped",[107,104,122]],[[13202,13202],"mapped",[109,104,122]],[[13203,13203],"mapped",[103,104,122]],[[13204,13204],"mapped",[116,104,122]],[[13205,13205],"mapped",[956,108]],[[13206,13206],"mapped",[109,108]],[[13207,13207],"mapped",[100,108]],[[13208,13208],"mapped",[107,108]],[[13209,13209],"mapped",[102,109]],[[13210,13210],"mapped",[110,109]],[[13211,13211],"mapped",[956,109]],[[13212,13212],"mapped",[109,109]],[[13213,13213],"mapped",[99,109]],[[13214,13214],"mapped",[107,109]],[[13215,13215],"mapped",[109,109,50]],[[13216,13216],"mapped",[99,109,50]],[[13217,13217],"mapped",[109,50]],[[13218,13218],"mapped",[107,109,50]],[[13219,13219],"mapped",[109,109,51]],[[13220,13220],"mapped",[99,109,51]],[[13221,13221],"mapped",[109,51]],[[13222,13222],"mapped",[107,109,51]],[[13223,13223],"mapped",[109,8725,115]],[[13224,13224],"mapped",[109,8725,115,50]],[[13225,13225],"mapped",[112,97]],[[13226,13226],"mapped",[107,112,97]],[[13227,13227],"mapped",[109,112,97]],[[13228,13228],"mapped",[103,112,97]],[[13229,13229],"mapped",[114,97,100]],[[13230,13230],"mapped",[114,97,100,8725,115]],[[13231,13231],"mapped",[114,97,100,8725,115,50]],[[13232,13232],"mapped",[112,115]],[[13233,13233],"mapped",[110,115]],[[13234,13234],"mapped",[956,115]],[[13235,13235],"mapped",[109,115]],[[13236,13236],"mapped",[112,118]],[[13237,13237],"mapped",[110,118]],[[13238,13238],"mapped",[956,118]],[[13239,13239],"mapped",[109,118]],[[13240,13240],"mapped",[107,118]],[[13241,13241],"mapped",[109,118]],[[13242,13242],"mapped",[112,119]],[[13243,13243],"mapped",[110,119]],[[13244,13244],"mapped",[956,119]],[[13245,13245],"mapped",[109,119]],[[13246,13246],"mapped",[107,119]],[[13247,13247],"mapped",[109,119]],[[13248,13248],"mapped",[107,969]],[[13249,13249],"mapped",[109,969]],[[13250,13250],"disallowed"],[[13251,13251],"mapped",[98,113]],[[13252,13252],"mapped",[99,99]],[[13253,13253],"mapped",[99,100]],[[13254,13254],"mapped",[99,8725,107,103]],[[13255,13255],"disallowed"],[[13256,13256],"mapped",[100,98]],[[13257,13257],"mapped",[103,121]],[[13258,13258],"mapped",[104,97]],[[13259,13259],"mapped",[104,112]],[[13260,13260],"mapped",[105,110]],[[13261,13261],"mapped",[107,107]],[[13262,13262],"mapped",[107,109]],[[13263,13263],"mapped",[107,116]],[[13264,13264],"mapped",[108,109]],[[13265,13265],"mapped",[108,110]],[[13266,13266],"mapped",[108,111,103]],[[13267,13267],"mapped",[108,120]],[[13268,13268],"mapped",[109,98]],[[13269,13269],"mapped",[109,105,108]],[[13270,13270],"mapped",[109,111,108]],[[13271,13271],"mapped",[112,104]],[[13272,13272],"disallowed"],[[13273,13273],"mapped",[112,112,109]],[[13274,13274],"mapped",[112,114]],[[13275,13275],"mapped",[115,114]],[[13276,13276],"mapped",[115,118]],[[13277,13277],"mapped",[119,98]],[[13278,13278],"mapped",[118,8725,109]],[[13279,13279],"mapped",[97,8725,109]],[[13280,13280],"mapped",[49,26085]],[[13281,13281],"mapped",[50,26085]],[[13282,13282],"mapped",[51,26085]],[[13283,13283],"mapped",[52,26085]],[[13284,13284],"mapped",[53,26085]],[[13285,13285],"mapped",[54,26085]],[[13286,13286],"mapped",[55,26085]],[[13287,13287],"mapped",[56,26085]],[[13288,13288],"mapped",[57,26085]],[[13289,13289],"mapped",[49,48,26085]],[[13290,13290],"mapped",[49,49,26085]],[[13291,13291],"mapped",[49,50,26085]],[[13292,13292],"mapped",[49,51,26085]],[[13293,13293],"mapped",[49,52,26085]],[[13294,13294],"mapped",[49,53,26085]],[[13295,13295],"mapped",[49,54,26085]],[[13296,13296],"mapped",[49,55,26085]],[[13297,13297],"mapped",[49,56,26085]],[[13298,13298],"mapped",[49,57,26085]],[[13299,13299],"mapped",[50,48,26085]],[[13300,13300],"mapped",[50,49,26085]],[[13301,13301],"mapped",[50,50,26085]],[[13302,13302],"mapped",[50,51,26085]],[[13303,13303],"mapped",[50,52,26085]],[[13304,13304],"mapped",[50,53,26085]],[[13305,13305],"mapped",[50,54,26085]],[[13306,13306],"mapped",[50,55,26085]],[[13307,13307],"mapped",[50,56,26085]],[[13308,13308],"mapped",[50,57,26085]],[[13309,13309],"mapped",[51,48,26085]],[[13310,13310],"mapped",[51,49,26085]],[[13311,13311],"mapped",[103,97,108]],[[13312,19893],"valid"],[[19894,19903],"disallowed"],[[19904,19967],"valid",[],"NV8"],[[19968,40869],"valid"],[[40870,40891],"valid"],[[40892,40899],"valid"],[[40900,40907],"valid"],[[40908,40908],"valid"],[[40909,40917],"valid"],[[40918,40959],"disallowed"],[[40960,42124],"valid"],[[42125,42127],"disallowed"],[[42128,42145],"valid",[],"NV8"],[[42146,42147],"valid",[],"NV8"],[[42148,42163],"valid",[],"NV8"],[[42164,42164],"valid",[],"NV8"],[[42165,42176],"valid",[],"NV8"],[[42177,42177],"valid",[],"NV8"],[[42178,42180],"valid",[],"NV8"],[[42181,42181],"valid",[],"NV8"],[[42182,42182],"valid",[],"NV8"],[[42183,42191],"disallowed"],[[42192,42237],"valid"],[[42238,42239],"valid",[],"NV8"],[[42240,42508],"valid"],[[42509,42511],"valid",[],"NV8"],[[42512,42539],"valid"],[[42540,42559],"disallowed"],[[42560,42560],"mapped",[42561]],[[42561,42561],"valid"],[[42562,42562],"mapped",[42563]],[[42563,42563],"valid"],[[42564,42564],"mapped",[42565]],[[42565,42565],"valid"],[[42566,42566],"mapped",[42567]],[[42567,42567],"valid"],[[42568,42568],"mapped",[42569]],[[42569,42569],"valid"],[[42570,42570],"mapped",[42571]],[[42571,42571],"valid"],[[42572,42572],"mapped",[42573]],[[42573,42573],"valid"],[[42574,42574],"mapped",[42575]],[[42575,42575],"valid"],[[42576,42576],"mapped",[42577]],[[42577,42577],"valid"],[[42578,42578],"mapped",[42579]],[[42579,42579],"valid"],[[42580,42580],"mapped",[42581]],[[42581,42581],"valid"],[[42582,42582],"mapped",[42583]],[[42583,42583],"valid"],[[42584,42584],"mapped",[42585]],[[42585,42585],"valid"],[[42586,42586],"mapped",[42587]],[[42587,42587],"valid"],[[42588,42588],"mapped",[42589]],[[42589,42589],"valid"],[[42590,42590],"mapped",[42591]],[[42591,42591],"valid"],[[42592,42592],"mapped",[42593]],[[42593,42593],"valid"],[[42594,42594],"mapped",[42595]],[[42595,42595],"valid"],[[42596,42596],"mapped",[42597]],[[42597,42597],"valid"],[[42598,42598],"mapped",[42599]],[[42599,42599],"valid"],[[42600,42600],"mapped",[42601]],[[42601,42601],"valid"],[[42602,42602],"mapped",[42603]],[[42603,42603],"valid"],[[42604,42604],"mapped",[42605]],[[42605,42607],"valid"],[[42608,42611],"valid",[],"NV8"],[[42612,42619],"valid"],[[42620,42621],"valid"],[[42622,42622],"valid",[],"NV8"],[[42623,42623],"valid"],[[42624,42624],"mapped",[42625]],[[42625,42625],"valid"],[[42626,42626],"mapped",[42627]],[[42627,42627],"valid"],[[42628,42628],"mapped",[42629]],[[42629,42629],"valid"],[[42630,42630],"mapped",[42631]],[[42631,42631],"valid"],[[42632,42632],"mapped",[42633]],[[42633,42633],"valid"],[[42634,42634],"mapped",[42635]],[[42635,42635],"valid"],[[42636,42636],"mapped",[42637]],[[42637,42637],"valid"],[[42638,42638],"mapped",[42639]],[[42639,42639],"valid"],[[42640,42640],"mapped",[42641]],[[42641,42641],"valid"],[[42642,42642],"mapped",[42643]],[[42643,42643],"valid"],[[42644,42644],"mapped",[42645]],[[42645,42645],"valid"],[[42646,42646],"mapped",[42647]],[[42647,42647],"valid"],[[42648,42648],"mapped",[42649]],[[42649,42649],"valid"],[[42650,42650],"mapped",[42651]],[[42651,42651],"valid"],[[42652,42652],"mapped",[1098]],[[42653,42653],"mapped",[1100]],[[42654,42654],"valid"],[[42655,42655],"valid"],[[42656,42725],"valid"],[[42726,42735],"valid",[],"NV8"],[[42736,42737],"valid"],[[42738,42743],"valid",[],"NV8"],[[42744,42751],"disallowed"],[[42752,42774],"valid",[],"NV8"],[[42775,42778],"valid"],[[42779,42783],"valid"],[[42784,42785],"valid",[],"NV8"],[[42786,42786],"mapped",[42787]],[[42787,42787],"valid"],[[42788,42788],"mapped",[42789]],[[42789,42789],"valid"],[[42790,42790],"mapped",[42791]],[[42791,42791],"valid"],[[42792,42792],"mapped",[42793]],[[42793,42793],"valid"],[[42794,42794],"mapped",[42795]],[[42795,42795],"valid"],[[42796,42796],"mapped",[42797]],[[42797,42797],"valid"],[[42798,42798],"mapped",[42799]],[[42799,42801],"valid"],[[42802,42802],"mapped",[42803]],[[42803,42803],"valid"],[[42804,42804],"mapped",[42805]],[[42805,42805],"valid"],[[42806,42806],"mapped",[42807]],[[42807,42807],"valid"],[[42808,42808],"mapped",[42809]],[[42809,42809],"valid"],[[42810,42810],"mapped",[42811]],[[42811,42811],"valid"],[[42812,42812],"mapped",[42813]],[[42813,42813],"valid"],[[42814,42814],"mapped",[42815]],[[42815,42815],"valid"],[[42816,42816],"mapped",[42817]],[[42817,42817],"valid"],[[42818,42818],"mapped",[42819]],[[42819,42819],"valid"],[[42820,42820],"mapped",[42821]],[[42821,42821],"valid"],[[42822,42822],"mapped",[42823]],[[42823,42823],"valid"],[[42824,42824],"mapped",[42825]],[[42825,42825],"valid"],[[42826,42826],"mapped",[42827]],[[42827,42827],"valid"],[[42828,42828],"mapped",[42829]],[[42829,42829],"valid"],[[42830,42830],"mapped",[42831]],[[42831,42831],"valid"],[[42832,42832],"mapped",[42833]],[[42833,42833],"valid"],[[42834,42834],"mapped",[42835]],[[42835,42835],"valid"],[[42836,42836],"mapped",[42837]],[[42837,42837],"valid"],[[42838,42838],"mapped",[42839]],[[42839,42839],"valid"],[[42840,42840],"mapped",[42841]],[[42841,42841],"valid"],[[42842,42842],"mapped",[42843]],[[42843,42843],"valid"],[[42844,42844],"mapped",[42845]],[[42845,42845],"valid"],[[42846,42846],"mapped",[42847]],[[42847,42847],"valid"],[[42848,42848],"mapped",[42849]],[[42849,42849],"valid"],[[42850,42850],"mapped",[42851]],[[42851,42851],"valid"],[[42852,42852],"mapped",[42853]],[[42853,42853],"valid"],[[42854,42854],"mapped",[42855]],[[42855,42855],"valid"],[[42856,42856],"mapped",[42857]],[[42857,42857],"valid"],[[42858,42858],"mapped",[42859]],[[42859,42859],"valid"],[[42860,42860],"mapped",[42861]],[[42861,42861],"valid"],[[42862,42862],"mapped",[42863]],[[42863,42863],"valid"],[[42864,42864],"mapped",[42863]],[[42865,42872],"valid"],[[42873,42873],"mapped",[42874]],[[42874,42874],"valid"],[[42875,42875],"mapped",[42876]],[[42876,42876],"valid"],[[42877,42877],"mapped",[7545]],[[42878,42878],"mapped",[42879]],[[42879,42879],"valid"],[[42880,42880],"mapped",[42881]],[[42881,42881],"valid"],[[42882,42882],"mapped",[42883]],[[42883,42883],"valid"],[[42884,42884],"mapped",[42885]],[[42885,42885],"valid"],[[42886,42886],"mapped",[42887]],[[42887,42888],"valid"],[[42889,42890],"valid",[],"NV8"],[[42891,42891],"mapped",[42892]],[[42892,42892],"valid"],[[42893,42893],"mapped",[613]],[[42894,42894],"valid"],[[42895,42895],"valid"],[[42896,42896],"mapped",[42897]],[[42897,42897],"valid"],[[42898,42898],"mapped",[42899]],[[42899,42899],"valid"],[[42900,42901],"valid"],[[42902,42902],"mapped",[42903]],[[42903,42903],"valid"],[[42904,42904],"mapped",[42905]],[[42905,42905],"valid"],[[42906,42906],"mapped",[42907]],[[42907,42907],"valid"],[[42908,42908],"mapped",[42909]],[[42909,42909],"valid"],[[42910,42910],"mapped",[42911]],[[42911,42911],"valid"],[[42912,42912],"mapped",[42913]],[[42913,42913],"valid"],[[42914,42914],"mapped",[42915]],[[42915,42915],"valid"],[[42916,42916],"mapped",[42917]],[[42917,42917],"valid"],[[42918,42918],"mapped",[42919]],[[42919,42919],"valid"],[[42920,42920],"mapped",[42921]],[[42921,42921],"valid"],[[42922,42922],"mapped",[614]],[[42923,42923],"mapped",[604]],[[42924,42924],"mapped",[609]],[[42925,42925],"mapped",[620]],[[42926,42927],"disallowed"],[[42928,42928],"mapped",[670]],[[42929,42929],"mapped",[647]],[[42930,42930],"mapped",[669]],[[42931,42931],"mapped",[43859]],[[42932,42932],"mapped",[42933]],[[42933,42933],"valid"],[[42934,42934],"mapped",[42935]],[[42935,42935],"valid"],[[42936,42998],"disallowed"],[[42999,42999],"valid"],[[43000,43000],"mapped",[295]],[[43001,43001],"mapped",[339]],[[43002,43002],"valid"],[[43003,43007],"valid"],[[43008,43047],"valid"],[[43048,43051],"valid",[],"NV8"],[[43052,43055],"disallowed"],[[43056,43065],"valid",[],"NV8"],[[43066,43071],"disallowed"],[[43072,43123],"valid"],[[43124,43127],"valid",[],"NV8"],[[43128,43135],"disallowed"],[[43136,43204],"valid"],[[43205,43213],"disallowed"],[[43214,43215],"valid",[],"NV8"],[[43216,43225],"valid"],[[43226,43231],"disallowed"],[[43232,43255],"valid"],[[43256,43258],"valid",[],"NV8"],[[43259,43259],"valid"],[[43260,43260],"valid",[],"NV8"],[[43261,43261],"valid"],[[43262,43263],"disallowed"],[[43264,43309],"valid"],[[43310,43311],"valid",[],"NV8"],[[43312,43347],"valid"],[[43348,43358],"disallowed"],[[43359,43359],"valid",[],"NV8"],[[43360,43388],"valid",[],"NV8"],[[43389,43391],"disallowed"],[[43392,43456],"valid"],[[43457,43469],"valid",[],"NV8"],[[43470,43470],"disallowed"],[[43471,43481],"valid"],[[43482,43485],"disallowed"],[[43486,43487],"valid",[],"NV8"],[[43488,43518],"valid"],[[43519,43519],"disallowed"],[[43520,43574],"valid"],[[43575,43583],"disallowed"],[[43584,43597],"valid"],[[43598,43599],"disallowed"],[[43600,43609],"valid"],[[43610,43611],"disallowed"],[[43612,43615],"valid",[],"NV8"],[[43616,43638],"valid"],[[43639,43641],"valid",[],"NV8"],[[43642,43643],"valid"],[[43644,43647],"valid"],[[43648,43714],"valid"],[[43715,43738],"disallowed"],[[43739,43741],"valid"],[[43742,43743],"valid",[],"NV8"],[[43744,43759],"valid"],[[43760,43761],"valid",[],"NV8"],[[43762,43766],"valid"],[[43767,43776],"disallowed"],[[43777,43782],"valid"],[[43783,43784],"disallowed"],[[43785,43790],"valid"],[[43791,43792],"disallowed"],[[43793,43798],"valid"],[[43799,43807],"disallowed"],[[43808,43814],"valid"],[[43815,43815],"disallowed"],[[43816,43822],"valid"],[[43823,43823],"disallowed"],[[43824,43866],"valid"],[[43867,43867],"valid",[],"NV8"],[[43868,43868],"mapped",[42791]],[[43869,43869],"mapped",[43831]],[[43870,43870],"mapped",[619]],[[43871,43871],"mapped",[43858]],[[43872,43875],"valid"],[[43876,43877],"valid"],[[43878,43887],"disallowed"],[[43888,43888],"mapped",[5024]],[[43889,43889],"mapped",[5025]],[[43890,43890],"mapped",[5026]],[[43891,43891],"mapped",[5027]],[[43892,43892],"mapped",[5028]],[[43893,43893],"mapped",[5029]],[[43894,43894],"mapped",[5030]],[[43895,43895],"mapped",[5031]],[[43896,43896],"mapped",[5032]],[[43897,43897],"mapped",[5033]],[[43898,43898],"mapped",[5034]],[[43899,43899],"mapped",[5035]],[[43900,43900],"mapped",[5036]],[[43901,43901],"mapped",[5037]],[[43902,43902],"mapped",[5038]],[[43903,43903],"mapped",[5039]],[[43904,43904],"mapped",[5040]],[[43905,43905],"mapped",[5041]],[[43906,43906],"mapped",[5042]],[[43907,43907],"mapped",[5043]],[[43908,43908],"mapped",[5044]],[[43909,43909],"mapped",[5045]],[[43910,43910],"mapped",[5046]],[[43911,43911],"mapped",[5047]],[[43912,43912],"mapped",[5048]],[[43913,43913],"mapped",[5049]],[[43914,43914],"mapped",[5050]],[[43915,43915],"mapped",[5051]],[[43916,43916],"mapped",[5052]],[[43917,43917],"mapped",[5053]],[[43918,43918],"mapped",[5054]],[[43919,43919],"mapped",[5055]],[[43920,43920],"mapped",[5056]],[[43921,43921],"mapped",[5057]],[[43922,43922],"mapped",[5058]],[[43923,43923],"mapped",[5059]],[[43924,43924],"mapped",[5060]],[[43925,43925],"mapped",[5061]],[[43926,43926],"mapped",[5062]],[[43927,43927],"mapped",[5063]],[[43928,43928],"mapped",[5064]],[[43929,43929],"mapped",[5065]],[[43930,43930],"mapped",[5066]],[[43931,43931],"mapped",[5067]],[[43932,43932],"mapped",[5068]],[[43933,43933],"mapped",[5069]],[[43934,43934],"mapped",[5070]],[[43935,43935],"mapped",[5071]],[[43936,43936],"mapped",[5072]],[[43937,43937],"mapped",[5073]],[[43938,43938],"mapped",[5074]],[[43939,43939],"mapped",[5075]],[[43940,43940],"mapped",[5076]],[[43941,43941],"mapped",[5077]],[[43942,43942],"mapped",[5078]],[[43943,43943],"mapped",[5079]],[[43944,43944],"mapped",[5080]],[[43945,43945],"mapped",[5081]],[[43946,43946],"mapped",[5082]],[[43947,43947],"mapped",[5083]],[[43948,43948],"mapped",[5084]],[[43949,43949],"mapped",[5085]],[[43950,43950],"mapped",[5086]],[[43951,43951],"mapped",[5087]],[[43952,43952],"mapped",[5088]],[[43953,43953],"mapped",[5089]],[[43954,43954],"mapped",[5090]],[[43955,43955],"mapped",[5091]],[[43956,43956],"mapped",[5092]],[[43957,43957],"mapped",[5093]],[[43958,43958],"mapped",[5094]],[[43959,43959],"mapped",[5095]],[[43960,43960],"mapped",[5096]],[[43961,43961],"mapped",[5097]],[[43962,43962],"mapped",[5098]],[[43963,43963],"mapped",[5099]],[[43964,43964],"mapped",[5100]],[[43965,43965],"mapped",[5101]],[[43966,43966],"mapped",[5102]],[[43967,43967],"mapped",[5103]],[[43968,44010],"valid"],[[44011,44011],"valid",[],"NV8"],[[44012,44013],"valid"],[[44014,44015],"disallowed"],[[44016,44025],"valid"],[[44026,44031],"disallowed"],[[44032,55203],"valid"],[[55204,55215],"disallowed"],[[55216,55238],"valid",[],"NV8"],[[55239,55242],"disallowed"],[[55243,55291],"valid",[],"NV8"],[[55292,55295],"disallowed"],[[55296,57343],"disallowed"],[[57344,63743],"disallowed"],[[63744,63744],"mapped",[35912]],[[63745,63745],"mapped",[26356]],[[63746,63746],"mapped",[36554]],[[63747,63747],"mapped",[36040]],[[63748,63748],"mapped",[28369]],[[63749,63749],"mapped",[20018]],[[63750,63750],"mapped",[21477]],[[63751,63752],"mapped",[40860]],[[63753,63753],"mapped",[22865]],[[63754,63754],"mapped",[37329]],[[63755,63755],"mapped",[21895]],[[63756,63756],"mapped",[22856]],[[63757,63757],"mapped",[25078]],[[63758,63758],"mapped",[30313]],[[63759,63759],"mapped",[32645]],[[63760,63760],"mapped",[34367]],[[63761,63761],"mapped",[34746]],[[63762,63762],"mapped",[35064]],[[63763,63763],"mapped",[37007]],[[63764,63764],"mapped",[27138]],[[63765,63765],"mapped",[27931]],[[63766,63766],"mapped",[28889]],[[63767,63767],"mapped",[29662]],[[63768,63768],"mapped",[33853]],[[63769,63769],"mapped",[37226]],[[63770,63770],"mapped",[39409]],[[63771,63771],"mapped",[20098]],[[63772,63772],"mapped",[21365]],[[63773,63773],"mapped",[27396]],[[63774,63774],"mapped",[29211]],[[63775,63775],"mapped",[34349]],[[63776,63776],"mapped",[40478]],[[63777,63777],"mapped",[23888]],[[63778,63778],"mapped",[28651]],[[63779,63779],"mapped",[34253]],[[63780,63780],"mapped",[35172]],[[63781,63781],"mapped",[25289]],[[63782,63782],"mapped",[33240]],[[63783,63783],"mapped",[34847]],[[63784,63784],"mapped",[24266]],[[63785,63785],"mapped",[26391]],[[63786,63786],"mapped",[28010]],[[63787,63787],"mapped",[29436]],[[63788,63788],"mapped",[37070]],[[63789,63789],"mapped",[20358]],[[63790,63790],"mapped",[20919]],[[63791,63791],"mapped",[21214]],[[63792,63792],"mapped",[25796]],[[63793,63793],"mapped",[27347]],[[63794,63794],"mapped",[29200]],[[63795,63795],"mapped",[30439]],[[63796,63796],"mapped",[32769]],[[63797,63797],"mapped",[34310]],[[63798,63798],"mapped",[34396]],[[63799,63799],"mapped",[36335]],[[63800,63800],"mapped",[38706]],[[63801,63801],"mapped",[39791]],[[63802,63802],"mapped",[40442]],[[63803,63803],"mapped",[30860]],[[63804,63804],"mapped",[31103]],[[63805,63805],"mapped",[32160]],[[63806,63806],"mapped",[33737]],[[63807,63807],"mapped",[37636]],[[63808,63808],"mapped",[40575]],[[63809,63809],"mapped",[35542]],[[63810,63810],"mapped",[22751]],[[63811,63811],"mapped",[24324]],[[63812,63812],"mapped",[31840]],[[63813,63813],"mapped",[32894]],[[63814,63814],"mapped",[29282]],[[63815,63815],"mapped",[30922]],[[63816,63816],"mapped",[36034]],[[63817,63817],"mapped",[38647]],[[63818,63818],"mapped",[22744]],[[63819,63819],"mapped",[23650]],[[63820,63820],"mapped",[27155]],[[63821,63821],"mapped",[28122]],[[63822,63822],"mapped",[28431]],[[63823,63823],"mapped",[32047]],[[63824,63824],"mapped",[32311]],[[63825,63825],"mapped",[38475]],[[63826,63826],"mapped",[21202]],[[63827,63827],"mapped",[32907]],[[63828,63828],"mapped",[20956]],[[63829,63829],"mapped",[20940]],[[63830,63830],"mapped",[31260]],[[63831,63831],"mapped",[32190]],[[63832,63832],"mapped",[33777]],[[63833,63833],"mapped",[38517]],[[63834,63834],"mapped",[35712]],[[63835,63835],"mapped",[25295]],[[63836,63836],"mapped",[27138]],[[63837,63837],"mapped",[35582]],[[63838,63838],"mapped",[20025]],[[63839,63839],"mapped",[23527]],[[63840,63840],"mapped",[24594]],[[63841,63841],"mapped",[29575]],[[63842,63842],"mapped",[30064]],[[63843,63843],"mapped",[21271]],[[63844,63844],"mapped",[30971]],[[63845,63845],"mapped",[20415]],[[63846,63846],"mapped",[24489]],[[63847,63847],"mapped",[19981]],[[63848,63848],"mapped",[27852]],[[63849,63849],"mapped",[25976]],[[63850,63850],"mapped",[32034]],[[63851,63851],"mapped",[21443]],[[63852,63852],"mapped",[22622]],[[63853,63853],"mapped",[30465]],[[63854,63854],"mapped",[33865]],[[63855,63855],"mapped",[35498]],[[63856,63856],"mapped",[27578]],[[63857,63857],"mapped",[36784]],[[63858,63858],"mapped",[27784]],[[63859,63859],"mapped",[25342]],[[63860,63860],"mapped",[33509]],[[63861,63861],"mapped",[25504]],[[63862,63862],"mapped",[30053]],[[63863,63863],"mapped",[20142]],[[63864,63864],"mapped",[20841]],[[63865,63865],"mapped",[20937]],[[63866,63866],"mapped",[26753]],[[63867,63867],"mapped",[31975]],[[63868,63868],"mapped",[33391]],[[63869,63869],"mapped",[35538]],[[63870,63870],"mapped",[37327]],[[63871,63871],"mapped",[21237]],[[63872,63872],"mapped",[21570]],[[63873,63873],"mapped",[22899]],[[63874,63874],"mapped",[24300]],[[63875,63875],"mapped",[26053]],[[63876,63876],"mapped",[28670]],[[63877,63877],"mapped",[31018]],[[63878,63878],"mapped",[38317]],[[63879,63879],"mapped",[39530]],[[63880,63880],"mapped",[40599]],[[63881,63881],"mapped",[40654]],[[63882,63882],"mapped",[21147]],[[63883,63883],"mapped",[26310]],[[63884,63884],"mapped",[27511]],[[63885,63885],"mapped",[36706]],[[63886,63886],"mapped",[24180]],[[63887,63887],"mapped",[24976]],[[63888,63888],"mapped",[25088]],[[63889,63889],"mapped",[25754]],[[63890,63890],"mapped",[28451]],[[63891,63891],"mapped",[29001]],[[63892,63892],"mapped",[29833]],[[63893,63893],"mapped",[31178]],[[63894,63894],"mapped",[32244]],[[63895,63895],"mapped",[32879]],[[63896,63896],"mapped",[36646]],[[63897,63897],"mapped",[34030]],[[63898,63898],"mapped",[36899]],[[63899,63899],"mapped",[37706]],[[63900,63900],"mapped",[21015]],[[63901,63901],"mapped",[21155]],[[63902,63902],"mapped",[21693]],[[63903,63903],"mapped",[28872]],[[63904,63904],"mapped",[35010]],[[63905,63905],"mapped",[35498]],[[63906,63906],"mapped",[24265]],[[63907,63907],"mapped",[24565]],[[63908,63908],"mapped",[25467]],[[63909,63909],"mapped",[27566]],[[63910,63910],"mapped",[31806]],[[63911,63911],"mapped",[29557]],[[63912,63912],"mapped",[20196]],[[63913,63913],"mapped",[22265]],[[63914,63914],"mapped",[23527]],[[63915,63915],"mapped",[23994]],[[63916,63916],"mapped",[24604]],[[63917,63917],"mapped",[29618]],[[63918,63918],"mapped",[29801]],[[63919,63919],"mapped",[32666]],[[63920,63920],"mapped",[32838]],[[63921,63921],"mapped",[37428]],[[63922,63922],"mapped",[38646]],[[63923,63923],"mapped",[38728]],[[63924,63924],"mapped",[38936]],[[63925,63925],"mapped",[20363]],[[63926,63926],"mapped",[31150]],[[63927,63927],"mapped",[37300]],[[63928,63928],"mapped",[38584]],[[63929,63929],"mapped",[24801]],[[63930,63930],"mapped",[20102]],[[63931,63931],"mapped",[20698]],[[63932,63932],"mapped",[23534]],[[63933,63933],"mapped",[23615]],[[63934,63934],"mapped",[26009]],[[63935,63935],"mapped",[27138]],[[63936,63936],"mapped",[29134]],[[63937,63937],"mapped",[30274]],[[63938,63938],"mapped",[34044]],[[63939,63939],"mapped",[36988]],[[63940,63940],"mapped",[40845]],[[63941,63941],"mapped",[26248]],[[63942,63942],"mapped",[38446]],[[63943,63943],"mapped",[21129]],[[63944,63944],"mapped",[26491]],[[63945,63945],"mapped",[26611]],[[63946,63946],"mapped",[27969]],[[63947,63947],"mapped",[28316]],[[63948,63948],"mapped",[29705]],[[63949,63949],"mapped",[30041]],[[63950,63950],"mapped",[30827]],[[63951,63951],"mapped",[32016]],[[63952,63952],"mapped",[39006]],[[63953,63953],"mapped",[20845]],[[63954,63954],"mapped",[25134]],[[63955,63955],"mapped",[38520]],[[63956,63956],"mapped",[20523]],[[63957,63957],"mapped",[23833]],[[63958,63958],"mapped",[28138]],[[63959,63959],"mapped",[36650]],[[63960,63960],"mapped",[24459]],[[63961,63961],"mapped",[24900]],[[63962,63962],"mapped",[26647]],[[63963,63963],"mapped",[29575]],[[63964,63964],"mapped",[38534]],[[63965,63965],"mapped",[21033]],[[63966,63966],"mapped",[21519]],[[63967,63967],"mapped",[23653]],[[63968,63968],"mapped",[26131]],[[63969,63969],"mapped",[26446]],[[63970,63970],"mapped",[26792]],[[63971,63971],"mapped",[27877]],[[63972,63972],"mapped",[29702]],[[63973,63973],"mapped",[30178]],[[63974,63974],"mapped",[32633]],[[63975,63975],"mapped",[35023]],[[63976,63976],"mapped",[35041]],[[63977,63977],"mapped",[37324]],[[63978,63978],"mapped",[38626]],[[63979,63979],"mapped",[21311]],[[63980,63980],"mapped",[28346]],[[63981,63981],"mapped",[21533]],[[63982,63982],"mapped",[29136]],[[63983,63983],"mapped",[29848]],[[63984,63984],"mapped",[34298]],[[63985,63985],"mapped",[38563]],[[63986,63986],"mapped",[40023]],[[63987,63987],"mapped",[40607]],[[63988,63988],"mapped",[26519]],[[63989,63989],"mapped",[28107]],[[63990,63990],"mapped",[33256]],[[63991,63991],"mapped",[31435]],[[63992,63992],"mapped",[31520]],[[63993,63993],"mapped",[31890]],[[63994,63994],"mapped",[29376]],[[63995,63995],"mapped",[28825]],[[63996,63996],"mapped",[35672]],[[63997,63997],"mapped",[20160]],[[63998,63998],"mapped",[33590]],[[63999,63999],"mapped",[21050]],[[64000,64000],"mapped",[20999]],[[64001,64001],"mapped",[24230]],[[64002,64002],"mapped",[25299]],[[64003,64003],"mapped",[31958]],[[64004,64004],"mapped",[23429]],[[64005,64005],"mapped",[27934]],[[64006,64006],"mapped",[26292]],[[64007,64007],"mapped",[36667]],[[64008,64008],"mapped",[34892]],[[64009,64009],"mapped",[38477]],[[64010,64010],"mapped",[35211]],[[64011,64011],"mapped",[24275]],[[64012,64012],"mapped",[20800]],[[64013,64013],"mapped",[21952]],[[64014,64015],"valid"],[[64016,64016],"mapped",[22618]],[[64017,64017],"valid"],[[64018,64018],"mapped",[26228]],[[64019,64020],"valid"],[[64021,64021],"mapped",[20958]],[[64022,64022],"mapped",[29482]],[[64023,64023],"mapped",[30410]],[[64024,64024],"mapped",[31036]],[[64025,64025],"mapped",[31070]],[[64026,64026],"mapped",[31077]],[[64027,64027],"mapped",[31119]],[[64028,64028],"mapped",[38742]],[[64029,64029],"mapped",[31934]],[[64030,64030],"mapped",[32701]],[[64031,64031],"valid"],[[64032,64032],"mapped",[34322]],[[64033,64033],"valid"],[[64034,64034],"mapped",[35576]],[[64035,64036],"valid"],[[64037,64037],"mapped",[36920]],[[64038,64038],"mapped",[37117]],[[64039,64041],"valid"],[[64042,64042],"mapped",[39151]],[[64043,64043],"mapped",[39164]],[[64044,64044],"mapped",[39208]],[[64045,64045],"mapped",[40372]],[[64046,64046],"mapped",[37086]],[[64047,64047],"mapped",[38583]],[[64048,64048],"mapped",[20398]],[[64049,64049],"mapped",[20711]],[[64050,64050],"mapped",[20813]],[[64051,64051],"mapped",[21193]],[[64052,64052],"mapped",[21220]],[[64053,64053],"mapped",[21329]],[[64054,64054],"mapped",[21917]],[[64055,64055],"mapped",[22022]],[[64056,64056],"mapped",[22120]],[[64057,64057],"mapped",[22592]],[[64058,64058],"mapped",[22696]],[[64059,64059],"mapped",[23652]],[[64060,64060],"mapped",[23662]],[[64061,64061],"mapped",[24724]],[[64062,64062],"mapped",[24936]],[[64063,64063],"mapped",[24974]],[[64064,64064],"mapped",[25074]],[[64065,64065],"mapped",[25935]],[[64066,64066],"mapped",[26082]],[[64067,64067],"mapped",[26257]],[[64068,64068],"mapped",[26757]],[[64069,64069],"mapped",[28023]],[[64070,64070],"mapped",[28186]],[[64071,64071],"mapped",[28450]],[[64072,64072],"mapped",[29038]],[[64073,64073],"mapped",[29227]],[[64074,64074],"mapped",[29730]],[[64075,64075],"mapped",[30865]],[[64076,64076],"mapped",[31038]],[[64077,64077],"mapped",[31049]],[[64078,64078],"mapped",[31048]],[[64079,64079],"mapped",[31056]],[[64080,64080],"mapped",[31062]],[[64081,64081],"mapped",[31069]],[[64082,64082],"mapped",[31117]],[[64083,64083],"mapped",[31118]],[[64084,64084],"mapped",[31296]],[[64085,64085],"mapped",[31361]],[[64086,64086],"mapped",[31680]],[[64087,64087],"mapped",[32244]],[[64088,64088],"mapped",[32265]],[[64089,64089],"mapped",[32321]],[[64090,64090],"mapped",[32626]],[[64091,64091],"mapped",[32773]],[[64092,64092],"mapped",[33261]],[[64093,64094],"mapped",[33401]],[[64095,64095],"mapped",[33879]],[[64096,64096],"mapped",[35088]],[[64097,64097],"mapped",[35222]],[[64098,64098],"mapped",[35585]],[[64099,64099],"mapped",[35641]],[[64100,64100],"mapped",[36051]],[[64101,64101],"mapped",[36104]],[[64102,64102],"mapped",[36790]],[[64103,64103],"mapped",[36920]],[[64104,64104],"mapped",[38627]],[[64105,64105],"mapped",[38911]],[[64106,64106],"mapped",[38971]],[[64107,64107],"mapped",[24693]],[[64108,64108],"mapped",[148206]],[[64109,64109],"mapped",[33304]],[[64110,64111],"disallowed"],[[64112,64112],"mapped",[20006]],[[64113,64113],"mapped",[20917]],[[64114,64114],"mapped",[20840]],[[64115,64115],"mapped",[20352]],[[64116,64116],"mapped",[20805]],[[64117,64117],"mapped",[20864]],[[64118,64118],"mapped",[21191]],[[64119,64119],"mapped",[21242]],[[64120,64120],"mapped",[21917]],[[64121,64121],"mapped",[21845]],[[64122,64122],"mapped",[21913]],[[64123,64123],"mapped",[21986]],[[64124,64124],"mapped",[22618]],[[64125,64125],"mapped",[22707]],[[64126,64126],"mapped",[22852]],[[64127,64127],"mapped",[22868]],[[64128,64128],"mapped",[23138]],[[64129,64129],"mapped",[23336]],[[64130,64130],"mapped",[24274]],[[64131,64131],"mapped",[24281]],[[64132,64132],"mapped",[24425]],[[64133,64133],"mapped",[24493]],[[64134,64134],"mapped",[24792]],[[64135,64135],"mapped",[24910]],[[64136,64136],"mapped",[24840]],[[64137,64137],"mapped",[24974]],[[64138,64138],"mapped",[24928]],[[64139,64139],"mapped",[25074]],[[64140,64140],"mapped",[25140]],[[64141,64141],"mapped",[25540]],[[64142,64142],"mapped",[25628]],[[64143,64143],"mapped",[25682]],[[64144,64144],"mapped",[25942]],[[64145,64145],"mapped",[26228]],[[64146,64146],"mapped",[26391]],[[64147,64147],"mapped",[26395]],[[64148,64148],"mapped",[26454]],[[64149,64149],"mapped",[27513]],[[64150,64150],"mapped",[27578]],[[64151,64151],"mapped",[27969]],[[64152,64152],"mapped",[28379]],[[64153,64153],"mapped",[28363]],[[64154,64154],"mapped",[28450]],[[64155,64155],"mapped",[28702]],[[64156,64156],"mapped",[29038]],[[64157,64157],"mapped",[30631]],[[64158,64158],"mapped",[29237]],[[64159,64159],"mapped",[29359]],[[64160,64160],"mapped",[29482]],[[64161,64161],"mapped",[29809]],[[64162,64162],"mapped",[29958]],[[64163,64163],"mapped",[30011]],[[64164,64164],"mapped",[30237]],[[64165,64165],"mapped",[30239]],[[64166,64166],"mapped",[30410]],[[64167,64167],"mapped",[30427]],[[64168,64168],"mapped",[30452]],[[64169,64169],"mapped",[30538]],[[64170,64170],"mapped",[30528]],[[64171,64171],"mapped",[30924]],[[64172,64172],"mapped",[31409]],[[64173,64173],"mapped",[31680]],[[64174,64174],"mapped",[31867]],[[64175,64175],"mapped",[32091]],[[64176,64176],"mapped",[32244]],[[64177,64177],"mapped",[32574]],[[64178,64178],"mapped",[32773]],[[64179,64179],"mapped",[33618]],[[64180,64180],"mapped",[33775]],[[64181,64181],"mapped",[34681]],[[64182,64182],"mapped",[35137]],[[64183,64183],"mapped",[35206]],[[64184,64184],"mapped",[35222]],[[64185,64185],"mapped",[35519]],[[64186,64186],"mapped",[35576]],[[64187,64187],"mapped",[35531]],[[64188,64188],"mapped",[35585]],[[64189,64189],"mapped",[35582]],[[64190,64190],"mapped",[35565]],[[64191,64191],"mapped",[35641]],[[64192,64192],"mapped",[35722]],[[64193,64193],"mapped",[36104]],[[64194,64194],"mapped",[36664]],[[64195,64195],"mapped",[36978]],[[64196,64196],"mapped",[37273]],[[64197,64197],"mapped",[37494]],[[64198,64198],"mapped",[38524]],[[64199,64199],"mapped",[38627]],[[64200,64200],"mapped",[38742]],[[64201,64201],"mapped",[38875]],[[64202,64202],"mapped",[38911]],[[64203,64203],"mapped",[38923]],[[64204,64204],"mapped",[38971]],[[64205,64205],"mapped",[39698]],[[64206,64206],"mapped",[40860]],[[64207,64207],"mapped",[141386]],[[64208,64208],"mapped",[141380]],[[64209,64209],"mapped",[144341]],[[64210,64210],"mapped",[15261]],[[64211,64211],"mapped",[16408]],[[64212,64212],"mapped",[16441]],[[64213,64213],"mapped",[152137]],[[64214,64214],"mapped",[154832]],[[64215,64215],"mapped",[163539]],[[64216,64216],"mapped",[40771]],[[64217,64217],"mapped",[40846]],[[64218,64255],"disallowed"],[[64256,64256],"mapped",[102,102]],[[64257,64257],"mapped",[102,105]],[[64258,64258],"mapped",[102,108]],[[64259,64259],"mapped",[102,102,105]],[[64260,64260],"mapped",[102,102,108]],[[64261,64262],"mapped",[115,116]],[[64263,64274],"disallowed"],[[64275,64275],"mapped",[1396,1398]],[[64276,64276],"mapped",[1396,1381]],[[64277,64277],"mapped",[1396,1387]],[[64278,64278],"mapped",[1406,1398]],[[64279,64279],"mapped",[1396,1389]],[[64280,64284],"disallowed"],[[64285,64285],"mapped",[1497,1460]],[[64286,64286],"valid"],[[64287,64287],"mapped",[1522,1463]],[[64288,64288],"mapped",[1506]],[[64289,64289],"mapped",[1488]],[[64290,64290],"mapped",[1491]],[[64291,64291],"mapped",[1492]],[[64292,64292],"mapped",[1499]],[[64293,64293],"mapped",[1500]],[[64294,64294],"mapped",[1501]],[[64295,64295],"mapped",[1512]],[[64296,64296],"mapped",[1514]],[[64297,64297],"disallowed_STD3_mapped",[43]],[[64298,64298],"mapped",[1513,1473]],[[64299,64299],"mapped",[1513,1474]],[[64300,64300],"mapped",[1513,1468,1473]],[[64301,64301],"mapped",[1513,1468,1474]],[[64302,64302],"mapped",[1488,1463]],[[64303,64303],"mapped",[1488,1464]],[[64304,64304],"mapped",[1488,1468]],[[64305,64305],"mapped",[1489,1468]],[[64306,64306],"mapped",[1490,1468]],[[64307,64307],"mapped",[1491,1468]],[[64308,64308],"mapped",[1492,1468]],[[64309,64309],"mapped",[1493,1468]],[[64310,64310],"mapped",[1494,1468]],[[64311,64311],"disallowed"],[[64312,64312],"mapped",[1496,1468]],[[64313,64313],"mapped",[1497,1468]],[[64314,64314],"mapped",[1498,1468]],[[64315,64315],"mapped",[1499,1468]],[[64316,64316],"mapped",[1500,1468]],[[64317,64317],"disallowed"],[[64318,64318],"mapped",[1502,1468]],[[64319,64319],"disallowed"],[[64320,64320],"mapped",[1504,1468]],[[64321,64321],"mapped",[1505,1468]],[[64322,64322],"disallowed"],[[64323,64323],"mapped",[1507,1468]],[[64324,64324],"mapped",[1508,1468]],[[64325,64325],"disallowed"],[[64326,64326],"mapped",[1510,1468]],[[64327,64327],"mapped",[1511,1468]],[[64328,64328],"mapped",[1512,1468]],[[64329,64329],"mapped",[1513,1468]],[[64330,64330],"mapped",[1514,1468]],[[64331,64331],"mapped",[1493,1465]],[[64332,64332],"mapped",[1489,1471]],[[64333,64333],"mapped",[1499,1471]],[[64334,64334],"mapped",[1508,1471]],[[64335,64335],"mapped",[1488,1500]],[[64336,64337],"mapped",[1649]],[[64338,64341],"mapped",[1659]],[[64342,64345],"mapped",[1662]],[[64346,64349],"mapped",[1664]],[[64350,64353],"mapped",[1658]],[[64354,64357],"mapped",[1663]],[[64358,64361],"mapped",[1657]],[[64362,64365],"mapped",[1700]],[[64366,64369],"mapped",[1702]],[[64370,64373],"mapped",[1668]],[[64374,64377],"mapped",[1667]],[[64378,64381],"mapped",[1670]],[[64382,64385],"mapped",[1671]],[[64386,64387],"mapped",[1677]],[[64388,64389],"mapped",[1676]],[[64390,64391],"mapped",[1678]],[[64392,64393],"mapped",[1672]],[[64394,64395],"mapped",[1688]],[[64396,64397],"mapped",[1681]],[[64398,64401],"mapped",[1705]],[[64402,64405],"mapped",[1711]],[[64406,64409],"mapped",[1715]],[[64410,64413],"mapped",[1713]],[[64414,64415],"mapped",[1722]],[[64416,64419],"mapped",[1723]],[[64420,64421],"mapped",[1728]],[[64422,64425],"mapped",[1729]],[[64426,64429],"mapped",[1726]],[[64430,64431],"mapped",[1746]],[[64432,64433],"mapped",[1747]],[[64434,64449],"valid",[],"NV8"],[[64450,64466],"disallowed"],[[64467,64470],"mapped",[1709]],[[64471,64472],"mapped",[1735]],[[64473,64474],"mapped",[1734]],[[64475,64476],"mapped",[1736]],[[64477,64477],"mapped",[1735,1652]],[[64478,64479],"mapped",[1739]],[[64480,64481],"mapped",[1733]],[[64482,64483],"mapped",[1737]],[[64484,64487],"mapped",[1744]],[[64488,64489],"mapped",[1609]],[[64490,64491],"mapped",[1574,1575]],[[64492,64493],"mapped",[1574,1749]],[[64494,64495],"mapped",[1574,1608]],[[64496,64497],"mapped",[1574,1735]],[[64498,64499],"mapped",[1574,1734]],[[64500,64501],"mapped",[1574,1736]],[[64502,64504],"mapped",[1574,1744]],[[64505,64507],"mapped",[1574,1609]],[[64508,64511],"mapped",[1740]],[[64512,64512],"mapped",[1574,1580]],[[64513,64513],"mapped",[1574,1581]],[[64514,64514],"mapped",[1574,1605]],[[64515,64515],"mapped",[1574,1609]],[[64516,64516],"mapped",[1574,1610]],[[64517,64517],"mapped",[1576,1580]],[[64518,64518],"mapped",[1576,1581]],[[64519,64519],"mapped",[1576,1582]],[[64520,64520],"mapped",[1576,1605]],[[64521,64521],"mapped",[1576,1609]],[[64522,64522],"mapped",[1576,1610]],[[64523,64523],"mapped",[1578,1580]],[[64524,64524],"mapped",[1578,1581]],[[64525,64525],"mapped",[1578,1582]],[[64526,64526],"mapped",[1578,1605]],[[64527,64527],"mapped",[1578,1609]],[[64528,64528],"mapped",[1578,1610]],[[64529,64529],"mapped",[1579,1580]],[[64530,64530],"mapped",[1579,1605]],[[64531,64531],"mapped",[1579,1609]],[[64532,64532],"mapped",[1579,1610]],[[64533,64533],"mapped",[1580,1581]],[[64534,64534],"mapped",[1580,1605]],[[64535,64535],"mapped",[1581,1580]],[[64536,64536],"mapped",[1581,1605]],[[64537,64537],"mapped",[1582,1580]],[[64538,64538],"mapped",[1582,1581]],[[64539,64539],"mapped",[1582,1605]],[[64540,64540],"mapped",[1587,1580]],[[64541,64541],"mapped",[1587,1581]],[[64542,64542],"mapped",[1587,1582]],[[64543,64543],"mapped",[1587,1605]],[[64544,64544],"mapped",[1589,1581]],[[64545,64545],"mapped",[1589,1605]],[[64546,64546],"mapped",[1590,1580]],[[64547,64547],"mapped",[1590,1581]],[[64548,64548],"mapped",[1590,1582]],[[64549,64549],"mapped",[1590,1605]],[[64550,64550],"mapped",[1591,1581]],[[64551,64551],"mapped",[1591,1605]],[[64552,64552],"mapped",[1592,1605]],[[64553,64553],"mapped",[1593,1580]],[[64554,64554],"mapped",[1593,1605]],[[64555,64555],"mapped",[1594,1580]],[[64556,64556],"mapped",[1594,1605]],[[64557,64557],"mapped",[1601,1580]],[[64558,64558],"mapped",[1601,1581]],[[64559,64559],"mapped",[1601,1582]],[[64560,64560],"mapped",[1601,1605]],[[64561,64561],"mapped",[1601,1609]],[[64562,64562],"mapped",[1601,1610]],[[64563,64563],"mapped",[1602,1581]],[[64564,64564],"mapped",[1602,1605]],[[64565,64565],"mapped",[1602,1609]],[[64566,64566],"mapped",[1602,1610]],[[64567,64567],"mapped",[1603,1575]],[[64568,64568],"mapped",[1603,1580]],[[64569,64569],"mapped",[1603,1581]],[[64570,64570],"mapped",[1603,1582]],[[64571,64571],"mapped",[1603,1604]],[[64572,64572],"mapped",[1603,1605]],[[64573,64573],"mapped",[1603,1609]],[[64574,64574],"mapped",[1603,1610]],[[64575,64575],"mapped",[1604,1580]],[[64576,64576],"mapped",[1604,1581]],[[64577,64577],"mapped",[1604,1582]],[[64578,64578],"mapped",[1604,1605]],[[64579,64579],"mapped",[1604,1609]],[[64580,64580],"mapped",[1604,1610]],[[64581,64581],"mapped",[1605,1580]],[[64582,64582],"mapped",[1605,1581]],[[64583,64583],"mapped",[1605,1582]],[[64584,64584],"mapped",[1605,1605]],[[64585,64585],"mapped",[1605,1609]],[[64586,64586],"mapped",[1605,1610]],[[64587,64587],"mapped",[1606,1580]],[[64588,64588],"mapped",[1606,1581]],[[64589,64589],"mapped",[1606,1582]],[[64590,64590],"mapped",[1606,1605]],[[64591,64591],"mapped",[1606,1609]],[[64592,64592],"mapped",[1606,1610]],[[64593,64593],"mapped",[1607,1580]],[[64594,64594],"mapped",[1607,1605]],[[64595,64595],"mapped",[1607,1609]],[[64596,64596],"mapped",[1607,1610]],[[64597,64597],"mapped",[1610,1580]],[[64598,64598],"mapped",[1610,1581]],[[64599,64599],"mapped",[1610,1582]],[[64600,64600],"mapped",[1610,1605]],[[64601,64601],"mapped",[1610,1609]],[[64602,64602],"mapped",[1610,1610]],[[64603,64603],"mapped",[1584,1648]],[[64604,64604],"mapped",[1585,1648]],[[64605,64605],"mapped",[1609,1648]],[[64606,64606],"disallowed_STD3_mapped",[32,1612,1617]],[[64607,64607],"disallowed_STD3_mapped",[32,1613,1617]],[[64608,64608],"disallowed_STD3_mapped",[32,1614,1617]],[[64609,64609],"disallowed_STD3_mapped",[32,1615,1617]],[[64610,64610],"disallowed_STD3_mapped",[32,1616,1617]],[[64611,64611],"disallowed_STD3_mapped",[32,1617,1648]],[[64612,64612],"mapped",[1574,1585]],[[64613,64613],"mapped",[1574,1586]],[[64614,64614],"mapped",[1574,1605]],[[64615,64615],"mapped",[1574,1606]],[[64616,64616],"mapped",[1574,1609]],[[64617,64617],"mapped",[1574,1610]],[[64618,64618],"mapped",[1576,1585]],[[64619,64619],"mapped",[1576,1586]],[[64620,64620],"mapped",[1576,1605]],[[64621,64621],"mapped",[1576,1606]],[[64622,64622],"mapped",[1576,1609]],[[64623,64623],"mapped",[1576,1610]],[[64624,64624],"mapped",[1578,1585]],[[64625,64625],"mapped",[1578,1586]],[[64626,64626],"mapped",[1578,1605]],[[64627,64627],"mapped",[1578,1606]],[[64628,64628],"mapped",[1578,1609]],[[64629,64629],"mapped",[1578,1610]],[[64630,64630],"mapped",[1579,1585]],[[64631,64631],"mapped",[1579,1586]],[[64632,64632],"mapped",[1579,1605]],[[64633,64633],"mapped",[1579,1606]],[[64634,64634],"mapped",[1579,1609]],[[64635,64635],"mapped",[1579,1610]],[[64636,64636],"mapped",[1601,1609]],[[64637,64637],"mapped",[1601,1610]],[[64638,64638],"mapped",[1602,1609]],[[64639,64639],"mapped",[1602,1610]],[[64640,64640],"mapped",[1603,1575]],[[64641,64641],"mapped",[1603,1604]],[[64642,64642],"mapped",[1603,1605]],[[64643,64643],"mapped",[1603,1609]],[[64644,64644],"mapped",[1603,1610]],[[64645,64645],"mapped",[1604,1605]],[[64646,64646],"mapped",[1604,1609]],[[64647,64647],"mapped",[1604,1610]],[[64648,64648],"mapped",[1605,1575]],[[64649,64649],"mapped",[1605,1605]],[[64650,64650],"mapped",[1606,1585]],[[64651,64651],"mapped",[1606,1586]],[[64652,64652],"mapped",[1606,1605]],[[64653,64653],"mapped",[1606,1606]],[[64654,64654],"mapped",[1606,1609]],[[64655,64655],"mapped",[1606,1610]],[[64656,64656],"mapped",[1609,1648]],[[64657,64657],"mapped",[1610,1585]],[[64658,64658],"mapped",[1610,1586]],[[64659,64659],"mapped",[1610,1605]],[[64660,64660],"mapped",[1610,1606]],[[64661,64661],"mapped",[1610,1609]],[[64662,64662],"mapped",[1610,1610]],[[64663,64663],"mapped",[1574,1580]],[[64664,64664],"mapped",[1574,1581]],[[64665,64665],"mapped",[1574,1582]],[[64666,64666],"mapped",[1574,1605]],[[64667,64667],"mapped",[1574,1607]],[[64668,64668],"mapped",[1576,1580]],[[64669,64669],"mapped",[1576,1581]],[[64670,64670],"mapped",[1576,1582]],[[64671,64671],"mapped",[1576,1605]],[[64672,64672],"mapped",[1576,1607]],[[64673,64673],"mapped",[1578,1580]],[[64674,64674],"mapped",[1578,1581]],[[64675,64675],"mapped",[1578,1582]],[[64676,64676],"mapped",[1578,1605]],[[64677,64677],"mapped",[1578,1607]],[[64678,64678],"mapped",[1579,1605]],[[64679,64679],"mapped",[1580,1581]],[[64680,64680],"mapped",[1580,1605]],[[64681,64681],"mapped",[1581,1580]],[[64682,64682],"mapped",[1581,1605]],[[64683,64683],"mapped",[1582,1580]],[[64684,64684],"mapped",[1582,1605]],[[64685,64685],"mapped",[1587,1580]],[[64686,64686],"mapped",[1587,1581]],[[64687,64687],"mapped",[1587,1582]],[[64688,64688],"mapped",[1587,1605]],[[64689,64689],"mapped",[1589,1581]],[[64690,64690],"mapped",[1589,1582]],[[64691,64691],"mapped",[1589,1605]],[[64692,64692],"mapped",[1590,1580]],[[64693,64693],"mapped",[1590,1581]],[[64694,64694],"mapped",[1590,1582]],[[64695,64695],"mapped",[1590,1605]],[[64696,64696],"mapped",[1591,1581]],[[64697,64697],"mapped",[1592,1605]],[[64698,64698],"mapped",[1593,1580]],[[64699,64699],"mapped",[1593,1605]],[[64700,64700],"mapped",[1594,1580]],[[64701,64701],"mapped",[1594,1605]],[[64702,64702],"mapped",[1601,1580]],[[64703,64703],"mapped",[1601,1581]],[[64704,64704],"mapped",[1601,1582]],[[64705,64705],"mapped",[1601,1605]],[[64706,64706],"mapped",[1602,1581]],[[64707,64707],"mapped",[1602,1605]],[[64708,64708],"mapped",[1603,1580]],[[64709,64709],"mapped",[1603,1581]],[[64710,64710],"mapped",[1603,1582]],[[64711,64711],"mapped",[1603,1604]],[[64712,64712],"mapped",[1603,1605]],[[64713,64713],"mapped",[1604,1580]],[[64714,64714],"mapped",[1604,1581]],[[64715,64715],"mapped",[1604,1582]],[[64716,64716],"mapped",[1604,1605]],[[64717,64717],"mapped",[1604,1607]],[[64718,64718],"mapped",[1605,1580]],[[64719,64719],"mapped",[1605,1581]],[[64720,64720],"mapped",[1605,1582]],[[64721,64721],"mapped",[1605,1605]],[[64722,64722],"mapped",[1606,1580]],[[64723,64723],"mapped",[1606,1581]],[[64724,64724],"mapped",[1606,1582]],[[64725,64725],"mapped",[1606,1605]],[[64726,64726],"mapped",[1606,1607]],[[64727,64727],"mapped",[1607,1580]],[[64728,64728],"mapped",[1607,1605]],[[64729,64729],"mapped",[1607,1648]],[[64730,64730],"mapped",[1610,1580]],[[64731,64731],"mapped",[1610,1581]],[[64732,64732],"mapped",[1610,1582]],[[64733,64733],"mapped",[1610,1605]],[[64734,64734],"mapped",[1610,1607]],[[64735,64735],"mapped",[1574,1605]],[[64736,64736],"mapped",[1574,1607]],[[64737,64737],"mapped",[1576,1605]],[[64738,64738],"mapped",[1576,1607]],[[64739,64739],"mapped",[1578,1605]],[[64740,64740],"mapped",[1578,1607]],[[64741,64741],"mapped",[1579,1605]],[[64742,64742],"mapped",[1579,1607]],[[64743,64743],"mapped",[1587,1605]],[[64744,64744],"mapped",[1587,1607]],[[64745,64745],"mapped",[1588,1605]],[[64746,64746],"mapped",[1588,1607]],[[64747,64747],"mapped",[1603,1604]],[[64748,64748],"mapped",[1603,1605]],[[64749,64749],"mapped",[1604,1605]],[[64750,64750],"mapped",[1606,1605]],[[64751,64751],"mapped",[1606,1607]],[[64752,64752],"mapped",[1610,1605]],[[64753,64753],"mapped",[1610,1607]],[[64754,64754],"mapped",[1600,1614,1617]],[[64755,64755],"mapped",[1600,1615,1617]],[[64756,64756],"mapped",[1600,1616,1617]],[[64757,64757],"mapped",[1591,1609]],[[64758,64758],"mapped",[1591,1610]],[[64759,64759],"mapped",[1593,1609]],[[64760,64760],"mapped",[1593,1610]],[[64761,64761],"mapped",[1594,1609]],[[64762,64762],"mapped",[1594,1610]],[[64763,64763],"mapped",[1587,1609]],[[64764,64764],"mapped",[1587,1610]],[[64765,64765],"mapped",[1588,1609]],[[64766,64766],"mapped",[1588,1610]],[[64767,64767],"mapped",[1581,1609]],[[64768,64768],"mapped",[1581,1610]],[[64769,64769],"mapped",[1580,1609]],[[64770,64770],"mapped",[1580,1610]],[[64771,64771],"mapped",[1582,1609]],[[64772,64772],"mapped",[1582,1610]],[[64773,64773],"mapped",[1589,1609]],[[64774,64774],"mapped",[1589,1610]],[[64775,64775],"mapped",[1590,1609]],[[64776,64776],"mapped",[1590,1610]],[[64777,64777],"mapped",[1588,1580]],[[64778,64778],"mapped",[1588,1581]],[[64779,64779],"mapped",[1588,1582]],[[64780,64780],"mapped",[1588,1605]],[[64781,64781],"mapped",[1588,1585]],[[64782,64782],"mapped",[1587,1585]],[[64783,64783],"mapped",[1589,1585]],[[64784,64784],"mapped",[1590,1585]],[[64785,64785],"mapped",[1591,1609]],[[64786,64786],"mapped",[1591,1610]],[[64787,64787],"mapped",[1593,1609]],[[64788,64788],"mapped",[1593,1610]],[[64789,64789],"mapped",[1594,1609]],[[64790,64790],"mapped",[1594,1610]],[[64791,64791],"mapped",[1587,1609]],[[64792,64792],"mapped",[1587,1610]],[[64793,64793],"mapped",[1588,1609]],[[64794,64794],"mapped",[1588,1610]],[[64795,64795],"mapped",[1581,1609]],[[64796,64796],"mapped",[1581,1610]],[[64797,64797],"mapped",[1580,1609]],[[64798,64798],"mapped",[1580,1610]],[[64799,64799],"mapped",[1582,1609]],[[64800,64800],"mapped",[1582,1610]],[[64801,64801],"mapped",[1589,1609]],[[64802,64802],"mapped",[1589,1610]],[[64803,64803],"mapped",[1590,1609]],[[64804,64804],"mapped",[1590,1610]],[[64805,64805],"mapped",[1588,1580]],[[64806,64806],"mapped",[1588,1581]],[[64807,64807],"mapped",[1588,1582]],[[64808,64808],"mapped",[1588,1605]],[[64809,64809],"mapped",[1588,1585]],[[64810,64810],"mapped",[1587,1585]],[[64811,64811],"mapped",[1589,1585]],[[64812,64812],"mapped",[1590,1585]],[[64813,64813],"mapped",[1588,1580]],[[64814,64814],"mapped",[1588,1581]],[[64815,64815],"mapped",[1588,1582]],[[64816,64816],"mapped",[1588,1605]],[[64817,64817],"mapped",[1587,1607]],[[64818,64818],"mapped",[1588,1607]],[[64819,64819],"mapped",[1591,1605]],[[64820,64820],"mapped",[1587,1580]],[[64821,64821],"mapped",[1587,1581]],[[64822,64822],"mapped",[1587,1582]],[[64823,64823],"mapped",[1588,1580]],[[64824,64824],"mapped",[1588,1581]],[[64825,64825],"mapped",[1588,1582]],[[64826,64826],"mapped",[1591,1605]],[[64827,64827],"mapped",[1592,1605]],[[64828,64829],"mapped",[1575,1611]],[[64830,64831],"valid",[],"NV8"],[[64832,64847],"disallowed"],[[64848,64848],"mapped",[1578,1580,1605]],[[64849,64850],"mapped",[1578,1581,1580]],[[64851,64851],"mapped",[1578,1581,1605]],[[64852,64852],"mapped",[1578,1582,1605]],[[64853,64853],"mapped",[1578,1605,1580]],[[64854,64854],"mapped",[1578,1605,1581]],[[64855,64855],"mapped",[1578,1605,1582]],[[64856,64857],"mapped",[1580,1605,1581]],[[64858,64858],"mapped",[1581,1605,1610]],[[64859,64859],"mapped",[1581,1605,1609]],[[64860,64860],"mapped",[1587,1581,1580]],[[64861,64861],"mapped",[1587,1580,1581]],[[64862,64862],"mapped",[1587,1580,1609]],[[64863,64864],"mapped",[1587,1605,1581]],[[64865,64865],"mapped",[1587,1605,1580]],[[64866,64867],"mapped",[1587,1605,1605]],[[64868,64869],"mapped",[1589,1581,1581]],[[64870,64870],"mapped",[1589,1605,1605]],[[64871,64872],"mapped",[1588,1581,1605]],[[64873,64873],"mapped",[1588,1580,1610]],[[64874,64875],"mapped",[1588,1605,1582]],[[64876,64877],"mapped",[1588,1605,1605]],[[64878,64878],"mapped",[1590,1581,1609]],[[64879,64880],"mapped",[1590,1582,1605]],[[64881,64882],"mapped",[1591,1605,1581]],[[64883,64883],"mapped",[1591,1605,1605]],[[64884,64884],"mapped",[1591,1605,1610]],[[64885,64885],"mapped",[1593,1580,1605]],[[64886,64887],"mapped",[1593,1605,1605]],[[64888,64888],"mapped",[1593,1605,1609]],[[64889,64889],"mapped",[1594,1605,1605]],[[64890,64890],"mapped",[1594,1605,1610]],[[64891,64891],"mapped",[1594,1605,1609]],[[64892,64893],"mapped",[1601,1582,1605]],[[64894,64894],"mapped",[1602,1605,1581]],[[64895,64895],"mapped",[1602,1605,1605]],[[64896,64896],"mapped",[1604,1581,1605]],[[64897,64897],"mapped",[1604,1581,1610]],[[64898,64898],"mapped",[1604,1581,1609]],[[64899,64900],"mapped",[1604,1580,1580]],[[64901,64902],"mapped",[1604,1582,1605]],[[64903,64904],"mapped",[1604,1605,1581]],[[64905,64905],"mapped",[1605,1581,1580]],[[64906,64906],"mapped",[1605,1581,1605]],[[64907,64907],"mapped",[1605,1581,1610]],[[64908,64908],"mapped",[1605,1580,1581]],[[64909,64909],"mapped",[1605,1580,1605]],[[64910,64910],"mapped",[1605,1582,1580]],[[64911,64911],"mapped",[1605,1582,1605]],[[64912,64913],"disallowed"],[[64914,64914],"mapped",[1605,1580,1582]],[[64915,64915],"mapped",[1607,1605,1580]],[[64916,64916],"mapped",[1607,1605,1605]],[[64917,64917],"mapped",[1606,1581,1605]],[[64918,64918],"mapped",[1606,1581,1609]],[[64919,64920],"mapped",[1606,1580,1605]],[[64921,64921],"mapped",[1606,1580,1609]],[[64922,64922],"mapped",[1606,1605,1610]],[[64923,64923],"mapped",[1606,1605,1609]],[[64924,64925],"mapped",[1610,1605,1605]],[[64926,64926],"mapped",[1576,1582,1610]],[[64927,64927],"mapped",[1578,1580,1610]],[[64928,64928],"mapped",[1578,1580,1609]],[[64929,64929],"mapped",[1578,1582,1610]],[[64930,64930],"mapped",[1578,1582,1609]],[[64931,64931],"mapped",[1578,1605,1610]],[[64932,64932],"mapped",[1578,1605,1609]],[[64933,64933],"mapped",[1580,1605,1610]],[[64934,64934],"mapped",[1580,1581,1609]],[[64935,64935],"mapped",[1580,1605,1609]],[[64936,64936],"mapped",[1587,1582,1609]],[[64937,64937],"mapped",[1589,1581,1610]],[[64938,64938],"mapped",[1588,1581,1610]],[[64939,64939],"mapped",[1590,1581,1610]],[[64940,64940],"mapped",[1604,1580,1610]],[[64941,64941],"mapped",[1604,1605,1610]],[[64942,64942],"mapped",[1610,1581,1610]],[[64943,64943],"mapped",[1610,1580,1610]],[[64944,64944],"mapped",[1610,1605,1610]],[[64945,64945],"mapped",[1605,1605,1610]],[[64946,64946],"mapped",[1602,1605,1610]],[[64947,64947],"mapped",[1606,1581,1610]],[[64948,64948],"mapped",[1602,1605,1581]],[[64949,64949],"mapped",[1604,1581,1605]],[[64950,64950],"mapped",[1593,1605,1610]],[[64951,64951],"mapped",[1603,1605,1610]],[[64952,64952],"mapped",[1606,1580,1581]],[[64953,64953],"mapped",[1605,1582,1610]],[[64954,64954],"mapped",[1604,1580,1605]],[[64955,64955],"mapped",[1603,1605,1605]],[[64956,64956],"mapped",[1604,1580,1605]],[[64957,64957],"mapped",[1606,1580,1581]],[[64958,64958],"mapped",[1580,1581,1610]],[[64959,64959],"mapped",[1581,1580,1610]],[[64960,64960],"mapped",[1605,1580,1610]],[[64961,64961],"mapped",[1601,1605,1610]],[[64962,64962],"mapped",[1576,1581,1610]],[[64963,64963],"mapped",[1603,1605,1605]],[[64964,64964],"mapped",[1593,1580,1605]],[[64965,64965],"mapped",[1589,1605,1605]],[[64966,64966],"mapped",[1587,1582,1610]],[[64967,64967],"mapped",[1606,1580,1610]],[[64968,64975],"disallowed"],[[64976,65007],"disallowed"],[[65008,65008],"mapped",[1589,1604,1746]],[[65009,65009],"mapped",[1602,1604,1746]],[[65010,65010],"mapped",[1575,1604,1604,1607]],[[65011,65011],"mapped",[1575,1603,1576,1585]],[[65012,65012],"mapped",[1605,1581,1605,1583]],[[65013,65013],"mapped",[1589,1604,1593,1605]],[[65014,65014],"mapped",[1585,1587,1608,1604]],[[65015,65015],"mapped",[1593,1604,1610,1607]],[[65016,65016],"mapped",[1608,1587,1604,1605]],[[65017,65017],"mapped",[1589,1604,1609]],[[65018,65018],"disallowed_STD3_mapped",[1589,1604,1609,32,1575,1604,1604,1607,32,1593,1604,1610,1607,32,1608,1587,1604,1605]],[[65019,65019],"disallowed_STD3_mapped",[1580,1604,32,1580,1604,1575,1604,1607]],[[65020,65020],"mapped",[1585,1740,1575,1604]],[[65021,65021],"valid",[],"NV8"],[[65022,65023],"disallowed"],[[65024,65039],"ignored"],[[65040,65040],"disallowed_STD3_mapped",[44]],[[65041,65041],"mapped",[12289]],[[65042,65042],"disallowed"],[[65043,65043],"disallowed_STD3_mapped",[58]],[[65044,65044],"disallowed_STD3_mapped",[59]],[[65045,65045],"disallowed_STD3_mapped",[33]],[[65046,65046],"disallowed_STD3_mapped",[63]],[[65047,65047],"mapped",[12310]],[[65048,65048],"mapped",[12311]],[[65049,65049],"disallowed"],[[65050,65055],"disallowed"],[[65056,65059],"valid"],[[65060,65062],"valid"],[[65063,65069],"valid"],[[65070,65071],"valid"],[[65072,65072],"disallowed"],[[65073,65073],"mapped",[8212]],[[65074,65074],"mapped",[8211]],[[65075,65076],"disallowed_STD3_mapped",[95]],[[65077,65077],"disallowed_STD3_mapped",[40]],[[65078,65078],"disallowed_STD3_mapped",[41]],[[65079,65079],"disallowed_STD3_mapped",[123]],[[65080,65080],"disallowed_STD3_mapped",[125]],[[65081,65081],"mapped",[12308]],[[65082,65082],"mapped",[12309]],[[65083,65083],"mapped",[12304]],[[65084,65084],"mapped",[12305]],[[65085,65085],"mapped",[12298]],[[65086,65086],"mapped",[12299]],[[65087,65087],"mapped",[12296]],[[65088,65088],"mapped",[12297]],[[65089,65089],"mapped",[12300]],[[65090,65090],"mapped",[12301]],[[65091,65091],"mapped",[12302]],[[65092,65092],"mapped",[12303]],[[65093,65094],"valid",[],"NV8"],[[65095,65095],"disallowed_STD3_mapped",[91]],[[65096,65096],"disallowed_STD3_mapped",[93]],[[65097,65100],"disallowed_STD3_mapped",[32,773]],[[65101,65103],"disallowed_STD3_mapped",[95]],[[65104,65104],"disallowed_STD3_mapped",[44]],[[65105,65105],"mapped",[12289]],[[65106,65106],"disallowed"],[[65107,65107],"disallowed"],[[65108,65108],"disallowed_STD3_mapped",[59]],[[65109,65109],"disallowed_STD3_mapped",[58]],[[65110,65110],"disallowed_STD3_mapped",[63]],[[65111,65111],"disallowed_STD3_mapped",[33]],[[65112,65112],"mapped",[8212]],[[65113,65113],"disallowed_STD3_mapped",[40]],[[65114,65114],"disallowed_STD3_mapped",[41]],[[65115,65115],"disallowed_STD3_mapped",[123]],[[65116,65116],"disallowed_STD3_mapped",[125]],[[65117,65117],"mapped",[12308]],[[65118,65118],"mapped",[12309]],[[65119,65119],"disallowed_STD3_mapped",[35]],[[65120,65120],"disallowed_STD3_mapped",[38]],[[65121,65121],"disallowed_STD3_mapped",[42]],[[65122,65122],"disallowed_STD3_mapped",[43]],[[65123,65123],"mapped",[45]],[[65124,65124],"disallowed_STD3_mapped",[60]],[[65125,65125],"disallowed_STD3_mapped",[62]],[[65126,65126],"disallowed_STD3_mapped",[61]],[[65127,65127],"disallowed"],[[65128,65128],"disallowed_STD3_mapped",[92]],[[65129,65129],"disallowed_STD3_mapped",[36]],[[65130,65130],"disallowed_STD3_mapped",[37]],[[65131,65131],"disallowed_STD3_mapped",[64]],[[65132,65135],"disallowed"],[[65136,65136],"disallowed_STD3_mapped",[32,1611]],[[65137,65137],"mapped",[1600,1611]],[[65138,65138],"disallowed_STD3_mapped",[32,1612]],[[65139,65139],"valid"],[[65140,65140],"disallowed_STD3_mapped",[32,1613]],[[65141,65141],"disallowed"],[[65142,65142],"disallowed_STD3_mapped",[32,1614]],[[65143,65143],"mapped",[1600,1614]],[[65144,65144],"disallowed_STD3_mapped",[32,1615]],[[65145,65145],"mapped",[1600,1615]],[[65146,65146],"disallowed_STD3_mapped",[32,1616]],[[65147,65147],"mapped",[1600,1616]],[[65148,65148],"disallowed_STD3_mapped",[32,1617]],[[65149,65149],"mapped",[1600,1617]],[[65150,65150],"disallowed_STD3_mapped",[32,1618]],[[65151,65151],"mapped",[1600,1618]],[[65152,65152],"mapped",[1569]],[[65153,65154],"mapped",[1570]],[[65155,65156],"mapped",[1571]],[[65157,65158],"mapped",[1572]],[[65159,65160],"mapped",[1573]],[[65161,65164],"mapped",[1574]],[[65165,65166],"mapped",[1575]],[[65167,65170],"mapped",[1576]],[[65171,65172],"mapped",[1577]],[[65173,65176],"mapped",[1578]],[[65177,65180],"mapped",[1579]],[[65181,65184],"mapped",[1580]],[[65185,65188],"mapped",[1581]],[[65189,65192],"mapped",[1582]],[[65193,65194],"mapped",[1583]],[[65195,65196],"mapped",[1584]],[[65197,65198],"mapped",[1585]],[[65199,65200],"mapped",[1586]],[[65201,65204],"mapped",[1587]],[[65205,65208],"mapped",[1588]],[[65209,65212],"mapped",[1589]],[[65213,65216],"mapped",[1590]],[[65217,65220],"mapped",[1591]],[[65221,65224],"mapped",[1592]],[[65225,65228],"mapped",[1593]],[[65229,65232],"mapped",[1594]],[[65233,65236],"mapped",[1601]],[[65237,65240],"mapped",[1602]],[[65241,65244],"mapped",[1603]],[[65245,65248],"mapped",[1604]],[[65249,65252],"mapped",[1605]],[[65253,65256],"mapped",[1606]],[[65257,65260],"mapped",[1607]],[[65261,65262],"mapped",[1608]],[[65263,65264],"mapped",[1609]],[[65265,65268],"mapped",[1610]],[[65269,65270],"mapped",[1604,1570]],[[65271,65272],"mapped",[1604,1571]],[[65273,65274],"mapped",[1604,1573]],[[65275,65276],"mapped",[1604,1575]],[[65277,65278],"disallowed"],[[65279,65279],"ignored"],[[65280,65280],"disallowed"],[[65281,65281],"disallowed_STD3_mapped",[33]],[[65282,65282],"disallowed_STD3_mapped",[34]],[[65283,65283],"disallowed_STD3_mapped",[35]],[[65284,65284],"disallowed_STD3_mapped",[36]],[[65285,65285],"disallowed_STD3_mapped",[37]],[[65286,65286],"disallowed_STD3_mapped",[38]],[[65287,65287],"disallowed_STD3_mapped",[39]],[[65288,65288],"disallowed_STD3_mapped",[40]],[[65289,65289],"disallowed_STD3_mapped",[41]],[[65290,65290],"disallowed_STD3_mapped",[42]],[[65291,65291],"disallowed_STD3_mapped",[43]],[[65292,65292],"disallowed_STD3_mapped",[44]],[[65293,65293],"mapped",[45]],[[65294,65294],"mapped",[46]],[[65295,65295],"disallowed_STD3_mapped",[47]],[[65296,65296],"mapped",[48]],[[65297,65297],"mapped",[49]],[[65298,65298],"mapped",[50]],[[65299,65299],"mapped",[51]],[[65300,65300],"mapped",[52]],[[65301,65301],"mapped",[53]],[[65302,65302],"mapped",[54]],[[65303,65303],"mapped",[55]],[[65304,65304],"mapped",[56]],[[65305,65305],"mapped",[57]],[[65306,65306],"disallowed_STD3_mapped",[58]],[[65307,65307],"disallowed_STD3_mapped",[59]],[[65308,65308],"disallowed_STD3_mapped",[60]],[[65309,65309],"disallowed_STD3_mapped",[61]],[[65310,65310],"disallowed_STD3_mapped",[62]],[[65311,65311],"disallowed_STD3_mapped",[63]],[[65312,65312],"disallowed_STD3_mapped",[64]],[[65313,65313],"mapped",[97]],[[65314,65314],"mapped",[98]],[[65315,65315],"mapped",[99]],[[65316,65316],"mapped",[100]],[[65317,65317],"mapped",[101]],[[65318,65318],"mapped",[102]],[[65319,65319],"mapped",[103]],[[65320,65320],"mapped",[104]],[[65321,65321],"mapped",[105]],[[65322,65322],"mapped",[106]],[[65323,65323],"mapped",[107]],[[65324,65324],"mapped",[108]],[[65325,65325],"mapped",[109]],[[65326,65326],"mapped",[110]],[[65327,65327],"mapped",[111]],[[65328,65328],"mapped",[112]],[[65329,65329],"mapped",[113]],[[65330,65330],"mapped",[114]],[[65331,65331],"mapped",[115]],[[65332,65332],"mapped",[116]],[[65333,65333],"mapped",[117]],[[65334,65334],"mapped",[118]],[[65335,65335],"mapped",[119]],[[65336,65336],"mapped",[120]],[[65337,65337],"mapped",[121]],[[65338,65338],"mapped",[122]],[[65339,65339],"disallowed_STD3_mapped",[91]],[[65340,65340],"disallowed_STD3_mapped",[92]],[[65341,65341],"disallowed_STD3_mapped",[93]],[[65342,65342],"disallowed_STD3_mapped",[94]],[[65343,65343],"disallowed_STD3_mapped",[95]],[[65344,65344],"disallowed_STD3_mapped",[96]],[[65345,65345],"mapped",[97]],[[65346,65346],"mapped",[98]],[[65347,65347],"mapped",[99]],[[65348,65348],"mapped",[100]],[[65349,65349],"mapped",[101]],[[65350,65350],"mapped",[102]],[[65351,65351],"mapped",[103]],[[65352,65352],"mapped",[104]],[[65353,65353],"mapped",[105]],[[65354,65354],"mapped",[106]],[[65355,65355],"mapped",[107]],[[65356,65356],"mapped",[108]],[[65357,65357],"mapped",[109]],[[65358,65358],"mapped",[110]],[[65359,65359],"mapped",[111]],[[65360,65360],"mapped",[112]],[[65361,65361],"mapped",[113]],[[65362,65362],"mapped",[114]],[[65363,65363],"mapped",[115]],[[65364,65364],"mapped",[116]],[[65365,65365],"mapped",[117]],[[65366,65366],"mapped",[118]],[[65367,65367],"mapped",[119]],[[65368,65368],"mapped",[120]],[[65369,65369],"mapped",[121]],[[65370,65370],"mapped",[122]],[[65371,65371],"disallowed_STD3_mapped",[123]],[[65372,65372],"disallowed_STD3_mapped",[124]],[[65373,65373],"disallowed_STD3_mapped",[125]],[[65374,65374],"disallowed_STD3_mapped",[126]],[[65375,65375],"mapped",[10629]],[[65376,65376],"mapped",[10630]],[[65377,65377],"mapped",[46]],[[65378,65378],"mapped",[12300]],[[65379,65379],"mapped",[12301]],[[65380,65380],"mapped",[12289]],[[65381,65381],"mapped",[12539]],[[65382,65382],"mapped",[12530]],[[65383,65383],"mapped",[12449]],[[65384,65384],"mapped",[12451]],[[65385,65385],"mapped",[12453]],[[65386,65386],"mapped",[12455]],[[65387,65387],"mapped",[12457]],[[65388,65388],"mapped",[12515]],[[65389,65389],"mapped",[12517]],[[65390,65390],"mapped",[12519]],[[65391,65391],"mapped",[12483]],[[65392,65392],"mapped",[12540]],[[65393,65393],"mapped",[12450]],[[65394,65394],"mapped",[12452]],[[65395,65395],"mapped",[12454]],[[65396,65396],"mapped",[12456]],[[65397,65397],"mapped",[12458]],[[65398,65398],"mapped",[12459]],[[65399,65399],"mapped",[12461]],[[65400,65400],"mapped",[12463]],[[65401,65401],"mapped",[12465]],[[65402,65402],"mapped",[12467]],[[65403,65403],"mapped",[12469]],[[65404,65404],"mapped",[12471]],[[65405,65405],"mapped",[12473]],[[65406,65406],"mapped",[12475]],[[65407,65407],"mapped",[12477]],[[65408,65408],"mapped",[12479]],[[65409,65409],"mapped",[12481]],[[65410,65410],"mapped",[12484]],[[65411,65411],"mapped",[12486]],[[65412,65412],"mapped",[12488]],[[65413,65413],"mapped",[12490]],[[65414,65414],"mapped",[12491]],[[65415,65415],"mapped",[12492]],[[65416,65416],"mapped",[12493]],[[65417,65417],"mapped",[12494]],[[65418,65418],"mapped",[12495]],[[65419,65419],"mapped",[12498]],[[65420,65420],"mapped",[12501]],[[65421,65421],"mapped",[12504]],[[65422,65422],"mapped",[12507]],[[65423,65423],"mapped",[12510]],[[65424,65424],"mapped",[12511]],[[65425,65425],"mapped",[12512]],[[65426,65426],"mapped",[12513]],[[65427,65427],"mapped",[12514]],[[65428,65428],"mapped",[12516]],[[65429,65429],"mapped",[12518]],[[65430,65430],"mapped",[12520]],[[65431,65431],"mapped",[12521]],[[65432,65432],"mapped",[12522]],[[65433,65433],"mapped",[12523]],[[65434,65434],"mapped",[12524]],[[65435,65435],"mapped",[12525]],[[65436,65436],"mapped",[12527]],[[65437,65437],"mapped",[12531]],[[65438,65438],"mapped",[12441]],[[65439,65439],"mapped",[12442]],[[65440,65440],"disallowed"],[[65441,65441],"mapped",[4352]],[[65442,65442],"mapped",[4353]],[[65443,65443],"mapped",[4522]],[[65444,65444],"mapped",[4354]],[[65445,65445],"mapped",[4524]],[[65446,65446],"mapped",[4525]],[[65447,65447],"mapped",[4355]],[[65448,65448],"mapped",[4356]],[[65449,65449],"mapped",[4357]],[[65450,65450],"mapped",[4528]],[[65451,65451],"mapped",[4529]],[[65452,65452],"mapped",[4530]],[[65453,65453],"mapped",[4531]],[[65454,65454],"mapped",[4532]],[[65455,65455],"mapped",[4533]],[[65456,65456],"mapped",[4378]],[[65457,65457],"mapped",[4358]],[[65458,65458],"mapped",[4359]],[[65459,65459],"mapped",[4360]],[[65460,65460],"mapped",[4385]],[[65461,65461],"mapped",[4361]],[[65462,65462],"mapped",[4362]],[[65463,65463],"mapped",[4363]],[[65464,65464],"mapped",[4364]],[[65465,65465],"mapped",[4365]],[[65466,65466],"mapped",[4366]],[[65467,65467],"mapped",[4367]],[[65468,65468],"mapped",[4368]],[[65469,65469],"mapped",[4369]],[[65470,65470],"mapped",[4370]],[[65471,65473],"disallowed"],[[65474,65474],"mapped",[4449]],[[65475,65475],"mapped",[4450]],[[65476,65476],"mapped",[4451]],[[65477,65477],"mapped",[4452]],[[65478,65478],"mapped",[4453]],[[65479,65479],"mapped",[4454]],[[65480,65481],"disallowed"],[[65482,65482],"mapped",[4455]],[[65483,65483],"mapped",[4456]],[[65484,65484],"mapped",[4457]],[[65485,65485],"mapped",[4458]],[[65486,65486],"mapped",[4459]],[[65487,65487],"mapped",[4460]],[[65488,65489],"disallowed"],[[65490,65490],"mapped",[4461]],[[65491,65491],"mapped",[4462]],[[65492,65492],"mapped",[4463]],[[65493,65493],"mapped",[4464]],[[65494,65494],"mapped",[4465]],[[65495,65495],"mapped",[4466]],[[65496,65497],"disallowed"],[[65498,65498],"mapped",[4467]],[[65499,65499],"mapped",[4468]],[[65500,65500],"mapped",[4469]],[[65501,65503],"disallowed"],[[65504,65504],"mapped",[162]],[[65505,65505],"mapped",[163]],[[65506,65506],"mapped",[172]],[[65507,65507],"disallowed_STD3_mapped",[32,772]],[[65508,65508],"mapped",[166]],[[65509,65509],"mapped",[165]],[[65510,65510],"mapped",[8361]],[[65511,65511],"disallowed"],[[65512,65512],"mapped",[9474]],[[65513,65513],"mapped",[8592]],[[65514,65514],"mapped",[8593]],[[65515,65515],"mapped",[8594]],[[65516,65516],"mapped",[8595]],[[65517,65517],"mapped",[9632]],[[65518,65518],"mapped",[9675]],[[65519,65528],"disallowed"],[[65529,65531],"disallowed"],[[65532,65532],"disallowed"],[[65533,65533],"disallowed"],[[65534,65535],"disallowed"],[[65536,65547],"valid"],[[65548,65548],"disallowed"],[[65549,65574],"valid"],[[65575,65575],"disallowed"],[[65576,65594],"valid"],[[65595,65595],"disallowed"],[[65596,65597],"valid"],[[65598,65598],"disallowed"],[[65599,65613],"valid"],[[65614,65615],"disallowed"],[[65616,65629],"valid"],[[65630,65663],"disallowed"],[[65664,65786],"valid"],[[65787,65791],"disallowed"],[[65792,65794],"valid",[],"NV8"],[[65795,65798],"disallowed"],[[65799,65843],"valid",[],"NV8"],[[65844,65846],"disallowed"],[[65847,65855],"valid",[],"NV8"],[[65856,65930],"valid",[],"NV8"],[[65931,65932],"valid",[],"NV8"],[[65933,65935],"disallowed"],[[65936,65947],"valid",[],"NV8"],[[65948,65951],"disallowed"],[[65952,65952],"valid",[],"NV8"],[[65953,65999],"disallowed"],[[66000,66044],"valid",[],"NV8"],[[66045,66045],"valid"],[[66046,66175],"disallowed"],[[66176,66204],"valid"],[[66205,66207],"disallowed"],[[66208,66256],"valid"],[[66257,66271],"disallowed"],[[66272,66272],"valid"],[[66273,66299],"valid",[],"NV8"],[[66300,66303],"disallowed"],[[66304,66334],"valid"],[[66335,66335],"valid"],[[66336,66339],"valid",[],"NV8"],[[66340,66351],"disallowed"],[[66352,66368],"valid"],[[66369,66369],"valid",[],"NV8"],[[66370,66377],"valid"],[[66378,66378],"valid",[],"NV8"],[[66379,66383],"disallowed"],[[66384,66426],"valid"],[[66427,66431],"disallowed"],[[66432,66461],"valid"],[[66462,66462],"disallowed"],[[66463,66463],"valid",[],"NV8"],[[66464,66499],"valid"],[[66500,66503],"disallowed"],[[66504,66511],"valid"],[[66512,66517],"valid",[],"NV8"],[[66518,66559],"disallowed"],[[66560,66560],"mapped",[66600]],[[66561,66561],"mapped",[66601]],[[66562,66562],"mapped",[66602]],[[66563,66563],"mapped",[66603]],[[66564,66564],"mapped",[66604]],[[66565,66565],"mapped",[66605]],[[66566,66566],"mapped",[66606]],[[66567,66567],"mapped",[66607]],[[66568,66568],"mapped",[66608]],[[66569,66569],"mapped",[66609]],[[66570,66570],"mapped",[66610]],[[66571,66571],"mapped",[66611]],[[66572,66572],"mapped",[66612]],[[66573,66573],"mapped",[66613]],[[66574,66574],"mapped",[66614]],[[66575,66575],"mapped",[66615]],[[66576,66576],"mapped",[66616]],[[66577,66577],"mapped",[66617]],[[66578,66578],"mapped",[66618]],[[66579,66579],"mapped",[66619]],[[66580,66580],"mapped",[66620]],[[66581,66581],"mapped",[66621]],[[66582,66582],"mapped",[66622]],[[66583,66583],"mapped",[66623]],[[66584,66584],"mapped",[66624]],[[66585,66585],"mapped",[66625]],[[66586,66586],"mapped",[66626]],[[66587,66587],"mapped",[66627]],[[66588,66588],"mapped",[66628]],[[66589,66589],"mapped",[66629]],[[66590,66590],"mapped",[66630]],[[66591,66591],"mapped",[66631]],[[66592,66592],"mapped",[66632]],[[66593,66593],"mapped",[66633]],[[66594,66594],"mapped",[66634]],[[66595,66595],"mapped",[66635]],[[66596,66596],"mapped",[66636]],[[66597,66597],"mapped",[66637]],[[66598,66598],"mapped",[66638]],[[66599,66599],"mapped",[66639]],[[66600,66637],"valid"],[[66638,66717],"valid"],[[66718,66719],"disallowed"],[[66720,66729],"valid"],[[66730,66815],"disallowed"],[[66816,66855],"valid"],[[66856,66863],"disallowed"],[[66864,66915],"valid"],[[66916,66926],"disallowed"],[[66927,66927],"valid",[],"NV8"],[[66928,67071],"disallowed"],[[67072,67382],"valid"],[[67383,67391],"disallowed"],[[67392,67413],"valid"],[[67414,67423],"disallowed"],[[67424,67431],"valid"],[[67432,67583],"disallowed"],[[67584,67589],"valid"],[[67590,67591],"disallowed"],[[67592,67592],"valid"],[[67593,67593],"disallowed"],[[67594,67637],"valid"],[[67638,67638],"disallowed"],[[67639,67640],"valid"],[[67641,67643],"disallowed"],[[67644,67644],"valid"],[[67645,67646],"disallowed"],[[67647,67647],"valid"],[[67648,67669],"valid"],[[67670,67670],"disallowed"],[[67671,67679],"valid",[],"NV8"],[[67680,67702],"valid"],[[67703,67711],"valid",[],"NV8"],[[67712,67742],"valid"],[[67743,67750],"disallowed"],[[67751,67759],"valid",[],"NV8"],[[67760,67807],"disallowed"],[[67808,67826],"valid"],[[67827,67827],"disallowed"],[[67828,67829],"valid"],[[67830,67834],"disallowed"],[[67835,67839],"valid",[],"NV8"],[[67840,67861],"valid"],[[67862,67865],"valid",[],"NV8"],[[67866,67867],"valid",[],"NV8"],[[67868,67870],"disallowed"],[[67871,67871],"valid",[],"NV8"],[[67872,67897],"valid"],[[67898,67902],"disallowed"],[[67903,67903],"valid",[],"NV8"],[[67904,67967],"disallowed"],[[67968,68023],"valid"],[[68024,68027],"disallowed"],[[68028,68029],"valid",[],"NV8"],[[68030,68031],"valid"],[[68032,68047],"valid",[],"NV8"],[[68048,68049],"disallowed"],[[68050,68095],"valid",[],"NV8"],[[68096,68099],"valid"],[[68100,68100],"disallowed"],[[68101,68102],"valid"],[[68103,68107],"disallowed"],[[68108,68115],"valid"],[[68116,68116],"disallowed"],[[68117,68119],"valid"],[[68120,68120],"disallowed"],[[68121,68147],"valid"],[[68148,68151],"disallowed"],[[68152,68154],"valid"],[[68155,68158],"disallowed"],[[68159,68159],"valid"],[[68160,68167],"valid",[],"NV8"],[[68168,68175],"disallowed"],[[68176,68184],"valid",[],"NV8"],[[68185,68191],"disallowed"],[[68192,68220],"valid"],[[68221,68223],"valid",[],"NV8"],[[68224,68252],"valid"],[[68253,68255],"valid",[],"NV8"],[[68256,68287],"disallowed"],[[68288,68295],"valid"],[[68296,68296],"valid",[],"NV8"],[[68297,68326],"valid"],[[68327,68330],"disallowed"],[[68331,68342],"valid",[],"NV8"],[[68343,68351],"disallowed"],[[68352,68405],"valid"],[[68406,68408],"disallowed"],[[68409,68415],"valid",[],"NV8"],[[68416,68437],"valid"],[[68438,68439],"disallowed"],[[68440,68447],"valid",[],"NV8"],[[68448,68466],"valid"],[[68467,68471],"disallowed"],[[68472,68479],"valid",[],"NV8"],[[68480,68497],"valid"],[[68498,68504],"disallowed"],[[68505,68508],"valid",[],"NV8"],[[68509,68520],"disallowed"],[[68521,68527],"valid",[],"NV8"],[[68528,68607],"disallowed"],[[68608,68680],"valid"],[[68681,68735],"disallowed"],[[68736,68736],"mapped",[68800]],[[68737,68737],"mapped",[68801]],[[68738,68738],"mapped",[68802]],[[68739,68739],"mapped",[68803]],[[68740,68740],"mapped",[68804]],[[68741,68741],"mapped",[68805]],[[68742,68742],"mapped",[68806]],[[68743,68743],"mapped",[68807]],[[68744,68744],"mapped",[68808]],[[68745,68745],"mapped",[68809]],[[68746,68746],"mapped",[68810]],[[68747,68747],"mapped",[68811]],[[68748,68748],"mapped",[68812]],[[68749,68749],"mapped",[68813]],[[68750,68750],"mapped",[68814]],[[68751,68751],"mapped",[68815]],[[68752,68752],"mapped",[68816]],[[68753,68753],"mapped",[68817]],[[68754,68754],"mapped",[68818]],[[68755,68755],"mapped",[68819]],[[68756,68756],"mapped",[68820]],[[68757,68757],"mapped",[68821]],[[68758,68758],"mapped",[68822]],[[68759,68759],"mapped",[68823]],[[68760,68760],"mapped",[68824]],[[68761,68761],"mapped",[68825]],[[68762,68762],"mapped",[68826]],[[68763,68763],"mapped",[68827]],[[68764,68764],"mapped",[68828]],[[68765,68765],"mapped",[68829]],[[68766,68766],"mapped",[68830]],[[68767,68767],"mapped",[68831]],[[68768,68768],"mapped",[68832]],[[68769,68769],"mapped",[68833]],[[68770,68770],"mapped",[68834]],[[68771,68771],"mapped",[68835]],[[68772,68772],"mapped",[68836]],[[68773,68773],"mapped",[68837]],[[68774,68774],"mapped",[68838]],[[68775,68775],"mapped",[68839]],[[68776,68776],"mapped",[68840]],[[68777,68777],"mapped",[68841]],[[68778,68778],"mapped",[68842]],[[68779,68779],"mapped",[68843]],[[68780,68780],"mapped",[68844]],[[68781,68781],"mapped",[68845]],[[68782,68782],"mapped",[68846]],[[68783,68783],"mapped",[68847]],[[68784,68784],"mapped",[68848]],[[68785,68785],"mapped",[68849]],[[68786,68786],"mapped",[68850]],[[68787,68799],"disallowed"],[[68800,68850],"valid"],[[68851,68857],"disallowed"],[[68858,68863],"valid",[],"NV8"],[[68864,69215],"disallowed"],[[69216,69246],"valid",[],"NV8"],[[69247,69631],"disallowed"],[[69632,69702],"valid"],[[69703,69709],"valid",[],"NV8"],[[69710,69713],"disallowed"],[[69714,69733],"valid",[],"NV8"],[[69734,69743],"valid"],[[69744,69758],"disallowed"],[[69759,69759],"valid"],[[69760,69818],"valid"],[[69819,69820],"valid",[],"NV8"],[[69821,69821],"disallowed"],[[69822,69825],"valid",[],"NV8"],[[69826,69839],"disallowed"],[[69840,69864],"valid"],[[69865,69871],"disallowed"],[[69872,69881],"valid"],[[69882,69887],"disallowed"],[[69888,69940],"valid"],[[69941,69941],"disallowed"],[[69942,69951],"valid"],[[69952,69955],"valid",[],"NV8"],[[69956,69967],"disallowed"],[[69968,70003],"valid"],[[70004,70005],"valid",[],"NV8"],[[70006,70006],"valid"],[[70007,70015],"disallowed"],[[70016,70084],"valid"],[[70085,70088],"valid",[],"NV8"],[[70089,70089],"valid",[],"NV8"],[[70090,70092],"valid"],[[70093,70093],"valid",[],"NV8"],[[70094,70095],"disallowed"],[[70096,70105],"valid"],[[70106,70106],"valid"],[[70107,70107],"valid",[],"NV8"],[[70108,70108],"valid"],[[70109,70111],"valid",[],"NV8"],[[70112,70112],"disallowed"],[[70113,70132],"valid",[],"NV8"],[[70133,70143],"disallowed"],[[70144,70161],"valid"],[[70162,70162],"disallowed"],[[70163,70199],"valid"],[[70200,70205],"valid",[],"NV8"],[[70206,70271],"disallowed"],[[70272,70278],"valid"],[[70279,70279],"disallowed"],[[70280,70280],"valid"],[[70281,70281],"disallowed"],[[70282,70285],"valid"],[[70286,70286],"disallowed"],[[70287,70301],"valid"],[[70302,70302],"disallowed"],[[70303,70312],"valid"],[[70313,70313],"valid",[],"NV8"],[[70314,70319],"disallowed"],[[70320,70378],"valid"],[[70379,70383],"disallowed"],[[70384,70393],"valid"],[[70394,70399],"disallowed"],[[70400,70400],"valid"],[[70401,70403],"valid"],[[70404,70404],"disallowed"],[[70405,70412],"valid"],[[70413,70414],"disallowed"],[[70415,70416],"valid"],[[70417,70418],"disallowed"],[[70419,70440],"valid"],[[70441,70441],"disallowed"],[[70442,70448],"valid"],[[70449,70449],"disallowed"],[[70450,70451],"valid"],[[70452,70452],"disallowed"],[[70453,70457],"valid"],[[70458,70459],"disallowed"],[[70460,70468],"valid"],[[70469,70470],"disallowed"],[[70471,70472],"valid"],[[70473,70474],"disallowed"],[[70475,70477],"valid"],[[70478,70479],"disallowed"],[[70480,70480],"valid"],[[70481,70486],"disallowed"],[[70487,70487],"valid"],[[70488,70492],"disallowed"],[[70493,70499],"valid"],[[70500,70501],"disallowed"],[[70502,70508],"valid"],[[70509,70511],"disallowed"],[[70512,70516],"valid"],[[70517,70783],"disallowed"],[[70784,70853],"valid"],[[70854,70854],"valid",[],"NV8"],[[70855,70855],"valid"],[[70856,70863],"disallowed"],[[70864,70873],"valid"],[[70874,71039],"disallowed"],[[71040,71093],"valid"],[[71094,71095],"disallowed"],[[71096,71104],"valid"],[[71105,71113],"valid",[],"NV8"],[[71114,71127],"valid",[],"NV8"],[[71128,71133],"valid"],[[71134,71167],"disallowed"],[[71168,71232],"valid"],[[71233,71235],"valid",[],"NV8"],[[71236,71236],"valid"],[[71237,71247],"disallowed"],[[71248,71257],"valid"],[[71258,71295],"disallowed"],[[71296,71351],"valid"],[[71352,71359],"disallowed"],[[71360,71369],"valid"],[[71370,71423],"disallowed"],[[71424,71449],"valid"],[[71450,71452],"disallowed"],[[71453,71467],"valid"],[[71468,71471],"disallowed"],[[71472,71481],"valid"],[[71482,71487],"valid",[],"NV8"],[[71488,71839],"disallowed"],[[71840,71840],"mapped",[71872]],[[71841,71841],"mapped",[71873]],[[71842,71842],"mapped",[71874]],[[71843,71843],"mapped",[71875]],[[71844,71844],"mapped",[71876]],[[71845,71845],"mapped",[71877]],[[71846,71846],"mapped",[71878]],[[71847,71847],"mapped",[71879]],[[71848,71848],"mapped",[71880]],[[71849,71849],"mapped",[71881]],[[71850,71850],"mapped",[71882]],[[71851,71851],"mapped",[71883]],[[71852,71852],"mapped",[71884]],[[71853,71853],"mapped",[71885]],[[71854,71854],"mapped",[71886]],[[71855,71855],"mapped",[71887]],[[71856,71856],"mapped",[71888]],[[71857,71857],"mapped",[71889]],[[71858,71858],"mapped",[71890]],[[71859,71859],"mapped",[71891]],[[71860,71860],"mapped",[71892]],[[71861,71861],"mapped",[71893]],[[71862,71862],"mapped",[71894]],[[71863,71863],"mapped",[71895]],[[71864,71864],"mapped",[71896]],[[71865,71865],"mapped",[71897]],[[71866,71866],"mapped",[71898]],[[71867,71867],"mapped",[71899]],[[71868,71868],"mapped",[71900]],[[71869,71869],"mapped",[71901]],[[71870,71870],"mapped",[71902]],[[71871,71871],"mapped",[71903]],[[71872,71913],"valid"],[[71914,71922],"valid",[],"NV8"],[[71923,71934],"disallowed"],[[71935,71935],"valid"],[[71936,72383],"disallowed"],[[72384,72440],"valid"],[[72441,73727],"disallowed"],[[73728,74606],"valid"],[[74607,74648],"valid"],[[74649,74649],"valid"],[[74650,74751],"disallowed"],[[74752,74850],"valid",[],"NV8"],[[74851,74862],"valid",[],"NV8"],[[74863,74863],"disallowed"],[[74864,74867],"valid",[],"NV8"],[[74868,74868],"valid",[],"NV8"],[[74869,74879],"disallowed"],[[74880,75075],"valid"],[[75076,77823],"disallowed"],[[77824,78894],"valid"],[[78895,82943],"disallowed"],[[82944,83526],"valid"],[[83527,92159],"disallowed"],[[92160,92728],"valid"],[[92729,92735],"disallowed"],[[92736,92766],"valid"],[[92767,92767],"disallowed"],[[92768,92777],"valid"],[[92778,92781],"disallowed"],[[92782,92783],"valid",[],"NV8"],[[92784,92879],"disallowed"],[[92880,92909],"valid"],[[92910,92911],"disallowed"],[[92912,92916],"valid"],[[92917,92917],"valid",[],"NV8"],[[92918,92927],"disallowed"],[[92928,92982],"valid"],[[92983,92991],"valid",[],"NV8"],[[92992,92995],"valid"],[[92996,92997],"valid",[],"NV8"],[[92998,93007],"disallowed"],[[93008,93017],"valid"],[[93018,93018],"disallowed"],[[93019,93025],"valid",[],"NV8"],[[93026,93026],"disallowed"],[[93027,93047],"valid"],[[93048,93052],"disallowed"],[[93053,93071],"valid"],[[93072,93951],"disallowed"],[[93952,94020],"valid"],[[94021,94031],"disallowed"],[[94032,94078],"valid"],[[94079,94094],"disallowed"],[[94095,94111],"valid"],[[94112,110591],"disallowed"],[[110592,110593],"valid"],[[110594,113663],"disallowed"],[[113664,113770],"valid"],[[113771,113775],"disallowed"],[[113776,113788],"valid"],[[113789,113791],"disallowed"],[[113792,113800],"valid"],[[113801,113807],"disallowed"],[[113808,113817],"valid"],[[113818,113819],"disallowed"],[[113820,113820],"valid",[],"NV8"],[[113821,113822],"valid"],[[113823,113823],"valid",[],"NV8"],[[113824,113827],"ignored"],[[113828,118783],"disallowed"],[[118784,119029],"valid",[],"NV8"],[[119030,119039],"disallowed"],[[119040,119078],"valid",[],"NV8"],[[119079,119080],"disallowed"],[[119081,119081],"valid",[],"NV8"],[[119082,119133],"valid",[],"NV8"],[[119134,119134],"mapped",[119127,119141]],[[119135,119135],"mapped",[119128,119141]],[[119136,119136],"mapped",[119128,119141,119150]],[[119137,119137],"mapped",[119128,119141,119151]],[[119138,119138],"mapped",[119128,119141,119152]],[[119139,119139],"mapped",[119128,119141,119153]],[[119140,119140],"mapped",[119128,119141,119154]],[[119141,119154],"valid",[],"NV8"],[[119155,119162],"disallowed"],[[119163,119226],"valid",[],"NV8"],[[119227,119227],"mapped",[119225,119141]],[[119228,119228],"mapped",[119226,119141]],[[119229,119229],"mapped",[119225,119141,119150]],[[119230,119230],"mapped",[119226,119141,119150]],[[119231,119231],"mapped",[119225,119141,119151]],[[119232,119232],"mapped",[119226,119141,119151]],[[119233,119261],"valid",[],"NV8"],[[119262,119272],"valid",[],"NV8"],[[119273,119295],"disallowed"],[[119296,119365],"valid",[],"NV8"],[[119366,119551],"disallowed"],[[119552,119638],"valid",[],"NV8"],[[119639,119647],"disallowed"],[[119648,119665],"valid",[],"NV8"],[[119666,119807],"disallowed"],[[119808,119808],"mapped",[97]],[[119809,119809],"mapped",[98]],[[119810,119810],"mapped",[99]],[[119811,119811],"mapped",[100]],[[119812,119812],"mapped",[101]],[[119813,119813],"mapped",[102]],[[119814,119814],"mapped",[103]],[[119815,119815],"mapped",[104]],[[119816,119816],"mapped",[105]],[[119817,119817],"mapped",[106]],[[119818,119818],"mapped",[107]],[[119819,119819],"mapped",[108]],[[119820,119820],"mapped",[109]],[[119821,119821],"mapped",[110]],[[119822,119822],"mapped",[111]],[[119823,119823],"mapped",[112]],[[119824,119824],"mapped",[113]],[[119825,119825],"mapped",[114]],[[119826,119826],"mapped",[115]],[[119827,119827],"mapped",[116]],[[119828,119828],"mapped",[117]],[[119829,119829],"mapped",[118]],[[119830,119830],"mapped",[119]],[[119831,119831],"mapped",[120]],[[119832,119832],"mapped",[121]],[[119833,119833],"mapped",[122]],[[119834,119834],"mapped",[97]],[[119835,119835],"mapped",[98]],[[119836,119836],"mapped",[99]],[[119837,119837],"mapped",[100]],[[119838,119838],"mapped",[101]],[[119839,119839],"mapped",[102]],[[119840,119840],"mapped",[103]],[[119841,119841],"mapped",[104]],[[119842,119842],"mapped",[105]],[[119843,119843],"mapped",[106]],[[119844,119844],"mapped",[107]],[[119845,119845],"mapped",[108]],[[119846,119846],"mapped",[109]],[[119847,119847],"mapped",[110]],[[119848,119848],"mapped",[111]],[[119849,119849],"mapped",[112]],[[119850,119850],"mapped",[113]],[[119851,119851],"mapped",[114]],[[119852,119852],"mapped",[115]],[[119853,119853],"mapped",[116]],[[119854,119854],"mapped",[117]],[[119855,119855],"mapped",[118]],[[119856,119856],"mapped",[119]],[[119857,119857],"mapped",[120]],[[119858,119858],"mapped",[121]],[[119859,119859],"mapped",[122]],[[119860,119860],"mapped",[97]],[[119861,119861],"mapped",[98]],[[119862,119862],"mapped",[99]],[[119863,119863],"mapped",[100]],[[119864,119864],"mapped",[101]],[[119865,119865],"mapped",[102]],[[119866,119866],"mapped",[103]],[[119867,119867],"mapped",[104]],[[119868,119868],"mapped",[105]],[[119869,119869],"mapped",[106]],[[119870,119870],"mapped",[107]],[[119871,119871],"mapped",[108]],[[119872,119872],"mapped",[109]],[[119873,119873],"mapped",[110]],[[119874,119874],"mapped",[111]],[[119875,119875],"mapped",[112]],[[119876,119876],"mapped",[113]],[[119877,119877],"mapped",[114]],[[119878,119878],"mapped",[115]],[[119879,119879],"mapped",[116]],[[119880,119880],"mapped",[117]],[[119881,119881],"mapped",[118]],[[119882,119882],"mapped",[119]],[[119883,119883],"mapped",[120]],[[119884,119884],"mapped",[121]],[[119885,119885],"mapped",[122]],[[119886,119886],"mapped",[97]],[[119887,119887],"mapped",[98]],[[119888,119888],"mapped",[99]],[[119889,119889],"mapped",[100]],[[119890,119890],"mapped",[101]],[[119891,119891],"mapped",[102]],[[119892,119892],"mapped",[103]],[[119893,119893],"disallowed"],[[119894,119894],"mapped",[105]],[[119895,119895],"mapped",[106]],[[119896,119896],"mapped",[107]],[[119897,119897],"mapped",[108]],[[119898,119898],"mapped",[109]],[[119899,119899],"mapped",[110]],[[119900,119900],"mapped",[111]],[[119901,119901],"mapped",[112]],[[119902,119902],"mapped",[113]],[[119903,119903],"mapped",[114]],[[119904,119904],"mapped",[115]],[[119905,119905],"mapped",[116]],[[119906,119906],"mapped",[117]],[[119907,119907],"mapped",[118]],[[119908,119908],"mapped",[119]],[[119909,119909],"mapped",[120]],[[119910,119910],"mapped",[121]],[[119911,119911],"mapped",[122]],[[119912,119912],"mapped",[97]],[[119913,119913],"mapped",[98]],[[119914,119914],"mapped",[99]],[[119915,119915],"mapped",[100]],[[119916,119916],"mapped",[101]],[[119917,119917],"mapped",[102]],[[119918,119918],"mapped",[103]],[[119919,119919],"mapped",[104]],[[119920,119920],"mapped",[105]],[[119921,119921],"mapped",[106]],[[119922,119922],"mapped",[107]],[[119923,119923],"mapped",[108]],[[119924,119924],"mapped",[109]],[[119925,119925],"mapped",[110]],[[119926,119926],"mapped",[111]],[[119927,119927],"mapped",[112]],[[119928,119928],"mapped",[113]],[[119929,119929],"mapped",[114]],[[119930,119930],"mapped",[115]],[[119931,119931],"mapped",[116]],[[119932,119932],"mapped",[117]],[[119933,119933],"mapped",[118]],[[119934,119934],"mapped",[119]],[[119935,119935],"mapped",[120]],[[119936,119936],"mapped",[121]],[[119937,119937],"mapped",[122]],[[119938,119938],"mapped",[97]],[[119939,119939],"mapped",[98]],[[119940,119940],"mapped",[99]],[[119941,119941],"mapped",[100]],[[119942,119942],"mapped",[101]],[[119943,119943],"mapped",[102]],[[119944,119944],"mapped",[103]],[[119945,119945],"mapped",[104]],[[119946,119946],"mapped",[105]],[[119947,119947],"mapped",[106]],[[119948,119948],"mapped",[107]],[[119949,119949],"mapped",[108]],[[119950,119950],"mapped",[109]],[[119951,119951],"mapped",[110]],[[119952,119952],"mapped",[111]],[[119953,119953],"mapped",[112]],[[119954,119954],"mapped",[113]],[[119955,119955],"mapped",[114]],[[119956,119956],"mapped",[115]],[[119957,119957],"mapped",[116]],[[119958,119958],"mapped",[117]],[[119959,119959],"mapped",[118]],[[119960,119960],"mapped",[119]],[[119961,119961],"mapped",[120]],[[119962,119962],"mapped",[121]],[[119963,119963],"mapped",[122]],[[119964,119964],"mapped",[97]],[[119965,119965],"disallowed"],[[119966,119966],"mapped",[99]],[[119967,119967],"mapped",[100]],[[119968,119969],"disallowed"],[[119970,119970],"mapped",[103]],[[119971,119972],"disallowed"],[[119973,119973],"mapped",[106]],[[119974,119974],"mapped",[107]],[[119975,119976],"disallowed"],[[119977,119977],"mapped",[110]],[[119978,119978],"mapped",[111]],[[119979,119979],"mapped",[112]],[[119980,119980],"mapped",[113]],[[119981,119981],"disallowed"],[[119982,119982],"mapped",[115]],[[119983,119983],"mapped",[116]],[[119984,119984],"mapped",[117]],[[119985,119985],"mapped",[118]],[[119986,119986],"mapped",[119]],[[119987,119987],"mapped",[120]],[[119988,119988],"mapped",[121]],[[119989,119989],"mapped",[122]],[[119990,119990],"mapped",[97]],[[119991,119991],"mapped",[98]],[[119992,119992],"mapped",[99]],[[119993,119993],"mapped",[100]],[[119994,119994],"disallowed"],[[119995,119995],"mapped",[102]],[[119996,119996],"disallowed"],[[119997,119997],"mapped",[104]],[[119998,119998],"mapped",[105]],[[119999,119999],"mapped",[106]],[[120000,120000],"mapped",[107]],[[120001,120001],"mapped",[108]],[[120002,120002],"mapped",[109]],[[120003,120003],"mapped",[110]],[[120004,120004],"disallowed"],[[120005,120005],"mapped",[112]],[[120006,120006],"mapped",[113]],[[120007,120007],"mapped",[114]],[[120008,120008],"mapped",[115]],[[120009,120009],"mapped",[116]],[[120010,120010],"mapped",[117]],[[120011,120011],"mapped",[118]],[[120012,120012],"mapped",[119]],[[120013,120013],"mapped",[120]],[[120014,120014],"mapped",[121]],[[120015,120015],"mapped",[122]],[[120016,120016],"mapped",[97]],[[120017,120017],"mapped",[98]],[[120018,120018],"mapped",[99]],[[120019,120019],"mapped",[100]],[[120020,120020],"mapped",[101]],[[120021,120021],"mapped",[102]],[[120022,120022],"mapped",[103]],[[120023,120023],"mapped",[104]],[[120024,120024],"mapped",[105]],[[120025,120025],"mapped",[106]],[[120026,120026],"mapped",[107]],[[120027,120027],"mapped",[108]],[[120028,120028],"mapped",[109]],[[120029,120029],"mapped",[110]],[[120030,120030],"mapped",[111]],[[120031,120031],"mapped",[112]],[[120032,120032],"mapped",[113]],[[120033,120033],"mapped",[114]],[[120034,120034],"mapped",[115]],[[120035,120035],"mapped",[116]],[[120036,120036],"mapped",[117]],[[120037,120037],"mapped",[118]],[[120038,120038],"mapped",[119]],[[120039,120039],"mapped",[120]],[[120040,120040],"mapped",[121]],[[120041,120041],"mapped",[122]],[[120042,120042],"mapped",[97]],[[120043,120043],"mapped",[98]],[[120044,120044],"mapped",[99]],[[120045,120045],"mapped",[100]],[[120046,120046],"mapped",[101]],[[120047,120047],"mapped",[102]],[[120048,120048],"mapped",[103]],[[120049,120049],"mapped",[104]],[[120050,120050],"mapped",[105]],[[120051,120051],"mapped",[106]],[[120052,120052],"mapped",[107]],[[120053,120053],"mapped",[108]],[[120054,120054],"mapped",[109]],[[120055,120055],"mapped",[110]],[[120056,120056],"mapped",[111]],[[120057,120057],"mapped",[112]],[[120058,120058],"mapped",[113]],[[120059,120059],"mapped",[114]],[[120060,120060],"mapped",[115]],[[120061,120061],"mapped",[116]],[[120062,120062],"mapped",[117]],[[120063,120063],"mapped",[118]],[[120064,120064],"mapped",[119]],[[120065,120065],"mapped",[120]],[[120066,120066],"mapped",[121]],[[120067,120067],"mapped",[122]],[[120068,120068],"mapped",[97]],[[120069,120069],"mapped",[98]],[[120070,120070],"disallowed"],[[120071,120071],"mapped",[100]],[[120072,120072],"mapped",[101]],[[120073,120073],"mapped",[102]],[[120074,120074],"mapped",[103]],[[120075,120076],"disallowed"],[[120077,120077],"mapped",[106]],[[120078,120078],"mapped",[107]],[[120079,120079],"mapped",[108]],[[120080,120080],"mapped",[109]],[[120081,120081],"mapped",[110]],[[120082,120082],"mapped",[111]],[[120083,120083],"mapped",[112]],[[120084,120084],"mapped",[113]],[[120085,120085],"disallowed"],[[120086,120086],"mapped",[115]],[[120087,120087],"mapped",[116]],[[120088,120088],"mapped",[117]],[[120089,120089],"mapped",[118]],[[120090,120090],"mapped",[119]],[[120091,120091],"mapped",[120]],[[120092,120092],"mapped",[121]],[[120093,120093],"disallowed"],[[120094,120094],"mapped",[97]],[[120095,120095],"mapped",[98]],[[120096,120096],"mapped",[99]],[[120097,120097],"mapped",[100]],[[120098,120098],"mapped",[101]],[[120099,120099],"mapped",[102]],[[120100,120100],"mapped",[103]],[[120101,120101],"mapped",[104]],[[120102,120102],"mapped",[105]],[[120103,120103],"mapped",[106]],[[120104,120104],"mapped",[107]],[[120105,120105],"mapped",[108]],[[120106,120106],"mapped",[109]],[[120107,120107],"mapped",[110]],[[120108,120108],"mapped",[111]],[[120109,120109],"mapped",[112]],[[120110,120110],"mapped",[113]],[[120111,120111],"mapped",[114]],[[120112,120112],"mapped",[115]],[[120113,120113],"mapped",[116]],[[120114,120114],"mapped",[117]],[[120115,120115],"mapped",[118]],[[120116,120116],"mapped",[119]],[[120117,120117],"mapped",[120]],[[120118,120118],"mapped",[121]],[[120119,120119],"mapped",[122]],[[120120,120120],"mapped",[97]],[[120121,120121],"mapped",[98]],[[120122,120122],"disallowed"],[[120123,120123],"mapped",[100]],[[120124,120124],"mapped",[101]],[[120125,120125],"mapped",[102]],[[120126,120126],"mapped",[103]],[[120127,120127],"disallowed"],[[120128,120128],"mapped",[105]],[[120129,120129],"mapped",[106]],[[120130,120130],"mapped",[107]],[[120131,120131],"mapped",[108]],[[120132,120132],"mapped",[109]],[[120133,120133],"disallowed"],[[120134,120134],"mapped",[111]],[[120135,120137],"disallowed"],[[120138,120138],"mapped",[115]],[[120139,120139],"mapped",[116]],[[120140,120140],"mapped",[117]],[[120141,120141],"mapped",[118]],[[120142,120142],"mapped",[119]],[[120143,120143],"mapped",[120]],[[120144,120144],"mapped",[121]],[[120145,120145],"disallowed"],[[120146,120146],"mapped",[97]],[[120147,120147],"mapped",[98]],[[120148,120148],"mapped",[99]],[[120149,120149],"mapped",[100]],[[120150,120150],"mapped",[101]],[[120151,120151],"mapped",[102]],[[120152,120152],"mapped",[103]],[[120153,120153],"mapped",[104]],[[120154,120154],"mapped",[105]],[[120155,120155],"mapped",[106]],[[120156,120156],"mapped",[107]],[[120157,120157],"mapped",[108]],[[120158,120158],"mapped",[109]],[[120159,120159],"mapped",[110]],[[120160,120160],"mapped",[111]],[[120161,120161],"mapped",[112]],[[120162,120162],"mapped",[113]],[[120163,120163],"mapped",[114]],[[120164,120164],"mapped",[115]],[[120165,120165],"mapped",[116]],[[120166,120166],"mapped",[117]],[[120167,120167],"mapped",[118]],[[120168,120168],"mapped",[119]],[[120169,120169],"mapped",[120]],[[120170,120170],"mapped",[121]],[[120171,120171],"mapped",[122]],[[120172,120172],"mapped",[97]],[[120173,120173],"mapped",[98]],[[120174,120174],"mapped",[99]],[[120175,120175],"mapped",[100]],[[120176,120176],"mapped",[101]],[[120177,120177],"mapped",[102]],[[120178,120178],"mapped",[103]],[[120179,120179],"mapped",[104]],[[120180,120180],"mapped",[105]],[[120181,120181],"mapped",[106]],[[120182,120182],"mapped",[107]],[[120183,120183],"mapped",[108]],[[120184,120184],"mapped",[109]],[[120185,120185],"mapped",[110]],[[120186,120186],"mapped",[111]],[[120187,120187],"mapped",[112]],[[120188,120188],"mapped",[113]],[[120189,120189],"mapped",[114]],[[120190,120190],"mapped",[115]],[[120191,120191],"mapped",[116]],[[120192,120192],"mapped",[117]],[[120193,120193],"mapped",[118]],[[120194,120194],"mapped",[119]],[[120195,120195],"mapped",[120]],[[120196,120196],"mapped",[121]],[[120197,120197],"mapped",[122]],[[120198,120198],"mapped",[97]],[[120199,120199],"mapped",[98]],[[120200,120200],"mapped",[99]],[[120201,120201],"mapped",[100]],[[120202,120202],"mapped",[101]],[[120203,120203],"mapped",[102]],[[120204,120204],"mapped",[103]],[[120205,120205],"mapped",[104]],[[120206,120206],"mapped",[105]],[[120207,120207],"mapped",[106]],[[120208,120208],"mapped",[107]],[[120209,120209],"mapped",[108]],[[120210,120210],"mapped",[109]],[[120211,120211],"mapped",[110]],[[120212,120212],"mapped",[111]],[[120213,120213],"mapped",[112]],[[120214,120214],"mapped",[113]],[[120215,120215],"mapped",[114]],[[120216,120216],"mapped",[115]],[[120217,120217],"mapped",[116]],[[120218,120218],"mapped",[117]],[[120219,120219],"mapped",[118]],[[120220,120220],"mapped",[119]],[[120221,120221],"mapped",[120]],[[120222,120222],"mapped",[121]],[[120223,120223],"mapped",[122]],[[120224,120224],"mapped",[97]],[[120225,120225],"mapped",[98]],[[120226,120226],"mapped",[99]],[[120227,120227],"mapped",[100]],[[120228,120228],"mapped",[101]],[[120229,120229],"mapped",[102]],[[120230,120230],"mapped",[103]],[[120231,120231],"mapped",[104]],[[120232,120232],"mapped",[105]],[[120233,120233],"mapped",[106]],[[120234,120234],"mapped",[107]],[[120235,120235],"mapped",[108]],[[120236,120236],"mapped",[109]],[[120237,120237],"mapped",[110]],[[120238,120238],"mapped",[111]],[[120239,120239],"mapped",[112]],[[120240,120240],"mapped",[113]],[[120241,120241],"mapped",[114]],[[120242,120242],"mapped",[115]],[[120243,120243],"mapped",[116]],[[120244,120244],"mapped",[117]],[[120245,120245],"mapped",[118]],[[120246,120246],"mapped",[119]],[[120247,120247],"mapped",[120]],[[120248,120248],"mapped",[121]],[[120249,120249],"mapped",[122]],[[120250,120250],"mapped",[97]],[[120251,120251],"mapped",[98]],[[120252,120252],"mapped",[99]],[[120253,120253],"mapped",[100]],[[120254,120254],"mapped",[101]],[[120255,120255],"mapped",[102]],[[120256,120256],"mapped",[103]],[[120257,120257],"mapped",[104]],[[120258,120258],"mapped",[105]],[[120259,120259],"mapped",[106]],[[120260,120260],"mapped",[107]],[[120261,120261],"mapped",[108]],[[120262,120262],"mapped",[109]],[[120263,120263],"mapped",[110]],[[120264,120264],"mapped",[111]],[[120265,120265],"mapped",[112]],[[120266,120266],"mapped",[113]],[[120267,120267],"mapped",[114]],[[120268,120268],"mapped",[115]],[[120269,120269],"mapped",[116]],[[120270,120270],"mapped",[117]],[[120271,120271],"mapped",[118]],[[120272,120272],"mapped",[119]],[[120273,120273],"mapped",[120]],[[120274,120274],"mapped",[121]],[[120275,120275],"mapped",[122]],[[120276,120276],"mapped",[97]],[[120277,120277],"mapped",[98]],[[120278,120278],"mapped",[99]],[[120279,120279],"mapped",[100]],[[120280,120280],"mapped",[101]],[[120281,120281],"mapped",[102]],[[120282,120282],"mapped",[103]],[[120283,120283],"mapped",[104]],[[120284,120284],"mapped",[105]],[[120285,120285],"mapped",[106]],[[120286,120286],"mapped",[107]],[[120287,120287],"mapped",[108]],[[120288,120288],"mapped",[109]],[[120289,120289],"mapped",[110]],[[120290,120290],"mapped",[111]],[[120291,120291],"mapped",[112]],[[120292,120292],"mapped",[113]],[[120293,120293],"mapped",[114]],[[120294,120294],"mapped",[115]],[[120295,120295],"mapped",[116]],[[120296,120296],"mapped",[117]],[[120297,120297],"mapped",[118]],[[120298,120298],"mapped",[119]],[[120299,120299],"mapped",[120]],[[120300,120300],"mapped",[121]],[[120301,120301],"mapped",[122]],[[120302,120302],"mapped",[97]],[[120303,120303],"mapped",[98]],[[120304,120304],"mapped",[99]],[[120305,120305],"mapped",[100]],[[120306,120306],"mapped",[101]],[[120307,120307],"mapped",[102]],[[120308,120308],"mapped",[103]],[[120309,120309],"mapped",[104]],[[120310,120310],"mapped",[105]],[[120311,120311],"mapped",[106]],[[120312,120312],"mapped",[107]],[[120313,120313],"mapped",[108]],[[120314,120314],"mapped",[109]],[[120315,120315],"mapped",[110]],[[120316,120316],"mapped",[111]],[[120317,120317],"mapped",[112]],[[120318,120318],"mapped",[113]],[[120319,120319],"mapped",[114]],[[120320,120320],"mapped",[115]],[[120321,120321],"mapped",[116]],[[120322,120322],"mapped",[117]],[[120323,120323],"mapped",[118]],[[120324,120324],"mapped",[119]],[[120325,120325],"mapped",[120]],[[120326,120326],"mapped",[121]],[[120327,120327],"mapped",[122]],[[120328,120328],"mapped",[97]],[[120329,120329],"mapped",[98]],[[120330,120330],"mapped",[99]],[[120331,120331],"mapped",[100]],[[120332,120332],"mapped",[101]],[[120333,120333],"mapped",[102]],[[120334,120334],"mapped",[103]],[[120335,120335],"mapped",[104]],[[120336,120336],"mapped",[105]],[[120337,120337],"mapped",[106]],[[120338,120338],"mapped",[107]],[[120339,120339],"mapped",[108]],[[120340,120340],"mapped",[109]],[[120341,120341],"mapped",[110]],[[120342,120342],"mapped",[111]],[[120343,120343],"mapped",[112]],[[120344,120344],"mapped",[113]],[[120345,120345],"mapped",[114]],[[120346,120346],"mapped",[115]],[[120347,120347],"mapped",[116]],[[120348,120348],"mapped",[117]],[[120349,120349],"mapped",[118]],[[120350,120350],"mapped",[119]],[[120351,120351],"mapped",[120]],[[120352,120352],"mapped",[121]],[[120353,120353],"mapped",[122]],[[120354,120354],"mapped",[97]],[[120355,120355],"mapped",[98]],[[120356,120356],"mapped",[99]],[[120357,120357],"mapped",[100]],[[120358,120358],"mapped",[101]],[[120359,120359],"mapped",[102]],[[120360,120360],"mapped",[103]],[[120361,120361],"mapped",[104]],[[120362,120362],"mapped",[105]],[[120363,120363],"mapped",[106]],[[120364,120364],"mapped",[107]],[[120365,120365],"mapped",[108]],[[120366,120366],"mapped",[109]],[[120367,120367],"mapped",[110]],[[120368,120368],"mapped",[111]],[[120369,120369],"mapped",[112]],[[120370,120370],"mapped",[113]],[[120371,120371],"mapped",[114]],[[120372,120372],"mapped",[115]],[[120373,120373],"mapped",[116]],[[120374,120374],"mapped",[117]],[[120375,120375],"mapped",[118]],[[120376,120376],"mapped",[119]],[[120377,120377],"mapped",[120]],[[120378,120378],"mapped",[121]],[[120379,120379],"mapped",[122]],[[120380,120380],"mapped",[97]],[[120381,120381],"mapped",[98]],[[120382,120382],"mapped",[99]],[[120383,120383],"mapped",[100]],[[120384,120384],"mapped",[101]],[[120385,120385],"mapped",[102]],[[120386,120386],"mapped",[103]],[[120387,120387],"mapped",[104]],[[120388,120388],"mapped",[105]],[[120389,120389],"mapped",[106]],[[120390,120390],"mapped",[107]],[[120391,120391],"mapped",[108]],[[120392,120392],"mapped",[109]],[[120393,120393],"mapped",[110]],[[120394,120394],"mapped",[111]],[[120395,120395],"mapped",[112]],[[120396,120396],"mapped",[113]],[[120397,120397],"mapped",[114]],[[120398,120398],"mapped",[115]],[[120399,120399],"mapped",[116]],[[120400,120400],"mapped",[117]],[[120401,120401],"mapped",[118]],[[120402,120402],"mapped",[119]],[[120403,120403],"mapped",[120]],[[120404,120404],"mapped",[121]],[[120405,120405],"mapped",[122]],[[120406,120406],"mapped",[97]],[[120407,120407],"mapped",[98]],[[120408,120408],"mapped",[99]],[[120409,120409],"mapped",[100]],[[120410,120410],"mapped",[101]],[[120411,120411],"mapped",[102]],[[120412,120412],"mapped",[103]],[[120413,120413],"mapped",[104]],[[120414,120414],"mapped",[105]],[[120415,120415],"mapped",[106]],[[120416,120416],"mapped",[107]],[[120417,120417],"mapped",[108]],[[120418,120418],"mapped",[109]],[[120419,120419],"mapped",[110]],[[120420,120420],"mapped",[111]],[[120421,120421],"mapped",[112]],[[120422,120422],"mapped",[113]],[[120423,120423],"mapped",[114]],[[120424,120424],"mapped",[115]],[[120425,120425],"mapped",[116]],[[120426,120426],"mapped",[117]],[[120427,120427],"mapped",[118]],[[120428,120428],"mapped",[119]],[[120429,120429],"mapped",[120]],[[120430,120430],"mapped",[121]],[[120431,120431],"mapped",[122]],[[120432,120432],"mapped",[97]],[[120433,120433],"mapped",[98]],[[120434,120434],"mapped",[99]],[[120435,120435],"mapped",[100]],[[120436,120436],"mapped",[101]],[[120437,120437],"mapped",[102]],[[120438,120438],"mapped",[103]],[[120439,120439],"mapped",[104]],[[120440,120440],"mapped",[105]],[[120441,120441],"mapped",[106]],[[120442,120442],"mapped",[107]],[[120443,120443],"mapped",[108]],[[120444,120444],"mapped",[109]],[[120445,120445],"mapped",[110]],[[120446,120446],"mapped",[111]],[[120447,120447],"mapped",[112]],[[120448,120448],"mapped",[113]],[[120449,120449],"mapped",[114]],[[120450,120450],"mapped",[115]],[[120451,120451],"mapped",[116]],[[120452,120452],"mapped",[117]],[[120453,120453],"mapped",[118]],[[120454,120454],"mapped",[119]],[[120455,120455],"mapped",[120]],[[120456,120456],"mapped",[121]],[[120457,120457],"mapped",[122]],[[120458,120458],"mapped",[97]],[[120459,120459],"mapped",[98]],[[120460,120460],"mapped",[99]],[[120461,120461],"mapped",[100]],[[120462,120462],"mapped",[101]],[[120463,120463],"mapped",[102]],[[120464,120464],"mapped",[103]],[[120465,120465],"mapped",[104]],[[120466,120466],"mapped",[105]],[[120467,120467],"mapped",[106]],[[120468,120468],"mapped",[107]],[[120469,120469],"mapped",[108]],[[120470,120470],"mapped",[109]],[[120471,120471],"mapped",[110]],[[120472,120472],"mapped",[111]],[[120473,120473],"mapped",[112]],[[120474,120474],"mapped",[113]],[[120475,120475],"mapped",[114]],[[120476,120476],"mapped",[115]],[[120477,120477],"mapped",[116]],[[120478,120478],"mapped",[117]],[[120479,120479],"mapped",[118]],[[120480,120480],"mapped",[119]],[[120481,120481],"mapped",[120]],[[120482,120482],"mapped",[121]],[[120483,120483],"mapped",[122]],[[120484,120484],"mapped",[305]],[[120485,120485],"mapped",[567]],[[120486,120487],"disallowed"],[[120488,120488],"mapped",[945]],[[120489,120489],"mapped",[946]],[[120490,120490],"mapped",[947]],[[120491,120491],"mapped",[948]],[[120492,120492],"mapped",[949]],[[120493,120493],"mapped",[950]],[[120494,120494],"mapped",[951]],[[120495,120495],"mapped",[952]],[[120496,120496],"mapped",[953]],[[120497,120497],"mapped",[954]],[[120498,120498],"mapped",[955]],[[120499,120499],"mapped",[956]],[[120500,120500],"mapped",[957]],[[120501,120501],"mapped",[958]],[[120502,120502],"mapped",[959]],[[120503,120503],"mapped",[960]],[[120504,120504],"mapped",[961]],[[120505,120505],"mapped",[952]],[[120506,120506],"mapped",[963]],[[120507,120507],"mapped",[964]],[[120508,120508],"mapped",[965]],[[120509,120509],"mapped",[966]],[[120510,120510],"mapped",[967]],[[120511,120511],"mapped",[968]],[[120512,120512],"mapped",[969]],[[120513,120513],"mapped",[8711]],[[120514,120514],"mapped",[945]],[[120515,120515],"mapped",[946]],[[120516,120516],"mapped",[947]],[[120517,120517],"mapped",[948]],[[120518,120518],"mapped",[949]],[[120519,120519],"mapped",[950]],[[120520,120520],"mapped",[951]],[[120521,120521],"mapped",[952]],[[120522,120522],"mapped",[953]],[[120523,120523],"mapped",[954]],[[120524,120524],"mapped",[955]],[[120525,120525],"mapped",[956]],[[120526,120526],"mapped",[957]],[[120527,120527],"mapped",[958]],[[120528,120528],"mapped",[959]],[[120529,120529],"mapped",[960]],[[120530,120530],"mapped",[961]],[[120531,120532],"mapped",[963]],[[120533,120533],"mapped",[964]],[[120534,120534],"mapped",[965]],[[120535,120535],"mapped",[966]],[[120536,120536],"mapped",[967]],[[120537,120537],"mapped",[968]],[[120538,120538],"mapped",[969]],[[120539,120539],"mapped",[8706]],[[120540,120540],"mapped",[949]],[[120541,120541],"mapped",[952]],[[120542,120542],"mapped",[954]],[[120543,120543],"mapped",[966]],[[120544,120544],"mapped",[961]],[[120545,120545],"mapped",[960]],[[120546,120546],"mapped",[945]],[[120547,120547],"mapped",[946]],[[120548,120548],"mapped",[947]],[[120549,120549],"mapped",[948]],[[120550,120550],"mapped",[949]],[[120551,120551],"mapped",[950]],[[120552,120552],"mapped",[951]],[[120553,120553],"mapped",[952]],[[120554,120554],"mapped",[953]],[[120555,120555],"mapped",[954]],[[120556,120556],"mapped",[955]],[[120557,120557],"mapped",[956]],[[120558,120558],"mapped",[957]],[[120559,120559],"mapped",[958]],[[120560,120560],"mapped",[959]],[[120561,120561],"mapped",[960]],[[120562,120562],"mapped",[961]],[[120563,120563],"mapped",[952]],[[120564,120564],"mapped",[963]],[[120565,120565],"mapped",[964]],[[120566,120566],"mapped",[965]],[[120567,120567],"mapped",[966]],[[120568,120568],"mapped",[967]],[[120569,120569],"mapped",[968]],[[120570,120570],"mapped",[969]],[[120571,120571],"mapped",[8711]],[[120572,120572],"mapped",[945]],[[120573,120573],"mapped",[946]],[[120574,120574],"mapped",[947]],[[120575,120575],"mapped",[948]],[[120576,120576],"mapped",[949]],[[120577,120577],"mapped",[950]],[[120578,120578],"mapped",[951]],[[120579,120579],"mapped",[952]],[[120580,120580],"mapped",[953]],[[120581,120581],"mapped",[954]],[[120582,120582],"mapped",[955]],[[120583,120583],"mapped",[956]],[[120584,120584],"mapped",[957]],[[120585,120585],"mapped",[958]],[[120586,120586],"mapped",[959]],[[120587,120587],"mapped",[960]],[[120588,120588],"mapped",[961]],[[120589,120590],"mapped",[963]],[[120591,120591],"mapped",[964]],[[120592,120592],"mapped",[965]],[[120593,120593],"mapped",[966]],[[120594,120594],"mapped",[967]],[[120595,120595],"mapped",[968]],[[120596,120596],"mapped",[969]],[[120597,120597],"mapped",[8706]],[[120598,120598],"mapped",[949]],[[120599,120599],"mapped",[952]],[[120600,120600],"mapped",[954]],[[120601,120601],"mapped",[966]],[[120602,120602],"mapped",[961]],[[120603,120603],"mapped",[960]],[[120604,120604],"mapped",[945]],[[120605,120605],"mapped",[946]],[[120606,120606],"mapped",[947]],[[120607,120607],"mapped",[948]],[[120608,120608],"mapped",[949]],[[120609,120609],"mapped",[950]],[[120610,120610],"mapped",[951]],[[120611,120611],"mapped",[952]],[[120612,120612],"mapped",[953]],[[120613,120613],"mapped",[954]],[[120614,120614],"mapped",[955]],[[120615,120615],"mapped",[956]],[[120616,120616],"mapped",[957]],[[120617,120617],"mapped",[958]],[[120618,120618],"mapped",[959]],[[120619,120619],"mapped",[960]],[[120620,120620],"mapped",[961]],[[120621,120621],"mapped",[952]],[[120622,120622],"mapped",[963]],[[120623,120623],"mapped",[964]],[[120624,120624],"mapped",[965]],[[120625,120625],"mapped",[966]],[[120626,120626],"mapped",[967]],[[120627,120627],"mapped",[968]],[[120628,120628],"mapped",[969]],[[120629,120629],"mapped",[8711]],[[120630,120630],"mapped",[945]],[[120631,120631],"mapped",[946]],[[120632,120632],"mapped",[947]],[[120633,120633],"mapped",[948]],[[120634,120634],"mapped",[949]],[[120635,120635],"mapped",[950]],[[120636,120636],"mapped",[951]],[[120637,120637],"mapped",[952]],[[120638,120638],"mapped",[953]],[[120639,120639],"mapped",[954]],[[120640,120640],"mapped",[955]],[[120641,120641],"mapped",[956]],[[120642,120642],"mapped",[957]],[[120643,120643],"mapped",[958]],[[120644,120644],"mapped",[959]],[[120645,120645],"mapped",[960]],[[120646,120646],"mapped",[961]],[[120647,120648],"mapped",[963]],[[120649,120649],"mapped",[964]],[[120650,120650],"mapped",[965]],[[120651,120651],"mapped",[966]],[[120652,120652],"mapped",[967]],[[120653,120653],"mapped",[968]],[[120654,120654],"mapped",[969]],[[120655,120655],"mapped",[8706]],[[120656,120656],"mapped",[949]],[[120657,120657],"mapped",[952]],[[120658,120658],"mapped",[954]],[[120659,120659],"mapped",[966]],[[120660,120660],"mapped",[961]],[[120661,120661],"mapped",[960]],[[120662,120662],"mapped",[945]],[[120663,120663],"mapped",[946]],[[120664,120664],"mapped",[947]],[[120665,120665],"mapped",[948]],[[120666,120666],"mapped",[949]],[[120667,120667],"mapped",[950]],[[120668,120668],"mapped",[951]],[[120669,120669],"mapped",[952]],[[120670,120670],"mapped",[953]],[[120671,120671],"mapped",[954]],[[120672,120672],"mapped",[955]],[[120673,120673],"mapped",[956]],[[120674,120674],"mapped",[957]],[[120675,120675],"mapped",[958]],[[120676,120676],"mapped",[959]],[[120677,120677],"mapped",[960]],[[120678,120678],"mapped",[961]],[[120679,120679],"mapped",[952]],[[120680,120680],"mapped",[963]],[[120681,120681],"mapped",[964]],[[120682,120682],"mapped",[965]],[[120683,120683],"mapped",[966]],[[120684,120684],"mapped",[967]],[[120685,120685],"mapped",[968]],[[120686,120686],"mapped",[969]],[[120687,120687],"mapped",[8711]],[[120688,120688],"mapped",[945]],[[120689,120689],"mapped",[946]],[[120690,120690],"mapped",[947]],[[120691,120691],"mapped",[948]],[[120692,120692],"mapped",[949]],[[120693,120693],"mapped",[950]],[[120694,120694],"mapped",[951]],[[120695,120695],"mapped",[952]],[[120696,120696],"mapped",[953]],[[120697,120697],"mapped",[954]],[[120698,120698],"mapped",[955]],[[120699,120699],"mapped",[956]],[[120700,120700],"mapped",[957]],[[120701,120701],"mapped",[958]],[[120702,120702],"mapped",[959]],[[120703,120703],"mapped",[960]],[[120704,120704],"mapped",[961]],[[120705,120706],"mapped",[963]],[[120707,120707],"mapped",[964]],[[120708,120708],"mapped",[965]],[[120709,120709],"mapped",[966]],[[120710,120710],"mapped",[967]],[[120711,120711],"mapped",[968]],[[120712,120712],"mapped",[969]],[[120713,120713],"mapped",[8706]],[[120714,120714],"mapped",[949]],[[120715,120715],"mapped",[952]],[[120716,120716],"mapped",[954]],[[120717,120717],"mapped",[966]],[[120718,120718],"mapped",[961]],[[120719,120719],"mapped",[960]],[[120720,120720],"mapped",[945]],[[120721,120721],"mapped",[946]],[[120722,120722],"mapped",[947]],[[120723,120723],"mapped",[948]],[[120724,120724],"mapped",[949]],[[120725,120725],"mapped",[950]],[[120726,120726],"mapped",[951]],[[120727,120727],"mapped",[952]],[[120728,120728],"mapped",[953]],[[120729,120729],"mapped",[954]],[[120730,120730],"mapped",[955]],[[120731,120731],"mapped",[956]],[[120732,120732],"mapped",[957]],[[120733,120733],"mapped",[958]],[[120734,120734],"mapped",[959]],[[120735,120735],"mapped",[960]],[[120736,120736],"mapped",[961]],[[120737,120737],"mapped",[952]],[[120738,120738],"mapped",[963]],[[120739,120739],"mapped",[964]],[[120740,120740],"mapped",[965]],[[120741,120741],"mapped",[966]],[[120742,120742],"mapped",[967]],[[120743,120743],"mapped",[968]],[[120744,120744],"mapped",[969]],[[120745,120745],"mapped",[8711]],[[120746,120746],"mapped",[945]],[[120747,120747],"mapped",[946]],[[120748,120748],"mapped",[947]],[[120749,120749],"mapped",[948]],[[120750,120750],"mapped",[949]],[[120751,120751],"mapped",[950]],[[120752,120752],"mapped",[951]],[[120753,120753],"mapped",[952]],[[120754,120754],"mapped",[953]],[[120755,120755],"mapped",[954]],[[120756,120756],"mapped",[955]],[[120757,120757],"mapped",[956]],[[120758,120758],"mapped",[957]],[[120759,120759],"mapped",[958]],[[120760,120760],"mapped",[959]],[[120761,120761],"mapped",[960]],[[120762,120762],"mapped",[961]],[[120763,120764],"mapped",[963]],[[120765,120765],"mapped",[964]],[[120766,120766],"mapped",[965]],[[120767,120767],"mapped",[966]],[[120768,120768],"mapped",[967]],[[120769,120769],"mapped",[968]],[[120770,120770],"mapped",[969]],[[120771,120771],"mapped",[8706]],[[120772,120772],"mapped",[949]],[[120773,120773],"mapped",[952]],[[120774,120774],"mapped",[954]],[[120775,120775],"mapped",[966]],[[120776,120776],"mapped",[961]],[[120777,120777],"mapped",[960]],[[120778,120779],"mapped",[989]],[[120780,120781],"disallowed"],[[120782,120782],"mapped",[48]],[[120783,120783],"mapped",[49]],[[120784,120784],"mapped",[50]],[[120785,120785],"mapped",[51]],[[120786,120786],"mapped",[52]],[[120787,120787],"mapped",[53]],[[120788,120788],"mapped",[54]],[[120789,120789],"mapped",[55]],[[120790,120790],"mapped",[56]],[[120791,120791],"mapped",[57]],[[120792,120792],"mapped",[48]],[[120793,120793],"mapped",[49]],[[120794,120794],"mapped",[50]],[[120795,120795],"mapped",[51]],[[120796,120796],"mapped",[52]],[[120797,120797],"mapped",[53]],[[120798,120798],"mapped",[54]],[[120799,120799],"mapped",[55]],[[120800,120800],"mapped",[56]],[[120801,120801],"mapped",[57]],[[120802,120802],"mapped",[48]],[[120803,120803],"mapped",[49]],[[120804,120804],"mapped",[50]],[[120805,120805],"mapped",[51]],[[120806,120806],"mapped",[52]],[[120807,120807],"mapped",[53]],[[120808,120808],"mapped",[54]],[[120809,120809],"mapped",[55]],[[120810,120810],"mapped",[56]],[[120811,120811],"mapped",[57]],[[120812,120812],"mapped",[48]],[[120813,120813],"mapped",[49]],[[120814,120814],"mapped",[50]],[[120815,120815],"mapped",[51]],[[120816,120816],"mapped",[52]],[[120817,120817],"mapped",[53]],[[120818,120818],"mapped",[54]],[[120819,120819],"mapped",[55]],[[120820,120820],"mapped",[56]],[[120821,120821],"mapped",[57]],[[120822,120822],"mapped",[48]],[[120823,120823],"mapped",[49]],[[120824,120824],"mapped",[50]],[[120825,120825],"mapped",[51]],[[120826,120826],"mapped",[52]],[[120827,120827],"mapped",[53]],[[120828,120828],"mapped",[54]],[[120829,120829],"mapped",[55]],[[120830,120830],"mapped",[56]],[[120831,120831],"mapped",[57]],[[120832,121343],"valid",[],"NV8"],[[121344,121398],"valid"],[[121399,121402],"valid",[],"NV8"],[[121403,121452],"valid"],[[121453,121460],"valid",[],"NV8"],[[121461,121461],"valid"],[[121462,121475],"valid",[],"NV8"],[[121476,121476],"valid"],[[121477,121483],"valid",[],"NV8"],[[121484,121498],"disallowed"],[[121499,121503],"valid"],[[121504,121504],"disallowed"],[[121505,121519],"valid"],[[121520,124927],"disallowed"],[[124928,125124],"valid"],[[125125,125126],"disallowed"],[[125127,125135],"valid",[],"NV8"],[[125136,125142],"valid"],[[125143,126463],"disallowed"],[[126464,126464],"mapped",[1575]],[[126465,126465],"mapped",[1576]],[[126466,126466],"mapped",[1580]],[[126467,126467],"mapped",[1583]],[[126468,126468],"disallowed"],[[126469,126469],"mapped",[1608]],[[126470,126470],"mapped",[1586]],[[126471,126471],"mapped",[1581]],[[126472,126472],"mapped",[1591]],[[126473,126473],"mapped",[1610]],[[126474,126474],"mapped",[1603]],[[126475,126475],"mapped",[1604]],[[126476,126476],"mapped",[1605]],[[126477,126477],"mapped",[1606]],[[126478,126478],"mapped",[1587]],[[126479,126479],"mapped",[1593]],[[126480,126480],"mapped",[1601]],[[126481,126481],"mapped",[1589]],[[126482,126482],"mapped",[1602]],[[126483,126483],"mapped",[1585]],[[126484,126484],"mapped",[1588]],[[126485,126485],"mapped",[1578]],[[126486,126486],"mapped",[1579]],[[126487,126487],"mapped",[1582]],[[126488,126488],"mapped",[1584]],[[126489,126489],"mapped",[1590]],[[126490,126490],"mapped",[1592]],[[126491,126491],"mapped",[1594]],[[126492,126492],"mapped",[1646]],[[126493,126493],"mapped",[1722]],[[126494,126494],"mapped",[1697]],[[126495,126495],"mapped",[1647]],[[126496,126496],"disallowed"],[[126497,126497],"mapped",[1576]],[[126498,126498],"mapped",[1580]],[[126499,126499],"disallowed"],[[126500,126500],"mapped",[1607]],[[126501,126502],"disallowed"],[[126503,126503],"mapped",[1581]],[[126504,126504],"disallowed"],[[126505,126505],"mapped",[1610]],[[126506,126506],"mapped",[1603]],[[126507,126507],"mapped",[1604]],[[126508,126508],"mapped",[1605]],[[126509,126509],"mapped",[1606]],[[126510,126510],"mapped",[1587]],[[126511,126511],"mapped",[1593]],[[126512,126512],"mapped",[1601]],[[126513,126513],"mapped",[1589]],[[126514,126514],"mapped",[1602]],[[126515,126515],"disallowed"],[[126516,126516],"mapped",[1588]],[[126517,126517],"mapped",[1578]],[[126518,126518],"mapped",[1579]],[[126519,126519],"mapped",[1582]],[[126520,126520],"disallowed"],[[126521,126521],"mapped",[1590]],[[126522,126522],"disallowed"],[[126523,126523],"mapped",[1594]],[[126524,126529],"disallowed"],[[126530,126530],"mapped",[1580]],[[126531,126534],"disallowed"],[[126535,126535],"mapped",[1581]],[[126536,126536],"disallowed"],[[126537,126537],"mapped",[1610]],[[126538,126538],"disallowed"],[[126539,126539],"mapped",[1604]],[[126540,126540],"disallowed"],[[126541,126541],"mapped",[1606]],[[126542,126542],"mapped",[1587]],[[126543,126543],"mapped",[1593]],[[126544,126544],"disallowed"],[[126545,126545],"mapped",[1589]],[[126546,126546],"mapped",[1602]],[[126547,126547],"disallowed"],[[126548,126548],"mapped",[1588]],[[126549,126550],"disallowed"],[[126551,126551],"mapped",[1582]],[[126552,126552],"disallowed"],[[126553,126553],"mapped",[1590]],[[126554,126554],"disallowed"],[[126555,126555],"mapped",[1594]],[[126556,126556],"disallowed"],[[126557,126557],"mapped",[1722]],[[126558,126558],"disallowed"],[[126559,126559],"mapped",[1647]],[[126560,126560],"disallowed"],[[126561,126561],"mapped",[1576]],[[126562,126562],"mapped",[1580]],[[126563,126563],"disallowed"],[[126564,126564],"mapped",[1607]],[[126565,126566],"disallowed"],[[126567,126567],"mapped",[1581]],[[126568,126568],"mapped",[1591]],[[126569,126569],"mapped",[1610]],[[126570,126570],"mapped",[1603]],[[126571,126571],"disallowed"],[[126572,126572],"mapped",[1605]],[[126573,126573],"mapped",[1606]],[[126574,126574],"mapped",[1587]],[[126575,126575],"mapped",[1593]],[[126576,126576],"mapped",[1601]],[[126577,126577],"mapped",[1589]],[[126578,126578],"mapped",[1602]],[[126579,126579],"disallowed"],[[126580,126580],"mapped",[1588]],[[126581,126581],"mapped",[1578]],[[126582,126582],"mapped",[1579]],[[126583,126583],"mapped",[1582]],[[126584,126584],"disallowed"],[[126585,126585],"mapped",[1590]],[[126586,126586],"mapped",[1592]],[[126587,126587],"mapped",[1594]],[[126588,126588],"mapped",[1646]],[[126589,126589],"disallowed"],[[126590,126590],"mapped",[1697]],[[126591,126591],"disallowed"],[[126592,126592],"mapped",[1575]],[[126593,126593],"mapped",[1576]],[[126594,126594],"mapped",[1580]],[[126595,126595],"mapped",[1583]],[[126596,126596],"mapped",[1607]],[[126597,126597],"mapped",[1608]],[[126598,126598],"mapped",[1586]],[[126599,126599],"mapped",[1581]],[[126600,126600],"mapped",[1591]],[[126601,126601],"mapped",[1610]],[[126602,126602],"disallowed"],[[126603,126603],"mapped",[1604]],[[126604,126604],"mapped",[1605]],[[126605,126605],"mapped",[1606]],[[126606,126606],"mapped",[1587]],[[126607,126607],"mapped",[1593]],[[126608,126608],"mapped",[1601]],[[126609,126609],"mapped",[1589]],[[126610,126610],"mapped",[1602]],[[126611,126611],"mapped",[1585]],[[126612,126612],"mapped",[1588]],[[126613,126613],"mapped",[1578]],[[126614,126614],"mapped",[1579]],[[126615,126615],"mapped",[1582]],[[126616,126616],"mapped",[1584]],[[126617,126617],"mapped",[1590]],[[126618,126618],"mapped",[1592]],[[126619,126619],"mapped",[1594]],[[126620,126624],"disallowed"],[[126625,126625],"mapped",[1576]],[[126626,126626],"mapped",[1580]],[[126627,126627],"mapped",[1583]],[[126628,126628],"disallowed"],[[126629,126629],"mapped",[1608]],[[126630,126630],"mapped",[1586]],[[126631,126631],"mapped",[1581]],[[126632,126632],"mapped",[1591]],[[126633,126633],"mapped",[1610]],[[126634,126634],"disallowed"],[[126635,126635],"mapped",[1604]],[[126636,126636],"mapped",[1605]],[[126637,126637],"mapped",[1606]],[[126638,126638],"mapped",[1587]],[[126639,126639],"mapped",[1593]],[[126640,126640],"mapped",[1601]],[[126641,126641],"mapped",[1589]],[[126642,126642],"mapped",[1602]],[[126643,126643],"mapped",[1585]],[[126644,126644],"mapped",[1588]],[[126645,126645],"mapped",[1578]],[[126646,126646],"mapped",[1579]],[[126647,126647],"mapped",[1582]],[[126648,126648],"mapped",[1584]],[[126649,126649],"mapped",[1590]],[[126650,126650],"mapped",[1592]],[[126651,126651],"mapped",[1594]],[[126652,126703],"disallowed"],[[126704,126705],"valid",[],"NV8"],[[126706,126975],"disallowed"],[[126976,127019],"valid",[],"NV8"],[[127020,127023],"disallowed"],[[127024,127123],"valid",[],"NV8"],[[127124,127135],"disallowed"],[[127136,127150],"valid",[],"NV8"],[[127151,127152],"disallowed"],[[127153,127166],"valid",[],"NV8"],[[127167,127167],"valid",[],"NV8"],[[127168,127168],"disallowed"],[[127169,127183],"valid",[],"NV8"],[[127184,127184],"disallowed"],[[127185,127199],"valid",[],"NV8"],[[127200,127221],"valid",[],"NV8"],[[127222,127231],"disallowed"],[[127232,127232],"disallowed"],[[127233,127233],"disallowed_STD3_mapped",[48,44]],[[127234,127234],"disallowed_STD3_mapped",[49,44]],[[127235,127235],"disallowed_STD3_mapped",[50,44]],[[127236,127236],"disallowed_STD3_mapped",[51,44]],[[127237,127237],"disallowed_STD3_mapped",[52,44]],[[127238,127238],"disallowed_STD3_mapped",[53,44]],[[127239,127239],"disallowed_STD3_mapped",[54,44]],[[127240,127240],"disallowed_STD3_mapped",[55,44]],[[127241,127241],"disallowed_STD3_mapped",[56,44]],[[127242,127242],"disallowed_STD3_mapped",[57,44]],[[127243,127244],"valid",[],"NV8"],[[127245,127247],"disallowed"],[[127248,127248],"disallowed_STD3_mapped",[40,97,41]],[[127249,127249],"disallowed_STD3_mapped",[40,98,41]],[[127250,127250],"disallowed_STD3_mapped",[40,99,41]],[[127251,127251],"disallowed_STD3_mapped",[40,100,41]],[[127252,127252],"disallowed_STD3_mapped",[40,101,41]],[[127253,127253],"disallowed_STD3_mapped",[40,102,41]],[[127254,127254],"disallowed_STD3_mapped",[40,103,41]],[[127255,127255],"disallowed_STD3_mapped",[40,104,41]],[[127256,127256],"disallowed_STD3_mapped",[40,105,41]],[[127257,127257],"disallowed_STD3_mapped",[40,106,41]],[[127258,127258],"disallowed_STD3_mapped",[40,107,41]],[[127259,127259],"disallowed_STD3_mapped",[40,108,41]],[[127260,127260],"disallowed_STD3_mapped",[40,109,41]],[[127261,127261],"disallowed_STD3_mapped",[40,110,41]],[[127262,127262],"disallowed_STD3_mapped",[40,111,41]],[[127263,127263],"disallowed_STD3_mapped",[40,112,41]],[[127264,127264],"disallowed_STD3_mapped",[40,113,41]],[[127265,127265],"disallowed_STD3_mapped",[40,114,41]],[[127266,127266],"disallowed_STD3_mapped",[40,115,41]],[[127267,127267],"disallowed_STD3_mapped",[40,116,41]],[[127268,127268],"disallowed_STD3_mapped",[40,117,41]],[[127269,127269],"disallowed_STD3_mapped",[40,118,41]],[[127270,127270],"disallowed_STD3_mapped",[40,119,41]],[[127271,127271],"disallowed_STD3_mapped",[40,120,41]],[[127272,127272],"disallowed_STD3_mapped",[40,121,41]],[[127273,127273],"disallowed_STD3_mapped",[40,122,41]],[[127274,127274],"mapped",[12308,115,12309]],[[127275,127275],"mapped",[99]],[[127276,127276],"mapped",[114]],[[127277,127277],"mapped",[99,100]],[[127278,127278],"mapped",[119,122]],[[127279,127279],"disallowed"],[[127280,127280],"mapped",[97]],[[127281,127281],"mapped",[98]],[[127282,127282],"mapped",[99]],[[127283,127283],"mapped",[100]],[[127284,127284],"mapped",[101]],[[127285,127285],"mapped",[102]],[[127286,127286],"mapped",[103]],[[127287,127287],"mapped",[104]],[[127288,127288],"mapped",[105]],[[127289,127289],"mapped",[106]],[[127290,127290],"mapped",[107]],[[127291,127291],"mapped",[108]],[[127292,127292],"mapped",[109]],[[127293,127293],"mapped",[110]],[[127294,127294],"mapped",[111]],[[127295,127295],"mapped",[112]],[[127296,127296],"mapped",[113]],[[127297,127297],"mapped",[114]],[[127298,127298],"mapped",[115]],[[127299,127299],"mapped",[116]],[[127300,127300],"mapped",[117]],[[127301,127301],"mapped",[118]],[[127302,127302],"mapped",[119]],[[127303,127303],"mapped",[120]],[[127304,127304],"mapped",[121]],[[127305,127305],"mapped",[122]],[[127306,127306],"mapped",[104,118]],[[127307,127307],"mapped",[109,118]],[[127308,127308],"mapped",[115,100]],[[127309,127309],"mapped",[115,115]],[[127310,127310],"mapped",[112,112,118]],[[127311,127311],"mapped",[119,99]],[[127312,127318],"valid",[],"NV8"],[[127319,127319],"valid",[],"NV8"],[[127320,127326],"valid",[],"NV8"],[[127327,127327],"valid",[],"NV8"],[[127328,127337],"valid",[],"NV8"],[[127338,127338],"mapped",[109,99]],[[127339,127339],"mapped",[109,100]],[[127340,127343],"disallowed"],[[127344,127352],"valid",[],"NV8"],[[127353,127353],"valid",[],"NV8"],[[127354,127354],"valid",[],"NV8"],[[127355,127356],"valid",[],"NV8"],[[127357,127358],"valid",[],"NV8"],[[127359,127359],"valid",[],"NV8"],[[127360,127369],"valid",[],"NV8"],[[127370,127373],"valid",[],"NV8"],[[127374,127375],"valid",[],"NV8"],[[127376,127376],"mapped",[100,106]],[[127377,127386],"valid",[],"NV8"],[[127387,127461],"disallowed"],[[127462,127487],"valid",[],"NV8"],[[127488,127488],"mapped",[12411,12363]],[[127489,127489],"mapped",[12467,12467]],[[127490,127490],"mapped",[12469]],[[127491,127503],"disallowed"],[[127504,127504],"mapped",[25163]],[[127505,127505],"mapped",[23383]],[[127506,127506],"mapped",[21452]],[[127507,127507],"mapped",[12487]],[[127508,127508],"mapped",[20108]],[[127509,127509],"mapped",[22810]],[[127510,127510],"mapped",[35299]],[[127511,127511],"mapped",[22825]],[[127512,127512],"mapped",[20132]],[[127513,127513],"mapped",[26144]],[[127514,127514],"mapped",[28961]],[[127515,127515],"mapped",[26009]],[[127516,127516],"mapped",[21069]],[[127517,127517],"mapped",[24460]],[[127518,127518],"mapped",[20877]],[[127519,127519],"mapped",[26032]],[[127520,127520],"mapped",[21021]],[[127521,127521],"mapped",[32066]],[[127522,127522],"mapped",[29983]],[[127523,127523],"mapped",[36009]],[[127524,127524],"mapped",[22768]],[[127525,127525],"mapped",[21561]],[[127526,127526],"mapped",[28436]],[[127527,127527],"mapped",[25237]],[[127528,127528],"mapped",[25429]],[[127529,127529],"mapped",[19968]],[[127530,127530],"mapped",[19977]],[[127531,127531],"mapped",[36938]],[[127532,127532],"mapped",[24038]],[[127533,127533],"mapped",[20013]],[[127534,127534],"mapped",[21491]],[[127535,127535],"mapped",[25351]],[[127536,127536],"mapped",[36208]],[[127537,127537],"mapped",[25171]],[[127538,127538],"mapped",[31105]],[[127539,127539],"mapped",[31354]],[[127540,127540],"mapped",[21512]],[[127541,127541],"mapped",[28288]],[[127542,127542],"mapped",[26377]],[[127543,127543],"mapped",[26376]],[[127544,127544],"mapped",[30003]],[[127545,127545],"mapped",[21106]],[[127546,127546],"mapped",[21942]],[[127547,127551],"disallowed"],[[127552,127552],"mapped",[12308,26412,12309]],[[127553,127553],"mapped",[12308,19977,12309]],[[127554,127554],"mapped",[12308,20108,12309]],[[127555,127555],"mapped",[12308,23433,12309]],[[127556,127556],"mapped",[12308,28857,12309]],[[127557,127557],"mapped",[12308,25171,12309]],[[127558,127558],"mapped",[12308,30423,12309]],[[127559,127559],"mapped",[12308,21213,12309]],[[127560,127560],"mapped",[12308,25943,12309]],[[127561,127567],"disallowed"],[[127568,127568],"mapped",[24471]],[[127569,127569],"mapped",[21487]],[[127570,127743],"disallowed"],[[127744,127776],"valid",[],"NV8"],[[127777,127788],"valid",[],"NV8"],[[127789,127791],"valid",[],"NV8"],[[127792,127797],"valid",[],"NV8"],[[127798,127798],"valid",[],"NV8"],[[127799,127868],"valid",[],"NV8"],[[127869,127869],"valid",[],"NV8"],[[127870,127871],"valid",[],"NV8"],[[127872,127891],"valid",[],"NV8"],[[127892,127903],"valid",[],"NV8"],[[127904,127940],"valid",[],"NV8"],[[127941,127941],"valid",[],"NV8"],[[127942,127946],"valid",[],"NV8"],[[127947,127950],"valid",[],"NV8"],[[127951,127955],"valid",[],"NV8"],[[127956,127967],"valid",[],"NV8"],[[127968,127984],"valid",[],"NV8"],[[127985,127991],"valid",[],"NV8"],[[127992,127999],"valid",[],"NV8"],[[128000,128062],"valid",[],"NV8"],[[128063,128063],"valid",[],"NV8"],[[128064,128064],"valid",[],"NV8"],[[128065,128065],"valid",[],"NV8"],[[128066,128247],"valid",[],"NV8"],[[128248,128248],"valid",[],"NV8"],[[128249,128252],"valid",[],"NV8"],[[128253,128254],"valid",[],"NV8"],[[128255,128255],"valid",[],"NV8"],[[128256,128317],"valid",[],"NV8"],[[128318,128319],"valid",[],"NV8"],[[128320,128323],"valid",[],"NV8"],[[128324,128330],"valid",[],"NV8"],[[128331,128335],"valid",[],"NV8"],[[128336,128359],"valid",[],"NV8"],[[128360,128377],"valid",[],"NV8"],[[128378,128378],"disallowed"],[[128379,128419],"valid",[],"NV8"],[[128420,128420],"disallowed"],[[128421,128506],"valid",[],"NV8"],[[128507,128511],"valid",[],"NV8"],[[128512,128512],"valid",[],"NV8"],[[128513,128528],"valid",[],"NV8"],[[128529,128529],"valid",[],"NV8"],[[128530,128532],"valid",[],"NV8"],[[128533,128533],"valid",[],"NV8"],[[128534,128534],"valid",[],"NV8"],[[128535,128535],"valid",[],"NV8"],[[128536,128536],"valid",[],"NV8"],[[128537,128537],"valid",[],"NV8"],[[128538,128538],"valid",[],"NV8"],[[128539,128539],"valid",[],"NV8"],[[128540,128542],"valid",[],"NV8"],[[128543,128543],"valid",[],"NV8"],[[128544,128549],"valid",[],"NV8"],[[128550,128551],"valid",[],"NV8"],[[128552,128555],"valid",[],"NV8"],[[128556,128556],"valid",[],"NV8"],[[128557,128557],"valid",[],"NV8"],[[128558,128559],"valid",[],"NV8"],[[128560,128563],"valid",[],"NV8"],[[128564,128564],"valid",[],"NV8"],[[128565,128576],"valid",[],"NV8"],[[128577,128578],"valid",[],"NV8"],[[128579,128580],"valid",[],"NV8"],[[128581,128591],"valid",[],"NV8"],[[128592,128639],"valid",[],"NV8"],[[128640,128709],"valid",[],"NV8"],[[128710,128719],"valid",[],"NV8"],[[128720,128720],"valid",[],"NV8"],[[128721,128735],"disallowed"],[[128736,128748],"valid",[],"NV8"],[[128749,128751],"disallowed"],[[128752,128755],"valid",[],"NV8"],[[128756,128767],"disallowed"],[[128768,128883],"valid",[],"NV8"],[[128884,128895],"disallowed"],[[128896,128980],"valid",[],"NV8"],[[128981,129023],"disallowed"],[[129024,129035],"valid",[],"NV8"],[[129036,129039],"disallowed"],[[129040,129095],"valid",[],"NV8"],[[129096,129103],"disallowed"],[[129104,129113],"valid",[],"NV8"],[[129114,129119],"disallowed"],[[129120,129159],"valid",[],"NV8"],[[129160,129167],"disallowed"],[[129168,129197],"valid",[],"NV8"],[[129198,129295],"disallowed"],[[129296,129304],"valid",[],"NV8"],[[129305,129407],"disallowed"],[[129408,129412],"valid",[],"NV8"],[[129413,129471],"disallowed"],[[129472,129472],"valid",[],"NV8"],[[129473,131069],"disallowed"],[[131070,131071],"disallowed"],[[131072,173782],"valid"],[[173783,173823],"disallowed"],[[173824,177972],"valid"],[[177973,177983],"disallowed"],[[177984,178205],"valid"],[[178206,178207],"disallowed"],[[178208,183969],"valid"],[[183970,194559],"disallowed"],[[194560,194560],"mapped",[20029]],[[194561,194561],"mapped",[20024]],[[194562,194562],"mapped",[20033]],[[194563,194563],"mapped",[131362]],[[194564,194564],"mapped",[20320]],[[194565,194565],"mapped",[20398]],[[194566,194566],"mapped",[20411]],[[194567,194567],"mapped",[20482]],[[194568,194568],"mapped",[20602]],[[194569,194569],"mapped",[20633]],[[194570,194570],"mapped",[20711]],[[194571,194571],"mapped",[20687]],[[194572,194572],"mapped",[13470]],[[194573,194573],"mapped",[132666]],[[194574,194574],"mapped",[20813]],[[194575,194575],"mapped",[20820]],[[194576,194576],"mapped",[20836]],[[194577,194577],"mapped",[20855]],[[194578,194578],"mapped",[132380]],[[194579,194579],"mapped",[13497]],[[194580,194580],"mapped",[20839]],[[194581,194581],"mapped",[20877]],[[194582,194582],"mapped",[132427]],[[194583,194583],"mapped",[20887]],[[194584,194584],"mapped",[20900]],[[194585,194585],"mapped",[20172]],[[194586,194586],"mapped",[20908]],[[194587,194587],"mapped",[20917]],[[194588,194588],"mapped",[168415]],[[194589,194589],"mapped",[20981]],[[194590,194590],"mapped",[20995]],[[194591,194591],"mapped",[13535]],[[194592,194592],"mapped",[21051]],[[194593,194593],"mapped",[21062]],[[194594,194594],"mapped",[21106]],[[194595,194595],"mapped",[21111]],[[194596,194596],"mapped",[13589]],[[194597,194597],"mapped",[21191]],[[194598,194598],"mapped",[21193]],[[194599,194599],"mapped",[21220]],[[194600,194600],"mapped",[21242]],[[194601,194601],"mapped",[21253]],[[194602,194602],"mapped",[21254]],[[194603,194603],"mapped",[21271]],[[194604,194604],"mapped",[21321]],[[194605,194605],"mapped",[21329]],[[194606,194606],"mapped",[21338]],[[194607,194607],"mapped",[21363]],[[194608,194608],"mapped",[21373]],[[194609,194611],"mapped",[21375]],[[194612,194612],"mapped",[133676]],[[194613,194613],"mapped",[28784]],[[194614,194614],"mapped",[21450]],[[194615,194615],"mapped",[21471]],[[194616,194616],"mapped",[133987]],[[194617,194617],"mapped",[21483]],[[194618,194618],"mapped",[21489]],[[194619,194619],"mapped",[21510]],[[194620,194620],"mapped",[21662]],[[194621,194621],"mapped",[21560]],[[194622,194622],"mapped",[21576]],[[194623,194623],"mapped",[21608]],[[194624,194624],"mapped",[21666]],[[194625,194625],"mapped",[21750]],[[194626,194626],"mapped",[21776]],[[194627,194627],"mapped",[21843]],[[194628,194628],"mapped",[21859]],[[194629,194630],"mapped",[21892]],[[194631,194631],"mapped",[21913]],[[194632,194632],"mapped",[21931]],[[194633,194633],"mapped",[21939]],[[194634,194634],"mapped",[21954]],[[194635,194635],"mapped",[22294]],[[194636,194636],"mapped",[22022]],[[194637,194637],"mapped",[22295]],[[194638,194638],"mapped",[22097]],[[194639,194639],"mapped",[22132]],[[194640,194640],"mapped",[20999]],[[194641,194641],"mapped",[22766]],[[194642,194642],"mapped",[22478]],[[194643,194643],"mapped",[22516]],[[194644,194644],"mapped",[22541]],[[194645,194645],"mapped",[22411]],[[194646,194646],"mapped",[22578]],[[194647,194647],"mapped",[22577]],[[194648,194648],"mapped",[22700]],[[194649,194649],"mapped",[136420]],[[194650,194650],"mapped",[22770]],[[194651,194651],"mapped",[22775]],[[194652,194652],"mapped",[22790]],[[194653,194653],"mapped",[22810]],[[194654,194654],"mapped",[22818]],[[194655,194655],"mapped",[22882]],[[194656,194656],"mapped",[136872]],[[194657,194657],"mapped",[136938]],[[194658,194658],"mapped",[23020]],[[194659,194659],"mapped",[23067]],[[194660,194660],"mapped",[23079]],[[194661,194661],"mapped",[23000]],[[194662,194662],"mapped",[23142]],[[194663,194663],"mapped",[14062]],[[194664,194664],"disallowed"],[[194665,194665],"mapped",[23304]],[[194666,194667],"mapped",[23358]],[[194668,194668],"mapped",[137672]],[[194669,194669],"mapped",[23491]],[[194670,194670],"mapped",[23512]],[[194671,194671],"mapped",[23527]],[[194672,194672],"mapped",[23539]],[[194673,194673],"mapped",[138008]],[[194674,194674],"mapped",[23551]],[[194675,194675],"mapped",[23558]],[[194676,194676],"disallowed"],[[194677,194677],"mapped",[23586]],[[194678,194678],"mapped",[14209]],[[194679,194679],"mapped",[23648]],[[194680,194680],"mapped",[23662]],[[194681,194681],"mapped",[23744]],[[194682,194682],"mapped",[23693]],[[194683,194683],"mapped",[138724]],[[194684,194684],"mapped",[23875]],[[194685,194685],"mapped",[138726]],[[194686,194686],"mapped",[23918]],[[194687,194687],"mapped",[23915]],[[194688,194688],"mapped",[23932]],[[194689,194689],"mapped",[24033]],[[194690,194690],"mapped",[24034]],[[194691,194691],"mapped",[14383]],[[194692,194692],"mapped",[24061]],[[194693,194693],"mapped",[24104]],[[194694,194694],"mapped",[24125]],[[194695,194695],"mapped",[24169]],[[194696,194696],"mapped",[14434]],[[194697,194697],"mapped",[139651]],[[194698,194698],"mapped",[14460]],[[194699,194699],"mapped",[24240]],[[194700,194700],"mapped",[24243]],[[194701,194701],"mapped",[24246]],[[194702,194702],"mapped",[24266]],[[194703,194703],"mapped",[172946]],[[194704,194704],"mapped",[24318]],[[194705,194706],"mapped",[140081]],[[194707,194707],"mapped",[33281]],[[194708,194709],"mapped",[24354]],[[194710,194710],"mapped",[14535]],[[194711,194711],"mapped",[144056]],[[194712,194712],"mapped",[156122]],[[194713,194713],"mapped",[24418]],[[194714,194714],"mapped",[24427]],[[194715,194715],"mapped",[14563]],[[194716,194716],"mapped",[24474]],[[194717,194717],"mapped",[24525]],[[194718,194718],"mapped",[24535]],[[194719,194719],"mapped",[24569]],[[194720,194720],"mapped",[24705]],[[194721,194721],"mapped",[14650]],[[194722,194722],"mapped",[14620]],[[194723,194723],"mapped",[24724]],[[194724,194724],"mapped",[141012]],[[194725,194725],"mapped",[24775]],[[194726,194726],"mapped",[24904]],[[194727,194727],"mapped",[24908]],[[194728,194728],"mapped",[24910]],[[194729,194729],"mapped",[24908]],[[194730,194730],"mapped",[24954]],[[194731,194731],"mapped",[24974]],[[194732,194732],"mapped",[25010]],[[194733,194733],"mapped",[24996]],[[194734,194734],"mapped",[25007]],[[194735,194735],"mapped",[25054]],[[194736,194736],"mapped",[25074]],[[194737,194737],"mapped",[25078]],[[194738,194738],"mapped",[25104]],[[194739,194739],"mapped",[25115]],[[194740,194740],"mapped",[25181]],[[194741,194741],"mapped",[25265]],[[194742,194742],"mapped",[25300]],[[194743,194743],"mapped",[25424]],[[194744,194744],"mapped",[142092]],[[194745,194745],"mapped",[25405]],[[194746,194746],"mapped",[25340]],[[194747,194747],"mapped",[25448]],[[194748,194748],"mapped",[25475]],[[194749,194749],"mapped",[25572]],[[194750,194750],"mapped",[142321]],[[194751,194751],"mapped",[25634]],[[194752,194752],"mapped",[25541]],[[194753,194753],"mapped",[25513]],[[194754,194754],"mapped",[14894]],[[194755,194755],"mapped",[25705]],[[194756,194756],"mapped",[25726]],[[194757,194757],"mapped",[25757]],[[194758,194758],"mapped",[25719]],[[194759,194759],"mapped",[14956]],[[194760,194760],"mapped",[25935]],[[194761,194761],"mapped",[25964]],[[194762,194762],"mapped",[143370]],[[194763,194763],"mapped",[26083]],[[194764,194764],"mapped",[26360]],[[194765,194765],"mapped",[26185]],[[194766,194766],"mapped",[15129]],[[194767,194767],"mapped",[26257]],[[194768,194768],"mapped",[15112]],[[194769,194769],"mapped",[15076]],[[194770,194770],"mapped",[20882]],[[194771,194771],"mapped",[20885]],[[194772,194772],"mapped",[26368]],[[194773,194773],"mapped",[26268]],[[194774,194774],"mapped",[32941]],[[194775,194775],"mapped",[17369]],[[194776,194776],"mapped",[26391]],[[194777,194777],"mapped",[26395]],[[194778,194778],"mapped",[26401]],[[194779,194779],"mapped",[26462]],[[194780,194780],"mapped",[26451]],[[194781,194781],"mapped",[144323]],[[194782,194782],"mapped",[15177]],[[194783,194783],"mapped",[26618]],[[194784,194784],"mapped",[26501]],[[194785,194785],"mapped",[26706]],[[194786,194786],"mapped",[26757]],[[194787,194787],"mapped",[144493]],[[194788,194788],"mapped",[26766]],[[194789,194789],"mapped",[26655]],[[194790,194790],"mapped",[26900]],[[194791,194791],"mapped",[15261]],[[194792,194792],"mapped",[26946]],[[194793,194793],"mapped",[27043]],[[194794,194794],"mapped",[27114]],[[194795,194795],"mapped",[27304]],[[194796,194796],"mapped",[145059]],[[194797,194797],"mapped",[27355]],[[194798,194798],"mapped",[15384]],[[194799,194799],"mapped",[27425]],[[194800,194800],"mapped",[145575]],[[194801,194801],"mapped",[27476]],[[194802,194802],"mapped",[15438]],[[194803,194803],"mapped",[27506]],[[194804,194804],"mapped",[27551]],[[194805,194805],"mapped",[27578]],[[194806,194806],"mapped",[27579]],[[194807,194807],"mapped",[146061]],[[194808,194808],"mapped",[138507]],[[194809,194809],"mapped",[146170]],[[194810,194810],"mapped",[27726]],[[194811,194811],"mapped",[146620]],[[194812,194812],"mapped",[27839]],[[194813,194813],"mapped",[27853]],[[194814,194814],"mapped",[27751]],[[194815,194815],"mapped",[27926]],[[194816,194816],"mapped",[27966]],[[194817,194817],"mapped",[28023]],[[194818,194818],"mapped",[27969]],[[194819,194819],"mapped",[28009]],[[194820,194820],"mapped",[28024]],[[194821,194821],"mapped",[28037]],[[194822,194822],"mapped",[146718]],[[194823,194823],"mapped",[27956]],[[194824,194824],"mapped",[28207]],[[194825,194825],"mapped",[28270]],[[194826,194826],"mapped",[15667]],[[194827,194827],"mapped",[28363]],[[194828,194828],"mapped",[28359]],[[194829,194829],"mapped",[147153]],[[194830,194830],"mapped",[28153]],[[194831,194831],"mapped",[28526]],[[194832,194832],"mapped",[147294]],[[194833,194833],"mapped",[147342]],[[194834,194834],"mapped",[28614]],[[194835,194835],"mapped",[28729]],[[194836,194836],"mapped",[28702]],[[194837,194837],"mapped",[28699]],[[194838,194838],"mapped",[15766]],[[194839,194839],"mapped",[28746]],[[194840,194840],"mapped",[28797]],[[194841,194841],"mapped",[28791]],[[194842,194842],"mapped",[28845]],[[194843,194843],"mapped",[132389]],[[194844,194844],"mapped",[28997]],[[194845,194845],"mapped",[148067]],[[194846,194846],"mapped",[29084]],[[194847,194847],"disallowed"],[[194848,194848],"mapped",[29224]],[[194849,194849],"mapped",[29237]],[[194850,194850],"mapped",[29264]],[[194851,194851],"mapped",[149000]],[[194852,194852],"mapped",[29312]],[[194853,194853],"mapped",[29333]],[[194854,194854],"mapped",[149301]],[[194855,194855],"mapped",[149524]],[[194856,194856],"mapped",[29562]],[[194857,194857],"mapped",[29579]],[[194858,194858],"mapped",[16044]],[[194859,194859],"mapped",[29605]],[[194860,194861],"mapped",[16056]],[[194862,194862],"mapped",[29767]],[[194863,194863],"mapped",[29788]],[[194864,194864],"mapped",[29809]],[[194865,194865],"mapped",[29829]],[[194866,194866],"mapped",[29898]],[[194867,194867],"mapped",[16155]],[[194868,194868],"mapped",[29988]],[[194869,194869],"mapped",[150582]],[[194870,194870],"mapped",[30014]],[[194871,194871],"mapped",[150674]],[[194872,194872],"mapped",[30064]],[[194873,194873],"mapped",[139679]],[[194874,194874],"mapped",[30224]],[[194875,194875],"mapped",[151457]],[[194876,194876],"mapped",[151480]],[[194877,194877],"mapped",[151620]],[[194878,194878],"mapped",[16380]],[[194879,194879],"mapped",[16392]],[[194880,194880],"mapped",[30452]],[[194881,194881],"mapped",[151795]],[[194882,194882],"mapped",[151794]],[[194883,194883],"mapped",[151833]],[[194884,194884],"mapped",[151859]],[[194885,194885],"mapped",[30494]],[[194886,194887],"mapped",[30495]],[[194888,194888],"mapped",[30538]],[[194889,194889],"mapped",[16441]],[[194890,194890],"mapped",[30603]],[[194891,194891],"mapped",[16454]],[[194892,194892],"mapped",[16534]],[[194893,194893],"mapped",[152605]],[[194894,194894],"mapped",[30798]],[[194895,194895],"mapped",[30860]],[[194896,194896],"mapped",[30924]],[[194897,194897],"mapped",[16611]],[[194898,194898],"mapped",[153126]],[[194899,194899],"mapped",[31062]],[[194900,194900],"mapped",[153242]],[[194901,194901],"mapped",[153285]],[[194902,194902],"mapped",[31119]],[[194903,194903],"mapped",[31211]],[[194904,194904],"mapped",[16687]],[[194905,194905],"mapped",[31296]],[[194906,194906],"mapped",[31306]],[[194907,194907],"mapped",[31311]],[[194908,194908],"mapped",[153980]],[[194909,194910],"mapped",[154279]],[[194911,194911],"disallowed"],[[194912,194912],"mapped",[16898]],[[194913,194913],"mapped",[154539]],[[194914,194914],"mapped",[31686]],[[194915,194915],"mapped",[31689]],[[194916,194916],"mapped",[16935]],[[194917,194917],"mapped",[154752]],[[194918,194918],"mapped",[31954]],[[194919,194919],"mapped",[17056]],[[194920,194920],"mapped",[31976]],[[194921,194921],"mapped",[31971]],[[194922,194922],"mapped",[32000]],[[194923,194923],"mapped",[155526]],[[194924,194924],"mapped",[32099]],[[194925,194925],"mapped",[17153]],[[194926,194926],"mapped",[32199]],[[194927,194927],"mapped",[32258]],[[194928,194928],"mapped",[32325]],[[194929,194929],"mapped",[17204]],[[194930,194930],"mapped",[156200]],[[194931,194931],"mapped",[156231]],[[194932,194932],"mapped",[17241]],[[194933,194933],"mapped",[156377]],[[194934,194934],"mapped",[32634]],[[194935,194935],"mapped",[156478]],[[194936,194936],"mapped",[32661]],[[194937,194937],"mapped",[32762]],[[194938,194938],"mapped",[32773]],[[194939,194939],"mapped",[156890]],[[194940,194940],"mapped",[156963]],[[194941,194941],"mapped",[32864]],[[194942,194942],"mapped",[157096]],[[194943,194943],"mapped",[32880]],[[194944,194944],"mapped",[144223]],[[194945,194945],"mapped",[17365]],[[194946,194946],"mapped",[32946]],[[194947,194947],"mapped",[33027]],[[194948,194948],"mapped",[17419]],[[194949,194949],"mapped",[33086]],[[194950,194950],"mapped",[23221]],[[194951,194951],"mapped",[157607]],[[194952,194952],"mapped",[157621]],[[194953,194953],"mapped",[144275]],[[194954,194954],"mapped",[144284]],[[194955,194955],"mapped",[33281]],[[194956,194956],"mapped",[33284]],[[194957,194957],"mapped",[36766]],[[194958,194958],"mapped",[17515]],[[194959,194959],"mapped",[33425]],[[194960,194960],"mapped",[33419]],[[194961,194961],"mapped",[33437]],[[194962,194962],"mapped",[21171]],[[194963,194963],"mapped",[33457]],[[194964,194964],"mapped",[33459]],[[194965,194965],"mapped",[33469]],[[194966,194966],"mapped",[33510]],[[194967,194967],"mapped",[158524]],[[194968,194968],"mapped",[33509]],[[194969,194969],"mapped",[33565]],[[194970,194970],"mapped",[33635]],[[194971,194971],"mapped",[33709]],[[194972,194972],"mapped",[33571]],[[194973,194973],"mapped",[33725]],[[194974,194974],"mapped",[33767]],[[194975,194975],"mapped",[33879]],[[194976,194976],"mapped",[33619]],[[194977,194977],"mapped",[33738]],[[194978,194978],"mapped",[33740]],[[194979,194979],"mapped",[33756]],[[194980,194980],"mapped",[158774]],[[194981,194981],"mapped",[159083]],[[194982,194982],"mapped",[158933]],[[194983,194983],"mapped",[17707]],[[194984,194984],"mapped",[34033]],[[194985,194985],"mapped",[34035]],[[194986,194986],"mapped",[34070]],[[194987,194987],"mapped",[160714]],[[194988,194988],"mapped",[34148]],[[194989,194989],"mapped",[159532]],[[194990,194990],"mapped",[17757]],[[194991,194991],"mapped",[17761]],[[194992,194992],"mapped",[159665]],[[194993,194993],"mapped",[159954]],[[194994,194994],"mapped",[17771]],[[194995,194995],"mapped",[34384]],[[194996,194996],"mapped",[34396]],[[194997,194997],"mapped",[34407]],[[194998,194998],"mapped",[34409]],[[194999,194999],"mapped",[34473]],[[195000,195000],"mapped",[34440]],[[195001,195001],"mapped",[34574]],[[195002,195002],"mapped",[34530]],[[195003,195003],"mapped",[34681]],[[195004,195004],"mapped",[34600]],[[195005,195005],"mapped",[34667]],[[195006,195006],"mapped",[34694]],[[195007,195007],"disallowed"],[[195008,195008],"mapped",[34785]],[[195009,195009],"mapped",[34817]],[[195010,195010],"mapped",[17913]],[[195011,195011],"mapped",[34912]],[[195012,195012],"mapped",[34915]],[[195013,195013],"mapped",[161383]],[[195014,195014],"mapped",[35031]],[[195015,195015],"mapped",[35038]],[[195016,195016],"mapped",[17973]],[[195017,195017],"mapped",[35066]],[[195018,195018],"mapped",[13499]],[[195019,195019],"mapped",[161966]],[[195020,195020],"mapped",[162150]],[[195021,195021],"mapped",[18110]],[[195022,195022],"mapped",[18119]],[[195023,195023],"mapped",[35488]],[[195024,195024],"mapped",[35565]],[[195025,195025],"mapped",[35722]],[[195026,195026],"mapped",[35925]],[[195027,195027],"mapped",[162984]],[[195028,195028],"mapped",[36011]],[[195029,195029],"mapped",[36033]],[[195030,195030],"mapped",[36123]],[[195031,195031],"mapped",[36215]],[[195032,195032],"mapped",[163631]],[[195033,195033],"mapped",[133124]],[[195034,195034],"mapped",[36299]],[[195035,195035],"mapped",[36284]],[[195036,195036],"mapped",[36336]],[[195037,195037],"mapped",[133342]],[[195038,195038],"mapped",[36564]],[[195039,195039],"mapped",[36664]],[[195040,195040],"mapped",[165330]],[[195041,195041],"mapped",[165357]],[[195042,195042],"mapped",[37012]],[[195043,195043],"mapped",[37105]],[[195044,195044],"mapped",[37137]],[[195045,195045],"mapped",[165678]],[[195046,195046],"mapped",[37147]],[[195047,195047],"mapped",[37432]],[[195048,195048],"mapped",[37591]],[[195049,195049],"mapped",[37592]],[[195050,195050],"mapped",[37500]],[[195051,195051],"mapped",[37881]],[[195052,195052],"mapped",[37909]],[[195053,195053],"mapped",[166906]],[[195054,195054],"mapped",[38283]],[[195055,195055],"mapped",[18837]],[[195056,195056],"mapped",[38327]],[[195057,195057],"mapped",[167287]],[[195058,195058],"mapped",[18918]],[[195059,195059],"mapped",[38595]],[[195060,195060],"mapped",[23986]],[[195061,195061],"mapped",[38691]],[[195062,195062],"mapped",[168261]],[[195063,195063],"mapped",[168474]],[[195064,195064],"mapped",[19054]],[[195065,195065],"mapped",[19062]],[[195066,195066],"mapped",[38880]],[[195067,195067],"mapped",[168970]],[[195068,195068],"mapped",[19122]],[[195069,195069],"mapped",[169110]],[[195070,195071],"mapped",[38923]],[[195072,195072],"mapped",[38953]],[[195073,195073],"mapped",[169398]],[[195074,195074],"mapped",[39138]],[[195075,195075],"mapped",[19251]],[[195076,195076],"mapped",[39209]],[[195077,195077],"mapped",[39335]],[[195078,195078],"mapped",[39362]],[[195079,195079],"mapped",[39422]],[[195080,195080],"mapped",[19406]],[[195081,195081],"mapped",[170800]],[[195082,195082],"mapped",[39698]],[[195083,195083],"mapped",[40000]],[[195084,195084],"mapped",[40189]],[[195085,195085],"mapped",[19662]],[[195086,195086],"mapped",[19693]],[[195087,195087],"mapped",[40295]],[[195088,195088],"mapped",[172238]],[[195089,195089],"mapped",[19704]],[[195090,195090],"mapped",[172293]],[[195091,195091],"mapped",[172558]],[[195092,195092],"mapped",[172689]],[[195093,195093],"mapped",[40635]],[[195094,195094],"mapped",[19798]],[[195095,195095],"mapped",[40697]],[[195096,195096],"mapped",[40702]],[[195097,195097],"mapped",[40709]],[[195098,195098],"mapped",[40719]],[[195099,195099],"mapped",[40726]],[[195100,195100],"mapped",[40763]],[[195101,195101],"mapped",[173568]],[[195102,196605],"disallowed"],[[196606,196607],"disallowed"],[[196608,262141],"disallowed"],[[262142,262143],"disallowed"],[[262144,327677],"disallowed"],[[327678,327679],"disallowed"],[[327680,393213],"disallowed"],[[393214,393215],"disallowed"],[[393216,458749],"disallowed"],[[458750,458751],"disallowed"],[[458752,524285],"disallowed"],[[524286,524287],"disallowed"],[[524288,589821],"disallowed"],[[589822,589823],"disallowed"],[[589824,655357],"disallowed"],[[655358,655359],"disallowed"],[[655360,720893],"disallowed"],[[720894,720895],"disallowed"],[[720896,786429],"disallowed"],[[786430,786431],"disallowed"],[[786432,851965],"disallowed"],[[851966,851967],"disallowed"],[[851968,917501],"disallowed"],[[917502,917503],"disallowed"],[[917504,917504],"disallowed"],[[917505,917505],"disallowed"],[[917506,917535],"disallowed"],[[917536,917631],"disallowed"],[[917632,917759],"disallowed"],[[917760,917999],"ignored"],[[918000,983037],"disallowed"],[[983038,983039],"disallowed"],[[983040,1048573],"disallowed"],[[1048574,1048575],"disallowed"],[[1048576,1114109],"disallowed"],[[1114110,1114111],"disallowed"]]'); + +/***/ }), + +/***/ 4244: +/***/ ((module) => { + +"use strict"; +module.exports = /*#__PURE__*/JSON.parse('[["8740","䏰䰲䘃䖦䕸𧉧䵷䖳𧲱䳢𧳅㮕䜶䝄䱇䱀𤊿𣘗𧍒𦺋𧃒䱗𪍑䝏䗚䲅𧱬䴇䪤䚡𦬣爥𥩔𡩣𣸆𣽡晍囻"],["8767","綕夝𨮹㷴霴𧯯寛𡵞媤㘥𩺰嫑宷峼杮薓𩥅瑡璝㡵𡵓𣚞𦀡㻬"],["87a1","𥣞㫵竼龗𤅡𨤍𣇪𠪊𣉞䌊蒄龖鐯䤰蘓墖靊鈘秐稲晠権袝瑌篅枂稬剏遆㓦珄𥶹瓆鿇垳䤯呌䄱𣚎堘穲𧭥讏䚮𦺈䆁𥶙箮𢒼鿈𢓁𢓉𢓌鿉蔄𣖻䂴鿊䓡𪷿拁灮鿋"],["8840","㇀",4,"𠄌㇅𠃑𠃍㇆㇇𠃋𡿨㇈𠃊㇉㇊㇋㇌𠄎㇍㇎ĀÁǍÀĒÉĚÈŌÓǑÒ࿿Ê̄Ế࿿Ê̌ỀÊāáǎàɑēéěèīíǐìōóǒòūúǔùǖǘǚ"],["88a1","ǜü࿿ê̄ế࿿ê̌ềêɡ⏚⏛"],["8940","𪎩𡅅"],["8943","攊"],["8946","丽滝鵎釟"],["894c","𧜵撑会伨侨兖兴农凤务动医华发变团声处备夲头学实実岚庆总斉柾栄桥济炼电纤纬纺织经统缆缷艺苏药视设询车轧轮"],["89a1","琑糼緍楆竉刧"],["89ab","醌碸酞肼"],["89b0","贋胶𠧧"],["89b5","肟黇䳍鷉鸌䰾𩷶𧀎鸊𪄳㗁"],["89c1","溚舾甙"],["89c5","䤑马骏龙禇𨑬𡷊𠗐𢫦两亁亀亇亿仫伷㑌侽㹈倃傈㑽㒓㒥円夅凛凼刅争剹劐匧㗇厩㕑厰㕓参吣㕭㕲㚁咓咣咴咹哐哯唘唣唨㖘唿㖥㖿嗗㗅"],["8a40","𧶄唥"],["8a43","𠱂𠴕𥄫喐𢳆㧬𠍁蹆𤶸𩓥䁓𨂾睺𢰸㨴䟕𨅝𦧲𤷪擝𠵼𠾴𠳕𡃴撍蹾𠺖𠰋𠽤𢲩𨉖𤓓"],["8a64","𠵆𩩍𨃩䟴𤺧𢳂骲㩧𩗴㿭㔆𥋇𩟔𧣈𢵄鵮頕"],["8a76","䏙𦂥撴哣𢵌𢯊𡁷㧻𡁯"],["8aa1","𦛚𦜖𧦠擪𥁒𠱃蹨𢆡𨭌𠜱"],["8aac","䠋𠆩㿺塳𢶍"],["8ab2","𤗈𠓼𦂗𠽌𠶖啹䂻䎺"],["8abb","䪴𢩦𡂝膪飵𠶜捹㧾𢝵跀嚡摼㹃"],["8ac9","𪘁𠸉𢫏𢳉"],["8ace","𡃈𣧂㦒㨆𨊛㕸𥹉𢃇噒𠼱𢲲𩜠㒼氽𤸻"],["8adf","𧕴𢺋𢈈𪙛𨳍𠹺𠰴𦠜羓𡃏𢠃𢤹㗻𥇣𠺌𠾍𠺪㾓𠼰𠵇𡅏𠹌"],["8af6","𠺫𠮩𠵈𡃀𡄽㿹𢚖搲𠾭"],["8b40","𣏴𧘹𢯎𠵾𠵿𢱑𢱕㨘𠺘𡃇𠼮𪘲𦭐𨳒𨶙𨳊閪哌苄喹"],["8b55","𩻃鰦骶𧝞𢷮煀腭胬尜𦕲脴㞗卟𨂽醶𠻺𠸏𠹷𠻻㗝𤷫㘉𠳖嚯𢞵𡃉𠸐𠹸𡁸𡅈𨈇𡑕𠹹𤹐𢶤婔𡀝𡀞𡃵𡃶垜𠸑"],["8ba1","𧚔𨋍𠾵𠹻𥅾㜃𠾶𡆀𥋘𪊽𤧚𡠺𤅷𨉼墙剨㘚𥜽箲孨䠀䬬鼧䧧鰟鮍𥭴𣄽嗻㗲嚉丨夂𡯁屮靑𠂆乛亻㔾尣彑忄㣺扌攵歺氵氺灬爫丬犭𤣩罒礻糹罓𦉪㓁"],["8bde","𦍋耂肀𦘒𦥑卝衤见𧢲讠贝钅镸长门𨸏韦页风飞饣𩠐鱼鸟黄歯龜丷𠂇阝户钢"],["8c40","倻淾𩱳龦㷉袏𤅎灷峵䬠𥇍㕙𥴰愢𨨲辧釶熑朙玺𣊁𪄇㲋𡦀䬐磤琂冮𨜏䀉橣𪊺䈣蘏𠩯稪𩥇𨫪靕灍匤𢁾鏴盙𨧣龧矝亣俰傼丯众龨吴綋墒壐𡶶庒庙忂𢜒斋"],["8ca1","𣏹椙橃𣱣泿"],["8ca7","爀𤔅玌㻛𤨓嬕璹讃𥲤𥚕窓篬糃繬苸薗龩袐龪躹龫迏蕟駠鈡龬𨶹𡐿䁱䊢娚"],["8cc9","顨杫䉶圽"],["8cce","藖𤥻芿𧄍䲁𦵴嵻𦬕𦾾龭龮宖龯曧繛湗秊㶈䓃𣉖𢞖䎚䔶"],["8ce6","峕𣬚諹屸㴒𣕑嵸龲煗䕘𤃬𡸣䱷㥸㑊𠆤𦱁諌侴𠈹妿腬顖𩣺弻"],["8d40","𠮟"],["8d42","𢇁𨥭䄂䚻𩁹㼇龳𪆵䃸㟖䛷𦱆䅼𨚲𧏿䕭㣔𥒚䕡䔛䶉䱻䵶䗪㿈𤬏㙡䓞䒽䇭崾嵈嵖㷼㠏嶤嶹㠠㠸幂庽弥徃㤈㤔㤿㥍惗愽峥㦉憷憹懏㦸戬抐拥挘㧸嚱"],["8da1","㨃揢揻搇摚㩋擀崕嘡龟㪗斆㪽旿晓㫲暒㬢朖㭂枤栀㭘桊梄㭲㭱㭻椉楃牜楤榟榅㮼槖㯝橥橴橱檂㯬檙㯲檫檵櫔櫶殁毁毪汵沪㳋洂洆洦涁㳯涤涱渕渘温溆𨧀溻滢滚齿滨滩漤漴㵆𣽁澁澾㵪㵵熷岙㶊瀬㶑灐灔灯灿炉𠌥䏁㗱𠻘"],["8e40","𣻗垾𦻓焾𥟠㙎榢𨯩孴穉𥣡𩓙穥穽𥦬窻窰竂竃燑𦒍䇊竚竝竪䇯咲𥰁笋筕笩𥌎𥳾箢筯莜𥮴𦱿篐萡箒箸𥴠㶭𥱥蒒篺簆簵𥳁籄粃𤢂粦晽𤕸糉糇糦籴糳糵糎"],["8ea1","繧䔝𦹄絝𦻖璍綉綫焵綳緒𤁗𦀩緤㴓緵𡟹緥𨍭縝𦄡𦅚繮纒䌫鑬縧罀罁罇礶𦋐駡羗𦍑羣𡙡𠁨䕜𣝦䔃𨌺翺𦒉者耈耝耨耯𪂇𦳃耻耼聡𢜔䦉𦘦𣷣𦛨朥肧𨩈脇脚墰𢛶汿𦒘𤾸擧𡒊舘𡡞橓𤩥𤪕䑺舩𠬍𦩒𣵾俹𡓽蓢荢𦬊𤦧𣔰𡝳𣷸芪椛芳䇛"],["8f40","蕋苐茚𠸖𡞴㛁𣅽𣕚艻苢茘𣺋𦶣𦬅𦮗𣗎㶿茝嗬莅䔋𦶥莬菁菓㑾𦻔橗蕚㒖𦹂𢻯葘𥯤葱㷓䓤檧葊𣲵祘蒨𦮖𦹷𦹃蓞萏莑䒠蒓蓤𥲑䉀𥳀䕃蔴嫲𦺙䔧蕳䔖枿蘖"],["8fa1","𨘥𨘻藁𧂈蘂𡖂𧃍䕫䕪蘨㙈𡢢号𧎚虾蝱𪃸蟮𢰧螱蟚蠏噡虬桖䘏衅衆𧗠𣶹𧗤衞袜䙛袴袵揁装睷𧜏覇覊覦覩覧覼𨨥觧𧤤𧪽誜瞓釾誐𧩙竩𧬺𣾏䜓𧬸煼謌謟𥐰𥕥謿譌譍誩𤩺讐讛誯𡛟䘕衏貛𧵔𧶏貫㜥𧵓賖𧶘𧶽贒贃𡤐賛灜贑𤳉㻐起"],["9040","趩𨀂𡀔𤦊㭼𨆼𧄌竧躭躶軃鋔輙輭𨍥𨐒辥錃𪊟𠩐辳䤪𨧞𨔽𣶻廸𣉢迹𪀔𨚼𨔁𢌥㦀𦻗逷𨔼𧪾遡𨕬𨘋邨𨜓郄𨛦邮都酧㫰醩釄粬𨤳𡺉鈎沟鉁鉢𥖹銹𨫆𣲛𨬌𥗛"],["90a1","𠴱錬鍫𨫡𨯫炏嫃𨫢𨫥䥥鉄𨯬𨰹𨯿鍳鑛躼閅閦鐦閠濶䊹𢙺𨛘𡉼𣸮䧟氜陻隖䅬隣𦻕懚隶磵𨫠隽双䦡𦲸𠉴𦐐𩂯𩃥𤫑𡤕𣌊霱虂霶䨏䔽䖅𤫩灵孁霛靜𩇕靗孊𩇫靟鐥僐𣂷𣂼鞉鞟鞱鞾韀韒韠𥑬韮琜𩐳響韵𩐝𧥺䫑頴頳顋顦㬎𧅵㵑𠘰𤅜"],["9140","𥜆飊颷飈飇䫿𦴧𡛓喰飡飦飬鍸餹𤨩䭲𩡗𩤅駵騌騻騐驘𥜥㛄𩂱𩯕髠髢𩬅髴䰎鬔鬭𨘀倴鬴𦦨㣃𣁽魐魀𩴾婅𡡣鮎𤉋鰂鯿鰌𩹨鷔𩾷𪆒𪆫𪃡𪄣𪇟鵾鶃𪄴鸎梈"],["91a1","鷄𢅛𪆓𪈠𡤻𪈳鴹𪂹𪊴麐麕麞麢䴴麪麯𤍤黁㭠㧥㴝伲㞾𨰫鼂鼈䮖鐤𦶢鼗鼖鼹嚟嚊齅馸𩂋韲葿齢齩竜龎爖䮾𤥵𤦻煷𤧸𤍈𤩑玞𨯚𡣺禟𨥾𨸶鍩鏳𨩄鋬鎁鏋𨥬𤒹爗㻫睲穃烐𤑳𤏸煾𡟯炣𡢾𣖙㻇𡢅𥐯𡟸㜢𡛻𡠹㛡𡝴𡣑𥽋㜣𡛀坛𤨥𡏾𡊨"],["9240","𡏆𡒶蔃𣚦蔃葕𤦔𧅥𣸱𥕜𣻻𧁒䓴𣛮𩦝𦼦柹㜳㰕㷧塬𡤢栐䁗𣜿𤃡𤂋𤄏𦰡哋嚞𦚱嚒𠿟𠮨𠸍鏆𨬓鎜仸儫㠙𤐶亼𠑥𠍿佋侊𥙑婨𠆫𠏋㦙𠌊𠐔㐵伩𠋀𨺳𠉵諚𠈌亘"],["92a1","働儍侢伃𤨎𣺊佂倮偬傁俌俥偘僼兙兛兝兞湶𣖕𣸹𣺿浲𡢄𣺉冨凃𠗠䓝𠒣𠒒𠒑赺𨪜𠜎剙劤𠡳勡鍮䙺熌𤎌𠰠𤦬𡃤槑𠸝瑹㻞璙琔瑖玘䮎𤪼𤂍叐㖄爏𤃉喴𠍅响𠯆圝鉝雴鍦埝垍坿㘾壋媙𨩆𡛺𡝯𡜐娬妸銏婾嫏娒𥥆𡧳𡡡𤊕㛵洅瑃娡𥺃"],["9340","媁𨯗𠐓鏠璌𡌃焅䥲鐈𨧻鎽㞠尞岞幞幈𡦖𡥼𣫮廍孏𡤃𡤄㜁𡢠㛝𡛾㛓脪𨩇𡶺𣑲𨦨弌弎𡤧𡞫婫𡜻孄蘔𧗽衠恾𢡠𢘫忛㺸𢖯𢖾𩂈𦽳懀𠀾𠁆𢘛憙憘恵𢲛𢴇𤛔𩅍"],["93a1","摱𤙥𢭪㨩𢬢𣑐𩣪𢹸挷𪑛撶挱揑𤧣𢵧护𢲡搻敫楲㯴𣂎𣊭𤦉𣊫唍𣋠𡣙𩐿曎𣊉𣆳㫠䆐𥖄𨬢𥖏𡛼𥕛𥐥磮𣄃𡠪𣈴㑤𣈏𣆂𤋉暎𦴤晫䮓昰𧡰𡷫晣𣋒𣋡昞𥡲㣑𣠺𣞼㮙𣞢𣏾瓐㮖枏𤘪梶栞㯄檾㡣𣟕𤒇樳橒櫉欅𡤒攑梘橌㯗橺歗𣿀𣲚鎠鋲𨯪𨫋"],["9440","銉𨀞𨧜鑧涥漋𤧬浧𣽿㶏渄𤀼娽渊塇洤硂焻𤌚𤉶烱牐犇犔𤞏𤜥兹𤪤𠗫瑺𣻸𣙟𤩊𤤗𥿡㼆㺱𤫟𨰣𣼵悧㻳瓌琼鎇琷䒟𦷪䕑疃㽣𤳙𤴆㽘畕癳𪗆㬙瑨𨫌𤦫𤦎㫻"],["94a1","㷍𤩎㻿𤧅𤣳釺圲鍂𨫣𡡤僟𥈡𥇧睸𣈲眎眏睻𤚗𣞁㩞𤣰琸璛㺿𤪺𤫇䃈𤪖𦆮錇𥖁砞碍碈磒珐祙𧝁𥛣䄎禛蒖禥樭𣻺稺秴䅮𡛦䄲鈵秱𠵌𤦌𠊙𣶺𡝮㖗啫㕰㚪𠇔𠰍竢婙𢛵𥪯𥪜娍𠉛磰娪𥯆竾䇹籝籭䈑𥮳𥺼𥺦糍𤧹𡞰粎籼粮檲緜縇緓罎𦉡"],["9540","𦅜𧭈綗𥺂䉪𦭵𠤖柖𠁎𣗏埄𦐒𦏸𤥢翝笧𠠬𥫩𥵃笌𥸎駦虅驣樜𣐿㧢𤧷𦖭騟𦖠蒀𧄧𦳑䓪脷䐂胆脉腂𦞴飃𦩂艢艥𦩑葓𦶧蘐𧈛媆䅿𡡀嬫𡢡嫤𡣘蚠蜨𣶏蠭𧐢娂"],["95a1","衮佅袇袿裦襥襍𥚃襔𧞅𧞄𨯵𨯙𨮜𨧹㺭蒣䛵䛏㟲訽訜𩑈彍鈫𤊄旔焩烄𡡅鵭貟賩𧷜妚矃姰䍮㛔踪躧𤰉輰轊䋴汘澻𢌡䢛潹溋𡟚鯩㚵𤤯邻邗啱䤆醻鐄𨩋䁢𨫼鐧𨰝𨰻蓥訫閙閧閗閖𨴴瑅㻂𤣿𤩂𤏪㻧𣈥随𨻧𨹦𨹥㻌𤧭𤩸𣿮琒瑫㻼靁𩂰"],["9640","桇䨝𩂓𥟟靝鍨𨦉𨰦𨬯𦎾銺嬑譩䤼珹𤈛鞛靱餸𠼦巁𨯅𤪲頟𩓚鋶𩗗釥䓀𨭐𤩧𨭤飜𨩅㼀鈪䤥萔餻饍𧬆㷽馛䭯馪驜𨭥𥣈檏騡嫾騯𩣱䮐𩥈馼䮽䮗鍽塲𡌂堢𤦸"],["96a1","𡓨硄𢜟𣶸棅㵽鑘㤧慐𢞁𢥫愇鱏鱓鱻鰵鰐魿鯏𩸭鮟𪇵𪃾鴡䲮𤄄鸘䲰鴌𪆴𪃭𪃳𩤯鶥蒽𦸒𦿟𦮂藼䔳𦶤𦺄𦷰萠藮𦸀𣟗𦁤秢𣖜𣙀䤭𤧞㵢鏛銾鍈𠊿碹鉷鑍俤㑀遤𥕝砽硔碶硋𡝗𣇉𤥁㚚佲濚濙瀞瀞吔𤆵垻壳垊鴖埗焴㒯𤆬燫𦱀𤾗嬨𡞵𨩉"],["9740","愌嫎娋䊼𤒈㜬䭻𨧼鎻鎸𡣖𠼝葲𦳀𡐓𤋺𢰦𤏁妔𣶷𦝁綨𦅛𦂤𤦹𤦋𨧺鋥珢㻩璴𨭣𡢟㻡𤪳櫘珳珻㻖𤨾𤪔𡟙𤩦𠎧𡐤𤧥瑈𤤖炥𤥶銄珦鍟𠓾錱𨫎𨨖鎆𨯧𥗕䤵𨪂煫"],["97a1","𤥃𠳿嚤𠘚𠯫𠲸唂秄𡟺緾𡛂𤩐𡡒䔮鐁㜊𨫀𤦭妰𡢿𡢃𧒄媡㛢𣵛㚰鉟婹𨪁𡡢鍴㳍𠪴䪖㦊僴㵩㵌𡎜煵䋻𨈘渏𩃤䓫浗𧹏灧沯㳖𣿭𣸭渂漌㵯𠏵畑㚼㓈䚀㻚䡱姄鉮䤾轁𨰜𦯀堒埈㛖𡑒烾𤍢𤩱𢿣𡊰𢎽梹楧𡎘𣓥𧯴𣛟𨪃𣟖𣏺𤲟樚𣚭𦲷萾䓟䓎"],["9840","𦴦𦵑𦲂𦿞漗𧄉茽𡜺菭𦲀𧁓𡟛妉媂𡞳婡婱𡤅𤇼㜭姯𡜼㛇熎鎐暚𤊥婮娫𤊓樫𣻹𧜶𤑛𤋊焝𤉙𨧡侰𦴨峂𤓎𧹍𤎽樌𤉖𡌄炦焳𤏩㶥泟勇𤩏繥姫崯㷳彜𤩝𡟟綤萦"],["98a1","咅𣫺𣌀𠈔坾𠣕𠘙㿥𡾞𪊶瀃𩅛嵰玏糓𨩙𩐠俈翧狍猐𧫴猸猹𥛶獁獈㺩𧬘遬燵𤣲珡臶㻊県㻑沢国琙琞琟㻢㻰㻴㻺瓓㼎㽓畂畭畲疍㽼痈痜㿀癍㿗癴㿜発𤽜熈嘣覀塩䀝睃䀹条䁅㗛瞘䁪䁯属瞾矋売砘点砜䂨砹硇硑硦葈𥔵礳栃礲䄃"],["9940","䄉禑禙辻稆込䅧窑䆲窼艹䇄竏竛䇏両筢筬筻簒簛䉠䉺类粜䊌粸䊔糭输烀𠳏総緔緐緽羮羴犟䎗耠耥笹耮耱联㷌垴炠肷胩䏭脌猪脎脒畠脔䐁㬹腖腙腚"],["99a1","䐓堺腼膄䐥膓䐭膥埯臁臤艔䒏芦艶苊苘苿䒰荗险榊萅烵葤惣蒈䔄蒾蓡蓸蔐蔸蕒䔻蕯蕰藠䕷虲蚒蚲蛯际螋䘆䘗袮裿褤襇覑𧥧訩訸誔誴豑賔賲贜䞘塟跃䟭仮踺嗘坔蹱嗵躰䠷軎転軤軭軲辷迁迊迌逳駄䢭飠鈓䤞鈨鉘鉫銱銮銿"],["9a40","鋣鋫鋳鋴鋽鍃鎄鎭䥅䥑麿鐗匁鐝鐭鐾䥪鑔鑹锭関䦧间阳䧥枠䨤靀䨵鞲韂噔䫤惨颹䬙飱塄餎餙冴餜餷饂饝饢䭰駅䮝騼鬏窃魩鮁鯝鯱鯴䱭鰠㝯𡯂鵉鰺"],["9aa1","黾噐鶓鶽鷀鷼银辶鹻麬麱麽黆铜黢黱黸竈齄𠂔𠊷𠎠椚铃妬𠓗塀铁㞹𠗕𠘕𠙶𡚺块煳𠫂𠫍𠮿呪吆𠯋咞𠯻𠰻𠱓𠱥𠱼惧𠲍噺𠲵𠳝𠳭𠵯𠶲𠷈楕鰯螥𠸄𠸎𠻗𠾐𠼭𠹳尠𠾼帋𡁜𡁏𡁶朞𡁻𡂈𡂖㙇𡂿𡃓𡄯𡄻卤蒭𡋣𡍵𡌶讁𡕷𡘙𡟃𡟇乸炻𡠭𡥪"],["9b40","𡨭𡩅𡰪𡱰𡲬𡻈拃𡻕𡼕熘桕𢁅槩㛈𢉼𢏗𢏺𢜪𢡱𢥏苽𢥧𢦓𢫕覥𢫨辠𢬎鞸𢬿顇骽𢱌"],["9b62","𢲈𢲷𥯨𢴈𢴒𢶷𢶕𢹂𢽴𢿌𣀳𣁦𣌟𣏞徱晈暿𧩹𣕧𣗳爁𤦺矗𣘚𣜖纇𠍆墵朎"],["9ba1","椘𣪧𧙗𥿢𣸑𣺹𧗾𢂚䣐䪸𤄙𨪚𤋮𤌍𤀻𤌴𤎖𤩅𠗊凒𠘑妟𡺨㮾𣳿𤐄𤓖垈𤙴㦛𤜯𨗨𩧉㝢𢇃譞𨭎駖𤠒𤣻𤨕爉𤫀𠱸奥𤺥𤾆𠝹軚𥀬劏圿煱𥊙𥐙𣽊𤪧喼𥑆𥑮𦭒釔㑳𥔿𧘲𥕞䜘𥕢𥕦𥟇𤤿𥡝偦㓻𣏌惞𥤃䝼𨥈𥪮𥮉𥰆𡶐垡煑澶𦄂𧰒遖𦆲𤾚譢𦐂𦑊"],["9c40","嵛𦯷輶𦒄𡤜諪𤧶𦒈𣿯𦔒䯀𦖿𦚵𢜛鑥𥟡憕娧晉侻嚹𤔡𦛼乪𤤴陖涏𦲽㘘襷𦞙𦡮𦐑𦡞營𦣇筂𩃀𠨑𦤦鄄𦤹穅鷰𦧺騦𦨭㙟𦑩𠀡禃𦨴𦭛崬𣔙菏𦮝䛐𦲤画补𦶮墶"],["9ca1","㜜𢖍𧁋𧇍㱔𧊀𧊅銁𢅺𧊋錰𧋦𤧐氹钟𧑐𠻸蠧裵𢤦𨑳𡞱溸𤨪𡠠㦤㚹尐秣䔿暶𩲭𩢤襃𧟌𧡘囖䃟𡘊㦡𣜯𨃨𡏅熭荦𧧝𩆨婧䲷𧂯𨦫𧧽𧨊𧬋𧵦𤅺筃祾𨀉澵𪋟樃𨌘厢𦸇鎿栶靝𨅯𨀣𦦵𡏭𣈯𨁈嶅𨰰𨂃圕頣𨥉嶫𤦈斾槕叒𤪥𣾁㰑朶𨂐𨃴𨄮𡾡𨅏"],["9d40","𨆉𨆯𨈚𨌆𨌯𨎊㗊𨑨𨚪䣺揦𨥖砈鉕𨦸䏲𨧧䏟𨧨𨭆𨯔姸𨰉輋𨿅𩃬筑𩄐𩄼㷷𩅞𤫊运犏嚋𩓧𩗩𩖰𩖸𩜲𩣑𩥉𩥪𩧃𩨨𩬎𩵚𩶛纟𩻸𩼣䲤镇𪊓熢𪋿䶑递𪗋䶜𠲜达嗁"],["9da1","辺𢒰边𤪓䔉繿潖檱仪㓤𨬬𧢝㜺躀𡟵𨀤𨭬𨮙𧨾𦚯㷫𧙕𣲷𥘵𥥖亚𥺁𦉘嚿𠹭踎孭𣺈𤲞揞拐𡟶𡡻攰嘭𥱊吚𥌑㷆𩶘䱽嘢嘞罉𥻘奵𣵀蝰东𠿪𠵉𣚺脗鵞贘瘻鱅癎瞹鍅吲腈苷嘥脲萘肽嗪祢噃吖𠺝㗎嘅嗱曱𨋢㘭甴嗰喺咗啲𠱁𠲖廐𥅈𠹶𢱢"],["9e40","𠺢麫絚嗞𡁵抝靭咔賍燶酶揼掹揾啩𢭃鱲𢺳冚㓟𠶧冧呍唞唓癦踭𦢊疱肶蠄螆裇膶萜𡃁䓬猄𤜆宐茋𦢓噻𢛴𧴯𤆣𧵳𦻐𧊶酰𡇙鈈𣳼𪚩𠺬𠻹牦𡲢䝎𤿂𧿹𠿫䃺"],["9ea1","鱝攟𢶠䣳𤟠𩵼𠿬𠸊恢𧖣𠿭"],["9ead","𦁈𡆇熣纎鵐业丄㕷嬍沲卧㚬㧜卽㚥𤘘墚𤭮舭呋垪𥪕𠥹"],["9ec5","㩒𢑥獴𩺬䴉鯭𣳾𩼰䱛𤾩𩖞𩿞葜𣶶𧊲𦞳𣜠挮紥𣻷𣸬㨪逈勌㹴㙺䗩𠒎癀嫰𠺶硺𧼮墧䂿噼鮋嵴癔𪐴麅䳡痹㟻愙𣃚𤏲"],["9ef5","噝𡊩垧𤥣𩸆刴𧂮㖭汊鵼"],["9f40","籖鬹埞𡝬屓擓𩓐𦌵𧅤蚭𠴨𦴢𤫢𠵱"],["9f4f","凾𡼏嶎霃𡷑麁遌笟鬂峑箣扨挵髿篏鬪籾鬮籂粆鰕篼鬉鼗鰛𤤾齚啳寃俽麘俲剠㸆勑坧偖妷帒韈鶫轜呩鞴饀鞺匬愰"],["9fa1","椬叚鰊鴂䰻陁榀傦畆𡝭駚剳"],["9fae","酙隁酜"],["9fb2","酑𨺗捿𦴣櫊嘑醎畺抅𠏼獏籰𥰡𣳽"],["9fc1","𤤙盖鮝个𠳔莾衂"],["9fc9","届槀僭坺刟巵从氱𠇲伹咜哚劚趂㗾弌㗳"],["9fdb","歒酼龥鮗頮颴骺麨麄煺笔"],["9fe7","毺蠘罸"],["9feb","嘠𪙊蹷齓"],["9ff0","跔蹏鸜踁抂𨍽踨蹵竓𤩷稾磘泪詧瘇"],["a040","𨩚鼦泎蟖痃𪊲硓咢贌狢獱謭猂瓱賫𤪻蘯徺袠䒷"],["a055","𡠻𦸅"],["a058","詾𢔛"],["a05b","惽癧髗鵄鍮鮏蟵"],["a063","蠏賷猬霡鮰㗖犲䰇籑饊𦅙慙䰄麖慽"],["a073","坟慯抦戹拎㩜懢厪𣏵捤栂㗒"],["a0a1","嵗𨯂迚𨸹"],["a0a6","僙𡵆礆匲阸𠼻䁥"],["a0ae","矾"],["a0b0","糂𥼚糚稭聦聣絍甅瓲覔舚朌聢𧒆聛瓰脃眤覉𦟌畓𦻑螩蟎臈螌詉貭譃眫瓸蓚㘵榲趦"],["a0d4","覩瑨涹蟁𤀑瓧㷛煶悤憜㳑煢恷"],["a0e2","罱𨬭牐惩䭾删㰘𣳇𥻗𧙖𥔱𡥄𡋾𩤃𦷜𧂭峁𦆭𨨏𣙷𠃮𦡆𤼎䕢嬟𦍌齐麦𦉫"],["a3c0","␀",31,"␡"],["c6a1","①",9,"⑴",9,"ⅰ",9,"丶丿亅亠冂冖冫勹匸卩厶夊宀巛⼳广廴彐彡攴无疒癶辵隶¨ˆヽヾゝゞ〃仝々〆〇ー[]✽ぁ",23],["c740","す",58,"ァアィイ"],["c7a1","ゥ",81,"А",5,"ЁЖ",4],["c840","Л",26,"ёж",25,"⇧↸↹㇏𠃌乚𠂊刂䒑"],["c8a1","龰冈龱𧘇"],["c8cd","¬¦'"㈱№℡゛゜⺀⺄⺆⺇⺈⺊⺌⺍⺕⺜⺝⺥⺧⺪⺬⺮⺶⺼⺾⻆⻊⻌⻍⻏⻖⻗⻞⻣"],["c8f5","ʃɐɛɔɵœøŋʊɪ"],["f9fe","■"],["fa40","𠕇鋛𠗟𣿅蕌䊵珯况㙉𤥂𨧤鍄𡧛苮𣳈砼杄拟𤤳𨦪𠊠𦮳𡌅侫𢓭倈𦴩𧪄𣘀𤪱𢔓倩𠍾徤𠎀𠍇滛𠐟偽儁㑺儎顬㝃萖𤦤𠒇兠𣎴兪𠯿𢃼𠋥𢔰𠖎𣈳𡦃宂蝽𠖳𣲙冲冸"],["faa1","鴴凉减凑㳜凓𤪦决凢卂凭菍椾𣜭彻刋刦刼劵剗劔効勅簕蕂勠蘍𦬓包𨫞啉滙𣾀𠥔𣿬匳卄𠯢泋𡜦栛珕恊㺪㣌𡛨燝䒢卭却𨚫卾卿𡖖𡘓矦厓𨪛厠厫厮玧𥝲㽙玜叁叅汉义埾叙㪫𠮏叠𣿫𢶣叶𠱷吓灹唫晗浛呭𦭓𠵴啝咏咤䞦𡜍𠻝㶴𠵍"],["fb40","𨦼𢚘啇䳭启琗喆喩嘅𡣗𤀺䕒𤐵暳𡂴嘷曍𣊊暤暭噍噏磱囱鞇叾圀囯园𨭦㘣𡉏坆𤆥汮炋坂㚱𦱾埦𡐖堃𡑔𤍣堦𤯵塜墪㕡壠壜𡈼壻寿坃𪅐𤉸鏓㖡够梦㛃湙"],["fba1","𡘾娤啓𡚒蔅姉𠵎𦲁𦴪𡟜姙𡟻𡞲𦶦浱𡠨𡛕姹𦹅媫婣㛦𤦩婷㜈媖瑥嫓𦾡𢕔㶅𡤑㜲𡚸広勐孶斈孼𧨎䀄䡝𠈄寕慠𡨴𥧌𠖥寳宝䴐尅𡭄尓珎尔𡲥𦬨屉䣝岅峩峯嶋𡷹𡸷崐崘嵆𡺤岺巗苼㠭𤤁𢁉𢅳芇㠶㯂帮檊幵幺𤒼𠳓厦亷廐厨𡝱帉廴𨒂"],["fc40","廹廻㢠廼栾鐛弍𠇁弢㫞䢮𡌺强𦢈𢏐彘𢑱彣鞽𦹮彲鍀𨨶徧嶶㵟𥉐𡽪𧃸𢙨釖𠊞𨨩怱暅𡡷㥣㷇㘹垐𢞴祱㹀悞悤悳𤦂𤦏𧩓璤僡媠慤萤慂慈𦻒憁凴𠙖憇宪𣾷"],["fca1","𢡟懓𨮝𩥝懐㤲𢦀𢣁怣慜攞掋𠄘担𡝰拕𢸍捬𤧟㨗搸揸𡎎𡟼撐澊𢸶頔𤂌𥜝擡擥鑻㩦携㩗敍漖𤨨𤨣斅敭敟𣁾斵𤥀䬷旑䃘𡠩无旣忟𣐀昘𣇷𣇸晄𣆤𣆥晋𠹵晧𥇦晳晴𡸽𣈱𨗴𣇈𥌓矅𢣷馤朂𤎜𤨡㬫槺𣟂杞杧杢𤇍𩃭柗䓩栢湐鈼栁𣏦𦶠桝"],["fd40","𣑯槡樋𨫟楳棃𣗍椁椀㴲㨁𣘼㮀枬楡𨩊䋼椶榘㮡𠏉荣傐槹𣙙𢄪橅𣜃檝㯳枱櫈𩆜㰍欝𠤣惞欵歴𢟍溵𣫛𠎵𡥘㝀吡𣭚毡𣻼毜氷𢒋𤣱𦭑汚舦汹𣶼䓅𣶽𤆤𤤌𤤀"],["fda1","𣳉㛥㳫𠴲鮃𣇹𢒑羏样𦴥𦶡𦷫涖浜湼漄𤥿𤂅𦹲蔳𦽴凇沜渝萮𨬡港𣸯瑓𣾂秌湏媑𣁋濸㜍澝𣸰滺𡒗𤀽䕕鏰潄潜㵎潴𩅰㴻澟𤅄濓𤂑𤅕𤀹𣿰𣾴𤄿凟𤅖𤅗𤅀𦇝灋灾炧炁烌烕烖烟䄄㷨熴熖𤉷焫煅媈煊煮岜𤍥煏鍢𤋁焬𤑚𤨧𤨢熺𨯨炽爎"],["fe40","鑂爕夑鑃爤鍁𥘅爮牀𤥴梽牕牗㹕𣁄栍漽犂猪猫𤠣𨠫䣭𨠄猨献珏玪𠰺𦨮珉瑉𤇢𡛧𤨤昣㛅𤦷𤦍𤧻珷琕椃𤨦琹𠗃㻗瑜𢢭瑠𨺲瑇珤瑶莹瑬㜰瑴鏱樬璂䥓𤪌"],["fea1","𤅟𤩹𨮏孆𨰃𡢞瓈𡦈甎瓩甞𨻙𡩋寗𨺬鎅畍畊畧畮𤾂㼄𤴓疎瑝疞疴瘂瘬癑癏癯癶𦏵皐臯㟸𦤑𦤎皡皥皷盌𦾟葢𥂝𥅽𡸜眞眦着撯𥈠睘𣊬瞯𨥤𨥨𡛁矴砉𡍶𤨒棊碯磇磓隥礮𥗠磗礴碱𧘌辸袄𨬫𦂃𢘜禆褀椂禀𥡗禝𧬹礼禩渪𧄦㺨秆𩄍秔"]]'); + +/***/ }), + +/***/ 8949: +/***/ ((module) => { + +"use strict"; +module.exports = /*#__PURE__*/JSON.parse('[["0","\\u0000",127,"€"],["8140","丂丄丅丆丏丒丗丟丠両丣並丩丮丯丱丳丵丷丼乀乁乂乄乆乊乑乕乗乚乛乢乣乤乥乧乨乪",5,"乲乴",9,"乿",6,"亇亊"],["8180","亐亖亗亙亜亝亞亣亪亯亰亱亴亶亷亸亹亼亽亾仈仌仏仐仒仚仛仜仠仢仦仧仩仭仮仯仱仴仸仹仺仼仾伀伂",6,"伋伌伒",4,"伜伝伡伣伨伩伬伭伮伱伳伵伷伹伻伾",4,"佄佅佇",5,"佒佔佖佡佢佦佨佪佫佭佮佱佲併佷佸佹佺佽侀侁侂侅來侇侊侌侎侐侒侓侕侖侘侙侚侜侞侟価侢"],["8240","侤侫侭侰",4,"侶",8,"俀俁係俆俇俈俉俋俌俍俒",4,"俙俛俠俢俤俥俧俫俬俰俲俴俵俶俷俹俻俼俽俿",11],["8280","個倎倐們倓倕倖倗倛倝倞倠倢倣値倧倫倯",10,"倻倽倿偀偁偂偄偅偆偉偊偋偍偐",4,"偖偗偘偙偛偝",7,"偦",5,"偭",8,"偸偹偺偼偽傁傂傃傄傆傇傉傊傋傌傎",20,"傤傦傪傫傭",4,"傳",6,"傼"],["8340","傽",17,"僐",5,"僗僘僙僛",10,"僨僩僪僫僯僰僱僲僴僶",4,"僼",9,"儈"],["8380","儉儊儌",5,"儓",13,"儢",28,"兂兇兊兌兎兏児兒兓兗兘兙兛兝",4,"兣兤兦內兩兪兯兲兺兾兿冃冄円冇冊冋冎冏冐冑冓冔冘冚冝冞冟冡冣冦",4,"冭冮冴冸冹冺冾冿凁凂凃凅凈凊凍凎凐凒",5],["8440","凘凙凚凜凞凟凢凣凥",5,"凬凮凱凲凴凷凾刄刅刉刋刌刏刐刓刔刕刜刞刟刡刢刣別刦刧刪刬刯刱刲刴刵刼刾剄",5,"剋剎剏剒剓剕剗剘"],["8480","剙剚剛剝剟剠剢剣剤剦剨剫剬剭剮剰剱剳",9,"剾劀劃",4,"劉",6,"劑劒劔",6,"劜劤劥劦劧劮劯劰労",9,"勀勁勂勄勅勆勈勊勌勍勎勏勑勓勔動勗務",5,"勠勡勢勣勥",10,"勱",7,"勻勼勽匁匂匃匄匇匉匊匋匌匎"],["8540","匑匒匓匔匘匛匜匞匟匢匤匥匧匨匩匫匬匭匯",9,"匼匽區卂卄卆卋卌卍卐協単卙卛卝卥卨卪卬卭卲卶卹卻卼卽卾厀厁厃厇厈厊厎厏"],["8580","厐",4,"厖厗厙厛厜厞厠厡厤厧厪厫厬厭厯",6,"厷厸厹厺厼厽厾叀參",4,"収叏叐叒叓叕叚叜叝叞叡叢叧叴叺叾叿吀吂吅吇吋吔吘吙吚吜吢吤吥吪吰吳吶吷吺吽吿呁呂呄呅呇呉呌呍呎呏呑呚呝",4,"呣呥呧呩",7,"呴呹呺呾呿咁咃咅咇咈咉咊咍咑咓咗咘咜咞咟咠咡"],["8640","咢咥咮咰咲咵咶咷咹咺咼咾哃哅哊哋哖哘哛哠",4,"哫哬哯哰哱哴",5,"哻哾唀唂唃唄唅唈唊",4,"唒唓唕",5,"唜唝唞唟唡唥唦"],["8680","唨唩唫唭唲唴唵唶唸唹唺唻唽啀啂啅啇啈啋",4,"啑啒啓啔啗",4,"啝啞啟啠啢啣啨啩啫啯",5,"啹啺啽啿喅喆喌喍喎喐喒喓喕喖喗喚喛喞喠",6,"喨",8,"喲喴営喸喺喼喿",4,"嗆嗇嗈嗊嗋嗎嗏嗐嗕嗗",4,"嗞嗠嗢嗧嗩嗭嗮嗰嗱嗴嗶嗸",4,"嗿嘂嘃嘄嘅"],["8740","嘆嘇嘊嘋嘍嘐",7,"嘙嘚嘜嘝嘠嘡嘢嘥嘦嘨嘩嘪嘫嘮嘯嘰嘳嘵嘷嘸嘺嘼嘽嘾噀",11,"噏",4,"噕噖噚噛噝",4],["8780","噣噥噦噧噭噮噯噰噲噳噴噵噷噸噹噺噽",7,"嚇",6,"嚐嚑嚒嚔",14,"嚤",10,"嚰",6,"嚸嚹嚺嚻嚽",12,"囋",8,"囕囖囘囙囜団囥",5,"囬囮囯囲図囶囷囸囻囼圀圁圂圅圇國",6],["8840","園",9,"圝圞圠圡圢圤圥圦圧圫圱圲圴",4,"圼圽圿坁坃坄坅坆坈坉坋坒",4,"坘坙坢坣坥坧坬坮坰坱坲坴坵坸坹坺坽坾坿垀"],["8880","垁垇垈垉垊垍",4,"垔",6,"垜垝垞垟垥垨垪垬垯垰垱垳垵垶垷垹",8,"埄",6,"埌埍埐埑埓埖埗埛埜埞埡埢埣埥",7,"埮埰埱埲埳埵埶執埻埼埾埿堁堃堄堅堈堉堊堌堎堏堐堒堓堔堖堗堘堚堛堜堝堟堢堣堥",4,"堫",4,"報堲堳場堶",7],["8940","堾",5,"塅",6,"塎塏塐塒塓塕塖塗塙",4,"塟",5,"塦",4,"塭",16,"塿墂墄墆墇墈墊墋墌"],["8980","墍",4,"墔",4,"墛墜墝墠",7,"墪",17,"墽墾墿壀壂壃壄壆",10,"壒壓壔壖",13,"壥",5,"壭壯壱売壴壵壷壸壺",7,"夃夅夆夈",4,"夎夐夑夒夓夗夘夛夝夞夠夡夢夣夦夨夬夰夲夳夵夶夻"],["8a40","夽夾夿奀奃奅奆奊奌奍奐奒奓奙奛",4,"奡奣奤奦",12,"奵奷奺奻奼奾奿妀妅妉妋妌妎妏妐妑妔妕妘妚妛妜妝妟妠妡妢妦"],["8a80","妧妬妭妰妱妳",5,"妺妼妽妿",6,"姇姈姉姌姍姎姏姕姖姙姛姞",4,"姤姦姧姩姪姫姭",11,"姺姼姽姾娀娂娊娋娍娎娏娐娒娔娕娖娗娙娚娛娝娞娡娢娤娦娧娨娪",6,"娳娵娷",4,"娽娾娿婁",4,"婇婈婋",9,"婖婗婘婙婛",5],["8b40","婡婣婤婥婦婨婩婫",8,"婸婹婻婼婽婾媀",17,"媓",6,"媜",13,"媫媬"],["8b80","媭",4,"媴媶媷媹",4,"媿嫀嫃",5,"嫊嫋嫍",4,"嫓嫕嫗嫙嫚嫛嫝嫞嫟嫢嫤嫥嫧嫨嫪嫬",4,"嫲",22,"嬊",11,"嬘",25,"嬳嬵嬶嬸",7,"孁",6],["8c40","孈",7,"孒孖孞孠孡孧孨孫孭孮孯孲孴孶孷學孹孻孼孾孿宂宆宊宍宎宐宑宒宔宖実宧宨宩宬宭宮宯宱宲宷宺宻宼寀寁寃寈寉寊寋寍寎寏"],["8c80","寑寔",8,"寠寢寣實寧審",4,"寯寱",6,"寽対尀専尃尅將專尋尌對導尐尒尓尗尙尛尞尟尠尡尣尦尨尩尪尫尭尮尯尰尲尳尵尶尷屃屄屆屇屌屍屒屓屔屖屗屘屚屛屜屝屟屢層屧",6,"屰屲",6,"屻屼屽屾岀岃",4,"岉岊岋岎岏岒岓岕岝",4,"岤",4],["8d40","岪岮岯岰岲岴岶岹岺岻岼岾峀峂峃峅",5,"峌",5,"峓",5,"峚",6,"峢峣峧峩峫峬峮峯峱",9,"峼",4],["8d80","崁崄崅崈",5,"崏",4,"崕崗崘崙崚崜崝崟",4,"崥崨崪崫崬崯",4,"崵",7,"崿",7,"嵈嵉嵍",10,"嵙嵚嵜嵞",10,"嵪嵭嵮嵰嵱嵲嵳嵵",12,"嶃",21,"嶚嶛嶜嶞嶟嶠"],["8e40","嶡",21,"嶸",12,"巆",6,"巎",12,"巜巟巠巣巤巪巬巭"],["8e80","巰巵巶巸",4,"巿帀帄帇帉帊帋帍帎帒帓帗帞",7,"帨",4,"帯帰帲",4,"帹帺帾帿幀幁幃幆",5,"幍",6,"幖",4,"幜幝幟幠幣",14,"幵幷幹幾庁庂広庅庈庉庌庍庎庒庘庛庝庡庢庣庤庨",4,"庮",4,"庴庺庻庼庽庿",6],["8f40","廆廇廈廋",5,"廔廕廗廘廙廚廜",11,"廩廫",8,"廵廸廹廻廼廽弅弆弇弉弌弍弎弐弒弔弖弙弚弜弝弞弡弢弣弤"],["8f80","弨弫弬弮弰弲",6,"弻弽弾弿彁",14,"彑彔彙彚彛彜彞彟彠彣彥彧彨彫彮彯彲彴彵彶彸彺彽彾彿徃徆徍徎徏徑従徔徖徚徛徝從徟徠徢",5,"復徫徬徯",5,"徶徸徹徺徻徾",4,"忇忈忊忋忎忓忔忕忚忛応忞忟忢忣忥忦忨忩忬忯忰忲忳忴忶忷忹忺忼怇"],["9040","怈怉怋怌怐怑怓怗怘怚怞怟怢怣怤怬怭怮怰",4,"怶",4,"怽怾恀恄",6,"恌恎恏恑恓恔恖恗恘恛恜恞恟恠恡恥恦恮恱恲恴恵恷恾悀"],["9080","悁悂悅悆悇悈悊悋悎悏悐悑悓悕悗悘悙悜悞悡悢悤悥悧悩悪悮悰悳悵悶悷悹悺悽",7,"惇惈惉惌",4,"惒惓惔惖惗惙惛惞惡",4,"惪惱惲惵惷惸惻",4,"愂愃愄愅愇愊愋愌愐",4,"愖愗愘愙愛愜愝愞愡愢愥愨愩愪愬",18,"慀",6],["9140","慇慉態慍慏慐慒慓慔慖",6,"慞慟慠慡慣慤慥慦慩",6,"慱慲慳慴慶慸",18,"憌憍憏",4,"憕"],["9180","憖",6,"憞",8,"憪憫憭",9,"憸",5,"憿懀懁懃",4,"應懌",4,"懓懕",16,"懧",13,"懶",8,"戀",5,"戇戉戓戔戙戜戝戞戠戣戦戧戨戩戫戭戯戰戱戲戵戶戸",4,"扂扄扅扆扊"],["9240","扏扐払扖扗扙扚扜",6,"扤扥扨扱扲扴扵扷扸扺扻扽抁抂抃抅抆抇抈抋",5,"抔抙抜抝択抣抦抧抩抪抭抮抯抰抲抳抴抶抷抸抺抾拀拁"],["9280","拃拋拏拑拕拝拞拠拡拤拪拫拰拲拵拸拹拺拻挀挃挄挅挆挊挋挌挍挏挐挒挓挔挕挗挘挙挜挦挧挩挬挭挮挰挱挳",5,"挻挼挾挿捀捁捄捇捈捊捑捒捓捔捖",7,"捠捤捥捦捨捪捫捬捯捰捲捳捴捵捸捹捼捽捾捿掁掃掄掅掆掋掍掑掓掔掕掗掙",6,"採掤掦掫掯掱掲掵掶掹掻掽掿揀"],["9340","揁揂揃揅揇揈揊揋揌揑揓揔揕揗",6,"揟揢揤",4,"揫揬揮揯揰揱揳揵揷揹揺揻揼揾搃搄搆",4,"損搎搑搒搕",5,"搝搟搢搣搤"],["9380","搥搧搨搩搫搮",5,"搵",4,"搻搼搾摀摂摃摉摋",6,"摓摕摖摗摙",4,"摟",7,"摨摪摫摬摮",9,"摻",6,"撃撆撈",8,"撓撔撗撘撚撛撜撝撟",4,"撥撦撧撨撪撫撯撱撲撳撴撶撹撻撽撾撿擁擃擄擆",6,"擏擑擓擔擕擖擙據"],["9440","擛擜擝擟擠擡擣擥擧",24,"攁",7,"攊",7,"攓",4,"攙",8],["9480","攢攣攤攦",4,"攬攭攰攱攲攳攷攺攼攽敀",4,"敆敇敊敋敍敎敐敒敓敔敗敘敚敜敟敠敡敤敥敧敨敩敪敭敮敯敱敳敵敶數",14,"斈斉斊斍斎斏斒斔斕斖斘斚斝斞斠斢斣斦斨斪斬斮斱",7,"斺斻斾斿旀旂旇旈旉旊旍旐旑旓旔旕旘",7,"旡旣旤旪旫"],["9540","旲旳旴旵旸旹旻",4,"昁昄昅昇昈昉昋昍昐昑昒昖昗昘昚昛昜昞昡昢昣昤昦昩昪昫昬昮昰昲昳昷",4,"昽昿晀時晄",6,"晍晎晐晑晘"],["9580","晙晛晜晝晞晠晢晣晥晧晩",4,"晱晲晳晵晸晹晻晼晽晿暀暁暃暅暆暈暉暊暋暍暎暏暐暒暓暔暕暘",4,"暞",8,"暩",4,"暯",4,"暵暶暷暸暺暻暼暽暿",25,"曚曞",7,"曧曨曪",5,"曱曵曶書曺曻曽朁朂會"],["9640","朄朅朆朇朌朎朏朑朒朓朖朘朙朚朜朞朠",5,"朧朩朮朰朲朳朶朷朸朹朻朼朾朿杁杄杅杇杊杋杍杒杔杕杗",4,"杝杢杣杤杦杧杫杬杮東杴杶"],["9680","杸杹杺杻杽枀枂枃枅枆枈枊枌枍枎枏枑枒枓枔枖枙枛枟枠枡枤枦枩枬枮枱枲枴枹",7,"柂柅",9,"柕柖柗柛柟柡柣柤柦柧柨柪柫柭柮柲柵",7,"柾栁栂栃栄栆栍栐栒栔栕栘",4,"栞栟栠栢",6,"栫",6,"栴栵栶栺栻栿桇桋桍桏桒桖",5],["9740","桜桝桞桟桪桬",7,"桵桸",8,"梂梄梇",7,"梐梑梒梔梕梖梘",9,"梣梤梥梩梪梫梬梮梱梲梴梶梷梸"],["9780","梹",6,"棁棃",5,"棊棌棎棏棐棑棓棔棖棗棙棛",4,"棡棢棤",9,"棯棲棳棴棶棷棸棻棽棾棿椀椂椃椄椆",4,"椌椏椑椓",11,"椡椢椣椥",7,"椮椯椱椲椳椵椶椷椸椺椻椼椾楀楁楃",16,"楕楖楘楙楛楜楟"],["9840","楡楢楤楥楧楨楩楪楬業楯楰楲",4,"楺楻楽楾楿榁榃榅榊榋榌榎",5,"榖榗榙榚榝",9,"榩榪榬榮榯榰榲榳榵榶榸榹榺榼榽"],["9880","榾榿槀槂",7,"構槍槏槑槒槓槕",5,"槜槝槞槡",11,"槮槯槰槱槳",9,"槾樀",9,"樋",11,"標",5,"樠樢",5,"権樫樬樭樮樰樲樳樴樶",6,"樿",4,"橅橆橈",7,"橑",6,"橚"],["9940","橜",4,"橢橣橤橦",10,"橲",6,"橺橻橽橾橿檁檂檃檅",8,"檏檒",4,"檘",7,"檡",5],["9980","檧檨檪檭",114,"欥欦欨",6],["9a40","欯欰欱欳欴欵欶欸欻欼欽欿歀歁歂歄歅歈歊歋歍",11,"歚",7,"歨歩歫",13,"歺歽歾歿殀殅殈"],["9a80","殌殎殏殐殑殔殕殗殘殙殜",4,"殢",7,"殫",7,"殶殸",6,"毀毃毄毆",4,"毌毎毐毑毘毚毜",4,"毢",7,"毬毭毮毰毱毲毴毶毷毸毺毻毼毾",6,"氈",4,"氎氒気氜氝氞氠氣氥氫氬氭氱氳氶氷氹氺氻氼氾氿汃汄汅汈汋",4,"汑汒汓汖汘"],["9b40","汙汚汢汣汥汦汧汫",4,"汱汳汵汷汸決汻汼汿沀沄沇沊沋沍沎沑沒沕沖沗沘沚沜沝沞沠沢沨沬沯沰沴沵沶沷沺泀況泂泃泆泇泈泋泍泎泏泑泒泘"],["9b80","泙泚泜泝泟泤泦泧泩泬泭泲泴泹泿洀洂洃洅洆洈洉洊洍洏洐洑洓洔洕洖洘洜洝洟",5,"洦洨洩洬洭洯洰洴洶洷洸洺洿浀浂浄浉浌浐浕浖浗浘浛浝浟浡浢浤浥浧浨浫浬浭浰浱浲浳浵浶浹浺浻浽",4,"涃涄涆涇涊涋涍涏涐涒涖",4,"涜涢涥涬涭涰涱涳涴涶涷涹",5,"淁淂淃淈淉淊"],["9c40","淍淎淏淐淒淓淔淕淗淚淛淜淟淢淣淥淧淨淩淪淭淯淰淲淴淵淶淸淺淽",7,"渆渇済渉渋渏渒渓渕渘渙減渜渞渟渢渦渧渨渪測渮渰渱渳渵"],["9c80","渶渷渹渻",7,"湅",7,"湏湐湑湒湕湗湙湚湜湝湞湠",10,"湬湭湯",14,"満溁溂溄溇溈溊",4,"溑",6,"溙溚溛溝溞溠溡溣溤溦溨溩溫溬溭溮溰溳溵溸溹溼溾溿滀滃滄滅滆滈滉滊滌滍滎滐滒滖滘滙滛滜滝滣滧滪",5],["9d40","滰滱滲滳滵滶滷滸滺",7,"漃漄漅漇漈漊",4,"漐漑漒漖",9,"漡漢漣漥漦漧漨漬漮漰漲漴漵漷",6,"漿潀潁潂"],["9d80","潃潄潅潈潉潊潌潎",9,"潙潚潛潝潟潠潡潣潤潥潧",5,"潯潰潱潳潵潶潷潹潻潽",6,"澅澆澇澊澋澏",12,"澝澞澟澠澢",4,"澨",10,"澴澵澷澸澺",5,"濁濃",5,"濊",6,"濓",10,"濟濢濣濤濥"],["9e40","濦",7,"濰",32,"瀒",7,"瀜",6,"瀤",6],["9e80","瀫",9,"瀶瀷瀸瀺",17,"灍灎灐",13,"灟",11,"灮灱灲灳灴灷灹灺灻災炁炂炃炄炆炇炈炋炌炍炏炐炑炓炗炘炚炛炞",12,"炰炲炴炵炶為炾炿烄烅烆烇烉烋",12,"烚"],["9f40","烜烝烞烠烡烢烣烥烪烮烰",6,"烸烺烻烼烾",10,"焋",4,"焑焒焔焗焛",10,"焧",7,"焲焳焴"],["9f80","焵焷",13,"煆煇煈煉煋煍煏",12,"煝煟",4,"煥煩",4,"煯煰煱煴煵煶煷煹煻煼煾",5,"熅",4,"熋熌熍熎熐熑熒熓熕熖熗熚",4,"熡",6,"熩熪熫熭",5,"熴熶熷熸熺",8,"燄",9,"燏",4],["a040","燖",9,"燡燢燣燤燦燨",5,"燯",9,"燺",11,"爇",19],["a080","爛爜爞",9,"爩爫爭爮爯爲爳爴爺爼爾牀",6,"牉牊牋牎牏牐牑牓牔牕牗牘牚牜牞牠牣牤牥牨牪牫牬牭牰牱牳牴牶牷牸牻牼牽犂犃犅",4,"犌犎犐犑犓",11,"犠",11,"犮犱犲犳犵犺",6,"狅狆狇狉狊狋狌狏狑狓狔狕狖狘狚狛"],["a1a1"," 、。·ˉˇ¨〃々—~‖…‘’“”〔〕〈",7,"〖〗【】±×÷∶∧∨∑∏∪∩∈∷√⊥∥∠⌒⊙∫∮≡≌≈∽∝≠≮≯≤≥∞∵∴♂♀°′″℃$¤¢£‰§№☆★○●◎◇◆□■△▲※→←↑↓〓"],["a2a1","ⅰ",9],["a2b1","⒈",19,"⑴",19,"①",9],["a2e5","㈠",9],["a2f1","Ⅰ",11],["a3a1","!"#¥%",88," ̄"],["a4a1","ぁ",82],["a5a1","ァ",85],["a6a1","Α",16,"Σ",6],["a6c1","α",16,"σ",6],["a6e0","︵︶︹︺︿﹀︽︾﹁﹂﹃﹄"],["a6ee","︻︼︷︸︱"],["a6f4","︳︴"],["a7a1","А",5,"ЁЖ",25],["a7d1","а",5,"ёж",25],["a840","ˊˋ˙–―‥‵℅℉↖↗↘↙∕∟∣≒≦≧⊿═",35,"▁",6],["a880","█",7,"▓▔▕▼▽◢◣◤◥☉⊕〒〝〞"],["a8a1","āáǎàēéěèīíǐìōóǒòūúǔùǖǘǚǜüêɑ"],["a8bd","ńň"],["a8c0","ɡ"],["a8c5","ㄅ",36],["a940","〡",8,"㊣㎎㎏㎜㎝㎞㎡㏄㏎㏑㏒㏕︰¬¦"],["a959","℡㈱"],["a95c","‐"],["a960","ー゛゜ヽヾ〆ゝゞ﹉",9,"﹔﹕﹖﹗﹙",8],["a980","﹢",4,"﹨﹩﹪﹫"],["a996","〇"],["a9a4","─",75],["aa40","狜狝狟狢",5,"狪狫狵狶狹狽狾狿猀猂猄",5,"猋猌猍猏猐猑猒猔猘猙猚猟猠猣猤猦猧猨猭猯猰猲猳猵猶猺猻猼猽獀",8],["aa80","獉獊獋獌獎獏獑獓獔獕獖獘",7,"獡",10,"獮獰獱"],["ab40","獲",11,"獿",4,"玅玆玈玊玌玍玏玐玒玓玔玕玗玘玙玚玜玝玞玠玡玣",5,"玪玬玭玱玴玵玶玸玹玼玽玾玿珁珃",4],["ab80","珋珌珎珒",6,"珚珛珜珝珟珡珢珣珤珦珨珪珫珬珮珯珰珱珳",4],["ac40","珸",10,"琄琇琈琋琌琍琎琑",8,"琜",5,"琣琤琧琩琫琭琯琱琲琷",4,"琽琾琿瑀瑂",11],["ac80","瑎",6,"瑖瑘瑝瑠",12,"瑮瑯瑱",4,"瑸瑹瑺"],["ad40","瑻瑼瑽瑿璂璄璅璆璈璉璊璌璍璏璑",10,"璝璟",7,"璪",15,"璻",12],["ad80","瓈",9,"瓓",8,"瓝瓟瓡瓥瓧",6,"瓰瓱瓲"],["ae40","瓳瓵瓸",6,"甀甁甂甃甅",7,"甎甐甒甔甕甖甗甛甝甞甠",4,"甦甧甪甮甴甶甹甼甽甿畁畂畃畄畆畇畉畊畍畐畑畒畓畕畖畗畘"],["ae80","畝",7,"畧畨畩畫",6,"畳畵當畷畺",4,"疀疁疂疄疅疇"],["af40","疈疉疊疌疍疎疐疓疕疘疛疜疞疢疦",4,"疭疶疷疺疻疿痀痁痆痋痌痎痏痐痑痓痗痙痚痜痝痟痠痡痥痩痬痭痮痯痲痳痵痶痷痸痺痻痽痾瘂瘄瘆瘇"],["af80","瘈瘉瘋瘍瘎瘏瘑瘒瘓瘔瘖瘚瘜瘝瘞瘡瘣瘧瘨瘬瘮瘯瘱瘲瘶瘷瘹瘺瘻瘽癁療癄"],["b040","癅",6,"癎",5,"癕癗",4,"癝癟癠癡癢癤",6,"癬癭癮癰",7,"癹発發癿皀皁皃皅皉皊皌皍皏皐皒皔皕皗皘皚皛"],["b080","皜",7,"皥",8,"皯皰皳皵",9,"盀盁盃啊阿埃挨哎唉哀皑癌蔼矮艾碍爱隘鞍氨安俺按暗岸胺案肮昂盎凹敖熬翱袄傲奥懊澳芭捌扒叭吧笆八疤巴拔跋靶把耙坝霸罢爸白柏百摆佰败拜稗斑班搬扳般颁板版扮拌伴瓣半办绊邦帮梆榜膀绑棒磅蚌镑傍谤苞胞包褒剥"],["b140","盄盇盉盋盌盓盕盙盚盜盝盞盠",4,"盦",7,"盰盳盵盶盷盺盻盽盿眀眂眃眅眆眊県眎",10,"眛眜眝眞眡眣眤眥眧眪眫"],["b180","眬眮眰",4,"眹眻眽眾眿睂睄睅睆睈",7,"睒",7,"睜薄雹保堡饱宝抱报暴豹鲍爆杯碑悲卑北辈背贝钡倍狈备惫焙被奔苯本笨崩绷甭泵蹦迸逼鼻比鄙笔彼碧蓖蔽毕毙毖币庇痹闭敝弊必辟壁臂避陛鞭边编贬扁便变卞辨辩辫遍标彪膘表鳖憋别瘪彬斌濒滨宾摈兵冰柄丙秉饼炳"],["b240","睝睞睟睠睤睧睩睪睭",11,"睺睻睼瞁瞂瞃瞆",5,"瞏瞐瞓",11,"瞡瞣瞤瞦瞨瞫瞭瞮瞯瞱瞲瞴瞶",4],["b280","瞼瞾矀",12,"矎",8,"矘矙矚矝",4,"矤病并玻菠播拨钵波博勃搏铂箔伯帛舶脖膊渤泊驳捕卜哺补埠不布步簿部怖擦猜裁材才财睬踩采彩菜蔡餐参蚕残惭惨灿苍舱仓沧藏操糙槽曹草厕策侧册测层蹭插叉茬茶查碴搽察岔差诧拆柴豺搀掺蝉馋谗缠铲产阐颤昌猖"],["b340","矦矨矪矯矰矱矲矴矵矷矹矺矻矼砃",5,"砊砋砎砏砐砓砕砙砛砞砠砡砢砤砨砪砫砮砯砱砲砳砵砶砽砿硁硂硃硄硆硈硉硊硋硍硏硑硓硔硘硙硚"],["b380","硛硜硞",11,"硯",7,"硸硹硺硻硽",6,"场尝常长偿肠厂敞畅唱倡超抄钞朝嘲潮巢吵炒车扯撤掣彻澈郴臣辰尘晨忱沉陈趁衬撑称城橙成呈乘程惩澄诚承逞骋秤吃痴持匙池迟弛驰耻齿侈尺赤翅斥炽充冲虫崇宠抽酬畴踌稠愁筹仇绸瞅丑臭初出橱厨躇锄雏滁除楚"],["b440","碄碅碆碈碊碋碏碐碒碔碕碖碙碝碞碠碢碤碦碨",7,"碵碶碷碸確碻碼碽碿磀磂磃磄磆磇磈磌磍磎磏磑磒磓磖磗磘磚",9],["b480","磤磥磦磧磩磪磫磭",4,"磳磵磶磸磹磻",5,"礂礃礄礆",6,"础储矗搐触处揣川穿椽传船喘串疮窗幢床闯创吹炊捶锤垂春椿醇唇淳纯蠢戳绰疵茨磁雌辞慈瓷词此刺赐次聪葱囱匆从丛凑粗醋簇促蹿篡窜摧崔催脆瘁粹淬翠村存寸磋撮搓措挫错搭达答瘩打大呆歹傣戴带殆代贷袋待逮"],["b540","礍",5,"礔",9,"礟",4,"礥",14,"礵",4,"礽礿祂祃祄祅祇祊",8,"祔祕祘祙祡祣"],["b580","祤祦祩祪祫祬祮祰",6,"祹祻",4,"禂禃禆禇禈禉禋禌禍禎禐禑禒怠耽担丹单郸掸胆旦氮但惮淡诞弹蛋当挡党荡档刀捣蹈倒岛祷导到稻悼道盗德得的蹬灯登等瞪凳邓堤低滴迪敌笛狄涤翟嫡抵底地蒂第帝弟递缔颠掂滇碘点典靛垫电佃甸店惦奠淀殿碉叼雕凋刁掉吊钓调跌爹碟蝶迭谍叠"],["b640","禓",6,"禛",11,"禨",10,"禴",4,"禼禿秂秄秅秇秈秊秌秎秏秐秓秔秖秗秙",5,"秠秡秢秥秨秪"],["b680","秬秮秱",6,"秹秺秼秾秿稁稄稅稇稈稉稊稌稏",4,"稕稖稘稙稛稜丁盯叮钉顶鼎锭定订丢东冬董懂动栋侗恫冻洞兜抖斗陡豆逗痘都督毒犊独读堵睹赌杜镀肚度渡妒端短锻段断缎堆兑队对墩吨蹲敦顿囤钝盾遁掇哆多夺垛躲朵跺舵剁惰堕蛾峨鹅俄额讹娥恶厄扼遏鄂饿恩而儿耳尔饵洱二"],["b740","稝稟稡稢稤",14,"稴稵稶稸稺稾穀",5,"穇",9,"穒",4,"穘",16],["b780","穩",6,"穱穲穳穵穻穼穽穾窂窅窇窉窊窋窌窎窏窐窓窔窙窚窛窞窡窢贰发罚筏伐乏阀法珐藩帆番翻樊矾钒繁凡烦反返范贩犯饭泛坊芳方肪房防妨仿访纺放菲非啡飞肥匪诽吠肺废沸费芬酚吩氛分纷坟焚汾粉奋份忿愤粪丰封枫蜂峰锋风疯烽逢冯缝讽奉凤佛否夫敷肤孵扶拂辐幅氟符伏俘服"],["b840","窣窤窧窩窪窫窮",4,"窴",10,"竀",10,"竌",9,"竗竘竚竛竜竝竡竢竤竧",5,"竮竰竱竲竳"],["b880","竴",4,"竻竼竾笀笁笂笅笇笉笌笍笎笐笒笓笖笗笘笚笜笝笟笡笢笣笧笩笭浮涪福袱弗甫抚辅俯釜斧脯腑府腐赴副覆赋复傅付阜父腹负富讣附妇缚咐噶嘎该改概钙盖溉干甘杆柑竿肝赶感秆敢赣冈刚钢缸肛纲岗港杠篙皋高膏羔糕搞镐稿告哥歌搁戈鸽胳疙割革葛格蛤阁隔铬个各给根跟耕更庚羹"],["b940","笯笰笲笴笵笶笷笹笻笽笿",5,"筆筈筊筍筎筓筕筗筙筜筞筟筡筣",10,"筯筰筳筴筶筸筺筼筽筿箁箂箃箄箆",6,"箎箏"],["b980","箑箒箓箖箘箙箚箛箞箟箠箣箤箥箮箯箰箲箳箵箶箷箹",7,"篂篃範埂耿梗工攻功恭龚供躬公宫弓巩汞拱贡共钩勾沟苟狗垢构购够辜菇咕箍估沽孤姑鼓古蛊骨谷股故顾固雇刮瓜剐寡挂褂乖拐怪棺关官冠观管馆罐惯灌贯光广逛瑰规圭硅归龟闺轨鬼诡癸桂柜跪贵刽辊滚棍锅郭国果裹过哈"],["ba40","篅篈築篊篋篍篎篏篐篒篔",4,"篛篜篞篟篠篢篣篤篧篨篩篫篬篭篯篰篲",4,"篸篹篺篻篽篿",7,"簈簉簊簍簎簐",5,"簗簘簙"],["ba80","簚",4,"簠",5,"簨簩簫",12,"簹",5,"籂骸孩海氦亥害骇酣憨邯韩含涵寒函喊罕翰撼捍旱憾悍焊汗汉夯杭航壕嚎豪毫郝好耗号浩呵喝荷菏核禾和何合盒貉阂河涸赫褐鹤贺嘿黑痕很狠恨哼亨横衡恒轰哄烘虹鸿洪宏弘红喉侯猴吼厚候后呼乎忽瑚壶葫胡蝴狐糊湖"],["bb40","籃",9,"籎",36,"籵",5,"籾",9],["bb80","粈粊",6,"粓粔粖粙粚粛粠粡粣粦粧粨粩粫粬粭粯粰粴",4,"粺粻弧虎唬护互沪户花哗华猾滑画划化话槐徊怀淮坏欢环桓还缓换患唤痪豢焕涣宦幻荒慌黄磺蝗簧皇凰惶煌晃幌恍谎灰挥辉徽恢蛔回毁悔慧卉惠晦贿秽会烩汇讳诲绘荤昏婚魂浑混豁活伙火获或惑霍货祸击圾基机畸稽积箕"],["bc40","粿糀糂糃糄糆糉糋糎",6,"糘糚糛糝糞糡",6,"糩",5,"糰",7,"糹糺糼",13,"紋",5],["bc80","紑",14,"紡紣紤紥紦紨紩紪紬紭紮細",6,"肌饥迹激讥鸡姬绩缉吉极棘辑籍集及急疾汲即嫉级挤几脊己蓟技冀季伎祭剂悸济寄寂计记既忌际妓继纪嘉枷夹佳家加荚颊贾甲钾假稼价架驾嫁歼监坚尖笺间煎兼肩艰奸缄茧检柬碱硷拣捡简俭剪减荐槛鉴践贱见键箭件"],["bd40","紷",54,"絯",7],["bd80","絸",32,"健舰剑饯渐溅涧建僵姜将浆江疆蒋桨奖讲匠酱降蕉椒礁焦胶交郊浇骄娇嚼搅铰矫侥脚狡角饺缴绞剿教酵轿较叫窖揭接皆秸街阶截劫节桔杰捷睫竭洁结解姐戒藉芥界借介疥诫届巾筋斤金今津襟紧锦仅谨进靳晋禁近烬浸"],["be40","継",12,"綧",6,"綯",42],["be80","線",32,"尽劲荆兢茎睛晶鲸京惊精粳经井警景颈静境敬镜径痉靖竟竞净炯窘揪究纠玖韭久灸九酒厩救旧臼舅咎就疚鞠拘狙疽居驹菊局咀矩举沮聚拒据巨具距踞锯俱句惧炬剧捐鹃娟倦眷卷绢撅攫抉掘倔爵觉决诀绝均菌钧军君峻"],["bf40","緻",62],["bf80","縺縼",4,"繂",4,"繈",21,"俊竣浚郡骏喀咖卡咯开揩楷凯慨刊堪勘坎砍看康慷糠扛抗亢炕考拷烤靠坷苛柯棵磕颗科壳咳可渴克刻客课肯啃垦恳坑吭空恐孔控抠口扣寇枯哭窟苦酷库裤夸垮挎跨胯块筷侩快宽款匡筐狂框矿眶旷况亏盔岿窥葵奎魁傀"],["c040","繞",35,"纃",23,"纜纝纞"],["c080","纮纴纻纼绖绤绬绹缊缐缞缷缹缻",6,"罃罆",9,"罒罓馈愧溃坤昆捆困括扩廓阔垃拉喇蜡腊辣啦莱来赖蓝婪栏拦篮阑兰澜谰揽览懒缆烂滥琅榔狼廊郎朗浪捞劳牢老佬姥酪烙涝勒乐雷镭蕾磊累儡垒擂肋类泪棱楞冷厘梨犁黎篱狸离漓理李里鲤礼莉荔吏栗丽厉励砾历利傈例俐"],["c140","罖罙罛罜罝罞罠罣",4,"罫罬罭罯罰罳罵罶罷罸罺罻罼罽罿羀羂",7,"羋羍羏",4,"羕",4,"羛羜羠羢羣羥羦羨",6,"羱"],["c180","羳",4,"羺羻羾翀翂翃翄翆翇翈翉翋翍翏",4,"翖翗翙",5,"翢翣痢立粒沥隶力璃哩俩联莲连镰廉怜涟帘敛脸链恋炼练粮凉梁粱良两辆量晾亮谅撩聊僚疗燎寥辽潦了撂镣廖料列裂烈劣猎琳林磷霖临邻鳞淋凛赁吝拎玲菱零龄铃伶羚凌灵陵岭领另令溜琉榴硫馏留刘瘤流柳六龙聋咙笼窿"],["c240","翤翧翨翪翫翬翭翯翲翴",6,"翽翾翿耂耇耈耉耊耎耏耑耓耚耛耝耞耟耡耣耤耫",5,"耲耴耹耺耼耾聀聁聄聅聇聈聉聎聏聐聑聓聕聖聗"],["c280","聙聛",13,"聫",5,"聲",11,"隆垄拢陇楼娄搂篓漏陋芦卢颅庐炉掳卤虏鲁麓碌露路赂鹿潞禄录陆戮驴吕铝侣旅履屡缕虑氯律率滤绿峦挛孪滦卵乱掠略抡轮伦仑沦纶论萝螺罗逻锣箩骡裸落洛骆络妈麻玛码蚂马骂嘛吗埋买麦卖迈脉瞒馒蛮满蔓曼慢漫"],["c340","聾肁肂肅肈肊肍",5,"肔肕肗肙肞肣肦肧肨肬肰肳肵肶肸肹肻胅胇",4,"胏",6,"胘胟胠胢胣胦胮胵胷胹胻胾胿脀脁脃脄脅脇脈脋"],["c380","脌脕脗脙脛脜脝脟",12,"脭脮脰脳脴脵脷脹",4,"脿谩芒茫盲氓忙莽猫茅锚毛矛铆卯茂冒帽貌贸么玫枚梅酶霉煤没眉媒镁每美昧寐妹媚门闷们萌蒙檬盟锰猛梦孟眯醚靡糜迷谜弥米秘觅泌蜜密幂棉眠绵冕免勉娩缅面苗描瞄藐秒渺庙妙蔑灭民抿皿敏悯闽明螟鸣铭名命谬摸"],["c440","腀",5,"腇腉腍腎腏腒腖腗腘腛",4,"腡腢腣腤腦腨腪腫腬腯腲腳腵腶腷腸膁膃",4,"膉膋膌膍膎膐膒",5,"膙膚膞",4,"膤膥"],["c480","膧膩膫",7,"膴",5,"膼膽膾膿臄臅臇臈臉臋臍",6,"摹蘑模膜磨摩魔抹末莫墨默沫漠寞陌谋牟某拇牡亩姆母墓暮幕募慕木目睦牧穆拿哪呐钠那娜纳氖乃奶耐奈南男难囊挠脑恼闹淖呢馁内嫩能妮霓倪泥尼拟你匿腻逆溺蔫拈年碾撵捻念娘酿鸟尿捏聂孽啮镊镍涅您柠狞凝宁"],["c540","臔",14,"臤臥臦臨臩臫臮",4,"臵",5,"臽臿舃與",4,"舎舏舑舓舕",5,"舝舠舤舥舦舧舩舮舲舺舼舽舿"],["c580","艀艁艂艃艅艆艈艊艌艍艎艐",7,"艙艛艜艝艞艠",7,"艩拧泞牛扭钮纽脓浓农弄奴努怒女暖虐疟挪懦糯诺哦欧鸥殴藕呕偶沤啪趴爬帕怕琶拍排牌徘湃派攀潘盘磐盼畔判叛乓庞旁耪胖抛咆刨炮袍跑泡呸胚培裴赔陪配佩沛喷盆砰抨烹澎彭蓬棚硼篷膨朋鹏捧碰坯砒霹批披劈琵毗"],["c640","艪艫艬艭艱艵艶艷艸艻艼芀芁芃芅芆芇芉芌芐芓芔芕芖芚芛芞芠芢芣芧芲芵芶芺芻芼芿苀苂苃苅苆苉苐苖苙苚苝苢苧苨苩苪苬苭苮苰苲苳苵苶苸"],["c680","苺苼",4,"茊茋茍茐茒茓茖茘茙茝",9,"茩茪茮茰茲茷茻茽啤脾疲皮匹痞僻屁譬篇偏片骗飘漂瓢票撇瞥拼频贫品聘乒坪苹萍平凭瓶评屏坡泼颇婆破魄迫粕剖扑铺仆莆葡菩蒲埔朴圃普浦谱曝瀑期欺栖戚妻七凄漆柒沏其棋奇歧畦崎脐齐旗祈祁骑起岂乞企启契砌器气迄弃汽泣讫掐"],["c740","茾茿荁荂荄荅荈荊",4,"荓荕",4,"荝荢荰",6,"荹荺荾",6,"莇莈莊莋莌莍莏莐莑莔莕莖莗莙莚莝莟莡",6,"莬莭莮"],["c780","莯莵莻莾莿菂菃菄菆菈菉菋菍菎菐菑菒菓菕菗菙菚菛菞菢菣菤菦菧菨菫菬菭恰洽牵扦钎铅千迁签仟谦乾黔钱钳前潜遣浅谴堑嵌欠歉枪呛腔羌墙蔷强抢橇锹敲悄桥瞧乔侨巧鞘撬翘峭俏窍切茄且怯窃钦侵亲秦琴勤芹擒禽寝沁青轻氢倾卿清擎晴氰情顷请庆琼穷秋丘邱球求囚酋泅趋区蛆曲躯屈驱渠"],["c840","菮華菳",4,"菺菻菼菾菿萀萂萅萇萈萉萊萐萒",5,"萙萚萛萞",5,"萩",7,"萲",5,"萹萺萻萾",7,"葇葈葉"],["c880","葊",6,"葒",4,"葘葝葞葟葠葢葤",4,"葪葮葯葰葲葴葷葹葻葼取娶龋趣去圈颧权醛泉全痊拳犬券劝缺炔瘸却鹊榷确雀裙群然燃冉染瓤壤攘嚷让饶扰绕惹热壬仁人忍韧任认刃妊纫扔仍日戎茸蓉荣融熔溶容绒冗揉柔肉茹蠕儒孺如辱乳汝入褥软阮蕊瑞锐闰润若弱撒洒萨腮鳃塞赛三叁"],["c940","葽",4,"蒃蒄蒅蒆蒊蒍蒏",7,"蒘蒚蒛蒝蒞蒟蒠蒢",12,"蒰蒱蒳蒵蒶蒷蒻蒼蒾蓀蓂蓃蓅蓆蓇蓈蓋蓌蓎蓏蓒蓔蓕蓗"],["c980","蓘",4,"蓞蓡蓢蓤蓧",4,"蓭蓮蓯蓱",10,"蓽蓾蔀蔁蔂伞散桑嗓丧搔骚扫嫂瑟色涩森僧莎砂杀刹沙纱傻啥煞筛晒珊苫杉山删煽衫闪陕擅赡膳善汕扇缮墒伤商赏晌上尚裳梢捎稍烧芍勺韶少哨邵绍奢赊蛇舌舍赦摄射慑涉社设砷申呻伸身深娠绅神沈审婶甚肾慎渗声生甥牲升绳"],["ca40","蔃",8,"蔍蔎蔏蔐蔒蔔蔕蔖蔘蔙蔛蔜蔝蔞蔠蔢",8,"蔭",9,"蔾",4,"蕄蕅蕆蕇蕋",10],["ca80","蕗蕘蕚蕛蕜蕝蕟",4,"蕥蕦蕧蕩",8,"蕳蕵蕶蕷蕸蕼蕽蕿薀薁省盛剩胜圣师失狮施湿诗尸虱十石拾时什食蚀实识史矢使屎驶始式示士世柿事拭誓逝势是嗜噬适仕侍释饰氏市恃室视试收手首守寿授售受瘦兽蔬枢梳殊抒输叔舒淑疏书赎孰熟薯暑曙署蜀黍鼠属术述树束戍竖墅庶数漱"],["cb40","薂薃薆薈",6,"薐",10,"薝",6,"薥薦薧薩薫薬薭薱",5,"薸薺",6,"藂",6,"藊",4,"藑藒"],["cb80","藔藖",5,"藝",6,"藥藦藧藨藪",14,"恕刷耍摔衰甩帅栓拴霜双爽谁水睡税吮瞬顺舜说硕朔烁斯撕嘶思私司丝死肆寺嗣四伺似饲巳松耸怂颂送宋讼诵搜艘擞嗽苏酥俗素速粟僳塑溯宿诉肃酸蒜算虽隋随绥髓碎岁穗遂隧祟孙损笋蓑梭唆缩琐索锁所塌他它她塔"],["cc40","藹藺藼藽藾蘀",4,"蘆",10,"蘒蘓蘔蘕蘗",15,"蘨蘪",13,"蘹蘺蘻蘽蘾蘿虀"],["cc80","虁",11,"虒虓處",4,"虛虜虝號虠虡虣",7,"獭挞蹋踏胎苔抬台泰酞太态汰坍摊贪瘫滩坛檀痰潭谭谈坦毯袒碳探叹炭汤塘搪堂棠膛唐糖倘躺淌趟烫掏涛滔绦萄桃逃淘陶讨套特藤腾疼誊梯剔踢锑提题蹄啼体替嚏惕涕剃屉天添填田甜恬舔腆挑条迢眺跳贴铁帖厅听烃"],["cd40","虭虯虰虲",6,"蚃",6,"蚎",4,"蚔蚖",5,"蚞",4,"蚥蚦蚫蚭蚮蚲蚳蚷蚸蚹蚻",4,"蛁蛂蛃蛅蛈蛌蛍蛒蛓蛕蛖蛗蛚蛜"],["cd80","蛝蛠蛡蛢蛣蛥蛦蛧蛨蛪蛫蛬蛯蛵蛶蛷蛺蛻蛼蛽蛿蜁蜄蜅蜆蜋蜌蜎蜏蜐蜑蜔蜖汀廷停亭庭挺艇通桐酮瞳同铜彤童桶捅筒统痛偷投头透凸秃突图徒途涂屠土吐兔湍团推颓腿蜕褪退吞屯臀拖托脱鸵陀驮驼椭妥拓唾挖哇蛙洼娃瓦袜歪外豌弯湾玩顽丸烷完碗挽晚皖惋宛婉万腕汪王亡枉网往旺望忘妄威"],["ce40","蜙蜛蜝蜟蜠蜤蜦蜧蜨蜪蜫蜬蜭蜯蜰蜲蜳蜵蜶蜸蜹蜺蜼蜽蝀",6,"蝊蝋蝍蝏蝐蝑蝒蝔蝕蝖蝘蝚",5,"蝡蝢蝦",7,"蝯蝱蝲蝳蝵"],["ce80","蝷蝸蝹蝺蝿螀螁螄螆螇螉螊螌螎",4,"螔螕螖螘",6,"螠",4,"巍微危韦违桅围唯惟为潍维苇萎委伟伪尾纬未蔚味畏胃喂魏位渭谓尉慰卫瘟温蚊文闻纹吻稳紊问嗡翁瓮挝蜗涡窝我斡卧握沃巫呜钨乌污诬屋无芜梧吾吴毋武五捂午舞伍侮坞戊雾晤物勿务悟误昔熙析西硒矽晰嘻吸锡牺"],["cf40","螥螦螧螩螪螮螰螱螲螴螶螷螸螹螻螼螾螿蟁",4,"蟇蟈蟉蟌",4,"蟔",6,"蟜蟝蟞蟟蟡蟢蟣蟤蟦蟧蟨蟩蟫蟬蟭蟯",9],["cf80","蟺蟻蟼蟽蟿蠀蠁蠂蠄",5,"蠋",7,"蠔蠗蠘蠙蠚蠜",4,"蠣稀息希悉膝夕惜熄烯溪汐犀檄袭席习媳喜铣洗系隙戏细瞎虾匣霞辖暇峡侠狭下厦夏吓掀锨先仙鲜纤咸贤衔舷闲涎弦嫌显险现献县腺馅羡宪陷限线相厢镶香箱襄湘乡翔祥详想响享项巷橡像向象萧硝霄削哮嚣销消宵淆晓"],["d040","蠤",13,"蠳",5,"蠺蠻蠽蠾蠿衁衂衃衆",5,"衎",5,"衕衖衘衚",6,"衦衧衪衭衯衱衳衴衵衶衸衹衺"],["d080","衻衼袀袃袆袇袉袊袌袎袏袐袑袓袔袕袗",4,"袝",4,"袣袥",5,"小孝校肖啸笑效楔些歇蝎鞋协挟携邪斜胁谐写械卸蟹懈泄泻谢屑薪芯锌欣辛新忻心信衅星腥猩惺兴刑型形邢行醒幸杏性姓兄凶胸匈汹雄熊休修羞朽嗅锈秀袖绣墟戌需虚嘘须徐许蓄酗叙旭序畜恤絮婿绪续轩喧宣悬旋玄"],["d140","袬袮袯袰袲",4,"袸袹袺袻袽袾袿裀裃裄裇裈裊裋裌裍裏裐裑裓裖裗裚",4,"裠裡裦裧裩",6,"裲裵裶裷裺裻製裿褀褁褃",5],["d180","褉褋",4,"褑褔",4,"褜",4,"褢褣褤褦褧褨褩褬褭褮褯褱褲褳褵褷选癣眩绚靴薛学穴雪血勋熏循旬询寻驯巡殉汛训讯逊迅压押鸦鸭呀丫芽牙蚜崖衙涯雅哑亚讶焉咽阉烟淹盐严研蜒岩延言颜阎炎沿奄掩眼衍演艳堰燕厌砚雁唁彦焰宴谚验殃央鸯秧杨扬佯疡羊洋阳氧仰痒养样漾邀腰妖瑶"],["d240","褸",8,"襂襃襅",24,"襠",5,"襧",19,"襼"],["d280","襽襾覀覂覄覅覇",26,"摇尧遥窑谣姚咬舀药要耀椰噎耶爷野冶也页掖业叶曳腋夜液一壹医揖铱依伊衣颐夷遗移仪胰疑沂宜姨彝椅蚁倚已乙矣以艺抑易邑屹亿役臆逸肄疫亦裔意毅忆义益溢诣议谊译异翼翌绎茵荫因殷音阴姻吟银淫寅饮尹引隐"],["d340","覢",30,"觃觍觓觔觕觗觘觙觛觝觟觠觡觢觤觧觨觩觪觬觭觮觰觱觲觴",6],["d380","觻",4,"訁",5,"計",21,"印英樱婴鹰应缨莹萤营荧蝇迎赢盈影颖硬映哟拥佣臃痈庸雍踊蛹咏泳涌永恿勇用幽优悠忧尤由邮铀犹油游酉有友右佑釉诱又幼迂淤于盂榆虞愚舆余俞逾鱼愉渝渔隅予娱雨与屿禹宇语羽玉域芋郁吁遇喻峪御愈欲狱育誉"],["d440","訞",31,"訿",8,"詉",21],["d480","詟",25,"詺",6,"浴寓裕预豫驭鸳渊冤元垣袁原援辕园员圆猿源缘远苑愿怨院曰约越跃钥岳粤月悦阅耘云郧匀陨允运蕴酝晕韵孕匝砸杂栽哉灾宰载再在咱攒暂赞赃脏葬遭糟凿藻枣早澡蚤躁噪造皂灶燥责择则泽贼怎增憎曾赠扎喳渣札轧"],["d540","誁",7,"誋",7,"誔",46],["d580","諃",32,"铡闸眨栅榨咋乍炸诈摘斋宅窄债寨瞻毡詹粘沾盏斩辗崭展蘸栈占战站湛绽樟章彰漳张掌涨杖丈帐账仗胀瘴障招昭找沼赵照罩兆肇召遮折哲蛰辙者锗蔗这浙珍斟真甄砧臻贞针侦枕疹诊震振镇阵蒸挣睁征狰争怔整拯正政"],["d640","諤",34,"謈",27],["d680","謤謥謧",30,"帧症郑证芝枝支吱蜘知肢脂汁之织职直植殖执值侄址指止趾只旨纸志挚掷至致置帜峙制智秩稚质炙痔滞治窒中盅忠钟衷终种肿重仲众舟周州洲诌粥轴肘帚咒皱宙昼骤珠株蛛朱猪诸诛逐竹烛煮拄瞩嘱主著柱助蛀贮铸筑"],["d740","譆",31,"譧",4,"譭",25],["d780","讇",24,"讬讱讻诇诐诪谉谞住注祝驻抓爪拽专砖转撰赚篆桩庄装妆撞壮状椎锥追赘坠缀谆准捉拙卓桌琢茁酌啄着灼浊兹咨资姿滋淄孜紫仔籽滓子自渍字鬃棕踪宗综总纵邹走奏揍租足卒族祖诅阻组钻纂嘴醉最罪尊遵昨左佐柞做作坐座"],["d840","谸",8,"豂豃豄豅豈豊豋豍",7,"豖豗豘豙豛",5,"豣",6,"豬",6,"豴豵豶豷豻",6,"貃貄貆貇"],["d880","貈貋貍",6,"貕貖貗貙",20,"亍丌兀丐廿卅丕亘丞鬲孬噩丨禺丿匕乇夭爻卮氐囟胤馗毓睾鼗丶亟鼐乜乩亓芈孛啬嘏仄厍厝厣厥厮靥赝匚叵匦匮匾赜卦卣刂刈刎刭刳刿剀剌剞剡剜蒯剽劂劁劐劓冂罔亻仃仉仂仨仡仫仞伛仳伢佤仵伥伧伉伫佞佧攸佚佝"],["d940","貮",62],["d980","賭",32,"佟佗伲伽佶佴侑侉侃侏佾佻侪佼侬侔俦俨俪俅俚俣俜俑俟俸倩偌俳倬倏倮倭俾倜倌倥倨偾偃偕偈偎偬偻傥傧傩傺僖儆僭僬僦僮儇儋仝氽佘佥俎龠汆籴兮巽黉馘冁夔勹匍訇匐凫夙兕亠兖亳衮袤亵脔裒禀嬴蠃羸冫冱冽冼"],["da40","贎",14,"贠赑赒赗赟赥赨赩赪赬赮赯赱赲赸",8,"趂趃趆趇趈趉趌",4,"趒趓趕",9,"趠趡"],["da80","趢趤",12,"趲趶趷趹趻趽跀跁跂跅跇跈跉跊跍跐跒跓跔凇冖冢冥讠讦讧讪讴讵讷诂诃诋诏诎诒诓诔诖诘诙诜诟诠诤诨诩诮诰诳诶诹诼诿谀谂谄谇谌谏谑谒谔谕谖谙谛谘谝谟谠谡谥谧谪谫谮谯谲谳谵谶卩卺阝阢阡阱阪阽阼陂陉陔陟陧陬陲陴隈隍隗隰邗邛邝邙邬邡邴邳邶邺"],["db40","跕跘跙跜跠跡跢跥跦跧跩跭跮跰跱跲跴跶跼跾",6,"踆踇踈踋踍踎踐踑踒踓踕",7,"踠踡踤",4,"踫踭踰踲踳踴踶踷踸踻踼踾"],["db80","踿蹃蹅蹆蹌",4,"蹓",5,"蹚",11,"蹧蹨蹪蹫蹮蹱邸邰郏郅邾郐郄郇郓郦郢郜郗郛郫郯郾鄄鄢鄞鄣鄱鄯鄹酃酆刍奂劢劬劭劾哿勐勖勰叟燮矍廴凵凼鬯厶弁畚巯坌垩垡塾墼壅壑圩圬圪圳圹圮圯坜圻坂坩垅坫垆坼坻坨坭坶坳垭垤垌垲埏垧垴垓垠埕埘埚埙埒垸埴埯埸埤埝"],["dc40","蹳蹵蹷",4,"蹽蹾躀躂躃躄躆躈",6,"躑躒躓躕",6,"躝躟",11,"躭躮躰躱躳",6,"躻",7],["dc80","軃",10,"軏",21,"堋堍埽埭堀堞堙塄堠塥塬墁墉墚墀馨鼙懿艹艽艿芏芊芨芄芎芑芗芙芫芸芾芰苈苊苣芘芷芮苋苌苁芩芴芡芪芟苄苎芤苡茉苷苤茏茇苜苴苒苘茌苻苓茑茚茆茔茕苠苕茜荑荛荜茈莒茼茴茱莛荞茯荏荇荃荟荀茗荠茭茺茳荦荥"],["dd40","軥",62],["dd80","輤",32,"荨茛荩荬荪荭荮莰荸莳莴莠莪莓莜莅荼莶莩荽莸荻莘莞莨莺莼菁萁菥菘堇萘萋菝菽菖萜萸萑萆菔菟萏萃菸菹菪菅菀萦菰菡葜葑葚葙葳蒇蒈葺蒉葸萼葆葩葶蒌蒎萱葭蓁蓍蓐蓦蒽蓓蓊蒿蒺蓠蒡蒹蒴蒗蓥蓣蔌甍蔸蓰蔹蔟蔺"],["de40","轅",32,"轪辀辌辒辝辠辡辢辤辥辦辧辪辬辭辮辯農辳辴辵辷辸辺辻込辿迀迃迆"],["de80","迉",4,"迏迒迖迗迚迠迡迣迧迬迯迱迲迴迵迶迺迻迼迾迿逇逈逌逎逓逕逘蕖蔻蓿蓼蕙蕈蕨蕤蕞蕺瞢蕃蕲蕻薤薨薇薏蕹薮薜薅薹薷薰藓藁藜藿蘧蘅蘩蘖蘼廾弈夼奁耷奕奚奘匏尢尥尬尴扌扪抟抻拊拚拗拮挢拶挹捋捃掭揶捱捺掎掴捭掬掊捩掮掼揲揸揠揿揄揞揎摒揆掾摅摁搋搛搠搌搦搡摞撄摭撖"],["df40","這逜連逤逥逧",5,"逰",4,"逷逹逺逽逿遀遃遅遆遈",4,"過達違遖遙遚遜",5,"遤遦遧適遪遫遬遯",4,"遶",6,"遾邁"],["df80","還邅邆邇邉邊邌",4,"邒邔邖邘邚邜邞邟邠邤邥邧邨邩邫邭邲邷邼邽邿郀摺撷撸撙撺擀擐擗擤擢攉攥攮弋忒甙弑卟叱叽叩叨叻吒吖吆呋呒呓呔呖呃吡呗呙吣吲咂咔呷呱呤咚咛咄呶呦咝哐咭哂咴哒咧咦哓哔呲咣哕咻咿哌哙哚哜咩咪咤哝哏哞唛哧唠哽唔哳唢唣唏唑唧唪啧喏喵啉啭啁啕唿啐唼"],["e040","郂郃郆郈郉郋郌郍郒郔郕郖郘郙郚郞郟郠郣郤郥郩郪郬郮郰郱郲郳郵郶郷郹郺郻郼郿鄀鄁鄃鄅",19,"鄚鄛鄜"],["e080","鄝鄟鄠鄡鄤",10,"鄰鄲",6,"鄺",8,"酄唷啖啵啶啷唳唰啜喋嗒喃喱喹喈喁喟啾嗖喑啻嗟喽喾喔喙嗪嗷嗉嘟嗑嗫嗬嗔嗦嗝嗄嗯嗥嗲嗳嗌嗍嗨嗵嗤辔嘞嘈嘌嘁嘤嘣嗾嘀嘧嘭噘嘹噗嘬噍噢噙噜噌噔嚆噤噱噫噻噼嚅嚓嚯囔囗囝囡囵囫囹囿圄圊圉圜帏帙帔帑帱帻帼"],["e140","酅酇酈酑酓酔酕酖酘酙酛酜酟酠酦酧酨酫酭酳酺酻酼醀",4,"醆醈醊醎醏醓",6,"醜",5,"醤",5,"醫醬醰醱醲醳醶醷醸醹醻"],["e180","醼",10,"釈釋釐釒",9,"針",8,"帷幄幔幛幞幡岌屺岍岐岖岈岘岙岑岚岜岵岢岽岬岫岱岣峁岷峄峒峤峋峥崂崃崧崦崮崤崞崆崛嵘崾崴崽嵬嵛嵯嵝嵫嵋嵊嵩嵴嶂嶙嶝豳嶷巅彳彷徂徇徉後徕徙徜徨徭徵徼衢彡犭犰犴犷犸狃狁狎狍狒狨狯狩狲狴狷猁狳猃狺"],["e240","釦",62],["e280","鈥",32,"狻猗猓猡猊猞猝猕猢猹猥猬猸猱獐獍獗獠獬獯獾舛夥飧夤夂饣饧",5,"饴饷饽馀馄馇馊馍馐馑馓馔馕庀庑庋庖庥庠庹庵庾庳赓廒廑廛廨廪膺忄忉忖忏怃忮怄忡忤忾怅怆忪忭忸怙怵怦怛怏怍怩怫怊怿怡恸恹恻恺恂"],["e340","鉆",45,"鉵",16],["e380","銆",7,"銏",24,"恪恽悖悚悭悝悃悒悌悛惬悻悱惝惘惆惚悴愠愦愕愣惴愀愎愫慊慵憬憔憧憷懔懵忝隳闩闫闱闳闵闶闼闾阃阄阆阈阊阋阌阍阏阒阕阖阗阙阚丬爿戕氵汔汜汊沣沅沐沔沌汨汩汴汶沆沩泐泔沭泷泸泱泗沲泠泖泺泫泮沱泓泯泾"],["e440","銨",5,"銯",24,"鋉",31],["e480","鋩",32,"洹洧洌浃浈洇洄洙洎洫浍洮洵洚浏浒浔洳涑浯涞涠浞涓涔浜浠浼浣渚淇淅淞渎涿淠渑淦淝淙渖涫渌涮渫湮湎湫溲湟溆湓湔渲渥湄滟溱溘滠漭滢溥溧溽溻溷滗溴滏溏滂溟潢潆潇漤漕滹漯漶潋潴漪漉漩澉澍澌潸潲潼潺濑"],["e540","錊",51,"錿",10],["e580","鍊",31,"鍫濉澧澹澶濂濡濮濞濠濯瀚瀣瀛瀹瀵灏灞宀宄宕宓宥宸甯骞搴寤寮褰寰蹇謇辶迓迕迥迮迤迩迦迳迨逅逄逋逦逑逍逖逡逵逶逭逯遄遑遒遐遨遘遢遛暹遴遽邂邈邃邋彐彗彖彘尻咫屐屙孱屣屦羼弪弩弭艴弼鬻屮妁妃妍妩妪妣"],["e640","鍬",34,"鎐",27],["e680","鎬",29,"鏋鏌鏍妗姊妫妞妤姒妲妯姗妾娅娆姝娈姣姘姹娌娉娲娴娑娣娓婀婧婊婕娼婢婵胬媪媛婷婺媾嫫媲嫒嫔媸嫠嫣嫱嫖嫦嫘嫜嬉嬗嬖嬲嬷孀尕尜孚孥孳孑孓孢驵驷驸驺驿驽骀骁骅骈骊骐骒骓骖骘骛骜骝骟骠骢骣骥骧纟纡纣纥纨纩"],["e740","鏎",7,"鏗",54],["e780","鐎",32,"纭纰纾绀绁绂绉绋绌绐绔绗绛绠绡绨绫绮绯绱绲缍绶绺绻绾缁缂缃缇缈缋缌缏缑缒缗缙缜缛缟缡",6,"缪缫缬缭缯",4,"缵幺畿巛甾邕玎玑玮玢玟珏珂珑玷玳珀珉珈珥珙顼琊珩珧珞玺珲琏琪瑛琦琥琨琰琮琬"],["e840","鐯",14,"鐿",43,"鑬鑭鑮鑯"],["e880","鑰",20,"钑钖钘铇铏铓铔铚铦铻锜锠琛琚瑁瑜瑗瑕瑙瑷瑭瑾璜璎璀璁璇璋璞璨璩璐璧瓒璺韪韫韬杌杓杞杈杩枥枇杪杳枘枧杵枨枞枭枋杷杼柰栉柘栊柩枰栌柙枵柚枳柝栀柃枸柢栎柁柽栲栳桠桡桎桢桄桤梃栝桕桦桁桧桀栾桊桉栩梵梏桴桷梓桫棂楮棼椟椠棹"],["e940","锧锳锽镃镈镋镕镚镠镮镴镵長",7,"門",42],["e980","閫",32,"椤棰椋椁楗棣椐楱椹楠楂楝榄楫榀榘楸椴槌榇榈槎榉楦楣楹榛榧榻榫榭槔榱槁槊槟榕槠榍槿樯槭樗樘橥槲橄樾檠橐橛樵檎橹樽樨橘橼檑檐檩檗檫猷獒殁殂殇殄殒殓殍殚殛殡殪轫轭轱轲轳轵轶轸轷轹轺轼轾辁辂辄辇辋"],["ea40","闌",27,"闬闿阇阓阘阛阞阠阣",6,"阫阬阭阯阰阷阸阹阺阾陁陃陊陎陏陑陒陓陖陗"],["ea80","陘陙陚陜陝陞陠陣陥陦陫陭",4,"陳陸",12,"隇隉隊辍辎辏辘辚軎戋戗戛戟戢戡戥戤戬臧瓯瓴瓿甏甑甓攴旮旯旰昊昙杲昃昕昀炅曷昝昴昱昶昵耆晟晔晁晏晖晡晗晷暄暌暧暝暾曛曜曦曩贲贳贶贻贽赀赅赆赈赉赇赍赕赙觇觊觋觌觎觏觐觑牮犟牝牦牯牾牿犄犋犍犏犒挈挲掰"],["eb40","隌階隑隒隓隕隖隚際隝",9,"隨",7,"隱隲隴隵隷隸隺隻隿雂雃雈雊雋雐雑雓雔雖",9,"雡",6,"雫"],["eb80","雬雭雮雰雱雲雴雵雸雺電雼雽雿霂霃霅霊霋霌霐霑霒霔霕霗",4,"霝霟霠搿擘耄毪毳毽毵毹氅氇氆氍氕氘氙氚氡氩氤氪氲攵敕敫牍牒牖爰虢刖肟肜肓肼朊肽肱肫肭肴肷胧胨胩胪胛胂胄胙胍胗朐胝胫胱胴胭脍脎胲胼朕脒豚脶脞脬脘脲腈腌腓腴腙腚腱腠腩腼腽腭腧塍媵膈膂膑滕膣膪臌朦臊膻"],["ec40","霡",8,"霫霬霮霯霱霳",4,"霺霻霼霽霿",18,"靔靕靗靘靚靜靝靟靣靤靦靧靨靪",7],["ec80","靲靵靷",4,"靽",7,"鞆",4,"鞌鞎鞏鞐鞓鞕鞖鞗鞙",4,"臁膦欤欷欹歃歆歙飑飒飓飕飙飚殳彀毂觳斐齑斓於旆旄旃旌旎旒旖炀炜炖炝炻烀炷炫炱烨烊焐焓焖焯焱煳煜煨煅煲煊煸煺熘熳熵熨熠燠燔燧燹爝爨灬焘煦熹戾戽扃扈扉礻祀祆祉祛祜祓祚祢祗祠祯祧祺禅禊禚禧禳忑忐"],["ed40","鞞鞟鞡鞢鞤",6,"鞬鞮鞰鞱鞳鞵",46],["ed80","韤韥韨韮",4,"韴韷",23,"怼恝恚恧恁恙恣悫愆愍慝憩憝懋懑戆肀聿沓泶淼矶矸砀砉砗砘砑斫砭砜砝砹砺砻砟砼砥砬砣砩硎硭硖硗砦硐硇硌硪碛碓碚碇碜碡碣碲碹碥磔磙磉磬磲礅磴礓礤礞礴龛黹黻黼盱眄眍盹眇眈眚眢眙眭眦眵眸睐睑睇睃睚睨"],["ee40","頏",62],["ee80","顎",32,"睢睥睿瞍睽瞀瞌瞑瞟瞠瞰瞵瞽町畀畎畋畈畛畲畹疃罘罡罟詈罨罴罱罹羁罾盍盥蠲钅钆钇钋钊钌钍钏钐钔钗钕钚钛钜钣钤钫钪钭钬钯钰钲钴钶",4,"钼钽钿铄铈",6,"铐铑铒铕铖铗铙铘铛铞铟铠铢铤铥铧铨铪"],["ef40","顯",5,"颋颎颒颕颙颣風",37,"飏飐飔飖飗飛飜飝飠",4],["ef80","飥飦飩",30,"铩铫铮铯铳铴铵铷铹铼铽铿锃锂锆锇锉锊锍锎锏锒",4,"锘锛锝锞锟锢锪锫锩锬锱锲锴锶锷锸锼锾锿镂锵镄镅镆镉镌镎镏镒镓镔镖镗镘镙镛镞镟镝镡镢镤",8,"镯镱镲镳锺矧矬雉秕秭秣秫稆嵇稃稂稞稔"],["f040","餈",4,"餎餏餑",28,"餯",26],["f080","饊",9,"饖",12,"饤饦饳饸饹饻饾馂馃馉稹稷穑黏馥穰皈皎皓皙皤瓞瓠甬鸠鸢鸨",4,"鸲鸱鸶鸸鸷鸹鸺鸾鹁鹂鹄鹆鹇鹈鹉鹋鹌鹎鹑鹕鹗鹚鹛鹜鹞鹣鹦",6,"鹱鹭鹳疒疔疖疠疝疬疣疳疴疸痄疱疰痃痂痖痍痣痨痦痤痫痧瘃痱痼痿瘐瘀瘅瘌瘗瘊瘥瘘瘕瘙"],["f140","馌馎馚",10,"馦馧馩",47],["f180","駙",32,"瘛瘼瘢瘠癀瘭瘰瘿瘵癃瘾瘳癍癞癔癜癖癫癯翊竦穸穹窀窆窈窕窦窠窬窨窭窳衤衩衲衽衿袂袢裆袷袼裉裢裎裣裥裱褚裼裨裾裰褡褙褓褛褊褴褫褶襁襦襻疋胥皲皴矜耒耔耖耜耠耢耥耦耧耩耨耱耋耵聃聆聍聒聩聱覃顸颀颃"],["f240","駺",62],["f280","騹",32,"颉颌颍颏颔颚颛颞颟颡颢颥颦虍虔虬虮虿虺虼虻蚨蚍蚋蚬蚝蚧蚣蚪蚓蚩蚶蛄蚵蛎蚰蚺蚱蚯蛉蛏蚴蛩蛱蛲蛭蛳蛐蜓蛞蛴蛟蛘蛑蜃蜇蛸蜈蜊蜍蜉蜣蜻蜞蜥蜮蜚蜾蝈蜴蜱蜩蜷蜿螂蜢蝽蝾蝻蝠蝰蝌蝮螋蝓蝣蝼蝤蝙蝥螓螯螨蟒"],["f340","驚",17,"驲骃骉骍骎骔骕骙骦骩",6,"骲骳骴骵骹骻骽骾骿髃髄髆",4,"髍髎髏髐髒體髕髖髗髙髚髛髜"],["f380","髝髞髠髢髣髤髥髧髨髩髪髬髮髰",8,"髺髼",6,"鬄鬅鬆蟆螈螅螭螗螃螫蟥螬螵螳蟋蟓螽蟑蟀蟊蟛蟪蟠蟮蠖蠓蟾蠊蠛蠡蠹蠼缶罂罄罅舐竺竽笈笃笄笕笊笫笏筇笸笪笙笮笱笠笥笤笳笾笞筘筚筅筵筌筝筠筮筻筢筲筱箐箦箧箸箬箝箨箅箪箜箢箫箴篑篁篌篝篚篥篦篪簌篾篼簏簖簋"],["f440","鬇鬉",5,"鬐鬑鬒鬔",10,"鬠鬡鬢鬤",10,"鬰鬱鬳",7,"鬽鬾鬿魀魆魊魋魌魎魐魒魓魕",5],["f480","魛",32,"簟簪簦簸籁籀臾舁舂舄臬衄舡舢舣舭舯舨舫舸舻舳舴舾艄艉艋艏艚艟艨衾袅袈裘裟襞羝羟羧羯羰羲籼敉粑粝粜粞粢粲粼粽糁糇糌糍糈糅糗糨艮暨羿翎翕翥翡翦翩翮翳糸絷綦綮繇纛麸麴赳趄趔趑趱赧赭豇豉酊酐酎酏酤"],["f540","魼",62],["f580","鮻",32,"酢酡酰酩酯酽酾酲酴酹醌醅醐醍醑醢醣醪醭醮醯醵醴醺豕鹾趸跫踅蹙蹩趵趿趼趺跄跖跗跚跞跎跏跛跆跬跷跸跣跹跻跤踉跽踔踝踟踬踮踣踯踺蹀踹踵踽踱蹉蹁蹂蹑蹒蹊蹰蹶蹼蹯蹴躅躏躔躐躜躞豸貂貊貅貘貔斛觖觞觚觜"],["f640","鯜",62],["f680","鰛",32,"觥觫觯訾謦靓雩雳雯霆霁霈霏霎霪霭霰霾龀龃龅",5,"龌黾鼋鼍隹隼隽雎雒瞿雠銎銮鋈錾鍪鏊鎏鐾鑫鱿鲂鲅鲆鲇鲈稣鲋鲎鲐鲑鲒鲔鲕鲚鲛鲞",5,"鲥",4,"鲫鲭鲮鲰",7,"鲺鲻鲼鲽鳄鳅鳆鳇鳊鳋"],["f740","鰼",62],["f780","鱻鱽鱾鲀鲃鲄鲉鲊鲌鲏鲓鲖鲗鲘鲙鲝鲪鲬鲯鲹鲾",4,"鳈鳉鳑鳒鳚鳛鳠鳡鳌",4,"鳓鳔鳕鳗鳘鳙鳜鳝鳟鳢靼鞅鞑鞒鞔鞯鞫鞣鞲鞴骱骰骷鹘骶骺骼髁髀髅髂髋髌髑魅魃魇魉魈魍魑飨餍餮饕饔髟髡髦髯髫髻髭髹鬈鬏鬓鬟鬣麽麾縻麂麇麈麋麒鏖麝麟黛黜黝黠黟黢黩黧黥黪黯鼢鼬鼯鼹鼷鼽鼾齄"],["f840","鳣",62],["f880","鴢",32],["f940","鵃",62],["f980","鶂",32],["fa40","鶣",62],["fa80","鷢",32],["fb40","鸃",27,"鸤鸧鸮鸰鸴鸻鸼鹀鹍鹐鹒鹓鹔鹖鹙鹝鹟鹠鹡鹢鹥鹮鹯鹲鹴",9,"麀"],["fb80","麁麃麄麅麆麉麊麌",5,"麔",8,"麞麠",5,"麧麨麩麪"],["fc40","麫",8,"麵麶麷麹麺麼麿",4,"黅黆黇黈黊黋黌黐黒黓黕黖黗黙黚點黡黣黤黦黨黫黬黭黮黰",8,"黺黽黿",6],["fc80","鼆",4,"鼌鼏鼑鼒鼔鼕鼖鼘鼚",5,"鼡鼣",8,"鼭鼮鼰鼱"],["fd40","鼲",4,"鼸鼺鼼鼿",4,"齅",10,"齒",38],["fd80","齹",5,"龁龂龍",11,"龜龝龞龡",4,"郎凉秊裏隣"],["fe40","兀嗀﨎﨏﨑﨓﨔礼﨟蘒﨡﨣﨤﨧﨨﨩"]]'); + +/***/ }), + +/***/ 5923: +/***/ ((module) => { + +"use strict"; +module.exports = /*#__PURE__*/JSON.parse('[["0","\\u0000",127],["8141","갂갃갅갆갋",4,"갘갞갟갡갢갣갥",6,"갮갲갳갴"],["8161","갵갶갷갺갻갽갾갿걁",9,"걌걎",5,"걕"],["8181","걖걗걙걚걛걝",18,"걲걳걵걶걹걻",4,"겂겇겈겍겎겏겑겒겓겕",6,"겞겢",5,"겫겭겮겱",6,"겺겾겿곀곂곃곅곆곇곉곊곋곍",7,"곖곘",7,"곢곣곥곦곩곫곭곮곲곴곷",4,"곾곿괁괂괃괅괇",4,"괎괐괒괓"],["8241","괔괕괖괗괙괚괛괝괞괟괡",7,"괪괫괮",5],["8261","괶괷괹괺괻괽",6,"굆굈굊",5,"굑굒굓굕굖굗"],["8281","굙",7,"굢굤",7,"굮굯굱굲굷굸굹굺굾궀궃",4,"궊궋궍궎궏궑",10,"궞",5,"궥",17,"궸",7,"귂귃귅귆귇귉",6,"귒귔",7,"귝귞귟귡귢귣귥",18],["8341","귺귻귽귾긂",5,"긊긌긎",5,"긕",7],["8361","긝",18,"긲긳긵긶긹긻긼"],["8381","긽긾긿깂깄깇깈깉깋깏깑깒깓깕깗",4,"깞깢깣깤깦깧깪깫깭깮깯깱",6,"깺깾",5,"꺆",5,"꺍",46,"꺿껁껂껃껅",6,"껎껒",5,"껚껛껝",8],["8441","껦껧껩껪껬껮",5,"껵껶껷껹껺껻껽",8],["8461","꼆꼉꼊꼋꼌꼎꼏꼑",18],["8481","꼤",7,"꼮꼯꼱꼳꼵",6,"꼾꽀꽄꽅꽆꽇꽊",5,"꽑",10,"꽞",5,"꽦",18,"꽺",5,"꾁꾂꾃꾅꾆꾇꾉",6,"꾒꾓꾔꾖",5,"꾝",26,"꾺꾻꾽꾾"],["8541","꾿꿁",5,"꿊꿌꿏",4,"꿕",6,"꿝",4],["8561","꿢",5,"꿪",5,"꿲꿳꿵꿶꿷꿹",6,"뀂뀃"],["8581","뀅",6,"뀍뀎뀏뀑뀒뀓뀕",6,"뀞",9,"뀩",26,"끆끇끉끋끍끏끐끑끒끖끘끚끛끜끞",29,"끾끿낁낂낃낅",6,"낎낐낒",5,"낛낝낞낣낤"],["8641","낥낦낧낪낰낲낶낷낹낺낻낽",6,"냆냊",5,"냒"],["8661","냓냕냖냗냙",6,"냡냢냣냤냦",10],["8681","냱",22,"넊넍넎넏넑넔넕넖넗넚넞",4,"넦넧넩넪넫넭",6,"넶넺",5,"녂녃녅녆녇녉",6,"녒녓녖녗녙녚녛녝녞녟녡",22,"녺녻녽녾녿놁놃",4,"놊놌놎놏놐놑놕놖놗놙놚놛놝"],["8741","놞",9,"놩",15],["8761","놹",18,"뇍뇎뇏뇑뇒뇓뇕"],["8781","뇖",5,"뇞뇠",7,"뇪뇫뇭뇮뇯뇱",7,"뇺뇼뇾",5,"눆눇눉눊눍",6,"눖눘눚",5,"눡",18,"눵",6,"눽",26,"뉙뉚뉛뉝뉞뉟뉡",6,"뉪",4],["8841","뉯",4,"뉶",5,"뉽",6,"늆늇늈늊",4],["8861","늏늒늓늕늖늗늛",4,"늢늤늧늨늩늫늭늮늯늱늲늳늵늶늷"],["8881","늸",15,"닊닋닍닎닏닑닓",4,"닚닜닞닟닠닡닣닧닩닪닰닱닲닶닼닽닾댂댃댅댆댇댉",6,"댒댖",5,"댝",54,"덗덙덚덝덠덡덢덣"],["8941","덦덨덪덬덭덯덲덳덵덶덷덹",6,"뎂뎆",5,"뎍"],["8961","뎎뎏뎑뎒뎓뎕",10,"뎢",5,"뎩뎪뎫뎭"],["8981","뎮",21,"돆돇돉돊돍돏돑돒돓돖돘돚돜돞돟돡돢돣돥돦돧돩",18,"돽",18,"됑",6,"됙됚됛됝됞됟됡",6,"됪됬",7,"됵",15],["8a41","둅",10,"둒둓둕둖둗둙",6,"둢둤둦"],["8a61","둧",4,"둭",18,"뒁뒂"],["8a81","뒃",4,"뒉",19,"뒞",5,"뒥뒦뒧뒩뒪뒫뒭",7,"뒶뒸뒺",5,"듁듂듃듅듆듇듉",6,"듑듒듓듔듖",5,"듞듟듡듢듥듧",4,"듮듰듲",5,"듹",26,"딖딗딙딚딝"],["8b41","딞",5,"딦딫",4,"딲딳딵딶딷딹",6,"땂땆"],["8b61","땇땈땉땊땎땏땑땒땓땕",6,"땞땢",8],["8b81","땫",52,"떢떣떥떦떧떩떬떭떮떯떲떶",4,"떾떿뗁뗂뗃뗅",6,"뗎뗒",5,"뗙",18,"뗭",18],["8c41","똀",15,"똒똓똕똖똗똙",4],["8c61","똞",6,"똦",5,"똭",6,"똵",5],["8c81","똻",12,"뙉",26,"뙥뙦뙧뙩",50,"뚞뚟뚡뚢뚣뚥",5,"뚭뚮뚯뚰뚲",16],["8d41","뛃",16,"뛕",8],["8d61","뛞",17,"뛱뛲뛳뛵뛶뛷뛹뛺"],["8d81","뛻",4,"뜂뜃뜄뜆",33,"뜪뜫뜭뜮뜱",6,"뜺뜼",7,"띅띆띇띉띊띋띍",6,"띖",9,"띡띢띣띥띦띧띩",6,"띲띴띶",5,"띾띿랁랂랃랅",6,"랎랓랔랕랚랛랝랞"],["8e41","랟랡",6,"랪랮",5,"랶랷랹",8],["8e61","럂",4,"럈럊",19],["8e81","럞",13,"럮럯럱럲럳럵",6,"럾렂",4,"렊렋렍렎렏렑",6,"렚렜렞",5,"렦렧렩렪렫렭",6,"렶렺",5,"롁롂롃롅",11,"롒롔",7,"롞롟롡롢롣롥",6,"롮롰롲",5,"롹롺롻롽",7],["8f41","뢅",7,"뢎",17],["8f61","뢠",7,"뢩",6,"뢱뢲뢳뢵뢶뢷뢹",4],["8f81","뢾뢿룂룄룆",5,"룍룎룏룑룒룓룕",7,"룞룠룢",5,"룪룫룭룮룯룱",6,"룺룼룾",5,"뤅",18,"뤙",6,"뤡",26,"뤾뤿륁륂륃륅",6,"륍륎륐륒",5],["9041","륚륛륝륞륟륡",6,"륪륬륮",5,"륶륷륹륺륻륽"],["9061","륾",5,"릆릈릋릌릏",15],["9081","릟",12,"릮릯릱릲릳릵",6,"릾맀맂",5,"맊맋맍맓",4,"맚맜맟맠맢맦맧맩맪맫맭",6,"맶맻",4,"먂",5,"먉",11,"먖",33,"먺먻먽먾먿멁멃멄멅멆"],["9141","멇멊멌멏멐멑멒멖멗멙멚멛멝",6,"멦멪",5],["9161","멲멳멵멶멷멹",9,"몆몈몉몊몋몍",5],["9181","몓",20,"몪몭몮몯몱몳",4,"몺몼몾",5,"뫅뫆뫇뫉",14,"뫚",33,"뫽뫾뫿묁묂묃묅",7,"묎묐묒",5,"묙묚묛묝묞묟묡",6],["9241","묨묪묬",7,"묷묹묺묿",4,"뭆뭈뭊뭋뭌뭎뭑뭒"],["9261","뭓뭕뭖뭗뭙",7,"뭢뭤",7,"뭭",4],["9281","뭲",21,"뮉뮊뮋뮍뮎뮏뮑",18,"뮥뮦뮧뮩뮪뮫뮭",6,"뮵뮶뮸",7,"믁믂믃믅믆믇믉",6,"믑믒믔",35,"믺믻믽믾밁"],["9341","밃",4,"밊밎밐밒밓밙밚밠밡밢밣밦밨밪밫밬밮밯밲밳밵"],["9361","밶밷밹",6,"뱂뱆뱇뱈뱊뱋뱎뱏뱑",8],["9381","뱚뱛뱜뱞",37,"벆벇벉벊벍벏",4,"벖벘벛",4,"벢벣벥벦벩",6,"벲벶",5,"벾벿볁볂볃볅",7,"볎볒볓볔볖볗볙볚볛볝",22,"볷볹볺볻볽"],["9441","볾",5,"봆봈봊",5,"봑봒봓봕",8],["9461","봞",5,"봥",6,"봭",12],["9481","봺",5,"뵁",6,"뵊뵋뵍뵎뵏뵑",6,"뵚",9,"뵥뵦뵧뵩",22,"붂붃붅붆붋",4,"붒붔붖붗붘붛붝",6,"붥",10,"붱",6,"붹",24],["9541","뷒뷓뷖뷗뷙뷚뷛뷝",11,"뷪",5,"뷱"],["9561","뷲뷳뷵뷶뷷뷹",6,"븁븂븄븆",5,"븎븏븑븒븓"],["9581","븕",6,"븞븠",35,"빆빇빉빊빋빍빏",4,"빖빘빜빝빞빟빢빣빥빦빧빩빫",4,"빲빶",4,"빾빿뺁뺂뺃뺅",6,"뺎뺒",5,"뺚",13,"뺩",14],["9641","뺸",23,"뻒뻓"],["9661","뻕뻖뻙",6,"뻡뻢뻦",5,"뻭",8],["9681","뻶",10,"뼂",5,"뼊",13,"뼚뼞",33,"뽂뽃뽅뽆뽇뽉",6,"뽒뽓뽔뽖",44],["9741","뾃",16,"뾕",8],["9761","뾞",17,"뾱",7],["9781","뾹",11,"뿆",5,"뿎뿏뿑뿒뿓뿕",6,"뿝뿞뿠뿢",89,"쀽쀾쀿"],["9841","쁀",16,"쁒",5,"쁙쁚쁛"],["9861","쁝쁞쁟쁡",6,"쁪",15],["9881","쁺",21,"삒삓삕삖삗삙",6,"삢삤삦",5,"삮삱삲삷",4,"삾샂샃샄샆샇샊샋샍샎샏샑",6,"샚샞",5,"샦샧샩샪샫샭",6,"샶샸샺",5,"섁섂섃섅섆섇섉",6,"섑섒섓섔섖",5,"섡섢섥섨섩섪섫섮"],["9941","섲섳섴섵섷섺섻섽섾섿셁",6,"셊셎",5,"셖셗"],["9961","셙셚셛셝",6,"셦셪",5,"셱셲셳셵셶셷셹셺셻"],["9981","셼",8,"솆",5,"솏솑솒솓솕솗",4,"솞솠솢솣솤솦솧솪솫솭솮솯솱",11,"솾",5,"쇅쇆쇇쇉쇊쇋쇍",6,"쇕쇖쇙",6,"쇡쇢쇣쇥쇦쇧쇩",6,"쇲쇴",7,"쇾쇿숁숂숃숅",6,"숎숐숒",5,"숚숛숝숞숡숢숣"],["9a41","숤숥숦숧숪숬숮숰숳숵",16],["9a61","쉆쉇쉉",6,"쉒쉓쉕쉖쉗쉙",6,"쉡쉢쉣쉤쉦"],["9a81","쉧",4,"쉮쉯쉱쉲쉳쉵",6,"쉾슀슂",5,"슊",5,"슑",6,"슙슚슜슞",5,"슦슧슩슪슫슮",5,"슶슸슺",33,"싞싟싡싢싥",5,"싮싰싲싳싴싵싷싺싽싾싿쌁",6,"쌊쌋쌎쌏"],["9b41","쌐쌑쌒쌖쌗쌙쌚쌛쌝",6,"쌦쌧쌪",8],["9b61","쌳",17,"썆",7],["9b81","썎",25,"썪썫썭썮썯썱썳",4,"썺썻썾",5,"쎅쎆쎇쎉쎊쎋쎍",50,"쏁",22,"쏚"],["9c41","쏛쏝쏞쏡쏣",4,"쏪쏫쏬쏮",5,"쏶쏷쏹",5],["9c61","쏿",8,"쐉",6,"쐑",9],["9c81","쐛",8,"쐥",6,"쐭쐮쐯쐱쐲쐳쐵",6,"쐾",9,"쑉",26,"쑦쑧쑩쑪쑫쑭",6,"쑶쑷쑸쑺",5,"쒁",18,"쒕",6,"쒝",12],["9d41","쒪",13,"쒹쒺쒻쒽",8],["9d61","쓆",25],["9d81","쓠",8,"쓪",5,"쓲쓳쓵쓶쓷쓹쓻쓼쓽쓾씂",9,"씍씎씏씑씒씓씕",6,"씝",10,"씪씫씭씮씯씱",6,"씺씼씾",5,"앆앇앋앏앐앑앒앖앚앛앜앟앢앣앥앦앧앩",6,"앲앶",5,"앾앿얁얂얃얅얆얈얉얊얋얎얐얒얓얔"],["9e41","얖얙얚얛얝얞얟얡",7,"얪",9,"얶"],["9e61","얷얺얿",4,"엋엍엏엒엓엕엖엗엙",6,"엢엤엦엧"],["9e81","엨엩엪엫엯엱엲엳엵엸엹엺엻옂옃옄옉옊옋옍옎옏옑",6,"옚옝",6,"옦옧옩옪옫옯옱옲옶옸옺옼옽옾옿왂왃왅왆왇왉",6,"왒왖",5,"왞왟왡",10,"왭왮왰왲",5,"왺왻왽왾왿욁",6,"욊욌욎",5,"욖욗욙욚욛욝",6,"욦"],["9f41","욨욪",5,"욲욳욵욶욷욻",4,"웂웄웆",5,"웎"],["9f61","웏웑웒웓웕",6,"웞웟웢",5,"웪웫웭웮웯웱웲"],["9f81","웳",4,"웺웻웼웾",5,"윆윇윉윊윋윍",6,"윖윘윚",5,"윢윣윥윦윧윩",6,"윲윴윶윸윹윺윻윾윿읁읂읃읅",4,"읋읎읐읙읚읛읝읞읟읡",6,"읩읪읬",7,"읶읷읹읺읻읿잀잁잂잆잋잌잍잏잒잓잕잙잛",4,"잢잧",4,"잮잯잱잲잳잵잶잷"],["a041","잸잹잺잻잾쟂",5,"쟊쟋쟍쟏쟑",6,"쟙쟚쟛쟜"],["a061","쟞",5,"쟥쟦쟧쟩쟪쟫쟭",13],["a081","쟻",4,"젂젃젅젆젇젉젋",4,"젒젔젗",4,"젞젟젡젢젣젥",6,"젮젰젲",5,"젹젺젻젽젾젿졁",6,"졊졋졎",5,"졕",26,"졲졳졵졶졷졹졻",4,"좂좄좈좉좊좎",5,"좕",7,"좞좠좢좣좤"],["a141","좥좦좧좩",18,"좾좿죀죁"],["a161","죂죃죅죆죇죉죊죋죍",6,"죖죘죚",5,"죢죣죥"],["a181","죦",14,"죶",5,"죾죿줁줂줃줇",4,"줎 、。·‥…¨〃­―∥\∼‘’“”〔〕〈",9,"±×÷≠≤≥∞∴°′″℃Å¢£¥♂♀∠⊥⌒∂∇≡≒§※☆★○●◎◇◆□■△▲▽▼→←↑↓↔〓≪≫√∽∝∵∫∬∈∋⊆⊇⊂⊃∪∩∧∨¬"],["a241","줐줒",5,"줙",18],["a261","줭",6,"줵",18],["a281","쥈",7,"쥒쥓쥕쥖쥗쥙",6,"쥢쥤",7,"쥭쥮쥯⇒⇔∀∃´~ˇ˘˝˚˙¸˛¡¿ː∮∑∏¤℉‰◁◀▷▶♤♠♡♥♧♣⊙◈▣◐◑▒▤▥▨▧▦▩♨☏☎☜☞¶†‡↕↗↙↖↘♭♩♪♬㉿㈜№㏇™㏂㏘℡€®"],["a341","쥱쥲쥳쥵",6,"쥽",10,"즊즋즍즎즏"],["a361","즑",6,"즚즜즞",16],["a381","즯",16,"짂짃짅짆짉짋",4,"짒짔짗짘짛!",58,"₩]",32," ̄"],["a441","짞짟짡짣짥짦짨짩짪짫짮짲",5,"짺짻짽짾짿쨁쨂쨃쨄"],["a461","쨅쨆쨇쨊쨎",5,"쨕쨖쨗쨙",12],["a481","쨦쨧쨨쨪",28,"ㄱ",93],["a541","쩇",4,"쩎쩏쩑쩒쩓쩕",6,"쩞쩢",5,"쩩쩪"],["a561","쩫",17,"쩾",5,"쪅쪆"],["a581","쪇",16,"쪙",14,"ⅰ",9],["a5b0","Ⅰ",9],["a5c1","Α",16,"Σ",6],["a5e1","α",16,"σ",6],["a641","쪨",19,"쪾쪿쫁쫂쫃쫅"],["a661","쫆",5,"쫎쫐쫒쫔쫕쫖쫗쫚",5,"쫡",6],["a681","쫨쫩쫪쫫쫭",6,"쫵",18,"쬉쬊─│┌┐┘└├┬┤┴┼━┃┏┓┛┗┣┳┫┻╋┠┯┨┷┿┝┰┥┸╂┒┑┚┙┖┕┎┍┞┟┡┢┦┧┩┪┭┮┱┲┵┶┹┺┽┾╀╁╃",7],["a741","쬋",4,"쬑쬒쬓쬕쬖쬗쬙",6,"쬢",7],["a761","쬪",22,"쭂쭃쭄"],["a781","쭅쭆쭇쭊쭋쭍쭎쭏쭑",6,"쭚쭛쭜쭞",5,"쭥",7,"㎕㎖㎗ℓ㎘㏄㎣㎤㎥㎦㎙",9,"㏊㎍㎎㎏㏏㎈㎉㏈㎧㎨㎰",9,"㎀",4,"㎺",5,"㎐",4,"Ω㏀㏁㎊㎋㎌㏖㏅㎭㎮㎯㏛㎩㎪㎫㎬㏝㏐㏓㏃㏉㏜㏆"],["a841","쭭",10,"쭺",14],["a861","쮉",18,"쮝",6],["a881","쮤",19,"쮹",11,"ÆЪĦ"],["a8a6","IJ"],["a8a8","ĿŁØŒºÞŦŊ"],["a8b1","㉠",27,"ⓐ",25,"①",14,"½⅓⅔¼¾⅛⅜⅝⅞"],["a941","쯅",14,"쯕",10],["a961","쯠쯡쯢쯣쯥쯦쯨쯪",18],["a981","쯽",14,"찎찏찑찒찓찕",6,"찞찟찠찣찤æđðħıijĸŀłøœßþŧŋʼn㈀",27,"⒜",25,"⑴",14,"¹²³⁴ⁿ₁₂₃₄"],["aa41","찥찦찪찫찭찯찱",6,"찺찿",4,"챆챇챉챊챋챍챎"],["aa61","챏",4,"챖챚",5,"챡챢챣챥챧챩",6,"챱챲"],["aa81","챳챴챶",29,"ぁ",82],["ab41","첔첕첖첗첚첛첝첞첟첡",6,"첪첮",5,"첶첷첹"],["ab61","첺첻첽",6,"쳆쳈쳊",5,"쳑쳒쳓쳕",5],["ab81","쳛",8,"쳥",6,"쳭쳮쳯쳱",12,"ァ",85],["ac41","쳾쳿촀촂",5,"촊촋촍촎촏촑",6,"촚촜촞촟촠"],["ac61","촡촢촣촥촦촧촩촪촫촭",11,"촺",4],["ac81","촿",28,"쵝쵞쵟А",5,"ЁЖ",25],["acd1","а",5,"ёж",25],["ad41","쵡쵢쵣쵥",6,"쵮쵰쵲",5,"쵹",7],["ad61","춁",6,"춉",10,"춖춗춙춚춛춝춞춟"],["ad81","춠춡춢춣춦춨춪",5,"춱",18,"췅"],["ae41","췆",5,"췍췎췏췑",16],["ae61","췢",5,"췩췪췫췭췮췯췱",6,"췺췼췾",4],["ae81","츃츅츆츇츉츊츋츍",6,"츕츖츗츘츚",5,"츢츣츥츦츧츩츪츫"],["af41","츬츭츮츯츲츴츶",19],["af61","칊",13,"칚칛칝칞칢",5,"칪칬"],["af81","칮",5,"칶칷칹칺칻칽",6,"캆캈캊",5,"캒캓캕캖캗캙"],["b041","캚",5,"캢캦",5,"캮",12],["b061","캻",5,"컂",19],["b081","컖",13,"컦컧컩컪컭",6,"컶컺",5,"가각간갇갈갉갊감",7,"같",4,"갠갤갬갭갯갰갱갸갹갼걀걋걍걔걘걜거걱건걷걸걺검겁것겄겅겆겉겊겋게겐겔겜겝겟겠겡겨격겪견겯결겸겹겻겼경곁계곈곌곕곗고곡곤곧골곪곬곯곰곱곳공곶과곽관괄괆"],["b141","켂켃켅켆켇켉",6,"켒켔켖",5,"켝켞켟켡켢켣"],["b161","켥",6,"켮켲",5,"켹",11],["b181","콅",14,"콖콗콙콚콛콝",6,"콦콨콪콫콬괌괍괏광괘괜괠괩괬괭괴괵괸괼굄굅굇굉교굔굘굡굣구국군굳굴굵굶굻굼굽굿궁궂궈궉권궐궜궝궤궷귀귁귄귈귐귑귓규균귤그극근귿글긁금급긋긍긔기긱긴긷길긺김깁깃깅깆깊까깍깎깐깔깖깜깝깟깠깡깥깨깩깬깰깸"],["b241","콭콮콯콲콳콵콶콷콹",6,"쾁쾂쾃쾄쾆",5,"쾍"],["b261","쾎",18,"쾢",5,"쾩"],["b281","쾪",5,"쾱",18,"쿅",6,"깹깻깼깽꺄꺅꺌꺼꺽꺾껀껄껌껍껏껐껑께껙껜껨껫껭껴껸껼꼇꼈꼍꼐꼬꼭꼰꼲꼴꼼꼽꼿꽁꽂꽃꽈꽉꽐꽜꽝꽤꽥꽹꾀꾄꾈꾐꾑꾕꾜꾸꾹꾼꿀꿇꿈꿉꿋꿍꿎꿔꿜꿨꿩꿰꿱꿴꿸뀀뀁뀄뀌뀐뀔뀜뀝뀨끄끅끈끊끌끎끓끔끕끗끙"],["b341","쿌",19,"쿢쿣쿥쿦쿧쿩"],["b361","쿪",5,"쿲쿴쿶",5,"쿽쿾쿿퀁퀂퀃퀅",5],["b381","퀋",5,"퀒",5,"퀙",19,"끝끼끽낀낄낌낍낏낑나낙낚난낟날낡낢남납낫",4,"낱낳내낵낸낼냄냅냇냈냉냐냑냔냘냠냥너넉넋넌널넒넓넘넙넛넜넝넣네넥넨넬넴넵넷넸넹녀녁년녈념녑녔녕녘녜녠노녹논놀놂놈놉놋농높놓놔놘놜놨뇌뇐뇔뇜뇝"],["b441","퀮",5,"퀶퀷퀹퀺퀻퀽",6,"큆큈큊",5],["b461","큑큒큓큕큖큗큙",6,"큡",10,"큮큯"],["b481","큱큲큳큵",6,"큾큿킀킂",18,"뇟뇨뇩뇬뇰뇹뇻뇽누눅눈눋눌눔눕눗눙눠눴눼뉘뉜뉠뉨뉩뉴뉵뉼늄늅늉느늑는늘늙늚늠늡늣능늦늪늬늰늴니닉닌닐닒님닙닛닝닢다닥닦단닫",4,"닳담답닷",4,"닿대댁댄댈댐댑댓댔댕댜더덕덖던덛덜덞덟덤덥"],["b541","킕",14,"킦킧킩킪킫킭",5],["b561","킳킶킸킺",5,"탂탃탅탆탇탊",5,"탒탖",4],["b581","탛탞탟탡탢탣탥",6,"탮탲",5,"탹",11,"덧덩덫덮데덱덴델뎀뎁뎃뎄뎅뎌뎐뎔뎠뎡뎨뎬도독돈돋돌돎돐돔돕돗동돛돝돠돤돨돼됐되된될됨됩됫됴두둑둔둘둠둡둣둥둬뒀뒈뒝뒤뒨뒬뒵뒷뒹듀듄듈듐듕드득든듣들듦듬듭듯등듸디딕딘딛딜딤딥딧딨딩딪따딱딴딸"],["b641","턅",7,"턎",17],["b661","턠",15,"턲턳턵턶턷턹턻턼턽턾"],["b681","턿텂텆",5,"텎텏텑텒텓텕",6,"텞텠텢",5,"텩텪텫텭땀땁땃땄땅땋때땍땐땔땜땝땟땠땡떠떡떤떨떪떫떰떱떳떴떵떻떼떽뗀뗄뗌뗍뗏뗐뗑뗘뗬또똑똔똘똥똬똴뙈뙤뙨뚜뚝뚠뚤뚫뚬뚱뛔뛰뛴뛸뜀뜁뜅뜨뜩뜬뜯뜰뜸뜹뜻띄띈띌띔띕띠띤띨띰띱띳띵라락란랄람랍랏랐랑랒랖랗"],["b741","텮",13,"텽",6,"톅톆톇톉톊"],["b761","톋",20,"톢톣톥톦톧"],["b781","톩",6,"톲톴톶톷톸톹톻톽톾톿퇁",14,"래랙랜랠램랩랫랬랭랴략랸럇량러럭런럴럼럽럿렀렁렇레렉렌렐렘렙렛렝려력련렬렴렵렷렸령례롄롑롓로록론롤롬롭롯롱롸롼뢍뢨뢰뢴뢸룀룁룃룅료룐룔룝룟룡루룩룬룰룸룹룻룽뤄뤘뤠뤼뤽륀륄륌륏륑류륙륜률륨륩"],["b841","퇐",7,"퇙",17],["b861","퇫",8,"퇵퇶퇷퇹",13],["b881","툈툊",5,"툑",24,"륫륭르륵른를름릅릇릉릊릍릎리릭린릴림립릿링마막만많",4,"맘맙맛망맞맡맣매맥맨맬맴맵맷맸맹맺먀먁먈먕머먹먼멀멂멈멉멋멍멎멓메멕멘멜멤멥멧멨멩며멱면멸몃몄명몇몌모목몫몬몰몲몸몹못몽뫄뫈뫘뫙뫼"],["b941","툪툫툮툯툱툲툳툵",6,"툾퉀퉂",5,"퉉퉊퉋퉌"],["b961","퉍",14,"퉝",6,"퉥퉦퉧퉨"],["b981","퉩",22,"튂튃튅튆튇튉튊튋튌묀묄묍묏묑묘묜묠묩묫무묵묶문묻물묽묾뭄뭅뭇뭉뭍뭏뭐뭔뭘뭡뭣뭬뮈뮌뮐뮤뮨뮬뮴뮷므믄믈믐믓미믹민믿밀밂밈밉밋밌밍및밑바",4,"받",4,"밤밥밧방밭배백밴밸뱀뱁뱃뱄뱅뱉뱌뱍뱐뱝버벅번벋벌벎범법벗"],["ba41","튍튎튏튒튓튔튖",5,"튝튞튟튡튢튣튥",6,"튭"],["ba61","튮튯튰튲",5,"튺튻튽튾틁틃",4,"틊틌",5],["ba81","틒틓틕틖틗틙틚틛틝",6,"틦",9,"틲틳틵틶틷틹틺벙벚베벡벤벧벨벰벱벳벴벵벼벽변별볍볏볐병볕볘볜보복볶본볼봄봅봇봉봐봔봤봬뵀뵈뵉뵌뵐뵘뵙뵤뵨부북분붇불붉붊붐붑붓붕붙붚붜붤붰붸뷔뷕뷘뷜뷩뷰뷴뷸븀븃븅브븍븐블븜븝븟비빅빈빌빎빔빕빗빙빚빛빠빡빤"],["bb41","틻",4,"팂팄팆",5,"팏팑팒팓팕팗",4,"팞팢팣"],["bb61","팤팦팧팪팫팭팮팯팱",6,"팺팾",5,"퍆퍇퍈퍉"],["bb81","퍊",31,"빨빪빰빱빳빴빵빻빼빽뺀뺄뺌뺍뺏뺐뺑뺘뺙뺨뻐뻑뻔뻗뻘뻠뻣뻤뻥뻬뼁뼈뼉뼘뼙뼛뼜뼝뽀뽁뽄뽈뽐뽑뽕뾔뾰뿅뿌뿍뿐뿔뿜뿟뿡쀼쁑쁘쁜쁠쁨쁩삐삑삔삘삠삡삣삥사삭삯산삳살삵삶삼삽삿샀상샅새색샌샐샘샙샛샜생샤"],["bc41","퍪",17,"퍾퍿펁펂펃펅펆펇"],["bc61","펈펉펊펋펎펒",5,"펚펛펝펞펟펡",6,"펪펬펮"],["bc81","펯",4,"펵펶펷펹펺펻펽",6,"폆폇폊",5,"폑",5,"샥샨샬샴샵샷샹섀섄섈섐섕서",4,"섣설섦섧섬섭섯섰성섶세섹센셀셈셉셋셌셍셔셕션셜셤셥셧셨셩셰셴셸솅소속솎손솔솖솜솝솟송솥솨솩솬솰솽쇄쇈쇌쇔쇗쇘쇠쇤쇨쇰쇱쇳쇼쇽숀숄숌숍숏숑수숙순숟술숨숩숫숭"],["bd41","폗폙",7,"폢폤",7,"폮폯폱폲폳폵폶폷"],["bd61","폸폹폺폻폾퐀퐂",5,"퐉",13],["bd81","퐗",5,"퐞",25,"숯숱숲숴쉈쉐쉑쉔쉘쉠쉥쉬쉭쉰쉴쉼쉽쉿슁슈슉슐슘슛슝스슥슨슬슭슴습슷승시식신싣실싫심십싯싱싶싸싹싻싼쌀쌈쌉쌌쌍쌓쌔쌕쌘쌜쌤쌥쌨쌩썅써썩썬썰썲썸썹썼썽쎄쎈쎌쏀쏘쏙쏜쏟쏠쏢쏨쏩쏭쏴쏵쏸쐈쐐쐤쐬쐰"],["be41","퐸",7,"푁푂푃푅",14],["be61","푔",7,"푝푞푟푡푢푣푥",7,"푮푰푱푲"],["be81","푳",4,"푺푻푽푾풁풃",4,"풊풌풎",5,"풕",8,"쐴쐼쐽쑈쑤쑥쑨쑬쑴쑵쑹쒀쒔쒜쒸쒼쓩쓰쓱쓴쓸쓺쓿씀씁씌씐씔씜씨씩씬씰씸씹씻씽아악안앉않알앍앎앓암압앗았앙앝앞애액앤앨앰앱앳앴앵야약얀얄얇얌얍얏양얕얗얘얜얠얩어억언얹얻얼얽얾엄",6,"엌엎"],["bf41","풞",10,"풪",14],["bf61","풹",18,"퓍퓎퓏퓑퓒퓓퓕"],["bf81","퓖",5,"퓝퓞퓠",7,"퓩퓪퓫퓭퓮퓯퓱",6,"퓹퓺퓼에엑엔엘엠엡엣엥여역엮연열엶엷염",5,"옅옆옇예옌옐옘옙옛옜오옥온올옭옮옰옳옴옵옷옹옻와왁완왈왐왑왓왔왕왜왝왠왬왯왱외왹왼욀욈욉욋욍요욕욘욜욤욥욧용우욱운울욹욺움웁웃웅워웍원월웜웝웠웡웨"],["c041","퓾",5,"픅픆픇픉픊픋픍",6,"픖픘",5],["c061","픞",25],["c081","픸픹픺픻픾픿핁핂핃핅",6,"핎핐핒",5,"핚핛핝핞핟핡핢핣웩웬웰웸웹웽위윅윈윌윔윕윗윙유육윤율윰윱윳융윷으윽은을읊음읍읏응",7,"읜읠읨읫이익인일읽읾잃임입잇있잉잊잎자작잔잖잗잘잚잠잡잣잤장잦재잭잰잴잼잽잿쟀쟁쟈쟉쟌쟎쟐쟘쟝쟤쟨쟬저적전절젊"],["c141","핤핦핧핪핬핮",5,"핶핷핹핺핻핽",6,"햆햊햋"],["c161","햌햍햎햏햑",19,"햦햧"],["c181","햨",31,"점접젓정젖제젝젠젤젬젭젯젱져젼졀졈졉졌졍졔조족존졸졺좀좁좃종좆좇좋좌좍좔좝좟좡좨좼좽죄죈죌죔죕죗죙죠죡죤죵주죽준줄줅줆줌줍줏중줘줬줴쥐쥑쥔쥘쥠쥡쥣쥬쥰쥴쥼즈즉즌즐즘즙즛증지직진짇질짊짐집짓"],["c241","헊헋헍헎헏헑헓",4,"헚헜헞",5,"헦헧헩헪헫헭헮"],["c261","헯",4,"헶헸헺",5,"혂혃혅혆혇혉",6,"혒"],["c281","혖",5,"혝혞혟혡혢혣혥",7,"혮",9,"혺혻징짖짙짚짜짝짠짢짤짧짬짭짯짰짱째짹짼쨀쨈쨉쨋쨌쨍쨔쨘쨩쩌쩍쩐쩔쩜쩝쩟쩠쩡쩨쩽쪄쪘쪼쪽쫀쫄쫌쫍쫏쫑쫓쫘쫙쫠쫬쫴쬈쬐쬔쬘쬠쬡쭁쭈쭉쭌쭐쭘쭙쭝쭤쭸쭹쮜쮸쯔쯤쯧쯩찌찍찐찔찜찝찡찢찧차착찬찮찰참찹찻"],["c341","혽혾혿홁홂홃홄홆홇홊홌홎홏홐홒홓홖홗홙홚홛홝",4],["c361","홢",4,"홨홪",5,"홲홳홵",11],["c381","횁횂횄횆",5,"횎횏횑횒횓횕",7,"횞횠횢",5,"횩횪찼창찾채책챈챌챔챕챗챘챙챠챤챦챨챰챵처척천철첨첩첫첬청체첵첸첼쳄쳅쳇쳉쳐쳔쳤쳬쳰촁초촉촌촐촘촙촛총촤촨촬촹최쵠쵤쵬쵭쵯쵱쵸춈추축춘출춤춥춧충춰췄췌췐취췬췰췸췹췻췽츄츈츌츔츙츠측츤츨츰츱츳층"],["c441","횫횭횮횯횱",7,"횺횼",7,"훆훇훉훊훋"],["c461","훍훎훏훐훒훓훕훖훘훚",5,"훡훢훣훥훦훧훩",4],["c481","훮훯훱훲훳훴훶",5,"훾훿휁휂휃휅",11,"휒휓휔치칙친칟칠칡침칩칫칭카칵칸칼캄캅캇캉캐캑캔캘캠캡캣캤캥캬캭컁커컥컨컫컬컴컵컷컸컹케켁켄켈켐켑켓켕켜켠켤켬켭켯켰켱켸코콕콘콜콤콥콧콩콰콱콴콸쾀쾅쾌쾡쾨쾰쿄쿠쿡쿤쿨쿰쿱쿳쿵쿼퀀퀄퀑퀘퀭퀴퀵퀸퀼"],["c541","휕휖휗휚휛휝휞휟휡",6,"휪휬휮",5,"휶휷휹"],["c561","휺휻휽",6,"흅흆흈흊",5,"흒흓흕흚",4],["c581","흟흢흤흦흧흨흪흫흭흮흯흱흲흳흵",6,"흾흿힀힂",5,"힊힋큄큅큇큉큐큔큘큠크큭큰클큼큽킁키킥킨킬킴킵킷킹타탁탄탈탉탐탑탓탔탕태택탠탤탬탭탯탰탱탸턍터턱턴털턺텀텁텃텄텅테텍텐텔템텝텟텡텨텬텼톄톈토톡톤톨톰톱톳통톺톼퇀퇘퇴퇸툇툉툐투툭툰툴툼툽툿퉁퉈퉜"],["c641","힍힎힏힑",6,"힚힜힞",5],["c6a1","퉤튀튁튄튈튐튑튕튜튠튤튬튱트특튼튿틀틂틈틉틋틔틘틜틤틥티틱틴틸팀팁팃팅파팍팎판팔팖팜팝팟팠팡팥패팩팬팰팸팹팻팼팽퍄퍅퍼퍽펀펄펌펍펏펐펑페펙펜펠펨펩펫펭펴편펼폄폅폈평폐폘폡폣포폭폰폴폼폽폿퐁"],["c7a1","퐈퐝푀푄표푠푤푭푯푸푹푼푿풀풂품풉풋풍풔풩퓌퓐퓔퓜퓟퓨퓬퓰퓸퓻퓽프픈플픔픕픗피픽핀필핌핍핏핑하학한할핥함합핫항해핵핸핼햄햅햇했행햐향허헉헌헐헒험헙헛헝헤헥헨헬헴헵헷헹혀혁현혈혐협혓혔형혜혠"],["c8a1","혤혭호혹혼홀홅홈홉홋홍홑화확환활홧황홰홱홴횃횅회획횐횔횝횟횡효횬횰횹횻후훅훈훌훑훔훗훙훠훤훨훰훵훼훽휀휄휑휘휙휜휠휨휩휫휭휴휵휸휼흄흇흉흐흑흔흖흗흘흙흠흡흣흥흩희흰흴흼흽힁히힉힌힐힘힙힛힝"],["caa1","伽佳假價加可呵哥嘉嫁家暇架枷柯歌珂痂稼苛茄街袈訶賈跏軻迦駕刻却各恪慤殼珏脚覺角閣侃刊墾奸姦干幹懇揀杆柬桿澗癎看磵稈竿簡肝艮艱諫間乫喝曷渴碣竭葛褐蝎鞨勘坎堪嵌感憾戡敢柑橄減甘疳監瞰紺邯鑑鑒龕"],["cba1","匣岬甲胛鉀閘剛堈姜岡崗康强彊慷江畺疆糠絳綱羌腔舡薑襁講鋼降鱇介价個凱塏愷愾慨改槪漑疥皆盖箇芥蓋豈鎧開喀客坑更粳羹醵倨去居巨拒据據擧渠炬祛距踞車遽鉅鋸乾件健巾建愆楗腱虔蹇鍵騫乞傑杰桀儉劍劒檢"],["cca1","瞼鈐黔劫怯迲偈憩揭擊格檄激膈覡隔堅牽犬甄絹繭肩見譴遣鵑抉決潔結缺訣兼慊箝謙鉗鎌京俓倞傾儆勁勍卿坰境庚徑慶憬擎敬景暻更梗涇炅烱璟璥瓊痙硬磬竟競絅經耕耿脛莖警輕逕鏡頃頸驚鯨係啓堺契季屆悸戒桂械"],["cda1","棨溪界癸磎稽系繫繼計誡谿階鷄古叩告呱固姑孤尻庫拷攷故敲暠枯槁沽痼皐睾稿羔考股膏苦苽菰藁蠱袴誥賈辜錮雇顧高鼓哭斛曲梏穀谷鵠困坤崑昆梱棍滾琨袞鯤汨滑骨供公共功孔工恐恭拱控攻珙空蚣貢鞏串寡戈果瓜"],["cea1","科菓誇課跨過鍋顆廓槨藿郭串冠官寬慣棺款灌琯瓘管罐菅觀貫關館刮恝括适侊光匡壙廣曠洸炚狂珖筐胱鑛卦掛罫乖傀塊壞怪愧拐槐魁宏紘肱轟交僑咬喬嬌嶠巧攪敎校橋狡皎矯絞翹膠蕎蛟較轎郊餃驕鮫丘久九仇俱具勾"],["cfa1","區口句咎嘔坵垢寇嶇廐懼拘救枸柩構歐毆毬求溝灸狗玖球瞿矩究絿耉臼舅舊苟衢謳購軀逑邱鉤銶駒驅鳩鷗龜國局菊鞠鞫麴君窘群裙軍郡堀屈掘窟宮弓穹窮芎躬倦券勸卷圈拳捲權淃眷厥獗蕨蹶闕机櫃潰詭軌饋句晷歸貴"],["d0a1","鬼龜叫圭奎揆槻珪硅窺竅糾葵規赳逵閨勻均畇筠菌鈞龜橘克剋劇戟棘極隙僅劤勤懃斤根槿瑾筋芹菫覲謹近饉契今妗擒昑檎琴禁禽芩衾衿襟金錦伋及急扱汲級給亘兢矜肯企伎其冀嗜器圻基埼夔奇妓寄岐崎己幾忌技旗旣"],["d1a1","朞期杞棋棄機欺氣汽沂淇玘琦琪璂璣畸畿碁磯祁祇祈祺箕紀綺羈耆耭肌記譏豈起錡錤飢饑騎騏驥麒緊佶吉拮桔金喫儺喇奈娜懦懶拏拿癩",5,"那樂",4,"諾酪駱亂卵暖欄煖爛蘭難鸞捏捺南嵐枏楠湳濫男藍襤拉"],["d2a1","納臘蠟衲囊娘廊",4,"乃來內奈柰耐冷女年撚秊念恬拈捻寧寗努勞奴弩怒擄櫓爐瑙盧",5,"駑魯",10,"濃籠聾膿農惱牢磊腦賂雷尿壘",7,"嫩訥杻紐勒",5,"能菱陵尼泥匿溺多茶"],["d3a1","丹亶但單團壇彖斷旦檀段湍短端簞緞蛋袒鄲鍛撻澾獺疸達啖坍憺擔曇淡湛潭澹痰聃膽蕁覃談譚錟沓畓答踏遝唐堂塘幢戇撞棠當糖螳黨代垈坮大對岱帶待戴擡玳臺袋貸隊黛宅德悳倒刀到圖堵塗導屠島嶋度徒悼挑掉搗桃"],["d4a1","棹櫂淘渡滔濤燾盜睹禱稻萄覩賭跳蹈逃途道都鍍陶韜毒瀆牘犢獨督禿篤纛讀墩惇敦旽暾沌焞燉豚頓乭突仝冬凍動同憧東桐棟洞潼疼瞳童胴董銅兜斗杜枓痘竇荳讀豆逗頭屯臀芚遁遯鈍得嶝橙燈登等藤謄鄧騰喇懶拏癩羅"],["d5a1","蘿螺裸邏樂洛烙珞絡落諾酪駱丹亂卵欄欒瀾爛蘭鸞剌辣嵐擥攬欖濫籃纜藍襤覽拉臘蠟廊朗浪狼琅瑯螂郞來崍徠萊冷掠略亮倆兩凉梁樑粮粱糧良諒輛量侶儷勵呂廬慮戾旅櫚濾礪藜蠣閭驢驪麗黎力曆歷瀝礫轢靂憐戀攣漣"],["d6a1","煉璉練聯蓮輦連鍊冽列劣洌烈裂廉斂殮濂簾獵令伶囹寧岺嶺怜玲笭羚翎聆逞鈴零靈領齡例澧禮醴隷勞怒撈擄櫓潞瀘爐盧老蘆虜路輅露魯鷺鹵碌祿綠菉錄鹿麓論壟弄朧瀧瓏籠聾儡瀨牢磊賂賚賴雷了僚寮廖料燎療瞭聊蓼"],["d7a1","遼鬧龍壘婁屢樓淚漏瘻累縷蔞褸鏤陋劉旒柳榴流溜瀏琉瑠留瘤硫謬類六戮陸侖倫崙淪綸輪律慄栗率隆勒肋凜凌楞稜綾菱陵俚利厘吏唎履悧李梨浬犁狸理璃異痢籬罹羸莉裏裡里釐離鯉吝潾燐璘藺躪隣鱗麟林淋琳臨霖砬"],["d8a1","立笠粒摩瑪痲碼磨馬魔麻寞幕漠膜莫邈万卍娩巒彎慢挽晩曼滿漫灣瞞萬蔓蠻輓饅鰻唜抹末沫茉襪靺亡妄忘忙望網罔芒茫莽輞邙埋妹媒寐昧枚梅每煤罵買賣邁魅脈貊陌驀麥孟氓猛盲盟萌冪覓免冕勉棉沔眄眠綿緬面麵滅"],["d9a1","蔑冥名命明暝椧溟皿瞑茗蓂螟酩銘鳴袂侮冒募姆帽慕摸摹暮某模母毛牟牡瑁眸矛耗芼茅謀謨貌木沐牧目睦穆鶩歿沒夢朦蒙卯墓妙廟描昴杳渺猫竗苗錨務巫憮懋戊拇撫无楙武毋無珷畝繆舞茂蕪誣貿霧鵡墨默們刎吻問文"],["daa1","汶紊紋聞蚊門雯勿沕物味媚尾嵋彌微未梶楣渼湄眉米美薇謎迷靡黴岷悶愍憫敏旻旼民泯玟珉緡閔密蜜謐剝博拍搏撲朴樸泊珀璞箔粕縛膊舶薄迫雹駁伴半反叛拌搬攀斑槃泮潘班畔瘢盤盼磐磻礬絆般蟠返頒飯勃拔撥渤潑"],["dba1","發跋醱鉢髮魃倣傍坊妨尨幇彷房放方旁昉枋榜滂磅紡肪膀舫芳蒡蚌訪謗邦防龐倍俳北培徘拜排杯湃焙盃背胚裴裵褙賠輩配陪伯佰帛柏栢白百魄幡樊煩燔番磻繁蕃藩飜伐筏罰閥凡帆梵氾汎泛犯範范法琺僻劈壁擘檗璧癖"],["dca1","碧蘗闢霹便卞弁變辨辯邊別瞥鱉鼈丙倂兵屛幷昞昺柄棅炳甁病秉竝輧餠騈保堡報寶普步洑湺潽珤甫菩補褓譜輔伏僕匐卜宓復服福腹茯蔔複覆輹輻馥鰒本乶俸奉封峯峰捧棒烽熢琫縫蓬蜂逢鋒鳳不付俯傅剖副否咐埠夫婦"],["dda1","孚孵富府復扶敷斧浮溥父符簿缶腐腑膚艀芙莩訃負賦賻赴趺部釜阜附駙鳧北分吩噴墳奔奮忿憤扮昐汾焚盆粉糞紛芬賁雰不佛弗彿拂崩朋棚硼繃鵬丕備匕匪卑妃婢庇悲憊扉批斐枇榧比毖毗毘沸泌琵痺砒碑秕秘粃緋翡肥"],["dea1","脾臂菲蜚裨誹譬費鄙非飛鼻嚬嬪彬斌檳殯浜濱瀕牝玭貧賓頻憑氷聘騁乍事些仕伺似使俟僿史司唆嗣四士奢娑寫寺射巳師徙思捨斜斯柶査梭死沙泗渣瀉獅砂社祀祠私篩紗絲肆舍莎蓑蛇裟詐詞謝賜赦辭邪飼駟麝削數朔索"],["dfa1","傘刪山散汕珊産疝算蒜酸霰乷撒殺煞薩三參杉森渗芟蔘衫揷澁鈒颯上傷像償商喪嘗孀尙峠常床庠廂想桑橡湘爽牀狀相祥箱翔裳觴詳象賞霜塞璽賽嗇塞穡索色牲生甥省笙墅壻嶼序庶徐恕抒捿敍暑曙書栖棲犀瑞筮絮緖署"],["e0a1","胥舒薯西誓逝鋤黍鼠夕奭席惜昔晳析汐淅潟石碩蓆釋錫仙僊先善嬋宣扇敾旋渲煽琁瑄璇璿癬禪線繕羨腺膳船蘚蟬詵跣選銑鐥饍鮮卨屑楔泄洩渫舌薛褻設說雪齧剡暹殲纖蟾贍閃陝攝涉燮葉城姓宬性惺成星晟猩珹盛省筬"],["e1a1","聖聲腥誠醒世勢歲洗稅笹細說貰召嘯塑宵小少巢所掃搔昭梳沼消溯瀟炤燒甦疏疎瘙笑篠簫素紹蔬蕭蘇訴逍遡邵銷韶騷俗屬束涑粟續謖贖速孫巽損蓀遜飡率宋悚松淞訟誦送頌刷殺灑碎鎖衰釗修受嗽囚垂壽嫂守岫峀帥愁"],["e2a1","戍手授搜收數樹殊水洙漱燧狩獸琇璲瘦睡秀穗竪粹綏綬繡羞脩茱蒐蓚藪袖誰讐輸遂邃酬銖銹隋隧隨雖需須首髓鬚叔塾夙孰宿淑潚熟琡璹肅菽巡徇循恂旬栒楯橓殉洵淳珣盾瞬筍純脣舜荀蓴蕣詢諄醇錞順馴戌術述鉥崇崧"],["e3a1","嵩瑟膝蝨濕拾習褶襲丞乘僧勝升承昇繩蠅陞侍匙嘶始媤尸屎屍市弑恃施是時枾柴猜矢示翅蒔蓍視試詩諡豕豺埴寔式息拭植殖湜熄篒蝕識軾食飾伸侁信呻娠宸愼新晨燼申神紳腎臣莘薪藎蜃訊身辛辰迅失室實悉審尋心沁"],["e4a1","沈深瀋甚芯諶什十拾雙氏亞俄兒啞娥峨我牙芽莪蛾衙訝阿雅餓鴉鵝堊岳嶽幄惡愕握樂渥鄂鍔顎鰐齷安岸按晏案眼雁鞍顔鮟斡謁軋閼唵岩巖庵暗癌菴闇壓押狎鴨仰央怏昻殃秧鴦厓哀埃崖愛曖涯碍艾隘靄厄扼掖液縊腋額"],["e5a1","櫻罌鶯鸚也倻冶夜惹揶椰爺耶若野弱掠略約若葯蒻藥躍亮佯兩凉壤孃恙揚攘敭暘梁楊樣洋瀁煬痒瘍禳穰糧羊良襄諒讓釀陽量養圄御於漁瘀禦語馭魚齬億憶抑檍臆偃堰彦焉言諺孼蘖俺儼嚴奄掩淹嶪業円予余勵呂女如廬"],["e6a1","旅歟汝濾璵礖礪與艅茹輿轝閭餘驪麗黎亦力域役易曆歷疫繹譯轢逆驛嚥堧姸娟宴年延憐戀捐挻撚椽沇沿涎涓淵演漣烟然煙煉燃燕璉硏硯秊筵緣練縯聯衍軟輦蓮連鉛鍊鳶列劣咽悅涅烈熱裂說閱厭廉念捻染殮炎焰琰艶苒"],["e7a1","簾閻髥鹽曄獵燁葉令囹塋寧嶺嶸影怜映暎楹榮永泳渶潁濚瀛瀯煐營獰玲瑛瑩瓔盈穎纓羚聆英詠迎鈴鍈零霙靈領乂倪例刈叡曳汭濊猊睿穢芮藝蘂禮裔詣譽豫醴銳隸霓預五伍俉傲午吾吳嗚塢墺奧娛寤悟惡懊敖旿晤梧汚澳"],["e8a1","烏熬獒筽蜈誤鰲鼇屋沃獄玉鈺溫瑥瘟穩縕蘊兀壅擁瓮甕癰翁邕雍饔渦瓦窩窪臥蛙蝸訛婉完宛梡椀浣玩琓琬碗緩翫脘腕莞豌阮頑曰往旺枉汪王倭娃歪矮外嵬巍猥畏了僚僥凹堯夭妖姚寥寮尿嶢拗搖撓擾料曜樂橈燎燿瑤療"],["e9a1","窈窯繇繞耀腰蓼蟯要謠遙遼邀饒慾欲浴縟褥辱俑傭冗勇埇墉容庸慂榕涌湧溶熔瑢用甬聳茸蓉踊鎔鏞龍于佑偶優又友右宇寓尤愚憂旴牛玗瑀盂祐禑禹紆羽芋藕虞迂遇郵釪隅雨雩勖彧旭昱栯煜稶郁頊云暈橒殞澐熉耘芸蕓"],["eaa1","運隕雲韻蔚鬱亐熊雄元原員圓園垣媛嫄寃怨愿援沅洹湲源爰猿瑗苑袁轅遠阮院願鴛月越鉞位偉僞危圍委威尉慰暐渭爲瑋緯胃萎葦蔿蝟衛褘謂違韋魏乳侑儒兪劉唯喩孺宥幼幽庾悠惟愈愉揄攸有杻柔柚柳楡楢油洧流游溜"],["eba1","濡猶猷琉瑜由留癒硫紐維臾萸裕誘諛諭踰蹂遊逾遺酉釉鍮類六堉戮毓肉育陸倫允奫尹崙淪潤玧胤贇輪鈗閏律慄栗率聿戎瀜絨融隆垠恩慇殷誾銀隱乙吟淫蔭陰音飮揖泣邑凝應膺鷹依倚儀宜意懿擬椅毅疑矣義艤薏蟻衣誼"],["eca1","議醫二以伊利吏夷姨履已弛彛怡易李梨泥爾珥理異痍痢移罹而耳肄苡荑裏裡貽貳邇里離飴餌匿溺瀷益翊翌翼謚人仁刃印吝咽因姻寅引忍湮燐璘絪茵藺蚓認隣靭靷鱗麟一佚佾壹日溢逸鎰馹任壬妊姙恁林淋稔臨荏賃入卄"],["eda1","立笠粒仍剩孕芿仔刺咨姉姿子字孜恣慈滋炙煮玆瓷疵磁紫者自茨蔗藉諮資雌作勺嚼斫昨灼炸爵綽芍酌雀鵲孱棧殘潺盞岑暫潛箴簪蠶雜丈仗匠場墻壯奬將帳庄張掌暲杖樟檣欌漿牆狀獐璋章粧腸臟臧莊葬蔣薔藏裝贓醬長"],["eea1","障再哉在宰才材栽梓渽滓災縡裁財載齋齎爭箏諍錚佇低儲咀姐底抵杵楮樗沮渚狙猪疽箸紵苧菹著藷詛貯躇這邸雎齟勣吊嫡寂摘敵滴狄炙的積笛籍績翟荻謫賊赤跡蹟迪迹適鏑佃佺傳全典前剪塡塼奠專展廛悛戰栓殿氈澱"],["efa1","煎琠田甸畑癲筌箋箭篆纏詮輾轉鈿銓錢鐫電顚顫餞切截折浙癤竊節絶占岾店漸点粘霑鮎點接摺蝶丁井亭停偵呈姃定幀庭廷征情挺政整旌晶晸柾楨檉正汀淀淨渟湞瀞炡玎珽町睛碇禎程穽精綎艇訂諪貞鄭酊釘鉦鋌錠霆靖"],["f0a1","靜頂鼎制劑啼堤帝弟悌提梯濟祭第臍薺製諸蹄醍除際霽題齊俎兆凋助嘲弔彫措操早晁曺曹朝條棗槽漕潮照燥爪璪眺祖祚租稠窕粗糟組繰肇藻蚤詔調趙躁造遭釣阻雕鳥族簇足鏃存尊卒拙猝倧宗從悰慫棕淙琮種終綜縱腫"],["f1a1","踪踵鍾鐘佐坐左座挫罪主住侏做姝胄呪周嗾奏宙州廚晝朱柱株注洲湊澍炷珠疇籌紂紬綢舟蛛註誅走躊輳週酎酒鑄駐竹粥俊儁准埈寯峻晙樽浚準濬焌畯竣蠢逡遵雋駿茁中仲衆重卽櫛楫汁葺增憎曾拯烝甑症繒蒸證贈之只"],["f2a1","咫地址志持指摯支旨智枝枳止池沚漬知砥祉祗紙肢脂至芝芷蜘誌識贄趾遲直稙稷織職唇嗔塵振搢晉晋桭榛殄津溱珍瑨璡畛疹盡眞瞋秦縉縝臻蔯袗診賑軫辰進鎭陣陳震侄叱姪嫉帙桎瓆疾秩窒膣蛭質跌迭斟朕什執潗緝輯"],["f3a1","鏶集徵懲澄且侘借叉嗟嵯差次此磋箚茶蹉車遮捉搾着窄錯鑿齪撰澯燦璨瓚竄簒纂粲纘讚贊鑽餐饌刹察擦札紮僭參塹慘慙懺斬站讒讖倉倡創唱娼廠彰愴敞昌昶暢槍滄漲猖瘡窓脹艙菖蒼債埰寀寨彩採砦綵菜蔡采釵冊柵策"],["f4a1","責凄妻悽處倜刺剔尺慽戚拓擲斥滌瘠脊蹠陟隻仟千喘天川擅泉淺玔穿舛薦賤踐遷釧闡阡韆凸哲喆徹撤澈綴輟轍鐵僉尖沾添甛瞻簽籤詹諂堞妾帖捷牒疊睫諜貼輒廳晴淸聽菁請靑鯖切剃替涕滯締諦逮遞體初剿哨憔抄招梢"],["f5a1","椒楚樵炒焦硝礁礎秒稍肖艸苕草蕉貂超酢醋醮促囑燭矗蜀觸寸忖村邨叢塚寵悤憁摠總聰蔥銃撮催崔最墜抽推椎楸樞湫皺秋芻萩諏趨追鄒酋醜錐錘鎚雛騶鰍丑畜祝竺筑築縮蓄蹙蹴軸逐春椿瑃出朮黜充忠沖蟲衝衷悴膵萃"],["f6a1","贅取吹嘴娶就炊翠聚脆臭趣醉驟鷲側仄厠惻測層侈値嗤峙幟恥梔治淄熾痔痴癡稚穉緇緻置致蚩輜雉馳齒則勅飭親七柒漆侵寢枕沈浸琛砧針鍼蟄秤稱快他咤唾墮妥惰打拖朶楕舵陀馱駝倬卓啄坼度托拓擢晫柝濁濯琢琸託"],["f7a1","鐸呑嘆坦彈憚歎灘炭綻誕奪脫探眈耽貪塔搭榻宕帑湯糖蕩兌台太怠態殆汰泰笞胎苔跆邰颱宅擇澤撑攄兎吐土討慟桶洞痛筒統通堆槌腿褪退頹偸套妬投透鬪慝特闖坡婆巴把播擺杷波派爬琶破罷芭跛頗判坂板版瓣販辦鈑"],["f8a1","阪八叭捌佩唄悖敗沛浿牌狽稗覇貝彭澎烹膨愎便偏扁片篇編翩遍鞭騙貶坪平枰萍評吠嬖幣廢弊斃肺蔽閉陛佈包匍匏咆哺圃布怖抛抱捕暴泡浦疱砲胞脯苞葡蒲袍褒逋鋪飽鮑幅暴曝瀑爆輻俵剽彪慓杓標漂瓢票表豹飇飄驃"],["f9a1","品稟楓諷豊風馮彼披疲皮被避陂匹弼必泌珌畢疋筆苾馝乏逼下何厦夏廈昰河瑕荷蝦賀遐霞鰕壑學虐謔鶴寒恨悍旱汗漢澣瀚罕翰閑閒限韓割轄函含咸啣喊檻涵緘艦銜陷鹹合哈盒蛤閤闔陜亢伉姮嫦巷恒抗杭桁沆港缸肛航"],["faa1","行降項亥偕咳垓奚孩害懈楷海瀣蟹解該諧邂駭骸劾核倖幸杏荇行享向嚮珦鄕響餉饗香噓墟虛許憲櫶獻軒歇險驗奕爀赫革俔峴弦懸晛泫炫玄玹現眩睍絃絢縣舷衒見賢鉉顯孑穴血頁嫌俠協夾峽挾浹狹脅脇莢鋏頰亨兄刑型"],["fba1","形泂滎瀅灐炯熒珩瑩荊螢衡逈邢鎣馨兮彗惠慧暳蕙蹊醯鞋乎互呼壕壺好岵弧戶扈昊晧毫浩淏湖滸澔濠濩灝狐琥瑚瓠皓祜糊縞胡芦葫蒿虎號蝴護豪鎬頀顥惑或酷婚昏混渾琿魂忽惚笏哄弘汞泓洪烘紅虹訌鴻化和嬅樺火畵"],["fca1","禍禾花華話譁貨靴廓擴攫確碻穫丸喚奐宦幻患換歡晥桓渙煥環紈還驩鰥活滑猾豁闊凰幌徨恍惶愰慌晃晄榥況湟滉潢煌璜皇篁簧荒蝗遑隍黃匯回廻徊恢悔懷晦會檜淮澮灰獪繪膾茴蛔誨賄劃獲宖橫鐄哮嚆孝效斅曉梟涍淆"],["fda1","爻肴酵驍侯候厚后吼喉嗅帿後朽煦珝逅勛勳塤壎焄熏燻薰訓暈薨喧暄煊萱卉喙毁彙徽揮暉煇諱輝麾休携烋畦虧恤譎鷸兇凶匈洶胸黑昕欣炘痕吃屹紇訖欠欽歆吸恰洽翕興僖凞喜噫囍姬嬉希憙憘戱晞曦熙熹熺犧禧稀羲詰"]]'); + +/***/ }), + +/***/ 6517: +/***/ ((module) => { + +"use strict"; +module.exports = /*#__PURE__*/JSON.parse('[["0","\\u0000",127],["a140"," ,、。.‧;:?!︰…‥﹐﹑﹒·﹔﹕﹖﹗|–︱—︳╴︴﹏()︵︶{}︷︸〔〕︹︺【】︻︼《》︽︾〈〉︿﹀「」﹁﹂『』﹃﹄﹙﹚"],["a1a1","﹛﹜﹝﹞‘’“”〝〞‵′#&*※§〃○●△▲◎☆★◇◆□■▽▼㊣℅¯ ̄_ˍ﹉﹊﹍﹎﹋﹌﹟﹠﹡+-×÷±√<>=≦≧≠∞≒≡﹢",4,"~∩∪⊥∠∟⊿㏒㏑∫∮∵∴♀♂⊕⊙↑↓←→↖↗↙↘∥∣/"],["a240","\∕﹨$¥〒¢£%@℃℉﹩﹪﹫㏕㎜㎝㎞㏎㎡㎎㎏㏄°兙兛兞兝兡兣嗧瓩糎▁",7,"▏▎▍▌▋▊▉┼┴┬┤├▔─│▕┌┐└┘╭"],["a2a1","╮╰╯═╞╪╡◢◣◥◤╱╲╳0",9,"Ⅰ",9,"〡",8,"十卄卅A",25,"a",21],["a340","wxyzΑ",16,"Σ",6,"α",16,"σ",6,"ㄅ",10],["a3a1","ㄐ",25,"˙ˉˊˇˋ"],["a3e1","€"],["a440","一乙丁七乃九了二人儿入八几刀刁力匕十卜又三下丈上丫丸凡久么也乞于亡兀刃勺千叉口土士夕大女子孑孓寸小尢尸山川工己已巳巾干廾弋弓才"],["a4a1","丑丐不中丰丹之尹予云井互五亢仁什仃仆仇仍今介仄元允內六兮公冗凶分切刈勻勾勿化匹午升卅卞厄友及反壬天夫太夭孔少尤尺屯巴幻廿弔引心戈戶手扎支文斗斤方日曰月木欠止歹毋比毛氏水火爪父爻片牙牛犬王丙"],["a540","世丕且丘主乍乏乎以付仔仕他仗代令仙仞充兄冉冊冬凹出凸刊加功包匆北匝仟半卉卡占卯卮去可古右召叮叩叨叼司叵叫另只史叱台句叭叻四囚外"],["a5a1","央失奴奶孕它尼巨巧左市布平幼弁弘弗必戊打扔扒扑斥旦朮本未末札正母民氐永汁汀氾犯玄玉瓜瓦甘生用甩田由甲申疋白皮皿目矛矢石示禾穴立丞丟乒乓乩亙交亦亥仿伉伙伊伕伍伐休伏仲件任仰仳份企伋光兇兆先全"],["a640","共再冰列刑划刎刖劣匈匡匠印危吉吏同吊吐吁吋各向名合吃后吆吒因回囝圳地在圭圬圯圩夙多夷夸妄奸妃好她如妁字存宇守宅安寺尖屹州帆并年"],["a6a1","式弛忙忖戎戌戍成扣扛托收早旨旬旭曲曳有朽朴朱朵次此死氖汝汗汙江池汐汕污汛汍汎灰牟牝百竹米糸缶羊羽老考而耒耳聿肉肋肌臣自至臼舌舛舟艮色艾虫血行衣西阡串亨位住佇佗佞伴佛何估佐佑伽伺伸佃佔似但佣"],["a740","作你伯低伶余佝佈佚兌克免兵冶冷別判利刪刨劫助努劬匣即卵吝吭吞吾否呎吧呆呃吳呈呂君吩告吹吻吸吮吵吶吠吼呀吱含吟听囪困囤囫坊坑址坍"],["a7a1","均坎圾坐坏圻壯夾妝妒妨妞妣妙妖妍妤妓妊妥孝孜孚孛完宋宏尬局屁尿尾岐岑岔岌巫希序庇床廷弄弟彤形彷役忘忌志忍忱快忸忪戒我抄抗抖技扶抉扭把扼找批扳抒扯折扮投抓抑抆改攻攸旱更束李杏材村杜杖杞杉杆杠"],["a840","杓杗步每求汞沙沁沈沉沅沛汪決沐汰沌汨沖沒汽沃汲汾汴沆汶沍沔沘沂灶灼災灸牢牡牠狄狂玖甬甫男甸皂盯矣私秀禿究系罕肖肓肝肘肛肚育良芒"],["a8a1","芋芍見角言谷豆豕貝赤走足身車辛辰迂迆迅迄巡邑邢邪邦那酉釆里防阮阱阪阬並乖乳事些亞享京佯依侍佳使佬供例來侃佰併侈佩佻侖佾侏侑佺兔兒兕兩具其典冽函刻券刷刺到刮制剁劾劻卒協卓卑卦卷卸卹取叔受味呵"],["a940","咖呸咕咀呻呷咄咒咆呼咐呱呶和咚呢周咋命咎固垃坷坪坩坡坦坤坼夜奉奇奈奄奔妾妻委妹妮姑姆姐姍始姓姊妯妳姒姅孟孤季宗定官宜宙宛尚屈居"],["a9a1","屆岷岡岸岩岫岱岳帘帚帖帕帛帑幸庚店府底庖延弦弧弩往征彿彼忝忠忽念忿怏怔怯怵怖怪怕怡性怩怫怛或戕房戾所承拉拌拄抿拂抹拒招披拓拔拋拈抨抽押拐拙拇拍抵拚抱拘拖拗拆抬拎放斧於旺昔易昌昆昂明昀昏昕昊"],["aa40","昇服朋杭枋枕東果杳杷枇枝林杯杰板枉松析杵枚枓杼杪杲欣武歧歿氓氛泣注泳沱泌泥河沽沾沼波沫法泓沸泄油況沮泗泅泱沿治泡泛泊沬泯泜泖泠"],["aaa1","炕炎炒炊炙爬爭爸版牧物狀狎狙狗狐玩玨玟玫玥甽疝疙疚的盂盲直知矽社祀祁秉秈空穹竺糾罔羌羋者肺肥肢肱股肫肩肴肪肯臥臾舍芳芝芙芭芽芟芹花芬芥芯芸芣芰芾芷虎虱初表軋迎返近邵邸邱邶采金長門阜陀阿阻附"],["ab40","陂隹雨青非亟亭亮信侵侯便俠俑俏保促侶俘俟俊俗侮俐俄係俚俎俞侷兗冒冑冠剎剃削前剌剋則勇勉勃勁匍南卻厚叛咬哀咨哎哉咸咦咳哇哂咽咪品"],["aba1","哄哈咯咫咱咻咩咧咿囿垂型垠垣垢城垮垓奕契奏奎奐姜姘姿姣姨娃姥姪姚姦威姻孩宣宦室客宥封屎屏屍屋峙峒巷帝帥帟幽庠度建弈弭彥很待徊律徇後徉怒思怠急怎怨恍恰恨恢恆恃恬恫恪恤扁拜挖按拼拭持拮拽指拱拷"],["ac40","拯括拾拴挑挂政故斫施既春昭映昧是星昨昱昤曷柿染柱柔某柬架枯柵柩柯柄柑枴柚查枸柏柞柳枰柙柢柝柒歪殃殆段毒毗氟泉洋洲洪流津洌洱洞洗"],["aca1","活洽派洶洛泵洹洧洸洩洮洵洎洫炫為炳炬炯炭炸炮炤爰牲牯牴狩狠狡玷珊玻玲珍珀玳甚甭畏界畎畋疫疤疥疢疣癸皆皇皈盈盆盃盅省盹相眉看盾盼眇矜砂研砌砍祆祉祈祇禹禺科秒秋穿突竿竽籽紂紅紀紉紇約紆缸美羿耄"],["ad40","耐耍耑耶胖胥胚胃胄背胡胛胎胞胤胝致舢苧范茅苣苛苦茄若茂茉苒苗英茁苜苔苑苞苓苟苯茆虐虹虻虺衍衫要觔計訂訃貞負赴赳趴軍軌述迦迢迪迥"],["ada1","迭迫迤迨郊郎郁郃酋酊重閂限陋陌降面革韋韭音頁風飛食首香乘亳倌倍倣俯倦倥俸倩倖倆值借倚倒們俺倀倔倨俱倡個候倘俳修倭倪俾倫倉兼冤冥冢凍凌准凋剖剜剔剛剝匪卿原厝叟哨唐唁唷哼哥哲唆哺唔哩哭員唉哮哪"],["ae40","哦唧唇哽唏圃圄埂埔埋埃堉夏套奘奚娑娘娜娟娛娓姬娠娣娩娥娌娉孫屘宰害家宴宮宵容宸射屑展屐峭峽峻峪峨峰島崁峴差席師庫庭座弱徒徑徐恙"],["aea1","恣恥恐恕恭恩息悄悟悚悍悔悌悅悖扇拳挈拿捎挾振捕捂捆捏捉挺捐挽挪挫挨捍捌效敉料旁旅時晉晏晃晒晌晅晁書朔朕朗校核案框桓根桂桔栩梳栗桌桑栽柴桐桀格桃株桅栓栘桁殊殉殷氣氧氨氦氤泰浪涕消涇浦浸海浙涓"],["af40","浬涉浮浚浴浩涌涊浹涅浥涔烊烘烤烙烈烏爹特狼狹狽狸狷玆班琉珮珠珪珞畔畝畜畚留疾病症疲疳疽疼疹痂疸皋皰益盍盎眩真眠眨矩砰砧砸砝破砷"],["afa1","砥砭砠砟砲祕祐祠祟祖神祝祗祚秤秣秧租秦秩秘窄窈站笆笑粉紡紗紋紊素索純紐紕級紜納紙紛缺罟羔翅翁耆耘耕耙耗耽耿胱脂胰脅胭胴脆胸胳脈能脊胼胯臭臬舀舐航舫舨般芻茫荒荔荊茸荐草茵茴荏茲茹茶茗荀茱茨荃"],["b040","虔蚊蚪蚓蚤蚩蚌蚣蚜衰衷袁袂衽衹記訐討訌訕訊託訓訖訏訑豈豺豹財貢起躬軒軔軏辱送逆迷退迺迴逃追逅迸邕郡郝郢酒配酌釘針釗釜釙閃院陣陡"],["b0a1","陛陝除陘陞隻飢馬骨高鬥鬲鬼乾偺偽停假偃偌做偉健偶偎偕偵側偷偏倏偯偭兜冕凰剪副勒務勘動匐匏匙匿區匾參曼商啪啦啄啞啡啃啊唱啖問啕唯啤唸售啜唬啣唳啁啗圈國圉域堅堊堆埠埤基堂堵執培夠奢娶婁婉婦婪婀"],["b140","娼婢婚婆婊孰寇寅寄寂宿密尉專將屠屜屝崇崆崎崛崖崢崑崩崔崙崤崧崗巢常帶帳帷康庸庶庵庾張強彗彬彩彫得徙從徘御徠徜恿患悉悠您惋悴惦悽"],["b1a1","情悻悵惜悼惘惕惆惟悸惚惇戚戛扈掠控捲掖探接捷捧掘措捱掩掉掃掛捫推掄授掙採掬排掏掀捻捩捨捺敝敖救教敗啟敏敘敕敔斜斛斬族旋旌旎晝晚晤晨晦晞曹勗望梁梯梢梓梵桿桶梱梧梗械梃棄梭梆梅梔條梨梟梡梂欲殺"],["b240","毫毬氫涎涼淳淙液淡淌淤添淺清淇淋涯淑涮淞淹涸混淵淅淒渚涵淚淫淘淪深淮淨淆淄涪淬涿淦烹焉焊烽烯爽牽犁猜猛猖猓猙率琅琊球理現琍瓠瓶"],["b2a1","瓷甜產略畦畢異疏痔痕疵痊痍皎盔盒盛眷眾眼眶眸眺硫硃硎祥票祭移窒窕笠笨笛第符笙笞笮粒粗粕絆絃統紮紹紼絀細紳組累終紲紱缽羞羚翌翎習耜聊聆脯脖脣脫脩脰脤舂舵舷舶船莎莞莘荸莢莖莽莫莒莊莓莉莠荷荻荼"],["b340","莆莧處彪蛇蛀蚶蛄蚵蛆蛋蚱蚯蛉術袞袈被袒袖袍袋覓規訪訝訣訥許設訟訛訢豉豚販責貫貨貪貧赧赦趾趺軛軟這逍通逗連速逝逐逕逞造透逢逖逛途"],["b3a1","部郭都酗野釵釦釣釧釭釩閉陪陵陳陸陰陴陶陷陬雀雪雩章竟頂頃魚鳥鹵鹿麥麻傢傍傅備傑傀傖傘傚最凱割剴創剩勞勝勛博厥啻喀喧啼喊喝喘喂喜喪喔喇喋喃喳單喟唾喲喚喻喬喱啾喉喫喙圍堯堪場堤堰報堡堝堠壹壺奠"],["b440","婷媚婿媒媛媧孳孱寒富寓寐尊尋就嵌嵐崴嵇巽幅帽幀幃幾廊廁廂廄弼彭復循徨惑惡悲悶惠愜愣惺愕惰惻惴慨惱愎惶愉愀愒戟扉掣掌描揀揩揉揆揍"],["b4a1","插揣提握揖揭揮捶援揪換摒揚揹敞敦敢散斑斐斯普晰晴晶景暑智晾晷曾替期朝棺棕棠棘棗椅棟棵森棧棹棒棲棣棋棍植椒椎棉棚楮棻款欺欽殘殖殼毯氮氯氬港游湔渡渲湧湊渠渥渣減湛湘渤湖湮渭渦湯渴湍渺測湃渝渾滋"],["b540","溉渙湎湣湄湲湩湟焙焚焦焰無然煮焜牌犄犀猶猥猴猩琺琪琳琢琥琵琶琴琯琛琦琨甥甦畫番痢痛痣痙痘痞痠登發皖皓皴盜睏短硝硬硯稍稈程稅稀窘"],["b5a1","窗窖童竣等策筆筐筒答筍筋筏筑粟粥絞結絨絕紫絮絲絡給絢絰絳善翔翕耋聒肅腕腔腋腑腎脹腆脾腌腓腴舒舜菩萃菸萍菠菅萋菁華菱菴著萊菰萌菌菽菲菊萸萎萄菜萇菔菟虛蛟蛙蛭蛔蛛蛤蛐蛞街裁裂袱覃視註詠評詞証詁"],["b640","詔詛詐詆訴診訶詖象貂貯貼貳貽賁費賀貴買貶貿貸越超趁跎距跋跚跑跌跛跆軻軸軼辜逮逵週逸進逶鄂郵鄉郾酣酥量鈔鈕鈣鈉鈞鈍鈐鈇鈑閔閏開閑"],["b6a1","間閒閎隊階隋陽隅隆隍陲隄雁雅雄集雇雯雲韌項順須飧飪飯飩飲飭馮馭黃黍黑亂傭債傲傳僅傾催傷傻傯僇剿剷剽募勦勤勢勣匯嗟嗨嗓嗦嗎嗜嗇嗑嗣嗤嗯嗚嗡嗅嗆嗥嗉園圓塞塑塘塗塚塔填塌塭塊塢塒塋奧嫁嫉嫌媾媽媼"],["b740","媳嫂媲嵩嵯幌幹廉廈弒彙徬微愚意慈感想愛惹愁愈慎慌慄慍愾愴愧愍愆愷戡戢搓搾搞搪搭搽搬搏搜搔損搶搖搗搆敬斟新暗暉暇暈暖暄暘暍會榔業"],["b7a1","楚楷楠楔極椰概楊楨楫楞楓楹榆楝楣楛歇歲毀殿毓毽溢溯滓溶滂源溝滇滅溥溘溼溺溫滑準溜滄滔溪溧溴煎煙煩煤煉照煜煬煦煌煥煞煆煨煖爺牒猷獅猿猾瑯瑚瑕瑟瑞瑁琿瑙瑛瑜當畸瘀痰瘁痲痱痺痿痴痳盞盟睛睫睦睞督"],["b840","睹睪睬睜睥睨睢矮碎碰碗碘碌碉硼碑碓硿祺祿禁萬禽稜稚稠稔稟稞窟窠筷節筠筮筧粱粳粵經絹綑綁綏絛置罩罪署義羨群聖聘肆肄腱腰腸腥腮腳腫"],["b8a1","腹腺腦舅艇蒂葷落萱葵葦葫葉葬葛萼萵葡董葩葭葆虞虜號蛹蜓蜈蜇蜀蛾蛻蜂蜃蜆蜊衙裟裔裙補裘裝裡裊裕裒覜解詫該詳試詩詰誇詼詣誠話誅詭詢詮詬詹詻訾詨豢貊貉賊資賈賄貲賃賂賅跡跟跨路跳跺跪跤跦躲較載軾輊"],["b940","辟農運遊道遂達逼違遐遇遏過遍遑逾遁鄒鄗酬酪酩釉鈷鉗鈸鈽鉀鈾鉛鉋鉤鉑鈴鉉鉍鉅鈹鈿鉚閘隘隔隕雍雋雉雊雷電雹零靖靴靶預頑頓頊頒頌飼飴"],["b9a1","飽飾馳馱馴髡鳩麂鼎鼓鼠僧僮僥僖僭僚僕像僑僱僎僩兢凳劃劂匱厭嗾嘀嘛嘗嗽嘔嘆嘉嘍嘎嗷嘖嘟嘈嘐嗶團圖塵塾境墓墊塹墅塽壽夥夢夤奪奩嫡嫦嫩嫗嫖嫘嫣孵寞寧寡寥實寨寢寤察對屢嶄嶇幛幣幕幗幔廓廖弊彆彰徹慇"],["ba40","愿態慷慢慣慟慚慘慵截撇摘摔撤摸摟摺摑摧搴摭摻敲斡旗旖暢暨暝榜榨榕槁榮槓構榛榷榻榫榴槐槍榭槌榦槃榣歉歌氳漳演滾漓滴漩漾漠漬漏漂漢"],["baa1","滿滯漆漱漸漲漣漕漫漯澈漪滬漁滲滌滷熔熙煽熊熄熒爾犒犖獄獐瑤瑣瑪瑰瑭甄疑瘧瘍瘋瘉瘓盡監瞄睽睿睡磁碟碧碳碩碣禎福禍種稱窪窩竭端管箕箋筵算箝箔箏箸箇箄粹粽精綻綰綜綽綾綠緊綴網綱綺綢綿綵綸維緒緇綬"],["bb40","罰翠翡翟聞聚肇腐膀膏膈膊腿膂臧臺與舔舞艋蓉蒿蓆蓄蒙蒞蒲蒜蓋蒸蓀蓓蒐蒼蓑蓊蜿蜜蜻蜢蜥蜴蜘蝕蜷蜩裳褂裴裹裸製裨褚裯誦誌語誣認誡誓誤"],["bba1","說誥誨誘誑誚誧豪貍貌賓賑賒赫趙趕跼輔輒輕輓辣遠遘遜遣遙遞遢遝遛鄙鄘鄞酵酸酷酴鉸銀銅銘銖鉻銓銜銨鉼銑閡閨閩閣閥閤隙障際雌雒需靼鞅韶頗領颯颱餃餅餌餉駁骯骰髦魁魂鳴鳶鳳麼鼻齊億儀僻僵價儂儈儉儅凜"],["bc40","劇劈劉劍劊勰厲嘮嘻嘹嘲嘿嘴嘩噓噎噗噴嘶嘯嘰墀墟增墳墜墮墩墦奭嬉嫻嬋嫵嬌嬈寮寬審寫層履嶝嶔幢幟幡廢廚廟廝廣廠彈影德徵慶慧慮慝慕憂"],["bca1","慼慰慫慾憧憐憫憎憬憚憤憔憮戮摩摯摹撞撲撈撐撰撥撓撕撩撒撮播撫撚撬撙撢撳敵敷數暮暫暴暱樣樟槨樁樞標槽模樓樊槳樂樅槭樑歐歎殤毅毆漿潼澄潑潦潔澆潭潛潸潮澎潺潰潤澗潘滕潯潠潟熟熬熱熨牖犛獎獗瑩璋璃"],["bd40","瑾璀畿瘠瘩瘟瘤瘦瘡瘢皚皺盤瞎瞇瞌瞑瞋磋磅確磊碾磕碼磐稿稼穀稽稷稻窯窮箭箱範箴篆篇篁箠篌糊締練緯緻緘緬緝編緣線緞緩綞緙緲緹罵罷羯"],["bda1","翩耦膛膜膝膠膚膘蔗蔽蔚蓮蔬蔭蔓蔑蔣蔡蔔蓬蔥蓿蔆螂蝴蝶蝠蝦蝸蝨蝙蝗蝌蝓衛衝褐複褒褓褕褊誼諒談諄誕請諸課諉諂調誰論諍誶誹諛豌豎豬賠賞賦賤賬賭賢賣賜質賡赭趟趣踫踐踝踢踏踩踟踡踞躺輝輛輟輩輦輪輜輞"],["be40","輥適遮遨遭遷鄰鄭鄧鄱醇醉醋醃鋅銻銷鋪銬鋤鋁銳銼鋒鋇鋰銲閭閱霄霆震霉靠鞍鞋鞏頡頫頜颳養餓餒餘駝駐駟駛駑駕駒駙骷髮髯鬧魅魄魷魯鴆鴉"],["bea1","鴃麩麾黎墨齒儒儘儔儐儕冀冪凝劑劓勳噙噫噹噩噤噸噪器噥噱噯噬噢噶壁墾壇壅奮嬝嬴學寰導彊憲憑憩憊懍憶憾懊懈戰擅擁擋撻撼據擄擇擂操撿擒擔撾整曆曉暹曄曇暸樽樸樺橙橫橘樹橄橢橡橋橇樵機橈歙歷氅濂澱澡"],["bf40","濃澤濁澧澳激澹澶澦澠澴熾燉燐燒燈燕熹燎燙燜燃燄獨璜璣璘璟璞瓢甌甍瘴瘸瘺盧盥瞠瞞瞟瞥磨磚磬磧禦積穎穆穌穋窺篙簑築篤篛篡篩篦糕糖縊"],["bfa1","縑縈縛縣縞縝縉縐罹羲翰翱翮耨膳膩膨臻興艘艙蕊蕙蕈蕨蕩蕃蕉蕭蕪蕞螃螟螞螢融衡褪褲褥褫褡親覦諦諺諫諱謀諜諧諮諾謁謂諷諭諳諶諼豫豭貓賴蹄踱踴蹂踹踵輻輯輸輳辨辦遵遴選遲遼遺鄴醒錠錶鋸錳錯錢鋼錫錄錚"],["c040","錐錦錡錕錮錙閻隧隨險雕霎霑霖霍霓霏靛靜靦鞘頰頸頻頷頭頹頤餐館餞餛餡餚駭駢駱骸骼髻髭鬨鮑鴕鴣鴦鴨鴒鴛默黔龍龜優償儡儲勵嚎嚀嚐嚅嚇"],["c0a1","嚏壕壓壑壎嬰嬪嬤孺尷屨嶼嶺嶽嶸幫彌徽應懂懇懦懋戲戴擎擊擘擠擰擦擬擱擢擭斂斃曙曖檀檔檄檢檜櫛檣橾檗檐檠歜殮毚氈濘濱濟濠濛濤濫濯澀濬濡濩濕濮濰燧營燮燦燥燭燬燴燠爵牆獰獲璩環璦璨癆療癌盪瞳瞪瞰瞬"],["c140","瞧瞭矯磷磺磴磯礁禧禪穗窿簇簍篾篷簌篠糠糜糞糢糟糙糝縮績繆縷縲繃縫總縱繅繁縴縹繈縵縿縯罄翳翼聱聲聰聯聳臆臃膺臂臀膿膽臉膾臨舉艱薪"],["c1a1","薄蕾薜薑薔薯薛薇薨薊虧蟀蟑螳蟒蟆螫螻螺蟈蟋褻褶襄褸褽覬謎謗謙講謊謠謝謄謐豁谿豳賺賽購賸賻趨蹉蹋蹈蹊轄輾轂轅輿避遽還邁邂邀鄹醣醞醜鍍鎂錨鍵鍊鍥鍋錘鍾鍬鍛鍰鍚鍔闊闋闌闈闆隱隸雖霜霞鞠韓顆颶餵騁"],["c240","駿鮮鮫鮪鮭鴻鴿麋黏點黜黝黛鼾齋叢嚕嚮壙壘嬸彝懣戳擴擲擾攆擺擻擷斷曜朦檳檬櫃檻檸櫂檮檯歟歸殯瀉瀋濾瀆濺瀑瀏燻燼燾燸獷獵璧璿甕癖癘"],["c2a1","癒瞽瞿瞻瞼礎禮穡穢穠竄竅簫簧簪簞簣簡糧織繕繞繚繡繒繙罈翹翻職聶臍臏舊藏薩藍藐藉薰薺薹薦蟯蟬蟲蟠覆覲觴謨謹謬謫豐贅蹙蹣蹦蹤蹟蹕軀轉轍邇邃邈醫醬釐鎔鎊鎖鎢鎳鎮鎬鎰鎘鎚鎗闔闖闐闕離雜雙雛雞霤鞣鞦"],["c340","鞭韹額顏題顎顓颺餾餿餽餮馥騎髁鬃鬆魏魎魍鯊鯉鯽鯈鯀鵑鵝鵠黠鼕鼬儳嚥壞壟壢寵龐廬懲懷懶懵攀攏曠曝櫥櫝櫚櫓瀛瀟瀨瀚瀝瀕瀘爆爍牘犢獸"],["c3a1","獺璽瓊瓣疇疆癟癡矇礙禱穫穩簾簿簸簽簷籀繫繭繹繩繪羅繳羶羹羸臘藩藝藪藕藤藥藷蟻蠅蠍蟹蟾襠襟襖襞譁譜識證譚譎譏譆譙贈贊蹼蹲躇蹶蹬蹺蹴轔轎辭邊邋醱醮鏡鏑鏟鏃鏈鏜鏝鏖鏢鏍鏘鏤鏗鏨關隴難霪霧靡韜韻類"],["c440","願顛颼饅饉騖騙鬍鯨鯧鯖鯛鶉鵡鵲鵪鵬麒麗麓麴勸嚨嚷嚶嚴嚼壤孀孃孽寶巉懸懺攘攔攙曦朧櫬瀾瀰瀲爐獻瓏癢癥礦礪礬礫竇競籌籃籍糯糰辮繽繼"],["c4a1","纂罌耀臚艦藻藹蘑藺蘆蘋蘇蘊蠔蠕襤覺觸議譬警譯譟譫贏贍躉躁躅躂醴釋鐘鐃鏽闡霰飄饒饑馨騫騰騷騵鰓鰍鹹麵黨鼯齟齣齡儷儸囁囀囂夔屬巍懼懾攝攜斕曩櫻欄櫺殲灌爛犧瓖瓔癩矓籐纏續羼蘗蘭蘚蠣蠢蠡蠟襪襬覽譴"],["c540","護譽贓躊躍躋轟辯醺鐮鐳鐵鐺鐸鐲鐫闢霸霹露響顧顥饗驅驃驀騾髏魔魑鰭鰥鶯鶴鷂鶸麝黯鼙齜齦齧儼儻囈囊囉孿巔巒彎懿攤權歡灑灘玀瓤疊癮癬"],["c5a1","禳籠籟聾聽臟襲襯觼讀贖贗躑躓轡酈鑄鑑鑒霽霾韃韁顫饕驕驍髒鬚鱉鰱鰾鰻鷓鷗鼴齬齪龔囌巖戀攣攫攪曬欐瓚竊籤籣籥纓纖纔臢蘸蘿蠱變邐邏鑣鑠鑤靨顯饜驚驛驗髓體髑鱔鱗鱖鷥麟黴囑壩攬灞癱癲矗罐羈蠶蠹衢讓讒"],["c640","讖艷贛釀鑪靂靈靄韆顰驟鬢魘鱟鷹鷺鹼鹽鼇齷齲廳欖灣籬籮蠻觀躡釁鑲鑰顱饞髖鬣黌灤矚讚鑷韉驢驥纜讜躪釅鑽鑾鑼鱷鱸黷豔鑿鸚爨驪鬱鸛鸞籲"],["c940","乂乜凵匚厂万丌乇亍囗兀屮彳丏冇与丮亓仂仉仈冘勼卬厹圠夃夬尐巿旡殳毌气爿丱丼仨仜仩仡仝仚刌匜卌圢圣夗夯宁宄尒尻屴屳帄庀庂忉戉扐氕"],["c9a1","氶汃氿氻犮犰玊禸肊阞伎优伬仵伔仱伀价伈伝伂伅伢伓伄仴伒冱刓刉刐劦匢匟卍厊吇囡囟圮圪圴夼妀奼妅奻奾奷奿孖尕尥屼屺屻屾巟幵庄异弚彴忕忔忏扜扞扤扡扦扢扙扠扚扥旯旮朾朹朸朻机朿朼朳氘汆汒汜汏汊汔汋"],["ca40","汌灱牞犴犵玎甪癿穵网艸艼芀艽艿虍襾邙邗邘邛邔阢阤阠阣佖伻佢佉体佤伾佧佒佟佁佘伭伳伿佡冏冹刜刞刡劭劮匉卣卲厎厏吰吷吪呔呅吙吜吥吘"],["caa1","吽呏呁吨吤呇囮囧囥坁坅坌坉坋坒夆奀妦妘妠妗妎妢妐妏妧妡宎宒尨尪岍岏岈岋岉岒岊岆岓岕巠帊帎庋庉庌庈庍弅弝彸彶忒忑忐忭忨忮忳忡忤忣忺忯忷忻怀忴戺抃抌抎抏抔抇扱扻扺扰抁抈扷扽扲扴攷旰旴旳旲旵杅杇"],["cb40","杙杕杌杈杝杍杚杋毐氙氚汸汧汫沄沋沏汱汯汩沚汭沇沕沜汦汳汥汻沎灴灺牣犿犽狃狆狁犺狅玕玗玓玔玒町甹疔疕皁礽耴肕肙肐肒肜芐芏芅芎芑芓"],["cba1","芊芃芄豸迉辿邟邡邥邞邧邠阰阨阯阭丳侘佼侅佽侀侇佶佴侉侄佷佌侗佪侚佹侁佸侐侜侔侞侒侂侕佫佮冞冼冾刵刲刳剆刱劼匊匋匼厒厔咇呿咁咑咂咈呫呺呾呥呬呴呦咍呯呡呠咘呣呧呤囷囹坯坲坭坫坱坰坶垀坵坻坳坴坢"],["cc40","坨坽夌奅妵妺姏姎妲姌姁妶妼姃姖妱妽姀姈妴姇孢孥宓宕屄屇岮岤岠岵岯岨岬岟岣岭岢岪岧岝岥岶岰岦帗帔帙弨弢弣弤彔徂彾彽忞忥怭怦怙怲怋"],["cca1","怴怊怗怳怚怞怬怢怍怐怮怓怑怌怉怜戔戽抭抴拑抾抪抶拊抮抳抯抻抩抰抸攽斨斻昉旼昄昒昈旻昃昋昍昅旽昑昐曶朊枅杬枎枒杶杻枘枆构杴枍枌杺枟枑枙枃杽极杸杹枔欥殀歾毞氝沓泬泫泮泙沶泔沭泧沷泐泂沺泃泆泭泲"],["cd40","泒泝沴沊沝沀泞泀洰泍泇沰泹泏泩泑炔炘炅炓炆炄炑炖炂炚炃牪狖狋狘狉狜狒狔狚狌狑玤玡玭玦玢玠玬玝瓝瓨甿畀甾疌疘皯盳盱盰盵矸矼矹矻矺"],["cda1","矷祂礿秅穸穻竻籵糽耵肏肮肣肸肵肭舠芠苀芫芚芘芛芵芧芮芼芞芺芴芨芡芩苂芤苃芶芢虰虯虭虮豖迒迋迓迍迖迕迗邲邴邯邳邰阹阽阼阺陃俍俅俓侲俉俋俁俔俜俙侻侳俛俇俖侺俀侹俬剄剉勀勂匽卼厗厖厙厘咺咡咭咥哏"],["ce40","哃茍咷咮哖咶哅哆咠呰咼咢咾呲哞咰垵垞垟垤垌垗垝垛垔垘垏垙垥垚垕壴复奓姡姞姮娀姱姝姺姽姼姶姤姲姷姛姩姳姵姠姾姴姭宨屌峐峘峌峗峋峛"],["cea1","峞峚峉峇峊峖峓峔峏峈峆峎峟峸巹帡帢帣帠帤庰庤庢庛庣庥弇弮彖徆怷怹恔恲恞恅恓恇恉恛恌恀恂恟怤恄恘恦恮扂扃拏挍挋拵挎挃拫拹挏挌拸拶挀挓挔拺挕拻拰敁敃斪斿昶昡昲昵昜昦昢昳昫昺昝昴昹昮朏朐柁柲柈枺"],["cf40","柜枻柸柘柀枷柅柫柤柟枵柍枳柷柶柮柣柂枹柎柧柰枲柼柆柭柌枮柦柛柺柉柊柃柪柋欨殂殄殶毖毘毠氠氡洨洴洭洟洼洿洒洊泚洳洄洙洺洚洑洀洝浂"],["cfa1","洁洘洷洃洏浀洇洠洬洈洢洉洐炷炟炾炱炰炡炴炵炩牁牉牊牬牰牳牮狊狤狨狫狟狪狦狣玅珌珂珈珅玹玶玵玴珫玿珇玾珃珆玸珋瓬瓮甮畇畈疧疪癹盄眈眃眄眅眊盷盻盺矧矨砆砑砒砅砐砏砎砉砃砓祊祌祋祅祄秕种秏秖秎窀"],["d040","穾竑笀笁籺籸籹籿粀粁紃紈紁罘羑羍羾耇耎耏耔耷胘胇胠胑胈胂胐胅胣胙胜胊胕胉胏胗胦胍臿舡芔苙苾苹茇苨茀苕茺苫苖苴苬苡苲苵茌苻苶苰苪"],["d0a1","苤苠苺苳苭虷虴虼虳衁衎衧衪衩觓訄訇赲迣迡迮迠郱邽邿郕郅邾郇郋郈釔釓陔陏陑陓陊陎倞倅倇倓倢倰倛俵俴倳倷倬俶俷倗倜倠倧倵倯倱倎党冔冓凊凄凅凈凎剡剚剒剞剟剕剢勍匎厞唦哢唗唒哧哳哤唚哿唄唈哫唑唅哱"],["d140","唊哻哷哸哠唎唃唋圁圂埌堲埕埒垺埆垽垼垸垶垿埇埐垹埁夎奊娙娖娭娮娕娏娗娊娞娳孬宧宭宬尃屖屔峬峿峮峱峷崀峹帩帨庨庮庪庬弳弰彧恝恚恧"],["d1a1","恁悢悈悀悒悁悝悃悕悛悗悇悜悎戙扆拲挐捖挬捄捅挶捃揤挹捋捊挼挩捁挴捘捔捙挭捇挳捚捑挸捗捀捈敊敆旆旃旄旂晊晟晇晑朒朓栟栚桉栲栳栻桋桏栖栱栜栵栫栭栯桎桄栴栝栒栔栦栨栮桍栺栥栠欬欯欭欱欴歭肂殈毦毤"],["d240","毨毣毢毧氥浺浣浤浶洍浡涒浘浢浭浯涑涍淯浿涆浞浧浠涗浰浼浟涂涘洯浨涋浾涀涄洖涃浻浽浵涐烜烓烑烝烋缹烢烗烒烞烠烔烍烅烆烇烚烎烡牂牸"],["d2a1","牷牶猀狺狴狾狶狳狻猁珓珙珥珖玼珧珣珩珜珒珛珔珝珚珗珘珨瓞瓟瓴瓵甡畛畟疰痁疻痄痀疿疶疺皊盉眝眛眐眓眒眣眑眕眙眚眢眧砣砬砢砵砯砨砮砫砡砩砳砪砱祔祛祏祜祓祒祑秫秬秠秮秭秪秜秞秝窆窉窅窋窌窊窇竘笐"],["d340","笄笓笅笏笈笊笎笉笒粄粑粊粌粈粍粅紞紝紑紎紘紖紓紟紒紏紌罜罡罞罠罝罛羖羒翃翂翀耖耾耹胺胲胹胵脁胻脀舁舯舥茳茭荄茙荑茥荖茿荁茦茜茢"],["d3a1","荂荎茛茪茈茼荍茖茤茠茷茯茩荇荅荌荓茞茬荋茧荈虓虒蚢蚨蚖蚍蚑蚞蚇蚗蚆蚋蚚蚅蚥蚙蚡蚧蚕蚘蚎蚝蚐蚔衃衄衭衵衶衲袀衱衿衯袃衾衴衼訒豇豗豻貤貣赶赸趵趷趶軑軓迾迵适迿迻逄迼迶郖郠郙郚郣郟郥郘郛郗郜郤酐"],["d440","酎酏釕釢釚陜陟隼飣髟鬯乿偰偪偡偞偠偓偋偝偲偈偍偁偛偊偢倕偅偟偩偫偣偤偆偀偮偳偗偑凐剫剭剬剮勖勓匭厜啵啶唼啍啐唴唪啑啢唶唵唰啒啅"],["d4a1","唌唲啥啎唹啈唭唻啀啋圊圇埻堔埢埶埜埴堀埭埽堈埸堋埳埏堇埮埣埲埥埬埡堎埼堐埧堁堌埱埩埰堍堄奜婠婘婕婧婞娸娵婭婐婟婥婬婓婤婗婃婝婒婄婛婈媎娾婍娹婌婰婩婇婑婖婂婜孲孮寁寀屙崞崋崝崚崠崌崨崍崦崥崏"],["d540","崰崒崣崟崮帾帴庱庴庹庲庳弶弸徛徖徟悊悐悆悾悰悺惓惔惏惤惙惝惈悱惛悷惊悿惃惍惀挲捥掊掂捽掽掞掭掝掗掫掎捯掇掐据掯捵掜捭掮捼掤挻掟"],["d5a1","捸掅掁掑掍捰敓旍晥晡晛晙晜晢朘桹梇梐梜桭桮梮梫楖桯梣梬梩桵桴梲梏桷梒桼桫桲梪梀桱桾梛梖梋梠梉梤桸桻梑梌梊桽欶欳欷欸殑殏殍殎殌氪淀涫涴涳湴涬淩淢涷淶淔渀淈淠淟淖涾淥淜淝淛淴淊涽淭淰涺淕淂淏淉"],["d640","淐淲淓淽淗淍淣涻烺焍烷焗烴焌烰焄烳焐烼烿焆焓焀烸烶焋焂焎牾牻牼牿猝猗猇猑猘猊猈狿猏猞玈珶珸珵琄琁珽琇琀珺珼珿琌琋珴琈畤畣痎痒痏"],["d6a1","痋痌痑痐皏皉盓眹眯眭眱眲眴眳眽眥眻眵硈硒硉硍硊硌砦硅硐祤祧祩祪祣祫祡离秺秸秶秷窏窔窐笵筇笴笥笰笢笤笳笘笪笝笱笫笭笯笲笸笚笣粔粘粖粣紵紽紸紶紺絅紬紩絁絇紾紿絊紻紨罣羕羜羝羛翊翋翍翐翑翇翏翉耟"],["d740","耞耛聇聃聈脘脥脙脛脭脟脬脞脡脕脧脝脢舑舸舳舺舴舲艴莐莣莨莍荺荳莤荴莏莁莕莙荵莔莩荽莃莌莝莛莪莋荾莥莯莈莗莰荿莦莇莮荶莚虙虖蚿蚷"],["d7a1","蛂蛁蛅蚺蚰蛈蚹蚳蚸蛌蚴蚻蚼蛃蚽蚾衒袉袕袨袢袪袚袑袡袟袘袧袙袛袗袤袬袌袓袎覂觖觙觕訰訧訬訞谹谻豜豝豽貥赽赻赹趼跂趹趿跁軘軞軝軜軗軠軡逤逋逑逜逌逡郯郪郰郴郲郳郔郫郬郩酖酘酚酓酕釬釴釱釳釸釤釹釪"],["d840","釫釷釨釮镺閆閈陼陭陫陱陯隿靪頄飥馗傛傕傔傞傋傣傃傌傎傝偨傜傒傂傇兟凔匒匑厤厧喑喨喥喭啷噅喢喓喈喏喵喁喣喒喤啽喌喦啿喕喡喎圌堩堷"],["d8a1","堙堞堧堣堨埵塈堥堜堛堳堿堶堮堹堸堭堬堻奡媯媔媟婺媢媞婸媦婼媥媬媕媮娷媄媊媗媃媋媩婻婽媌媜媏媓媝寪寍寋寔寑寊寎尌尰崷嵃嵫嵁嵋崿崵嵑嵎嵕崳崺嵒崽崱嵙嵂崹嵉崸崼崲崶嵀嵅幄幁彘徦徥徫惉悹惌惢惎惄愔"],["d940","惲愊愖愅惵愓惸惼惾惁愃愘愝愐惿愄愋扊掔掱掰揎揥揨揯揃撝揳揊揠揶揕揲揵摡揟掾揝揜揄揘揓揂揇揌揋揈揰揗揙攲敧敪敤敜敨敥斌斝斞斮旐旒"],["d9a1","晼晬晻暀晱晹晪晲朁椌棓椄棜椪棬棪棱椏棖棷棫棤棶椓椐棳棡椇棌椈楰梴椑棯棆椔棸棐棽棼棨椋椊椗棎棈棝棞棦棴棑椆棔棩椕椥棇欹欻欿欼殔殗殙殕殽毰毲毳氰淼湆湇渟湉溈渼渽湅湢渫渿湁湝湳渜渳湋湀湑渻渃渮湞"],["da40","湨湜湡渱渨湠湱湫渹渢渰湓湥渧湸湤湷湕湹湒湦渵渶湚焠焞焯烻焮焱焣焥焢焲焟焨焺焛牋牚犈犉犆犅犋猒猋猰猢猱猳猧猲猭猦猣猵猌琮琬琰琫琖"],["daa1","琚琡琭琱琤琣琝琩琠琲瓻甯畯畬痧痚痡痦痝痟痤痗皕皒盚睆睇睄睍睅睊睎睋睌矞矬硠硤硥硜硭硱硪确硰硩硨硞硢祴祳祲祰稂稊稃稌稄窙竦竤筊笻筄筈筌筎筀筘筅粢粞粨粡絘絯絣絓絖絧絪絏絭絜絫絒絔絩絑絟絎缾缿罥"],["db40","罦羢羠羡翗聑聏聐胾胔腃腊腒腏腇脽腍脺臦臮臷臸臹舄舼舽舿艵茻菏菹萣菀菨萒菧菤菼菶萐菆菈菫菣莿萁菝菥菘菿菡菋菎菖菵菉萉萏菞萑萆菂菳"],["dba1","菕菺菇菑菪萓菃菬菮菄菻菗菢萛菛菾蛘蛢蛦蛓蛣蛚蛪蛝蛫蛜蛬蛩蛗蛨蛑衈衖衕袺裗袹袸裀袾袶袼袷袽袲褁裉覕覘覗觝觚觛詎詍訹詙詀詗詘詄詅詒詈詑詊詌詏豟貁貀貺貾貰貹貵趄趀趉跘跓跍跇跖跜跏跕跙跈跗跅軯軷軺"],["dc40","軹軦軮軥軵軧軨軶軫軱軬軴軩逭逴逯鄆鄬鄄郿郼鄈郹郻鄁鄀鄇鄅鄃酡酤酟酢酠鈁鈊鈥鈃鈚鈦鈏鈌鈀鈒釿釽鈆鈄鈧鈂鈜鈤鈙鈗鈅鈖镻閍閌閐隇陾隈"],["dca1","隉隃隀雂雈雃雱雰靬靰靮頇颩飫鳦黹亃亄亶傽傿僆傮僄僊傴僈僂傰僁傺傱僋僉傶傸凗剺剸剻剼嗃嗛嗌嗐嗋嗊嗝嗀嗔嗄嗩喿嗒喍嗏嗕嗢嗖嗈嗲嗍嗙嗂圔塓塨塤塏塍塉塯塕塎塝塙塥塛堽塣塱壼嫇嫄嫋媺媸媱媵媰媿嫈媻嫆"],["dd40","媷嫀嫊媴媶嫍媹媐寖寘寙尟尳嵱嵣嵊嵥嵲嵬嵞嵨嵧嵢巰幏幎幊幍幋廅廌廆廋廇彀徯徭惷慉慊愫慅愶愲愮慆愯慏愩慀戠酨戣戥戤揅揱揫搐搒搉搠搤"],["dda1","搳摃搟搕搘搹搷搢搣搌搦搰搨摁搵搯搊搚摀搥搧搋揧搛搮搡搎敯斒旓暆暌暕暐暋暊暙暔晸朠楦楟椸楎楢楱椿楅楪椹楂楗楙楺楈楉椵楬椳椽楥棰楸椴楩楀楯楄楶楘楁楴楌椻楋椷楜楏楑椲楒椯楻椼歆歅歃歂歈歁殛嗀毻毼"],["de40","毹毷毸溛滖滈溏滀溟溓溔溠溱溹滆滒溽滁溞滉溷溰滍溦滏溲溾滃滜滘溙溒溎溍溤溡溿溳滐滊溗溮溣煇煔煒煣煠煁煝煢煲煸煪煡煂煘煃煋煰煟煐煓"],["dea1","煄煍煚牏犍犌犑犐犎猼獂猻猺獀獊獉瑄瑊瑋瑒瑑瑗瑀瑏瑐瑎瑂瑆瑍瑔瓡瓿瓾瓽甝畹畷榃痯瘏瘃痷痾痼痹痸瘐痻痶痭痵痽皙皵盝睕睟睠睒睖睚睩睧睔睙睭矠碇碚碔碏碄碕碅碆碡碃硹碙碀碖硻祼禂祽祹稑稘稙稒稗稕稢稓"],["df40","稛稐窣窢窞竫筦筤筭筴筩筲筥筳筱筰筡筸筶筣粲粴粯綈綆綀綍絿綅絺綎絻綃絼綌綔綄絽綒罭罫罧罨罬羦羥羧翛翜耡腤腠腷腜腩腛腢腲朡腞腶腧腯"],["dfa1","腄腡舝艉艄艀艂艅蓱萿葖葶葹蒏蒍葥葑葀蒆葧萰葍葽葚葙葴葳葝蔇葞萷萺萴葺葃葸萲葅萩菙葋萯葂萭葟葰萹葎葌葒葯蓅蒎萻葇萶萳葨葾葄萫葠葔葮葐蜋蜄蛷蜌蛺蛖蛵蝍蛸蜎蜉蜁蛶蜍蜅裖裋裍裎裞裛裚裌裐覅覛觟觥觤"],["e040","觡觠觢觜触詶誆詿詡訿詷誂誄詵誃誁詴詺谼豋豊豥豤豦貆貄貅賌赨赩趑趌趎趏趍趓趔趐趒跰跠跬跱跮跐跩跣跢跧跲跫跴輆軿輁輀輅輇輈輂輋遒逿"],["e0a1","遄遉逽鄐鄍鄏鄑鄖鄔鄋鄎酮酯鉈鉒鈰鈺鉦鈳鉥鉞銃鈮鉊鉆鉭鉬鉏鉠鉧鉯鈶鉡鉰鈱鉔鉣鉐鉲鉎鉓鉌鉖鈲閟閜閞閛隒隓隑隗雎雺雽雸雵靳靷靸靲頏頍頎颬飶飹馯馲馰馵骭骫魛鳪鳭鳧麀黽僦僔僗僨僳僛僪僝僤僓僬僰僯僣僠"],["e140","凘劀劁勩勫匰厬嘧嘕嘌嘒嗼嘏嘜嘁嘓嘂嗺嘝嘄嗿嗹墉塼墐墘墆墁塿塴墋塺墇墑墎塶墂墈塻墔墏壾奫嫜嫮嫥嫕嫪嫚嫭嫫嫳嫢嫠嫛嫬嫞嫝嫙嫨嫟孷寠"],["e1a1","寣屣嶂嶀嵽嶆嵺嶁嵷嶊嶉嶈嵾嵼嶍嵹嵿幘幙幓廘廑廗廎廜廕廙廒廔彄彃彯徶愬愨慁慞慱慳慒慓慲慬憀慴慔慺慛慥愻慪慡慖戩戧戫搫摍摛摝摴摶摲摳摽摵摦撦摎撂摞摜摋摓摠摐摿搿摬摫摙摥摷敳斠暡暠暟朅朄朢榱榶槉"],["e240","榠槎榖榰榬榼榑榙榎榧榍榩榾榯榿槄榽榤槔榹槊榚槏榳榓榪榡榞槙榗榐槂榵榥槆歊歍歋殞殟殠毃毄毾滎滵滱漃漥滸漷滻漮漉潎漙漚漧漘漻漒滭漊"],["e2a1","漶潳滹滮漭潀漰漼漵滫漇漎潃漅滽滶漹漜滼漺漟漍漞漈漡熇熐熉熀熅熂熏煻熆熁熗牄牓犗犕犓獃獍獑獌瑢瑳瑱瑵瑲瑧瑮甀甂甃畽疐瘖瘈瘌瘕瘑瘊瘔皸瞁睼瞅瞂睮瞀睯睾瞃碲碪碴碭碨硾碫碞碥碠碬碢碤禘禊禋禖禕禔禓"],["e340","禗禈禒禐稫穊稰稯稨稦窨窫窬竮箈箜箊箑箐箖箍箌箛箎箅箘劄箙箤箂粻粿粼粺綧綷緂綣綪緁緀緅綝緎緄緆緋緌綯綹綖綼綟綦綮綩綡緉罳翢翣翥翞"],["e3a1","耤聝聜膉膆膃膇膍膌膋舕蒗蒤蒡蒟蒺蓎蓂蒬蒮蒫蒹蒴蓁蓍蒪蒚蒱蓐蒝蒧蒻蒢蒔蓇蓌蒛蒩蒯蒨蓖蒘蒶蓏蒠蓗蓔蓒蓛蒰蒑虡蜳蜣蜨蝫蝀蜮蜞蜡蜙蜛蝃蜬蝁蜾蝆蜠蜲蜪蜭蜼蜒蜺蜱蜵蝂蜦蜧蜸蜤蜚蜰蜑裷裧裱裲裺裾裮裼裶裻"],["e440","裰裬裫覝覡覟覞觩觫觨誫誙誋誒誏誖谽豨豩賕賏賗趖踉踂跿踍跽踊踃踇踆踅跾踀踄輐輑輎輍鄣鄜鄠鄢鄟鄝鄚鄤鄡鄛酺酲酹酳銥銤鉶銛鉺銠銔銪銍"],["e4a1","銦銚銫鉹銗鉿銣鋮銎銂銕銢鉽銈銡銊銆銌銙銧鉾銇銩銝銋鈭隞隡雿靘靽靺靾鞃鞀鞂靻鞄鞁靿韎韍頖颭颮餂餀餇馝馜駃馹馻馺駂馽駇骱髣髧鬾鬿魠魡魟鳱鳲鳵麧僿儃儰僸儆儇僶僾儋儌僽儊劋劌勱勯噈噂噌嘵噁噊噉噆噘"],["e540","噚噀嘳嘽嘬嘾嘸嘪嘺圚墫墝墱墠墣墯墬墥墡壿嫿嫴嫽嫷嫶嬃嫸嬂嫹嬁嬇嬅嬏屧嶙嶗嶟嶒嶢嶓嶕嶠嶜嶡嶚嶞幩幝幠幜緳廛廞廡彉徲憋憃慹憱憰憢憉"],["e5a1","憛憓憯憭憟憒憪憡憍慦憳戭摮摰撖撠撅撗撜撏撋撊撌撣撟摨撱撘敶敺敹敻斲斳暵暰暩暲暷暪暯樀樆樗槥槸樕槱槤樠槿槬槢樛樝槾樧槲槮樔槷槧橀樈槦槻樍槼槫樉樄樘樥樏槶樦樇槴樖歑殥殣殢殦氁氀毿氂潁漦潾澇濆澒"],["e640","澍澉澌潢潏澅潚澖潶潬澂潕潲潒潐潗澔澓潝漀潡潫潽潧澐潓澋潩潿澕潣潷潪潻熲熯熛熰熠熚熩熵熝熥熞熤熡熪熜熧熳犘犚獘獒獞獟獠獝獛獡獚獙"],["e6a1","獢璇璉璊璆璁瑽璅璈瑼瑹甈甇畾瘥瘞瘙瘝瘜瘣瘚瘨瘛皜皝皞皛瞍瞏瞉瞈磍碻磏磌磑磎磔磈磃磄磉禚禡禠禜禢禛歶稹窲窴窳箷篋箾箬篎箯箹篊箵糅糈糌糋緷緛緪緧緗緡縃緺緦緶緱緰緮緟罶羬羰羭翭翫翪翬翦翨聤聧膣膟"],["e740","膞膕膢膙膗舖艏艓艒艐艎艑蔤蔻蔏蔀蔩蔎蔉蔍蔟蔊蔧蔜蓻蔫蓺蔈蔌蓴蔪蓲蔕蓷蓫蓳蓼蔒蓪蓩蔖蓾蔨蔝蔮蔂蓽蔞蓶蔱蔦蓧蓨蓰蓯蓹蔘蔠蔰蔋蔙蔯虢"],["e7a1","蝖蝣蝤蝷蟡蝳蝘蝔蝛蝒蝡蝚蝑蝞蝭蝪蝐蝎蝟蝝蝯蝬蝺蝮蝜蝥蝏蝻蝵蝢蝧蝩衚褅褌褔褋褗褘褙褆褖褑褎褉覢覤覣觭觰觬諏諆誸諓諑諔諕誻諗誾諀諅諘諃誺誽諙谾豍貏賥賟賙賨賚賝賧趠趜趡趛踠踣踥踤踮踕踛踖踑踙踦踧"],["e840","踔踒踘踓踜踗踚輬輤輘輚輠輣輖輗遳遰遯遧遫鄯鄫鄩鄪鄲鄦鄮醅醆醊醁醂醄醀鋐鋃鋄鋀鋙銶鋏鋱鋟鋘鋩鋗鋝鋌鋯鋂鋨鋊鋈鋎鋦鋍鋕鋉鋠鋞鋧鋑鋓"],["e8a1","銵鋡鋆銴镼閬閫閮閰隤隢雓霅霈霂靚鞊鞎鞈韐韏頞頝頦頩頨頠頛頧颲餈飺餑餔餖餗餕駜駍駏駓駔駎駉駖駘駋駗駌骳髬髫髳髲髱魆魃魧魴魱魦魶魵魰魨魤魬鳼鳺鳽鳿鳷鴇鴀鳹鳻鴈鴅鴄麃黓鼏鼐儜儓儗儚儑凞匴叡噰噠噮"],["e940","噳噦噣噭噲噞噷圜圛壈墽壉墿墺壂墼壆嬗嬙嬛嬡嬔嬓嬐嬖嬨嬚嬠嬞寯嶬嶱嶩嶧嶵嶰嶮嶪嶨嶲嶭嶯嶴幧幨幦幯廩廧廦廨廥彋徼憝憨憖懅憴懆懁懌憺"],["e9a1","憿憸憌擗擖擐擏擉撽撉擃擛擳擙攳敿敼斢曈暾曀曊曋曏暽暻暺曌朣樴橦橉橧樲橨樾橝橭橶橛橑樨橚樻樿橁橪橤橐橏橔橯橩橠樼橞橖橕橍橎橆歕歔歖殧殪殫毈毇氄氃氆澭濋澣濇澼濎濈潞濄澽澞濊澨瀄澥澮澺澬澪濏澿澸"],["ea40","澢濉澫濍澯澲澰燅燂熿熸燖燀燁燋燔燊燇燏熽燘熼燆燚燛犝犞獩獦獧獬獥獫獪瑿璚璠璔璒璕璡甋疀瘯瘭瘱瘽瘳瘼瘵瘲瘰皻盦瞚瞝瞡瞜瞛瞢瞣瞕瞙"],["eaa1","瞗磝磩磥磪磞磣磛磡磢磭磟磠禤穄穈穇窶窸窵窱窷篞篣篧篝篕篥篚篨篹篔篪篢篜篫篘篟糒糔糗糐糑縒縡縗縌縟縠縓縎縜縕縚縢縋縏縖縍縔縥縤罃罻罼罺羱翯耪耩聬膱膦膮膹膵膫膰膬膴膲膷膧臲艕艖艗蕖蕅蕫蕍蕓蕡蕘"],["eb40","蕀蕆蕤蕁蕢蕄蕑蕇蕣蔾蕛蕱蕎蕮蕵蕕蕧蕠薌蕦蕝蕔蕥蕬虣虥虤螛螏螗螓螒螈螁螖螘蝹螇螣螅螐螑螝螄螔螜螚螉褞褦褰褭褮褧褱褢褩褣褯褬褟觱諠"],["eba1","諢諲諴諵諝謔諤諟諰諈諞諡諨諿諯諻貑貒貐賵賮賱賰賳赬赮趥趧踳踾踸蹀蹅踶踼踽蹁踰踿躽輶輮輵輲輹輷輴遶遹遻邆郺鄳鄵鄶醓醐醑醍醏錧錞錈錟錆錏鍺錸錼錛錣錒錁鍆錭錎錍鋋錝鋺錥錓鋹鋷錴錂錤鋿錩錹錵錪錔錌"],["ec40","錋鋾錉錀鋻錖閼闍閾閹閺閶閿閵閽隩雔霋霒霐鞙鞗鞔韰韸頵頯頲餤餟餧餩馞駮駬駥駤駰駣駪駩駧骹骿骴骻髶髺髹髷鬳鮀鮅鮇魼魾魻鮂鮓鮒鮐魺鮕"],["eca1","魽鮈鴥鴗鴠鴞鴔鴩鴝鴘鴢鴐鴙鴟麈麆麇麮麭黕黖黺鼒鼽儦儥儢儤儠儩勴嚓嚌嚍嚆嚄嚃噾嚂噿嚁壖壔壏壒嬭嬥嬲嬣嬬嬧嬦嬯嬮孻寱寲嶷幬幪徾徻懃憵憼懧懠懥懤懨懞擯擩擣擫擤擨斁斀斶旚曒檍檖檁檥檉檟檛檡檞檇檓檎"],["ed40","檕檃檨檤檑橿檦檚檅檌檒歛殭氉濌澩濴濔濣濜濭濧濦濞濲濝濢濨燡燱燨燲燤燰燢獳獮獯璗璲璫璐璪璭璱璥璯甐甑甒甏疄癃癈癉癇皤盩瞵瞫瞲瞷瞶"],["eda1","瞴瞱瞨矰磳磽礂磻磼磲礅磹磾礄禫禨穜穛穖穘穔穚窾竀竁簅簏篲簀篿篻簎篴簋篳簂簉簃簁篸篽簆篰篱簐簊糨縭縼繂縳顈縸縪繉繀繇縩繌縰縻縶繄縺罅罿罾罽翴翲耬膻臄臌臊臅臇膼臩艛艚艜薃薀薏薧薕薠薋薣蕻薤薚薞"],["ee40","蕷蕼薉薡蕺蕸蕗薎薖薆薍薙薝薁薢薂薈薅蕹蕶薘薐薟虨螾螪螭蟅螰螬螹螵螼螮蟉蟃蟂蟌螷螯蟄蟊螴螶螿螸螽蟞螲褵褳褼褾襁襒褷襂覭覯覮觲觳謞"],["eea1","謘謖謑謅謋謢謏謒謕謇謍謈謆謜謓謚豏豰豲豱豯貕貔賹赯蹎蹍蹓蹐蹌蹇轃轀邅遾鄸醚醢醛醙醟醡醝醠鎡鎃鎯鍤鍖鍇鍼鍘鍜鍶鍉鍐鍑鍠鍭鎏鍌鍪鍹鍗鍕鍒鍏鍱鍷鍻鍡鍞鍣鍧鎀鍎鍙闇闀闉闃闅閷隮隰隬霠霟霘霝霙鞚鞡鞜"],["ef40","鞞鞝韕韔韱顁顄顊顉顅顃餥餫餬餪餳餲餯餭餱餰馘馣馡騂駺駴駷駹駸駶駻駽駾駼騃骾髾髽鬁髼魈鮚鮨鮞鮛鮦鮡鮥鮤鮆鮢鮠鮯鴳鵁鵧鴶鴮鴯鴱鴸鴰"],["efa1","鵅鵂鵃鴾鴷鵀鴽翵鴭麊麉麍麰黈黚黻黿鼤鼣鼢齔龠儱儭儮嚘嚜嚗嚚嚝嚙奰嬼屩屪巀幭幮懘懟懭懮懱懪懰懫懖懩擿攄擽擸攁攃擼斔旛曚曛曘櫅檹檽櫡櫆檺檶檷櫇檴檭歞毉氋瀇瀌瀍瀁瀅瀔瀎濿瀀濻瀦濼濷瀊爁燿燹爃燽獶"],["f040","璸瓀璵瓁璾璶璻瓂甔甓癜癤癙癐癓癗癚皦皽盬矂瞺磿礌礓礔礉礐礒礑禭禬穟簜簩簙簠簟簭簝簦簨簢簥簰繜繐繖繣繘繢繟繑繠繗繓羵羳翷翸聵臑臒"],["f0a1","臐艟艞薴藆藀藃藂薳薵薽藇藄薿藋藎藈藅薱薶藒蘤薸薷薾虩蟧蟦蟢蟛蟫蟪蟥蟟蟳蟤蟔蟜蟓蟭蟘蟣螤蟗蟙蠁蟴蟨蟝襓襋襏襌襆襐襑襉謪謧謣謳謰謵譇謯謼謾謱謥謷謦謶謮謤謻謽謺豂豵貙貘貗賾贄贂贀蹜蹢蹠蹗蹖蹞蹥蹧"],["f140","蹛蹚蹡蹝蹩蹔轆轇轈轋鄨鄺鄻鄾醨醥醧醯醪鎵鎌鎒鎷鎛鎝鎉鎧鎎鎪鎞鎦鎕鎈鎙鎟鎍鎱鎑鎲鎤鎨鎴鎣鎥闒闓闑隳雗雚巂雟雘雝霣霢霥鞬鞮鞨鞫鞤鞪"],["f1a1","鞢鞥韗韙韖韘韺顐顑顒颸饁餼餺騏騋騉騍騄騑騊騅騇騆髀髜鬈鬄鬅鬩鬵魊魌魋鯇鯆鯃鮿鯁鮵鮸鯓鮶鯄鮹鮽鵜鵓鵏鵊鵛鵋鵙鵖鵌鵗鵒鵔鵟鵘鵚麎麌黟鼁鼀鼖鼥鼫鼪鼩鼨齌齕儴儵劖勷厴嚫嚭嚦嚧嚪嚬壚壝壛夒嬽嬾嬿巃幰"],["f240","徿懻攇攐攍攉攌攎斄旞旝曞櫧櫠櫌櫑櫙櫋櫟櫜櫐櫫櫏櫍櫞歠殰氌瀙瀧瀠瀖瀫瀡瀢瀣瀩瀗瀤瀜瀪爌爊爇爂爅犥犦犤犣犡瓋瓅璷瓃甖癠矉矊矄矱礝礛"],["f2a1","礡礜礗礞禰穧穨簳簼簹簬簻糬糪繶繵繸繰繷繯繺繲繴繨罋罊羃羆羷翽翾聸臗臕艤艡艣藫藱藭藙藡藨藚藗藬藲藸藘藟藣藜藑藰藦藯藞藢蠀蟺蠃蟶蟷蠉蠌蠋蠆蟼蠈蟿蠊蠂襢襚襛襗襡襜襘襝襙覈覷覶觶譐譈譊譀譓譖譔譋譕"],["f340","譑譂譒譗豃豷豶貚贆贇贉趬趪趭趫蹭蹸蹳蹪蹯蹻軂轒轑轏轐轓辴酀鄿醰醭鏞鏇鏏鏂鏚鏐鏹鏬鏌鏙鎩鏦鏊鏔鏮鏣鏕鏄鏎鏀鏒鏧镽闚闛雡霩霫霬霨霦"],["f3a1","鞳鞷鞶韝韞韟顜顙顝顗颿颽颻颾饈饇饃馦馧騚騕騥騝騤騛騢騠騧騣騞騜騔髂鬋鬊鬎鬌鬷鯪鯫鯠鯞鯤鯦鯢鯰鯔鯗鯬鯜鯙鯥鯕鯡鯚鵷鶁鶊鶄鶈鵱鶀鵸鶆鶋鶌鵽鵫鵴鵵鵰鵩鶅鵳鵻鶂鵯鵹鵿鶇鵨麔麑黀黼鼭齀齁齍齖齗齘匷嚲"],["f440","嚵嚳壣孅巆巇廮廯忀忁懹攗攖攕攓旟曨曣曤櫳櫰櫪櫨櫹櫱櫮櫯瀼瀵瀯瀷瀴瀱灂瀸瀿瀺瀹灀瀻瀳灁爓爔犨獽獼璺皫皪皾盭矌矎矏矍矲礥礣礧礨礤礩"],["f4a1","禲穮穬穭竷籉籈籊籇籅糮繻繾纁纀羺翿聹臛臙舋艨艩蘢藿蘁藾蘛蘀藶蘄蘉蘅蘌藽蠙蠐蠑蠗蠓蠖襣襦覹觷譠譪譝譨譣譥譧譭趮躆躈躄轙轖轗轕轘轚邍酃酁醷醵醲醳鐋鐓鏻鐠鐏鐔鏾鐕鐐鐨鐙鐍鏵鐀鏷鐇鐎鐖鐒鏺鐉鏸鐊鏿"],["f540","鏼鐌鏶鐑鐆闞闠闟霮霯鞹鞻韽韾顠顢顣顟飁飂饐饎饙饌饋饓騲騴騱騬騪騶騩騮騸騭髇髊髆鬐鬒鬑鰋鰈鯷鰅鰒鯸鱀鰇鰎鰆鰗鰔鰉鶟鶙鶤鶝鶒鶘鶐鶛"],["f5a1","鶠鶔鶜鶪鶗鶡鶚鶢鶨鶞鶣鶿鶩鶖鶦鶧麙麛麚黥黤黧黦鼰鼮齛齠齞齝齙龑儺儹劘劗囃嚽嚾孈孇巋巏廱懽攛欂櫼欃櫸欀灃灄灊灈灉灅灆爝爚爙獾甗癪矐礭礱礯籔籓糲纊纇纈纋纆纍罍羻耰臝蘘蘪蘦蘟蘣蘜蘙蘧蘮蘡蘠蘩蘞蘥"],["f640","蠩蠝蠛蠠蠤蠜蠫衊襭襩襮襫觺譹譸譅譺譻贐贔趯躎躌轞轛轝酆酄酅醹鐿鐻鐶鐩鐽鐼鐰鐹鐪鐷鐬鑀鐱闥闤闣霵霺鞿韡顤飉飆飀饘饖騹騽驆驄驂驁騺"],["f6a1","騿髍鬕鬗鬘鬖鬺魒鰫鰝鰜鰬鰣鰨鰩鰤鰡鶷鶶鶼鷁鷇鷊鷏鶾鷅鷃鶻鶵鷎鶹鶺鶬鷈鶱鶭鷌鶳鷍鶲鹺麜黫黮黭鼛鼘鼚鼱齎齥齤龒亹囆囅囋奱孋孌巕巑廲攡攠攦攢欋欈欉氍灕灖灗灒爞爟犩獿瓘瓕瓙瓗癭皭礵禴穰穱籗籜籙籛籚"],["f740","糴糱纑罏羇臞艫蘴蘵蘳蘬蘲蘶蠬蠨蠦蠪蠥襱覿覾觻譾讄讂讆讅譿贕躕躔躚躒躐躖躗轠轢酇鑌鑐鑊鑋鑏鑇鑅鑈鑉鑆霿韣顪顩飋饔饛驎驓驔驌驏驈驊"],["f7a1","驉驒驐髐鬙鬫鬻魖魕鱆鱈鰿鱄鰹鰳鱁鰼鰷鰴鰲鰽鰶鷛鷒鷞鷚鷋鷐鷜鷑鷟鷩鷙鷘鷖鷵鷕鷝麶黰鼵鼳鼲齂齫龕龢儽劙壨壧奲孍巘蠯彏戁戃戄攩攥斖曫欑欒欏毊灛灚爢玂玁玃癰矔籧籦纕艬蘺虀蘹蘼蘱蘻蘾蠰蠲蠮蠳襶襴襳觾"],["f840","讌讎讋讈豅贙躘轤轣醼鑢鑕鑝鑗鑞韄韅頀驖驙鬞鬟鬠鱒鱘鱐鱊鱍鱋鱕鱙鱌鱎鷻鷷鷯鷣鷫鷸鷤鷶鷡鷮鷦鷲鷰鷢鷬鷴鷳鷨鷭黂黐黲黳鼆鼜鼸鼷鼶齃齏"],["f8a1","齱齰齮齯囓囍孎屭攭曭曮欓灟灡灝灠爣瓛瓥矕礸禷禶籪纗羉艭虃蠸蠷蠵衋讔讕躞躟躠躝醾醽釂鑫鑨鑩雥靆靃靇韇韥驞髕魙鱣鱧鱦鱢鱞鱠鸂鷾鸇鸃鸆鸅鸀鸁鸉鷿鷽鸄麠鼞齆齴齵齶囔攮斸欘欙欗欚灢爦犪矘矙礹籩籫糶纚"],["f940","纘纛纙臠臡虆虇虈襹襺襼襻觿讘讙躥躤躣鑮鑭鑯鑱鑳靉顲饟鱨鱮鱭鸋鸍鸐鸏鸒鸑麡黵鼉齇齸齻齺齹圞灦籯蠼趲躦釃鑴鑸鑶鑵驠鱴鱳鱱鱵鸔鸓黶鼊"],["f9a1","龤灨灥糷虪蠾蠽蠿讞貜躩軉靋顳顴飌饡馫驤驦驧鬤鸕鸗齈戇欞爧虌躨钂钀钁驩驨鬮鸙爩虋讟钃鱹麷癵驫鱺鸝灩灪麤齾齉龘碁銹裏墻恒粧嫺╔╦╗╠╬╣╚╩╝╒╤╕╞╪╡╘╧╛╓╥╖╟╫╢╙╨╜║═╭╮╰╯▓"]]'); + +/***/ }), + +/***/ 4231: +/***/ ((module) => { + +"use strict"; +module.exports = /*#__PURE__*/JSON.parse('[["0","\\u0000",127],["8ea1","。",62],["a1a1"," 、。,.・:;?!゛゜´`¨^ ̄_ヽヾゝゞ〃仝々〆〇ー―‐/\~∥|…‥‘’“”()〔〕[]{}〈",9,"+-±×÷=≠<>≦≧∞∴♂♀°′″℃¥$¢£%#&*@§☆★○●◎◇"],["a2a1","◆□■△▲▽▼※〒→←↑↓〓"],["a2ba","∈∋⊆⊇⊂⊃∪∩"],["a2ca","∧∨¬⇒⇔∀∃"],["a2dc","∠⊥⌒∂∇≡≒≪≫√∽∝∵∫∬"],["a2f2","ʼn♯♭♪†‡¶"],["a2fe","◯"],["a3b0","0",9],["a3c1","A",25],["a3e1","a",25],["a4a1","ぁ",82],["a5a1","ァ",85],["a6a1","Α",16,"Σ",6],["a6c1","α",16,"σ",6],["a7a1","А",5,"ЁЖ",25],["a7d1","а",5,"ёж",25],["a8a1","─│┌┐┘└├┬┤┴┼━┃┏┓┛┗┣┳┫┻╋┠┯┨┷┿┝┰┥┸╂"],["ada1","①",19,"Ⅰ",9],["adc0","㍉㌔㌢㍍㌘㌧㌃㌶㍑㍗㌍㌦㌣㌫㍊㌻㎜㎝㎞㎎㎏㏄㎡"],["addf","㍻〝〟№㏍℡㊤",4,"㈱㈲㈹㍾㍽㍼≒≡∫∮∑√⊥∠∟⊿∵∩∪"],["b0a1","亜唖娃阿哀愛挨姶逢葵茜穐悪握渥旭葦芦鯵梓圧斡扱宛姐虻飴絢綾鮎或粟袷安庵按暗案闇鞍杏以伊位依偉囲夷委威尉惟意慰易椅為畏異移維緯胃萎衣謂違遺医井亥域育郁磯一壱溢逸稲茨芋鰯允印咽員因姻引飲淫胤蔭"],["b1a1","院陰隠韻吋右宇烏羽迂雨卯鵜窺丑碓臼渦嘘唄欝蔚鰻姥厩浦瓜閏噂云運雲荏餌叡営嬰影映曳栄永泳洩瑛盈穎頴英衛詠鋭液疫益駅悦謁越閲榎厭円園堰奄宴延怨掩援沿演炎焔煙燕猿縁艶苑薗遠鉛鴛塩於汚甥凹央奥往応"],["b2a1","押旺横欧殴王翁襖鴬鴎黄岡沖荻億屋憶臆桶牡乙俺卸恩温穏音下化仮何伽価佳加可嘉夏嫁家寡科暇果架歌河火珂禍禾稼箇花苛茄荷華菓蝦課嘩貨迦過霞蚊俄峨我牙画臥芽蛾賀雅餓駕介会解回塊壊廻快怪悔恢懐戒拐改"],["b3a1","魁晦械海灰界皆絵芥蟹開階貝凱劾外咳害崖慨概涯碍蓋街該鎧骸浬馨蛙垣柿蛎鈎劃嚇各廓拡撹格核殻獲確穫覚角赫較郭閣隔革学岳楽額顎掛笠樫橿梶鰍潟割喝恰括活渇滑葛褐轄且鰹叶椛樺鞄株兜竃蒲釜鎌噛鴨栢茅萱"],["b4a1","粥刈苅瓦乾侃冠寒刊勘勧巻喚堪姦完官寛干幹患感慣憾換敢柑桓棺款歓汗漢澗潅環甘監看竿管簡緩缶翰肝艦莞観諌貫還鑑間閑関陥韓館舘丸含岸巌玩癌眼岩翫贋雁頑顔願企伎危喜器基奇嬉寄岐希幾忌揮机旗既期棋棄"],["b5a1","機帰毅気汽畿祈季稀紀徽規記貴起軌輝飢騎鬼亀偽儀妓宜戯技擬欺犠疑祇義蟻誼議掬菊鞠吉吃喫桔橘詰砧杵黍却客脚虐逆丘久仇休及吸宮弓急救朽求汲泣灸球究窮笈級糾給旧牛去居巨拒拠挙渠虚許距鋸漁禦魚亨享京"],["b6a1","供侠僑兇競共凶協匡卿叫喬境峡強彊怯恐恭挟教橋況狂狭矯胸脅興蕎郷鏡響饗驚仰凝尭暁業局曲極玉桐粁僅勤均巾錦斤欣欽琴禁禽筋緊芹菌衿襟謹近金吟銀九倶句区狗玖矩苦躯駆駈駒具愚虞喰空偶寓遇隅串櫛釧屑屈"],["b7a1","掘窟沓靴轡窪熊隈粂栗繰桑鍬勲君薫訓群軍郡卦袈祁係傾刑兄啓圭珪型契形径恵慶慧憩掲携敬景桂渓畦稽系経継繋罫茎荊蛍計詣警軽頚鶏芸迎鯨劇戟撃激隙桁傑欠決潔穴結血訣月件倹倦健兼券剣喧圏堅嫌建憲懸拳捲"],["b8a1","検権牽犬献研硯絹県肩見謙賢軒遣鍵険顕験鹸元原厳幻弦減源玄現絃舷言諺限乎個古呼固姑孤己庫弧戸故枯湖狐糊袴股胡菰虎誇跨鈷雇顧鼓五互伍午呉吾娯後御悟梧檎瑚碁語誤護醐乞鯉交佼侯候倖光公功効勾厚口向"],["b9a1","后喉坑垢好孔孝宏工巧巷幸広庚康弘恒慌抗拘控攻昂晃更杭校梗構江洪浩港溝甲皇硬稿糠紅紘絞綱耕考肯肱腔膏航荒行衡講貢購郊酵鉱砿鋼閤降項香高鴻剛劫号合壕拷濠豪轟麹克刻告国穀酷鵠黒獄漉腰甑忽惚骨狛込"],["baa1","此頃今困坤墾婚恨懇昏昆根梱混痕紺艮魂些佐叉唆嵯左差査沙瑳砂詐鎖裟坐座挫債催再最哉塞妻宰彩才採栽歳済災采犀砕砦祭斎細菜裁載際剤在材罪財冴坂阪堺榊肴咲崎埼碕鷺作削咋搾昨朔柵窄策索錯桜鮭笹匙冊刷"],["bba1","察拶撮擦札殺薩雑皐鯖捌錆鮫皿晒三傘参山惨撒散桟燦珊産算纂蚕讃賛酸餐斬暫残仕仔伺使刺司史嗣四士始姉姿子屍市師志思指支孜斯施旨枝止死氏獅祉私糸紙紫肢脂至視詞詩試誌諮資賜雌飼歯事似侍児字寺慈持時"],["bca1","次滋治爾璽痔磁示而耳自蒔辞汐鹿式識鴫竺軸宍雫七叱執失嫉室悉湿漆疾質実蔀篠偲柴芝屡蕊縞舎写射捨赦斜煮社紗者謝車遮蛇邪借勺尺杓灼爵酌釈錫若寂弱惹主取守手朱殊狩珠種腫趣酒首儒受呪寿授樹綬需囚収周"],["bda1","宗就州修愁拾洲秀秋終繍習臭舟蒐衆襲讐蹴輯週酋酬集醜什住充十従戎柔汁渋獣縦重銃叔夙宿淑祝縮粛塾熟出術述俊峻春瞬竣舜駿准循旬楯殉淳準潤盾純巡遵醇順処初所暑曙渚庶緒署書薯藷諸助叙女序徐恕鋤除傷償"],["bea1","勝匠升召哨商唱嘗奨妾娼宵将小少尚庄床廠彰承抄招掌捷昇昌昭晶松梢樟樵沼消渉湘焼焦照症省硝礁祥称章笑粧紹肖菖蒋蕉衝裳訟証詔詳象賞醤鉦鍾鐘障鞘上丈丞乗冗剰城場壌嬢常情擾条杖浄状畳穣蒸譲醸錠嘱埴飾"],["bfa1","拭植殖燭織職色触食蝕辱尻伸信侵唇娠寝審心慎振新晋森榛浸深申疹真神秦紳臣芯薪親診身辛進針震人仁刃塵壬尋甚尽腎訊迅陣靭笥諏須酢図厨逗吹垂帥推水炊睡粋翠衰遂酔錐錘随瑞髄崇嵩数枢趨雛据杉椙菅頗雀裾"],["c0a1","澄摺寸世瀬畝是凄制勢姓征性成政整星晴棲栖正清牲生盛精聖声製西誠誓請逝醒青静斉税脆隻席惜戚斥昔析石積籍績脊責赤跡蹟碩切拙接摂折設窃節説雪絶舌蝉仙先千占宣専尖川戦扇撰栓栴泉浅洗染潜煎煽旋穿箭線"],["c1a1","繊羨腺舛船薦詮賎践選遷銭銑閃鮮前善漸然全禅繕膳糎噌塑岨措曾曽楚狙疏疎礎祖租粗素組蘇訴阻遡鼠僧創双叢倉喪壮奏爽宋層匝惣想捜掃挿掻操早曹巣槍槽漕燥争痩相窓糟総綜聡草荘葬蒼藻装走送遭鎗霜騒像増憎"],["c2a1","臓蔵贈造促側則即息捉束測足速俗属賊族続卒袖其揃存孫尊損村遜他多太汰詑唾堕妥惰打柁舵楕陀駄騨体堆対耐岱帯待怠態戴替泰滞胎腿苔袋貸退逮隊黛鯛代台大第醍題鷹滝瀧卓啄宅托択拓沢濯琢託鐸濁諾茸凧蛸只"],["c3a1","叩但達辰奪脱巽竪辿棚谷狸鱈樽誰丹単嘆坦担探旦歎淡湛炭短端箪綻耽胆蛋誕鍛団壇弾断暖檀段男談値知地弛恥智池痴稚置致蜘遅馳築畜竹筑蓄逐秩窒茶嫡着中仲宙忠抽昼柱注虫衷註酎鋳駐樗瀦猪苧著貯丁兆凋喋寵"],["c4a1","帖帳庁弔張彫徴懲挑暢朝潮牒町眺聴脹腸蝶調諜超跳銚長頂鳥勅捗直朕沈珍賃鎮陳津墜椎槌追鎚痛通塚栂掴槻佃漬柘辻蔦綴鍔椿潰坪壷嬬紬爪吊釣鶴亭低停偵剃貞呈堤定帝底庭廷弟悌抵挺提梯汀碇禎程締艇訂諦蹄逓"],["c5a1","邸鄭釘鼎泥摘擢敵滴的笛適鏑溺哲徹撤轍迭鉄典填天展店添纏甜貼転顛点伝殿澱田電兎吐堵塗妬屠徒斗杜渡登菟賭途都鍍砥砺努度土奴怒倒党冬凍刀唐塔塘套宕島嶋悼投搭東桃梼棟盗淘湯涛灯燈当痘祷等答筒糖統到"],["c6a1","董蕩藤討謄豆踏逃透鐙陶頭騰闘働動同堂導憧撞洞瞳童胴萄道銅峠鴇匿得徳涜特督禿篤毒独読栃橡凸突椴届鳶苫寅酉瀞噸屯惇敦沌豚遁頓呑曇鈍奈那内乍凪薙謎灘捺鍋楢馴縄畷南楠軟難汝二尼弐迩匂賑肉虹廿日乳入"],["c7a1","如尿韮任妊忍認濡禰祢寧葱猫熱年念捻撚燃粘乃廼之埜嚢悩濃納能脳膿農覗蚤巴把播覇杷波派琶破婆罵芭馬俳廃拝排敗杯盃牌背肺輩配倍培媒梅楳煤狽買売賠陪這蝿秤矧萩伯剥博拍柏泊白箔粕舶薄迫曝漠爆縛莫駁麦"],["c8a1","函箱硲箸肇筈櫨幡肌畑畠八鉢溌発醗髪伐罰抜筏閥鳩噺塙蛤隼伴判半反叛帆搬斑板氾汎版犯班畔繁般藩販範釆煩頒飯挽晩番盤磐蕃蛮匪卑否妃庇彼悲扉批披斐比泌疲皮碑秘緋罷肥被誹費避非飛樋簸備尾微枇毘琵眉美"],["c9a1","鼻柊稗匹疋髭彦膝菱肘弼必畢筆逼桧姫媛紐百謬俵彪標氷漂瓢票表評豹廟描病秒苗錨鋲蒜蛭鰭品彬斌浜瀕貧賓頻敏瓶不付埠夫婦富冨布府怖扶敷斧普浮父符腐膚芙譜負賦赴阜附侮撫武舞葡蕪部封楓風葺蕗伏副復幅服"],["caa1","福腹複覆淵弗払沸仏物鮒分吻噴墳憤扮焚奮粉糞紛雰文聞丙併兵塀幣平弊柄並蔽閉陛米頁僻壁癖碧別瞥蔑箆偏変片篇編辺返遍便勉娩弁鞭保舗鋪圃捕歩甫補輔穂募墓慕戊暮母簿菩倣俸包呆報奉宝峰峯崩庖抱捧放方朋"],["cba1","法泡烹砲縫胞芳萌蓬蜂褒訪豊邦鋒飽鳳鵬乏亡傍剖坊妨帽忘忙房暴望某棒冒紡肪膨謀貌貿鉾防吠頬北僕卜墨撲朴牧睦穆釦勃没殆堀幌奔本翻凡盆摩磨魔麻埋妹昧枚毎哩槙幕膜枕鮪柾鱒桝亦俣又抹末沫迄侭繭麿万慢満"],["cca1","漫蔓味未魅巳箕岬密蜜湊蓑稔脈妙粍民眠務夢無牟矛霧鵡椋婿娘冥名命明盟迷銘鳴姪牝滅免棉綿緬面麺摸模茂妄孟毛猛盲網耗蒙儲木黙目杢勿餅尤戻籾貰問悶紋門匁也冶夜爺耶野弥矢厄役約薬訳躍靖柳薮鑓愉愈油癒"],["cda1","諭輸唯佑優勇友宥幽悠憂揖有柚湧涌猶猷由祐裕誘遊邑郵雄融夕予余与誉輿預傭幼妖容庸揚揺擁曜楊様洋溶熔用窯羊耀葉蓉要謡踊遥陽養慾抑欲沃浴翌翼淀羅螺裸来莱頼雷洛絡落酪乱卵嵐欄濫藍蘭覧利吏履李梨理璃"],["cea1","痢裏裡里離陸律率立葎掠略劉流溜琉留硫粒隆竜龍侶慮旅虜了亮僚両凌寮料梁涼猟療瞭稜糧良諒遼量陵領力緑倫厘林淋燐琳臨輪隣鱗麟瑠塁涙累類令伶例冷励嶺怜玲礼苓鈴隷零霊麗齢暦歴列劣烈裂廉恋憐漣煉簾練聯"],["cfa1","蓮連錬呂魯櫓炉賂路露労婁廊弄朗楼榔浪漏牢狼篭老聾蝋郎六麓禄肋録論倭和話歪賄脇惑枠鷲亙亘鰐詫藁蕨椀湾碗腕"],["d0a1","弌丐丕个丱丶丼丿乂乖乘亂亅豫亊舒弍于亞亟亠亢亰亳亶从仍仄仆仂仗仞仭仟价伉佚估佛佝佗佇佶侈侏侘佻佩佰侑佯來侖儘俔俟俎俘俛俑俚俐俤俥倚倨倔倪倥倅伜俶倡倩倬俾俯們倆偃假會偕偐偈做偖偬偸傀傚傅傴傲"],["d1a1","僉僊傳僂僖僞僥僭僣僮價僵儉儁儂儖儕儔儚儡儺儷儼儻儿兀兒兌兔兢竸兩兪兮冀冂囘册冉冏冑冓冕冖冤冦冢冩冪冫决冱冲冰况冽凅凉凛几處凩凭凰凵凾刄刋刔刎刧刪刮刳刹剏剄剋剌剞剔剪剴剩剳剿剽劍劔劒剱劈劑辨"],["d2a1","辧劬劭劼劵勁勍勗勞勣勦飭勠勳勵勸勹匆匈甸匍匐匏匕匚匣匯匱匳匸區卆卅丗卉卍凖卞卩卮夘卻卷厂厖厠厦厥厮厰厶參簒雙叟曼燮叮叨叭叺吁吽呀听吭吼吮吶吩吝呎咏呵咎呟呱呷呰咒呻咀呶咄咐咆哇咢咸咥咬哄哈咨"],["d3a1","咫哂咤咾咼哘哥哦唏唔哽哮哭哺哢唹啀啣啌售啜啅啖啗唸唳啝喙喀咯喊喟啻啾喘喞單啼喃喩喇喨嗚嗅嗟嗄嗜嗤嗔嘔嗷嘖嗾嗽嘛嗹噎噐營嘴嘶嘲嘸噫噤嘯噬噪嚆嚀嚊嚠嚔嚏嚥嚮嚶嚴囂嚼囁囃囀囈囎囑囓囗囮囹圀囿圄圉"],["d4a1","圈國圍圓團圖嗇圜圦圷圸坎圻址坏坩埀垈坡坿垉垓垠垳垤垪垰埃埆埔埒埓堊埖埣堋堙堝塲堡塢塋塰毀塒堽塹墅墹墟墫墺壞墻墸墮壅壓壑壗壙壘壥壜壤壟壯壺壹壻壼壽夂夊夐夛梦夥夬夭夲夸夾竒奕奐奎奚奘奢奠奧奬奩"],["d5a1","奸妁妝佞侫妣妲姆姨姜妍姙姚娥娟娑娜娉娚婀婬婉娵娶婢婪媚媼媾嫋嫂媽嫣嫗嫦嫩嫖嫺嫻嬌嬋嬖嬲嫐嬪嬶嬾孃孅孀孑孕孚孛孥孩孰孳孵學斈孺宀它宦宸寃寇寉寔寐寤實寢寞寥寫寰寶寳尅將專對尓尠尢尨尸尹屁屆屎屓"],["d6a1","屐屏孱屬屮乢屶屹岌岑岔妛岫岻岶岼岷峅岾峇峙峩峽峺峭嶌峪崋崕崗嵜崟崛崑崔崢崚崙崘嵌嵒嵎嵋嵬嵳嵶嶇嶄嶂嶢嶝嶬嶮嶽嶐嶷嶼巉巍巓巒巖巛巫已巵帋帚帙帑帛帶帷幄幃幀幎幗幔幟幢幤幇幵并幺麼广庠廁廂廈廐廏"],["d7a1","廖廣廝廚廛廢廡廨廩廬廱廳廰廴廸廾弃弉彝彜弋弑弖弩弭弸彁彈彌彎弯彑彖彗彙彡彭彳彷徃徂彿徊很徑徇從徙徘徠徨徭徼忖忻忤忸忱忝悳忿怡恠怙怐怩怎怱怛怕怫怦怏怺恚恁恪恷恟恊恆恍恣恃恤恂恬恫恙悁悍惧悃悚"],["d8a1","悄悛悖悗悒悧悋惡悸惠惓悴忰悽惆悵惘慍愕愆惶惷愀惴惺愃愡惻惱愍愎慇愾愨愧慊愿愼愬愴愽慂慄慳慷慘慙慚慫慴慯慥慱慟慝慓慵憙憖憇憬憔憚憊憑憫憮懌懊應懷懈懃懆憺懋罹懍懦懣懶懺懴懿懽懼懾戀戈戉戍戌戔戛"],["d9a1","戞戡截戮戰戲戳扁扎扞扣扛扠扨扼抂抉找抒抓抖拔抃抔拗拑抻拏拿拆擔拈拜拌拊拂拇抛拉挌拮拱挧挂挈拯拵捐挾捍搜捏掖掎掀掫捶掣掏掉掟掵捫捩掾揩揀揆揣揉插揶揄搖搴搆搓搦搶攝搗搨搏摧摯摶摎攪撕撓撥撩撈撼"],["daa1","據擒擅擇撻擘擂擱擧舉擠擡抬擣擯攬擶擴擲擺攀擽攘攜攅攤攣攫攴攵攷收攸畋效敖敕敍敘敞敝敲數斂斃變斛斟斫斷旃旆旁旄旌旒旛旙无旡旱杲昊昃旻杳昵昶昴昜晏晄晉晁晞晝晤晧晨晟晢晰暃暈暎暉暄暘暝曁暹曉暾暼"],["dba1","曄暸曖曚曠昿曦曩曰曵曷朏朖朞朦朧霸朮朿朶杁朸朷杆杞杠杙杣杤枉杰枩杼杪枌枋枦枡枅枷柯枴柬枳柩枸柤柞柝柢柮枹柎柆柧檜栞框栩桀桍栲桎梳栫桙档桷桿梟梏梭梔條梛梃檮梹桴梵梠梺椏梍桾椁棊椈棘椢椦棡椌棍"],["dca1","棔棧棕椶椒椄棗棣椥棹棠棯椨椪椚椣椡棆楹楷楜楸楫楔楾楮椹楴椽楙椰楡楞楝榁楪榲榮槐榿槁槓榾槎寨槊槝榻槃榧樮榑榠榜榕榴槞槨樂樛槿權槹槲槧樅榱樞槭樔槫樊樒櫁樣樓橄樌橲樶橸橇橢橙橦橈樸樢檐檍檠檄檢檣"],["dda1","檗蘗檻櫃櫂檸檳檬櫞櫑櫟檪櫚櫪櫻欅蘖櫺欒欖鬱欟欸欷盜欹飮歇歃歉歐歙歔歛歟歡歸歹歿殀殄殃殍殘殕殞殤殪殫殯殲殱殳殷殼毆毋毓毟毬毫毳毯麾氈氓气氛氤氣汞汕汢汪沂沍沚沁沛汾汨汳沒沐泄泱泓沽泗泅泝沮沱沾"],["dea1","沺泛泯泙泪洟衍洶洫洽洸洙洵洳洒洌浣涓浤浚浹浙涎涕濤涅淹渕渊涵淇淦涸淆淬淞淌淨淒淅淺淙淤淕淪淮渭湮渮渙湲湟渾渣湫渫湶湍渟湃渺湎渤滿渝游溂溪溘滉溷滓溽溯滄溲滔滕溏溥滂溟潁漑灌滬滸滾漿滲漱滯漲滌"],["dfa1","漾漓滷澆潺潸澁澀潯潛濳潭澂潼潘澎澑濂潦澳澣澡澤澹濆澪濟濕濬濔濘濱濮濛瀉瀋濺瀑瀁瀏濾瀛瀚潴瀝瀘瀟瀰瀾瀲灑灣炙炒炯烱炬炸炳炮烟烋烝烙焉烽焜焙煥煕熈煦煢煌煖煬熏燻熄熕熨熬燗熹熾燒燉燔燎燠燬燧燵燼"],["e0a1","燹燿爍爐爛爨爭爬爰爲爻爼爿牀牆牋牘牴牾犂犁犇犒犖犢犧犹犲狃狆狄狎狒狢狠狡狹狷倏猗猊猜猖猝猴猯猩猥猾獎獏默獗獪獨獰獸獵獻獺珈玳珎玻珀珥珮珞璢琅瑯琥珸琲琺瑕琿瑟瑙瑁瑜瑩瑰瑣瑪瑶瑾璋璞璧瓊瓏瓔珱"],["e1a1","瓠瓣瓧瓩瓮瓲瓰瓱瓸瓷甄甃甅甌甎甍甕甓甞甦甬甼畄畍畊畉畛畆畚畩畤畧畫畭畸當疆疇畴疊疉疂疔疚疝疥疣痂疳痃疵疽疸疼疱痍痊痒痙痣痞痾痿痼瘁痰痺痲痳瘋瘍瘉瘟瘧瘠瘡瘢瘤瘴瘰瘻癇癈癆癜癘癡癢癨癩癪癧癬癰"],["e2a1","癲癶癸發皀皃皈皋皎皖皓皙皚皰皴皸皹皺盂盍盖盒盞盡盥盧盪蘯盻眈眇眄眩眤眞眥眦眛眷眸睇睚睨睫睛睥睿睾睹瞎瞋瞑瞠瞞瞰瞶瞹瞿瞼瞽瞻矇矍矗矚矜矣矮矼砌砒礦砠礪硅碎硴碆硼碚碌碣碵碪碯磑磆磋磔碾碼磅磊磬"],["e3a1","磧磚磽磴礇礒礑礙礬礫祀祠祗祟祚祕祓祺祿禊禝禧齋禪禮禳禹禺秉秕秧秬秡秣稈稍稘稙稠稟禀稱稻稾稷穃穗穉穡穢穩龝穰穹穽窈窗窕窘窖窩竈窰窶竅竄窿邃竇竊竍竏竕竓站竚竝竡竢竦竭竰笂笏笊笆笳笘笙笞笵笨笶筐"],["e4a1","筺笄筍笋筌筅筵筥筴筧筰筱筬筮箝箘箟箍箜箚箋箒箏筝箙篋篁篌篏箴篆篝篩簑簔篦篥籠簀簇簓篳篷簗簍篶簣簧簪簟簷簫簽籌籃籔籏籀籐籘籟籤籖籥籬籵粃粐粤粭粢粫粡粨粳粲粱粮粹粽糀糅糂糘糒糜糢鬻糯糲糴糶糺紆"],["e5a1","紂紜紕紊絅絋紮紲紿紵絆絳絖絎絲絨絮絏絣經綉絛綏絽綛綺綮綣綵緇綽綫總綢綯緜綸綟綰緘緝緤緞緻緲緡縅縊縣縡縒縱縟縉縋縢繆繦縻縵縹繃縷縲縺繧繝繖繞繙繚繹繪繩繼繻纃緕繽辮繿纈纉續纒纐纓纔纖纎纛纜缸缺"],["e6a1","罅罌罍罎罐网罕罔罘罟罠罨罩罧罸羂羆羃羈羇羌羔羞羝羚羣羯羲羹羮羶羸譱翅翆翊翕翔翡翦翩翳翹飜耆耄耋耒耘耙耜耡耨耿耻聊聆聒聘聚聟聢聨聳聲聰聶聹聽聿肄肆肅肛肓肚肭冐肬胛胥胙胝胄胚胖脉胯胱脛脩脣脯腋"],["e7a1","隋腆脾腓腑胼腱腮腥腦腴膃膈膊膀膂膠膕膤膣腟膓膩膰膵膾膸膽臀臂膺臉臍臑臙臘臈臚臟臠臧臺臻臾舁舂舅與舊舍舐舖舩舫舸舳艀艙艘艝艚艟艤艢艨艪艫舮艱艷艸艾芍芒芫芟芻芬苡苣苟苒苴苳苺莓范苻苹苞茆苜茉苙"],["e8a1","茵茴茖茲茱荀茹荐荅茯茫茗茘莅莚莪莟莢莖茣莎莇莊荼莵荳荵莠莉莨菴萓菫菎菽萃菘萋菁菷萇菠菲萍萢萠莽萸蔆菻葭萪萼蕚蒄葷葫蒭葮蒂葩葆萬葯葹萵蓊葢蒹蒿蒟蓙蓍蒻蓚蓐蓁蓆蓖蒡蔡蓿蓴蔗蔘蔬蔟蔕蔔蓼蕀蕣蕘蕈"],["e9a1","蕁蘂蕋蕕薀薤薈薑薊薨蕭薔薛藪薇薜蕷蕾薐藉薺藏薹藐藕藝藥藜藹蘊蘓蘋藾藺蘆蘢蘚蘰蘿虍乕虔號虧虱蚓蚣蚩蚪蚋蚌蚶蚯蛄蛆蚰蛉蠣蚫蛔蛞蛩蛬蛟蛛蛯蜒蜆蜈蜀蜃蛻蜑蜉蜍蛹蜊蜴蜿蜷蜻蜥蜩蜚蝠蝟蝸蝌蝎蝴蝗蝨蝮蝙"],["eaa1","蝓蝣蝪蠅螢螟螂螯蟋螽蟀蟐雖螫蟄螳蟇蟆螻蟯蟲蟠蠏蠍蟾蟶蟷蠎蟒蠑蠖蠕蠢蠡蠱蠶蠹蠧蠻衄衂衒衙衞衢衫袁衾袞衵衽袵衲袂袗袒袮袙袢袍袤袰袿袱裃裄裔裘裙裝裹褂裼裴裨裲褄褌褊褓襃褞褥褪褫襁襄褻褶褸襌褝襠襞"],["eba1","襦襤襭襪襯襴襷襾覃覈覊覓覘覡覩覦覬覯覲覺覽覿觀觚觜觝觧觴觸訃訖訐訌訛訝訥訶詁詛詒詆詈詼詭詬詢誅誂誄誨誡誑誥誦誚誣諄諍諂諚諫諳諧諤諱謔諠諢諷諞諛謌謇謚諡謖謐謗謠謳鞫謦謫謾謨譁譌譏譎證譖譛譚譫"],["eca1","譟譬譯譴譽讀讌讎讒讓讖讙讚谺豁谿豈豌豎豐豕豢豬豸豺貂貉貅貊貍貎貔豼貘戝貭貪貽貲貳貮貶賈賁賤賣賚賽賺賻贄贅贊贇贏贍贐齎贓賍贔贖赧赭赱赳趁趙跂趾趺跏跚跖跌跛跋跪跫跟跣跼踈踉跿踝踞踐踟蹂踵踰踴蹊"],["eda1","蹇蹉蹌蹐蹈蹙蹤蹠踪蹣蹕蹶蹲蹼躁躇躅躄躋躊躓躑躔躙躪躡躬躰軆躱躾軅軈軋軛軣軼軻軫軾輊輅輕輒輙輓輜輟輛輌輦輳輻輹轅轂輾轌轉轆轎轗轜轢轣轤辜辟辣辭辯辷迚迥迢迪迯邇迴逅迹迺逑逕逡逍逞逖逋逧逶逵逹迸"],["eea1","遏遐遑遒逎遉逾遖遘遞遨遯遶隨遲邂遽邁邀邊邉邏邨邯邱邵郢郤扈郛鄂鄒鄙鄲鄰酊酖酘酣酥酩酳酲醋醉醂醢醫醯醪醵醴醺釀釁釉釋釐釖釟釡釛釼釵釶鈞釿鈔鈬鈕鈑鉞鉗鉅鉉鉤鉈銕鈿鉋鉐銜銖銓銛鉚鋏銹銷鋩錏鋺鍄錮"],["efa1","錙錢錚錣錺錵錻鍜鍠鍼鍮鍖鎰鎬鎭鎔鎹鏖鏗鏨鏥鏘鏃鏝鏐鏈鏤鐚鐔鐓鐃鐇鐐鐶鐫鐵鐡鐺鑁鑒鑄鑛鑠鑢鑞鑪鈩鑰鑵鑷鑽鑚鑼鑾钁鑿閂閇閊閔閖閘閙閠閨閧閭閼閻閹閾闊濶闃闍闌闕闔闖關闡闥闢阡阨阮阯陂陌陏陋陷陜陞"],["f0a1","陝陟陦陲陬隍隘隕隗險隧隱隲隰隴隶隸隹雎雋雉雍襍雜霍雕雹霄霆霈霓霎霑霏霖霙霤霪霰霹霽霾靄靆靈靂靉靜靠靤靦靨勒靫靱靹鞅靼鞁靺鞆鞋鞏鞐鞜鞨鞦鞣鞳鞴韃韆韈韋韜韭齏韲竟韶韵頏頌頸頤頡頷頽顆顏顋顫顯顰"],["f1a1","顱顴顳颪颯颱颶飄飃飆飩飫餃餉餒餔餘餡餝餞餤餠餬餮餽餾饂饉饅饐饋饑饒饌饕馗馘馥馭馮馼駟駛駝駘駑駭駮駱駲駻駸騁騏騅駢騙騫騷驅驂驀驃騾驕驍驛驗驟驢驥驤驩驫驪骭骰骼髀髏髑髓體髞髟髢髣髦髯髫髮髴髱髷"],["f2a1","髻鬆鬘鬚鬟鬢鬣鬥鬧鬨鬩鬪鬮鬯鬲魄魃魏魍魎魑魘魴鮓鮃鮑鮖鮗鮟鮠鮨鮴鯀鯊鮹鯆鯏鯑鯒鯣鯢鯤鯔鯡鰺鯲鯱鯰鰕鰔鰉鰓鰌鰆鰈鰒鰊鰄鰮鰛鰥鰤鰡鰰鱇鰲鱆鰾鱚鱠鱧鱶鱸鳧鳬鳰鴉鴈鳫鴃鴆鴪鴦鶯鴣鴟鵄鴕鴒鵁鴿鴾鵆鵈"],["f3a1","鵝鵞鵤鵑鵐鵙鵲鶉鶇鶫鵯鵺鶚鶤鶩鶲鷄鷁鶻鶸鶺鷆鷏鷂鷙鷓鷸鷦鷭鷯鷽鸚鸛鸞鹵鹹鹽麁麈麋麌麒麕麑麝麥麩麸麪麭靡黌黎黏黐黔黜點黝黠黥黨黯黴黶黷黹黻黼黽鼇鼈皷鼕鼡鼬鼾齊齒齔齣齟齠齡齦齧齬齪齷齲齶龕龜龠"],["f4a1","堯槇遙瑤凜熙"],["f9a1","纊褜鍈銈蓜俉炻昱棈鋹曻彅丨仡仼伀伃伹佖侒侊侚侔俍偀倢俿倞偆偰偂傔僴僘兊兤冝冾凬刕劜劦勀勛匀匇匤卲厓厲叝﨎咜咊咩哿喆坙坥垬埈埇﨏塚增墲夋奓奛奝奣妤妺孖寀甯寘寬尞岦岺峵崧嵓﨑嵂嵭嶸嶹巐弡弴彧德"],["faa1","忞恝悅悊惞惕愠惲愑愷愰憘戓抦揵摠撝擎敎昀昕昻昉昮昞昤晥晗晙晴晳暙暠暲暿曺朎朗杦枻桒柀栁桄棏﨓楨﨔榘槢樰橫橆橳橾櫢櫤毖氿汜沆汯泚洄涇浯涖涬淏淸淲淼渹湜渧渼溿澈澵濵瀅瀇瀨炅炫焏焄煜煆煇凞燁燾犱"],["fba1","犾猤猪獷玽珉珖珣珒琇珵琦琪琩琮瑢璉璟甁畯皂皜皞皛皦益睆劯砡硎硤硺礰礼神祥禔福禛竑竧靖竫箞精絈絜綷綠緖繒罇羡羽茁荢荿菇菶葈蒴蕓蕙蕫﨟薰蘒﨡蠇裵訒訷詹誧誾諟諸諶譓譿賰賴贒赶﨣軏﨤逸遧郞都鄕鄧釚"],["fca1","釗釞釭釮釤釥鈆鈐鈊鈺鉀鈼鉎鉙鉑鈹鉧銧鉷鉸鋧鋗鋙鋐﨧鋕鋠鋓錥錡鋻﨨錞鋿錝錂鍰鍗鎤鏆鏞鏸鐱鑅鑈閒隆﨩隝隯霳霻靃靍靏靑靕顗顥飯飼餧館馞驎髙髜魵魲鮏鮱鮻鰀鵰鵫鶴鸙黑"],["fcf1","ⅰ",9,"¬¦'""],["8fa2af","˘ˇ¸˙˝¯˛˚~΄΅"],["8fa2c2","¡¦¿"],["8fa2eb","ºª©®™¤№"],["8fa6e1","ΆΈΉΊΪ"],["8fa6e7","Ό"],["8fa6e9","ΎΫ"],["8fa6ec","Ώ"],["8fa6f1","άέήίϊΐόςύϋΰώ"],["8fa7c2","Ђ",10,"ЎЏ"],["8fa7f2","ђ",10,"ўџ"],["8fa9a1","ÆĐ"],["8fa9a4","Ħ"],["8fa9a6","IJ"],["8fa9a8","ŁĿ"],["8fa9ab","ŊØŒ"],["8fa9af","ŦÞ"],["8fa9c1","æđðħıijĸłŀʼnŋøœßŧþ"],["8faaa1","ÁÀÄÂĂǍĀĄÅÃĆĈČÇĊĎÉÈËÊĚĖĒĘ"],["8faaba","ĜĞĢĠĤÍÌÏÎǏİĪĮĨĴĶĹĽĻŃŇŅÑÓÒÖÔǑŐŌÕŔŘŖŚŜŠŞŤŢÚÙÜÛŬǓŰŪŲŮŨǗǛǙǕŴÝŸŶŹŽŻ"],["8faba1","áàäâăǎāąåãćĉčçċďéèëêěėēęǵĝğ"],["8fabbd","ġĥíìïîǐ"],["8fabc5","īįĩĵķĺľļńňņñóòöôǒőōõŕřŗśŝšşťţúùüûŭǔűūųůũǘǜǚǖŵýÿŷźžż"],["8fb0a1","丂丄丅丌丒丟丣两丨丫丮丯丰丵乀乁乄乇乑乚乜乣乨乩乴乵乹乿亍亖亗亝亯亹仃仐仚仛仠仡仢仨仯仱仳仵份仾仿伀伂伃伈伋伌伒伕伖众伙伮伱你伳伵伷伹伻伾佀佂佈佉佋佌佒佔佖佘佟佣佪佬佮佱佷佸佹佺佽佾侁侂侄"],["8fb1a1","侅侉侊侌侎侐侒侓侔侗侙侚侞侟侲侷侹侻侼侽侾俀俁俅俆俈俉俋俌俍俏俒俜俠俢俰俲俼俽俿倀倁倄倇倊倌倎倐倓倗倘倛倜倝倞倢倧倮倰倲倳倵偀偁偂偅偆偊偌偎偑偒偓偗偙偟偠偢偣偦偧偪偭偰偱倻傁傃傄傆傊傎傏傐"],["8fb2a1","傒傓傔傖傛傜傞",4,"傪傯傰傹傺傽僀僃僄僇僌僎僐僓僔僘僜僝僟僢僤僦僨僩僯僱僶僺僾儃儆儇儈儋儌儍儎僲儐儗儙儛儜儝儞儣儧儨儬儭儯儱儳儴儵儸儹兂兊兏兓兕兗兘兟兤兦兾冃冄冋冎冘冝冡冣冭冸冺冼冾冿凂"],["8fb3a1","凈减凑凒凓凕凘凞凢凥凮凲凳凴凷刁刂刅划刓刕刖刘刢刨刱刲刵刼剅剉剕剗剘剚剜剟剠剡剦剮剷剸剹劀劂劅劊劌劓劕劖劗劘劚劜劤劥劦劧劯劰劶劷劸劺劻劽勀勄勆勈勌勏勑勔勖勛勜勡勥勨勩勪勬勰勱勴勶勷匀匃匊匋"],["8fb4a1","匌匑匓匘匛匜匞匟匥匧匨匩匫匬匭匰匲匵匼匽匾卂卌卋卙卛卡卣卥卬卭卲卹卾厃厇厈厎厓厔厙厝厡厤厪厫厯厲厴厵厷厸厺厽叀叅叏叒叓叕叚叝叞叠另叧叵吂吓吚吡吧吨吪启吱吴吵呃呄呇呍呏呞呢呤呦呧呩呫呭呮呴呿"],["8fb5a1","咁咃咅咈咉咍咑咕咖咜咟咡咦咧咩咪咭咮咱咷咹咺咻咿哆哊响哎哠哪哬哯哶哼哾哿唀唁唅唈唉唌唍唎唕唪唫唲唵唶唻唼唽啁啇啉啊啍啐啑啘啚啛啞啠啡啤啦啿喁喂喆喈喎喏喑喒喓喔喗喣喤喭喲喿嗁嗃嗆嗉嗋嗌嗎嗑嗒"],["8fb6a1","嗓嗗嗘嗛嗞嗢嗩嗶嗿嘅嘈嘊嘍",5,"嘙嘬嘰嘳嘵嘷嘹嘻嘼嘽嘿噀噁噃噄噆噉噋噍噏噔噞噠噡噢噣噦噩噭噯噱噲噵嚄嚅嚈嚋嚌嚕嚙嚚嚝嚞嚟嚦嚧嚨嚩嚫嚬嚭嚱嚳嚷嚾囅囉囊囋囏囐囌囍囙囜囝囟囡囤",4,"囱囫园"],["8fb7a1","囶囷圁圂圇圊圌圑圕圚圛圝圠圢圣圤圥圩圪圬圮圯圳圴圽圾圿坅坆坌坍坒坢坥坧坨坫坭",4,"坳坴坵坷坹坺坻坼坾垁垃垌垔垗垙垚垜垝垞垟垡垕垧垨垩垬垸垽埇埈埌埏埕埝埞埤埦埧埩埭埰埵埶埸埽埾埿堃堄堈堉埡"],["8fb8a1","堌堍堛堞堟堠堦堧堭堲堹堿塉塌塍塏塐塕塟塡塤塧塨塸塼塿墀墁墇墈墉墊墌墍墏墐墔墖墝墠墡墢墦墩墱墲壄墼壂壈壍壎壐壒壔壖壚壝壡壢壩壳夅夆夋夌夒夓夔虁夝夡夣夤夨夯夰夳夵夶夿奃奆奒奓奙奛奝奞奟奡奣奫奭"],["8fb9a1","奯奲奵奶她奻奼妋妌妎妒妕妗妟妤妧妭妮妯妰妳妷妺妼姁姃姄姈姊姍姒姝姞姟姣姤姧姮姯姱姲姴姷娀娄娌娍娎娒娓娞娣娤娧娨娪娭娰婄婅婇婈婌婐婕婞婣婥婧婭婷婺婻婾媋媐媓媖媙媜媞媟媠媢媧媬媱媲媳媵媸媺媻媿"],["8fbaa1","嫄嫆嫈嫏嫚嫜嫠嫥嫪嫮嫵嫶嫽嬀嬁嬈嬗嬴嬙嬛嬝嬡嬥嬭嬸孁孋孌孒孖孞孨孮孯孼孽孾孿宁宄宆宊宎宐宑宓宔宖宨宩宬宭宯宱宲宷宺宼寀寁寍寏寖",4,"寠寯寱寴寽尌尗尞尟尣尦尩尫尬尮尰尲尵尶屙屚屜屢屣屧屨屩"],["8fbba1","屭屰屴屵屺屻屼屽岇岈岊岏岒岝岟岠岢岣岦岪岲岴岵岺峉峋峒峝峗峮峱峲峴崁崆崍崒崫崣崤崦崧崱崴崹崽崿嵂嵃嵆嵈嵕嵑嵙嵊嵟嵠嵡嵢嵤嵪嵭嵰嵹嵺嵾嵿嶁嶃嶈嶊嶒嶓嶔嶕嶙嶛嶟嶠嶧嶫嶰嶴嶸嶹巃巇巋巐巎巘巙巠巤"],["8fbca1","巩巸巹帀帇帍帒帔帕帘帟帠帮帨帲帵帾幋幐幉幑幖幘幛幜幞幨幪",4,"幰庀庋庎庢庤庥庨庪庬庱庳庽庾庿廆廌廋廎廑廒廔廕廜廞廥廫异弆弇弈弎弙弜弝弡弢弣弤弨弫弬弮弰弴弶弻弽弿彀彄彅彇彍彐彔彘彛彠彣彤彧"],["8fbda1","彯彲彴彵彸彺彽彾徉徍徏徖徜徝徢徧徫徤徬徯徰徱徸忄忇忈忉忋忐",4,"忞忡忢忨忩忪忬忭忮忯忲忳忶忺忼怇怊怍怓怔怗怘怚怟怤怭怳怵恀恇恈恉恌恑恔恖恗恝恡恧恱恾恿悂悆悈悊悎悑悓悕悘悝悞悢悤悥您悰悱悷"],["8fbea1","悻悾惂惄惈惉惊惋惎惏惔惕惙惛惝惞惢惥惲惵惸惼惽愂愇愊愌愐",4,"愖愗愙愜愞愢愪愫愰愱愵愶愷愹慁慅慆慉慞慠慬慲慸慻慼慿憀憁憃憄憋憍憒憓憗憘憜憝憟憠憥憨憪憭憸憹憼懀懁懂懎懏懕懜懝懞懟懡懢懧懩懥"],["8fbfa1","懬懭懯戁戃戄戇戓戕戜戠戢戣戧戩戫戹戽扂扃扄扆扌扐扑扒扔扖扚扜扤扭扯扳扺扽抍抎抏抐抦抨抳抶抷抺抾抿拄拎拕拖拚拪拲拴拼拽挃挄挊挋挍挐挓挖挘挩挪挭挵挶挹挼捁捂捃捄捆捊捋捎捒捓捔捘捛捥捦捬捭捱捴捵"],["8fc0a1","捸捼捽捿掂掄掇掊掐掔掕掙掚掞掤掦掭掮掯掽揁揅揈揎揑揓揔揕揜揠揥揪揬揲揳揵揸揹搉搊搐搒搔搘搞搠搢搤搥搩搪搯搰搵搽搿摋摏摑摒摓摔摚摛摜摝摟摠摡摣摭摳摴摻摽撅撇撏撐撑撘撙撛撝撟撡撣撦撨撬撳撽撾撿"],["8fc1a1","擄擉擊擋擌擎擐擑擕擗擤擥擩擪擭擰擵擷擻擿攁攄攈攉攊攏攓攔攖攙攛攞攟攢攦攩攮攱攺攼攽敃敇敉敐敒敔敟敠敧敫敺敽斁斅斊斒斕斘斝斠斣斦斮斲斳斴斿旂旈旉旎旐旔旖旘旟旰旲旴旵旹旾旿昀昄昈昉昍昑昒昕昖昝"],["8fc2a1","昞昡昢昣昤昦昩昪昫昬昮昰昱昳昹昷晀晅晆晊晌晑晎晗晘晙晛晜晠晡曻晪晫晬晾晳晵晿晷晸晹晻暀晼暋暌暍暐暒暙暚暛暜暟暠暤暭暱暲暵暻暿曀曂曃曈曌曎曏曔曛曟曨曫曬曮曺朅朇朎朓朙朜朠朢朳朾杅杇杈杌杔杕杝"],["8fc3a1","杦杬杮杴杶杻极构枎枏枑枓枖枘枙枛枰枱枲枵枻枼枽柹柀柂柃柅柈柉柒柗柙柜柡柦柰柲柶柷桒栔栙栝栟栨栧栬栭栯栰栱栳栻栿桄桅桊桌桕桗桘桛桫桮",4,"桵桹桺桻桼梂梄梆梈梖梘梚梜梡梣梥梩梪梮梲梻棅棈棌棏"],["8fc4a1","棐棑棓棖棙棜棝棥棨棪棫棬棭棰棱棵棶棻棼棽椆椉椊椐椑椓椖椗椱椳椵椸椻楂楅楉楎楗楛楣楤楥楦楨楩楬楰楱楲楺楻楿榀榍榒榖榘榡榥榦榨榫榭榯榷榸榺榼槅槈槑槖槗槢槥槮槯槱槳槵槾樀樁樃樏樑樕樚樝樠樤樨樰樲"],["8fc5a1","樴樷樻樾樿橅橆橉橊橎橐橑橒橕橖橛橤橧橪橱橳橾檁檃檆檇檉檋檑檛檝檞檟檥檫檯檰檱檴檽檾檿櫆櫉櫈櫌櫐櫔櫕櫖櫜櫝櫤櫧櫬櫰櫱櫲櫼櫽欂欃欆欇欉欏欐欑欗欛欞欤欨欫欬欯欵欶欻欿歆歊歍歒歖歘歝歠歧歫歮歰歵歽"],["8fc6a1","歾殂殅殗殛殟殠殢殣殨殩殬殭殮殰殸殹殽殾毃毄毉毌毖毚毡毣毦毧毮毱毷毹毿氂氄氅氉氍氎氐氒氙氟氦氧氨氬氮氳氵氶氺氻氿汊汋汍汏汒汔汙汛汜汫汭汯汴汶汸汹汻沅沆沇沉沔沕沗沘沜沟沰沲沴泂泆泍泏泐泑泒泔泖"],["8fc7a1","泚泜泠泧泩泫泬泮泲泴洄洇洊洎洏洑洓洚洦洧洨汧洮洯洱洹洼洿浗浞浟浡浥浧浯浰浼涂涇涑涒涔涖涗涘涪涬涴涷涹涽涿淄淈淊淎淏淖淛淝淟淠淢淥淩淯淰淴淶淼渀渄渞渢渧渲渶渹渻渼湄湅湈湉湋湏湑湒湓湔湗湜湝湞"],["8fc8a1","湢湣湨湳湻湽溍溓溙溠溧溭溮溱溳溻溿滀滁滃滇滈滊滍滎滏滫滭滮滹滻滽漄漈漊漌漍漖漘漚漛漦漩漪漯漰漳漶漻漼漭潏潑潒潓潗潙潚潝潞潡潢潨潬潽潾澃澇澈澋澌澍澐澒澓澔澖澚澟澠澥澦澧澨澮澯澰澵澶澼濅濇濈濊"],["8fc9a1","濚濞濨濩濰濵濹濼濽瀀瀅瀆瀇瀍瀗瀠瀣瀯瀴瀷瀹瀼灃灄灈灉灊灋灔灕灝灞灎灤灥灬灮灵灶灾炁炅炆炔",4,"炛炤炫炰炱炴炷烊烑烓烔烕烖烘烜烤烺焃",4,"焋焌焏焞焠焫焭焯焰焱焸煁煅煆煇煊煋煐煒煗煚煜煞煠"],["8fcaa1","煨煹熀熅熇熌熒熚熛熠熢熯熰熲熳熺熿燀燁燄燋燌燓燖燙燚燜燸燾爀爇爈爉爓爗爚爝爟爤爫爯爴爸爹牁牂牃牅牎牏牐牓牕牖牚牜牞牠牣牨牫牮牯牱牷牸牻牼牿犄犉犍犎犓犛犨犭犮犱犴犾狁狇狉狌狕狖狘狟狥狳狴狺狻"],["8fcba1","狾猂猄猅猇猋猍猒猓猘猙猞猢猤猧猨猬猱猲猵猺猻猽獃獍獐獒獖獘獝獞獟獠獦獧獩獫獬獮獯獱獷獹獼玀玁玃玅玆玎玐玓玕玗玘玜玞玟玠玢玥玦玪玫玭玵玷玹玼玽玿珅珆珉珋珌珏珒珓珖珙珝珡珣珦珧珩珴珵珷珹珺珻珽"],["8fcca1","珿琀琁琄琇琊琑琚琛琤琦琨",9,"琹瑀瑃瑄瑆瑇瑋瑍瑑瑒瑗瑝瑢瑦瑧瑨瑫瑭瑮瑱瑲璀璁璅璆璇璉璏璐璑璒璘璙璚璜璟璠璡璣璦璨璩璪璫璮璯璱璲璵璹璻璿瓈瓉瓌瓐瓓瓘瓚瓛瓞瓟瓤瓨瓪瓫瓯瓴瓺瓻瓼瓿甆"],["8fcda1","甒甖甗甠甡甤甧甩甪甯甶甹甽甾甿畀畃畇畈畎畐畒畗畞畟畡畯畱畹",5,"疁疅疐疒疓疕疙疜疢疤疴疺疿痀痁痄痆痌痎痏痗痜痟痠痡痤痧痬痮痯痱痹瘀瘂瘃瘄瘇瘈瘊瘌瘏瘒瘓瘕瘖瘙瘛瘜瘝瘞瘣瘥瘦瘩瘭瘲瘳瘵瘸瘹"],["8fcea1","瘺瘼癊癀癁癃癄癅癉癋癕癙癟癤癥癭癮癯癱癴皁皅皌皍皕皛皜皝皟皠皢",6,"皪皭皽盁盅盉盋盌盎盔盙盠盦盨盬盰盱盶盹盼眀眆眊眎眒眔眕眗眙眚眜眢眨眭眮眯眴眵眶眹眽眾睂睅睆睊睍睎睏睒睖睗睜睞睟睠睢"],["8fcfa1","睤睧睪睬睰睲睳睴睺睽瞀瞄瞌瞍瞔瞕瞖瞚瞟瞢瞧瞪瞮瞯瞱瞵瞾矃矉矑矒矕矙矞矟矠矤矦矪矬矰矱矴矸矻砅砆砉砍砎砑砝砡砢砣砭砮砰砵砷硃硄硇硈硌硎硒硜硞硠硡硣硤硨硪确硺硾碊碏碔碘碡碝碞碟碤碨碬碭碰碱碲碳"],["8fd0a1","碻碽碿磇磈磉磌磎磒磓磕磖磤磛磟磠磡磦磪磲磳礀磶磷磺磻磿礆礌礐礚礜礞礟礠礥礧礩礭礱礴礵礻礽礿祄祅祆祊祋祏祑祔祘祛祜祧祩祫祲祹祻祼祾禋禌禑禓禔禕禖禘禛禜禡禨禩禫禯禱禴禸离秂秄秇秈秊秏秔秖秚秝秞"],["8fd1a1","秠秢秥秪秫秭秱秸秼稂稃稇稉稊稌稑稕稛稞稡稧稫稭稯稰稴稵稸稹稺穄穅穇穈穌穕穖穙穜穝穟穠穥穧穪穭穵穸穾窀窂窅窆窊窋窐窑窔窞窠窣窬窳窵窹窻窼竆竉竌竎竑竛竨竩竫竬竱竴竻竽竾笇笔笟笣笧笩笪笫笭笮笯笰"],["8fd2a1","笱笴笽笿筀筁筇筎筕筠筤筦筩筪筭筯筲筳筷箄箉箎箐箑箖箛箞箠箥箬箯箰箲箵箶箺箻箼箽篂篅篈篊篔篖篗篙篚篛篨篪篲篴篵篸篹篺篼篾簁簂簃簄簆簉簋簌簎簏簙簛簠簥簦簨簬簱簳簴簶簹簺籆籊籕籑籒籓籙",5],["8fd3a1","籡籣籧籩籭籮籰籲籹籼籽粆粇粏粔粞粠粦粰粶粷粺粻粼粿糄糇糈糉糍糏糓糔糕糗糙糚糝糦糩糫糵紃紇紈紉紏紑紒紓紖紝紞紣紦紪紭紱紼紽紾絀絁絇絈絍絑絓絗絙絚絜絝絥絧絪絰絸絺絻絿綁綂綃綅綆綈綋綌綍綑綖綗綝"],["8fd4a1","綞綦綧綪綳綶綷綹緂",4,"緌緍緎緗緙縀緢緥緦緪緫緭緱緵緶緹緺縈縐縑縕縗縜縝縠縧縨縬縭縯縳縶縿繄繅繇繎繐繒繘繟繡繢繥繫繮繯繳繸繾纁纆纇纊纍纑纕纘纚纝纞缼缻缽缾缿罃罄罇罏罒罓罛罜罝罡罣罤罥罦罭"],["8fd5a1","罱罽罾罿羀羋羍羏羐羑羖羗羜羡羢羦羪羭羴羼羿翀翃翈翎翏翛翟翣翥翨翬翮翯翲翺翽翾翿耇耈耊耍耎耏耑耓耔耖耝耞耟耠耤耦耬耮耰耴耵耷耹耺耼耾聀聄聠聤聦聭聱聵肁肈肎肜肞肦肧肫肸肹胈胍胏胒胔胕胗胘胠胭胮"],["8fd6a1","胰胲胳胶胹胺胾脃脋脖脗脘脜脞脠脤脧脬脰脵脺脼腅腇腊腌腒腗腠腡腧腨腩腭腯腷膁膐膄膅膆膋膎膖膘膛膞膢膮膲膴膻臋臃臅臊臎臏臕臗臛臝臞臡臤臫臬臰臱臲臵臶臸臹臽臿舀舃舏舓舔舙舚舝舡舢舨舲舴舺艃艄艅艆"],["8fd7a1","艋艎艏艑艖艜艠艣艧艭艴艻艽艿芀芁芃芄芇芉芊芎芑芔芖芘芚芛芠芡芣芤芧芨芩芪芮芰芲芴芷芺芼芾芿苆苐苕苚苠苢苤苨苪苭苯苶苷苽苾茀茁茇茈茊茋荔茛茝茞茟茡茢茬茭茮茰茳茷茺茼茽荂荃荄荇荍荎荑荕荖荗荰荸"],["8fd8a1","荽荿莀莂莄莆莍莒莔莕莘莙莛莜莝莦莧莩莬莾莿菀菇菉菏菐菑菔菝荓菨菪菶菸菹菼萁萆萊萏萑萕萙莭萯萹葅葇葈葊葍葏葑葒葖葘葙葚葜葠葤葥葧葪葰葳葴葶葸葼葽蒁蒅蒒蒓蒕蒞蒦蒨蒩蒪蒯蒱蒴蒺蒽蒾蓀蓂蓇蓈蓌蓏蓓"],["8fd9a1","蓜蓧蓪蓯蓰蓱蓲蓷蔲蓺蓻蓽蔂蔃蔇蔌蔎蔐蔜蔞蔢蔣蔤蔥蔧蔪蔫蔯蔳蔴蔶蔿蕆蕏",4,"蕖蕙蕜",6,"蕤蕫蕯蕹蕺蕻蕽蕿薁薅薆薉薋薌薏薓薘薝薟薠薢薥薧薴薶薷薸薼薽薾薿藂藇藊藋藎薭藘藚藟藠藦藨藭藳藶藼"],["8fdaa1","藿蘀蘄蘅蘍蘎蘐蘑蘒蘘蘙蘛蘞蘡蘧蘩蘶蘸蘺蘼蘽虀虂虆虒虓虖虗虘虙虝虠",4,"虩虬虯虵虶虷虺蚍蚑蚖蚘蚚蚜蚡蚦蚧蚨蚭蚱蚳蚴蚵蚷蚸蚹蚿蛀蛁蛃蛅蛑蛒蛕蛗蛚蛜蛠蛣蛥蛧蚈蛺蛼蛽蜄蜅蜇蜋蜎蜏蜐蜓蜔蜙蜞蜟蜡蜣"],["8fdba1","蜨蜮蜯蜱蜲蜹蜺蜼蜽蜾蝀蝃蝅蝍蝘蝝蝡蝤蝥蝯蝱蝲蝻螃",6,"螋螌螐螓螕螗螘螙螞螠螣螧螬螭螮螱螵螾螿蟁蟈蟉蟊蟎蟕蟖蟙蟚蟜蟟蟢蟣蟤蟪蟫蟭蟱蟳蟸蟺蟿蠁蠃蠆蠉蠊蠋蠐蠙蠒蠓蠔蠘蠚蠛蠜蠞蠟蠨蠭蠮蠰蠲蠵"],["8fdca1","蠺蠼衁衃衅衈衉衊衋衎衑衕衖衘衚衜衟衠衤衩衱衹衻袀袘袚袛袜袟袠袨袪袺袽袾裀裊",4,"裑裒裓裛裞裧裯裰裱裵裷褁褆褍褎褏褕褖褘褙褚褜褠褦褧褨褰褱褲褵褹褺褾襀襂襅襆襉襏襒襗襚襛襜襡襢襣襫襮襰襳襵襺"],["8fdda1","襻襼襽覉覍覐覔覕覛覜覟覠覥覰覴覵覶覷覼觔",4,"觥觩觫觭觱觳觶觹觽觿訄訅訇訏訑訒訔訕訞訠訢訤訦訫訬訯訵訷訽訾詀詃詅詇詉詍詎詓詖詗詘詜詝詡詥詧詵詶詷詹詺詻詾詿誀誃誆誋誏誐誒誖誗誙誟誧誩誮誯誳"],["8fdea1","誶誷誻誾諃諆諈諉諊諑諓諔諕諗諝諟諬諰諴諵諶諼諿謅謆謋謑謜謞謟謊謭謰謷謼譂",4,"譈譒譓譔譙譍譞譣譭譶譸譹譼譾讁讄讅讋讍讏讔讕讜讞讟谸谹谽谾豅豇豉豋豏豑豓豔豗豘豛豝豙豣豤豦豨豩豭豳豵豶豻豾貆"],["8fdfa1","貇貋貐貒貓貙貛貜貤貹貺賅賆賉賋賏賖賕賙賝賡賨賬賯賰賲賵賷賸賾賿贁贃贉贒贗贛赥赩赬赮赿趂趄趈趍趐趑趕趞趟趠趦趫趬趯趲趵趷趹趻跀跅跆跇跈跊跎跑跔跕跗跙跤跥跧跬跰趼跱跲跴跽踁踄踅踆踋踑踔踖踠踡踢"],["8fe0a1","踣踦踧踱踳踶踷踸踹踽蹀蹁蹋蹍蹎蹏蹔蹛蹜蹝蹞蹡蹢蹩蹬蹭蹯蹰蹱蹹蹺蹻躂躃躉躐躒躕躚躛躝躞躢躧躩躭躮躳躵躺躻軀軁軃軄軇軏軑軔軜軨軮軰軱軷軹軺軭輀輂輇輈輏輐輖輗輘輞輠輡輣輥輧輨輬輭輮輴輵輶輷輺轀轁"],["8fe1a1","轃轇轏轑",4,"轘轝轞轥辝辠辡辤辥辦辵辶辸达迀迁迆迊迋迍运迒迓迕迠迣迤迨迮迱迵迶迻迾适逄逈逌逘逛逨逩逯逪逬逭逳逴逷逿遃遄遌遛遝遢遦遧遬遰遴遹邅邈邋邌邎邐邕邗邘邙邛邠邡邢邥邰邲邳邴邶邽郌邾郃"],["8fe2a1","郄郅郇郈郕郗郘郙郜郝郟郥郒郶郫郯郰郴郾郿鄀鄄鄅鄆鄈鄍鄐鄔鄖鄗鄘鄚鄜鄞鄠鄥鄢鄣鄧鄩鄮鄯鄱鄴鄶鄷鄹鄺鄼鄽酃酇酈酏酓酗酙酚酛酡酤酧酭酴酹酺酻醁醃醅醆醊醎醑醓醔醕醘醞醡醦醨醬醭醮醰醱醲醳醶醻醼醽醿"],["8fe3a1","釂釃釅釓釔釗釙釚釞釤釥釩釪釬",5,"釷釹釻釽鈀鈁鈄鈅鈆鈇鈉鈊鈌鈐鈒鈓鈖鈘鈜鈝鈣鈤鈥鈦鈨鈮鈯鈰鈳鈵鈶鈸鈹鈺鈼鈾鉀鉂鉃鉆鉇鉊鉍鉎鉏鉑鉘鉙鉜鉝鉠鉡鉥鉧鉨鉩鉮鉯鉰鉵",4,"鉻鉼鉽鉿銈銉銊銍銎銒銗"],["8fe4a1","銙銟銠銤銥銧銨銫銯銲銶銸銺銻銼銽銿",4,"鋅鋆鋇鋈鋋鋌鋍鋎鋐鋓鋕鋗鋘鋙鋜鋝鋟鋠鋡鋣鋥鋧鋨鋬鋮鋰鋹鋻鋿錀錂錈錍錑錔錕錜錝錞錟錡錤錥錧錩錪錳錴錶錷鍇鍈鍉鍐鍑鍒鍕鍗鍘鍚鍞鍤鍥鍧鍩鍪鍭鍯鍰鍱鍳鍴鍶"],["8fe5a1","鍺鍽鍿鎀鎁鎂鎈鎊鎋鎍鎏鎒鎕鎘鎛鎞鎡鎣鎤鎦鎨鎫鎴鎵鎶鎺鎩鏁鏄鏅鏆鏇鏉",4,"鏓鏙鏜鏞鏟鏢鏦鏧鏹鏷鏸鏺鏻鏽鐁鐂鐄鐈鐉鐍鐎鐏鐕鐖鐗鐟鐮鐯鐱鐲鐳鐴鐻鐿鐽鑃鑅鑈鑊鑌鑕鑙鑜鑟鑡鑣鑨鑫鑭鑮鑯鑱鑲钄钃镸镹"],["8fe6a1","镾閄閈閌閍閎閝閞閟閡閦閩閫閬閴閶閺閽閿闆闈闉闋闐闑闒闓闙闚闝闞闟闠闤闦阝阞阢阤阥阦阬阱阳阷阸阹阺阼阽陁陒陔陖陗陘陡陮陴陻陼陾陿隁隂隃隄隉隑隖隚隝隟隤隥隦隩隮隯隳隺雊雒嶲雘雚雝雞雟雩雯雱雺霂"],["8fe7a1","霃霅霉霚霛霝霡霢霣霨霱霳靁靃靊靎靏靕靗靘靚靛靣靧靪靮靳靶靷靸靻靽靿鞀鞉鞕鞖鞗鞙鞚鞞鞟鞢鞬鞮鞱鞲鞵鞶鞸鞹鞺鞼鞾鞿韁韄韅韇韉韊韌韍韎韐韑韔韗韘韙韝韞韠韛韡韤韯韱韴韷韸韺頇頊頙頍頎頔頖頜頞頠頣頦"],["8fe8a1","頫頮頯頰頲頳頵頥頾顄顇顊顑顒顓顖顗顙顚顢顣顥顦顪顬颫颭颮颰颴颷颸颺颻颿飂飅飈飌飡飣飥飦飧飪飳飶餂餇餈餑餕餖餗餚餛餜餟餢餦餧餫餱",4,"餹餺餻餼饀饁饆饇饈饍饎饔饘饙饛饜饞饟饠馛馝馟馦馰馱馲馵"],["8fe9a1","馹馺馽馿駃駉駓駔駙駚駜駞駧駪駫駬駰駴駵駹駽駾騂騃騄騋騌騐騑騖騞騠騢騣騤騧騭騮騳騵騶騸驇驁驄驊驋驌驎驑驔驖驝骪骬骮骯骲骴骵骶骹骻骾骿髁髃髆髈髎髐髒髕髖髗髛髜髠髤髥髧髩髬髲髳髵髹髺髽髿",4],["8feaa1","鬄鬅鬈鬉鬋鬌鬍鬎鬐鬒鬖鬙鬛鬜鬠鬦鬫鬭鬳鬴鬵鬷鬹鬺鬽魈魋魌魕魖魗魛魞魡魣魥魦魨魪",4,"魳魵魷魸魹魿鮀鮄鮅鮆鮇鮉鮊鮋鮍鮏鮐鮔鮚鮝鮞鮦鮧鮩鮬鮰鮱鮲鮷鮸鮻鮼鮾鮿鯁鯇鯈鯎鯐鯗鯘鯝鯟鯥鯧鯪鯫鯯鯳鯷鯸"],["8feba1","鯹鯺鯽鯿鰀鰂鰋鰏鰑鰖鰘鰙鰚鰜鰞鰢鰣鰦",4,"鰱鰵鰶鰷鰽鱁鱃鱄鱅鱉鱊鱎鱏鱐鱓鱔鱖鱘鱛鱝鱞鱟鱣鱩鱪鱜鱫鱨鱮鱰鱲鱵鱷鱻鳦鳲鳷鳹鴋鴂鴑鴗鴘鴜鴝鴞鴯鴰鴲鴳鴴鴺鴼鵅鴽鵂鵃鵇鵊鵓鵔鵟鵣鵢鵥鵩鵪鵫鵰鵶鵷鵻"],["8feca1","鵼鵾鶃鶄鶆鶊鶍鶎鶒鶓鶕鶖鶗鶘鶡鶪鶬鶮鶱鶵鶹鶼鶿鷃鷇鷉鷊鷔鷕鷖鷗鷚鷞鷟鷠鷥鷧鷩鷫鷮鷰鷳鷴鷾鸊鸂鸇鸎鸐鸑鸒鸕鸖鸙鸜鸝鹺鹻鹼麀麂麃麄麅麇麎麏麖麘麛麞麤麨麬麮麯麰麳麴麵黆黈黋黕黟黤黧黬黭黮黰黱黲黵"],["8feda1","黸黿鼂鼃鼉鼏鼐鼑鼒鼔鼖鼗鼙鼚鼛鼟鼢鼦鼪鼫鼯鼱鼲鼴鼷鼹鼺鼼鼽鼿齁齃",4,"齓齕齖齗齘齚齝齞齨齩齭",4,"齳齵齺齽龏龐龑龒龔龖龗龞龡龢龣龥"]]'); + +/***/ }), + +/***/ 7302: +/***/ ((module) => { + +"use strict"; +module.exports = /*#__PURE__*/JSON.parse('{"uChars":[128,165,169,178,184,216,226,235,238,244,248,251,253,258,276,284,300,325,329,334,364,463,465,467,469,471,473,475,477,506,594,610,712,716,730,930,938,962,970,1026,1104,1106,8209,8215,8218,8222,8231,8241,8244,8246,8252,8365,8452,8454,8458,8471,8482,8556,8570,8596,8602,8713,8720,8722,8726,8731,8737,8740,8742,8748,8751,8760,8766,8777,8781,8787,8802,8808,8816,8854,8858,8870,8896,8979,9322,9372,9548,9588,9616,9622,9634,9652,9662,9672,9676,9680,9702,9735,9738,9793,9795,11906,11909,11913,11917,11928,11944,11947,11951,11956,11960,11964,11979,12284,12292,12312,12319,12330,12351,12436,12447,12535,12543,12586,12842,12850,12964,13200,13215,13218,13253,13263,13267,13270,13384,13428,13727,13839,13851,14617,14703,14801,14816,14964,15183,15471,15585,16471,16736,17208,17325,17330,17374,17623,17997,18018,18212,18218,18301,18318,18760,18811,18814,18820,18823,18844,18848,18872,19576,19620,19738,19887,40870,59244,59336,59367,59413,59417,59423,59431,59437,59443,59452,59460,59478,59493,63789,63866,63894,63976,63986,64016,64018,64021,64025,64034,64037,64042,65074,65093,65107,65112,65127,65132,65375,65510,65536],"gbChars":[0,36,38,45,50,81,89,95,96,100,103,104,105,109,126,133,148,172,175,179,208,306,307,308,309,310,311,312,313,341,428,443,544,545,558,741,742,749,750,805,819,820,7922,7924,7925,7927,7934,7943,7944,7945,7950,8062,8148,8149,8152,8164,8174,8236,8240,8262,8264,8374,8380,8381,8384,8388,8390,8392,8393,8394,8396,8401,8406,8416,8419,8424,8437,8439,8445,8482,8485,8496,8521,8603,8936,8946,9046,9050,9063,9066,9076,9092,9100,9108,9111,9113,9131,9162,9164,9218,9219,11329,11331,11334,11336,11346,11361,11363,11366,11370,11372,11375,11389,11682,11686,11687,11692,11694,11714,11716,11723,11725,11730,11736,11982,11989,12102,12336,12348,12350,12384,12393,12395,12397,12510,12553,12851,12962,12973,13738,13823,13919,13933,14080,14298,14585,14698,15583,15847,16318,16434,16438,16481,16729,17102,17122,17315,17320,17402,17418,17859,17909,17911,17915,17916,17936,17939,17961,18664,18703,18814,18962,19043,33469,33470,33471,33484,33485,33490,33497,33501,33505,33513,33520,33536,33550,37845,37921,37948,38029,38038,38064,38065,38066,38069,38075,38076,38078,39108,39109,39113,39114,39115,39116,39265,39394,189000]}'); + +/***/ }), + +/***/ 9603: +/***/ ((module) => { + +"use strict"; +module.exports = /*#__PURE__*/JSON.parse('[["a140","",62],["a180","",32],["a240","",62],["a280","",32],["a2ab","",5],["a2e3","€"],["a2ef",""],["a2fd",""],["a340","",62],["a380","",31," "],["a440","",62],["a480","",32],["a4f4","",10],["a540","",62],["a580","",32],["a5f7","",7],["a640","",62],["a680","",32],["a6b9","",7],["a6d9","",6],["a6ec",""],["a6f3",""],["a6f6","",8],["a740","",62],["a780","",32],["a7c2","",14],["a7f2","",12],["a896","",10],["a8bc","ḿ"],["a8bf","ǹ"],["a8c1",""],["a8ea","",20],["a958",""],["a95b",""],["a95d",""],["a989","〾⿰",11],["a997","",12],["a9f0","",14],["aaa1","",93],["aba1","",93],["aca1","",93],["ada1","",93],["aea1","",93],["afa1","",93],["d7fa","",4],["f8a1","",93],["f9a1","",93],["faa1","",93],["fba1","",93],["fca1","",93],["fda1","",93],["fe50","⺁⺄㑳㑇⺈⺋㖞㘚㘎⺌⺗㥮㤘㧏㧟㩳㧐㭎㱮㳠⺧⺪䁖䅟⺮䌷⺳⺶⺷䎱䎬⺻䏝䓖䙡䙌"],["fe80","䜣䜩䝼䞍⻊䥇䥺䥽䦂䦃䦅䦆䦟䦛䦷䦶䲣䲟䲠䲡䱷䲢䴓",6,"䶮",93],["8135f437",""]]'); + +/***/ }), + +/***/ 7572: +/***/ ((module) => { + +"use strict"; +module.exports = /*#__PURE__*/JSON.parse('[["0","\\u0000",128],["a1","。",62],["8140"," 、。,.・:;?!゛゜´`¨^ ̄_ヽヾゝゞ〃仝々〆〇ー―‐/\~∥|…‥‘’“”()〔〕[]{}〈",9,"+-±×"],["8180","÷=≠<>≦≧∞∴♂♀°′″℃¥$¢£%#&*@§☆★○●◎◇◆□■△▲▽▼※〒→←↑↓〓"],["81b8","∈∋⊆⊇⊂⊃∪∩"],["81c8","∧∨¬⇒⇔∀∃"],["81da","∠⊥⌒∂∇≡≒≪≫√∽∝∵∫∬"],["81f0","ʼn♯♭♪†‡¶"],["81fc","◯"],["824f","0",9],["8260","A",25],["8281","a",25],["829f","ぁ",82],["8340","ァ",62],["8380","ム",22],["839f","Α",16,"Σ",6],["83bf","α",16,"σ",6],["8440","А",5,"ЁЖ",25],["8470","а",5,"ёж",7],["8480","о",17],["849f","─│┌┐┘└├┬┤┴┼━┃┏┓┛┗┣┳┫┻╋┠┯┨┷┿┝┰┥┸╂"],["8740","①",19,"Ⅰ",9],["875f","㍉㌔㌢㍍㌘㌧㌃㌶㍑㍗㌍㌦㌣㌫㍊㌻㎜㎝㎞㎎㎏㏄㎡"],["877e","㍻"],["8780","〝〟№㏍℡㊤",4,"㈱㈲㈹㍾㍽㍼≒≡∫∮∑√⊥∠∟⊿∵∩∪"],["889f","亜唖娃阿哀愛挨姶逢葵茜穐悪握渥旭葦芦鯵梓圧斡扱宛姐虻飴絢綾鮎或粟袷安庵按暗案闇鞍杏以伊位依偉囲夷委威尉惟意慰易椅為畏異移維緯胃萎衣謂違遺医井亥域育郁磯一壱溢逸稲茨芋鰯允印咽員因姻引飲淫胤蔭"],["8940","院陰隠韻吋右宇烏羽迂雨卯鵜窺丑碓臼渦嘘唄欝蔚鰻姥厩浦瓜閏噂云運雲荏餌叡営嬰影映曳栄永泳洩瑛盈穎頴英衛詠鋭液疫益駅悦謁越閲榎厭円"],["8980","園堰奄宴延怨掩援沿演炎焔煙燕猿縁艶苑薗遠鉛鴛塩於汚甥凹央奥往応押旺横欧殴王翁襖鴬鴎黄岡沖荻億屋憶臆桶牡乙俺卸恩温穏音下化仮何伽価佳加可嘉夏嫁家寡科暇果架歌河火珂禍禾稼箇花苛茄荷華菓蝦課嘩貨迦過霞蚊俄峨我牙画臥芽蛾賀雅餓駕介会解回塊壊廻快怪悔恢懐戒拐改"],["8a40","魁晦械海灰界皆絵芥蟹開階貝凱劾外咳害崖慨概涯碍蓋街該鎧骸浬馨蛙垣柿蛎鈎劃嚇各廓拡撹格核殻獲確穫覚角赫較郭閣隔革学岳楽額顎掛笠樫"],["8a80","橿梶鰍潟割喝恰括活渇滑葛褐轄且鰹叶椛樺鞄株兜竃蒲釜鎌噛鴨栢茅萱粥刈苅瓦乾侃冠寒刊勘勧巻喚堪姦完官寛干幹患感慣憾換敢柑桓棺款歓汗漢澗潅環甘監看竿管簡緩缶翰肝艦莞観諌貫還鑑間閑関陥韓館舘丸含岸巌玩癌眼岩翫贋雁頑顔願企伎危喜器基奇嬉寄岐希幾忌揮机旗既期棋棄"],["8b40","機帰毅気汽畿祈季稀紀徽規記貴起軌輝飢騎鬼亀偽儀妓宜戯技擬欺犠疑祇義蟻誼議掬菊鞠吉吃喫桔橘詰砧杵黍却客脚虐逆丘久仇休及吸宮弓急救"],["8b80","朽求汲泣灸球究窮笈級糾給旧牛去居巨拒拠挙渠虚許距鋸漁禦魚亨享京供侠僑兇競共凶協匡卿叫喬境峡強彊怯恐恭挟教橋況狂狭矯胸脅興蕎郷鏡響饗驚仰凝尭暁業局曲極玉桐粁僅勤均巾錦斤欣欽琴禁禽筋緊芹菌衿襟謹近金吟銀九倶句区狗玖矩苦躯駆駈駒具愚虞喰空偶寓遇隅串櫛釧屑屈"],["8c40","掘窟沓靴轡窪熊隈粂栗繰桑鍬勲君薫訓群軍郡卦袈祁係傾刑兄啓圭珪型契形径恵慶慧憩掲携敬景桂渓畦稽系経継繋罫茎荊蛍計詣警軽頚鶏芸迎鯨"],["8c80","劇戟撃激隙桁傑欠決潔穴結血訣月件倹倦健兼券剣喧圏堅嫌建憲懸拳捲検権牽犬献研硯絹県肩見謙賢軒遣鍵険顕験鹸元原厳幻弦減源玄現絃舷言諺限乎個古呼固姑孤己庫弧戸故枯湖狐糊袴股胡菰虎誇跨鈷雇顧鼓五互伍午呉吾娯後御悟梧檎瑚碁語誤護醐乞鯉交佼侯候倖光公功効勾厚口向"],["8d40","后喉坑垢好孔孝宏工巧巷幸広庚康弘恒慌抗拘控攻昂晃更杭校梗構江洪浩港溝甲皇硬稿糠紅紘絞綱耕考肯肱腔膏航荒行衡講貢購郊酵鉱砿鋼閤降"],["8d80","項香高鴻剛劫号合壕拷濠豪轟麹克刻告国穀酷鵠黒獄漉腰甑忽惚骨狛込此頃今困坤墾婚恨懇昏昆根梱混痕紺艮魂些佐叉唆嵯左差査沙瑳砂詐鎖裟坐座挫債催再最哉塞妻宰彩才採栽歳済災采犀砕砦祭斎細菜裁載際剤在材罪財冴坂阪堺榊肴咲崎埼碕鷺作削咋搾昨朔柵窄策索錯桜鮭笹匙冊刷"],["8e40","察拶撮擦札殺薩雑皐鯖捌錆鮫皿晒三傘参山惨撒散桟燦珊産算纂蚕讃賛酸餐斬暫残仕仔伺使刺司史嗣四士始姉姿子屍市師志思指支孜斯施旨枝止"],["8e80","死氏獅祉私糸紙紫肢脂至視詞詩試誌諮資賜雌飼歯事似侍児字寺慈持時次滋治爾璽痔磁示而耳自蒔辞汐鹿式識鴫竺軸宍雫七叱執失嫉室悉湿漆疾質実蔀篠偲柴芝屡蕊縞舎写射捨赦斜煮社紗者謝車遮蛇邪借勺尺杓灼爵酌釈錫若寂弱惹主取守手朱殊狩珠種腫趣酒首儒受呪寿授樹綬需囚収周"],["8f40","宗就州修愁拾洲秀秋終繍習臭舟蒐衆襲讐蹴輯週酋酬集醜什住充十従戎柔汁渋獣縦重銃叔夙宿淑祝縮粛塾熟出術述俊峻春瞬竣舜駿准循旬楯殉淳"],["8f80","準潤盾純巡遵醇順処初所暑曙渚庶緒署書薯藷諸助叙女序徐恕鋤除傷償勝匠升召哨商唱嘗奨妾娼宵将小少尚庄床廠彰承抄招掌捷昇昌昭晶松梢樟樵沼消渉湘焼焦照症省硝礁祥称章笑粧紹肖菖蒋蕉衝裳訟証詔詳象賞醤鉦鍾鐘障鞘上丈丞乗冗剰城場壌嬢常情擾条杖浄状畳穣蒸譲醸錠嘱埴飾"],["9040","拭植殖燭織職色触食蝕辱尻伸信侵唇娠寝審心慎振新晋森榛浸深申疹真神秦紳臣芯薪親診身辛進針震人仁刃塵壬尋甚尽腎訊迅陣靭笥諏須酢図厨"],["9080","逗吹垂帥推水炊睡粋翠衰遂酔錐錘随瑞髄崇嵩数枢趨雛据杉椙菅頗雀裾澄摺寸世瀬畝是凄制勢姓征性成政整星晴棲栖正清牲生盛精聖声製西誠誓請逝醒青静斉税脆隻席惜戚斥昔析石積籍績脊責赤跡蹟碩切拙接摂折設窃節説雪絶舌蝉仙先千占宣専尖川戦扇撰栓栴泉浅洗染潜煎煽旋穿箭線"],["9140","繊羨腺舛船薦詮賎践選遷銭銑閃鮮前善漸然全禅繕膳糎噌塑岨措曾曽楚狙疏疎礎祖租粗素組蘇訴阻遡鼠僧創双叢倉喪壮奏爽宋層匝惣想捜掃挿掻"],["9180","操早曹巣槍槽漕燥争痩相窓糟総綜聡草荘葬蒼藻装走送遭鎗霜騒像増憎臓蔵贈造促側則即息捉束測足速俗属賊族続卒袖其揃存孫尊損村遜他多太汰詑唾堕妥惰打柁舵楕陀駄騨体堆対耐岱帯待怠態戴替泰滞胎腿苔袋貸退逮隊黛鯛代台大第醍題鷹滝瀧卓啄宅托択拓沢濯琢託鐸濁諾茸凧蛸只"],["9240","叩但達辰奪脱巽竪辿棚谷狸鱈樽誰丹単嘆坦担探旦歎淡湛炭短端箪綻耽胆蛋誕鍛団壇弾断暖檀段男談値知地弛恥智池痴稚置致蜘遅馳築畜竹筑蓄"],["9280","逐秩窒茶嫡着中仲宙忠抽昼柱注虫衷註酎鋳駐樗瀦猪苧著貯丁兆凋喋寵帖帳庁弔張彫徴懲挑暢朝潮牒町眺聴脹腸蝶調諜超跳銚長頂鳥勅捗直朕沈珍賃鎮陳津墜椎槌追鎚痛通塚栂掴槻佃漬柘辻蔦綴鍔椿潰坪壷嬬紬爪吊釣鶴亭低停偵剃貞呈堤定帝底庭廷弟悌抵挺提梯汀碇禎程締艇訂諦蹄逓"],["9340","邸鄭釘鼎泥摘擢敵滴的笛適鏑溺哲徹撤轍迭鉄典填天展店添纏甜貼転顛点伝殿澱田電兎吐堵塗妬屠徒斗杜渡登菟賭途都鍍砥砺努度土奴怒倒党冬"],["9380","凍刀唐塔塘套宕島嶋悼投搭東桃梼棟盗淘湯涛灯燈当痘祷等答筒糖統到董蕩藤討謄豆踏逃透鐙陶頭騰闘働動同堂導憧撞洞瞳童胴萄道銅峠鴇匿得徳涜特督禿篤毒独読栃橡凸突椴届鳶苫寅酉瀞噸屯惇敦沌豚遁頓呑曇鈍奈那内乍凪薙謎灘捺鍋楢馴縄畷南楠軟難汝二尼弐迩匂賑肉虹廿日乳入"],["9440","如尿韮任妊忍認濡禰祢寧葱猫熱年念捻撚燃粘乃廼之埜嚢悩濃納能脳膿農覗蚤巴把播覇杷波派琶破婆罵芭馬俳廃拝排敗杯盃牌背肺輩配倍培媒梅"],["9480","楳煤狽買売賠陪這蝿秤矧萩伯剥博拍柏泊白箔粕舶薄迫曝漠爆縛莫駁麦函箱硲箸肇筈櫨幡肌畑畠八鉢溌発醗髪伐罰抜筏閥鳩噺塙蛤隼伴判半反叛帆搬斑板氾汎版犯班畔繁般藩販範釆煩頒飯挽晩番盤磐蕃蛮匪卑否妃庇彼悲扉批披斐比泌疲皮碑秘緋罷肥被誹費避非飛樋簸備尾微枇毘琵眉美"],["9540","鼻柊稗匹疋髭彦膝菱肘弼必畢筆逼桧姫媛紐百謬俵彪標氷漂瓢票表評豹廟描病秒苗錨鋲蒜蛭鰭品彬斌浜瀕貧賓頻敏瓶不付埠夫婦富冨布府怖扶敷"],["9580","斧普浮父符腐膚芙譜負賦赴阜附侮撫武舞葡蕪部封楓風葺蕗伏副復幅服福腹複覆淵弗払沸仏物鮒分吻噴墳憤扮焚奮粉糞紛雰文聞丙併兵塀幣平弊柄並蔽閉陛米頁僻壁癖碧別瞥蔑箆偏変片篇編辺返遍便勉娩弁鞭保舗鋪圃捕歩甫補輔穂募墓慕戊暮母簿菩倣俸包呆報奉宝峰峯崩庖抱捧放方朋"],["9640","法泡烹砲縫胞芳萌蓬蜂褒訪豊邦鋒飽鳳鵬乏亡傍剖坊妨帽忘忙房暴望某棒冒紡肪膨謀貌貿鉾防吠頬北僕卜墨撲朴牧睦穆釦勃没殆堀幌奔本翻凡盆"],["9680","摩磨魔麻埋妹昧枚毎哩槙幕膜枕鮪柾鱒桝亦俣又抹末沫迄侭繭麿万慢満漫蔓味未魅巳箕岬密蜜湊蓑稔脈妙粍民眠務夢無牟矛霧鵡椋婿娘冥名命明盟迷銘鳴姪牝滅免棉綿緬面麺摸模茂妄孟毛猛盲網耗蒙儲木黙目杢勿餅尤戻籾貰問悶紋門匁也冶夜爺耶野弥矢厄役約薬訳躍靖柳薮鑓愉愈油癒"],["9740","諭輸唯佑優勇友宥幽悠憂揖有柚湧涌猶猷由祐裕誘遊邑郵雄融夕予余与誉輿預傭幼妖容庸揚揺擁曜楊様洋溶熔用窯羊耀葉蓉要謡踊遥陽養慾抑欲"],["9780","沃浴翌翼淀羅螺裸来莱頼雷洛絡落酪乱卵嵐欄濫藍蘭覧利吏履李梨理璃痢裏裡里離陸律率立葎掠略劉流溜琉留硫粒隆竜龍侶慮旅虜了亮僚両凌寮料梁涼猟療瞭稜糧良諒遼量陵領力緑倫厘林淋燐琳臨輪隣鱗麟瑠塁涙累類令伶例冷励嶺怜玲礼苓鈴隷零霊麗齢暦歴列劣烈裂廉恋憐漣煉簾練聯"],["9840","蓮連錬呂魯櫓炉賂路露労婁廊弄朗楼榔浪漏牢狼篭老聾蝋郎六麓禄肋録論倭和話歪賄脇惑枠鷲亙亘鰐詫藁蕨椀湾碗腕"],["989f","弌丐丕个丱丶丼丿乂乖乘亂亅豫亊舒弍于亞亟亠亢亰亳亶从仍仄仆仂仗仞仭仟价伉佚估佛佝佗佇佶侈侏侘佻佩佰侑佯來侖儘俔俟俎俘俛俑俚俐俤俥倚倨倔倪倥倅伜俶倡倩倬俾俯們倆偃假會偕偐偈做偖偬偸傀傚傅傴傲"],["9940","僉僊傳僂僖僞僥僭僣僮價僵儉儁儂儖儕儔儚儡儺儷儼儻儿兀兒兌兔兢竸兩兪兮冀冂囘册冉冏冑冓冕冖冤冦冢冩冪冫决冱冲冰况冽凅凉凛几處凩凭"],["9980","凰凵凾刄刋刔刎刧刪刮刳刹剏剄剋剌剞剔剪剴剩剳剿剽劍劔劒剱劈劑辨辧劬劭劼劵勁勍勗勞勣勦飭勠勳勵勸勹匆匈甸匍匐匏匕匚匣匯匱匳匸區卆卅丗卉卍凖卞卩卮夘卻卷厂厖厠厦厥厮厰厶參簒雙叟曼燮叮叨叭叺吁吽呀听吭吼吮吶吩吝呎咏呵咎呟呱呷呰咒呻咀呶咄咐咆哇咢咸咥咬哄哈咨"],["9a40","咫哂咤咾咼哘哥哦唏唔哽哮哭哺哢唹啀啣啌售啜啅啖啗唸唳啝喙喀咯喊喟啻啾喘喞單啼喃喩喇喨嗚嗅嗟嗄嗜嗤嗔嘔嗷嘖嗾嗽嘛嗹噎噐營嘴嘶嘲嘸"],["9a80","噫噤嘯噬噪嚆嚀嚊嚠嚔嚏嚥嚮嚶嚴囂嚼囁囃囀囈囎囑囓囗囮囹圀囿圄圉圈國圍圓團圖嗇圜圦圷圸坎圻址坏坩埀垈坡坿垉垓垠垳垤垪垰埃埆埔埒埓堊埖埣堋堙堝塲堡塢塋塰毀塒堽塹墅墹墟墫墺壞墻墸墮壅壓壑壗壙壘壥壜壤壟壯壺壹壻壼壽夂夊夐夛梦夥夬夭夲夸夾竒奕奐奎奚奘奢奠奧奬奩"],["9b40","奸妁妝佞侫妣妲姆姨姜妍姙姚娥娟娑娜娉娚婀婬婉娵娶婢婪媚媼媾嫋嫂媽嫣嫗嫦嫩嫖嫺嫻嬌嬋嬖嬲嫐嬪嬶嬾孃孅孀孑孕孚孛孥孩孰孳孵學斈孺宀"],["9b80","它宦宸寃寇寉寔寐寤實寢寞寥寫寰寶寳尅將專對尓尠尢尨尸尹屁屆屎屓屐屏孱屬屮乢屶屹岌岑岔妛岫岻岶岼岷峅岾峇峙峩峽峺峭嶌峪崋崕崗嵜崟崛崑崔崢崚崙崘嵌嵒嵎嵋嵬嵳嵶嶇嶄嶂嶢嶝嶬嶮嶽嶐嶷嶼巉巍巓巒巖巛巫已巵帋帚帙帑帛帶帷幄幃幀幎幗幔幟幢幤幇幵并幺麼广庠廁廂廈廐廏"],["9c40","廖廣廝廚廛廢廡廨廩廬廱廳廰廴廸廾弃弉彝彜弋弑弖弩弭弸彁彈彌彎弯彑彖彗彙彡彭彳彷徃徂彿徊很徑徇從徙徘徠徨徭徼忖忻忤忸忱忝悳忿怡恠"],["9c80","怙怐怩怎怱怛怕怫怦怏怺恚恁恪恷恟恊恆恍恣恃恤恂恬恫恙悁悍惧悃悚悄悛悖悗悒悧悋惡悸惠惓悴忰悽惆悵惘慍愕愆惶惷愀惴惺愃愡惻惱愍愎慇愾愨愧慊愿愼愬愴愽慂慄慳慷慘慙慚慫慴慯慥慱慟慝慓慵憙憖憇憬憔憚憊憑憫憮懌懊應懷懈懃懆憺懋罹懍懦懣懶懺懴懿懽懼懾戀戈戉戍戌戔戛"],["9d40","戞戡截戮戰戲戳扁扎扞扣扛扠扨扼抂抉找抒抓抖拔抃抔拗拑抻拏拿拆擔拈拜拌拊拂拇抛拉挌拮拱挧挂挈拯拵捐挾捍搜捏掖掎掀掫捶掣掏掉掟掵捫"],["9d80","捩掾揩揀揆揣揉插揶揄搖搴搆搓搦搶攝搗搨搏摧摯摶摎攪撕撓撥撩撈撼據擒擅擇撻擘擂擱擧舉擠擡抬擣擯攬擶擴擲擺攀擽攘攜攅攤攣攫攴攵攷收攸畋效敖敕敍敘敞敝敲數斂斃變斛斟斫斷旃旆旁旄旌旒旛旙无旡旱杲昊昃旻杳昵昶昴昜晏晄晉晁晞晝晤晧晨晟晢晰暃暈暎暉暄暘暝曁暹曉暾暼"],["9e40","曄暸曖曚曠昿曦曩曰曵曷朏朖朞朦朧霸朮朿朶杁朸朷杆杞杠杙杣杤枉杰枩杼杪枌枋枦枡枅枷柯枴柬枳柩枸柤柞柝柢柮枹柎柆柧檜栞框栩桀桍栲桎"],["9e80","梳栫桙档桷桿梟梏梭梔條梛梃檮梹桴梵梠梺椏梍桾椁棊椈棘椢椦棡椌棍棔棧棕椶椒椄棗棣椥棹棠棯椨椪椚椣椡棆楹楷楜楸楫楔楾楮椹楴椽楙椰楡楞楝榁楪榲榮槐榿槁槓榾槎寨槊槝榻槃榧樮榑榠榜榕榴槞槨樂樛槿權槹槲槧樅榱樞槭樔槫樊樒櫁樣樓橄樌橲樶橸橇橢橙橦橈樸樢檐檍檠檄檢檣"],["9f40","檗蘗檻櫃櫂檸檳檬櫞櫑櫟檪櫚櫪櫻欅蘖櫺欒欖鬱欟欸欷盜欹飮歇歃歉歐歙歔歛歟歡歸歹歿殀殄殃殍殘殕殞殤殪殫殯殲殱殳殷殼毆毋毓毟毬毫毳毯"],["9f80","麾氈氓气氛氤氣汞汕汢汪沂沍沚沁沛汾汨汳沒沐泄泱泓沽泗泅泝沮沱沾沺泛泯泙泪洟衍洶洫洽洸洙洵洳洒洌浣涓浤浚浹浙涎涕濤涅淹渕渊涵淇淦涸淆淬淞淌淨淒淅淺淙淤淕淪淮渭湮渮渙湲湟渾渣湫渫湶湍渟湃渺湎渤滿渝游溂溪溘滉溷滓溽溯滄溲滔滕溏溥滂溟潁漑灌滬滸滾漿滲漱滯漲滌"],["e040","漾漓滷澆潺潸澁澀潯潛濳潭澂潼潘澎澑濂潦澳澣澡澤澹濆澪濟濕濬濔濘濱濮濛瀉瀋濺瀑瀁瀏濾瀛瀚潴瀝瀘瀟瀰瀾瀲灑灣炙炒炯烱炬炸炳炮烟烋烝"],["e080","烙焉烽焜焙煥煕熈煦煢煌煖煬熏燻熄熕熨熬燗熹熾燒燉燔燎燠燬燧燵燼燹燿爍爐爛爨爭爬爰爲爻爼爿牀牆牋牘牴牾犂犁犇犒犖犢犧犹犲狃狆狄狎狒狢狠狡狹狷倏猗猊猜猖猝猴猯猩猥猾獎獏默獗獪獨獰獸獵獻獺珈玳珎玻珀珥珮珞璢琅瑯琥珸琲琺瑕琿瑟瑙瑁瑜瑩瑰瑣瑪瑶瑾璋璞璧瓊瓏瓔珱"],["e140","瓠瓣瓧瓩瓮瓲瓰瓱瓸瓷甄甃甅甌甎甍甕甓甞甦甬甼畄畍畊畉畛畆畚畩畤畧畫畭畸當疆疇畴疊疉疂疔疚疝疥疣痂疳痃疵疽疸疼疱痍痊痒痙痣痞痾痿"],["e180","痼瘁痰痺痲痳瘋瘍瘉瘟瘧瘠瘡瘢瘤瘴瘰瘻癇癈癆癜癘癡癢癨癩癪癧癬癰癲癶癸發皀皃皈皋皎皖皓皙皚皰皴皸皹皺盂盍盖盒盞盡盥盧盪蘯盻眈眇眄眩眤眞眥眦眛眷眸睇睚睨睫睛睥睿睾睹瞎瞋瞑瞠瞞瞰瞶瞹瞿瞼瞽瞻矇矍矗矚矜矣矮矼砌砒礦砠礪硅碎硴碆硼碚碌碣碵碪碯磑磆磋磔碾碼磅磊磬"],["e240","磧磚磽磴礇礒礑礙礬礫祀祠祗祟祚祕祓祺祿禊禝禧齋禪禮禳禹禺秉秕秧秬秡秣稈稍稘稙稠稟禀稱稻稾稷穃穗穉穡穢穩龝穰穹穽窈窗窕窘窖窩竈窰"],["e280","窶竅竄窿邃竇竊竍竏竕竓站竚竝竡竢竦竭竰笂笏笊笆笳笘笙笞笵笨笶筐筺笄筍笋筌筅筵筥筴筧筰筱筬筮箝箘箟箍箜箚箋箒箏筝箙篋篁篌篏箴篆篝篩簑簔篦篥籠簀簇簓篳篷簗簍篶簣簧簪簟簷簫簽籌籃籔籏籀籐籘籟籤籖籥籬籵粃粐粤粭粢粫粡粨粳粲粱粮粹粽糀糅糂糘糒糜糢鬻糯糲糴糶糺紆"],["e340","紂紜紕紊絅絋紮紲紿紵絆絳絖絎絲絨絮絏絣經綉絛綏絽綛綺綮綣綵緇綽綫總綢綯緜綸綟綰緘緝緤緞緻緲緡縅縊縣縡縒縱縟縉縋縢繆繦縻縵縹繃縷"],["e380","縲縺繧繝繖繞繙繚繹繪繩繼繻纃緕繽辮繿纈纉續纒纐纓纔纖纎纛纜缸缺罅罌罍罎罐网罕罔罘罟罠罨罩罧罸羂羆羃羈羇羌羔羞羝羚羣羯羲羹羮羶羸譱翅翆翊翕翔翡翦翩翳翹飜耆耄耋耒耘耙耜耡耨耿耻聊聆聒聘聚聟聢聨聳聲聰聶聹聽聿肄肆肅肛肓肚肭冐肬胛胥胙胝胄胚胖脉胯胱脛脩脣脯腋"],["e440","隋腆脾腓腑胼腱腮腥腦腴膃膈膊膀膂膠膕膤膣腟膓膩膰膵膾膸膽臀臂膺臉臍臑臙臘臈臚臟臠臧臺臻臾舁舂舅與舊舍舐舖舩舫舸舳艀艙艘艝艚艟艤"],["e480","艢艨艪艫舮艱艷艸艾芍芒芫芟芻芬苡苣苟苒苴苳苺莓范苻苹苞茆苜茉苙茵茴茖茲茱荀茹荐荅茯茫茗茘莅莚莪莟莢莖茣莎莇莊荼莵荳荵莠莉莨菴萓菫菎菽萃菘萋菁菷萇菠菲萍萢萠莽萸蔆菻葭萪萼蕚蒄葷葫蒭葮蒂葩葆萬葯葹萵蓊葢蒹蒿蒟蓙蓍蒻蓚蓐蓁蓆蓖蒡蔡蓿蓴蔗蔘蔬蔟蔕蔔蓼蕀蕣蕘蕈"],["e540","蕁蘂蕋蕕薀薤薈薑薊薨蕭薔薛藪薇薜蕷蕾薐藉薺藏薹藐藕藝藥藜藹蘊蘓蘋藾藺蘆蘢蘚蘰蘿虍乕虔號虧虱蚓蚣蚩蚪蚋蚌蚶蚯蛄蛆蚰蛉蠣蚫蛔蛞蛩蛬"],["e580","蛟蛛蛯蜒蜆蜈蜀蜃蛻蜑蜉蜍蛹蜊蜴蜿蜷蜻蜥蜩蜚蝠蝟蝸蝌蝎蝴蝗蝨蝮蝙蝓蝣蝪蠅螢螟螂螯蟋螽蟀蟐雖螫蟄螳蟇蟆螻蟯蟲蟠蠏蠍蟾蟶蟷蠎蟒蠑蠖蠕蠢蠡蠱蠶蠹蠧蠻衄衂衒衙衞衢衫袁衾袞衵衽袵衲袂袗袒袮袙袢袍袤袰袿袱裃裄裔裘裙裝裹褂裼裴裨裲褄褌褊褓襃褞褥褪褫襁襄褻褶褸襌褝襠襞"],["e640","襦襤襭襪襯襴襷襾覃覈覊覓覘覡覩覦覬覯覲覺覽覿觀觚觜觝觧觴觸訃訖訐訌訛訝訥訶詁詛詒詆詈詼詭詬詢誅誂誄誨誡誑誥誦誚誣諄諍諂諚諫諳諧"],["e680","諤諱謔諠諢諷諞諛謌謇謚諡謖謐謗謠謳鞫謦謫謾謨譁譌譏譎證譖譛譚譫譟譬譯譴譽讀讌讎讒讓讖讙讚谺豁谿豈豌豎豐豕豢豬豸豺貂貉貅貊貍貎貔豼貘戝貭貪貽貲貳貮貶賈賁賤賣賚賽賺賻贄贅贊贇贏贍贐齎贓賍贔贖赧赭赱赳趁趙跂趾趺跏跚跖跌跛跋跪跫跟跣跼踈踉跿踝踞踐踟蹂踵踰踴蹊"],["e740","蹇蹉蹌蹐蹈蹙蹤蹠踪蹣蹕蹶蹲蹼躁躇躅躄躋躊躓躑躔躙躪躡躬躰軆躱躾軅軈軋軛軣軼軻軫軾輊輅輕輒輙輓輜輟輛輌輦輳輻輹轅轂輾轌轉轆轎轗轜"],["e780","轢轣轤辜辟辣辭辯辷迚迥迢迪迯邇迴逅迹迺逑逕逡逍逞逖逋逧逶逵逹迸遏遐遑遒逎遉逾遖遘遞遨遯遶隨遲邂遽邁邀邊邉邏邨邯邱邵郢郤扈郛鄂鄒鄙鄲鄰酊酖酘酣酥酩酳酲醋醉醂醢醫醯醪醵醴醺釀釁釉釋釐釖釟釡釛釼釵釶鈞釿鈔鈬鈕鈑鉞鉗鉅鉉鉤鉈銕鈿鉋鉐銜銖銓銛鉚鋏銹銷鋩錏鋺鍄錮"],["e840","錙錢錚錣錺錵錻鍜鍠鍼鍮鍖鎰鎬鎭鎔鎹鏖鏗鏨鏥鏘鏃鏝鏐鏈鏤鐚鐔鐓鐃鐇鐐鐶鐫鐵鐡鐺鑁鑒鑄鑛鑠鑢鑞鑪鈩鑰鑵鑷鑽鑚鑼鑾钁鑿閂閇閊閔閖閘閙"],["e880","閠閨閧閭閼閻閹閾闊濶闃闍闌闕闔闖關闡闥闢阡阨阮阯陂陌陏陋陷陜陞陝陟陦陲陬隍隘隕隗險隧隱隲隰隴隶隸隹雎雋雉雍襍雜霍雕雹霄霆霈霓霎霑霏霖霙霤霪霰霹霽霾靄靆靈靂靉靜靠靤靦靨勒靫靱靹鞅靼鞁靺鞆鞋鞏鞐鞜鞨鞦鞣鞳鞴韃韆韈韋韜韭齏韲竟韶韵頏頌頸頤頡頷頽顆顏顋顫顯顰"],["e940","顱顴顳颪颯颱颶飄飃飆飩飫餃餉餒餔餘餡餝餞餤餠餬餮餽餾饂饉饅饐饋饑饒饌饕馗馘馥馭馮馼駟駛駝駘駑駭駮駱駲駻駸騁騏騅駢騙騫騷驅驂驀驃"],["e980","騾驕驍驛驗驟驢驥驤驩驫驪骭骰骼髀髏髑髓體髞髟髢髣髦髯髫髮髴髱髷髻鬆鬘鬚鬟鬢鬣鬥鬧鬨鬩鬪鬮鬯鬲魄魃魏魍魎魑魘魴鮓鮃鮑鮖鮗鮟鮠鮨鮴鯀鯊鮹鯆鯏鯑鯒鯣鯢鯤鯔鯡鰺鯲鯱鯰鰕鰔鰉鰓鰌鰆鰈鰒鰊鰄鰮鰛鰥鰤鰡鰰鱇鰲鱆鰾鱚鱠鱧鱶鱸鳧鳬鳰鴉鴈鳫鴃鴆鴪鴦鶯鴣鴟鵄鴕鴒鵁鴿鴾鵆鵈"],["ea40","鵝鵞鵤鵑鵐鵙鵲鶉鶇鶫鵯鵺鶚鶤鶩鶲鷄鷁鶻鶸鶺鷆鷏鷂鷙鷓鷸鷦鷭鷯鷽鸚鸛鸞鹵鹹鹽麁麈麋麌麒麕麑麝麥麩麸麪麭靡黌黎黏黐黔黜點黝黠黥黨黯"],["ea80","黴黶黷黹黻黼黽鼇鼈皷鼕鼡鼬鼾齊齒齔齣齟齠齡齦齧齬齪齷齲齶龕龜龠堯槇遙瑤凜熙"],["ed40","纊褜鍈銈蓜俉炻昱棈鋹曻彅丨仡仼伀伃伹佖侒侊侚侔俍偀倢俿倞偆偰偂傔僴僘兊兤冝冾凬刕劜劦勀勛匀匇匤卲厓厲叝﨎咜咊咩哿喆坙坥垬埈埇﨏"],["ed80","塚增墲夋奓奛奝奣妤妺孖寀甯寘寬尞岦岺峵崧嵓﨑嵂嵭嶸嶹巐弡弴彧德忞恝悅悊惞惕愠惲愑愷愰憘戓抦揵摠撝擎敎昀昕昻昉昮昞昤晥晗晙晴晳暙暠暲暿曺朎朗杦枻桒柀栁桄棏﨓楨﨔榘槢樰橫橆橳橾櫢櫤毖氿汜沆汯泚洄涇浯涖涬淏淸淲淼渹湜渧渼溿澈澵濵瀅瀇瀨炅炫焏焄煜煆煇凞燁燾犱"],["ee40","犾猤猪獷玽珉珖珣珒琇珵琦琪琩琮瑢璉璟甁畯皂皜皞皛皦益睆劯砡硎硤硺礰礼神祥禔福禛竑竧靖竫箞精絈絜綷綠緖繒罇羡羽茁荢荿菇菶葈蒴蕓蕙"],["ee80","蕫﨟薰蘒﨡蠇裵訒訷詹誧誾諟諸諶譓譿賰賴贒赶﨣軏﨤逸遧郞都鄕鄧釚釗釞釭釮釤釥鈆鈐鈊鈺鉀鈼鉎鉙鉑鈹鉧銧鉷鉸鋧鋗鋙鋐﨧鋕鋠鋓錥錡鋻﨨錞鋿錝錂鍰鍗鎤鏆鏞鏸鐱鑅鑈閒隆﨩隝隯霳霻靃靍靏靑靕顗顥飯飼餧館馞驎髙髜魵魲鮏鮱鮻鰀鵰鵫鶴鸙黑"],["eeef","ⅰ",9,"¬¦'""],["f040","",62],["f080","",124],["f140","",62],["f180","",124],["f240","",62],["f280","",124],["f340","",62],["f380","",124],["f440","",62],["f480","",124],["f540","",62],["f580","",124],["f640","",62],["f680","",124],["f740","",62],["f780","",124],["f840","",62],["f880","",124],["f940",""],["fa40","ⅰ",9,"Ⅰ",9,"¬¦'"㈱№℡∵纊褜鍈銈蓜俉炻昱棈鋹曻彅丨仡仼伀伃伹佖侒侊侚侔俍偀倢俿倞偆偰偂傔僴僘兊"],["fa80","兤冝冾凬刕劜劦勀勛匀匇匤卲厓厲叝﨎咜咊咩哿喆坙坥垬埈埇﨏塚增墲夋奓奛奝奣妤妺孖寀甯寘寬尞岦岺峵崧嵓﨑嵂嵭嶸嶹巐弡弴彧德忞恝悅悊惞惕愠惲愑愷愰憘戓抦揵摠撝擎敎昀昕昻昉昮昞昤晥晗晙晴晳暙暠暲暿曺朎朗杦枻桒柀栁桄棏﨓楨﨔榘槢樰橫橆橳橾櫢櫤毖氿汜沆汯泚洄涇浯"],["fb40","涖涬淏淸淲淼渹湜渧渼溿澈澵濵瀅瀇瀨炅炫焏焄煜煆煇凞燁燾犱犾猤猪獷玽珉珖珣珒琇珵琦琪琩琮瑢璉璟甁畯皂皜皞皛皦益睆劯砡硎硤硺礰礼神"],["fb80","祥禔福禛竑竧靖竫箞精絈絜綷綠緖繒罇羡羽茁荢荿菇菶葈蒴蕓蕙蕫﨟薰蘒﨡蠇裵訒訷詹誧誾諟諸諶譓譿賰賴贒赶﨣軏﨤逸遧郞都鄕鄧釚釗釞釭釮釤釥鈆鈐鈊鈺鉀鈼鉎鉙鉑鈹鉧銧鉷鉸鋧鋗鋙鋐﨧鋕鋠鋓錥錡鋻﨨錞鋿錝錂鍰鍗鎤鏆鏞鏸鐱鑅鑈閒隆﨩隝隯霳霻靃靍靏靑靕顗顥飯飼餧館馞驎髙"],["fc40","髜魵魲鮏鮱鮻鰀鵰鵫鶴鸙黑"]]'); + +/***/ }) + +/******/ }); +/************************************************************************/ +/******/ // The module cache +/******/ var __webpack_module_cache__ = {}; +/******/ +/******/ // The require function +/******/ function __nccwpck_require__(moduleId) { +/******/ // Check if module is in cache +/******/ var cachedModule = __webpack_module_cache__[moduleId]; +/******/ if (cachedModule !== undefined) { +/******/ return cachedModule.exports; +/******/ } +/******/ // Create a new module (and put it into the cache) +/******/ var module = __webpack_module_cache__[moduleId] = { +/******/ // no module.id needed +/******/ // no module.loaded needed +/******/ exports: {} +/******/ }; +/******/ +/******/ // Execute the module function +/******/ var threw = true; +/******/ try { +/******/ __webpack_modules__[moduleId].call(module.exports, module, module.exports, __nccwpck_require__); +/******/ threw = false; +/******/ } finally { +/******/ if(threw) delete __webpack_module_cache__[moduleId]; +/******/ } +/******/ +/******/ // Return the exports of the module +/******/ return module.exports; +/******/ } +/******/ +/************************************************************************/ +/******/ /* webpack/runtime/compat */ +/******/ +/******/ if (typeof __nccwpck_require__ !== 'undefined') __nccwpck_require__.ab = __dirname + "/"; +/******/ +/************************************************************************/ +var __webpack_exports__ = {}; +const core = __nccwpck_require__(7484) +const { analyzeMemoryTrendsAcrossVersions } = __nccwpck_require__(1548) + +async function run() { + try { + const mixpanelUser = core.getInput('mixpanel-user', { required: true }) + const mixpanelSecret = core.getInput('mixpanel-secret', { required: true }) + const mixpanelProjectId = core.getInput('mixpanel-project-id', { + required: true, + }) + const previousVersionCount = parseInt( + core.getInput('previous-version-count') || '2' + ) + + core.info('Beginning analysis...') + const memoryAnalysis = await analyzeMemoryTrendsAcrossVersions({ + previousVersionCount, + uname: mixpanelUser, + pwd: mixpanelSecret, + projectId: mixpanelProjectId, + }) + + console.log( + 'ODD Available Memory and Processes with Increasing Memory Trend or Selectively Observed by Version (Rolling 1 Month Analysis Window):' + ) + console.log(JSON.stringify(memoryAnalysis, null, 2)) + + const outputText = + 'ODD Available Memory and Processes with Increasing Memory Trend or Selectively Observed by Version (Rolling 1 Month Analysis Window):\n' + + Object.entries(memoryAnalysis) + .map( + ([version, analysis]) => + `\n${version}: ${JSON.stringify(analysis, null, 2)}` + ) + .join('\n') + + core.setOutput('analysis-results', JSON.stringify(memoryAnalysis)) + + await core.summary + .addHeading('ODD Memory Usage Results') + .addCodeBlock(outputText, 'json') + .write() + } catch (error) { + core.setFailed(error.message) + } +} + +run() + +module.exports = __webpack_exports__; +/******/ })() +; \ No newline at end of file diff --git a/.github/actions/odd-resource-analysis/package-lock.json b/.github/actions/odd-resource-analysis/package-lock.json new file mode 100644 index 00000000000..f7f21ad07d0 --- /dev/null +++ b/.github/actions/odd-resource-analysis/package-lock.json @@ -0,0 +1,331 @@ +{ + "name": "odd-memory-analysis", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "odd-memory-analysis", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "@actions/core": "1.11.1", + "@actions/github": "6.0.0", + "calculate-correlation": "^1.2.3", + "node-fetch": "2.6.9" + }, + "devDependencies": { + "@vercel/ncc": "0.38.3", + "prettier": "2.7.1" + } + }, + "node_modules/@actions/core": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@actions/core/-/core-1.11.1.tgz", + "integrity": "sha512-hXJCSrkwfA46Vd9Z3q4cpEpHB1rL5NG04+/rbqW9d3+CSvtB1tYe8UTpAlixa1vj0m/ULglfEK2UKxMGxCxv5A==", + "dependencies": { + "@actions/exec": "^1.1.1", + "@actions/http-client": "^2.0.1" + } + }, + "node_modules/@actions/exec": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@actions/exec/-/exec-1.1.1.tgz", + "integrity": "sha512-+sCcHHbVdk93a0XT19ECtO/gIXoxvdsgQLzb2fE2/5sIZmWQuluYyjPQtrtTHdU1YzTZ7bAPN4sITq2xi1679w==", + "dependencies": { + "@actions/io": "^1.0.1" + } + }, + "node_modules/@actions/github": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@actions/github/-/github-6.0.0.tgz", + "integrity": "sha512-alScpSVnYmjNEXboZjarjukQEzgCRmjMv6Xj47fsdnqGS73bjJNDpiiXmp8jr0UZLdUB6d9jW63IcmddUP+l0g==", + "dependencies": { + "@actions/http-client": "^2.2.0", + "@octokit/core": "^5.0.1", + "@octokit/plugin-paginate-rest": "^9.0.0", + "@octokit/plugin-rest-endpoint-methods": "^10.0.0" + } + }, + "node_modules/@actions/http-client": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-2.2.3.tgz", + "integrity": "sha512-mx8hyJi/hjFvbPokCg4uRd4ZX78t+YyRPtnKWwIl+RzNaVuFpQHfmlGVfsKEJN8LwTCvL+DfVgAM04XaHkm6bA==", + "dependencies": { + "tunnel": "^0.0.6", + "undici": "^5.25.4" + } + }, + "node_modules/@actions/io": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@actions/io/-/io-1.1.3.tgz", + "integrity": "sha512-wi9JjgKLYS7U/z8PPbco+PvTb/nRWjeoFlJ1Qer83k/3C5PHQi28hiVdeE2kHXmIL99mQFawx8qt/JPjZilJ8Q==" + }, + "node_modules/@fastify/busboy": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz", + "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==", + "engines": { + "node": ">=14" + } + }, + "node_modules/@octokit/auth-token": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-4.0.0.tgz", + "integrity": "sha512-tY/msAuJo6ARbK6SPIxZrPBms3xPbfwBrulZe0Wtr/DIY9lje2HeV1uoebShn6mx7SjCHif6EjMvoREj+gZ+SA==", + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/core": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@octokit/core/-/core-5.2.0.tgz", + "integrity": "sha512-1LFfa/qnMQvEOAdzlQymH0ulepxbxnCYAKJZfMci/5XJyIHWgEYnDmgnKakbTh7CH2tFQ5O60oYDvns4i9RAIg==", + "dependencies": { + "@octokit/auth-token": "^4.0.0", + "@octokit/graphql": "^7.1.0", + "@octokit/request": "^8.3.1", + "@octokit/request-error": "^5.1.0", + "@octokit/types": "^13.0.0", + "before-after-hook": "^2.2.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/endpoint": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-9.0.5.tgz", + "integrity": "sha512-ekqR4/+PCLkEBF6qgj8WqJfvDq65RH85OAgrtnVp1mSxaXF03u2xW/hUdweGS5654IlC0wkNYC18Z50tSYTAFw==", + "dependencies": { + "@octokit/types": "^13.1.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/graphql": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-7.1.0.tgz", + "integrity": "sha512-r+oZUH7aMFui1ypZnAvZmn0KSqAUgE1/tUXIWaqUCa1758ts/Jio84GZuzsvUkme98kv0WFY8//n0J1Z+vsIsQ==", + "dependencies": { + "@octokit/request": "^8.3.0", + "@octokit/types": "^13.0.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/openapi-types": { + "version": "22.2.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-22.2.0.tgz", + "integrity": "sha512-QBhVjcUa9W7Wwhm6DBFu6ZZ+1/t/oYxqc2tp81Pi41YNuJinbFRx8B133qVOrAaBbF7D/m0Et6f9/pZt9Rc+tg==" + }, + "node_modules/@octokit/plugin-paginate-rest": { + "version": "9.2.1", + "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-9.2.1.tgz", + "integrity": "sha512-wfGhE/TAkXZRLjksFXuDZdmGnJQHvtU/joFQdweXUgzo1XwvBCD4o4+75NtFfjfLK5IwLf9vHTfSiU3sLRYpRw==", + "dependencies": { + "@octokit/types": "^12.6.0" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@octokit/core": "5" + } + }, + "node_modules/@octokit/plugin-paginate-rest/node_modules/@octokit/openapi-types": { + "version": "20.0.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-20.0.0.tgz", + "integrity": "sha512-EtqRBEjp1dL/15V7WiX5LJMIxxkdiGJnabzYx5Apx4FkQIFgAfKumXeYAqqJCj1s+BMX4cPFIFC4OLCR6stlnA==" + }, + "node_modules/@octokit/plugin-paginate-rest/node_modules/@octokit/types": { + "version": "12.6.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-12.6.0.tgz", + "integrity": "sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw==", + "dependencies": { + "@octokit/openapi-types": "^20.0.0" + } + }, + "node_modules/@octokit/plugin-rest-endpoint-methods": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-10.4.1.tgz", + "integrity": "sha512-xV1b+ceKV9KytQe3zCVqjg+8GTGfDYwaT1ATU5isiUyVtlVAO3HNdzpS4sr4GBx4hxQ46s7ITtZrAsxG22+rVg==", + "dependencies": { + "@octokit/types": "^12.6.0" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@octokit/core": "5" + } + }, + "node_modules/@octokit/plugin-rest-endpoint-methods/node_modules/@octokit/openapi-types": { + "version": "20.0.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-20.0.0.tgz", + "integrity": "sha512-EtqRBEjp1dL/15V7WiX5LJMIxxkdiGJnabzYx5Apx4FkQIFgAfKumXeYAqqJCj1s+BMX4cPFIFC4OLCR6stlnA==" + }, + "node_modules/@octokit/plugin-rest-endpoint-methods/node_modules/@octokit/types": { + "version": "12.6.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-12.6.0.tgz", + "integrity": "sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw==", + "dependencies": { + "@octokit/openapi-types": "^20.0.0" + } + }, + "node_modules/@octokit/request": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-8.4.0.tgz", + "integrity": "sha512-9Bb014e+m2TgBeEJGEbdplMVWwPmL1FPtggHQRkV+WVsMggPtEkLKPlcVYm/o8xKLkpJ7B+6N8WfQMtDLX2Dpw==", + "dependencies": { + "@octokit/endpoint": "^9.0.1", + "@octokit/request-error": "^5.1.0", + "@octokit/types": "^13.1.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/request-error": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-5.1.0.tgz", + "integrity": "sha512-GETXfE05J0+7H2STzekpKObFe765O5dlAKUTLNGeH+x47z7JjXHfsHKo5z21D/o/IOZTUEI6nyWyR+bZVP/n5Q==", + "dependencies": { + "@octokit/types": "^13.1.0", + "deprecation": "^2.0.0", + "once": "^1.4.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/types": { + "version": "13.6.1", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.6.1.tgz", + "integrity": "sha512-PHZE9Z+kWXb23Ndik8MKPirBPziOc0D2/3KH1P+6jK5nGWe96kadZuE4jev2/Jq7FvIfTlT2Ltg8Fv2x1v0a5g==", + "dependencies": { + "@octokit/openapi-types": "^22.2.0" + } + }, + "node_modules/@vercel/ncc": { + "version": "0.38.3", + "resolved": "https://registry.npmjs.org/@vercel/ncc/-/ncc-0.38.3.tgz", + "integrity": "sha512-rnK6hJBS6mwc+Bkab+PGPs9OiS0i/3kdTO+CkI8V0/VrW3vmz7O2Pxjw/owOlmo6PKEIxRSeZKv/kuL9itnpYA==", + "dev": true, + "bin": { + "ncc": "dist/ncc/cli.js" + } + }, + "node_modules/before-after-hook": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz", + "integrity": "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==" + }, + "node_modules/calculate-correlation": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/calculate-correlation/-/calculate-correlation-1.2.3.tgz", + "integrity": "sha512-WLoM4ZXJcFcnqhiYtTCBoKaNoWM6O0daL/7jbctM+ZJU2tNdUHH+mvxeLSc11Kgd/jsNHtnVW6lGlxA0H+MmiQ==" + }, + "node_modules/deprecation": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz", + "integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==" + }, + "node_modules/node-fetch": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.9.tgz", + "integrity": "sha512-DJm/CJkZkRjKKj4Zi4BsKVZh3ValV5IR5s7LVZnW+6YMh0W1BfNA8XSs6DLMGYlId5F3KnA70uu2qepcR08Qqg==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/prettier": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.7.1.tgz", + "integrity": "sha512-ujppO+MkdPqoVINuDFDRLClm7D78qbDt0/NR+wp5FqEZOoTNAjPHWj17QRhu7geIHJfcNhRk1XVQmF8Bp3ye+g==", + "dev": true, + "bin": { + "prettier": "bin-prettier.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "node_modules/tunnel": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", + "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==", + "engines": { + "node": ">=0.6.11 <=0.7.0 || >=0.7.3" + } + }, + "node_modules/undici": { + "version": "5.28.4", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.28.4.tgz", + "integrity": "sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==", + "dependencies": { + "@fastify/busboy": "^2.0.0" + }, + "engines": { + "node": ">=14.0" + } + }, + "node_modules/universal-user-agent": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.1.tgz", + "integrity": "sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ==" + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + } + } +} diff --git a/.github/actions/odd-resource-analysis/package.json b/.github/actions/odd-resource-analysis/package.json new file mode 100644 index 00000000000..30a28ac529d --- /dev/null +++ b/.github/actions/odd-resource-analysis/package.json @@ -0,0 +1,22 @@ +{ + "name": "odd-memory-analysis", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "build": "ncc build action/index.js -o dist" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "@actions/core": "1.11.1", + "@actions/github": "6.0.0", + "calculate-correlation": "^1.2.3", + "node-fetch": "2.6.9" + }, + "devDependencies": { + "@vercel/ncc": "0.38.3", + "prettier": "2.7.1" + } +} diff --git a/.github/workflows/abr-testing-lint-test.yaml b/.github/workflows/abr-testing-lint-test.yaml index e103c61efdd..447597c2b89 100644 --- a/.github/workflows/abr-testing-lint-test.yaml +++ b/.github/workflows/abr-testing-lint-test.yaml @@ -36,11 +36,11 @@ jobs: runs-on: 'windows-latest' steps: - name: Checkout opentrons repo - uses: 'actions/checkout@v3' + uses: 'actions/checkout@v4' with: fetch-depth: 0 - name: Setup Node - uses: 'actions/setup-node@v3' + uses: 'actions/setup-node@v4' with: node-version: '12' - name: Setup Python @@ -52,8 +52,6 @@ jobs: with: project: 'abr-testing' - name: lint - run: - make -C abr-testing lint + run: make -C abr-testing lint - name: test - run: - make -C abr-testing test + run: make -C abr-testing test diff --git a/.github/workflows/analyses-snapshot-test.yaml b/.github/workflows/analyses-snapshot-test.yaml index 09539d873e9..fffdd6b667d 100644 --- a/.github/workflows/analyses-snapshot-test.yaml +++ b/.github/workflows/analyses-snapshot-test.yaml @@ -45,12 +45,13 @@ jobs: timeout-minutes: 15 runs-on: ubuntu-latest env: + BASE_IMAGE_NAME: opentrons-python-base:3.10 ANALYSIS_REF: ${{ github.event.inputs.ANALYSIS_REF || github.head_ref || 'edge' }} SNAPSHOT_REF: ${{ github.event.inputs.SNAPSHOT_REF || github.head_ref || 'edge' }} # If we're running because of workflow_dispatch, use the user input to decide # whether to open a PR on failure. Otherwise, there is no user input, # so we only open a PR if the PR has the label 'gen-analyses-snapshot-pr' - OPEN_PR_ON_FAILURE: ${{ (github.event_name == 'workflow_dispatch' && github.events.inputs.OPEN_PR_ON_FAILURE) || ((github.event_name != 'workflow_dispatch') && (contains(github.event.pull_request.labels.*.name, 'gen-analyses-snapshot-pr'))) }} + OPEN_PR_ON_FAILURE: ${{ (github.event_name == 'workflow_dispatch' && github.event.inputs.OPEN_PR_ON_FAILURE) || ((github.event_name != 'workflow_dispatch') && (contains(github.event.pull_request.labels.*.name, 'gen-analyses-snapshot-pr'))) }} PR_TARGET_BRANCH: ${{ github.event.pull_request.base.ref || 'not a pr'}} steps: - name: Checkout Repository @@ -71,9 +72,24 @@ jobs: echo "Analyses snapshots match ${{ env.PR_TARGET_BRANCH }} snapshots." fi - - name: Docker Build + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build base image + id: build_base_image + uses: docker/build-push-action@v6 + with: + context: analyses-snapshot-testing/citools + file: analyses-snapshot-testing/citools/Dockerfile.base + push: false + load: true + tags: ${{ env.BASE_IMAGE_NAME }} + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Build analysis image working-directory: analyses-snapshot-testing - run: make build-opentrons-analysis + run: make build-opentrons-analysis BASE_IMAGE_NAME=${{ env.BASE_IMAGE_NAME }} ANALYSIS_REF=${{ env.ANALYSIS_REF }} CACHEBUST=${{ github.run_number }} - name: Set up Python 3.13 uses: actions/setup-python@v5 @@ -112,8 +128,8 @@ jobs: commit-message: 'fix(analyses-snapshot-testing): heal analyses snapshots' title: 'fix(analyses-snapshot-testing): heal ${{ env.ANALYSIS_REF }} snapshots' body: 'This PR was requested on the PR https://github.com/${{ github.repository }}/pull/${{ github.event.pull_request.number }}' - branch: 'analyses-snapshot-testing/${{ env.ANALYSIS_REF }}-from-${{ env.SNAPSHOT_REF}}' - base: ${{ env.SNAPSHOT_REF}} + branch: 'analyses-snapshot-testing/${{ env.ANALYSIS_REF }}-from-${{ env.SNAPSHOT_REF }}' + base: ${{ env.SNAPSHOT_REF }} - name: Comment on feature PR if: always() && steps.create_pull_request.outcome == 'success' && github.event_name == 'pull_request' @@ -135,5 +151,5 @@ jobs: commit-message: 'fix(analyses-snapshot-testing): heal ${{ env.ANALYSIS_REF }} snapshots' title: 'fix(analyses-snapshot-testing): heal ${{ env.ANALYSIS_REF }} snapshots' body: 'The ${{ env.ANALYSIS_REF }} overnight analyses snapshot test is failing. This PR was opened to alert us to the failure.' - branch: 'analyses-snapshot-testing/${{ env.ANALYSIS_REF }}-from-${{ env.SNAPSHOT_REF}}' - base: ${{ env.SNAPSHOT_REF}} + branch: 'analyses-snapshot-testing/${{ env.ANALYSIS_REF }}-from-${{ env.SNAPSHOT_REF }}' + base: ${{ env.SNAPSHOT_REF }} diff --git a/.github/workflows/api-test-lint-deploy.yaml b/.github/workflows/api-test-lint-deploy.yaml index 5143c6e8021..e1790f28f20 100644 --- a/.github/workflows/api-test-lint-deploy.yaml +++ b/.github/workflows/api-test-lint-deploy.yaml @@ -51,12 +51,12 @@ jobs: timeout-minutes: 10 runs-on: 'ubuntu-22.04' steps: - - uses: 'actions/checkout@v3' + - uses: 'actions/checkout@v4' with: fetch-depth: 0 - - uses: 'actions/setup-node@v3' + - uses: 'actions/setup-node@v4' with: - node-version: '18.19.0' + node-version: '22.11.0' - uses: 'actions/setup-python@v4' with: python-version: '3.10' @@ -73,8 +73,6 @@ jobs: strategy: matrix: os: ['windows-2022', 'ubuntu-22.04', 'macos-latest'] - # TODO(mc, 2022-02-24): expand this matrix to 3.8 and 3.9, - # preferably in a nightly cronjob on edge or something python: ['3.10'] with-ot-hardware: ['true', 'false'] exclude: @@ -84,7 +82,7 @@ jobs: with-ot-hardware: 'true' runs-on: '${{ matrix.os }}' steps: - - uses: 'actions/checkout@v3' + - uses: 'actions/checkout@v4' with: fetch-depth: 0 # https://github.com/actions/checkout/issues/290 @@ -93,9 +91,9 @@ jobs: run: | git fetch -f origin ${{ github.ref }}:${{ github.ref }} git checkout ${{ github.ref }} - - uses: 'actions/setup-node@v3' + - uses: 'actions/setup-node@v4' with: - node-version: '18.19.0' + node-version: '22.11.0' - uses: 'actions/setup-python@v4' with: python-version: ${{ matrix.python }} @@ -128,13 +126,67 @@ jobs: files: ./api/coverage.xml flags: api + test-package: + name: 'installed package tests on ${{ matrix.os }}' + timeout-minutes: 5 + strategy: + matrix: + os: ['ubuntu-22.04', 'macos-latest', 'windows-2022'] + runs-on: '${{ matrix.os }}' + steps: + - uses: 'actions/checkout@v4' + - name: 'Fix actions/checkout odd handling of tags' + if: startsWith(github.ref, 'refs/tags') + run: | + git fetch -f origin ${{ github.ref }}:${{ github.ref }} + git checkout ${{ github.ref }} + - uses: 'actions/setup-python@v4' + with: + python-version: '3.10' + - name: Set up package-testing + id: setup + if: ${{ matrix.os != 'windows-2022' }} + working-directory: package-testing + shell: bash + run: make setup + - name: Set up package-testing (Windows) + id: setup-windows + if: ${{ matrix.os == 'windows-2022' }} + working-directory: package-testing + shell: pwsh + run: make setup-windows + - name: Run the tests + if: ${{ matrix.os != 'windows-2022' }} + shell: bash + id: test + working-directory: package-testing + run: make test + - name: Run the tests (Windows) + shell: pwsh + id: test-windows + working-directory: package-testing + run: make test-windows + - name: Save the test results + if: ${{ always() && steps.setup.outcome == 'success' || steps.setup-windows.outcome == 'success' }} + id: results + uses: actions/upload-artifact@v4 + with: + name: package-test-results-${{ matrix.os }} + path: package-testing/results + - name: Set job summary + if: ${{ always() }} + run: | + echo "## Opentrons Package Test Results ${{matrix.os}}" >> $GITHUB_STEP_SUMMARY + echo "### Test Outcome: Unixy ${{ steps.test.outcome }} Windows: ${{ steps.test-windows.outcome }}" >> $GITHUB_STEP_SUMMARY + echo "[Download the test results artifact](${{steps.results.outputs.artifact-url}})" >> $GITHUB_STEP_SUMMARY + deploy: name: 'deploy opentrons package' needs: [test] runs-on: 'ubuntu-22.04' if: github.event_name == 'push' steps: - - uses: 'actions/checkout@v3' + - uses: 'actions/checkout@v4' with: fetch-depth: 0 # https://github.com/actions/checkout/issues/290 @@ -143,9 +195,9 @@ jobs: run: | git fetch -f origin ${{ github.ref }}:${{ github.ref }} git checkout ${{ github.ref }} - - uses: 'actions/setup-node@v3' + - uses: 'actions/setup-node@v4' with: - node-version: '18.19.0' + node-version: '22.11.0' - uses: 'actions/setup-python@v4' with: python-version: '3.10' diff --git a/.github/workflows/app-test-build-deploy.yaml b/.github/workflows/app-test-build-deploy.yaml index 0b5c1cbe35f..873bfe65c07 100644 --- a/.github/workflows/app-test-build-deploy.yaml +++ b/.github/workflows/app-test-build-deploy.yaml @@ -56,10 +56,10 @@ jobs: name: 'opentrons app frontend unit tests' timeout-minutes: 60 steps: - - uses: 'actions/checkout@v3' - - uses: 'actions/setup-node@v3' + - uses: 'actions/checkout@v4' + - uses: 'actions/setup-node@v4' with: - node-version: '18.19.0' + node-version: '22.11.0' - name: 'install udev' run: | # WORKAROUND: Remove microsoft debian repo due to https://github.com/microsoft/linux-package-repositories/issues/130. Remove line below after it is resolved @@ -105,7 +105,7 @@ jobs: timeout-minutes: 60 runs-on: ${{ matrix.os }} steps: - - uses: 'actions/checkout@v3' + - uses: 'actions/checkout@v4' with: fetch-depth: 0 # https://github.com/actions/checkout/issues/290 @@ -114,9 +114,9 @@ jobs: run: | git fetch -f origin ${{ github.ref }}:${{ github.ref }} git checkout ${{ github.ref }} - - uses: 'actions/setup-node@v3' + - uses: 'actions/setup-node@v4' with: - node-version: '18.19.0' + node-version: '22.11.0' - uses: actions/setup-python@v4 with: python-version: '3.10' @@ -261,7 +261,7 @@ jobs: echo "bucket=${{env._APP_DEPLOY_BUCKET_OT3}}" >> $GITHUB_OUTPUT echo "folder=${{env._APP_DEPLOY_FOLDER_OT3}}" >> $GITHUB_OUTPUT fi - - uses: 'actions/checkout@v3' + - uses: 'actions/checkout@v4' with: fetch-depth: 0 # https://github.com/actions/checkout/issues/290 @@ -270,9 +270,9 @@ jobs: run: | git fetch -f origin ${{ github.ref }}:${{ github.ref }} git checkout ${{ github.ref }} - - uses: 'actions/setup-node@v3' + - uses: 'actions/setup-node@v4' with: - node-version: '18.19.0' + node-version: '22.11.0' - uses: actions/setup-python@v4 with: python-version: '3.10' @@ -370,7 +370,7 @@ jobs: - name: 'upload github artifact' if: matrix.target == 'desktop' - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: 'opentrons-${{matrix.variant}}-${{ matrix.os }}' path: app-shell/dist/publish @@ -392,7 +392,7 @@ jobs: if: contains(fromJSON(needs.determine-build-type.outputs.variants), 'release') || contains(fromJSON(needs.determine-build-type.outputs.variants), 'internal-release') steps: - name: 'download run app builds' - uses: 'actions/download-artifact@v3' + uses: 'actions/download-artifact@v4' with: path: ./artifacts - name: 'separate release and internal-release artifacts' @@ -488,7 +488,7 @@ jobs: _ACCESS_URL: https://${{env._APP_DEPLOY_BUCKET_ROBOTSTACK}}/${{env._APP_DEPLOY_FOLDER_ROBOTSTACK}} - name: 'pull repo for scripts' - uses: 'actions/checkout@v3' + uses: 'actions/checkout@v4' with: path: ./monorepo # https://github.com/actions/checkout/issues/290 @@ -498,9 +498,9 @@ jobs: cd ./monorepo git fetch -f origin ${{ github.ref }}:${{ github.ref }} git checkout ${{ github.ref }} - - uses: 'actions/setup-node@v3' + - uses: 'actions/setup-node@v4' with: - node-version: '18.19.0' + node-version: '22.11.0' - name: 'install udev' run: | # WORKAROUND: Remove microsoft debian repo due to https://github.com/microsoft/linux-package-repositories/issues/130. Remove line below after it is resolved diff --git a/.github/workflows/components-test-build-deploy.yaml b/.github/workflows/components-test-build-deploy.yaml index 01d4355e355..2b10617c283 100644 --- a/.github/workflows/components-test-build-deploy.yaml +++ b/.github/workflows/components-test-build-deploy.yaml @@ -43,10 +43,10 @@ jobs: timeout-minutes: 30 runs-on: 'ubuntu-22.04' steps: - - uses: 'actions/checkout@v3' - - uses: 'actions/setup-node@v3' + - uses: 'actions/checkout@v4' + - uses: 'actions/setup-node@v4' with: - node-version: '18.19.0' + node-version: '22.11.0' - name: 'install udev for usb-detection' run: | # WORKAROUND: Remove microsoft debian repo due to https://github.com/microsoft/linux-package-repositories/issues/130. Remove line below after it is resolved @@ -79,10 +79,10 @@ jobs: if: github.event_name != 'pull_request' needs: ['js-unit-test'] steps: - - uses: 'actions/checkout@v3' - - uses: 'actions/setup-node@v3' + - uses: 'actions/checkout@v4' + - uses: 'actions/setup-node@v4' with: - node-version: '18.19.0' + node-version: '22.11.0' - name: 'install udev for usb-detection' run: | # WORKAROUND: Remove microsoft debian repo due to https://github.com/microsoft/linux-package-repositories/issues/130. Remove line below after it is resolved @@ -105,7 +105,7 @@ jobs: - name: 'build components' run: make -C components - name: 'upload github artifact' - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: 'components-artifact' path: storybook-static @@ -140,16 +140,16 @@ jobs: ['js-unit-test', 'build-components-storybook', 'determine-build-type'] if: needs.determine-build-type.outputs.type != 'none' steps: - - uses: 'actions/checkout@v3' + - uses: 'actions/checkout@v4' # https://github.com/actions/checkout/issues/290 - name: 'Fix actions/checkout odd handling of tags' if: startsWith(github.ref, 'refs/tags') run: | git fetch -f origin ${{ github.ref }}:${{ github.ref }} git checkout ${{ github.ref }} - - uses: 'actions/setup-node@v3' + - uses: 'actions/setup-node@v4' with: - node-version: '18.19.0' + node-version: '22.11.0' - name: 'set complex environment variables' id: 'set-vars' uses: actions/github-script@v6 @@ -158,7 +158,7 @@ jobs: const { buildComplexEnvVars } = require(`${process.env.GITHUB_WORKSPACE}/.github/workflows/utils.js`) buildComplexEnvVars(core, context) - name: 'download components build' - uses: 'actions/download-artifact@v3' + uses: 'actions/download-artifact@v4' with: name: components-artifact path: ./dist @@ -180,16 +180,16 @@ jobs: needs: ['js-unit-test', 'determine-build-type'] if: needs.determine-build-type.outputs.type == 'publish' steps: - - uses: 'actions/checkout@v3' + - uses: 'actions/checkout@v4' # https://github.com/actions/checkout/issues/290 - name: 'Fix actions/checkout odd handling of tags' if: startsWith(github.ref, 'refs/tags') run: | git fetch -f origin ${{ github.ref }}:${{ github.ref }} git checkout ${{ github.ref }} - - uses: 'actions/setup-node@v3' + - uses: 'actions/setup-node@v4' with: - node-version: '18.19.0' + node-version: '22.11.0' registry-url: 'https://registry.npmjs.org' - name: 'install udev for usb-detection' run: | @@ -203,9 +203,6 @@ jobs: make setup-js - name: 'build typescript types' run: make -C components build-ts - - name: 'build js bundle' - run: | - make -C components lib # replace package.json stub version number with version from tag - name: 'set version number' run: | @@ -213,12 +210,15 @@ jobs: VERSION_STRING=$(echo ${{ github.ref }} | sed 's/refs\/tags\/components@//') json -I -f ./components/package.json -e "this.version=\"$VERSION_STRING\"" json -I -f ./components/package.json -e "this.dependencies['@opentrons/shared-data']=\"$VERSION_STRING\"" - - uses: 'actions/setup-node@v3' + - uses: 'actions/setup-node@v4' with: - node-version: '18.19.0' + node-version: '22.11.0' registry-url: 'https://registry.npmjs.org' - name: 'publish to npm registry' env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} run: | - cd ./components && echo "//registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN}" > ./.npmrc && npm publish --access public + cd ./components + echo "//registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN}" > ./.npmrc + ls -R # Debug: View contents of ./components + npm publish --access public diff --git a/.github/workflows/docs-build.yaml b/.github/workflows/docs-build.yaml index 08b1c2b76cf..6a4f49f0d20 100644 --- a/.github/workflows/docs-build.yaml +++ b/.github/workflows/docs-build.yaml @@ -40,7 +40,7 @@ jobs: name: opentrons documentation build runs-on: 'ubuntu-22.04' steps: - - uses: 'actions/checkout@v3' + - uses: 'actions/checkout@v4' with: fetch-depth: 0 # https://github.com/actions/checkout/issues/290 @@ -49,9 +49,9 @@ jobs: run: | git fetch -f origin ${{ github.ref }}:${{ github.ref }} git checkout ${{ github.ref }} - - uses: 'actions/setup-node@v3' + - uses: 'actions/setup-node@v4' with: - node-version: '18.19.0' + node-version: '22.11.0' - uses: 'actions/setup-python@v3' with: python-version: '3.10' diff --git a/.github/workflows/g-code-confirm-tests.yaml b/.github/workflows/g-code-confirm-tests.yaml index 146fa96b9a2..9c43bfe16d8 100644 --- a/.github/workflows/g-code-confirm-tests.yaml +++ b/.github/workflows/g-code-confirm-tests.yaml @@ -1,4 +1,4 @@ -name: "G-Code-Confirm" +name: 'G-Code-Confirm' on: # Run on any change to the api directory @@ -31,20 +31,14 @@ jobs: confirm-g-code: strategy: matrix: - command: [ - '2-modules', - 'swift-smoke', - 'swift-turbo', - 'omega', - 'fast' - ] + command: ['2-modules', 'swift-smoke', 'swift-turbo', 'omega', 'fast'] name: 'Confirm G-Code (${{ matrix.command }})' runs-on: 'ubuntu-22.04' steps: - - uses: 'actions/checkout@v3' + - uses: 'actions/checkout@v4' with: fetch-depth: 0 - - uses: 'actions/setup-node@v3' + - uses: 'actions/setup-node@v4' with: node-version: '12' - uses: 'actions/setup-python@v3' @@ -54,7 +48,7 @@ jobs: with: project: 'g-code-testing' - - name: "Verify no missing comparison files" + - name: 'Verify no missing comparison files' run: make -C g-code-testing check-for-missing-comparison-files - name: 'Run & Compare to comparison files' diff --git a/.github/workflows/g-code-testing-lint-test.yaml b/.github/workflows/g-code-testing-lint-test.yaml index e174bc7ac52..9da53b78182 100644 --- a/.github/workflows/g-code-testing-lint-test.yaml +++ b/.github/workflows/g-code-testing-lint-test.yaml @@ -42,7 +42,7 @@ jobs: name: 'g-code-testing package linting and tests' runs-on: 'ubuntu-22.04' steps: - - uses: 'actions/checkout@v3' + - uses: 'actions/checkout@v4' with: fetch-depth: 0 - name: 'install udev' @@ -50,9 +50,9 @@ jobs: # WORKAROUND: Remove microsoft debian repo due to https://github.com/microsoft/linux-package-repositories/issues/130. Remove line below after it is resolved sudo rm -f /etc/apt/sources.list.d/microsoft-prod.list sudo apt-get update && sudo apt-get install libudev-dev - - uses: 'actions/setup-node@v3' + - uses: 'actions/setup-node@v4' with: - node-version: '18.19.0' + node-version: '22.11.0' - name: 'set complex environment variables' id: 'set-vars' uses: actions/github-script@v6 diff --git a/.github/workflows/hardware-lint-test.yaml b/.github/workflows/hardware-lint-test.yaml index f5e701ea883..8714a40d250 100644 --- a/.github/workflows/hardware-lint-test.yaml +++ b/.github/workflows/hardware-lint-test.yaml @@ -44,11 +44,11 @@ jobs: runs-on: 'ubuntu-20.04' steps: - name: Checkout opentrons repo - uses: 'actions/checkout@v3' + uses: 'actions/checkout@v4' with: fetch-depth: 0 - name: Setup Node - uses: 'actions/setup-node@v3' + uses: 'actions/setup-node@v4' with: node-version: '12' diff --git a/.github/workflows/hardware-testing-protocols.yaml b/.github/workflows/hardware-testing-protocols.yaml index ee59d2dc25c..1573c69380a 100644 --- a/.github/workflows/hardware-testing-protocols.yaml +++ b/.github/workflows/hardware-testing-protocols.yaml @@ -38,12 +38,12 @@ jobs: runs-on: 'ubuntu-20.04' steps: - name: Checkout opentrons repo - uses: 'actions/checkout@v3' + uses: 'actions/checkout@v4' with: fetch-depth: 0 - name: Setup Node - uses: 'actions/setup-node@v3' + uses: 'actions/setup-node@v4' with: node-version: '12' diff --git a/.github/workflows/hardware-testing.yaml b/.github/workflows/hardware-testing.yaml index bb738b13e4b..116ca872c85 100644 --- a/.github/workflows/hardware-testing.yaml +++ b/.github/workflows/hardware-testing.yaml @@ -43,12 +43,12 @@ jobs: runs-on: 'ubuntu-20.04' steps: - name: Checkout opentrons repo - uses: 'actions/checkout@v3' + uses: 'actions/checkout@v4' with: fetch-depth: 0 - name: Setup Node - uses: 'actions/setup-node@v3' + uses: 'actions/setup-node@v4' with: node-version: '12' diff --git a/.github/workflows/http-docs-build.yaml b/.github/workflows/http-docs-build.yaml index 6294eeb2172..f2c21368d5e 100644 --- a/.github/workflows/http-docs-build.yaml +++ b/.github/workflows/http-docs-build.yaml @@ -40,7 +40,7 @@ jobs: name: HTTP API reference build runs-on: 'ubuntu-22.04' steps: - - uses: 'actions/checkout@v3' + - uses: 'actions/checkout@v4' with: fetch-depth: 0 # https://github.com/actions/checkout/issues/290 @@ -52,9 +52,9 @@ jobs: - uses: 'actions/setup-python@v3' with: python-version: '3.10' - - uses: 'actions/setup-node@v3' + - uses: 'actions/setup-node@v4' with: - node-version: '18.19.0' + node-version: '22.11.0' - uses: './.github/actions/python/setup' with: project: 'robot-server' diff --git a/.github/workflows/js-check.yaml b/.github/workflows/js-check.yaml index 139d3b618ad..807d4a2570c 100644 --- a/.github/workflows/js-check.yaml +++ b/.github/workflows/js-check.yaml @@ -42,10 +42,10 @@ jobs: runs-on: 'ubuntu-22.04' timeout-minutes: 20 steps: - - uses: 'actions/checkout@v3' - - uses: 'actions/setup-node@v3' + - uses: 'actions/checkout@v4' + - uses: 'actions/setup-node@v4' with: - node-version: '18.19.0' + node-version: '22.11.0' - name: 'set complex environment variables' id: 'set-vars' uses: actions/github-script@v6 diff --git a/.github/workflows/ll-test-build-deploy.yaml b/.github/workflows/ll-test-build-deploy.yaml index 140537593e2..35cbc96eced 100644 --- a/.github/workflows/ll-test-build-deploy.yaml +++ b/.github/workflows/ll-test-build-deploy.yaml @@ -40,12 +40,12 @@ jobs: js-unit-test: name: 'labware library unit tests' timeout-minutes: 20 - runs-on: 'ubuntu-20.04' + runs-on: 'ubuntu-22.04' steps: - - uses: 'actions/checkout@v3' - - uses: 'actions/setup-node@v3' + - uses: 'actions/checkout@v4' + - uses: 'actions/setup-node@v4' with: - node-version: '18.19.0' + node-version: '22.11.0' # https://github.com/actions/checkout/issues/290 - name: 'Fix actions/checkout odd handling of tags' if: startsWith(github.ref, 'refs/tags') @@ -83,18 +83,18 @@ jobs: name: 'labware library e2e tests' needs: ['js-unit-test'] timeout-minutes: 30 - runs-on: 'ubuntu-20.04' + runs-on: 'ubuntu-22.04' steps: - - uses: 'actions/checkout@v3' + - uses: 'actions/checkout@v4' # https://github.com/actions/checkout/issues/290 - name: 'Fix actions/checkout odd handling of tags' if: startsWith(github.ref, 'refs/tags') run: | git fetch -f origin ${{ github.ref }}:${{ github.ref }} git checkout ${{ github.ref }} - - uses: 'actions/setup-node@v3' + - uses: 'actions/setup-node@v4' with: - node-version: '18.19.0' + node-version: '22.11.0' - name: 'install libudev for usb-detection' run: | # WORKAROUND: Remove microsoft debian repo due to https://github.com/microsoft/linux-package-repositories/issues/130. Remove line below after it is resolved @@ -123,10 +123,10 @@ jobs: name: 'build labware library artifact' needs: ['js-unit-test'] timeout-minutes: 30 - runs-on: 'ubuntu-20.04' + runs-on: 'ubuntu-22.04' if: github.event_name != 'pull_request' steps: - - uses: 'actions/checkout@v3' + - uses: 'actions/checkout@v4' with: fetch-depth: 0 # https://github.com/actions/checkout/issues/290 @@ -135,9 +135,9 @@ jobs: run: | git fetch -f origin ${{ github.ref }}:${{ github.ref }} git checkout ${{ github.ref }} - - uses: 'actions/setup-node@v3' + - uses: 'actions/setup-node@v4' with: - node-version: '18.19.0' + node-version: '22.11.0' - name: 'install libudev for usb-detection' run: | # WORKAROUND: Remove microsoft debian repo due to https://github.com/microsoft/linux-package-repositories/issues/130. Remove line below after it is resolved @@ -164,26 +164,26 @@ jobs: run: | make -C labware-library - name: 'upload github artifact' - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: 'll-artifact' path: labware-library/dist deploy-ll: name: 'deploy LL artifact to S3' - runs-on: 'ubuntu-20.04' + runs-on: 'ubuntu-22.04' needs: ['js-unit-test', 'e2e-test', 'build-ll'] if: github.event_name != 'pull_request' steps: - - uses: 'actions/checkout@v3' + - uses: 'actions/checkout@v4' # https://github.com/actions/checkout/issues/290 - name: 'Fix actions/checkout odd handling of tags' if: startsWith(github.ref, 'refs/tags') run: | git fetch -f origin ${{ github.ref }}:${{ github.ref }} git checkout ${{ github.ref }} - - uses: 'actions/setup-node@v3' + - uses: 'actions/setup-node@v4' with: - node-version: '18.19.0' + node-version: '22.11.0' - name: 'install udev for usb-detection' run: | # WORKAROUND: Remove microsoft debian repo due to https://github.com/microsoft/linux-package-repositories/issues/130. Remove line below after it is resolved @@ -197,7 +197,7 @@ jobs: const { buildComplexEnvVars } = require(`${process.env.GITHUB_WORKSPACE}/.github/workflows/utils.js`) buildComplexEnvVars(core, context) - name: 'download LL build' - uses: 'actions/download-artifact@v3' + uses: 'actions/download-artifact@v4' with: name: ll-artifact path: ./dist diff --git a/.github/workflows/odd-memory-usage-test.yaml b/.github/workflows/odd-memory-usage-test.yaml new file mode 100644 index 00000000000..15b78fc79a3 --- /dev/null +++ b/.github/workflows/odd-memory-usage-test.yaml @@ -0,0 +1,24 @@ +name: 'ODD Memory Usage Test' + +on: + schedule: + - cron: '30 12 * * *' + workflow_dispatch: + +jobs: + analyze-memory: + name: 'ODD Memory Usage Test' + runs-on: 'ubuntu-latest' + timeout-minutes: 15 + + steps: + - name: Checkout repository + uses: 'actions/checkout@v4' + + - name: Run memory analysis + uses: ./.github/actions/odd-resource-analysis + with: + mixpanel-user: ${{ secrets.MIXPANEL_INGEST_USER }} + mixpanel-secret: ${{ secrets.MIXPANEL_INGEST_SECRET }} + mixpanel-project-id: ${{ secrets.OT_MIXPANEL_PROJECT_ID }} + previous-version-count: '2' \ No newline at end of file diff --git a/.github/workflows/opentrons-ai-client-staging-continuous-deploy.yaml b/.github/workflows/opentrons-ai-client-staging-continuous-deploy.yaml index 7a89bfa02dd..88c7c70d3ec 100644 --- a/.github/workflows/opentrons-ai-client-staging-continuous-deploy.yaml +++ b/.github/workflows/opentrons-ai-client-staging-continuous-deploy.yaml @@ -23,10 +23,10 @@ jobs: name: 'OpentronsAI client edge continuous deployment to staging' timeout-minutes: 10 steps: - - uses: 'actions/checkout@v3' - - uses: 'actions/setup-node@v3' + - uses: 'actions/checkout@v4' + - uses: 'actions/setup-node@v4' with: - node-version: '18.19.0' + node-version: '22.11.0' - name: 'install udev' run: | # WORKAROUND: Remove microsoft debian repo due to https://github.com/microsoft/linux-package-repositories/issues/130. Remove line below after it is resolved diff --git a/.github/workflows/opentrons-ai-client-test.yaml b/.github/workflows/opentrons-ai-client-test.yaml index 2c5cc6cfc64..0a78cf73da3 100644 --- a/.github/workflows/opentrons-ai-client-test.yaml +++ b/.github/workflows/opentrons-ai-client-test.yaml @@ -39,10 +39,10 @@ jobs: name: 'opentrons ai frontend unit tests' timeout-minutes: 60 steps: - - uses: 'actions/checkout@v3' - - uses: 'actions/setup-node@v3' + - uses: 'actions/checkout@v4' + - uses: 'actions/setup-node@v4' with: - node-version: '18.19.0' + node-version: '22.11.0' - name: 'install udev' run: | # WORKAROUND: Remove microsoft debian repo due to https://github.com/microsoft/linux-package-repositories/issues/130. Remove line below after it is resolved diff --git a/.github/workflows/opentrons-ai-production-deploy.yaml b/.github/workflows/opentrons-ai-production-deploy.yaml index 2327b48ecad..1850400bbd0 100644 --- a/.github/workflows/opentrons-ai-production-deploy.yaml +++ b/.github/workflows/opentrons-ai-production-deploy.yaml @@ -23,10 +23,10 @@ jobs: name: 'OpentronsAI client prod deploy' timeout-minutes: 10 steps: - - uses: 'actions/checkout@v3' - - uses: 'actions/setup-node@v3' + - uses: 'actions/checkout@v4' + - uses: 'actions/setup-node@v4' with: - node-version: '18.19.0' + node-version: '22.11.0' - name: 'install udev' run: | # WORKAROUND: Remove microsoft debian repo due to https://github.com/microsoft/linux-package-repositories/issues/130. Remove line below after it is resolved diff --git a/.github/workflows/pd-test-build-deploy.yaml b/.github/workflows/pd-test-build-deploy.yaml index 306a475aacc..60a52ec38fd 100644 --- a/.github/workflows/pd-test-build-deploy.yaml +++ b/.github/workflows/pd-test-build-deploy.yaml @@ -42,23 +42,23 @@ jobs: runs-on: 'ubuntu-22.04' timeout-minutes: 30 steps: - - uses: 'actions/checkout@v3' + - uses: 'actions/checkout@v4' # https://github.com/actions/checkout/issues/290 - name: 'Fix actions/checkout odd handling of tags' if: startsWith(github.ref, 'refs/tags') run: | git fetch -f origin ${{ github.ref }}:${{ github.ref }} git checkout ${{ github.ref }} - - uses: 'actions/setup-node@v3' + - uses: 'actions/setup-node@v4' with: - node-version: '18.19.0' + node-version: '22.11.0' - name: 'install udev for usb-detection' run: | # WORKAROUND: Remove microsoft debian repo due to https://github.com/microsoft/linux-package-repositories/issues/130. Remove line below after it is resolved sudo rm -f /etc/apt/sources.list.d/microsoft-prod.list sudo apt-get update && sudo apt-get install libudev-dev - name: 'cache yarn cache' - uses: actions/cache@v2 + uses: actions/cache@v3 with: path: | ${{ github.workspace }}/.yarn-cache @@ -88,7 +88,7 @@ jobs: os: ['ubuntu-22.04'] runs-on: '${{ matrix.os }}' steps: - - uses: 'actions/checkout@v3' + - uses: 'actions/checkout@v4' with: fetch-depth: 0 # https://github.com/actions/checkout/issues/290 @@ -97,9 +97,9 @@ jobs: run: | git fetch -f origin ${{ github.ref }}:${{ github.ref }} git checkout ${{ github.ref }} - - uses: 'actions/setup-node@v3' + - uses: 'actions/setup-node@v4' with: - node-version: '18.19.0' + node-version: '22.11.0' - name: 'install udev for usb-detection' if: startsWith(matrix.os, 'ubuntu') run: | @@ -128,7 +128,7 @@ jobs: runs-on: 'ubuntu-22.04' if: github.event_name != 'pull_request' steps: - - uses: 'actions/checkout@v3' + - uses: 'actions/checkout@v4' with: fetch-depth: 0 # https://github.com/actions/checkout/issues/290 @@ -137,9 +137,9 @@ jobs: run: | git fetch -f origin ${{ github.ref }}:${{ github.ref }} git checkout ${{ github.ref }} - - uses: 'actions/setup-node@v3' + - uses: 'actions/setup-node@v4' with: - node-version: '18.19.0' + node-version: '22.11.0' - name: 'install udev for usb-detection' run: | # WORKAROUND: Remove microsoft debian repo due to https://github.com/microsoft/linux-package-repositories/issues/130. Remove line below after it is resolved @@ -164,7 +164,7 @@ jobs: run: | make -C protocol-designer NODE_ENV=development - name: 'upload github artifact' - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: 'pd-artifact' path: protocol-designer/dist @@ -174,16 +174,16 @@ jobs: needs: ['js-unit-test', 'build-pd'] if: github.event_name != 'pull_request' steps: - - uses: 'actions/checkout@v3' + - uses: 'actions/checkout@v4' # https://github.com/actions/checkout/issues/290 - name: 'Fix actions/checkout odd handling of tags' if: startsWith(github.ref, 'refs/tags') run: | git fetch -f origin ${{ github.ref }}:${{ github.ref }} git checkout ${{ github.ref }} - - uses: 'actions/setup-node@v3' + - uses: 'actions/setup-node@v4' with: - node-version: '18.19.0' + node-version: '22.11.0' - name: 'install udev for usb-detection' run: | # WORKAROUND: Remove microsoft debian repo due to https://github.com/microsoft/linux-package-repositories/issues/130. Remove line below after it is resolved @@ -197,7 +197,7 @@ jobs: const { buildComplexEnvVars } = require(`${process.env.GITHUB_WORKSPACE}/.github/workflows/utils.js`) buildComplexEnvVars(core, context) - name: 'download PD build' - uses: 'actions/download-artifact@v3' + uses: 'actions/download-artifact@v4' with: name: pd-artifact path: ./dist diff --git a/.github/workflows/react-api-client-test.yaml b/.github/workflows/react-api-client-test.yaml index 2bccadae770..8a8759f12e1 100644 --- a/.github/workflows/react-api-client-test.yaml +++ b/.github/workflows/react-api-client-test.yaml @@ -36,10 +36,10 @@ jobs: timeout-minutes: 30 runs-on: 'ubuntu-22.04' steps: - - uses: 'actions/checkout@v3' - - uses: 'actions/setup-node@v3' + - uses: 'actions/checkout@v4' + - uses: 'actions/setup-node@v4' with: - node-version: '18.19.0' + node-version: '22.11.0' - name: 'install libudev for usb-detection' run: | # WORKAROUND: Remove microsoft debian repo due to https://github.com/microsoft/linux-package-repositories/issues/130. Remove line below after it is resolved diff --git a/.github/workflows/robot-server-lint-test.yaml b/.github/workflows/robot-server-lint-test.yaml index 96d1969121b..6735ab36136 100644 --- a/.github/workflows/robot-server-lint-test.yaml +++ b/.github/workflows/robot-server-lint-test.yaml @@ -56,12 +56,12 @@ jobs: matrix: with-ot-hardware: ['true', 'false'] steps: - - uses: 'actions/checkout@v3' + - uses: 'actions/checkout@v4' with: fetch-depth: 0 - - uses: 'actions/setup-node@v3' + - uses: 'actions/setup-node@v4' with: - node-version: '18.19.0' + node-version: '22.11.0' - uses: 'actions/setup-python@v4' with: python-version: '3.10' diff --git a/.github/workflows/server-utils-lint-test.yaml b/.github/workflows/server-utils-lint-test.yaml index 240d9e0bd25..9c8f37b6c79 100644 --- a/.github/workflows/server-utils-lint-test.yaml +++ b/.github/workflows/server-utils-lint-test.yaml @@ -41,12 +41,12 @@ jobs: timeout-minutes: 10 runs-on: 'ubuntu-22.04' steps: - - uses: 'actions/checkout@v3' + - uses: 'actions/checkout@v4' with: fetch-depth: 0 - - uses: 'actions/setup-node@v3' + - uses: 'actions/setup-node@v4' with: - node-version: '18.19.0' + node-version: '22.11.0' - uses: 'actions/setup-python@v4' with: python-version: '3.10' @@ -62,12 +62,12 @@ jobs: needs: [lint] runs-on: 'ubuntu-22.04' steps: - - uses: 'actions/checkout@v3' + - uses: 'actions/checkout@v4' with: fetch-depth: 0 - - uses: 'actions/setup-node@v3' + - uses: 'actions/setup-node@v4' with: - node-version: '18.19.0' + node-version: '22.11.0' - uses: 'actions/setup-python@v4' with: python-version: '3.10' diff --git a/.github/workflows/shared-data-test-lint-deploy.yaml b/.github/workflows/shared-data-test-lint-deploy.yaml index 39cc4cd30e4..c5858d5da0e 100644 --- a/.github/workflows/shared-data-test-lint-deploy.yaml +++ b/.github/workflows/shared-data-test-lint-deploy.yaml @@ -46,12 +46,12 @@ jobs: timeout-minutes: 10 runs-on: 'ubuntu-22.04' steps: - - uses: 'actions/checkout@v3' + - uses: 'actions/checkout@v4' with: fetch-depth: 0 - - uses: 'actions/setup-node@v3' + - uses: 'actions/setup-node@v4' with: - node-version: '18.19.0' + node-version: '22.11.0' - uses: 'actions/setup-python@v3' with: python-version: '3.10' @@ -75,7 +75,7 @@ jobs: runs-on: '${{ matrix.os }}' steps: - - uses: 'actions/checkout@v3' + - uses: 'actions/checkout@v4' with: fetch-depth: 0 - name: 'install udev for usb-detection' @@ -86,7 +86,7 @@ jobs: sudo apt-get update && sudo apt-get install libudev-dev - uses: 'actions/setup-node@v1' with: - node-version: '18.19.0' + node-version: '22.11.0' - uses: 'actions/setup-python@v4' with: python-version: ${{ matrix.python }} @@ -115,10 +115,10 @@ jobs: runs-on: 'ubuntu-22.04' timeout-minutes: 30 steps: - - uses: 'actions/checkout@v3' - - uses: 'actions/setup-node@v3' + - uses: 'actions/checkout@v4' + - uses: 'actions/setup-node@v4' with: - node-version: '18.19.0' + node-version: '22.11.0' - name: 'install udev' run: | # WORKAROUND: Remove microsoft debian repo due to https://github.com/microsoft/linux-package-repositories/issues/130. Remove line below after it is resolved @@ -152,7 +152,7 @@ jobs: runs-on: 'ubuntu-22.04' if: github.event_name == 'push' steps: - - uses: 'actions/checkout@v3' + - uses: 'actions/checkout@v4' with: fetch-depth: 0 # https://github.com/actions/checkout/issues/290 @@ -161,9 +161,9 @@ jobs: run: | git fetch -f origin ${{ github.ref }}:${{ github.ref }} git checkout ${{ github.ref }} - - uses: 'actions/setup-node@v3' + - uses: 'actions/setup-node@v4' with: - node-version: '18.19.0' + node-version: '22.11.0' - name: 'install udev for usb-detection' run: | # WORKAROUND: Remove microsoft debian repo due to https://github.com/microsoft/linux-package-repositories/issues/130. Remove line below after it is resolved @@ -223,16 +223,16 @@ jobs: needs: ['js-test', 'publish-switch'] if: needs.publish-switch.outputs.should_publish == 'true' steps: - - uses: 'actions/checkout@v3' + - uses: 'actions/checkout@v4' # https://github.com/actions/checkout/issues/290 - name: 'Fix actions/checkout odd handling of tags' if: startsWith(github.ref, 'refs/tags') run: | git fetch -f origin ${{ github.ref }}:${{ github.ref }} git checkout ${{ github.ref }} - - uses: 'actions/setup-node@v3' + - uses: 'actions/setup-node@v4' with: - node-version: '18.19.0' + node-version: '22.11.0' registry-url: 'https://registry.npmjs.org' - name: 'install udev for usb-detection' run: | @@ -252,7 +252,7 @@ jobs: run: | npm config set cache ./.npm-cache yarn config set cache-folder ./.yarn-cache - make setup-js + make setup-js - name: 'build typescript' run: make build-ts - name: 'build library' @@ -265,9 +265,9 @@ jobs: VERSION_STRING=$(echo ${{ github.ref }} | sed -E 's/refs\/tags\/(components|shared-data)@//') json -I -f ./shared-data/package.json -e "this.version=\"$VERSION_STRING\"" cd ./shared-data - - uses: 'actions/setup-node@v3' + - uses: 'actions/setup-node@v4' with: - node-version: '18.19.0' + node-version: '22.11.0' registry-url: 'https://registry.npmjs.org' - name: 'publish to npm registry' env: diff --git a/.github/workflows/step-generation-test.yaml b/.github/workflows/step-generation-test.yaml index 7ac65f3997e..ac435cf999d 100644 --- a/.github/workflows/step-generation-test.yaml +++ b/.github/workflows/step-generation-test.yaml @@ -35,10 +35,10 @@ jobs: runs-on: 'ubuntu-22.04' timeout-minutes: 30 steps: - - uses: 'actions/checkout@v3' - - uses: 'actions/setup-node@v3' + - uses: 'actions/checkout@v4' + - uses: 'actions/setup-node@v4' with: - node-version: '18.19.0' + node-version: '22.11.0' - name: 'install udev for usb-detection' run: | # WORKAROUND: Remove microsoft debian repo due to https://github.com/microsoft/linux-package-repositories/issues/130. Remove line below after it is resolved diff --git a/.github/workflows/system-server-lint-test.yaml b/.github/workflows/system-server-lint-test.yaml index 720ca905bd7..ffd526c6834 100644 --- a/.github/workflows/system-server-lint-test.yaml +++ b/.github/workflows/system-server-lint-test.yaml @@ -43,12 +43,12 @@ jobs: timeout-minutes: 10 runs-on: 'ubuntu-22.04' steps: - - uses: 'actions/checkout@v3' + - uses: 'actions/checkout@v4' with: fetch-depth: 0 - - uses: 'actions/setup-node@v3' + - uses: 'actions/setup-node@v4' with: - node-version: '18.19.0' + node-version: '22.11.0' - uses: 'actions/setup-python@v4' with: python-version: '3.10' @@ -64,12 +64,12 @@ jobs: needs: [lint] runs-on: 'ubuntu-22.04' steps: - - uses: 'actions/checkout@v3' + - uses: 'actions/checkout@v4' with: fetch-depth: 0 - - uses: 'actions/setup-node@v3' + - uses: 'actions/setup-node@v4' with: - node-version: '18.19.0' + node-version: '22.11.0' - uses: 'actions/setup-python@v4' with: python-version: '3.10' diff --git a/.github/workflows/tag-releases.yaml b/.github/workflows/tag-releases.yaml index 864f1e45b36..1f8c3ee153a 100644 --- a/.github/workflows/tag-releases.yaml +++ b/.github/workflows/tag-releases.yaml @@ -19,12 +19,12 @@ jobs: # git fetch origin ${{ github.ref_name }}:${{ github.ref_name }} # git checkout ${{ github.ref_name }} # This would pull history for only the tag in question. - - uses: 'actions/checkout@v3' + - uses: 'actions/checkout@v4' with: fetch-depth: 0 - - uses: 'actions/setup-node@v3' + - uses: 'actions/setup-node@v4' with: - node-version: '18.19.0' + node-version: '22.11.0' - name: 'cache yarn cache' uses: actions/cache@v3 with: diff --git a/.github/workflows/test-edge-with-hypothesis.yaml b/.github/workflows/test-edge-with-hypothesis.yaml index 15e452dc33a..596a5339d45 100644 --- a/.github/workflows/test-edge-with-hypothesis.yaml +++ b/.github/workflows/test-edge-with-hypothesis.yaml @@ -2,8 +2,8 @@ name: 'Testing Edge with Hypothesis' on: schedule: - - cron: '45 22 * * 1-5' - + - cron: '45 22 * * 1-5' + workflow_dispatch: concurrency: @@ -20,23 +20,23 @@ jobs: timeout-minutes: 120 runs-on: 'ubuntu-latest' steps: - - name: 'Checkout opentrons repo' - uses: 'actions/checkout@v3' - with: - ref: 'edge' - fetch-depth: 0 - - - name: 'Setup Python' - uses: 'actions/setup-python@v5' - with: - python-version: '3.10' - cache: 'pipenv' - cache-dependency-path: 'test-data-generation/Pipfile.lock' - - - name: 'Install Python deps' - uses: './.github/actions/python/setup' - with: - project: 'test-data-generation' - - - name: 'Run Hypothesis tests' - run: 'make -C test-data-generation test' + - name: 'Checkout opentrons repo' + uses: 'actions/checkout@v4' + with: + ref: 'edge' + fetch-depth: 0 + + - name: 'Setup Python' + uses: 'actions/setup-python@v5' + with: + python-version: '3.10' + cache: 'pipenv' + cache-dependency-path: 'test-data-generation/Pipfile.lock' + + - name: 'Install Python deps' + uses: './.github/actions/python/setup' + with: + project: 'test-data-generation' + + - name: 'Run Hypothesis tests' + run: 'make -C test-data-generation test' diff --git a/.github/workflows/update-server-lint-test.yaml b/.github/workflows/update-server-lint-test.yaml index b4d1435838f..1d3164a63cf 100644 --- a/.github/workflows/update-server-lint-test.yaml +++ b/.github/workflows/update-server-lint-test.yaml @@ -41,12 +41,12 @@ jobs: timeout-minutes: 10 runs-on: 'ubuntu-22.04' steps: - - uses: 'actions/checkout@v3' + - uses: 'actions/checkout@v4' with: fetch-depth: 0 - - uses: 'actions/setup-node@v3' + - uses: 'actions/setup-node@v4' with: - node-version: '18.19.0' + node-version: '22.11.0' - uses: 'actions/setup-python@v4' with: python-version: '3.10' @@ -62,12 +62,12 @@ jobs: needs: [lint] runs-on: 'ubuntu-22.04' steps: - - uses: 'actions/checkout@v3' + - uses: 'actions/checkout@v4' with: fetch-depth: 0 - - uses: 'actions/setup-node@v3' + - uses: 'actions/setup-node@v4' with: - node-version: '18.19.0' + node-version: '22.11.0' - uses: 'actions/setup-python@v4' with: python-version: '3.10' diff --git a/.github/workflows/usb-bridge-lint-test.yaml b/.github/workflows/usb-bridge-lint-test.yaml index 2888291871a..bfe11aed61b 100644 --- a/.github/workflows/usb-bridge-lint-test.yaml +++ b/.github/workflows/usb-bridge-lint-test.yaml @@ -41,12 +41,12 @@ jobs: timeout-minutes: 10 runs-on: 'ubuntu-22.04' steps: - - uses: 'actions/checkout@v3' + - uses: 'actions/checkout@v4' with: fetch-depth: 0 - - uses: 'actions/setup-node@v3' + - uses: 'actions/setup-node@v4' with: - node-version: '18.19.0' + node-version: '22.11.0' - uses: 'actions/setup-python@v4' with: python-version: '3.10' @@ -62,12 +62,12 @@ jobs: needs: [lint] runs-on: 'ubuntu-22.04' steps: - - uses: 'actions/checkout@v3' + - uses: 'actions/checkout@v4' with: fetch-depth: 0 - - uses: 'actions/setup-node@v3' + - uses: 'actions/setup-node@v4' with: - node-version: '18.19.0' + node-version: '22.11.0' - uses: 'actions/setup-python@v4' with: python-version: '3.10' diff --git a/.gitignore b/.gitignore index 319ccc32e67..2d8b2ff20cb 100755 --- a/.gitignore +++ b/.gitignore @@ -163,3 +163,5 @@ opentrons-robot-app.tar.gz mock_dir .npm-cache/ .eslintcache + +package-testing/results diff --git a/.nvmrc b/.nvmrc index 3c032078a4a..fdb2eaaff0c 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -18 +22.11.0 \ No newline at end of file diff --git a/DEV_SETUP.md b/DEV_SETUP.md index 238f2c7fda3..a334b047745 100644 --- a/DEV_SETUP.md +++ b/DEV_SETUP.md @@ -13,7 +13,7 @@ You will need the following tools installed to develop on the Opentrons platform - curl - ssh - Python v3.10 -- Node.js v18 +- Node.js v22.11.0 ### macOS @@ -85,7 +85,7 @@ nvs --version Now we can use `nvs` to install the currently required Node.js version set in `.nvmrc`. The `auto` command selects the correct version of Node.js any time we're in the `opentrons` project directory. Without `auto`, we would have to manually run `use` or `install` each time we work on the project. ```shell -nvs add 18 +nvs add 22 nvs auto on ``` @@ -202,7 +202,7 @@ Once you are inside the repository for the first time, you should do two things: 3. Run `python --version` to confirm your chosen version. If you get the incorrect version and you're using an Apple silicon Mac, try running `eval "$(pyenv init --path)"` and then `pyenv local 3.10.13`. Then check `python --version` again. ```shell -# confirm Node v18 +# confirm Node v22.11.0 or greater node --version # set Python version, and confirm diff --git a/Makefile b/Makefile index ffdbb8509c0..7a5bfc7a7e9 100755 --- a/Makefile +++ b/Makefile @@ -152,6 +152,10 @@ push: sleep 1 $(MAKE) -C $(UPDATE_SERVER_DIR) push +.PHONY: push-folder +PUSH_HELPER := abr-testing/abr_testing/tools/make_push.py +push-folder: + $(OT_PYTHON) $(PUSH_HELPER) .PHONY: push-ot3 push-ot3: @@ -228,7 +232,7 @@ lint-js-prettier: .PHONY: lint-json lint-json: - yarn eslint --max-warnings 0 --ext .json . + yarn eslint --ignore-pattern "abr-testing/protocols/" --max-warnings 0 --ext .json . .PHONY: lint-css lint-css: diff --git a/abr-testing/.flake8 b/abr-testing/.flake8 index cc618b04ba2..ec1d7b91184 100644 --- a/abr-testing/.flake8 +++ b/abr-testing/.flake8 @@ -21,4 +21,5 @@ docstring-convention = google noqa-require-code = true -# per-file-ignores = +per-file-ignores = + abr_testing/protocols/*: C901 diff --git a/abr-testing/Pipfile b/abr-testing/Pipfile index 90534f708ae..613ca5203f7 100644 --- a/abr-testing/Pipfile +++ b/abr-testing/Pipfile @@ -19,6 +19,7 @@ slack-sdk = "*" pandas = "*" pandas-stubs = "*" paramiko = "*" +prettier = "*" [dev-packages] atomicwrites = "==1.4.1" diff --git a/abr-testing/Pipfile.lock b/abr-testing/Pipfile.lock index 79885cdc940..a2f82b44925 100644 --- a/abr-testing/Pipfile.lock +++ b/abr-testing/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "a537d1f1a5f5d0658a3ba2c62deabf390fd7b9e72acbee6704f8d095c1b535e9" + "sha256": "f773f4880fa452637eeaf5e1aebee4ca6a1dc34907f588e0c6f71f0f222dc725" }, "pipfile-spec": 6, "requires": { @@ -22,108 +22,93 @@ }, "aiohappyeyeballs": { "hashes": [ - "sha256:75cf88a15106a5002a8eb1dab212525c00d1f4c0fa96e551c9fbe6f09a621586", - "sha256:8a7a83727b2756f394ab2895ea0765a0a8c475e3c71e98d43d76f22b4b435572" + "sha256:5fdd7d87889c63183afc18ce9271f9b0a7d32c2303e394468dd45d514a757745", + "sha256:a980909d50efcd44795c4afeca523296716d50cd756ddca6af8c65b996e27de8" ], "markers": "python_version >= '3.8'", - "version": "==2.4.3" + "version": "==2.4.4" }, "aiohttp": { "hashes": [ - "sha256:007ec22fbc573e5eb2fb7dec4198ef8f6bf2fe4ce20020798b2eb5d0abda6138", - "sha256:00819de9e45d42584bed046314c40ea7e9aea95411b38971082cad449392b08c", - "sha256:01948b1d570f83ee7bbf5a60ea2375a89dfb09fd419170e7f5af029510033d24", - "sha256:038f514fe39e235e9fef6717fbf944057bfa24f9b3db9ee551a7ecf584b5b480", - "sha256:03a42ac7895406220124c88911ebee31ba8b2d24c98507f4a8bf826b2937c7f2", - "sha256:05646ebe6b94cc93407b3bf34b9eb26c20722384d068eb7339de802154d61bc5", - "sha256:0631dd7c9f0822cc61c88586ca76d5b5ada26538097d0f1df510b082bad3411a", - "sha256:0b00807e2605f16e1e198f33a53ce3c4523114059b0c09c337209ae55e3823a8", - "sha256:0e1b370d8007c4ae31ee6db7f9a2fe801a42b146cec80a86766e7ad5c4a259cf", - "sha256:15ecd889a709b0080f02721255b3f80bb261c2293d3c748151274dfea93ac871", - "sha256:1b66ccafef7336a1e1f0e389901f60c1d920102315a56df85e49552308fc0486", - "sha256:1bbb122c557a16fafc10354b9d99ebf2f2808a660d78202f10ba9d50786384b9", - "sha256:1eb89d3d29adaf533588f209768a9c02e44e4baf832b08118749c5fad191781d", - "sha256:258c5dd01afc10015866114e210fb7365f0d02d9d059c3c3415382ab633fcbcb", - "sha256:2609e9ab08474702cc67b7702dbb8a80e392c54613ebe80db7e8dbdb79837c68", - "sha256:274cfa632350225ce3fdeb318c23b4a10ec25c0e2c880eff951a3842cf358ac1", - "sha256:28529e08fde6f12eba8677f5a8608500ed33c086f974de68cc65ab218713a59d", - "sha256:2b606353da03edcc71130b52388d25f9a30a126e04caef1fd637e31683033abd", - "sha256:30ca7c3b94708a9d7ae76ff281b2f47d8eaf2579cd05971b5dc681db8caac6e1", - "sha256:333cf6cf8e65f6a1e06e9eb3e643a0c515bb850d470902274239fea02033e9a8", - "sha256:3455522392fb15ff549d92fbf4b73b559d5e43dc522588f7eb3e54c3f38beee7", - "sha256:362f641f9071e5f3ee6f8e7d37d5ed0d95aae656adf4ef578313ee585b585959", - "sha256:3bcd391d083f636c06a68715e69467963d1f9600f85ef556ea82e9ef25f043f7", - "sha256:3dffb610a30d643983aeb185ce134f97f290f8935f0abccdd32c77bed9388b42", - "sha256:3fe407bf93533a6fa82dece0e74dbcaaf5d684e5a51862887f9eaebe6372cd79", - "sha256:413251f6fcf552a33c981c4709a6bba37b12710982fec8e558ae944bfb2abd38", - "sha256:438cd072f75bb6612f2aca29f8bd7cdf6e35e8f160bc312e49fbecab77c99e3a", - "sha256:4470c73c12cd9109db8277287d11f9dd98f77fc54155fc71a7738a83ffcc8ea8", - "sha256:45c3b868724137f713a38376fef8120c166d1eadd50da1855c112fe97954aed8", - "sha256:486f7aabfa292719a2753c016cc3a8f8172965cabb3ea2e7f7436c7f5a22a151", - "sha256:4f05e9727ce409358baa615dbeb9b969db94324a79b5a5cea45d39bdb01d82e6", - "sha256:50aed5155f819873d23520919e16703fc8925e509abbb1a1491b0087d1cd969e", - "sha256:50edbcad60d8f0e3eccc68da67f37268b5144ecc34d59f27a02f9611c1d4eec7", - "sha256:54ca74df1be3c7ca1cf7f4c971c79c2daf48d9aa65dea1a662ae18926f5bc8ce", - "sha256:578a4b875af3e0daaf1ac6fa983d93e0bbfec3ead753b6d6f33d467100cdc67b", - "sha256:597a079284b7ee65ee102bc3a6ea226a37d2b96d0418cc9047490f231dc09fe8", - "sha256:59bb3c54aa420521dc4ce3cc2c3fe2ad82adf7b09403fa1f48ae45c0cbde6628", - "sha256:5c6a5b8c7926ba5d8545c7dd22961a107526562da31a7a32fa2456baf040939f", - "sha256:64f6c17757251e2b8d885d728b6433d9d970573586a78b78ba8929b0f41d045a", - "sha256:679abe5d3858b33c2cf74faec299fda60ea9de62916e8b67e625d65bf069a3b7", - "sha256:741a46d58677d8c733175d7e5aa618d277cd9d880301a380fd296975a9cdd7bc", - "sha256:7789050d9e5d0c309c706953e5e8876e38662d57d45f936902e176d19f1c58ab", - "sha256:77abf6665ae54000b98b3c742bc6ea1d1fb31c394bcabf8b5d2c1ac3ebfe7f3b", - "sha256:79019094f87c9fb44f8d769e41dbb664d6e8fcfd62f665ccce36762deaa0e911", - "sha256:7b06b7843929e41a94ea09eb1ce3927865387e3e23ebe108e0d0d09b08d25be9", - "sha256:7e338c0523d024fad378b376a79faff37fafb3c001872a618cde1d322400a572", - "sha256:7ea7ffc6d6d6f8a11e6f40091a1040995cdff02cfc9ba4c2f30a516cb2633554", - "sha256:8105fd8a890df77b76dd3054cddf01a879fc13e8af576805d667e0fa0224c35d", - "sha256:84afcdea18eda514c25bc68b9af2a2b1adea7c08899175a51fe7c4fb6d551257", - "sha256:9294bbb581f92770e6ed5c19559e1e99255e4ca604a22c5c6397b2f9dd3ee42c", - "sha256:93429602396f3383a797a2a70e5f1de5df8e35535d7806c9f91df06f297e109b", - "sha256:9627cc1a10c8c409b5822a92d57a77f383b554463d1884008e051c32ab1b3742", - "sha256:998f3bd3cfc95e9424a6acd7840cbdd39e45bc09ef87533c006f94ac47296090", - "sha256:9c72109213eb9d3874f7ac8c0c5fa90e072d678e117d9061c06e30c85b4cf0e6", - "sha256:9fc1500fd2a952c5c8e3b29aaf7e3cc6e27e9cfc0a8819b3bce48cc1b849e4cc", - "sha256:a3f00003de6eba42d6e94fabb4125600d6e484846dbf90ea8e48a800430cc142", - "sha256:a45d85cf20b5e0d0aa5a8dca27cce8eddef3292bc29d72dcad1641f4ed50aa16", - "sha256:a7d8d14fe962153fc681f6366bdec33d4356f98a3e3567782aac1b6e0e40109a", - "sha256:a8fa23fe62c436ccf23ff930149c047f060c7126eae3ccea005f0483f27b2e28", - "sha256:aa6658732517ddabe22c9036479eabce6036655ba87a0224c612e1ae6af2087e", - "sha256:aafc8ee9b742ce75044ae9a4d3e60e3d918d15a4c2e08a6c3c3e38fa59b92d94", - "sha256:ab5a5a0c7a7991d90446a198689c0535be89bbd6b410a1f9a66688f0880ec026", - "sha256:acd48d5b80ee80f9432a165c0ac8cbf9253eaddb6113269a5e18699b33958dbb", - "sha256:ad7593bb24b2ab09e65e8a1d385606f0f47c65b5a2ae6c551db67d6653e78c28", - "sha256:baa42524a82f75303f714108fea528ccacf0386af429b69fff141ffef1c534f9", - "sha256:bdfcf6443637c148c4e1a20c48c566aa694fa5e288d34b20fcdc58507882fed3", - "sha256:be7443669ae9c016b71f402e43208e13ddf00912f47f623ee5994e12fc7d4b3f", - "sha256:c02a30b904282777d872266b87b20ed8cc0d1501855e27f831320f471d54d983", - "sha256:c1277cd707c465cd09572a774559a3cc7c7a28802eb3a2a9472588f062097205", - "sha256:c30a0eafc89d28e7f959281b58198a9fa5e99405f716c0289b7892ca345fe45f", - "sha256:c5ce2ce7c997e1971b7184ee37deb6ea9922ef5163c6ee5aa3c274b05f9e12fa", - "sha256:c823bc3971c44ab93e611ab1a46b1eafeae474c0c844aff4b7474287b75fe49c", - "sha256:ce0cdc074d540265bfeb31336e678b4e37316849d13b308607efa527e981f5c2", - "sha256:d1720b4f14c78a3089562b8875b53e36b51c97c51adc53325a69b79b4b48ebcb", - "sha256:d183cf9c797a5291e8301790ed6d053480ed94070637bfaad914dd38b0981f67", - "sha256:d9010c31cd6fa59438da4e58a7f19e4753f7f264300cd152e7f90d4602449762", - "sha256:d9e5e4a85bdb56d224f412d9c98ae4cbd032cc4f3161818f692cd81766eee65a", - "sha256:da1dee8948d2137bb51fbb8a53cce6b1bcc86003c6b42565f008438b806cccd8", - "sha256:df9270660711670e68803107d55c2b5949c2e0f2e4896da176e1ecfc068b974a", - "sha256:e00e3505cd80440f6c98c6d69269dcc2a119f86ad0a9fd70bccc59504bebd68a", - "sha256:e48d5021a84d341bcaf95c8460b152cfbad770d28e5fe14a768988c461b821bc", - "sha256:e7f8b04d83483577fd9200461b057c9f14ced334dcb053090cea1da9c8321a91", - "sha256:edfe3341033a6b53a5c522c802deb2079eee5cbfbb0af032a55064bd65c73a23", - "sha256:ef9c33cc5cbca35808f6c74be11eb7f5f6b14d2311be84a15b594bd3e58b5527", - "sha256:f2d4324a98062be0525d16f768a03e0bbb3b9fe301ceee99611dc9a7953124e6", - "sha256:f3935f82f6f4a3820270842e90456ebad3af15810cf65932bd24da4463bc0a4c", - "sha256:f614ab0c76397661b90b6851a030004dac502e48260ea10f2441abd2207fbcc7", - "sha256:f7db54c7914cc99d901d93a34704833568d86c20925b2762f9fa779f9cd2e70f", - "sha256:fbc6264158392bad9df19537e872d476f7c57adf718944cc1e4495cbabf38e2a", - "sha256:fe2fb38c2ed905a2582948e2de560675e9dfbee94c6d5ccdb1301c6d0a5bf092", - "sha256:ffe595f10566f8276b76dc3a11ae4bb7eba1aac8ddd75811736a15b0d5311414" + "sha256:012f176945af138abc10c4a48743327a92b4ca9adc7a0e078077cdb5dbab7be0", + "sha256:02c13415b5732fb6ee7ff64583a5e6ed1c57aa68f17d2bda79c04888dfdc2769", + "sha256:03b6002e20938fc6ee0918c81d9e776bebccc84690e2b03ed132331cca065ee5", + "sha256:04814571cb72d65a6899db6099e377ed00710bf2e3eafd2985166f2918beaf59", + "sha256:0580f2e12de2138f34debcd5d88894786453a76e98febaf3e8fe5db62d01c9bf", + "sha256:06a8e2ee1cbac16fe61e51e0b0c269400e781b13bcfc33f5425912391a542985", + "sha256:076bc454a7e6fd646bc82ea7f98296be0b1219b5e3ef8a488afbdd8e81fbac50", + "sha256:0c9527819b29cd2b9f52033e7fb9ff08073df49b4799c89cb5754624ecd98299", + "sha256:0dc49f42422163efb7e6f1df2636fe3db72713f6cd94688e339dbe33fe06d61d", + "sha256:14cdb5a9570be5a04eec2ace174a48ae85833c2aadc86de68f55541f66ce42ab", + "sha256:15fccaf62a4889527539ecb86834084ecf6e9ea70588efde86e8bc775e0e7542", + "sha256:24213ba85a419103e641e55c27dc7ff03536c4873470c2478cce3311ba1eee7b", + "sha256:31d5093d3acd02b31c649d3a69bb072d539d4c7659b87caa4f6d2bcf57c2fa2b", + "sha256:3691ed7726fef54e928fe26344d930c0c8575bc968c3e239c2e1a04bd8cf7838", + "sha256:386fbe79863eb564e9f3615b959e28b222259da0c48fd1be5929ac838bc65683", + "sha256:3bbbfff4c679c64e6e23cb213f57cc2c9165c9a65d63717108a644eb5a7398df", + "sha256:3de34936eb1a647aa919655ff8d38b618e9f6b7f250cc19a57a4bf7fd2062b6d", + "sha256:40d1c7a7f750b5648642586ba7206999650208dbe5afbcc5284bcec6579c9b91", + "sha256:44224d815853962f48fe124748227773acd9686eba6dc102578defd6fc99e8d9", + "sha256:47ad15a65fb41c570cd0ad9a9ff8012489e68176e7207ec7b82a0940dddfd8be", + "sha256:482cafb7dc886bebeb6c9ba7925e03591a62ab34298ee70d3dd47ba966370d2c", + "sha256:49c7dbbc1a559ae14fc48387a115b7d4bbc84b4a2c3b9299c31696953c2a5219", + "sha256:4b2c7ac59c5698a7a8207ba72d9e9c15b0fc484a560be0788b31312c2c5504e4", + "sha256:4cca22a61b7fe45da8fc73c3443150c3608750bbe27641fc7558ec5117b27fdf", + "sha256:4cfce37f31f20800a6a6620ce2cdd6737b82e42e06e6e9bd1b36f546feb3c44f", + "sha256:502a1464ccbc800b4b1995b302efaf426e8763fadf185e933c2931df7db9a199", + "sha256:53bf2097e05c2accc166c142a2090e4c6fd86581bde3fd9b2d3f9e93dda66ac1", + "sha256:593c114a2221444f30749cc5e5f4012488f56bd14de2af44fe23e1e9894a9c60", + "sha256:5d6958671b296febe7f5f859bea581a21c1d05430d1bbdcf2b393599b1cdce77", + "sha256:5ef359ebc6949e3a34c65ce20230fae70920714367c63afd80ea0c2702902ccf", + "sha256:613e5169f8ae77b1933e42e418a95931fb4867b2991fc311430b15901ed67079", + "sha256:61b9bae80ed1f338c42f57c16918853dc51775fb5cb61da70d590de14d8b5fb4", + "sha256:6362cc6c23c08d18ddbf0e8c4d5159b5df74fea1a5278ff4f2c79aed3f4e9f46", + "sha256:65a96e3e03300b41f261bbfd40dfdbf1c301e87eab7cd61c054b1f2e7c89b9e8", + "sha256:65e55ca7debae8faaffee0ebb4b47a51b4075f01e9b641c31e554fd376595c6c", + "sha256:68386d78743e6570f054fe7949d6cb37ef2b672b4d3405ce91fafa996f7d9b4d", + "sha256:68ff6f48b51bd78ea92b31079817aff539f6c8fc80b6b8d6ca347d7c02384e33", + "sha256:6ab29b8a0beb6f8eaf1e5049252cfe74adbaafd39ba91e10f18caeb0e99ffb34", + "sha256:77ae58586930ee6b2b6f696c82cf8e78c8016ec4795c53e36718365f6959dc82", + "sha256:77c4aa15a89847b9891abf97f3d4048f3c2d667e00f8a623c89ad2dccee6771b", + "sha256:78153314f26d5abef3239b4a9af20c229c6f3ecb97d4c1c01b22c4f87669820c", + "sha256:7852bbcb4d0d2f0c4d583f40c3bc750ee033265d80598d0f9cb6f372baa6b836", + "sha256:7e97d622cb083e86f18317282084bc9fbf261801b0192c34fe4b1febd9f7ae69", + "sha256:7f3dc0e330575f5b134918976a645e79adf333c0a1439dcf6899a80776c9ab39", + "sha256:80886dac673ceaef499de2f393fc80bb4481a129e6cb29e624a12e3296cc088f", + "sha256:811f23b3351ca532af598405db1093f018edf81368e689d1b508c57dcc6b6a32", + "sha256:86a5dfcc39309470bd7b68c591d84056d195428d5d2e0b5ccadfbaf25b026ebc", + "sha256:8b3cf2dc0f0690a33f2d2b2cb15db87a65f1c609f53c37e226f84edb08d10f52", + "sha256:8cc5203b817b748adccb07f36390feb730b1bc5f56683445bfe924fc270b8816", + "sha256:909af95a72cedbefe5596f0bdf3055740f96c1a4baa0dd11fd74ca4de0b4e3f1", + "sha256:974d3a2cce5fcfa32f06b13ccc8f20c6ad9c51802bb7f829eae8a1845c4019ec", + "sha256:98283b94cc0e11c73acaf1c9698dea80c830ca476492c0fe2622bd931f34b487", + "sha256:98f5635f7b74bcd4f6f72fcd85bea2154b323a9f05226a80bc7398d0c90763b0", + "sha256:99b7920e7165be5a9e9a3a7f1b680f06f68ff0d0328ff4079e5163990d046767", + "sha256:9bca390cb247dbfaec3c664326e034ef23882c3f3bfa5fbf0b56cad0320aaca5", + "sha256:9e2e576caec5c6a6b93f41626c9c02fc87cd91538b81a3670b2e04452a63def6", + "sha256:9ef405356ba989fb57f84cac66f7b0260772836191ccefbb987f414bcd2979d9", + "sha256:a55d2ad345684e7c3dd2c20d2f9572e9e1d5446d57200ff630e6ede7612e307f", + "sha256:ab7485222db0959a87fbe8125e233b5a6f01f4400785b36e8a7878170d8c3138", + "sha256:b1fc6b45010a8d0ff9e88f9f2418c6fd408c99c211257334aff41597ebece42e", + "sha256:b78f053a7ecfc35f0451d961dacdc671f4bcbc2f58241a7c820e9d82559844cf", + "sha256:b99acd4730ad1b196bfb03ee0803e4adac371ae8efa7e1cbc820200fc5ded109", + "sha256:be2b516f56ea883a3e14dda17059716593526e10fb6303189aaf5503937db408", + "sha256:beb39a6d60a709ae3fb3516a1581777e7e8b76933bb88c8f4420d875bb0267c6", + "sha256:bf3d1a519a324af764a46da4115bdbd566b3c73fb793ffb97f9111dbc684fc4d", + "sha256:c49a76c1038c2dd116fa443eba26bbb8e6c37e924e2513574856de3b6516be99", + "sha256:c5532f0441fc09c119e1dca18fbc0687e64fbeb45aa4d6a87211ceaee50a74c4", + "sha256:c6b9e6d7e41656d78e37ce754813fa44b455c3d0d0dced2a047def7dc5570b74", + "sha256:c87bf31b7fdab94ae3adbe4a48e711bfc5f89d21cf4c197e75561def39e223bc", + "sha256:cbad88a61fa743c5d283ad501b01c153820734118b65aee2bd7dbb735475ce0d", + "sha256:cf14627232dfa8730453752e9cdc210966490992234d77ff90bc8dc0dce361d5", + "sha256:db1d0b28fcb7f1d35600150c3e4b490775251dea70f894bf15c678fdd84eda6a", + "sha256:ddf5f7d877615f6a1e75971bfa5ac88609af3b74796ff3e06879e8422729fd01", + "sha256:e44a9a3c053b90c6f09b1bb4edd880959f5328cf63052503f892c41ea786d99f", + "sha256:efb15a17a12497685304b2d976cb4939e55137df7b09fa53f1b6a023f01fcb4e", + "sha256:fbbaea811a2bba171197b08eea288b9402faa2bab2ba0858eecdd0a4105753a3" ], - "markers": "python_version >= '3.8'", - "version": "==3.10.10" + "markers": "python_version >= '3.9'", + "version": "==3.11.10" }, "aionotify": { "hashes": [ @@ -141,6 +126,14 @@ "markers": "python_version >= '3.7'", "version": "==1.3.1" }, + "annotated-types": { + "hashes": [ + "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", + "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89" + ], + "markers": "python_version >= '3.8'", + "version": "==0.7.0" + }, "anyio": { "hashes": [ "sha256:44a3c9aba0f5defa43261a8b3efb97891f2bd7d804e0e1f56419befa1adfc780", @@ -151,11 +144,11 @@ }, "async-timeout": { "hashes": [ - "sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f", - "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028" + "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c", + "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3" ], - "markers": "python_version < '3.11'", - "version": "==4.0.3" + "markers": "python_version >= '3.8'", + "version": "==5.0.1" }, "attrs": { "hashes": [ @@ -167,36 +160,34 @@ }, "bcrypt": { "hashes": [ - "sha256:096a15d26ed6ce37a14c1ac1e48119660f21b24cba457f160a4b830f3fe6b5cb", - "sha256:0da52759f7f30e83f1e30a888d9163a81353ef224d82dc58eb5bb52efcabc399", - "sha256:1bb429fedbe0249465cdd85a58e8376f31bb315e484f16e68ca4c786dcc04291", - "sha256:1d84cf6d877918620b687b8fd1bf7781d11e8a0998f576c7aa939776b512b98d", - "sha256:1ee38e858bf5d0287c39b7a1fc59eec64bbf880c7d504d3a06a96c16e14058e7", - "sha256:1ff39b78a52cf03fdf902635e4c81e544714861ba3f0efc56558979dd4f09170", - "sha256:27fe0f57bb5573104b5a6de5e4153c60814c711b29364c10a75a54bb6d7ff48d", - "sha256:3413bd60460f76097ee2e0a493ccebe4a7601918219c02f503984f0a7ee0aebe", - "sha256:3698393a1b1f1fd5714524193849d0c6d524d33523acca37cd28f02899285060", - "sha256:373db9abe198e8e2c70d12b479464e0d5092cc122b20ec504097b5f2297ed184", - "sha256:39e1d30c7233cfc54f5c3f2c825156fe044efdd3e0b9d309512cc514a263ec2a", - "sha256:3bbbfb2734f0e4f37c5136130405332640a1e46e6b23e000eeff2ba8d005da68", - "sha256:3d3a6d28cb2305b43feac298774b997e372e56c7c7afd90a12b3dc49b189151c", - "sha256:5a1e8aa9b28ae28020a3ac4b053117fb51c57a010b9f969603ed885f23841458", - "sha256:61ed14326ee023917ecd093ee6ef422a72f3aec6f07e21ea5f10622b735538a9", - "sha256:655ea221910bcac76ea08aaa76df427ef8625f92e55a8ee44fbf7753dbabb328", - "sha256:762a2c5fb35f89606a9fde5e51392dad0cd1ab7ae64149a8b935fe8d79dd5ed7", - "sha256:77800b7147c9dc905db1cba26abe31e504d8247ac73580b4aa179f98e6608f34", - "sha256:8ac68872c82f1add6a20bd489870c71b00ebacd2e9134a8aa3f98a0052ab4b0e", - "sha256:8d7bb9c42801035e61c109c345a28ed7e84426ae4865511eb82e913df18f58c2", - "sha256:8f6ede91359e5df88d1f5c1ef47428a4420136f3ce97763e31b86dd8280fbdf5", - "sha256:9c1c4ad86351339c5f320ca372dfba6cb6beb25e8efc659bedd918d921956bae", - "sha256:c02d944ca89d9b1922ceb8a46460dd17df1ba37ab66feac4870f6862a1533c00", - "sha256:c52aac18ea1f4a4f65963ea4f9530c306b56ccd0c6f8c8da0c06976e34a6e841", - "sha256:cb2a8ec2bc07d3553ccebf0746bbf3d19426d1c6d1adbd4fa48925f66af7b9e8", - "sha256:cf69eaf5185fd58f268f805b505ce31f9b9fc2d64b376642164e9244540c1221", - "sha256:f4f4acf526fcd1c34e7ce851147deedd4e26e6402369304220250598b26448db" + "sha256:041fa0155c9004eb98a232d54da05c0b41d4b8e66b6fc3cb71b4b3f6144ba837", + "sha256:04e56e3fe8308a88b77e0afd20bec516f74aecf391cdd6e374f15cbed32783d6", + "sha256:1340411a0894b7d3ef562fb233e4b6ed58add185228650942bdc885362f32c17", + "sha256:533e7f3bcf2f07caee7ad98124fab7499cb3333ba2274f7a36cf1daee7409d99", + "sha256:6765386e3ab87f569b276988742039baab087b2cdb01e809d74e74503c2faafe", + "sha256:687cf30e6681eeda39548a93ce9bfbb300e48b4d445a43db4298d2474d2a1e54", + "sha256:76132c176a6d9953cdc83c296aeaed65e1a708485fd55abf163e0d9f8f16ce0e", + "sha256:76d3e352b32f4eeb34703370e370997065d28a561e4a18afe4fef07249cb4396", + "sha256:807261df60a8b1ccd13e6599c779014a362ae4e795f5c59747f60208daddd96d", + "sha256:89df2aea2c43be1e1fa066df5f86c8ce822ab70a30e4c210968669565c0f4685", + "sha256:8ad2f4528cbf0febe80e5a3a57d7a74e6635e41af1ea5675282a33d769fba413", + "sha256:8c458cd103e6c5d1d85cf600e546a639f234964d0228909d8f8dbeebff82d526", + "sha256:8dbd0747208912b1e4ce730c6725cb56c07ac734b3629b60d4398f082ea718ad", + "sha256:909faa1027900f2252a9ca5dfebd25fc0ef1417943824783d1c8418dd7d6df4a", + "sha256:aaa2e285be097050dba798d537b6efd9b698aa88eef52ec98d23dcd6d7cf6fea", + "sha256:adadd36274510a01f33e6dc08f5824b97c9580583bd4487c564fc4617b328005", + "sha256:b1ee315739bc8387aa36ff127afc99120ee452924e0df517a8f3e4c0187a0f5f", + "sha256:b588af02b89d9fad33e5f98f7838bf590d6d692df7153647724a7f20c186f6bf", + "sha256:b7703ede632dc945ed1172d6f24e9f30f27b1b1a067f32f68bf169c5f08d0425", + "sha256:c6f5fa3775966cca251848d4d5393ab016b3afed251163c1436fefdec3b02c84", + "sha256:cde78d385d5e93ece5479a0a87f73cd6fa26b171c786a884f955e165032b262c", + "sha256:cfdf3d7530c790432046c40cda41dfee8c83e29482e6a604f8930b9930e94139", + "sha256:e158009a54c4c8bc91d5e0da80920d048f918c61a581f0a63e4e93bb556d362f", + "sha256:e84e0e6f8e40a242b11bce56c313edc2be121cec3e0ec2d76fce01f6af33c07c", + "sha256:f85b1ffa09240c89aa2e1ae9f3b1c687104f7b2b9d2098da4e923f1b7082d331" ], "markers": "python_version >= '3.7'", - "version": "==4.2.0" + "version": "==4.2.1" }, "cachetools": { "hashes": [ @@ -284,7 +275,7 @@ "sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87", "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b" ], - "markers": "platform_python_implementation != 'PyPy'", + "markers": "python_version >= '3.8'", "version": "==1.17.1" }, "charset-normalizer": { @@ -406,53 +397,45 @@ "markers": "python_version >= '3.7'", "version": "==8.1.7" }, - "colorama": { - "hashes": [ - "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", - "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6" - ], - "markers": "platform_system == 'Windows'", - "version": "==0.4.6" - }, "cryptography": { "hashes": [ - "sha256:0c580952eef9bf68c4747774cde7ec1d85a6e61de97281f2dba83c7d2c806362", - "sha256:0f996e7268af62598f2fc1204afa98a3b5712313a55c4c9d434aef49cadc91d4", - "sha256:1ec0bcf7e17c0c5669d881b1cd38c4972fade441b27bda1051665faaa89bdcaa", - "sha256:281c945d0e28c92ca5e5930664c1cefd85efe80e5c0d2bc58dd63383fda29f83", - "sha256:2ce6fae5bdad59577b44e4dfed356944fbf1d925269114c28be377692643b4ff", - "sha256:315b9001266a492a6ff443b61238f956b214dbec9910a081ba5b6646a055a805", - "sha256:443c4a81bb10daed9a8f334365fe52542771f25aedaf889fd323a853ce7377d6", - "sha256:4a02ded6cd4f0a5562a8887df8b3bd14e822a90f97ac5e544c162899bc467664", - "sha256:53a583b6637ab4c4e3591a15bc9db855b8d9dee9a669b550f311480acab6eb08", - "sha256:63efa177ff54aec6e1c0aefaa1a241232dcd37413835a9b674b6e3f0ae2bfd3e", - "sha256:74f57f24754fe349223792466a709f8e0c093205ff0dca557af51072ff47ab18", - "sha256:7e1ce50266f4f70bf41a2c6dc4358afadae90e2a1e5342d3c08883df1675374f", - "sha256:81ef806b1fef6b06dcebad789f988d3b37ccaee225695cf3e07648eee0fc6b73", - "sha256:846da004a5804145a5f441b8530b4bf35afbf7da70f82409f151695b127213d5", - "sha256:8ac43ae87929a5982f5948ceda07001ee5e83227fd69cf55b109144938d96984", - "sha256:9762ea51a8fc2a88b70cf2995e5675b38d93bf36bd67d91721c309df184f49bd", - "sha256:a2a431ee15799d6db9fe80c82b055bae5a752bef645bba795e8e52687c69efe3", - "sha256:bf7a1932ac4176486eab36a19ed4c0492da5d97123f1406cf15e41b05e787d2e", - "sha256:c2e6fc39c4ab499049df3bdf567f768a723a5e8464816e8f009f121a5a9f4405", - "sha256:cbeb489927bd7af4aa98d4b261af9a5bc025bd87f0e3547e11584be9e9427be2", - "sha256:d03b5621a135bffecad2c73e9f4deb1a0f977b9a8ffe6f8e002bf6c9d07b918c", - "sha256:d56e96520b1020449bbace2b78b603442e7e378a9b3bd68de65c782db1507995", - "sha256:df6b6c6d742395dd77a23ea3728ab62f98379eff8fb61be2744d4679ab678f73", - "sha256:e1be4655c7ef6e1bbe6b5d0403526601323420bcf414598955968c9ef3eb7d16", - "sha256:f18c716be16bc1fea8e95def49edf46b82fccaa88587a45f8dc0ff6ab5d8e0a7", - "sha256:f46304d6f0c6ab8e52770addfa2fc41e6629495548862279641972b6215451cd", - "sha256:f7b178f11ed3664fd0e995a47ed2b5ff0a12d893e41dd0494f406d1cf555cab7" - ], - "markers": "python_version >= '3.7'", - "version": "==43.0.3" + "sha256:1923cb251c04be85eec9fda837661c67c1049063305d6be5721643c22dd4e2b7", + "sha256:37d76e6863da3774cd9db5b409a9ecfd2c71c981c38788d3fcfaf177f447b731", + "sha256:3c672a53c0fb4725a29c303be906d3c1fa99c32f58abe008a82705f9ee96f40b", + "sha256:404fdc66ee5f83a1388be54300ae978b2efd538018de18556dde92575e05defc", + "sha256:4ac4c9f37eba52cb6fbeaf5b59c152ea976726b865bd4cf87883a7e7006cc543", + "sha256:62901fb618f74d7d81bf408c8719e9ec14d863086efe4185afd07c352aee1d2c", + "sha256:660cb7312a08bc38be15b696462fa7cc7cd85c3ed9c576e81f4dc4d8b2b31591", + "sha256:708ee5f1bafe76d041b53a4f95eb28cdeb8d18da17e597d46d7833ee59b97ede", + "sha256:761817a3377ef15ac23cd7834715081791d4ec77f9297ee694ca1ee9c2c7e5eb", + "sha256:831c3c4d0774e488fdc83a1923b49b9957d33287de923d58ebd3cec47a0ae43f", + "sha256:84111ad4ff3f6253820e6d3e58be2cc2a00adb29335d4cacb5ab4d4d34f2a123", + "sha256:8b3e6eae66cf54701ee7d9c83c30ac0a1e3fa17be486033000f2a73a12ab507c", + "sha256:9e6fc8a08e116fb7c7dd1f040074c9d7b51d74a8ea40d4df2fc7aa08b76b9e6c", + "sha256:a01956ddfa0a6790d594f5b34fc1bfa6098aca434696a03cfdbe469b8ed79285", + "sha256:abc998e0c0eee3c8a1904221d3f67dcfa76422b23620173e28c11d3e626c21bd", + "sha256:b15492a11f9e1b62ba9d73c210e2416724633167de94607ec6069ef724fad092", + "sha256:be4ce505894d15d5c5037167ffb7f0ae90b7be6f2a98f9a5c3442395501c32fa", + "sha256:c5eb858beed7835e5ad1faba59e865109f3e52b3783b9ac21e7e47dc5554e289", + "sha256:cd4e834f340b4293430701e772ec543b0fbe6c2dea510a5286fe0acabe153a02", + "sha256:d2436114e46b36d00f8b72ff57e598978b37399d2786fd39793c36c6d5cb1c64", + "sha256:eb33480f1bad5b78233b0ad3e1b0be21e8ef1da745d8d2aecbb20671658b9053", + "sha256:eca27345e1214d1b9f9490d200f9db5a874479be914199194e746c893788d417", + "sha256:ed3534eb1090483c96178fcb0f8893719d96d5274dfde98aa6add34614e97c8e", + "sha256:f3f6fdfa89ee2d9d496e2c087cebef9d4fcbb0ad63c40e821b39f74bf48d9c5e", + "sha256:f53c2c87e0fb4b0c00fa9571082a057e37690a8f12233306161c8f4b819960b7", + "sha256:f5e7cb1e5e56ca0933b4873c0220a78b773b24d40d186b6738080b73d3d0a756", + "sha256:f677e1268c4e23420c3acade68fac427fffcb8d19d7df95ed7ad17cdef8404f4" + ], + "markers": "python_version >= '3.7' and python_full_version not in '3.9.0, 3.9.1'", + "version": "==44.0.0" }, "exceptiongroup": { "hashes": [ "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b", "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc" ], - "markers": "python_version < '3.11'", + "markers": "python_version >= '3.7'", "version": "==1.2.2" }, "frozenlist": { @@ -555,28 +538,28 @@ }, "google-api-core": { "hashes": [ - "sha256:4a152fd11a9f774ea606388d423b68aa7e6d6a0ffe4c8266f74979613ec09f81", - "sha256:6869eacb2a37720380ba5898312af79a4d30b8bca1548fb4093e0697dc4bdf5d" + "sha256:10d82ac0fca69c82a25b3efdeefccf6f28e02ebb97925a8cce8edbfe379929d9", + "sha256:e255640547a597a4da010876d333208ddac417d60add22b6851a0c66a831fcaf" ], "markers": "python_version >= '3.7'", - "version": "==2.21.0" + "version": "==2.24.0" }, "google-api-python-client": { "hashes": [ - "sha256:1a5232e9cfed8c201799d9327e4d44dc7ea7daa3c6e1627fca41aa201539c0da", - "sha256:b9d68c6b14ec72580d66001bd33c5816b78e2134b93ccc5cf8f624516b561750" + "sha256:1b420062e03bfcaa1c79e2e00a612d29a6a934151ceb3d272fe150a656dc8f17", + "sha256:a521bbbb2ec0ba9d6f307cdd64ed6e21eeac372d1bd7493a4ab5022941f784ad" ], "index": "pypi", "markers": "python_version >= '3.7'", - "version": "==2.149.0" + "version": "==2.154.0" }, "google-auth": { "hashes": [ - "sha256:25df55f327ef021de8be50bad0dfd4a916ad0de96da86cd05661c9297723ad3f", - "sha256:f4c64ed4e01e8e8b646ef34c018f8bf3338df0c8e37d8b3bba40e7f574a3278a" + "sha256:51a15d47028b66fd36e5c64a82d2d57480075bccc7da37cde257fc94177a61fb", + "sha256:545e9618f2df0bcbb7dcbc45a546485b1212624716975a1ea5ae8149ce769ab1" ], "markers": "python_version >= '3.7'", - "version": "==2.35.0" + "version": "==2.36.0" }, "google-auth-httplib2": { "hashes": [ @@ -595,11 +578,11 @@ }, "googleapis-common-protos": { "hashes": [ - "sha256:2972e6c496f435b92590fd54045060867f3fe9be2c82ab148fc8885035479a63", - "sha256:334a29d07cddc3aa01dee4988f9afd9b2916ee2ff49d6b757155dc0d197852c0" + "sha256:c3e7b33d15fdca5374cc0a7346dd92ffa847425cc4ea941d970f13680052ec8c", + "sha256:d7abcd75fabb2e0ec9f74466401f6c119a0b498e27370e9be4c94cb7e382b8ed" ], "markers": "python_version >= '3.7'", - "version": "==1.65.0" + "version": "==1.66.0" }, "gspread": { "hashes": [ @@ -633,11 +616,81 @@ }, "jsonschema": { "hashes": [ - "sha256:0f864437ab8b6076ba6707453ef8f98a6a0d512a80e93f8abdb676f737ecb60d", - "sha256:a870ad254da1a8ca84b6a2905cac29d265f805acc57af304784962a2aa6508f6" + "sha256:d71497fef26351a33265337fa77ffeb82423f3ea21283cd9467bb03999266bc4", + "sha256:fbadb6f8b144a8f8cf9f0b89ba94501d143e50411a1278633f56a7acf7fd5566" ], - "markers": "python_version >= '3.7'", - "version": "==4.17.3" + "markers": "python_version >= '3.8'", + "version": "==4.23.0" + }, + "jsonschema-specifications": { + "hashes": [ + "sha256:0f38b83639958ce1152d02a7f062902c41c8fd20d558b0c34344292d417ae272", + "sha256:a09a0680616357d9a0ecf05c12ad234479f549239d0f5b55f3deea67475da9bf" + ], + "markers": "python_version >= '3.9'", + "version": "==2024.10.1" + }, + "msgpack": { + "hashes": [ + "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": "python_version >= '3.8'", + "version": "==1.0.8" }, "multidict": { "hashes": [ @@ -817,11 +870,11 @@ }, "packaging": { "hashes": [ - "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002", - "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124" + "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", + "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f" ], "markers": "python_version >= '3.8'", - "version": "==24.1" + "version": "==24.2" }, "pandas": { "hashes": [ @@ -874,12 +927,12 @@ }, "pandas-stubs": { "hashes": [ - "sha256:3a6f8f142105a42550be677ba741ba532621f4e0acad2155c0e7b2450f114cfa", - "sha256:d4ab618253f0acf78a5d0d2bfd6dffdd92d91a56a69bdc8144e5a5c6d25be3b5" + "sha256:74aa79c167af374fe97068acc90776c0ebec5266a6e5c69fe11e9c2cf51f2267", + "sha256:cf819383c6d9ae7d4dabf34cd47e1e45525bb2f312e6ad2939c2c204cb708acd" ], "index": "pypi", "markers": "python_version >= '3.10'", - "version": "==2.2.3.241009" + "version": "==2.2.3.241126" }, "paramiko": { "hashes": [ @@ -890,134 +943,126 @@ "markers": "python_version >= '3.6'", "version": "==3.5.0" }, + "prettier": { + "hashes": [ + "sha256:20e76791de41cafe481328dd49552303f29ca192151cee1b120c26f66cae9bfc", + "sha256:6c34b8cd09fd9c8956c05d6395ea3f575e0122dce494ba57685c07065abed427" + ], + "index": "pypi", + "version": "==0.0.7" + }, "propcache": { "hashes": [ - "sha256:00181262b17e517df2cd85656fcd6b4e70946fe62cd625b9d74ac9977b64d8d9", - "sha256:0e53cb83fdd61cbd67202735e6a6687a7b491c8742dfc39c9e01e80354956763", - "sha256:1235c01ddaa80da8235741e80815ce381c5267f96cc49b1477fdcf8c047ef325", - "sha256:140fbf08ab3588b3468932974a9331aff43c0ab8a2ec2c608b6d7d1756dbb6cb", - "sha256:191db28dc6dcd29d1a3e063c3be0b40688ed76434622c53a284e5427565bbd9b", - "sha256:1e41d67757ff4fbc8ef2af99b338bfb955010444b92929e9e55a6d4dcc3c4f09", - "sha256:1ec43d76b9677637a89d6ab86e1fef70d739217fefa208c65352ecf0282be957", - "sha256:20a617c776f520c3875cf4511e0d1db847a076d720714ae35ffe0df3e440be68", - "sha256:218db2a3c297a3768c11a34812e63b3ac1c3234c3a086def9c0fee50d35add1f", - "sha256:22aa8f2272d81d9317ff5756bb108021a056805ce63dd3630e27d042c8092798", - "sha256:25a1f88b471b3bc911d18b935ecb7115dff3a192b6fef46f0bfaf71ff4f12418", - "sha256:25c8d773a62ce0451b020c7b29a35cfbc05de8b291163a7a0f3b7904f27253e6", - "sha256:2a60ad3e2553a74168d275a0ef35e8c0a965448ffbc3b300ab3a5bb9956c2162", - "sha256:2a66df3d4992bc1d725b9aa803e8c5a66c010c65c741ad901e260ece77f58d2f", - "sha256:2ccc28197af5313706511fab3a8b66dcd6da067a1331372c82ea1cb74285e036", - "sha256:2e900bad2a8456d00a113cad8c13343f3b1f327534e3589acc2219729237a2e8", - "sha256:2ee7606193fb267be4b2e3b32714f2d58cad27217638db98a60f9efb5efeccc2", - "sha256:33ac8f098df0585c0b53009f039dfd913b38c1d2edafed0cedcc0c32a05aa110", - "sha256:3444cdba6628accf384e349014084b1cacd866fbb88433cd9d279d90a54e0b23", - "sha256:363ea8cd3c5cb6679f1c2f5f1f9669587361c062e4899fce56758efa928728f8", - "sha256:375a12d7556d462dc64d70475a9ee5982465fbb3d2b364f16b86ba9135793638", - "sha256:388f3217649d6d59292b722d940d4d2e1e6a7003259eb835724092a1cca0203a", - "sha256:3947483a381259c06921612550867b37d22e1df6d6d7e8361264b6d037595f44", - "sha256:39e104da444a34830751715f45ef9fc537475ba21b7f1f5b0f4d71a3b60d7fe2", - "sha256:3c997f8c44ec9b9b0bcbf2d422cc00a1d9b9c681f56efa6ca149a941e5560da2", - "sha256:3dfafb44f7bb35c0c06eda6b2ab4bfd58f02729e7c4045e179f9a861b07c9850", - "sha256:3ebbcf2a07621f29638799828b8d8668c421bfb94c6cb04269130d8de4fb7136", - "sha256:3f88a4095e913f98988f5b338c1d4d5d07dbb0b6bad19892fd447484e483ba6b", - "sha256:439e76255daa0f8151d3cb325f6dd4a3e93043e6403e6491813bcaaaa8733887", - "sha256:4569158070180c3855e9c0791c56be3ceeb192defa2cdf6a3f39e54319e56b89", - "sha256:466c219deee4536fbc83c08d09115249db301550625c7fef1c5563a584c9bc87", - "sha256:4a9d9b4d0a9b38d1c391bb4ad24aa65f306c6f01b512e10a8a34a2dc5675d348", - "sha256:4c7dde9e533c0a49d802b4f3f218fa9ad0a1ce21f2c2eb80d5216565202acab4", - "sha256:53d1bd3f979ed529f0805dd35ddaca330f80a9a6d90bc0121d2ff398f8ed8861", - "sha256:55346705687dbd7ef0d77883ab4f6fabc48232f587925bdaf95219bae072491e", - "sha256:56295eb1e5f3aecd516d91b00cfd8bf3a13991de5a479df9e27dd569ea23959c", - "sha256:56bb5c98f058a41bb58eead194b4db8c05b088c93d94d5161728515bd52b052b", - "sha256:5a5b3bb545ead161be780ee85a2b54fdf7092815995661947812dde94a40f6fb", - "sha256:5f2564ec89058ee7c7989a7b719115bdfe2a2fb8e7a4543b8d1c0cc4cf6478c1", - "sha256:608cce1da6f2672a56b24a015b42db4ac612ee709f3d29f27a00c943d9e851de", - "sha256:63f13bf09cc3336eb04a837490b8f332e0db41da66995c9fd1ba04552e516354", - "sha256:662dd62358bdeaca0aee5761de8727cfd6861432e3bb828dc2a693aa0471a563", - "sha256:676135dcf3262c9c5081cc8f19ad55c8a64e3f7282a21266d05544450bffc3a5", - "sha256:67aeb72e0f482709991aa91345a831d0b707d16b0257e8ef88a2ad246a7280bf", - "sha256:67b69535c870670c9f9b14a75d28baa32221d06f6b6fa6f77a0a13c5a7b0a5b9", - "sha256:682a7c79a2fbf40f5dbb1eb6bfe2cd865376deeac65acf9beb607505dced9e12", - "sha256:6994984550eaf25dd7fc7bd1b700ff45c894149341725bb4edc67f0ffa94efa4", - "sha256:69d3a98eebae99a420d4b28756c8ce6ea5a29291baf2dc9ff9414b42676f61d5", - "sha256:6e2e54267980349b723cff366d1e29b138b9a60fa376664a157a342689553f71", - "sha256:73e4b40ea0eda421b115248d7e79b59214411109a5bc47d0d48e4c73e3b8fcf9", - "sha256:74acd6e291f885678631b7ebc85d2d4aec458dd849b8c841b57ef04047833bed", - "sha256:7665f04d0c7f26ff8bb534e1c65068409bf4687aa2534faf7104d7182debb336", - "sha256:7735e82e3498c27bcb2d17cb65d62c14f1100b71723b68362872bca7d0913d90", - "sha256:77a86c261679ea5f3896ec060be9dc8e365788248cc1e049632a1be682442063", - "sha256:7cf18abf9764746b9c8704774d8b06714bcb0a63641518a3a89c7f85cc02c2ad", - "sha256:83928404adf8fb3d26793665633ea79b7361efa0287dfbd372a7e74311d51ee6", - "sha256:8e40876731f99b6f3c897b66b803c9e1c07a989b366c6b5b475fafd1f7ba3fb8", - "sha256:8f188cfcc64fb1266f4684206c9de0e80f54622c3f22a910cbd200478aeae61e", - "sha256:91997d9cb4a325b60d4e3f20967f8eb08dfcb32b22554d5ef78e6fd1dda743a2", - "sha256:91ee8fc02ca52e24bcb77b234f22afc03288e1dafbb1f88fe24db308910c4ac7", - "sha256:92fe151145a990c22cbccf9ae15cae8ae9eddabfc949a219c9f667877e40853d", - "sha256:945db8ee295d3af9dbdbb698cce9bbc5c59b5c3fe328bbc4387f59a8a35f998d", - "sha256:9517d5e9e0731957468c29dbfd0f976736a0e55afaea843726e887f36fe017df", - "sha256:952e0d9d07609d9c5be361f33b0d6d650cd2bae393aabb11d9b719364521984b", - "sha256:97a58a28bcf63284e8b4d7b460cbee1edaab24634e82059c7b8c09e65284f178", - "sha256:97e48e8875e6c13909c800fa344cd54cc4b2b0db1d5f911f840458a500fde2c2", - "sha256:9e0f07b42d2a50c7dd2d8675d50f7343d998c64008f1da5fef888396b7f84630", - "sha256:a3dc1a4b165283bd865e8f8cb5f0c64c05001e0718ed06250d8cac9bec115b48", - "sha256:a3ebe9a75be7ab0b7da2464a77bb27febcb4fab46a34f9288f39d74833db7f61", - "sha256:a64e32f8bd94c105cc27f42d3b658902b5bcc947ece3c8fe7bc1b05982f60e89", - "sha256:a6ed8db0a556343d566a5c124ee483ae113acc9a557a807d439bcecc44e7dfbb", - "sha256:ad9c9b99b05f163109466638bd30ada1722abb01bbb85c739c50b6dc11f92dc3", - "sha256:b33d7a286c0dc1a15f5fc864cc48ae92a846df287ceac2dd499926c3801054a6", - "sha256:bc092ba439d91df90aea38168e11f75c655880c12782facf5cf9c00f3d42b562", - "sha256:c436130cc779806bdf5d5fae0d848713105472b8566b75ff70048c47d3961c5b", - "sha256:c5869b8fd70b81835a6f187c5fdbe67917a04d7e52b6e7cc4e5fe39d55c39d58", - "sha256:c5ecca8f9bab618340c8e848d340baf68bcd8ad90a8ecd7a4524a81c1764b3db", - "sha256:cfac69017ef97db2438efb854edf24f5a29fd09a536ff3a992b75990720cdc99", - "sha256:d2f0d0f976985f85dfb5f3d685697ef769faa6b71993b46b295cdbbd6be8cc37", - "sha256:d5bed7f9805cc29c780f3aee05de3262ee7ce1f47083cfe9f77471e9d6777e83", - "sha256:d6a21ef516d36909931a2967621eecb256018aeb11fc48656e3257e73e2e247a", - "sha256:d9b6ddac6408194e934002a69bcaadbc88c10b5f38fb9307779d1c629181815d", - "sha256:db47514ffdbd91ccdc7e6f8407aac4ee94cc871b15b577c1c324236b013ddd04", - "sha256:df81779732feb9d01e5d513fad0122efb3d53bbc75f61b2a4f29a020bc985e70", - "sha256:e4a91d44379f45f5e540971d41e4626dacd7f01004826a18cb048e7da7e96544", - "sha256:e63e3e1e0271f374ed489ff5ee73d4b6e7c60710e1f76af5f0e1a6117cd26394", - "sha256:e70fac33e8b4ac63dfc4c956fd7d85a0b1139adcfc0d964ce288b7c527537fea", - "sha256:ecddc221a077a8132cf7c747d5352a15ed763b674c0448d811f408bf803d9ad7", - "sha256:f45eec587dafd4b2d41ac189c2156461ebd0c1082d2fe7013571598abb8505d1", - "sha256:f52a68c21363c45297aca15561812d542f8fc683c85201df0bebe209e349f793", - "sha256:f571aea50ba5623c308aa146eb650eebf7dbe0fd8c5d946e28343cb3b5aad577", - "sha256:f60f0ac7005b9f5a6091009b09a419ace1610e163fa5deaba5ce3484341840e7", - "sha256:f6475a1b2ecb310c98c28d271a30df74f9dd436ee46d09236a6b750a7599ce57", - "sha256:f6d5749fdd33d90e34c2efb174c7e236829147a2713334d708746e94c4bde40d", - "sha256:f902804113e032e2cdf8c71015651c97af6418363bea8d78dc0911d56c335032", - "sha256:fa1076244f54bb76e65e22cb6910365779d5c3d71d1f18b275f1dfc7b0d71b4d", - "sha256:fc2db02409338bf36590aa985a461b2c96fce91f8e7e0f14c50c5fcc4f229016", - "sha256:ffcad6c564fe6b9b8916c1aefbb37a362deebf9394bd2974e9d84232e3e08504" + "sha256:03ff9d3f665769b2a85e6157ac8b439644f2d7fd17615a82fa55739bc97863f4", + "sha256:049324ee97bb67285b49632132db351b41e77833678432be52bdd0289c0e05e4", + "sha256:081a430aa8d5e8876c6909b67bd2d937bfd531b0382d3fdedb82612c618bc41a", + "sha256:0f022d381747f0dfe27e99d928e31bc51a18b65bb9e481ae0af1380a6725dd1f", + "sha256:12d1083f001ace206fe34b6bdc2cb94be66d57a850866f0b908972f90996b3e9", + "sha256:14d86fe14b7e04fa306e0c43cdbeebe6b2c2156a0c9ce56b815faacc193e320d", + "sha256:160291c60081f23ee43d44b08a7e5fb76681221a8e10b3139618c5a9a291b84e", + "sha256:1672137af7c46662a1c2be1e8dc78cb6d224319aaa40271c9257d886be4363a6", + "sha256:19a0f89a7bb9d8048d9c4370c9c543c396e894c76be5525f5e1ad287f1750ddf", + "sha256:1ac2f5fe02fa75f56e1ad473f1175e11f475606ec9bd0be2e78e4734ad575034", + "sha256:1cd9a1d071158de1cc1c71a26014dcdfa7dd3d5f4f88c298c7f90ad6f27bb46d", + "sha256:1ffc3cca89bb438fb9c95c13fc874012f7b9466b89328c3c8b1aa93cdcfadd16", + "sha256:297878dc9d0a334358f9b608b56d02e72899f3b8499fc6044133f0d319e2ec30", + "sha256:2d3af2e79991102678f53e0dbf4c35de99b6b8b58f29a27ca0325816364caaba", + "sha256:30b43e74f1359353341a7adb783c8f1b1c676367b011709f466f42fda2045e95", + "sha256:3156628250f46a0895f1f36e1d4fbe062a1af8718ec3ebeb746f1d23f0c5dc4d", + "sha256:31f5af773530fd3c658b32b6bdc2d0838543de70eb9a2156c03e410f7b0d3aae", + "sha256:3935bfa5fede35fb202c4b569bb9c042f337ca4ff7bd540a0aa5e37131659348", + "sha256:39d51fbe4285d5db5d92a929e3e21536ea3dd43732c5b177c7ef03f918dff9f2", + "sha256:3f77ce728b19cb537714499928fe800c3dda29e8d9428778fc7c186da4c09a64", + "sha256:4160d9283bd382fa6c0c2b5e017acc95bc183570cd70968b9202ad6d8fc48dce", + "sha256:4a571d97dbe66ef38e472703067021b1467025ec85707d57e78711c085984e54", + "sha256:4e6281aedfca15301c41f74d7005e6e3f4ca143584ba696ac69df4f02f40d629", + "sha256:52277518d6aae65536e9cea52d4e7fd2f7a66f4aa2d30ed3f2fcea620ace3c54", + "sha256:556fc6c10989f19a179e4321e5d678db8eb2924131e64652a51fe83e4c3db0e1", + "sha256:574faa3b79e8ebac7cb1d7930f51184ba1ccf69adfdec53a12f319a06030a68b", + "sha256:58791550b27d5488b1bb52bc96328456095d96206a250d28d874fafe11b3dfaf", + "sha256:5b750a8e5a1262434fb1517ddf64b5de58327f1adc3524a5e44c2ca43305eb0b", + "sha256:5d97151bc92d2b2578ff7ce779cdb9174337390a535953cbb9452fb65164c587", + "sha256:5eee736daafa7af6d0a2dc15cc75e05c64f37fc37bafef2e00d77c14171c2097", + "sha256:6445804cf4ec763dc70de65a3b0d9954e868609e83850a47ca4f0cb64bd79fea", + "sha256:647894f5ae99c4cf6bb82a1bb3a796f6e06af3caa3d32e26d2350d0e3e3faf24", + "sha256:66d4cfda1d8ed687daa4bc0274fcfd5267873db9a5bc0418c2da19273040eeb7", + "sha256:6a9a8c34fb7bb609419a211e59da8887eeca40d300b5ea8e56af98f6fbbb1541", + "sha256:6b3f39a85d671436ee3d12c017f8fdea38509e4f25b28eb25877293c98c243f6", + "sha256:6b6fb63ae352e13748289f04f37868099e69dba4c2b3e271c46061e82c745634", + "sha256:70693319e0b8fd35dd863e3e29513875eb15c51945bf32519ef52927ca883bc3", + "sha256:781e65134efaf88feb447e8c97a51772aa75e48b794352f94cb7ea717dedda0d", + "sha256:819ce3b883b7576ca28da3861c7e1a88afd08cc8c96908e08a3f4dd64a228034", + "sha256:857112b22acd417c40fa4595db2fe28ab900c8c5fe4670c7989b1c0230955465", + "sha256:887d9b0a65404929641a9fabb6452b07fe4572b269d901d622d8a34a4e9043b2", + "sha256:8b3489ff1ed1e8315674d0775dc7d2195fb13ca17b3808721b54dbe9fd020faf", + "sha256:92fc4500fcb33899b05ba73276dfb684a20d31caa567b7cb5252d48f896a91b1", + "sha256:9403db39be1393618dd80c746cb22ccda168efce239c73af13c3763ef56ffc04", + "sha256:98110aa363f1bb4c073e8dcfaefd3a5cea0f0834c2aab23dda657e4dab2f53b5", + "sha256:999779addc413181912e984b942fbcc951be1f5b3663cd80b2687758f434c583", + "sha256:9caac6b54914bdf41bcc91e7eb9147d331d29235a7c967c150ef5df6464fd1bb", + "sha256:a7a078f5d37bee6690959c813977da5291b24286e7b962e62a94cec31aa5188b", + "sha256:a7e65eb5c003a303b94aa2c3852ef130230ec79e349632d030e9571b87c4698c", + "sha256:a96dc1fa45bd8c407a0af03b2d5218392729e1822b0c32e62c5bf7eeb5fb3958", + "sha256:aca405706e0b0a44cc6bfd41fbe89919a6a56999157f6de7e182a990c36e37bc", + "sha256:accb6150ce61c9c4b7738d45550806aa2b71c7668c6942f17b0ac182b6142fd4", + "sha256:ad1af54a62ffe39cf34db1aa6ed1a1873bd548f6401db39d8e7cd060b9211f82", + "sha256:ae1aa1cd222c6d205853b3013c69cd04515f9d6ab6de4b0603e2e1c33221303e", + "sha256:b2d0a12018b04f4cb820781ec0dffb5f7c7c1d2a5cd22bff7fb055a2cb19ebce", + "sha256:b480c6a4e1138e1aa137c0079b9b6305ec6dcc1098a8ca5196283e8a49df95a9", + "sha256:b74c261802d3d2b85c9df2dfb2fa81b6f90deeef63c2db9f0e029a3cac50b518", + "sha256:ba278acf14471d36316159c94a802933d10b6a1e117b8554fe0d0d9b75c9d536", + "sha256:bb6178c241278d5fe853b3de743087be7f5f4c6f7d6d22a3b524d323eecec505", + "sha256:bf72af5e0fb40e9babf594308911436c8efde3cb5e75b6f206c34ad18be5c052", + "sha256:bfd3223c15bebe26518d58ccf9a39b93948d3dcb3e57a20480dfdd315356baff", + "sha256:c214999039d4f2a5b2073ac506bba279945233da8c786e490d411dfc30f855c1", + "sha256:c2f992c07c0fca81655066705beae35fc95a2fa7366467366db627d9f2ee097f", + "sha256:cba4cfa1052819d16699e1d55d18c92b6e094d4517c41dd231a8b9f87b6fa681", + "sha256:cea7daf9fc7ae6687cf1e2c049752f19f146fdc37c2cc376e7d0032cf4f25347", + "sha256:cf6c4150f8c0e32d241436526f3c3f9cbd34429492abddbada2ffcff506c51af", + "sha256:d09c333d36c1409d56a9d29b3a1b800a42c76a57a5a8907eacdbce3f18768246", + "sha256:d27b84d5880f6d8aa9ae3edb253c59d9f6642ffbb2c889b78b60361eed449787", + "sha256:d2ccec9ac47cf4e04897619c0e0c1a48c54a71bdf045117d3a26f80d38ab1fb0", + "sha256:d71264a80f3fcf512eb4f18f59423fe82d6e346ee97b90625f283df56aee103f", + "sha256:d93f3307ad32a27bda2e88ec81134b823c240aa3abb55821a8da553eed8d9439", + "sha256:d9631c5e8b5b3a0fda99cb0d29c18133bca1e18aea9effe55adb3da1adef80d3", + "sha256:ddfab44e4489bd79bda09d84c430677fc7f0a4939a73d2bba3073036f487a0a6", + "sha256:e7048abd75fe40712005bcfc06bb44b9dfcd8e101dda2ecf2f5aa46115ad07ca", + "sha256:e73091191e4280403bde6c9a52a6999d69cdfde498f1fdf629105247599b57ec", + "sha256:e800776a79a5aabdb17dcc2346a7d66d0777e942e4cd251defeb084762ecd17d", + "sha256:edc9fc7051e3350643ad929df55c451899bb9ae6d24998a949d2e4c87fb596d3", + "sha256:f089118d584e859c62b3da0892b88a83d611c2033ac410e929cb6754eec0ed16", + "sha256:f174bbd484294ed9fdf09437f889f95807e5f229d5d93588d34e92106fbf6717", + "sha256:f508b0491767bb1f2b87fdfacaba5f7eddc2f867740ec69ece6d1946d29029a6", + "sha256:f7a31fc1e1bd362874863fdeed71aed92d348f5336fd84f2197ba40c59f061bd", + "sha256:f9479aa06a793c5aeba49ce5c5692ffb51fcd9a7016e017d555d5e2b0045d212" ], - "markers": "python_version >= '3.8'", - "version": "==0.2.0" + "markers": "python_version >= '3.9'", + "version": "==0.2.1" }, "proto-plus": { "hashes": [ - "sha256:30b72a5ecafe4406b0d339db35b56c4059064e69227b8c3bda7462397f966445", - "sha256:402576830425e5f6ce4c2a6702400ac79897dab0b4343821aa5188b0fab81a12" + "sha256:c91fc4a65074ade8e458e95ef8bac34d4008daa7cce4a12d6707066fca648961", + "sha256:fbb17f57f7bd05a68b7707e745e26528b0b3c34e378db91eef93912c54982d91" ], "markers": "python_version >= '3.7'", - "version": "==1.24.0" + "version": "==1.25.0" }, "protobuf": { "hashes": [ - "sha256:0c4eec6f987338617072592b97943fdbe30d019c56126493111cf24344c1cc24", - "sha256:135658402f71bbd49500322c0f736145731b16fc79dc8f367ab544a17eab4535", - "sha256:27b246b3723692bf1068d5734ddaf2fccc2cdd6e0c9b47fe099244d80200593b", - "sha256:3e6101d095dfd119513cde7259aa703d16c6bbdfae2554dfe5cfdbe94e32d548", - "sha256:3fa2de6b8b29d12c61911505d893afe7320ce7ccba4df913e2971461fa36d584", - "sha256:64badbc49180a5e401f373f9ce7ab1d18b63f7dd4a9cdc43c92b9f0b481cef7b", - "sha256:70585a70fc2dd4818c51287ceef5bdba6387f88a578c86d47bb34669b5552c36", - "sha256:712319fbdddb46f21abb66cd33cb9e491a5763b2febd8f228251add221981135", - "sha256:91fba8f445723fcf400fdbe9ca796b19d3b1242cd873907979b9ed71e4afe868", - "sha256:a3f6857551e53ce35e60b403b8a27b0295f7d6eb63d10484f12bc6879c715687", - "sha256:cee1757663fa32a1ee673434fcf3bf24dd54763c79690201208bafec62f19eed" + "sha256:012ce28d862ff417fd629285aca5d9772807f15ceb1a0dbd15b88f58c776c98c", + "sha256:027fbcc48cea65a6b17028510fdd054147057fa78f4772eb547b9274e5219331", + "sha256:1fc55267f086dd4050d18ef839d7bd69300d0d08c2a53ca7df3920cc271a3c34", + "sha256:22c1f539024241ee545cbcb00ee160ad1877975690b16656ff87dde107b5f110", + "sha256:32600ddb9c2a53dedc25b8581ea0f1fd8ea04956373c0c07577ce58d312522e0", + "sha256:50879eb0eb1246e3a5eabbbe566b44b10348939b7cc1b267567e8c3d07213853", + "sha256:5a41deccfa5e745cef5c65a560c76ec0ed8e70908a67cc8f4da5fce588b50d57", + "sha256:683be02ca21a6ffe80db6dd02c0b5b2892322c59ca57fd6c872d652cb80549cb", + "sha256:8ee1461b3af56145aca2800e6a3e2f928108c749ba8feccc6f5dd0062c410c0d", + "sha256:b5ba1d0e4c8a40ae0496d0e2ecfdbb82e1776928a205106d14ad6985a09ec155", + "sha256:d473655e29c0c4bbf8b69e9a8fb54645bc289dead6d753b952e7aa660254ae18" ], "markers": "python_version >= '3.8'", - "version": "==5.28.3" + "version": "==5.29.1" }, "pyasn1": { "hashes": [ @@ -1045,52 +1090,125 @@ }, "pydantic": { "hashes": [ - "sha256:069b9c9fc645474d5ea3653788b544a9e0ccd3dca3ad8c900c4c6eac844b4620", - "sha256:06a189b81ffc52746ec9c8c007f16e5167c8b0a696e1a726369327e3db7b2a82", - "sha256:11d9d9b87b50338b1b7de4ebf34fd29fdb0d219dc07ade29effc74d3d2609c62", - "sha256:15fdbe568beaca9aacfccd5ceadfb5f1a235087a127e8af5e48df9d8a45ae85c", - "sha256:19a3bd00b9dafc2cd7250d94d5b578edf7a0bd7daf102617153ff9a8fa37871c", - "sha256:23e8ec1ce4e57b4f441fc91e3c12adba023fedd06868445a5b5f1d48f0ab3682", - "sha256:24a4a159d0f7a8e26bf6463b0d3d60871d6a52eac5bb6a07a7df85c806f4c048", - "sha256:2ce3fcf75b2bae99aa31bd4968de0474ebe8c8258a0110903478bd83dfee4e3b", - "sha256:335a32d72c51a313b33fa3a9b0fe283503272ef6467910338e123f90925f0f03", - "sha256:3445426da503c7e40baccefb2b2989a0c5ce6b163679dd75f55493b460f05a8f", - "sha256:34a3613c7edb8c6fa578e58e9abe3c0f5e7430e0fc34a65a415a1683b9c32d9a", - "sha256:3d5492dbf953d7d849751917e3b2433fb26010d977aa7a0765c37425a4026ff1", - "sha256:44ae8a3e35a54d2e8fa88ed65e1b08967a9ef8c320819a969bfa09ce5528fafe", - "sha256:467a14ee2183bc9c902579bb2f04c3d3dac00eff52e252850509a562255b2a33", - "sha256:46f379b8cb8a3585e3f61bf9ae7d606c70d133943f339d38b76e041ec234953f", - "sha256:49e26c51ca854286bffc22b69787a8d4063a62bf7d83dc21d44d2ff426108518", - "sha256:65f7361a09b07915a98efd17fdec23103307a54db2000bb92095457ca758d485", - "sha256:6951f3f47cb5ca4da536ab161ac0163cab31417d20c54c6de5ddcab8bc813c3f", - "sha256:72fa46abace0a7743cc697dbb830a41ee84c9db8456e8d77a46d79b537efd7ec", - "sha256:74fe19dda960b193b0eb82c1f4d2c8e5e26918d9cda858cbf3f41dd28549cb70", - "sha256:7a4c5eec138a9b52c67f664c7d51d4c7234c5ad65dd8aacd919fb47445a62c86", - "sha256:80b982d42515632eb51f60fa1d217dfe0729f008e81a82d1544cc392e0a50ddf", - "sha256:941a2eb0a1509bd7f31e355912eb33b698eb0051730b2eaf9e70e2e1589cae1d", - "sha256:9f463abafdc92635da4b38807f5b9972276be7c8c5121989768549fceb8d2588", - "sha256:a00e63104346145389b8e8f500bc6a241e729feaf0559b88b8aa513dd2065481", - "sha256:aad8771ec8dbf9139b01b56f66386537c6fe4e76c8f7a47c10261b69ad25c2c9", - "sha256:ae6fa2008e1443c46b7b3a5eb03800121868d5ab6bc7cda20b5df3e133cde8b3", - "sha256:b661ce52c7b5e5f600c0c3c5839e71918346af2ef20062705ae76b5c16914cab", - "sha256:b74be007703547dc52e3c37344d130a7bfacca7df112a9e5ceeb840a9ce195c7", - "sha256:baebdff1907d1d96a139c25136a9bb7d17e118f133a76a2ef3b845e831e3403a", - "sha256:c20f682defc9ef81cd7eaa485879ab29a86a0ba58acf669a78ed868e72bb89e0", - "sha256:c3e742f62198c9eb9201781fbebe64533a3bbf6a76a91b8d438d62b813079dbc", - "sha256:c5ae6b7c8483b1e0bf59e5f1843e4fd8fd405e11df7de217ee65b98eb5462861", - "sha256:c6d0a9f9eccaf7f438671a64acf654ef0d045466e63f9f68a579e2383b63f357", - "sha256:cbfbca662ed3729204090c4d09ee4beeecc1a7ecba5a159a94b5a4eb24e3759a", - "sha256:d5389eb3b48a72da28c6e061a247ab224381435256eb541e175798483368fdd3", - "sha256:e306e280ebebc65040034bff1a0a81fd86b2f4f05daac0131f29541cafd80b80", - "sha256:e405ffcc1254d76bb0e760db101ee8916b620893e6edfbfee563b3c6f7a67c02", - "sha256:e9ee4e6ca1d9616797fa2e9c0bfb8815912c7d67aca96f77428e316741082a1b", - "sha256:ef0fe7ad7cbdb5f372463d42e6ed4ca9c443a52ce544472d8842a0576d830da5", - "sha256:efbc8a7f9cb5fe26122acba1852d8dcd1e125e723727c59dcd244da7bdaa54f2", - "sha256:fcb20d4cb355195c75000a49bb4a31d75e4295200df620f454bbc6bdf60ca890", - "sha256:fe734914977eed33033b70bfc097e1baaffb589517863955430bf2e0846ac30f" + "sha256:be04d85bbc7b65651c5f8e6b9976ed9c6f41782a55524cef079a34a0bb82144d", + "sha256:cb5ac360ce894ceacd69c403187900a02c4b20b693a9dd1d643e1effab9eadf9" ], - "markers": "python_version >= '3.7'", - "version": "==1.10.18" + "markers": "python_version >= '3.8'", + "version": "==2.10.3" + }, + "pydantic-core": { + "hashes": [ + "sha256:00e6424f4b26fe82d44577b4c842d7df97c20be6439e8e685d0d715feceb9fb9", + "sha256:029d9757eb621cc6e1848fa0b0310310de7301057f623985698ed7ebb014391b", + "sha256:02a3d637bd387c41d46b002f0e49c52642281edacd2740e5a42f7017feea3f2c", + "sha256:0325336f348dbee6550d129b1627cb8f5351a9dc91aad141ffb96d4937bd9529", + "sha256:062f60e512fc7fff8b8a9d680ff0ddaaef0193dba9fa83e679c0c5f5fbd018bc", + "sha256:0b3dfe500de26c52abe0477dde16192ac39c98f05bf2d80e76102d394bd13854", + "sha256:0e4216e64d203e39c62df627aa882f02a2438d18a5f21d7f721621f7a5d3611d", + "sha256:121ceb0e822f79163dd4699e4c54f5ad38b157084d97b34de8b232bcaad70278", + "sha256:159cac0a3d096f79ab6a44d77a961917219707e2a130739c64d4dd46281f5c2a", + "sha256:15aae984e46de8d376df515f00450d1522077254ef6b7ce189b38ecee7c9677c", + "sha256:15cc53a3179ba0fcefe1e3ae50beb2784dede4003ad2dfd24f81bba4b23a454f", + "sha256:161c27ccce13b6b0c8689418da3885d3220ed2eae2ea5e9b2f7f3d48f1d52c27", + "sha256:19910754e4cc9c63bc1c7f6d73aa1cfee82f42007e407c0f413695c2f7ed777f", + "sha256:1ba5e3963344ff25fc8c40da90f44b0afca8cfd89d12964feb79ac1411a260ac", + "sha256:1c00666a3bd2f84920a4e94434f5974d7bbc57e461318d6bb34ce9cdbbc1f6b2", + "sha256:1c39b07d90be6b48968ddc8c19e7585052088fd7ec8d568bb31ff64c70ae3c97", + "sha256:206b5cf6f0c513baffaeae7bd817717140770c74528f3e4c3e1cec7871ddd61a", + "sha256:258c57abf1188926c774a4c94dd29237e77eda19462e5bb901d88adcab6af919", + "sha256:2cdf7d86886bc6982354862204ae3b2f7f96f21a3eb0ba5ca0ac42c7b38598b9", + "sha256:2d4567c850905d5eaaed2f7a404e61012a51caf288292e016360aa2b96ff38d4", + "sha256:35c14ac45fcfdf7167ca76cc80b2001205a8d5d16d80524e13508371fb8cdd9c", + "sha256:38de0a70160dd97540335b7ad3a74571b24f1dc3ed33f815f0880682e6880131", + "sha256:3af385b0cee8df3746c3f406f38bcbfdc9041b5c2d5ce3e5fc6637256e60bbc5", + "sha256:3b748c44bb9f53031c8cbc99a8a061bc181c1000c60a30f55393b6e9c45cc5bd", + "sha256:3bbd5d8cc692616d5ef6fbbbd50dbec142c7e6ad9beb66b78a96e9c16729b089", + "sha256:3ccaa88b24eebc0f849ce0a4d09e8a408ec5a94afff395eb69baf868f5183107", + "sha256:3fa80ac2bd5856580e242dbc202db873c60a01b20309c8319b5c5986fbe53ce6", + "sha256:4228b5b646caa73f119b1ae756216b59cc6e2267201c27d3912b592c5e323b60", + "sha256:42b0e23f119b2b456d07ca91b307ae167cc3f6c846a7b169fca5326e32fdc6cf", + "sha256:45cf8588c066860b623cd11c4ba687f8d7175d5f7ef65f7129df8a394c502de5", + "sha256:45d9c5eb9273aa50999ad6adc6be5e0ecea7e09dbd0d31bd0c65a55a2592ca08", + "sha256:4603137322c18eaf2e06a4495f426aa8d8388940f3c457e7548145011bb68e05", + "sha256:46ccfe3032b3915586e469d4972973f893c0a2bb65669194a5bdea9bacc088c2", + "sha256:4fefee876e07a6e9aad7a8c8c9f85b0cdbe7df52b8a9552307b09050f7512c7e", + "sha256:5556470f1a2157031e676f776c2bc20acd34c1990ca5f7e56f1ebf938b9ab57c", + "sha256:57866a76e0b3823e0b56692d1a0bf722bffb324839bb5b7226a7dbd6c9a40b17", + "sha256:5897bec80a09b4084aee23f9b73a9477a46c3304ad1d2d07acca19723fb1de62", + "sha256:58ca98a950171f3151c603aeea9303ef6c235f692fe555e883591103da709b23", + "sha256:5ca038c7f6a0afd0b2448941b6ef9d5e1949e999f9e5517692eb6da58e9d44be", + "sha256:5f6c8a66741c5f5447e047ab0ba7a1c61d1e95580d64bce852e3df1f895c4067", + "sha256:5f8c4718cd44ec1580e180cb739713ecda2bdee1341084c1467802a417fe0f02", + "sha256:5fde892e6c697ce3e30c61b239330fc5d569a71fefd4eb6512fc6caec9dd9e2f", + "sha256:62a763352879b84aa31058fc931884055fd75089cccbd9d58bb6afd01141b235", + "sha256:62ba45e21cf6571d7f716d903b5b7b6d2617e2d5d67c0923dc47b9d41369f840", + "sha256:64c65f40b4cd8b0e049a8edde07e38b476da7e3aaebe63287c899d2cff253fa5", + "sha256:655d7dd86f26cb15ce8a431036f66ce0318648f8853d709b4167786ec2fa4807", + "sha256:66ff044fd0bb1768688aecbe28b6190f6e799349221fb0de0e6f4048eca14c16", + "sha256:672ebbe820bb37988c4d136eca2652ee114992d5d41c7e4858cdd90ea94ffe5c", + "sha256:6b9af86e1d8e4cfc82c2022bfaa6f459381a50b94a29e95dcdda8442d6d83864", + "sha256:6e0bd57539da59a3e4671b90a502da9a28c72322a4f17866ba3ac63a82c4498e", + "sha256:71a5e35c75c021aaf400ac048dacc855f000bdfed91614b4a726f7432f1f3d6a", + "sha256:7597c07fbd11515f654d6ece3d0e4e5093edc30a436c63142d9a4b8e22f19c35", + "sha256:764be71193f87d460a03f1f7385a82e226639732214b402f9aa61f0d025f0737", + "sha256:7699b1df36a48169cdebda7ab5a2bac265204003f153b4bd17276153d997670a", + "sha256:7ccebf51efc61634f6c2344da73e366c75e735960b5654b63d7e6f69a5885fa3", + "sha256:7f7059ca8d64fea7f238994c97d91f75965216bcbe5f695bb44f354893f11d52", + "sha256:8065914ff79f7eab1599bd80406681f0ad08f8e47c880f17b416c9f8f7a26d05", + "sha256:816f5aa087094099fff7edabb5e01cc370eb21aa1a1d44fe2d2aefdfb5599b31", + "sha256:81f2ec23ddc1b476ff96563f2e8d723830b06dceae348ce02914a37cb4e74b89", + "sha256:84286494f6c5d05243456e04223d5a9417d7f443c3b76065e75001beb26f88de", + "sha256:8bf7b66ce12a2ac52d16f776b31d16d91033150266eb796967a7e4621707e4f6", + "sha256:8f1edcea27918d748c7e5e4d917297b2a0ab80cad10f86631e488b7cddf76a36", + "sha256:981fb88516bd1ae8b0cbbd2034678a39dedc98752f264ac9bc5839d3923fa04c", + "sha256:98476c98b02c8e9b2eec76ac4156fd006628b1b2d0ef27e548ffa978393fd154", + "sha256:992cea5f4f3b29d6b4f7f1726ed8ee46c8331c6b4eed6db5b40134c6fe1768bb", + "sha256:9a3b0793b1bbfd4146304e23d90045f2a9b5fd5823aa682665fbdaf2a6c28f3e", + "sha256:9a42d6a8156ff78981f8aa56eb6394114e0dedb217cf8b729f438f643608cbcd", + "sha256:9c10c309e18e443ddb108f0ef64e8729363adbfd92d6d57beec680f6261556f3", + "sha256:9cbd94fc661d2bab2bc702cddd2d3370bbdcc4cd0f8f57488a81bcce90c7a54f", + "sha256:9fdcf339322a3fae5cbd504edcefddd5a50d9ee00d968696846f089b4432cf78", + "sha256:a0697803ed7d4af5e4c1adf1670af078f8fcab7a86350e969f454daf598c4960", + "sha256:a28af0695a45f7060e6f9b7092558a928a28553366519f64083c63a44f70e618", + "sha256:a2e02889071850bbfd36b56fd6bc98945e23670773bc7a76657e90e6b6603c08", + "sha256:a33cd6ad9017bbeaa9ed78a2e0752c5e250eafb9534f308e7a5f7849b0b1bfb4", + "sha256:a3cb37038123447cf0f3ea4c74751f6a9d7afef0eb71aa07bf5f652b5e6a132c", + "sha256:a57847b090d7892f123726202b7daa20df6694cbd583b67a592e856bff603d6c", + "sha256:a5a8e19d7c707c4cadb8c18f5f60c843052ae83c20fa7d44f41594c644a1d330", + "sha256:ac3b20653bdbe160febbea8aa6c079d3df19310d50ac314911ed8cc4eb7f8cb8", + "sha256:ac6c2c45c847bbf8f91930d88716a0fb924b51e0c6dad329b793d670ec5db792", + "sha256:acc07b2cfc5b835444b44a9956846b578d27beeacd4b52e45489e93276241025", + "sha256:aee66be87825cdf72ac64cb03ad4c15ffef4143dbf5c113f64a5ff4f81477bf9", + "sha256:af52d26579b308921b73b956153066481f064875140ccd1dfd4e77db89dbb12f", + "sha256:b94d4ba43739bbe8b0ce4262bcc3b7b9f31459ad120fb595627eaeb7f9b9ca01", + "sha256:ba630d5e3db74c79300d9a5bdaaf6200172b107f263c98a0539eeecb857b2337", + "sha256:bed0f8a0eeea9fb72937ba118f9db0cb7e90773462af7962d382445f3005e5a4", + "sha256:bf99c8404f008750c846cb4ac4667b798a9f7de673ff719d705d9b2d6de49c5f", + "sha256:c3027001c28434e7ca5a6e1e527487051136aa81803ac812be51802150d880dd", + "sha256:c65af9088ac534313e1963443d0ec360bb2b9cba6c2909478d22c2e363d98a51", + "sha256:d0165ab2914379bd56908c02294ed8405c252250668ebcb438a55494c69f44ab", + "sha256:d1b26e1dff225c31897696cab7d4f0a315d4c0d9e8666dbffdb28216f3b17fdc", + "sha256:d950caa237bb1954f1b8c9227b5065ba6875ac9771bb8ec790d956a699b78676", + "sha256:dc61505e73298a84a2f317255fcc72b710b72980f3a1f670447a21efc88f8381", + "sha256:e173486019cc283dc9778315fa29a363579372fe67045e971e89b6365cc035ed", + "sha256:e1f735dc43da318cad19b4173dd1ffce1d84aafd6c9b782b3abc04a0d5a6f5bb", + "sha256:e9386266798d64eeb19dd3677051f5705bf873e98e15897ddb7d76f477131967", + "sha256:f216dbce0e60e4d03e0c4353c7023b202d95cbaeff12e5fd2e82ea0a66905073", + "sha256:f4e5658dbffe8843a0f12366a4c2d1c316dbe09bb4dfbdc9d2d9cd6031de8aae", + "sha256:f5a823165e6d04ccea61a9f0576f345f8ce40ed533013580e087bd4d7442b52c", + "sha256:f69ed81ab24d5a3bd93861c8c4436f54afdf8e8cc421562b0c7504cf3be58206", + "sha256:f82d068a2d6ecfc6e054726080af69a6764a10015467d7d7b9f66d6ed5afa23b" + ], + "markers": "python_version >= '3.8'", + "version": "==2.27.1" + }, + "pydantic-settings": { + "hashes": [ + "sha256:7fb0637c786a558d3103436278a7c4f1cfd29ba8973238a50c5bb9a55387da87", + "sha256:e0f92546d8a9923cb8941689abf85d6601a8c19a23e97a34b2964a2e3f813ca0" + ], + "markers": "python_version >= '3.8'", + "version": "==2.6.1" }, "pynacl": { "hashes": [ @@ -1113,47 +1231,9 @@ "sha256:93d9577b88da0bbea8cc8334ee8b918ed014968fd2ec383e868fb8afb1ccef84", "sha256:cbf74e27246d595d9a74b186b810f6fbb86726dbf3b9532efb343f6d7294fe9c" ], - "markers": "python_version >= '3.1'", + "markers": "python_version >= '3.9'", "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" - }, "pyserial": { "hashes": [ "sha256:3c77e014170dfffbd816e6ffc205e9842efb10be9f58ec16d3e8675b4925cddb", @@ -1174,9 +1254,17 @@ "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", "version": "==2.9.0.post0" }, + "python-dotenv": { + "hashes": [ + "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca", + "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a" + ], + "markers": "python_version >= '3.8'", + "version": "==1.0.1" + }, "pytz": { "hashes": [ "sha256:2aa355083c50a0f93fa581709deac0c9ad65cca8a9e9beac660adcbd493c798a", @@ -1192,29 +1280,13 @@ "markers": "python_full_version >= '3.6.0'", "version": "==1.2.1" }, - "pywin32": { - "hashes": [ - "sha256:00b3e11ef09ede56c6a43c71f2d31857cf7c54b0ab6e78ac659497abd2834f47", - "sha256:100a5442b7332070983c4cd03f2e906a5648a5104b8a7f50175f7906efd16bb6", - "sha256:13dcb914ed4347019fbec6697a01a0aec61019c1046c2b905410d197856326a6", - "sha256:1c44539a37a5b7b21d02ab34e6a4d314e0788f1690d65b48e9b0b89f31abbbed", - "sha256:1f696ab352a2ddd63bd07430080dd598e6369152ea13a25ebcdd2f503a38f1ff", - "sha256:3b92622e29d651c6b783e368ba7d6722b1634b8e70bd376fd7610fe1992e19de", - "sha256:4fc888c59b3c0bef905ce7eb7e2106a07712015ea1c8234b703a088d46110e8e", - "sha256:575621b90f0dc2695fec346b2d6302faebd4f0f45c05ea29404cefe35d89442b", - "sha256:5794e764ebcabf4ff08c555b31bd348c9025929371763b2183172ff4708152f0", - "sha256:587f3e19696f4bf96fde9d8a57cec74a57021ad5f204c9e627e15c33ff568897", - "sha256:5d8c8015b24a7d6855b1550d8e660d8daa09983c80e5daf89a273e5c6fb5095a", - "sha256:71b3322d949b4cc20776436a9c9ba0eeedcbc9c650daa536df63f0ff111bb920", - "sha256:7873ca4dc60ab3287919881a7d4f88baee4a6e639aa6962de25a98ba6b193341", - "sha256:796ff4426437896550d2981b9c2ac0ffd75238ad9ea2d3bfa67a1abd546d262e", - "sha256:9b4de86c8d909aed15b7011182c8cab38c8850de36e6afb1f0db22b8959e3091", - "sha256:a5ab5381813b40f264fa3495b98af850098f814a25a63589a8e9eb12560f450c", - "sha256:ef313c46d4c18dfb82a2431e3051ac8f112ccee1a34f29c263c583c568db63cd", - "sha256:fd380990e792eaf6827fcb7e187b2b4b1cede0585e3d0c9e84201ec27b9905e4" - ], - "markers": "platform_system == 'Windows' and platform_python_implementation == 'CPython'", - "version": "==308" + "referencing": { + "hashes": [ + "sha256:25b42124a6c8b632a425174f24087783efb348a6f1e0008e63cd4466fedf703c", + "sha256:eda6d3234d62814d1c64e305c1331c9a3a6132da475ab6382eaa997b21ee75de" + ], + "markers": "python_version >= '3.8'", + "version": "==0.35.1" }, "requests": { "hashes": [ @@ -1232,6 +1304,115 @@ "markers": "python_version >= '3.4'", "version": "==2.0.0" }, + "rpds-py": { + "hashes": [ + "sha256:009de23c9c9ee54bf11303a966edf4d9087cd43a6003672e6aa7def643d06518", + "sha256:02fbb9c288ae08bcb34fb41d516d5eeb0455ac35b5512d03181d755d80810059", + "sha256:0a0461200769ab3b9ab7e513f6013b7a97fdeee41c29b9db343f3c5a8e2b9e61", + "sha256:0b09865a9abc0ddff4e50b5ef65467cd94176bf1e0004184eb915cbc10fc05c5", + "sha256:0b8db6b5b2d4491ad5b6bdc2bc7c017eec108acbf4e6785f42a9eb0ba234f4c9", + "sha256:0c150c7a61ed4a4f4955a96626574e9baf1adf772c2fb61ef6a5027e52803543", + "sha256:0f3cec041684de9a4684b1572fe28c7267410e02450f4561700ca5a3bc6695a2", + "sha256:1352ae4f7c717ae8cba93421a63373e582d19d55d2ee2cbb184344c82d2ae55a", + "sha256:177c7c0fce2855833819c98e43c262007f42ce86651ffbb84f37883308cb0e7d", + "sha256:1978d0021e943aae58b9b0b196fb4895a25cc53d3956b8e35e0b7682eefb6d56", + "sha256:1a60bce91f81ddaac922a40bbb571a12c1070cb20ebd6d49c48e0b101d87300d", + "sha256:1aef18820ef3e4587ebe8b3bc9ba6e55892a6d7b93bac6d29d9f631a3b4befbd", + "sha256:1e9663daaf7a63ceccbbb8e3808fe90415b0757e2abddbfc2e06c857bf8c5e2b", + "sha256:20070c65396f7373f5df4005862fa162db5d25d56150bddd0b3e8214e8ef45b4", + "sha256:214b7a953d73b5e87f0ebece4a32a5bd83c60a3ecc9d4ec8f1dca968a2d91e99", + "sha256:22bebe05a9ffc70ebfa127efbc429bc26ec9e9b4ee4d15a740033efda515cf3d", + "sha256:24e8abb5878e250f2eb0d7859a8e561846f98910326d06c0d51381fed59357bd", + "sha256:26fd7cac7dd51011a245f29a2cc6489c4608b5a8ce8d75661bb4a1066c52dfbe", + "sha256:27b1d3b3915a99208fee9ab092b8184c420f2905b7d7feb4aeb5e4a9c509b8a1", + "sha256:27e98004595899949bd7a7b34e91fa7c44d7a97c40fcaf1d874168bb652ec67e", + "sha256:2b8f60e1b739a74bab7e01fcbe3dddd4657ec685caa04681df9d562ef15b625f", + "sha256:2de29005e11637e7a2361fa151f780ff8eb2543a0da1413bb951e9f14b699ef3", + "sha256:2e8b55d8517a2fda8d95cb45d62a5a8bbf9dd0ad39c5b25c8833efea07b880ca", + "sha256:2fa4331c200c2521512595253f5bb70858b90f750d39b8cbfd67465f8d1b596d", + "sha256:3445e07bf2e8ecfeef6ef67ac83de670358abf2996916039b16a218e3d95e97e", + "sha256:3453e8d41fe5f17d1f8e9c383a7473cd46a63661628ec58e07777c2fff7196dc", + "sha256:378753b4a4de2a7b34063d6f95ae81bfa7b15f2c1a04a9518e8644e81807ebea", + "sha256:3af6e48651c4e0d2d166dc1b033b7042ea3f871504b6805ba5f4fe31581d8d38", + "sha256:3dfcbc95bd7992b16f3f7ba05af8a64ca694331bd24f9157b49dadeeb287493b", + "sha256:3f21f0495edea7fdbaaa87e633a8689cd285f8f4af5c869f27bc8074638ad69c", + "sha256:4041711832360a9b75cfb11b25a6a97c8fb49c07b8bd43d0d02b45d0b499a4ff", + "sha256:44d61b4b7d0c2c9ac019c314e52d7cbda0ae31078aabd0f22e583af3e0d79723", + "sha256:4617e1915a539a0d9a9567795023de41a87106522ff83fbfaf1f6baf8e85437e", + "sha256:4b232061ca880db21fa14defe219840ad9b74b6158adb52ddf0e87bead9e8493", + "sha256:5246b14ca64a8675e0a7161f7af68fe3e910e6b90542b4bfb5439ba752191df6", + "sha256:5725dd9cc02068996d4438d397e255dcb1df776b7ceea3b9cb972bdb11260a83", + "sha256:583f6a1993ca3369e0f80ba99d796d8e6b1a3a2a442dd4e1a79e652116413091", + "sha256:59259dc58e57b10e7e18ce02c311804c10c5a793e6568f8af4dead03264584d1", + "sha256:593eba61ba0c3baae5bc9be2f5232430453fb4432048de28399ca7376de9c627", + "sha256:59f4a79c19232a5774aee369a0c296712ad0e77f24e62cad53160312b1c1eaa1", + "sha256:5f0e260eaf54380380ac3808aa4ebe2d8ca28b9087cf411649f96bad6900c728", + "sha256:62d9cfcf4948683a18a9aff0ab7e1474d407b7bab2ca03116109f8464698ab16", + "sha256:64607d4cbf1b7e3c3c8a14948b99345eda0e161b852e122c6bb71aab6d1d798c", + "sha256:655ca44a831ecb238d124e0402d98f6212ac527a0ba6c55ca26f616604e60a45", + "sha256:666ecce376999bf619756a24ce15bb14c5bfaf04bf00abc7e663ce17c3f34fe7", + "sha256:68049202f67380ff9aa52f12e92b1c30115f32e6895cd7198fa2a7961621fc5a", + "sha256:69803198097467ee7282750acb507fba35ca22cc3b85f16cf45fb01cb9097730", + "sha256:6c7b99ca52c2c1752b544e310101b98a659b720b21db00e65edca34483259967", + "sha256:6dd9412824c4ce1aca56c47b0991e65bebb7ac3f4edccfd3f156150c96a7bf25", + "sha256:70eb60b3ae9245ddea20f8a4190bd79c705a22f8028aaf8bbdebe4716c3fab24", + "sha256:70fb28128acbfd264eda9bf47015537ba3fe86e40d046eb2963d75024be4d055", + "sha256:7b2513ba235829860b13faa931f3b6846548021846ac808455301c23a101689d", + "sha256:7ef9d9da710be50ff6809fed8f1963fecdfecc8b86656cadfca3bc24289414b0", + "sha256:81e69b0a0e2537f26d73b4e43ad7bc8c8efb39621639b4434b76a3de50c6966e", + "sha256:8633e471c6207a039eff6aa116e35f69f3156b3989ea3e2d755f7bc41754a4a7", + "sha256:8bd7c8cfc0b8247c8799080fbff54e0b9619e17cdfeb0478ba7295d43f635d7c", + "sha256:9253fc214112405f0afa7db88739294295f0e08466987f1d70e29930262b4c8f", + "sha256:99b37292234e61325e7a5bb9689e55e48c3f5f603af88b1642666277a81f1fbd", + "sha256:9bd7228827ec7bb817089e2eb301d907c0d9827a9e558f22f762bb690b131652", + "sha256:9beeb01d8c190d7581a4d59522cd3d4b6887040dcfc744af99aa59fef3e041a8", + "sha256:a63cbdd98acef6570c62b92a1e43266f9e8b21e699c363c0fef13bd530799c11", + "sha256:a76e42402542b1fae59798fab64432b2d015ab9d0c8c47ba7addddbaf7952333", + "sha256:ac0a03221cdb5058ce0167ecc92a8c89e8d0decdc9e99a2ec23380793c4dcb96", + "sha256:b0b4136a252cadfa1adb705bb81524eee47d9f6aab4f2ee4fa1e9d3cd4581f64", + "sha256:b25bc607423935079e05619d7de556c91fb6adeae9d5f80868dde3468657994b", + "sha256:b3d504047aba448d70cf6fa22e06cb09f7cbd761939fdd47604f5e007675c24e", + "sha256:bb47271f60660803ad11f4c61b42242b8c1312a31c98c578f79ef9387bbde21c", + "sha256:bbb232860e3d03d544bc03ac57855cd82ddf19c7a07651a7c0fdb95e9efea8b9", + "sha256:bc27863442d388870c1809a87507727b799c8460573cfbb6dc0eeaef5a11b5ec", + "sha256:bc51abd01f08117283c5ebf64844a35144a0843ff7b2983e0648e4d3d9f10dbb", + "sha256:be2eb3f2495ba669d2a985f9b426c1797b7d48d6963899276d22f23e33d47e37", + "sha256:bf9db5488121b596dbfc6718c76092fda77b703c1f7533a226a5a9f65248f8ad", + "sha256:c58e2339def52ef6b71b8f36d13c3688ea23fa093353f3a4fee2556e62086ec9", + "sha256:cfbc454a2880389dbb9b5b398e50d439e2e58669160f27b60e5eca11f68ae17c", + "sha256:cff63a0272fcd259dcc3be1657b07c929c466b067ceb1c20060e8d10af56f5bf", + "sha256:d115bffdd417c6d806ea9069237a4ae02f513b778e3789a359bc5856e0404cc4", + "sha256:d20cfb4e099748ea39e6f7b16c91ab057989712d31761d3300d43134e26e165f", + "sha256:d48424e39c2611ee1b84ad0f44fb3b2b53d473e65de061e3f460fc0be5f1939d", + "sha256:e0fa2d4ec53dc51cf7d3bb22e0aa0143966119f42a0c3e4998293a3dd2856b09", + "sha256:e32fee8ab45d3c2db6da19a5323bc3362237c8b653c70194414b892fd06a080d", + "sha256:e35ba67d65d49080e8e5a1dd40101fccdd9798adb9b050ff670b7d74fa41c566", + "sha256:e3fb866d9932a3d7d0c82da76d816996d1667c44891bd861a0f97ba27e84fc74", + "sha256:e61b02c3f7a1e0b75e20c3978f7135fd13cb6cf551bf4a6d29b999a88830a338", + "sha256:e67ba3c290821343c192f7eae1d8fd5999ca2dc99994114643e2f2d3e6138b15", + "sha256:e79dd39f1e8c3504be0607e5fc6e86bb60fe3584bec8b782578c3b0fde8d932c", + "sha256:e89391e6d60251560f0a8f4bd32137b077a80d9b7dbe6d5cab1cd80d2746f648", + "sha256:ea7433ce7e4bfc3a85654aeb6747babe3f66eaf9a1d0c1e7a4435bbdf27fea84", + "sha256:eaf16ae9ae519a0e237a0f528fd9f0197b9bb70f40263ee57ae53c2b8d48aeb3", + "sha256:eb0c341fa71df5a4595f9501df4ac5abfb5a09580081dffbd1ddd4654e6e9123", + "sha256:f276b245347e6e36526cbd4a266a417796fc531ddf391e43574cf6466c492520", + "sha256:f47ad3d5f3258bd7058d2d506852217865afefe6153a36eb4b6928758041d831", + "sha256:f56a6b404f74ab372da986d240e2e002769a7d7102cc73eb238a4f72eec5284e", + "sha256:f5cf2a0c2bdadf3791b5c205d55a37a54025c6e18a71c71f82bb536cf9a454bf", + "sha256:f5d36399a1b96e1a5fdc91e0522544580dbebeb1f77f27b2b0ab25559e103b8b", + "sha256:f60bd8423be1d9d833f230fdbccf8f57af322d96bcad6599e5a771b151398eb2", + "sha256:f612463ac081803f243ff13cccc648578e2279295048f2a8d5eb430af2bae6e3", + "sha256:f73d3fef726b3243a811121de45193c0ca75f6407fe66f3f4e183c983573e130", + "sha256:f82a116a1d03628a8ace4859556fb39fd1424c933341a08ea3ed6de1edb0283b", + "sha256:fb0ba113b4983beac1a2eb16faffd76cb41e176bf58c4afe3e14b9c681f702de", + "sha256:fb4f868f712b2dd4bcc538b0a0c1f63a2b1d584c925e69a224d759e7070a12d5", + "sha256:fb6116dfb8d1925cbdb52595560584db42a7f664617a1f7d7f6e32f138cdf37d", + "sha256:fda7cb070f442bf80b642cd56483b5548e43d366fe3f39b98e67cce780cded00", + "sha256:feea821ee2a9273771bae61194004ee2fc33f8ec7db08117ef9147d4bbcbca8e" + ], + "markers": "python_version >= '3.9'", + "version": "==0.22.3" + }, "rsa": { "hashes": [ "sha256:90260d9058e514786967344d0ef75fa8727eed8a7d2e43ce9f4bcf1b536174f7", @@ -1242,28 +1423,28 @@ }, "setuptools": { "hashes": [ - "sha256:753bb6ebf1f465a1912e19ed1d41f403a79173a9acf66a42e7e6aec45c3c16ec", - "sha256:a7fcb66f68b4d9e8e66b42f9876150a3371558f98fa32222ffaa5bced76406f8" + "sha256:8199222558df7c86216af4f84c30e9b34a61d8ba19366cc914424cdbd28252f6", + "sha256:ce74b49e8f7110f9bf04883b730f4765b774ef3ef28f722cce7c273d253aaf7d" ], - "markers": "python_version >= '3.8'", - "version": "==75.2.0" + "markers": "python_version >= '3.9'", + "version": "==75.6.0" }, "six": { "hashes": [ - "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", - "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" + "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", + "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==1.16.0" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", + "version": "==1.17.0" }, "slack-sdk": { "hashes": [ - "sha256:e328bb661d95db5f66b993b1d64288ac7c72201a745b4c7cf8848dafb7b74e40", - "sha256:ef93beec3ce9c8f64da02fd487598a05ec4bc9c92ceed58f122dbe632691cbe2" + "sha256:a5e74c00c99dc844ad93e501ab764a20d86fa8184bbc9432af217496f632c4ee", + "sha256:b8cccadfa3d4005a5e6529f52000d25c583f46173fda8e9136fdd2bc58923ff6" ], "index": "pypi", "markers": "python_version >= '3.6'", - "version": "==3.33.1" + "version": "==3.33.5" }, "slackclient": { "hashes": [ @@ -1340,167 +1521,162 @@ }, "wrapt": { "hashes": [ - "sha256:0d2691979e93d06a95a26257adb7bfd0c93818e89b1406f5a28f36e0d8c1e1fc", - "sha256:14d7dc606219cdd7405133c713f2c218d4252f2a469003f8c46bb92d5d095d81", - "sha256:1a5db485fe2de4403f13fafdc231b0dbae5eca4359232d2efc79025527375b09", - "sha256:1acd723ee2a8826f3d53910255643e33673e1d11db84ce5880675954183ec47e", - "sha256:1ca9b6085e4f866bd584fb135a041bfc32cab916e69f714a7d1d397f8c4891ca", - "sha256:1dd50a2696ff89f57bd8847647a1c363b687d3d796dc30d4dd4a9d1689a706f0", - "sha256:2076fad65c6736184e77d7d4729b63a6d1ae0b70da4868adeec40989858eb3fb", - "sha256:2a88e6010048489cda82b1326889ec075a8c856c2e6a256072b28eaee3ccf487", - "sha256:3ebf019be5c09d400cf7b024aa52b1f3aeebeff51550d007e92c3c1c4afc2a40", - "sha256:418abb18146475c310d7a6dc71143d6f7adec5b004ac9ce08dc7a34e2babdc5c", - "sha256:43aa59eadec7890d9958748db829df269f0368521ba6dc68cc172d5d03ed8060", - "sha256:44a2754372e32ab315734c6c73b24351d06e77ffff6ae27d2ecf14cf3d229202", - "sha256:490b0ee15c1a55be9c1bd8609b8cecd60e325f0575fc98f50058eae366e01f41", - "sha256:49aac49dc4782cb04f58986e81ea0b4768e4ff197b57324dcbd7699c5dfb40b9", - "sha256:5eb404d89131ec9b4f748fa5cfb5346802e5ee8836f57d516576e61f304f3b7b", - "sha256:5f15814a33e42b04e3de432e573aa557f9f0f56458745c2074952f564c50e664", - "sha256:5f370f952971e7d17c7d1ead40e49f32345a7f7a5373571ef44d800d06b1899d", - "sha256:66027d667efe95cc4fa945af59f92c5a02c6f5bb6012bff9e60542c74c75c362", - "sha256:66dfbaa7cfa3eb707bbfcd46dab2bc6207b005cbc9caa2199bcbc81d95071a00", - "sha256:685f568fa5e627e93f3b52fda002c7ed2fa1800b50ce51f6ed1d572d8ab3e7fc", - "sha256:6906c4100a8fcbf2fa735f6059214bb13b97f75b1a61777fcf6432121ef12ef1", - "sha256:6a42cd0cfa8ffc1915aef79cb4284f6383d8a3e9dcca70c445dcfdd639d51267", - "sha256:6dcfcffe73710be01d90cae08c3e548d90932d37b39ef83969ae135d36ef3956", - "sha256:6f6eac2360f2d543cc875a0e5efd413b6cbd483cb3ad7ebf888884a6e0d2e966", - "sha256:72554a23c78a8e7aa02abbd699d129eead8b147a23c56e08d08dfc29cfdddca1", - "sha256:73870c364c11f03ed072dda68ff7aea6d2a3a5c3fe250d917a429c7432e15228", - "sha256:73aa7d98215d39b8455f103de64391cb79dfcad601701a3aa0dddacf74911d72", - "sha256:75ea7d0ee2a15733684badb16de6794894ed9c55aa5e9903260922f0482e687d", - "sha256:7bd2d7ff69a2cac767fbf7a2b206add2e9a210e57947dd7ce03e25d03d2de292", - "sha256:807cc8543a477ab7422f1120a217054f958a66ef7314f76dd9e77d3f02cdccd0", - "sha256:8e9723528b9f787dc59168369e42ae1c3b0d3fadb2f1a71de14531d321ee05b0", - "sha256:9090c9e676d5236a6948330e83cb89969f433b1943a558968f659ead07cb3b36", - "sha256:9153ed35fc5e4fa3b2fe97bddaa7cbec0ed22412b85bcdaf54aeba92ea37428c", - "sha256:9159485323798c8dc530a224bd3ffcf76659319ccc7bbd52e01e73bd0241a0c5", - "sha256:941988b89b4fd6b41c3f0bfb20e92bd23746579736b7343283297c4c8cbae68f", - "sha256:94265b00870aa407bd0cbcfd536f17ecde43b94fb8d228560a1e9d3041462d73", - "sha256:98b5e1f498a8ca1858a1cdbffb023bfd954da4e3fa2c0cb5853d40014557248b", - "sha256:9b201ae332c3637a42f02d1045e1d0cccfdc41f1f2f801dafbaa7e9b4797bfc2", - "sha256:a0ea261ce52b5952bf669684a251a66df239ec6d441ccb59ec7afa882265d593", - "sha256:a33a747400b94b6d6b8a165e4480264a64a78c8a4c734b62136062e9a248dd39", - "sha256:a452f9ca3e3267cd4d0fcf2edd0d035b1934ac2bd7e0e57ac91ad6b95c0c6389", - "sha256:a86373cf37cd7764f2201b76496aba58a52e76dedfaa698ef9e9688bfd9e41cf", - "sha256:ac83a914ebaf589b69f7d0a1277602ff494e21f4c2f743313414378f8f50a4cf", - "sha256:aefbc4cb0a54f91af643660a0a150ce2c090d3652cf4052a5397fb2de549cd89", - "sha256:b3646eefa23daeba62643a58aac816945cadc0afaf21800a1421eeba5f6cfb9c", - "sha256:b47cfad9e9bbbed2339081f4e346c93ecd7ab504299403320bf85f7f85c7d46c", - "sha256:b935ae30c6e7400022b50f8d359c03ed233d45b725cfdd299462f41ee5ffba6f", - "sha256:bb2dee3874a500de01c93d5c71415fcaef1d858370d405824783e7a8ef5db440", - "sha256:bc57efac2da352a51cc4658878a68d2b1b67dbe9d33c36cb826ca449d80a8465", - "sha256:bf5703fdeb350e36885f2875d853ce13172ae281c56e509f4e6eca049bdfb136", - "sha256:c31f72b1b6624c9d863fc095da460802f43a7c6868c5dda140f51da24fd47d7b", - "sha256:c5cd603b575ebceca7da5a3a251e69561bec509e0b46e4993e1cac402b7247b8", - "sha256:d2efee35b4b0a347e0d99d28e884dfd82797852d62fcd7ebdeee26f3ceb72cf3", - "sha256:d462f28826f4657968ae51d2181a074dfe03c200d6131690b7d65d55b0f360f8", - "sha256:d5e49454f19ef621089e204f862388d29e6e8d8b162efce05208913dde5b9ad6", - "sha256:da4813f751142436b075ed7aa012a8778aa43a99f7b36afe9b742d3ed8bdc95e", - "sha256:db2e408d983b0e61e238cf579c09ef7020560441906ca990fe8412153e3b291f", - "sha256:db98ad84a55eb09b3c32a96c576476777e87c520a34e2519d3e59c44710c002c", - "sha256:dbed418ba5c3dce92619656802cc5355cb679e58d0d89b50f116e4a9d5a9603e", - "sha256:dcdba5c86e368442528f7060039eda390cc4091bfd1dca41e8046af7c910dda8", - "sha256:decbfa2f618fa8ed81c95ee18a387ff973143c656ef800c9f24fb7e9c16054e2", - "sha256:e4fdb9275308292e880dcbeb12546df7f3e0f96c6b41197e0cf37d2826359020", - "sha256:eb1b046be06b0fce7249f1d025cd359b4b80fc1c3e24ad9eca33e0dcdb2e4a35", - "sha256:eb6e651000a19c96f452c85132811d25e9264d836951022d6e81df2fff38337d", - "sha256:ed867c42c268f876097248e05b6117a65bcd1e63b779e916fe2e33cd6fd0d3c3", - "sha256:edfad1d29c73f9b863ebe7082ae9321374ccb10879eeabc84ba3b69f2579d537", - "sha256:f2058f813d4f2b5e3a9eb2eb3faf8f1d99b81c3e51aeda4b168406443e8ba809", - "sha256:f6b2d0c6703c988d334f297aa5df18c45e97b0af3679bb75059e0e0bd8b1069d", - "sha256:f8212564d49c50eb4565e502814f694e240c55551a5f1bc841d4fcaabb0a9b8a", - "sha256:ffa565331890b90056c01db69c0fe634a776f8019c143a5ae265f9c6bc4bd6d4" + "sha256:0229b247b0fc7dee0d36176cbb79dbaf2a9eb7ecc50ec3121f40ef443155fb1d", + "sha256:0698d3a86f68abc894d537887b9bbf84d29bcfbc759e23f4644be27acf6da301", + "sha256:0a0a1a1ec28b641f2a3a2c35cbe86c00051c04fffcfcc577ffcdd707df3f8635", + "sha256:0b48554952f0f387984da81ccfa73b62e52817a4386d070c75e4db7d43a28c4a", + "sha256:0f2a28eb35cf99d5f5bd12f5dd44a0f41d206db226535b37b0c60e9da162c3ed", + "sha256:140ea00c87fafc42739bd74a94a5a9003f8e72c27c47cd4f61d8e05e6dec8721", + "sha256:16187aa2317c731170a88ef35e8937ae0f533c402872c1ee5e6d079fcf320801", + "sha256:17fcf043d0b4724858f25b8826c36e08f9fb2e475410bece0ec44a22d533da9b", + "sha256:18b956061b8db634120b58f668592a772e87e2e78bc1f6a906cfcaa0cc7991c1", + "sha256:2399408ac33ffd5b200480ee858baa58d77dd30e0dd0cab6a8a9547135f30a88", + "sha256:2a0c23b8319848426f305f9cb0c98a6e32ee68a36264f45948ccf8e7d2b941f8", + "sha256:2dfb7cff84e72e7bf975b06b4989477873dcf160b2fd89959c629535df53d4e0", + "sha256:2f495b6754358979379f84534f8dd7a43ff8cff2558dcdea4a148a6e713a758f", + "sha256:33539c6f5b96cf0b1105a0ff4cf5db9332e773bb521cc804a90e58dc49b10578", + "sha256:3c34f6896a01b84bab196f7119770fd8466c8ae3dfa73c59c0bb281e7b588ce7", + "sha256:498fec8da10e3e62edd1e7368f4b24aa362ac0ad931e678332d1b209aec93045", + "sha256:4d63f4d446e10ad19ed01188d6c1e1bb134cde8c18b0aa2acfd973d41fcc5ada", + "sha256:4e4b4385363de9052dac1a67bfb535c376f3d19c238b5f36bddc95efae15e12d", + "sha256:4e547b447073fc0dbfcbff15154c1be8823d10dab4ad401bdb1575e3fdedff1b", + "sha256:4f643df3d4419ea3f856c5c3f40fec1d65ea2e89ec812c83f7767c8730f9827a", + "sha256:4f763a29ee6a20c529496a20a7bcb16a73de27f5da6a843249c7047daf135977", + "sha256:5ae271862b2142f4bc687bdbfcc942e2473a89999a54231aa1c2c676e28f29ea", + "sha256:5d8fd17635b262448ab8f99230fe4dac991af1dabdbb92f7a70a6afac8a7e346", + "sha256:69c40d4655e078ede067a7095544bcec5a963566e17503e75a3a3e0fe2803b13", + "sha256:69d093792dc34a9c4c8a70e4973a3361c7a7578e9cd86961b2bbf38ca71e4e22", + "sha256:6a9653131bda68a1f029c52157fd81e11f07d485df55410401f745007bd6d339", + "sha256:6ff02a91c4fc9b6a94e1c9c20f62ea06a7e375f42fe57587f004d1078ac86ca9", + "sha256:714c12485aa52efbc0fc0ade1e9ab3a70343db82627f90f2ecbc898fdf0bb181", + "sha256:7264cbb4a18dc4acfd73b63e4bcfec9c9802614572025bdd44d0721983fc1d9c", + "sha256:73a96fd11d2b2e77d623a7f26e004cc31f131a365add1ce1ce9a19e55a1eef90", + "sha256:74bf625b1b4caaa7bad51d9003f8b07a468a704e0644a700e936c357c17dd45a", + "sha256:81b1289e99cf4bad07c23393ab447e5e96db0ab50974a280f7954b071d41b489", + "sha256:8425cfce27b8b20c9b89d77fb50e368d8306a90bf2b6eef2cdf5cd5083adf83f", + "sha256:875d240fdbdbe9e11f9831901fb8719da0bd4e6131f83aa9f69b96d18fae7504", + "sha256:879591c2b5ab0a7184258274c42a126b74a2c3d5a329df16d69f9cee07bba6ea", + "sha256:89fc28495896097622c3fc238915c79365dd0ede02f9a82ce436b13bd0ab7569", + "sha256:8a5e7cc39a45fc430af1aefc4d77ee6bad72c5bcdb1322cfde852c15192b8bd4", + "sha256:8f8909cdb9f1b237786c09a810e24ee5e15ef17019f7cecb207ce205b9b5fcce", + "sha256:914f66f3b6fc7b915d46c1cc424bc2441841083de01b90f9e81109c9759e43ab", + "sha256:92a3d214d5e53cb1db8b015f30d544bc9d3f7179a05feb8f16df713cecc2620a", + "sha256:948a9bd0fb2c5120457b07e59c8d7210cbc8703243225dbd78f4dfc13c8d2d1f", + "sha256:9c900108df470060174108012de06d45f514aa4ec21a191e7ab42988ff42a86c", + "sha256:9f2939cd4a2a52ca32bc0b359015718472d7f6de870760342e7ba295be9ebaf9", + "sha256:a4192b45dff127c7d69b3bdfb4d3e47b64179a0b9900b6351859f3001397dabf", + "sha256:a8fc931382e56627ec4acb01e09ce66e5c03c384ca52606111cee50d931a342d", + "sha256:ad47b095f0bdc5585bced35bd088cbfe4177236c7df9984b3cc46b391cc60627", + "sha256:b1ca5f060e205f72bec57faae5bd817a1560fcfc4af03f414b08fa29106b7e2d", + "sha256:ba1739fb38441a27a676f4de4123d3e858e494fac05868b7a281c0a383c098f4", + "sha256:baa7ef4e0886a6f482e00d1d5bcd37c201b383f1d314643dfb0367169f94f04c", + "sha256:bb90765dd91aed05b53cd7a87bd7f5c188fcd95960914bae0d32c5e7f899719d", + "sha256:bc7f729a72b16ee21795a943f85c6244971724819819a41ddbaeb691b2dd85ad", + "sha256:bdf62d25234290db1837875d4dceb2151e4ea7f9fff2ed41c0fde23ed542eb5b", + "sha256:c30970bdee1cad6a8da2044febd824ef6dc4cc0b19e39af3085c763fdec7de33", + "sha256:d2c63b93548eda58abf5188e505ffed0229bf675f7c3090f8e36ad55b8cbc371", + "sha256:d751300b94e35b6016d4b1e7d0e7bbc3b5e1751e2405ef908316c2a9024008a1", + "sha256:da427d311782324a376cacb47c1a4adc43f99fd9d996ffc1b3e8529c4074d393", + "sha256:daba396199399ccabafbfc509037ac635a6bc18510ad1add8fd16d4739cdd106", + "sha256:e185ec6060e301a7e5f8461c86fb3640a7beb1a0f0208ffde7a65ec4074931df", + "sha256:e4a557d97f12813dc5e18dad9fa765ae44ddd56a672bb5de4825527c847d6379", + "sha256:e5ed16d95fd142e9c72b6c10b06514ad30e846a0d0917ab406186541fe68b451", + "sha256:e711fc1acc7468463bc084d1b68561e40d1eaa135d8c509a65dd534403d83d7b", + "sha256:f28b29dc158ca5d6ac396c8e0a2ef45c4e97bb7e65522bfc04c989e6fe814575", + "sha256:f335579a1b485c834849e9075191c9898e0731af45705c2ebf70e0cd5d58beed", + "sha256:fce6fee67c318fdfb7f285c29a82d84782ae2579c0e1b385b7f36c6e8074fffb", + "sha256:fd136bb85f4568fffca995bd3c8d52080b1e5b225dbf1c2b17b66b4c5fa02838" ], - "markers": "python_version >= '3.6'", - "version": "==1.16.0" + "markers": "python_version >= '3.8'", + "version": "==1.17.0" }, "yarl": { "hashes": [ - "sha256:019f5d58093402aa8f6661e60fd82a28746ad6d156f6c5336a70a39bd7b162b9", - "sha256:0fd9c227990f609c165f56b46107d0bc34553fe0387818c42c02f77974402c36", - "sha256:1208ca14eed2fda324042adf8d6c0adf4a31522fa95e0929027cd487875f0240", - "sha256:122d8e7986043d0549e9eb23c7fd23be078be4b70c9eb42a20052b3d3149c6f2", - "sha256:147b0fcd0ee33b4b5f6edfea80452d80e419e51b9a3f7a96ce98eaee145c1581", - "sha256:178ccb856e265174a79f59721031060f885aca428983e75c06f78aa24b91d929", - "sha256:1a5cf32539373ff39d97723e39a9283a7277cbf1224f7aef0c56c9598b6486c3", - "sha256:1a5e9d8ce1185723419c487758d81ac2bde693711947032cce600ca7c9cda7d6", - "sha256:1bc22e00edeb068f71967ab99081e9406cd56dbed864fc3a8259442999d71552", - "sha256:1cf936ba67bc6c734f3aa1c01391da74ab7fc046a9f8bbfa230b8393b90cf472", - "sha256:234f3a3032b505b90e65b5bc6652c2329ea7ea8855d8de61e1642b74b4ee65d2", - "sha256:26768342f256e6e3c37533bf9433f5f15f3e59e3c14b2409098291b3efaceacb", - "sha256:27e11db3f1e6a51081a981509f75617b09810529de508a181319193d320bc5c7", - "sha256:2bd6a51010c7284d191b79d3b56e51a87d8e1c03b0902362945f15c3d50ed46b", - "sha256:2f1fe2b2e3ee418862f5ebc0c0083c97f6f6625781382f828f6d4e9b614eba9b", - "sha256:32468f41242d72b87ab793a86d92f885355bcf35b3355aa650bfa846a5c60058", - "sha256:35b4f7842154176523e0a63c9b871168c69b98065d05a4f637fce342a6a2693a", - "sha256:38fec8a2a94c58bd47c9a50a45d321ab2285ad133adefbbadf3012c054b7e656", - "sha256:3a91654adb7643cb21b46f04244c5a315a440dcad63213033826549fa2435f71", - "sha256:3ab3ed42c78275477ea8e917491365e9a9b69bb615cb46169020bd0aa5e2d6d3", - "sha256:3d375a19ba2bfe320b6d873f3fb165313b002cef8b7cc0a368ad8b8a57453837", - "sha256:4199db024b58a8abb2cfcedac7b1292c3ad421684571aeb622a02f242280e8d6", - "sha256:4f32c4cb7386b41936894685f6e093c8dfaf0960124d91fe0ec29fe439e201d0", - "sha256:4ffb7c129707dd76ced0a4a4128ff452cecf0b0e929f2668ea05a371d9e5c104", - "sha256:504e1fe1cc4f170195320eb033d2b0ccf5c6114ce5bf2f617535c01699479bca", - "sha256:542fa8e09a581bcdcbb30607c7224beff3fdfb598c798ccd28a8184ffc18b7eb", - "sha256:5570e6d47bcb03215baf4c9ad7bf7c013e56285d9d35013541f9ac2b372593e7", - "sha256:571f781ae8ac463ce30bacebfaef2c6581543776d5970b2372fbe31d7bf31a07", - "sha256:595ca5e943baed31d56b33b34736461a371c6ea0038d3baec399949dd628560b", - "sha256:5b8e265a0545637492a7e12fd7038370d66c9375a61d88c5567d0e044ded9202", - "sha256:5b9101f528ae0f8f65ac9d64dda2bb0627de8a50344b2f582779f32fda747c1d", - "sha256:5ff96da263740779b0893d02b718293cc03400c3a208fc8d8cd79d9b0993e532", - "sha256:621280719c4c5dad4c1391160a9b88925bb8b0ff6a7d5af3224643024871675f", - "sha256:62c7da0ad93a07da048b500514ca47b759459ec41924143e2ddb5d7e20fd3db5", - "sha256:649bddcedee692ee8a9b7b6e38582cb4062dc4253de9711568e5620d8707c2a3", - "sha256:66ea8311422a7ba1fc79b4c42c2baa10566469fe5a78500d4e7754d6e6db8724", - "sha256:676d96bafc8c2d0039cea0cd3fd44cee7aa88b8185551a2bb93354668e8315c2", - "sha256:707ae579ccb3262dfaef093e202b4c3fb23c3810e8df544b1111bd2401fd7b09", - "sha256:7118bdb5e3ed81acaa2095cba7ec02a0fe74b52a16ab9f9ac8e28e53ee299732", - "sha256:789a3423f28a5fff46fbd04e339863c169ece97c827b44de16e1a7a42bc915d2", - "sha256:7ace71c4b7a0c41f317ae24be62bb61e9d80838d38acb20e70697c625e71f120", - "sha256:7c7c30fb38c300fe8140df30a046a01769105e4cf4282567a29b5cdb635b66c4", - "sha256:7d7aaa8ff95d0840e289423e7dc35696c2b058d635f945bf05b5cd633146b027", - "sha256:7f8713717a09acbfee7c47bfc5777e685539fefdd34fa72faf504c8be2f3df4e", - "sha256:858728086914f3a407aa7979cab743bbda1fe2bdf39ffcd991469a370dd7414d", - "sha256:8791d66d81ee45866a7bb15a517b01a2bcf583a18ebf5d72a84e6064c417e64b", - "sha256:87dd10bc0618991c66cee0cc65fa74a45f4ecb13bceec3c62d78ad2e42b27a16", - "sha256:8994c42f4ca25df5380ddf59f315c518c81df6a68fed5bb0c159c6cb6b92f120", - "sha256:8a0296040e5cddf074c7f5af4a60f3fc42c0237440df7bcf5183be5f6c802ed5", - "sha256:8b37d5ec034e668b22cf0ce1074d6c21fd2a08b90d11b1b73139b750a8b0dd97", - "sha256:8c42998fd1cbeb53cd985bff0e4bc25fbe55fd6eb3a545a724c1012d69d5ec84", - "sha256:8f639e3f5795a6568aa4f7d2ac6057c757dcd187593679f035adbf12b892bb00", - "sha256:921b81b8d78f0e60242fb3db615ea3f368827a76af095d5a69f1c3366db3f596", - "sha256:995d0759004c08abd5d1b81300a91d18c8577c6389300bed1c7c11675105a44d", - "sha256:99a9dcd4b71dd5f5f949737ab3f356cfc058c709b4f49833aeffedc2652dac56", - "sha256:9a91217208306d82357c67daeef5162a41a28c8352dab7e16daa82e3718852a7", - "sha256:a5ace0177520bd4caa99295a9b6fb831d0e9a57d8e0501a22ffaa61b4c024283", - "sha256:a5b6c09b9b4253d6a208b0f4a2f9206e511ec68dce9198e0fbec4f160137aa67", - "sha256:a9394c65ae0ed95679717d391c862dece9afacd8fa311683fc8b4362ce8a410c", - "sha256:aa7943f04f36d6cafc0cf53ea89824ac2c37acbdb4b316a654176ab8ffd0f968", - "sha256:ab2b2ac232110a1fdb0d3ffcd087783edd3d4a6ced432a1bf75caf7b7be70916", - "sha256:ad7a852d1cd0b8d8b37fc9d7f8581152add917a98cfe2ea6e241878795f917ae", - "sha256:b140e532fe0266003c936d017c1ac301e72ee4a3fd51784574c05f53718a55d8", - "sha256:b439cae82034ade094526a8f692b9a2b5ee936452de5e4c5f0f6c48df23f8604", - "sha256:b6f687ced5510a9a2474bbae96a4352e5ace5fa34dc44a217b0537fec1db00b4", - "sha256:b9ca7b9147eb1365c8bab03c003baa1300599575effad765e0b07dd3501ea9af", - "sha256:bdcf667a5dec12a48f669e485d70c54189f0639c2157b538a4cffd24a853624f", - "sha256:cdcffe1dbcb4477d2b4202f63cd972d5baa155ff5a3d9e35801c46a415b7f71a", - "sha256:d1aab176dd55b59f77a63b27cffaca67d29987d91a5b615cbead41331e6b7428", - "sha256:d1b0796168b953bca6600c5f97f5ed407479889a36ad7d17183366260f29a6b9", - "sha256:d3f1cc3d3d4dc574bebc9b387f6875e228ace5748a7c24f49d8f01ac1bc6c31b", - "sha256:d743e3118b2640cef7768ea955378c3536482d95550222f908f392167fe62059", - "sha256:d8643975a0080f361639787415a038bfc32d29208a4bf6b783ab3075a20b1ef3", - "sha256:d9525f03269e64310416dbe6c68d3b23e5d34aaa8f47193a1c45ac568cecbc49", - "sha256:de6c14dd7c7c0badba48157474ea1f03ebee991530ba742d381b28d4f314d6f3", - "sha256:e49e0fd86c295e743fd5be69b8b0712f70a686bc79a16e5268386c2defacaade", - "sha256:e6980a558d8461230c457218bd6c92dfc1d10205548215c2c21d79dc8d0a96f3", - "sha256:e8be3aff14f0120ad049121322b107f8a759be76a6a62138322d4c8a337a9e2c", - "sha256:e9951afe6557c75a71045148890052cb942689ee4c9ec29f5436240e1fcc73b7", - "sha256:ed097b26f18a1f5ff05f661dc36528c5f6735ba4ce8c9645e83b064665131349", - "sha256:f1d1f45e3e8d37c804dca99ab3cf4ab3ed2e7a62cd82542924b14c0a4f46d243", - "sha256:fe8bba2545427418efc1929c5c42852bdb4143eb8d0a46b09de88d1fe99258e7" + "sha256:00e5a1fea0fd4f5bfa7440a47eff01d9822a65b4488f7cff83155a0f31a2ecba", + "sha256:02ddb6756f8f4517a2d5e99d8b2f272488e18dd0bfbc802f31c16c6c20f22193", + "sha256:045b8482ce9483ada4f3f23b3774f4e1bf4f23a2d5c912ed5170f68efb053318", + "sha256:09c7907c8548bcd6ab860e5f513e727c53b4a714f459b084f6580b49fa1b9cee", + "sha256:0b0cad37311123211dc91eadcb322ef4d4a66008d3e1bdc404808992260e1a0e", + "sha256:0b3c92fa08759dbf12b3a59579a4096ba9af8dd344d9a813fc7f5070d86bbab1", + "sha256:0fb2171a4486bb075316ee754c6d8382ea6eb8b399d4ec62fde2b591f879778a", + "sha256:1a74a13a4c857a84a845505fd2d68e54826a2cd01935a96efb1e9d86c728e186", + "sha256:1d407181cfa6e70077df3377938c08012d18893f9f20e92f7d2f314a437c30b1", + "sha256:1dd4bdd05407ced96fed3d7f25dbbf88d2ffb045a0db60dbc247f5b3c5c25d50", + "sha256:25b411eddcfd56a2f0cd6a384e9f4f7aa3efee14b188de13048c25b5e91f1640", + "sha256:2d06d3005e668744e11ed80812e61efd77d70bb7f03e33c1598c301eea20efbb", + "sha256:2ec9bbba33b2d00999af4631a3397d1fd78290c48e2a3e52d8dd72db3a067ac8", + "sha256:3236da9272872443f81fedc389bace88408f64f89f75d1bdb2256069a8730ccc", + "sha256:35098b24e0327fc4ebdc8ffe336cee0a87a700c24ffed13161af80124b7dc8e5", + "sha256:41f7ce59d6ee7741af71d82020346af364949314ed3d87553763a2df1829cc58", + "sha256:436c4fc0a4d66b2badc6c5fc5ef4e47bb10e4fd9bf0c79524ac719a01f3607c2", + "sha256:4891ed92157e5430874dad17b15eb1fda57627710756c27422200c52d8a4e393", + "sha256:4ac515b860c36becb81bb84b667466885096b5fc85596948548b667da3bf9f24", + "sha256:5094d9206c64181d0f6e76ebd8fb2f8fe274950a63890ee9e0ebfd58bf9d787b", + "sha256:54d6921f07555713b9300bee9c50fb46e57e2e639027089b1d795ecd9f7fa910", + "sha256:578e281c393af575879990861823ef19d66e2b1d0098414855dd367e234f5b3c", + "sha256:5a3f356548e34a70b0172d8890006c37be92995f62d95a07b4a42e90fba54272", + "sha256:602d98f2c2d929f8e697ed274fbadc09902c4025c5a9963bf4e9edfc3ab6f7ed", + "sha256:61b1a825a13bef4a5f10b1885245377d3cd0bf87cba068e1d9a88c2ae36880e1", + "sha256:61e5e68cb65ac8f547f6b5ef933f510134a6bf31bb178be428994b0cb46c2a04", + "sha256:61ee62ead9b68b9123ec24bc866cbef297dd266175d53296e2db5e7f797f902d", + "sha256:6333c5a377c8e2f5fae35e7b8f145c617b02c939d04110c76f29ee3676b5f9a5", + "sha256:6748dbf9bfa5ba1afcc7556b71cda0d7ce5f24768043a02a58846e4a443d808d", + "sha256:67a283dd2882ac98cc6318384f565bffc751ab564605959df4752d42483ad889", + "sha256:75674776d96d7b851b6498f17824ba17849d790a44d282929c42dbb77d4f17ae", + "sha256:757e81cae69244257d125ff31663249b3013b5dc0a8520d73694aed497fb195b", + "sha256:77a6e85b90a7641d2e07184df5557132a337f136250caafc9ccaa4a2a998ca2c", + "sha256:7c33dd1931a95e5d9a772d0ac5e44cac8957eaf58e3c8da8c1414de7dd27c576", + "sha256:7df647e8edd71f000a5208fe6ff8c382a1de8edfbccdbbfe649d263de07d8c34", + "sha256:7e2ee16578af3b52ac2f334c3b1f92262f47e02cc6193c598502bd46f5cd1477", + "sha256:80316a8bd5109320d38eef8833ccf5f89608c9107d02d2a7f985f98ed6876990", + "sha256:82123d0c954dc58db301f5021a01854a85bf1f3bb7d12ae0c01afc414a882ca2", + "sha256:84b2deecba4a3f1a398df819151eb72d29bfeb3b69abb145a00ddc8d30094512", + "sha256:8503ad47387b8ebd39cbbbdf0bf113e17330ffd339ba1144074da24c545f0069", + "sha256:877d209b6aebeb5b16c42cbb377f5f94d9e556626b1bfff66d7b0d115be88d0a", + "sha256:8874027a53e3aea659a6d62751800cf6e63314c160fd607489ba5c2edd753cf6", + "sha256:88a19f62ff30117e706ebc9090b8ecc79aeb77d0b1f5ec10d2d27a12bc9f66d0", + "sha256:8d39d351e7faf01483cc7ff7c0213c412e38e5a340238826be7e0e4da450fdc8", + "sha256:90adb47ad432332d4f0bc28f83a5963f426ce9a1a8809f5e584e704b82685dcb", + "sha256:913829534200eb0f789d45349e55203a091f45c37a2674678744ae52fae23efa", + "sha256:93b2e109287f93db79210f86deb6b9bbb81ac32fc97236b16f7433db7fc437d8", + "sha256:9d41beda9dc97ca9ab0b9888cb71f7539124bc05df02c0cff6e5acc5a19dcc6e", + "sha256:a440a2a624683108a1b454705ecd7afc1c3438a08e890a1513d468671d90a04e", + "sha256:a4bb030cf46a434ec0225bddbebd4b89e6471814ca851abb8696170adb163985", + "sha256:a9ca04806f3be0ac6d558fffc2fdf8fcef767e0489d2684a21912cc4ed0cd1b8", + "sha256:ac1801c45cbf77b6c99242eeff4fffb5e4e73a800b5c4ad4fc0be5def634d2e1", + "sha256:ac36703a585e0929b032fbaab0707b75dc12703766d0b53486eabd5139ebadd5", + "sha256:b1771de9944d875f1b98a745bc547e684b863abf8f8287da8466cf470ef52690", + "sha256:b464c4ab4bfcb41e3bfd3f1c26600d038376c2de3297760dfe064d2cb7ea8e10", + "sha256:b4f6450109834af88cb4cc5ecddfc5380ebb9c228695afc11915a0bf82116789", + "sha256:b57f4f58099328dfb26c6a771d09fb20dbbae81d20cfb66141251ea063bd101b", + "sha256:b643562c12680b01e17239be267bc306bbc6aac1f34f6444d1bded0c5ce438ca", + "sha256:b958ddd075ddba5b09bb0be8a6d9906d2ce933aee81100db289badbeb966f54e", + "sha256:b9d60031cf568c627d028239693fd718025719c02c9f55df0a53e587aab951b5", + "sha256:ba23302c0c61a9999784e73809427c9dbedd79f66a13d84ad1b1943802eaaf59", + "sha256:ba87babd629f8af77f557b61e49e7c7cac36f22f871156b91e10a6e9d4f829e9", + "sha256:c017a3b6df3a1bd45b9fa49a0f54005e53fbcad16633870104b66fa1a30a29d8", + "sha256:c1e1cc06da1491e6734f0ea1e6294ce00792193c463350626571c287c9a704db", + "sha256:c654d5207c78e0bd6d749f6dae1dcbbfde3403ad3a4b11f3c5544d9906969dde", + "sha256:c69697d3adff5aa4f874b19c0e4ed65180ceed6318ec856ebc423aa5850d84f7", + "sha256:c7d79f7d9aabd6011004e33b22bc13056a3e3fb54794d138af57f5ee9d9032cb", + "sha256:ccaa3a4b521b780a7e771cc336a2dba389a0861592bbce09a476190bb0c8b4b3", + "sha256:ccd17349166b1bee6e529b4add61727d3f55edb7babbe4069b5764c9587a8cc6", + "sha256:ce1af883b94304f493698b00d0f006d56aea98aeb49d75ec7d98cd4a777e9285", + "sha256:d0e883008013c0e4aef84dcfe2a0b172c4d23c2669412cf5b3371003941f72bb", + "sha256:d980e0325b6eddc81331d3f4551e2a333999fb176fd153e075c6d1c2530aa8a8", + "sha256:e17c9361d46a4d5addf777c6dd5eab0715a7684c2f11b88c67ac37edfba6c482", + "sha256:e2c08cc9b16f4f4bc522771d96734c7901e7ebef70c6c5c35dd0f10845270bcd", + "sha256:e35ef8683211db69ffe129a25d5634319a677570ab6b2eba4afa860f54eeaf75", + "sha256:e3b9fd71836999aad54084906f8663dffcd2a7fb5cdafd6c37713b2e72be1760", + "sha256:ef9f7768395923c3039055c14334ba4d926f3baf7b776c923c93d80195624782", + "sha256:f52a265001d830bc425f82ca9eabda94a64a4d753b07d623a9f2863fde532b53", + "sha256:f91c4803173928a25e1a55b943c81f55b8872f0018be83e3ad4938adffb77dd2", + "sha256:fbd6748e8ab9b41171bb95c6142faf068f5ef1511935a0aa07025438dd9a9bc1", + "sha256:fe57328fbc1bfd0bd0514470ac692630f3901c0ee39052ae47acd1d90a436719", + "sha256:fea09ca13323376a2fdfb353a5fa2e59f90cd18d7ca4eaa1fd31f0a8b4f91e62" ], "markers": "python_version >= '3.9'", - "version": "==1.16.0" + "version": "==1.18.3" } }, "develop": { @@ -1686,79 +1862,80 @@ }, "colorama": { "hashes": [ - "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", - "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6" + "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b", + "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2" ], - "markers": "platform_system == 'Windows'", - "version": "==0.4.6" + "index": "pypi", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==0.4.4" }, "coverage": { "hashes": [ - "sha256:00a1d69c112ff5149cabe60d2e2ee948752c975d95f1e1096742e6077affd376", - "sha256:023bf8ee3ec6d35af9c1c6ccc1d18fa69afa1cb29eaac57cb064dbb262a517f9", - "sha256:0294ca37f1ba500667b1aef631e48d875ced93ad5e06fa665a3295bdd1d95111", - "sha256:06babbb8f4e74b063dbaeb74ad68dfce9186c595a15f11f5d5683f748fa1d172", - "sha256:0809082ee480bb8f7416507538243c8863ac74fd8a5d2485c46f0f7499f2b491", - "sha256:0b3fb02fe73bed561fa12d279a417b432e5b50fe03e8d663d61b3d5990f29546", - "sha256:0b58c672d14f16ed92a48db984612f5ce3836ae7d72cdd161001cc54512571f2", - "sha256:0bcd1069e710600e8e4cf27f65c90c7843fa8edfb4520fb0ccb88894cad08b11", - "sha256:1032e178b76a4e2b5b32e19d0fd0abbce4b58e77a1ca695820d10e491fa32b08", - "sha256:11a223a14e91a4693d2d0755c7a043db43d96a7450b4f356d506c2562c48642c", - "sha256:12394842a3a8affa3ba62b0d4ab7e9e210c5e366fbac3e8b2a68636fb19892c2", - "sha256:182e6cd5c040cec0a1c8d415a87b67ed01193ed9ad458ee427741c7d8513d963", - "sha256:1d5b8007f81b88696d06f7df0cb9af0d3b835fe0c8dbf489bad70b45f0e45613", - "sha256:1f76846299ba5c54d12c91d776d9605ae33f8ae2b9d1d3c3703cf2db1a67f2c0", - "sha256:27fb4a050aaf18772db513091c9c13f6cb94ed40eacdef8dad8411d92d9992db", - "sha256:29155cd511ee058e260db648b6182c419422a0d2e9a4fa44501898cf918866cf", - "sha256:29fc0f17b1d3fea332f8001d4558f8214af7f1d87a345f3a133c901d60347c73", - "sha256:2b6b4c83d8e8ea79f27ab80778c19bc037759aea298da4b56621f4474ffeb117", - "sha256:2fdef0d83a2d08d69b1f2210a93c416d54e14d9eb398f6ab2f0a209433db19e1", - "sha256:3c65d37f3a9ebb703e710befdc489a38683a5b152242664b973a7b7b22348a4e", - "sha256:4f704f0998911abf728a7783799444fcbbe8261c4a6c166f667937ae6a8aa522", - "sha256:51b44306032045b383a7a8a2c13878de375117946d68dcb54308111f39775a25", - "sha256:53d202fd109416ce011578f321460795abfe10bb901b883cafd9b3ef851bacfc", - "sha256:58809e238a8a12a625c70450b48e8767cff9eb67c62e6154a642b21ddf79baea", - "sha256:5915fcdec0e54ee229926868e9b08586376cae1f5faa9bbaf8faf3561b393d52", - "sha256:5beb1ee382ad32afe424097de57134175fea3faf847b9af002cc7895be4e2a5a", - "sha256:5f8ae553cba74085db385d489c7a792ad66f7f9ba2ee85bfa508aeb84cf0ba07", - "sha256:5fbd612f8a091954a0c8dd4c0b571b973487277d26476f8480bfa4b2a65b5d06", - "sha256:6bd818b7ea14bc6e1f06e241e8234508b21edf1b242d49831831a9450e2f35fa", - "sha256:6f01ba56b1c0e9d149f9ac85a2f999724895229eb36bd997b61e62999e9b0901", - "sha256:73d2b73584446e66ee633eaad1a56aad577c077f46c35ca3283cd687b7715b0b", - "sha256:7bb92c539a624cf86296dd0c68cd5cc286c9eef2d0c3b8b192b604ce9de20a17", - "sha256:8165b796df0bd42e10527a3f493c592ba494f16ef3c8b531288e3d0d72c1f6f0", - "sha256:862264b12ebb65ad8d863d51f17758b1684560b66ab02770d4f0baf2ff75da21", - "sha256:8902dd6a30173d4ef09954bfcb24b5d7b5190cf14a43170e386979651e09ba19", - "sha256:8cf717ee42012be8c0cb205dbbf18ffa9003c4cbf4ad078db47b95e10748eec5", - "sha256:8ed9281d1b52628e81393f5eaee24a45cbd64965f41857559c2b7ff19385df51", - "sha256:99b41d18e6b2a48ba949418db48159d7a2e81c5cc290fc934b7d2380515bd0e3", - "sha256:9cb7fa111d21a6b55cbf633039f7bc2749e74932e3aa7cb7333f675a58a58bf3", - "sha256:a181e99301a0ae128493a24cfe5cfb5b488c4e0bf2f8702091473d033494d04f", - "sha256:a413a096c4cbac202433c850ee43fa326d2e871b24554da8327b01632673a076", - "sha256:a6b1e54712ba3474f34b7ef7a41e65bd9037ad47916ccb1cc78769bae324c01a", - "sha256:ade3ca1e5f0ff46b678b66201f7ff477e8fa11fb537f3b55c3f0568fbfe6e718", - "sha256:b0ac3d42cb51c4b12df9c5f0dd2f13a4f24f01943627120ec4d293c9181219ba", - "sha256:b369ead6527d025a0fe7bd3864e46dbee3aa8f652d48df6174f8d0bac9e26e0e", - "sha256:b57b768feb866f44eeed9f46975f3d6406380275c5ddfe22f531a2bf187eda27", - "sha256:b8d3a03d9bfcaf5b0141d07a88456bb6a4c3ce55c080712fec8418ef3610230e", - "sha256:bc66f0bf1d7730a17430a50163bb264ba9ded56739112368ba985ddaa9c3bd09", - "sha256:bf20494da9653f6410213424f5f8ad0ed885e01f7e8e59811f572bdb20b8972e", - "sha256:c48167910a8f644671de9f2083a23630fbf7a1cb70ce939440cd3328e0919f70", - "sha256:c481b47f6b5845064c65a7bc78bc0860e635a9b055af0df46fdf1c58cebf8e8f", - "sha256:c7c8b95bf47db6d19096a5e052ffca0a05f335bc63cef281a6e8fe864d450a72", - "sha256:c9b8e184898ed014884ca84c70562b4a82cbc63b044d366fedc68bc2b2f3394a", - "sha256:cc8ff50b50ce532de2fa7a7daae9dd12f0a699bfcd47f20945364e5c31799fef", - "sha256:d541423cdd416b78626b55f123412fcf979d22a2c39fce251b350de38c15c15b", - "sha256:dab4d16dfef34b185032580e2f2f89253d302facba093d5fa9dbe04f569c4f4b", - "sha256:dacbc52de979f2823a819571f2e3a350a7e36b8cb7484cdb1e289bceaf35305f", - "sha256:df57bdbeffe694e7842092c5e2e0bc80fff7f43379d465f932ef36f027179806", - "sha256:ed8fe9189d2beb6edc14d3ad19800626e1d9f2d975e436f84e19efb7fa19469b", - "sha256:f3ddf056d3ebcf6ce47bdaf56142af51bb7fad09e4af310241e9db7a3a8022e1", - "sha256:f8fe4984b431f8621ca53d9380901f62bfb54ff759a1348cd140490ada7b693c", - "sha256:fe439416eb6380de434886b00c859304338f8b19f6f54811984f3420a2e03858" + "sha256:0824a28ec542a0be22f60c6ac36d679e0e262e5353203bea81d44ee81fe9c6d4", + "sha256:085161be5f3b30fd9b3e7b9a8c301f935c8313dcf928a07b116324abea2c1c2c", + "sha256:0ae1387db4aecb1f485fb70a6c0148c6cdaebb6038f1d40089b1fc84a5db556f", + "sha256:0d59fd927b1f04de57a2ba0137166d31c1a6dd9e764ad4af552912d70428c92b", + "sha256:0f957943bc718b87144ecaee70762bc2bc3f1a7a53c7b861103546d3a403f0a6", + "sha256:13a9e2d3ee855db3dd6ea1ba5203316a1b1fd8eaeffc37c5b54987e61e4194ae", + "sha256:1a330812d9cc7ac2182586f6d41b4d0fadf9be9049f350e0efb275c8ee8eb692", + "sha256:22be16571504c9ccea919fcedb459d5ab20d41172056206eb2994e2ff06118a4", + "sha256:2d10e07aa2b91835d6abec555ec8b2733347956991901eea6ffac295f83a30e4", + "sha256:35371f8438028fdccfaf3570b31d98e8d9eda8bb1d6ab9473f5a390969e98717", + "sha256:3c026eb44f744acaa2bda7493dad903aa5bf5fc4f2554293a798d5606710055d", + "sha256:41ff7b0da5af71a51b53f501a3bac65fb0ec311ebed1632e58fc6107f03b9198", + "sha256:4401ae5fc52ad8d26d2a5d8a7428b0f0c72431683f8e63e42e70606374c311a1", + "sha256:44349150f6811b44b25574839b39ae35291f6496eb795b7366fef3bd3cf112d3", + "sha256:447af20e25fdbe16f26e84eb714ba21d98868705cb138252d28bc400381f6ffb", + "sha256:4a8d8977b0c6ef5aeadcb644da9e69ae0dcfe66ec7f368c89c72e058bd71164d", + "sha256:4e12ae8cc979cf83d258acb5e1f1cf2f3f83524d1564a49d20b8bec14b637f08", + "sha256:592ac539812e9b46046620341498caf09ca21023c41c893e1eb9dbda00a70cbf", + "sha256:5e6b86b5847a016d0fbd31ffe1001b63355ed309651851295315031ea7eb5a9b", + "sha256:608a7fd78c67bee8936378299a6cb9f5149bb80238c7a566fc3e6717a4e68710", + "sha256:61f70dc68bd36810972e55bbbe83674ea073dd1dcc121040a08cdf3416c5349c", + "sha256:65dad5a248823a4996724a88eb51d4b31587aa7aa428562dbe459c684e5787ae", + "sha256:777abfab476cf83b5177b84d7486497e034eb9eaea0d746ce0c1268c71652077", + "sha256:7e216d8044a356fc0337c7a2a0536d6de07888d7bcda76febcb8adc50bdbbd00", + "sha256:85d9636f72e8991a1706b2b55b06c27545448baf9f6dbf51c4004609aacd7dcb", + "sha256:899b8cd4781c400454f2f64f7776a5d87bbd7b3e7f7bda0cb18f857bb1334664", + "sha256:8a289d23d4c46f1a82d5db4abeb40b9b5be91731ee19a379d15790e53031c014", + "sha256:8d2dfa71665a29b153a9681edb1c8d9c1ea50dfc2375fb4dac99ea7e21a0bcd9", + "sha256:8e3c3e38930cfb729cb8137d7f055e5a473ddaf1217966aa6238c88bd9fd50e6", + "sha256:8f8770dfc6e2c6a2d4569f411015c8d751c980d17a14b0530da2d7f27ffdd88e", + "sha256:932fc826442132dde42ee52cf66d941f581c685a6313feebed358411238f60f9", + "sha256:96d636c77af18b5cb664ddf12dab9b15a0cfe9c0bde715da38698c8cea748bfa", + "sha256:97ddc94d46088304772d21b060041c97fc16bdda13c6c7f9d8fcd8d5ae0d8611", + "sha256:98caba4476a6c8d59ec1eb00c7dd862ba9beca34085642d46ed503cc2d440d4b", + "sha256:9901d36492009a0a9b94b20e52ebfc8453bf49bb2b27bca2c9706f8b4f5a554a", + "sha256:99e266ae0b5d15f1ca8d278a668df6f51cc4b854513daab5cae695ed7b721cf8", + "sha256:9c38bf15a40ccf5619fa2fe8f26106c7e8e080d7760aeccb3722664c8656b030", + "sha256:a27801adef24cc30871da98a105f77995e13a25a505a0161911f6aafbd66e678", + "sha256:abd3e72dd5b97e3af4246cdada7738ef0e608168de952b837b8dd7e90341f015", + "sha256:adb697c0bd35100dc690de83154627fbab1f4f3c0386df266dded865fc50a902", + "sha256:b12c6b18269ca471eedd41c1b6a1065b2f7827508edb9a7ed5555e9a56dcfc97", + "sha256:b9389a429e0e5142e69d5bf4a435dd688c14478a19bb901735cdf75e57b13845", + "sha256:ba9e7484d286cd5a43744e5f47b0b3fb457865baf07bafc6bee91896364e1419", + "sha256:bb5555cff66c4d3d6213a296b360f9e1a8e323e74e0426b6c10ed7f4d021e464", + "sha256:be57b6d56e49c2739cdf776839a92330e933dd5e5d929966fbbd380c77f060be", + "sha256:c69e42c892c018cd3c8d90da61d845f50a8243062b19d228189b0224150018a9", + "sha256:ccc660a77e1c2bf24ddbce969af9447a9474790160cfb23de6be4fa88e3951c7", + "sha256:d5275455b3e4627c8e7154feaf7ee0743c2e7af82f6e3b561967b1cca755a0be", + "sha256:d75cded8a3cff93da9edc31446872d2997e327921d8eed86641efafd350e1df1", + "sha256:d872ec5aeb086cbea771c573600d47944eea2dcba8be5f3ee649bfe3cb8dc9ba", + "sha256:d891c136b5b310d0e702e186d70cd16d1119ea8927347045124cb286b29297e5", + "sha256:db1dab894cc139f67822a92910466531de5ea6034ddfd2b11c0d4c6257168073", + "sha256:e28bf44afa2b187cc9f41749138a64435bf340adfcacb5b2290c070ce99839d4", + "sha256:e5ea1cf0872ee455c03e5674b5bca5e3e68e159379c1af0903e89f5eba9ccc3a", + "sha256:e77363e8425325384f9d49272c54045bbed2f478e9dd698dbc65dbc37860eb0a", + "sha256:ee5defd1733fd6ec08b168bd4f5387d5b322f45ca9e0e6c817ea6c4cd36313e3", + "sha256:f1592791f8204ae9166de22ba7e6705fa4ebd02936c09436a1bb85aabca3e599", + "sha256:f2d1ec60d6d256bdf298cb86b78dd715980828f50c46701abc3b0a2b3f8a0dc0", + "sha256:f3ca78518bc6bc92828cd11867b121891d75cae4ea9e908d72030609b996db1b", + "sha256:f7b15f589593110ae767ce997775d645b47e5cbbf54fd322f8ebea6277466cec", + "sha256:fd1213c86e48dfdc5a0cc676551db467495a95a662d2396ecd58e719191446e1", + "sha256:ff74026a461eb0660366fb01c650c1d00f833a086b336bdad7ab00cc952072b3" ], "markers": "python_version >= '3.9'", - "version": "==7.6.4" + "version": "==7.6.9" }, "flake8": { "hashes": [ @@ -1797,20 +1974,20 @@ }, "google-api-core": { "hashes": [ - "sha256:4a152fd11a9f774ea606388d423b68aa7e6d6a0ffe4c8266f74979613ec09f81", - "sha256:6869eacb2a37720380ba5898312af79a4d30b8bca1548fb4093e0697dc4bdf5d" + "sha256:10d82ac0fca69c82a25b3efdeefccf6f28e02ebb97925a8cce8edbfe379929d9", + "sha256:e255640547a597a4da010876d333208ddac417d60add22b6851a0c66a831fcaf" ], "markers": "python_version >= '3.7'", - "version": "==2.21.0" + "version": "==2.24.0" }, "google-api-python-client": { "hashes": [ - "sha256:1a5232e9cfed8c201799d9327e4d44dc7ea7daa3c6e1627fca41aa201539c0da", - "sha256:b9d68c6b14ec72580d66001bd33c5816b78e2134b93ccc5cf8f624516b561750" + "sha256:1b420062e03bfcaa1c79e2e00a612d29a6a934151ceb3d272fe150a656dc8f17", + "sha256:a521bbbb2ec0ba9d6f307cdd64ed6e21eeac372d1bd7493a4ab5022941f784ad" ], "index": "pypi", "markers": "python_version >= '3.7'", - "version": "==2.149.0" + "version": "==2.154.0" }, "google-api-python-client-stubs": { "hashes": [ @@ -1823,11 +2000,11 @@ }, "google-auth": { "hashes": [ - "sha256:25df55f327ef021de8be50bad0dfd4a916ad0de96da86cd05661c9297723ad3f", - "sha256:f4c64ed4e01e8e8b646ef34c018f8bf3338df0c8e37d8b3bba40e7f574a3278a" + "sha256:51a15d47028b66fd36e5c64a82d2d57480075bccc7da37cde257fc94177a61fb", + "sha256:545e9618f2df0bcbb7dcbc45a546485b1212624716975a1ea5ae8149ce769ab1" ], "markers": "python_version >= '3.7'", - "version": "==2.35.0" + "version": "==2.36.0" }, "google-auth-httplib2": { "hashes": [ @@ -1838,11 +2015,11 @@ }, "googleapis-common-protos": { "hashes": [ - "sha256:2972e6c496f435b92590fd54045060867f3fe9be2c82ab148fc8885035479a63", - "sha256:334a29d07cddc3aa01dee4988f9afd9b2916ee2ff49d6b757155dc0d197852c0" + "sha256:c3e7b33d15fdca5374cc0a7346dd92ffa847425cc4ea941d970f13680052ec8c", + "sha256:d7abcd75fabb2e0ec9f74466401f6c119a0b498e27370e9be4c94cb7e382b8ed" ], "markers": "python_version >= '3.7'", - "version": "==1.65.0" + "version": "==1.66.0" }, "httplib2": { "hashes": [ @@ -1920,11 +2097,11 @@ }, "packaging": { "hashes": [ - "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002", - "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124" + "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", + "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f" ], "markers": "python_version >= '3.8'", - "version": "==24.1" + "version": "==24.2" }, "pathspec": { "hashes": [ @@ -1952,28 +2129,28 @@ }, "proto-plus": { "hashes": [ - "sha256:30b72a5ecafe4406b0d339db35b56c4059064e69227b8c3bda7462397f966445", - "sha256:402576830425e5f6ce4c2a6702400ac79897dab0b4343821aa5188b0fab81a12" + "sha256:c91fc4a65074ade8e458e95ef8bac34d4008daa7cce4a12d6707066fca648961", + "sha256:fbb17f57f7bd05a68b7707e745e26528b0b3c34e378db91eef93912c54982d91" ], "markers": "python_version >= '3.7'", - "version": "==1.24.0" + "version": "==1.25.0" }, "protobuf": { "hashes": [ - "sha256:0c4eec6f987338617072592b97943fdbe30d019c56126493111cf24344c1cc24", - "sha256:135658402f71bbd49500322c0f736145731b16fc79dc8f367ab544a17eab4535", - "sha256:27b246b3723692bf1068d5734ddaf2fccc2cdd6e0c9b47fe099244d80200593b", - "sha256:3e6101d095dfd119513cde7259aa703d16c6bbdfae2554dfe5cfdbe94e32d548", - "sha256:3fa2de6b8b29d12c61911505d893afe7320ce7ccba4df913e2971461fa36d584", - "sha256:64badbc49180a5e401f373f9ce7ab1d18b63f7dd4a9cdc43c92b9f0b481cef7b", - "sha256:70585a70fc2dd4818c51287ceef5bdba6387f88a578c86d47bb34669b5552c36", - "sha256:712319fbdddb46f21abb66cd33cb9e491a5763b2febd8f228251add221981135", - "sha256:91fba8f445723fcf400fdbe9ca796b19d3b1242cd873907979b9ed71e4afe868", - "sha256:a3f6857551e53ce35e60b403b8a27b0295f7d6eb63d10484f12bc6879c715687", - "sha256:cee1757663fa32a1ee673434fcf3bf24dd54763c79690201208bafec62f19eed" + "sha256:012ce28d862ff417fd629285aca5d9772807f15ceb1a0dbd15b88f58c776c98c", + "sha256:027fbcc48cea65a6b17028510fdd054147057fa78f4772eb547b9274e5219331", + "sha256:1fc55267f086dd4050d18ef839d7bd69300d0d08c2a53ca7df3920cc271a3c34", + "sha256:22c1f539024241ee545cbcb00ee160ad1877975690b16656ff87dde107b5f110", + "sha256:32600ddb9c2a53dedc25b8581ea0f1fd8ea04956373c0c07577ce58d312522e0", + "sha256:50879eb0eb1246e3a5eabbbe566b44b10348939b7cc1b267567e8c3d07213853", + "sha256:5a41deccfa5e745cef5c65a560c76ec0ed8e70908a67cc8f4da5fce588b50d57", + "sha256:683be02ca21a6ffe80db6dd02c0b5b2892322c59ca57fd6c872d652cb80549cb", + "sha256:8ee1461b3af56145aca2800e6a3e2f928108c749ba8feccc6f5dd0062c410c0d", + "sha256:b5ba1d0e4c8a40ae0496d0e2ecfdbb82e1776928a205106d14ad6985a09ec155", + "sha256:d473655e29c0c4bbf8b69e9a8fb54645bc289dead6d753b952e7aa660254ae18" ], "markers": "python_version >= '3.8'", - "version": "==5.28.3" + "version": "==5.29.1" }, "py": { "hashes": [ @@ -2028,7 +2205,7 @@ "sha256:93d9577b88da0bbea8cc8334ee8b918ed014968fd2ec383e868fb8afb1ccef84", "sha256:cbf74e27246d595d9a74b186b810f6fbb86726dbf3b9532efb343f6d7294fe9c" ], - "markers": "python_version >= '3.1'", + "markers": "python_version >= '3.9'", "version": "==3.2.0" }, "pytest": { @@ -2074,11 +2251,41 @@ }, "tomli": { "hashes": [ - "sha256:2ebe24485c53d303f690b0ec092806a085f07af5a5aa1464f3931eec36caaa38", - "sha256:d46d457a85337051c36524bc5349dd91b1877838e2979ac5ced3e710ed8a60ed" + "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", + "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", + "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", + "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", + "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", + "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", + "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", + "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", + "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", + "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", + "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", + "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", + "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", + "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", + "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", + "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", + "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", + "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", + "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", + "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", + "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", + "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", + "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", + "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", + "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", + "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", + "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", + "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", + "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", + "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", + "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", + "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7" ], - "markers": "python_version < '3.11'", - "version": "==2.0.2" + "markers": "python_version >= '3.8'", + "version": "==2.2.1" }, "types-httplib2": { "hashes": [ diff --git a/abr-testing/abr_testing/automation/google_drive_tool.py b/abr-testing/abr_testing/automation/google_drive_tool.py index 45464d35c3f..d9ac35f76d1 100644 --- a/abr-testing/abr_testing/automation/google_drive_tool.py +++ b/abr-testing/abr_testing/automation/google_drive_tool.py @@ -56,6 +56,8 @@ def list_folder(self, delete: Any = False, folder: bool = False) -> Set[str]: else "" # type: ignore if self.parent_folder else None, + supportsAllDrives=True, + includeItemsFromAllDrives=True, pageSize=1000, fields="nextPageToken, files(id, name, mimeType)", pageToken=page_token, diff --git a/abr-testing/abr_testing/automation/jira_tool.py b/abr-testing/abr_testing/automation/jira_tool.py index d43db612561..a61c16c7f46 100644 --- a/abr-testing/abr_testing/automation/jira_tool.py +++ b/abr-testing/abr_testing/automation/jira_tool.py @@ -107,6 +107,12 @@ def open_issue(self, issue_key: str) -> str: webbrowser.open(url) return url + def get_labels(self) -> List[str]: + """Get list of available labels.""" + url = f"{self.url}/rest/api/3/label" + response = requests.request("GET", url, headers=self.headers, auth=self.auth) + return response.json() + def create_ticket( self, summary: str, @@ -118,10 +124,12 @@ def create_ticket( priority: str, components: list, affects_versions: str, - robot: str, + labels: list, + parent_name: str, ) -> Tuple[str, str]: """Create ticket.""" # Check if software version is a field on JIRA, if not replaces with existing version + # TODO: automate parent linking data = { "fields": { "project": {"id": "10273", "key": project_key}, @@ -129,7 +137,8 @@ def create_ticket( "summary": summary, "reporter": {"id": reporter_id}, "assignee": {"id": assignee_id}, - "parent": {"key": robot}, + # "parent": {"key": parent_name}, + "labels": labels, "priority": {"name": priority}, "components": [{"name": component} for component in components], "description": { @@ -194,6 +203,7 @@ def post_attachment_to_ticket(self, issue_id: str, attachment_path: str) -> None def get_project_issues(self, project_key: str) -> Dict[str, Any]: """Retrieve all issues for the given project key.""" + # TODO: add field for ticket type. headers = {"Accept": "application/json"} query = {"jql": f"project={project_key}"} response = requests.request( @@ -203,7 +213,6 @@ def get_project_issues(self, project_key: str) -> Dict[str, Any]: params=query, auth=self.auth, ) - response.raise_for_status() return response.json() def get_project_versions(self, project_key: str) -> List[str]: diff --git a/abr-testing/abr_testing/data_collection/abr_calibration_logs.py b/abr-testing/abr_testing/data_collection/abr_calibration_logs.py index 1440dfd70a8..46cc409e53d 100644 --- a/abr-testing/abr_testing/data_collection/abr_calibration_logs.py +++ b/abr-testing/abr_testing/data_collection/abr_calibration_logs.py @@ -82,7 +82,7 @@ def module_helper( y = one_module["moduleOffset"]["offset"].get("y", "") z = one_module["moduleOffset"]["offset"].get("z", "") except KeyError: - pass + continue if mod_serial in module_sheet_serials and modified in module_modify_dates: continue module_row = ( @@ -286,6 +286,7 @@ def run( ip_json_file = os.path.join(storage_directory, "IPs.json") try: ip_file = json.load(open(ip_json_file)) + robot_dict = ip_file.get("ip_address_list") except FileNotFoundError: print(f"Add .json file with robot IPs to: {storage_directory}.") sys.exit() @@ -294,7 +295,7 @@ def run( ip_or_all = input("IP Address or ALL: ") calibration_data = [] if ip_or_all.upper() == "ALL": - ip_address_list = ip_file["ip_address_list"] + ip_address_list = list(robot_dict.keys()) for ip in ip_address_list: saved_file_path, calibration = read_robot_logs.get_calibration_offsets( ip, storage_directory @@ -363,3 +364,4 @@ def run( folder_name = args.folder_name[0] google_sheet_name = args.google_sheet_name[0] email = args.email[0] + run(storage_directory, folder_name, google_sheet_name, email) 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 88ed55cab82..655200745a9 100644 --- a/abr-testing/abr_testing/data_collection/abr_google_drive.py +++ b/abr-testing/abr_testing/data_collection/abr_google_drive.py @@ -34,8 +34,6 @@ def create_data_dictionary( runs_to_save: Union[Set[str], str], storage_directory: str, issue_url: str, - plate: str, - accuracy: Any, hellma_plate_standards: List[Dict[str, Any]], ) -> Tuple[List[List[Any]], List[str], List[List[Any]], List[str]]: """Pull data from run files and format into a dictionary.""" @@ -43,11 +41,16 @@ def create_data_dictionary( runs_and_lpc: List[Dict[str, Any]] = [] headers: List[str] = [] headers_lpc: List[str] = [] + hellma_plate_orientation = False # default hellma plate is not rotated. for filename in os.listdir(storage_directory): file_path = os.path.join(storage_directory, filename) if file_path.endswith(".json"): with open(file_path) as file: - file_results = json.load(file) + try: + file_results = json.load(file) + except json.decoder.JSONDecodeError: + print(f"Skipped file {file_path} bc no data.") + continue else: continue if not isinstance(file_results, dict): @@ -62,6 +65,10 @@ def create_data_dictionary( if run_id in runs_to_save: print(f"started reading run {run_id}.") robot = file_results.get("robot_name") + parameters = file_results.get("runTimeParameters", "") + for parameter in parameters: + if parameter["displayName"] == "Hellma Plate Orientation": + hellma_plate_orientation = bool(parameter["value"]) protocol_name = file_results["protocol"]["metadata"].get("protocolName", "") software_version = file_results.get("API_Version", "") left_pipette = file_results.get("left", "") @@ -93,12 +100,15 @@ def create_data_dictionary( run_time_min = run_time.total_seconds() / 60 except ValueError: pass # Handle datetime parsing errors if necessary + # Get protocol version # + version_number = read_robot_logs.get_protocol_version_number(file_results) if run_time_min > 0: run_row = { "Robot": robot, "Run_ID": run_id, "Protocol_Name": protocol_name, + "Protocol Version": version_number, "Software Version": software_version, "Date": start_date, "Start_Time": start_time_str, @@ -118,12 +128,12 @@ def create_data_dictionary( file_results, labware_name="opentrons_tough_pcr_auto_sealing_lid" ) plate_reader_dict = read_robot_logs.plate_reader_commands( - file_results, hellma_plate_standards + file_results, hellma_plate_standards, hellma_plate_orientation ) notes = {"Note1": "", "Jira Link": issue_url} + liquid_height = read_robot_logs.get_liquid_waste_height(file_results) plate_measure = { - "Plate Measured": plate, - "End Volume Accuracy (%)": accuracy, + "Liquid Waste Height (mm)": liquid_height, "Average Temp (oC)": "", "Average RH(%)": "", } @@ -155,7 +165,12 @@ def create_data_dictionary( print(f"Number of runs read: {num_of_runs_read}") transposed_runs_and_robots = list(map(list, zip(*runs_and_robots))) transposed_runs_and_lpc = list(map(list, zip(*runs_and_lpc))) - return transposed_runs_and_robots, headers, transposed_runs_and_lpc, headers_lpc + return ( + transposed_runs_and_robots, + headers, + transposed_runs_and_lpc, + headers_lpc, + ) def run( @@ -173,7 +188,8 @@ def run( credentials_path, google_sheet_name, 0 ) # Get run ids on google sheet - run_ids_on_gs = set(google_sheet.get_column(2)) + run_ids_on_gs: Set[str] = set(google_sheet.get_column(2)) + # Get robots on google sheet # Uploads files that are not in google drive directory google_drive.upload_missing_files(storage_directory) @@ -195,9 +211,7 @@ def run( missing_runs_from_gs, storage_directory, "", - "", - "", - hellma_plate_standards=file_values, + file_values, ) start_row = google_sheet.get_index_row() + 1 google_sheet.batch_update_cells(transposed_runs_and_robots, "A", start_row, "0") diff --git a/abr-testing/abr_testing/data_collection/abr_robot_error.py b/abr-testing/abr_testing/data_collection/abr_robot_error.py index 1849699bfa1..c2dadaae54c 100644 --- a/abr-testing/abr_testing/data_collection/abr_robot_error.py +++ b/abr-testing/abr_testing/data_collection/abr_robot_error.py @@ -11,11 +11,35 @@ import sys import json import re +from pathlib import Path import pandas as pd from statistics import mean, StatisticsError from abr_testing.tools import plate_reader +def retrieve_protocol_file( + protocol_id: str, + robot_ip: str, + storage: str, +) -> Path | str: + """Find and copy protocol file on robot with error.""" + protocol_dir = f"/var/lib/opentrons-robot-server/7.1/protocols/{protocol_id}" + + print(f"FILE TO FIND: {protocol_dir}/{protocol_id}") + # Copy protocol file found in robot oto host computer + save_dir = Path(f"{storage}/protocol_errors") + command = ["scp", "-r", f"root@{robot_ip}:{protocol_dir}", save_dir] + try: + # If file found and copied return path to file + subprocess.run(command, check=True) # type: ignore + print("File transfer successful!") + return save_dir + except subprocess.CalledProcessError as e: + print(f"Error during file transfer: {e}") + # Return empty string if file can't be copied + return "" + + def compare_current_trh_to_average( robot: str, start_time: Any, @@ -38,9 +62,13 @@ def compare_current_trh_to_average( # Find average conditions of errored time period df_all_trh = pd.DataFrame(all_trh_data) # Convert timestamps to datetime objects - df_all_trh["Timestamp"] = pd.to_datetime( - df_all_trh["Timestamp"], format="mixed", utc=True - ).dt.tz_localize(None) + print(f'TIMESTAMP: {df_all_trh["Timestamp"]}') + try: + df_all_trh["Timestamp"] = pd.to_datetime( + df_all_trh["Timestamp"], format="mixed", utc=True + ).dt.tz_localize(None) + except Exception: + print(f'The following timestamp is invalid: {df_all_trh["Timestamp"]}') # Ensure start_time is timezone-naive start_time = start_time.replace(tzinfo=None) relevant_temp_rhs = df_all_trh[ @@ -125,15 +153,20 @@ def compare_lpc_to_historical_data( & (df_lpc_data["Robot"] == robot) & (df_lpc_data["Module"] == labware_dict["Module"]) & (df_lpc_data["Adapter"] == labware_dict["Adapter"]) - & (df_lpc_data["Run Ending Error"] < 1) + & (df_lpc_data["Run Ending Error"]) + < 1 ] # Converts coordinates to floats and finds averages. - x_float = [float(value) for value in relevant_lpc["X"]] - y_float = [float(value) for value in relevant_lpc["Y"]] - z_float = [float(value) for value in relevant_lpc["Z"]] - current_x = round(labware_dict["X"], 2) - current_y = round(labware_dict["Y"], 2) - current_z = round(labware_dict["Z"], 2) + try: + x_float = [float(value) for value in relevant_lpc["X"]] + y_float = [float(value) for value in relevant_lpc["Y"]] + z_float = [float(value) for value in relevant_lpc["Z"]] + current_x = round(labware_dict["X"], 2) + current_y = round(labware_dict["Y"], 2) + current_z = round(labware_dict["Z"], 2) + except (ValueError): + x_float, y_float, z_float = [0.0], [0.0], [0.0] + current_x, current_y, current_z = 0.0, 0.0, 0.0 try: avg_x = round(mean(x_float), 2) avg_y = round(mean(y_float), 2) @@ -240,25 +273,29 @@ def get_user_id(user_file_path: str, assignee_name: str) -> str: return assignee_id -def get_error_runs_from_robot(ip: str) -> List[str]: +def get_error_runs_from_robot(ip: str) -> Tuple[List[str], List[str]]: """Get runs that have errors from robot.""" error_run_ids = [] + protocol_ids = [] response = requests.get( f"http://{ip}:31950/runs", headers={"opentrons-version": "3"} ) run_data = response.json() - run_list = run_data["data"] + run_list = run_data.get("data", []) for run in run_list: run_id = run["id"] + protocol_id = run["protocolId"] 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 + # Protocol ID will identify the correct folder on the robot of the protocol file + protocol_ids.append(protocol_id) + return (error_run_ids, protocol_ids) def get_robot_state( ip: str, reported_string: str -) -> Tuple[Any, Any, Any, List[str], str]: +) -> Tuple[Any, Any, Any, List[str], List[str], str]: """Get robot status in case of non run error.""" description = dict() # Get instruments attached to robot @@ -274,10 +311,11 @@ def get_robot_state( f"http://{ip}:31950/health", headers={"opentrons-version": "3"} ) health_data = response.json() - parent = health_data.get("name", "") + print(f"health data {health_data}") + robot = health_data.get("name", "") # Create summary name - description["robot_name"] = parent - summary = parent + "_" + reported_string + description["robot_name"] = robot + summary = robot + "_" + reported_string affects_version = health_data.get("api_version", "") description["affects_version"] = affects_version # Instruments Attached @@ -297,6 +335,12 @@ def get_robot_state( description[module["moduleType"]] = module components = ["Flex-RABR"] components = match_error_to_component("RABR", reported_string, components) + if "alpha" in affects_version: + components.append("flex internal releases") + labels = [robot] + if "8.2" in affects_version: + labels.append("8_2_0") + parent = affects_version + " Bugs" print(components) end_time = datetime.now() print(end_time) @@ -317,13 +361,14 @@ def get_robot_state( parent, affects_version, components, + labels, whole_description_str, ) def get_run_error_info_from_robot( - ip: str, one_run: str, storage_directory: str -) -> Tuple[str, str, str, List[str], str, str]: + ip: str, one_run: str, storage_directory: str, protocol_found: bool +) -> Tuple[str, str, str, List[str], List[str], str, str]: """Get error information from robot to fill out ticket.""" description = dict() # get run information @@ -339,20 +384,26 @@ def get_run_error_info_from_robot( error_code = error_dict["Error_Code"] error_instrument = error_dict["Error_Instrument"] # JIRA Ticket Fields - + robot = results.get("robot_name", "") failure_level = "Level " + str(error_level) + " Failure" components = [failure_level, "Flex-RABR"] components = match_error_to_component("RABR", str(error_type), components) - print(components) affects_version = results["API_Version"] - parent = results.get("robot_name", "") - print(parent) - summary = parent + "_" + str(one_run) + "_" + str(error_code) + "_" + error_type + if "alpha" in affects_version: + components.append("flex internal releases") + labels = [robot] + if "8.2" in affects_version: + labels.append("8_2_0") + parent = affects_version + " Bugs" + summary = robot + "_" + str(one_run) + "_" + str(error_code) + "_" + error_type # Description of error description["protocol_name"] = results["protocol"]["metadata"].get( "protocolName", "" ) + + # If Protocol was successfully retrieved from the robot + description["protocol_found_on_robot"] = protocol_found # Get start and end time of run start_time = datetime.strptime( results.get("startedAt", ""), "%Y-%m-%dT%H:%M:%S.%f%z" @@ -430,6 +481,7 @@ def get_run_error_info_from_robot( parent, affects_version, components, + labels, whole_description_str, saved_file_path, ) @@ -494,27 +546,40 @@ def get_run_error_info_from_robot( users_file_path = ticket.get_jira_users(storage_directory) assignee_id = get_user_id(users_file_path, assignee) run_log_file_path = "" + protocol_found = False try: - error_runs = get_error_runs_from_robot(ip) + error_runs, protocol_ids = get_error_runs_from_robot(ip) except requests.exceptions.InvalidURL: print("Invalid IP address.") sys.exit() if len(run_or_other) < 1: + # Retrieve the most recently run protocol file + protocol_files_path = retrieve_protocol_file( + protocol_ids[-1], ip, storage_directory + ) + # Set protocol_found to true if python protocol was successfully copied over + if protocol_files_path: + protocol_found = True + one_run = error_runs[-1] # Most recent run with error. ( summary, - robot, + parent, affects_version, components, + labels, whole_description_str, run_log_file_path, - ) = get_run_error_info_from_robot(ip, one_run, storage_directory) + ) = get_run_error_info_from_robot( + ip, one_run, storage_directory, protocol_found + ) else: ( summary, - robot, + parent, affects_version, components, + labels, whole_description_str, ) = get_robot_state(ip, run_or_other) # Get Calibration Data @@ -525,16 +590,8 @@ def get_run_error_info_from_robot( print(f"Making ticket for {summary}.") # TODO: make argument or see if I can get rid of with using board_id. project_key = "RABR" - print(robot) - try: - parent_key = project_key + "-" + robot.split("ABR")[1] - except IndexError: - parent_key = "" - - # Grab all previous issues - all_issues = ticket.issues_on_board(project_key) - # TODO: read board to see if ticket for run id already exists. + all_issues = ticket.issues_on_board(project_key) # CREATE TICKET issue_key, raw_issue_url = ticket.create_ticket( summary, @@ -546,7 +603,8 @@ def get_run_error_info_from_robot( "Medium", components, affects_version, - parent_key, + labels, + parent, ) # Link Tickets to_link = ticket.match_issues(all_issues, summary) @@ -554,8 +612,15 @@ def get_run_error_info_from_robot( # OPEN TICKET issue_url = ticket.open_issue(issue_key) # MOVE FILES TO ERROR FOLDER. + print(protocol_files_path) error_files = [saved_file_path_calibration, run_log_file_path] + file_paths - error_folder_path = os.path.join(storage_directory, issue_key) + + # Move protocol file(s) to error folder + if protocol_files_path: + for file in os.listdir(protocol_files_path): + error_files.append(os.path.join(protocol_files_path, file)) + + error_folder_path = os.path.join(storage_directory, "issue_key") os.makedirs(error_folder_path, exist_ok=True) for source_file in error_files: try: @@ -565,7 +630,7 @@ def get_run_error_info_from_robot( shutil.move(source_file, destination_file) except shutil.Error: continue - # POST FILES TO TICKET + # POST ALL FILES TO TICKET list_of_files = os.listdir(error_folder_path) for file in list_of_files: file_to_attach = os.path.join(error_folder_path, file) @@ -606,9 +671,7 @@ def get_run_error_info_from_robot( run_id, error_folder_path, issue_url, - "", - "", - hellma_plate_standards=file_values, + file_values, ) start_row = google_sheet.get_index_row() + 1 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 24d5aaf4f3b..fe89f9f1543 100644 --- a/abr-testing/abr_testing/data_collection/get_run_logs.py +++ b/abr-testing/abr_testing/data_collection/get_run_logs.py @@ -17,7 +17,7 @@ def get_run_ids_from_robot(ip: str) -> Set[str]: f"http://{ip}:31950/runs", headers={"opentrons-version": "3"} ) run_data = response.json() - run_list = run_data["data"] + run_list = run_data.get("data", "") except requests.exceptions.RequestException: print(f"Could not connect to robot with IP {ip}") run_list = [] @@ -104,10 +104,11 @@ def get_all_run_logs( ip_json_file = os.path.join(storage_directory, "IPs.json") try: ip_file = json.load(open(ip_json_file)) + robot_dict = ip_file.get("ip_address_list") except FileNotFoundError: print(f"Add .json file with robot IPs to: {storage_directory}.") sys.exit() - ip_address_list = ip_file["ip_address_list"] + ip_address_list = list(robot_dict.keys()) runs_from_storage = read_robot_logs.get_run_ids_from_google_drive(google_drive) for ip in ip_address_list: runs = get_run_ids_from_robot(ip) diff --git a/abr-testing/abr_testing/data_collection/read_robot_logs.py b/abr-testing/abr_testing/data_collection/read_robot_logs.py index d740518c7ac..d4570d20110 100644 --- a/abr-testing/abr_testing/data_collection/read_robot_logs.py +++ b/abr-testing/abr_testing/data_collection/read_robot_logs.py @@ -110,6 +110,7 @@ def identify_labware_ids( file_results: Dict[str, Any], labware_name: Optional[str] ) -> List[str]: """Determine what type of labware is being picked up.""" + list_of_labware_ids: List[str] = [] if labware_name: labwares = file_results.get("labware", "") list_of_labware_ids = [] @@ -133,7 +134,8 @@ def match_pipette_to_action( left_pipette_add = 0 for command in commandTypes: command_type = command_dict["commandType"] - command_pipette = command_dict.get("pipetteId", "") + command_params = command_dict.get("params", "") + command_pipette = command_params.get("pipetteId", "") if command_type == command and command_pipette == right_pipette: right_pipette_add = 1 elif command_type == command and command_pipette == left_pipette: @@ -212,8 +214,89 @@ def instrument_commands( return pipette_dict +def get_comment_result_by_string(file_results: Dict[str, Any], key_phrase: str) -> str: + """Get comment string based off ky phrase.""" + commandData = file_results.get("commands", "") + result_str = command_str = "" + for command in commandData: + commandType = command["commandType"] + if commandType == "comment": + command_str = command["params"].get("message", "") + try: + result_str = command_str.split(key_phrase)[1] + except IndexError: + continue + return result_str + + +def get_protocol_version_number(file_results: Dict[str, Any]) -> str: + """Get protocol version number.""" + return get_comment_result_by_string(file_results, "Protocol Version: ") + + +def get_liquid_waste_height(file_results: Dict[str, Any]) -> float: + """Find liquid waste height.""" + result_str = get_comment_result_by_string( + file_results, "Liquid Waste Total Height: " + ) + try: + height = float(result_str) + except ValueError: + height = 0.0 + return height + + +def liquid_height_commands( + file_results: Dict[str, Any], all_heights_list: List[List[Any]] +) -> List[List[Any]]: + """Record found liquid heights during a protocol.""" + commandData = file_results.get("commands", "") + robot = file_results.get("robot_name", "") + run_id = file_results.get("run_id", "") + list_of_heights = [] + print(robot) + liquid_waste_height = 0.0 + for command in commandData: + commandType = command["commandType"] + if commandType == "comment": + result = command["params"].get("message", "") + try: + result_str = "'" + result.split("result: {")[1] + "'" + entries = result_str.split(", (") + comment_time = command["completedAt"] + for entry in entries: + height = float(entry.split(": ")[1].split("'")[0].split("}")[0]) + labware_type = str( + entry.split(",")[0].replace("'", "").replace("(", "") + ) + well_location = str(entry.split(", ")[1].split(" ")[0]) + slot_location = str(entry.split("slot ")[1].split(")")[0]) + labware_name = str(entry.split("of ")[1].split(" on")[0]) + if labware_name == "Liquid Waste": + liquid_waste_height += height + one_entry = { + "Timestamp": comment_time, + "Labware Name": labware_name, + "Labware Type": labware_type, + "Slot Location": slot_location, + "Well Location": well_location, + "All Heights (mm)": height, + } + list_of_heights.append(one_entry) + except (IndexError, ValueError): + continue + if len(list_of_heights) > 0: + all_heights_list[0].append(robot) + all_heights_list[1].append(run_id) + all_heights_list[2].append(list_of_heights) + all_heights_list[3].append(liquid_waste_height) + return all_heights_list + + def plate_reader_commands( - file_results: Dict[str, Any], hellma_plate_standards: List[Dict[str, Any]] + file_results: Dict[str, Any], + hellma_plate_standards: List[Dict[str, Any]], + orientation: bool, ) -> Dict[str, object]: """Plate Reader Command Counts.""" commandData = file_results.get("commands", "") @@ -242,38 +325,49 @@ def plate_reader_commands( read = "yes" elif read == "yes" and commandType == "comment": result = command["params"].get("message", "") - formatted_result = result.split("result: ")[1] - result_dict = eval(formatted_result) - result_dict_keys = list(result_dict.keys()) - if len(result_dict_keys) > 1: - read_type = "multi" - else: - read_type = "single" - for wavelength in result_dict_keys: - one_wavelength_dict = result_dict.get(wavelength) - result_ndarray = plate_reader.convert_read_dictionary_to_array( - one_wavelength_dict - ) - for item in hellma_plate_standards: - wavelength_of_interest = item["wavelength"] - if str(wavelength) == str(wavelength_of_interest): - error_cells = plate_reader.check_byonoy_data_accuracy( - result_ndarray, item, False + if "result:" in result or "Result:" in result: + try: + plate_name = result.split("result:")[0] + formatted_result = result.split("result: ")[1] + except IndexError: + plate_name = result.split("Result:")[0] + formatted_result = result.split("Result: ")[1] + result_dict = eval(formatted_result) + result_dict_keys = list(result_dict.keys()) + if len(result_dict_keys) > 1: + read_type = "multi" + else: + read_type = "single" + if "hellma_plate" in plate_name: + for wavelength in result_dict_keys: + one_wavelength_dict = result_dict.get(wavelength) + result_ndarray = plate_reader.convert_read_dictionary_to_array( + one_wavelength_dict ) - if len(error_cells[0]) > 0: - percent = (96 - len(error_cells)) / 96 * 100 - for cell in error_cells: - print( - "FAIL: Cell " + str(cell) + " out of accuracy spec." + for item in hellma_plate_standards: + wavelength_of_interest = item["wavelength"] + if str(wavelength) == str(wavelength_of_interest): + error_cells = plate_reader.check_byonoy_data_accuracy( + result_ndarray, item, orientation ) - else: - percent = 100 - print( - f"PASS: {wavelength_of_interest} meet accuracy specification" - ) - final_result[read_type, wavelength, read_num] = percent - read_num += 1 - read = "no" + if len(error_cells[0]) > 0: + percent = (96 - len(error_cells)) / 96 * 100 + for cell in error_cells: + print( + "FAIL: Cell " + + str(cell) + + " out of accuracy spec." + ) + else: + percent = 100 + print( + f"PASS: {wavelength_of_interest} meet accuracy spec." + ) + final_result[read_type, wavelength, read_num] = percent + read_num += 1 + else: + final_result = result_dict + read = "no" plate_dict = { "Plate Reader # of Reads": read_count, "Plate Reader Avg Read Time (sec)": avg_read_time, @@ -341,8 +435,9 @@ def hs_commands(file_results: Dict[str, Any]) -> Dict[str, float]: ) if temp_time is not None and deactivate_time is None: # If heater shaker module is not deactivated, protocol completedAt time stamp used. + default = commandData[len(commandData) - 1].get("completedAt") protocol_end = datetime.strptime( - file_results.get("completedAt", ""), "%Y-%m-%dT%H:%M:%S.%f%z" + file_results.get("completedAt", default), "%Y-%m-%dT%H:%M:%S.%f%z" ) temp_duration = (protocol_end - temp_time).total_seconds() hs_temps[hs_temp] = hs_temps.get(hs_temp, 0.0) + temp_duration @@ -389,8 +484,9 @@ def temperature_module_commands(file_results: Dict[str, Any]) -> Dict[str, Any]: 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. + default = commandData[len(commandData) - 1].get("completedAt") protocol_end = datetime.strptime( - file_results.get("completedAt", ""), "%Y-%m-%dT%H:%M:%S.%f%z" + file_results.get("completedAt", default), "%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 @@ -473,15 +569,17 @@ def thermocycler_commands(file_results: Dict[str, Any]) -> Dict[str, float]: 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 + default = commandData[len(commandData) - 1].get("completedAt") protocol_end = datetime.strptime( - file_results.get("completedAt", ""), "%Y-%m-%dT%H:%M:%S.%f%z" + file_results.get("completedAt", default), "%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 + default = commandData[len(commandData) - 1].get("completedAt") protocol_end = datetime.strptime( - file_results.get("completedAt", ""), "%Y-%m-%dT%H:%M:%S.%f%z" + file_results.get("completedAt", default), "%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 diff --git a/abr-testing/abr_testing/data_collection/single_run_log_reader.py b/abr-testing/abr_testing/data_collection/single_run_log_reader.py index 39060529c89..bf10347faff 100644 --- a/abr-testing/abr_testing/data_collection/single_run_log_reader.py +++ b/abr-testing/abr_testing/data_collection/single_run_log_reader.py @@ -38,10 +38,9 @@ run_ids_in_storage, run_log_file_path, "", - "", - "", - hellma_plate_standards=file_values, + file_values, ) + print("list_of_heights not recorded.") transposed_list = list(zip(*runs_and_robots)) # Adds Run to local csv sheet_location = os.path.join(run_log_file_path, "saved_data.csv") diff --git a/abr-testing/abr_testing/protocol_simulation/abr_sim_check.py b/abr-testing/abr_testing/protocol_simulation/abr_sim_check.py index 513860baa9b..76852f70b9c 100644 --- a/abr-testing/abr_testing/protocol_simulation/abr_sim_check.py +++ b/abr-testing/abr_testing/protocol_simulation/abr_sim_check.py @@ -1,34 +1,107 @@ """Check ABR Protocols Simulate Successfully.""" from abr_testing.protocol_simulation import simulation_metrics import os -import traceback from pathlib import Path +from typing import Dict, List, Tuple, Union +import traceback -def run(file_to_simulate: str) -> None: +def run( + file_dict: Dict[str, Dict[str, Union[str, Path]]], labware_defs: List[Path] +) -> None: """Simulate protocol and raise errors.""" - protocol_name = Path(file_to_simulate).stem - try: - simulation_metrics.main(file_to_simulate, False) - except Exception: - print(f"Error in protocol: {protocol_name}") - traceback.print_exc() + for file in file_dict: + path = file_dict[file]["path"] + csv_params = "" + try: + csv_params = str(file_dict[file]["csv"]) + except KeyError: + pass + try: + print(f"Simulating {file}") + simulation_metrics.main( + protocol_file_path=Path(path), + save=False, + parameters=csv_params, + extra_files=labware_defs, + ) + except Exception as e: + traceback.print_exc() + print(str(e)) + print("\n") + + +def search(seq: str, dictionary: dict) -> str: + """Search for specific sequence in file.""" + for key in dictionary.keys(): + parts = key.split("_") + if parts[0] == seq: + return key + return "" + + +def get_files() -> Tuple[Dict[str, Dict[str, Union[str, Path]]], List[Path]]: + """Map protocols with corresponding csv files.""" + file_dict: Dict[str, Dict[str, Union[str, Path]]] = {} + labware_defs = [] + for root, directories, _ in os.walk(root_dir): + for directory in directories: + if directory not in exclude: + active_dir = os.path.join(root, directory) + for file in os.listdir( + active_dir + ): # Iterate over files in `active_protocols` + if file.endswith(".py") and file not in exclude: + file_dict[file] = {} + file_dict[file]["path"] = Path( + os.path.abspath( + os.path.join(root_dir, os.path.join(directory, file)) + ) + ) + if directory == "csv_parameters": + active_dir = os.path.join(root, directory) + for file in os.listdir( + active_dir + ): # Iterate over files in `active_protocols` + if file.endswith(".csv") and file not in exclude: + search_str = file.split("_")[0] + protocol = search(search_str, file_dict) + if protocol: + file_dict[protocol]["csv"] = str( + os.path.abspath( + os.path.join( + root_dir, os.path.join(directory, file) + ) + ) + ) + if directory == "custom_labware": + active_dir = os.path.join(root, directory) + for file in os.listdir( + active_dir + ): # Iterate over files in `active_protocols` + if file.endswith(".json") and file not in exclude: + labware_defs.append( + Path( + os.path.abspath( + os.path.join( + root_dir, os.path.join(directory, file) + ) + ) + ) + ) + return (file_dict, labware_defs) if __name__ == "__main__": # Directory to search + global root_dir root_dir = "abr_testing/protocols" - + global exclude exclude = [ "__init__.py", - "shared_vars_and_funcs.py", + "helpers.py", ] - # Walk through the root directory and its subdirectories - for root, dirs, files in os.walk(root_dir): - for file in files: - if file.endswith(".py"): # If it's a Python file - if file in exclude: - continue - file_path = os.path.join(root, file) - print(f"Simulating protocol: {file_path}") - run(file_path) + print("Simulating Protocols") + file_dict, labware_defs = get_files() + # print(file_dict) + run(file_dict, labware_defs) diff --git a/abr-testing/abr_testing/protocol_simulation/simulation_metrics.py b/abr-testing/abr_testing/protocol_simulation/simulation_metrics.py index 418c1e1aacd..10c7ea12782 100644 --- a/abr-testing/abr_testing/protocol_simulation/simulation_metrics.py +++ b/abr-testing/abr_testing/protocol_simulation/simulation_metrics.py @@ -6,6 +6,7 @@ from opentrons.cli import analyze import json import argparse +import traceback from datetime import datetime from abr_testing.automation import google_sheets_tool from abr_testing.data_collection import read_robot_logs @@ -13,13 +14,39 @@ from abr_testing.tools import plate_reader +def build_parser() -> Any: + """Builds argument parser.""" + 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( + "sheet_name", + metavar="SHEET_NAME", + type=str, + nargs=1, + help="Name of sheet to upload results to", + ) + parser.add_argument( + "protocol_file_path", + metavar="PROTOCOL_FILE_PATH", + type=str, + nargs="*", + help="Path to protocol file(s)", + ) + return parser + + def set_api_level(protocol_file_path: str) -> None: """Set API level for analysis.""" with open(protocol_file_path, "r") as file: file_contents = file.readlines() # Look for current'apiLevel:' for i, line in enumerate(file_contents): - print(line) if "apiLevel" in line: print(f"The current API level of this protocol is: {line}") change = ( @@ -27,12 +54,10 @@ def set_api_level(protocol_file_path: str) -> None: .strip() .upper() ) - if change == "Y": api_level = input("Protocol API Level to Simulate with: ") # Update new API level file_contents[i] = f"apiLevel: {api_level}\n" - print(f"Updated line: {file_contents[i]}") break with open(protocol_file_path, "w") as file: file.writelines(file_contents) @@ -200,7 +225,11 @@ def parse_results_volume( else: print(f"Expected JSON object (dict) but got {type(json_data).__name__}.") commands = {} - + hellma_plate_orientation = False + parameters = json_data.get("runTimeParameters", "") + for parameter in parameters: + if parameter["displayName"] == "Hellma Plate Orientation": + hellma_plate_orientation = bool(parameter["value"]) start_time = datetime.fromisoformat(commands[0]["createdAt"]) end_time = datetime.fromisoformat(commands[len(commands) - 1]["completedAt"]) header = ["", "Protocol Name", "Date", "Time"] @@ -241,6 +270,7 @@ def parse_results_volume( "Right Pipette Total Aspirates", "Right Pipette Total Dispenses", "Gripper Pick Ups", + "Gripper Pick Ups of opentrons_tough_pcr_auto_sealing_lid", "Total Liquid Probes", "Average Liquid Probe Time (sec)", ] @@ -257,7 +287,7 @@ def parse_results_volume( temp_module_dict = read_robot_logs.temperature_module_commands(json_data) thermo_cycler_dict = read_robot_logs.thermocycler_commands(json_data) plate_reader_dict = read_robot_logs.plate_reader_commands( - json_data, hellma_plate_standards + json_data, hellma_plate_standards, hellma_plate_orientation ) instrument_dict = read_robot_logs.instrument_commands( json_data, labware_name=None @@ -302,6 +332,7 @@ def parse_results_volume( total_time_row.append(str(end_time - start_time)) for metric in metrics: + print(f"Dictionary: {metric}\n\n") for cmd in metric.keys(): values_row.append(str(metric[cmd])) return ( @@ -320,21 +351,24 @@ def parse_results_volume( def main( - protocol_file_path_name: str, + protocol_file_path: Path, save: bool, storage_directory: str = os.curdir, google_sheet_name: str = "", + parameters: str = "", + extra_files: List[Path] = [], ) -> None: """Main module control.""" sys.exit = mock_exit # Replace sys.exit with the mock function - # Read file path from arguments - protocol_file_path = Path(protocol_file_path_name) - protocol_name = protocol_file_path.stem - print("Simulating", protocol_name) + # Simulation run date file_date = datetime.now() file_date_formatted = file_date.strftime("%Y-%m-%d_%H-%M-%S") error_output = f"{storage_directory}\\test_debug" - # Run protocol simulation + protocol_name = protocol_file_path.stem + protocol_files = [protocol_file_path] + if extra_files != []: + protocol_files += extra_files + print("Simulating....") try: with Context(analyze) as ctx: if save: @@ -344,29 +378,62 @@ def main( ) json_file_output = open(json_file_path, "wb+") # log_output_file = f"{protocol_name}_log" - ctx.invoke( - analyze, - files=[protocol_file_path], - json_output=json_file_output, - human_json_output=None, - log_output=error_output, - log_level="ERROR", - check=False, - ) + if parameters: + csv_params = {} + csv_params["parameters_csv"] = parameters + rtp_json = json.dumps(csv_params) + ctx.invoke( + analyze, + files=protocol_files, + rtp_files=rtp_json, + json_output=json_file_output, + human_json_output=None, + log_output=error_output, + log_level="ERROR", + check=False, + ) + + else: + ctx.invoke( + analyze, + files=protocol_files, + json_output=json_file_output, + human_json_output=None, + log_output=error_output, + log_level="ERROR", + check=False, + ) json_file_output.close() else: - ctx.invoke( - analyze, - files=[protocol_file_path], - json_output=None, - human_json_output=None, - log_output=error_output, - log_level="ERROR", - check=True, - ) - + if parameters: + csv_params = {} + csv_params["parameters_csv"] = parameters + rtp_json = json.dumps(csv_params) + ctx.invoke( + analyze, + files=protocol_files, + rtp_files=rtp_json, + json_output=None, + human_json_output=None, + log_output=error_output, + log_level="ERROR", + check=True, + ) + else: + ctx.invoke( + analyze, + files=protocol_files, + json_output=None, + human_json_output=None, + log_output=error_output, + log_level="ERROR", + check=True, + ) + print("done!") except SystemExit as e: print(f"SystemExit caught with code: {e}") + if e != 0: + traceback.print_exc finally: # Reset sys.exit to the original behavior sys.exit = original_exit @@ -376,11 +443,12 @@ def main( if not errors: pass else: - print(errors) - sys.exit(1) + print(f"Error:\n{errors}") + raise except FileNotFoundError: print("error simulating ...") - sys.exit() + raise + open_file.close if save: try: credentials_path = os.path.join(storage_directory, "credentials.json") @@ -395,6 +463,7 @@ def main( credentials_path, google_sheet_name, 0 ) google_sheet.write_to_row([]) + for row in parse_results_volume( json_file_path, protocol_name, @@ -407,34 +476,51 @@ def main( google_sheet.write_to_row(row) +def check_params(protocol_path: str) -> str: + """Check if protocol requires supporting files.""" + print("checking for parameters") + with open(protocol_path, "r") as f: + lines = f.readlines() + file_as_str = "".join(lines) + if ( + "parameters.add_csv_file" in file_as_str + or "helpers.create_csv_parameter" in file_as_str + ): + params = "" + while not params: + name = Path(protocol_file_path).stem + params = input( + f"Protocol {name} needs a CSV parameter file. Please enter the path: " + ) + if os.path.exists(params): + return params + else: + params = "" + print("Invalid file path") + return "" + + +def get_extra_files(protocol_file_path: str) -> tuple[str, List[Path]]: + """Get supporting files for protocol simulation if needed.""" + params = check_params(protocol_file_path) + needs_files = input("Does your protocol utilize custom labware? (Y/N): ") + labware_files = [] + if needs_files == "Y": + num_labware = input("How many custom labware?: ") + for labware_num in range(int(num_labware)): + path = input("Enter custom labware definition path: ") + labware_files.append(Path(path)) + return (params, labware_files) + + if __name__ == "__main__": CLEAN_PROTOCOL = True - 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( - "sheet_name", - metavar="SHEET_NAME", - type=str, - nargs=1, - help="Name of sheet to upload results to", - ) - parser.add_argument( - "protocol_file_path", - metavar="PROTOCOL_FILE_PATH", - type=str, - nargs=1, - help="Path to protocol file", - ) - args = parser.parse_args() + args = build_parser().parse_args() storage_directory = args.storage_directory[0] sheet_name = args.sheet_name[0] protocol_file_path: str = args.protocol_file_path[0] + parameters: List[str] = args.protocol_file_path[1:] + print(parameters) SETUP = True while SETUP: print( @@ -445,7 +531,7 @@ def main( choice = "" while not choice: choice = input( - "Remove air_gap commands to ensure accurate results? (Y/N): " + "Remove air_gap commands to ensure accurate results: (continue)? (Y/N): " ) if choice.upper() == "Y": SETUP = False @@ -462,11 +548,18 @@ def main( # Change api level if CLEAN_PROTOCOL: set_api_level(protocol_file_path) - main( - protocol_file_path, - True, - storage_directory, - sheet_name, - ) + params, extra_files = get_extra_files(protocol_file_path) + try: + main( + protocol_file_path=Path(protocol_file_path), + save=True, + storage_directory=storage_directory, + google_sheet_name=sheet_name, + parameters=params, + extra_files=extra_files, + ) + except Exception as e: + traceback.print_exc() + sys.exit(str(e)) else: sys.exit(0) diff --git a/abr-testing/abr_testing/protocols/__init__.py b/abr-testing/abr_testing/protocols/__init__.py new file mode 100644 index 00000000000..2f0d01ea241 --- /dev/null +++ b/abr-testing/abr_testing/protocols/__init__.py @@ -0,0 +1 @@ +"""protocols.""" diff --git a/abr-testing/abr_testing/protocols/active_protocols/10_ZymoBIOMICS_Magbead_DNA_Cells_Flex.py b/abr-testing/abr_testing/protocols/active_protocols/10_ZymoBIOMICS_Magbead_DNA_Cells_Flex.py new file mode 100644 index 00000000000..fbc77c9ed46 --- /dev/null +++ b/abr-testing/abr_testing/protocols/active_protocols/10_ZymoBIOMICS_Magbead_DNA_Cells_Flex.py @@ -0,0 +1,545 @@ +"""Flex ZymoBIOMICS Magbead DNA Extraction: Cells.""" +import math +from opentrons import types +from typing import List, Dict +from opentrons import protocol_api +from opentrons.protocol_api import Well, InstrumentContext +import numpy as np +from opentrons.protocol_api.module_contexts import ( + HeaterShakerContext, + TemperatureModuleContext, + MagneticBlockContext, +) +from abr_testing.protocols import helpers + +metadata = { + "author": "Zach Galluzzo ", + "protocolName": "Flex ZymoBIOMICS Magbead DNA Extraction: Cells", +} + +requirements = {"robotType": "Flex", "apiLevel": "2.21"} +""" +Slot A1: Tips 1000 +Slot A2: Tips 1000 +Slot A3: Temperature module (gen2) with 96 well PCR block and Armadillo 96 well PCR Plate +Slot B1: Tips 1000 +Slot B3: Nest 1 Well Reservoir +Slot C1: Magblock +Slot C2: Nest 12 well 15 ml Reservoir +Slot D1: H-S with Nest 96 Well Deepwell and DW Adapter +Slot D2: Nest 12 well 15 ml Reservoir +Slot D3: Trash + +Reservoir 1: +Well 1 - 12,320 ul +Wells 2-4 - 11,875 ul +Wells 5-6 - 13,500 ul +Wells 7-8 - 13,500 ul +Well 12 - 5,200 ul + +Reservoir 2: +Wells 1-12 - 9,000 ul + +""" +whichwash = 0 +wash_volume_tracker = 0.0 +sample_max = 48 +tip1k = 0 +drop_count = 0 +m1000_tips = 0 + + +def add_parameters(parameters: protocol_api.ParameterContext) -> None: + """Define parameters.""" + helpers.create_hs_speed_parameter(parameters) + helpers.create_single_pipette_mount_parameter(parameters) + helpers.create_dot_bottom_parameter(parameters) + helpers.create_deactivate_modules_parameter(parameters) + + +def run(protocol: protocol_api.ProtocolContext) -> None: + """Protocol Set Up.""" + heater_shaker_speed = protocol.params.heater_shaker_speed # type: ignore[attr-defined] + mount = protocol.params.pipette_mount # type: ignore[attr-defined] + dot_bottom = protocol.params.dot_bottom # type: ignore[attr-defined] + deactivate_modules_bool = protocol.params.deactivate_modules # type: ignore[attr-defined] + helpers.comment_protocol_version(protocol, "01") + + dry_run = False + TIP_TRASH = ( + False # True = Used tips go in Trash, False = Used tips go back into rack + ) + res_type = "nest_12_reservoir_15ml" + global m1000_tips + num_samples = 96 + wash1_vol = wash2_vol = wash3_vol = 400.0 + lysis_vol = 90.0 + sample_vol = 10.0 # Sample should be pelleted tissue/bacteria/cells + bind_vol = 600.0 + bind2_vol = 500.0 + elution_vol = 75.0 + + def tipcheck(m1000: InstrumentContext) -> None: + """Tip tracking function.""" + global m1000_tips + if m1000_tips >= 3 * 96: + m1000.reset_tipracks() + m1000_tips == 0 + m1000.pick_up_tip() + m1000_tips += 8 + + # Protocol Parameters + deepwell_type = "nest_96_wellplate_2ml_deep" + + if not dry_run: + settling_time = 2.0 + lysis_incubation = 30.0 + bind_time_1 = 10.0 + bind_time_2 = 1.0 + wash_time = 5.0 + drybeads = 9.0 + lysis_rep_1 = 3 + lysis_rep_2 = 5 + bead_reps_2 = 8 + else: + settling_time = 0.25 + lysis_incubation = 0.25 + bind_time_1 = bind_time_2 = wash_time = 0.25 + drybeads = 0.5 + lysis_rep_1 = lysis_rep_2 = bead_reps_2 = 1 + bead_vol = 25.0 + starting_vol = lysis_vol + sample_vol + binding_buffer_vol = bind_vol + bead_vol + protocol.load_trash_bin("A3") + h_s: HeaterShakerContext = protocol.load_module( + helpers.hs_str, "D1" + ) # type: ignore[assignment] + labware_name = "Samples" + sample_plate, h_s_adapter = helpers.load_hs_adapter_and_labware( + deepwell_type, h_s, labware_name + ) + h_s.close_labware_latch() + + temp: TemperatureModuleContext = protocol.load_module( + helpers.temp_str, "D3" + ) # type: ignore[assignment] + elutionplate, temp_adapter = helpers.load_temp_adapter_and_labware( + "armadillo_96_wellplate_200ul_pcr_full_skirt", temp, "Elution Plate" + ) + magblock: MagneticBlockContext = protocol.load_module( + helpers.mag_str, "C1" + ) # type: ignore[assignment] + waste_reservoir = protocol.load_labware( + "nest_1_reservoir_290ml", "B3", "Liquid Waste" + ) + waste = waste_reservoir.wells()[0].top() + res1 = protocol.load_labware(res_type, "D2", "reagent reservoir 1") + res2 = protocol.load_labware(res_type, "C2", "reagent reservoir 2") + res3 = protocol.load_labware(res_type, "B2", "reagent reservoir 3") + num_cols = math.ceil(num_samples / 8) + + # Load tips and combine all similar boxes + tips1000 = protocol.load_labware("opentrons_flex_96_tiprack_1000ul", "A1", "Tips 1") + tips1001 = protocol.load_labware("opentrons_flex_96_tiprack_1000ul", "A2", "Tips 2") + tips1002 = protocol.load_labware("opentrons_flex_96_tiprack_1000ul", "B1", "Tips 3") + tips_sn = tips1000.wells()[:num_samples] + # load instruments + m1000 = protocol.load_instrument( + "flex_8channel_1000", mount, tip_racks=[tips1000, tips1001, tips1002] + ) + + """ + Here is where you can define the locations of your reagents. + """ + lysis_ = res1.wells()[0] + binding_buffer = res1.wells()[1:8] + bind2_res = res1.wells()[8:12] + all_washes = res2.wells()[1:] + elution_solution = res2.wells()[0] + all_washes.extend(res3.wells()[:2]) + samples_m = sample_plate.rows()[0][:num_cols] + elution_samples_m = elutionplate.rows()[0][:num_cols] + # Redefine per well for liquid definitions + samps = sample_plate.wells()[: (8 * num_cols)] + liquid_vols_and_wells: Dict[str, List[Dict[str, Well | List[Well] | float]]] = { + "Lysis and PK": [{"well": lysis_, "volume": 12320.0}], + "Beads and Binding": [{"well": binding_buffer, "volume": 11875.0}], + "Binding 2": [{"well": bind2_res, "volume": 13500.0}], + "Final Elution": [{"well": elution_solution, "volume": 7500.0}], + "Samples": [{"well": samps, "volume": 0.0}], + "Reagents": [{"well": all_washes, "volume": 9800.0}], + } + helpers.find_liquid_height_of_loaded_liquids(protocol, liquid_vols_and_wells, m1000) + + m1000.flow_rate.aspirate = 300 + m1000.flow_rate.dispense = 300 + m1000.flow_rate.blow_out = 300 + + def remove_supernatant(vol: float) -> None: + """Remove supernatant.""" + protocol.comment("-----Removing Supernatant-----") + m1000.flow_rate.aspirate = 30 + num_trans = math.ceil(vol / 980) + vol_per_trans = vol / num_trans + + for i, m in enumerate(samples_m): + m1000.pick_up_tip(tips_sn[8 * i]) + loc = m.bottom(dot_bottom) + for _ in range(num_trans): + if m1000.current_volume > 0: + # void air gap if necessary + m1000.dispense(m1000.current_volume, m.top()) + m1000.move_to(m.center()) + m1000.transfer(vol_per_trans, loc, waste, new_tip="never", air_gap=20) + m1000.blow_out(waste) + m1000.air_gap(20) + m1000.drop_tip(tips_sn[8 * i]) if TIP_TRASH else m1000.return_tip() + m1000.flow_rate.aspirate = 300 + + # Transfer from Magdeck plate to H-S + helpers.move_labware_to_hs(protocol, sample_plate, h_s, h_s_adapter) + + def bead_mixing( + well: Well, pip: InstrumentContext, mvol: float, reps: int = 8 + ) -> None: + """Mixing. + + 'mixing' will mix liquid that contains beads. This will be done by + aspirating from the bottom of the well and dispensing from the top as to + mix the beads with the other liquids as much as possible. Aspiration and + dispensing will also be reversed for a short to to ensure maximal mixing. + param well: The current well that the mixing will occur in. + param pip: The pipet that is currently attached/ being used. + param mvol: The volume that is transferred before the mixing steps. + param reps: The number of mix repetitions that should occur. Note~ + During each mix rep, there are 2 cycles of aspirating from bottom, + dispensing at the top and 2 cycles of aspirating from middle, + dispensing at the bottom + """ + center = well.top().move(types.Point(x=0, y=0, z=5)) + aspbot = well.bottom().move(types.Point(x=0, y=2, z=1)) + asptop = well.bottom().move(types.Point(x=0, y=-2, z=2)) + disbot = well.bottom().move(types.Point(x=0, y=2, z=3)) + distop = well.top().move(types.Point(x=0, y=1, z=-5)) + + if mvol > 1000: + mvol = 1000 + + vol = mvol * 0.9 + + pip.flow_rate.aspirate = 500 + pip.flow_rate.dispense = 500 + + pip.move_to(center) + for _ in range(reps): + pip.aspirate(vol, aspbot) + pip.dispense(vol, distop) + pip.aspirate(vol, asptop) + pip.dispense(vol, disbot) + if _ == reps - 1: + pip.flow_rate.aspirate = 150 + pip.flow_rate.dispense = 100 + pip.aspirate(vol, aspbot) + pip.dispense(vol, aspbot) + + pip.flow_rate.aspirate = 300 + pip.flow_rate.dispense = 300 + + def mixing(well: Well, pip: InstrumentContext, mvol: float, reps: int = 8) -> None: + """Mixing. + + 'mixing' will mix liquid that contains beads. This will be done by + aspirating from the bottom of the well and dispensing from the top as to + mix the beads with the other liquids as much as possible. Aspiration and + dispensing will also be reversed for a short to to ensure maximal mixing. + param well: The current well that the mixing will occur in. + param pip: The pipet that is currently attached/ being used. + param mvol: The volume that is transferred before the mixing steps. + param reps: The number of mix repetitions that should occur. Note~ + During each mix rep, there are 2 cycles of aspirating from bottom, + dispensing at the top and 2 cycles of aspirating from middle, + dispensing at the bottom + """ + pip.liquid_presence_detection = False + center = well.top(5) + asp = well.bottom(1) + disp = well.top(-8) + + if mvol > 1000: + mvol = 1000 + + vol = mvol * 0.9 + + pip.flow_rate.aspirate = 500 + pip.flow_rate.dispense = 500 + + pip.move_to(center) + for _ in range(reps): + pip.aspirate(vol, asp) + pip.dispense(vol, disp) + pip.aspirate(vol, asp) + pip.dispense(vol, disp) + if _ == reps - 1: + pip.flow_rate.aspirate = 150 + pip.flow_rate.dispense = 100 + pip.aspirate(vol, asp) + pip.dispense(vol, asp) + + pip.flow_rate.aspirate = 300 + pip.flow_rate.dispense = 300 + + def lysis(vol: float, source: Well) -> None: + """Lysis.""" + protocol.comment("-----Beginning Lysis Steps-----") + num_transfers = math.ceil(vol / 980) + tipcheck(m1000) + total_lysis_aspirated = 0.0 + for i in range(num_cols): + src = source + tvol = vol / num_transfers + # Mix Shield and PK before transferring first time + if i == 0: + m1000.liquid_presence_detection = ( + False # turn off liquid detection during mixing + ) + for x in range(lysis_rep_1): + m1000.aspirate(vol, src.bottom(1)) + m1000.dispense(vol, src.bottom(8)) + # Transfer Shield and PK + for t in range(num_transfers): + m1000.require_liquid_presence(src) + m1000.aspirate(tvol, src.bottom(1)) + m1000.air_gap(10) + m1000.dispense(m1000.current_volume, samples_m[i].top()) + total_lysis_aspirated += tvol * 8 + # Mix shield and pk with samples + for i in range(num_cols): + if i != 0: + tipcheck(m1000) + mixing(samples_m[i], m1000, tvol, reps=lysis_rep_2) + m1000.drop_tip() if TIP_TRASH else m1000.return_tip() + helpers.set_hs_speed(protocol, h_s, heater_shaker_speed, lysis_incubation, True) + + def bind(vol1: float, vol2: float) -> None: + """Binding. + + `bind` will perform magnetic bead binding on each sample in the + deepwell plate. Each channel of binding beads will be mixed before + transfer, and the samples will be mixed with the binding beads after + the transfer. The magnetic deck activates after the addition to all + samples, and the supernatant is removed after bead bining. + :param vol (float): The amount of volume to aspirate from the elution + buffer source and dispense to each well containing + beads. + :param park (boolean): Whether to save sample-corresponding tips + between adding elution buffer and transferring + supernatant to the final clean elutions PCR + plate. + """ + protocol.comment("-----Beginning Binding Steps-----") + for i, well in enumerate(samples_m): + tipcheck(m1000) + num_trans = math.ceil(vol1 / 980) + vol_per_trans = vol1 / num_trans + source = binding_buffer[i // 2] + if i == 0: + reps = 5 + else: + reps = 2 + bead_mixing(source, m1000, vol_per_trans, reps=reps if not dry_run else 1) + # Transfer beads and binding from source to H-S plate + for t in range(num_trans): + if m1000.current_volume > 0: + # void air gap if necessary + m1000.dispense(m1000.current_volume, source.top()) + m1000.require_liquid_presence(source) + m1000.transfer( + vol_per_trans, source, well.top(), air_gap=20, new_tip="never" + ) + m1000.air_gap(20) + bead_mixing(well, m1000, vol_per_trans, reps=bead_reps_2) + m1000.blow_out() + m1000.air_gap(10) + m1000.drop_tip() if TIP_TRASH else m1000.return_tip() + + helpers.set_hs_speed( + protocol, h_s, heater_shaker_speed * 0.9, bind_time_1, True + ) + + # Transfer from H-S plate to Magdeck plate + helpers.move_labware_from_hs_to_destination( + protocol, sample_plate, h_s, magblock + ) + + for bindi in np.arange( + settling_time + 1, 0, -0.5 + ): # Settling time delay with countdown timer + protocol.delay( + minutes=0.5, + msg="There are " + str(bindi) + " minutes left in the incubation.", + ) + + # remove initial supernatant + remove_supernatant(vol1 + starting_vol) + + protocol.comment("-----Beginning Bind #2 Steps-----") + tipcheck(m1000) + for i, well in enumerate(samples_m): + num_trans = math.ceil(vol2 / 980) + vol_per_trans = vol2 / num_trans + source = bind2_res[i // 3] + if i == 0 or i == 3: + height = 10 + else: + height = 1 + # Transfer beads and binding from source to H-S plate + for t in range(num_trans): + if m1000.current_volume > 0: + # void air gap if necessary + m1000.dispense(m1000.current_volume, source.top()) + m1000.transfer( + vol_per_trans, + source.bottom(height), + well.top(), + air_gap=20, + new_tip="never", + ) + m1000.air_gap(20) + + for i in range(num_cols): + if i != 0: + tipcheck(m1000) + bead_mixing( + samples_m[i], m1000, vol_per_trans, reps=3 if not dry_run else 1 + ) + m1000.drop_tip() if TIP_TRASH else m1000.return_tip() + helpers.set_hs_speed(protocol, h_s, heater_shaker_speed, bind_time_2, True) + + # Transfer from H-S plate to Magdeck plate + helpers.move_labware_from_hs_to_destination( + protocol, sample_plate, h_s, magblock + ) + + for bindi in np.arange( + settling_time + 1, 0, -0.5 + ): # Settling time delay with countdown timer + protocol.delay( + minutes=0.5, + msg="There are " + str(bindi) + " minutes left in the incubation.", + ) + + # remove initial supernatant + remove_supernatant(vol2 + 25) + + def wash(vol: float, source: List[Well]) -> None: + """Wash Steps.""" + global whichwash # Defines which wash the protocol is on to log on the app + protocol.comment("-----Now starting Wash #" + str(whichwash) + "-----") + global wash_volume_tracker + + num_trans = math.ceil(vol / 980) + vol_per_trans = vol / num_trans + + tipcheck(m1000) + for i, m in enumerate(samples_m): + src = source[whichwash] + for n in range(num_trans): + if m1000.current_volume > 0: + m1000.dispense(m1000.current_volume, src.top()) + m1000.require_liquid_presence(src) + m1000.transfer( + vol_per_trans, + src.bottom(dot_bottom), + m.top(), + air_gap=20, + new_tip="never", + ) + wash_volume_tracker += vol_per_trans * 8 + if wash_volume_tracker >= 9600: + whichwash += 1 + src = source[whichwash] + protocol.comment(f"new wash source {whichwash}") + wash_volume_tracker = 0.0 + m1000.drop_tip() if TIP_TRASH else m1000.return_tip() + helpers.set_hs_speed(protocol, h_s, heater_shaker_speed * 0.9, wash_time, True) + + helpers.move_labware_from_hs_to_destination( + protocol, sample_plate, h_s, magblock + ) + + for washi in np.arange( + settling_time, 0, -0.5 + ): # settling time timer for washes + protocol.delay( + minutes=0.5, + msg="There are " + + str(washi) + + " minutes left in wash " + + str(whichwash) + + " incubation.", + ) + + remove_supernatant(vol) + + def elute(vol: float) -> None: + tipcheck(m1000) + total_elution_vol = 0.0 + for i, m in enumerate(samples_m): + m1000.require_liquid_presence(elution_solution) + m1000.aspirate(vol, elution_solution) + m1000.air_gap(20) + m1000.dispense(m1000.current_volume, m.top(-3)) + total_elution_vol += vol * 8 + m1000.drop_tip() if TIP_TRASH else m1000.return_tip() + helpers.set_hs_speed(protocol, h_s, heater_shaker_speed * 0.9, wash_time, True) + + # Transfer back to magnet + helpers.move_labware_from_hs_to_destination( + protocol, sample_plate, h_s, magblock + ) + + for elutei in np.arange(settling_time, 0, -0.5): + protocol.delay( + minutes=0.5, + msg="Incubating on MagDeck for " + str(elutei) + " more minutes.", + ) + + for i, (m, e) in enumerate(zip(samples_m, elution_samples_m)): + tipcheck(m1000) + m1000.flow_rate.dispense = 100 + m1000.flow_rate.aspirate = 25 + m1000.transfer( + vol, m.bottom(dot_bottom), e.bottom(5), air_gap=20, new_tip="never" + ) + m1000.blow_out(e.top(-2)) + m1000.air_gap(20) + m1000.drop_tip() if TIP_TRASH else m1000.return_tip() + + m1000.flow_rate.aspirate = 150 + + """ + Here is where you can call the methods defined above to fit your specific + protocol. The normal sequence is: + """ + lysis(lysis_vol, lysis_) + bind(binding_buffer_vol, bind2_vol) + wash(wash1_vol, all_washes) + wash(wash2_vol, all_washes) + wash(wash3_vol, all_washes) + h_s.set_and_wait_for_temperature(55) + for beaddry in np.arange(drybeads, 0, -0.5): + protocol.delay( + minutes=0.5, + msg="There are " + str(beaddry) + " minutes left in the drying step.", + ) + elute(elution_vol) + h_s.deactivate_heater() + helpers.clean_up_plates( + m1000, + [elutionplate, sample_plate, res1, res3, res2], + waste_reservoir["A1"], + 1000, + ) + helpers.find_liquid_height_of_all_wells(protocol, m1000, [waste_reservoir["A1"]]) + if deactivate_modules_bool: + helpers.deactivate_modules(protocol) diff --git a/abr-testing/abr_testing/protocols/active_protocols/11_Dynabeads_IP_Flex_96well_RIT.py b/abr-testing/abr_testing/protocols/active_protocols/11_Dynabeads_IP_Flex_96well_RIT.py new file mode 100644 index 00000000000..44db654cc1f --- /dev/null +++ b/abr-testing/abr_testing/protocols/active_protocols/11_Dynabeads_IP_Flex_96well_RIT.py @@ -0,0 +1,299 @@ +"""Immunoprecipitation by Dynabeads.""" +from opentrons.protocol_api import ProtocolContext, ParameterContext, Well, Labware +from opentrons.protocol_api.module_contexts import ( + HeaterShakerContext, + TemperatureModuleContext, + MagneticBlockContext, +) +from abr_testing.protocols import helpers +from typing import List, Dict, Union + +metadata = { + "protocolName": "Immunoprecipitation by Dynabeads - (Reagents in 15 mL tubes)", + "author": "Boren Lin, Opentrons", + "description": "Isolates protein from liquid samples using protein A /G coupled magnetic beads", +} + +requirements = { + "robotType": "Flex", + "apiLevel": "2.21", +} + + +def add_parameters(parameters: ParameterContext) -> None: + """Define parameters.""" + helpers.create_hs_speed_parameter(parameters) + helpers.create_two_pipette_mount_parameters(parameters) + helpers.create_dot_bottom_parameter(parameters) + helpers.create_deactivate_modules_parameter(parameters) + + +NUM_COL = 12 + +MAG_DELAY_MIN = 1 + +BEADS_VOL = 50 +AB_VOL = 50 +SAMPLE_VOL = 200 +WASH_TIMES = 6 +WASH_VOL = 200 +ELUTION_VOL = 50 + +WASTE_VOL_MAX = 275000 + +READY_FOR_SDSPAGE = 0 + +waste_vol_chk = 0.0 +waste_vol = 0.0 + +TIP_TRASH = False + + +def run(protocol: ProtocolContext) -> None: + """Protocol.""" + # defining variables inside def run + heater_shaker_speed = protocol.params.heater_shaker_speed # type: ignore[attr-defined] + ASP_HEIGHT = protocol.params.dot_bottom # type: ignore[attr-defined] + single_channel_mount = protocol.params.pipette_mount_1 # type: ignore[attr-defined] + eight_channel_mount = protocol.params.pipette_mount_2 # type: ignore[attr-defined] + deactivate_modules_bool = protocol.params.deactivate_modules # type: ignore[attr-defined] + helpers.comment_protocol_version(protocol, "01") + + MIX_SPEED = heater_shaker_speed + MIX_SEC = 10 + + # if on deck: + INCUBATION_SPEED = heater_shaker_speed * 0.5 + INCUBATION_MIN = 60 + # load labware + + sample_plate_1 = protocol.load_labware( + "nest_96_wellplate_2ml_deep", "B2", "sample plate 1" + ) + sample_plate_2 = protocol.load_labware( + "nest_96_wellplate_2ml_deep", "C4", "sample plate 2" + ) + + wash_res = protocol.load_labware("nest_12_reservoir_15ml", "B1", "wash") + reagent_res = protocol.load_labware( + "opentrons_15_tuberack_nest_15ml_conical", "C3", "reagents" + ) + waste_res = protocol.load_labware("nest_1_reservoir_290ml", "D2", "Liquid Waste") + + tips = protocol.load_labware("opentrons_flex_96_tiprack_1000ul", "B3") + tips_sample = protocol.load_labware( + "opentrons_flex_96_tiprack_1000ul", "A2", "sample tips" + ) + tips_sample_loc = tips_sample.wells()[:95] + if READY_FOR_SDSPAGE == 0: + tips_elu = protocol.load_labware( + "opentrons_flex_96_tiprack_1000ul", "A1", "elution tips" + ) + tips_elu_loc = tips_elu.wells()[:95] + tips_reused = protocol.load_labware( + "opentrons_flex_96_tiprack_1000ul", "C2", "reused tips" + ) + tips_reused_loc = tips_reused.wells()[:95] + p1000 = protocol.load_instrument( + "flex_8channel_1000", eight_channel_mount, tip_racks=[tips] + ) + p1000_single = protocol.load_instrument( + "flex_1channel_1000", single_channel_mount, tip_racks=[tips] + ) + h_s: HeaterShakerContext = protocol.load_module( + helpers.hs_str, "D1" + ) # type: ignore[assignment] + working_plate, h_s_adapter = helpers.load_hs_adapter_and_labware( + "nest_96_wellplate_2ml_deep", h_s, "Working Plate" + ) + + if READY_FOR_SDSPAGE == 0: + temp: TemperatureModuleContext = protocol.load_module( + helpers.temp_str, "D3" + ) # type: ignore[assignment] + final_plate, temp_adapter = helpers.load_temp_adapter_and_labware( + "nest_96_wellplate_2ml_deep", temp, "Final Plate" + ) + mag: MagneticBlockContext = protocol.load_module( + helpers.mag_str, "C1" + ) # type: ignore[assignment] + + # liquids + samples1 = sample_plate_1.rows()[0][:NUM_COL] # 1 + samples2 = sample_plate_2.rows()[0][:NUM_COL] # 1 + beads = reagent_res.wells()[0] # 2 + ab = reagent_res.wells()[1] # 3 + elu = reagent_res.wells()[2] # 4 + wash = wash_res.rows()[0][:NUM_COL] # 5 + waste = waste_res.wells()[0] + working_cols = working_plate.rows()[0][:NUM_COL] # 6 + working_wells = working_plate.wells()[: NUM_COL * 8] # 6 + if READY_FOR_SDSPAGE == 0: + final_cols = final_plate.rows()[0][:NUM_COL] + # Define Liquids + liquid_vols_and_wells: Dict[ + str, List[Dict[str, Union[Well, List[Well], float]]] + ] = { + "Beads": [{"well": beads, "volume": 9800.0}], + "AB": [{"well": ab, "volume": 9800.0}], + "Elution": [{"well": elu, "volume": 9800.0}], + "Wash": [{"well": wash, "volume": 1500.0}], + "Samples 1": [{"well": samples1, "volume": 250.0}], + "Samples 2": [{"well": samples2, "volume": 250.0}], + } + helpers.find_liquid_height_of_loaded_liquids( + protocol, liquid_vols_and_wells, p1000_single + ) + + def transfer_plate_to_plate( + vol1: float, start: List[Well], end: List[Well], liquid: int + ) -> None: + """Transfer from plate to plate.""" + for i in range(NUM_COL): + if liquid == 1: + p1000.pick_up_tip(tips_sample_loc[i * 8]) + else: + p1000.pick_up_tip(tips_elu_loc[i * 8]) + start_loc = start[i] + end_loc = end[i] + p1000.aspirate(vol1, start_loc.bottom(z=ASP_HEIGHT), rate=2) + p1000.air_gap(10) + p1000.dispense(vol1 + 10, end_loc.bottom(z=15), rate=2) + p1000.blow_out() + p1000.touch_tip() + p1000.return_tip() if not TIP_TRASH else p1000.drop_tip() + + def transfer_well_to_plate( + vol2: float, + start: Union[List[Well], Well], + end: List[Well], + liquid: int, + drop_height: int = -20, + ) -> None: + """Transfer from well to plate.""" + if liquid == 5 and type(start) == List: + p1000.pick_up_tip() + for j in range(NUM_COL): + start_loc = start[j] + p1000.require_liquid_presence(start_loc) + end_loc = end[j] + p1000.aspirate(vol2, start_loc.bottom(z=ASP_HEIGHT), rate=2) + p1000.air_gap(10) + p1000.dispense(vol2 + 10, end_loc.top(z=drop_height), rate=2) + p1000.blow_out() + p1000.return_tip() if not TIP_TRASH else p1000.drop_tip() + + elif type(start) == Well: + p1000_single.pick_up_tip() + vol = vol2 * 8 + p1000_single.mix(5, vol * 0.75, start.bottom(z=ASP_HEIGHT * 5), rate=2) + p1000_single.mix(5, vol * 0.75, start.bottom(z=ASP_HEIGHT * 20), rate=2) + for j in range(NUM_COL): + end_loc_gap = end[j * 8] + if liquid == 2: + p1000_single.mix( + 2, vol * 0.75, start.bottom(z=ASP_HEIGHT * 5), rate=2 + ) + p1000_single.require_liquid_presence(start) + p1000_single.aspirate(vol, start.bottom(z=ASP_HEIGHT * 5), rate=2) + p1000_single.air_gap(10) + p1000_single.dispense(10, end_loc_gap.top(z=-5)) + for jj in range(8): + end_loc = end[j * 8 + jj] + p1000_single.dispense(vol2, end_loc.bottom(z=10), rate=0.75) + p1000_single.touch_tip() + p1000_single.blow_out() + p1000_single.return_tip() if not TIP_TRASH else p1000.drop_tip() + + def discard(vol3: float, start: List[Well]) -> None: + """Discard function.""" + global waste_vol + global waste_vol_chk + waste_vol = 0.0 + for k in range(NUM_COL): + p1000.pick_up_tip(tips_reused_loc[k * 8]) + start_loc = start[k] + end_loc = waste + p1000.aspirate(vol3, start_loc.bottom(z=ASP_HEIGHT), rate=0.3) + p1000.air_gap(10) + p1000.dispense(vol3 + 10, end_loc.top(z=-5), rate=2) + p1000.blow_out() + p1000.return_tip() + waste_vol = vol3 * NUM_COL * 8.0 + waste_vol_chk = waste_vol_chk + waste_vol + + # protocol + def run(sample_plate: Labware) -> None: + """Protocol.""" + # Add beads, samples and antibody solution + samples = sample_plate.rows()[0][:NUM_COL] # 1 + h_s.close_labware_latch() + transfer_well_to_plate(BEADS_VOL, beads, working_wells, 2) + + helpers.move_labware_from_hs_to_destination(protocol, working_plate, h_s, mag) + + protocol.delay(minutes=MAG_DELAY_MIN) + discard(BEADS_VOL * 1.1, working_cols) + + helpers.move_labware_to_hs(protocol, working_plate, h_s, h_s_adapter) + + transfer_plate_to_plate(SAMPLE_VOL, samples, working_cols, 1) + transfer_well_to_plate(AB_VOL, ab, working_wells, 3) + + h_s.set_and_wait_for_shake_speed(rpm=MIX_SPEED) + protocol.delay(seconds=MIX_SEC) + + h_s.set_and_wait_for_shake_speed(rpm=INCUBATION_SPEED) + protocol.delay(seconds=INCUBATION_MIN * 60) + h_s.deactivate_shaker() + + helpers.move_labware_from_hs_to_destination(protocol, working_plate, h_s, mag) + + protocol.delay(minutes=MAG_DELAY_MIN) + vol_total = SAMPLE_VOL + AB_VOL + discard(vol_total * 1.1, working_cols) + + # Wash + for _ in range(WASH_TIMES): + helpers.move_labware_to_hs(protocol, working_plate, h_s, h_s_adapter) + + transfer_well_to_plate(WASH_VOL, wash, working_cols, 5) + helpers.set_hs_speed(protocol, h_s, MIX_SPEED, MIX_SEC / 60, True) + helpers.move_labware_from_hs_to_destination( + protocol, working_plate, h_s, mag + ) + protocol.delay(minutes=MAG_DELAY_MIN) + discard(WASH_VOL * 1.1, working_cols) + # Elution + helpers.move_labware_to_hs(protocol, working_plate, h_s, h_s_adapter) + transfer_well_to_plate(ELUTION_VOL, elu, working_wells, 4) + if READY_FOR_SDSPAGE == 1: + protocol.pause("Seal the Working Plate") + h_s.set_and_wait_for_temperature(70) + helpers.set_hs_speed(protocol, h_s, MIX_SPEED, (MIX_SEC / 60) + 10, True) + h_s.deactivate_heater() + h_s.open_labware_latch() + protocol.pause("Protocol Complete") + elif READY_FOR_SDSPAGE == 0: + helpers.set_hs_speed(protocol, h_s, MIX_SPEED, (MIX_SEC / 60) + 2, True) + + temp.set_temperature(4) + helpers.move_labware_from_hs_to_destination( + protocol, working_plate, h_s, mag + ) + protocol.delay(minutes=MAG_DELAY_MIN) + transfer_plate_to_plate(ELUTION_VOL * 1.1, working_cols, final_cols, 6) + temp.deactivate() + helpers.clean_up_plates(p1000_single, [sample_plate], waste, 1000) + helpers.move_labware_to_hs(protocol, working_plate, h_s, h_s_adapter) + + run(sample_plate_1) + # swap plates + protocol.move_labware(sample_plate_1, "B4", True) + protocol.move_labware(sample_plate_2, "B2", True) + run(sample_plate_2) + + helpers.clean_up_plates(p1000_single, [wash_res], waste, 1000) + helpers.find_liquid_height_of_all_wells(protocol, p1000_single, [waste_res["A1"]]) + if deactivate_modules_bool: + helpers.deactivate_modules(protocol) diff --git a/abr-testing/abr_testing/protocols/active_protocols/12_KAPA HyperPlus Library Prep.py b/abr-testing/abr_testing/protocols/active_protocols/12_KAPA HyperPlus Library Prep.py new file mode 100644 index 00000000000..ff38e8cf7c7 --- /dev/null +++ b/abr-testing/abr_testing/protocols/active_protocols/12_KAPA HyperPlus Library Prep.py @@ -0,0 +1,972 @@ +"""KAPA HyperPlus Library Preparation.""" +from opentrons.protocol_api import ( + ProtocolContext, + ParameterContext, + Labware, + Well, + InstrumentContext, +) +from opentrons import types +import math +from abr_testing.protocols import helpers +from opentrons.protocol_api.module_contexts import ( + TemperatureModuleContext, + MagneticBlockContext, + ThermocyclerContext, +) +from typing import List, Tuple, Dict + +metadata = { + "protocolName": "KAPA HyperPlus Library Preparation", + "author": "Tony Ngumah ", +} + +requirements = {"robotType": "Flex", "apiLevel": "2.21"} + + +def add_parameters(parameters: ParameterContext) -> None: + """Parameters.""" + parameters.add_bool( + variable_name="dry_run", + display_name="Dry Run", + description="Skip incubation delays and shorten mix steps.", + default=False, + ) + parameters.add_bool( + variable_name="trash_tips", + display_name="Trash tip", + description="tip trashes after every use", + default=False, + ) + helpers.create_disposable_lid_parameter(parameters) + helpers.create_tc_lid_deck_riser_parameter(parameters) + helpers.create_two_pipette_mount_parameters(parameters) + helpers.create_deactivate_modules_parameter(parameters) + parameters.add_int( + variable_name="num_samples", + display_name="number of samples", + description="How many samples to be perform for library prep", + default=48, + minimum=8, + maximum=48, + ) + parameters.add_int( + variable_name="PCR_CYCLES", + display_name="number of PCR Cycles", + description="How many pcr cycles to be perform for library prep", + default=2, + minimum=2, + maximum=16, + ) + + parameters.add_int( + variable_name="Fragmentation_time", + display_name="time on thermocycler", + description="Fragmentation time in thermocycler", + default=30, + minimum=10, + maximum=30, + ) + + +def run(protocol: ProtocolContext) -> None: + """Protocol.""" + USE_GRIPPER = True + deactivate_mods = protocol.params.deactivate_modules # type: ignore[attr-defined] + trash_tips = protocol.params.trash_tips # type: ignore[attr-defined] + dry_run = protocol.params.dry_run # type: ignore[attr-defined] + pipette_1000_mount = protocol.params.pipette_mount_1 # type: ignore[attr-defined] + pipette_50_mount = protocol.params.pipette_mount_2 # type: ignore[attr-defined] + deck_riser = protocol.params.deck_riser # type: ignore[attr-defined] + helpers.comment_protocol_version(protocol, "01") + + REUSE_ETOH_TIPS = True + REUSE_RSB_TIPS = ( + True # Reuse tips for RSB buffer (adding RSB, mixing, and transferring) + ) + REUSE_REMOVE_TIPS = True # Reuse tips for supernatant removal + num_samples = protocol.params.num_samples # type: ignore[attr-defined] + PCRCYCLES = protocol.params.PCR_CYCLES # type: ignore[attr-defined] + disposable_lid = protocol.params.disposable_lid # type: ignore[attr-defined] + Fragmentation_time = 10 + ligation_tc_time = 15 + used_lids: List[Labware] = [] + if dry_run: + trash_tips = False + + num_cols = math.ceil(num_samples / 8) + + # Pre-set parameters + # sample_vol = 35.0 + frag_vol = 15.0 + end_repair_vol = 10.0 + adapter_vol = 5.0 + ligation_vol = 45.0 + amplification_vol = 30.0 + bead_vol_1 = 88.0 + bead_vol_2 = 50.0 + # bead_vol = bead_vol_1 + bead_vol_2 + bead_inc = 2.0 + rsb_vol_1 = 25.0 + rsb_vol_2 = 20.0 + # rsb_vol = rsb_vol_1 + rsb_vol_2 + elution_vol = 20.0 + elution_vol_2 = 17.0 + # etoh_vol = 400.0 + + # Importing Labware, Modules and Instruments + magblock: MagneticBlockContext = protocol.load_module( + helpers.mag_str, "D2" + ) # type: ignore[assignment] + temp_mod: TemperatureModuleContext = protocol.load_module( + helpers.temp_str, "B3" + ) # type: ignore[assignment] + temp_plate, temp_adapter = helpers.load_temp_adapter_and_labware( + "armadillo_96_wellplate_200ul_pcr_full_skirt", + temp_mod, + "Temp Module Reservoir Plate", + ) + + if not dry_run: + temp_mod.set_temperature(4) + tc_mod: ThermocyclerContext = protocol.load_module(helpers.tc_str) # type: ignore[assignment] + # Just in case + tc_mod.open_lid() + + FLP_plate = magblock.load_labware( + "armadillo_96_wellplate_200ul_pcr_full_skirt", "FLP Plate" + ) + samples_flp = FLP_plate.rows()[0][:num_cols] + + sample_plate = protocol.load_labware( + "armadillo_96_wellplate_200ul_pcr_full_skirt", "D1", "Sample Plate 1" + ) + + sample_plate_2 = protocol.load_labware( + "armadillo_96_wellplate_200ul_pcr_full_skirt", "B2", "Sample Plate 2" + ) + samples_2 = sample_plate_2.rows()[0][:num_cols] + samples = sample_plate.rows()[0][:num_cols] + reservoir = protocol.load_labware( + "nest_96_wellplate_2ml_deep", "C2", "Beads + Buffer + Ethanol" + ) + # Load tipracks + tiprack_50_1 = protocol.load_labware("opentrons_flex_96_tiprack_50ul", "A3") + tiprack_50_2 = protocol.load_labware("opentrons_flex_96_tiprack_50ul", "A2") + + tiprack_200_1 = protocol.load_labware("opentrons_flex_96_tiprack_200ul", "C1") + tiprack_200_2 = protocol.load_labware("opentrons_flex_96_tiprack_200ul", "C3") + + if trash_tips: + protocol.load_waste_chute() + + unused_lids: List[Labware] = [] + # Load TC Lids + if disposable_lid: + unused_lids = helpers.load_disposable_lids(protocol, 5, ["C4"], deck_riser) + # Import Global Variables + + global tip50 + global tip200 + global p50_rack_count + global p200_rack_count + tip_count = {1000: 0, 50: 0} + + p200 = protocol.load_instrument( + "flex_8channel_1000", + pipette_1000_mount, + tip_racks=[tiprack_200_1, tiprack_200_2], + ) + p50 = protocol.load_instrument( + "flex_8channel_50", pipette_50_mount, tip_racks=[tiprack_50_1, tiprack_50_2] + ) + + # Load Reagent Locations in Reservoirs + lib_amplification_wells: List[Well] = temp_plate.columns()[num_cols + 3] + amplification_res = lib_amplification_wells[0] + adapters = temp_plate.rows()[0][:num_cols] # used for filling liquids + end_repair_cols: List[Well] = temp_plate.columns()[ + num_cols + ] # used for filling liquids + er_res = end_repair_cols[0] + frag: List[Well] = temp_plate.columns()[num_cols + 1] + frag_res = frag[0] + ligation: List[Well] = temp_plate.columns()[num_cols + 2] + ligation_res = ligation[0] + # Room Temp Res (deepwell) + bead = reservoir.columns()[0] + bead_res = bead[0] + rsb = reservoir.columns()[3] + rsb_res = rsb[0] + etoh1 = reservoir.columns()[4] + etoh1_res = etoh1[0] + etoh2 = reservoir.columns()[5] + etoh2_res = etoh2[0] + + liquid_vols_and_wells: Dict[str, List[Dict[str, Well | List[Well] | float]]] = { + "Samples": [{"well": sample_plate.wells()[: 8 * num_cols], "volume": 35.0}], + "Final Library": [ + {"well": sample_plate_2.wells()[: 8 * num_cols], "volume": 17.0} + ], + "Adapters": [{"well": adapters, "volume": 10.0}], + "End Repair Mix": [ + {"well": temp_plate.wells()[: 8 * num_cols], "volume": 61.0} + ], + "Fragmentation Mix": [{"well": frag, "volume": 91.5}], + "Ligation Mix": [{"well": ligation, "volume": 200.0}], + "Amplification Mix": [{"well": lib_amplification_wells, "volume": 183.0}], + "Ampure Beads": [{"well": bead, "volume": 910.8}], + "Resuspension Buffer": [{"well": rsb, "volume": 297.0}], + "Ethanol 80%": [ + {"well": etoh1, "volume": 2000.0}, + {"well": etoh2, "volume": 2000.0}, + ], + } + waste1 = reservoir.columns()[6] + waste1_res = waste1[0] + + waste2 = reservoir.columns()[7] + waste2_res = waste2[0] + + helpers.find_liquid_height_of_loaded_liquids(protocol, liquid_vols_and_wells, p50) + + def tip_track(pipette: InstrumentContext, tip_count: Dict) -> None: + """Track tip usage.""" + # Get the current tip count for the pipette + current_tips = tip_count[pipette.max_volume] + + # Check if tip count exceeds the maximum tips per rack + if current_tips >= (96 * 2): + tip_count[pipette.max_volume] = 0 + pipette.reset_tipracks() + + # Pick up a new tip and update the count + if not pipette._has_tip: + pipette.pick_up_tip() + tip_count[ + pipette.max_volume + ] += 8 # Adjust increment based on multi-channel pipette + + def run_tag_profile( + unused_lids: List[Labware], used_lids: List[Labware] + ) -> Tuple[List[Labware], List[Labware]]: + """Run Tag Profile.""" + # Presetting Thermocycler Temps + protocol.comment( + "****Starting Fragmentation Profile (37C for 10 minutes with 100C lid)****" + ) + tc_mod.set_lid_temperature(100) + tc_mod.set_block_temperature(37) + + # Move Plate to TC + protocol.comment("****Moving Plate to Pre-Warmed TC Module Block****") + protocol.move_labware(sample_plate, tc_mod, use_gripper=USE_GRIPPER) + + if disposable_lid: + lid_on_plate, unused_lids, used_lids = helpers.use_disposable_lid_with_tc( + protocol, unused_lids, used_lids, sample_plate, tc_mod + ) + else: + tc_mod.close_lid() + tc_mod.set_block_temperature( + temperature=37, hold_time_minutes=Fragmentation_time, block_max_volume=50 + ) + tc_mod.open_lid() + + if disposable_lid: + if len(used_lids) <= 1: + protocol.move_labware(lid_on_plate, "D4", use_gripper=True) + else: + protocol.move_labware(lid_on_plate, used_lids[-2], use_gripper=True) + # #Move Plate to H-S + protocol.comment("****Moving Plate off of TC****") + + protocol.move_labware(sample_plate, "D1", use_gripper=USE_GRIPPER) + return unused_lids, used_lids + + def run_er_profile( + unused_lids: List[Labware], used_lids: List[Labware] + ) -> Tuple[List[Labware], List[Labware]]: + """End Repair Profile.""" + # Presetting Thermocycler Temps + protocol.comment( + "****Starting End Repair Profile (65C for 30 minutes with 100C lid)****" + ) + tc_mod.set_lid_temperature(100) + tc_mod.set_block_temperature(65) + + # Move Plate to TC + protocol.comment("****Moving Plate to Pre-Warmed TC Module Block****") + protocol.move_labware(sample_plate, tc_mod, use_gripper=USE_GRIPPER) + + if disposable_lid: + lid_on_plate, unused_lids, used_lids = helpers.use_disposable_lid_with_tc( + protocol, unused_lids, used_lids, sample_plate, tc_mod + ) + else: + tc_mod.close_lid() + tc_mod.set_block_temperature( + temperature=65, hold_time_minutes=30, block_max_volume=50 + ) + + tc_mod.deactivate_block() + tc_mod.open_lid() + + if disposable_lid: + # move lid + if len(used_lids) <= 1: + protocol.move_labware(lid_on_plate, "C4", use_gripper=True) + else: + protocol.move_labware(lid_on_plate, used_lids[-2], use_gripper=True) + # #Move Plate to H-S + protocol.comment("****Moving Plate off of TC****") + + protocol.move_labware(sample_plate, "D1", use_gripper=USE_GRIPPER) + return unused_lids, used_lids + + def run_ligation_profile( + unused_lids: List[Labware], used_lids: List[Labware] + ) -> Tuple[List[Labware], List[Labware]]: + """Run Ligation Profile.""" + # Presetting Thermocycler Temps + protocol.comment( + "****Starting Ligation Profile (20C for 15 minutes with 100C lid)****" + ) + tc_mod.set_lid_temperature(100) + tc_mod.set_block_temperature(20) + + # Move Plate to TC + protocol.comment("****Moving Plate to Pre-Warmed TC Module Block****") + + protocol.move_labware(sample_plate, tc_mod, use_gripper=USE_GRIPPER) + + if disposable_lid: + lid_on_plate, unused_lids, used_lids = helpers.use_disposable_lid_with_tc( + protocol, unused_lids, used_lids, sample_plate, tc_mod + ) + else: + tc_mod.close_lid() + tc_mod.set_block_temperature( + temperature=20, hold_time_minutes=ligation_tc_time, block_max_volume=50 + ) + + tc_mod.deactivate_block() + + tc_mod.open_lid() + # Move lid + tc_mod.open_lid() + if disposable_lid: + if len(used_lids) <= 1: + protocol.move_labware(lid_on_plate, "C4", use_gripper=True) + else: + protocol.move_labware(lid_on_plate, used_lids[-2], use_gripper=True) + + # #Move Plate to H-S + protocol.comment("****Moving Plate off of TC****") + + protocol.move_labware(sample_plate, "D1", use_gripper=USE_GRIPPER) + return unused_lids, used_lids + + def run_amplification_profile( + unused_lids: List[Labware], used_lids: List[Labware] + ) -> Tuple[List[Labware], List[Labware]]: + """Run Amplification Profile.""" + # Presetting Thermocycler Temps + protocol.comment( + "Amplification Profile (37C for 5 min, 50C for 5 min with 100C lid)" + ) + tc_mod.set_lid_temperature(100) + tc_mod.set_block_temperature(98) + + # Move Plate to TC + protocol.comment("****Moving Sample Plate onto TC****") + protocol.move_labware(sample_plate_2, tc_mod, use_gripper=USE_GRIPPER) + + if not dry_run: + tc_mod.set_lid_temperature(105) + if disposable_lid: + lid_on_plate, unused_lids, used_lids = helpers.use_disposable_lid_with_tc( + protocol, unused_lids, used_lids, sample_plate_2, tc_mod + ) + else: + tc_mod.close_lid() + if not dry_run: + helpers.perform_pcr( + protocol, + tc_mod, + initial_denature_time_sec=45, + denaturation_time_sec=15, + anneal_time_sec=30, + extension_time_sec=30, + cycle_repetitions=PCRCYCLES, + final_extension_time_min=1, + ) + tc_mod.set_block_temperature(4) + tc_mod.open_lid() + if disposable_lid: + if len(used_lids) <= 1: + protocol.move_labware(lid_on_plate, "C4", use_gripper=True) + else: + protocol.move_labware(lid_on_plate, used_lids[-2], use_gripper=True) + + # Move Sample Plate to H-S + protocol.comment("****Moving Sample Plate back to H-S****") + protocol.move_labware(sample_plate_2, "D1", use_gripper=USE_GRIPPER) + # get FLP plate out of the way + protocol.comment("****Moving FLP Plate back to TC****") + protocol.move_labware(FLP_plate, tc_mod, use_gripper=USE_GRIPPER) + return unused_lids, used_lids + + def mix_beads( + pip: InstrumentContext, res: Well, vol: float, reps: int, col: int + ) -> None: + """Mix beads function.""" + # Multiplier tells + mix_vol = (num_cols - col) * vol + if pip == p50: + if mix_vol > 50: + mix_vol = 50 + if pip == p200: + if mix_vol > 200: + mix_vol = 200 + + if res == bead_res: + width = res.width + else: + width = res.diameter + if width: + move = (width / 2) - 1 + + loc_center_a = res.bottom().move(types.Point(x=0, y=0, z=0.5)) + loc_center_d = res.bottom().move(types.Point(x=0, y=0, z=0.5)) + loc1 = res.bottom().move(types.Point(x=move, y=0, z=5)) + loc2 = res.bottom().move(types.Point(x=0, y=move, z=5)) + loc3 = res.bottom().move(types.Point(x=-move, y=0, z=5)) + loc4 = res.bottom().move(types.Point(x=0, y=-move, z=5)) + loc5 = res.bottom().move(types.Point(x=move / 2, y=move / 2, z=5)) + loc6 = res.bottom().move(types.Point(x=-move / 2, y=move / 2, z=5)) + loc7 = res.bottom().move(types.Point(x=-move / 2, y=-move / 2, z=5)) + loc8 = res.bottom().move(types.Point(x=move / 2, y=-move / 2, z=5)) + + loc = [loc_center_d, loc1, loc5, loc2, loc6, loc3, loc7, loc4, loc8] + if not pip._has_tip: + tip_track(pip, tip_count) + pip.aspirate( + mix_vol, res.bottom().move(types.Point(x=0, y=0, z=10)) + ) # Blow bubbles to start + pip.dispense(mix_vol, loc_center_d) + for x in range(reps): + pip.aspirate(mix_vol, loc_center_a) + pip.dispense(mix_vol, loc[x]) + pip.flow_rate.aspirate = 10 + pip.flow_rate.dispense = 10 + pip.aspirate(mix_vol, loc_center_a) + pip.dispense(mix_vol, loc_center_d) + pip.flow_rate.aspirate = 150 + pip.flow_rate.dispense = 150 + + def remove_supernatant(well: Well, vol: float, waste_: Well, column: int) -> None: + """Remove supernatant.""" + protocol.comment("-------Removing " + str(vol) + "ul of Supernatant-------") + p200.flow_rate.aspirate = 15 + num_trans = math.ceil(vol / 190) + vol_per_trans = vol / num_trans + tip_track(p200, tip_count) + for x in range(num_trans): + p200.aspirate(vol_per_trans / 2, well.bottom(0.2)) + protocol.delay(seconds=1) + p200.aspirate(vol_per_trans / 2, well.bottom(0.2)) + p200.air_gap(10) + p200.dispense(p200.current_volume, waste_) + p200.air_gap(10) + if REUSE_REMOVE_TIPS: + p200.return_tip() + protocol.comment("****Dropping Tip Back in Tip Box****") + else: + if trash_tips: + p200.drop_tip() + protocol.comment("****Dropping Tip in Waste shoot****") + else: + p200.return_tip() + protocol.comment("****Dropping Tip Back in Tip Box****") + p200.flow_rate.aspirate = 150 + + def Fragmentation( + unused_lids: List[Labware], used_lids: List[Labware] + ) -> Tuple[List[Labware], List[Labware]]: + """Fragmentation Function.""" + protocol.comment("-------Starting Fragmentation-------") + + for i in range(num_cols): + + protocol.comment("Mixing and Transfering beads to column " + str(i + 1)) + + p50.flow_rate.dispense = 15 + tip_track(p50, tip_count) + p50.aspirate(frag_vol, frag_res) + p50.dispense(p50.current_volume, samples[i]) + p50.flow_rate.dispense = 150 + for x in range(10 if not dry_run else 1): + if x == 9: + p50.flow_rate.aspirate = 15 + p50.flow_rate.dispense = 15 + p50.aspirate(frag_vol, samples[i].bottom(1)) + p50.dispense(p50.current_volume, samples[i].bottom(5)) + p50.flow_rate.aspirate = 150 + p50.flow_rate.dispense = 150 + if trash_tips: + p50.drop_tip() + protocol.comment("****Dropping Tip in Waste shoot****") + else: + p50.return_tip() + protocol.comment("****Dropping Tip Back in Tip Box****") + + unused_lids, used_lids = run_tag_profile( + unused_lids, used_lids + ) # Heats TC --> moves plate to TC --> TAG Profile --> removes plate from TC + return unused_lids, used_lids + + def end_repair( + unused_lids: List[Labware], used_lids: List[Labware] + ) -> Tuple[List[Labware], List[Labware]]: + """End Repair Function.""" + protocol.comment("-------Starting end_repair-------") + + for i in range(num_cols): + + protocol.comment( + "**** Mixing and Transfering beads to column " + str(i + 1) + " ****" + ) + + p50.flow_rate.dispense = 15 + tip_track(p50, tip_count) + p50.aspirate(end_repair_vol, er_res) + p50.dispense(p50.current_volume, samples[i]) + p50.flow_rate.dispense = 150 + for x in range(10 if not dry_run else 1): + if x == 9: + p50.flow_rate.aspirate = 15 + p50.flow_rate.dispense = 15 + p50.aspirate(end_repair_vol, samples[i].bottom(1)) + p50.dispense(p50.current_volume, samples[i].bottom(5)) + p50.flow_rate.aspirate = 150 + p50.flow_rate.dispense = 150 + if trash_tips: + p50.drop_tip() + protocol.comment("****Dropping Tip in Waste shoot****") + else: + p50.return_tip() + protocol.comment("****Dropping Tip Back in Tip Box****") + + unused_lids, used_lids = run_er_profile( + unused_lids, used_lids + ) # Heats TC --> moves plate to TC --> TAG Profile --> removes plate from TC + return unused_lids, used_lids + + # Index Ligation + + def index_ligation( + unused_lids: List[Labware], used_lids: List[Labware] + ) -> Tuple[List[Labware], List[Labware]]: + """Index Ligation.""" + protocol.comment("-------Ligating Indexes-------") + protocol.comment("-------Adding and Mixing ELM-------") + for i in samples: + tip_track(p50, tip_count) + p50.aspirate(ligation_vol, ligation_res) + p50.dispense(p50.current_volume, i) + for x in range(10 if not dry_run else 1): + if x == 9: + p50.flow_rate.aspirate = 75 + p50.flow_rate.dispense = 75 + p50.aspirate(ligation_vol - 10, i) + p50.dispense(p50.current_volume, i.bottom(8)) + p50.flow_rate.aspirate = 150 + p50.flow_rate.dispense = 150 + if trash_tips: + p50.drop_tip() + protocol.comment("****Dropping Tip in Waste shoot****") + else: + p50.return_tip() + protocol.comment("****Dropping Tip Back in Tip Box****") + + # Add and mix adapters + protocol.comment("-------Adding and Mixing Adapters-------") + for i_well, x_well in zip(samples, adapters): + tip_track(p50, tip_count) + p50.aspirate(adapter_vol, x_well) + p50.dispense(p50.current_volume, i_well) + for y in range(10 if not dry_run else 1): + if y == 9: + p50.flow_rate.aspirate = 75 + p50.flow_rate.dispense = 75 + p50.aspirate(40, i_well) + p50.dispense(40, i_well.bottom(8)) + if trash_tips: + p50.drop_tip() + protocol.comment("****Dropping Tip in Waste shoot****") + else: + p50.return_tip() + protocol.comment("****Dropping Tip Back in Tip Box****") + + p50.flow_rate.aspirate = 150 + p50.flow_rate.dispense = 150 + + unused_lids, used_lids = run_ligation_profile(unused_lids, used_lids) + return unused_lids, used_lids + + def lib_cleanup() -> None: + """Litigation Clean up.""" + protocol.comment("-------Starting Cleanup-------") + protocol.comment("-------Adding and Mixing Cleanup Beads-------") + + # Move FLP plate off magnetic module if it's there + if FLP_plate.parent == magblock: + protocol.comment("****Moving FLP Plate off Magnetic Module****") + protocol.move_labware(FLP_plate, tc_mod, use_gripper=USE_GRIPPER) + + for x, i in enumerate(samples): + mix_beads(p200, bead_res, bead_vol_1, 7 if x == 0 else 2, x) + p200.aspirate(bead_vol_1, bead_res) + p200.dispense(bead_vol_1, i) + mix_beads(p200, i, bead_vol_1, 7 if not dry_run else 1, num_cols - 1) + for x in range(10 if not dry_run else 1): + if x == 9: + p200.flow_rate.aspirate = 75 + p200.flow_rate.dispense = 75 + p200.aspirate(bead_vol_1, i) + p200.dispense(bead_vol_1, i.bottom(8)) + p200.flow_rate.aspirate = 150 + p200.flow_rate.dispense = 150 + if trash_tips: + p200.drop_tip() + protocol.comment("****Dropping Tip in Waste shoot****") + else: + p200.return_tip() + protocol.comment("****Dropping Tip Back in Tip Box****") + + protocol.delay( + minutes=bead_inc, + msg="Please wait " + + str(bead_inc) + + " minutes while samples incubate at RT.", + ) + + protocol.comment("****Moving Labware to Magnet for Pelleting****") + protocol.move_labware(sample_plate, magblock, use_gripper=USE_GRIPPER) + + protocol.delay(minutes=4.5, msg="Time for Pelleting") + + for col, i in enumerate(samples): + remove_supernatant(i, 130, waste1_res, col) + samp_list = samples + + # Wash 2 x with 80% Ethanol + p200.flow_rate.aspirate = 75 + p200.flow_rate.dispense = 75 + for y in range(2 if not dry_run else 1): + protocol.comment(f"-------Wash # {y+1} with Ethanol-------") + if y == 0: # First wash + this_res = etoh1_res + this_waste_res = waste1_res + else: # Second Wash + this_res = etoh2_res + this_waste_res = waste2_res + tip_track(p200, tip_count) + p200.aspirate(150, this_res) + p200.air_gap(10) + p200.dispense(p200.current_volume, i.top()) + protocol.delay(seconds=1) + p200.air_gap(10) + if not REUSE_ETOH_TIPS: + p200.drop_tip() if trash_tips else p200.return_tip() + + protocol.delay(seconds=10) + # Remove the ethanol wash + for x, i in enumerate(samp_list): + tip_track(p200, tip_count) + p200.aspirate(155, i) + p200.air_gap(10) + p200.dispense(p200.current_volume, this_waste_res) + protocol.delay(seconds=1) + p200.air_gap(10) + if trash_tips: + p200.drop_tip() + protocol.comment("****Dropping Tip in Waste shoot****") + else: + p200.return_tip() + protocol.comment("****Dropping Tip Back in Tip Box****") + + p200.flow_rate.aspirate = 150 + p200.flow_rate.dispense = 150 + + # Wash complete, move on to drying steps. + protocol.delay(minutes=2, msg="Allow 3 minutes for residual ethanol to dry") + + # Return Plate to H-S from Magnet + + protocol.comment("****Moving sample plate off of Magnet****") + protocol.move_labware(sample_plate, "D1", use_gripper=USE_GRIPPER) + + # Adding RSB and Mixing + + for col, i in enumerate(samp_list): + protocol.comment(f"****Adding RSB to Columns: {col+1}****") + tip_track(p50, tip_count) + p50.aspirate(rsb_vol_1, rsb_res) + p50.air_gap(5) + p50.dispense(p50.current_volume, i) + for x in range(10 if not dry_run else 1): + if x == 9: + p50.flow_rate.aspirate = 15 + p50.flow_rate.dispense = 15 + p50.aspirate(15, i.bottom(1)) + p50.dispense(15, i.bottom(4)) + p50.flow_rate.aspirate = 100 + p50.flow_rate.dispense = 100 + p50.air_gap(5) + if REUSE_RSB_TIPS: + p50.return_tip() + protocol.comment("****Dropping Tip Back in Tip Box****") + else: + if trash_tips: + p50.drop_tip() + protocol.comment("****Dropping Tip in Waste shoot****") + else: + p50.return_tip() + protocol.comment("****Dropping Tip Back in Tip Box****") + + protocol.delay( + minutes=3, msg="Allow 3 minutes for incubation and liquid aggregation." + ) + + protocol.comment("****Move Samples to Magnet for Pelleting****") + protocol.move_labware(sample_plate, magblock, use_gripper=USE_GRIPPER) + + protocol.delay(minutes=2, msg="Please allow 2 minutes for beads to pellet.") + + p200.flow_rate.aspirate = 10 + for i_int, (s, e) in enumerate(zip(samp_list, samples_2)): + tip_track(p50, tip_count) + p50.aspirate(elution_vol, s) + p50.air_gap(5) + p50.dispense(p50.current_volume, e.bottom(1), push_out=3) + p50.air_gap(5) + if trash_tips: + p50.drop_tip() + protocol.comment("****Dropping Tip in Waste shoot****") + else: + p50.return_tip() + protocol.comment("****Dropping Tip Back in Tip Box****") + + # move new sample plate to D1 or heatershaker + protocol.comment("****Moving sample plate off of Magnet****") + protocol.move_labware(sample_plate_2, "D1", use_gripper=USE_GRIPPER) + + # Keep Sample PLate 1 to B2 + protocol.comment("****Moving Sample_plate_1 Plate off magnet to B2****") + protocol.move_labware(sample_plate, "B2", use_gripper=USE_GRIPPER) + + protocol.comment("****Moving FLP Plate off TC****") + protocol.move_labware(FLP_plate, magblock, use_gripper=USE_GRIPPER) + + def lib_amplification( + unused_lids: List[Labware], used_lids: List[Labware] + ) -> Tuple[List[Labware], List[Labware]]: + """Library Amplification.""" + protocol.comment("-------Starting lib_amplification-------") + + for i in range(num_cols): + + protocol.comment( + "**** Mixing and Transfering beads to column " + str(i + 1) + " ****" + ) + mix_beads( + p50, amplification_res, amplification_vol, 7 if i == 0 else 2, i + ) # 5 reps for first mix in reservoir + p50.flow_rate.dispense = 15 + tip_track(p50, tip_count) + p50.aspirate(amplification_vol, amplification_res) + p50.dispense(p50.current_volume, samples_2[i]) + p50.flow_rate.dispense = 150 + for x in range(10 if not dry_run else 1): + if x == 9: + p50.flow_rate.aspirate = 15 + p50.flow_rate.dispense = 15 + p50.aspirate(amplification_vol, samples_2[i].bottom(1)) + p50.dispense(p50.current_volume, samples_2[i].bottom(5)) + p50.flow_rate.aspirate = 150 + p50.flow_rate.dispense = 150 + if trash_tips: + p50.drop_tip() + protocol.comment("****Dropping Tip in Waste shoot****") + else: + p50.return_tip() + protocol.comment("****Dropping Tip Back in Tip Box****") + + unused_lids, used_lids = run_amplification_profile( + unused_lids, used_lids + ) # moves plate to TC --> TAG Profile --> removes plate from TC + return unused_lids, used_lids + + def lib_cleanup_2() -> None: + """Final Library Clean up.""" + protocol.comment("-------Starting Cleanup-------") + protocol.comment("-------Adding and Mixing Cleanup Beads-------") + for x, i in enumerate(samples_2): + mix_beads(p200, bead_res, bead_vol_2, 7 if x == 0 else 2, x) + tip_track(p200, tip_count) + p200.aspirate(bead_vol_2, bead_res) + p200.dispense(bead_vol_2, i) + p200.return_tip() + mix_beads(p200, i, bead_vol_2, 7 if not dry_run else 1, num_cols - 1) + for x in range(10 if not dry_run else 1): + if x == 9: + p200.flow_rate.aspirate = 75 + p200.flow_rate.dispense = 75 + p200.aspirate(bead_vol_2, i) + p200.dispense(bead_vol_2, i.bottom(8)) + p200.flow_rate.aspirate = 150 + p200.flow_rate.dispense = 150 + if trash_tips: + p200.drop_tip() + protocol.comment("****Dropping Tip in Waste shoot****") + else: + p200.return_tip() + protocol.comment("****Dropping Tip Back in Tip Box****") + + protocol.delay( + minutes=bead_inc, + msg="Please wait " + + str(bead_inc) + + " minutes while samples incubate at RT.", + ) + + protocol.comment("****Moving Labware to Magnet for Pelleting****") + protocol.move_labware(sample_plate_2, magblock, use_gripper=USE_GRIPPER) + + protocol.delay(minutes=4.5, msg="Time for Pelleting") + + for col, i in enumerate(samples_2): + remove_supernatant(i, 130, waste1_res, col) + samp_list_2 = samples_2 + # Wash 2 x with 80% Ethanol + + p200.flow_rate.aspirate = 75 + p200.flow_rate.dispense = 75 + for y in range(2 if not dry_run else 1): + protocol.comment(f"-------Wash # {y+1} with Ethanol-------") + if y == 0: # First wash + this_res = etoh1_res + this_waste_res = waste1_res + else: # Second Wash + this_res = etoh2_res + this_waste_res = waste2_res + tip_track(p200, tip_count) + p200.aspirate(150, this_res) + p200.air_gap(10) + p200.dispense(p200.current_volume, i.top()) + protocol.delay(seconds=1) + p200.air_gap(10) + if not REUSE_ETOH_TIPS: + p200.drop_tip() if trash_tips else p200.return_tip() + + protocol.delay(seconds=10) + # Remove the ethanol wash + for x, i in enumerate(samp_list_2): + tip_track(p200, tip_count) + p200.aspirate(155, i) + p200.air_gap(10) + p200.dispense(p200.current_volume, this_waste_res) + protocol.delay(seconds=1) + p200.air_gap(10) + if trash_tips: + p200.drop_tip() + protocol.comment("****Dropping Tip in Waste shoot****") + else: + p200.return_tip() + protocol.comment("****Dropping Tip Back in Tip Box****") + + p200.flow_rate.aspirate = 150 + p200.flow_rate.dispense = 150 + + # Washes Complete, Move on to Drying Steps + + protocol.delay(minutes=3, msg="Allow 3 minutes for residual ethanol to dry") + + protocol.comment("****Moving sample plate off of Magnet****") + protocol.move_labware(sample_plate_2, "D1", use_gripper=USE_GRIPPER) + + # Adding RSB and Mixing + + for col, i in enumerate(samp_list_2): + protocol.comment(f"****Adding RSB to Columns: {col+1}****") + tip_track(p50, tip_count) + p50.aspirate(rsb_vol_2, rsb_res) + p50.air_gap(5) + p50.dispense(p50.current_volume, i) + for x in range(10 if not dry_run else 1): + if x == 9: + p50.flow_rate.aspirate = 15 + p50.flow_rate.dispense = 15 + p50.aspirate(15, i.bottom(1)) + p50.dispense(15, i.bottom(4)) + p50.flow_rate.aspirate = 100 + p50.flow_rate.dispense = 100 + p50.air_gap(5) + if REUSE_RSB_TIPS: + p50.return_tip() + protocol.comment("****Dropping Tip Back in Tip Box****") + else: + if trash_tips: + p50.drop_tip() + protocol.comment("****Dropping Tip in Waste shoot****") + else: + p50.return_tip() + protocol.comment("****Dropping Tip Back in Tip Box****") + + protocol.delay( + minutes=3, msg="Allow 3 minutes for incubation and liquid aggregation." + ) + + protocol.comment("****Move Samples to Magnet for Pelleting****") + protocol.move_labware(sample_plate_2, magblock, use_gripper=USE_GRIPPER) + + protocol.delay(minutes=2, msg="Please allow 2 minutes for beads to pellet.") + + p200.flow_rate.aspirate = 10 + for i_int, (s, e) in enumerate(zip(samp_list_2, samples_flp)): + tip_track(p50, tip_count) + p50.aspirate(elution_vol_2, s) + p50.air_gap(5) + p50.dispense(p50.current_volume, e.bottom(1), push_out=3) + p50.air_gap(5) + if trash_tips: + p50.drop_tip() + protocol.comment("****Dropping Tip in Waste shoot****") + else: + p50.return_tip() + protocol.comment("****Dropping Tip Back in Tip Box****") + + # Set Block Temp for Final Plate + tc_mod.set_block_temperature(4) + + unused_lids, used_lids = Fragmentation(unused_lids, used_lids) + unused_lids, used_lids = end_repair(unused_lids, used_lids) + unused_lids, used_lids = index_ligation(unused_lids, used_lids) + lib_cleanup() + unused_lids, used_lids = lib_amplification(unused_lids, used_lids) + lib_cleanup_2() + + # Probe liquid waste + reservoir.label = "Liquid Waste" # type: ignore[attr-defined] + waste1 = reservoir.columns()[6] + waste1_res = waste1[0] + + waste2 = reservoir.columns()[7] + waste2_res = waste2[0] + + end_probed_wells = [waste1_res, waste2_res] + helpers.find_liquid_height_of_all_wells(protocol, p50, end_probed_wells) + if deactivate_mods: + helpers.deactivate_modules(protocol) diff --git a/abr-testing/abr_testing/protocols/active_protocols/1_Simple Normalize Long Right.py b/abr-testing/abr_testing/protocols/active_protocols/1_Simple Normalize Long Right.py new file mode 100644 index 00000000000..6162e6ab34e --- /dev/null +++ b/abr-testing/abr_testing/protocols/active_protocols/1_Simple Normalize Long Right.py @@ -0,0 +1,342 @@ +"""Simple Normalize Long with LPD and Single Tip.""" +from opentrons.protocol_api import ( + ProtocolContext, + ParameterContext, + Labware, + SINGLE, + ALL, + InstrumentContext, + Well, +) +from abr_testing.protocols import helpers +from typing import List, Dict + +metadata = { + "protocolName": "Simple Normalize Long with LPD and Single Tip", + "author": "Opentrons ", + "source": "Protocol Library", +} + +requirements = {"robotType": "Flex", "apiLevel": "2.21"} + + +def add_parameters(parameters: ParameterContext) -> None: + """Parameters.""" + helpers.create_single_pipette_mount_parameter(parameters) + helpers.create_csv_parameter(parameters) + helpers.create_dot_bottom_parameter(parameters) + + +def get_next_tip_by_row(tip_rack: Labware, pipette: InstrumentContext) -> Well | None: + """Get next tip by row. + + This function returns the well name of the next tip to pick up for a given + tiprack with row-bias. Returns None if the pipette is out of tips + """ + if tip_rack.is_tiprack: + if pipette.channels == 8: + for passes in range( + 0, int(len(tip_rack.columns()[0]) / pipette.active_channels) + ): + for column in tip_rack.columns(): + # When the pipette's starting channels is H1, consume tips starting at top row. + if pipette._core.get_nozzle_map().starting_nozzle == "H1": + active_column = column + else: + # We reverse our tiprack reference to consume tips starting at bottom. + active_column = column[::-1] + + if len(active_column) >= ( + ((pipette.active_channels * passes) + pipette.active_channels) + ) and all( + well.has_tip is True + for well in active_column[ + (pipette.active_channels * passes) : ( + ( + (pipette.active_channels * passes) + + pipette.active_channels + ) + ) + ] + ): + return active_column[ + ( + (pipette.active_channels * passes) + + (pipette.active_channels - 1) + ) + ] + # No valid tips were found for current pipette configuration in provided tip rack. + return None + else: + raise ValueError( + "Parameter 'pipette' of get_next_tip_by_row must be an 8 Channel Pipette." + ) + else: + raise ValueError( + "Parameter 'tip_rack' of get_next_tip_by_row must be a recognized Tip Rack labware." + ) + + +def run(protocol: ProtocolContext) -> None: + """Protocol.""" + dot_bottom = protocol.params.dot_bottom # type: ignore[attr-defined] + mount_pos = protocol.params.pipette_mount # type: ignore[attr-defined] + all_data = protocol.params.parameters_csv.parse_as_csv() # type: ignore[attr-defined] + data = all_data[1:] + helpers.comment_protocol_version(protocol, "01") + # DECK SETUP AND LABWARE + protocol.comment("THIS IS A NO MODULE RUN") + tiprack_x_1 = protocol.load_labware("opentrons_flex_96_tiprack_200ul", "D1") + tiprack_x_2 = protocol.load_labware("opentrons_flex_96_tiprack_200ul", "D2") + tiprack_x_3 = protocol.load_labware("opentrons_flex_96_tiprack_200ul", "A1") + sample_plate_1 = protocol.load_labware( + "armadillo_96_wellplate_200ul_pcr_full_skirt", "D3" + ) + + reservoir = protocol.load_labware("nest_12_reservoir_15ml", "B3") + waste_reservoir = protocol.load_labware( + "nest_1_reservoir_195ml", "C1", "Liquid Waste" + ) + sample_plate_2 = protocol.load_labware( + "armadillo_96_wellplate_200ul_pcr_full_skirt", "C2" + ) + sample_plate_3 = protocol.load_labware( + "armadillo_96_wellplate_200ul_pcr_full_skirt", "B2" + ) + sample_plate_4 = protocol.load_labware( + "armadillo_96_wellplate_200ul_pcr_full_skirt", "A2" + ) + protocol.load_trash_bin("A3") + + # reagentg146 + Dye_1 = reservoir["A1"] + Dye_2 = reservoir["A2"] + Dye_3 = reservoir["A3"] + Diluent_1 = reservoir["A4"] + Diluent_2 = reservoir["A5"] + Diluent_3 = reservoir["A6"] + + # pipette + p1000 = protocol.load_instrument( + "flex_8channel_1000", mount_pos, liquid_presence_detection=True + ) + p1000_single = protocol.load_instrument( + "flex_1channel_1000", + "right", + liquid_presence_detection=True, + tip_racks=[tiprack_x_2, tiprack_x_3], + ) + # LOAD LIQUIDS + liquid_volumes = [675.0, 675.0, 675.0, 675.0, 675.0] + wells = [Dye_1, Dye_2, Dye_3, Diluent_1, Diluent_2, Diluent_3] + helpers.load_wells_with_water(protocol, wells, liquid_volumes) + liquid_vols_and_wells: Dict[str, List[Dict[str, Well | List[Well] | float]]] = { + "Dye": [{"well": [Dye_1, Dye_2, Dye_3], "volume": 675.0}], + "Diluent": [{"well": [Diluent_1, Diluent_2, Diluent_3], "volume": 675.0}], + } + current_rack = tiprack_x_1 + # CONFIGURE SINGLE LAYOUT + p1000.configure_nozzle_layout(style=SINGLE, start="H1", tip_racks=[tiprack_x_1]) + helpers.find_liquid_height_of_loaded_liquids( + protocol, liquid_vols_and_wells, p1000_single + ) + + for X in range(1): + protocol.comment("==============================================") + protocol.comment("Adding Dye Sample Plate 1") + protocol.comment("==============================================") + + current = 0 + + well = get_next_tip_by_row(current_rack, p1000) + p1000.pick_up_tip(well) + while current < len(data): + CurrentWell = str(data[current][0]) + DyeVol = float(data[current][1]) + if DyeVol != 0 and DyeVol < 100: + p1000.liquid_presence_detection = False + p1000.transfer( + DyeVol, + Dye_1.bottom(z=2), + sample_plate_1.wells_by_name()[CurrentWell].top(z=1), + new_tip="never", + ) + if DyeVol > 20: + wells.append(sample_plate_1.wells_by_name()[CurrentWell]) + current += 1 + p1000.blow_out(location=waste_reservoir["A1"]) + p1000.touch_tip() + p1000.drop_tip() + p1000.liquid_presence_detection = True + + protocol.comment("==============================================") + protocol.comment("Adding Diluent Sample Plate 1") + protocol.comment("==============================================") + + current = 0 + while current < len(data): + CurrentWell = str(data[current][0]) + DilutionVol = float(data[current][2]) + if DilutionVol != 0 and DilutionVol < 100: + well = get_next_tip_by_row(current_rack, p1000) + p1000.pick_up_tip(well) + p1000.aspirate(DilutionVol, Diluent_1.bottom(z=dot_bottom)) + p1000.dispense( + DilutionVol, sample_plate_1.wells_by_name()[CurrentWell].top(z=0.2) + ) + if DilutionVol > 20: + wells.append(sample_plate_1.wells_by_name()[CurrentWell]) + p1000.blow_out(location=waste_reservoir["A1"]) + p1000.touch_tip() + p1000.drop_tip() + current += 1 + + protocol.comment("Changing pipette configuration to 8ch.") + + protocol.comment("==============================================") + protocol.comment("Adding Dye Sample Plate 2") + protocol.comment("==============================================") + current = 0 + p1000_single.pick_up_tip() + while current < len(data): + CurrentWell = str(data[current][0]) + DyeVol = float(data[current][1]) + if DyeVol != 0 and DyeVol < 100: + p1000_single.transfer( + DyeVol, + Dye_2.bottom(z=2), + sample_plate_2.wells_by_name()[CurrentWell].top(z=1), + new_tip="never", + ) + if DyeVol > 20: + wells.append(sample_plate_2.wells_by_name()[CurrentWell]) + current += 1 + p1000_single.blow_out(location=waste_reservoir["A1"]) + p1000_single.touch_tip() + p1000_single.return_tip() + + protocol.comment("==============================================") + protocol.comment("Adding Diluent Sample Plate 2") + protocol.comment("==============================================") + + current = 0 + while current < len(data): + CurrentWell = str(data[current][0]) + DilutionVol = float(data[current][2]) + if DilutionVol != 0 and DilutionVol < 100: + p1000_single.pick_up_tip() + p1000_single.aspirate(DilutionVol, Diluent_2.bottom(z=dot_bottom)) + p1000_single.dispense( + DilutionVol, sample_plate_2.wells_by_name()[CurrentWell].top(z=0.2) + ) + if DilutionVol > 20: + wells.append(sample_plate_2.wells_by_name()[CurrentWell]) + p1000_single.blow_out(location=waste_reservoir["A1"]) + p1000_single.touch_tip() + p1000_single.return_tip() + current += 1 + + protocol.comment("==============================================") + protocol.comment("Adding Dye Sample Plate 3") + protocol.comment("==============================================") + + current = 0 + p1000_single.pick_up_tip() + while current < len(data): + CurrentWell = str(data[current][0]) + DyeVol = float(data[current][1]) + if DyeVol != 0 and DyeVol < 100: + p1000_single.liquid_presence_detection = False + p1000_single.transfer( + DyeVol, + Dye_3.bottom(z=2), + sample_plate_3.wells_by_name()[CurrentWell].top(z=1), + blow_out=True, + blowout_location="destination well", + new_tip="never", + ) + if DyeVol > 20: + wells.append(sample_plate_3.wells_by_name()[CurrentWell]) + current += 1 + p1000_single.liquid_presence_detection = True + p1000_single.blow_out(location=waste_reservoir["A1"]) + p1000_single.touch_tip() + p1000_single.return_tip() + protocol.comment("==============================================") + protocol.comment("Adding Diluent Sample Plate 3") + protocol.comment("==============================================") + current = 0 + while current < len(data): + CurrentWell = str(data[current][0]) + DilutionVol = float(data[current][2]) + if DilutionVol != 0 and DilutionVol < 100: + p1000_single.pick_up_tip() + p1000_single.aspirate(DilutionVol, Diluent_3.bottom(z=dot_bottom)) + p1000_single.dispense( + DilutionVol, sample_plate_3.wells_by_name()[CurrentWell].top(z=0.2) + ) + if DilutionVol > 20: + wells.append(sample_plate_3.wells_by_name()[CurrentWell]) + p1000_single.blow_out(location=waste_reservoir["A1"]) + p1000_single.touch_tip() + p1000_single.return_tip() + current += 1 + + protocol.comment("==============================================") + protocol.comment("Adding Dye Sample Plate 4") + protocol.comment("==============================================") + p1000_single.reset_tipracks() + current = 0 + p1000_single.pick_up_tip() + while current < len(data): + CurrentWell = str(data[current][0]) + DyeVol = float(data[current][1]) + if DyeVol != 0 and DyeVol < 100: + p1000_single.liquid_presence_detection = False + p1000_single.transfer( + DyeVol, + Dye_3.bottom(z=2), + sample_plate_4.wells_by_name()[CurrentWell].top(z=1), + blow_out=True, + blowout_location="destination well", + new_tip="never", + ) + if DyeVol > 20: + wells.append(sample_plate_4.wells_by_name()[CurrentWell]) + current += 1 + p1000_single.liquid_presence_detection = True + p1000_single.blow_out(location=waste_reservoir["A1"]) + p1000_single.touch_tip() + p1000_single.return_tip() + protocol.comment("==============================================") + protocol.comment("Adding Diluent Sample Plate 4") + protocol.comment("==============================================") + current = 0 + while current < len(data): + CurrentWell = str(data[current][0]) + DilutionVol = float(data[current][2]) + if DilutionVol != 0 and DilutionVol < 100: + p1000_single.pick_up_tip() + p1000_single.aspirate(DilutionVol, Diluent_3.bottom(z=dot_bottom)) + p1000_single.dispense( + DilutionVol, sample_plate_4.wells_by_name()[CurrentWell].top(z=0.2) + ) + if DilutionVol > 20: + wells.append(sample_plate_4.wells_by_name()[CurrentWell]) + p1000_single.blow_out(location=waste_reservoir["A1"]) + p1000_single.touch_tip() + p1000_single.return_tip() + current += 1 + + current = 0 + # Probe heights + p1000.configure_nozzle_layout(style=ALL, tip_racks=[tiprack_x_3]) + helpers.clean_up_plates( + p1000, + [sample_plate_1, sample_plate_2, sample_plate_3, sample_plate_4], + waste_reservoir["A1"], + 200, + ) + helpers.find_liquid_height_of_all_wells( + protocol, p1000_single, [waste_reservoir["A1"]] + ) diff --git a/abr-testing/abr_testing/protocols/active_protocols/2_BMS_PCR_Protocol.py b/abr-testing/abr_testing/protocols/active_protocols/2_BMS_PCR_Protocol.py new file mode 100644 index 00000000000..3b11b51b7fe --- /dev/null +++ b/abr-testing/abr_testing/protocols/active_protocols/2_BMS_PCR_Protocol.py @@ -0,0 +1,217 @@ +"""BMS PCR Protocol.""" + +from opentrons.protocol_api import ParameterContext, ProtocolContext, Labware +from opentrons.protocol_api.module_contexts import ( + ThermocyclerContext, + TemperatureModuleContext, +) +from opentrons.protocol_api import SINGLE, Well, ALL +from abr_testing.protocols import helpers +from typing import List, Dict + + +metadata = { + "protocolName": "PCR Protocol with TC Auto Sealing Lid", + "author": "Rami Farawi None: + """Parameters.""" + helpers.create_single_pipette_mount_parameter(parameters) + helpers.create_disposable_lid_parameter(parameters) + helpers.create_csv_parameter(parameters) + helpers.create_tc_lid_deck_riser_parameter(parameters) + helpers.create_deactivate_modules_parameter(parameters) + + +def run(protocol: ProtocolContext) -> None: + """Protocol.""" + pipette_mount = protocol.params.pipette_mount # type: ignore[attr-defined] + disposable_lid = protocol.params.disposable_lid # type: ignore[attr-defined] + parsed_csv = protocol.params.parameters_csv.parse_as_csv() # type: ignore[attr-defined] + deck_riser = protocol.params.deck_riser # type: ignore[attr-defined] + deactivate_modules_bool = protocol.params.deactivate_modules # type: ignore[attr-defined] + helpers.comment_protocol_version(protocol, "01") + + rxn_vol = 50 + real_mode = True + # DECK SETUP AND LABWARE + + tc_mod: ThermocyclerContext = protocol.load_module( + helpers.tc_str + ) # type: ignore[assignment] + + tc_mod.open_lid() + tc_mod.set_lid_temperature(105) + temp_mod: TemperatureModuleContext = protocol.load_module( + helpers.temp_str, location="D3" + ) # type: ignore[assignment] + reagent_rack = temp_mod.load_labware( + "opentrons_24_aluminumblock_nest_1.5ml_snapcap", "Reagent Rack" + ) + dest_plate_1 = tc_mod.load_labware( + "opentrons_96_wellplate_200ul_pcr_full_skirt", "Destination Plate 1" + ) + + source_plate_1 = protocol.load_labware( + "opentrons_96_wellplate_200ul_pcr_full_skirt", "D1", "DNA Plate 1" + ) + waste = protocol.load_labware("nest_1_reservoir_195ml", "D2", "Liquid Waste") + liquid_waste = waste["A1"] + tiprack_50 = [ + protocol.load_labware("opentrons_flex_96_tiprack_50ul", slot) for slot in [8, 9] + ] + + # Opentrons tough pcr auto sealing lids + if disposable_lid: + unused_lids = helpers.load_disposable_lids(protocol, 3, ["C3"], deck_riser) + used_lids: List[Labware] = [] + + # LOAD PIPETTES + p50 = protocol.load_instrument( + "flex_8channel_50", + pipette_mount, + tip_racks=tiprack_50, + liquid_presence_detection=True, + ) + p50.configure_nozzle_layout(style=SINGLE, start="A1", tip_racks=tiprack_50) + protocol.load_trash_bin("A3") + + temp_mod.set_temperature(4) + + # LOAD LIQUIDS + water: Well = reagent_rack["B1"] + mmx_pic: List[Well] = reagent_rack.rows()[0] + dna_pic: List[Well] = source_plate_1.wells() + liquid_vols_and_wells: Dict[str, List[Dict[str, Well | List[Well] | float]]] = { + "Water": [{"well": water, "volume": 500.0}], + "Mastermix": [{"well": mmx_pic, "volume": 500.0}], + "DNA": [{"well": dna_pic, "volume": 100.0}], + } + helpers.find_liquid_height_of_loaded_liquids(protocol, liquid_vols_and_wells, p50) + # adding water + protocol.comment("\n\n----------ADDING WATER----------\n") + p50.pick_up_tip() + p50.aspirate(40, water) # prewet + p50.dispense(40, water) + parsed_csv = parsed_csv[1:] + num_of_rows = len(parsed_csv) + for row_index in range(num_of_rows): + row_values = parsed_csv[row_index] + water_vol = row_values[1] + if water_vol.lower() == "x": + continue + water_vol = int(water_vol) + dest_well = row_values[0] + if water_vol == 0: + break + + p50.configure_for_volume(water_vol) + p50.aspirate(water_vol, water) + p50.dispense(water_vol, dest_plate_1[dest_well], rate=0.5) + p50.configure_for_volume(50) + p50.blow_out() + p50.drop_tip() + + # adding Mastermix + protocol.comment("\n\n----------ADDING MASTERMIX----------\n") + for i, row in enumerate(parsed_csv): + p50.pick_up_tip() + mmx_vol = row[3] + if mmx_vol.lower() == "x": + continue + + if i == 0: + mmx_tube = row[4] + mmx_tube_check = mmx_tube + mmx_tube = row[4] + if mmx_tube_check != mmx_tube: + + p50.drop_tip() + p50.pick_up_tip() + + if not p50.has_tip: + p50.pick_up_tip() + + mmx_vol = int(row[3]) + dest_well = row[0] + + if mmx_vol == 0: + break + p50.configure_for_volume(mmx_vol) + p50.aspirate(mmx_vol, reagent_rack[mmx_tube]) + p50.dispense(mmx_vol, dest_plate_1[dest_well].top()) + protocol.delay(seconds=2) + p50.blow_out() + p50.touch_tip() + p50.configure_for_volume(50) + p50.drop_tip() + if p50.has_tip: + p50.drop_tip() + + # adding DNA + protocol.comment("\n\n----------ADDING DNA----------\n") + for row in parsed_csv: + dna_vol = row[2] + if dna_vol.lower() == "x": + continue + + p50.pick_up_tip() + + dna_vol = int(row[2]) + dest_and_source_well = row[0] + + if dna_vol == 0: + break + p50.configure_for_volume(dna_vol) + p50.aspirate(dna_vol, source_plate_1[dest_and_source_well]) + p50.dispense(dna_vol, dest_plate_1[dest_and_source_well], rate=0.5) + + p50.mix( + 10, + 0.7 * rxn_vol if 0.7 * rxn_vol < 30 else 30, + dest_plate_1[dest_and_source_well], + ) + p50.drop_tip() + p50.configure_for_volume(50) + + protocol.comment("\n\n-----------Running PCR------------\n") + + if real_mode: + if disposable_lid: + lid_on_plate, unused_lids, used_lids = helpers.use_disposable_lid_with_tc( + protocol, unused_lids, used_lids, dest_plate_1, tc_mod + ) + else: + tc_mod.close_lid() + helpers.perform_pcr( + protocol, + tc_mod, + initial_denature_time_sec=120, + denaturation_time_sec=10, + anneal_time_sec=10, + extension_time_sec=30, + cycle_repetitions=30, + final_extension_time_min=5, + ) + + tc_mod.set_block_temperature(4) + + tc_mod.open_lid() + if disposable_lid: + if len(used_lids) <= 1: + protocol.move_labware(lid_on_plate, "C2", use_gripper=True) + else: + protocol.move_labware(lid_on_plate, used_lids[-2], use_gripper=True) + p50.drop_tip() + p50.configure_nozzle_layout(style=SINGLE, start="A1", tip_racks=tiprack_50) + mmx_pic.append(water) + # Empty plates into liquid waste + p50.configure_nozzle_layout(style=ALL, tip_racks=tiprack_50) + helpers.clean_up_plates(p50, [source_plate_1, dest_plate_1], liquid_waste, 50) + # Probe liquid waste + helpers.find_liquid_height_of_all_wells(protocol, p50, [liquid_waste]) + if deactivate_modules_bool: + helpers.deactivate_modules(protocol) diff --git a/abr-testing/abr_testing/protocols/active_protocols/3_Tartrazine Protocol.py b/abr-testing/abr_testing/protocols/active_protocols/3_Tartrazine Protocol.py new file mode 100644 index 00000000000..9916ef7f7fc --- /dev/null +++ b/abr-testing/abr_testing/protocols/active_protocols/3_Tartrazine Protocol.py @@ -0,0 +1,235 @@ +"""Tartrazine Protocol.""" +from opentrons.protocol_api import ( + ProtocolContext, + ParameterContext, + Well, + InstrumentContext, +) +from abr_testing.protocols import helpers +from opentrons.protocol_api.module_contexts import ( + AbsorbanceReaderContext, + HeaterShakerContext, +) +from datetime import datetime +from typing import Dict, List +import statistics + +metadata = { + "protocolName": "Tartrazine Protocol", + "author": "Opentrons ", + "source": "Protocol Library", +} + +requirements = {"robotType": "Flex", "apiLevel": "2.21"} + + +def add_parameters(parameters: ParameterContext) -> None: + """Add parameters.""" + parameters.add_int( + variable_name="number_of_plates", + display_name="Number of Plates", + default=1, + minimum=1, + maximum=4, + ) + helpers.create_channel_parameter(parameters) + helpers.create_plate_reader_compatible_labware_parameter(parameters) + + +def run(protocol: ProtocolContext) -> None: + """Protocol.""" + # Load parameters + number_of_plates = protocol.params.number_of_plates # type: ignore [attr-defined] + channels = protocol.params.channels # type: ignore [attr-defined] + plate_type = protocol.params.labware_plate_reader_compatible # type: ignore [attr-defined] + + helpers.comment_protocol_version(protocol, "01") + # Plate Reader + plate_reader: AbsorbanceReaderContext = protocol.load_module( + helpers.abs_mod_str, "A3" + ) # type: ignore[assignment] + hs: HeaterShakerContext = protocol.load_module(helpers.hs_str, "A1") # type: ignore[assignment] + # Load Plates based off of number_of_plates parameter + available_deck_slots = ["D1", "D2", "C1", "B1"] + sample_plate_list = [] + for plate_num, slot in zip(range(number_of_plates), available_deck_slots): + plate = protocol.load_labware(plate_type, slot, f"Sample Plate {plate_num + 1}") + sample_plate_list.append(plate) + available_tip_rack_slots = ["D3", "C3", "B3", "B2"] + # LOAD PIPETTES AND TIP RACKS + # 50 CHANNEL + tip_racks_50 = [] + for plate_num, slot_2 in zip(range(number_of_plates), available_tip_rack_slots): + tiprack_50 = protocol.load_labware("opentrons_flex_96_tiprack_50ul", slot_2) + tip_racks_50.append(tiprack_50) + p50 = protocol.load_instrument( + f"flex_{channels}_50", "left", tip_racks=tip_racks_50 + ) + # 1000 CHANNEL + tiprack_1000_1 = protocol.load_labware("opentrons_flex_96_tiprack_1000ul", "A2") + p1000 = protocol.load_instrument( + f"flex_{channels}_1000", "right", tip_racks=[tiprack_1000_1] + ) + # DETERMINE RESERVOIR BASED OFF # OF PIPETTE CHANNELS + # 1 CHANNEL = TUBE RACK + if p50.active_channels == 1: + reservoir = protocol.load_labware( + "opentrons_10_tuberack_nest_4x50ml_6x15ml_conical", "C2", "Reservoir" + ) + water_max_vol = reservoir["A3"].max_volume - 500 + reservoir_wells = reservoir.wells()[6:] # Skip first 4 bc they are 15ml + else: + # 8 CHANNEL = 12 WELL RESERVOIR + reservoir = protocol.load_labware("nest_12_reservoir_15ml", "C2", "Reservoir") + water_max_vol = reservoir["A1"].max_volume - 500 + reservoir_wells = reservoir.wells()[ + 1: + ] # Skip A1 as it's reserved for tartrazine + + # LABEL RESERVOIR WELLS AND DETERMINE NEEDED LIQUID + tartrazine_well = reservoir["A1"] + # NEEDED TARTRAZINE + needed_tartrazine: float = ( + float(number_of_plates) * 96.0 + ) * 10.0 + 1000.0 # loading extra as a safety factor + # NEEDED WATER + needed_water: float = ( + float(number_of_plates) * 96.0 * 250 + ) # loading extra as a safety factor + # CALCULATING NEEDED # OF WATER WELLS + needed_wells = round(needed_water / water_max_vol) + water_wells = [] + for i in range(needed_wells + 1): + water_wells.append(reservoir_wells[i]) + + def _mix_tartrazine(pipette: InstrumentContext, well_to_probe: Well) -> None: + """Mix Tartrazine.""" + # Mix step is needed to ensure tartrazine does not settle between plates. + pipette.pick_up_tip() + top_of_tartrazine = helpers.find_liquid_height(pipette, well_to_probe) + for i in range(20): + p50.aspirate(1, well_to_probe.bottom(z=1)) + p50.dispense(1, well_to_probe.bottom(z=top_of_tartrazine + 1)) + pipette.return_tip() + + # LOAD LIQUIDS AND PROBE WELLS + liquid_vols_and_wells: Dict[str, List[Dict[str, Well | List[Well] | float]]] = { + "Tartrazine": [{"well": tartrazine_well, "volume": needed_tartrazine}], + "Water": [{"well": water_wells, "volume": water_max_vol}], + } + helpers.find_liquid_height_of_loaded_liquids(protocol, liquid_vols_and_wells, p50) + tip_count = 1 * p50.active_channels # number of 50 ul tip uses. + p50.reset_tipracks() + i = 0 + all_percent_error_dict = {} + cv_dict = {} + vol = 0.0 # counter to track available water volume + water_tip_count = 0 * p1000.active_channels # number of 1000 ul tip uses + well_num = 0 # index of well being used for water + for sample_plate in sample_plate_list[:number_of_plates]: + return_location = sample_plate.parent + # Mix Tartrazine to ensure no settling as occurred + _mix_tartrazine(p50, tartrazine_well) + tip_count += 1 * p50.active_channels + # Determine list of wells to probe + if p50.active_channels == 1: + well_list = sample_plate.wells() + elif p50.active_channels == 8: + well_list = sample_plate.rows()[0] + for well in well_list: + p1000.pick_up_tip() + # Determine which water well to aspirate from. + if vol < water_max_vol - 6000: + well_of_choice = water_wells[well_num] + else: + well_num += 1 + well_of_choice = water_wells[well_num] + vol = 0.0 + p50.pick_up_tip() + p1000.aspirate(190, well_of_choice) + p1000.air_gap(10) + p1000.dispense(10, well.top()) + p1000.dispense(190, well) + # Two blow outs ensures water is completely removed from pipette + p1000.blow_out(well.top()) + protocol.delay(minutes=0.1) + p1000.blow_out(well.top()) + vol += 190 * p1000.active_channels + # Probe to find liquid height of tartrazine to ensure correct amount is aspirated + height = helpers.find_liquid_height(p50, tartrazine_well) + if height <= 0.0: + # If a negative tartrazine height is found, + # the protocol will pause, prompt a refill, and reprobe. + protocol.pause("Fill tartrazine") + height = helpers.find_liquid_height(p50, tartrazine_well) + p50.aspirate(10, tartrazine_well.bottom(z=height), rate=0.15) + p50.air_gap(5) + p50.dispense(5, well.top()) + p50.dispense(10, well.bottom(z=0.5), rate=0.15) + p50.blow_out() + protocol.delay(minutes=0.1) + p50.blow_out() + p50.return_tip() + tip_count += p50.active_channels + if tip_count >= (96 * len(tip_racks_50)): + p50.reset_tipracks() + tip_count = 0 + p1000.return_tip() + water_tip_count += p1000.active_channels + if water_tip_count >= 96: + p1000.reset_tipracks() + water_tip_count = 0 + # Move labware to heater shaker to be mixed + helpers.move_labware_to_hs(protocol, sample_plate, hs, hs) + helpers.set_hs_speed(protocol, hs, 1500, 2.0, True) + hs.open_labware_latch() + # Initialize plate reader + plate_reader.close_lid() + plate_reader.initialize("single", [450]) + plate_reader.open_lid() + # Move sample plate into plate reader + protocol.move_labware(sample_plate, plate_reader, use_gripper=True) + sample_plate_name = "sample plate_" + str(i + 1) + csv_string = sample_plate_name + "_" + str(datetime.now()) + plate_reader.close_lid() + result = plate_reader.read(csv_string) + # Calculate CV and % error of expected value. + for wavelength in result: + dict_of_wells = result[wavelength] + readings_and_wells = dict_of_wells.items() + readings = dict_of_wells.values() + avg = statistics.mean(readings) + # Check if every average is within +/- 5% of 2.85 + percent_error_dict = {} + percent_error_sum = 0.0 + for reading in readings_and_wells: + well_name = str(reading[0]) + measurement = reading[1] + percent_error = (measurement - 2.85) / 2.85 * 100 + percent_error_dict[well_name] = percent_error + percent_error_sum += percent_error + avg_percent_error = percent_error_sum / 96.0 + standard_deviation = statistics.stdev(readings) + try: + cv = standard_deviation / avg + except ZeroDivisionError: + cv = 0.0 + cv_percent = cv * 100 + cv_dict[sample_plate_name] = { + "CV": cv_percent, + "Mean": avg, + "SD": standard_deviation, + "Avg Percent Error": avg_percent_error, + } + # Move Plate back to original location + all_percent_error_dict[sample_plate_name] = percent_error_dict + plate_reader.open_lid() + protocol.comment( + f"------plate {sample_plate}. {cv_dict[sample_plate_name]}------" + ) + protocol.move_labware(sample_plate, return_location, use_gripper=True) + i += 1 + # Print percent error dictionary + protocol.comment("Percent Error: " + str(all_percent_error_dict)) + # Print cv dictionary + protocol.comment("Plate Reader Result: " + str(cv_dict)) diff --git a/abr-testing/abr_testing/protocols/active_protocols/4_Illumina DNA Enrichment.py b/abr-testing/abr_testing/protocols/active_protocols/4_Illumina DNA Enrichment.py new file mode 100644 index 00000000000..ff9a9807c92 --- /dev/null +++ b/abr-testing/abr_testing/protocols/active_protocols/4_Illumina DNA Enrichment.py @@ -0,0 +1,1016 @@ +"""DVT1ABR4: Illumina DNA Enrichment.""" +from opentrons.protocol_api import ( + ParameterContext, + ProtocolContext, + Labware, + Well, + InstrumentContext, +) +from opentrons import types +from abr_testing.protocols import helpers +from opentrons.protocol_api.module_contexts import ( + HeaterShakerContext, + MagneticBlockContext, + ThermocyclerContext, + TemperatureModuleContext, +) +from opentrons.hardware_control.modules.types import ThermocyclerStep +from typing import List, Dict + + +metadata = { + "protocolName": "Illumina DNA Enrichment v4 with TC Auto Sealing Lid", + "author": "Opentrons ", + "source": "Protocol Library", +} + +requirements = { + "robotType": "Flex", + "apiLevel": "2.21", +} + +# SCRIPT SETTINGS +DRYRUN = False # True = skip incubation times, shorten mix, for testing purposes +USE_GRIPPER = True # True = Uses Gripper, False = Manual Move +TIP_TRASH = False # True = Used tips go in Trash, False = Used tips go back into rack +HYBRID_PAUSE = True # True = sets a pause on the Hybridization + +# PROTOCOL SETTINGS +COLUMNS = 4 # 1-4 +HYBRIDDECK = True +HYBRIDTIME = 1.6 # Hours + +# PROTOCOL BLOCKS +STEP_VOLPOOL = 0 +STEP_HYB = 0 +STEP_CAPTURE = 1 +STEP_WASH = 1 +STEP_PCR = 1 +STEP_PCRDECK = 1 +STEP_CLEANUP = 1 + +p200_tips = 0 +p50_tips = 0 +total_waste_volume = 0.0 + + +RUN = 1 + + +def add_parameters(parameters: ParameterContext) -> None: + """Add parameters.""" + helpers.create_hs_speed_parameter(parameters) + helpers.create_dot_bottom_parameter(parameters) + helpers.create_disposable_lid_parameter(parameters) + helpers.create_tc_lid_deck_riser_parameter(parameters) + helpers.create_disposable_lid_trash_location(parameters) + helpers.create_deactivate_modules_parameter(parameters) + + +def run(protocol: ProtocolContext) -> None: + """Protocol.""" + heater_shaker_speed = protocol.params.heater_shaker_speed # type: ignore[attr-defined] + dot_bottom = protocol.params.dot_bottom # type: ignore[attr-defined] + disposable_lid = protocol.params.disposable_lid # type: ignore[attr-defined] + deck_riser = protocol.params.deck_riser # type: ignore[attr-defined] + trash_lid = protocol.params.trash_lid # type: ignore[attr-defined] + deactivate_modules_bool = protocol.params.deactivate_modules # type: ignore[attr-defined] + helpers.comment_protocol_version(protocol, "01") + + unused_lids: List[Labware] = [] + used_lids: List[Labware] = [] + global p200_tips + global p50_tips + + protocol.comment("THIS IS A DRY RUN") if DRYRUN else protocol.comment( + "THIS IS A REACTION RUN" + ) + protocol.comment("USED TIPS WILL GO IN TRASH") if TIP_TRASH else protocol.comment( + "USED TIPS WILL BE RE-RACKED" + ) + + # DECK SETUP AND LABWARE + # ========== FIRST ROW =========== + heatershaker: HeaterShakerContext = protocol.load_module( + helpers.hs_str, "1" + ) # type: ignore[assignment] + heatershaker.close_labware_latch() + sample_plate_2 = heatershaker.load_labware( + "thermoscientificnunc_96_wellplate_1300ul" + ) + reservoir = protocol.load_labware("nest_96_wellplate_2ml_deep", "2", "Liquid Waste") + temp_block: TemperatureModuleContext = protocol.load_module( + helpers.temp_str, "3" + ) # type: ignore[assignment] + reagent_plate, temp_adapter = helpers.load_temp_adapter_and_labware( + "armadillo_96_wellplate_200ul_pcr_full_skirt", temp_block, "Reagent Plate" + ) + # ========== SECOND ROW ========== + MAG_PLATE_SLOT: MagneticBlockContext = protocol.load_module( + helpers.mag_str, "C1" + ) # type: ignore[assignment] + tiprack_200_1 = protocol.load_labware("opentrons_flex_96_tiprack_200ul", "5") + tiprack_50_1 = protocol.load_labware("opentrons_flex_96_tiprack_50ul", "6") + # Opentrons tough pcr auto sealing lids + if disposable_lid: + unused_lids = helpers.load_disposable_lids(protocol, 3, ["C4"], deck_riser) + # ========== THIRD ROW =========== + thermocycler: ThermocyclerContext = protocol.load_module( + helpers.tc_str + ) # type: ignore[assignment] + sample_plate_1 = thermocycler.load_labware( + "armadillo_96_wellplate_200ul_pcr_full_skirt" + ) + thermocycler.open_lid() + tiprack_200_2 = protocol.load_labware("opentrons_flex_96_tiprack_200ul", "8") + tiprack_50_2 = protocol.load_labware("opentrons_flex_96_tiprack_50ul", "9") + # ========== FOURTH ROW ========== + tiprack_200_3 = protocol.load_labware("opentrons_flex_96_tiprack_200ul", "11") + trash_bin = protocol.load_trash_bin("A3") + # reagent + AMPure = reservoir["A1"] + SMB = reservoir["A2"] + + EtOH = reservoir["A4"] + RSB = reservoir["A5"] + Liquid_trash_well_1 = reservoir["A9"] + Liquid_trash_well_2 = reservoir["A10"] + Liquid_trash_well_3 = reservoir["A11"] + Liquid_trash_well_4 = reservoir["A12"] + liquid_trash_list = { + Liquid_trash_well_1: 0.0, + Liquid_trash_well_2: 0.0, + Liquid_trash_well_3: 0.0, + Liquid_trash_well_4: 0.0, + } + + def trash_liquid( + protocol: ProtocolContext, + pipette: InstrumentContext, + vol_to_trash: float, + liquid_trash_list: Dict[Well, float], + ) -> None: + """Determine which wells to use as liquid waste.""" + remaining_volume = vol_to_trash + max_capacity = 1500.0 + # Determine liquid waste location depending on current total volume + # Distribute the liquid volume sequentially + for well, current_volume in liquid_trash_list.items(): + if remaining_volume <= 0.0: + break + available_capacity = max_capacity - current_volume + if available_capacity < remaining_volume: + continue + pipette.dispense(remaining_volume, well.top()) + protocol.delay(minutes=0.1) + pipette.blow_out(well.top()) + liquid_trash_list[well] += remaining_volume + if pipette.current_volume <= 0.0: + break + + # Will Be distributed during the protocol + EEW_1 = sample_plate_2.wells_by_name()["A9"] + EEW_2 = sample_plate_2.wells_by_name()["A10"] + EEW_3 = sample_plate_2.wells_by_name()["A11"] + EEW_4 = sample_plate_2.wells_by_name()["A12"] + + NHB2 = reagent_plate.wells_by_name()["A1"] + Panel = reagent_plate.wells_by_name()["A2"] + EHB2 = reagent_plate.wells_by_name()["A3"] + Elute = reagent_plate.wells_by_name()["A4"] + ET2 = reagent_plate.wells_by_name()["A5"] + PPC = reagent_plate.wells_by_name()["A6"] + EPM = reagent_plate.wells_by_name()["A7"] + + # pipette + p1000 = protocol.load_instrument( + "flex_8channel_1000", + "left", + tip_racks=[tiprack_200_1, tiprack_200_2, tiprack_200_3], + ) + p50 = protocol.load_instrument( + "flex_8channel_50", "right", tip_racks=[tiprack_50_1, tiprack_50_2] + ) + reagent_plate.columns()[3] + # Load liquids and probe + liquid_vols_and_wells: Dict[str, List[Dict[str, Well | List[Well] | float]]] = { + "Reagents": [ + {"well": reagent_plate.columns()[3], "volume": 75.0}, + {"well": reagent_plate.columns()[4], "volume": 15.0}, + {"well": reagent_plate.columns()[5], "volume": 20.0}, + {"well": reagent_plate.columns()[6], "volume": 65.0}, + ], + "AMPure": [{"well": reservoir.columns()[0], "volume": 120.0}], + "SMB": [{"well": reservoir.columns()[1], "volume": 750.0}], + "EtOH": [{"well": reservoir.columns()[3], "volume": 900.0}], + "RSB": [{"well": reservoir.columns()[4], "volume": 96.0}], + "Wash": [ + {"well": sample_plate_2.columns()[8], "volume": 1000.0}, + {"well": sample_plate_2.columns()[9], "volume": 1000.0}, + {"well": sample_plate_2.columns()[10], "volume": 1000.0}, + {"well": sample_plate_2.columns()[11], "volume": 1000.0}, + ], + "Samples": [{"well": sample_plate_1.wells(), "volume": 150.0}], + } + helpers.find_liquid_height_of_loaded_liquids(protocol, liquid_vols_and_wells, p50) + # tip and sample tracking + if COLUMNS == 1: + column_1_list = ["A1"] # Plate 1 + column_2_list = ["A1"] # Plate 2 + column_3_list = ["A4"] # Plate 2 + column_4_list = ["A4"] # Plate 1 + column_5_list = ["A7"] # Plate 2 + column_6_list = ["A7"] # Plate 1 + WASHES = [EEW_1] + if COLUMNS == 2: + column_1_list = ["A1", "A2"] # Plate 1 + column_2_list = ["A1", "A2"] # Plate 2 + column_3_list = ["A4", "A5"] # Plate 2 + column_4_list = ["A4", "A5"] # Plate 1 + column_5_list = ["A7", "A8"] # Plate 2 + column_6_list = ["A7", "A8"] # Plate 1 + WASHES = [EEW_1, EEW_2] + if COLUMNS == 3: + column_1_list = ["A1", "A2", "A3"] # Plate 1 + column_2_list = ["A1", "A2", "A3"] # Plate 2 + column_3_list = ["A4", "A5", "A6"] # Plate 2 + column_4_list = ["A4", "A5", "A6"] # Plate 1 + column_5_list = ["A7", "A8", "A9"] # Plate 2 + column_6_list = ["A7", "A8", "A9"] # Plate 1 + WASHES = [EEW_1, EEW_2, EEW_3] + if COLUMNS == 4: + column_1_list = ["A1", "A2", "A3", "A4"] # Plate 1 + column_2_list = ["A1", "A2", "A3", "A4"] # Plate 2 + column_3_list = ["A5", "A6", "A7", "A8"] # Plate 2 + column_4_list = ["A5", "A6", "A7", "A8"] # Plate 1 + column_5_list = ["A9", "A10", "A11", "A12"] # Plate 2 + column_6_list = ["A9", "A10", "A11", "A12"] # Plate 1 + WASHES = [EEW_1, EEW_2, EEW_3, EEW_4] + + def tipcheck() -> None: + """Tip tracking function.""" + if p200_tips >= 3 * 12: + p1000.reset_tipracks() + p200_tips == 0 + if p50_tips >= 2 * 12: + p50.reset_tipracks() + p50_tips == 0 + + # commands + for loop in range(RUN): + thermocycler.open_lid() + heatershaker.open_labware_latch() + if DRYRUN is False: + if STEP_HYB == 1: + protocol.comment("SETTING THERMO and TEMP BLOCK Temperature") + thermocycler.set_block_temperature(4) + thermocycler.set_lid_temperature(100) + temp_block.set_temperature(4) + else: + protocol.comment("SETTING THERMO and TEMP BLOCK Temperature") + thermocycler.set_block_temperature(58) + thermocycler.set_lid_temperature(58) + heatershaker.set_and_wait_for_temperature(58) + heatershaker.close_labware_latch() + + # Sample Plate contains 30ul of DNA + + if STEP_VOLPOOL == 1: + protocol.comment("==============================================") + protocol.comment("--> Quick Vol Pool") + protocol.comment("==============================================") + + if STEP_HYB == 1: + protocol.comment("==============================================") + protocol.comment("--> HYB") + protocol.comment("==============================================") + + protocol.comment("--> Adding NHB2") + NHB2Vol = 50 + for loop, X in enumerate(column_1_list): + p50.pick_up_tip() + p50.aspirate(NHB2Vol, NHB2.bottom(z=dot_bottom)) # original = () + p50.dispense( + NHB2Vol, sample_plate_1[X].bottom(z=dot_bottom) + ) # original = () + p50.return_tip() if TIP_TRASH is False else p50.drop_tip() + p50_tips += 1 + tipcheck() + + protocol.comment("--> Adding Panel") + PanelVol = 10 + for loop, X in enumerate(column_1_list): + p50.pick_up_tip() + p50.aspirate(PanelVol, Panel.bottom(z=dot_bottom)) # original = () + p50.dispense( + PanelVol, sample_plate_1[X].bottom(z=dot_bottom) + ) # original = () + p50.return_tip() if TIP_TRASH is False else p50.drop_tip() + p50_tips += 1 + tipcheck() + + protocol.comment("--> Adding EHB2") + EHB2Vol = 10 + EHB2MixRep = 10 if DRYRUN is False else 1 + EHB2MixVol = 90 + for loop, X in enumerate(column_1_list): + p1000.pick_up_tip() + p1000.aspirate(EHB2Vol, EHB2.bottom(z=dot_bottom)) # original = () + p1000.dispense( + EHB2Vol, sample_plate_1[X].bottom(z=dot_bottom) + ) # original = () + p1000.move_to(sample_plate_1[X].bottom(z=dot_bottom)) # original = () + p1000.mix(EHB2MixRep, EHB2MixVol) + p1000.return_tip() if TIP_TRASH is False else p1000.drop_tip() + p50_tips += 1 + tipcheck() + + if HYBRIDDECK: + protocol.comment("Hybridize on Deck") + if disposable_lid: + ( + lid_on_plate, + unused_lids, + used_lids, + ) = helpers.use_disposable_lid_with_tc( + protocol, unused_lids, used_lids, sample_plate_1, thermocycler + ) + else: + thermocycler.close_lid() + if DRYRUN is False: + profile_TAGSTOP: List[ThermocyclerStep] = [ + {"temperature": 98, "hold_time_minutes": 5}, + {"temperature": 97, "hold_time_minutes": 1}, + {"temperature": 95, "hold_time_minutes": 1}, + {"temperature": 93, "hold_time_minutes": 1}, + {"temperature": 91, "hold_time_minutes": 1}, + {"temperature": 89, "hold_time_minutes": 1}, + {"temperature": 87, "hold_time_minutes": 1}, + {"temperature": 85, "hold_time_minutes": 1}, + {"temperature": 83, "hold_time_minutes": 1}, + {"temperature": 81, "hold_time_minutes": 1}, + {"temperature": 79, "hold_time_minutes": 1}, + {"temperature": 77, "hold_time_minutes": 1}, + {"temperature": 75, "hold_time_minutes": 1}, + {"temperature": 73, "hold_time_minutes": 1}, + {"temperature": 71, "hold_time_minutes": 1}, + {"temperature": 69, "hold_time_minutes": 1}, + {"temperature": 67, "hold_time_minutes": 1}, + {"temperature": 65, "hold_time_minutes": 1}, + {"temperature": 63, "hold_time_minutes": 1}, + {"temperature": 62, "hold_time_minutes": HYBRIDTIME * 60}, + ] + thermocycler.execute_profile( + steps=profile_TAGSTOP, repetitions=1, block_max_volume=100 + ) + thermocycler.set_block_temperature(62) + if HYBRID_PAUSE: + protocol.comment("HYBRIDIZATION PAUSED") + thermocycler.set_block_temperature(10) + thermocycler.open_lid() + if disposable_lid: + if trash_lid: + protocol.move_labware(lid_on_plate, trash_bin, use_gripper=True) + elif len(used_lids) <= 1: + protocol.move_labware(lid_on_plate, "B4", use_gripper=True) + else: + protocol.move_labware( + lid_on_plate, used_lids[-2], use_gripper=True + ) + else: + protocol.comment("Hybridize off Deck") + + if STEP_CAPTURE == 1: + protocol.comment("==============================================") + protocol.comment("--> Capture") + protocol.comment("==============================================") + # Standard Setup + + if DRYRUN is False: + protocol.comment("SETTING THERMO and TEMP BLOCK Temperature") + thermocycler.set_block_temperature(58) + thermocycler.set_lid_temperature(58) + + if DRYRUN is False: + heatershaker.set_and_wait_for_temperature(58) + + protocol.comment("--> Transfer Hybridization") + TransferSup = 100 + for loop, X in enumerate(column_1_list): + p1000.pick_up_tip() + p1000.move_to(sample_plate_1[X].bottom(z=0.5)) + p1000.aspirate(TransferSup + 1, rate=0.25) + p1000.dispense( + TransferSup + 1, sample_plate_2[column_2_list[loop]].bottom(z=1) + ) + p1000.return_tip() if TIP_TRASH is False else p1000.drop_tip() + p200_tips += 1 + tipcheck() + if disposable_lid: + ( + lid_on_plate, + unused_lids, + used_lids, + ) = helpers.use_disposable_lid_with_tc( + protocol, + unused_lids, + used_lids, + sample_plate_1, + thermocycler, + ) + else: + thermocycler.close_lid() + + protocol.comment("--> ADDING SMB") + SMBVol = 250 + SMBMixRPM = heater_shaker_speed + SMBMixRep = 5.0 if DRYRUN is False else 0.1 # minutes + SMBPremix = 3 if DRYRUN is False else 1 + # ============================== + for loop, X in enumerate(column_2_list): + p1000.pick_up_tip() + p1000.mix(SMBPremix, 200, SMB.bottom(z=1)) + p1000.aspirate(SMBVol / 2, SMB.bottom(z=1), rate=0.25) + p1000.dispense(SMBVol / 2, sample_plate_2[X].top(z=-7), rate=0.25) + p1000.aspirate(SMBVol / 2, SMB.bottom(z=1), rate=0.25) + p1000.dispense(SMBVol / 2, sample_plate_2[X].bottom(z=1), rate=0.25) + p1000.default_speed = 5 + p1000.move_to(sample_plate_2[X].bottom(z=5)) + for Mix in range(2): + p1000.aspirate(100, rate=0.5) + p1000.move_to(sample_plate_2[X].bottom(z=1)) + p1000.aspirate(80, rate=0.5) + p1000.dispense(80, rate=0.5) + p1000.move_to(sample_plate_2[X].bottom(z=5)) + p1000.dispense(100, rate=0.5) + Mix += 1 + p1000.blow_out(sample_plate_2[X].top(z=-7)) + p1000.default_speed = 400 + p1000.move_to(sample_plate_2[X].top(z=5)) + p1000.move_to(sample_plate_2[X].top(z=0)) + p1000.move_to(sample_plate_2[X].top(z=5)) + p1000.return_tip() if TIP_TRASH is False else p1000.drop_tip() + p200_tips += 1 + tipcheck() + # ============================== + helpers.set_hs_speed(protocol, heatershaker, SMBMixRPM, SMBMixRep, True) + + # GRIPPER MOVE sample_plate_2 FROM heatershaker TO MAGPLATE + helpers.move_labware_from_hs_to_destination( + protocol, sample_plate_2, heatershaker, MAG_PLATE_SLOT + ) + + thermocycler.open_lid() + if disposable_lid: + if trash_lid: + protocol.move_labware(lid_on_plate, trash_bin, use_gripper=True) + elif len(used_lids) <= 1: + protocol.move_labware(lid_on_plate, "B4", use_gripper=True) + else: + protocol.move_labware(lid_on_plate, used_lids[-2], use_gripper=True) + + if DRYRUN is False: + protocol.delay(minutes=2) + + protocol.comment("==============================================") + protocol.comment("--> WASH") + protocol.comment("==============================================") + # Setting Labware to Resume at Cleanup 1 + + protocol.comment("--> Remove SUPERNATANT") + for loop, X in enumerate(column_2_list): + p1000.pick_up_tip() + p1000.move_to(sample_plate_2[X].bottom(4)) + p1000.aspirate(200, rate=0.25) + trash_liquid(protocol, p1000, 200.0, liquid_trash_list) + p1000.move_to(sample_plate_2[X].bottom(0.5)) + p1000.aspirate(200, rate=0.25) + trash_liquid(protocol, p1000, 200.0, liquid_trash_list) + p1000.aspirate(20) + p1000.return_tip() if TIP_TRASH is False else p1000.drop_tip() + p200_tips += 1 + tipcheck() + + # GRIPPER MOVE sample_plate_2 FROM MAGPLATE TO heatershaker + helpers.move_labware_to_hs( + protocol, sample_plate_2, heatershaker, heatershaker + ) + + protocol.comment("--> Repeating 6 washes") + washreps = 6 + washcount = 0 + for wash in range(washreps): + + protocol.comment("--> Adding EEW") + EEWVol = 200 + for loop, X in enumerate(column_2_list): + p1000.pick_up_tip() + p1000.aspirate( + EEWVol, WASHES[loop].bottom(z=dot_bottom) + ) # original = () + p1000.dispense( + EEWVol, sample_plate_2[X].bottom(z=dot_bottom) + ) # original = () + p1000.return_tip() if TIP_TRASH is False else p1000.drop_tip() + p200_tips += 1 + tipcheck() + helpers.set_hs_speed( + protocol, heatershaker, int(heater_shaker_speed * 0.9), 4.0, True + ) + heatershaker.open_labware_latch() + + if DRYRUN is False: + protocol.delay(seconds=5 * 60) + + # GRIPPER MOVE sample_plate_2 FROM heatershaker TO MAGPLATE + helpers.move_labware_from_hs_to_destination( + protocol, sample_plate_2, heatershaker, MAG_PLATE_SLOT + ) + + if DRYRUN is False: + protocol.delay(seconds=1 * 60) + + protocol.comment("--> Removing Supernatant") + RemoveSup = 200 + for loop, X in enumerate(column_2_list): + p1000.pick_up_tip() + p1000.move_to(sample_plate_2[X].bottom(z=3.5)) + p1000.aspirate(RemoveSup - 100, rate=0.25) + protocol.delay(minutes=0.1) + p1000.move_to(sample_plate_2[X].bottom(z=0.5)) + p1000.aspirate(100, rate=0.25) + trash_liquid(protocol, p1000, RemoveSup, liquid_trash_list) + p1000.return_tip() if TIP_TRASH is False else p1000.drop_tip() + p200_tips += 1 + tipcheck() + + # ============================================================================================ + # GRIPPER MOVE sample_plate_2 FROM MAGPLATE TO heatershaker + helpers.move_labware_to_hs( + protocol, sample_plate_2, heatershaker, heatershaker + ) + washcount += 1 + + protocol.comment("--> Adding EEW") + EEWVol = 200 + for loop, X in enumerate(column_2_list): + p1000.pick_up_tip() + p1000.aspirate( + EEWVol, WASHES[loop].bottom(z=dot_bottom) + ) # original = () + p1000.dispense( + EEWVol, sample_plate_2[X].bottom(z=dot_bottom) + ) # original = () + p1000.return_tip() if TIP_TRASH is False else p1000.drop_tip() + p200_tips += 1 + tipcheck() + + helpers.set_hs_speed( + protocol, heatershaker, int(heater_shaker_speed * 0.9), 4.0, True + ) + + if DRYRUN is False: + protocol.delay(seconds=1 * 60) + + protocol.comment("--> Transfer Hybridization") + TransferSup = 200 + for loop, X in enumerate(column_2_list): + p1000.pick_up_tip() + p1000.move_to(sample_plate_2[X].bottom(z=0.5)) + p1000.aspirate(TransferSup, rate=0.25) + p1000.dispense( + TransferSup, sample_plate_2[column_3_list[loop]].bottom(z=1) + ) + p1000.return_tip() if TIP_TRASH is False else p1000.drop_tip() + p200_tips += 1 + tipcheck() + + if DRYRUN is False: + protocol.delay(seconds=5 * 60) + + # GRIPPER MOVE sample_plate_2 FROM heatershaker TO MAGPLATE + helpers.move_labware_from_hs_to_destination( + protocol, sample_plate_2, heatershaker, MAG_PLATE_SLOT + ) + if DRYRUN is False: + protocol.delay(seconds=1 * 60) + + protocol.comment("--> Removing Supernatant") + RemoveSup = 200 + for loop, X in enumerate(column_3_list): + p1000.pick_up_tip() + p1000.move_to(sample_plate_2[X].bottom(z=3.5)) + p1000.aspirate(RemoveSup - 100, rate=0.25) + protocol.delay(minutes=0.1) + p1000.move_to(sample_plate_2[X].bottom(z=0.5)) + p1000.aspirate(100, rate=0.25) + p1000.move_to(sample_plate_2[X].top(z=0.5)) + trash_liquid(protocol, p1000, 100, liquid_trash_list) + p1000.aspirate(20) + p1000.return_tip() if TIP_TRASH is False else p1000.drop_tip() + p200_tips += 1 + tipcheck() + + protocol.comment("--> Removing Residual") + for loop, X in enumerate(column_3_list): + p50.pick_up_tip() + p50.move_to(sample_plate_2[X].bottom(z=dot_bottom)) # original = z=0 + p50.aspirate(50, rate=0.25) + p50.default_speed = 200 + trash_liquid(protocol, p50, 50, liquid_trash_list) + p50.return_tip() if TIP_TRASH is False else p50.drop_tip() + p50_tips += 1 + tipcheck() + + protocol.comment("==============================================") + protocol.comment("--> ELUTE") + protocol.comment("==============================================") + + protocol.comment("--> Adding Elute") + EluteVol = 23 + for loop, X in enumerate(column_3_list): + p50.pick_up_tip() + p50.aspirate(EluteVol, Elute.bottom(z=dot_bottom)) # original = () + p50.dispense( + EluteVol, sample_plate_2[X].bottom(z=dot_bottom) + ) # original = () + p50.return_tip() if TIP_TRASH is False else p50.drop_tip() + p50_tips += 1 + tipcheck() + + # ============================================================================================ + # GRIPPER MOVE sample_plate_2 FROM MAGPLATE TO heatershaker + helpers.move_labware_to_hs( + protocol, sample_plate_2, heatershaker, heatershaker + ) + # ============================================================================================ + helpers.set_hs_speed( + protocol, heatershaker, int(heater_shaker_speed * 0.9), 2.0, True + ) + heatershaker.open_labware_latch() + + if DRYRUN is False: + protocol.delay(minutes=2) + + # ============================================================================================ + # GRIPPER MOVE sample_plate_2 FROM heatershaker TO MAGPLATE + helpers.move_labware_from_hs_to_destination( + protocol, sample_plate_2, heatershaker, MAG_PLATE_SLOT + ) + protocol.comment("--> Transfer Elution") + TransferSup = 21 + for loop, X in enumerate(column_3_list): + p50.pick_up_tip() + p50.move_to(sample_plate_2[X].bottom(z=0.5)) + p50.aspirate(TransferSup + 1, rate=0.25) + p50.dispense( + TransferSup + 1, sample_plate_1[column_4_list[loop]].bottom(z=1) + ) + p50.return_tip() if TIP_TRASH is False else p50.drop_tip() + p50_tips += 1 + tipcheck() + + protocol.comment("--> Adding ET2") + ET2Vol = 4 + ET2MixRep = 10 if DRYRUN is False else 1 + ET2MixVol = 20 + for loop, X in enumerate(column_4_list): + p50.pick_up_tip() + p50.aspirate(ET2Vol, ET2.bottom(z=dot_bottom)) # original = () + p50.dispense( + ET2Vol, sample_plate_1[X].bottom(z=dot_bottom) + ) # original = () + p50.move_to(sample_plate_1[X].bottom(z=dot_bottom)) # original = () + p50.mix(ET2MixRep, ET2MixVol) + p50.return_tip() if TIP_TRASH is False else p50.drop_tip() + p50_tips += 1 + tipcheck() + + if STEP_PCR == 1: + protocol.comment("==============================================") + protocol.comment("--> AMPLIFICATION") + protocol.comment("==============================================") + + protocol.comment("--> Adding PPC") + PPCVol = 5 + for loop, X in enumerate(column_4_list): + p50.pick_up_tip() + p50.aspirate(PPCVol, PPC.bottom(z=dot_bottom)) # original = () + p50.dispense( + PPCVol, sample_plate_1[X].bottom(z=dot_bottom) + ) # original = () + p50.return_tip() if TIP_TRASH is False else p50.drop_tip() + p50_tips += 1 + tipcheck() + + protocol.comment("--> Adding EPM") + EPMVol = 20 + EPMMixRep = 10 if DRYRUN is False else 1 + EPMMixVol = 45 + for loop, X in enumerate(column_4_list): + p50.pick_up_tip() + p50.aspirate(EPMVol, EPM.bottom(z=dot_bottom)) # original = () + p50.dispense( + EPMVol, sample_plate_1[X].bottom(z=dot_bottom) + ) # original = () + p50.move_to(sample_plate_1[X].bottom(z=dot_bottom)) # original = () + p50.mix(EPMMixRep, EPMMixVol) + p50.return_tip() if TIP_TRASH is False else p50.drop_tip() + p50_tips += 1 + tipcheck() + + if DRYRUN is False: + heatershaker.deactivate_heater() + + if STEP_PCRDECK == 1: + if DRYRUN is False: + if DRYRUN is False: + if disposable_lid: + ( + lid_on_plate, + unused_lids, + used_lids, + ) = helpers.use_disposable_lid_with_tc( + protocol, + unused_lids, + used_lids, + sample_plate_1, + thermocycler, + ) + else: + thermocycler.close_lid() + profile_PCR_1: List[ThermocyclerStep] = [ + {"temperature": 98, "hold_time_seconds": 45} + ] + thermocycler.execute_profile( + steps=profile_PCR_1, repetitions=1, block_max_volume=50 + ) + profile_PCR_2: List[ThermocyclerStep] = [ + {"temperature": 98, "hold_time_seconds": 30}, + {"temperature": 60, "hold_time_seconds": 30}, + {"temperature": 72, "hold_time_seconds": 30}, + ] + thermocycler.execute_profile( + steps=profile_PCR_2, repetitions=12, block_max_volume=50 + ) + profile_PCR_3: List[ThermocyclerStep] = [ + {"temperature": 72, "hold_time_minutes": 1} + ] + thermocycler.execute_profile( + steps=profile_PCR_3, repetitions=1, block_max_volume=50 + ) + thermocycler.set_block_temperature(10) + + thermocycler.open_lid() + if disposable_lid: + if trash_lid: + protocol.move_labware(lid_on_plate, trash_bin, use_gripper=True) + elif len(used_lids) <= 1: + protocol.move_labware(lid_on_plate, "B4", use_gripper=True) + else: + protocol.move_labware( + lid_on_plate, used_lids[-2], use_gripper=True + ) + + if STEP_CLEANUP == 1: + protocol.comment("==============================================") + protocol.comment("--> Cleanup") + protocol.comment("==============================================") + + # GRIPPER MOVE sample_plate_2 FROM MAGPLATE TO heatershaker + helpers.move_labware_to_hs( + protocol, sample_plate_2, heatershaker, heatershaker + ) + + protocol.comment("--> Transfer Elution") + TransferSup = 45 + for loop, X in enumerate(column_4_list): + p50.pick_up_tip() + p50.move_to(sample_plate_1[X].bottom(z=0.5)) + p50.aspirate(TransferSup + 1, rate=0.25) + p50.dispense( + TransferSup + 1, sample_plate_2[column_5_list[loop]].bottom(z=1) + ) + p50.return_tip() if TIP_TRASH is False else p50.drop_tip() + p50_tips += 1 + tipcheck() + + protocol.comment("--> ADDING AMPure (0.8x)") + AMPureVol = 40.5 + AMPureMixRep = 5.0 if DRYRUN is False else 0.1 + AMPurePremix = 3 if DRYRUN is False else 1 + # ========NEW SINGLE TIP DISPENSE=========== + for loop, X in enumerate(column_5_list): + p1000.pick_up_tip() + p1000.mix(AMPurePremix, AMPureVol + 10, AMPure.bottom(z=1)) + p1000.aspirate(AMPureVol, AMPure.bottom(z=1), rate=0.25) + p1000.dispense(AMPureVol, sample_plate_2[X].bottom(z=1), rate=0.25) + p1000.default_speed = 5 + p1000.move_to(sample_plate_2[X].bottom(z=5)) + for Mix in range(2): + p1000.aspirate(60, rate=0.5) + p1000.move_to(sample_plate_2[X].bottom(z=1)) + p1000.aspirate(60, rate=0.5) + p1000.dispense(60, rate=0.5) + p1000.move_to(sample_plate_2[X].bottom(z=5)) + p1000.dispense(30, rate=0.5) + Mix += 1 + p1000.blow_out(sample_plate_2[X].top(z=2)) + p1000.default_speed = 400 + p1000.move_to(sample_plate_2[X].top(z=5)) + p1000.move_to(sample_plate_2[X].top(z=0)) + p1000.move_to(sample_plate_2[X].top(z=5)) + p1000.return_tip() if TIP_TRASH is False else p1000.drop_tip() + p200_tips += 1 + tipcheck() + # ========NEW HS MIX========================= + helpers.set_hs_speed( + protocol, + heatershaker, + int(heater_shaker_speed * 0.9), + AMPureMixRep, + True, + ) + + # GRIPPER MOVE PLATE FROM HEATER SHAKER TO MAG PLATE + helpers.move_labware_from_hs_to_destination( + protocol, sample_plate_2, heatershaker, MAG_PLATE_SLOT + ) + + if DRYRUN is False: + protocol.delay(minutes=4) + + protocol.comment("--> Removing Supernatant") + RemoveSup = 200 + for loop, X in enumerate(column_5_list): + p1000.pick_up_tip() + p1000.move_to(sample_plate_2[X].bottom(z=3.5)) + p1000.aspirate(RemoveSup - 100, rate=0.25) + protocol.delay(minutes=0.1) + p1000.move_to(sample_plate_2[X].bottom(z=0.5)) + p1000.aspirate(100, rate=0.25) + p1000.default_speed = 5 + p1000.move_to(sample_plate_2[X].top(z=2)) + p1000.default_speed = 200 + trash_liquid(protocol, p1000, 200, liquid_trash_list) + p1000.return_tip() if TIP_TRASH is False else p1000.drop_tip() + p200_tips += 1 + tipcheck() + + for X_times in range(2): + protocol.comment("--> ETOH Wash") + ETOHMaxVol = 150 + for loop, X in enumerate(column_5_list): + p1000.pick_up_tip() + p1000.aspirate(ETOHMaxVol, EtOH.bottom(z=1)) + p1000.move_to(EtOH.top(z=0)) + p1000.move_to(EtOH.top(z=-5)) + p1000.move_to(EtOH.top(z=0)) + p1000.move_to(sample_plate_2[X].top(z=-2)) + p1000.dispense(ETOHMaxVol, rate=1) + protocol.delay(minutes=0.1) + p1000.blow_out() + p1000.move_to(sample_plate_2[X].top(z=5)) + p1000.move_to(sample_plate_2[X].top(z=0)) + p1000.move_to(sample_plate_2[X].top(z=5)) + p1000.return_tip() if TIP_TRASH is False else p1000.drop_tip() + p200_tips += 1 + tipcheck() + + if DRYRUN is False: + protocol.delay(minutes=0.5) + + protocol.comment("--> Remove ETOH Wash") + for loop, X in enumerate(column_5_list): + p1000.pick_up_tip() + p1000.move_to(sample_plate_2[X].bottom(z=3.5)) + p1000.aspirate(RemoveSup - 100, rate=0.25) + protocol.delay(minutes=0.1) + p1000.move_to(sample_plate_2[X].bottom(z=0.5)) + p1000.aspirate(100, rate=0.25) + p1000.default_speed = 5 + p1000.move_to(sample_plate_2[X].top(z=2)) + p1000.default_speed = 200 + trash_liquid(protocol, p1000, RemoveSup, liquid_trash_list) + p1000.return_tip() if TIP_TRASH is False else p1000.drop_tip() + p200_tips += 1 + tipcheck() + + if DRYRUN is False: + protocol.delay(minutes=2) + + protocol.comment("--> Removing Residual ETOH") + for loop, X in enumerate(column_5_list): + p1000.pick_up_tip() + p1000.move_to( + sample_plate_2[X].bottom(z=dot_bottom) + ) # original = (z=0) + p1000.aspirate(50, rate=0.25) + p1000.default_speed = 200 + trash_liquid(protocol, p1000, 50, liquid_trash_list) + p1000.return_tip() if TIP_TRASH is False else p1000.drop_tip() + p200_tips += 1 + tipcheck() + + if DRYRUN is False: + protocol.delay(minutes=1) + + # GRIPPER MOVE PLATE FROM MAG PLATE TO HEATER SHAKER + helpers.move_labware_to_hs( + protocol, sample_plate_2, heatershaker, heatershaker + ) + + protocol.comment("--> Adding RSB") + RSBVol = 32 + RSBMixRep = 1.0 if DRYRUN is False else 0.1 # minutes + for loop, X in enumerate(column_5_list): + p1000.pick_up_tip() + p1000.aspirate(RSBVol, RSB.bottom(z=1)) + + p1000.move_to( + ( + sample_plate_2.wells_by_name()[X] + .center() + .move(types.Point(x=1.3 * 0.8, y=0, z=-4)) + ) + ) + p1000.dispense(RSBVol, rate=1) + p1000.move_to(sample_plate_2.wells_by_name()[X].bottom(z=1)) + p1000.aspirate(RSBVol, rate=1) + p1000.move_to( + ( + sample_plate_2.wells_by_name()[X] + .center() + .move(types.Point(x=0, y=1.3 * 0.8, z=-4)) + ) + ) + p1000.dispense(RSBVol, rate=1) + p1000.move_to(sample_plate_2.wells_by_name()[X].bottom(z=1)) + p1000.aspirate(RSBVol, rate=1) + p1000.move_to( + ( + sample_plate_2.wells_by_name()[X] + .center() + .move(types.Point(x=1.3 * -0.8, y=0, z=-4)) + ) + ) + p1000.dispense(RSBVol, rate=1) + p1000.move_to(sample_plate_2.wells_by_name()[X].bottom(z=1)) + p1000.aspirate(RSBVol, rate=1) + p1000.move_to( + ( + sample_plate_2.wells_by_name()[X] + .center() + .move(types.Point(x=0, y=1.3 * -0.8, z=-4)) + ) + ) + p1000.dispense(RSBVol, rate=1) + p1000.move_to(sample_plate_2.wells_by_name()[X].bottom(z=1)) + p1000.aspirate(RSBVol, rate=1) + p1000.dispense(RSBVol, rate=1) + + p1000.blow_out(sample_plate_2.wells_by_name()[X].center()) + p1000.move_to(sample_plate_2.wells_by_name()[X].top(z=5)) + p1000.move_to(sample_plate_2.wells_by_name()[X].top(z=0)) + p1000.move_to(sample_plate_2.wells_by_name()[X].top(z=5)) + p1000.return_tip() if TIP_TRASH is False else p1000.drop_tip() + p200_tips += 1 + tipcheck() + if DRYRUN is False: + helpers.set_hs_speed( + protocol, + heatershaker, + int(heater_shaker_speed * 0.8), + RSBMixRep, + True, + ) + + # GRIPPER MOVE PLATE FROM HEATER SHAKER TO MAG PLATE + helpers.move_labware_from_hs_to_destination( + protocol, sample_plate_2, heatershaker, MAG_PLATE_SLOT + ) + + if DRYRUN is False: + protocol.delay(minutes=3) + + protocol.comment("--> Transferring Supernatant") + TransferSup = 30 + for loop, X in enumerate(column_5_list): + p1000.pick_up_tip() + p1000.move_to(sample_plate_2[X].bottom(z=0.5)) + p1000.aspirate(TransferSup + 1, rate=0.25) + p1000.dispense( + TransferSup + 1, sample_plate_1[column_6_list[loop]].bottom(z=1) + ) + p1000.return_tip() if TIP_TRASH is False else p1000.drop_tip() + p200_tips += 1 + tipcheck() + liquids_to_probe_at_end = [ + Liquid_trash_well_1, + Liquid_trash_well_2, + Liquid_trash_well_3, + Liquid_trash_well_4, + ] + helpers.find_liquid_height_of_all_wells(protocol, p50, liquids_to_probe_at_end) + if deactivate_modules_bool: + helpers.deactivate_modules(protocol) diff --git a/abr-testing/abr_testing/protocols/active_protocols/5_96ch complex protocol with single tip Pick Up.py b/abr-testing/abr_testing/protocols/active_protocols/5_96ch complex protocol with single tip Pick Up.py new file mode 100644 index 00000000000..ca7506cf6f0 --- /dev/null +++ b/abr-testing/abr_testing/protocols/active_protocols/5_96ch complex protocol with single tip Pick Up.py @@ -0,0 +1,418 @@ +"""96 ch Test Single Tip and Gripper Moves.""" +from opentrons.protocol_api import ( + COLUMN, + SINGLE, + ALL, + ParameterContext, + ProtocolContext, + Labware, +) +from opentrons.protocol_api.module_contexts import ( + HeaterShakerContext, + ThermocyclerContext, + TemperatureModuleContext, +) +from abr_testing.protocols import helpers +from typing import List + +metadata = { + "protocolName": "96ch protocol with modules gripper moves and SINGLE tip pickup", + "author": "Derek Maggio ", +} + +requirements = { + "robotType": "Flex", + "apiLevel": "2.21", +} + + +# prefer to move off deck, instead of waste chute disposal, if possible +PREFER_MOVE_OFF_DECK = False + + +PCR_PLATE_96_NAME = "armadillo_96_wellplate_200ul_pcr_full_skirt" +RESERVOIR_NAME = "nest_96_wellplate_2ml_deep" +TIPRACK_96_ADAPTER_NAME = "opentrons_flex_96_tiprack_adapter" +PIPETTE_96_CHANNEL_NAME = "flex_96channel_1000" + +USING_GRIPPER = True +RESET_AFTER_EACH_MOVE = True + + +def add_parameters(parameters: ParameterContext) -> None: + """Parameters.""" + helpers.create_tip_size_parameter(parameters) + helpers.create_dot_bottom_parameter(parameters) + helpers.create_disposable_lid_parameter(parameters) + helpers.create_tc_lid_deck_riser_parameter(parameters) + helpers.create_deactivate_modules_parameter(parameters) + + +def run(protocol: ProtocolContext) -> None: + """Protocol.""" + b = protocol.params.dot_bottom # type: ignore[attr-defined] + TIPRACK_96_NAME = protocol.params.tip_size # type: ignore[attr-defined] + disposable_lid = protocol.params.disposable_lid # type: ignore[attr-defined] + deck_riser = protocol.params.deck_riser # type: ignore[attr-defined] + deactivate_modules_bool = protocol.params.deactivate_modules # type: ignore[attr-defined] + + waste_chute = protocol.load_waste_chute() + helpers.comment_protocol_version(protocol, "01") + + thermocycler: ThermocyclerContext = protocol.load_module( + helpers.tc_str + ) # type: ignore[assignment] + h_s: HeaterShakerContext = protocol.load_module( + helpers.hs_str, "D1" + ) # type: ignore[assignment] + temperature_module: TemperatureModuleContext = protocol.load_module( + helpers.temp_str, "C1" + ) # type: ignore[assignment] + if disposable_lid: + unused_lids = helpers.load_disposable_lids(protocol, 3, ["A2"], deck_riser) + used_lids: List[Labware] = [] + thermocycler.open_lid() + h_s.open_labware_latch() + + temperature_module_adapter = temperature_module.load_adapter( + "opentrons_96_well_aluminum_block" + ) + h_s_adapter = h_s.load_adapter("opentrons_96_pcr_adapter") + + adapters = [temperature_module_adapter, h_s_adapter] + + source_reservoir = protocol.load_labware(RESERVOIR_NAME, "D2") + dest_pcr_plate = protocol.load_labware(PCR_PLATE_96_NAME, "C2") + liquid_waste = protocol.load_labware("nest_1_reservoir_195ml", "B2", "Liquid Waste") + + tip_rack_1 = protocol.load_labware( + TIPRACK_96_NAME, "A3", adapter="opentrons_flex_96_tiprack_adapter" + ) + tip_rack_2 = protocol.load_labware(TIPRACK_96_NAME, "C3") + tip_rack_3 = protocol.load_labware(TIPRACK_96_NAME, "C4") + + tip_racks = [ + tip_rack_1, + tip_rack_2, + tip_rack_3, + ] + + pipette_96_channel = protocol.load_instrument( + PIPETTE_96_CHANNEL_NAME, + mount="left", + tip_racks=tip_racks, + liquid_presence_detection=True, + ) + + water = protocol.define_liquid( + name="water", description="H₂O", display_color="#42AB2D" + ) + source_reservoir.wells_by_name()["A1"].load_liquid(liquid=water, volume=29000) + + def run_moves( + labware: Labware, move_sequences: List, reset_location: str, use_gripper: bool + ) -> None: + """Perform a series of moves for a given labware using specified move sequences. + + Will perform 2 versions of the moves: + 1.Moves to each location, resetting to the reset location after each move. + 2.Moves to each location, resetting to the reset location after all moves. + """ + + def move_to_locations( + labware_to_move: Labware, + move_locations: List, + reset_after_each_move: bool, + use_gripper: bool, + reset_location: str, + ) -> None: + """Move labware to specific destinations.""" + + def reset_labware() -> None: + """Reset the labware to the reset location.""" + protocol.move_labware( + labware_to_move, reset_location, use_gripper=use_gripper + ) + + if len(move_locations) == 0: + return + + for location in move_locations: + protocol.move_labware( + labware_to_move, location, use_gripper=use_gripper + ) + + if reset_after_each_move: + reset_labware() + + if not reset_after_each_move: + reset_labware() + + for move_sequence in move_sequences: + move_to_locations( + labware, + move_sequence, + RESET_AFTER_EACH_MOVE, + use_gripper, + reset_location, + ) + move_to_locations( + labware, + move_sequence, + not RESET_AFTER_EACH_MOVE, + use_gripper, + reset_location, + ) + + def test_gripper_moves() -> None: + """Function to test the movement of the gripper in various locations.""" + + def deck_moves(labware: Labware, reset_location: str) -> None: + """Function to perform the movement of labware.""" + deck_move_sequence = [ + ["B3"], # Deck Moves + ["C3"], # Staging Area Slot 3 Moves + ["C4", "D4"], # Staging Area Slot 4 Moves + [ + thermocycler, + temperature_module_adapter, + h_s_adapter, + ], # Module Moves + ] + + run_moves(labware, deck_move_sequence, reset_location, USING_GRIPPER) + + def staging_area_slot_3_moves(labware: Labware, reset_location: str) -> None: + """Function to perform the movement of labware, starting w/ staging area slot 3.""" + staging_area_slot_3_move_sequence = [ + ["B3", "C2"], # Deck Moves + [], # Don't have Staging Area Slot 3 open + ["C4", "D4"], # Staging Area Slot 4 Moves + [ + thermocycler, + temperature_module_adapter, + h_s_adapter, + ], # Module Moves + ] + + run_moves( + labware, + staging_area_slot_3_move_sequence, + reset_location, + USING_GRIPPER, + ) + + def staging_area_slot_4_moves(labware: Labware, reset_location: str) -> None: + """Function to perform the movement of labware, starting with staging area slot 4.""" + staging_area_slot_4_move_sequence = [ + ["C2", "B3"], # Deck Moves + ["C3"], # Staging Area Slot 3 Moves + ["C4"], # Staging Area Slot 4 Moves + [ + thermocycler, + temperature_module_adapter, + h_s_adapter, + ], # Module Moves + ] + + run_moves( + labware, + staging_area_slot_4_move_sequence, + reset_location, + USING_GRIPPER, + ) + + def module_moves(labware: Labware, module_locations: List) -> None: + """Function to perform the movement of labware, starting on a module.""" + module_move_sequence = [ + ["C2", "B3"], # Deck Moves + ["C3"], # Staging Area Slot 3 Moves + ["C4", "D4"], # Staging Area Slot 4 Moves + ] + + for module_starting_location in module_locations: + labware_move_to_locations = module_locations.copy() + labware_move_to_locations.remove(module_starting_location) + all_sequences = module_move_sequence.copy() + all_sequences.append(labware_move_to_locations) + protocol.move_labware( + labware, module_starting_location, use_gripper=USING_GRIPPER + ) + run_moves( + labware, all_sequences, module_starting_location, USING_GRIPPER + ) + + DECK_MOVE_RESET_LOCATION = "C2" + STAGING_AREA_SLOT_3_RESET_LOCATION = "C3" + STAGING_AREA_SLOT_4_RESET_LOCATION = "D4" + + deck_moves(dest_pcr_plate, DECK_MOVE_RESET_LOCATION) + + protocol.move_labware( + dest_pcr_plate, + STAGING_AREA_SLOT_3_RESET_LOCATION, + use_gripper=USING_GRIPPER, + ) + staging_area_slot_3_moves(dest_pcr_plate, STAGING_AREA_SLOT_3_RESET_LOCATION) + + protocol.move_labware( + dest_pcr_plate, + STAGING_AREA_SLOT_4_RESET_LOCATION, + use_gripper=USING_GRIPPER, + ) + staging_area_slot_4_moves(dest_pcr_plate, STAGING_AREA_SLOT_4_RESET_LOCATION) + + module_locations = [thermocycler] + adapters + module_moves(dest_pcr_plate, module_locations) + protocol.move_labware(dest_pcr_plate, thermocycler, use_gripper=USING_GRIPPER) + + def test_manual_moves() -> None: + """Test manual moves.""" + protocol.move_labware(source_reservoir, "D4", use_gripper=USING_GRIPPER) + + def test_pipetting() -> None: + """Test pipetting.""" + + def test_single_tip_pickup_usage() -> None: + """Test Single Tip Pick Up.""" + pipette_96_channel.configure_nozzle_layout(style=SINGLE, start="H12") + pipette_96_channel.liquid_presence_detection = True + tip_count = 0 # Tip counter to ensure proper tip usage + rows = ["A", "B", "C", "D", "E", "F", "G", "H"] # 8 rows + columns = range(1, 13) # 12 columns + for row in rows: + for col in columns: + well_position = f"{row}{col}" + pipette_96_channel.pick_up_tip(tip_rack_2) + + pipette_96_channel.aspirate(45, source_reservoir[well_position]) + pipette_96_channel.air_gap(5) + + pipette_96_channel.dispense( + 25, dest_pcr_plate[well_position].bottom(b) + ) + pipette_96_channel.blow_out(location=liquid_waste["A1"]) + pipette_96_channel.drop_tip() + tip_count += 1 + # leave this dropping in waste chute, do not use get_disposal_preference + # want to test partial drop + protocol.move_labware(tip_rack_2, waste_chute, use_gripper=USING_GRIPPER) + + def test_column_tip_rack_usage() -> None: + """Column Tip Pick Up.""" + list_of_columns = list(range(1, 13)) + pipette_96_channel.configure_nozzle_layout( + style=COLUMN, start="A12", tip_racks=[tip_rack_3] + ) + protocol.comment("------------------------------") + protocol.comment(f"channels {pipette_96_channel.active_channels}") + protocol.move_labware(tip_rack_3, "C3", use_gripper=USING_GRIPPER) + for well in list_of_columns: + tiprack_well = "A" + str(well) + well_name = "A" + str(well) + pipette_96_channel.liquid_presence_detection = True + pipette_96_channel.pick_up_tip(tip_rack_3[tiprack_well]) + pipette_96_channel.aspirate(45, source_reservoir[well_name]) + pipette_96_channel.liquid_presence_detection = False + pipette_96_channel.air_gap(5) + pipette_96_channel.dispense(25, dest_pcr_plate[tiprack_well].bottom(b)) + pipette_96_channel.blow_out(location=liquid_waste["A1"]) + pipette_96_channel.drop_tip() + protocol.move_labware(tip_rack_3, waste_chute, use_gripper=USING_GRIPPER) + + def test_full_tip_rack_usage() -> None: + """Full Tip Pick Up.""" + pipette_96_channel.configure_nozzle_layout( + style=ALL, tip_racks=[tip_rack_1] + ) + protocol.comment(f"channels {pipette_96_channel.active_channels}") + pipette_96_channel.liquid_presence_detection = True + pipette_96_channel.pick_up_tip() + pipette_96_channel.aspirate(45, source_reservoir["A1"]) + pipette_96_channel.liquid_presence_detection = False + pipette_96_channel.air_gap(5) + pipette_96_channel.dispense(25, dest_pcr_plate["A1"].bottom(b)) + pipette_96_channel.blow_out(location=liquid_waste["A1"]) + pipette_96_channel.return_tip() + pipette_96_channel.reset_tipracks() + + test_single_tip_pickup_usage() + test_column_tip_rack_usage() + test_full_tip_rack_usage() + + def test_module_usage(unused_lids: List[Labware], used_lids: List[Labware]) -> None: + """Test Module Use.""" + + def test_thermocycler( + unused_lids: List[Labware], used_lids: List[Labware] + ) -> None: + if disposable_lid: + ( + lid_on_plate, + unused_lids, + used_lids, + ) = helpers.use_disposable_lid_with_tc( + protocol, unused_lids, used_lids, dest_pcr_plate, thermocycler + ) + thermocycler.set_block_temperature(4) + thermocycler.set_lid_temperature(105) + # Close lid + thermocycler.close_lid() + helpers.perform_pcr( + protocol, + thermocycler, + initial_denature_time_sec=45, + denaturation_time_sec=30, + anneal_time_sec=30, + extension_time_sec=10, + cycle_repetitions=30, + final_extension_time_min=5, + ) + # Cool to 4° + thermocycler.set_block_temperature(4) + thermocycler.set_lid_temperature(105) + # Open lid + thermocycler.open_lid() + if disposable_lid: + if len(used_lids) <= 1: + protocol.move_labware(lid_on_plate, waste_chute, use_gripper=True) + else: + protocol.move_labware(lid_on_plate, used_lids[-2], use_gripper=True) + thermocycler.deactivate() + + def test_h_s() -> None: + """Tests heatershaker.""" + h_s.open_labware_latch() + h_s.close_labware_latch() + + h_s.set_target_temperature(75.0) + h_s.set_and_wait_for_shake_speed(1000) + h_s.wait_for_temperature() + + h_s.deactivate_heater() + h_s.deactivate_shaker() + + def test_temperature_module() -> None: + """Tests temperature module.""" + temperature_module.set_temperature(80) + temperature_module.set_temperature(10) + temperature_module.deactivate() + + test_thermocycler(unused_lids, used_lids) + test_h_s() + test_temperature_module() + + test_pipetting() + test_gripper_moves() + test_module_usage(unused_lids, used_lids) + test_manual_moves() + protocol.move_labware(source_reservoir, "C2", use_gripper=True) + helpers.clean_up_plates( + pipette_96_channel, [dest_pcr_plate, source_reservoir], liquid_waste["A1"], 50 + ) + pipette_96_channel.reset_tipracks() + helpers.find_liquid_height_of_all_wells( + protocol, pipette_96_channel, [liquid_waste["A1"]] + ) + if deactivate_modules_bool: + helpers.deactivate_modules(protocol) diff --git a/abr-testing/abr_testing/protocols/active_protocols/6_Omega_HDQ_DNA_Cells-Flex_96_channel.py b/abr-testing/abr_testing/protocols/active_protocols/6_Omega_HDQ_DNA_Cells-Flex_96_channel.py new file mode 100644 index 00000000000..894f80dcdea --- /dev/null +++ b/abr-testing/abr_testing/protocols/active_protocols/6_Omega_HDQ_DNA_Cells-Flex_96_channel.py @@ -0,0 +1,434 @@ +"""Omega Bio-tek Mag-Bind Blood & Tissue DNA HDQ - Bacteria.""" +from typing import List, Dict +from abr_testing.protocols import helpers +from opentrons.protocol_api import ( + ProtocolContext, + ParameterContext, + Well, + InstrumentContext, +) +from opentrons.protocol_api.module_contexts import ( + HeaterShakerContext, + MagneticBlockContext, + TemperatureModuleContext, +) +from opentrons import types +import numpy as np + +metadata = { + "author": "Zach Galluzzo ", + "protocolName": "Omega Bio-tek Mag-Bind Blood & Tissue DNA HDQ - Bacteria", +} + +requirements = { + "robotType": "Flex", + "apiLevel": "2.21", +} + + +def add_parameters(parameters: ParameterContext) -> None: + """Parameters.""" + helpers.create_dot_bottom_parameter(parameters) + helpers.create_deactivate_modules_parameter(parameters) + parameters.add_int( + variable_name="number_of_runs", + display_name="Number of Runs", + default=2, + minimum=1, + maximum=4, + ) + + +# Start protocol +def run(protocol: ProtocolContext) -> None: + """Protocol.""" + dot_bottom = protocol.params.dot_bottom # type: ignore[attr-defined] + deactivate_modules = protocol.params.deactivate_modules # type: ignore[attr-defined] + number_of_runs = protocol.params.number_of_runs # type: ignore[attr-defined] + dry_run = False + tip_mixing = False + + wash_vol = 600.0 + AL_vol = 230.0 + bind_vol = 300.0 + sample_vol = 180.0 + elution_vol = 100.0 + helpers.comment_protocol_version(protocol, "01") + # Same for all HDQ Extractions + deepwell_type = "nest_96_wellplate_2ml_deep" + if not dry_run: + settling_time = 2.0 + num_washes = 3 + if dry_run: + settling_time = 0.5 + num_washes = 1 + bead_vol = PK_vol = 20.0 + inc_temp = 55.0 + AL_total_vol = AL_vol + PK_vol + binding_buffer_vol = bead_vol + bind_vol + starting_vol = AL_total_vol + sample_vol + + h_s: HeaterShakerContext = protocol.load_module( + helpers.hs_str, "D1" + ) # type: ignore[assignment] + sample_plate, h_s_adapter = helpers.load_hs_adapter_and_labware( + deepwell_type, h_s, "Sample Plate" + ) + h_s.close_labware_latch() + samples_m = sample_plate.wells()[0] + + temp: TemperatureModuleContext = protocol.load_module( + helpers.temp_str, "A3" + ) # type: ignore[assignment] + elutionplate, tempblock = helpers.load_temp_adapter_and_labware( + "armadillo_96_wellplate_200ul_pcr_full_skirt", temp, "Elution Plate/Reservoir" + ) + + magblock: MagneticBlockContext = protocol.load_module( + "magneticBlockV1", "C1" + ) # type: ignore[assignment] + liquid_waste = protocol.load_labware("nest_1_reservoir_195ml", "B3", "Liquid Waste") + waste = liquid_waste.wells()[0].top() + + lysis_reservoir = protocol.load_labware(deepwell_type, "D2", "Lysis reservoir") + lysis_res = lysis_reservoir.wells()[0] + bind_reservoir = protocol.load_labware( + deepwell_type, "C2", "Beads and binding reservoir" + ) + bind_res = bind_reservoir.wells()[0] + wash1_reservoir = protocol.load_labware(deepwell_type, "C3", "Wash 1 reservoir") + wash1_res = wash1_reservoir.wells()[0] + wash2_reservoir = protocol.load_labware(deepwell_type, "B1", "Wash 2 reservoir") + wash2_res = wash2_reservoir.wells()[0] + elution_res = elutionplate.wells()[0] + # Load Pipette and tip racks + # Load tips + tiprack_1 = protocol.load_labware( + "opentrons_flex_96_tiprack_1000ul", + "A1", + adapter="opentrons_flex_96_tiprack_adapter", + ) + tips = tiprack_1.wells()[0] + + tiprack_2 = protocol.load_labware( + "opentrons_flex_96_tiprack_1000ul", + "A2", + adapter="opentrons_flex_96_tiprack_adapter", + ) + tips1 = tiprack_2.wells()[0] + # load 96 channel pipette + pip: InstrumentContext = protocol.load_instrument( + "flex_96channel_1000", mount="left", tip_racks=[tiprack_1, tiprack_2] + ) + # Load Liquids and probe + liquid_vols_and_wells: Dict[str, List[Dict[str, Well | List[Well] | float]]] = { + "Lysis Buffer": [{"well": lysis_reservoir.wells(), "volume": AL_vol + 92.0}], + "PK Buffer": [{"well": lysis_reservoir.wells(), "volume": PK_vol + 8.0}], + "Binding Buffer": [{"well": bind_reservoir.wells(), "volume": bind_vol + 91.5}], + "Magnetic Beads": [{"well": bind_reservoir.wells(), "volume": bead_vol + 8.5}], + "Wash 1 and 2 Buffer": [ + {"well": wash1_reservoir.wells(), "volume": (wash_vol * 2.0) + 100.0} + ], + "Wash 3 Buffer": [ + {"well": wash2_reservoir.wells(), "volume": wash_vol + 100.0} + ], + "Elution Buffer": [{"well": elutionplate.wells(), "volume": elution_vol + 5}], + "Samples": [{"well": sample_plate.wells(), "volume": sample_vol}], + } + + helpers.find_liquid_height_of_loaded_liquids(protocol, liquid_vols_and_wells, pip) + + pip.flow_rate.aspirate = 50 + pip.flow_rate.dispense = 150 + pip.flow_rate.blow_out = 300 + + def resuspend_pellet(vol: float, plate: Well, reps: int = 3) -> None: + """Re-suspend pellets.""" + pip.flow_rate.aspirate = 200 + pip.flow_rate.dispense = 300 + + loc1 = plate.bottom().move(types.Point(x=1, y=0, z=1)) + loc2 = plate.bottom().move(types.Point(x=0.75, y=0.75, z=1)) + loc3 = plate.bottom().move(types.Point(x=0, y=1, z=1)) + loc4 = plate.bottom().move(types.Point(x=-0.75, y=0.75, z=1)) + loc5 = plate.bottom().move(types.Point(x=-1, y=0, z=1)) + loc6 = plate.bottom().move(types.Point(x=-0.75, y=0 - 0.75, z=1)) + loc7 = plate.bottom().move(types.Point(x=0, y=-1, z=1)) + loc8 = plate.bottom().move(types.Point(x=0.75, y=-0.75, z=1)) + + if vol > 1000: + vol = 1000 + + mixvol = vol * 0.9 + + for _ in range(reps): + pip.aspirate(mixvol, loc1) + pip.dispense(mixvol, loc1) + pip.aspirate(mixvol, loc2) + pip.dispense(mixvol, loc2) + pip.aspirate(mixvol, loc3) + pip.dispense(mixvol, loc3) + pip.aspirate(mixvol, loc4) + pip.dispense(mixvol, loc4) + pip.aspirate(mixvol, loc5) + pip.dispense(mixvol, loc5) + pip.aspirate(mixvol, loc6) + pip.dispense(mixvol, loc6) + pip.aspirate(mixvol, loc7) + pip.dispense(mixvol, loc7) + pip.aspirate(mixvol, loc8) + pip.dispense(mixvol, loc8) + if _ == reps - 1: + pip.flow_rate.aspirate = 50 + pip.flow_rate.dispense = 30 + pip.aspirate(mixvol, loc8) + pip.dispense(mixvol, loc8) + + pip.flow_rate.aspirate = 150 + pip.flow_rate.dispense = 200 + + def bead_mix(vol: float, plate: Well, reps: int = 5) -> None: + """Bead mix.""" + pip.flow_rate.aspirate = 200 + pip.flow_rate.dispense = 300 + + loc1 = plate.bottom().move(types.Point(x=0, y=0, z=1)) + loc2 = plate.bottom().move(types.Point(x=0, y=0, z=8)) + loc3 = plate.bottom().move(types.Point(x=0, y=0, z=16)) + loc4 = plate.bottom().move(types.Point(x=0, y=0, z=24)) + + if vol > 1000: + vol = 1000 + + mixvol = vol * 0.9 + + for _ in range(reps): + pip.aspirate(mixvol, loc1) + pip.dispense(mixvol, loc1) + pip.aspirate(mixvol, loc1) + pip.dispense(mixvol, loc2) + pip.aspirate(mixvol, loc1) + pip.dispense(mixvol, loc3) + pip.aspirate(mixvol, loc1) + pip.dispense(mixvol, loc4) + if _ == reps - 1: + pip.flow_rate.aspirate = 50 + pip.flow_rate.dispense = 30 + pip.aspirate(mixvol, loc1) + pip.dispense(mixvol, loc1) + + pip.flow_rate.aspirate = 150 + pip.flow_rate.dispense = 200 + + def protocol_function() -> None: + # Start Protocol + temp.set_temperature(inc_temp) + # Transfer and mix lysis + pip.pick_up_tip(tips) + pip.aspirate(AL_total_vol, lysis_res) + pip.dispense(AL_total_vol, samples_m) + resuspend_pellet(200, samples_m, reps=4 if not dry_run else 1) + if not tip_mixing: + pip.return_tip() + + # Mix, then heat + protocol.comment("Lysis Mixing") + helpers.set_hs_speed(protocol, h_s, 1800, 10, False) + if not dry_run: + h_s.set_and_wait_for_temperature(55) + protocol.delay( + minutes=10 if not dry_run else 0.25, + msg="Please allow another 10 minutes of 55C incubation to complete lysis.", + ) + h_s.deactivate_shaker() + + # Transfer and mix bind&beads + pip.pick_up_tip(tips) + bead_mix(binding_buffer_vol, bind_res, reps=4 if not dry_run else 1) + pip.aspirate(binding_buffer_vol, bind_res) + pip.dispense(binding_buffer_vol, samples_m) + bead_mix( + binding_buffer_vol + starting_vol, samples_m, reps=4 if not dry_run else 1 + ) + if not tip_mixing: + pip.return_tip() + pip.home() + + # Shake for binding incubation + protocol.comment("Binding incubation") + helpers.set_hs_speed(protocol, h_s, 1800, 10, True) + + # Transfer plate to magnet + helpers.move_labware_from_hs_to_destination( + protocol, sample_plate, h_s, magblock + ) + + protocol.delay( + minutes=settling_time, + msg="Please wait " + str(settling_time) + " minute(s) for beads to pellet.", + ) + + # Remove Supernatant and move off magnet + pip.pick_up_tip(tips) + pip.aspirate(550, samples_m.bottom(dot_bottom)) + pip.dispense(550, waste) + if starting_vol + binding_buffer_vol > 1000: + pip.aspirate(550, samples_m.bottom(dot_bottom)) + pip.dispense(550, waste) + pip.return_tip() + + # Transfer plate from magnet to H/S + helpers.move_labware_to_hs(protocol, sample_plate, h_s, h_s_adapter) + + # Washes + for i in range(num_washes if not dry_run else 1): + if i == 0 or i == 1: + wash_res = wash1_res + else: + wash_res = wash2_res + + pip.pick_up_tip(tips) + pip.aspirate(wash_vol, wash_res) + pip.dispense(wash_vol, samples_m) + if not tip_mixing: + pip.return_tip() + helpers.set_hs_speed(protocol, h_s, 1800, 5, True) + + # Transfer plate to magnet + helpers.move_labware_from_hs_to_destination( + protocol, sample_plate, h_s, magblock + ) + + protocol.delay( + minutes=settling_time, + msg="Please wait " + + str(settling_time) + + " minute(s) for beads to pellet.", + ) + + # Remove Supernatant and move off magnet + pip.pick_up_tip(tips) + pip.aspirate(473, samples_m.bottom(dot_bottom)) + pip.dispense(473, bind_res.top()) + if wash_vol > 1000: + pip.aspirate(473, samples_m.bottom(dot_bottom)) + pip.dispense(473, bind_res.top()) + pip.return_tip() + + # Transfer plate from magnet to H/S + helpers.move_labware_to_hs(protocol, sample_plate, h_s, h_s_adapter) + + # Dry beads + if dry_run: + drybeads = 0.5 + else: + drybeads = 10 + # Number of minutes you want to dry for + for beaddry in np.arange(drybeads, 0, -0.5): + protocol.delay( + minutes=0.5, + msg="There are " + str(beaddry) + " minutes left in the drying step.", + ) + + # Elution + pip.pick_up_tip(tips1) + pip.aspirate(elution_vol, elution_res) + pip.dispense(elution_vol, samples_m) + resuspend_pellet(elution_vol, samples_m, reps=3 if not dry_run else 1) + if not tip_mixing: + pip.return_tip() + pip.home() + + helpers.set_hs_speed(protocol, h_s, 2000, 5, True) + + # Transfer plate to magnet + helpers.move_labware_from_hs_to_destination( + protocol, sample_plate, h_s, magblock + ) + + protocol.delay( + minutes=settling_time, + msg="Please wait " + str(settling_time) + " minute(s) for beads to pellet.", + ) + + pip.pick_up_tip(tips1) + pip.aspirate(elution_vol, samples_m) + pip.dispense(elution_vol, elutionplate.wells()[0]) + pip.return_tip() + + pip.home() + pip.reset_tipracks() + + # Empty Plates + pip.pick_up_tip() + pip.aspirate(500, samples_m) + pip.dispense(500, liquid_waste["A1"].top()) + pip.aspirate(500, wash1_res) + pip.dispense(500, liquid_waste["A1"].top()) + pip.aspirate(500, wash2_res) + pip.dispense(500, liquid_waste["A1"].top()) + pip.return_tip() + helpers.find_liquid_height_of_all_wells(protocol, pip, [liquid_waste["A1"]]) + helpers.move_labware_to_hs(protocol, sample_plate, h_s, h_s_adapter) + + def setup() -> None: + pip.pick_up_tip() + pip.transfer( + volume=250, + source=liquid_waste["A1"].bottom(z=2), + dest=lysis_reservoir["A1"], + blow_out=True, + blowout_location="source well", + new_tip="never", + trash=False, + ) + pip.transfer( + 1700, + liquid_waste["A1"].bottom(z=2), + wash1_reservoir["A1"], + blow_out=True, + blowout_location="source well", + new_tip="never", + trash=False, + ) + pip.transfer( + 1100, + bind_reservoir["A1"].bottom(z=2), + wash2_reservoir["A1"], + blow_out=True, + blowout_location="source well", + new_tip="never", + trash=False, + ) + pip.transfer( + 100, + liquid_waste["A1"].bottom(z=2), + sample_plate["A1"], + blow_out=True, + blowout_location="source well", + new_tip="never", + trash=False, + ) + pip.return_tip() + + def clean() -> None: + plates_to_clean = [ + sample_plate, + elutionplate, + wash2_reservoir, + wash1_reservoir, + liquid_waste, + ] + helpers.clean_up_plates(pip, plates_to_clean, liquid_waste["A1"], 1000) + + for i in range(number_of_runs): + protocol_function() + + pip.reset_tipracks() + if i < number_of_runs - 1: + setup() + pip.reset_tipracks() + clean() + if deactivate_modules: + helpers.deactivate_modules(protocol) + helpers.find_liquid_height_of_all_wells(protocol, pip, [liquid_waste["A1"]]) diff --git a/abr-testing/abr_testing/protocols/active_protocols/7_HDQ_DNA_Bacteria_Flex.py b/abr-testing/abr_testing/protocols/active_protocols/7_HDQ_DNA_Bacteria_Flex.py new file mode 100644 index 00000000000..4350888b0d6 --- /dev/null +++ b/abr-testing/abr_testing/protocols/active_protocols/7_HDQ_DNA_Bacteria_Flex.py @@ -0,0 +1,527 @@ +"""Omega HDQ DNA Extraction: Bacteria - Tissue Protocol.""" +from abr_testing.protocols import helpers +import math +from opentrons import types +from opentrons.protocol_api import ( + ProtocolContext, + Well, + ParameterContext, + InstrumentContext, +) +import numpy as np +from opentrons.protocol_api.module_contexts import ( + HeaterShakerContext, + TemperatureModuleContext, + MagneticBlockContext, +) +from typing import List, Dict + +metadata = { + "author": "Zach Galluzzo ", + "protocolName": "Omega HDQ DNA Extraction: Bacteria- Tissue Protocol", +} + +requirements = { + "robotType": "Flex", + "apiLevel": "2.21", +} +""" +Slot A1: Tips 1000 +Slot A2: Tips 1000 +Slot A3: Temperature module (gen2) with 96 well PCR block and Armadillo 96 well PCR Plate +Slot B1: Tips 1000 +Slot B2: +Slot B3: Nest 1 Well Reservoir +Slot C1: Magblock +Slot C2: +Slot C3: +Slot D1: H-S with Nest 96 Well Deep well and DW Adapter +Slot D2: Nest 12 well 15 ml Reservoir +Slot D3: Trash + +Reservoir 1: +Wells 1-2 - 9,900 ul +Well 3 - 14,310 ul +Wells 4-12 - 11,400 ul +""" + +whichwash = 1 +sample_max = 48 +tip1k = 0 +drop_count = 0 +waste_vol = 0 + + +def add_parameters(parameters: ParameterContext) -> None: + """Define Parameters.""" + helpers.create_single_pipette_mount_parameter(parameters) + helpers.create_hs_speed_parameter(parameters) + helpers.create_dot_bottom_parameter(parameters) + helpers.create_deactivate_modules_parameter(parameters) + + +def run(protocol: ProtocolContext) -> None: + """Protocol.""" + heater_shaker_speed = protocol.params.heater_shaker_speed # type: ignore[attr-defined] + mount = protocol.params.pipette_mount # type: ignore[attr-defined] + dot_bottom = protocol.params.dot_bottom # type: ignore[attr-defined] + deactivate_modules_bool = protocol.params.deactivate_modules # type: ignore[attr-defined] + helpers.comment_protocol_version(protocol, "01") + + dry_run = False + TIP_TRASH = False + res_type = "nest_12_reservoir_22ml" + + num_samples = 96 + wash1_vol = 600.0 + wash2_vol = 600.0 + wash3_vol = 600.0 + AL_vol = 230.0 + sample_vol = 180.0 + bind_vol = 320.0 + elution_vol = 100.0 + + # Protocol Parameters + deepwell_type = "nest_96_wellplate_2ml_deep" + res_type = "nest_12_reservoir_15ml" + if not dry_run: + settling_time = 2.0 + A_lysis_time_1 = 15.0 + A_lysis_time_2 = 10.0 + bind_time = 10.0 + elute_wash_time = 5.0 + else: + settling_time = ( + elute_wash_time + ) = A_lysis_time_1 = A_lysis_time_2 = bind_time = 0.25 + PK_vol = bead_vol = 20 + AL_total_vol = AL_vol + PK_vol + starting_vol = AL_vol + sample_vol + binding_buffer_vol = bind_vol + bead_vol + + protocol.load_trash_bin("A3") + h_s: HeaterShakerContext = protocol.load_module( + helpers.hs_str, "D1" + ) # type: ignore[assignment] + sample_plate, h_s_adapter = helpers.load_hs_adapter_and_labware( + deepwell_type, h_s, "Sample Plate" + ) + h_s.close_labware_latch() + temp: TemperatureModuleContext = protocol.load_module( + helpers.temp_str, "D3" + ) # type: ignore[assignment] + elutionplate, temp_adapter = helpers.load_temp_adapter_and_labware( + "armadillo_96_wellplate_200ul_pcr_full_skirt", temp, "Elution Plate" + ) + magnetic_block: MagneticBlockContext = protocol.load_module( + helpers.mag_str, "C1" + ) # type: ignore[assignment] + waste_reservoir = protocol.load_labware( + "nest_1_reservoir_195ml", "B3", "Liquid Waste" + ) + waste = waste_reservoir.wells()[0].top() + + res1 = protocol.load_labware(res_type, "D2", "Reagent Reservoir 1") + num_cols = math.ceil(num_samples / 8) + # Load tips and combine all similar boxes + tips1000 = protocol.load_labware("opentrons_flex_96_tiprack_1000ul", "A1", "Tips 1") + tips1001 = protocol.load_labware("opentrons_flex_96_tiprack_1000ul", "A2", "Tips 2") + tips1002 = protocol.load_labware("opentrons_flex_96_tiprack_1000ul", "B1", "Tips 3") + tips1003 = protocol.load_labware("opentrons_flex_96_tiprack_1000ul", "B2", "Tips 4") + tips1004 = protocol.load_labware("opentrons_flex_96_tiprack_1000ul", "C2", "Tips 5") + + tips = [ + *tips1000.wells()[num_samples:96], + *tips1001.wells(), + *tips1002.wells(), + *tips1003.wells(), + ] + tips_sn = tips1000.wells()[:num_samples] + + # load instruments + m1000 = protocol.load_instrument( + "flex_8channel_1000", mount, tip_racks=[tips1000, tips1001, tips1002, tips1003] + ) + + """ + Here is where you can define the locations of your reagents. + """ + binding_buffer = res1.wells()[:2] + AL = res1.wells()[2] + wash1 = res1.wells()[3:6] + wash2 = res1.wells()[6:9] + wash3 = res1.wells()[9:] + + samples_m = sample_plate.rows()[0][:num_cols] + elution_samples_m = elutionplate.rows()[0][:num_cols] + + # Probe wells + liquid_vols_and_wells: Dict[str, List[Dict[str, Well | List[Well] | float]]] = { + "AL Lysis": [{"well": AL, "volume": AL_vol}], + "PK": [{"well": AL, "volume": PK_vol}], + "Beads": [{"well": binding_buffer, "volume": bead_vol}], + "Binding": [{"well": binding_buffer, "volume": bind_vol}], + "Wash 1": [{"well": wash1, "volume": wash1_vol}], + "Wash 2": [{"well": wash2, "volume": wash2_vol}], + "Wash 3": [{"well": wash3, "volume": wash3_vol}], + "Samples": [{"well": sample_plate.wells()[:num_samples], "volume": sample_vol}], + "Elution Buffer": [ + {"well": elutionplate.wells()[:num_samples], "volume": elution_vol} + ], + } + + m1000.flow_rate.aspirate = 300 + m1000.flow_rate.dispense = 300 + m1000.flow_rate.blow_out = 300 + helpers.find_liquid_height_of_loaded_liquids(protocol, liquid_vols_and_wells, m1000) + + def tiptrack(tipbox: List[Well]) -> None: + """Track Tips.""" + global tip1k + global drop_count + if tipbox == tips: + m1000.pick_up_tip(tipbox[int(tip1k)]) + tip1k = tip1k + 8 + if tip1k >= len(tipbox): + tip1k = 0 + drop_count = drop_count + 8 + if drop_count >= 150: + drop_count = 0 + + def remove_supernatant(vol: float) -> None: + """Remove supernatants.""" + protocol.comment("-----Removing Supernatant-----") + m1000.flow_rate.aspirate = 150 + num_trans = math.ceil(vol / 980) + vol_per_trans = vol / num_trans + + for i, m in enumerate(samples_m): + m1000.pick_up_tip(tips_sn[8 * i]) + loc = m.bottom(dot_bottom) + for _ in range(num_trans): + if m1000.current_volume > 0: + # void air gap if necessary + m1000.dispense(m1000.current_volume, m.top()) + m1000.move_to(m.center()) + m1000.transfer(vol_per_trans, loc, waste, new_tip="never", air_gap=20) + m1000.blow_out(waste) + m1000.air_gap(20) + m1000.drop_tip(tips_sn[8 * i]) if TIP_TRASH else m1000.return_tip() + m1000.flow_rate.aspirate = 300 + helpers.move_labware_to_hs(protocol, sample_plate, h_s, h_s_adapter) + + def bead_mixing( + well: Well, pip: InstrumentContext, mvol: float, reps: int = 8 + ) -> None: + """Bead Mixing. + + 'mixing' will mix liquid that contains beads. This will be done by + aspirating from the bottom of the well and dispensing from the top as to + mix the beads with the other liquids as much as possible. Aspiration and + dispensing will also be reversed for a short to to ensure maximal mixing. + param well: The current well that the mixing will occur in. + param pip: The pipet that is currently attached/ being used. + param mvol: The volume that is transferred before the mixing steps. + param reps: The number of mix repetitions that should occur. Note~ + During each mix rep, there are 2 cycles of aspirating from bottom, + dispensing at the top and 2 cycles of aspirating from middle, + dispensing at the bottom + """ + center = well.top().move(types.Point(x=0, y=0, z=5)) + aspbot = well.bottom().move(types.Point(x=0, y=2, z=1)) + asptop = well.bottom().move(types.Point(x=0, y=-2, z=2.5)) + disbot = well.bottom().move(types.Point(x=0, y=1.5, z=3)) + distop = well.top().move(types.Point(x=0, y=1.5, z=0)) + + if mvol > 1000: + mvol = 1000 + + vol = mvol * 0.9 + + pip.flow_rate.aspirate = 500 + pip.flow_rate.dispense = 500 + + pip.move_to(center) + for _ in range(reps): + pip.aspirate(vol, aspbot) + pip.dispense(vol, distop) + pip.aspirate(vol, asptop) + pip.dispense(vol, disbot) + if _ == reps - 1: + pip.flow_rate.aspirate = 150 + pip.flow_rate.dispense = 100 + pip.aspirate(vol, aspbot) + pip.dispense(vol, aspbot) + + pip.flow_rate.aspirate = 300 + pip.flow_rate.dispense = 300 + + def mixing(well: Well, pip: InstrumentContext, mvol: float, reps: int = 8) -> None: + """Mixing. + + 'mixing' will mix liquid that contains beads. This will be done by + aspirating from the bottom of the well and dispensing from the top as to + mix the beads with the other liquids as much as possible. Aspiration and + dispensing will also be reversed for a short to to ensure maximal mixing. + param well: The current well that the mixing will occur in. + param pip: The pipet that is currently attached/ being used. + param mvol: The volume that is transferred before the mixing steps. + param reps: The number of mix repetitions that should occur. Note~ + During each mix rep, there are 2 cycles of aspirating from bottom, + dispensing at the top and 2 cycles of aspirating from middle, + dispensing at the bottom + """ + center = well.top(5) + asp = well.bottom(1) + disp = well.top(-8) + + if mvol > 1000: + mvol = 1000 + + vol = mvol * 0.9 + + pip.flow_rate.aspirate = 500 + pip.flow_rate.dispense = 500 + + pip.move_to(center) + for _ in range(reps): + pip.aspirate(vol, asp) + pip.dispense(vol, disp) + pip.aspirate(vol, asp) + pip.dispense(vol, disp) + if _ == reps - 1: + pip.flow_rate.aspirate = 150 + pip.flow_rate.dispense = 100 + pip.aspirate(vol, asp) + pip.dispense(vol, asp) + + pip.flow_rate.aspirate = 300 + pip.flow_rate.dispense = 300 + + def A_lysis(vol: float, source: Well) -> None: + """A Lysis.""" + protocol.comment("-----Mixing then transferring AL buffer-----") + num_transfers = math.ceil(vol / 980) + tiptrack(tips) + for i in range(num_cols): + if num_cols >= 5: + if i == 0: + height = 10 + else: + height = 1 + else: + height = 1 + src = source + tvol = vol / num_transfers + for t in range(num_transfers): + if i == 0 and t == 0: + for _ in range(3): + m1000.require_liquid_presence(src) + m1000.aspirate(tvol, src.bottom(1)) + m1000.dispense(tvol, src.bottom(4)) + m1000.aspirate(tvol, src.bottom(height)) + m1000.air_gap(10) + m1000.dispense(m1000.current_volume, samples_m[i].top()) + m1000.air_gap(20) + + for i in range(num_cols): + if i != 0: + tiptrack(tips) + mixing( + samples_m[i], m1000, tvol - 40, reps=10 if not dry_run else 1 + ) # vol is 250 AL + 180 sample + m1000.air_gap(20) + m1000.drop_tip() if TIP_TRASH else m1000.return_tip() + + protocol.comment("-----Mixing then Heating AL and Sample-----") + + helpers.set_hs_speed(protocol, h_s, heater_shaker_speed, A_lysis_time_1, False) + if not dry_run: + h_s.set_and_wait_for_temperature(55) + protocol.delay( + minutes=A_lysis_time_2, + msg="Incubating at 55C " + + str(heater_shaker_speed) + + " rpm for 10 minutes.", + ) + h_s.deactivate_shaker() + + def bind(vol: float) -> None: + """Bind. + + `bind` will perform magnetic bead binding on each sample in the + deepwell plate. Each channel of binding beads will be mixed before + transfer, and the samples will be mixed with the binding beads after + the transfer. The magnetic deck activates after the addition to all + samples, and the supernatant is removed after bead bining. + :param vol (float): The amount of volume to aspirate from the elution + buffer source and dispense to each well containing + beads. + :param park (boolean): Whether to save sample-corresponding tips + between adding elution buffer and transferring + supernatant to the final clean elutions PCR + plate. + """ + protocol.comment("-----Beginning Bind Steps-----") + tiptrack(tips) + for i, well in enumerate(samples_m): + num_trans = math.ceil(vol / 980) + vol_per_trans = vol / num_trans + source = binding_buffer[i // 7] + if i == 0: + reps = 6 if not dry_run else 1 + else: + reps = 1 + protocol.comment("-----Mixing Beads in Reservoir-----") + bead_mixing(source, m1000, vol_per_trans, reps=reps if not dry_run else 1) + # Transfer beads and binding from source to H-S plate + for t in range(num_trans): + if m1000.current_volume > 0: + # void air gap if necessary + m1000.dispense(m1000.current_volume, source.top()) + m1000.transfer( + vol_per_trans, source, well.top(), air_gap=20, new_tip="never" + ) + if t < num_trans - 1: + m1000.air_gap(20) + + protocol.comment("-----Mixing Beads in Plate-----") + for i in range(num_cols): + if i != 0: + tiptrack(tips) + mixing( + samples_m[i], m1000, vol + starting_vol, reps=10 if not dry_run else 1 + ) + m1000.drop_tip() if TIP_TRASH else m1000.return_tip() + + protocol.comment("-----Incubating Beads and Bind on H-S-----") + + speed_val = heater_shaker_speed * 0.9 + helpers.set_hs_speed(protocol, h_s, speed_val, bind_time, True) + + # Transfer from H-S plate to Magdeck plate + helpers.move_labware_from_hs_to_destination( + protocol, sample_plate, h_s, magnetic_block + ) + for bindi in np.arange( + settling_time + 1, 0, -0.5 + ): # Settling time delay with countdown timer + protocol.delay( + minutes=0.5, + msg="There are " + str(bindi) + " minutes left in the incubation.", + ) + + # remove initial supernatant + remove_supernatant(vol + starting_vol) + + def wash(vol: float, source: List[Well]) -> None: + """Wash function.""" + global whichwash # Defines which wash the protocol is on to log on the app + + if source == wash1: + whichwash = 1 + if source == wash2: + whichwash = 2 + if source == wash3: + whichwash = 3 + + protocol.comment("-----Beginning Wash #" + str(whichwash) + "-----") + + num_trans = math.ceil(vol / 980) + vol_per_trans = vol / num_trans + tiptrack(tips) + for i, m in enumerate(samples_m): + src = source[i // 4] + for n in range(num_trans): + if m1000.current_volume > 0: + m1000.dispense(m1000.current_volume, src.top()) + m1000.transfer(vol_per_trans, src, m.top(), air_gap=20, new_tip="never") + m1000.drop_tip() if TIP_TRASH else m1000.return_tip() + + helpers.set_hs_speed(protocol, h_s, heater_shaker_speed, elute_wash_time, True) + + helpers.move_labware_from_hs_to_destination( + protocol, sample_plate, h_s, magnetic_block + ) + + for washi in np.arange( + settling_time, 0, -0.5 + ): # settling time timer for washes + protocol.delay( + minutes=0.5, + msg="There are " + + str(washi) + + " minutes left in wash " + + str(whichwash) + + " incubation.", + ) + + remove_supernatant(vol) + + def elute(vol: float) -> None: + """Elution Function.""" + protocol.comment("-----Beginning Elution Steps-----") + tiptrack(tips) + for i, (m, e) in enumerate(zip(samples_m, elution_samples_m)): + m1000.flow_rate.aspirate = 25 + m1000.aspirate(vol, e.bottom(dot_bottom)) + m1000.air_gap(20) + m1000.dispense(m1000.current_volume, m.top()) + m1000.flow_rate.aspirate = 150 + m1000.drop_tip() if TIP_TRASH else m1000.return_tip() + + h_s.set_and_wait_for_shake_speed(heater_shaker_speed * 1.1) + speed_val = heater_shaker_speed * 1.1 + helpers.set_hs_speed(protocol, h_s, speed_val, elute_wash_time, True) + + # Transfer back to magnet + helpers.move_labware_from_hs_to_destination( + protocol, sample_plate, h_s, magnetic_block + ) + + for elutei in np.arange(settling_time, 0, -0.5): + protocol.delay( + minutes=0.5, + msg="Incubating on MagDeck for " + str(elutei) + " more minutes.", + ) + + for i, (m, e) in enumerate(zip(samples_m, elution_samples_m)): + tiptrack(tips) + m1000.flow_rate.dispense = 100 + m1000.flow_rate.aspirate = 150 + m1000.transfer( + vol, m.bottom(dot_bottom), e.bottom(5), air_gap=20, new_tip="never" + ) + m1000.blow_out(e.top(-2)) + m1000.air_gap(20) + m1000.drop_tip() if TIP_TRASH else m1000.return_tip() + + """ + Here is where you can call the methods defined above to fit your specific + protocol. The normal sequence is: + """ + A_lysis(AL_total_vol, AL) + bind(binding_buffer_vol) + wash(wash1_vol, wash1) + wash(wash2_vol, wash2) + wash(wash3_vol, wash3) + if not dry_run: + drybeads = 10.0 # Number of minutes you want to dry for + else: + drybeads = 0.5 + for beaddry in np.arange(drybeads, 0, -0.5): + protocol.delay( + minutes=0.5, + msg="There are " + str(beaddry) + " minutes left in the drying step.", + ) + elute(elution_vol) + + # Probe wells + end_wells_with_liquid = [ + waste_reservoir.wells()[0], + ] + m1000.tip_racks = [tips1004] + helpers.clean_up_plates(m1000, [res1, elutionplate], waste_reservoir["A1"], 1000) + helpers.find_liquid_height_of_all_wells(protocol, m1000, end_wells_with_liquid) + if deactivate_modules_bool: + helpers.deactivate_modules(protocol) diff --git a/abr-testing/abr_testing/protocols/active_protocols/8_Illumina and Plate Reader.py b/abr-testing/abr_testing/protocols/active_protocols/8_Illumina and Plate Reader.py new file mode 100644 index 00000000000..3d8c664956c --- /dev/null +++ b/abr-testing/abr_testing/protocols/active_protocols/8_Illumina and Plate Reader.py @@ -0,0 +1,996 @@ +"""Illumina DNA Prep and Plate Reader Test.""" +from opentrons.protocol_api import ( + ParameterContext, + ProtocolContext, + Labware, + Well, + InstrumentContext, +) +from abr_testing.protocols import helpers +from opentrons.protocol_api.module_contexts import ( + AbsorbanceReaderContext, + ThermocyclerContext, + HeaterShakerContext, + TemperatureModuleContext, + MagneticBlockContext, +) +from datetime import datetime +from opentrons.hardware_control.modules.types import ThermocyclerStep +from typing import List, Dict +from opentrons import types + + +metadata = { + "protocolName": "Illumina DNA Prep and Plate Reader Test", + "author": "Platform Expansion", +} + + +requirements = {"robotType": "Flex", "apiLevel": "2.21"} + +HELLMA_PLATE_SLOT = "D4" +PLATE_READER_SLOT = "C3" + +# SCRIPT SETTINGS +DRYRUN = False # True = skip incubation times, shorten mix, for testing purposes +USE_GRIPPER = True # True = Uses Gripper, False = Manual Move +TIP_TRASH = False # True = Used tips go in Trash, False = Used tips go back into rack +HYBRID_PAUSE = True # True = sets a pause on the Hybridization + +# PROTOCOL SETTINGS +COLUMNS = 4 # 1-4 +HYBRIDDECK = True +HYBRIDTIME = 1.6 # Hours + +# PROTOCOL BLOCKS +STEP_VOLPOOL = 0 +STEP_HYB = 0 +STEP_CAPTURE = 1 +STEP_WASH = 1 +STEP_PCR = 1 +STEP_PCRDECK = 1 +STEP_CLEANUP = 1 + +p200_tips = 0 +p50_tips = 0 + + +RUN = 1 + + +def add_parameters(parameters: ParameterContext) -> None: + """Add Parameters.""" + helpers.create_hs_speed_parameter(parameters) + helpers.create_dot_bottom_parameter(parameters) + helpers.create_deactivate_modules_parameter(parameters) + helpers.create_plate_reader_compatible_labware_parameter(parameters) + parameters.add_bool( + variable_name="plate_orientation", + display_name="Hellma Plate Orientation", + default=True, + description="If hellma plate is rotated, set to True.", + ) + + +def plate_reader_actions( + protocol: ProtocolContext, + plate_reader: AbsorbanceReaderContext, + hellma_plate: Labware, + hellma_plate_name: str, +) -> None: + """Plate reader single and multi wavelength readings.""" + wavelengths = [450, 650] + # Single Wavelength Readings + plate_reader.close_lid() + for wavelength in wavelengths: + plate_reader.initialize("single", [wavelength], reference_wavelength=wavelength) + plate_reader.open_lid() + protocol.move_labware(hellma_plate, plate_reader, use_gripper=True) + plate_reader.close_lid() + result = plate_reader.read(str(datetime.now())) + msg = f"{hellma_plate_name} result: {result}" + protocol.comment(msg=msg) + plate_reader.open_lid() + protocol.move_labware(hellma_plate, HELLMA_PLATE_SLOT, use_gripper=True) + plate_reader.close_lid() + # Multi Wavelength + plate_reader.initialize("multi", [450, 650]) + plate_reader.open_lid() + protocol.move_labware(hellma_plate, plate_reader, use_gripper=True) + plate_reader.close_lid() + result = plate_reader.read(str(datetime.now())) + msg = f"{hellma_plate_name} result: {result}" + protocol.comment(msg=msg) + plate_reader.open_lid() + protocol.move_labware(hellma_plate, HELLMA_PLATE_SLOT, use_gripper=True) + plate_reader.close_lid() + + +def run(protocol: ProtocolContext) -> None: + """Protocol.""" + # LOAD PARAMETERS + heater_shaker_speed = protocol.params.heater_shaker_speed # type: ignore[attr-defined] + dot_bottom = protocol.params.dot_bottom # type: ignore[attr-defined] + plate_orientation = protocol.params.plate_orientation # type: ignore[attr-defined] + deactivate_modules_bool = protocol.params.deactivate_modules # type: ignore[attr-defined] + plate_type = protocol.params.labware_plate_reader_compatible # type: ignore [attr-defined] + helpers.comment_protocol_version(protocol, "01") + + plate_name_str = "hellma_plate_" + str(plate_orientation) + global p200_tips + global p50_tips + # WASTE BIN + protocol.load_waste_chute() + # TIP RACKS + tiprack_200_1 = protocol.load_labware("opentrons_flex_96_tiprack_200ul", "B2") + tiprack_200_2 = protocol.load_labware("opentrons_flex_96_tiprack_200ul", "C2") + tiprack_50_1 = protocol.load_labware("opentrons_flex_96_tiprack_50ul", "A2") + tiprack_50_2 = protocol.load_labware("opentrons_flex_96_tiprack_50ul", "A3") + # MODULES + LABWARE + # Reservoir + reservoir = protocol.load_labware( + "nest_96_wellplate_2ml_deep", "D2", "Liquid Waste" + ) + # Heatershaker + heatershaker: HeaterShakerContext = protocol.load_module( + helpers.hs_str, "D1" + ) # type: ignore[assignment] + sample_plate_2 = heatershaker.load_labware( + "thermoscientificnunc_96_wellplate_1300ul" + ) + heatershaker.close_labware_latch() + # Magnetic Block + mag_block: MagneticBlockContext = protocol.load_module( + helpers.mag_str, "C1" + ) # type: ignore[assignment] + thermocycler: ThermocyclerContext = protocol.load_module( + helpers.tc_str + ) # type: ignore[assignment] + sample_plate_1 = thermocycler.load_labware( + "armadillo_96_wellplate_200ul_pcr_full_skirt" + ) + thermocycler.open_lid() + # Temperature Module + temp_block: TemperatureModuleContext = protocol.load_module( + helpers.temp_str, "B3" + ) # type: ignore[assignment] + reagent_plate, temp_adapter = helpers.load_temp_adapter_and_labware( + "armadillo_96_wellplate_200ul_pcr_full_skirt", temp_block, "Reagent Plate" + ) + # Plate Reader + plate_reader: AbsorbanceReaderContext = protocol.load_module( + helpers.abs_mod_str, PLATE_READER_SLOT + ) # type: ignore[assignment] + hellma_plate = protocol.load_labware(plate_type, HELLMA_PLATE_SLOT) + # PIPETTES + p1000 = protocol.load_instrument( + "flex_8channel_1000", + "left", + tip_racks=[tiprack_200_1, tiprack_200_2], + ) + p50 = protocol.load_instrument( + "flex_8channel_50", "right", tip_racks=[tiprack_50_1, tiprack_50_2] + ) + + # Load liquids and probe + liquid_vols_and_wells: Dict[str, List[Dict[str, Well | List[Well] | float]]] = { + "Reagents": [ + {"well": reagent_plate.columns()[3], "volume": 75.0}, + {"well": reagent_plate.columns()[4], "volume": 15.0}, + {"well": reagent_plate.columns()[5], "volume": 20.0}, + {"well": reagent_plate.columns()[6], "volume": 65.0}, + ], + "AMPure": [{"well": reservoir.columns()[0], "volume": 120.0}], + "SMB": [{"well": reservoir.columns()[1], "volume": 750.0}], + "EtOH": [{"well": reservoir.columns()[3], "volume": 900.0}], + "RSB": [{"well": reservoir.columns()[4], "volume": 96.0}], + "Wash": [ + {"well": sample_plate_2.columns()[9], "volume": 1000.0}, + {"well": sample_plate_2.columns()[10], "volume": 1000.0}, + {"well": sample_plate_2.columns()[11], "volume": 1000.0}, + ], + "Samples": [{"well": sample_plate_1.wells(), "volume": 150.0}], + } + helpers.find_liquid_height_of_loaded_liquids(protocol, liquid_vols_and_wells, p50) + + # reagent + AMPure = reservoir["A1"] + SMB = reservoir["A2"] + + EtOH = reservoir["A4"] + RSB = reservoir["A5"] + + Liquid_trash_well_1 = reservoir["A9"] + Liquid_trash_well_2 = reservoir["A10"] + Liquid_trash_well_3 = reservoir["A11"] + Liquid_trash_well_4 = reservoir["A12"] + liquid_trash_list = { + Liquid_trash_well_1: 0.0, + Liquid_trash_well_2: 0.0, + Liquid_trash_well_3: 0.0, + Liquid_trash_well_4: 0.0, + } + + def trash_liquid( + protocol: ProtocolContext, + pipette: InstrumentContext, + vol_to_trash: float, + liquid_trash_list: Dict[Well, float], + ) -> None: + """Determine which wells to use as liquid waste.""" + remaining_volume = vol_to_trash + max_capacity = 1500.0 + # Determine liquid waste location depending on current total volume + # Distribute the liquid volume sequentially + for well, current_volume in liquid_trash_list.items(): + if remaining_volume <= 0.0: + break + available_capacity = max_capacity - current_volume + if available_capacity < remaining_volume: + continue + pipette.dispense(remaining_volume, well.top()) + protocol.delay(minutes=0.1) + pipette.blow_out(well.top()) + liquid_trash_list[well] += remaining_volume + if pipette.current_volume <= 0.0: + break + + # Will Be distributed during the protocol + EEW_1 = sample_plate_2.wells_by_name()["A9"] + EEW_2 = sample_plate_2.wells_by_name()["A10"] + EEW_3 = sample_plate_2.wells_by_name()["A11"] + EEW_4 = sample_plate_2.wells_by_name()["A12"] + + NHB2 = reagent_plate.wells_by_name()["A1"] + Panel = reagent_plate.wells_by_name()["A2"] + EHB2 = reagent_plate.wells_by_name()["A3"] + Elute = reagent_plate.wells_by_name()["A4"] + ET2 = reagent_plate.wells_by_name()["A5"] + PPC = reagent_plate.wells_by_name()["A6"] + EPM = reagent_plate.wells_by_name()["A7"] + # Load Liquids + plate_reader_actions(protocol, plate_reader, hellma_plate, plate_name_str) + + # tip and sample tracking + if COLUMNS == 1: + column_1_list = ["A1"] # Plate 1 + column_2_list = ["A1"] # Plate 2 + column_3_list = ["A4"] # Plate 2 + column_4_list = ["A4"] # Plate 1 + column_5_list = ["A7"] # Plate 2 + column_6_list = ["A7"] # Plate 1 + WASHES = [EEW_1] + if COLUMNS == 2: + column_1_list = ["A1", "A2"] # Plate 1 + column_2_list = ["A1", "A2"] # Plate 2 + column_3_list = ["A4", "A5"] # Plate 2 + column_4_list = ["A4", "A5"] # Plate 1 + column_5_list = ["A7", "A8"] # Plate 2 + column_6_list = ["A7", "A8"] # Plate 1 + WASHES = [EEW_1, EEW_2] + if COLUMNS == 3: + column_1_list = ["A1", "A2", "A3"] # Plate 1 + column_2_list = ["A1", "A2", "A3"] # Plate 2 + column_3_list = ["A4", "A5", "A6"] # Plate 2 + column_4_list = ["A4", "A5", "A6"] # Plate 1 + column_5_list = ["A7", "A8", "A9"] # Plate 2 + column_6_list = ["A7", "A8", "A9"] # Plate 1 + WASHES = [EEW_1, EEW_2, EEW_3] + if COLUMNS == 4: + column_1_list = ["A1", "A2", "A3", "A4"] # Plate 1 + column_2_list = ["A1", "A2", "A3", "A4"] # Plate 2 + column_3_list = ["A5", "A6", "A7", "A8"] # Plate 2 + column_4_list = ["A5", "A6", "A7", "A8"] # Plate 1 + column_5_list = ["A9", "A10", "A11", "A12"] # Plate 2 + column_6_list = ["A9", "A10", "A11", "A12"] # Plate 1 + WASHES = [EEW_1, EEW_2, EEW_3, EEW_4] + + def tipcheck() -> None: + """Check tips.""" + if p200_tips >= 2 * 12: + p1000.reset_tipracks() + p200_tips == 0 + if p50_tips >= 2 * 12: + p50.reset_tipracks() + p50_tips == 0 + + # commands + for loop in range(RUN): + thermocycler.open_lid() + heatershaker.open_labware_latch() + if DRYRUN is False: + if STEP_HYB == 1: + protocol.comment("SETTING THERMO and TEMP BLOCK Temperature") + thermocycler.set_block_temperature(4) + thermocycler.set_lid_temperature(100) + temp_block.set_temperature(4) + else: + protocol.comment("SETTING THERMO and TEMP BLOCK Temperature") + thermocycler.set_block_temperature(58) + thermocycler.set_lid_temperature(58) + heatershaker.set_and_wait_for_temperature(58) + heatershaker.close_labware_latch() + + # Sample Plate contains 30ul of DNA + + if STEP_VOLPOOL == 1: + protocol.comment("==============================================") + protocol.comment("--> Quick Vol Pool") + protocol.comment("==============================================") + + if STEP_HYB == 1: + protocol.comment("==============================================") + protocol.comment("--> HYB") + protocol.comment("==============================================") + + protocol.comment("--> Adding NHB2") + NHB2Vol = 50 + for loop, X in enumerate(column_1_list): + p50.pick_up_tip() + p50.aspirate(NHB2Vol, NHB2.bottom(z=dot_bottom)) # original = () + p50.dispense( + NHB2Vol, sample_plate_1[X].bottom(z=dot_bottom) + ) # original = () + p50.return_tip() if TIP_TRASH is False else p50.drop_tip() + p50_tips += 1 + tipcheck() + + protocol.comment("--> Adding Panel") + PanelVol = 10 + for loop, X in enumerate(column_1_list): + p50.pick_up_tip() + p50.aspirate(PanelVol, Panel.bottom(z=dot_bottom)) # original = () + p50.dispense( + PanelVol, sample_plate_1[X].bottom(z=dot_bottom) + ) # original = () + p50.return_tip() if TIP_TRASH is False else p50.drop_tip() + p50_tips += 1 + tipcheck() + + protocol.comment("--> Adding EHB2") + EHB2Vol = 10 + EHB2MixRep = 10 if DRYRUN is False else 1 + EHB2MixVol = 90 + for loop, X in enumerate(column_1_list): + p1000.pick_up_tip() + p1000.aspirate(EHB2Vol, EHB2.bottom(z=dot_bottom)) # original = () + p1000.dispense( + EHB2Vol, sample_plate_1[X].bottom(z=dot_bottom) + ) # original = () + p1000.move_to(sample_plate_1[X].bottom(z=dot_bottom)) # original = () + p1000.mix(EHB2MixRep, EHB2MixVol) + p1000.return_tip() if TIP_TRASH is False else p1000.drop_tip() + p50_tips += 1 + tipcheck() + + if HYBRIDDECK: + protocol.comment("Hybridize on Deck") + thermocycler.close_lid() + if DRYRUN is False: + profile_TAGSTOP: List[ThermocyclerStep] = [ + {"temperature": 98, "hold_time_minutes": 5}, + {"temperature": 97, "hold_time_minutes": 1}, + {"temperature": 95, "hold_time_minutes": 1}, + {"temperature": 93, "hold_time_minutes": 1}, + {"temperature": 91, "hold_time_minutes": 1}, + {"temperature": 89, "hold_time_minutes": 1}, + {"temperature": 87, "hold_time_minutes": 1}, + {"temperature": 85, "hold_time_minutes": 1}, + {"temperature": 83, "hold_time_minutes": 1}, + {"temperature": 81, "hold_time_minutes": 1}, + {"temperature": 79, "hold_time_minutes": 1}, + {"temperature": 77, "hold_time_minutes": 1}, + {"temperature": 75, "hold_time_minutes": 1}, + {"temperature": 73, "hold_time_minutes": 1}, + {"temperature": 71, "hold_time_minutes": 1}, + {"temperature": 69, "hold_time_minutes": 1}, + {"temperature": 67, "hold_time_minutes": 1}, + {"temperature": 65, "hold_time_minutes": 1}, + {"temperature": 63, "hold_time_minutes": 1}, + {"temperature": 62, "hold_time_minutes": HYBRIDTIME * 60}, + ] + thermocycler.execute_profile( + steps=profile_TAGSTOP, repetitions=1, block_max_volume=100 + ) + thermocycler.set_block_temperature(62) + if HYBRID_PAUSE: + protocol.comment("HYBRIDIZATION PAUSED") + thermocycler.set_block_temperature(10) + thermocycler.open_lid() + else: + protocol.comment("Hybridize off Deck") + + if STEP_CAPTURE == 1: + protocol.comment("==============================================") + protocol.comment("--> Capture") + protocol.comment("==============================================") + # Standard Setup + + if DRYRUN is False: + protocol.comment("SETTING THERMO and TEMP BLOCK Temperature") + thermocycler.set_block_temperature(58) + thermocycler.set_lid_temperature(58) + + if DRYRUN is False: + heatershaker.set_and_wait_for_temperature(58) + + protocol.comment("--> Transfer Hybridization") + TransferSup = 100 + for loop, X in enumerate(column_1_list): + p1000.pick_up_tip() + p1000.move_to(sample_plate_1[X].bottom(z=0.5)) + p1000.aspirate(TransferSup + 1, rate=0.25) + p1000.dispense( + TransferSup + 1, sample_plate_2[column_2_list[loop]].bottom(z=1) + ) + p1000.return_tip() if TIP_TRASH is False else p1000.drop_tip() + p200_tips += 1 + tipcheck() + + thermocycler.close_lid() + + protocol.comment("--> ADDING SMB") + SMBVol = 250 + SMBMixRPM = heater_shaker_speed + SMBMixRep = 5 * 60 if DRYRUN is False else 0.1 * 60 + SMBPremix = 3 if DRYRUN is False else 1 + # ============================== + for loop, X in enumerate(column_2_list): + p1000.pick_up_tip() + p1000.mix(SMBPremix, 200, SMB.bottom(z=1)) + p1000.aspirate(SMBVol / 2, SMB.bottom(z=1), rate=0.25) + p1000.dispense(SMBVol / 2, sample_plate_2[X].top(z=-7), rate=0.25) + p1000.aspirate(SMBVol / 2, SMB.bottom(z=1), rate=0.25) + p1000.dispense(SMBVol / 2, sample_plate_2[X].bottom(z=1), rate=0.25) + p1000.default_speed = 5 + p1000.move_to(sample_plate_2[X].bottom(z=5)) + for Mix in range(2): + p1000.aspirate(100, rate=0.5) + p1000.move_to(sample_plate_2[X].bottom(z=1)) + p1000.aspirate(80, rate=0.5) + p1000.dispense(80, rate=0.5) + p1000.move_to(sample_plate_2[X].bottom(z=5)) + p1000.dispense(100, rate=0.5) + Mix += 1 + p1000.blow_out(sample_plate_2[X].top(z=-7)) + p1000.default_speed = 400 + p1000.move_to(sample_plate_2[X].top(z=5)) + p1000.move_to(sample_plate_2[X].top(z=0)) + p1000.move_to(sample_plate_2[X].top(z=5)) + p1000.return_tip() if TIP_TRASH is False else p1000.drop_tip() + p200_tips += 1 + tipcheck() + # ============================== + heatershaker.set_and_wait_for_shake_speed(rpm=SMBMixRPM) + protocol.delay(SMBMixRep) + heatershaker.deactivate_shaker() + + # ============================================================================================ + # GRIPPER MOVE sample_plate_2 FROM heatershaker TO MAGPLATE + helpers.move_labware_from_hs_to_destination( + protocol, sample_plate_2, heatershaker, mag_block + ) + thermocycler.open_lid() + + if DRYRUN is False: + protocol.delay(minutes=2) + + protocol.comment("==============================================") + protocol.comment("--> WASH") + protocol.comment("==============================================") + # Setting Labware to Resume at Cleanup 1 + + protocol.comment("--> Remove SUPERNATANT") + for loop, X in enumerate(column_2_list): + p1000.pick_up_tip() + p1000.move_to(sample_plate_2[X].bottom(4)) + p1000.aspirate(200, rate=0.25) + trash_liquid(protocol, p1000, 200, liquid_trash_list) + p1000.move_to(sample_plate_2[X].bottom(0.5)) + p1000.aspirate(200, rate=0.25) + trash_liquid(protocol, p1000, 200, liquid_trash_list) + p1000.return_tip() if TIP_TRASH is False else p1000.drop_tip() + p200_tips += 1 + tipcheck() + + # ============================================================================================ + # GRIPPER MOVE sample_plate_2 FROM MAGPLATE TO heatershaker + helpers.move_labware_to_hs( + protocol, sample_plate_2, heatershaker, heatershaker + ) + # ============================================================================================ + + protocol.comment("--> Repeating 3 washes") + washreps = 3 + washcount = 0 + for wash in range(washreps): + + protocol.comment("--> Adding EEW") + EEWVol = 200 + for loop, X in enumerate(column_2_list): + p1000.pick_up_tip() + p1000.aspirate( + EEWVol, WASHES[loop].bottom(z=dot_bottom) + ) # original = () + p1000.dispense( + EEWVol, sample_plate_2[X].bottom(z=dot_bottom) + ) # original = () + p1000.return_tip() if TIP_TRASH is False else p1000.drop_tip() + p200_tips += 1 + tipcheck() + heatershaker.close_labware_latch() + heatershaker.set_and_wait_for_shake_speed( + rpm=(heater_shaker_speed * 0.9) + ) + if DRYRUN is False: + protocol.delay(seconds=4 * 60) + heatershaker.deactivate_shaker() + heatershaker.open_labware_latch() + + if DRYRUN is False: + protocol.delay(seconds=5 * 60) + + # ============================================================================================ + # GRIPPER MOVE sample_plate_2 FROM heatershaker TO MAGPLATE + helpers.move_labware_from_hs_to_destination( + protocol, sample_plate_2, heatershaker, mag_block + ) + + if DRYRUN is False: + protocol.delay(seconds=1 * 60) + + protocol.comment("--> Removing Supernatant") + RemoveSup = 200 + for loop, X in enumerate(column_2_list): + p1000.pick_up_tip() + p1000.move_to(sample_plate_2[X].bottom(z=3.5)) + p1000.aspirate(RemoveSup - 100, rate=0.25) + protocol.delay(minutes=0.1) + p1000.move_to(sample_plate_2[X].bottom(z=0.5)) + p1000.aspirate(100, rate=0.25) + p1000.move_to(sample_plate_2[X].top(z=0.5)) + trash_liquid(protocol, p1000, 200, liquid_trash_list) + p1000.return_tip() if TIP_TRASH is False else p1000.drop_tip() + p200_tips += 1 + tipcheck() + + # ============================================================================================ + # GRIPPER MOVE sample_plate_2 FROM MAGPLATE TO heatershaker + helpers.move_labware_to_hs( + protocol, sample_plate_2, heatershaker, heatershaker + ) + washcount += 1 + + protocol.comment("--> Adding EEW") + EEWVol = 200 + for loop, X in enumerate(column_2_list): + p1000.pick_up_tip() + p1000.aspirate( + EEWVol, WASHES[loop].bottom(z=dot_bottom) + ) # original = () + p1000.dispense( + EEWVol, sample_plate_2[X].bottom(z=dot_bottom) + ) # original = () + p1000.return_tip() if TIP_TRASH is False else p1000.drop_tip() + p200_tips += 1 + tipcheck() + + heatershaker.set_and_wait_for_shake_speed(rpm=(heater_shaker_speed * 0.9)) + if DRYRUN is False: + protocol.delay(seconds=4 * 60) + heatershaker.deactivate_shaker() + + if DRYRUN is False: + protocol.delay(seconds=1 * 60) + + protocol.comment("--> Transfer Hybridization") + TransferSup = 200 + for loop, X in enumerate(column_2_list): + p1000.pick_up_tip() + p1000.move_to(sample_plate_2[X].bottom(z=0.5)) + p1000.aspirate(TransferSup, rate=0.25) + p1000.dispense( + TransferSup, sample_plate_2[column_3_list[loop]].bottom(z=1) + ) + p1000.return_tip() if TIP_TRASH is False else p1000.drop_tip() + p200_tips += 1 + tipcheck() + + if DRYRUN is False: + protocol.delay(seconds=5 * 60) + + # ============================================================================================ + # GRIPPER MOVE sample_plate_2 FROM heatershaker TO MAGPLATE + helpers.move_labware_from_hs_to_destination( + protocol, sample_plate_2, heatershaker, mag_block + ) + + if DRYRUN is False: + protocol.delay(seconds=1 * 60) + + protocol.comment("--> Removing Supernatant") + RemoveSup = 200 + for loop, X in enumerate(column_3_list): + p1000.pick_up_tip() + p1000.move_to(sample_plate_2[X].bottom(z=3.5)) + p1000.aspirate(RemoveSup - 100, rate=0.25) + protocol.delay(minutes=0.1) + p1000.move_to(sample_plate_2[X].bottom(z=0.5)) + p1000.aspirate(100, rate=0.25) + p1000.move_to(sample_plate_2[X].top(z=0.5)) + trash_liquid(protocol, p1000, 200, liquid_trash_list) + p1000.return_tip() if TIP_TRASH is False else p1000.drop_tip() + p200_tips += 1 + tipcheck() + + protocol.comment("--> Removing Residual") + for loop, X in enumerate(column_3_list): + p50.pick_up_tip() + p50.move_to(sample_plate_2[X].bottom(z=dot_bottom)) # original = z=0 + p50.aspirate(50, rate=0.25) + p50.default_speed = 200 + trash_liquid(protocol, p50, 50, liquid_trash_list) + p50.return_tip() if TIP_TRASH is False else p50.drop_tip() + p50_tips += 1 + tipcheck() + + protocol.comment("==============================================") + protocol.comment("--> ELUTE") + protocol.comment("==============================================") + + protocol.comment("--> Adding Elute") + EluteVol = 23 + for loop, X in enumerate(column_3_list): + p50.pick_up_tip() + p50.aspirate(EluteVol, Elute.bottom(z=dot_bottom)) # original = () + p50.dispense( + EluteVol, sample_plate_2[X].bottom(z=dot_bottom) + ) # original = () + p50.return_tip() if TIP_TRASH is False else p50.drop_tip() + p50_tips += 1 + tipcheck() + + # ============================================================================================ + # GRIPPER MOVE sample_plate_2 FROM MAGPLATE TO heatershaker + helpers.move_labware_to_hs( + protocol, sample_plate_2, heatershaker, heatershaker + ) + # ============================================================================================ + + heatershaker.close_labware_latch() + heatershaker.set_and_wait_for_shake_speed(rpm=(heater_shaker_speed * 0.9)) + if DRYRUN is False: + protocol.delay(seconds=2 * 60) + heatershaker.deactivate_shaker() + heatershaker.open_labware_latch() + + if DRYRUN is False: + protocol.delay(minutes=2) + + # ============================================================================================ + # GRIPPER MOVE sample_plate_2 FROM heatershaker TO MAGPLATE + helpers.move_labware_from_hs_to_destination( + protocol, sample_plate_2, heatershaker, mag_block + ) + protocol.comment("--> Transfer Elution") + TransferSup = 21 + for loop, X in enumerate(column_3_list): + p50.pick_up_tip() + p50.move_to(sample_plate_2[X].bottom(z=0.5)) + p50.aspirate(TransferSup + 1, rate=0.25) + p50.dispense( + TransferSup + 1, sample_plate_1[column_4_list[loop]].bottom(z=1) + ) + p50.return_tip() if TIP_TRASH is False else p50.drop_tip() + p50_tips += 1 + tipcheck() + + protocol.comment("--> Adding ET2") + ET2Vol = 4 + ET2MixRep = 10 if DRYRUN is False else 1 + ET2MixVol = 20 + for loop, X in enumerate(column_4_list): + p50.pick_up_tip() + p50.aspirate(ET2Vol, ET2.bottom(z=dot_bottom)) # original = () + p50.dispense( + ET2Vol, sample_plate_1[X].bottom(z=dot_bottom) + ) # original = () + p50.move_to(sample_plate_1[X].bottom(z=dot_bottom)) # original = () + p50.mix(ET2MixRep, ET2MixVol) + p50.return_tip() if TIP_TRASH is False else p50.drop_tip() + p50_tips += 1 + tipcheck() + + if STEP_PCR == 1: + protocol.comment("==============================================") + protocol.comment("--> AMPLIFICATION") + protocol.comment("==============================================") + + protocol.comment("--> Adding PPC") + PPCVol = 5 + for loop, X in enumerate(column_4_list): + p50.pick_up_tip() + p50.aspirate(PPCVol, PPC.bottom(z=dot_bottom)) # original = () + p50.dispense( + PPCVol, sample_plate_1[X].bottom(z=dot_bottom) + ) # original = () + p50.return_tip() if TIP_TRASH is False else p50.drop_tip() + p50_tips += 1 + tipcheck() + + protocol.comment("--> Adding EPM") + EPMVol = 20 + EPMMixRep = 10 if DRYRUN is False else 1 + EPMMixVol = 45 + for loop, X in enumerate(column_4_list): + p50.pick_up_tip() + p50.aspirate(EPMVol, EPM.bottom(z=dot_bottom)) # original = () + p50.dispense( + EPMVol, sample_plate_1[X].bottom(z=dot_bottom) + ) # original = () + p50.move_to(sample_plate_1[X].bottom(z=dot_bottom)) # original = () + p50.mix(EPMMixRep, EPMMixVol) + p50.return_tip() if TIP_TRASH is False else p50.drop_tip() + p50_tips += 1 + tipcheck() + + if DRYRUN is False: + heatershaker.deactivate_heater() + + if STEP_PCRDECK == 1: + if DRYRUN is False: + if DRYRUN is False: + thermocycler.close_lid() + helpers.perform_pcr( + protocol, + thermocycler, + initial_denature_time_sec=45, + denaturation_time_sec=30, + anneal_time_sec=30, + extension_time_sec=30, + cycle_repetitions=12, + final_extension_time_min=1, + ) + thermocycler.set_block_temperature(10) + + thermocycler.open_lid() + + if STEP_CLEANUP == 1: + protocol.comment("==============================================") + protocol.comment("--> Cleanup") + protocol.comment("==============================================") + + # GRIPPER MOVE sample_plate_2 FROM MAGPLATE TO heatershaker + helpers.move_labware_to_hs( + protocol, sample_plate_2, heatershaker, heatershaker + ) + + protocol.comment("--> Transfer Elution") + TransferSup = 45 + for loop, X in enumerate(column_4_list): + p50.pick_up_tip() + p50.move_to(sample_plate_1[X].bottom(z=0.5)) + p50.aspirate(TransferSup + 1, rate=0.25) + p50.dispense( + TransferSup + 1, sample_plate_2[column_5_list[loop]].bottom(z=1) + ) + p50.return_tip() if TIP_TRASH is False else p50.drop_tip() + p50_tips += 1 + tipcheck() + + protocol.comment("--> ADDING AMPure (0.8x)") + AMPureVol = 40.5 + AMPureMixRep = 5 * 60 if DRYRUN is False else 0.1 * 60 + AMPurePremix = 3 if DRYRUN is False else 1 + # ========NEW SINGLE TIP DISPENSE=========== + for loop, X in enumerate(column_5_list): + p1000.pick_up_tip() + p1000.mix(AMPurePremix, AMPureVol + 10, AMPure.bottom(z=1)) + p1000.aspirate(AMPureVol, AMPure.bottom(z=1), rate=0.25) + p1000.dispense(AMPureVol, sample_plate_2[X].bottom(z=1), rate=0.25) + p1000.default_speed = 5 + p1000.move_to(sample_plate_2[X].bottom(z=5)) + for Mix in range(2): + p1000.aspirate(60, rate=0.5) + p1000.move_to(sample_plate_2[X].bottom(z=1)) + p1000.aspirate(60, rate=0.5) + p1000.dispense(60, rate=0.5) + p1000.move_to(sample_plate_2[X].bottom(z=5)) + p1000.dispense(30, rate=0.5) + Mix += 1 + p1000.blow_out(sample_plate_2[X].top(z=2)) + p1000.default_speed = 400 + p1000.move_to(sample_plate_2[X].top(z=5)) + p1000.move_to(sample_plate_2[X].top(z=0)) + p1000.move_to(sample_plate_2[X].top(z=5)) + p1000.return_tip() if TIP_TRASH is False else p1000.drop_tip() + p200_tips += 1 + tipcheck() + # ========NEW HS MIX========================= + heatershaker.set_and_wait_for_shake_speed(rpm=(heater_shaker_speed * 0.9)) + protocol.delay(AMPureMixRep) + heatershaker.deactivate_shaker() + + # GRIPPER MOVE PLATE FROM HEATER SHAKER TO MAG PLATE + helpers.move_labware_from_hs_to_destination( + protocol, sample_plate_2, heatershaker, mag_block + ) + + if DRYRUN is False: + protocol.delay(minutes=4) + + protocol.comment("--> Removing Supernatant") + RemoveSup = 200 + for loop, X in enumerate(column_5_list): + p1000.pick_up_tip() + p1000.move_to(sample_plate_2[X].bottom(z=3.5)) + p1000.aspirate(RemoveSup - 100, rate=0.25) + protocol.delay(minutes=0.1) + p1000.move_to(sample_plate_2[X].bottom(z=0.5)) + p1000.aspirate(100, rate=0.25) + p1000.default_speed = 5 + p1000.move_to(sample_plate_2[X].top(z=2)) + p1000.default_speed = 200 + trash_liquid(protocol, p1000, 200, liquid_trash_list) + p1000.return_tip() if TIP_TRASH is False else p1000.drop_tip() + p200_tips += 1 + tipcheck() + + for well_num in ["A1", "A2"]: + protocol.comment("--> ETOH Wash") + ETOHMaxVol = 150 + for loop, X in enumerate(column_5_list): + p1000.pick_up_tip() + p1000.aspirate(ETOHMaxVol, EtOH.bottom(z=1)) + p1000.move_to(EtOH.top(z=0)) + p1000.move_to(EtOH.top(z=-5)) + p1000.move_to(EtOH.top(z=0)) + p1000.move_to(sample_plate_2[well_num].top(z=-2)) + p1000.dispense(ETOHMaxVol, rate=1) + protocol.delay(minutes=0.1) + p1000.blow_out() + p1000.move_to(sample_plate_2[well_num].top(z=5)) + p1000.move_to(sample_plate_2[well_num].top(z=0)) + p1000.move_to(sample_plate_2[well_num].top(z=5)) + p1000.return_tip() if TIP_TRASH is False else p1000.drop_tip() + p200_tips += 1 + tipcheck() + + if DRYRUN is False: + protocol.delay(minutes=0.5) + + protocol.comment("--> Remove ETOH Wash") + for loop, X in enumerate(column_5_list): + p1000.pick_up_tip() + p1000.move_to(sample_plate_2[X].bottom(z=3.5)) + p1000.aspirate(RemoveSup - 100, rate=0.25) + protocol.delay(minutes=0.1) + p1000.move_to(sample_plate_2[X].bottom(z=0.5)) + p1000.aspirate(100, rate=0.25) + p1000.default_speed = 5 + p1000.move_to(sample_plate_2[X].top(z=2)) + p1000.default_speed = 200 + trash_liquid(protocol, p1000, 200, liquid_trash_list) + p1000.return_tip() if TIP_TRASH is False else p1000.drop_tip() + p200_tips += 1 + tipcheck() + + if DRYRUN is False: + protocol.delay(minutes=2) + + protocol.comment("--> Removing Residual ETOH") + for loop, X in enumerate(column_5_list): + p1000.pick_up_tip() + p1000.move_to( + sample_plate_2[X].bottom(z=dot_bottom) + ) # original = (z=0) + p1000.aspirate(50, rate=0.25) + p1000.default_speed = 200 + trash_liquid(protocol, p1000, 50, liquid_trash_list) + p1000.return_tip() if TIP_TRASH is False else p1000.drop_tip() + p200_tips += 1 + tipcheck() + + if DRYRUN is False: + protocol.delay(minutes=1) + + # ============================================================================================ + # GRIPPER MOVE PLATE FROM MAG PLATE TO HEATER SHAKER + helpers.move_labware_to_hs( + protocol, sample_plate_2, heatershaker, heatershaker + ) + + protocol.comment("--> Adding RSB") + RSBVol = 32 + RSBMixRep = 1 * 60 if DRYRUN is False else 0.1 * 60 + for loop, X in enumerate(column_5_list): + p1000.pick_up_tip() + p1000.aspirate(RSBVol, RSB.bottom(z=1)) + + p1000.move_to( + ( + sample_plate_2.wells_by_name()[X] + .center() + .move(types.Point(x=1.3 * 0.8, y=0, z=-4)) + ) + ) + p1000.dispense(RSBVol, rate=1) + p1000.move_to(sample_plate_2.wells_by_name()[X].bottom(z=1)) + p1000.aspirate(RSBVol, rate=1) + p1000.move_to( + ( + sample_plate_2.wells_by_name()[X] + .center() + .move(types.Point(x=0, y=1.3 * 0.8, z=-4)) + ) + ) + p1000.dispense(RSBVol, rate=1) + p1000.move_to(sample_plate_2.wells_by_name()[X].bottom(z=1)) + p1000.aspirate(RSBVol, rate=1) + p1000.move_to( + ( + sample_plate_2.wells_by_name()[X] + .center() + .move(types.Point(x=1.3 * -0.8, y=0, z=-4)) + ) + ) + p1000.dispense(RSBVol, rate=1) + p1000.move_to(sample_plate_2.wells_by_name()[X].bottom(z=1)) + p1000.aspirate(RSBVol, rate=1) + p1000.move_to( + ( + sample_plate_2.wells_by_name()[X] + .center() + .move(types.Point(x=0, y=1.3 * -0.8, z=-4)) + ) + ) + p1000.dispense(RSBVol, rate=1) + p1000.move_to(sample_plate_2.wells_by_name()[X].bottom(z=1)) + p1000.aspirate(RSBVol, rate=1) + p1000.dispense(RSBVol, rate=1) + + p1000.blow_out(sample_plate_2.wells_by_name()[X].center()) + p1000.move_to(sample_plate_2.wells_by_name()[X].top(z=5)) + p1000.move_to(sample_plate_2.wells_by_name()[X].top(z=0)) + p1000.move_to(sample_plate_2.wells_by_name()[X].top(z=5)) + p1000.return_tip() if TIP_TRASH is False else p1000.drop_tip() + p200_tips += 1 + tipcheck() + if DRYRUN is False: + heatershaker.set_and_wait_for_shake_speed( + rpm=(heater_shaker_speed * 0.8) + ) + protocol.delay(RSBMixRep) + heatershaker.deactivate_shaker() + + # ============================================================================================ + # GRIPPER MOVE PLATE FROM HEATER SHAKER TO MAG PLATE + helpers.move_labware_from_hs_to_destination( + protocol, sample_plate_2, heatershaker, mag_block + ) + + if DRYRUN is False: + protocol.delay(minutes=3) + + protocol.comment("--> Transferring Supernatant") + TransferSup = 30 + for loop, X in enumerate(column_5_list): + p1000.pick_up_tip() + p1000.move_to(sample_plate_2[X].bottom(z=0.5)) + p1000.aspirate(TransferSup + 1, rate=0.25) + p1000.dispense( + TransferSup + 1, sample_plate_1[column_6_list[loop]].bottom(z=1) + ) + p1000.return_tip() if TIP_TRASH is False else p1000.drop_tip() + p200_tips += 1 + tipcheck() + liquids_to_probe_at_end = [ + Liquid_trash_well_1, + Liquid_trash_well_2, + Liquid_trash_well_3, + Liquid_trash_well_4, + ] + helpers.find_liquid_height_of_all_wells(protocol, p50, liquids_to_probe_at_end) + plate_reader_actions(protocol, plate_reader, hellma_plate, plate_name_str) + if deactivate_modules_bool: + helpers.deactivate_modules(protocol) diff --git a/abr-testing/abr_testing/protocols/active_protocols/9_Magmax_RNA_Cells_Flex.py b/abr-testing/abr_testing/protocols/active_protocols/9_Magmax_RNA_Cells_Flex.py new file mode 100644 index 00000000000..c44e8111490 --- /dev/null +++ b/abr-testing/abr_testing/protocols/active_protocols/9_Magmax_RNA_Cells_Flex.py @@ -0,0 +1,590 @@ +"""Thermo MagMax RNA Extraction: Cells Multi-Channel.""" +import math +from opentrons import types +from opentrons.protocol_api import ( + ProtocolContext, + ParameterContext, + Well, + InstrumentContext, +) +from typing import List +from opentrons.protocol_api.module_contexts import ( + HeaterShakerContext, + MagneticBlockContext, + TemperatureModuleContext, +) + +import numpy as np +from abr_testing.protocols import helpers +from typing import Dict + +metadata = { + "author": "Zach Galluzzo ", + "protocolName": "Thermo MagMax RNA Extraction: Cells Multi-Channel", +} + +requirements = { + "robotType": "Flex", + "apiLevel": "2.21", +} +""" +Slot A1: Tips 200 +Slot A2: Tips 200 +Slot A3: Temperature module (gen2) with 96 well PCR block and Armadillo 96 well PCR Plate +** Plate gets 55 ul per well in each well of the entire plate +Slot B1: Tips 200 +Slot B2: Tips 200 +Slot B3: Nest 1 Well Reservoir +Slot C1: Magblock +Slot C2: +Slot C3: +Slot D1: H-S with Nest 96 Well Deepwell and DW Adapter +Slot D2: Nest 12 well 15 ml Reservoir +Slot D3: Trash + +Reservoir 1: +Well 1 - 8120 ul +Well 2 - 8120 ul +Well 3 - 12800 ul +Well 4-12 - 9500 ul + +""" + +whichwash = 0 +tip_pick_up = 0 +drop_count = 0 +waste_vol = 0 +wash_volume_tracker = 0.0 + + +# Start protocol +def add_parameters(parameters: ParameterContext) -> None: + """Parameters.""" + helpers.create_dot_bottom_parameter(parameters) + helpers.create_single_pipette_mount_parameter(parameters) + helpers.create_hs_speed_parameter(parameters) + helpers.create_deactivate_modules_parameter(parameters) + + +def run(protocol: ProtocolContext) -> None: + """Protocol.""" + dry_run = False + inc_lysis = True + res_type = "nest_12_reservoir_15ml" + TIP_TRASH = False + num_samples = 96 + wash_vol = 150.0 + lysis_vol = 140.0 + stop_vol = 100.0 + elution_vol = dnase_vol = 55.0 + heater_shaker_speed = protocol.params.heater_shaker_speed # type: ignore[attr-defined] + dot_bottom = protocol.params.dot_bottom # type: ignore[attr-defined] + pipette_mount = protocol.params.pipette_mount # type: ignore[attr-defined] + deactivate_modules_bool = protocol.params.deactivate_modules # type: ignore[attr-defined] + helpers.comment_protocol_version(protocol, "01") + + # Protocol Parameters + deepwell_type = "nest_96_wellplate_2ml_deep" + if not dry_run: + settling_time = 2.0 + lysis_time = 1.0 + drybeads = 2.0 # Number of minutes you want to dry for + bind_time = wash_time = 5.0 + dnase_time = 10.0 + stop_time = elute_time = 3.0 + else: + settling_time = 0.25 + lysis_time = 0.25 + drybeads = elute_time = 0.25 + bind_time = wash_time = dnase_time = stop_time = 0.25 + bead_vol = 20.0 + protocol.load_trash_bin("A3") + h_s: HeaterShakerContext = protocol.load_module( + helpers.hs_str, "D1" + ) # type: ignore[assignment] + sample_plate, h_s_adapter = helpers.load_hs_adapter_and_labware( + deepwell_type, h_s, "Sample Plate" + ) + h_s.close_labware_latch() + temp: TemperatureModuleContext = protocol.load_module( + helpers.temp_str, "D3" + ) # type: ignore[assignment] + elutionplate, temp_adapter = helpers.load_temp_adapter_and_labware( + "armadillo_96_wellplate_200ul_pcr_full_skirt", temp, "Elution Plate" + ) + temp.set_temperature(4) + magblock: MagneticBlockContext = protocol.load_module( + helpers.mag_str, "C1" + ) # type: ignore[assignment] + waste_reservoir = protocol.load_labware( + "nest_1_reservoir_195ml", "B3", "Liquid Waste" + ) + waste = waste_reservoir.wells()[0].top() + res1 = protocol.load_labware(res_type, "D2", "reagent reservoir 1") + num_cols = math.ceil(num_samples / 8) + + # Load tips and combine all similar boxes + tips200 = protocol.load_labware("opentrons_flex_96_tiprack_200ul", "A1", "Tips 1") + tips201 = protocol.load_labware("opentrons_flex_96_tiprack_200ul", "A2", "Tips 2") + tips202 = protocol.load_labware("opentrons_flex_96_tiprack_200ul", "B1", "Tips 3") + tips203 = protocol.load_labware("opentrons_flex_96_tiprack_200ul", "B2", "Tips 4") + tips204 = protocol.load_labware("opentrons_flex_96_tiprack_200ul", "C2", "Tips 5") + + tips_sn = tips200.wells()[:num_samples] + + # load P1000M pipette + m1000 = protocol.load_instrument( + "flex_8channel_1000", + pipette_mount, + tip_racks=[tips200, tips201, tips202, tips203, tips204], + ) + + # Load Liquid Locations in Reservoir + elution_solution = elutionplate.rows()[0][:num_cols] + dnase1 = elutionplate.rows()[0][:num_cols] + lysis_ = res1.wells()[0:2] + stopreaction = res1.wells()[2] + wash1 = res1.wells()[3] + wash2 = res1.wells()[4] + wash3 = res1.wells()[5] + wash4 = res1.wells()[6] + wash5 = res1.wells()[7] + wash6 = res1.wells()[8] + wash7 = res1.wells()[9] + wash8 = res1.wells()[10] + wash9 = res1.wells()[11] + all_washes = res1.wells()[3:12] + """ + Here is where you can define the locations of your reagents. + """ + samples_m = sample_plate.rows()[0][:num_cols] # 20ul beads each well + cells_m = sample_plate.rows()[0][:num_cols] + elution_samples_m = elutionplate.rows()[0][:num_cols] + # Do the same for color mapping + beads_ = sample_plate.wells()[: (8 * num_cols)] + cells_ = sample_plate.wells()[(8 * num_cols) : (16 * num_cols)] + elution_samps = elutionplate.wells()[: (8 * num_cols)] + dnase1_ = elutionplate.wells()[(8 * num_cols) : (16 * num_cols)] + + # Add liquids to non-reservoir labware + liquid_vols_and_wells: Dict[str, List[Dict[str, Well | List[Well] | float]]] = { + "Beads": [{"well": beads_, "volume": bead_vol}], + "Sample": [{"well": cells_, "volume": 0.0}], + "DNAse": [{"well": dnase1_, "volume": dnase_vol}], + "Elution Buffer": [{"well": elution_samps, "volume": elution_vol}], + "Lysis": [{"well": lysis_, "volume": 8120.0}], + "Stop": [{"well": stopreaction, "volume": 6400.0}], + "Wash 1": [{"well": wash1, "volume": 9500.0}], + "Wash 2": [{"well": wash2, "volume": 9500.0}], + "Wash 3": [{"well": wash3, "volume": 9500.0}], + "Wash 4": [{"well": wash4, "volume": 9500.0}], + "Wash 5": [{"well": wash5, "volume": 9500.0}], + "Wash 6": [{"well": wash6, "volume": 9500.0}], + "Wash 7": [{"well": wash7, "volume": 9500.0}], + "Wash 8": [{"well": wash8, "volume": 9500.0}], + "Wash 9": [{"well": wash9, "volume": 9500.0}], + } + + helpers.find_liquid_height_of_loaded_liquids(protocol, liquid_vols_and_wells, m1000) + + m1000.flow_rate.aspirate = 50 + m1000.flow_rate.dispense = 150 + m1000.flow_rate.blow_out = 300 + + def tiptrack(pip: InstrumentContext) -> None: + """Tip Track.""" + global tip_pick_up + global drop_count + pip.pick_up_tip() + tip_pick_up += 1 + drop_count = drop_count + 8 + if drop_count >= 250: + drop_count = 0 + if TIP_TRASH: + protocol.pause("Empty Trash bin.") + if tip_pick_up >= 59: + pip.reset_tipracks() + + def remove_supernatant(vol: float) -> None: + """Remove Supernatant.""" + protocol.comment("-----Removing Supernatant-----") + m1000.flow_rate.aspirate = 30 + num_trans = math.ceil(vol / 180) + vol_per_trans = vol / num_trans + + for i, m in enumerate(samples_m): + m1000.pick_up_tip(tips_sn[8 * i]) + loc = m.bottom(dot_bottom) + for _ in range(num_trans): + if m1000.current_volume > 0: + # void air gap if necessary + m1000.dispense(m1000.current_volume, m.top()) + m1000.move_to(m.center()) + m1000.transfer(vol_per_trans, loc, waste, new_tip="never", air_gap=20) + m1000.blow_out(waste) + m1000.air_gap(20) + m1000.drop_tip(tips_sn[8 * i]) if TIP_TRASH else m1000.return_tip() + m1000.flow_rate.aspirate = 300 + # Move Plate From Magnet to H-S + helpers.move_labware_to_hs(protocol, sample_plate, h_s, h_s_adapter) + + def bead_mixing( + well: Well, pip: InstrumentContext, mvol: float, reps: int = 8 + ) -> None: + """Bead Mixing. + + 'mixing' will mix liquid that contains beads. This will be done by + aspirating from the bottom of the well and dispensing from the top as to + mix the beads with the other liquids as much as possible. Aspiration and + dispensing will also be reversed for a short to to ensure maximal mixing. + param well: The current well that the mixing will occur in. + param pip: The pipet that is currently attached/ being used. + param mvol: The volume that is transferred before the mixing steps. + param reps: The number of mix repetitions that should occur. Note~ + During each mix rep, there are 2 cycles of aspirating from bottom, + dispensing at the top and 2 cycles of aspirating from middle, + dispensing at the bottom + """ + center = well.top().move(types.Point(x=0, y=0, z=5)) + aspbot = well.bottom().move(types.Point(x=0, y=0, z=1)) + asptop = well.bottom().move(types.Point(x=2, y=-2, z=1)) + disbot = well.bottom().move(types.Point(x=-2, y=1.5, z=2)) + distop = well.bottom().move(types.Point(x=0, y=0, z=6)) + + if mvol > 1000: + mvol = 1000 + + vol = mvol * 0.9 + + pip.flow_rate.aspirate = 500 + pip.flow_rate.dispense = 500 + + pip.move_to(center) + for _ in range(reps): + pip.aspirate(vol, aspbot) + pip.dispense(vol, distop) + pip.aspirate(vol, asptop) + pip.dispense(vol, disbot) + if _ == reps - 1: + pip.flow_rate.aspirate = 100 + pip.flow_rate.dispense = 75 + pip.aspirate(vol, aspbot) + pip.dispense(vol, aspbot) + + pip.flow_rate.aspirate = 300 + pip.flow_rate.dispense = 300 + + def mixing(well: Well, pip: InstrumentContext, mvol: float, reps: int = 8) -> None: + """Mixing. + + 'mixing' will mix liquid that contains beads. This will be done by + aspirating from the bottom of the well and dispensing from the top as to + mix the beads with the other liquids as much as possible. Aspiration and + dispensing will also be reversed for a short to to ensure maximal mixing. + param well: The current well that the mixing will occur in. + param pip: The pipet that is currently attached/ being used. + param mvol: The volume that is transferred before the mixing steps. + param reps: The number of mix repetitions that should occur. Note~ + During each mix rep, there are 2 cycles of aspirating from bottom, + dispensing at the top and 2 cycles of aspirating from middle, + dispensing at the bottom + """ + center = well.top(5) + asp = well.bottom(dot_bottom) + disp = well.top(-8) + + if mvol > 1000: + mvol = 1000 + + vol = mvol * 0.9 + + pip.flow_rate.aspirate = 500 + pip.flow_rate.dispense = 500 + + pip.move_to(center) + for _ in range(reps): + pip.aspirate(vol, asp) + pip.dispense(vol, disp) + pip.aspirate(vol, asp) + pip.dispense(vol, disp) + if _ == reps - 1: + pip.flow_rate.aspirate = 100 + pip.flow_rate.dispense = 75 + pip.aspirate(vol, asp) + pip.dispense(vol, asp) + + pip.flow_rate.aspirate = 300 + pip.flow_rate.dispense = 300 + + def lysis(vol: float, source: List[Well]) -> None: + """Lysis Steps.""" + tvol_total = 0.0 + protocol.comment("-----Beginning lysis steps-----") + num_transfers = math.ceil(vol / 180) + tiptrack(m1000) + src = source[0] + for i in range(num_cols): + tvol = vol / num_transfers + for t in range(num_transfers): + m1000.require_liquid_presence(src) + m1000.aspirate(tvol, src.bottom(1)) + m1000.dispense(m1000.current_volume, cells_m[i].top(-3)) + tvol_total += tvol * 8 + if tvol_total > 8000.0: + protocol.comment("-----Changing to second lysis well.------") + src = source[1] + protocol.comment(f"new source {src}") + tvol_total = 0.0 + + # mix after adding all reagent to wells with cells + for i in range(num_cols): + if i != 0: + tiptrack(m1000) + for x in range(8 if not dry_run else 1): + m1000.aspirate(tvol * 0.75, cells_m[i].bottom(dot_bottom)) + m1000.dispense(tvol * 0.75, cells_m[i].bottom(8)) + if x == 3: + protocol.delay(minutes=0.0167) + m1000.blow_out(cells_m[i].bottom(1)) + m1000.drop_tip() if TIP_TRASH else m1000.return_tip() + + helpers.set_hs_speed(protocol, h_s, heater_shaker_speed, lysis_time, True) + + def bind() -> None: + """Bind. + + `bind` will perform magnetic bead binding on each sample in the + deepwell plate. Each channel of binding beads will be mixed before + transfer, and the samples will be mixed with the binding beads after + the transfer. The magnetic deck activates after the addition to all + samples, and the supernatant is removed after bead binding. + :param vol (float): The amount of volume to aspirate from the elution + buffer source and dispense to each well containing + beads. + :param park (boolean): Whether to save sample-corresponding tips + between adding elution buffer and transferring + supernatant to the final clean elutions PCR + plate. + """ + protocol.comment("-----Beginning bind steps-----") + for i, well in enumerate(samples_m): + # Transfer cells+lysis/bind to wells with beads + tiptrack(m1000) + m1000.aspirate(185, cells_m[i].bottom(dot_bottom)) + m1000.air_gap(10) + m1000.dispense(m1000.current_volume, well.bottom(8)) + # Mix after transfer + bead_mixing(well, m1000, 130, reps=5 if not dry_run else 1) + m1000.air_gap(10) + m1000.drop_tip() if TIP_TRASH else m1000.return_tip() + helpers.set_hs_speed(protocol, h_s, heater_shaker_speed, bind_time, True) + + # Transfer from H-S plate to Magdeck plate + helpers.move_labware_from_hs_to_destination( + protocol, sample_plate, h_s, magblock + ) + + for bindi in np.arange( + settling_time, 0, -0.5 + ): # Settling time delay with countdown timer + protocol.delay( + minutes=0.5, + msg="There are " + str(bindi) + " minutes left in the incubation.", + ) + + # remove initial supernatant + remove_supernatant(180) + + def wash(vol: float, source: List[Well]) -> None: + """Wash Function.""" + global whichwash # Defines which wash the protocol is on to log on the app + protocol.comment("-----Now starting Wash #" + str(whichwash) + "-----") + global wash_volume_tracker + tiptrack(m1000) + num_trans = math.ceil(vol / 180) + vol_per_trans = vol / num_trans + for i, m in enumerate(samples_m): + src = source[whichwash] + for n in range(num_trans): + m1000.aspirate(vol_per_trans, src) + m1000.air_gap(10) + m1000.dispense(m1000.current_volume, m.top(-2)) + protocol.delay(seconds=2) + m1000.blow_out(m.top(-2)) + wash_volume_tracker += vol_per_trans * 8 + if wash_volume_tracker > 9600: + whichwash += 1 + src = source[whichwash] + protocol.comment(f"new wash source {whichwash}") + wash_volume_tracker = 0.0 + m1000.air_gap(10) + m1000.drop_tip() if TIP_TRASH else m1000.return_tip() + + # Shake for 5 minutes to mix wash with beads + helpers.set_hs_speed(protocol, h_s, heater_shaker_speed, wash_time, True) + + # Transfer from H-S plate to Magdeck plate + helpers.move_labware_from_hs_to_destination( + protocol, sample_plate, h_s, magblock + ) + + for washi in np.arange( + settling_time, 0, -0.5 + ): # settling time timer for washes + protocol.delay( + minutes=0.5, + msg="There are " + + str(washi) + + " minutes left in wash " + + str(whichwash) + + " incubation.", + ) + + remove_supernatant(vol) + protocol.comment(f"final wash source {whichwash}") + + def dnase(vol: float, source: List[Well]) -> None: + """Steps for DNAseI.""" + protocol.comment("-----DNAseI Steps Beginning-----") + num_trans = math.ceil(vol / 180) + vol_per_trans = vol / num_trans + tiptrack(m1000) + for i, m in enumerate(samples_m): + src = source[i] + m1000.flow_rate.aspirate = 10 + for n in range(num_trans): + if m1000.current_volume > 0: + m1000.dispense(m1000.current_volume, src.top()) + m1000.aspirate(vol_per_trans, src.bottom(dot_bottom)) + m1000.dispense(vol_per_trans, m.top(-3)) + m1000.blow_out(m.top(-3)) + m1000.air_gap(20) + + m1000.flow_rate.aspirate = 300 + + # Is this mixing needed? \/\/\/ + for i in range(num_cols): + if i != 0: + tiptrack(m1000) + mixing(samples_m[i], m1000, 45, reps=5 if not dry_run else 1) + m1000.drop_tip() if TIP_TRASH else m1000.return_tip() + + # Shake for 10 minutes to mix DNAseI + helpers.set_hs_speed(protocol, h_s, heater_shaker_speed, dnase_time, True) + + def stop_reaction(vol: float, source: Well) -> None: + """Adding stop solution.""" + protocol.comment("-----Adding Stop Solution-----") + tiptrack(m1000) + num_trans = math.ceil(vol / 180) + vol_per_trans = vol / num_trans + for i, m in enumerate(samples_m): + src = source + for n in range(num_trans): + if m1000.current_volume > 0: + m1000.dispense(m1000.current_volume, src.top()) + m1000.transfer(vol_per_trans, src, m.top(), air_gap=20, new_tip="never") + m1000.blow_out(m.top(-3)) + m1000.air_gap(20) + + m1000.drop_tip() if TIP_TRASH else m1000.return_tip() + + # Shake for 3 minutes to mix wash with beads + helpers.set_hs_speed(protocol, h_s, heater_shaker_speed, stop_time, True) + + # Transfer from H-S plate to Magdeck plate + helpers.move_labware_from_hs_to_destination( + protocol, sample_plate, h_s, magblock + ) + + for stop in np.arange(settling_time, 0, -0.5): + protocol.delay( + minutes=0.5, + msg="There are " + str(stop) + " minutes left in this incubation.", + ) + + remove_supernatant(vol + 50) + + def elute(vol: float) -> None: + """Elution.""" + protocol.comment("-----Elution Beginning-----") + tiptrack(m1000) + m1000.flow_rate.aspirate = 10 + for i, m in enumerate(samples_m): + loc = m.top(-2) + m1000.aspirate(vol, elution_solution[i]) + m1000.air_gap(10) + m1000.dispense(m1000.current_volume, loc) + m1000.blow_out(m.top(-3)) + m1000.air_gap(10) + + m1000.flow_rate.aspirate = 300 + + # Is this mixing needed? \/\/\/ + for i in range(num_cols): + if i != 0: + tiptrack(m1000) + for mixes in range(10): + m1000.aspirate(elution_vol - 10, samples_m[i]) + m1000.dispense(elution_vol - 10, samples_m[i].bottom(10)) + if mixes == 9: + m1000.flow_rate.dispense = 20 + m1000.aspirate(elution_vol - 10, samples_m[i]) + m1000.dispense(elution_vol - 10, samples_m[i].bottom(10)) + m1000.flow_rate.dispense = 300 + m1000.drop_tip() if TIP_TRASH else m1000.return_tip() + + # Shake for 3 minutes to mix wash with beads + helpers.set_hs_speed(protocol, h_s, heater_shaker_speed, elute_time, True) + + # Transfer from H-S plate to Magdeck plate + helpers.move_labware_from_hs_to_destination( + protocol, sample_plate, h_s, magblock + ) + + for elutei in np.arange(settling_time, 0, -0.5): + protocol.delay( + minutes=0.5, + msg="Incubating on MagDeck for " + str(elutei) + " more minutes.", + ) + + protocol.comment("-----Trasnferring Sample to Elution Plate-----") + for i, (m, e) in enumerate(zip(samples_m, elution_samples_m)): + tiptrack(m1000) + loc = m.bottom(dot_bottom) + m1000.transfer(vol, loc, e.bottom(5), air_gap=20, new_tip="never") + m1000.blow_out(e.top(-2)) + m1000.air_gap(20) + m1000.drop_tip() if TIP_TRASH else m1000.return_tip() + + """ + Here is where you can call the methods defined above to fit your specific + protocol. The normal sequence is: + """ + if inc_lysis: + lysis(lysis_vol, lysis_) + bind() + wash(wash_vol, all_washes) + wash(wash_vol, all_washes) + wash(wash_vol, all_washes) + # dnase1 treatment + dnase(dnase_vol, dnase1) + stop_reaction(stop_vol, stopreaction) + # Resume washes + wash(wash_vol, all_washes) + wash(wash_vol, all_washes) + wash(wash_vol, all_washes) + + for beaddry in np.arange(drybeads, 0, -0.5): + protocol.delay( + minutes=0.5, + msg="There are " + str(beaddry) + " minutes left in the drying step.", + ) + elute(elution_vol) + end_list_of_wells_to_probe = [waste_reservoir["A1"]] + helpers.clean_up_plates( + m1000, [elutionplate, sample_plate], waste_reservoir["A1"], 200 + ) + helpers.find_liquid_height_of_all_wells(protocol, m1000, end_list_of_wells_to_probe) + if deactivate_modules_bool: + helpers.deactivate_modules(protocol) diff --git a/abr-testing/abr_testing/protocols/csv_parameters/1_samplevols.csv b/abr-testing/abr_testing/protocols/csv_parameters/1_samplevols.csv new file mode 100644 index 00000000000..132b4dc70fb --- /dev/null +++ b/abr-testing/abr_testing/protocols/csv_parameters/1_samplevols.csv @@ -0,0 +1,97 @@ +Well,Dye,Diluent +A1,0,100 +B1,5,95 +C1,10,90 +D1,20,80 +E1,40,60 +F1,15,40 +G1,40,20 +H1,40,0 +A2,35,65 +B2,38,42 +C2,42,58 +D2,32,8 +E2,38,12 +F2,26,74 +G2,31,69 +H2,46,4 +A3,47,13 +B3,42,18 +C3,46,64 +D3,48,22 +E3,26,74 +F3,34,66 +G3,43,37 +H3,20,80 +A4,44,16 +B4,49,41 +C4,48,42 +D4,44,16 +E4,47,53 +F4,47,33 +G4,42,48 +H4,39,21 +A5,30,20 +B5,36,14 +C5,31,59 +D5,38,52 +E5,36,4 +F5,32,28 +G5,35,55 +H5,39,1 +A6,31,59 +B6,20,80 +C6,38,2 +D6,34,46 +E6,30,70 +F6,32,58 +G6,21,79 +H6,38,52 +A7,33,27 +B7,34,16 +C7,40,60 +D7,34,26 +E7,30,20 +F7,44,56 +G7,26,74 +H7,45,55 +A8,39,1 +B8,38,2 +C8,34,66 +D8,39,11 +E8,46,54 +F8,37,63 +G8,38,42 +H8,34,66 +A9,44,56 +B9,39,11 +C9,30,70 +D9,37,33 +E9,46,54 +F9,39,21 +G9,29,41 +H9,23,77 +A10,26,74 +B10,39,1 +C10,31,49 +D10,38,62 +E10,29,1 +F10,21,79 +G10,29,41 +H10,28,42 +A11,15,55 +B11,28,72 +C11,11,49 +D11,34,66 +E11,27,73 +F11,30,40 +G11,33,67 +H11,31,39 +A12,39,31 +B12,47,53 +C12,46,54 +D12,13,7 +E12,34,46 +F12,45,35 +G12,28,42 +H12,37,63 \ No newline at end of file diff --git a/abr-testing/abr_testing/protocols/csv_parameters/2_samplevols.csv b/abr-testing/abr_testing/protocols/csv_parameters/2_samplevols.csv new file mode 100644 index 00000000000..424aae072c3 --- /dev/null +++ b/abr-testing/abr_testing/protocols/csv_parameters/2_samplevols.csv @@ -0,0 +1,97 @@ +Destination Well,Water Transfer Volume (ul),DNA Transfer Volume (ul),Mastermix Volume (ul),Mastermix Source Tube +A1,3,7,40,A1 +B1,0,10,40,A1 +C1,10,0,40,A1 +D1,3,7,40,A1 +E1,2,8,40,A2 +F1,1,9,40,A2 +G1,5,5,40,A2 +H1,3,7,40,A2 +A2,3,7,40,A3 +B2,3,7,40,A3 +C2,3,7,40,A3 +D2,3,7,40,A3 +E2,3,7,40,A4 +F2,3,7,40,A4 +G2,3,7,40,A4 +H2,3,7,40,A4 +A3,3,7,40,A5 +B3,3,7,40,A5 +C3,3,7,45,A5 +D3,3,7,45,A5 +E3,3,5,45,A6 +F3,3,5,45,A6 +G3,3,5,45,A6 +H3,3,4,45,A6 +A4,3,7,40,A1 +B4,0,10,40,A1 +C4,10,0,40,A1 +D4,3,7,40,A1 +E4,2,8,40,A2 +F4,1,9,40,A2 +G4,5,5,40,A2 +H4,3,7,40,A2 +A5,3,7,40,A3 +B5,3,7,40,A3 +C5,3,7,40,A3 +D5,3,7,40,A3 +E5,3,7,40,A4 +F5,3,7,40,A4 +G5,3,7,40,A4 +H5,3,7,40,A4 +A6,3,7,40,A5 +B6,3,7,40,A5 +C6,3,7,45,A5 +D6,3,7,45,A5 +E6,3,5,45,A6 +F6,3,5,45,A6 +G6,3,5,45,A6 +H6,3,4,45,A6 +A7,3,7,40,A1 +B7,0,10,40,A1 +C7,10,0,40,A1 +D7,3,7,40,A1 +E7,2,8,40,A2 +F7,1,9,40,A2 +G7,5,5,40,A2 +H7,3,7,40,A2 +A8,3,7,40,A3 +B8,3,7,40,A3 +C8,3,7,40,A3 +D8,3,7,40,A3 +E8,3,7,40,A4 +F8,3,7,40,A4 +G8,3,7,40,A4 +H8,3,7,40,A4 +A9,3,7,40,A5 +B9,3,7,40,A5 +C9,3,7,45,A5 +D9,3,7,45,A5 +E9,3,5,45,A6 +F9,3,5,45,A6 +G9,3,5,45,A6 +H9,3,4,45,A6 +A10,3,7,40,A1 +B10,0,10,40,A1 +C10,10,0,40,A1 +D10,3,7,40,A1 +E10,2,8,40,A2 +F10,1,9,40,A2 +G10,5,5,40,A2 +H10,3,7,40,A2 +A11,3,7,40,A3 +B11,3,7,40,A3 +C11,3,7,40,A3 +D11,3,7,40,A3 +E11,3,7,40,A4 +F11,3,7,40,A4 +G11,3,7,40,A4 +H11,3,7,40,A4 +A12,3,7,40,A5 +B12,3,7,40,A5 +C12,3,7,45,A5 +D12,3,7,45,A5 +E12,3,5,45,A6 +F12,3,5,45,A6 +G12,3,5,45,A6 +H12,3,4,45,A6 diff --git a/abr-testing/abr_testing/protocols/custom_labware/hellma_reference_plate.json b/abr-testing/abr_testing/protocols/custom_labware/hellma_reference_plate.json new file mode 100644 index 00000000000..0abef3ca15f --- /dev/null +++ b/abr-testing/abr_testing/protocols/custom_labware/hellma_reference_plate.json @@ -0,0 +1,1017 @@ +{ + "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": "Hellma", + "brandId": ["666-R013 Reference Plate"] + }, + "metadata": { + "displayName": "Hellma Reference Plate", + "displayCategory": "wellPlate", + "displayVolumeUnits": "µL", + "tags": [] + }, + "dimensions": { + "xDimension": 127, + "yDimension": 85.5, + "zDimension": 13 + }, + "wells": { + "A1": { + "depth": 1, + "totalLiquidVolume": 1, + "shape": "circular", + "diameter": 6.6, + "x": 14.38, + "y": 74.26, + "z": 12 + }, + "B1": { + "depth": 1, + "totalLiquidVolume": 1, + "shape": "circular", + "diameter": 6.6, + "x": 14.38, + "y": 65.26, + "z": 12 + }, + "C1": { + "depth": 1, + "totalLiquidVolume": 1, + "shape": "circular", + "diameter": 6.6, + "x": 14.38, + "y": 56.26, + "z": 12 + }, + "D1": { + "depth": 1, + "totalLiquidVolume": 1, + "shape": "circular", + "diameter": 6.6, + "x": 14.38, + "y": 47.26, + "z": 12 + }, + "E1": { + "depth": 1, + "totalLiquidVolume": 1, + "shape": "circular", + "diameter": 6.6, + "x": 14.38, + "y": 38.26, + "z": 12 + }, + "F1": { + "depth": 1, + "totalLiquidVolume": 1, + "shape": "circular", + "diameter": 6.6, + "x": 14.38, + "y": 29.26, + "z": 12 + }, + "G1": { + "depth": 1, + "totalLiquidVolume": 1, + "shape": "circular", + "diameter": 6.6, + "x": 14.38, + "y": 20.26, + "z": 12 + }, + "H1": { + "depth": 1, + "totalLiquidVolume": 1, + "shape": "circular", + "diameter": 6.6, + "x": 14.38, + "y": 11.26, + "z": 12 + }, + "A2": { + "depth": 1, + "totalLiquidVolume": 1, + "shape": "circular", + "diameter": 6.6, + "x": 23.38, + "y": 74.26, + "z": 12 + }, + "B2": { + "depth": 1, + "totalLiquidVolume": 1, + "shape": "circular", + "diameter": 6.6, + "x": 23.38, + "y": 65.26, + "z": 12 + }, + "C2": { + "depth": 1, + "totalLiquidVolume": 1, + "shape": "circular", + "diameter": 6.6, + "x": 23.38, + "y": 56.26, + "z": 12 + }, + "D2": { + "depth": 1, + "totalLiquidVolume": 1, + "shape": "circular", + "diameter": 6.6, + "x": 23.38, + "y": 47.26, + "z": 12 + }, + "E2": { + "depth": 1, + "totalLiquidVolume": 1, + "shape": "circular", + "diameter": 6.6, + "x": 23.38, + "y": 38.26, + "z": 12 + }, + "F2": { + "depth": 1, + "totalLiquidVolume": 1, + "shape": "circular", + "diameter": 6.6, + "x": 23.38, + "y": 29.26, + "z": 12 + }, + "G2": { + "depth": 1, + "totalLiquidVolume": 1, + "shape": "circular", + "diameter": 6.6, + "x": 23.38, + "y": 20.26, + "z": 12 + }, + "H2": { + "depth": 1, + "totalLiquidVolume": 1, + "shape": "circular", + "diameter": 6.6, + "x": 23.38, + "y": 11.26, + "z": 12 + }, + "A3": { + "depth": 1, + "totalLiquidVolume": 1, + "shape": "circular", + "diameter": 6.6, + "x": 32.38, + "y": 74.26, + "z": 12 + }, + "B3": { + "depth": 1, + "totalLiquidVolume": 1, + "shape": "circular", + "diameter": 6.6, + "x": 32.38, + "y": 65.26, + "z": 12 + }, + "C3": { + "depth": 1, + "totalLiquidVolume": 1, + "shape": "circular", + "diameter": 6.6, + "x": 32.38, + "y": 56.26, + "z": 12 + }, + "D3": { + "depth": 1, + "totalLiquidVolume": 1, + "shape": "circular", + "diameter": 6.6, + "x": 32.38, + "y": 47.26, + "z": 12 + }, + "E3": { + "depth": 1, + "totalLiquidVolume": 1, + "shape": "circular", + "diameter": 6.6, + "x": 32.38, + "y": 38.26, + "z": 12 + }, + "F3": { + "depth": 1, + "totalLiquidVolume": 1, + "shape": "circular", + "diameter": 6.6, + "x": 32.38, + "y": 29.26, + "z": 12 + }, + "G3": { + "depth": 1, + "totalLiquidVolume": 1, + "shape": "circular", + "diameter": 6.6, + "x": 32.38, + "y": 20.26, + "z": 12 + }, + "H3": { + "depth": 1, + "totalLiquidVolume": 1, + "shape": "circular", + "diameter": 6.6, + "x": 32.38, + "y": 11.26, + "z": 12 + }, + "A4": { + "depth": 1, + "totalLiquidVolume": 1, + "shape": "circular", + "diameter": 6.6, + "x": 41.38, + "y": 74.26, + "z": 12 + }, + "B4": { + "depth": 1, + "totalLiquidVolume": 1, + "shape": "circular", + "diameter": 6.6, + "x": 41.38, + "y": 65.26, + "z": 12 + }, + "C4": { + "depth": 1, + "totalLiquidVolume": 1, + "shape": "circular", + "diameter": 6.6, + "x": 41.38, + "y": 56.26, + "z": 12 + }, + "D4": { + "depth": 1, + "totalLiquidVolume": 1, + "shape": "circular", + "diameter": 6.6, + "x": 41.38, + "y": 47.26, + "z": 12 + }, + "E4": { + "depth": 1, + "totalLiquidVolume": 1, + "shape": "circular", + "diameter": 6.6, + "x": 41.38, + "y": 38.26, + "z": 12 + }, + "F4": { + "depth": 1, + "totalLiquidVolume": 1, + "shape": "circular", + "diameter": 6.6, + "x": 41.38, + "y": 29.26, + "z": 12 + }, + "G4": { + "depth": 1, + "totalLiquidVolume": 1, + "shape": "circular", + "diameter": 6.6, + "x": 41.38, + "y": 20.26, + "z": 12 + }, + "H4": { + "depth": 1, + "totalLiquidVolume": 1, + "shape": "circular", + "diameter": 6.6, + "x": 41.38, + "y": 11.26, + "z": 12 + }, + "A5": { + "depth": 1, + "totalLiquidVolume": 1, + "shape": "circular", + "diameter": 6.6, + "x": 50.38, + "y": 74.26, + "z": 12 + }, + "B5": { + "depth": 1, + "totalLiquidVolume": 1, + "shape": "circular", + "diameter": 6.6, + "x": 50.38, + "y": 65.26, + "z": 12 + }, + "C5": { + "depth": 1, + "totalLiquidVolume": 1, + "shape": "circular", + "diameter": 6.6, + "x": 50.38, + "y": 56.26, + "z": 12 + }, + "D5": { + "depth": 1, + "totalLiquidVolume": 1, + "shape": "circular", + "diameter": 6.6, + "x": 50.38, + "y": 47.26, + "z": 12 + }, + "E5": { + "depth": 1, + "totalLiquidVolume": 1, + "shape": "circular", + "diameter": 6.6, + "x": 50.38, + "y": 38.26, + "z": 12 + }, + "F5": { + "depth": 1, + "totalLiquidVolume": 1, + "shape": "circular", + "diameter": 6.6, + "x": 50.38, + "y": 29.26, + "z": 12 + }, + "G5": { + "depth": 1, + "totalLiquidVolume": 1, + "shape": "circular", + "diameter": 6.6, + "x": 50.38, + "y": 20.26, + "z": 12 + }, + "H5": { + "depth": 1, + "totalLiquidVolume": 1, + "shape": "circular", + "diameter": 6.6, + "x": 50.38, + "y": 11.26, + "z": 12 + }, + "A6": { + "depth": 1, + "totalLiquidVolume": 1, + "shape": "circular", + "diameter": 6.6, + "x": 59.38, + "y": 74.26, + "z": 12 + }, + "B6": { + "depth": 1, + "totalLiquidVolume": 1, + "shape": "circular", + "diameter": 6.6, + "x": 59.38, + "y": 65.26, + "z": 12 + }, + "C6": { + "depth": 1, + "totalLiquidVolume": 1, + "shape": "circular", + "diameter": 6.6, + "x": 59.38, + "y": 56.26, + "z": 12 + }, + "D6": { + "depth": 1, + "totalLiquidVolume": 1, + "shape": "circular", + "diameter": 6.6, + "x": 59.38, + "y": 47.26, + "z": 12 + }, + "E6": { + "depth": 1, + "totalLiquidVolume": 1, + "shape": "circular", + "diameter": 6.6, + "x": 59.38, + "y": 38.26, + "z": 12 + }, + "F6": { + "depth": 1, + "totalLiquidVolume": 1, + "shape": "circular", + "diameter": 6.6, + "x": 59.38, + "y": 29.26, + "z": 12 + }, + "G6": { + "depth": 1, + "totalLiquidVolume": 1, + "shape": "circular", + "diameter": 6.6, + "x": 59.38, + "y": 20.26, + "z": 12 + }, + "H6": { + "depth": 1, + "totalLiquidVolume": 1, + "shape": "circular", + "diameter": 6.6, + "x": 59.38, + "y": 11.26, + "z": 12 + }, + "A7": { + "depth": 1, + "totalLiquidVolume": 1, + "shape": "circular", + "diameter": 6.6, + "x": 68.38, + "y": 74.26, + "z": 12 + }, + "B7": { + "depth": 1, + "totalLiquidVolume": 1, + "shape": "circular", + "diameter": 6.6, + "x": 68.38, + "y": 65.26, + "z": 12 + }, + "C7": { + "depth": 1, + "totalLiquidVolume": 1, + "shape": "circular", + "diameter": 6.6, + "x": 68.38, + "y": 56.26, + "z": 12 + }, + "D7": { + "depth": 1, + "totalLiquidVolume": 1, + "shape": "circular", + "diameter": 6.6, + "x": 68.38, + "y": 47.26, + "z": 12 + }, + "E7": { + "depth": 1, + "totalLiquidVolume": 1, + "shape": "circular", + "diameter": 6.6, + "x": 68.38, + "y": 38.26, + "z": 12 + }, + "F7": { + "depth": 1, + "totalLiquidVolume": 1, + "shape": "circular", + "diameter": 6.6, + "x": 68.38, + "y": 29.26, + "z": 12 + }, + "G7": { + "depth": 1, + "totalLiquidVolume": 1, + "shape": "circular", + "diameter": 6.6, + "x": 68.38, + "y": 20.26, + "z": 12 + }, + "H7": { + "depth": 1, + "totalLiquidVolume": 1, + "shape": "circular", + "diameter": 6.6, + "x": 68.38, + "y": 11.26, + "z": 12 + }, + "A8": { + "depth": 1, + "totalLiquidVolume": 1, + "shape": "circular", + "diameter": 6.6, + "x": 77.38, + "y": 74.26, + "z": 12 + }, + "B8": { + "depth": 1, + "totalLiquidVolume": 1, + "shape": "circular", + "diameter": 6.6, + "x": 77.38, + "y": 65.26, + "z": 12 + }, + "C8": { + "depth": 1, + "totalLiquidVolume": 1, + "shape": "circular", + "diameter": 6.6, + "x": 77.38, + "y": 56.26, + "z": 12 + }, + "D8": { + "depth": 1, + "totalLiquidVolume": 1, + "shape": "circular", + "diameter": 6.6, + "x": 77.38, + "y": 47.26, + "z": 12 + }, + "E8": { + "depth": 1, + "totalLiquidVolume": 1, + "shape": "circular", + "diameter": 6.6, + "x": 77.38, + "y": 38.26, + "z": 12 + }, + "F8": { + "depth": 1, + "totalLiquidVolume": 1, + "shape": "circular", + "diameter": 6.6, + "x": 77.38, + "y": 29.26, + "z": 12 + }, + "G8": { + "depth": 1, + "totalLiquidVolume": 1, + "shape": "circular", + "diameter": 6.6, + "x": 77.38, + "y": 20.26, + "z": 12 + }, + "H8": { + "depth": 1, + "totalLiquidVolume": 1, + "shape": "circular", + "diameter": 6.6, + "x": 77.38, + "y": 11.26, + "z": 12 + }, + "A9": { + "depth": 1, + "totalLiquidVolume": 1, + "shape": "circular", + "diameter": 6.6, + "x": 86.38, + "y": 74.26, + "z": 12 + }, + "B9": { + "depth": 1, + "totalLiquidVolume": 1, + "shape": "circular", + "diameter": 6.6, + "x": 86.38, + "y": 65.26, + "z": 12 + }, + "C9": { + "depth": 1, + "totalLiquidVolume": 1, + "shape": "circular", + "diameter": 6.6, + "x": 86.38, + "y": 56.26, + "z": 12 + }, + "D9": { + "depth": 1, + "totalLiquidVolume": 1, + "shape": "circular", + "diameter": 6.6, + "x": 86.38, + "y": 47.26, + "z": 12 + }, + "E9": { + "depth": 1, + "totalLiquidVolume": 1, + "shape": "circular", + "diameter": 6.6, + "x": 86.38, + "y": 38.26, + "z": 12 + }, + "F9": { + "depth": 1, + "totalLiquidVolume": 1, + "shape": "circular", + "diameter": 6.6, + "x": 86.38, + "y": 29.26, + "z": 12 + }, + "G9": { + "depth": 1, + "totalLiquidVolume": 1, + "shape": "circular", + "diameter": 6.6, + "x": 86.38, + "y": 20.26, + "z": 12 + }, + "H9": { + "depth": 1, + "totalLiquidVolume": 1, + "shape": "circular", + "diameter": 6.6, + "x": 86.38, + "y": 11.26, + "z": 12 + }, + "A10": { + "depth": 1, + "totalLiquidVolume": 1, + "shape": "circular", + "diameter": 6.6, + "x": 95.38, + "y": 74.26, + "z": 12 + }, + "B10": { + "depth": 1, + "totalLiquidVolume": 1, + "shape": "circular", + "diameter": 6.6, + "x": 95.38, + "y": 65.26, + "z": 12 + }, + "C10": { + "depth": 1, + "totalLiquidVolume": 1, + "shape": "circular", + "diameter": 6.6, + "x": 95.38, + "y": 56.26, + "z": 12 + }, + "D10": { + "depth": 1, + "totalLiquidVolume": 1, + "shape": "circular", + "diameter": 6.6, + "x": 95.38, + "y": 47.26, + "z": 12 + }, + "E10": { + "depth": 1, + "totalLiquidVolume": 1, + "shape": "circular", + "diameter": 6.6, + "x": 95.38, + "y": 38.26, + "z": 12 + }, + "F10": { + "depth": 1, + "totalLiquidVolume": 1, + "shape": "circular", + "diameter": 6.6, + "x": 95.38, + "y": 29.26, + "z": 12 + }, + "G10": { + "depth": 1, + "totalLiquidVolume": 1, + "shape": "circular", + "diameter": 6.6, + "x": 95.38, + "y": 20.26, + "z": 12 + }, + "H10": { + "depth": 1, + "totalLiquidVolume": 1, + "shape": "circular", + "diameter": 6.6, + "x": 95.38, + "y": 11.26, + "z": 12 + }, + "A11": { + "depth": 1, + "totalLiquidVolume": 1, + "shape": "circular", + "diameter": 6.6, + "x": 104.38, + "y": 74.26, + "z": 12 + }, + "B11": { + "depth": 1, + "totalLiquidVolume": 1, + "shape": "circular", + "diameter": 6.6, + "x": 104.38, + "y": 65.26, + "z": 12 + }, + "C11": { + "depth": 1, + "totalLiquidVolume": 1, + "shape": "circular", + "diameter": 6.6, + "x": 104.38, + "y": 56.26, + "z": 12 + }, + "D11": { + "depth": 1, + "totalLiquidVolume": 1, + "shape": "circular", + "diameter": 6.6, + "x": 104.38, + "y": 47.26, + "z": 12 + }, + "E11": { + "depth": 1, + "totalLiquidVolume": 1, + "shape": "circular", + "diameter": 6.6, + "x": 104.38, + "y": 38.26, + "z": 12 + }, + "F11": { + "depth": 1, + "totalLiquidVolume": 1, + "shape": "circular", + "diameter": 6.6, + "x": 104.38, + "y": 29.26, + "z": 12 + }, + "G11": { + "depth": 1, + "totalLiquidVolume": 1, + "shape": "circular", + "diameter": 6.6, + "x": 104.38, + "y": 20.26, + "z": 12 + }, + "H11": { + "depth": 1, + "totalLiquidVolume": 1, + "shape": "circular", + "diameter": 6.6, + "x": 104.38, + "y": 11.26, + "z": 12 + }, + "A12": { + "depth": 1, + "totalLiquidVolume": 1, + "shape": "circular", + "diameter": 6.6, + "x": 113.38, + "y": 74.26, + "z": 12 + }, + "B12": { + "depth": 1, + "totalLiquidVolume": 1, + "shape": "circular", + "diameter": 6.6, + "x": 113.38, + "y": 65.26, + "z": 12 + }, + "C12": { + "depth": 1, + "totalLiquidVolume": 1, + "shape": "circular", + "diameter": 6.6, + "x": 113.38, + "y": 56.26, + "z": 12 + }, + "D12": { + "depth": 1, + "totalLiquidVolume": 1, + "shape": "circular", + "diameter": 6.6, + "x": 113.38, + "y": 47.26, + "z": 12 + }, + "E12": { + "depth": 1, + "totalLiquidVolume": 1, + "shape": "circular", + "diameter": 6.6, + "x": 113.38, + "y": 38.26, + "z": 12 + }, + "F12": { + "depth": 1, + "totalLiquidVolume": 1, + "shape": "circular", + "diameter": 6.6, + "x": 113.38, + "y": 29.26, + "z": 12 + }, + "G12": { + "depth": 1, + "totalLiquidVolume": 1, + "shape": "circular", + "diameter": 6.6, + "x": 113.38, + "y": 20.26, + "z": 12 + }, + "H12": { + "depth": 1, + "totalLiquidVolume": 1, + "shape": "circular", + "diameter": 6.6, + "x": 113.38, + "y": 11.26, + "z": 12 + } + }, + "groups": [ + { + "metadata": { + "wellBottomShape": "flat" + }, + "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": "irregular", + "quirks": [], + "isTiprack": false, + "isMagneticModuleCompatible": false, + "loadName": "hellma_reference_plate" + }, + "namespace": "custom_beta", + "version": 1, + "schemaVersion": 2, + "cornerOffsetFromSlot": { + "x": 0, + "y": 0, + "z": 0 + } +} diff --git a/abr-testing/abr_testing/protocols/helpers.py b/abr-testing/abr_testing/protocols/helpers.py new file mode 100644 index 00000000000..31a1d1a9244 --- /dev/null +++ b/abr-testing/abr_testing/protocols/helpers.py @@ -0,0 +1,672 @@ +"""Helper functions commonly used in protocols.""" + +from opentrons.protocol_api import ( + ProtocolContext, + Labware, + InstrumentContext, + ParameterContext, + Well, +) +from opentrons.protocol_api.module_contexts import ( + HeaterShakerContext, + MagneticBlockContext, + ThermocyclerContext, + TemperatureModuleContext, + MagneticModuleContext, + AbsorbanceReaderContext, +) +from typing import List, Union, Dict, Tuple +from opentrons.hardware_control.modules.types import ThermocyclerStep +from opentrons_shared_data.errors.exceptions import PipetteLiquidNotFoundError + +# FUNCTIONS FOR LOADING COMMON CONFIGURATIONS + + +def load_common_liquid_setup_labware_and_instruments( + protocol: ProtocolContext, +) -> Tuple[Labware, Labware, InstrumentContext]: + """Load Commonly used Labware and Instruments.""" + # Tip rack + tip_rack = protocol.load_labware("opentrons_flex_96_tiprack_1000ul", "D1") + # Pipette + p1000 = protocol.load_instrument( + instrument_name="flex_8channel_1000", mount="left", tip_racks=[tip_rack] + ) + # Source_reservoir + source_reservoir = protocol.load_labware("nest_1_reservoir_290ml", "C2") + protocol.load_trash_bin("A3") + return source_reservoir, tip_rack, p1000 + + +def load_disposable_lids( + protocol: ProtocolContext, + num_of_lids: int, + deck_slot: List[str], + deck_riser: bool = False, +) -> List[Labware]: + """Load Stack of Disposable lids.""" + if deck_riser: + deck_riser_adapter = protocol.load_adapter( + "opentrons_flex_deck_riser", deck_slot[0] + ) + unused_lids = [ + deck_riser_adapter.load_labware("opentrons_tough_pcr_auto_sealing_lid") + ] + else: + unused_lids = [ + protocol.load_labware("opentrons_tough_pcr_auto_sealing_lid", deck_slot[0]) + ] + + if len(deck_slot) == 1: + for i in range(num_of_lids - 1): + unused_lids.append( + unused_lids[-1].load_labware("opentrons_tough_pcr_auto_sealing_lid") + ) + else: + for i in range(len(deck_slot) - 1): + unused_lids.append( + protocol.load_labware( + "opentrons_tough_pcr_auto_sealing_lid", deck_slot[i] + ) + ) + unused_lids.reverse() + return unused_lids + + +def load_hs_adapter_and_labware( + labware_str: str, heatershaker: HeaterShakerContext, labware_name: str +) -> Tuple[Labware, Labware]: + """Load appropriate adapter on heatershaker based off labware type.""" + heatershaker_adapters = { + "nest_96_wellplate_2ml_deep": "opentrons_96_deep_well_adapter", + "armadillo_96_wellplate_200ul_pcr_full_skirt": "opentrons_96_pcr_adapter", + } + hs_adapter_type = heatershaker_adapters.get(labware_str, "") + if hs_adapter_type: + hs_adapter = heatershaker.load_adapter(hs_adapter_type) + labware_on_hs = hs_adapter.load_labware(labware_str, labware_name) + else: + heatershaker.load_labware(labware_str, labware_name) + return labware_on_hs, hs_adapter + + +def load_temp_adapter_and_labware( + labware_str: str, temp_mod: TemperatureModuleContext, labware_name: str +) -> Tuple[Labware, Labware]: + """Load appropriate adapter on temperature module based off labware type.""" + temp_mod_adapters = { + "nest_96_wellplate_2ml_deep": "opentrons_96_deep_well_temp_mod_adapter", + "armadillo_96_wellplate_200ul_pcr_full_skirt": "opentrons_96_well_aluminum_block", + } + temp_adapter_type = temp_mod_adapters.get(labware_str, "") + if temp_adapter_type: + temp_adapter = temp_mod.load_adapter(temp_adapter_type) + labware_on_temp_mod = temp_adapter.load_labware(labware_str, labware_name) + else: + labware_on_temp_mod = temp_mod.load_labware(labware_str, labware_name) + return labware_on_temp_mod, temp_adapter + + +# FUNCTIONS FOR COMMON COMMENTS + + +def comment_protocol_version(protocol: ProtocolContext, version: str) -> None: + """Comment version number of protocol.""" + protocol.comment(f"Protocol Version: {version}") + + +# FUNCTIONS FOR LOADING COMMON PARAMETERS +def create_channel_parameter(parameters: ParameterContext) -> None: + """Create pipette channel parameter.""" + parameters.add_str( + variable_name="channels", + display_name="Number of Pipette Channels", + choices=[ + {"display_name": "1 Channel", "value": "1channel"}, + {"display_name": "8 Channel", "value": "8channel"}, + ], + default="8channel", + ) + + +def create_pipette_parameters(parameters: ParameterContext) -> None: + """Create parameter for pipettes.""" + # NOTE: Place function inside def add_parameters(parameters) in protocol. + # NOTE: Copy ctx.params.left mount, ctx.params.right_mount # type: ignore[attr-defined] + # to get result + # Left Mount + parameters.add_str( + variable_name="left_mount", + display_name="Left Mount", + description="Pipette Type on Left Mount.", + choices=[ + {"display_name": "8ch 50ul", "value": "flex_8channel_50"}, + {"display_name": "8ch 1000ul", "value": "flex_8channel_1000"}, + {"display_name": "1ch 50ul", "value": "flex_1channel_50"}, + {"display_name": "1ch 1000ul", "value": "flex_1channel_1000"}, + {"display_name": "96ch 1000ul", "value": "flex_96channel_1000"}, + {"display_name": "None", "value": "none"}, + ], + default="flex_8channel_1000", + ) + # Right Mount + parameters.add_str( + variable_name="right_mount", + display_name="Right Mount", + description="Pipette Type on Right Mount.", + choices=[ + {"display_name": "8ch 50ul", "value": "flex_8channel_50"}, + {"display_name": "8ch 1000ul", "value": "flex_8channel_1000"}, + {"display_name": "1ch 50ul", "value": "flex_1channel_50"}, + {"display_name": "1ch 1000ul", "value": "flex_1channel_1000"}, + {"display_name": "None", "value": "none"}, + ], + default="none", + ) + + +def create_single_pipette_mount_parameter(parameters: ParameterContext) -> None: + """Create parameter to specify pipette mount.""" + parameters.add_str( + variable_name="pipette_mount", + display_name="Pipette Mount", + choices=[ + {"display_name": "Left", "value": "left"}, + {"display_name": "Right", "value": "right"}, + ], + default="left", + ) + + +def create_two_pipette_mount_parameters(parameters: ParameterContext) -> None: + """Create mount parameters for 2 pipettes.""" + parameters.add_str( + variable_name="pipette_mount_1", + display_name="Pipette Mount 1", + choices=[ + {"display_name": "Left", "value": "left"}, + {"display_name": "Right", "value": "right"}, + ], + default="left", + ) + parameters.add_str( + variable_name="pipette_mount_2", + display_name="Pipette Mount 2", + choices=[ + {"display_name": "Left", "value": "left"}, + {"display_name": "Right", "value": "right"}, + ], + default="right", + ) + + +def create_csv_parameter(parameters: ParameterContext) -> None: + """Create parameter for sample volume csvs.""" + parameters.add_csv_file( + variable_name="parameters_csv", + display_name="Sample CSV", + description="CSV File for Protocol.", + ) + + +def create_disposable_lid_parameter(parameters: ParameterContext) -> None: + """Create parameter to use/not use disposable lid.""" + parameters.add_bool( + variable_name="disposable_lid", + display_name="Disposable Lid", + description="True means use lid.", + default=True, + ) + + +def create_disposable_lid_trash_location(parameters: ParameterContext) -> None: + """Create a parameter for lid placement after use.""" + parameters.add_bool( + variable_name="trash_lid", + display_name="Trash Disposable Lid", + description="True means trash lid, false means keep on deck.", + default=True, + ) + + +def create_tc_lid_deck_riser_parameter(parameters: ParameterContext) -> None: + """Create parameter for tc lid deck riser.""" + parameters.add_bool( + variable_name="deck_riser", + display_name="Deck Riser", + description="True means use deck riser.", + default=False, + ) + + +def create_tip_size_parameter(parameters: ParameterContext) -> None: + """Create parameter for tip size.""" + parameters.add_str( + variable_name="tip_size", + display_name="Tip Size", + description="Set Tip Size", + choices=[ + {"display_name": "50 uL", "value": "opentrons_flex_96_tiprack_50ul"}, + {"display_name": "200 µL", "value": "opentrons_flex_96_tiprack_200ul"}, + {"display_name": "1000 µL", "value": "opentrons_flex_96_tiprack_1000ul"}, + ], + default="opentrons_flex_96_tiprack_50ul", + ) + + +def create_dot_bottom_parameter(parameters: ParameterContext) -> None: + """Create parameter for dot bottom value.""" + parameters.add_float( + variable_name="dot_bottom", + display_name=".bottom", + description="Lowest value pipette will go to.", + default=0.3, + choices=[ + {"display_name": "0.0", "value": 0.0}, + {"display_name": "0.1", "value": 0.1}, + {"display_name": "0.2", "value": 0.2}, + {"display_name": "0.3", "value": 0.3}, + {"display_name": "0.4", "value": 0.4}, + {"display_name": "0.5", "value": 0.5}, + {"display_name": "0.6", "value": 0.6}, + {"display_name": "0.7", "value": 0.7}, + {"display_name": "0.8", "value": 0.8}, + {"display_name": "0.9", "value": 0.9}, + {"display_name": "1.0", "value": 1.0}, + ], + ) + + +def create_hs_speed_parameter(parameters: ParameterContext) -> None: + """Create parameter for max heatershaker speed.""" + parameters.add_int( + variable_name="heater_shaker_speed", + display_name="Heater Shaker Shake Speed", + description="Speed to set the heater shaker to", + default=2000, + minimum=200, + maximum=3000, + unit="rpm", + ) + + +def create_plate_reader_compatible_labware_parameter( + parameters: ParameterContext, +) -> None: + """Create parameter for flat bottom plates compatible with plate reader.""" + parameters.add_str( + variable_name="labware_plate_reader_compatible", + display_name="Plate Reader Labware", + default="nest_96_wellplate_200ul_flat", + choices=[ + { + "display_name": "Corning_96well", + "value": "corning_96_wellplate_360ul_flat", + }, + {"display_name": "Hellma Plate", "value": "hellma_reference_plate"}, + {"display_name": "Nest_96well", "value": "nest_96_wellplate_200ul_flat"}, + ], + ) + + +def create_tc_compatible_labware_parameter(parameters: ParameterContext) -> None: + """Create parameter for labware type compatible with thermocycler.""" + parameters.add_str( + variable_name="labware_tc_compatible", + display_name="Labware Type for Thermocycler", + description="labware compatible with thermocycler.", + default="biorad_96_wellplate_200ul_pcr", + choices=[ + { + "display_name": "Armadillo_200ul", + "value": "armadillo_96_wellplate_200ul_pcr_full_skirt", + }, + {"display_name": "Bio-Rad_200ul", "value": "biorad_96_wellplate_200ul_pcr"}, + { + "display_name": "NEST_100ul", + "value": "nest_96_wellplate_100ul_pcr_full_skirt", + }, + { + "display_name": "Opentrons_200ul", + "value": "opentrons_96_wellplate_200ul_pcr_full_skirt", + }, + ], + ) + + +def create_deactivate_modules_parameter(parameters: ParameterContext) -> None: + """Create parameter for deactivating modules at the end fof run.""" + parameters.add_bool( + variable_name="deactivate_modules", + display_name="Deactivate Modules", + description="deactivate all modules at end of run", + default=True, + ) + + +# FUNCTIONS FOR COMMON MODULE SEQUENCES +def deactivate_modules(protocol: ProtocolContext) -> None: + """Deactivate all loaded modules.""" + print("Deactivating Modules") + modules = protocol.loaded_modules + + if modules: + for module in modules.values(): + if isinstance(module, HeaterShakerContext): + module.deactivate_shaker() + module.deactivate_heater() + elif isinstance(module, TemperatureModuleContext): + module.deactivate() + elif isinstance(module, MagneticModuleContext): + module.disengage() + elif isinstance(module, ThermocyclerContext): + module.deactivate() + + +def move_labware_from_hs_to_destination( + protocol: ProtocolContext, + labware_to_move: Labware, + hs: HeaterShakerContext, + new_module: Union[MagneticBlockContext, ThermocyclerContext], +) -> None: + """Move labware from heatershaker to magnetic block.""" + hs.open_labware_latch() + protocol.move_labware(labware_to_move, new_module, use_gripper=True) + hs.close_labware_latch() + + +def move_labware_to_hs( + protocol: ProtocolContext, + labware_to_move: Labware, + hs: HeaterShakerContext, + hs_adapter: Union[Labware, HeaterShakerContext], +) -> None: + """Move labware to heatershaker.""" + hs.open_labware_latch() + protocol.move_labware(labware_to_move, hs_adapter, use_gripper=True) + hs.close_labware_latch() + + +def set_hs_speed( + protocol: ProtocolContext, + hs: HeaterShakerContext, + hs_speed: int, + time_min: float, + deactivate: bool, +) -> None: + """Set heatershaker for a speed and duration.""" + hs.close_labware_latch() + hs.set_and_wait_for_shake_speed(hs_speed) + protocol.delay( + minutes=time_min, + msg=f"Shake at {hs_speed} rpm for {time_min} minutes.", + ) + if deactivate: + hs.deactivate_shaker() + + +def use_disposable_lid_with_tc( + protocol: ProtocolContext, + unused_lids: List[Labware], + used_lids: List[Labware], + plate_in_thermocycler: Labware, + thermocycler: ThermocyclerContext, +) -> Tuple[Labware, List[Labware], List[Labware]]: + """Use disposable lid with thermocycler.""" + lid_on_plate = unused_lids[0] + protocol.move_labware(lid_on_plate, plate_in_thermocycler, use_gripper=True) + # Remove lid from the list + unused_lids.pop(0) + used_lids.append(lid_on_plate) + thermocycler.close_lid() + return lid_on_plate, unused_lids, used_lids + + +# FUNCTIONS FOR COMMON PIPETTE COMMAND SEQUENCES + + +def clean_up_plates( + pipette: InstrumentContext, + list_of_labware: List[Labware], + liquid_waste: Well, + tip_size: int, +) -> None: + """Aspirate liquid from labware and dispense into liquid waste.""" + pipette.pick_up_tip() + pipette.liquid_presence_detection = False + num_of_active_channels = pipette.active_channels + for labware in list_of_labware: + if num_of_active_channels == 8: + list_of_wells = labware.rows()[0] + elif num_of_active_channels == 1: + list_of_wells = labware.wells() + elif num_of_active_channels == 96: + list_of_wells = [labware.wells()[0]] + for well in list_of_wells: + vol_removed = 0.0 + while well.max_volume > vol_removed: + pipette.aspirate(tip_size, well) + pipette.dispense( + tip_size, + liquid_waste.top(), + ) + pipette.blow_out(liquid_waste.top()) + vol_removed += pipette.max_volume + if pipette.channels != num_of_active_channels: + pipette.drop_tip() + else: + pipette.return_tip() + + +def find_liquid_height(pipette: InstrumentContext, well_to_probe: Well) -> float: + """Find liquid height of well.""" + try: + liquid_height = ( + pipette.measure_liquid_height(well_to_probe) + - well_to_probe.bottom().point.z + ) + except PipetteLiquidNotFoundError: + liquid_height = 0 + return liquid_height + + +def load_wells_with_custom_liquids( + protocol: ProtocolContext, + liquid_vols_and_wells: Dict[str, List[Dict[str, Union[Well, List[Well], float]]]], +) -> None: + """Load custom liquids into wells.""" + liquid_colors = [ + "#008000", + "#A52A2A", + "#00FFFF", + "#0000FF", + "#800080", + "#ADD8E6", + "#FF0000", + "#FFFF00", + "#FF00FF", + "#00008B", + "#7FFFD4", + "#FFC0CB", + "#FFA500", + "#00FF00", + "#C0C0C0", + ] + i = 0 + volume = 0.0 + for liquid_name, wells_info in liquid_vols_and_wells.items(): + # Define the liquid with a color + liquid = protocol.define_liquid( + liquid_name, display_color=liquid_colors[i % len(liquid_colors)] + ) + i += 1 + # Load liquid into each specified well or list of wells + for well_info in wells_info: + if isinstance(well_info["well"], list): + wells = well_info["well"] + elif isinstance(well_info["well"], Well): + wells = [well_info["well"]] + else: + wells = [] + if isinstance(well_info["volume"], float): + volume = well_info["volume"] + + # Load liquid into each well + for well in wells: + well.load_liquid(liquid, volume) + + +def comment_height_of_specific_labware( + protocol: ProtocolContext, labware_name: str, dict_of_labware_heights: Dict +) -> None: + """Comment height found of specific labware.""" + total_height = 0.0 + for key in dict_of_labware_heights.keys(): + if key[0] == labware_name: + height = dict_of_labware_heights[key] + total_height += height + protocol.comment(f"Liquid Waste Total Height: {total_height}") + + +def find_liquid_height_of_all_wells( + protocol: ProtocolContext, + pipette: InstrumentContext, + wells: List[Well], +) -> Dict: + """Find the liquid height of all wells in protocol.""" + dict_of_labware_heights = {} + pipette.pick_up_tip() + pip_channels = pipette.active_channels + for well in wells: + labware_name = well.parent.name + total_number_of_wells_in_plate = len(well.parent.wells()) + # if pip_channels is > 1 and total_wells > 12 - only probe 1st row. + if ( + pip_channels > 1 + and total_number_of_wells_in_plate > 12 + and well.well_name.startswith("A") + ): + liquid_height_of_well = find_liquid_height(pipette, well) + dict_of_labware_heights[labware_name, well] = liquid_height_of_well + elif total_number_of_wells_in_plate <= 12: + liquid_height_of_well = find_liquid_height(pipette, well) + dict_of_labware_heights[labware_name, well] = liquid_height_of_well + if pip_channels != pipette.channels: + pipette.drop_tip() + else: + pipette.return_tip() + pipette.reset_tipracks() + msg = f"result: {dict_of_labware_heights}" + protocol.comment(msg=msg) + comment_height_of_specific_labware( + protocol, "Liquid Waste", dict_of_labware_heights + ) + return dict_of_labware_heights + + +def find_liquid_height_of_loaded_liquids( + ctx: ProtocolContext, + liquid_vols_and_wells: Dict[str, List[Dict[str, Union[Well, List[Well], float]]]], + pipette: InstrumentContext, +) -> List[Well]: + """Find Liquid height of loaded liquids.""" + load_wells_with_custom_liquids(ctx, liquid_vols_and_wells) + # Get flattened list of wells. + wells: list[Well] = [ + well + for items in liquid_vols_and_wells.values() + for entry in items + if isinstance(entry["well"], (Well, list)) and entry["volume"] != 0.0 + # Ensure "well" is Well or list of Well + for well in ( + entry["well"] if isinstance(entry["well"], list) else [entry["well"]] + ) + ] + if pipette.active_channels == 96: + wells = [well for well in wells if well.display_name.split(" ")[0] == "A1"] + find_liquid_height_of_all_wells(ctx, pipette, wells) + return wells + + +def load_wells_with_water( + protocol: ProtocolContext, wells: List[Well], volumes: List[float] +) -> None: + """Load liquids into wells.""" + water = protocol.define_liquid("Water", display_color="#0000FF") + for well, volume in zip(wells, volumes): + well.load_liquid(water, volume) + + +# CONSTANTS + +hs_str = "heaterShakerModuleV1" +mag_str = "magneticBlockV1" +temp_str = "temperature module gen2" +tc_str = "thermocycler module gen2" +abs_mod_str = "absorbanceReaderV1" +liquid_colors = [ + "#008000", + "#008000", + "#A52A2A", + "#A52A2A", + "#00FFFF", + "#0000FF", + "#800080", + "#ADD8E6", + "#FF0000", + "#FFFF00", + "#FF00FF", + "#00008B", + "#7FFFD4", + "#FFC0CB", + "#FFA500", + "#00FF00", + "#C0C0C0", +] + +# Modules with deactivate +ModuleTypes = Union[ + TemperatureModuleContext, + ThermocyclerContext, + HeaterShakerContext, + MagneticModuleContext, + AbsorbanceReaderContext, +] +# THERMOCYCLER PROFILES + + +def perform_pcr( + protocol: ProtocolContext, + thermocycler: ThermocyclerContext, + initial_denature_time_sec: int, + denaturation_time_sec: int, + anneal_time_sec: int, + extension_time_sec: int, + cycle_repetitions: int, + final_extension_time_min: int, +) -> None: + """Perform PCR.""" + # Define profiles. + initial_denaturation_profile: List[ThermocyclerStep] = [ + {"temperature": 98, "hold_time_seconds": initial_denature_time_sec} + ] + cycling_profile: List[ThermocyclerStep] = [ + {"temperature": 98, "hold_time_seconds": denaturation_time_sec}, + {"temperature": 60, "hold_time_seconds": anneal_time_sec}, + {"temperature": 72, "hold_time_seconds": extension_time_sec}, + ] + final_extension_profile: List[ThermocyclerStep] = [ + {"temperature": 72, "hold_time_minutes": final_extension_time_min} + ] + protocol.comment(f"Initial Denaturation for {initial_denature_time_sec} seconds.") + thermocycler.execute_profile( + steps=initial_denaturation_profile, repetitions=1, block_max_volume=50 + ) + protocol.comment(f"PCR for {cycle_repetitions} cycles.") + thermocycler.execute_profile( + steps=cycling_profile, repetitions=cycle_repetitions, block_max_volume=50 + ) + protocol.comment(f"Final Extension profile for {final_extension_time_min} minutes.") + thermocycler.execute_profile( + steps=final_extension_profile, repetitions=1, block_max_volume=50 + ) diff --git a/abr-testing/abr_testing/protocols/liquid_setups/10_ZymoBIOMICS Magbead Liquid Setup.py b/abr-testing/abr_testing/protocols/liquid_setups/10_ZymoBIOMICS Magbead Liquid Setup.py new file mode 100644 index 00000000000..7fed5d5d052 --- /dev/null +++ b/abr-testing/abr_testing/protocols/liquid_setups/10_ZymoBIOMICS Magbead Liquid Setup.py @@ -0,0 +1,93 @@ +"""Plate Filler Protocol for Zymobiomics DNA Extraction.""" +from opentrons import protocol_api +from abr_testing.protocols.helpers import ( + load_common_liquid_setup_labware_and_instruments, +) + + +metadata = { + "protocolName": "PVT1ABR10 Liquids: ZymoBIOMICS Magbead DNA Extraction", + "author": "Rhyann Clarke ", + "source": "Protocol Library", +} + +requirements = { + "robotType": "Flex", + "apiLevel": "2.21", +} + + +def run(protocol: protocol_api.ProtocolContext) -> None: + """Protocol.""" + # Initiate Labware + ( + source_reservoir, + tip_rack, + p1000, + ) = load_common_liquid_setup_labware_and_instruments(protocol) + + res1 = protocol.load_labware("nest_12_reservoir_15ml", "D3", "Reagent Reservoir 1") + res2 = protocol.load_labware("nest_12_reservoir_15ml", "C3", "Reagent Reservoir 2") + res3 = protocol.load_labware("nest_12_reservoir_15ml", "B3", "Reagent Reservoir 3") + + lysis_and_pk = 12320 / 8 + beads_and_binding = 11875 / 8 + binding2 = 13500 / 8 + wash2 = 9800 / 8 + wash2_list = [wash2] * 12 + final_elution = 7500 / 8 + + # Fill up Plates + # Res1 + p1000.transfer( + volume=[ + lysis_and_pk, + beads_and_binding, + beads_and_binding, + beads_and_binding, + beads_and_binding, + beads_and_binding, + beads_and_binding, + beads_and_binding, + binding2, + binding2, + binding2, + binding2, + ], + source=source_reservoir["A1"].bottom(z=0.2), + dest=[ + res1["A1"].top(), + res1["A2"].top(), + res1["A3"].top(), + res1["A4"].top(), + res1["A5"].top(), + res1["A6"].top(), + res1["A7"].top(), + res1["A8"].top(), + res1["A9"].top(), + res1["A10"].top(), + res1["A11"].top(), + res1["A12"].top(), + ], + blow_out=True, + blowout_location="source well", + trash=False, + ) + # Res2 + p1000.transfer( + volume=[final_elution] + wash2_list[:11], + source=[source_reservoir["A1"]] * 12, + dest=res2.wells(), + blow_out=True, + blowout_location="source well", + trash=False, + ) + # Res 3 + p1000.transfer( + volume=[wash2, wash2], + source=[source_reservoir["A1"], source_reservoir["A1"]], + dest=[res3["A1"], res3["A2"]], + blow_out=True, + blowout_location="source well", + trash=False, + ) diff --git a/abr-testing/abr_testing/protocols/liquid_setups/11_Dynabeads RIT Liquid Setup.py b/abr-testing/abr_testing/protocols/liquid_setups/11_Dynabeads RIT Liquid Setup.py new file mode 100644 index 00000000000..2d722d410e5 --- /dev/null +++ b/abr-testing/abr_testing/protocols/liquid_setups/11_Dynabeads RIT Liquid Setup.py @@ -0,0 +1,78 @@ +"""Plate Filler Protocol for Immunoprecipitation by Dynabeads.""" +from opentrons import protocol_api +from abr_testing.protocols.helpers import ( + load_common_liquid_setup_labware_and_instruments, +) + + +metadata = { + "protocolName": "PVT1ABR11 Liquids: Immunoprecipitation by Dynabeads - 96-well", + "author": "Rhyann Clarke ", + "source": "Protocol Library", +} + +requirements = { + "robotType": "Flex", + "apiLevel": "2.21", +} + + +def run(protocol: protocol_api.ProtocolContext) -> None: + """Protocol.""" + # Deck Setup + ( + source_reservoir, + tip_rack, + p1000, + ) = load_common_liquid_setup_labware_and_instruments(protocol) + + reservoir_wash = protocol.load_labware("nest_12_reservoir_15ml", "D2", "Reservoir") + sample_plate1 = protocol.load_labware( + "nest_96_wellplate_2ml_deep", "C3", "Sample Plate 1" + ) + sample_plate2 = protocol.load_labware( + "nest_96_wellplate_2ml_deep", "B3", "Sample Plate 2" + ) + + columns = [ + "A1", + "A2", + "A3", + "A4", + "A5", + "A6", + "A7", + "A8", + "A9", + "A10", + "A11", + "A12", + ] + # 1 column 6000 uL + p1000.pick_up_tip() + for i in columns: + p1000.aspirate(750, source_reservoir["A1"].bottom(z=0.5)) + p1000.dispense(750, reservoir_wash[i].top()) + p1000.blow_out(location=source_reservoir["A1"].top()) + p1000.return_tip() + # 1 column 6000 uL + p1000.pick_up_tip() + for i in columns: + p1000.aspirate(750, source_reservoir["A1"].bottom(z=0.5)) + p1000.dispense(750, reservoir_wash[i].top()) + p1000.blow_out(location=source_reservoir["A1"].top()) + p1000.return_tip() + # Nest 96 Deep Well Plate 2 mL: 250 uL per well + p1000.pick_up_tip() + for n in columns: + p1000.aspirate(250, source_reservoir["A1"].bottom(z=0.5)) + p1000.dispense(250, sample_plate1[n].bottom(z=1)) + p1000.blow_out(location=source_reservoir["A1"].top()) + p1000.return_tip() + # Nest 96 Deep Well Plate 2 mL: 250 uL per well + p1000.pick_up_tip() + for n in columns: + p1000.aspirate(250, source_reservoir["A1"].bottom(z=0.5)) + p1000.dispense(250, sample_plate2[n].bottom(z=1)) + p1000.blow_out(location=source_reservoir["A1"].top()) + p1000.return_tip() diff --git a/abr-testing/abr_testing/protocols/liquid_setups/12_KAPA HyperPlus Library Prep Liquid Setup.py b/abr-testing/abr_testing/protocols/liquid_setups/12_KAPA HyperPlus Library Prep Liquid Setup.py new file mode 100644 index 00000000000..688533ffd55 --- /dev/null +++ b/abr-testing/abr_testing/protocols/liquid_setups/12_KAPA HyperPlus Library Prep Liquid Setup.py @@ -0,0 +1,118 @@ +"""KAPA HyperPlus Library Preparation Liquid Setup.""" +from opentrons import protocol_api +from abr_testing.protocols.helpers import ( + load_common_liquid_setup_labware_and_instruments, +) + + +metadata = { + "protocolName": "PVT1ABR12: KAPA HyperPlus Library Preparation Liquid Setup", + "author": "Rhyann Clarke ", + "source": "Protocol Library", +} + +requirements = { + "robotType": "Flex", + "apiLevel": "2.21", +} + + +def run(protocol: protocol_api.ProtocolContext) -> None: + """Protocol.""" + ( + source_reservoir, + tip_rack, + p1000, + ) = load_common_liquid_setup_labware_and_instruments(protocol) + + reservoir = protocol.load_labware( + "nest_96_wellplate_2ml_deep", "D2", "Beads + Buffer + Ethanol" + ) # Reservoir + temp_plate = protocol.load_labware( + "opentrons_96_wellplate_200ul_pcr_full_skirt", + "B3", + "Temp Module Reservoir Plate", + ) + sample_plate_1 = protocol.load_labware( + "opentrons_96_wellplate_200ul_pcr_full_skirt", "D3", "Sample Plate 1" + ) # Sample Plate + sample_plate_2 = protocol.load_labware( + "opentrons_96_wellplate_200ul_pcr_full_skirt", "C3", "Sample Plate 2" + ) # Sample Plate + + # Sample Plate 1 Prep: dispense 17 ul into column 1 total 136 ul + p1000.transfer( + volume=[35, 35, 35, 35, 35, 35], + source=source_reservoir["A1"].bottom(z=2), + dest=[ + sample_plate_1["A1"].top(), + sample_plate_1["A2"].top(), + sample_plate_1["A3"].top(), + sample_plate_1["A4"].top(), + sample_plate_1["A5"].top(), + sample_plate_1["A6"].top(), + ], + blow_out=True, + blowout_location="source well", + new_tip="once", + trash=False, + ) + + # Sample Plate 2 Prep: dispense 17 ul into column 1 total 136 ul + p1000.transfer( + volume=[17, 17, 17, 17, 17, 17], + source=source_reservoir["A1"].bottom(z=2), + dest=[ + sample_plate_2["A1"].top(), + sample_plate_2["A2"].top(), + sample_plate_2["A3"].top(), + sample_plate_2["A4"].top(), + sample_plate_2["A5"].top(), + sample_plate_2["A6"].top(), + ], + blow_out=True, + blowout_location="source well", + new_tip="once", + trash=False, + ) + + # Reservoir Plate Prep: + p1000.transfer( + volume=[910.8, 297, 2000, 2000], + source=source_reservoir["A1"].bottom(z=2), + dest=[ + reservoir["A1"].top(), + reservoir["A4"].top(), + reservoir["A5"].top(), + reservoir["A6"].top(), + ], + blow_out=True, + blowout_location="source well", + new_tip="once", + trash=False, + ) + + # Temp Module Res Prep: dispense 30 and 200 ul into columns 1 and 3 - total 1840 ul + # adapters + + # Rest of liquids + p1000.transfer( + volume=[10, 10, 10, 10, 10, 10, 61, 91.5, 200, 183], + source=source_reservoir["A1"].bottom(z=2), + dest=[ + temp_plate["A1"].top(), + temp_plate["A2"].top(), + temp_plate["A3"].top(), + temp_plate["A4"].top(), + temp_plate["A5"].top(), + temp_plate["A6"].top(), + temp_plate["A7"].top(), + temp_plate["A8"].top(), + temp_plate["A9"].top(), + temp_plate["A10"].top(), + ], + blow_out=True, + blowout_location="source well", + new_tip="once", + trash=False, + ) diff --git a/abr-testing/abr_testing/protocols/liquid_setups/1_Simple normalize long Liquid Setup.py b/abr-testing/abr_testing/protocols/liquid_setups/1_Simple normalize long Liquid Setup.py new file mode 100644 index 00000000000..a99bb0568cf --- /dev/null +++ b/abr-testing/abr_testing/protocols/liquid_setups/1_Simple normalize long Liquid Setup.py @@ -0,0 +1,47 @@ +"""Plate Filler Protocol for Simple Normalize Long.""" +from opentrons import protocol_api +from abr_testing.protocols.helpers import ( + load_common_liquid_setup_labware_and_instruments, +) + +metadata = { + "protocolName": "DVT1ABR1 Liquids: Simple Normalize Long", + "author": "Rhyann clarke ", + "source": "Protocol Library", +} + +requirements = { + "robotType": "Flex", + "apiLevel": "2.21", +} + + +def run(protocol: protocol_api.ProtocolContext) -> None: + """Protocol.""" + # Initiate Labware + ( + source_reservoir, + tip_rack, + p1000, + ) = load_common_liquid_setup_labware_and_instruments(protocol) + reservoir = protocol.load_labware("nest_12_reservoir_15ml", "D2", "Reservoir") + # Transfer Liquid + vol = 6175 / 8 + columns = ["A1", "A2", "A3", "A4", "A5"] + for i in columns: + p1000.transfer( + vol, + source=source_reservoir["A1"].bottom(z=2), + dest=reservoir[i].top(), + blowout=True, + blowout_location="source well", + trash=False, + ) + p1000.transfer( + 8500 / 8, + source=source_reservoir["A1"].bottom(z=2), + dest=reservoir["A6"], + blowout=True, + blowout_location="source well", + trash=False, + ) diff --git a/abr-testing/abr_testing/protocols/liquid_setups/2_BMS_PCR_protocol Liquid Setup.py b/abr-testing/abr_testing/protocols/liquid_setups/2_BMS_PCR_protocol Liquid Setup.py new file mode 100644 index 00000000000..1ffefc48f19 --- /dev/null +++ b/abr-testing/abr_testing/protocols/liquid_setups/2_BMS_PCR_protocol Liquid Setup.py @@ -0,0 +1,41 @@ +"""Plate Filler Protocol for Simple Normalize Long.""" +from opentrons import protocol_api +from abr_testing.protocols.helpers import ( + load_common_liquid_setup_labware_and_instruments, +) + + +metadata = { + "protocolName": "DVT1ABR2 Liquids: BMS PCR Protocol", + "author": "Rhyann Clarke ", + "source": "Protocol Library", +} + +requirements = { + "robotType": "Flex", + "apiLevel": "2.21", +} + + +def run(protocol: protocol_api.ProtocolContext) -> None: + """Protocol.""" + # Initiate Labware + ( + source_reservoir, + tip_rack, + p1000, + ) = load_common_liquid_setup_labware_and_instruments(protocol) + pcr_plate_1 = protocol.load_labware( + "armadillo_96_wellplate_200ul_pcr_full_skirt", "C3", "PCR Plate 1" + ) + snap_caps = protocol.load_labware( + "opentrons_24_aluminumblock_nest_1.5ml_snapcap", "B3", "Snap Caps" + ) + # Steps + # Dispense into plate 1 + p1000.transfer(100, source_reservoir["A1"], pcr_plate_1.wells(), trash=False) + + # Dispense + p1000.configure_nozzle_layout(protocol_api.SINGLE, start="H1", tip_racks=[tip_rack]) + p1000.transfer(1000, source_reservoir["A1"], snap_caps["B1"]) + p1000.transfer(1000, source_reservoir["A1"], snap_caps.rows()[0]) diff --git a/abr-testing/abr_testing/protocols/liquid_setups/3_Tartrazine Liquid Setup.py b/abr-testing/abr_testing/protocols/liquid_setups/3_Tartrazine Liquid Setup.py new file mode 100644 index 00000000000..f0941bb398b --- /dev/null +++ b/abr-testing/abr_testing/protocols/liquid_setups/3_Tartrazine Liquid Setup.py @@ -0,0 +1,86 @@ +"""Plate Filler Protocol for Tartrazine Protocol.""" +from opentrons import protocol_api +from abr_testing.protocols import helpers + +metadata = { + "protocolName": "DVT1ABR3 Liquids: Tartrazine Protocol", + "author": "Rhyann clarke ", + "source": "Protocol Library", +} + +requirements = { + "robotType": "Flex", + "apiLevel": "2.21", +} + + +def add_parameters(parameters: protocol_api.ParameterContext) -> None: + """Add parameters.""" + parameters.add_int( + variable_name="number_of_plates", + display_name="Number of Plates", + default=4, + minimum=1, + maximum=4, + ) + helpers.create_channel_parameter(parameters) + + +def run(protocol: protocol_api.ProtocolContext) -> None: + """Protocol.""" + number_of_plates = protocol.params.number_of_plates # type: ignore [attr-defined] + channels = protocol.params.channels # type: ignore [attr-defined] + # Initiate Labware + ( + source_reservoir, + tip_rack, + p1000, + ) = helpers.load_common_liquid_setup_labware_and_instruments(protocol) + if channels == "1channel": + reagent_tube = protocol.load_labware( + "opentrons_10_tuberack_falcon_4x50ml_6x15ml_conical", "D3", "Reagent Tube" + ) + p1000.configure_nozzle_layout( + style=protocol_api.SINGLE, start="H1", tip_racks=[tip_rack] + ) + # Transfer Liquid + p1000.transfer( + 45000, + source_reservoir["A1"], + reagent_tube["B3"].top(), + blowout=True, + blowout_location="source well", + ) + p1000.transfer( + 45000, + source_reservoir["A1"], + reagent_tube["A4"].top(), + blowout=True, + blowout_location="source well", + ) + elif channels == "8channel": + reservoir = protocol.load_labware("nest_12_reservoir_15ml", "D3", "Reservoir") + water_max_vol = reservoir["A1"].max_volume - 500 + reservoir_wells = reservoir.wells()[ + 1: + ] # Skip A1 as it's reserved for tartrazine + # NEEDED WATER + needed_water: float = ( + float(number_of_plates) * 96.0 * 250.0 + ) # loading extra as a safety factor + # CALCULATING NEEDED # OF WATER WELLS + needed_wells = round(needed_water / water_max_vol) + water_wells = [] + for i in range(needed_wells + 1): + water_wells.append(reservoir_wells[i]) + # Create lists of volumes and source that matches wells to fill + water_max_vol_list = [water_max_vol] * len(water_wells) + source_list = [source_reservoir["A1"]] * len(water_wells) + p1000.transfer( + water_max_vol_list, + source_list, + water_wells, + blowout=True, + blowout_locaiton="source", + trash=False, + ) diff --git a/abr-testing/abr_testing/protocols/liquid_setups/4_Illumina DNA Enrichment Liquid Setup.py b/abr-testing/abr_testing/protocols/liquid_setups/4_Illumina DNA Enrichment Liquid Setup.py new file mode 100644 index 00000000000..937c9f4dafd --- /dev/null +++ b/abr-testing/abr_testing/protocols/liquid_setups/4_Illumina DNA Enrichment Liquid Setup.py @@ -0,0 +1,96 @@ +"""Illumina DNA Enrichment Liquid Set up.""" +from opentrons import protocol_api +from abr_testing.protocols.helpers import ( + load_common_liquid_setup_labware_and_instruments, +) + +metadata = { + "protocolName": "DVT1ABR4/8: Illumina DNA Enrichment Liquid Set Up", + "author": "Tony Ngumah ", + "source": "Protocol Library", +} + +requirements = { + "robotType": "Flex", + "apiLevel": "2.21", +} + + +def run(protocol: protocol_api.ProtocolContext) -> None: + """Protocol.""" + ( + source_reservoir, + tip_rack, + p1000, + ) = load_common_liquid_setup_labware_and_instruments(protocol) + + reservoir_1 = protocol.load_labware( + "nest_96_wellplate_2ml_deep", "D2", "Reservoir 1" + ) # Reservoir + sample_plate_2 = protocol.load_labware( + "thermoscientificnunc_96_wellplate_1300ul", "D3", "Sample Plate 2" + ) # Reservoir + sample_plate_1 = protocol.load_labware( + "armadillo_96_wellplate_200ul_pcr_full_skirt", "C3", "Sample Plate 1" + ) # Sample Plate + reagent_plate_1 = protocol.load_labware( + "armadillo_96_wellplate_200ul_pcr_full_skirt", "B3", "Reagent Plate" + ) # reagent Plate + + # Reagent Plate Prep: dispense liquid into columns 4 - 7 - total 156 ul + p1000.transfer( + volume=[75, 15, 20, 65], + source=source_reservoir["A1"].bottom(z=0.5), + dest=[ + reagent_plate_1["A4"], + reagent_plate_1["A5"], + reagent_plate_1["A6"], + reagent_plate_1["A7"], + ], + blow_out=True, + blowout_location="source well", + trash=False, + ) + + # Reservoir 1 Plate Prep: dispense liquid into columns 1, 2, 4, 5 total 1866 ul + p1000.transfer( + volume=[120, 750, 900, 96], + source=source_reservoir["A1"], + dest=[ + reservoir_1["A1"].top(), # AMPure + reservoir_1["A2"].top(), # SMB + reservoir_1["A4"].top(), # EtOH + reservoir_1["A5"].top(), # RSB + ], + blow_out=True, + blowout_location="source well", + trash=False, + ) + + # Reservoir 2 Plate Prep: dispense liquid into columns 1-9 total 3690 ul + reservoir_2_wells = sample_plate_1.wells() + list_of_locations = [well_location.top() for well_location in reservoir_2_wells] + p1000.transfer( + volume=[150, 150, 150, 150, 150, 150, 150, 150, 150, 150, 150, 150], + source=source_reservoir["A1"], + dest=list_of_locations, + blow_out=True, + blowout_location="source well", + trash=False, + ) + + # Sample Plate Prep: total 303 + dest_list = [ + sample_plate_2["A9"], + sample_plate_2["A10"], + sample_plate_2["A11"], + sample_plate_2["A12"], + ] + p1000.transfer( + volume=[1000, 1000, 1000, 1000], + source=source_reservoir["A1"], + dest=dest_list, + blow_out=True, + blowout_location="source well", + trash=False, + ) diff --git a/abr-testing/abr_testing/protocols/liquid_setups/5_96ch Complex Protocol Liquid Setup.py b/abr-testing/abr_testing/protocols/liquid_setups/5_96ch Complex Protocol Liquid Setup.py new file mode 100644 index 00000000000..e7a726b6b46 --- /dev/null +++ b/abr-testing/abr_testing/protocols/liquid_setups/5_96ch Complex Protocol Liquid Setup.py @@ -0,0 +1,53 @@ +"""Plate Filler Protocol for 96ch Complex Protocol.""" +from opentrons import protocol_api +from abr_testing.protocols.helpers import ( + load_common_liquid_setup_labware_and_instruments, +) + +metadata = { + "protocolName": "DVT2ABR5 Liquids: 96ch Complex Protocol", + "author": "Rhyann clarke ", + "source": "Protocol Library", +} + +requirements = { + "robotType": "Flex", + "apiLevel": "2.21", +} + + +def run(protocol: protocol_api.ProtocolContext) -> None: + """Protocol.""" + # Initiate Labware + ( + source_reservoir, + tip_rack, + p1000, + ) = load_common_liquid_setup_labware_and_instruments(protocol) + + reservoir = protocol.load_labware( + "nest_96_wellplate_2ml_deep", "D2", "Reservoir" + ) # Reservoir + + vol = 1000 + + column_list = [ + "A1", + "A2", + "A3", + "A4", + "A5", + "A6", + "A7", + "A8", + "A9", + "A10", + "A11", + "A12", + ] + p1000.pick_up_tip() + for i in column_list: + p1000.aspirate(vol, source_reservoir["A1"].bottom(z=0.5)) + p1000.dispense(vol, reservoir[i].top()) + p1000.blow_out(location=source_reservoir["A1"].top()) + p1000.return_tip() diff --git a/abr-testing/abr_testing/protocols/liquid_setups/6_Omega_HDQ_DNA_Cells Liquid Setup.py b/abr-testing/abr_testing/protocols/liquid_setups/6_Omega_HDQ_DNA_Cells Liquid Setup.py new file mode 100644 index 00000000000..da40b983a1d --- /dev/null +++ b/abr-testing/abr_testing/protocols/liquid_setups/6_Omega_HDQ_DNA_Cells Liquid Setup.py @@ -0,0 +1,92 @@ +"""Plate Filler Protocol for Omega HDQ DNA Cell Protocol.""" +from opentrons import protocol_api +from abr_testing.protocols.helpers import ( + load_common_liquid_setup_labware_and_instruments, +) + +metadata = { + "protocolName": "DVT2ABR6: Omega HDQ DNA Cells Protocol", + "author": "Rhyann clarke ", + "source": "Protocol Library", +} + +requirements = { + "robotType": "Flex", + "apiLevel": "2.21", +} + + +def run(protocol: protocol_api.ProtocolContext) -> None: + """Protocol.""" + # Initiate Labware + ( + source_reservoir, + tip_rack, + p1000, + ) = load_common_liquid_setup_labware_and_instruments(protocol) + deepwell_type = "nest_96_wellplate_2ml_deep" + + lysis_reservoir = protocol.load_labware(deepwell_type, "D2", "Lysis reservoir") + bind_reservoir = protocol.load_labware( + deepwell_type, "D3", "Beads and binding reservoir" + ) + wash1_reservoir = protocol.load_labware(deepwell_type, "C3", "Wash 1 reservoir") + wash2_reservoir = protocol.load_labware(deepwell_type, "B3", "Wash 2 reservoir") + sample_plate = protocol.load_labware(deepwell_type, "B2", "Sample Plate") + elution_plate = protocol.load_labware( + "armadillo_96_wellplate_200ul_pcr_full_skirt", "B1", "Elution Plate/ reservoir" + ) + p1000.transfer( + volume=350, + source=source_reservoir["A1"].bottom(z=2), + dest=lysis_reservoir.wells(), + blow_out=True, + blowout_location="source well", + new_tip="once", + trash=False, + ) + p1000.transfer( + 440, + source=source_reservoir["A1"].bottom(z=2), + dest=bind_reservoir.wells(), + blow_out=True, + blowout_location="source well", + new_tip="once", + trash=False, + ) + p1000.transfer( + 1300, + source_reservoir["A1"].bottom(z=2), + wash1_reservoir.wells(), + blow_out=True, + blowout_location="source well", + new_tip="once", + trash=False, + ) + p1000.transfer( + 700, + source_reservoir["A1"].bottom(z=2), + wash2_reservoir.wells(), + blow_out=True, + blowout_location="source well", + new_tip="once", + trash=False, + ) + p1000.transfer( + 180, + source_reservoir["A1"].bottom(z=2), + sample_plate.wells(), + blow_out=True, + blowout_location="source well", + new_tip="once", + trash=False, + ) + p1000.transfer( + 100, + source_reservoir["A1"].bottom(z=2), + elution_plate.wells(), + blow_out=True, + blowout_location="source well", + new_tip="once", + trash=False, + ) diff --git a/abr-testing/abr_testing/protocols/liquid_setups/7_HDQ DNA Bacteria Extraction Liquid Setup.py b/abr-testing/abr_testing/protocols/liquid_setups/7_HDQ DNA Bacteria Extraction Liquid Setup.py new file mode 100644 index 00000000000..309f9916d03 --- /dev/null +++ b/abr-testing/abr_testing/protocols/liquid_setups/7_HDQ DNA Bacteria Extraction Liquid Setup.py @@ -0,0 +1,140 @@ +"""Plate Filler Protocol for HDQ DNA Bacteria Extraction.""" +from opentrons import protocol_api +from abr_testing.protocols.helpers import ( + load_common_liquid_setup_labware_and_instruments, +) + + +metadata = { + "protocolName": "PVT1ABR7 Liquids: HDQ DNA Bacteria Extraction", + "author": "Rhyann Clarke ", + "source": "Protocol Library", +} + +requirements = { + "robotType": "Flex", + "apiLevel": "2.21", +} + + +def run(protocol: protocol_api.ProtocolContext) -> None: + """Protocol.""" + # Deck Setup + ( + source_reservoir, + tip_rack, + p1000, + ) = load_common_liquid_setup_labware_and_instruments(protocol) + + sample_plate = protocol.load_labware( + "nest_96_wellplate_2ml_deep", "C3", "Sample Plate" + ) + elution_plate = protocol.load_labware( + "opentrons_96_wellplate_200ul_pcr_full_skirt", "B3", "Elution Plate" + ) + res1 = protocol.load_labware("nest_12_reservoir_15ml", "D2", "reagent reservoir 1") + # Label Reservoirs + well1 = res1["A1"].top() + well2 = res1["A2"].top() + well3 = res1["A3"].top() + well4 = res1["A4"].top() + well5 = res1["A5"].top() + well6 = res1["A6"].top() + well7 = res1["A7"].top() + well8 = res1["A8"].top() + well9 = res1["A9"].top() + well10 = res1["A10"].top() + well11 = res1["A11"].top() + well12 = res1["A12"].top() + # Volumes + wash = 600 + binding = 320 + beads = 230 + pk = 230 + lysis = 230 + + # Sample Plate + p1000.transfer( + volume=200, + source=source_reservoir["A1"].bottom(z=0.5), + dest=[ + sample_plate["A1"].top(), + sample_plate["A2"].top(), + sample_plate["A3"].top(), + sample_plate["A4"].top(), + sample_plate["A5"].top(), + sample_plate["A6"].top(), + sample_plate["A7"].top(), + sample_plate["A8"].top(), + sample_plate["A9"].top(), + sample_plate["A10"].top(), + sample_plate["A11"].top(), + sample_plate["A12"].top(), + ], + blowout=True, + blowout_location="source well", + trash=False, + ) + # Elution Plate + p1000.transfer( + volume=100, + source=source_reservoir["A1"].bottom(z=0.5), + dest=[ + elution_plate["A1"].top(), + elution_plate["A2"].top(), + elution_plate["A3"].top(), + elution_plate["A4"].top(), + elution_plate["A5"].top(), + elution_plate["A6"].top(), + elution_plate["A7"].top(), + elution_plate["A8"].top(), + elution_plate["A9"].top(), + elution_plate["A10"].top(), + elution_plate["A11"].top(), + elution_plate["A12"].top(), + ], + blowout=True, + blowout_location="source well", + trash=False, + ) + # Res 1 + p1000.transfer( + volume=[ + binding, + beads, + binding, + beads, + lysis, + pk, + wash, + wash, + wash, + wash, + wash, + wash, + wash, + wash, + wash, + ], + source=source_reservoir["A1"].bottom(z=0.5), + dest=[ + well1, + well1, + well2, + well2, + well3, + well3, + well4, + well5, + well6, + well7, + well8, + well9, + well10, + well11, + well12, + ], + blowout=True, + blowout_location="source well", + trash=False, + ) diff --git a/abr-testing/abr_testing/protocols/liquid_setups/9_Thermo MagMax RNA Extraction Liquid Setup.py b/abr-testing/abr_testing/protocols/liquid_setups/9_Thermo MagMax RNA Extraction Liquid Setup.py new file mode 100644 index 00000000000..e935063ed16 --- /dev/null +++ b/abr-testing/abr_testing/protocols/liquid_setups/9_Thermo MagMax RNA Extraction Liquid Setup.py @@ -0,0 +1,147 @@ +"""Plate Filler Protocol for Thermo MagMax RNA Extraction.""" +from opentrons import protocol_api +from abr_testing.protocols.helpers import ( + load_common_liquid_setup_labware_and_instruments, +) + + +metadata = { + "protocolName": "PVT1ABR9 Liquids: Thermo MagMax RNA Extraction", + "author": "Rhyann Clarke ", + "source": "Protocol Library", +} + +requirements = { + "robotType": "Flex", + "apiLevel": "2.21", +} + + +def run(protocol: protocol_api.ProtocolContext) -> None: + """Protocol.""" + # Initiate Labware + ( + source_reservoir, + tip_rack, + p1000, + ) = load_common_liquid_setup_labware_and_instruments(protocol) + res1 = protocol.load_labware("nest_12_reservoir_15ml", "D2", "Reservoir") + elution_plate = protocol.load_labware( + "opentrons_96_wellplate_200ul_pcr_full_skirt", "C3", "Elution Plate" + ) + sample_plate = protocol.load_labware( + "nest_96_wellplate_2ml_deep", "B3", "Sample Plate" + ) + + # Volumes + lysis = 8120 / 8 + stop_reaction_vol = 6400 / 8 + elution_vol = 55 + well4_12 = 9500 / 8 + sample_vol = 100 + + # Reservoir + p1000.transfer( + volume=[ + lysis, + lysis, + stop_reaction_vol, + well4_12, + well4_12, + well4_12, + well4_12, + well4_12, + well4_12, + well4_12, + well4_12, + well4_12, + ], + source=source_reservoir["A1"].bottom(z=0.2), + dest=[ + res1["A1"].top(), + res1["A2"].top(), + res1["A3"].top(), + res1["A4"].top(), + res1["A5"].top(), + res1["A6"].top(), + res1["A7"].top(), + res1["A8"].top(), + res1["A9"].top(), + res1["A10"].top(), + res1["A11"].top(), + res1["A12"].top(), + ], + blow_out=True, + blowout_location="source well", + trash=False, + ) + # Elution Plate + p1000.transfer( + volume=[ + elution_vol, + elution_vol, + elution_vol, + elution_vol, + elution_vol, + elution_vol, + elution_vol, + elution_vol, + elution_vol, + elution_vol, + elution_vol, + elution_vol, + ], + source=source_reservoir["A1"].bottom(z=0.2), + dest=[ + elution_plate["A1"].bottom(z=0.3), + elution_plate["A2"].bottom(z=0.3), + elution_plate["A3"].bottom(z=0.3), + elution_plate["A4"].bottom(z=0.3), + elution_plate["A5"].bottom(z=0.3), + elution_plate["A6"].bottom(z=0.3), + elution_plate["A7"].bottom(z=0.3), + elution_plate["A8"].bottom(z=0.3), + elution_plate["A9"].bottom(z=0.3), + elution_plate["A10"].bottom(z=0.3), + elution_plate["A11"].bottom(z=0.3), + elution_plate["A12"].bottom(z=0.3), + ], + blow_out=True, + blowout_location="source well", + trash=False, + ) + # Sample Plate + p1000.transfer( + volume=[ + sample_vol, + sample_vol, + sample_vol, + sample_vol, + sample_vol, + sample_vol, + sample_vol, + sample_vol, + sample_vol, + sample_vol, + sample_vol, + sample_vol, + ], + source=source_reservoir["A1"].bottom(z=0.2), + dest=[ + sample_plate["A1"].top(), + sample_plate["A2"].top(), + sample_plate["A3"].top(), + sample_plate["A4"].top(), + sample_plate["A5"].top(), + sample_plate["A6"].top(), + sample_plate["A7"].top(), + sample_plate["A8"].top(), + sample_plate["A9"].top(), + sample_plate["A10"].top(), + sample_plate["A11"].top(), + sample_plate["A12"].top(), + ], + blow_out=True, + blowout_location="source well", + trash=False, + ) diff --git a/abr-testing/abr_testing/protocols/test_protocols/tc_biorad_evap_test.py b/abr-testing/abr_testing/protocols/test_protocols/tc_biorad_evap_test.py new file mode 100644 index 00000000000..4593c06f425 --- /dev/null +++ b/abr-testing/abr_testing/protocols/test_protocols/tc_biorad_evap_test.py @@ -0,0 +1,109 @@ +"""Test TC Disposable Lid with BioRad Plate.""" + +from opentrons.protocol_api import ( + ProtocolContext, + ParameterContext, + Well, + Labware, + InstrumentContext, +) +from typing import List +from abr_testing.protocols import helpers +from opentrons.protocol_api.module_contexts import ThermocyclerContext +from opentrons.hardware_control.modules.types import ThermocyclerStep + +metadata = {"protocolName": "Tough Auto Seal Lid Evaporation Test"} +requirements = {"robotType": "Flex", "apiLevel": "2.21"} + + +def add_parameters(parameters: ParameterContext) -> None: + """Add Parameters.""" + helpers.create_single_pipette_mount_parameter(parameters) + helpers.create_tc_lid_deck_riser_parameter(parameters) + helpers.create_tc_compatible_labware_parameter(parameters) + + +def _pcr_cycle(thermocycler: ThermocyclerContext) -> None: + """30x cycles of: 70° for 30s 72° for 30s 95° for 10s.""" + profile_TAG2: List[ThermocyclerStep] = [ + {"temperature": 70, "hold_time_seconds": 30}, + {"temperature": 72, "hold_time_seconds": 30}, + {"temperature": 95, "hold_time_seconds": 10}, + ] + thermocycler.execute_profile( + steps=profile_TAG2, repetitions=30, block_max_volume=50 + ) + + +def _fill_with_liquid_and_measure( + protocol: ProtocolContext, + pipette: InstrumentContext, + reservoir: Labware, + plate_in_cycler: Labware, +) -> None: + """Fill plate with 10 ul per well.""" + locations: List[Well] = [ + plate_in_cycler["A1"], + plate_in_cycler["A2"], + plate_in_cycler["A3"], + plate_in_cycler["A4"], + plate_in_cycler["A5"], + plate_in_cycler["A6"], + plate_in_cycler["A7"], + plate_in_cycler["A8"], + plate_in_cycler["A9"], + plate_in_cycler["A10"], + plate_in_cycler["A11"], + plate_in_cycler["A12"], + ] + volumes = [10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10] + protocol.pause("Weight Armadillo Plate, place on thermocycler") + # pipette 10uL into Armadillo wells + source_well: Well = reservoir["A1"] + pipette.distribute( + volume=volumes, + source=source_well, + dest=locations, + return_tips=True, + blow_out=False, + ) + protocol.pause("Weight Armadillo Plate, place on thermocycler, put on lid") + + +def run(ctx: ProtocolContext) -> None: + """Evaporation Test.""" + pipette_mount = ctx.params.pipette_mount # type: ignore[attr-defined] + deck_riser = ctx.params.deck_riser # type: ignore[attr-defined] + labware_tc_compatible = ctx.params.labware_tc_compatible # type: ignore[attr-defined] + tiprack_50 = ctx.load_labware("opentrons_flex_96_tiprack_50ul", "B2") + ctx.load_trash_bin("A3") + tc_mod: ThermocyclerContext = ctx.load_module( + helpers.tc_str + ) # type: ignore[assignment] + plate_in_cycler = tc_mod.load_labware(labware_tc_compatible) + p50 = ctx.load_instrument("flex_8channel_50", pipette_mount, tip_racks=[tiprack_50]) + unused_lids = helpers.load_disposable_lids(ctx, 5, ["D2"], deck_riser) + top_lid = unused_lids[0] + reservoir = ctx.load_labware("nest_12_reservoir_15ml", "A2") + tc_mod.open_lid() + tc_mod.set_block_temperature(4) + tc_mod.set_lid_temperature(105) + + # hold at 95° for 3 minutes + profile_TAG: List[ThermocyclerStep] = [{"temperature": 95, "hold_time_minutes": 3}] + # hold at 72° for 5min + profile_TAG3: List[ThermocyclerStep] = [{"temperature": 72, "hold_time_minutes": 5}] + tc_mod.open_lid() + _fill_with_liquid_and_measure(ctx, p50, reservoir, plate_in_cycler) + ctx.move_labware(top_lid, plate_in_cycler, use_gripper=True) + tc_mod.close_lid() + tc_mod.execute_profile(steps=profile_TAG, repetitions=1, block_max_volume=50) + _pcr_cycle(tc_mod) + tc_mod.execute_profile(steps=profile_TAG3, repetitions=1, block_max_volume=50) + # # # Cool to 4° + tc_mod.set_block_temperature(4) + tc_mod.set_lid_temperature(105) + # Open lid + tc_mod.open_lid() + ctx.move_labware(top_lid, "C2", use_gripper=True) + ctx.move_labware(top_lid, unused_lids[1], use_gripper=True) diff --git a/abr-testing/abr_testing/protocols/test_protocols/tc_lid_x_offset_test.py b/abr-testing/abr_testing/protocols/test_protocols/tc_lid_x_offset_test.py new file mode 100644 index 00000000000..9dd4ee7b578 --- /dev/null +++ b/abr-testing/abr_testing/protocols/test_protocols/tc_lid_x_offset_test.py @@ -0,0 +1,125 @@ +"""Protocol to Test the Stacking and Movement of Tough Auto Seal Lid.""" +from opentrons.protocol_api import ParameterContext, ProtocolContext, Labware +from opentrons.protocol_api.module_contexts import ( + ThermocyclerContext, +) +from typing import List + + +metadata = {"protocolName": "5 Stack Test"} +requirements = {"robotType": "Flex", "apiLevel": "2.20"} + + +def add_parameters(parameters: ParameterContext) -> None: + """Add parameters.""" + parameters.add_int( + variable_name="lids_in_a_stack", + display_name="Num of Lids in Stack", + minimum=1, + maximum=5, + default=5, + ) + parameters.add_float( + variable_name="num_offset", + display_name="Numerical Offset", + choices=[ + {"display_name": "0.0", "value": 0.0}, + {"display_name": "0.1", "value": 0.1}, + {"display_name": "0.2", "value": 0.2}, + {"display_name": "0.3", "value": 0.3}, + {"display_name": "0.4", "value": 0.4}, + {"display_name": "0.5", "value": 0.5}, + {"display_name": "0.6", "value": 0.6}, + {"display_name": "0.7", "value": 0.7}, + {"display_name": "0.8", "value": 0.8}, + {"display_name": "0.9", "value": 0.9}, + {"display_name": "1.0", "value": 1.0}, + {"display_name": "1.1", "value": 1.1}, + {"display_name": "1.2", "value": 1.2}, + {"display_name": "1.3", "value": 1.3}, + {"display_name": "1.4", "value": 1.4}, + {"display_name": "1.5", "value": 1.5}, + {"display_name": "1.6", "value": 1.6}, + {"display_name": "1.7", "value": 1.7}, + {"display_name": "1.8", "value": 1.8}, + {"display_name": "1.9", "value": 1.9}, + {"display_name": "2", "value": 2}, + ], + default=2, + ) + parameters.add_bool( + variable_name="negative", + display_name="Negative", + description="Turn on to make offset negative.", + default=False, + ) + parameters.add_str( + variable_name="offset", + display_name="Offset", + choices=[ + {"display_name": "Z", "value": "Z"}, + {"display_name": "Y", "value": "Y"}, + {"display_name": "X", "value": "X"}, + ], + default="X", + ) + parameters.add_bool( + variable_name="thermocycler_bool", display_name="thermocycler", default=False + ) + + +def run(protocol: ProtocolContext) -> None: + """Runs protocol that moves lids and stacks them.""" + # Load Parameters + lids_in_stack: int = protocol.params.lids_in_a_stack # type: ignore[attr-defined] + num_offset = protocol.params.num_offset # type: ignore[attr-defined] + + offset = protocol.params.offset # type: ignore[attr-defined] + negative = protocol.params.negative # type: ignore[attr-defined] + thermocycler_bool = protocol.params.thermocycler_bool # type: ignore[attr-defined] + if negative: + num_offset = num_offset * -1 + + # Thermocycler + if thermocycler_bool: + thermocycler: ThermocyclerContext = protocol.load_module( + "thermocyclerModuleV2" + ) # type: ignore[assignment] + plate_in_cycler: Labware = thermocycler.load_labware( + "armadillo_96_wellplate_200ul_pcr_full_skirt" + ) + thermocycler.open_lid() + else: + plate_in_cycler = protocol.load_labware( + "armadillo_96_wellplate_200ul_pcr_full_skirt", "D2" + ) + # Load Lids + deck_riser_adapter = protocol.load_adapter("opentrons_flex_deck_riser", "D3") + unused_lids: List[Labware] = [ + deck_riser_adapter.load_labware("opentrons_tough_pcr_auto_sealing_lid") + ] + if lids_in_stack > 1: + for i in range(lids_in_stack - 1): + unused_lids.append( + unused_lids[-1].load_labware("opentrons_tough_pcr_auto_sealing_lid") + ) + unused_lids.reverse() + pick_up_offset = { + "X": {"x": num_offset, "y": 0, "z": 0}, + "Y": {"x": 0, "y": num_offset, "z": 0}, + "Z": {"x": 0, "y": 0, "z": num_offset}, + } + slot = 0 + if len(unused_lids) > 1: + lid_to_move_back_to = unused_lids[1] # stack back on top + else: + lid_to_move_back_to = deck_riser_adapter + protocol.comment(f"{offset} Offset {num_offset}, Lid # {slot+1}") + # move lid to plate in thermocycler + protocol.move_labware( + unused_lids[0], + plate_in_cycler, + use_gripper=True, + pick_up_offset=pick_up_offset[offset], + ) + protocol.move_labware(unused_lids[0], lid_to_move_back_to, use_gripper=True) diff --git a/abr-testing/abr_testing/tools/abr_scale.py b/abr-testing/abr_testing/tools/abr_scale.py index 0f6c29c3f69..e91abf114b2 100644 --- a/abr-testing/abr_testing/tools/abr_scale.py +++ b/abr-testing/abr_testing/tools/abr_scale.py @@ -153,9 +153,7 @@ def get_most_recent_run_and_record( most_recent_run_id, storage_directory, "", - labware, - accuracy, - hellma_plate_standards=hellma_file_values, + hellma_file_values, ) google_sheet_abr_data = google_sheets_tool.google_sheet( credentials_path, "ABR-run-data", tab_number=0 diff --git a/abr-testing/abr_testing/tools/abr_setup.py b/abr-testing/abr_testing/tools/abr_setup.py index 67ed5bfb333..d17773965b3 100644 --- a/abr-testing/abr_testing/tools/abr_setup.py +++ b/abr-testing/abr_testing/tools/abr_setup.py @@ -4,7 +4,10 @@ import configparser import traceback import sys +from datetime import datetime, timedelta +from typing import Any from hardware_testing.scripts import ABRAsairScript # type: ignore +from abr_testing.automation import google_sheets_tool from abr_testing.data_collection import ( get_run_logs, abr_google_drive, @@ -13,6 +16,49 @@ from abr_testing.tools import sync_abr_sheet +def clean_sheet(sheet_name: str, credentials: str) -> Any: + """Remove data older than 60 days from sheet.""" + sheet = google_sheets_tool.google_sheet( + credentials=credentials, file_name=sheet_name, tab_number=0 + ) + date_columns = sheet.get_column(3) + curr_date = datetime.now() + cutoff_days = 60 # Cutoff period in days + cutoff_date = curr_date - timedelta(days=cutoff_days) + + rem_rows = [] + for row_id, date in enumerate(date_columns): + # Convert to datetime if needed + formatted_date = None + if isinstance(date, str): # Assuming dates might be strings + try: + formatted_date = datetime.strptime(date, "%m/%d/%Y") + except ValueError: + try: + formatted_date = datetime.strptime(date, "%Y-%m-%d") + except ValueError: + continue + + # Check if the date is older than the cutoff + if formatted_date < cutoff_date: + rem_rows.append(row_id) + if len(rem_rows) > 1500: + break + if len(rem_rows) == 0: + # No more rows to remove + print("Nothing to remove") + return + print(f"Rows to be removed: {rem_rows}") + try: + sheet.batch_delete_rows(rem_rows) + print("deleted rows") + except Exception: + print("could not delete rows") + return + + clean_sheet(sheet_name, credentials) + + def run_sync_abr_sheet( storage_directory: str, abr_data_sheet: str, room_conditions_sheet: str ) -> None: @@ -20,9 +66,11 @@ def run_sync_abr_sheet( sync_abr_sheet.run(storage_directory, abr_data_sheet, room_conditions_sheet) -def run_temp_sensor(ip_file: str) -> None: +def run_temp_sensor(ambient_conditions_sheet: str, credentials: str) -> None: """Run temperature sensors on all robots.""" - processes = ABRAsairScript.run(ip_file) + # Remove entries > 60 days + clean_sheet(ambient_conditions_sheet, credentials) + processes = ABRAsairScript.run() for process in processes: process.start() time.sleep(20) @@ -64,7 +112,6 @@ def get_calibration_data( def main(configurations: configparser.ConfigParser) -> None: """Main function.""" - ip_file = None storage_directory = None email = None drive_folder = None @@ -72,39 +119,27 @@ def main(configurations: configparser.ConfigParser) -> None: ambient_conditions_sheet = None sheet_url = None - has_defaults = False # If default is not specified get all values default = configurations["DEFAULT"] - if len(default) > 0: - has_defaults = True - try: - if has_defaults: - storage_directory = default["Storage"] - email = default["Email"] - drive_folder = default["Drive_Folder"] - sheet_name = default["Sheet_Name"] - sheet_url = default["Sheet_Url"] - except KeyError as e: - print("Cannot read config file\n" + str(e)) + credentials = "" + if default: + try: + credentials = default["Credentials"] + except KeyError as e: + print("Cannot read config file\n" + str(e)) # Run Temperature Sensors - if not has_defaults: - ip_file = configurations["TEMP-SENSOR"]["Robo_List"] - ambient_conditions_sheet = configurations["TEMP-SENSOR"]["Sheet_Url"] + ambient_conditions_sheet = configurations["TEMP-SENSOR"]["Sheet_Url"] + ambient_conditions_sheet_name = configurations["TEMP-SENSOR"]["Sheet_Name"] print("Starting temp sensors...") - if ip_file: - run_temp_sensor(ip_file) - print("Temp Sensors Started") - else: - print("Missing ip_file location, please fix configs") - sys.exit(1) + run_temp_sensor(ambient_conditions_sheet_name, credentials) + print("Temp Sensors Started") # Get Run Logs and Record - if not has_defaults: - storage_directory = configurations["RUN-LOG"]["Storage"] - email = configurations["RUN-LOG"]["Email"] - drive_folder = configurations["RUN-LOG"]["Drive_Folder"] - sheet_name = configurations["RUN-LOG"]["Sheet_Name"] - sheet_url = configurations["RUN-LOG"]["Sheet_Url"] + storage_directory = configurations["RUN-LOG"]["Storage"] + email = configurations["RUN-LOG"]["Email"] + drive_folder = configurations["RUN-LOG"]["Drive_Folder"] + sheet_name = configurations["RUN-LOG"]["Sheet_Name"] + sheet_url = configurations["RUN-LOG"]["Sheet_Url"] print(sheet_name) if storage_directory and drive_folder and sheet_name and email: print("Retrieving robot run logs...") @@ -119,11 +154,10 @@ def main(configurations: configparser.ConfigParser) -> None: if storage_directory and sheet_url and ambient_conditions_sheet: run_sync_abr_sheet(storage_directory, sheet_url, ambient_conditions_sheet) # Collect calibration data - if not has_defaults: - storage_directory = configurations["CALIBRATION"]["Storage"] - email = configurations["CALIBRATION"]["Email"] - drive_folder = configurations["CALIBRATION"]["Drive_Folder"] - sheet_name = configurations["CALIBRATION"]["Sheet_Name"] + storage_directory = configurations["CALIBRATION"]["Storage"] + email = configurations["CALIBRATION"]["Email"] + drive_folder = configurations["CALIBRATION"]["Drive_Folder"] + sheet_name = configurations["CALIBRATION"]["Sheet_Name"] if storage_directory and drive_folder and sheet_name and email: print("Retrieving and recording robot calibration data...") get_calibration_data(storage_directory, drive_folder, sheet_name, email) diff --git a/abr-testing/abr_testing/tools/make_push.py b/abr-testing/abr_testing/tools/make_push.py new file mode 100644 index 00000000000..665831158bc --- /dev/null +++ b/abr-testing/abr_testing/tools/make_push.py @@ -0,0 +1,91 @@ +"""Push one or more folders to one or more robots.""" +import subprocess +import json +from typing import List +from multiprocessing import Process, Queue + +global folders +# Opentrons folders that can be pushed to robot +folders = [ + "abr-testing", + "hardware-testing", + "abr-testing + hardware-testing", + "other", +] + + +def push_subroutine(cmd: str, queue: Queue) -> None: + """Pushes specified folder to specified robot.""" + try: + subprocess.run(cmd) + queue.put(f"{cmd}: SUCCESS!\n") + except Exception: + queue.put(f"{cmd}: FAILED\n") + + +def main(folder_to_push: str, robot_to_push: str) -> int: + """Main process!""" + cmd = "make -C {folder} push-ot3 host={ip}" + robot_ip_path = "" + push_cmd = "" + processes: List[Process] = [] + queue: Queue = Queue() + folder_int = int(folder_to_push) + if folders[folder_int].lower() == "abr-testing + hardware-testing": + if robot_to_push.lower() == "all": + robot_ip_path = input("Path to robot ips: ") + with open(robot_ip_path, "r") as ip_file: + robot_json = json.load(ip_file) + robot_ips_dict = robot_json.get("ip_address_list") + robot_ips = list(robot_ips_dict.keys()) + ip_file.close() + else: + robot_ips = [robot_to_push] + for folder_name in folders[:-2]: + # Push abr-testing and hardware-testing folders to all robots + for robot in robot_ips: + push_cmd = cmd.format(folder=folder_name, ip=robot) + process = Process( + target=push_subroutine, + args=( + push_cmd, + queue, + ), + ) + process.start() + processes.append(process) + else: + + if folder_int == (len(folders) - 1): + folder_name = input("Which folder? ") + else: + folder_name = folders[folder_int] + if robot_to_push.lower() == "all": + robot_ip_path = input("Path to robot ips: ") + with open(robot_ip_path, "r") as ip_file: + robot_json = json.load(ip_file) + robot_ips = robot_json.get("ip_address_list") + ip_file.close() + else: + robot_ips = [robot_to_push] + + # Push folder to robots + for robot in robot_ips: + push_cmd = cmd.format(folder=folder_name, ip=robot) + process = Process(target=push_subroutine, args=(push_cmd, queue)) + process.start() + processes.append(process) + + for process in processes: + process.join() + result = queue.get() + print(f"\n{result}") + return 0 + + +if __name__ == "__main__": + for i, folder in enumerate(folders): + print(f"{i}) {folder}") + folder_to_push = input("Please Select a Folder to Push: ") + robot_to_push = input("Type in robots ip (type all for all): ") + print(main(folder_to_push, robot_to_push)) diff --git a/abr-testing/abr_testing/tools/module_control.py b/abr-testing/abr_testing/tools/module_control.py new file mode 100644 index 00000000000..5bc1f5cfb1d --- /dev/null +++ b/abr-testing/abr_testing/tools/module_control.py @@ -0,0 +1,138 @@ +"""Interface with opentrons modules!""" +from serial import Serial # type: ignore[import-untyped] +import asyncio +import subprocess +from typing import Any + +# Generic +_READ_ALL = "readall" +_READ_LINE = "read" +_DONE = "done" + +# TC commands +_MOVE_SEAL = "ms" +_MOVE_LID = "ml" +tc_gcode_shortcuts = { + "status": "M119", + _MOVE_SEAL: "M241.D", # move seal motor + _MOVE_LID: "M240.D", # move lid stepper motor + "ol": "M126", # open lid + "cl": "M127", # close lid + "sw": "M901.D", # status of all switches + "lt": "M141.D", # get lid temperature + "pt": "M105.D", # get plate temperature +} + +# HS Commands +hs_gcode_shortcuts = { + "srpm": "M3 S{rpm}", # Set RPM + "grpm": "M123", # Get RPM + "home": "G28", # Home + "deactivate": "M106", # Deactivate +} + +gcode_shortcuts = tc_gcode_shortcuts | hs_gcode_shortcuts + + +async def message_read(dev: Serial) -> Any: + """Read message.""" + response = dev.readline().decode() + while not response: + await asyncio.sleep(1) + response = dev.readline().decode() + return response + + +async def message_return(dev: Serial) -> Any: + """Wait until message becomes available.""" + try: + response = await asyncio.wait_for(message_read(dev), timeout=30) + return response + except asyncio.exceptions.TimeoutError: + print("response timed out.") + return "" + + +async def handle_module_gcode_shortcut( + dev: Serial, command: str, in_commands: bool, output: str = "" +) -> None: + """Handle debugging commands that require followup.""" + if in_commands: + if command == _MOVE_SEAL: + distance = input("enter distance in steps => ") + dev.write( + f"{gcode_shortcuts[command]} {distance}\n".encode() + ) # (+) -> retract, (-) -> engage + # print(await message_return(dev)) + elif command == _MOVE_LID: + distance = input( + "enter angular distance in degrees => " + ) # (+) -> open, (-) -> close + dev.write(f"{gcode_shortcuts[command]} {distance}\n".encode()) + # print(await message_return(dev)) + # everything else + else: + dev.write(f"{gcode_shortcuts[command]}\n".encode()) + else: + dev.write(f"{command}\n".encode()) + try: + mr = await message_return(dev) + print(mr) + except TypeError: + print("Invalid input") + return + + if output: + try: + with open(output, "a") as result_file: + if "OK" in mr: + status = command + ": SUCCESS" + else: + status = command + ": FAILURE" + result_file.write(status) + result_file.write(f" {mr}") + result_file.close() + except FileNotFoundError: + print(f"cannot open file: {output}") + + +async def comms_loop(dev: Serial, commands: list, output: str = "") -> bool: + """Loop for commands.""" + _exit = False + try: + command = commands.pop(0) + except IndexError: + command = input("\n>>> ") + if command == _READ_ALL: + print(dev.readlines()) + elif command == _READ_LINE: + print(dev.readline()) + elif command == _DONE: + _exit = True + elif command in gcode_shortcuts: + await handle_module_gcode_shortcut(dev, command, True, output) + else: + await handle_module_gcode_shortcut(dev, command, False, output) + return _exit + + +async def _main(module: str, commands: list = [], output: str = "") -> bool: + """Main process.""" + module_name = ( + subprocess.check_output(["find", "/dev/", "-name", f"*{module}*"]) + .decode() + .strip() + ) + if not module_name: + print(f"{module} not found. Exiting.") + return False + dev = Serial(f"{module_name}", 9600, timeout=2) + _exit = False + while not _exit: + _exit = await comms_loop(dev, commands, output) + dev.close() + return True + + +if __name__ == "__main__": + asyncio.run(_main("heatershaker")) diff --git a/abr-testing/abr_testing/tools/test_modules.py b/abr-testing/abr_testing/tools/test_modules.py new file mode 100644 index 00000000000..8c372fbff53 --- /dev/null +++ b/abr-testing/abr_testing/tools/test_modules.py @@ -0,0 +1,155 @@ +"""Modules Tests Script!""" +import asyncio +import time +from datetime import datetime +import os +import module_control # type: ignore +from typing import Any, Tuple, Dict +import traceback + +# To run: +# SSH into robot +# cd /opt/opentrons-robot-server/abr-testing/tools +# python3 test_modules.py + + +async def tc_test_1(module: str, path_to_file: str) -> None: + """Thermocycler Test 1 Open and Close Lid.""" + duration = int(input("How long to run this test for? (in seconds): ")) + start = time.time() + while time.time() - start < duration: + try: + await (tc_open_lid(module, path_to_file)) + except asyncio.TimeoutError: + return + time.sleep(5) + try: + await (tc_close_lid(module, path_to_file)) + except asyncio.TimeoutError: + return + time.sleep(5) + + +async def hs_test_1(module: str, path_to_file: str) -> None: + """Heater Shaker Test 1. (Home and Shake).""" + duration = int(input("How long to run this test for? (in seconds): ")) + rpm = input("Target RPM (200-3000): ") + start = time.time() + while time.time() - start < duration: + try: + await (hs_test_home(module, path_to_file)) + except asyncio.TimeoutError: + return + time.sleep(5) + try: + await (hs_test_set_shake(module, rpm, path_to_file)) + except asyncio.TimeoutError: + return + time.sleep(10) + try: + await (hs_test_set_shake(module, "0", path_to_file)) + except asyncio.TimeoutError: + return + time.sleep(10) + + +async def input_codes(module: str, path_to_file: str) -> None: + """Opens serial for manual code input.""" + await module_control._main(module, output=path_to_file) + + +hs_tests: Dict[str, Tuple[Any, str]] = { + "Test 1": (hs_test_1, "Repeatedly home heater shaker then set shake speed"), + "Input GCodes": (input_codes, "Input g codes"), +} + +tc_tests: Dict[str, Tuple[Any, str]] = { + "Test 1": (tc_test_1, "Repeatedly open and close TC lid"), + "Input GCodes": (input_codes, "Input g codes"), +} + +global modules + +modules = { + "heatershaker": hs_tests, + "thermocycler": tc_tests, +} + + +async def main(module: str) -> None: + """Select test to be run.""" + # Select test to run + # Set directory for tests + BASE_DIRECTORY = "/userfs/data/testing_data/" + if not os.path.exists(BASE_DIRECTORY): + os.makedirs(BASE_DIRECTORY) + tests = modules[module] + for i, test in enumerate(tests.keys()): + function, description = tests[test] + print(f"{i}) {test} : {description}") + selected_test = int(input("Please select a test: ")) + try: + function, description = tests[list(tests.keys())[selected_test]] + test_dir = BASE_DIRECTORY + f"{module}/test/{list(tests.keys())[selected_test]}" + print(f"{i}, {description}") + print(f"TEST DIR: {test_dir}") + date = datetime.now() + filename = f"results_{datetime.strftime(date, '%Y-%m-%d_%H:%M:%S')}.txt" + output_file = os.path.join(test_dir, filename) + try: + if not os.path.exists(test_dir): + os.makedirs(test_dir) + open(output_file, "a").close() + except Exception: + traceback.print_exc() + print(f"PATH: {output_file} ") + await (function(module, output_file)) + except Exception: + print("Failed to run test") + traceback.print_exc() + + +# HS Test Functions +async def hs_test_home(module: str, path_to_file: str) -> None: + """Home heater shaker.""" + hs_gcodes = module_control.hs_gcode_shortcuts + home_gcode = hs_gcodes["home"] + await (module_control._main(module, [home_gcode, "done"], path_to_file)) + + +async def hs_test_set_shake(module: str, rpm: str, path_to_file: str) -> None: + """Shake heater shaker at specified speed.""" + hs_gcodes = module_control.hs_gcode_shortcuts + set_shake_gcode = hs_gcodes["srpm"].format(rpm=rpm) + await (module_control._main(module, [set_shake_gcode, "done"], path_to_file)) + + +async def hs_deactivate(module: str, path_to_file: str) -> None: + """Deactivate Heater Shaker.""" + hs_gcodes = module_control.hs_gcode_shortcuts + deactivate_gcode = hs_gcodes["deactivate"] + await (module_control._main(module, [deactivate_gcode, "done"], path_to_file)) + + +# TC Test Functions +async def tc_open_lid(module: str, path_to_file: str) -> None: + """Open thermocycler lid.""" + tc_gcodes = module_control.tc_gcode_shortcuts + open_lid_gcode = tc_gcodes["ol"] + await (module_control._main(module, [open_lid_gcode, "done"], path_to_file)) + + +async def tc_close_lid(module: str, path_to_file: str) -> None: + """Open thermocycler lid.""" + tc_gcodes = module_control.tc_gcode_shortcuts + close_lid_gcode = tc_gcodes["cl"] + await (module_control._main(module, [close_lid_gcode, "done"], path_to_file)) + + +if __name__ == "__main__": + print("Modules:") + for i, module in enumerate(modules): + print(f"{i}) {module}") + module_int = int(input("Please select a module: ")) + module = list(modules.keys())[module_int] + asyncio.run(main(module)) diff --git a/abr-testing/test_debug b/abr-testing/test_debug new file mode 100644 index 00000000000..e69de29bb2d diff --git a/analyses-snapshot-testing/Makefile b/analyses-snapshot-testing/Makefile index 13c4e603f3c..6918d17bf3e 100644 --- a/analyses-snapshot-testing/Makefile +++ b/analyses-snapshot-testing/Makefile @@ -1,38 +1,56 @@ +BASE_IMAGE_NAME ?= opentrons-python-base:3.10 +CACHEBUST ?= $(shell date +%s) +ANALYSIS_REF ?= edge +PROTOCOL_NAMES ?= all +OVERRIDE_PROTOCOL_NAMES ?= all +LOCAL_IMAGE_TAG ?= local +ANALYZER_IMAGE_NAME ?= opentrons-analysis + +export ANALYSIS_REF # tag, branch or commit for the opentrons repository. Used as the image tag for the analyzer image +export PROTOCOL_NAMES # tell the test which protocols to run +export OVERRIDE_PROTOCOL_NAMES # tell the test which override protocols to run + +ifeq ($(CI), true) + PYTHON=python +else + PYTHON=pyenv exec python +endif + .PHONY: black black: - python -m pipenv run python -m black . + $(PYTHON) -m pipenv run python -m black . .PHONY: black-check black-check: - python -m pipenv run python -m black . --check + $(PYTHON) -m pipenv run python -m black . --check .PHONY: ruff ruff: - python -m pipenv run python -m ruff check . --fix + $(PYTHON) -m pipenv run python -m ruff check . --fix .PHONY: ruff-check ruff-check: - python -m pipenv run python -m ruff check . + $(PYTHON) -m pipenv run python -m ruff check . .PHONY: mypy mypy: - python -m pipenv run python -m mypy automation tests citools + $(PYTHON) -m pipenv run python -m mypy automation tests citools .PHONY: lint lint: black-check ruff-check mypy .PHONY: format format: - @echo runnning black + @echo "Running black" $(MAKE) black - @echo running ruff + @echo "Running ruff" $(MAKE) ruff - @echo formatting the readme with yarn prettier + @echo "Formatting the readme with yarn prettier" $(MAKE) format-readme .PHONY: test-ci test-ci: - python -m pipenv run python -m pytest -m "emulated_alpha" + $(PYTHON) -m pipenv run python -m pytest -m "emulated_alpha" .PHONY: test-protocol-analysis test-protocol-analysis: @@ -40,66 +58,88 @@ test-protocol-analysis: .PHONY: setup setup: install-pipenv - python -m pipenv install + $(PYTHON) -m pipenv install .PHONY: teardown teardown: - python -m pipenv --rm + $(PYTHON) -m pipenv --rm .PHONY: format-readme format-readme: - yarn prettier --ignore-path .eslintignore --write analyses-snapshot-testing/**/*.md + yarn prettier --ignore-path .eslintignore --write analyses-snapshot-testing/**/*.md .github/workflows/analyses-snapshot-test.yaml .PHONY: install-pipenv install-pipenv: - python -m pip install -U pipenv - -ANALYSIS_REF ?= edge -PROTOCOL_NAMES ?= all -OVERRIDE_PROTOCOL_NAMES ?= all - -export ANALYSIS_REF -export PROTOCOL_NAMES -export OVERRIDE_PROTOCOL_NAMES + $(PYTHON) -m pip install -U pipenv .PHONY: snapshot-test snapshot-test: @echo "ANALYSIS_REF is $(ANALYSIS_REF)" @echo "PROTOCOL_NAMES is $(PROTOCOL_NAMES)" @echo "OVERRIDE_PROTOCOL_NAMES is $(OVERRIDE_PROTOCOL_NAMES)" - python -m pipenv run pytest -k analyses_snapshot_test -vv + $(PYTHON) -m pipenv run pytest -k analyses_snapshot_test -vv .PHONY: snapshot-test-update snapshot-test-update: @echo "ANALYSIS_REF is $(ANALYSIS_REF)" @echo "PROTOCOL_NAMES is $(PROTOCOL_NAMES)" @echo "OVERRIDE_PROTOCOL_NAMES is $(OVERRIDE_PROTOCOL_NAMES)" - python -m pipenv run pytest -k analyses_snapshot_test --snapshot-update + $(PYTHON) -m pipenv run pytest -k analyses_snapshot_test --snapshot-update -CACHEBUST := $(shell date +%s) +.PHONY: build-base-image +build-base-image: + @echo "Building the base image $(BASE_IMAGE_NAME)" + docker build --build-arg BASE_IMAGE_NAME=$(BASE_IMAGE_NAME) -f citools/Dockerfile.base -t $(BASE_IMAGE_NAME) citools/. .PHONY: build-opentrons-analysis build-opentrons-analysis: - @echo "Building docker image for $(ANALYSIS_REF)" - @echo "The image will be named opentrons-analysis:$(ANALYSIS_REF)" - @echo "If you want to build a different version, run 'make build-opentrons-analysis ANALYSIS_REF='" - @echo "Cache is always busted to ensure latest version of the code is used" - docker build --build-arg ANALYSIS_REF=$(ANALYSIS_REF) --build-arg CACHEBUST=$(CACHEBUST) -t opentrons-analysis:$(ANALYSIS_REF) citools/. + @echo "Building docker image for opentrons repository reference$(ANALYSIS_REF)" + @echo "The image will be named $(ANALYZER_IMAGE_NAME):$(ANALYSIS_REF)" + @echo "If you want to build a different version, run 'make build-opentrons-analysis ANALYSIS_REF='" + docker build --build-arg BASE_IMAGE_NAME=$(BASE_IMAGE_NAME) --build-arg ANALYSIS_REF=$(ANALYSIS_REF) --build-arg CACHEBUST=$(CACHEBUST) -t $(ANALYZER_IMAGE_NAME):$(ANALYSIS_REF) -f citools/Dockerfile.analyze citools/. + +.PHONY: build-local +build-local: + @echo "Building docker image for your local opentrons code" + @echo "This image will be named $(ANALYZER_IMAGE_NAME):$(LOCAL_IMAGE_TAG)" + docker build --build-arg BASE_IMAGE_NAME=$(BASE_IMAGE_NAME) -t $(ANALYZER_IMAGE_NAME):$(LOCAL_IMAGE_TAG) -f citools/Dockerfile.local .. + @echo "Build complete" + +.PHONY: snapshot-test-local +snapshot-test-local: ANALYSIS_REF=$(LOCAL_IMAGE_TAG) +snapshot-test-local: build-base-image build-local + @echo "This target is overriding the ANALYSIS_REF to the LOCAL_IMAGE_TAG: $(LOCAL_IMAGE_TAG)" + @echo "ANALYSIS_REF is $(ANALYSIS_REF). The the test maps this env variable to the image tag." + @echo "The image the test will use is $(ANALYZER_IMAGE_NAME):$(LOCAL_IMAGE_TAG)" + @echo "PROTOCOL_NAMES is $(PROTOCOL_NAMES)" + @echo "OVERRIDE_PROTOCOL_NAMES is $(OVERRIDE_PROTOCOL_NAMES)" + $(PYTHON) -m pipenv run pytest -k analyses_snapshot_test -vv + +.PHONY: snapshot-test-update-local +snapshot-test-update-local: ANALYSIS_REF=$(LOCAL_IMAGE_TAG) +snapshot-test-update-local: build-base-image build-local + @echo "This target is overriding the ANALYSIS_REF to the LOCAL_IMAGE_TAG: $(LOCAL_IMAGE_TAG)" + @echo "ANALYSIS_REF is $(ANALYSIS_REF). The the test maps this env variable to the image tag." + @echo "The image the test will use is $(ANALYZER_IMAGE_NAME):$(LOCAL_IMAGE_TAG)" + @echo "PROTOCOL_NAMES is $(PROTOCOL_NAMES)" + @echo "OVERRIDE_PROTOCOL_NAMES is $(OVERRIDE_PROTOCOL_NAMES)" + $(PYTHON) -m pipenv run pytest -k analyses_snapshot_test --snapshot-update .PHONY: generate-protocols generate-protocols: - python -m pipenv run python -m automation.data.protocol_registry + $(PYTHON) -m pipenv run python -m automation.data.protocol_registry +# Tools for running the robot server in a container OPENTRONS_VERSION ?= edge -export OPENTRONS_VERSION +export OPENTRONS_VERSION # used for the robot server image as the tag, branch or commit for the opentrons repository .PHONY: build-rs build-rs: @echo "Building docker image for opentrons-robot-server:$(OPENTRONS_VERSION)" @echo "Cache is always busted to ensure latest version of the code is used" @echo "If you want to build a different version, run 'make build-rs OPENTRONS_VERSION=chore_release-8.0.0'" - docker build --build-arg OPENTRONS_VERSION=$(OPENTRONS_VERSION) --build-arg CACHEBUST=$(CACHEBUST) -t opentrons-robot-server:$(OPENTRONS_VERSION) -f citools/Dockerfile.server . + docker build --build-arg BASE_IMAGE_NAME=$(BASE_IMAGE_NAME) --build-arg OPENTRONS_VERSION=$(OPENTRONS_VERSION) --build-arg CACHEBUST=$(CACHEBUST) -t opentrons-robot-server:$(OPENTRONS_VERSION) -f citools/Dockerfile.server . .PHONY: run-flex run-flex: diff --git a/analyses-snapshot-testing/README.md b/analyses-snapshot-testing/README.md index 51a8e194ca1..03ce1d87518 100644 --- a/analyses-snapshot-testing/README.md +++ b/analyses-snapshot-testing/README.md @@ -4,7 +4,7 @@ 1. Follow the instructions in [DEV_SETUP.md](../DEV_SETUP.md) 1. `cd analyses-snapshot-testing` -1. use pyenv to install python 3.12 and set it as the local python version for this directory +1. use pyenv to install python 3.13 and set it as the local python version for this directory 1. `make setup` 1. Have docker installed and ready @@ -18,7 +18,10 @@ > This ALWAYS gets the remote code pushed to Opentrons/opentrons for the specified ANALYSIS_REF -`make build-opentrons-analysis ANALYSIS_REF=chore_release-8.0.0` +- build the base image + - `make build-base-image` +- build the opentrons-analysis image + - `make build-opentrons-analysis ANALYSIS_REF=release` ## Running the tests locally @@ -51,10 +54,35 @@ ```shell cd analyses-snapshot-testing \ -&& make build-rs OPENTRONS_VERSION=chore_release-8.0.0 \ -&& make run-rs OPENTRONS_VERSION=chore_release-8.0.0` +&& make build-base-image \ +&& make build-rs OPENTRONS_VERSION=release \ +&& make run-rs OPENTRONS_VERSION=release` ``` ### Default OPENTRONS_VERSION=edge in the Makefile so you can omit it if you want latest edge -`cd analyses-snapshot-testing && make build-rs && make run-rs` +```shell +cd analyses-snapshot-testing \ +&& make build-base-image \ +&& make build-rs \ +&& make run-rs +``` + +## Running the Analyses Battery against your local code + +> This copies in your local code to the container and runs the analyses battery against it. + +`cd PYENV_ROOT && git pull` - make sure pyenv is up to date so you may install python 3.13.0 +`pyenv install 3.13.0` - install python 3.13.0 +`cd /analyses-snapshot-testing` - navigate to the analyses-snapshot-testing directory +`pyenv local 3.13.0` - set the local python version to 3.13.0 +`make setup` - install the requirements +`make snapshot-test-local` - this target builds the base image, builds the local code into the base image, then runs the analyses battery against the image you just created + +You have the option to specify one or many protocols to run the analyses on. This is also described above [Running the tests against specific protocols](#running-the-tests-against-specific-protocols) + +- `make snapshot-test-local PROTOCOL_NAMES=Flex_S_v2_19_Illumina_DNA_PCR_Free OVERRIDE_PROTOCOL_NAMES=none` + +### Updating the snapshots locally + +- `make snapshot-test-update-local` - this target builds the base image, builds the local code into the base image, then runs the analyses battery against the image you just created, updating the snapshots by passing the `--update-snapshots` flag to the test diff --git a/analyses-snapshot-testing/automation/data/protocols.py b/analyses-snapshot-testing/automation/data/protocols.py index ada74a736e0..4acad3c0702 100644 --- a/analyses-snapshot-testing/automation/data/protocols.py +++ b/analyses-snapshot-testing/automation/data/protocols.py @@ -709,6 +709,13 @@ class Protocols: robot="Flex", ) + # analyses-snapshot-testing/files/protocols/Flex_S_v2_21_P1000_96_GRIP_HS_MB_TC_TM_Smoke.py + Flex_S_v2_21_P1000_96_GRIP_HS_MB_TC_TM_Smoke: Protocol = Protocol( + file_stem="Flex_S_v2_21_P1000_96_GRIP_HS_MB_TC_TM_Smoke", + file_extension="py", + robot="Flex", + ) + OT2_X_v2_18_None_None_duplicateRTPVariableName: Protocol = Protocol( file_stem="OT2_X_v2_18_None_None_duplicateRTPVariableName", file_extension="py", diff --git a/analyses-snapshot-testing/citools/Dockerfile b/analyses-snapshot-testing/citools/Dockerfile deleted file mode 100644 index 123b7636652..00000000000 --- a/analyses-snapshot-testing/citools/Dockerfile +++ /dev/null @@ -1,26 +0,0 @@ -# Use 3.10 just like the app does -FROM python:3.10-slim-bullseye - -# Update packages and install git -RUN apt-get update && \ - apt-get upgrade -y && \ - apt-get install -y git libsystemd-dev - -# Define build arguments -ARG ANALYSIS_REF=edge - -# Set the working directory in the container -WORKDIR /opentrons - -# Clone the Opentrons repository at the specified commit or tag -ARG CACHEBUST=1 -RUN git clone --branch $ANALYSIS_REF --depth 1 https://github.com/Opentrons/opentrons . - -# Install packages from local directories -RUN python -m pip install -U ./shared-data/python -RUN python -m pip install -U ./hardware[flex] -RUN python -m pip install -U ./api -RUN python -m pip install -U pandas==1.4.3 - -# The default command to run when starting the container -CMD ["tail", "-f", "/dev/null"] diff --git a/analyses-snapshot-testing/citools/Dockerfile.analyze b/analyses-snapshot-testing/citools/Dockerfile.analyze new file mode 100644 index 00000000000..1b85981cdaf --- /dev/null +++ b/analyses-snapshot-testing/citools/Dockerfile.analyze @@ -0,0 +1,22 @@ +ARG BASE_IMAGE_NAME=opentrons-python-base:3.10 + +FROM ${BASE_IMAGE_NAME} + +# Define build arguments +ARG ANALYSIS_REF=edge + +# Set the working directory in the container +WORKDIR /opentrons + +# Clone the Opentrons repository at the specified commit or tag +ARG CACHEBUST=1 +RUN git clone --branch $ANALYSIS_REF --depth 1 https://github.com/Opentrons/opentrons . + +# Install packages from local directories +RUN python -m pip install -U ./shared-data/python +RUN python -m pip install -U ./hardware[flex] +RUN python -m pip install -U ./api +RUN python -m pip install -U pandas==1.4.3 + +# The default command to run when starting the container +CMD ["tail", "-f", "/dev/null"] diff --git a/analyses-snapshot-testing/citools/Dockerfile.base b/analyses-snapshot-testing/citools/Dockerfile.base new file mode 100644 index 00000000000..086987e671b --- /dev/null +++ b/analyses-snapshot-testing/citools/Dockerfile.base @@ -0,0 +1,7 @@ +# Use Python 3.10 as the base image +FROM python:3.10-slim-bullseye + +# Update packages and install dependencies +RUN apt-get update && \ + apt-get upgrade -y && \ + apt-get install -y git libsystemd-dev build-essential pkg-config network-manager diff --git a/analyses-snapshot-testing/citools/Dockerfile.local b/analyses-snapshot-testing/citools/Dockerfile.local new file mode 100644 index 00000000000..2346b4680c2 --- /dev/null +++ b/analyses-snapshot-testing/citools/Dockerfile.local @@ -0,0 +1,19 @@ +ARG BASE_IMAGE_NAME=opentrons-python-base:3.10 + +FROM ${BASE_IMAGE_NAME} + +# Set the working directory in the container +WORKDIR /opentrons + +# Copy everything from the build context into the /opentrons directory +# root directory .dockerignore file is respected +COPY . /opentrons + +# Install required packages from the copied code +RUN python -m pip install -U ./shared-data/python +RUN python -m pip install -U ./hardware[flex] +RUN python -m pip install -U ./api +RUN python -m pip install -U pandas==1.4.3 + +# The default command to keep the container running +CMD ["tail", "-f", "/dev/null"] diff --git a/analyses-snapshot-testing/citools/Dockerfile.server b/analyses-snapshot-testing/citools/Dockerfile.server index 6d4d9edcda3..0c44c1e04f0 100644 --- a/analyses-snapshot-testing/citools/Dockerfile.server +++ b/analyses-snapshot-testing/citools/Dockerfile.server @@ -1,10 +1,6 @@ -# Use Python 3.10 as the base image -FROM python:3.10-slim-bullseye +ARG BASE_IMAGE_NAME=opentrons-python-base:3.10 -# Update packages and install dependencies -RUN apt-get update && \ - apt-get upgrade -y && \ - apt-get install -y git libsystemd-dev build-essential pkg-config network-manager +FROM ${BASE_IMAGE_NAME} # Define build arguments ARG OPENTRONS_VERSION=edge diff --git a/analyses-snapshot-testing/citools/generate_analyses.py b/analyses-snapshot-testing/citools/generate_analyses.py index 52aba70363b..7d550b47776 100644 --- a/analyses-snapshot-testing/citools/generate_analyses.py +++ b/analyses-snapshot-testing/citools/generate_analyses.py @@ -24,7 +24,7 @@ HOST_RESULTS: Path = Path(Path(__file__).parent.parent, "analysis_results") ANALYSIS_SUFFIX: str = "analysis.json" ANALYSIS_TIMEOUT_SECONDS: int = 30 -ANALYSIS_CONTAINER_INSTANCES: int = 5 +MAX_ANALYSIS_CONTAINER_INSTANCES: int = 5 console = Console() @@ -241,6 +241,12 @@ def analyze_against_image(tag: str, protocols: List[TargetProtocol], num_contain return protocols +def get_container_instances(protocol_len: int) -> int: + # Scaling linearly with the number of protocols + instances = max(1, min(MAX_ANALYSIS_CONTAINER_INSTANCES, protocol_len // 10)) + return instances + + def generate_analyses_from_test(tag: str, protocols: List[Protocol]) -> None: """Generate analyses from the tests.""" start_time = time.time() @@ -260,6 +266,7 @@ def generate_analyses_from_test(tag: str, protocols: List[Protocol]) -> None: protocol_custom_labware_paths_in_container(test_protocol), ) ) - analyze_against_image(tag, protocols_to_process, ANALYSIS_CONTAINER_INSTANCES) + instance_count = get_container_instances(len(protocols_to_process)) + analyze_against_image(tag, protocols_to_process, instance_count) end_time = time.time() console.print(f"Clock time to generate analyses: {end_time - start_time:.2f} seconds.") diff --git a/analyses-snapshot-testing/files/protocols/Flex_S_v2_21_P1000_96_GRIP_HS_MB_TC_TM_Smoke.py b/analyses-snapshot-testing/files/protocols/Flex_S_v2_21_P1000_96_GRIP_HS_MB_TC_TM_Smoke.py new file mode 100644 index 00000000000..20d77fd560f --- /dev/null +++ b/analyses-snapshot-testing/files/protocols/Flex_S_v2_21_P1000_96_GRIP_HS_MB_TC_TM_Smoke.py @@ -0,0 +1,458 @@ +############# +# CHANGELOG # +############# + +# ---- +# 2.21 +# ---- + +# - - Run protocols that use the Absorbance Plate Reader and check the status of the module on the robot details screen for your Flex. +# - - Run protocols that use the new Opentrons Tough PCR Auto-Sealing Lid with the Thermocycler Module GEN2. Stacks of these lids appear in a consolidated view when setting up labware. +# - - Error recovery now works in more situations and has more options - Gripper Errors, Drop Tip Errors, additional recovery options for tip errors, disable error recovery entirely (8.2.0) + +# ---- +# 2.20 +# ---- + +# - configure_nozzle_layout() now accepts row, single, and partial column layout constants. See Partial Tip Pickup. +# - You can now call ProtocolContext.define_liquid() without supplying a description or display_color. +# - You can now call ProtocolContext.liquid_presence_detection() or ProtocolContext.require_liquid_presence() to use LLD on instrument context or on well +# - You now have the option to set RTP using CSV files +# - Error Recovery will now initiate for miss tip pick up, overpressure on aspirate and dispense, and if liquid presence isn't detected (8.0.0) + +# ---- +# 2.19 +# ---- + +# - NO FEATURES OR API CHANGES +# - New values for how much a tip overlaps with the pipette nozzle when the pipette picks up tips + +# ---- +# 2.18 +# ---- + +# - labware.set_offset +# - Runtime Parameters added +# - TrashContainer.top() and Well.top() now return objects of the same type +# - pipette.drop_tip() if location argument not specified the tips will be dropped at different locations in the bin +# - pipette.drop_tip() if location is specified, the tips will be dropped in the same place every time + +# ---- +# 2.17 +# ---- + +# NOTHING NEW +# 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 protocol + +# ---- +# 2.16 +# ---- + +# - prepare_to_aspirate added +# - fixed_trash property changed +# - instrument_context.trash_container property changed + +# ---- +# 2.15 +# ---- + +# - move_labware added - Manual Deck State Modification +# - ProtocolContext.load_adapter added +# - OFF_DECK location added + +from typing import List +from opentrons import protocol_api, types +from opentrons.protocol_api import Labware + + +metadata = { + "protocolName": "Flex Smoke Test - v2.21", + "author": "QA team", +} + +requirements = { + "robotType": "Flex", + "apiLevel": "2.21", +} + + +################# +### CONSTANTS ### +################# + +DeckSlots = [ + "A1", + "A2", + "A3", + "A4", + "B1", + "B2", + "B3", + "B4", + "C1", + "C2", + "C3", + "C4", + "D1", + "D2", + "D3", + "D4", +] + +HEATER_SHAKER_ADAPTER_NAME = "opentrons_96_pcr_adapter" +HEATER_SHAKER_NAME = "heaterShakerModuleV1" +MAGNETIC_BLOCK_NAME = "magneticBlockV1" +TEMPERATURE_MODULE_ADAPTER_NAME = "opentrons_96_well_aluminum_block" +TEMPERATURE_MODULE_NAME = "temperature module gen2" +THERMOCYCLER_NAME = "thermocycler module gen2" +ABSORBANCE_READER = "absorbanceReaderV1" +DECK_RISER_NAME = "opentrons_flex_deck_riser" +TC_LID = "opentrons_tough_pcr_auto_sealing_lid" +LID_COUNT = 5 +TIPRACK_96_ADAPTER_NAME = "opentrons_flex_96_tiprack_adapter" +TIPRACK_96_NAME = "opentrons_flex_96_tiprack_1000ul" +PIPETTE_96_CHANNEL_NAME = "flex_96channel_1000" +WELL_PLATE_STARTING_POSITION = "C2" +RESERVOIR_STARTING_POSITION = "D2" + + +def comment_tip_rack_status(ctx, tip_rack): + """ + Print out the tip status for each row in a tip rack. + Each row (A-H) will print the well statuses for columns 1-12 in a single comment, + with a '🟢' for present tips and a '❌' for missing tips. + """ + range_A_to_H = [chr(i) for i in range(ord("A"), ord("H") + 1)] + range_1_to_12 = range(1, 13) + + ctx.comment(f"Tip rack in {tip_rack.parent}") + + for row in range_A_to_H: + status_line = f"{row}: " + for col in range_1_to_12: + well = f"{row}{col}" + has_tip = tip_rack.wells_by_name()[well].has_tip + status_emoji = "🟢" if has_tip else "❌" + status_line += f"{well} {status_emoji} " + + # Print the full status line for the row + ctx.comment(status_line) + + +############################## +# Runtime Parameters Support # +############################## + +# -------------------------- # +# Added in API version: 2.18 # +# -------------------------- # + + +def add_parameters(parameters: protocol_api.Parameters): + """This is the standard use of parameters""" + + # We are using the defaults for every case. + # Other tests cover regression testing for + # other types of parameters and UI appearance + # there are many tests in Analyses Battery that cover errors and edge cases + + parameters.add_str( + variable_name="test_configuration", + display_name="Test Configuration", + description="Configuration of QA test to perform", + default="full", + choices=[{"display_name": "Full Smoke Test", "value": "full"}], + ) + + parameters.add_str( + variable_name="reservoir_name", + display_name="Reservoir Name", + description="Name of the reservoir", + default="nest_1_reservoir_290ml", + choices=[{"display_name": "Nest 1 Well 290 mL", "value": "nest_1_reservoir_290ml"}], + ) + + parameters.add_str( + variable_name="well_plate_name", + display_name="Well Plate Name", + description="Name of the well plate", + default="opentrons_96_wellplate_200ul_pcr_full_skirt", + choices=[{"display_name": "Opentrons Tough 96 Well 200 µL", "value": "opentrons_96_wellplate_200ul_pcr_full_skirt"}], + ) + + +def log_position(ctx, item): + ctx.comment(f"Item {item.load_name} is at {item.parent}") + + +def run(ctx: protocol_api.ProtocolContext) -> None: + + ################ + ### FIXTURES ### + ################ + + waste_chute = ctx.load_waste_chute() + + ############### + ### MODULES ### + ############### + deck_riser_adapter = ctx.load_adapter(DECK_RISER_NAME, "A4") + thermocycler = ctx.load_module(THERMOCYCLER_NAME) # A1 & B1 + magnetic_block = ctx.load_module(MAGNETIC_BLOCK_NAME, "A3") + heater_shaker = ctx.load_module(HEATER_SHAKER_NAME, "C1") + temperature_module = ctx.load_module(TEMPERATURE_MODULE_NAME, "D1") + absorbance_module = ctx.load_module(ABSORBANCE_READER, "B3") + + lids: List[Labware] = [deck_riser_adapter.load_labware(TC_LID)] + for i in range(LID_COUNT - 1): + lids.append(lids[-1].load_labware(TC_LID)) + lids.reverse() + + thermocycler.open_lid() + heater_shaker.open_labware_latch() + absorbance_module.close_lid() + absorbance_module.initialize("single", [600], 450) + absorbance_module.open_lid() + + ####################### + ### MODULE ADAPTERS ### + ####################### + + temperature_module_adapter = temperature_module.load_adapter(TEMPERATURE_MODULE_ADAPTER_NAME) + heater_shaker_adapter = heater_shaker.load_adapter(HEATER_SHAKER_ADAPTER_NAME) + adapters = [temperature_module_adapter, heater_shaker_adapter] + + ############### + ### LABWARE ### + ############### + + # Load these directly with the RTP + source_reservoir = ctx.load_labware(ctx.params.reservoir_name, RESERVOIR_STARTING_POSITION) + dest_pcr_plate = ctx.load_labware(ctx.params.well_plate_name, WELL_PLATE_STARTING_POSITION) + + tip_rack_1 = ctx.load_labware(TIPRACK_96_NAME, "A2", adapter=TIPRACK_96_ADAPTER_NAME) + tip_rack_adapter = tip_rack_1.parent + + tip_rack_2 = ctx.load_labware(TIPRACK_96_NAME, "C3") + tip_rack_3 = ctx.load_labware(TIPRACK_96_NAME, "C4") + tip_rack_5 = ctx.load_labware(TIPRACK_96_NAME, protocol_api.OFF_DECK) + + tip_racks = [tip_rack_1, tip_rack_2, tip_rack_3] + + ########################## + ### PIPETTE DEFINITION ### + ########################## + + pipette_96_channel = ctx.load_instrument(PIPETTE_96_CHANNEL_NAME, mount="left", tip_racks=tip_racks, liquid_presence_detection=True) + pipette_96_channel.trash_container = waste_chute + + assert isinstance(pipette_96_channel.trash_container, protocol_api.WasteChute) + + ######################## + ### LOAD SOME LIQUID ### + ######################## + + water = ctx.define_liquid(name="water", description="High Quality H₂O", display_color="#42AB2D") + source_reservoir.wells_by_name()["A1"].load_liquid(liquid=water, volume=29000) + + ################################ + ### GRIPPER LABWARE MOVEMENT ### + ################################ + + log_position(ctx, dest_pcr_plate) + ctx.move_labware(dest_pcr_plate, thermocycler, use_gripper=True) + log_position(ctx, dest_pcr_plate) + # Move it back to the deck + ctx.move_labware(dest_pcr_plate, "C2", use_gripper=True) + log_position(ctx, dest_pcr_plate) + + # Other important moves? + + # manual move + log_position(ctx, source_reservoir) + ctx.move_labware(source_reservoir, "D4", use_gripper=False) + log_position(ctx, source_reservoir) + ctx.move_labware(source_reservoir, RESERVOIR_STARTING_POSITION, use_gripper=True) + log_position(ctx, source_reservoir) + + # Other important manual moves? + + # 96 channel column pickup + pipette_96_channel.configure_nozzle_layout(style=protocol_api.COLUMN, start="A12") + pipette_96_channel.pick_up_tip(tip_rack_2["A1"]) + comment_tip_rack_status(ctx, tip_rack_2) + pipette_96_channel.aspirate(5, source_reservoir["A1"]) + pipette_96_channel.touch_tip() + pipette_96_channel.dispense(5, dest_pcr_plate[f"A1"]) + pipette_96_channel.drop_tip(waste_chute) + + # 96 channel single pickup + pipette_96_channel.configure_nozzle_layout(style=protocol_api.SINGLE, start="H12") + pipette_96_channel.pick_up_tip(tip_rack_2) + pipette_96_channel.aspirate(5, source_reservoir["A1"]) + pipette_96_channel.touch_tip() + pipette_96_channel.dispense(5, source_reservoir["A1"]) + pipette_96_channel.aspirate(500, source_reservoir["A1"]) + pipette_96_channel.dispense(500, source_reservoir["A1"]) + pipette_96_channel.drop_tip(waste_chute) + comment_tip_rack_status(ctx, tip_rack_2) + + # put the tip rack in the trash + # since it cannot have a row pickup + ctx.move_labware(tip_rack_2, waste_chute, use_gripper=True) + ctx.move_labware(tip_rack_3, "C3", use_gripper=True) + + # 96 channel row pickup + pipette_96_channel.configure_nozzle_layout(style=protocol_api.ROW, start="H1") + pipette_96_channel.pick_up_tip(tip_rack_3) + pipette_96_channel.mix(3, 500, source_reservoir["A1"]) + pipette_96_channel.drop_tip(waste_chute) + comment_tip_rack_status(ctx, tip_rack_3) + + # 96 channel full rack pickup + pipette_96_channel.configure_nozzle_layout(style=protocol_api.ALL, start="A1") + pipette_96_channel.pick_up_tip(tip_rack_1["A1"]) + comment_tip_rack_status(ctx, tip_rack_1) + pipette_96_channel.aspirate(5, source_reservoir["A1"]) + pipette_96_channel.touch_tip() + pipette_96_channel.air_gap(height=30) + pipette_96_channel.blow_out(waste_chute) + pipette_96_channel.prepare_to_aspirate() + pipette_96_channel.aspirate(5, source_reservoir["A1"]) + pipette_96_channel.touch_tip() + pipette_96_channel.air_gap(height=30) + pipette_96_channel.blow_out(waste_chute) + pipette_96_channel.prepare_to_aspirate() + pipette_96_channel.aspirate(10, source_reservoir["A1"]) + pipette_96_channel.touch_tip() + pipette_96_channel.dispense(10, dest_pcr_plate["A1"]) + pipette_96_channel.mix(repetitions=5, volume=15) + pipette_96_channel.return_tip() + comment_tip_rack_status(ctx, tip_rack_1) + pipette_96_channel.pick_up_tip(tip_rack_1["A1"]) + pipette_96_channel.transfer( + volume=10, + source=source_reservoir["A1"], + dest=dest_pcr_plate["A1"], + new_tip="never", + touch_tip=True, + blow_out=True, + blowout_location="trash", + mix_before=(3, 5), + mix_after=(1, 5), + ) + comment_tip_rack_status(ctx, tip_rack_1) + pipette_96_channel.return_tip(home_after=False) + comment_tip_rack_status(ctx, tip_rack_1) + ctx.comment("I think the above should not be empty?") + # Thermocycler lid moves + ctx.move_labware(dest_pcr_plate, thermocycler, use_gripper=True) + ctx.move_labware(lids[0], dest_pcr_plate, use_gripper=True) # we reversed this list earlier + thermocycler.close_lid() + thermocycler.set_block_temperature(38, hold_time_seconds=5.0) + thermocycler.set_lid_temperature(38) + thermocycler.open_lid() + ctx.move_labware(lids[0], waste_chute, use_gripper=True) + thermocycler.deactivate() + + heater_shaker.open_labware_latch() + ctx.move_labware(dest_pcr_plate, heater_shaker_adapter, use_gripper=True) + heater_shaker.close_labware_latch() + + heater_shaker.set_target_temperature(38) + heater_shaker.set_and_wait_for_shake_speed(777) + heater_shaker.wait_for_temperature() + + heater_shaker.deactivate_heater() + heater_shaker.deactivate_shaker() + heater_shaker.open_labware_latch() + + ctx.move_labware(dest_pcr_plate, temperature_module_adapter, use_gripper=True) + temperature_module.set_temperature(38) + temperature_module.deactivate() + + ctx.move_labware(dest_pcr_plate, absorbance_module, use_gripper=True) + absorbance_module.close_lid() + + result = absorbance_module.read(export_filename="smoke_APR_data.csv") + msg = f"single: {result}" + ctx.comment(msg=msg) + ctx.pause(msg=msg) + absorbance_module.open_lid() + ctx.move_labware(dest_pcr_plate, "C2", use_gripper=True) + + # ###################### + # # labware.set_offset # + # ###################### + + # # -------------------------- # + # # Added in API version: 2.18 # + # # -------------------------- # + + SET_OFFSET_AMOUNT = 10.0 + ctx.move_labware(labware=source_reservoir, new_location=protocol_api.OFF_DECK, use_gripper=False) + pipette_96_channel.pick_up_tip(tip_rack_1["A1"]) + pipette_96_channel.move_to(dest_pcr_plate.wells_by_name()["A1"].top()) + + ctx.pause("Is the pipette tip in the middle of the PCR Plate, well A1, in slot C2? It should be at the LPC calibrated height.") + + dest_pcr_plate.set_offset( + x=0.0, + y=0.0, + z=SET_OFFSET_AMOUNT, + ) + + pipette_96_channel.move_to(dest_pcr_plate.wells_by_name()["A1"].top()) + ctx.pause( + "Is the pipette tip in the middle of the PCR Plate, well A1, in slot C2? It should be 10mm higher than the LPC calibrated height." + ) + + ctx.move_labware(labware=dest_pcr_plate, new_location="D2", use_gripper=False) + pipette_96_channel.move_to(dest_pcr_plate.wells_by_name()["A1"].top()) + + ctx.pause("Is the pipette tip in the middle of the PCR Plate, well A1, in slot D2? It should be at the LPC calibrated height.") + + dest_pcr_plate.set_offset( + x=0.0, + y=0.0, + z=SET_OFFSET_AMOUNT, + ) + + pipette_96_channel.move_to(dest_pcr_plate.wells_by_name()["A1"].top()) + ctx.pause( + "Is the pipette tip in the middle of the PCR Plate, well A1, in slot D2? It should be 10mm higher than the LPC calibrated height." + ) + + ctx.move_labware(labware=dest_pcr_plate, new_location="C2", use_gripper=False) + pipette_96_channel.move_to(dest_pcr_plate.wells_by_name()["A1"].top()) + + ctx.pause( + "Is the pipette tip in the middle of the PCR Plate, well A1, in slot C2? It should be 10mm higher than the LPC calibrated height." + ) + + ctx.move_labware(labware=source_reservoir, new_location="D2", use_gripper=False) + pipette_96_channel.move_to(source_reservoir.wells_by_name()["A1"].top()) + + ctx.pause("Is the pipette tip in the middle of the reservoir , well A1, in slot D2? It should be at the LPC calibrated height.") + + pipette_96_channel.return_tip() + ctx.move_labware(tip_rack_3, waste_chute, use_gripper=True) + + ctx.pause("!!!!!!!!!!YOU NEED TO REDO LPC!!!!!!!!!!") + + # Test the unique top() methods for TrashBin and WasteChute. + # Well objects should remain the same + + ######################## + # unique top() methods # + ######################## + + # ---------------------------- # + # Changed in API version: 2.18 # + # ---------------------------- # + + assert isinstance(waste_chute.top(), protocol_api.WasteChute) + assert isinstance(source_reservoir.wells_by_name()["A1"].top(), types.Location) diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[004ebb2b82][OT2_S_v2_11_P10S_P300M_MM_TC1_TM_Swift].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[004ebb2b82][OT2_S_v2_11_P10S_P300M_MM_TC1_TM_Swift].json index 3d8b4b072eb..8dc5a722eab 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[004ebb2b82][OT2_S_v2_11_P10S_P300M_MM_TC1_TM_Swift].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[004ebb2b82][OT2_S_v2_11_P10S_P300M_MM_TC1_TM_Swift].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -6875,9 +6876,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -6889,9 +6890,9 @@ "y": 0.0, "z": 0.0 }, - "tipDiameter": 0, + "tipDiameter": 0.0, "tipLength": 35.910000000000004, - "tipVolume": 20 + "tipVolume": 20.0 }, "startedAt": "TIMESTAMP", "status": "succeeded" @@ -6910,9 +6911,9 @@ "volume": 10.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -6944,9 +6945,9 @@ "volume": 10.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -6977,9 +6978,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -7010,9 +7011,9 @@ "volume": 10.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -7044,9 +7045,9 @@ "volume": 10.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -7078,9 +7079,9 @@ "volume": 10.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -7112,9 +7113,9 @@ "volume": 10.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -7175,9 +7176,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -7207,9 +7208,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -7237,9 +7238,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -7251,9 +7252,9 @@ "y": 0.0, "z": 0.0 }, - "tipDiameter": 0, + "tipDiameter": 0.0, "tipLength": 35.910000000000004, - "tipVolume": 20 + "tipVolume": 20.0 }, "startedAt": "TIMESTAMP", "status": "succeeded" @@ -7272,9 +7273,9 @@ "volume": 10.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -7306,9 +7307,9 @@ "volume": 10.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -7339,9 +7340,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -7372,9 +7373,9 @@ "volume": 10.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -7406,9 +7407,9 @@ "volume": 10.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -7440,9 +7441,9 @@ "volume": 10.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -7474,9 +7475,9 @@ "volume": 10.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -7537,9 +7538,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -7569,9 +7570,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -7599,9 +7600,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -7613,9 +7614,9 @@ "y": 0.0, "z": 0.0 }, - "tipDiameter": 0, + "tipDiameter": 0.0, "tipLength": 35.910000000000004, - "tipVolume": 20 + "tipVolume": 20.0 }, "startedAt": "TIMESTAMP", "status": "succeeded" @@ -7634,9 +7635,9 @@ "volume": 10.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -7668,9 +7669,9 @@ "volume": 10.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -7701,9 +7702,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -7734,9 +7735,9 @@ "volume": 10.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -7768,9 +7769,9 @@ "volume": 10.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -7802,9 +7803,9 @@ "volume": 10.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -7836,9 +7837,9 @@ "volume": 10.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -7899,9 +7900,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -7931,9 +7932,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -7961,9 +7962,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -7975,9 +7976,9 @@ "y": 0.0, "z": 0.0 }, - "tipDiameter": 0, + "tipDiameter": 0.0, "tipLength": 35.910000000000004, - "tipVolume": 20 + "tipVolume": 20.0 }, "startedAt": "TIMESTAMP", "status": "succeeded" @@ -7996,9 +7997,9 @@ "volume": 10.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -8030,9 +8031,9 @@ "volume": 10.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -8063,9 +8064,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -8096,9 +8097,9 @@ "volume": 10.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -8130,9 +8131,9 @@ "volume": 10.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -8164,9 +8165,9 @@ "volume": 10.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -8198,9 +8199,9 @@ "volume": 10.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -8261,9 +8262,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -8293,9 +8294,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -8323,9 +8324,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -8337,9 +8338,9 @@ "y": 0.0, "z": 0.0 }, - "tipDiameter": 0, + "tipDiameter": 0.0, "tipLength": 35.910000000000004, - "tipVolume": 20 + "tipVolume": 20.0 }, "startedAt": "TIMESTAMP", "status": "succeeded" @@ -8358,9 +8359,9 @@ "volume": 10.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -8392,9 +8393,9 @@ "volume": 10.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -8425,9 +8426,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -8458,9 +8459,9 @@ "volume": 10.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -8492,9 +8493,9 @@ "volume": 10.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -8526,9 +8527,9 @@ "volume": 10.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -8560,9 +8561,9 @@ "volume": 10.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -8623,9 +8624,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -8655,9 +8656,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -8685,9 +8686,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -8699,9 +8700,9 @@ "y": 0.0, "z": 0.0 }, - "tipDiameter": 0, + "tipDiameter": 0.0, "tipLength": 35.910000000000004, - "tipVolume": 20 + "tipVolume": 20.0 }, "startedAt": "TIMESTAMP", "status": "succeeded" @@ -8720,9 +8721,9 @@ "volume": 10.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -8754,9 +8755,9 @@ "volume": 10.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -8787,9 +8788,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -8820,9 +8821,9 @@ "volume": 10.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -8854,9 +8855,9 @@ "volume": 10.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -8888,9 +8889,9 @@ "volume": 10.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -8922,9 +8923,9 @@ "volume": 10.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -8985,9 +8986,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -9017,9 +9018,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -9047,9 +9048,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -9061,9 +9062,9 @@ "y": 0.0, "z": 0.0 }, - "tipDiameter": 0, + "tipDiameter": 0.0, "tipLength": 35.910000000000004, - "tipVolume": 20 + "tipVolume": 20.0 }, "startedAt": "TIMESTAMP", "status": "succeeded" @@ -9082,9 +9083,9 @@ "volume": 10.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -9116,9 +9117,9 @@ "volume": 10.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -9149,9 +9150,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -9182,9 +9183,9 @@ "volume": 10.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -9216,9 +9217,9 @@ "volume": 10.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -9250,9 +9251,9 @@ "volume": 10.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -9284,9 +9285,9 @@ "volume": 10.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -9347,9 +9348,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -9379,9 +9380,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -9409,9 +9410,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -9423,9 +9424,9 @@ "y": 0.0, "z": 0.0 }, - "tipDiameter": 0, + "tipDiameter": 0.0, "tipLength": 35.910000000000004, - "tipVolume": 20 + "tipVolume": 20.0 }, "startedAt": "TIMESTAMP", "status": "succeeded" @@ -9444,9 +9445,9 @@ "volume": 10.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -9478,9 +9479,9 @@ "volume": 10.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -9511,9 +9512,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -9544,9 +9545,9 @@ "volume": 10.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -9578,9 +9579,9 @@ "volume": 10.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -9612,9 +9613,9 @@ "volume": 10.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -9646,9 +9647,9 @@ "volume": 10.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -9709,9 +9710,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -9741,9 +9742,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -9891,9 +9892,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -9905,9 +9906,9 @@ "y": 0.0, "z": 0.0 }, - "tipDiameter": 0, + "tipDiameter": 0.0, "tipLength": 51.099999999999994, - "tipVolume": 300 + "tipVolume": 300.0 }, "startedAt": "TIMESTAMP", "status": "succeeded" @@ -9926,9 +9927,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -9960,9 +9961,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -9993,9 +9994,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -10025,9 +10026,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -10055,9 +10056,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -10069,9 +10070,9 @@ "y": 0.0, "z": 0.0 }, - "tipDiameter": 0, + "tipDiameter": 0.0, "tipLength": 51.099999999999994, - "tipVolume": 300 + "tipVolume": 300.0 }, "startedAt": "TIMESTAMP", "status": "succeeded" @@ -10090,9 +10091,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -10124,9 +10125,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -10158,9 +10159,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -10192,9 +10193,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -10226,9 +10227,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -10275,9 +10276,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -10308,9 +10309,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -10341,9 +10342,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -10375,9 +10376,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -10409,9 +10410,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -10443,9 +10444,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -10476,9 +10477,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -10508,9 +10509,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -10553,9 +10554,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -10567,9 +10568,9 @@ "y": 0.0, "z": 0.0 }, - "tipDiameter": 0, + "tipDiameter": 0.0, "tipLength": 51.099999999999994, - "tipVolume": 300 + "tipVolume": 300.0 }, "startedAt": "TIMESTAMP", "status": "succeeded" @@ -10588,9 +10589,9 @@ "volume": 18.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -10622,9 +10623,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -10671,9 +10672,9 @@ "volume": 19.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -10705,9 +10706,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -10738,9 +10739,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -10768,9 +10769,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -10782,9 +10783,9 @@ "y": 0.0, "z": 0.0 }, - "tipDiameter": 0, + "tipDiameter": 0.0, "tipLength": 51.099999999999994, - "tipVolume": 300 + "tipVolume": 300.0 }, "startedAt": "TIMESTAMP", "status": "succeeded" @@ -10803,9 +10804,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -10836,9 +10837,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -10911,9 +10912,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -10925,9 +10926,9 @@ "y": 0.0, "z": 0.0 }, - "tipDiameter": 0, + "tipDiameter": 0.0, "tipLength": 51.099999999999994, - "tipVolume": 300 + "tipVolume": 300.0 }, "startedAt": "TIMESTAMP", "status": "succeeded" @@ -10946,9 +10947,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -10980,9 +10981,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -11013,9 +11014,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -11045,9 +11046,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -11090,9 +11091,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -11104,9 +11105,9 @@ "y": 0.0, "z": 0.0 }, - "tipDiameter": 0, + "tipDiameter": 0.0, "tipLength": 35.910000000000004, - "tipVolume": 20 + "tipVolume": 20.0 }, "startedAt": "TIMESTAMP", "status": "succeeded" @@ -11125,9 +11126,9 @@ "volume": 5.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -11159,9 +11160,9 @@ "volume": 5.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -11192,9 +11193,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -11224,9 +11225,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -11254,9 +11255,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -11268,9 +11269,9 @@ "y": 0.0, "z": 0.0 }, - "tipDiameter": 0, + "tipDiameter": 0.0, "tipLength": 35.910000000000004, - "tipVolume": 20 + "tipVolume": 20.0 }, "startedAt": "TIMESTAMP", "status": "succeeded" @@ -11289,9 +11290,9 @@ "volume": 5.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -11323,9 +11324,9 @@ "volume": 5.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -11356,9 +11357,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -11388,9 +11389,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -11418,9 +11419,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -11432,9 +11433,9 @@ "y": 0.0, "z": 0.0 }, - "tipDiameter": 0, + "tipDiameter": 0.0, "tipLength": 35.910000000000004, - "tipVolume": 20 + "tipVolume": 20.0 }, "startedAt": "TIMESTAMP", "status": "succeeded" @@ -11453,9 +11454,9 @@ "volume": 5.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -11487,9 +11488,9 @@ "volume": 5.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -11520,9 +11521,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -11552,9 +11553,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -11582,9 +11583,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -11596,9 +11597,9 @@ "y": 0.0, "z": 0.0 }, - "tipDiameter": 0, + "tipDiameter": 0.0, "tipLength": 35.910000000000004, - "tipVolume": 20 + "tipVolume": 20.0 }, "startedAt": "TIMESTAMP", "status": "succeeded" @@ -11617,9 +11618,9 @@ "volume": 5.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -11651,9 +11652,9 @@ "volume": 5.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -11684,9 +11685,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -11716,9 +11717,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -11746,9 +11747,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -11760,9 +11761,9 @@ "y": 0.0, "z": 0.0 }, - "tipDiameter": 0, + "tipDiameter": 0.0, "tipLength": 35.910000000000004, - "tipVolume": 20 + "tipVolume": 20.0 }, "startedAt": "TIMESTAMP", "status": "succeeded" @@ -11781,9 +11782,9 @@ "volume": 5.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -11815,9 +11816,9 @@ "volume": 5.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -11848,9 +11849,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -11880,9 +11881,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -11910,9 +11911,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -11924,9 +11925,9 @@ "y": 0.0, "z": 0.0 }, - "tipDiameter": 0, + "tipDiameter": 0.0, "tipLength": 35.910000000000004, - "tipVolume": 20 + "tipVolume": 20.0 }, "startedAt": "TIMESTAMP", "status": "succeeded" @@ -11945,9 +11946,9 @@ "volume": 5.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -11979,9 +11980,9 @@ "volume": 5.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -12012,9 +12013,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -12044,9 +12045,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -12074,9 +12075,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -12088,9 +12089,9 @@ "y": 0.0, "z": 0.0 }, - "tipDiameter": 0, + "tipDiameter": 0.0, "tipLength": 35.910000000000004, - "tipVolume": 20 + "tipVolume": 20.0 }, "startedAt": "TIMESTAMP", "status": "succeeded" @@ -12109,9 +12110,9 @@ "volume": 5.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -12143,9 +12144,9 @@ "volume": 5.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -12176,9 +12177,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -12208,9 +12209,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -12238,9 +12239,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -12252,9 +12253,9 @@ "y": 0.0, "z": 0.0 }, - "tipDiameter": 0, + "tipDiameter": 0.0, "tipLength": 35.910000000000004, - "tipVolume": 20 + "tipVolume": 20.0 }, "startedAt": "TIMESTAMP", "status": "succeeded" @@ -12273,9 +12274,9 @@ "volume": 5.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -12307,9 +12308,9 @@ "volume": 5.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -12340,9 +12341,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -12372,9 +12373,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -12402,9 +12403,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -12416,9 +12417,9 @@ "y": 0.0, "z": 0.0 }, - "tipDiameter": 0, + "tipDiameter": 0.0, "tipLength": 35.910000000000004, - "tipVolume": 20 + "tipVolume": 20.0 }, "startedAt": "TIMESTAMP", "status": "succeeded" @@ -12437,9 +12438,9 @@ "volume": 10.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -12471,9 +12472,9 @@ "volume": 10.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -12505,9 +12506,9 @@ "volume": 10.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -12539,9 +12540,9 @@ "volume": 10.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -12572,9 +12573,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -12602,9 +12603,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -12616,9 +12617,9 @@ "y": 0.0, "z": 0.0 }, - "tipDiameter": 0, + "tipDiameter": 0.0, "tipLength": 35.910000000000004, - "tipVolume": 20 + "tipVolume": 20.0 }, "startedAt": "TIMESTAMP", "status": "succeeded" @@ -12637,9 +12638,9 @@ "volume": 8.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -12671,9 +12672,9 @@ "volume": 8.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -12704,9 +12705,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -12736,9 +12737,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -12766,9 +12767,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -12780,9 +12781,9 @@ "y": 0.0, "z": 0.0 }, - "tipDiameter": 0, + "tipDiameter": 0.0, "tipLength": 35.910000000000004, - "tipVolume": 20 + "tipVolume": 20.0 }, "startedAt": "TIMESTAMP", "status": "succeeded" @@ -12801,9 +12802,9 @@ "volume": 7.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -12835,9 +12836,9 @@ "volume": 7.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -12869,9 +12870,9 @@ "volume": 8.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -12903,9 +12904,9 @@ "volume": 8.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -12937,9 +12938,9 @@ "volume": 8.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -12971,9 +12972,9 @@ "volume": 8.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -13004,9 +13005,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -13036,9 +13037,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -13066,9 +13067,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -13080,9 +13081,9 @@ "y": 0.0, "z": 0.0 }, - "tipDiameter": 0, + "tipDiameter": 0.0, "tipLength": 35.910000000000004, - "tipVolume": 20 + "tipVolume": 20.0 }, "startedAt": "TIMESTAMP", "status": "succeeded" @@ -13101,9 +13102,9 @@ "volume": 8.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -13135,9 +13136,9 @@ "volume": 8.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -13168,9 +13169,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -13200,9 +13201,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -13230,9 +13231,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -13244,9 +13245,9 @@ "y": 0.0, "z": 0.0 }, - "tipDiameter": 0, + "tipDiameter": 0.0, "tipLength": 35.910000000000004, - "tipVolume": 20 + "tipVolume": 20.0 }, "startedAt": "TIMESTAMP", "status": "succeeded" @@ -13265,9 +13266,9 @@ "volume": 7.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -13299,9 +13300,9 @@ "volume": 7.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -13333,9 +13334,9 @@ "volume": 8.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -13367,9 +13368,9 @@ "volume": 8.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -13401,9 +13402,9 @@ "volume": 8.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -13435,9 +13436,9 @@ "volume": 8.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -13468,9 +13469,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -13500,9 +13501,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -13530,9 +13531,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -13544,9 +13545,9 @@ "y": 0.0, "z": 0.0 }, - "tipDiameter": 0, + "tipDiameter": 0.0, "tipLength": 35.910000000000004, - "tipVolume": 20 + "tipVolume": 20.0 }, "startedAt": "TIMESTAMP", "status": "succeeded" @@ -13565,9 +13566,9 @@ "volume": 8.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -13599,9 +13600,9 @@ "volume": 8.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -13632,9 +13633,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -13664,9 +13665,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -13694,9 +13695,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -13708,9 +13709,9 @@ "y": 0.0, "z": 0.0 }, - "tipDiameter": 0, + "tipDiameter": 0.0, "tipLength": 35.910000000000004, - "tipVolume": 20 + "tipVolume": 20.0 }, "startedAt": "TIMESTAMP", "status": "succeeded" @@ -13729,9 +13730,9 @@ "volume": 7.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -13763,9 +13764,9 @@ "volume": 7.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -13797,9 +13798,9 @@ "volume": 8.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -13831,9 +13832,9 @@ "volume": 8.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -13865,9 +13866,9 @@ "volume": 8.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -13899,9 +13900,9 @@ "volume": 8.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -13932,9 +13933,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -13964,9 +13965,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -13994,9 +13995,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -14008,9 +14009,9 @@ "y": 0.0, "z": 0.0 }, - "tipDiameter": 0, + "tipDiameter": 0.0, "tipLength": 35.910000000000004, - "tipVolume": 20 + "tipVolume": 20.0 }, "startedAt": "TIMESTAMP", "status": "succeeded" @@ -14029,9 +14030,9 @@ "volume": 8.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -14063,9 +14064,9 @@ "volume": 8.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -14096,9 +14097,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -14128,9 +14129,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -14158,9 +14159,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -14172,9 +14173,9 @@ "y": 0.0, "z": 0.0 }, - "tipDiameter": 0, + "tipDiameter": 0.0, "tipLength": 35.910000000000004, - "tipVolume": 20 + "tipVolume": 20.0 }, "startedAt": "TIMESTAMP", "status": "succeeded" @@ -14193,9 +14194,9 @@ "volume": 7.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -14227,9 +14228,9 @@ "volume": 7.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -14261,9 +14262,9 @@ "volume": 8.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -14295,9 +14296,9 @@ "volume": 8.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -14329,9 +14330,9 @@ "volume": 8.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -14363,9 +14364,9 @@ "volume": 8.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -14396,9 +14397,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -14428,9 +14429,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -14458,9 +14459,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -14472,9 +14473,9 @@ "y": 0.0, "z": 0.0 }, - "tipDiameter": 0, + "tipDiameter": 0.0, "tipLength": 35.910000000000004, - "tipVolume": 20 + "tipVolume": 20.0 }, "startedAt": "TIMESTAMP", "status": "succeeded" @@ -14493,9 +14494,9 @@ "volume": 8.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -14527,9 +14528,9 @@ "volume": 8.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -14560,9 +14561,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -14592,9 +14593,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -14622,9 +14623,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -14636,9 +14637,9 @@ "y": 0.0, "z": 0.0 }, - "tipDiameter": 0, + "tipDiameter": 0.0, "tipLength": 35.910000000000004, - "tipVolume": 20 + "tipVolume": 20.0 }, "startedAt": "TIMESTAMP", "status": "succeeded" @@ -14657,9 +14658,9 @@ "volume": 7.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -14691,9 +14692,9 @@ "volume": 7.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -14725,9 +14726,9 @@ "volume": 8.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -14759,9 +14760,9 @@ "volume": 8.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -14793,9 +14794,9 @@ "volume": 8.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -14827,9 +14828,9 @@ "volume": 8.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -14860,9 +14861,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -14892,9 +14893,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -14922,9 +14923,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -14936,9 +14937,9 @@ "y": 0.0, "z": 0.0 }, - "tipDiameter": 0, + "tipDiameter": 0.0, "tipLength": 35.910000000000004, - "tipVolume": 20 + "tipVolume": 20.0 }, "startedAt": "TIMESTAMP", "status": "succeeded" @@ -14957,9 +14958,9 @@ "volume": 8.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -14991,9 +14992,9 @@ "volume": 8.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -15024,9 +15025,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -15056,9 +15057,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -15086,9 +15087,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -15100,9 +15101,9 @@ "y": 0.0, "z": 0.0 }, - "tipDiameter": 0, + "tipDiameter": 0.0, "tipLength": 35.910000000000004, - "tipVolume": 20 + "tipVolume": 20.0 }, "startedAt": "TIMESTAMP", "status": "succeeded" @@ -15121,9 +15122,9 @@ "volume": 7.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -15155,9 +15156,9 @@ "volume": 7.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -15189,9 +15190,9 @@ "volume": 8.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -15223,9 +15224,9 @@ "volume": 8.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -15257,9 +15258,9 @@ "volume": 8.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -15291,9 +15292,9 @@ "volume": 8.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -15324,9 +15325,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -15356,9 +15357,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -15386,9 +15387,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -15400,9 +15401,9 @@ "y": 0.0, "z": 0.0 }, - "tipDiameter": 0, + "tipDiameter": 0.0, "tipLength": 35.910000000000004, - "tipVolume": 20 + "tipVolume": 20.0 }, "startedAt": "TIMESTAMP", "status": "succeeded" @@ -15421,9 +15422,9 @@ "volume": 8.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -15455,9 +15456,9 @@ "volume": 8.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -15488,9 +15489,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -15520,9 +15521,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -15550,9 +15551,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -15564,9 +15565,9 @@ "y": 0.0, "z": 0.0 }, - "tipDiameter": 0, + "tipDiameter": 0.0, "tipLength": 35.910000000000004, - "tipVolume": 20 + "tipVolume": 20.0 }, "startedAt": "TIMESTAMP", "status": "succeeded" @@ -15585,9 +15586,9 @@ "volume": 7.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -15619,9 +15620,9 @@ "volume": 7.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -15653,9 +15654,9 @@ "volume": 8.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -15687,9 +15688,9 @@ "volume": 8.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -15721,9 +15722,9 @@ "volume": 8.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -15755,9 +15756,9 @@ "volume": 8.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -15788,9 +15789,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -15820,9 +15821,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -15850,9 +15851,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -15864,9 +15865,9 @@ "y": 0.0, "z": 0.0 }, - "tipDiameter": 0, + "tipDiameter": 0.0, "tipLength": 35.910000000000004, - "tipVolume": 20 + "tipVolume": 20.0 }, "startedAt": "TIMESTAMP", "status": "succeeded" @@ -15885,9 +15886,9 @@ "volume": 8.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -15919,9 +15920,9 @@ "volume": 8.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -15952,9 +15953,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -15984,9 +15985,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -16014,9 +16015,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -16028,9 +16029,9 @@ "y": 0.0, "z": 0.0 }, - "tipDiameter": 0, + "tipDiameter": 0.0, "tipLength": 35.910000000000004, - "tipVolume": 20 + "tipVolume": 20.0 }, "startedAt": "TIMESTAMP", "status": "succeeded" @@ -16049,9 +16050,9 @@ "volume": 7.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -16083,9 +16084,9 @@ "volume": 7.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -16117,9 +16118,9 @@ "volume": 8.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -16151,9 +16152,9 @@ "volume": 8.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -16185,9 +16186,9 @@ "volume": 8.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -16219,9 +16220,9 @@ "volume": 8.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -16252,9 +16253,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -16284,9 +16285,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -16404,9 +16405,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -16418,9 +16419,9 @@ "y": 0.0, "z": 0.0 }, - "tipDiameter": 0, + "tipDiameter": 0.0, "tipLength": 51.099999999999994, - "tipVolume": 300 + "tipVolume": 300.0 }, "startedAt": "TIMESTAMP", "status": "succeeded" @@ -16439,9 +16440,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -16473,9 +16474,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -16506,9 +16507,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -16538,9 +16539,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -16668,6 +16669,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "apiLevel": "2.11" diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[00574c503f][pl_BacteriaInoculation_Flex_6plates].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[00574c503f][pl_BacteriaInoculation_Flex_6plates].json index ddb334a58e0..0cf14e731b6 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[00574c503f][pl_BacteriaInoculation_Flex_6plates].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[00574c503f][pl_BacteriaInoculation_Flex_6plates].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -981,6 +982,11 @@ }, "schemaVersion": 2, "stackingOffsetWithLabware": { + "evotips_flex_96_tiprack_adapter": { + "x": 0, + "y": 0, + "z": 102 + }, "opentrons_96_deep_well_adapter": { "x": 0, "y": 0, @@ -41584,6 +41590,7 @@ "location": "offDeck" } ], + "liquidClasses": [], "liquids": [ { "description": "Bacterial culture medium (e.g., LB broth)", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[011481812b][OT2_S_v2_7_P20S_None_Walkthrough].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[011481812b][OT2_S_v2_7_P20S_None_Walkthrough].json index 38872b09ff8..3a1a9bca236 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[011481812b][OT2_S_v2_7_P20S_None_Walkthrough].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[011481812b][OT2_S_v2_7_P20S_None_Walkthrough].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -2347,9 +2348,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -2361,9 +2362,9 @@ "y": 0.0, "z": 0.0 }, - "tipDiameter": 0, + "tipDiameter": 0.0, "tipLength": 30.950000000000003, - "tipVolume": 20 + "tipVolume": 20.0 }, "startedAt": "TIMESTAMP", "status": "succeeded" @@ -2397,9 +2398,9 @@ "volume": 13.333333333333334, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -2431,9 +2432,9 @@ "volume": 13.333333333333334, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -2465,9 +2466,9 @@ "volume": 13.333333333333334, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -2499,9 +2500,9 @@ "volume": 13.333333333333334, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -2533,9 +2534,9 @@ "volume": 13.333333333333334, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -2567,9 +2568,9 @@ "volume": 13.333333333333334, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -2601,9 +2602,9 @@ "volume": 13.333333333333334, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -2635,9 +2636,9 @@ "volume": 13.333333333333334, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -2669,9 +2670,9 @@ "volume": 13.333333333333334, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -2703,9 +2704,9 @@ "volume": 13.333333333333334, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -2737,9 +2738,9 @@ "volume": 13.333333333333334, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -2771,9 +2772,9 @@ "volume": 13.333333333333334, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -2805,9 +2806,9 @@ "volume": 13.333333333333334, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -2839,9 +2840,9 @@ "volume": 13.333333333333334, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -2873,9 +2874,9 @@ "volume": 13.333333333333334, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -2907,9 +2908,9 @@ "volume": 13.333333333333334, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -2941,9 +2942,9 @@ "volume": 13.333333333333334, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -2975,9 +2976,9 @@ "volume": 13.333333333333334, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -3009,9 +3010,9 @@ "volume": 13.333333333333334, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -3058,9 +3059,9 @@ "volume": 13.333333333333334, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -3092,9 +3093,9 @@ "volume": 13.333333333333334, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -3141,9 +3142,9 @@ "volume": 13.333333333333334, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -3175,9 +3176,9 @@ "volume": 13.333333333333334, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -3209,9 +3210,9 @@ "volume": 13.333333333333334, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -3242,9 +3243,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -3275,9 +3276,9 @@ "volume": 13.333333333333334, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -3309,9 +3310,9 @@ "volume": 13.333333333333334, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -3342,9 +3343,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -3375,9 +3376,9 @@ "volume": 13.333333333333334, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -3409,9 +3410,9 @@ "volume": 13.333333333333334, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -3442,9 +3443,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -3474,9 +3475,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -3504,9 +3505,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -3518,9 +3519,9 @@ "y": 0.0, "z": 0.0 }, - "tipDiameter": 0, + "tipDiameter": 0.0, "tipLength": 30.950000000000003, - "tipVolume": 20 + "tipVolume": 20.0 }, "startedAt": "TIMESTAMP", "status": "succeeded" @@ -3554,9 +3555,9 @@ "volume": 4.444444444444445, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -3588,9 +3589,9 @@ "volume": 6.666666666666667, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -3637,9 +3638,9 @@ "volume": 13.333333333333334, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -3671,9 +3672,9 @@ "volume": 4.444444444444445, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -3705,9 +3706,9 @@ "volume": 6.666666666666667, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -3754,9 +3755,9 @@ "volume": 13.333333333333334, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -3788,9 +3789,9 @@ "volume": 4.444444444444445, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -3822,9 +3823,9 @@ "volume": 6.666666666666667, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -3871,9 +3872,9 @@ "volume": 13.333333333333334, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -3905,9 +3906,9 @@ "volume": 1.6666666666666667, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -3939,9 +3940,9 @@ "volume": 2.5, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -3973,9 +3974,9 @@ "volume": 1.6666666666666667, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -4007,9 +4008,9 @@ "volume": 2.5, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -4055,9 +4056,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -4087,9 +4088,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -4117,9 +4118,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -4131,9 +4132,9 @@ "y": 0.0, "z": 0.0 }, - "tipDiameter": 0, + "tipDiameter": 0.0, "tipLength": 30.950000000000003, - "tipVolume": 20 + "tipVolume": 20.0 }, "startedAt": "TIMESTAMP", "status": "succeeded" @@ -4152,9 +4153,9 @@ "volume": 1.6666666666666667, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -4186,9 +4187,9 @@ "volume": 1.6666666666666667, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -4220,9 +4221,9 @@ "volume": 1.6666666666666667, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -4254,9 +4255,9 @@ "volume": 1.6666666666666667, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -4288,9 +4289,9 @@ "volume": 1.6666666666666667, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -4322,9 +4323,9 @@ "volume": 1.6666666666666667, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -4356,9 +4357,9 @@ "volume": 1.6666666666666667, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -4390,9 +4391,9 @@ "volume": 1.6666666666666667, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -4424,9 +4425,9 @@ "volume": 13.333333333333332, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -4457,9 +4458,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -4487,9 +4488,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -4501,9 +4502,9 @@ "y": 0.0, "z": 0.0 }, - "tipDiameter": 0, + "tipDiameter": 0.0, "tipLength": 30.950000000000003, - "tipVolume": 20 + "tipVolume": 20.0 }, "startedAt": "TIMESTAMP", "status": "succeeded" @@ -4522,9 +4523,9 @@ "volume": 14.333333333333332, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -4556,9 +4557,9 @@ "volume": 1.6666666666666667, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -4590,9 +4591,9 @@ "volume": 1.6666666666666667, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -4624,9 +4625,9 @@ "volume": 1.6666666666666667, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -4658,9 +4659,9 @@ "volume": 1.6666666666666667, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -4692,9 +4693,9 @@ "volume": 1.6666666666666667, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -4726,9 +4727,9 @@ "volume": 1.6666666666666667, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -4760,9 +4761,9 @@ "volume": 1.6666666666666667, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -4794,9 +4795,9 @@ "volume": 1.6666666666666667, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -4827,9 +4828,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -4859,9 +4860,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -4919,6 +4920,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "apiLevel": "2.7", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[01255c3f3b][pl_Flex_Protein_Digestion_Protocol].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[01255c3f3b][pl_Flex_Protein_Digestion_Protocol].json index aac975221e8..67327c84b0e 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[01255c3f3b][pl_Flex_Protein_Digestion_Protocol].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[01255c3f3b][pl_Flex_Protein_Digestion_Protocol].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -11824,6 +11825,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "author": "", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0190369ce5][Flex_S_v2_16_P1000_96_GRIP_HS_MB_TC_TM_DeckConfiguration1NoFixtures].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0190369ce5][Flex_S_v2_16_P1000_96_GRIP_HS_MB_TC_TM_DeckConfiguration1NoFixtures].json index ff626992e43..9d5405e8f89 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0190369ce5][Flex_S_v2_16_P1000_96_GRIP_HS_MB_TC_TM_DeckConfiguration1NoFixtures].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0190369ce5][Flex_S_v2_16_P1000_96_GRIP_HS_MB_TC_TM_DeckConfiguration1NoFixtures].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -10788,9 +10789,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -10800,7 +10801,7 @@ "position": { "x": 342.38, "y": 181.38, - "z": 90.88 + "z": 42.12400000000001 } }, "startedAt": "TIMESTAMP", @@ -10920,9 +10921,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -10932,7 +10933,7 @@ "position": { "x": 178.38, "y": 74.38, - "z": 90.88 + "z": 42.12400000000001 } }, "startedAt": "TIMESTAMP", @@ -11120,9 +11121,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -11132,7 +11133,7 @@ "position": { "x": 342.38, "y": 181.38, - "z": 90.88 + "z": 42.12400000000001 } }, "startedAt": "TIMESTAMP", @@ -11252,9 +11253,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -11264,7 +11265,7 @@ "position": { "x": 178.38, "y": 74.38, - "z": 90.88 + "z": 42.12400000000001 } }, "startedAt": "TIMESTAMP", @@ -11452,6 +11453,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "High Quality H₂O", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0256665840][OT2_S_v2_16_P300M_P20S_aspirateDispenseMix0Volume].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0256665840][OT2_S_v2_16_P300M_P20S_aspirateDispenseMix0Volume].json index 8cd99860d7e..6d0b815689f 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0256665840][OT2_S_v2_16_P300M_P20S_aspirateDispenseMix0Volume].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0256665840][OT2_S_v2_16_P300M_P20S_aspirateDispenseMix0Volume].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -2917,6 +2918,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "H₂O", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[041ad55e7b][OT2_S_v2_15_P300M_P20S_HS_TC_TM_dispense_changes].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[041ad55e7b][OT2_S_v2_15_P300M_P20S_HS_TC_TM_dispense_changes].json index c62ceb23edd..eba6890b1dc 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[041ad55e7b][OT2_S_v2_15_P300M_P20S_HS_TC_TM_dispense_changes].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[041ad55e7b][OT2_S_v2_15_P300M_P20S_HS_TC_TM_dispense_changes].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -3113,6 +3114,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "author": "Opentrons Engineering ", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[057de2035d][OT2_S_v2_19_P300M_P20S_HS_TC_TM_SmokeTestV3].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[057de2035d][OT2_S_v2_19_P300M_P20S_HS_TC_TM_SmokeTestV3].json index a2aca7e252a..f88d031bd87 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[057de2035d][OT2_S_v2_19_P300M_P20S_HS_TC_TM_SmokeTestV3].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[057de2035d][OT2_S_v2_19_P300M_P20S_HS_TC_TM_SmokeTestV3].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -9569,6 +9570,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "author": "Opentrons Engineering ", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0903a95825][Flex_S_v2_19_QIASeq_FX_48x].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0903a95825][Flex_S_v2_19_QIASeq_FX_48x].json index bce38cbe476..fccf2aae96c 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0903a95825][Flex_S_v2_19_QIASeq_FX_48x].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0903a95825][Flex_S_v2_19_QIASeq_FX_48x].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -5143,6 +5144,11 @@ }, "schemaVersion": 2, "stackingOffsetWithLabware": { + "evotips_flex_96_tiprack_adapter": { + "x": 0, + "y": 0, + "z": 102 + }, "opentrons_96_deep_well_adapter": { "x": 0, "y": 0, @@ -20182,9 +20188,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -20582,9 +20588,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -20982,9 +20988,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -23989,9 +23995,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -24486,9 +24492,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -24983,9 +24989,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -26803,9 +26809,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -27300,9 +27306,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -27797,9 +27803,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -27955,9 +27961,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -28085,9 +28091,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -28215,9 +28221,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -28497,9 +28503,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -28693,9 +28699,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -28889,9 +28895,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -29264,9 +29270,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -29508,9 +29514,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -29752,9 +29758,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -35049,9 +35055,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -35546,9 +35552,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -36043,9 +36049,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -37849,9 +37855,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -38332,9 +38338,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -38815,9 +38821,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -41808,9 +41814,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -42291,9 +42297,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -42774,9 +42780,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -42932,9 +42938,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -43062,9 +43068,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -43192,9 +43198,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -44731,9 +44737,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -44961,9 +44967,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -45191,9 +45197,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -56009,9 +56015,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -56506,9 +56512,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -57003,9 +57009,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -58823,9 +58829,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -59320,9 +59326,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -59817,9 +59823,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -62860,9 +62866,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -63357,9 +63363,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -63854,9 +63860,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -64012,9 +64018,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -64142,9 +64148,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -64272,9 +64278,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -64588,9 +64594,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -64818,9 +64824,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -65048,9 +65054,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -65423,9 +65429,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -65667,9 +65673,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -65911,9 +65917,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -66156,6 +66162,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "author": "Opentrons ", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[09676b9f7e][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_south_west].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[09676b9f7e][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_south_west].json index 2ca289680ef..e81582f2998 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[09676b9f7e][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_south_west].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[09676b9f7e][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_south_west].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -1481,6 +1482,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "description": "oooo", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[09ba51132a][OT2_S_v2_14_NO_PIPETTES_TC_VerifyThermocyclerLoadedSlots].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[09ba51132a][OT2_S_v2_14_NO_PIPETTES_TC_VerifyThermocyclerLoadedSlots].json index 7a7269decb6..112049809ad 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[09ba51132a][OT2_S_v2_14_NO_PIPETTES_TC_VerifyThermocyclerLoadedSlots].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[09ba51132a][OT2_S_v2_14_NO_PIPETTES_TC_VerifyThermocyclerLoadedSlots].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -154,6 +155,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": {}, "modules": [ diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0a9ef592c8][Flex_S_v2_18_Illumina_DNA_Prep_48x].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0a9ef592c8][Flex_S_v2_18_Illumina_DNA_Prep_48x].json index 4891466d0b7..b49eaeb6609 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0a9ef592c8][Flex_S_v2_18_Illumina_DNA_Prep_48x].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0a9ef592c8][Flex_S_v2_18_Illumina_DNA_Prep_48x].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -5204,6 +5205,11 @@ }, "schemaVersion": 2, "stackingOffsetWithLabware": { + "evotips_flex_96_tiprack_adapter": { + "x": 0, + "y": 0, + "z": 102 + }, "opentrons_96_deep_well_adapter": { "x": 0, "y": 0, @@ -14066,9 +14072,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -14198,9 +14204,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -14330,9 +14336,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -14909,9 +14915,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -15341,9 +15347,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -15773,9 +15779,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -19116,9 +19122,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -19548,9 +19554,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -19980,9 +19986,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -22136,9 +22142,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -22568,9 +22574,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -23000,9 +23006,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -26343,9 +26349,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -26775,9 +26781,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -27207,9 +27213,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -27587,9 +27593,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -27939,9 +27945,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -28291,9 +28297,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -36139,9 +36145,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -36539,9 +36545,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -36939,9 +36945,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -39945,9 +39951,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -40345,9 +40351,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -40745,9 +40751,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -42564,9 +42570,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -42964,9 +42970,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -43364,9 +43370,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -43744,9 +43750,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -44096,9 +44102,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -44448,9 +44454,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -46021,9 +46027,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -46285,9 +46291,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -46549,9 +46555,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -46844,9 +46850,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -47008,9 +47014,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -47172,9 +47178,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -49707,6 +49713,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "CleanupBead Beads", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0affe60373][Flex_X_v2_18_NO_PIPETTES_Overrides_BadTypesInRTP_Override_wrong_type_in_maximum].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0affe60373][Flex_X_v2_18_NO_PIPETTES_Overrides_BadTypesInRTP_Override_wrong_type_in_maximum].json index 64072eb8834..bba25d3dccf 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0affe60373][Flex_X_v2_18_NO_PIPETTES_Overrides_BadTypesInRTP_Override_wrong_type_in_maximum].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0affe60373][Flex_X_v2_18_NO_PIPETTES_Overrides_BadTypesInRTP_Override_wrong_type_in_maximum].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [], "config": { "apiVersion": [ @@ -42,6 +43,7 @@ } ], "labware": [], + "liquidClasses": [], "liquids": [], "metadata": { "protocolName": "Description Too Long 2.18" diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0b42cfc151][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_north_row].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0b42cfc151][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_north_row].json index dfef8b35364..3951be19f24 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0b42cfc151][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_north_row].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0b42cfc151][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_north_row].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -1481,6 +1482,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "description": "oooo", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0c4ae179bb][OT2_S_v2_15_P300M_P20S_HS_TC_TM_SmokeTestV3].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0c4ae179bb][OT2_S_v2_15_P300M_P20S_HS_TC_TM_SmokeTestV3].json index 0096a483ffe..84e33cd17bd 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0c4ae179bb][OT2_S_v2_15_P300M_P20S_HS_TC_TM_SmokeTestV3].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0c4ae179bb][OT2_S_v2_15_P300M_P20S_HS_TC_TM_SmokeTestV3].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -11843,9 +11844,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -15852,9 +15853,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -16207,9 +16208,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -16561,9 +16562,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -16839,9 +16840,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -16938,9 +16939,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -17072,6 +17073,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "H₂O", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0cbde10c37][OT2_S_v2_18_P300M_P20S_HS_TC_TM_SmokeTestV3].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0cbde10c37][OT2_S_v2_18_P300M_P20S_HS_TC_TM_SmokeTestV3].json index e4924262e1a..e9c6717fdd0 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0cbde10c37][OT2_S_v2_18_P300M_P20S_HS_TC_TM_SmokeTestV3].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0cbde10c37][OT2_S_v2_18_P300M_P20S_HS_TC_TM_SmokeTestV3].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -9569,6 +9570,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "author": "Opentrons Engineering ", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0dd21c0ee5][pl_EM_seq_48Samples_AllSteps_Edits_150].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0dd21c0ee5][pl_EM_seq_48Samples_AllSteps_Edits_150].json index 7bff37154bf..c407d673c2b 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0dd21c0ee5][pl_EM_seq_48Samples_AllSteps_Edits_150].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0dd21c0ee5][pl_EM_seq_48Samples_AllSteps_Edits_150].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -8630,6 +8631,11 @@ }, "schemaVersion": 2, "stackingOffsetWithLabware": { + "evotips_flex_96_tiprack_adapter": { + "x": 0, + "y": 0, + "z": 102 + }, "opentrons_96_deep_well_adapter": { "x": 0, "y": 0, @@ -9908,6 +9914,11 @@ }, "schemaVersion": 2, "stackingOffsetWithLabware": { + "evotips_flex_96_tiprack_adapter": { + "x": 0, + "y": 0, + "z": 102 + }, "opentrons_96_deep_well_adapter": { "x": 0, "y": 0, @@ -49392,6 +49403,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "Beads", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[109b7ad1f0][Flex_S_v2_20_96_None_ROW_HappyPath].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[109b7ad1f0][Flex_S_v2_20_96_None_ROW_HappyPath].json index d0b11f42740..64a1766fb6a 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[109b7ad1f0][Flex_S_v2_20_96_None_ROW_HappyPath].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[109b7ad1f0][Flex_S_v2_20_96_None_ROW_HappyPath].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -6263,6 +6264,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "description": "96 channel pipette and a ROW partial tip configuration.", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[10c2386b92][Flex_S_v2_20_96_AllCorners].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[10c2386b92][Flex_S_v2_20_96_AllCorners].json index f7457a3c48d..581a3e3ea51 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[10c2386b92][Flex_S_v2_20_96_AllCorners].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[10c2386b92][Flex_S_v2_20_96_AllCorners].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -33697,6 +33698,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": {}, "modules": [], diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[11020a4e17][pl_Bradford_proteinassay].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[11020a4e17][pl_Bradford_proteinassay].json index 6fb9e302070..d63c91e8a57 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[11020a4e17][pl_Bradford_proteinassay].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[11020a4e17][pl_Bradford_proteinassay].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -19352,6 +19353,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "Coomassie Brilliant Blue G-250 solution ", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[134037b2aa][OT2_X_v6_P300M_P20S_HS_MM_TM_TC_AllMods].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[134037b2aa][OT2_X_v6_P300M_P20S_HS_MM_TM_TC_AllMods].json index f2c63721b33..a0e4e4b52b4 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[134037b2aa][OT2_X_v6_P300M_P20S_HS_MM_TM_TC_AllMods].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[134037b2aa][OT2_X_v6_P300M_P20S_HS_MM_TM_TC_AllMods].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -6111,9 +6112,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -6146,8 +6147,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 1.0 }, "origin": "bottom", @@ -6180,8 +6181,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -6213,9 +6214,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -6243,9 +6244,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -6278,8 +6279,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 1.0 }, "origin": "bottom", @@ -6312,8 +6313,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -6345,9 +6346,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -6375,9 +6376,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -6410,8 +6411,8 @@ "volume": 25.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 1.0 }, "origin": "bottom", @@ -6444,8 +6445,8 @@ "volume": 25.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -6477,9 +6478,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -6507,9 +6508,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -6542,8 +6543,8 @@ "volume": 25.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 1.0 }, "origin": "bottom", @@ -6576,8 +6577,8 @@ "volume": 25.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -6609,9 +6610,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -6639,9 +6640,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -6674,8 +6675,8 @@ "volume": 25.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 1.0 }, "origin": "bottom", @@ -6708,8 +6709,8 @@ "volume": 25.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -6741,9 +6742,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -6771,9 +6772,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -6806,8 +6807,8 @@ "volume": 25.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 1.0 }, "origin": "bottom", @@ -6840,8 +6841,8 @@ "volume": 25.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -6873,9 +6874,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -6903,9 +6904,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -6938,8 +6939,8 @@ "volume": 22.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 1.0 }, "origin": "bottom", @@ -6972,8 +6973,8 @@ "volume": 22.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -7005,9 +7006,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -7035,9 +7036,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -7070,8 +7071,8 @@ "volume": 22.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 1.0 }, "origin": "bottom", @@ -7104,8 +7105,8 @@ "volume": 22.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -7137,9 +7138,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -7167,9 +7168,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -7202,8 +7203,8 @@ "volume": 22.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 1.0 }, "origin": "bottom", @@ -7236,8 +7237,8 @@ "volume": 22.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -7269,9 +7270,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -7299,9 +7300,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -7334,8 +7335,8 @@ "volume": 22.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 1.0 }, "origin": "bottom", @@ -7368,8 +7369,8 @@ "volume": 22.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -7401,9 +7402,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -7431,9 +7432,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -7466,8 +7467,8 @@ "volume": 22.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 1.0 }, "origin": "bottom", @@ -7500,8 +7501,8 @@ "volume": 22.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -7533,9 +7534,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -7563,9 +7564,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -7598,8 +7599,8 @@ "volume": 22.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 1.0 }, "origin": "bottom", @@ -7632,8 +7633,8 @@ "volume": 22.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -7665,9 +7666,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -7997,6 +7998,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[13e5a9c68b][Flex_S_v2_20_P8X1000_P50_LLD].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[13e5a9c68b][Flex_S_v2_20_P8X1000_P50_LLD].json index c463feb0552..9519a2e4438 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[13e5a9c68b][Flex_S_v2_20_P8X1000_P50_LLD].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[13e5a9c68b][Flex_S_v2_20_P8X1000_P50_LLD].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -6203,6 +6204,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "water for ER testing", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[13ec753045][Flex_S_v2_18_Illumina_Stranded_total_RNA_Ribo_Zero].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[13ec753045][Flex_S_v2_18_Illumina_Stranded_total_RNA_Ribo_Zero].json index 0b2e524dee6..8701aae97bc 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[13ec753045][Flex_S_v2_18_Illumina_Stranded_total_RNA_Ribo_Zero].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[13ec753045][Flex_S_v2_18_Illumina_Stranded_total_RNA_Ribo_Zero].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -3282,6 +3283,11 @@ }, "schemaVersion": 2, "stackingOffsetWithLabware": { + "evotips_flex_96_tiprack_adapter": { + "x": 0, + "y": 0, + "z": 102 + }, "opentrons_96_deep_well_adapter": { "x": 0, "y": 0, @@ -4492,6 +4498,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "author": "Dandra Howell ", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[15a60fccf4][pl_microBioID_beads_touchtip].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[15a60fccf4][pl_microBioID_beads_touchtip].json index 6053323ac4b..0363b1ca1df 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[15a60fccf4][pl_microBioID_beads_touchtip].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[15a60fccf4][pl_microBioID_beads_touchtip].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -3575,6 +3576,11 @@ }, "schemaVersion": 2, "stackingOffsetWithLabware": { + "evotips_flex_96_tiprack_adapter": { + "x": 0, + "y": 0, + "z": 102 + }, "opentrons_96_deep_well_adapter": { "x": 0, "y": 0, @@ -34590,6 +34596,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "Beads", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[194e3c49bb][pl_Normalization_with_PCR].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[194e3c49bb][pl_Normalization_with_PCR].json index ababd25acfa..15d4ad5cd55 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[194e3c49bb][pl_Normalization_with_PCR].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[194e3c49bb][pl_Normalization_with_PCR].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -6020,9 +6021,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -6635,9 +6636,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -7477,9 +7478,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -8321,9 +8322,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -9165,9 +9166,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -9297,6 +9298,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "author": "Rami Farawi \", line N, in \n\n File \"pl_langone_ribo_pt1_ramp.py\", line N, in run\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/module_contexts.py\", line N, in load_labware\n labware_core = self._protocol_core.load_labware(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/core/engine/protocol.py\", line N, in load_labware\n cmd.LoadLabwareParams(\n\n File \"/usr/local/lib/python3.10/site-packages/pydantic/main.py\", line N, in __init__\n validated_self = self.__pydantic_validator__.validate_python(data, self_instance=self)\n" + }, + "errorType": "PythonException", + "id": "UUID", + "isDefined": false, + "wrappedErrors": [] + } + ] + } + ], + "files": [ + { + "name": "pl_langone_ribo_pt1_ramp.py", + "role": "main" + } + ], + "labware": [ + { + "definitionUri": "opentrons/opentrons_1_trash_3200ml_fixed/1", + "id": "UUID", + "loadName": "opentrons_1_trash_3200ml_fixed", + "location": { + "slotName": "A3" + } }, { - "commandType": "loadLabware", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", + "definitionUri": "opentrons/opentrons_96_wellplate_200ul_pcr_full_skirt/2", "id": "UUID", - "key": "c06d611a9cd16f109d926a07372b0d89", - "notes": [], - "params": { - "displayName": "8", - "loadName": "nest_96_wellplate_2ml_deep", - "location": { - "moduleId": "UUID" - }, - "namespace": "opentrons", - "version": 2 - }, - "result": { - "definition": { - "allowedRoles": [], - "brand": { - "brand": "NEST", - "brandId": [ - "503001", - "503501" - ], - "links": [ - "https://www.nest-biotech.com/deep-well-plates/59253726.html" - ] - }, - "cornerOffsetFromSlot": { - "x": 0, - "y": 0, - "z": 0 - }, - "dimensions": { - "xDimension": 127.6, - "yDimension": 85.3, - "zDimension": 41 - }, - "gripForce": 15.0, - "gripHeightFromLabwareBottom": 21.9, - "gripperOffsets": {}, - "groups": [ - { - "brand": { - "brand": "NEST", - "brandId": [] - }, - "metadata": { - "displayCategory": "wellPlate", - "displayName": "NEST 96 Deep Well Plate 2mL", - "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 Deep Well Plate 2mL", - "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_2ml_deep", - "magneticModuleEngageHeight": 6.8, - "quirks": [] - }, - "schemaVersion": 2, - "stackingOffsetWithLabware": { - "opentrons_96_deep_well_adapter": { - "x": 0, - "y": 0, - "z": 16.3 - }, - "opentrons_96_deep_well_temp_mod_adapter": { - "x": 0, - "y": 0, - "z": 16.1 - } - }, - "stackingOffsetWithModule": { - "magneticBlockV1": { - "x": 0, - "y": 0, - "z": 2.66 - } - }, - "version": 2, - "wells": { - "A1": { - "depth": 38, - "shape": "rectangular", - "totalLiquidVolume": 2000, - "x": 14.3, - "xDimension": 8.2, - "y": 74.15, - "yDimension": 8.2, - "z": 3 - }, - "A10": { - "depth": 38, - "shape": "rectangular", - "totalLiquidVolume": 2000, - "x": 95.3, - "xDimension": 8.2, - "y": 74.15, - "yDimension": 8.2, - "z": 3 - }, - "A11": { - "depth": 38, - "shape": "rectangular", - "totalLiquidVolume": 2000, - "x": 104.3, - "xDimension": 8.2, - "y": 74.15, - "yDimension": 8.2, - "z": 3 - }, - "A12": { - "depth": 38, - "shape": "rectangular", - "totalLiquidVolume": 2000, - "x": 113.3, - "xDimension": 8.2, - "y": 74.15, - "yDimension": 8.2, - "z": 3 - }, - "A2": { - "depth": 38, - "shape": "rectangular", - "totalLiquidVolume": 2000, - "x": 23.3, - "xDimension": 8.2, - "y": 74.15, - "yDimension": 8.2, - "z": 3 - }, - "A3": { - "depth": 38, - "shape": "rectangular", - "totalLiquidVolume": 2000, - "x": 32.3, - "xDimension": 8.2, - "y": 74.15, - "yDimension": 8.2, - "z": 3 - }, - "A4": { - "depth": 38, - "shape": "rectangular", - "totalLiquidVolume": 2000, - "x": 41.3, - "xDimension": 8.2, - "y": 74.15, - "yDimension": 8.2, - "z": 3 - }, - "A5": { - "depth": 38, - "shape": "rectangular", - "totalLiquidVolume": 2000, - "x": 50.3, - "xDimension": 8.2, - "y": 74.15, - "yDimension": 8.2, - "z": 3 - }, - "A6": { - "depth": 38, - "shape": "rectangular", - "totalLiquidVolume": 2000, - "x": 59.3, - "xDimension": 8.2, - "y": 74.15, - "yDimension": 8.2, - "z": 3 - }, - "A7": { - "depth": 38, - "shape": "rectangular", - "totalLiquidVolume": 2000, - "x": 68.3, - "xDimension": 8.2, - "y": 74.15, - "yDimension": 8.2, - "z": 3 - }, - "A8": { - "depth": 38, - "shape": "rectangular", - "totalLiquidVolume": 2000, - "x": 77.3, - "xDimension": 8.2, - "y": 74.15, - "yDimension": 8.2, - "z": 3 - }, - "A9": { - "depth": 38, - "shape": "rectangular", - "totalLiquidVolume": 2000, - "x": 86.3, - "xDimension": 8.2, - "y": 74.15, - "yDimension": 8.2, - "z": 3 - }, - "B1": { - "depth": 38, - "shape": "rectangular", - "totalLiquidVolume": 2000, - "x": 14.3, - "xDimension": 8.2, - "y": 65.15, - "yDimension": 8.2, - "z": 3 - }, - "B10": { - "depth": 38, - "shape": "rectangular", - "totalLiquidVolume": 2000, - "x": 95.3, - "xDimension": 8.2, - "y": 65.15, - "yDimension": 8.2, - "z": 3 - }, - "B11": { - "depth": 38, - "shape": "rectangular", - "totalLiquidVolume": 2000, - "x": 104.3, - "xDimension": 8.2, - "y": 65.15, - "yDimension": 8.2, - "z": 3 - }, - "B12": { - "depth": 38, - "shape": "rectangular", - "totalLiquidVolume": 2000, - "x": 113.3, - "xDimension": 8.2, - "y": 65.15, - "yDimension": 8.2, - "z": 3 - }, - "B2": { - "depth": 38, - "shape": "rectangular", - "totalLiquidVolume": 2000, - "x": 23.3, - "xDimension": 8.2, - "y": 65.15, - "yDimension": 8.2, - "z": 3 - }, - "B3": { - "depth": 38, - "shape": "rectangular", - "totalLiquidVolume": 2000, - "x": 32.3, - "xDimension": 8.2, - "y": 65.15, - "yDimension": 8.2, - "z": 3 - }, - "B4": { - "depth": 38, - "shape": "rectangular", - "totalLiquidVolume": 2000, - "x": 41.3, - "xDimension": 8.2, - "y": 65.15, - "yDimension": 8.2, - "z": 3 - }, - "B5": { - "depth": 38, - "shape": "rectangular", - "totalLiquidVolume": 2000, - "x": 50.3, - "xDimension": 8.2, - "y": 65.15, - "yDimension": 8.2, - "z": 3 - }, - "B6": { - "depth": 38, - "shape": "rectangular", - "totalLiquidVolume": 2000, - "x": 59.3, - "xDimension": 8.2, - "y": 65.15, - "yDimension": 8.2, - "z": 3 - }, - "B7": { - "depth": 38, - "shape": "rectangular", - "totalLiquidVolume": 2000, - "x": 68.3, - "xDimension": 8.2, - "y": 65.15, - "yDimension": 8.2, - "z": 3 - }, - "B8": { - "depth": 38, - "shape": "rectangular", - "totalLiquidVolume": 2000, - "x": 77.3, - "xDimension": 8.2, - "y": 65.15, - "yDimension": 8.2, - "z": 3 - }, - "B9": { - "depth": 38, - "shape": "rectangular", - "totalLiquidVolume": 2000, - "x": 86.3, - "xDimension": 8.2, - "y": 65.15, - "yDimension": 8.2, - "z": 3 - }, - "C1": { - "depth": 38, - "shape": "rectangular", - "totalLiquidVolume": 2000, - "x": 14.3, - "xDimension": 8.2, - "y": 56.15, - "yDimension": 8.2, - "z": 3 - }, - "C10": { - "depth": 38, - "shape": "rectangular", - "totalLiquidVolume": 2000, - "x": 95.3, - "xDimension": 8.2, - "y": 56.15, - "yDimension": 8.2, - "z": 3 - }, - "C11": { - "depth": 38, - "shape": "rectangular", - "totalLiquidVolume": 2000, - "x": 104.3, - "xDimension": 8.2, - "y": 56.15, - "yDimension": 8.2, - "z": 3 - }, - "C12": { - "depth": 38, - "shape": "rectangular", - "totalLiquidVolume": 2000, - "x": 113.3, - "xDimension": 8.2, - "y": 56.15, - "yDimension": 8.2, - "z": 3 - }, - "C2": { - "depth": 38, - "shape": "rectangular", - "totalLiquidVolume": 2000, - "x": 23.3, - "xDimension": 8.2, - "y": 56.15, - "yDimension": 8.2, - "z": 3 - }, - "C3": { - "depth": 38, - "shape": "rectangular", - "totalLiquidVolume": 2000, - "x": 32.3, - "xDimension": 8.2, - "y": 56.15, - "yDimension": 8.2, - "z": 3 - }, - "C4": { - "depth": 38, - "shape": "rectangular", - "totalLiquidVolume": 2000, - "x": 41.3, - "xDimension": 8.2, - "y": 56.15, - "yDimension": 8.2, - "z": 3 - }, - "C5": { - "depth": 38, - "shape": "rectangular", - "totalLiquidVolume": 2000, - "x": 50.3, - "xDimension": 8.2, - "y": 56.15, - "yDimension": 8.2, - "z": 3 - }, - "C6": { - "depth": 38, - "shape": "rectangular", - "totalLiquidVolume": 2000, - "x": 59.3, - "xDimension": 8.2, - "y": 56.15, - "yDimension": 8.2, - "z": 3 - }, - "C7": { - "depth": 38, - "shape": "rectangular", - "totalLiquidVolume": 2000, - "x": 68.3, - "xDimension": 8.2, - "y": 56.15, - "yDimension": 8.2, - "z": 3 - }, - "C8": { - "depth": 38, - "shape": "rectangular", - "totalLiquidVolume": 2000, - "x": 77.3, - "xDimension": 8.2, - "y": 56.15, - "yDimension": 8.2, - "z": 3 - }, - "C9": { - "depth": 38, - "shape": "rectangular", - "totalLiquidVolume": 2000, - "x": 86.3, - "xDimension": 8.2, - "y": 56.15, - "yDimension": 8.2, - "z": 3 - }, - "D1": { - "depth": 38, - "shape": "rectangular", - "totalLiquidVolume": 2000, - "x": 14.3, - "xDimension": 8.2, - "y": 47.15, - "yDimension": 8.2, - "z": 3 - }, - "D10": { - "depth": 38, - "shape": "rectangular", - "totalLiquidVolume": 2000, - "x": 95.3, - "xDimension": 8.2, - "y": 47.15, - "yDimension": 8.2, - "z": 3 - }, - "D11": { - "depth": 38, - "shape": "rectangular", - "totalLiquidVolume": 2000, - "x": 104.3, - "xDimension": 8.2, - "y": 47.15, - "yDimension": 8.2, - "z": 3 - }, - "D12": { - "depth": 38, - "shape": "rectangular", - "totalLiquidVolume": 2000, - "x": 113.3, - "xDimension": 8.2, - "y": 47.15, - "yDimension": 8.2, - "z": 3 - }, - "D2": { - "depth": 38, - "shape": "rectangular", - "totalLiquidVolume": 2000, - "x": 23.3, - "xDimension": 8.2, - "y": 47.15, - "yDimension": 8.2, - "z": 3 - }, - "D3": { - "depth": 38, - "shape": "rectangular", - "totalLiquidVolume": 2000, - "x": 32.3, - "xDimension": 8.2, - "y": 47.15, - "yDimension": 8.2, - "z": 3 - }, - "D4": { - "depth": 38, - "shape": "rectangular", - "totalLiquidVolume": 2000, - "x": 41.3, - "xDimension": 8.2, - "y": 47.15, - "yDimension": 8.2, - "z": 3 - }, - "D5": { - "depth": 38, - "shape": "rectangular", - "totalLiquidVolume": 2000, - "x": 50.3, - "xDimension": 8.2, - "y": 47.15, - "yDimension": 8.2, - "z": 3 - }, - "D6": { - "depth": 38, - "shape": "rectangular", - "totalLiquidVolume": 2000, - "x": 59.3, - "xDimension": 8.2, - "y": 47.15, - "yDimension": 8.2, - "z": 3 - }, - "D7": { - "depth": 38, - "shape": "rectangular", - "totalLiquidVolume": 2000, - "x": 68.3, - "xDimension": 8.2, - "y": 47.15, - "yDimension": 8.2, - "z": 3 - }, - "D8": { - "depth": 38, - "shape": "rectangular", - "totalLiquidVolume": 2000, - "x": 77.3, - "xDimension": 8.2, - "y": 47.15, - "yDimension": 8.2, - "z": 3 - }, - "D9": { - "depth": 38, - "shape": "rectangular", - "totalLiquidVolume": 2000, - "x": 86.3, - "xDimension": 8.2, - "y": 47.15, - "yDimension": 8.2, - "z": 3 - }, - "E1": { - "depth": 38, - "shape": "rectangular", - "totalLiquidVolume": 2000, - "x": 14.3, - "xDimension": 8.2, - "y": 38.15, - "yDimension": 8.2, - "z": 3 - }, - "E10": { - "depth": 38, - "shape": "rectangular", - "totalLiquidVolume": 2000, - "x": 95.3, - "xDimension": 8.2, - "y": 38.15, - "yDimension": 8.2, - "z": 3 - }, - "E11": { - "depth": 38, - "shape": "rectangular", - "totalLiquidVolume": 2000, - "x": 104.3, - "xDimension": 8.2, - "y": 38.15, - "yDimension": 8.2, - "z": 3 - }, - "E12": { - "depth": 38, - "shape": "rectangular", - "totalLiquidVolume": 2000, - "x": 113.3, - "xDimension": 8.2, - "y": 38.15, - "yDimension": 8.2, - "z": 3 - }, - "E2": { - "depth": 38, - "shape": "rectangular", - "totalLiquidVolume": 2000, - "x": 23.3, - "xDimension": 8.2, - "y": 38.15, - "yDimension": 8.2, - "z": 3 - }, - "E3": { - "depth": 38, - "shape": "rectangular", - "totalLiquidVolume": 2000, - "x": 32.3, - "xDimension": 8.2, - "y": 38.15, - "yDimension": 8.2, - "z": 3 - }, - "E4": { - "depth": 38, - "shape": "rectangular", - "totalLiquidVolume": 2000, - "x": 41.3, - "xDimension": 8.2, - "y": 38.15, - "yDimension": 8.2, - "z": 3 - }, - "E5": { - "depth": 38, - "shape": "rectangular", - "totalLiquidVolume": 2000, - "x": 50.3, - "xDimension": 8.2, - "y": 38.15, - "yDimension": 8.2, - "z": 3 - }, - "E6": { - "depth": 38, - "shape": "rectangular", - "totalLiquidVolume": 2000, - "x": 59.3, - "xDimension": 8.2, - "y": 38.15, - "yDimension": 8.2, - "z": 3 - }, - "E7": { - "depth": 38, - "shape": "rectangular", - "totalLiquidVolume": 2000, - "x": 68.3, - "xDimension": 8.2, - "y": 38.15, - "yDimension": 8.2, - "z": 3 - }, - "E8": { - "depth": 38, - "shape": "rectangular", - "totalLiquidVolume": 2000, - "x": 77.3, - "xDimension": 8.2, - "y": 38.15, - "yDimension": 8.2, - "z": 3 - }, - "E9": { - "depth": 38, - "shape": "rectangular", - "totalLiquidVolume": 2000, - "x": 86.3, - "xDimension": 8.2, - "y": 38.15, - "yDimension": 8.2, - "z": 3 - }, - "F1": { - "depth": 38, - "shape": "rectangular", - "totalLiquidVolume": 2000, - "x": 14.3, - "xDimension": 8.2, - "y": 29.15, - "yDimension": 8.2, - "z": 3 - }, - "F10": { - "depth": 38, - "shape": "rectangular", - "totalLiquidVolume": 2000, - "x": 95.3, - "xDimension": 8.2, - "y": 29.15, - "yDimension": 8.2, - "z": 3 - }, - "F11": { - "depth": 38, - "shape": "rectangular", - "totalLiquidVolume": 2000, - "x": 104.3, - "xDimension": 8.2, - "y": 29.15, - "yDimension": 8.2, - "z": 3 - }, - "F12": { - "depth": 38, - "shape": "rectangular", - "totalLiquidVolume": 2000, - "x": 113.3, - "xDimension": 8.2, - "y": 29.15, - "yDimension": 8.2, - "z": 3 - }, - "F2": { - "depth": 38, - "shape": "rectangular", - "totalLiquidVolume": 2000, - "x": 23.3, - "xDimension": 8.2, - "y": 29.15, - "yDimension": 8.2, - "z": 3 - }, - "F3": { - "depth": 38, - "shape": "rectangular", - "totalLiquidVolume": 2000, - "x": 32.3, - "xDimension": 8.2, - "y": 29.15, - "yDimension": 8.2, - "z": 3 - }, - "F4": { - "depth": 38, - "shape": "rectangular", - "totalLiquidVolume": 2000, - "x": 41.3, - "xDimension": 8.2, - "y": 29.15, - "yDimension": 8.2, - "z": 3 - }, - "F5": { - "depth": 38, - "shape": "rectangular", - "totalLiquidVolume": 2000, - "x": 50.3, - "xDimension": 8.2, - "y": 29.15, - "yDimension": 8.2, - "z": 3 - }, - "F6": { - "depth": 38, - "shape": "rectangular", - "totalLiquidVolume": 2000, - "x": 59.3, - "xDimension": 8.2, - "y": 29.15, - "yDimension": 8.2, - "z": 3 - }, - "F7": { - "depth": 38, - "shape": "rectangular", - "totalLiquidVolume": 2000, - "x": 68.3, - "xDimension": 8.2, - "y": 29.15, - "yDimension": 8.2, - "z": 3 - }, - "F8": { - "depth": 38, - "shape": "rectangular", - "totalLiquidVolume": 2000, - "x": 77.3, - "xDimension": 8.2, - "y": 29.15, - "yDimension": 8.2, - "z": 3 - }, - "F9": { - "depth": 38, - "shape": "rectangular", - "totalLiquidVolume": 2000, - "x": 86.3, - "xDimension": 8.2, - "y": 29.15, - "yDimension": 8.2, - "z": 3 - }, - "G1": { - "depth": 38, - "shape": "rectangular", - "totalLiquidVolume": 2000, - "x": 14.3, - "xDimension": 8.2, - "y": 20.15, - "yDimension": 8.2, - "z": 3 - }, - "G10": { - "depth": 38, - "shape": "rectangular", - "totalLiquidVolume": 2000, - "x": 95.3, - "xDimension": 8.2, - "y": 20.15, - "yDimension": 8.2, - "z": 3 - }, - "G11": { - "depth": 38, - "shape": "rectangular", - "totalLiquidVolume": 2000, - "x": 104.3, - "xDimension": 8.2, - "y": 20.15, - "yDimension": 8.2, - "z": 3 - }, - "G12": { - "depth": 38, - "shape": "rectangular", - "totalLiquidVolume": 2000, - "x": 113.3, - "xDimension": 8.2, - "y": 20.15, - "yDimension": 8.2, - "z": 3 - }, - "G2": { - "depth": 38, - "shape": "rectangular", - "totalLiquidVolume": 2000, - "x": 23.3, - "xDimension": 8.2, - "y": 20.15, - "yDimension": 8.2, - "z": 3 - }, - "G3": { - "depth": 38, - "shape": "rectangular", - "totalLiquidVolume": 2000, - "x": 32.3, - "xDimension": 8.2, - "y": 20.15, - "yDimension": 8.2, - "z": 3 - }, - "G4": { - "depth": 38, - "shape": "rectangular", - "totalLiquidVolume": 2000, - "x": 41.3, - "xDimension": 8.2, - "y": 20.15, - "yDimension": 8.2, - "z": 3 - }, - "G5": { - "depth": 38, - "shape": "rectangular", - "totalLiquidVolume": 2000, - "x": 50.3, - "xDimension": 8.2, - "y": 20.15, - "yDimension": 8.2, - "z": 3 - }, - "G6": { - "depth": 38, - "shape": "rectangular", - "totalLiquidVolume": 2000, - "x": 59.3, - "xDimension": 8.2, - "y": 20.15, - "yDimension": 8.2, - "z": 3 - }, - "G7": { - "depth": 38, - "shape": "rectangular", - "totalLiquidVolume": 2000, - "x": 68.3, - "xDimension": 8.2, - "y": 20.15, - "yDimension": 8.2, - "z": 3 - }, - "G8": { - "depth": 38, - "shape": "rectangular", - "totalLiquidVolume": 2000, - "x": 77.3, - "xDimension": 8.2, - "y": 20.15, - "yDimension": 8.2, - "z": 3 - }, - "G9": { - "depth": 38, - "shape": "rectangular", - "totalLiquidVolume": 2000, - "x": 86.3, - "xDimension": 8.2, - "y": 20.15, - "yDimension": 8.2, - "z": 3 - }, - "H1": { - "depth": 38, - "shape": "rectangular", - "totalLiquidVolume": 2000, - "x": 14.3, - "xDimension": 8.2, - "y": 11.15, - "yDimension": 8.2, - "z": 3 - }, - "H10": { - "depth": 38, - "shape": "rectangular", - "totalLiquidVolume": 2000, - "x": 95.3, - "xDimension": 8.2, - "y": 11.15, - "yDimension": 8.2, - "z": 3 - }, - "H11": { - "depth": 38, - "shape": "rectangular", - "totalLiquidVolume": 2000, - "x": 104.3, - "xDimension": 8.2, - "y": 11.15, - "yDimension": 8.2, - "z": 3 - }, - "H12": { - "depth": 38, - "shape": "rectangular", - "totalLiquidVolume": 2000, - "x": 113.3, - "xDimension": 8.2, - "y": 11.15, - "yDimension": 8.2, - "z": 3 - }, - "H2": { - "depth": 38, - "shape": "rectangular", - "totalLiquidVolume": 2000, - "x": 23.3, - "xDimension": 8.2, - "y": 11.15, - "yDimension": 8.2, - "z": 3 - }, - "H3": { - "depth": 38, - "shape": "rectangular", - "totalLiquidVolume": 2000, - "x": 32.3, - "xDimension": 8.2, - "y": 11.15, - "yDimension": 8.2, - "z": 3 - }, - "H4": { - "depth": 38, - "shape": "rectangular", - "totalLiquidVolume": 2000, - "x": 41.3, - "xDimension": 8.2, - "y": 11.15, - "yDimension": 8.2, - "z": 3 - }, - "H5": { - "depth": 38, - "shape": "rectangular", - "totalLiquidVolume": 2000, - "x": 50.3, - "xDimension": 8.2, - "y": 11.15, - "yDimension": 8.2, - "z": 3 - }, - "H6": { - "depth": 38, - "shape": "rectangular", - "totalLiquidVolume": 2000, - "x": 59.3, - "xDimension": 8.2, - "y": 11.15, - "yDimension": 8.2, - "z": 3 - }, - "H7": { - "depth": 38, - "shape": "rectangular", - "totalLiquidVolume": 2000, - "x": 68.3, - "xDimension": 8.2, - "y": 11.15, - "yDimension": 8.2, - "z": 3 - }, - "H8": { - "depth": 38, - "shape": "rectangular", - "totalLiquidVolume": 2000, - "x": 77.3, - "xDimension": 8.2, - "y": 11.15, - "yDimension": 8.2, - "z": 3 - }, - "H9": { - "depth": 38, - "shape": "rectangular", - "totalLiquidVolume": 2000, - "x": 86.3, - "xDimension": 8.2, - "y": 11.15, - "yDimension": 8.2, - "z": 3 - } - } - }, - "labwareId": "UUID" - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "heaterShaker/closeLabwareLatch", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "7b9b3bc1ec8ef40fd43a1478e9a1896d", - "notes": [], - "params": { - "moduleId": "UUID" - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLabware", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "80d3c25d5533b9c4f4b83a66d4af895e", - "notes": [], - "params": { - "loadName": "nest_12_reservoir_15ml", - "location": { - "slotName": "A2" - }, - "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 - } - } - }, - "labwareId": "UUID" - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLabware", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "39705bcf605998607e3ff52963471b4d", - "notes": [], - "params": { - "loadName": "opentrons_flex_96_tiprack_50ul", - "location": { - "slotName": "C1" - }, - "namespace": "opentrons", - "version": 1 - }, - "result": { - "definition": { - "allowedRoles": [], - "brand": { - "brand": "Opentrons", - "brandId": [] - }, - "cornerOffsetFromSlot": { - "x": 0, - "y": 0, - "z": 0 - }, - "dimensions": { - "xDimension": 127.75, - "yDimension": 85.75, - "zDimension": 99 - }, - "gripForce": 16.0, - "gripHeightFromLabwareBottom": 23.9, - "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 Flex 96 Tip Rack 50 µ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_flex_96_tiprack_50ul", - "quirks": [], - "tipLength": 57.9, - "tipOverlap": 10.5 - }, - "schemaVersion": 2, - "stackingOffsetWithLabware": { - "opentrons_flex_96_tiprack_adapter": { - "x": 0, - "y": 0, - "z": 121 - } - }, - "stackingOffsetWithModule": {}, - "version": 1, - "wells": { - "A1": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 14.38, - "y": 74.38, - "z": 1.5 - }, - "A10": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 95.38, - "y": 74.38, - "z": 1.5 - }, - "A11": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 104.38, - "y": 74.38, - "z": 1.5 - }, - "A12": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 113.38, - "y": 74.38, - "z": 1.5 - }, - "A2": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 23.38, - "y": 74.38, - "z": 1.5 - }, - "A3": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 32.38, - "y": 74.38, - "z": 1.5 - }, - "A4": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 41.38, - "y": 74.38, - "z": 1.5 - }, - "A5": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 50.38, - "y": 74.38, - "z": 1.5 - }, - "A6": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 59.38, - "y": 74.38, - "z": 1.5 - }, - "A7": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 68.38, - "y": 74.38, - "z": 1.5 - }, - "A8": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 77.38, - "y": 74.38, - "z": 1.5 - }, - "A9": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 86.38, - "y": 74.38, - "z": 1.5 - }, - "B1": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 14.38, - "y": 65.38, - "z": 1.5 - }, - "B10": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 95.38, - "y": 65.38, - "z": 1.5 - }, - "B11": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 104.38, - "y": 65.38, - "z": 1.5 - }, - "B12": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 113.38, - "y": 65.38, - "z": 1.5 - }, - "B2": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 23.38, - "y": 65.38, - "z": 1.5 - }, - "B3": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 32.38, - "y": 65.38, - "z": 1.5 - }, - "B4": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 41.38, - "y": 65.38, - "z": 1.5 - }, - "B5": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 50.38, - "y": 65.38, - "z": 1.5 - }, - "B6": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 59.38, - "y": 65.38, - "z": 1.5 - }, - "B7": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 68.38, - "y": 65.38, - "z": 1.5 - }, - "B8": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 77.38, - "y": 65.38, - "z": 1.5 - }, - "B9": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 86.38, - "y": 65.38, - "z": 1.5 - }, - "C1": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 14.38, - "y": 56.38, - "z": 1.5 - }, - "C10": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 95.38, - "y": 56.38, - "z": 1.5 - }, - "C11": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 104.38, - "y": 56.38, - "z": 1.5 - }, - "C12": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 113.38, - "y": 56.38, - "z": 1.5 - }, - "C2": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 23.38, - "y": 56.38, - "z": 1.5 - }, - "C3": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 32.38, - "y": 56.38, - "z": 1.5 - }, - "C4": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 41.38, - "y": 56.38, - "z": 1.5 - }, - "C5": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 50.38, - "y": 56.38, - "z": 1.5 - }, - "C6": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 59.38, - "y": 56.38, - "z": 1.5 - }, - "C7": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 68.38, - "y": 56.38, - "z": 1.5 - }, - "C8": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 77.38, - "y": 56.38, - "z": 1.5 - }, - "C9": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 86.38, - "y": 56.38, - "z": 1.5 - }, - "D1": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 14.38, - "y": 47.38, - "z": 1.5 - }, - "D10": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 95.38, - "y": 47.38, - "z": 1.5 - }, - "D11": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 104.38, - "y": 47.38, - "z": 1.5 - }, - "D12": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 113.38, - "y": 47.38, - "z": 1.5 - }, - "D2": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 23.38, - "y": 47.38, - "z": 1.5 - }, - "D3": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 32.38, - "y": 47.38, - "z": 1.5 - }, - "D4": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 41.38, - "y": 47.38, - "z": 1.5 - }, - "D5": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 50.38, - "y": 47.38, - "z": 1.5 - }, - "D6": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 59.38, - "y": 47.38, - "z": 1.5 - }, - "D7": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 68.38, - "y": 47.38, - "z": 1.5 - }, - "D8": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 77.38, - "y": 47.38, - "z": 1.5 - }, - "D9": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 86.38, - "y": 47.38, - "z": 1.5 - }, - "E1": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 14.38, - "y": 38.38, - "z": 1.5 - }, - "E10": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 95.38, - "y": 38.38, - "z": 1.5 - }, - "E11": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 104.38, - "y": 38.38, - "z": 1.5 - }, - "E12": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 113.38, - "y": 38.38, - "z": 1.5 - }, - "E2": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 23.38, - "y": 38.38, - "z": 1.5 - }, - "E3": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 32.38, - "y": 38.38, - "z": 1.5 - }, - "E4": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 41.38, - "y": 38.38, - "z": 1.5 - }, - "E5": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 50.38, - "y": 38.38, - "z": 1.5 - }, - "E6": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 59.38, - "y": 38.38, - "z": 1.5 - }, - "E7": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 68.38, - "y": 38.38, - "z": 1.5 - }, - "E8": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 77.38, - "y": 38.38, - "z": 1.5 - }, - "E9": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 86.38, - "y": 38.38, - "z": 1.5 - }, - "F1": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 14.38, - "y": 29.38, - "z": 1.5 - }, - "F10": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 95.38, - "y": 29.38, - "z": 1.5 - }, - "F11": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 104.38, - "y": 29.38, - "z": 1.5 - }, - "F12": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 113.38, - "y": 29.38, - "z": 1.5 - }, - "F2": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 23.38, - "y": 29.38, - "z": 1.5 - }, - "F3": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 32.38, - "y": 29.38, - "z": 1.5 - }, - "F4": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 41.38, - "y": 29.38, - "z": 1.5 - }, - "F5": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 50.38, - "y": 29.38, - "z": 1.5 - }, - "F6": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 59.38, - "y": 29.38, - "z": 1.5 - }, - "F7": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 68.38, - "y": 29.38, - "z": 1.5 - }, - "F8": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 77.38, - "y": 29.38, - "z": 1.5 - }, - "F9": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 86.38, - "y": 29.38, - "z": 1.5 - }, - "G1": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 14.38, - "y": 20.38, - "z": 1.5 - }, - "G10": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 95.38, - "y": 20.38, - "z": 1.5 - }, - "G11": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 104.38, - "y": 20.38, - "z": 1.5 - }, - "G12": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 113.38, - "y": 20.38, - "z": 1.5 - }, - "G2": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 23.38, - "y": 20.38, - "z": 1.5 - }, - "G3": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 32.38, - "y": 20.38, - "z": 1.5 - }, - "G4": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 41.38, - "y": 20.38, - "z": 1.5 - }, - "G5": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 50.38, - "y": 20.38, - "z": 1.5 - }, - "G6": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 59.38, - "y": 20.38, - "z": 1.5 - }, - "G7": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 68.38, - "y": 20.38, - "z": 1.5 - }, - "G8": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 77.38, - "y": 20.38, - "z": 1.5 - }, - "G9": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 86.38, - "y": 20.38, - "z": 1.5 - }, - "H1": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 14.38, - "y": 11.38, - "z": 1.5 - }, - "H10": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 95.38, - "y": 11.38, - "z": 1.5 - }, - "H11": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 104.38, - "y": 11.38, - "z": 1.5 - }, - "H12": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 113.38, - "y": 11.38, - "z": 1.5 - }, - "H2": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 23.38, - "y": 11.38, - "z": 1.5 - }, - "H3": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 32.38, - "y": 11.38, - "z": 1.5 - }, - "H4": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 41.38, - "y": 11.38, - "z": 1.5 - }, - "H5": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 50.38, - "y": 11.38, - "z": 1.5 - }, - "H6": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 59.38, - "y": 11.38, - "z": 1.5 - }, - "H7": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 68.38, - "y": 11.38, - "z": 1.5 - }, - "H8": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 77.38, - "y": 11.38, - "z": 1.5 - }, - "H9": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 86.38, - "y": 11.38, - "z": 1.5 - } - } - }, - "labwareId": "UUID" - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLabware", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "16e6802c3a90fa35fd3ad8570a56c95d", - "notes": [], - "params": { - "loadName": "opentrons_flex_96_tiprack_50ul", - "location": { - "slotName": "C2" - }, - "namespace": "opentrons", - "version": 1 - }, - "result": { - "definition": { - "allowedRoles": [], - "brand": { - "brand": "Opentrons", - "brandId": [] - }, - "cornerOffsetFromSlot": { - "x": 0, - "y": 0, - "z": 0 - }, - "dimensions": { - "xDimension": 127.75, - "yDimension": 85.75, - "zDimension": 99 - }, - "gripForce": 16.0, - "gripHeightFromLabwareBottom": 23.9, - "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 Flex 96 Tip Rack 50 µ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_flex_96_tiprack_50ul", - "quirks": [], - "tipLength": 57.9, - "tipOverlap": 10.5 - }, - "schemaVersion": 2, - "stackingOffsetWithLabware": { - "opentrons_flex_96_tiprack_adapter": { - "x": 0, - "y": 0, - "z": 121 - } - }, - "stackingOffsetWithModule": {}, - "version": 1, - "wells": { - "A1": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 14.38, - "y": 74.38, - "z": 1.5 - }, - "A10": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 95.38, - "y": 74.38, - "z": 1.5 - }, - "A11": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 104.38, - "y": 74.38, - "z": 1.5 - }, - "A12": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 113.38, - "y": 74.38, - "z": 1.5 - }, - "A2": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 23.38, - "y": 74.38, - "z": 1.5 - }, - "A3": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 32.38, - "y": 74.38, - "z": 1.5 - }, - "A4": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 41.38, - "y": 74.38, - "z": 1.5 - }, - "A5": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 50.38, - "y": 74.38, - "z": 1.5 - }, - "A6": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 59.38, - "y": 74.38, - "z": 1.5 - }, - "A7": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 68.38, - "y": 74.38, - "z": 1.5 - }, - "A8": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 77.38, - "y": 74.38, - "z": 1.5 - }, - "A9": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 86.38, - "y": 74.38, - "z": 1.5 - }, - "B1": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 14.38, - "y": 65.38, - "z": 1.5 - }, - "B10": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 95.38, - "y": 65.38, - "z": 1.5 - }, - "B11": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 104.38, - "y": 65.38, - "z": 1.5 - }, - "B12": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 113.38, - "y": 65.38, - "z": 1.5 - }, - "B2": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 23.38, - "y": 65.38, - "z": 1.5 - }, - "B3": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 32.38, - "y": 65.38, - "z": 1.5 - }, - "B4": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 41.38, - "y": 65.38, - "z": 1.5 - }, - "B5": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 50.38, - "y": 65.38, - "z": 1.5 - }, - "B6": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 59.38, - "y": 65.38, - "z": 1.5 - }, - "B7": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 68.38, - "y": 65.38, - "z": 1.5 - }, - "B8": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 77.38, - "y": 65.38, - "z": 1.5 - }, - "B9": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 86.38, - "y": 65.38, - "z": 1.5 - }, - "C1": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 14.38, - "y": 56.38, - "z": 1.5 - }, - "C10": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 95.38, - "y": 56.38, - "z": 1.5 - }, - "C11": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 104.38, - "y": 56.38, - "z": 1.5 - }, - "C12": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 113.38, - "y": 56.38, - "z": 1.5 - }, - "C2": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 23.38, - "y": 56.38, - "z": 1.5 - }, - "C3": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 32.38, - "y": 56.38, - "z": 1.5 - }, - "C4": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 41.38, - "y": 56.38, - "z": 1.5 - }, - "C5": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 50.38, - "y": 56.38, - "z": 1.5 - }, - "C6": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 59.38, - "y": 56.38, - "z": 1.5 - }, - "C7": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 68.38, - "y": 56.38, - "z": 1.5 - }, - "C8": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 77.38, - "y": 56.38, - "z": 1.5 - }, - "C9": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 86.38, - "y": 56.38, - "z": 1.5 - }, - "D1": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 14.38, - "y": 47.38, - "z": 1.5 - }, - "D10": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 95.38, - "y": 47.38, - "z": 1.5 - }, - "D11": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 104.38, - "y": 47.38, - "z": 1.5 - }, - "D12": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 113.38, - "y": 47.38, - "z": 1.5 - }, - "D2": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 23.38, - "y": 47.38, - "z": 1.5 - }, - "D3": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 32.38, - "y": 47.38, - "z": 1.5 - }, - "D4": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 41.38, - "y": 47.38, - "z": 1.5 - }, - "D5": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 50.38, - "y": 47.38, - "z": 1.5 - }, - "D6": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 59.38, - "y": 47.38, - "z": 1.5 - }, - "D7": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 68.38, - "y": 47.38, - "z": 1.5 - }, - "D8": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 77.38, - "y": 47.38, - "z": 1.5 - }, - "D9": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 86.38, - "y": 47.38, - "z": 1.5 - }, - "E1": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 14.38, - "y": 38.38, - "z": 1.5 - }, - "E10": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 95.38, - "y": 38.38, - "z": 1.5 - }, - "E11": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 104.38, - "y": 38.38, - "z": 1.5 - }, - "E12": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 113.38, - "y": 38.38, - "z": 1.5 - }, - "E2": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 23.38, - "y": 38.38, - "z": 1.5 - }, - "E3": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 32.38, - "y": 38.38, - "z": 1.5 - }, - "E4": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 41.38, - "y": 38.38, - "z": 1.5 - }, - "E5": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 50.38, - "y": 38.38, - "z": 1.5 - }, - "E6": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 59.38, - "y": 38.38, - "z": 1.5 - }, - "E7": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 68.38, - "y": 38.38, - "z": 1.5 - }, - "E8": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 77.38, - "y": 38.38, - "z": 1.5 - }, - "E9": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 86.38, - "y": 38.38, - "z": 1.5 - }, - "F1": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 14.38, - "y": 29.38, - "z": 1.5 - }, - "F10": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 95.38, - "y": 29.38, - "z": 1.5 - }, - "F11": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 104.38, - "y": 29.38, - "z": 1.5 - }, - "F12": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 113.38, - "y": 29.38, - "z": 1.5 - }, - "F2": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 23.38, - "y": 29.38, - "z": 1.5 - }, - "F3": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 32.38, - "y": 29.38, - "z": 1.5 - }, - "F4": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 41.38, - "y": 29.38, - "z": 1.5 - }, - "F5": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 50.38, - "y": 29.38, - "z": 1.5 - }, - "F6": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 59.38, - "y": 29.38, - "z": 1.5 - }, - "F7": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 68.38, - "y": 29.38, - "z": 1.5 - }, - "F8": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 77.38, - "y": 29.38, - "z": 1.5 - }, - "F9": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 86.38, - "y": 29.38, - "z": 1.5 - }, - "G1": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 14.38, - "y": 20.38, - "z": 1.5 - }, - "G10": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 95.38, - "y": 20.38, - "z": 1.5 - }, - "G11": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 104.38, - "y": 20.38, - "z": 1.5 - }, - "G12": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 113.38, - "y": 20.38, - "z": 1.5 - }, - "G2": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 23.38, - "y": 20.38, - "z": 1.5 - }, - "G3": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 32.38, - "y": 20.38, - "z": 1.5 - }, - "G4": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 41.38, - "y": 20.38, - "z": 1.5 - }, - "G5": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 50.38, - "y": 20.38, - "z": 1.5 - }, - "G6": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 59.38, - "y": 20.38, - "z": 1.5 - }, - "G7": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 68.38, - "y": 20.38, - "z": 1.5 - }, - "G8": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 77.38, - "y": 20.38, - "z": 1.5 - }, - "G9": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 86.38, - "y": 20.38, - "z": 1.5 - }, - "H1": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 14.38, - "y": 11.38, - "z": 1.5 - }, - "H10": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 95.38, - "y": 11.38, - "z": 1.5 - }, - "H11": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 104.38, - "y": 11.38, - "z": 1.5 - }, - "H12": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 113.38, - "y": 11.38, - "z": 1.5 - }, - "H2": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 23.38, - "y": 11.38, - "z": 1.5 - }, - "H3": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 32.38, - "y": 11.38, - "z": 1.5 - }, - "H4": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 41.38, - "y": 11.38, - "z": 1.5 - }, - "H5": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 50.38, - "y": 11.38, - "z": 1.5 - }, - "H6": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 59.38, - "y": 11.38, - "z": 1.5 - }, - "H7": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 68.38, - "y": 11.38, - "z": 1.5 - }, - "H8": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 77.38, - "y": 11.38, - "z": 1.5 - }, - "H9": { - "depth": 97.5, - "diameter": 5.58, - "shape": "circular", - "totalLiquidVolume": 50, - "x": 86.38, - "y": 11.38, - "z": 1.5 - } - } - }, - "labwareId": "UUID" - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLabware", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "e293d3a3d34ffc36672d4ad8587006b8", - "notes": [], - "params": { - "loadName": "opentrons_flex_96_tiprack_200ul", - "location": { - "slotName": "B2" - }, - "namespace": "opentrons", - "version": 1 - }, - "result": { - "definition": { - "allowedRoles": [], - "brand": { - "brand": "Opentrons", - "brandId": [] - }, - "cornerOffsetFromSlot": { - "x": 0, - "y": 0, - "z": 0 - }, - "dimensions": { - "xDimension": 127.75, - "yDimension": 85.75, - "zDimension": 99 - }, - "gripForce": 16.0, - "gripHeightFromLabwareBottom": 23.9, - "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 Flex 96 Tip Rack 200 µ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_flex_96_tiprack_200ul", - "quirks": [], - "tipLength": 58.35, - "tipOverlap": 10.5 - }, - "schemaVersion": 2, - "stackingOffsetWithLabware": { - "opentrons_flex_96_tiprack_adapter": { - "x": 0, - "y": 0, - "z": 121 - } - }, - "stackingOffsetWithModule": {}, - "version": 1, - "wells": { - "A1": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 14.38, - "y": 74.38, - "z": 1.5 - }, - "A10": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 95.38, - "y": 74.38, - "z": 1.5 - }, - "A11": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 104.38, - "y": 74.38, - "z": 1.5 - }, - "A12": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 113.38, - "y": 74.38, - "z": 1.5 - }, - "A2": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 23.38, - "y": 74.38, - "z": 1.5 - }, - "A3": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 32.38, - "y": 74.38, - "z": 1.5 - }, - "A4": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 41.38, - "y": 74.38, - "z": 1.5 - }, - "A5": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 50.38, - "y": 74.38, - "z": 1.5 - }, - "A6": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 59.38, - "y": 74.38, - "z": 1.5 - }, - "A7": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 68.38, - "y": 74.38, - "z": 1.5 - }, - "A8": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 77.38, - "y": 74.38, - "z": 1.5 - }, - "A9": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 86.38, - "y": 74.38, - "z": 1.5 - }, - "B1": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 14.38, - "y": 65.38, - "z": 1.5 - }, - "B10": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 95.38, - "y": 65.38, - "z": 1.5 - }, - "B11": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 104.38, - "y": 65.38, - "z": 1.5 - }, - "B12": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 113.38, - "y": 65.38, - "z": 1.5 - }, - "B2": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 23.38, - "y": 65.38, - "z": 1.5 - }, - "B3": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 32.38, - "y": 65.38, - "z": 1.5 - }, - "B4": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 41.38, - "y": 65.38, - "z": 1.5 - }, - "B5": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 50.38, - "y": 65.38, - "z": 1.5 - }, - "B6": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 59.38, - "y": 65.38, - "z": 1.5 - }, - "B7": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 68.38, - "y": 65.38, - "z": 1.5 - }, - "B8": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 77.38, - "y": 65.38, - "z": 1.5 - }, - "B9": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 86.38, - "y": 65.38, - "z": 1.5 - }, - "C1": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 14.38, - "y": 56.38, - "z": 1.5 - }, - "C10": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 95.38, - "y": 56.38, - "z": 1.5 - }, - "C11": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 104.38, - "y": 56.38, - "z": 1.5 - }, - "C12": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 113.38, - "y": 56.38, - "z": 1.5 - }, - "C2": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 23.38, - "y": 56.38, - "z": 1.5 - }, - "C3": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 32.38, - "y": 56.38, - "z": 1.5 - }, - "C4": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 41.38, - "y": 56.38, - "z": 1.5 - }, - "C5": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 50.38, - "y": 56.38, - "z": 1.5 - }, - "C6": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 59.38, - "y": 56.38, - "z": 1.5 - }, - "C7": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 68.38, - "y": 56.38, - "z": 1.5 - }, - "C8": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 77.38, - "y": 56.38, - "z": 1.5 - }, - "C9": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 86.38, - "y": 56.38, - "z": 1.5 - }, - "D1": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 14.38, - "y": 47.38, - "z": 1.5 - }, - "D10": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 95.38, - "y": 47.38, - "z": 1.5 - }, - "D11": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 104.38, - "y": 47.38, - "z": 1.5 - }, - "D12": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 113.38, - "y": 47.38, - "z": 1.5 - }, - "D2": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 23.38, - "y": 47.38, - "z": 1.5 - }, - "D3": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 32.38, - "y": 47.38, - "z": 1.5 - }, - "D4": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 41.38, - "y": 47.38, - "z": 1.5 - }, - "D5": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 50.38, - "y": 47.38, - "z": 1.5 - }, - "D6": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 59.38, - "y": 47.38, - "z": 1.5 - }, - "D7": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 68.38, - "y": 47.38, - "z": 1.5 - }, - "D8": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 77.38, - "y": 47.38, - "z": 1.5 - }, - "D9": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 86.38, - "y": 47.38, - "z": 1.5 - }, - "E1": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 14.38, - "y": 38.38, - "z": 1.5 - }, - "E10": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 95.38, - "y": 38.38, - "z": 1.5 - }, - "E11": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 104.38, - "y": 38.38, - "z": 1.5 - }, - "E12": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 113.38, - "y": 38.38, - "z": 1.5 - }, - "E2": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 23.38, - "y": 38.38, - "z": 1.5 - }, - "E3": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 32.38, - "y": 38.38, - "z": 1.5 - }, - "E4": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 41.38, - "y": 38.38, - "z": 1.5 - }, - "E5": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 50.38, - "y": 38.38, - "z": 1.5 - }, - "E6": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 59.38, - "y": 38.38, - "z": 1.5 - }, - "E7": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 68.38, - "y": 38.38, - "z": 1.5 - }, - "E8": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 77.38, - "y": 38.38, - "z": 1.5 - }, - "E9": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 86.38, - "y": 38.38, - "z": 1.5 - }, - "F1": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 14.38, - "y": 29.38, - "z": 1.5 - }, - "F10": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 95.38, - "y": 29.38, - "z": 1.5 - }, - "F11": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 104.38, - "y": 29.38, - "z": 1.5 - }, - "F12": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 113.38, - "y": 29.38, - "z": 1.5 - }, - "F2": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 23.38, - "y": 29.38, - "z": 1.5 - }, - "F3": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 32.38, - "y": 29.38, - "z": 1.5 - }, - "F4": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 41.38, - "y": 29.38, - "z": 1.5 - }, - "F5": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 50.38, - "y": 29.38, - "z": 1.5 - }, - "F6": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 59.38, - "y": 29.38, - "z": 1.5 - }, - "F7": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 68.38, - "y": 29.38, - "z": 1.5 - }, - "F8": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 77.38, - "y": 29.38, - "z": 1.5 - }, - "F9": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 86.38, - "y": 29.38, - "z": 1.5 - }, - "G1": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 14.38, - "y": 20.38, - "z": 1.5 - }, - "G10": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 95.38, - "y": 20.38, - "z": 1.5 - }, - "G11": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 104.38, - "y": 20.38, - "z": 1.5 - }, - "G12": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 113.38, - "y": 20.38, - "z": 1.5 - }, - "G2": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 23.38, - "y": 20.38, - "z": 1.5 - }, - "G3": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 32.38, - "y": 20.38, - "z": 1.5 - }, - "G4": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 41.38, - "y": 20.38, - "z": 1.5 - }, - "G5": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 50.38, - "y": 20.38, - "z": 1.5 - }, - "G6": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 59.38, - "y": 20.38, - "z": 1.5 - }, - "G7": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 68.38, - "y": 20.38, - "z": 1.5 - }, - "G8": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 77.38, - "y": 20.38, - "z": 1.5 - }, - "G9": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 86.38, - "y": 20.38, - "z": 1.5 - }, - "H1": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 14.38, - "y": 11.38, - "z": 1.5 - }, - "H10": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 95.38, - "y": 11.38, - "z": 1.5 - }, - "H11": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 104.38, - "y": 11.38, - "z": 1.5 - }, - "H12": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 113.38, - "y": 11.38, - "z": 1.5 - }, - "H2": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 23.38, - "y": 11.38, - "z": 1.5 - }, - "H3": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 32.38, - "y": 11.38, - "z": 1.5 - }, - "H4": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 41.38, - "y": 11.38, - "z": 1.5 - }, - "H5": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 50.38, - "y": 11.38, - "z": 1.5 - }, - "H6": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 59.38, - "y": 11.38, - "z": 1.5 - }, - "H7": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 68.38, - "y": 11.38, - "z": 1.5 - }, - "H8": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 77.38, - "y": 11.38, - "z": 1.5 - }, - "H9": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 86.38, - "y": 11.38, - "z": 1.5 - } - } - }, - "labwareId": "UUID" - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLabware", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "c9227d2a0882401c3eb6f1f2f96b8e67", - "notes": [], - "params": { - "loadName": "opentrons_flex_96_tiprack_200ul", - "location": { - "slotName": "B3" - }, - "namespace": "opentrons", - "version": 1 - }, - "result": { - "definition": { - "allowedRoles": [], - "brand": { - "brand": "Opentrons", - "brandId": [] - }, - "cornerOffsetFromSlot": { - "x": 0, - "y": 0, - "z": 0 - }, - "dimensions": { - "xDimension": 127.75, - "yDimension": 85.75, - "zDimension": 99 - }, - "gripForce": 16.0, - "gripHeightFromLabwareBottom": 23.9, - "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 Flex 96 Tip Rack 200 µ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_flex_96_tiprack_200ul", - "quirks": [], - "tipLength": 58.35, - "tipOverlap": 10.5 - }, - "schemaVersion": 2, - "stackingOffsetWithLabware": { - "opentrons_flex_96_tiprack_adapter": { - "x": 0, - "y": 0, - "z": 121 - } - }, - "stackingOffsetWithModule": {}, - "version": 1, - "wells": { - "A1": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 14.38, - "y": 74.38, - "z": 1.5 - }, - "A10": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 95.38, - "y": 74.38, - "z": 1.5 - }, - "A11": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 104.38, - "y": 74.38, - "z": 1.5 - }, - "A12": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 113.38, - "y": 74.38, - "z": 1.5 - }, - "A2": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 23.38, - "y": 74.38, - "z": 1.5 - }, - "A3": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 32.38, - "y": 74.38, - "z": 1.5 - }, - "A4": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 41.38, - "y": 74.38, - "z": 1.5 - }, - "A5": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 50.38, - "y": 74.38, - "z": 1.5 - }, - "A6": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 59.38, - "y": 74.38, - "z": 1.5 - }, - "A7": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 68.38, - "y": 74.38, - "z": 1.5 - }, - "A8": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 77.38, - "y": 74.38, - "z": 1.5 - }, - "A9": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 86.38, - "y": 74.38, - "z": 1.5 - }, - "B1": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 14.38, - "y": 65.38, - "z": 1.5 - }, - "B10": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 95.38, - "y": 65.38, - "z": 1.5 - }, - "B11": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 104.38, - "y": 65.38, - "z": 1.5 - }, - "B12": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 113.38, - "y": 65.38, - "z": 1.5 - }, - "B2": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 23.38, - "y": 65.38, - "z": 1.5 - }, - "B3": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 32.38, - "y": 65.38, - "z": 1.5 - }, - "B4": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 41.38, - "y": 65.38, - "z": 1.5 - }, - "B5": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 50.38, - "y": 65.38, - "z": 1.5 - }, - "B6": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 59.38, - "y": 65.38, - "z": 1.5 - }, - "B7": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 68.38, - "y": 65.38, - "z": 1.5 - }, - "B8": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 77.38, - "y": 65.38, - "z": 1.5 - }, - "B9": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 86.38, - "y": 65.38, - "z": 1.5 - }, - "C1": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 14.38, - "y": 56.38, - "z": 1.5 - }, - "C10": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 95.38, - "y": 56.38, - "z": 1.5 - }, - "C11": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 104.38, - "y": 56.38, - "z": 1.5 - }, - "C12": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 113.38, - "y": 56.38, - "z": 1.5 - }, - "C2": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 23.38, - "y": 56.38, - "z": 1.5 - }, - "C3": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 32.38, - "y": 56.38, - "z": 1.5 - }, - "C4": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 41.38, - "y": 56.38, - "z": 1.5 - }, - "C5": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 50.38, - "y": 56.38, - "z": 1.5 - }, - "C6": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 59.38, - "y": 56.38, - "z": 1.5 - }, - "C7": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 68.38, - "y": 56.38, - "z": 1.5 - }, - "C8": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 77.38, - "y": 56.38, - "z": 1.5 - }, - "C9": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 86.38, - "y": 56.38, - "z": 1.5 - }, - "D1": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 14.38, - "y": 47.38, - "z": 1.5 - }, - "D10": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 95.38, - "y": 47.38, - "z": 1.5 - }, - "D11": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 104.38, - "y": 47.38, - "z": 1.5 - }, - "D12": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 113.38, - "y": 47.38, - "z": 1.5 - }, - "D2": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 23.38, - "y": 47.38, - "z": 1.5 - }, - "D3": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 32.38, - "y": 47.38, - "z": 1.5 - }, - "D4": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 41.38, - "y": 47.38, - "z": 1.5 - }, - "D5": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 50.38, - "y": 47.38, - "z": 1.5 - }, - "D6": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 59.38, - "y": 47.38, - "z": 1.5 - }, - "D7": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 68.38, - "y": 47.38, - "z": 1.5 - }, - "D8": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 77.38, - "y": 47.38, - "z": 1.5 - }, - "D9": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 86.38, - "y": 47.38, - "z": 1.5 - }, - "E1": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 14.38, - "y": 38.38, - "z": 1.5 - }, - "E10": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 95.38, - "y": 38.38, - "z": 1.5 - }, - "E11": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 104.38, - "y": 38.38, - "z": 1.5 - }, - "E12": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 113.38, - "y": 38.38, - "z": 1.5 - }, - "E2": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 23.38, - "y": 38.38, - "z": 1.5 - }, - "E3": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 32.38, - "y": 38.38, - "z": 1.5 - }, - "E4": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 41.38, - "y": 38.38, - "z": 1.5 - }, - "E5": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 50.38, - "y": 38.38, - "z": 1.5 - }, - "E6": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 59.38, - "y": 38.38, - "z": 1.5 - }, - "E7": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 68.38, - "y": 38.38, - "z": 1.5 - }, - "E8": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 77.38, - "y": 38.38, - "z": 1.5 - }, - "E9": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 86.38, - "y": 38.38, - "z": 1.5 - }, - "F1": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 14.38, - "y": 29.38, - "z": 1.5 - }, - "F10": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 95.38, - "y": 29.38, - "z": 1.5 - }, - "F11": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 104.38, - "y": 29.38, - "z": 1.5 - }, - "F12": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 113.38, - "y": 29.38, - "z": 1.5 - }, - "F2": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 23.38, - "y": 29.38, - "z": 1.5 - }, - "F3": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 32.38, - "y": 29.38, - "z": 1.5 - }, - "F4": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 41.38, - "y": 29.38, - "z": 1.5 - }, - "F5": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 50.38, - "y": 29.38, - "z": 1.5 - }, - "F6": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 59.38, - "y": 29.38, - "z": 1.5 - }, - "F7": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 68.38, - "y": 29.38, - "z": 1.5 - }, - "F8": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 77.38, - "y": 29.38, - "z": 1.5 - }, - "F9": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 86.38, - "y": 29.38, - "z": 1.5 - }, - "G1": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 14.38, - "y": 20.38, - "z": 1.5 - }, - "G10": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 95.38, - "y": 20.38, - "z": 1.5 - }, - "G11": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 104.38, - "y": 20.38, - "z": 1.5 - }, - "G12": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 113.38, - "y": 20.38, - "z": 1.5 - }, - "G2": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 23.38, - "y": 20.38, - "z": 1.5 - }, - "G3": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 32.38, - "y": 20.38, - "z": 1.5 - }, - "G4": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 41.38, - "y": 20.38, - "z": 1.5 - }, - "G5": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 50.38, - "y": 20.38, - "z": 1.5 - }, - "G6": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 59.38, - "y": 20.38, - "z": 1.5 - }, - "G7": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 68.38, - "y": 20.38, - "z": 1.5 - }, - "G8": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 77.38, - "y": 20.38, - "z": 1.5 - }, - "G9": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 86.38, - "y": 20.38, - "z": 1.5 - }, - "H1": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 14.38, - "y": 11.38, - "z": 1.5 - }, - "H10": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 95.38, - "y": 11.38, - "z": 1.5 - }, - "H11": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 104.38, - "y": 11.38, - "z": 1.5 - }, - "H12": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 113.38, - "y": 11.38, - "z": 1.5 - }, - "H2": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 23.38, - "y": 11.38, - "z": 1.5 - }, - "H3": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 32.38, - "y": 11.38, - "z": 1.5 - }, - "H4": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 41.38, - "y": 11.38, - "z": 1.5 - }, - "H5": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 50.38, - "y": 11.38, - "z": 1.5 - }, - "H6": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 59.38, - "y": 11.38, - "z": 1.5 - }, - "H7": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 68.38, - "y": 11.38, - "z": 1.5 - }, - "H8": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 77.38, - "y": 11.38, - "z": 1.5 - }, - "H9": { - "depth": 97.5, - "diameter": 5.59, - "shape": "circular", - "totalLiquidVolume": 200, - "x": 86.38, - "y": 11.38, - "z": 1.5 - } - } - }, - "labwareId": "UUID" - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadPipette", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "58bf66c06d4c9ef0133ceda2d3fcd0db", - "notes": [], - "params": { - "liquidPresenceDetection": false, - "mount": "left", - "pipetteName": "p50_multi_flex", - "tipOverlapNotAfterVersion": "v0" - }, - "result": { - "pipetteId": "UUID" - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadPipette", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "f41b352a27113bc3a9f0595d3f285410", - "notes": [], - "params": { - "liquidPresenceDetection": false, - "mount": "right", - "pipetteName": "p1000_multi_flex", - "tipOverlapNotAfterVersion": "v0" - }, - "result": { - "pipetteId": "UUID" - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "8f61100f29f0e3dae391082bcf9c4b33", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "A1": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "aaa5a61f4a6a6c60e1d02ab560e8ed30", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "B1": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "2e9b37a4b76c8be219754992b50eac87", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "C1": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "ddd20b2c38d4938cfbebedc9e32a0f3a", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "D1": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "5c8c68d760474150e360b8ccb0614254", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "E1": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "f8b997229880175082181fc529ea3906", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "F1": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "9818ef02347426a4ef817c25ccd44b3c", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "G1": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "d97431083e1366ed0a2b09b7195fae03", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "H1": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "779b7b1760eb1092027a3b5813510a32", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "A2": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "5eb0bca7d2181db9880c8105f08fa8fd", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "B2": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "008e8176fde7fd9caffd459f967acca1", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "C2": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "b66801b6f296760e22f9e210a386b1b1", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "D2": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "c8a1b0a4298bcec2592014f2bf37d576", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "E2": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "b693187f2934371da9b6eba9d20a3002", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "F2": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "e46b2068448ff0983a798cbef1a3cec0", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "G2": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "9f4f599a30f83a5420e5fb732ca2efc0", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "H2": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "4472cf6cf1f8e71cdd0be1633127c932", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "A3": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "15d9492cab2583479a4f816ef17a46a1", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "B3": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "7f4a24bd41db2e2f75e276418679944c", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "C3": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "5e389eada4ee80880ce64d17356c0c81", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "D3": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "7ce70a99c5d3071241add86ec7a937a6", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "E3": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "2bb7f73d43103f0343484b7194519e4c", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "F3": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "ef9fda0ac1ab209fe854ae7e7d7f4aa5", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "G3": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "47674b8e252ba5e9349dd8dbc2e66a40", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "H3": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "dcc8d9df462cd0c9a1b47ccd02f3eb2d", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "A4": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "2d70199bbce02415accfd10aa3c03c42", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "B4": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "aa816105a34323703c3bb8cb06146be1", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "C4": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "7e0516811ce65b3a61ec6786ea0219d9", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "D4": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "a8fd1948ecc0acbaae6cb41bb8e730de", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "E4": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "c6c5a01e13cc765533e432494ff7f07f", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "F4": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "a9c8d1c1d71ae30d2d011a21e5ffdb3e", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "G4": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "4bdb9bf5d751f40bb798826e2e676870", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "H4": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "5a73e1d4498d560d2951a9b0851d173c", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "A5": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "4bedaaa098e7bccd2f2bfdfdd6ec0a2e", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "B5": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "672d7bbe39d4d9fc5544a9075bfd1f79", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "C5": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "7efec2350ce946b98fbedca57bb2d546", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "D5": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "96567efe8321030016f11223d404621f", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "E5": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "4b85144700c274e59a52004fe6667c25", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "F5": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "60537125b393dbadfe94648248132f7c", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "G5": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "990dd741f3dcccafb23cc986b4088ad9", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "H5": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "d8ff455d6832a88e094daf9c9aa2eaa8", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "A6": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "80fa3121956cee042c5d6dc137e3ca71", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "B6": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "75bd4c7ef1588a67ea415705b2c8349c", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "C6": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "3874e2785cec5c6baf3dff0d664d90ef", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "D6": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "bfdae27d79cc5953ddadab4a39c10466", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "E6": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "e231e7cc6a278ce42d86d6129b01453b", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "F6": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "9aebd50719d690f81b6385ed1533157e", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "G6": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "bfa8b077772ea67face5b5da8ce31feb", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "H6": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "9a3b06420819d013bdd36b9b2b4d2358", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "A1": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "5f5aa88961947babf39b10e37ee23421", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "B1": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "1d856d812464812151e1d5a8dd667237", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "C1": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "ebed53310e7b29d035f6f7a016fff0f8", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "D1": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "2fd7d9f9978f8381228cccb93245fc2d", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "E1": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "d7c19c35227a0340e67f2e0ed968d7d3", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "F1": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "9ac59c380e99317f35830b98fb80871d", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "G1": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "a443653eceb8acf82a3ff5594ec68825", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "H1": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "8c6d4cad0acca6032cee9ed68f3b7b77", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "A2": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "1947ab90ab78e871a5093646a8ad92d0", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "B2": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "0c22e06378cc0cd2d4064974f2fa408b", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "C2": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "4193cbdc63a59a75bcafbf4d97663b9d", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "D2": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "d1103b5929b1b7a0185f58a74468440d", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "E2": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "1b3f07e9f1477115b69ce6331c90d9f1", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "F2": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "303f4d143d7b69932fe4fd18127c81db", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "G2": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "65f3615cb8230a6ddb6cf35b73c3c5aa", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "H2": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "d5e7c9ba1db7bf2b087d00ca46c472e9", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "A3": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "e01aa7bc7c31a8dffd23546b1945e675", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "B3": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "9247c62d2ead992011f223392961b7b2", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "C3": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "d293202097173e711d291e7259c65611", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "D3": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "45bcdd313fe6186beac0fabca6a7dbdb", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "E3": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "b550a26ebfeb445caf3784c322b41089", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "F3": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "d1fe7ad1290317d754709b52721cf783", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "G3": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "2830f4f34d964ab48b8957fa864e0316", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "H3": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "818860ae04707d24dc36e96743884690", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "A4": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "43aa0a49ca4c34112a19b99640e21beb", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "B4": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "8c658cd713c0ac4f52bc9855b872130a", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "C4": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "185a3fd2dbd29301fc7e92cb25f0e218", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "D4": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "8633954f726d96cb33537b14a848b4bc", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "E4": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "620b4f47c73faf6e0265f8741d23dc8b", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "F4": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "2dcce3b03d36d221c7ce487bae928383", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "G4": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "135d11296e5067afac8847f22d806e79", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "H4": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "d53f49d3f67df5cc18fb4547217e4da9", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "A5": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "78298f5f0fa0e9043ab4df0549173ba2", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "B5": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "84571d458086763fbd2d5e30f26a6d1b", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "C5": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "6fec441fec6751579035f49cb02fb651", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "D5": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "9774136549a3a20f61860acb85b83438", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "E5": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "cf2009489d7bcc50208d3cfec70bd176", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "F5": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "f8d7eeec9aabdf5a9e3e7e5f91046003", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "G5": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "5c26c2c63789e7307ea62537c48c1e42", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "H5": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "466fa95ac331fbb920ab792a52078999", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "A6": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "427f6c1570b503fcc1e04f26c2369aaf", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "B6": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "44d529133defc8ddfe4995ce63c3f914", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "C6": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "437671cc25e87718115772ec6ec66d57", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "D6": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "9fdddc1ec95caa85c91bab332d0af3e4", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "E6": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "f2d35b138c96e986cc8ca9d71bb9f6e8", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "F6": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "56c24389b4ad55beb7ed6159c4ad00d3", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "G6": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "120ece00f990ff4566b38892075fc007", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "H6": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "0bf90a90466930017162fadbea9477a6", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "A1": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "7d0d618d19cfbf3b537bed12e0f06f4d", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "B1": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "ffe0929eea2d196d78f265dd2e9926d3", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "C1": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "c8f5cc16090f48b1c5a31908c12e93a5", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "D1": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "4091b25131a60700b2e23a474cefb4f1", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "E1": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "e908f3817c89489f66d8e675e0f10e14", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "F1": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "afd0eadc7fd05676719741b36917d87c", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "G1": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "6dfb35b2956e28a771a594758e46a714", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "H1": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "7ca6001ebb059fefc71631552f8af3d3", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "A2": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "3287ad9cac60f592ab3536068b0f40ec", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "B2": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "aff52c45c3c1e93af130a41c8c80d19c", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "C2": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "b263b9522566c1802676980e23209041", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "D2": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "c8516f441443ee0604fd1a2df54ad7e0", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "E2": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "0e0454a517248a99751a99fe1c08a2f6", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "F2": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "7ea7d18690ec3e20e8ed3535c1defbb9", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "G2": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "511c7bf60e9439e100ce45ac967da49e", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "H2": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "81a78fd07484e98039dafa397331454b", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "A3": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "cd8e8d0d5c7aaa5af541da01bb63f584", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "B3": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "b0e15451bf7b1644960e32675173dacf", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "C3": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "d10f1489d9c872506ebb31cd4b808540", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "D3": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "43af4a7235622d22806f1be8ffd9519c", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "E3": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "ebb3c328819a42ccc866bf0dbdfbae33", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "F3": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "2753b91f935a5083f07680673d6a9adc", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "G3": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "33803f50aaecf245ec0655dc31be2eb8", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "H3": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "d982765564753512defadd4ee28cec77", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "A4": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "511b2c79928728669da32ce6154bd1bc", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "B4": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "9a5cddb2e8602ec8fec98f7bf28918e5", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "C4": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "0328846c95fa6210972c3d45f8b54a74", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "D4": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "5ad886b1e0fdb9708d9c20c267e62220", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "E4": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "927818362c3ea3c14823ccce4965f727", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "F4": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "1e50241cdb9fcd2bff1534b98ea618b1", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "G4": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "8811f7a3c5a117e7f63f04b83de01698", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "H4": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "5760b7a67c17d86093f73c375bf61015", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "A5": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "f67583d0274c6ef9c3d762329f725f93", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "B5": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "b2ecfd404c288732c092ce69b7da8815", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "C5": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "2d899a259c3a91c64efdf675ed9a2d33", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "D5": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "fcf6ee9795c0a86091de90e908b372f4", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "E5": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "73d5e752210cfb0d867e78215b598501", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "F5": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "9d289d19747f833f0aa03fbd1ec6a989", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "G5": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "8c5457ceac8bee1ce222d9547cd4d715", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "H5": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "4496f5d5a2f033bcfc803c3e6d47e8f7", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "A6": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "ca357881aa182f6f45bac132037803c2", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "B6": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "26a1dc7b1ba51f89274dc8c24c519b5a", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "C6": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "28533630621c4cb17a03486a46b030a5", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "D6": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "ad02948bafeaaa7d6fb5c0fd66d8c227", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "E6": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "2121bb825bc14b0ab45f167418dc4ad4", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "F6": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "2adb52e7ccd59d289b2a0d35cd314db9", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "G6": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "40fae1aecd14139e9abcd6c26fa2c28d", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "H6": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "38ea01a01df003b05f3e1cc4b045a10f", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "A7": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "63beaa12dba5ecc8fad39b64c3852db1", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "B7": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "c875efceaba3272710bc90e5a92c42f3", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "C7": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "7ca6e36ce75ba4acfcdb44922b69d484", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "D7": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "124f2a131f81abeac9cbd0e9fcfcacb1", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "E7": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "da169522a77ef45b236cb685cc140252", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "F7": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "386b7617e29115c4e2b6d4cb87185279", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "G7": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "e1d20d4c3166b98e92ab93717266d249", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "H7": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "eb5db2ecd8e328db99b64db59e32ae09", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "A8": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "92f6dc249826e5924338c0e1f80f5b65", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "B8": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "2b462a5f63f15b04d69c56da3ee88de3", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "C8": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "3b042a699af6be5f267567394035fe3b", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "D8": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "a45ffe349ff38d95744c4ef86fa0354f", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "E8": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "26bd2430c3c4cc2408ad9a365f2c2891", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "F8": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "df281882873d0e1445c2246c90c8a3c8", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "G8": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "b1ddf115c9e3f3fb1c3ef80e116749ac", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "H8": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "8e21511c6ffaf00d1bc58e527b893f9f", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "A9": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "7a8e561af25913e7702fe452cbf82920", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "B9": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "c5305a0cb530b54ebc46b676eb95f556", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "C9": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "5accdd553bd1bee18252a9ce7eea55b4", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "D9": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "0f61f84cea74148cf50b6f65cdf9d961", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "E9": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "e90e375e3c3ad3df214ffafe47b98111", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "F9": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "14a072b4d5ef396d36986342c1c9bae8", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "G9": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "06bfa9e5693709b0a2ac5d79a176296a", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "H9": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "af95a8489936f9fef2c4925547f3ab1a", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "A10": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "d2664a413fb9dba6010006408dd2fa5c", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "B10": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "506ab0fee68596d670f6e284b19f4751", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "C10": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "962a559444ed526d9dffa906045e8325", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "D10": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "c8cd0e9d4856a4d392b6ccab9a42d124", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "E10": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "f80d8d52b380ca2a2961237de98f7230", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "F10": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "3303870d02520796d5cab53d697ccbe1", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "G10": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "203dc9ea7ded51bd40035af6e158d593", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "H10": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "b4f0749950b9c0a0fad2015aaac48a0c", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "A11": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "6f01f1aaa7ee5cc8a4c62144af4d0042", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "B11": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "905c8fed115a922f2d89e2c02c27616d", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "C11": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "875a960a9980be4f9dd1ba8745bc24cd", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "D11": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "6aeac56ee731a57aa81f33e3ff861d64", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "E11": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "2f4754434c34c2c99e5b1465af5c188e", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "F11": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "e8854207245788771690d1521b43c12f", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "G11": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "811183ffe7c1a3ce615213e4e70d75e5", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "H11": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "2abee4fc1c9c31bd629912d9c8401687", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "A12": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "7835e1234f735842daa4f61c575ce72f", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "B12": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "14ff231157b24c887d1ec0813b3c55f2", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "C12": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "858cd1cc7c5c762193c7e4fd67ebb8a9", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "D12": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "a2cd720991fdbb2e317b7bada0952dc5", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "E12": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "1fd4c6b672e6afe1a21e43ad7f412eb2", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "F12": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "9ab95910878dd01da02ef13f3d4b1839", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "G12": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "loadLiquid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "8f57d1b217457bcf0a4f4f17a828f1ab", - "notes": [], - "params": { - "labwareId": "UUID", - "liquidId": "UUID", - "volumeByWell": { - "H12": 200.0 - } - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "temperatureModule/setTargetTemperature", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "d20affad4dcd31700fe9360291f7993d", - "notes": [], - "params": { - "celsius": 4.0, - "moduleId": "UUID" - }, - "result": { - "targetTemperature": 4.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "temperatureModule/waitForTemperature", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "1cf4e0014a9e172647fc6f4fc603bb61", - "notes": [], - "params": { - "moduleId": "UUID" - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "thermocycler/setTargetLidTemperature", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "b6f197a28bfdd6061e504f34a2e2834f", - "notes": [], - "params": { - "celsius": 100.0, - "moduleId": "UUID" - }, - "result": { - "targetLidTemperature": 100.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "thermocycler/waitForLidTemperature", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "c34383e8fe124015ee44b5838747baae", - "notes": [], - "params": { - "moduleId": "UUID" - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "comment", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "09d94a47b65ddbabfcf509b876834d52", - "notes": [], - "params": { - "message": "\n\n----------ADDING DB1----------\n" - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "pickUpTip", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "462ab3a580018a704b2f310fffed7dce", - "notes": [], - "params": { - "labwareId": "UUID", - "pipetteId": "UUID", - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": 0.0 - }, - "origin": "top" - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 14.38, - "y": 181.38, - "z": 99.0 - }, - "tipDiameter": 5.58, - "tipLength": 47.849999999999994, - "tipVolume": 50.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "configureForVolume", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "ab75b4de24840f7e2a2af5f5a66769d3", - "notes": [], - "params": { - "pipetteId": "UUID", - "tipOverlapNotAfterVersion": "v0", - "volume": 4.0 - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "aspirate", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "02e0185ec5df7ddbb734f5dc8bc35b02", - "notes": [], - "params": { - "flowRate": 35.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 4.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -37.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 342.3, - "y": 74.15, - "z": 13.0 - }, - "volume": 4.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "dispense", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "925dc71d217999516f0cb47e48fc7102", - "notes": [], - "params": { - "flowRate": 57.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 4.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -13.95 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": -5.624999999999998, - "y": 356.2, - "z": 2.3100000000000014 - }, - "volume": 4.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "aspirate", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "bcf7ddbf19d9e5d8da7a913a3776a392", - "notes": [], - "params": { - "flowRate": 35.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 10.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -13.95 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": -5.624999999999998, - "y": 356.2, - "z": 2.3100000000000014 - }, - "volume": 10.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "dispense", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "1802acc6c262e1a9417bd185903c0d2b", - "notes": [], - "params": { - "flowRate": 57.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 10.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -13.95 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": -5.624999999999998, - "y": 356.2, - "z": 2.3100000000000014 - }, - "volume": 10.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "aspirate", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "80f028f4fd8345d9763b55de0afe11b5", - "notes": [], - "params": { - "flowRate": 35.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 10.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -13.95 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": -5.624999999999998, - "y": 356.2, - "z": 2.3100000000000014 - }, - "volume": 10.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "dispense", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "9fbb21cf3a09b3c3af84e8a5b16fd652", - "notes": [], - "params": { - "flowRate": 57.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 10.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -13.95 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": -5.624999999999998, - "y": 356.2, - "z": 2.3100000000000014 - }, - "volume": 10.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "aspirate", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "82da1bd1888beed5c0a18c149b949bde", - "notes": [], - "params": { - "flowRate": 35.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 10.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -13.95 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": -5.624999999999998, - "y": 356.2, - "z": 2.3100000000000014 - }, - "volume": 10.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "dispense", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "08edda9fab95b7cff943521fcaf0f11b", - "notes": [], - "params": { - "flowRate": 57.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 10.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -13.95 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": -5.624999999999998, - "y": 356.2, - "z": 2.3100000000000014 - }, - "volume": 10.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "aspirate", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "c4c1742d26de73b050a5b75444da155c", - "notes": [], - "params": { - "flowRate": 35.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 10.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -13.95 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": -5.624999999999998, - "y": 356.2, - "z": 2.3100000000000014 - }, - "volume": 10.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "dispense", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "dfd81fde99a69c94a1a712d165467b14", - "notes": [], - "params": { - "flowRate": 57.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 10.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -13.95 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": -5.624999999999998, - "y": 356.2, - "z": 2.3100000000000014 - }, - "volume": 10.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "aspirate", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "da6f938d70af05383d7ca8dd87d0b3a8", - "notes": [], - "params": { - "flowRate": 35.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 10.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -13.95 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": -5.624999999999998, - "y": 356.2, - "z": 2.3100000000000014 - }, - "volume": 10.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "dispense", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "412ed9d67777f23478129af556b697b7", - "notes": [], - "params": { - "flowRate": 57.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 10.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -13.95 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": -5.624999999999998, - "y": 356.2, - "z": 2.3100000000000014 - }, - "volume": 10.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "moveToWell", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "952980a8bab99528215ea511050c1e3a", - "notes": [], - "params": { - "forceDirect": false, - "labwareId": "UUID", - "pipetteId": "UUID", - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": 0.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": -5.624999999999998, - "y": 356.2, - "z": 16.26 - } - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "waitForDuration", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "2020916ffbf2b5ea23427b3e0becd1d0", - "notes": [], - "params": { - "seconds": 1.0 - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "blowout", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "367507eeb74543f7aaf46d7edda50dd1", - "notes": [], - "params": { - "flowRate": 57.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": 0.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": -5.624999999999998, - "y": 356.2, - "z": 16.26 - } - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "touchTip", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "5a8cd45b5ca7727e55cb46681326b39e", - "notes": [], - "params": { - "labwareId": "UUID", - "pipetteId": "UUID", - "radius": 1.0, - "speed": 60.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -1.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": -5.624999999999998, - "y": 356.2, - "z": 15.260000000000002 - } - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "dropTip", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "5f25a3b0bd5eb88b36b555b4bb9ca0d5", - "notes": [], - "params": { - "alternateDropLocation": true, - "labwareId": "UUID", - "pipetteId": "UUID", - "wellLocation": { - "offset": { - "x": 0, - "y": 0, - "z": 0 - }, - "origin": "default" - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 412.25, - "y": 364.0, - "z": 40.0 - } - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "thermocycler/closeLid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "8c668c6ac45ac4c3c712ac78fe8974c4", - "notes": [], - "params": { - "moduleId": "UUID" - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "thermocycler/runProfile", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "19578f9b13c86b6cff60ae975d2d35c9", - "notes": [], - "params": { - "blockMaxVolumeUl": 15.0, - "moduleId": "UUID", - "profile": [ - { - "celsius": 95.0, - "holdSeconds": 120.0 - } - ] - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "thermocycler/setTargetBlockTemperature", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "60f6af8e29ff815ea688504e3da56a0f", - "notes": [], - "params": { - "celsius": 95.0, - "holdTimeSeconds": 0.0, - "moduleId": "UUID" - }, - "result": { - "targetBlockTemperature": 95.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "thermocycler/waitForBlockTemperature", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "7f3dfa13f21157d4c962c9e8764dd2c0", - "notes": [], - "params": { - "moduleId": "UUID" - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "waitForDuration", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "cf30e2a36a38713ab8f8b79bc22024d9", - "notes": [], - "params": { - "seconds": 10.0 - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "thermocycler/setTargetBlockTemperature", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "2791a45241c4974df9a0da7f6e512271", - "notes": [], - "params": { - "celsius": 94.0, - "holdTimeSeconds": 0.0, - "moduleId": "UUID" - }, - "result": { - "targetBlockTemperature": 94.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "thermocycler/waitForBlockTemperature", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "709dce4723e76f0601f01d77402c1e0c", - "notes": [], - "params": { - "moduleId": "UUID" - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "waitForDuration", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "d408d344171dddc8537fd97153ae3d18", - "notes": [], - "params": { - "seconds": 10.0 - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "thermocycler/setTargetBlockTemperature", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "3b9280d9bde865fb1f120ea09947ba66", - "notes": [], - "params": { - "celsius": 93.0, - "holdTimeSeconds": 0.0, - "moduleId": "UUID" - }, - "result": { - "targetBlockTemperature": 93.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "thermocycler/waitForBlockTemperature", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "4fdb2a6857f213443c61970e8715e8f3", - "notes": [], - "params": { - "moduleId": "UUID" - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "waitForDuration", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "c2afc2d7ef6f7750be6f39f23fa230cd", - "notes": [], - "params": { - "seconds": 10.0 - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "thermocycler/setTargetBlockTemperature", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "af324abb8812f6243ce4f60b561269bc", - "notes": [], - "params": { - "celsius": 92.0, - "holdTimeSeconds": 0.0, - "moduleId": "UUID" - }, - "result": { - "targetBlockTemperature": 92.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "thermocycler/waitForBlockTemperature", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "4750d94adf39183b246d31ae65c4bf52", - "notes": [], - "params": { - "moduleId": "UUID" - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "waitForDuration", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "2acf9066db8f856e48d442c2fd21ceef", - "notes": [], - "params": { - "seconds": 10.0 - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "thermocycler/setTargetBlockTemperature", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "f31a756630eff63d94ada46abc00afb5", - "notes": [], - "params": { - "celsius": 91.0, - "holdTimeSeconds": 0.0, - "moduleId": "UUID" - }, - "result": { - "targetBlockTemperature": 91.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "thermocycler/waitForBlockTemperature", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "bc0220bac8ea6f4f6e7365dc26e096d6", - "notes": [], - "params": { - "moduleId": "UUID" - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "waitForDuration", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "70dd2e8f92a0eb7c71a4327f9cf14331", - "notes": [], - "params": { - "seconds": 10.0 - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "thermocycler/setTargetBlockTemperature", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "a643f56b9157be084f5a664b564ce763", - "notes": [], - "params": { - "celsius": 90.0, - "holdTimeSeconds": 0.0, - "moduleId": "UUID" - }, - "result": { - "targetBlockTemperature": 90.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "thermocycler/waitForBlockTemperature", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "55729ac300dbb44fb5562e4a4fe52fa9", - "notes": [], - "params": { - "moduleId": "UUID" - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "waitForDuration", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "6fb71ad687b24fafd5c7375912be7aff", - "notes": [], - "params": { - "seconds": 10.0 - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "thermocycler/setTargetBlockTemperature", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "9a613c7df1dcf551715f4770504606d6", - "notes": [], - "params": { - "celsius": 89.0, - "holdTimeSeconds": 0.0, - "moduleId": "UUID" - }, - "result": { - "targetBlockTemperature": 89.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "thermocycler/waitForBlockTemperature", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "956fba3d0d41f707f022af95e8ea5b10", - "notes": [], - "params": { - "moduleId": "UUID" - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "waitForDuration", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "87032579d8ef9f40e333340126277ddc", - "notes": [], - "params": { - "seconds": 10.0 - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "thermocycler/setTargetBlockTemperature", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "48ec80ea228a99b4b087753fa2cf9cd2", - "notes": [], - "params": { - "celsius": 88.0, - "holdTimeSeconds": 0.0, - "moduleId": "UUID" - }, - "result": { - "targetBlockTemperature": 88.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "thermocycler/waitForBlockTemperature", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "a3d85e4e2bc7a287cf4b1fd04f3ad0e4", - "notes": [], - "params": { - "moduleId": "UUID" - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "waitForDuration", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "d919231fdfc16990e8288543729e68df", - "notes": [], - "params": { - "seconds": 10.0 - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "thermocycler/setTargetBlockTemperature", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "d5fce482cce5eb32f39e2e6e3612193b", - "notes": [], - "params": { - "celsius": 87.0, - "holdTimeSeconds": 0.0, - "moduleId": "UUID" - }, - "result": { - "targetBlockTemperature": 87.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "thermocycler/waitForBlockTemperature", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "f065fc7369167d7e8e2052b1d843d1c2", - "notes": [], - "params": { - "moduleId": "UUID" - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "waitForDuration", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "bacadfeea17404f3e979abd903a22f7a", - "notes": [], - "params": { - "seconds": 10.0 - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "thermocycler/setTargetBlockTemperature", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "2219cc1e8741ba80306cc6959eb4ef80", - "notes": [], - "params": { - "celsius": 86.0, - "holdTimeSeconds": 0.0, - "moduleId": "UUID" - }, - "result": { - "targetBlockTemperature": 86.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "thermocycler/waitForBlockTemperature", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "e7ec1b11ffd4865a59326a2557d7f0bb", - "notes": [], - "params": { - "moduleId": "UUID" - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "waitForDuration", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "2013780df01a388f678df3e0e1ae266b", - "notes": [], - "params": { - "seconds": 10.0 - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "thermocycler/setTargetBlockTemperature", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "7209d0484d7bfa189d1732a7bbbc3a59", - "notes": [], - "params": { - "celsius": 85.0, - "holdTimeSeconds": 0.0, - "moduleId": "UUID" - }, - "result": { - "targetBlockTemperature": 85.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "thermocycler/waitForBlockTemperature", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "20d2514acdab3e3448618809c358ae7a", - "notes": [], - "params": { - "moduleId": "UUID" - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "waitForDuration", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "ae4feb9d69016017a5b909c5643ce34a", - "notes": [], - "params": { - "seconds": 10.0 - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "thermocycler/setTargetBlockTemperature", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "885f0a614feac857c796bd6c25671d77", - "notes": [], - "params": { - "celsius": 84.0, - "holdTimeSeconds": 0.0, - "moduleId": "UUID" - }, - "result": { - "targetBlockTemperature": 84.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "thermocycler/waitForBlockTemperature", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "8ca2e0d8f75dad16c1d6fdc572333619", - "notes": [], - "params": { - "moduleId": "UUID" - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "waitForDuration", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "47b118048e54aef9c5cdf7b6b5c2d74b", - "notes": [], - "params": { - "seconds": 10.0 - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "thermocycler/setTargetBlockTemperature", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "18dbb0a1a3cc2a907f7b387d6697bf90", - "notes": [], - "params": { - "celsius": 83.0, - "holdTimeSeconds": 0.0, - "moduleId": "UUID" - }, - "result": { - "targetBlockTemperature": 83.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "thermocycler/waitForBlockTemperature", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "766d87e7fe8f4eaa1cff13ab7b37b364", - "notes": [], - "params": { - "moduleId": "UUID" - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "waitForDuration", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "08e08d2948fdf31653040e6221e6b24c", - "notes": [], - "params": { - "seconds": 10.0 - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "thermocycler/setTargetBlockTemperature", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "91cca07b39ae72da34b8a6dbe7a4483c", - "notes": [], - "params": { - "celsius": 82.0, - "holdTimeSeconds": 0.0, - "moduleId": "UUID" - }, - "result": { - "targetBlockTemperature": 82.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "thermocycler/waitForBlockTemperature", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "7d662560e11e84d650900d67098731b9", - "notes": [], - "params": { - "moduleId": "UUID" - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "waitForDuration", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "9bbdfb63c0ce8de6eb75f26e9d39f9e3", - "notes": [], - "params": { - "seconds": 10.0 - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "thermocycler/setTargetBlockTemperature", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "629fa6ae8b71abab25bbe66f9f393308", - "notes": [], - "params": { - "celsius": 81.0, - "holdTimeSeconds": 0.0, - "moduleId": "UUID" - }, - "result": { - "targetBlockTemperature": 81.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "thermocycler/waitForBlockTemperature", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "99322871b0b7c6f13091d0df69b5f937", - "notes": [], - "params": { - "moduleId": "UUID" - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "waitForDuration", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "0e3572550a02b0c1337971ff7798330e", - "notes": [], - "params": { - "seconds": 10.0 - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "thermocycler/setTargetBlockTemperature", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "732b29e7066c4f0600aab01d2d99c1f0", - "notes": [], - "params": { - "celsius": 80.0, - "holdTimeSeconds": 0.0, - "moduleId": "UUID" - }, - "result": { - "targetBlockTemperature": 80.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "thermocycler/waitForBlockTemperature", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "c60e07b6aed1e9c1642a0cd211384b96", - "notes": [], - "params": { - "moduleId": "UUID" - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "waitForDuration", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "35df839e4614f877bedec488e21f1d20", - "notes": [], - "params": { - "seconds": 10.0 - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "thermocycler/setTargetBlockTemperature", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "5f98a478c5a31272f942cf085b9193e0", - "notes": [], - "params": { - "celsius": 79.0, - "holdTimeSeconds": 0.0, - "moduleId": "UUID" - }, - "result": { - "targetBlockTemperature": 79.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "thermocycler/waitForBlockTemperature", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "d9b3ede674c7069718215c83ce60dd28", - "notes": [], - "params": { - "moduleId": "UUID" - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "waitForDuration", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "5287c6592db2bec6f737849c5e0dec53", - "notes": [], - "params": { - "seconds": 10.0 - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "thermocycler/setTargetBlockTemperature", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "5abef8b61964fccdf8486fbd9ee83f1f", - "notes": [], - "params": { - "celsius": 78.0, - "holdTimeSeconds": 0.0, - "moduleId": "UUID" - }, - "result": { - "targetBlockTemperature": 78.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "thermocycler/waitForBlockTemperature", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "4e3c62782ec783f1c57dbd98a8796429", - "notes": [], - "params": { - "moduleId": "UUID" - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "waitForDuration", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "b5da778f8177c0f33003547e07ac1590", - "notes": [], - "params": { - "seconds": 10.0 - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "thermocycler/setTargetBlockTemperature", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "2172d3afb18d1ae61e017c32495eee0e", - "notes": [], - "params": { - "celsius": 77.0, - "holdTimeSeconds": 0.0, - "moduleId": "UUID" - }, - "result": { - "targetBlockTemperature": 77.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "thermocycler/waitForBlockTemperature", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "06484f7d18a9b6445458cd2ad2f6f507", - "notes": [], - "params": { - "moduleId": "UUID" - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "waitForDuration", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "011eb9735c3f22751b5286eda84e986a", - "notes": [], - "params": { - "seconds": 10.0 - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "thermocycler/setTargetBlockTemperature", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "c97542e9721ff61656d06228cdab2ddf", - "notes": [], - "params": { - "celsius": 76.0, - "holdTimeSeconds": 0.0, - "moduleId": "UUID" - }, - "result": { - "targetBlockTemperature": 76.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "thermocycler/waitForBlockTemperature", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "2907edf068ca4a350457ad05b9fcea66", - "notes": [], - "params": { - "moduleId": "UUID" - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "waitForDuration", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "38cde5b0b2b6be57808d0505452776f9", - "notes": [], - "params": { - "seconds": 10.0 - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "thermocycler/setTargetBlockTemperature", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "7e260670d38ee78f3bf77dbca2222256", - "notes": [], - "params": { - "celsius": 75.0, - "holdTimeSeconds": 0.0, - "moduleId": "UUID" - }, - "result": { - "targetBlockTemperature": 75.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "thermocycler/waitForBlockTemperature", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "d3440f65cbee4d12fd1c819b29a5a944", - "notes": [], - "params": { - "moduleId": "UUID" - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "waitForDuration", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "62453517166a7f2c67b88512af37d922", - "notes": [], - "params": { - "seconds": 10.0 - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "thermocycler/setTargetBlockTemperature", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "071775894f0f17b1cd0da7f9bec77426", - "notes": [], - "params": { - "celsius": 74.0, - "holdTimeSeconds": 0.0, - "moduleId": "UUID" - }, - "result": { - "targetBlockTemperature": 74.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "thermocycler/waitForBlockTemperature", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "84f1fed8949a865c2f8637b308b4d9e1", - "notes": [], - "params": { - "moduleId": "UUID" - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "waitForDuration", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "12b9cafebf04032b6172e122bb9fe0f3", - "notes": [], - "params": { - "seconds": 10.0 - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "thermocycler/setTargetBlockTemperature", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "7f876f981c311529287bda518287cda8", - "notes": [], - "params": { - "celsius": 73.0, - "holdTimeSeconds": 0.0, - "moduleId": "UUID" - }, - "result": { - "targetBlockTemperature": 73.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "thermocycler/waitForBlockTemperature", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "8b7f9d4867e506d964a5d5d0e4308da7", - "notes": [], - "params": { - "moduleId": "UUID" - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "waitForDuration", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "7e94578f552f97db94b4c8b010ff31e5", - "notes": [], - "params": { - "seconds": 10.0 - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "thermocycler/setTargetBlockTemperature", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "5fdae4f3bb9d2f5a48957eb26f720a2e", - "notes": [], - "params": { - "celsius": 72.0, - "holdTimeSeconds": 0.0, - "moduleId": "UUID" - }, - "result": { - "targetBlockTemperature": 72.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "thermocycler/waitForBlockTemperature", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "f45c9fcb71e0f54d5b93d33ae8ead783", - "notes": [], - "params": { - "moduleId": "UUID" - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "waitForDuration", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "3f31cb260adc9c9560280a4bfb01c414", - "notes": [], - "params": { - "seconds": 10.0 - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "thermocycler/setTargetBlockTemperature", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "f7b42a96363bb86230ca6f85799f5a39", - "notes": [], - "params": { - "celsius": 71.0, - "holdTimeSeconds": 0.0, - "moduleId": "UUID" - }, - "result": { - "targetBlockTemperature": 71.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "thermocycler/waitForBlockTemperature", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "b8bc1d21e31bf72fa658f9bee0506704", - "notes": [], - "params": { - "moduleId": "UUID" - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "waitForDuration", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "cfda1c125e8fc2b993bc6577e7c79302", - "notes": [], - "params": { - "seconds": 10.0 - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "thermocycler/setTargetBlockTemperature", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "51a35c65b72f8facc402983cee134cf1", - "notes": [], - "params": { - "celsius": 70.0, - "holdTimeSeconds": 0.0, - "moduleId": "UUID" - }, - "result": { - "targetBlockTemperature": 70.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "thermocycler/waitForBlockTemperature", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "2ba43999b435e4e381c66ad10f0b6849", - "notes": [], - "params": { - "moduleId": "UUID" - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "waitForDuration", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "1fe5265fa866a637822ecac83c532662", - "notes": [], - "params": { - "seconds": 10.0 - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "thermocycler/setTargetBlockTemperature", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "7f18d9073f427de54acb37b4e35434e9", - "notes": [], - "params": { - "celsius": 69.0, - "holdTimeSeconds": 0.0, - "moduleId": "UUID" - }, - "result": { - "targetBlockTemperature": 69.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "thermocycler/waitForBlockTemperature", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "831ee6cfe31051b44a94fb3d2c6d441a", - "notes": [], - "params": { - "moduleId": "UUID" - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "waitForDuration", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "2c56d24c082ef09de8874b653f2d6c31", - "notes": [], - "params": { - "seconds": 10.0 - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "thermocycler/setTargetBlockTemperature", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "0b7f04a03a08371286139a202f2712a3", - "notes": [], - "params": { - "celsius": 68.0, - "holdTimeSeconds": 0.0, - "moduleId": "UUID" - }, - "result": { - "targetBlockTemperature": 68.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "thermocycler/waitForBlockTemperature", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "161c4347857dd8598ce3dded3acdfa3c", - "notes": [], - "params": { - "moduleId": "UUID" - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "waitForDuration", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "af099f13af8744d459dd30809edebab8", - "notes": [], - "params": { - "seconds": 10.0 - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "thermocycler/setTargetBlockTemperature", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "31c5cd8831b441e14b342dc0318ffe9f", - "notes": [], - "params": { - "celsius": 67.0, - "holdTimeSeconds": 0.0, - "moduleId": "UUID" - }, - "result": { - "targetBlockTemperature": 67.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "thermocycler/waitForBlockTemperature", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "1a064b599037ddf4e4e7ea30c28b0aa1", - "notes": [], - "params": { - "moduleId": "UUID" - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "waitForDuration", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "5de020e8575d5face143bcce12a9db58", - "notes": [], - "params": { - "seconds": 10.0 - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "thermocycler/setTargetBlockTemperature", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "ae11b2fcf8ff20cc4c8b27805bc3764a", - "notes": [], - "params": { - "celsius": 66.0, - "holdTimeSeconds": 0.0, - "moduleId": "UUID" - }, - "result": { - "targetBlockTemperature": 66.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "thermocycler/waitForBlockTemperature", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "cebd72effd7bf193c520cac7e5925291", - "notes": [], - "params": { - "moduleId": "UUID" - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "waitForDuration", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "32fc1942a3c2eac289ababaf76b4d9a5", - "notes": [], - "params": { - "seconds": 10.0 - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "thermocycler/setTargetBlockTemperature", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "9619f00c47abba041ad3f9746b80836d", - "notes": [], - "params": { - "celsius": 65.0, - "holdTimeSeconds": 0.0, - "moduleId": "UUID" - }, - "result": { - "targetBlockTemperature": 65.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "thermocycler/waitForBlockTemperature", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "175f79a2dbc8fd5bd61e87d988a74239", - "notes": [], - "params": { - "moduleId": "UUID" - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "waitForDuration", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "e05d8703343f5f263d7ea70dc1967221", - "notes": [], - "params": { - "seconds": 10.0 - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "thermocycler/setTargetBlockTemperature", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "3ba5b9a01f55e59c95711d78c47a9f36", - "notes": [], - "params": { - "celsius": 64.0, - "holdTimeSeconds": 0.0, - "moduleId": "UUID" - }, - "result": { - "targetBlockTemperature": 64.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "thermocycler/waitForBlockTemperature", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "d5594dd1033606855033ef1ad41542f6", - "notes": [], - "params": { - "moduleId": "UUID" - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "waitForDuration", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "089a2feab8db04a58ef33c9066918f17", - "notes": [], - "params": { - "seconds": 10.0 - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "thermocycler/setTargetBlockTemperature", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "75d9121a8719b566ba0f0e5d9ffcdc74", - "notes": [], - "params": { - "celsius": 63.0, - "holdTimeSeconds": 0.0, - "moduleId": "UUID" - }, - "result": { - "targetBlockTemperature": 63.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "thermocycler/waitForBlockTemperature", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "d73623f356eb89cfe19ecd18d90cd985", - "notes": [], - "params": { - "moduleId": "UUID" - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "waitForDuration", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "d636b95a32388ae938c8a332243fb5da", - "notes": [], - "params": { - "seconds": 10.0 - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "thermocycler/setTargetBlockTemperature", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "1dcf255b9b80e01fdbdaeb37d32564de", - "notes": [], - "params": { - "celsius": 62.0, - "holdTimeSeconds": 0.0, - "moduleId": "UUID" - }, - "result": { - "targetBlockTemperature": 62.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "thermocycler/waitForBlockTemperature", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "5deb8c94c4f1b96cfc805108b40eedca", - "notes": [], - "params": { - "moduleId": "UUID" - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "waitForDuration", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "e5feb25ff7197995390beb67cca092b0", - "notes": [], - "params": { - "seconds": 10.0 - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "thermocycler/setTargetBlockTemperature", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "548caab309c6e891029022781b4f7c72", - "notes": [], - "params": { - "celsius": 61.0, - "holdTimeSeconds": 0.0, - "moduleId": "UUID" - }, - "result": { - "targetBlockTemperature": 61.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "thermocycler/waitForBlockTemperature", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "b051a6dd705c10c0b5171576fb461135", - "notes": [], - "params": { - "moduleId": "UUID" - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "waitForDuration", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "7df7995ccb04711873f7c88ce4f3b22d", - "notes": [], - "params": { - "seconds": 10.0 - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "thermocycler/setTargetBlockTemperature", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "626a331d3e0a18c6b40f4a2a9470eece", - "notes": [], - "params": { - "celsius": 60.0, - "holdTimeSeconds": 0.0, - "moduleId": "UUID" - }, - "result": { - "targetBlockTemperature": 60.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "thermocycler/waitForBlockTemperature", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "0a15ba28b7f16a96829f07c52f3ccd5e", - "notes": [], - "params": { - "moduleId": "UUID" - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "waitForDuration", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "03a6b5ea04e006f00320dbb47e31dc3e", - "notes": [], - "params": { - "seconds": 10.0 - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "thermocycler/setTargetBlockTemperature", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "c660e95ac85702c6c7d5e5ac8ebf8094", - "notes": [], - "params": { - "celsius": 59.0, - "holdTimeSeconds": 0.0, - "moduleId": "UUID" - }, - "result": { - "targetBlockTemperature": 59.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "thermocycler/waitForBlockTemperature", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "50ccd309e74a683fa9423c800e81a336", - "notes": [], - "params": { - "moduleId": "UUID" - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "waitForDuration", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "5cb07049db96dd11130ded42d143ce50", - "notes": [], - "params": { - "seconds": 10.0 - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "thermocycler/setTargetBlockTemperature", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "206876e176f7bab12078fe5f8ba5bf99", - "notes": [], - "params": { - "celsius": 58.0, - "holdTimeSeconds": 0.0, - "moduleId": "UUID" - }, - "result": { - "targetBlockTemperature": 58.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "thermocycler/waitForBlockTemperature", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "84e968aaca6cc5104f379b6e107d1f70", - "notes": [], - "params": { - "moduleId": "UUID" - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "waitForDuration", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "31fde73939cca3e88201d07cdabca09b", - "notes": [], - "params": { - "seconds": 10.0 - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "thermocycler/setTargetBlockTemperature", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "365d1e4f535103137b1a83ae21285135", - "notes": [], - "params": { - "celsius": 57.0, - "holdTimeSeconds": 0.0, - "moduleId": "UUID" - }, - "result": { - "targetBlockTemperature": 57.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "thermocycler/waitForBlockTemperature", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "0d45a7384b5bcc64aec5fea945fbcc05", - "notes": [], - "params": { - "moduleId": "UUID" - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "waitForDuration", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "33372e812c8e8be65ad1f5704aa7af28", - "notes": [], - "params": { - "seconds": 10.0 - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "thermocycler/setTargetBlockTemperature", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "a82d40b62d22d6db9826410daf9cfdf0", - "notes": [], - "params": { - "celsius": 56.0, - "holdTimeSeconds": 0.0, - "moduleId": "UUID" - }, - "result": { - "targetBlockTemperature": 56.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "thermocycler/waitForBlockTemperature", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "a7257d111ac2c8a1aaec1f29e3c067cb", - "notes": [], - "params": { - "moduleId": "UUID" - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "waitForDuration", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "cea7b98870afd782e0bc58ef2b906000", - "notes": [], - "params": { - "seconds": 10.0 - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "thermocycler/setTargetBlockTemperature", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "b1029a72e746a925225221dd43d60a6b", - "notes": [], - "params": { - "celsius": 55.0, - "holdTimeSeconds": 0.0, - "moduleId": "UUID" - }, - "result": { - "targetBlockTemperature": 55.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "thermocycler/waitForBlockTemperature", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "449c44fb4436c9d1ad65957a33c7b9de", - "notes": [], - "params": { - "moduleId": "UUID" - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "waitForDuration", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "0b796c9b826b201f121c71d768c71b05", - "notes": [], - "params": { - "seconds": 10.0 - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "thermocycler/setTargetBlockTemperature", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "c385ccb14390aba280ebcffd846da996", - "notes": [], - "params": { - "celsius": 54.0, - "holdTimeSeconds": 0.0, - "moduleId": "UUID" - }, - "result": { - "targetBlockTemperature": 54.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "thermocycler/waitForBlockTemperature", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "260cd32bf3faa4a107a5d67b82ea947a", - "notes": [], - "params": { - "moduleId": "UUID" - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "waitForDuration", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "84b6c721649b947be32a2ecb31238de2", - "notes": [], - "params": { - "seconds": 10.0 - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "thermocycler/setTargetBlockTemperature", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "450ef1c606121c85f8fd0b546bff883f", - "notes": [], - "params": { - "celsius": 53.0, - "holdTimeSeconds": 0.0, - "moduleId": "UUID" - }, - "result": { - "targetBlockTemperature": 53.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "thermocycler/waitForBlockTemperature", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "76eac448d9f0b9052c76d612b4e0f58f", - "notes": [], - "params": { - "moduleId": "UUID" - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "waitForDuration", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "8a9f3dac77b1e0c2cb093b0ca8ce64bc", - "notes": [], - "params": { - "seconds": 10.0 - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "thermocycler/setTargetBlockTemperature", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "228773bdb653246dc79389d67ccddea7", - "notes": [], - "params": { - "celsius": 52.0, - "holdTimeSeconds": 0.0, - "moduleId": "UUID" - }, - "result": { - "targetBlockTemperature": 52.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "thermocycler/waitForBlockTemperature", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "8ffa3f17d4d4cf4931bb465b2ab1a655", - "notes": [], - "params": { - "moduleId": "UUID" - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "waitForDuration", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "30692ac27903508d3e3433863079b731", - "notes": [], - "params": { - "seconds": 10.0 - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "thermocycler/setTargetBlockTemperature", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "dc1cdca16de05f0011ff5351962af844", - "notes": [], - "params": { - "celsius": 51.0, - "holdTimeSeconds": 0.0, - "moduleId": "UUID" - }, - "result": { - "targetBlockTemperature": 51.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "thermocycler/waitForBlockTemperature", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "f78d14c896c5238880944e3d2745bb61", - "notes": [], - "params": { - "moduleId": "UUID" - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "waitForDuration", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "424273bdfd37282cd7225b949612ea5c", - "notes": [], - "params": { - "seconds": 10.0 - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "thermocycler/setTargetBlockTemperature", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "730e8c948918c889e9f66b4dc3cff123", - "notes": [], - "params": { - "celsius": 50.0, - "holdTimeSeconds": 0.0, - "moduleId": "UUID" - }, - "result": { - "targetBlockTemperature": 50.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "thermocycler/waitForBlockTemperature", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "e6b7ecbc818b48c811b7dd10382006ec", - "notes": [], - "params": { - "moduleId": "UUID" - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "waitForDuration", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "4414bdaf007dc3ddc5d4235a713d50e8", - "notes": [], - "params": { - "seconds": 10.0 - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "thermocycler/setTargetBlockTemperature", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "27e30028dbd12744f9b6e78f8a7df7a3", - "notes": [], - "params": { - "celsius": 49.0, - "holdTimeSeconds": 0.0, - "moduleId": "UUID" - }, - "result": { - "targetBlockTemperature": 49.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "thermocycler/waitForBlockTemperature", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "71139901ce04945ce2b73ba849e88520", - "notes": [], - "params": { - "moduleId": "UUID" - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "waitForDuration", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "8431c3dea998a92b69fb1f3cc2767753", - "notes": [], - "params": { - "seconds": 10.0 - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "thermocycler/setTargetBlockTemperature", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "5bec11762ff91cdded71fa33ea46b65a", - "notes": [], - "params": { - "celsius": 48.0, - "holdTimeSeconds": 0.0, - "moduleId": "UUID" - }, - "result": { - "targetBlockTemperature": 48.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "thermocycler/waitForBlockTemperature", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "a17cd184838f78893d476e0186ba4029", - "notes": [], - "params": { - "moduleId": "UUID" - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "waitForDuration", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "2b082656959e967bc5c58cf0243ce1d9", - "notes": [], - "params": { - "seconds": 10.0 - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "thermocycler/setTargetBlockTemperature", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "4b2941da4add362fb9eb8eb17c942d25", - "notes": [], - "params": { - "celsius": 47.0, - "holdTimeSeconds": 0.0, - "moduleId": "UUID" - }, - "result": { - "targetBlockTemperature": 47.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "thermocycler/waitForBlockTemperature", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "338175f6bcbb9df6b2afbca596d62f8d", - "notes": [], - "params": { - "moduleId": "UUID" - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "waitForDuration", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "770b726c2a1101d3f84ff9be0f86a273", - "notes": [], - "params": { - "seconds": 10.0 - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "thermocycler/setTargetBlockTemperature", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "c9d6d2a0cecd8f1fafd7de19920e634b", - "notes": [], - "params": { - "celsius": 46.0, - "holdTimeSeconds": 0.0, - "moduleId": "UUID" - }, - "result": { - "targetBlockTemperature": 46.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "thermocycler/waitForBlockTemperature", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "25224b56b31f21d331bc7fea57f06eff", - "notes": [], - "params": { - "moduleId": "UUID" - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "waitForDuration", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "61fca62767e1fbc35a19e50fe5215f56", - "notes": [], - "params": { - "seconds": 10.0 - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "thermocycler/setTargetBlockTemperature", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "e25db724d46f78376141eff3174266ac", - "notes": [], - "params": { - "celsius": 45.0, - "holdTimeSeconds": 0.0, - "moduleId": "UUID" - }, - "result": { - "targetBlockTemperature": 45.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "thermocycler/waitForBlockTemperature", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "cdc239bf1cceae10be83ba50b4eb32e8", - "notes": [], - "params": { - "moduleId": "UUID" - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "waitForDuration", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "9f340b97951e0170b7c9b12addd11353", - "notes": [], - "params": { - "seconds": 10.0 - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "thermocycler/setTargetBlockTemperature", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "a194b47da99405c0b99b81cb8ac1a90f", - "notes": [], - "params": { - "celsius": 44.0, - "holdTimeSeconds": 0.0, - "moduleId": "UUID" - }, - "result": { - "targetBlockTemperature": 44.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "thermocycler/waitForBlockTemperature", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "aabdde51705e8fa92e5b2483c082510a", - "notes": [], - "params": { - "moduleId": "UUID" - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "waitForDuration", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "0d8b069bfa036f96833adb4b4fd4e41d", - "notes": [], - "params": { - "seconds": 10.0 - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "thermocycler/setTargetBlockTemperature", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "ca8d6a3d407c7bbc74530c5ac1a7b8a1", - "notes": [], - "params": { - "celsius": 43.0, - "holdTimeSeconds": 0.0, - "moduleId": "UUID" - }, - "result": { - "targetBlockTemperature": 43.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "thermocycler/waitForBlockTemperature", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "2e4d0e45c3eab9e8aeda4508ce715edc", - "notes": [], - "params": { - "moduleId": "UUID" - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "waitForDuration", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "3e6f2d8e3a169b73e8fee74dbd3544f1", - "notes": [], - "params": { - "seconds": 10.0 - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "thermocycler/setTargetBlockTemperature", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "41a6dd957b991a602b58dd0565687a8f", - "notes": [], - "params": { - "celsius": 42.0, - "holdTimeSeconds": 0.0, - "moduleId": "UUID" - }, - "result": { - "targetBlockTemperature": 42.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "thermocycler/waitForBlockTemperature", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "5dbb3aa2977749901bf83f56a47269af", - "notes": [], - "params": { - "moduleId": "UUID" - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "waitForDuration", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "27e410b0b7b9d49face7717b6868f7dd", - "notes": [], - "params": { - "seconds": 10.0 - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "thermocycler/setTargetBlockTemperature", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "4e04eba549e4a0fd1eac894e9aa2481d", - "notes": [], - "params": { - "celsius": 41.0, - "holdTimeSeconds": 0.0, - "moduleId": "UUID" - }, - "result": { - "targetBlockTemperature": 41.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "thermocycler/waitForBlockTemperature", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "f29e5c61e2a5a78a573d1d7034522573", - "notes": [], - "params": { - "moduleId": "UUID" - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "waitForDuration", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "6b07572218f8fecff3825fac91e1eaf5", - "notes": [], - "params": { - "seconds": 10.0 - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "thermocycler/setTargetBlockTemperature", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "c595de43769e39fe0e8c379642befc46", - "notes": [], - "params": { - "celsius": 40.0, - "holdTimeSeconds": 0.0, - "moduleId": "UUID" - }, - "result": { - "targetBlockTemperature": 40.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "thermocycler/waitForBlockTemperature", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "2a530d0e062a7a71d6a219738d801fb2", - "notes": [], - "params": { - "moduleId": "UUID" - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "waitForDuration", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "ca807ab76984b49bdd7a4aefafcfb6b3", - "notes": [], - "params": { - "seconds": 10.0 - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "thermocycler/setTargetBlockTemperature", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "9f13904fee725ea59eda60ccbb02c6ca", - "notes": [], - "params": { - "celsius": 39.0, - "holdTimeSeconds": 0.0, - "moduleId": "UUID" - }, - "result": { - "targetBlockTemperature": 39.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "thermocycler/waitForBlockTemperature", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "6b8b49d9c6502d5a84727f58e2d030ca", - "notes": [], - "params": { - "moduleId": "UUID" - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "waitForDuration", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "e94bdb33e2e01e20bf87d4715804876d", - "notes": [], - "params": { - "seconds": 10.0 - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "thermocycler/setTargetBlockTemperature", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "e47f9391e6ee5ac78a7c679316246b71", - "notes": [], - "params": { - "celsius": 38.0, - "holdTimeSeconds": 0.0, - "moduleId": "UUID" - }, - "result": { - "targetBlockTemperature": 38.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "thermocycler/waitForBlockTemperature", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "dcb4796feeb12b68fa485a69b2016da7", - "notes": [], - "params": { - "moduleId": "UUID" - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "waitForDuration", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "7de14b27f54084e4c5eb36098b04c04c", - "notes": [], - "params": { - "seconds": 10.0 - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "thermocycler/openLid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "6a5013ed2f97714d3cbfd897c559e6fd", - "notes": [], - "params": { - "moduleId": "UUID" - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "comment", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "88f890408b39bf040605a23d06f7c983", - "notes": [], - "params": { - "message": "\n\n----------ADDING RDB----------\n" - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "pickUpTip", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "00677224c457c69ca08840b54601fa0f", - "notes": [], - "params": { - "labwareId": "UUID", - "pipetteId": "UUID", - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": 0.0 - }, - "origin": "top" - }, - "wellName": "A2" - }, - "result": { - "position": { - "x": 23.38, - "y": 181.38, - "z": 99.0 - }, - "tipDiameter": 5.58, - "tipLength": 47.849999999999994, - "tipVolume": 50.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "configureForVolume", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "830e6f207fd56e9294fba38734d9df00", - "notes": [], - "params": { - "pipetteId": "UUID", - "tipOverlapNotAfterVersion": "v0", - "volume": 5.0 - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "aspirate", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "32df21b33f21b23b7250e4b145e9e4cd", - "notes": [], - "params": { - "flowRate": 35.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 5.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -37.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A2" - }, - "result": { - "position": { - "x": 351.3, - "y": 74.15, - "z": 13.0 - }, - "volume": 5.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "dispense", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "ed91e0a804eeaf60876f2bacf6365ee4", - "notes": [], - "params": { - "flowRate": 57.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 5.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -13.95 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": -5.624999999999998, - "y": 356.2, - "z": 2.3100000000000014 - }, - "volume": 5.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "aspirate", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "ede7c09ba888bf4583780b011f910519", - "notes": [], - "params": { - "flowRate": 35.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 15.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -13.95 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": -5.624999999999998, - "y": 356.2, - "z": 2.3100000000000014 - }, - "volume": 15.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "dispense", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "69b2b49701561ded20dd7708b73f05cd", - "notes": [], - "params": { - "flowRate": 57.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 15.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -13.95 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": -5.624999999999998, - "y": 356.2, - "z": 2.3100000000000014 - }, - "volume": 15.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "aspirate", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "7361d69000d89863baebe5a648b5c0ef", - "notes": [], - "params": { - "flowRate": 35.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 15.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -13.95 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": -5.624999999999998, - "y": 356.2, - "z": 2.3100000000000014 - }, - "volume": 15.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "dispense", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "796026df1d808f79dd281efb7d87305e", - "notes": [], - "params": { - "flowRate": 57.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 15.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -13.95 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": -5.624999999999998, - "y": 356.2, - "z": 2.3100000000000014 - }, - "volume": 15.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "aspirate", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "0c69dbf3f575d6810b555c6da10136ee", - "notes": [], - "params": { - "flowRate": 35.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 15.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -13.95 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": -5.624999999999998, - "y": 356.2, - "z": 2.3100000000000014 - }, - "volume": 15.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "dispense", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "d720aded9efb2b95abddc2cbfa5a0a22", - "notes": [], - "params": { - "flowRate": 57.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 15.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -13.95 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": -5.624999999999998, - "y": 356.2, - "z": 2.3100000000000014 - }, - "volume": 15.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "aspirate", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "cc4c04a66bdb066b5722e47ddf845100", - "notes": [], - "params": { - "flowRate": 35.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 15.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -13.95 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": -5.624999999999998, - "y": 356.2, - "z": 2.3100000000000014 - }, - "volume": 15.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "dispense", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "79ff88e8014131935040cab6ea3382c1", - "notes": [], - "params": { - "flowRate": 57.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 15.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -13.95 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": -5.624999999999998, - "y": 356.2, - "z": 2.3100000000000014 - }, - "volume": 15.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "aspirate", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "e011fef8002fb189f1a676f45099df69", - "notes": [], - "params": { - "flowRate": 35.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 15.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -13.95 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": -5.624999999999998, - "y": 356.2, - "z": 2.3100000000000014 - }, - "volume": 15.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "dispense", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "b4778b46da0d23b3b96ffafec1a0d265", - "notes": [], - "params": { - "flowRate": 57.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 15.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -13.95 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": -5.624999999999998, - "y": 356.2, - "z": 2.3100000000000014 - }, - "volume": 15.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "moveToWell", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "75e0f72bae3c26d8516056fb2dfa748c", - "notes": [], - "params": { - "forceDirect": false, - "labwareId": "UUID", - "pipetteId": "UUID", - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": 0.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": -5.624999999999998, - "y": 356.2, - "z": 16.26 - } - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "waitForDuration", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "aa7d6109f7ee2df4f13fa882503b6002", - "notes": [], - "params": { - "seconds": 1.0 - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "blowout", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "b8ab7b6b86fb6e8e07de27a93ee82eba", - "notes": [], - "params": { - "flowRate": 57.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": 0.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": -5.624999999999998, - "y": 356.2, - "z": 16.26 - } - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "touchTip", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "8ceffaa91c7327a61028b344b3190f2e", - "notes": [], - "params": { - "labwareId": "UUID", - "pipetteId": "UUID", - "radius": 1.0, - "speed": 60.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -1.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": -5.624999999999998, - "y": 356.2, - "z": 15.260000000000002 - } - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "dropTip", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "c772791d372fbc5e453ceecc022ce28d", - "notes": [], - "params": { - "alternateDropLocation": true, - "labwareId": "UUID", - "pipetteId": "UUID", - "wellLocation": { - "offset": { - "x": 0, - "y": 0, - "z": 0 - }, - "origin": "default" - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 359.25, - "y": 364.0, - "z": 40.0 - } - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "thermocycler/closeLid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "a121310b4027654d03d49fc3f61b0f16", - "notes": [], - "params": { - "moduleId": "UUID" - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "thermocycler/runProfile", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "c800fc18faaa550aa645015b51f5ed30", - "notes": [], - "params": { - "blockMaxVolumeUl": 20.0, - "moduleId": "UUID", - "profile": [ - { - "celsius": 37.0, - "holdSeconds": 900.0 - } - ] - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "thermocycler/setTargetBlockTemperature", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "6be8a1b8d682bea9507b96d9687e7d50", - "notes": [], - "params": { - "celsius": 4.0, - "holdTimeSeconds": 0.0, - "moduleId": "UUID" - }, - "result": { - "targetBlockTemperature": 4.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "thermocycler/waitForBlockTemperature", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "c0842bfe516e54d06bf216f39e1f7feb", - "notes": [], - "params": { - "moduleId": "UUID" - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "thermocycler/openLid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "5337f23583d7160c7bd3fb11be7ccdb7", - "notes": [], - "params": { - "moduleId": "UUID" - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "comment", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "45460103ce1c28279b31a9f9f0797a3f", - "notes": [], - "params": { - "message": "\n\n----------ADDING PRB----------\n" - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "pickUpTip", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "4ec57e303dd4a19dab032dd2a031224a", - "notes": [], - "params": { - "labwareId": "UUID", - "pipetteId": "UUID", - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": 0.0 - }, - "origin": "top" - }, - "wellName": "A3" - }, - "result": { - "position": { - "x": 32.38, - "y": 181.38, - "z": 99.0 - }, - "tipDiameter": 5.58, - "tipLength": 47.849999999999994, - "tipVolume": 50.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "aspirate", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "d240cb5e4c9aae5f378c25d2787a7dac", - "notes": [], - "params": { - "flowRate": 35.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 10.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -37.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A3" - }, - "result": { - "position": { - "x": 360.3, - "y": 74.15, - "z": 13.0 - }, - "volume": 10.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "dispense", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "8614b0539beeec0494fa8391960ddff7", - "notes": [], - "params": { - "flowRate": 57.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 10.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -13.95 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": -5.624999999999998, - "y": 356.2, - "z": 2.3100000000000014 - }, - "volume": 10.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "aspirate", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "afd5d367ed06b78b729d43f653b0d67d", - "notes": [], - "params": { - "flowRate": 35.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 20.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -13.95 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": -5.624999999999998, - "y": 356.2, - "z": 2.3100000000000014 - }, - "volume": 20.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "dispense", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "4248f46e4810d2b632eaa0b0faf9d7dc", - "notes": [], - "params": { - "flowRate": 57.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 20.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -13.95 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": -5.624999999999998, - "y": 356.2, - "z": 2.3100000000000014 - }, - "volume": 20.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "aspirate", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "8c430d6585cd69d8322b9e28f7be02e1", - "notes": [], - "params": { - "flowRate": 35.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 20.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -13.95 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": -5.624999999999998, - "y": 356.2, - "z": 2.3100000000000014 - }, - "volume": 20.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "dispense", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "58fd795fb77022bad31f9f85a2259829", - "notes": [], - "params": { - "flowRate": 57.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 20.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -13.95 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": -5.624999999999998, - "y": 356.2, - "z": 2.3100000000000014 - }, - "volume": 20.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "aspirate", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "14d00b88ebfa904eb28fdea883f7f20a", - "notes": [], - "params": { - "flowRate": 35.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 20.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -13.95 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": -5.624999999999998, - "y": 356.2, - "z": 2.3100000000000014 - }, - "volume": 20.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "dispense", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "abccec97db2fa96f086ce79c3dcd9526", - "notes": [], - "params": { - "flowRate": 57.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 20.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -13.95 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": -5.624999999999998, - "y": 356.2, - "z": 2.3100000000000014 - }, - "volume": 20.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "aspirate", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "ec8628678ddab77e94bd0db329c855d2", - "notes": [], - "params": { - "flowRate": 35.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 20.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -13.95 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": -5.624999999999998, - "y": 356.2, - "z": 2.3100000000000014 - }, - "volume": 20.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "dispense", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "1dbeb989c1418c1f1b41591f9256bd2c", - "notes": [], - "params": { - "flowRate": 57.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 20.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -13.95 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": -5.624999999999998, - "y": 356.2, - "z": 2.3100000000000014 - }, - "volume": 20.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "aspirate", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "61d6aef48803860526736dd654e18168", - "notes": [], - "params": { - "flowRate": 35.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 20.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -13.95 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": -5.624999999999998, - "y": 356.2, - "z": 2.3100000000000014 - }, - "volume": 20.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "dispense", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "b9383b900841cb7e1e8004ca09e8d98a", - "notes": [], - "params": { - "flowRate": 57.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 20.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -13.95 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": -5.624999999999998, - "y": 356.2, - "z": 2.3100000000000014 - }, - "volume": 20.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "moveToWell", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "feb3786ebd0c0d587e1ef83536b20d70", - "notes": [], - "params": { - "forceDirect": false, - "labwareId": "UUID", - "pipetteId": "UUID", - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": 0.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": -5.624999999999998, - "y": 356.2, - "z": 16.26 - } - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "waitForDuration", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "ab8317f307f5b5ae3c5dc1c2d51ad4ef", - "notes": [], - "params": { - "seconds": 1.0 - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "blowout", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "812a72ab8e68ec0870c5af969fd673f2", - "notes": [], - "params": { - "flowRate": 57.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": 0.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": -5.624999999999998, - "y": 356.2, - "z": 16.26 - } - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "touchTip", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "6e3bad6ca7601812c99f71e758fbb4ee", - "notes": [], - "params": { - "labwareId": "UUID", - "pipetteId": "UUID", - "radius": 1.0, - "speed": 60.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -1.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": -5.624999999999998, - "y": 356.2, - "z": 15.260000000000002 - } - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "dropTip", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "fc683ca797abaf2da486ca70b798d012", - "notes": [], - "params": { - "alternateDropLocation": true, - "labwareId": "UUID", - "pipetteId": "UUID", - "wellLocation": { - "offset": { - "x": 0, - "y": 0, - "z": 0 - }, - "origin": "default" - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 412.25, - "y": 364.0, - "z": 40.0 - } - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "thermocycler/closeLid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "4c4e5bae16743ea62b1205bacff45893", - "notes": [], - "params": { - "moduleId": "UUID" - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "thermocycler/runProfile", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "c720a403f4196e3ba8db997f36bc8996", - "notes": [], - "params": { - "blockMaxVolumeUl": 30.0, - "moduleId": "UUID", - "profile": [ - { - "celsius": 37.0, - "holdSeconds": 900.0 - }, - { - "celsius": 70.0, - "holdSeconds": 900.0 - } - ] - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "thermocycler/setTargetBlockTemperature", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "85771372837a21e64e5272f105af8da8", - "notes": [], - "params": { - "celsius": 4.0, - "holdTimeSeconds": 0.0, - "moduleId": "UUID" - }, - "result": { - "targetBlockTemperature": 4.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "thermocycler/waitForBlockTemperature", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "f147f7cc9ad093b5a4ac9c7cdd80a9b1", - "notes": [], - "params": { - "moduleId": "UUID" - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "thermocycler/openLid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "eec3952968005fd92c62ad251d9765fb", - "notes": [], - "params": { - "moduleId": "UUID" - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "moveLabware", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "853bada9386caea7f474f43b5b9942cc", - "notes": [], - "params": { - "labwareId": "UUID", - "newLocation": { - "slotName": "D2" - }, - "strategy": "usingGripper" - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "thermocycler/setTargetBlockTemperature", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "1c77c5644cb42239be59cf33ba420dcd", - "notes": [], - "params": { - "celsius": 21.0, - "holdTimeSeconds": 0.0, - "moduleId": "UUID" - }, - "result": { - "targetBlockTemperature": 21.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "thermocycler/waitForBlockTemperature", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "b23a4cdccd3f19e07bd7ecc8c9ec139c", - "notes": [], - "params": { - "moduleId": "UUID" - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "moveLabware", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "dd6de2abb6148dc2577700d5e24ff06b", - "notes": [], - "params": { - "labwareId": "UUID", - "newLocation": { - "moduleId": "UUID" - }, - "strategy": "manualMoveWithPause" - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "comment", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "0e8ab8e504563c90b6e712c2efd40eb7", - "notes": [], - "params": { - "message": "\n\n----------ADDING BEADS----------\n" - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "pickUpTip", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "368d2987166a287f15376baca3fdeb39", - "notes": [], - "params": { - "labwareId": "UUID", - "pipetteId": "UUID", - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": 0.0 - }, - "origin": "top" - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 178.38, - "y": 288.38, - "z": 99.0 - }, - "tipDiameter": 5.59, - "tipLength": 48.25, - "tipVolume": 200.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "aspirate", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "bfdbfbfe2e68f390eab027219030bd81", - "notes": [], - "params": { - "flowRate": 716.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 200.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -37.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 14.3, - "y": 74.15, - "z": 22.95 - }, - "volume": 200.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "dispense", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "815b95c25828650f547a6b9af13ef9cc", - "notes": [], - "params": { - "flowRate": 716.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 200.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -37.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 14.3, - "y": 74.15, - "z": 22.95 - }, - "volume": 200.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "aspirate", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "e3f66cb38bcd61ee1aba73adcaa202d2", - "notes": [], - "params": { - "flowRate": 716.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 200.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -37.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 14.3, - "y": 74.15, - "z": 22.95 - }, - "volume": 200.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "dispense", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "8c558f5aa78272ec1b161696b0ad56c4", - "notes": [], - "params": { - "flowRate": 716.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 200.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -37.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 14.3, - "y": 74.15, - "z": 22.95 - }, - "volume": 200.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "aspirate", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "269d5c8565bb1c5c434a42418f67b0d7", - "notes": [], - "params": { - "flowRate": 716.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 200.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -37.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 14.3, - "y": 74.15, - "z": 22.95 - }, - "volume": 200.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "dispense", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "cbd61ce6087bf9281211ab915e439ec2", - "notes": [], - "params": { - "flowRate": 716.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 200.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -37.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 14.3, - "y": 74.15, - "z": 22.95 - }, - "volume": 200.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "aspirate", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "3c66ebd9863837e70126759d45c3acab", - "notes": [], - "params": { - "flowRate": 716.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 200.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -37.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 14.3, - "y": 74.15, - "z": 22.95 - }, - "volume": 200.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "dispense", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "032af20af9ee8a5158ec1e187cb7f6ba", - "notes": [], - "params": { - "flowRate": 716.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 200.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -37.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 14.3, - "y": 74.15, - "z": 22.95 - }, - "volume": 200.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "aspirate", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "a86d2a7c488f7fee4d773c789f355911", - "notes": [], - "params": { - "flowRate": 716.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 200.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -37.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 14.3, - "y": 74.15, - "z": 22.95 - }, - "volume": 200.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "dispense", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "bcbcf4bb60b057f39b3c55634a9e6d3a", - "notes": [], - "params": { - "flowRate": 716.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 200.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -37.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 14.3, - "y": 74.15, - "z": 22.95 - }, - "volume": 200.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "aspirate", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "32b0ab44a5c49dad8b216d4b5ce0fced", - "notes": [], - "params": { - "flowRate": 716.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 200.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -37.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 14.3, - "y": 74.15, - "z": 22.95 - }, - "volume": 200.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "dispense", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "28123d12168fdc0f3bf245f6f10d0793", - "notes": [], - "params": { - "flowRate": 716.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 200.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -37.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 14.3, - "y": 74.15, - "z": 22.95 - }, - "volume": 200.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "aspirate", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "40715bf4fccfbbe911420e7d9abeb6c5", - "notes": [], - "params": { - "flowRate": 716.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 200.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -37.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 14.3, - "y": 74.15, - "z": 22.95 - }, - "volume": 200.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "dispense", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "5f79a39dadeba2f3514ffacbd60bba56", - "notes": [], - "params": { - "flowRate": 716.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 200.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -37.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 14.3, - "y": 74.15, - "z": 22.95 - }, - "volume": 200.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "aspirate", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "7f68bbee78afe7f6b884e47efa2e1072", - "notes": [], - "params": { - "flowRate": 716.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 200.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -37.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 14.3, - "y": 74.15, - "z": 22.95 - }, - "volume": 200.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "dispense", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "13ffd96786d1ceee4edc58d93d8f2b4d", - "notes": [], - "params": { - "flowRate": 716.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 200.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -37.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 14.3, - "y": 74.15, - "z": 22.95 - }, - "volume": 200.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "aspirate", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "0818d997908d21ce9cee773882c3f58f", - "notes": [], - "params": { - "flowRate": 716.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 200.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -37.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 14.3, - "y": 74.15, - "z": 22.95 - }, - "volume": 200.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "dispense", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "d15279d8a3bf0d1a076f44c3e83a4fdc", - "notes": [], - "params": { - "flowRate": 716.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 200.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -37.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 14.3, - "y": 74.15, - "z": 22.95 - }, - "volume": 200.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "aspirate", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "032aa89e17bbfd09dcacf52a15ce18e0", - "notes": [], - "params": { - "flowRate": 716.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 200.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -37.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 14.3, - "y": 74.15, - "z": 22.95 - }, - "volume": 200.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "dispense", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "30fa39e04a180e805ccf3bde812e4416", - "notes": [], - "params": { - "flowRate": 716.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 200.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -37.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 14.3, - "y": 74.15, - "z": 22.95 - }, - "volume": 200.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "aspirate", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "bb468de0a0819c83de68c459d2bc2a3b", - "notes": [], - "params": { - "flowRate": 716.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 200.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -37.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 14.3, - "y": 74.15, - "z": 22.95 - }, - "volume": 200.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "dispense", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "65fabe75fbb76b8ae64fbe095dbd2b3f", - "notes": [], - "params": { - "flowRate": 716.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 200.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -37.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 14.3, - "y": 74.15, - "z": 22.95 - }, - "volume": 200.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "aspirate", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "573b710f381645ce3be8001d3c236981", - "notes": [], - "params": { - "flowRate": 716.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 200.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -37.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 14.3, - "y": 74.15, - "z": 22.95 - }, - "volume": 200.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "dispense", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "25d1f00e790d48a9dce31f3e088618e6", - "notes": [], - "params": { - "flowRate": 716.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 200.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -37.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 14.3, - "y": 74.15, - "z": 22.95 - }, - "volume": 200.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "aspirate", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "7c1bbb04bafc14155df034c2f15da553", - "notes": [], - "params": { - "flowRate": 716.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 200.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -37.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 14.3, - "y": 74.15, - "z": 22.95 - }, - "volume": 200.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "dispense", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "529af4dfcaa0c5d1f571bc1be9fb3d1c", - "notes": [], - "params": { - "flowRate": 716.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 200.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -37.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 14.3, - "y": 74.15, - "z": 22.95 - }, - "volume": 200.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "aspirate", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "ed452939255d89a625ff82f96c65c52c", - "notes": [], - "params": { - "flowRate": 716.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 200.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -37.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 14.3, - "y": 74.15, - "z": 22.95 - }, - "volume": 200.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "dispense", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "9c9a02898278e87d7215b1c9e392490c", - "notes": [], - "params": { - "flowRate": 716.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 200.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -37.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 14.3, - "y": 74.15, - "z": 22.95 - }, - "volume": 200.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "aspirate", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "c2bb1ddf7b51c0dbc497654d4f8c4dc1", - "notes": [], - "params": { - "flowRate": 716.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 200.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -37.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 14.3, - "y": 74.15, - "z": 22.95 - }, - "volume": 200.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "dispense", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "67497cde9ed8faaf874e00dbf11eb43e", - "notes": [], - "params": { - "flowRate": 716.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 200.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -37.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 14.3, - "y": 74.15, - "z": 22.95 - }, - "volume": 200.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "aspirate", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "47b2201b41a49fdf972b02b2c43c3a3a", - "notes": [], - "params": { - "flowRate": 716.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 200.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -37.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 14.3, - "y": 74.15, - "z": 22.95 - }, - "volume": 200.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "dispense", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "7ad69f1477f0f7065240c3bf5b33ac35", - "notes": [], - "params": { - "flowRate": 716.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 200.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -37.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 14.3, - "y": 74.15, - "z": 22.95 - }, - "volume": 200.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "aspirate", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "2fe9faf60fd27b27b0f5a1011f0fd554", - "notes": [], - "params": { - "flowRate": 716.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 200.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -37.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 14.3, - "y": 74.15, - "z": 22.95 - }, - "volume": 200.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "dispense", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "d552b62cebac83b58a3410feab357780", - "notes": [], - "params": { - "flowRate": 716.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 200.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -37.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 14.3, - "y": 74.15, - "z": 22.95 - }, - "volume": 200.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "aspirate", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "ea5695819d50dee07e2c9218921f7cdb", - "notes": [], - "params": { - "flowRate": 716.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 200.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -37.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 14.3, - "y": 74.15, - "z": 22.95 - }, - "volume": 200.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "dispense", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "cca1372859c26b02f6746653b6ee3fc2", - "notes": [], - "params": { - "flowRate": 716.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 200.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -37.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 14.3, - "y": 74.15, - "z": 22.95 - }, - "volume": 200.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "aspirate", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "9da7d58b925ad38b592bd304000b89b0", - "notes": [], - "params": { - "flowRate": 716.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 200.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -37.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 14.3, - "y": 74.15, - "z": 22.95 - }, - "volume": 200.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "dispense", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "0e355f0015602654601b36f3fe733c7c", - "notes": [], - "params": { - "flowRate": 716.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 200.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -37.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 14.3, - "y": 74.15, - "z": 22.95 - }, - "volume": 200.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "aspirate", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "9fa0f5b1ce2cc7efc1afaeac0bc92572", - "notes": [], - "params": { - "flowRate": 716.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 200.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -37.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 14.3, - "y": 74.15, - "z": 22.95 - }, - "volume": 200.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "dispense", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "395ee0029cbb60d9f36f5a4955a252e4", - "notes": [], - "params": { - "flowRate": 716.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 200.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -37.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 14.3, - "y": 74.15, - "z": 22.95 - }, - "volume": 200.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "aspirate", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "7f92051293652d7e5b633281dad5de5d", - "notes": [], - "params": { - "flowRate": 716.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 60.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -37.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 14.3, - "y": 74.15, - "z": 22.95 - }, - "volume": 60.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "moveToWell", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "494376b85c763d47e43c64a86a1af8d0", - "notes": [], - "params": { - "forceDirect": false, - "labwareId": "UUID", - "pipetteId": "UUID", - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -3.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 14.3, - "y": 74.15, - "z": 56.95 - } - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "dispense", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "7616f9e1448ab2aaf8444a22a0e5c309", - "notes": [], - "params": { - "flowRate": 716.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 60.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -13.95 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 178.38, - "y": 74.24, - "z": 2.05 - }, - "volume": 60.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "aspirate", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "2a75bed14de91780767a2dba2fc63659", - "notes": [], - "params": { - "flowRate": 716.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 70.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -13.95 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 178.38, - "y": 74.24, - "z": 2.05 - }, - "volume": 70.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "dispense", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "a33188eb7c4689cbbbc5120fbcfc7c30", - "notes": [], - "params": { - "flowRate": 716.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 70.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -13.95 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 178.38, - "y": 74.24, - "z": 2.05 - }, - "volume": 70.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "aspirate", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "4c04d4a332881f459e5b3b64f7dd35f6", - "notes": [], - "params": { - "flowRate": 716.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 70.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -13.95 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 178.38, - "y": 74.24, - "z": 2.05 - }, - "volume": 70.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "dispense", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "ef7294819777242eba8889672edb5e0a", - "notes": [], - "params": { - "flowRate": 716.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 70.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -13.95 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 178.38, - "y": 74.24, - "z": 2.05 - }, - "volume": 70.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "aspirate", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "7f903dac68a209c04b9f7807783467f3", - "notes": [], - "params": { - "flowRate": 716.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 70.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -13.95 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 178.38, - "y": 74.24, - "z": 2.05 - }, - "volume": 70.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "dispense", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "746f439bb4418e6fc7dbf0baa79818f3", - "notes": [], - "params": { - "flowRate": 716.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 70.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -13.95 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 178.38, - "y": 74.24, - "z": 2.05 - }, - "volume": 70.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "aspirate", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "8e7fb9e6e6e9498092442eb6b0de80e5", - "notes": [], - "params": { - "flowRate": 716.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 70.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -13.95 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 178.38, - "y": 74.24, - "z": 2.05 - }, - "volume": 70.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "dispense", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "42e1ff8e9bb0d10f536f38547dca4217", - "notes": [], - "params": { - "flowRate": 716.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 70.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -13.95 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 178.38, - "y": 74.24, - "z": 2.05 - }, - "volume": 70.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "aspirate", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "f6ac2c0707b3dd297cfefccf2441b654", - "notes": [], - "params": { - "flowRate": 716.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 70.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -13.95 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 178.38, - "y": 74.24, - "z": 2.05 - }, - "volume": 70.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "dispense", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "e62d568ad177fb6f7b765a2c1f89bbde", - "notes": [], - "params": { - "flowRate": 716.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 70.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -13.95 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 178.38, - "y": 74.24, - "z": 2.05 - }, - "volume": 70.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "aspirate", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "3c1a1246725e07da8cbf379c35ba374f", - "notes": [], - "params": { - "flowRate": 716.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 70.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -13.95 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 178.38, - "y": 74.24, - "z": 2.05 - }, - "volume": 70.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "dispense", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "17c1e8a18b96c71d15bf365a791c3a7d", - "notes": [], - "params": { - "flowRate": 716.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 70.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -13.95 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 178.38, - "y": 74.24, - "z": 2.05 - }, - "volume": 70.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "aspirate", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "078072d91042c3399cc75c18749eb9a5", - "notes": [], - "params": { - "flowRate": 716.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 70.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -13.95 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 178.38, - "y": 74.24, - "z": 2.05 - }, - "volume": 70.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "dispense", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "4ea555ffe7f4ea8ec3c16c6d88be7c03", - "notes": [], - "params": { - "flowRate": 716.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 70.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -13.95 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 178.38, - "y": 74.24, - "z": 2.05 - }, - "volume": 70.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "aspirate", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "002c53f35b6556a3302128bd94c7951f", - "notes": [], - "params": { - "flowRate": 716.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 70.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -13.95 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 178.38, - "y": 74.24, - "z": 2.05 - }, - "volume": 70.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "dispense", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "0c735b1d5cc19e89268c81062ebf8e30", - "notes": [], - "params": { - "flowRate": 716.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 70.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -13.95 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 178.38, - "y": 74.24, - "z": 2.05 - }, - "volume": 70.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "aspirate", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "deeaf527616058731ddf81b63a7b4261", - "notes": [], - "params": { - "flowRate": 716.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 70.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -13.95 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 178.38, - "y": 74.24, - "z": 2.05 - }, - "volume": 70.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "dispense", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "1b082b45d6e92bdb566b26933a376376", - "notes": [], - "params": { - "flowRate": 716.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 70.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -13.95 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 178.38, - "y": 74.24, - "z": 2.05 - }, - "volume": 70.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "aspirate", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "490719174a027ee2f8ed33c33c375e69", - "notes": [], - "params": { - "flowRate": 716.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 70.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -13.95 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 178.38, - "y": 74.24, - "z": 2.05 - }, - "volume": 70.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "dispense", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "800841f6da189a765d8e4c20455930f1", - "notes": [], - "params": { - "flowRate": 716.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 70.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -13.95 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 178.38, - "y": 74.24, - "z": 2.05 - }, - "volume": 70.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "moveToWell", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "8d2c9a496395a9a1ded71c6b76b65915", - "notes": [], - "params": { - "forceDirect": false, - "labwareId": "UUID", - "pipetteId": "UUID", - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -3.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 178.38, - "y": 74.24, - "z": 13.0 - } - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "waitForDuration", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "d4ba58f8ff62e7ae41c88f29d03938bd", - "notes": [], - "params": { - "seconds": 2.0 - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "blowout", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "757be6c16526883c8a48707f09591fad", - "notes": [], - "params": { - "flowRate": 716.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -3.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 178.38, - "y": 74.24, - "z": 13.0 - } - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "dropTip", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "5a1213e3a9c70ead82d418fdd2a1b549", - "notes": [], - "params": { - "alternateDropLocation": true, - "labwareId": "UUID", - "pipetteId": "UUID", - "wellLocation": { - "offset": { - "x": 0, - "y": 0, - "z": 0 - }, - "origin": "default" - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 359.25, - "y": 364.0, - "z": 40.0 - } - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "waitForDuration", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "7ed0688b2edae638d7f07384107a0117", - "notes": [], - "params": { - "seconds": 300.0 - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "moveLabware", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "5d0b7e3a34dccde14447e701edc94117", - "notes": [], - "params": { - "labwareId": "UUID", - "newLocation": { - "moduleId": "UUID" - }, - "strategy": "usingGripper" - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "waitForDuration", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "4d311b4eff2d0b6db3959cb80a612a86", - "notes": [], - "params": { - "seconds": 180.0 - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "comment", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "244b0c4142617762011b5821edd2e5cb", - "notes": [], - "params": { - "message": "\n\n----------REMOVE SUPER----------\n" - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "pickUpTip", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "11e24eda4aaea17d19fe594f29cff94c", - "notes": [], - "params": { - "labwareId": "UUID", - "pipetteId": "UUID", - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": 0.0 - }, - "origin": "top" - }, - "wellName": "A2" - }, - "result": { - "position": { - "x": 187.38, - "y": 288.38, - "z": 99.0 - }, - "tipDiameter": 5.59, - "tipLength": 48.25, - "tipVolume": 200.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "aspirate", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "e03e23625068b2fe01340f043fbcf7e6", - "notes": [], - "params": { - "flowRate": 107.39999999999999, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 90.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -13.949999999999996 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 342.38, - "y": 181.24, - "z": 36.510000000000005 - }, - "volume": 90.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "aspirate", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "66746d07fdd08608be1343cfc9bee132", - "notes": [], - "params": { - "flowRate": 107.39999999999999, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 10.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -14.149999999999999 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 342.38, - "y": 181.24, - "z": 36.31 - }, - "volume": 10.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "moveToWell", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "a9a2748035ea5336157e177af0fd1625", - "notes": [], - "params": { - "forceDirect": false, - "labwareId": "UUID", - "pipetteId": "UUID", - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -3.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 342.38, - "y": 181.24, - "z": 47.459999999999994 - } - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "dispense", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "d7c0cecf7abee7221e56bf1562305413", - "notes": [], - "params": { - "flowRate": 716.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 100.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -3.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 178.38, - "y": 363.78, - "z": 28.400000000000002 - }, - "volume": 100.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "waitForDuration", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "02a1ad48dc99e7d22afe884c206ad3c0", - "notes": [], - "params": { - "seconds": 2.0 - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "blowout", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "d86a0db1fc9ac4641d8be7762d5a4a59", - "notes": [], - "params": { - "flowRate": 716.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -3.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 178.38, - "y": 363.78, - "z": 28.400000000000002 - } - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "dropTip", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "72c6a56b1237e317ea78c68fd44d1ec9", - "notes": [], - "params": { - "alternateDropLocation": true, - "labwareId": "UUID", - "pipetteId": "UUID", - "wellLocation": { - "offset": { - "x": 0, - "y": 0, - "z": 0 - }, - "origin": "default" - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 509.25, - "y": 364.0, - "z": 40.0 - } - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "comment", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "f453352b591a769a121caaa4d699e97a", - "notes": [], - "params": { - "message": "\n\n----------TWO WASHES----------\n" - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "pickUpTip", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "f9004be73e3f887db59400d4fec9612e", - "notes": [], - "params": { - "labwareId": "UUID", - "pipetteId": "UUID", - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": 0.0 - }, - "origin": "top" - }, - "wellName": "A3" - }, - "result": { - "position": { - "x": 196.38, - "y": 288.38, - "z": 99.0 - }, - "tipDiameter": 5.59, - "tipLength": 48.25, - "tipVolume": 200.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "aspirate", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "73d648448c42c3fe9f59ebee82a03bcf", - "notes": [], - "params": { - "flowRate": 716.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 150.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -37.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A2" - }, - "result": { - "position": { - "x": 23.3, - "y": 74.15, - "z": 22.95 - }, - "volume": 150.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "moveToWell", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "8c6870f661fd245f58e78cd630530cd2", - "notes": [], - "params": { - "forceDirect": false, - "labwareId": "UUID", - "pipetteId": "UUID", - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -3.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A2" - }, - "result": { - "position": { - "x": 23.3, - "y": 74.15, - "z": 56.95 - } - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "dispense", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "22886609f80251648d4df31266899f92", - "notes": [], - "params": { - "flowRate": 716.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 150.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": 0.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 342.38, - "y": 181.24, - "z": 50.459999999999994 - }, - "volume": 150.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "moveToWell", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "7bd0a6dc80fe6c5b937c1c62786dccb6", - "notes": [], - "params": { - "forceDirect": false, - "labwareId": "UUID", - "pipetteId": "UUID", - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": 20.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 342.38, - "y": 181.24, - "z": 70.46 - } - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "waitForDuration", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "40328e27da7174b4bc692d61c929fca5", - "notes": [], - "params": { - "seconds": 30.0 - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "aspirate", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "84a9a8cda676dfc363d11e5236a637e8", - "notes": [], - "params": { - "flowRate": 107.39999999999999, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 150.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -13.949999999999996 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 342.38, - "y": 181.24, - "z": 36.510000000000005 - }, - "volume": 150.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "aspirate", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "2b9c2e0a5bdbcb142a4c9b7ec03923ce", - "notes": [], - "params": { - "flowRate": 107.39999999999999, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 10.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -14.149999999999999 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 342.38, - "y": 181.24, - "z": 36.31 - }, - "volume": 10.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "moveToWell", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "8e1d29cdf6a90b3fbe469e54509b5a06", - "notes": [], - "params": { - "forceDirect": false, - "labwareId": "UUID", - "pipetteId": "UUID", - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -3.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 342.38, - "y": 181.24, - "z": 47.459999999999994 - } - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "dispense", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "1a2647fa45f954247dafedf43a81334a", - "notes": [], - "params": { - "flowRate": 716.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 160.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -3.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 178.38, - "y": 363.78, - "z": 28.400000000000002 - }, - "volume": 160.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "waitForDuration", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "fd65fece18bfe42a335b896faba7164a", - "notes": [], - "params": { - "seconds": 2.0 - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "blowout", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "13b220ee6e41cc137d2aa98490b4e537", - "notes": [], - "params": { - "flowRate": 716.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -3.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 178.38, - "y": 363.78, - "z": 28.400000000000002 - } - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "dropTip", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "674eee01810aa366e74e617ed3b99f5a", - "notes": [], - "params": { - "alternateDropLocation": true, - "labwareId": "UUID", - "pipetteId": "UUID", - "wellLocation": { - "offset": { - "x": 0, - "y": 0, - "z": 0 - }, - "origin": "default" - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 359.25, - "y": 364.0, - "z": 40.0 - } - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "pickUpTip", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "d77fb0a118401dafaec8690ea33837e5", - "notes": [], - "params": { - "labwareId": "UUID", - "pipetteId": "UUID", - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": 0.0 - }, - "origin": "top" - }, - "wellName": "A4" - }, - "result": { - "position": { - "x": 205.38, - "y": 288.38, - "z": 99.0 - }, - "tipDiameter": 5.59, - "tipLength": 48.25, - "tipVolume": 200.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "aspirate", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "bac23d746d550fbebf80bd28b859e0ff", - "notes": [], - "params": { - "flowRate": 716.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 150.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -37.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A2" - }, - "result": { - "position": { - "x": 23.3, - "y": 74.15, - "z": 22.95 - }, - "volume": 150.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "moveToWell", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "331a6cfd64ef8957a1d5ec1223362fb3", - "notes": [], - "params": { - "forceDirect": false, - "labwareId": "UUID", - "pipetteId": "UUID", - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -3.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A2" - }, - "result": { - "position": { - "x": 23.3, - "y": 74.15, - "z": 56.95 - } - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "dispense", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "5337ce78a813c809f6bb00618c4fe20a", - "notes": [], - "params": { - "flowRate": 716.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 150.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": 0.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 342.38, - "y": 181.24, - "z": 50.459999999999994 - }, - "volume": 150.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "moveToWell", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "989ca5a87edf4643238fbc9a3364efc9", - "notes": [], - "params": { - "forceDirect": false, - "labwareId": "UUID", - "pipetteId": "UUID", - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": 20.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 342.38, - "y": 181.24, - "z": 70.46 - } - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "waitForDuration", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "5364401abcf4dc920adc9ab447efdc73", - "notes": [], - "params": { - "seconds": 30.0 - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "aspirate", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "1ee8b5cfcf4165f39a0099b7bbeea54d", - "notes": [], - "params": { - "flowRate": 107.39999999999999, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 150.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -13.949999999999996 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 342.38, - "y": 181.24, - "z": 36.510000000000005 - }, - "volume": 150.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "aspirate", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "34e58fe0b2c459fbc3f06920d0769270", - "notes": [], - "params": { - "flowRate": 107.39999999999999, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 10.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -14.149999999999999 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 342.38, - "y": 181.24, - "z": 36.31 - }, - "volume": 10.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "moveToWell", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "3057863e0b46bc6d14cfddcaa0aacf84", - "notes": [], - "params": { - "forceDirect": false, - "labwareId": "UUID", - "pipetteId": "UUID", - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -3.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 342.38, - "y": 181.24, - "z": 47.459999999999994 - } - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "dispense", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "495582e5fad7bd1901c08fb6e7eb50f5", - "notes": [], - "params": { - "flowRate": 716.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 160.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -3.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 178.38, - "y": 363.78, - "z": 28.400000000000002 - }, - "volume": 160.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "waitForDuration", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "ccd5133c5c5d866132fe063c1ae22f9c", - "notes": [], - "params": { - "seconds": 2.0 - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "blowout", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "ab97584f3d517de829a20f13ff5791c0", - "notes": [], - "params": { - "flowRate": 716.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -3.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 178.38, - "y": 363.78, - "z": 28.400000000000002 - } - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "dropTip", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "21c4f328e8c27e5b8eeae02aa6e69a14", - "notes": [], - "params": { - "alternateDropLocation": true, - "labwareId": "UUID", - "pipetteId": "UUID", - "wellLocation": { - "offset": { - "x": 0, - "y": 0, - "z": 0 - }, - "origin": "default" - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 509.25, - "y": 364.0, - "z": 40.0 - } - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "waitForDuration", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "bdc01a9029d0a64275e42cdd918ff509", - "notes": [], - "params": { - "seconds": 120.0 - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "comment", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "20a08a7ac1651a88eeff1c42d5a7fd83", - "notes": [], - "params": { - "message": "\n\n----------ADDING ELUTION BUFFER----------\n" - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "moveLabware", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "bcd6060410179a5511d523068e7aa8ca", - "notes": [], - "params": { - "labwareId": "UUID", - "newLocation": { - "slotName": "D2" - }, - "strategy": "usingGripper" - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "pickUpTip", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "a4af1bda7fb80f69e2384d2636266568", - "notes": [], - "params": { - "labwareId": "UUID", - "pipetteId": "UUID", - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": 0.0 - }, - "origin": "top" - }, - "wellName": "A4" - }, - "result": { - "position": { - "x": 41.38, - "y": 181.38, - "z": 99.0 - }, - "tipDiameter": 5.58, - "tipLength": 47.849999999999994, - "tipVolume": 50.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "aspirate", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "f5f746debed8dcac8a497a29b31cf186", - "notes": [], - "params": { - "flowRate": 35.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 10.5, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -37.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A4" - }, - "result": { - "position": { - "x": 41.3, - "y": 74.15, - "z": 22.95 - }, - "volume": 10.5 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "dispense", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "c65c363a455fb00b832481ddec027e5a", - "notes": [], - "params": { - "flowRate": 57.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 10.5, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -13.95 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 178.38, - "y": 74.24, - "z": 2.05 - }, - "volume": 10.5 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "aspirate", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "e2d37a0b98bad9f18001accc5d48f2ec", - "notes": [], - "params": { - "flowRate": 35.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 7.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -14.15 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 178.38, - "y": 74.24, - "z": 1.849999999999999 - }, - "volume": 7.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "dispense", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "c93b23c1e0fc630e0602f22bf92771f4", - "notes": [], - "params": { - "flowRate": 114.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 7.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -8.95 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 178.38, - "y": 74.24, - "z": 7.05 - }, - "volume": 7.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "aspirate", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "560c787161a5b7b6bc1e4789f309c7eb", - "notes": [], - "params": { - "flowRate": 35.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 7.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -14.15 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 178.38, - "y": 74.24, - "z": 1.849999999999999 - }, - "volume": 7.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "dispense", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "64468195bc0d12a28b7d33dd318ae9b7", - "notes": [], - "params": { - "flowRate": 114.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 7.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -8.95 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 178.38, - "y": 74.24, - "z": 7.05 - }, - "volume": 7.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "aspirate", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "c1b170c37265b40564968626c228e40a", - "notes": [], - "params": { - "flowRate": 35.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 7.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -14.15 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 178.38, - "y": 74.24, - "z": 1.849999999999999 - }, - "volume": 7.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "dispense", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "5e77139a1eda8ea5a627879417e3412e", - "notes": [], - "params": { - "flowRate": 114.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 7.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -8.95 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 178.38, - "y": 74.24, - "z": 7.05 - }, - "volume": 7.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "aspirate", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "cd2e6a14139282eb596cbe91b211df6f", - "notes": [], - "params": { - "flowRate": 35.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 7.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -14.15 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 178.38, - "y": 74.24, - "z": 1.849999999999999 - }, - "volume": 7.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "dispense", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "aa01358ba5e110ec8e0ae314196f6ab8", - "notes": [], - "params": { - "flowRate": 114.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 7.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -8.95 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 178.38, - "y": 74.24, - "z": 7.05 - }, - "volume": 7.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "aspirate", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "9658f3926f4e574c676dcb0556367cfe", - "notes": [], - "params": { - "flowRate": 35.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 7.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -14.15 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 178.38, - "y": 74.24, - "z": 1.849999999999999 - }, - "volume": 7.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "dispense", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "1a78c357fe1b8ac9d4cc5b6d3ad8889c", - "notes": [], - "params": { - "flowRate": 114.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 7.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -8.95 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 178.38, - "y": 74.24, - "z": 7.05 - }, - "volume": 7.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "aspirate", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "9b09c58201ed7d85d177642432167b83", - "notes": [], - "params": { - "flowRate": 35.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 7.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -14.15 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 178.38, - "y": 74.24, - "z": 1.849999999999999 - }, - "volume": 7.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "dispense", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "122379ae04201731c95128ecc84a8c53", - "notes": [], - "params": { - "flowRate": 114.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 7.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -8.95 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 178.38, - "y": 74.24, - "z": 7.05 - }, - "volume": 7.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "aspirate", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "e6e1bdf34d0a30334e1498f9c8cb0162", - "notes": [], - "params": { - "flowRate": 35.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 7.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -14.15 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 178.38, - "y": 74.24, - "z": 1.849999999999999 - }, - "volume": 7.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "dispense", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "f0b7fc1d3b9f5df73fe8fbf36b73ee2d", - "notes": [], - "params": { - "flowRate": 114.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 7.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -8.95 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 178.38, - "y": 74.24, - "z": 7.05 - }, - "volume": 7.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "aspirate", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "73cb056bee8d98e0d185fb641ccf0730", - "notes": [], - "params": { - "flowRate": 52.5, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 7.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -14.25 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 178.38, - "y": 74.24, - "z": 1.7499999999999993 - }, - "volume": 7.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "dispense", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "31fc0701b0138e9d517802ce3dae49e8", - "notes": [], - "params": { - "flowRate": 85.5, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 7.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -14.25 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 178.38, - "y": 74.24, - "z": 1.7499999999999993 - }, - "volume": 7.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "aspirate", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "0d4e94c8be945d6ebfbde9a7ce5e43a5", - "notes": [], - "params": { - "flowRate": 52.5, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 7.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -14.25 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 178.38, - "y": 74.24, - "z": 1.7499999999999993 - }, - "volume": 7.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "dispense", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "a808ea340d49477099ea1a71dd6f05ba", - "notes": [], - "params": { - "flowRate": 85.5, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 7.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -14.25 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 178.38, - "y": 74.24, - "z": 1.7499999999999993 - }, - "volume": 7.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "aspirate", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "580660e11127c5dfdbcda68853a49ba0", - "notes": [], - "params": { - "flowRate": 52.5, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 7.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -14.25 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 178.38, - "y": 74.24, - "z": 1.7499999999999993 - }, - "volume": 7.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "dispense", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "23fad1294e92a433838e8d5b886cbaa0", - "notes": [], - "params": { - "flowRate": 85.5, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 7.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -14.25 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 178.38, - "y": 74.24, - "z": 1.7499999999999993 - }, - "volume": 7.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "aspirate", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "f60561ce4427bcef72abbbe058be0186", - "notes": [], - "params": { - "flowRate": 52.5, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 7.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -14.25 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 178.38, - "y": 74.24, - "z": 1.7499999999999993 - }, - "volume": 7.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "dispense", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "aa9a60b020a6368ec75a24dc20864c5e", - "notes": [], - "params": { - "flowRate": 85.5, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 7.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -14.25 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 178.38, - "y": 74.24, - "z": 1.7499999999999993 - }, - "volume": 7.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "aspirate", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "bb927ea6fd625099361703d6ab193a10", - "notes": [], - "params": { - "flowRate": 52.5, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 7.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -14.25 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 178.38, - "y": 74.24, - "z": 1.7499999999999993 - }, - "volume": 7.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "dispense", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "9f2659860d0651d5084898e58f18db37", - "notes": [], - "params": { - "flowRate": 85.5, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 7.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -14.25 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 178.38, - "y": 74.24, - "z": 1.7499999999999993 - }, - "volume": 7.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "moveToWell", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "ddc9614e29345c82acaff74cacac8bbc", - "notes": [], - "params": { - "forceDirect": false, - "labwareId": "UUID", - "pipetteId": "UUID", - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": 0.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 178.38, - "y": 74.24, - "z": 16.0 - } - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "waitForDuration", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "141e8300f581875761e9dc308946e092", - "notes": [], - "params": { - "seconds": 1.0 - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "blowout", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "b22db511c943124628d5b9e12a5214fc", - "notes": [], - "params": { - "flowRate": 57.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": 0.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 178.38, - "y": 74.24, - "z": 16.0 - } - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "touchTip", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "4fc79926d971451151d9871e600b7387", - "notes": [], - "params": { - "labwareId": "UUID", - "pipetteId": "UUID", - "radius": 1.0, - "speed": 60.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -1.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 178.38, - "y": 74.24, - "z": 15.0 - } - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "dropTip", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "cfc51d330ae4be9ed06dea079045b04c", - "notes": [], - "params": { - "alternateDropLocation": true, - "labwareId": "UUID", - "pipetteId": "UUID", - "wellLocation": { - "offset": { - "x": 0, - "y": 0, - "z": 0 - }, - "origin": "default" - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 359.25, - "y": 364.0, - "z": 40.0 - } - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "moveLabware", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "642fd4b9b268619544cb12f55a88304c", - "notes": [], - "params": { - "labwareId": "UUID", - "newLocation": { - "moduleId": "UUID" - }, - "strategy": "usingGripper" - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "waitForDuration", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "e95a1b9a129d8ccf78f7d6ffd4275c64", - "notes": [], - "params": { - "seconds": 90.0 - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "comment", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "88a37b605a7fd179714001e800e4d53a", - "notes": [], - "params": { - "message": "\n\n----------ADDING EPH3----------\n" - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "pickUpTip", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "128ade80924cb01a491759f7e15242cc", - "notes": [], - "params": { - "labwareId": "UUID", - "pipetteId": "UUID", - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": 0.0 - }, - "origin": "top" - }, - "wellName": "A5" - }, - "result": { - "position": { - "x": 50.38, - "y": 181.38, - "z": 99.0 - }, - "tipDiameter": 5.58, - "tipLength": 47.849999999999994, - "tipVolume": 50.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "aspirate", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "985b28047bc9bf85068fcd1cad7765a7", - "notes": [], - "params": { - "flowRate": 35.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 8.5, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -37.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A4" - }, - "result": { - "position": { - "x": 369.3, - "y": 74.15, - "z": 13.0 - }, - "volume": 8.5 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "dispense", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "773226e52da048d688c0e39ff7d205bd", - "notes": [], - "params": { - "flowRate": 57.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 8.5, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -13.95 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": -5.624999999999998, - "y": 356.2, - "z": 2.3100000000000014 - }, - "volume": 8.5 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "moveToWell", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "6acd32649b09ba4ee59c2e8230d1f1ba", - "notes": [], - "params": { - "forceDirect": false, - "labwareId": "UUID", - "pipetteId": "UUID", - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": 0.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": -5.624999999999998, - "y": 356.2, - "z": 16.26 - } - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "blowout", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "0bb89ce96ead87c396c38b105a1733b2", - "notes": [], - "params": { - "flowRate": 57.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": 0.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": -5.624999999999998, - "y": 356.2, - "z": 16.26 - } - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "dropTip", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "b22f3f2d2723811944d129032a376005", - "notes": [], - "params": { - "alternateDropLocation": true, - "labwareId": "UUID", - "pipetteId": "UUID", - "wellLocation": { - "offset": { - "x": 0, - "y": 0, - "z": 0 - }, - "origin": "default" - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 412.25, - "y": 364.0, - "z": 40.0 - } - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "comment", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "3a725551ff77626c661addc11c85e79c", - "notes": [], - "params": { - "message": "\n\n----------TRANSFERRING ELUTE----------\n" - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "pickUpTip", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "34064f6b319716e8d4bd456e74a85f67", - "notes": [], - "params": { - "labwareId": "UUID", - "pipetteId": "UUID", - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": 0.0 - }, - "origin": "top" - }, - "wellName": "A6" - }, - "result": { - "position": { - "x": 59.38, - "y": 181.38, - "z": 99.0 - }, - "tipDiameter": 5.58, - "tipLength": 47.849999999999994, - "tipVolume": 50.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "aspirate", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "f1b5a1227acc8e0c996fac4a5b1eed5a", - "notes": [], - "params": { - "flowRate": 7.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 8.5, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -14.149999999999999 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 342.38, - "y": 181.24, - "z": 36.31 - }, - "volume": 8.5 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "dispense", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "389b353fee2a9932503c102689b31a7d", - "notes": [], - "params": { - "flowRate": 57.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 8.5, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -13.95 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": -5.624999999999998, - "y": 356.2, - "z": 2.3100000000000014 - }, - "volume": 8.5 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "moveToWell", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "f37014c13bbbdd7cf997d41b3e9f2555", - "notes": [], - "params": { - "forceDirect": false, - "labwareId": "UUID", - "pipetteId": "UUID", - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": 0.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 342.38, - "y": 181.24, - "z": 50.459999999999994 - } - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "blowout", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "4f2926b08d8ff5716d3d388e5ff3aeda", - "notes": [], - "params": { - "flowRate": 57.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": 0.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": -5.624999999999998, - "y": 356.2, - "z": 16.26 - } - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "dropTip", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "199861e98b6784b606bc3a798cc21adc", - "notes": [], - "params": { - "alternateDropLocation": true, - "labwareId": "UUID", - "pipetteId": "UUID", - "wellLocation": { - "offset": { - "x": 0, - "y": 0, - "z": 0 - }, - "origin": "default" - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 359.25, - "y": 364.0, - "z": 40.0 - } - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "moveLabware", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "5e7ffb1e17edf90dd21bb6402f4e61cb", - "notes": [], - "params": { - "labwareId": "UUID", - "newLocation": "offDeck", - "strategy": "manualMoveWithPause" - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "thermocycler/closeLid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "f596cf492a4aa18c0a78962946317c9f", - "notes": [], - "params": { - "moduleId": "UUID" - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "thermocycler/runProfile", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "13c1a4f2e91bd57161ee09096db29059", - "notes": [], - "params": { - "blockMaxVolumeUl": 17.0, - "moduleId": "UUID", - "profile": [ - { - "celsius": 94.0, - "holdSeconds": 60.0 - } - ] - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "thermocycler/setTargetBlockTemperature", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "c4f468125fb7219cc751c069d3c67477", - "notes": [], - "params": { - "celsius": 4.0, - "holdTimeSeconds": 0.0, - "moduleId": "UUID" - }, - "result": { - "targetBlockTemperature": 4.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "thermocycler/waitForBlockTemperature", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "da97683b7f34741bc65ff23fa048b830", - "notes": [], - "params": { - "moduleId": "UUID" - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "thermocycler/openLid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "4221bd22c8a1eb22232ef8e6a7e7ff45", - "notes": [], - "params": { - "moduleId": "UUID" - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "comment", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "0f091935a1a7399794f9581e659dcb0e", - "notes": [], - "params": { - "message": "\n\n----------ADDING FSA----------\n" - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "pickUpTip", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "7a176bc03bbf77898f7ffa407ea492a5", - "notes": [], - "params": { - "labwareId": "UUID", - "pipetteId": "UUID", - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": 0.0 - }, - "origin": "top" - }, - "wellName": "A7" - }, - "result": { - "position": { - "x": 68.38, - "y": 181.38, - "z": 99.0 - }, - "tipDiameter": 5.58, - "tipLength": 47.849999999999994, - "tipVolume": 50.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "aspirate", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "fdd4c037f72192630e23143203453fe6", - "notes": [], - "params": { - "flowRate": 35.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 8.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -37.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A5" - }, - "result": { - "position": { - "x": 378.3, - "y": 74.15, - "z": 13.0 - }, - "volume": 8.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "dispense", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "c3748efc9cf7809283f676220c486f7c", - "notes": [], - "params": { - "flowRate": 57.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 8.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -13.95 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": -5.624999999999998, - "y": 356.2, - "z": 2.3100000000000014 - }, - "volume": 8.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "aspirate", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "12ff084c76776a7fc82b5bb240453304", - "notes": [], - "params": { - "flowRate": 35.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 19.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -13.95 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": -5.624999999999998, - "y": 356.2, - "z": 2.3100000000000014 - }, - "volume": 19.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "dispense", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "6b6f0aae318f1cb134fc40f53ba5c29b", - "notes": [], - "params": { - "flowRate": 57.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 19.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -13.95 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": -5.624999999999998, - "y": 356.2, - "z": 2.3100000000000014 - }, - "volume": 19.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "aspirate", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "79985b87ab70d2619333184824beb425", - "notes": [], - "params": { - "flowRate": 35.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 19.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -13.95 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": -5.624999999999998, - "y": 356.2, - "z": 2.3100000000000014 - }, - "volume": 19.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "dispense", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "d46fe87cae878bc66c847a0cbf663c5f", - "notes": [], - "params": { - "flowRate": 57.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 19.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -13.95 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": -5.624999999999998, - "y": 356.2, - "z": 2.3100000000000014 - }, - "volume": 19.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "aspirate", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "d8d223bc3df2ffe1916a94b78c6f75d4", - "notes": [], - "params": { - "flowRate": 35.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 19.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -13.95 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": -5.624999999999998, - "y": 356.2, - "z": 2.3100000000000014 - }, - "volume": 19.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "dispense", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "55236da67108ce78c1b2fd22b7f58cf5", - "notes": [], - "params": { - "flowRate": 57.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 19.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -13.95 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": -5.624999999999998, - "y": 356.2, - "z": 2.3100000000000014 - }, - "volume": 19.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "aspirate", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "5013bedc495e54d9c994ca6ecb6a3304", - "notes": [], - "params": { - "flowRate": 35.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 19.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -13.95 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": -5.624999999999998, - "y": 356.2, - "z": 2.3100000000000014 - }, - "volume": 19.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "dispense", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "6a8b334c3c6dd89d77d727db4a2d0905", - "notes": [], - "params": { - "flowRate": 57.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 19.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -13.95 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": -5.624999999999998, - "y": 356.2, - "z": 2.3100000000000014 - }, - "volume": 19.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "aspirate", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "d4697967048dec03f8379d91b2122941", - "notes": [], - "params": { - "flowRate": 35.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 19.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -13.95 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": -5.624999999999998, - "y": 356.2, - "z": 2.3100000000000014 - }, - "volume": 19.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "dispense", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "0e95bac1c6b7e126737543371150ebfd", - "notes": [], - "params": { - "flowRate": 57.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 19.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -13.95 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": -5.624999999999998, - "y": 356.2, - "z": 2.3100000000000014 - }, - "volume": 19.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "moveToWell", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "8b1193f8aad3d883df9b1236a7035a97", - "notes": [], - "params": { - "forceDirect": false, - "labwareId": "UUID", - "pipetteId": "UUID", - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": 0.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": -5.624999999999998, - "y": 356.2, - "z": 16.26 - } - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "waitForDuration", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "8998c1ea61f8979eee45de54caa22fb7", - "notes": [], - "params": { - "seconds": 1.0 - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "blowout", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "5cbfa56632670eaa5d85c6dc55eeb9ea", - "notes": [], - "params": { - "flowRate": 57.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": 0.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": -5.624999999999998, - "y": 356.2, - "z": 16.26 - } - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "touchTip", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "0a798cd4aad04f049902c7b9f500d1c3", - "notes": [], - "params": { - "labwareId": "UUID", - "pipetteId": "UUID", - "radius": 1.0, - "speed": 60.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -1.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": -5.624999999999998, - "y": 356.2, - "z": 15.260000000000002 - } - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "dropTip", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "59b2e26b7244cb274df157ebedebc0a1", - "notes": [], - "params": { - "alternateDropLocation": true, - "labwareId": "UUID", - "pipetteId": "UUID", - "wellLocation": { - "offset": { - "x": 0, - "y": 0, - "z": 0 - }, - "origin": "default" - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 412.25, - "y": 364.0, - "z": 40.0 - } - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "thermocycler/closeLid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "1caf8473424e9499f11a1c12ff240937", - "notes": [], - "params": { - "moduleId": "UUID" - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "thermocycler/runProfile", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "7d0b200036ec0bd09dcf980764dbd257", - "notes": [], - "params": { - "blockMaxVolumeUl": 25.0, - "moduleId": "UUID", - "profile": [ - { - "celsius": 25.0, - "holdSeconds": 600.0 - }, - { - "celsius": 42.0, - "holdSeconds": 900.0 - }, - { - "celsius": 70.0, - "holdSeconds": 900.0 - } - ] - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "thermocycler/setTargetBlockTemperature", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "ce231cd2e87c00ca926aa296519bac6a", - "notes": [], - "params": { - "celsius": 4.0, - "holdTimeSeconds": 0.0, - "moduleId": "UUID" - }, - "result": { - "targetBlockTemperature": 4.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "thermocycler/waitForBlockTemperature", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "dab2ad7a7aa227d17ae84bfe3ebe67f1", - "notes": [], - "params": { - "moduleId": "UUID" - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "thermocycler/openLid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "3160b183c42da9abcdfd618d6b375807", - "notes": [], - "params": { - "moduleId": "UUID" - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "comment", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "c7ca1b17d1348ed1dd69565e0ab6d559", - "notes": [], - "params": { - "message": "\n\n----------ADDING SMM----------\n" - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "pickUpTip", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "cbf5f3ec41a0236af7ddf7292227da40", - "notes": [], - "params": { - "labwareId": "UUID", - "pipetteId": "UUID", - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": 0.0 - }, - "origin": "top" - }, - "wellName": "A8" - }, - "result": { - "position": { - "x": 77.38, - "y": 181.38, - "z": 99.0 - }, - "tipDiameter": 5.58, - "tipLength": 47.849999999999994, - "tipVolume": 50.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "aspirate", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "d59a18a21f50aec382438feabac86b5d", - "notes": [], - "params": { - "flowRate": 35.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 25.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -37.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A6" - }, - "result": { - "position": { - "x": 387.3, - "y": 74.15, - "z": 13.0 - }, - "volume": 25.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "dispense", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "fa2874da436a4ce1e1b518b5929a4b7d", - "notes": [], - "params": { - "flowRate": 57.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 25.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -13.95 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": -5.624999999999998, - "y": 356.2, - "z": 2.3100000000000014 - }, - "volume": 25.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "aspirate", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "ed2888e9812d6ed5b579098d5f7dcadb", - "notes": [], - "params": { - "flowRate": 35.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 40.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -13.95 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": -5.624999999999998, - "y": 356.2, - "z": 2.3100000000000014 - }, - "volume": 40.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "dispense", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "6f984f796140a36b7e4b047af2580cad", - "notes": [], - "params": { - "flowRate": 57.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 40.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -13.95 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": -5.624999999999998, - "y": 356.2, - "z": 2.3100000000000014 - }, - "volume": 40.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "aspirate", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "056bf9604a87b6b7251a0dd7ee0a0533", - "notes": [], - "params": { - "flowRate": 35.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 40.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -13.95 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": -5.624999999999998, - "y": 356.2, - "z": 2.3100000000000014 - }, - "volume": 40.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "dispense", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "2dd228ae53cef058bed3795b223cbd9c", - "notes": [], - "params": { - "flowRate": 57.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 40.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -13.95 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": -5.624999999999998, - "y": 356.2, - "z": 2.3100000000000014 - }, - "volume": 40.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "aspirate", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "401120eb90da95947308e31b43dccb8d", - "notes": [], - "params": { - "flowRate": 35.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 40.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -13.95 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": -5.624999999999998, - "y": 356.2, - "z": 2.3100000000000014 - }, - "volume": 40.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "dispense", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "2f09e6ed15dd6a9c607537f22fd56482", - "notes": [], - "params": { - "flowRate": 57.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 40.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -13.95 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": -5.624999999999998, - "y": 356.2, - "z": 2.3100000000000014 - }, - "volume": 40.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "aspirate", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "57370e705965f5648df3898f2301cd9e", - "notes": [], - "params": { - "flowRate": 35.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 40.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -13.95 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": -5.624999999999998, - "y": 356.2, - "z": 2.3100000000000014 - }, - "volume": 40.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "dispense", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "06c1334811158883f7c36da25a22e33e", - "notes": [], - "params": { - "flowRate": 57.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 40.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -13.95 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": -5.624999999999998, - "y": 356.2, - "z": 2.3100000000000014 - }, - "volume": 40.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "aspirate", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "0fca0fa0c64f8503a2837c9006667146", - "notes": [], - "params": { - "flowRate": 35.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 40.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -13.95 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": -5.624999999999998, - "y": 356.2, - "z": 2.3100000000000014 - }, - "volume": 40.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "dispense", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "48f33362c3c7f619223ac2cc8c337c38", - "notes": [], - "params": { - "flowRate": 57.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 40.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -13.95 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": -5.624999999999998, - "y": 356.2, - "z": 2.3100000000000014 - }, - "volume": 40.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "moveToWell", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "8799e4f3726c598289f8bd7b23f0abf3", - "notes": [], - "params": { - "forceDirect": false, - "labwareId": "UUID", - "pipetteId": "UUID", - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": 0.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": -5.624999999999998, - "y": 356.2, - "z": 16.26 - } - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "waitForDuration", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "4da4535a7b8dbf796c7361ed841dc830", - "notes": [], - "params": { - "seconds": 1.0 - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "blowout", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "09e205b3ec47cd7f5c507ef7f6f180e1", - "notes": [], - "params": { - "flowRate": 57.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": 0.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": -5.624999999999998, - "y": 356.2, - "z": 16.26 - } - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "touchTip", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "86ab24cd106991df03153585488a6bd8", - "notes": [], - "params": { - "labwareId": "UUID", - "pipetteId": "UUID", - "radius": 1.0, - "speed": 60.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -1.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": -5.624999999999998, - "y": 356.2, - "z": 15.260000000000002 - } - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "dropTip", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "c212e5981120f727c6decc1943c152ad", - "notes": [], - "params": { - "alternateDropLocation": true, - "labwareId": "UUID", - "pipetteId": "UUID", - "wellLocation": { - "offset": { - "x": 0, - "y": 0, - "z": 0 - }, - "origin": "default" - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 359.25, - "y": 364.0, - "z": 40.0 - } - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "thermocycler/setTargetLidTemperature", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "e842bc8a137ba1d36a17d3faf61e07b1", - "notes": [], - "params": { - "celsius": 40.0, - "moduleId": "UUID" - }, - "result": { - "targetLidTemperature": 40.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "thermocycler/waitForLidTemperature", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "3a91d0a6da70911cf187829fea8a1c6c", - "notes": [], - "params": { - "moduleId": "UUID" - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "thermocycler/closeLid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "60ac672f2f39611656b99757dd9bcf74", - "notes": [], - "params": { - "moduleId": "UUID" - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "thermocycler/runProfile", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "12e135979be4e41816f8008d391ac2ff", - "notes": [], - "params": { - "blockMaxVolumeUl": 50.0, - "moduleId": "UUID", - "profile": [ - { - "celsius": 16.0, - "holdSeconds": 3600.0 - } - ] - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "thermocycler/setTargetBlockTemperature", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "b719f4e921a8f757f7c4d993f3da43d9", - "notes": [], - "params": { - "celsius": 4.0, - "holdTimeSeconds": 0.0, - "moduleId": "UUID" - }, - "result": { - "targetBlockTemperature": 4.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "thermocycler/waitForBlockTemperature", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "8c75d278bb185b008e22212d155ef5dc", - "notes": [], - "params": { - "moduleId": "UUID" - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "thermocycler/openLid", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "b5f410ba45b213bd477900bef4951843", - "notes": [], - "params": { - "moduleId": "UUID" - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "moveLabware", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "b27ed28512232cb0551a5af34c2261e5", - "notes": [], - "params": { - "labwareId": "UUID", - "newLocation": { - "slotName": "D2" - }, - "strategy": "usingGripper" - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "thermocycler/setTargetBlockTemperature", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "094738334022c0efdb7d09feb70485a4", - "notes": [], - "params": { - "celsius": 21.0, - "holdTimeSeconds": 0.0, - "moduleId": "UUID" - }, - "result": { - "targetBlockTemperature": 21.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "thermocycler/waitForBlockTemperature", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "f53679fa22cc21426034afb9166c4c09", - "notes": [], - "params": { - "moduleId": "UUID" - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "moveLabware", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "16840a171c215462a28e2f4e1363f2c0", - "notes": [], - "params": { - "labwareId": "UUID", - "newLocation": { - "moduleId": "UUID" - }, - "strategy": "manualMoveWithPause" - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "comment", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "e1a5efdc0f7ef98106e4306b5dd2e6b8", - "notes": [], - "params": { - "message": "\n\n----------ADDING BEADS----------\n" - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "pickUpTip", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "682b477a087dd92ac6159f900115fb35", - "notes": [], - "params": { - "labwareId": "UUID", - "pipetteId": "UUID", - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": 0.0 - }, - "origin": "top" - }, - "wellName": "A5" - }, - "result": { - "position": { - "x": 214.38, - "y": 288.38, - "z": 99.0 - }, - "tipDiameter": 5.59, - "tipLength": 48.25, - "tipVolume": 200.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "aspirate", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "43d5d251497da47db350309e9d0f34dd", - "notes": [], - "params": { - "flowRate": 716.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 200.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -37.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A5" - }, - "result": { - "position": { - "x": 50.3, - "y": 74.15, - "z": 22.95 - }, - "volume": 200.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "dispense", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "f49285a51e7d0e936653b2e5d52c3bea", - "notes": [], - "params": { - "flowRate": 716.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 200.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -37.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A5" - }, - "result": { - "position": { - "x": 50.3, - "y": 74.15, - "z": 22.95 - }, - "volume": 200.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "aspirate", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "5d4d46d37afbd6ae16e079c02fb55bbc", - "notes": [], - "params": { - "flowRate": 716.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 200.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -37.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A5" - }, - "result": { - "position": { - "x": 50.3, - "y": 74.15, - "z": 22.95 - }, - "volume": 200.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "dispense", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "65635c49da6868a4305fac7d5abf5425", - "notes": [], - "params": { - "flowRate": 716.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 200.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -37.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A5" - }, - "result": { - "position": { - "x": 50.3, - "y": 74.15, - "z": 22.95 - }, - "volume": 200.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "aspirate", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "b71e5cacbf131d7daa8a13fb5dd1cf07", - "notes": [], - "params": { - "flowRate": 716.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 200.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -37.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A5" - }, - "result": { - "position": { - "x": 50.3, - "y": 74.15, - "z": 22.95 - }, - "volume": 200.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "dispense", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "fedf2d26f50dc893ce9ee6b0d7eb9fb6", - "notes": [], - "params": { - "flowRate": 716.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 200.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -37.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A5" - }, - "result": { - "position": { - "x": 50.3, - "y": 74.15, - "z": 22.95 - }, - "volume": 200.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "aspirate", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "f1d932332dc990ca22ca874303e26873", - "notes": [], - "params": { - "flowRate": 716.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 200.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -37.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A5" - }, - "result": { - "position": { - "x": 50.3, - "y": 74.15, - "z": 22.95 - }, - "volume": 200.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "dispense", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "d25680d8647d4de548ebd009fab58608", - "notes": [], - "params": { - "flowRate": 716.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 200.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -37.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A5" - }, - "result": { - "position": { - "x": 50.3, - "y": 74.15, - "z": 22.95 - }, - "volume": 200.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "aspirate", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "9cd005defed08a0daad0d5b1bd4d03e8", - "notes": [], - "params": { - "flowRate": 716.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 200.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -37.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A5" - }, - "result": { - "position": { - "x": 50.3, - "y": 74.15, - "z": 22.95 - }, - "volume": 200.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "dispense", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "bf755956d5e9addade84d0cef3331b6e", - "notes": [], - "params": { - "flowRate": 716.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 200.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -37.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A5" - }, - "result": { - "position": { - "x": 50.3, - "y": 74.15, - "z": 22.95 - }, - "volume": 200.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "aspirate", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "d6786c7d0cd83c2509a9959f0f04a593", - "notes": [], - "params": { - "flowRate": 716.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 200.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -37.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A5" - }, - "result": { - "position": { - "x": 50.3, - "y": 74.15, - "z": 22.95 - }, - "volume": 200.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "dispense", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "a025fc0aa12c342b03016accb2973631", - "notes": [], - "params": { - "flowRate": 716.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 200.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -37.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A5" - }, - "result": { - "position": { - "x": 50.3, - "y": 74.15, - "z": 22.95 - }, - "volume": 200.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "aspirate", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "b47dfd3ccbda1a5b28a165e301a5a0f9", - "notes": [], - "params": { - "flowRate": 716.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 200.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -37.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A5" - }, - "result": { - "position": { - "x": 50.3, - "y": 74.15, - "z": 22.95 - }, - "volume": 200.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "dispense", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "216472b31cc3c3d454bbfd7d76b3b7ed", - "notes": [], - "params": { - "flowRate": 716.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 200.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -37.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A5" - }, - "result": { - "position": { - "x": 50.3, - "y": 74.15, - "z": 22.95 - }, - "volume": 200.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "aspirate", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "a4a946ea27a345fbb7e85ad53a45ade1", - "notes": [], - "params": { - "flowRate": 716.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 200.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -37.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A5" - }, - "result": { - "position": { - "x": 50.3, - "y": 74.15, - "z": 22.95 - }, - "volume": 200.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "dispense", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "4e8f5cfcd829473d768d8cc759a5dac2", - "notes": [], - "params": { - "flowRate": 716.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 200.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -37.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A5" - }, - "result": { - "position": { - "x": 50.3, - "y": 74.15, - "z": 22.95 - }, - "volume": 200.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "aspirate", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "3b94f21a0c88800073a4303564fc63b7", - "notes": [], - "params": { - "flowRate": 716.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 200.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -37.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A5" - }, - "result": { - "position": { - "x": 50.3, - "y": 74.15, - "z": 22.95 - }, - "volume": 200.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "dispense", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "1b4c2edaee80fe222e4a5b83472ddf1d", - "notes": [], - "params": { - "flowRate": 716.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 200.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -37.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A5" - }, - "result": { - "position": { - "x": 50.3, - "y": 74.15, - "z": 22.95 - }, - "volume": 200.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "aspirate", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "8e03cc5452fe5bd73f794968e234d70b", - "notes": [], - "params": { - "flowRate": 716.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 200.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -37.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A5" - }, - "result": { - "position": { - "x": 50.3, - "y": 74.15, - "z": 22.95 - }, - "volume": 200.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "dispense", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "5cc98b1e554ad32ca80cb5951f8161d7", - "notes": [], - "params": { - "flowRate": 716.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 200.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -37.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A5" - }, - "result": { - "position": { - "x": 50.3, - "y": 74.15, - "z": 22.95 - }, - "volume": 200.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "aspirate", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "c95e6f872df87081a47ea61603e9b4ca", - "notes": [], - "params": { - "flowRate": 716.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 200.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -37.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A5" - }, - "result": { - "position": { - "x": 50.3, - "y": 74.15, - "z": 22.95 - }, - "volume": 200.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "dispense", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "0ab71ffdb60d9e4794eb2a6deb883cf4", - "notes": [], - "params": { - "flowRate": 716.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 200.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -37.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A5" - }, - "result": { - "position": { - "x": 50.3, - "y": 74.15, - "z": 22.95 - }, - "volume": 200.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "aspirate", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "dc4eaa79925d9c73b6fbd25f5441dba9", - "notes": [], - "params": { - "flowRate": 716.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 200.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -37.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A5" - }, - "result": { - "position": { - "x": 50.3, - "y": 74.15, - "z": 22.95 - }, - "volume": 200.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "dispense", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "8da7cc1166fc139926f6c84baa8c70cc", - "notes": [], - "params": { - "flowRate": 716.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 200.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -37.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A5" - }, - "result": { - "position": { - "x": 50.3, - "y": 74.15, - "z": 22.95 - }, - "volume": 200.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "aspirate", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "5b6f914bea0c4ffdc1f04b6de79ef05b", - "notes": [], - "params": { - "flowRate": 716.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 200.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -37.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A5" - }, - "result": { - "position": { - "x": 50.3, - "y": 74.15, - "z": 22.95 - }, - "volume": 200.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "dispense", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "4ce0076f932a86c0082742c8e5e7467c", - "notes": [], - "params": { - "flowRate": 716.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 200.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -37.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A5" - }, - "result": { - "position": { - "x": 50.3, - "y": 74.15, - "z": 22.95 - }, - "volume": 200.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "aspirate", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "d928c7a3181f8a98f441585a86e9a972", - "notes": [], - "params": { - "flowRate": 716.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 200.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -37.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A5" - }, - "result": { - "position": { - "x": 50.3, - "y": 74.15, - "z": 22.95 - }, - "volume": 200.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "dispense", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "712e51b617d0f48491ce05e0058d50c0", - "notes": [], - "params": { - "flowRate": 716.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 200.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -37.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A5" - }, - "result": { - "position": { - "x": 50.3, - "y": 74.15, - "z": 22.95 - }, - "volume": 200.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "aspirate", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "89f3ac0193b80bcd369e8ecb32ba1546", - "notes": [], - "params": { - "flowRate": 716.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 200.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -37.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A5" - }, - "result": { - "position": { - "x": 50.3, - "y": 74.15, - "z": 22.95 - }, - "volume": 200.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "dispense", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "2702670c323384ed41cdad11001069f0", - "notes": [], - "params": { - "flowRate": 716.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 200.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -37.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A5" - }, - "result": { - "position": { - "x": 50.3, - "y": 74.15, - "z": 22.95 - }, - "volume": 200.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "aspirate", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "d0057ae136577bbd0ac88b74ef799412", - "notes": [], - "params": { - "flowRate": 716.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 200.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -37.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A5" - }, - "result": { - "position": { - "x": 50.3, - "y": 74.15, - "z": 22.95 - }, - "volume": 200.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "dispense", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "d83978b1ca8133a1bba1a15230260e3d", - "notes": [], - "params": { - "flowRate": 716.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 200.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -37.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A5" - }, - "result": { - "position": { - "x": 50.3, - "y": 74.15, - "z": 22.95 - }, - "volume": 200.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "aspirate", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "cb4fd30824e9e428b29558d180c71b46", - "notes": [], - "params": { - "flowRate": 716.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 200.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -37.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A5" - }, - "result": { - "position": { - "x": 50.3, - "y": 74.15, - "z": 22.95 - }, - "volume": 200.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "dispense", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "3acbc3464821ecc3e7280c0b35081001", - "notes": [], - "params": { - "flowRate": 716.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 200.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -37.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A5" - }, - "result": { - "position": { - "x": 50.3, - "y": 74.15, - "z": 22.95 - }, - "volume": 200.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "aspirate", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "9b8f8c48eae538d5ae472f988cd63797", - "notes": [], - "params": { - "flowRate": 716.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 200.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -37.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A5" - }, - "result": { - "position": { - "x": 50.3, - "y": 74.15, - "z": 22.95 - }, - "volume": 200.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "dispense", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "7be87f8236a14202c03d6e6252b17165", - "notes": [], - "params": { - "flowRate": 716.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 200.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -37.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A5" - }, - "result": { - "position": { - "x": 50.3, - "y": 74.15, - "z": 22.95 - }, - "volume": 200.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "aspirate", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "e9de57ce33c992b57fc0ca013dd166bf", - "notes": [], - "params": { - "flowRate": 716.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 200.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -37.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A5" - }, - "result": { - "position": { - "x": 50.3, - "y": 74.15, - "z": 22.95 - }, - "volume": 200.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "dispense", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "a78f8aa319c9395675cfdfd11d727ed2", - "notes": [], - "params": { - "flowRate": 716.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 200.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -37.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A5" - }, - "result": { - "position": { - "x": 50.3, - "y": 74.15, - "z": 22.95 - }, - "volume": 200.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "aspirate", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "48e844e531197d305f35b100f4222ec9", - "notes": [], - "params": { - "flowRate": 716.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 200.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -37.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A5" - }, - "result": { - "position": { - "x": 50.3, - "y": 74.15, - "z": 22.95 - }, - "volume": 200.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "dispense", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "912ca45f89f86e458cb1415ae6926fe0", - "notes": [], - "params": { - "flowRate": 716.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 200.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -37.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A5" - }, - "result": { - "position": { - "x": 50.3, - "y": 74.15, - "z": 22.95 - }, - "volume": 200.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "aspirate", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "377e827673e4ed63668be12d0ccd5fd6", - "notes": [], - "params": { - "flowRate": 716.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 90.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -37.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A5" - }, - "result": { - "position": { - "x": 50.3, - "y": 74.15, - "z": 22.95 - }, - "volume": 90.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "moveToWell", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "f618f13c5e3cc165d6606d115ad97d83", - "notes": [], - "params": { - "forceDirect": false, - "labwareId": "UUID", - "pipetteId": "UUID", - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -3.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A5" - }, - "result": { - "position": { - "x": 50.3, - "y": 74.15, - "z": 56.95 - } - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "dispense", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "cd81a730a89ff4b52dcb40b50feb3ceb", - "notes": [], - "params": { - "flowRate": 716.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 90.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -13.95 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 178.38, - "y": 74.24, - "z": 2.05 - }, - "volume": 90.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "aspirate", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "75669bcf0b341326a1d77f0c00436a88", - "notes": [], - "params": { - "flowRate": 716.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 100.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -13.95 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 178.38, - "y": 74.24, - "z": 2.05 - }, - "volume": 100.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "dispense", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "9168388b37a91f3d345226ab465dc5ee", - "notes": [], - "params": { - "flowRate": 716.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 100.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -13.95 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 178.38, - "y": 74.24, - "z": 2.05 - }, - "volume": 100.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "aspirate", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "139f3e84c7e8105b637c0272055fe353", - "notes": [], - "params": { - "flowRate": 716.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 100.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -13.95 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 178.38, - "y": 74.24, - "z": 2.05 - }, - "volume": 100.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "dispense", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "1a6b6222ba580a3f05861298b380d650", - "notes": [], - "params": { - "flowRate": 716.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 100.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -13.95 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 178.38, - "y": 74.24, - "z": 2.05 - }, - "volume": 100.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "aspirate", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "7633fe17308172238547ac37e87ed4ef", - "notes": [], - "params": { - "flowRate": 716.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 100.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -13.95 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 178.38, - "y": 74.24, - "z": 2.05 - }, - "volume": 100.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "dispense", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "d57db4e9aa442e5462704781bd4431bd", - "notes": [], - "params": { - "flowRate": 716.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 100.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -13.95 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 178.38, - "y": 74.24, - "z": 2.05 - }, - "volume": 100.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "aspirate", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "ba85e92133639b1920fe10d9972440c4", - "notes": [], - "params": { - "flowRate": 716.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 100.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -13.95 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 178.38, - "y": 74.24, - "z": 2.05 - }, - "volume": 100.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "dispense", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "cf2afde9c174df0484ba43de9c07306b", - "notes": [], - "params": { - "flowRate": 716.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 100.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -13.95 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 178.38, - "y": 74.24, - "z": 2.05 - }, - "volume": 100.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "aspirate", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "b496115ac86d35cea2168e5ad70548cd", - "notes": [], - "params": { - "flowRate": 716.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 100.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -13.95 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 178.38, - "y": 74.24, - "z": 2.05 - }, - "volume": 100.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "dispense", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "5eb6124ba5865aeede1f06c4e5744095", - "notes": [], - "params": { - "flowRate": 716.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 100.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -13.95 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 178.38, - "y": 74.24, - "z": 2.05 - }, - "volume": 100.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "aspirate", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "1fe26400f63a02249e4c68b0d424e780", - "notes": [], - "params": { - "flowRate": 716.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 100.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -13.95 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 178.38, - "y": 74.24, - "z": 2.05 - }, - "volume": 100.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "dispense", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "122fa7f2b2ac35027f935938ac50c8ab", - "notes": [], - "params": { - "flowRate": 716.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 100.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -13.95 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 178.38, - "y": 74.24, - "z": 2.05 - }, - "volume": 100.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "aspirate", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "05da1119ab89f4124a4b0566dee1ce0d", - "notes": [], - "params": { - "flowRate": 716.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 100.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -13.95 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 178.38, - "y": 74.24, - "z": 2.05 - }, - "volume": 100.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "dispense", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "0067c6aa4e4e4c88079d66322f1f414a", - "notes": [], - "params": { - "flowRate": 716.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 100.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -13.95 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 178.38, - "y": 74.24, - "z": 2.05 - }, - "volume": 100.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "aspirate", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "8533c051c7c1c3d40baa81ce816a87ea", - "notes": [], - "params": { - "flowRate": 716.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 100.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -13.95 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 178.38, - "y": 74.24, - "z": 2.05 - }, - "volume": 100.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "dispense", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "21796119f6ba48327ee82af43d70dedb", - "notes": [], - "params": { - "flowRate": 716.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 100.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -13.95 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 178.38, - "y": 74.24, - "z": 2.05 - }, - "volume": 100.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "aspirate", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "b69b894ad8c8f464e7fa039124fdb44f", - "notes": [], - "params": { - "flowRate": 716.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 100.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -13.95 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 178.38, - "y": 74.24, - "z": 2.05 - }, - "volume": 100.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "dispense", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "206d69df9193928f581d8049e9644896", - "notes": [], - "params": { - "flowRate": 716.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 100.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -13.95 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 178.38, - "y": 74.24, - "z": 2.05 - }, - "volume": 100.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "aspirate", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "4bc1dd42516b721e81fe511e76c1bce8", - "notes": [], - "params": { - "flowRate": 716.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 100.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -13.95 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 178.38, - "y": 74.24, - "z": 2.05 - }, - "volume": 100.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "dispense", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "6e01eb7ee577e28580043089ccebffbf", - "notes": [], - "params": { - "flowRate": 716.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 100.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -13.95 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 178.38, - "y": 74.24, - "z": 2.05 - }, - "volume": 100.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "moveToWell", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "d70cf2f99fcfd1a78d2918c2a3640277", - "notes": [], - "params": { - "forceDirect": false, - "labwareId": "UUID", - "pipetteId": "UUID", - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -3.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 178.38, - "y": 74.24, - "z": 13.0 - } - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "waitForDuration", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "b7ac87f3143a8e94167c971444ea082f", - "notes": [], - "params": { - "seconds": 2.0 - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "blowout", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "74e0fde95013cef52a4b86aca1e31911", - "notes": [], - "params": { - "flowRate": 716.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -3.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 178.38, - "y": 74.24, - "z": 13.0 - } - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "dropTip", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "2167c790163b647e1673430d8407acbe", - "notes": [], - "params": { - "alternateDropLocation": true, - "labwareId": "UUID", - "pipetteId": "UUID", - "wellLocation": { - "offset": { - "x": 0, - "y": 0, - "z": 0 - }, - "origin": "default" - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 509.25, - "y": 364.0, - "z": 40.0 - } - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "waitForDuration", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "2fe1007571bb088ac9fddbe0e59b54aa", - "notes": [], - "params": { - "seconds": 300.0 - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "moveLabware", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "ef7903a646d62d50c9d0cfc39aa7c364", - "notes": [], - "params": { - "labwareId": "UUID", - "newLocation": { - "moduleId": "UUID" - }, - "strategy": "usingGripper" - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "waitForDuration", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "d0001ee8a8bb6a3a3065465468162f92", - "notes": [], - "params": { - "seconds": 180.0 - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "comment", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "1a990ae34fde9280ff36803d451ead8b", - "notes": [], - "params": { - "message": "\n\n----------REMOVE SUPER----------\n" - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "pickUpTip", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "6d4d0312b5bfe8f4fdf1c50d16bcded9", - "notes": [], - "params": { - "labwareId": "UUID", - "pipetteId": "UUID", - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": 0.0 - }, - "origin": "top" - }, - "wellName": "A6" - }, - "result": { - "position": { - "x": 223.38, - "y": 288.38, - "z": 99.0 - }, - "tipDiameter": 5.59, - "tipLength": 48.25, - "tipVolume": 200.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "aspirate", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "35d5ea9b45c6e4fe04eaefbe07faa4ca", - "notes": [], - "params": { - "flowRate": 107.39999999999999, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 140.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -13.949999999999996 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 342.38, - "y": 181.24, - "z": 36.510000000000005 - }, - "volume": 140.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "aspirate", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "c83da5f07fe58aa4acf6e71a83840a1e", - "notes": [], - "params": { - "flowRate": 107.39999999999999, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 10.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -14.149999999999999 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 342.38, - "y": 181.24, - "z": 36.31 - }, - "volume": 10.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "moveToWell", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "34f5ef829533277943b396863d2d3090", - "notes": [], - "params": { - "forceDirect": false, - "labwareId": "UUID", - "pipetteId": "UUID", - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -3.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 342.38, - "y": 181.24, - "z": 47.459999999999994 - } - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "dispense", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "561ee6926eb74c52d372fe3bc0c2b891", - "notes": [], - "params": { - "flowRate": 716.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 150.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -3.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 178.38, - "y": 363.78, - "z": 28.400000000000002 - }, - "volume": 150.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "waitForDuration", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "f4bea182967bfe77f477c8f134f6edd2", - "notes": [], - "params": { - "seconds": 2.0 - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "blowout", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "4d724015d6e939461f04f9b8d3bcf44a", - "notes": [], - "params": { - "flowRate": 716.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -3.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 178.38, - "y": 363.78, - "z": 28.400000000000002 - } - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "dropTip", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "f7cf56fe34fc2fdea9799801b8399a0e", - "notes": [], - "params": { - "alternateDropLocation": true, - "labwareId": "UUID", - "pipetteId": "UUID", - "wellLocation": { - "offset": { - "x": 0, - "y": 0, - "z": 0 - }, - "origin": "default" - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 359.25, - "y": 364.0, - "z": 40.0 - } - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "comment", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "fc1348cb1ac415a29f8f4f565aded186", - "notes": [], - "params": { - "message": "\n\n----------TWO WASHES----------\n" - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "pickUpTip", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "8017ae087c370713f8e84e54e27337da", - "notes": [], - "params": { - "labwareId": "UUID", - "pipetteId": "UUID", - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": 0.0 - }, - "origin": "top" - }, - "wellName": "A7" - }, - "result": { - "position": { - "x": 232.38, - "y": 288.38, - "z": 99.0 - }, - "tipDiameter": 5.59, - "tipLength": 48.25, - "tipVolume": 200.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "aspirate", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "cda0933085243585cf946275be748ccd", - "notes": [], - "params": { - "flowRate": 716.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 150.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -37.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A3" - }, - "result": { - "position": { - "x": 32.3, - "y": 74.15, - "z": 22.95 - }, - "volume": 150.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "moveToWell", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "d9ca89370731eefd12eab9fd6077bc55", - "notes": [], - "params": { - "forceDirect": false, - "labwareId": "UUID", - "pipetteId": "UUID", - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -3.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A3" - }, - "result": { - "position": { - "x": 32.3, - "y": 74.15, - "z": 56.95 - } - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "dispense", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "d0652d79b21be209840a2a681e44ddc6", - "notes": [], - "params": { - "flowRate": 716.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 150.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": 0.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 342.38, - "y": 181.24, - "z": 50.459999999999994 - }, - "volume": 150.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "moveToWell", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "4a7e59dbe17b1adf900c5a6d9f2e7e45", - "notes": [], - "params": { - "forceDirect": false, - "labwareId": "UUID", - "pipetteId": "UUID", - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": 20.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 342.38, - "y": 181.24, - "z": 70.46 - } - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "waitForDuration", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "36c3086a297b20a85ef8232b0c21b51a", - "notes": [], - "params": { - "seconds": 30.0 - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "aspirate", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "7bbcfa08c751fdaba7d6d581370d2955", - "notes": [], - "params": { - "flowRate": 107.39999999999999, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 150.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -13.949999999999996 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 342.38, - "y": 181.24, - "z": 36.510000000000005 - }, - "volume": 150.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "aspirate", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "b7308b04d29fd37cc9be1436c087d9b4", - "notes": [], - "params": { - "flowRate": 107.39999999999999, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 10.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -14.149999999999999 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 342.38, - "y": 181.24, - "z": 36.31 - }, - "volume": 10.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "moveToWell", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "35d196d5fe5cabfd879b20a2c61cdb10", - "notes": [], - "params": { - "forceDirect": false, - "labwareId": "UUID", - "pipetteId": "UUID", - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -3.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 342.38, - "y": 181.24, - "z": 47.459999999999994 - } - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "dispense", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "5837f318dfb28b3436b919706eb936c3", - "notes": [], - "params": { - "flowRate": 716.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 160.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -3.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 178.38, - "y": 363.78, - "z": 28.400000000000002 - }, - "volume": 160.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "waitForDuration", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "bf173b3c58d4fb352e0372d47213119d", - "notes": [], - "params": { - "seconds": 2.0 - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "blowout", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "02cd32879185c6bfb84a65d23807a880", - "notes": [], - "params": { - "flowRate": 716.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -3.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 178.38, - "y": 363.78, - "z": 28.400000000000002 - } - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "dropTip", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "15ea5c4abc794d6e7c634b6588a6e9c1", - "notes": [], - "params": { - "alternateDropLocation": true, - "labwareId": "UUID", - "pipetteId": "UUID", - "wellLocation": { - "offset": { - "x": 0, - "y": 0, - "z": 0 - }, - "origin": "default" - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 509.25, - "y": 364.0, - "z": 40.0 - } - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "pickUpTip", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "3cf3b07d8998d888692eb223b05060df", - "notes": [], - "params": { - "labwareId": "UUID", - "pipetteId": "UUID", - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": 0.0 - }, - "origin": "top" - }, - "wellName": "A8" - }, - "result": { - "position": { - "x": 241.38, - "y": 288.38, - "z": 99.0 - }, - "tipDiameter": 5.59, - "tipLength": 48.25, - "tipVolume": 200.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "aspirate", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "04961eb23d9f6254142ee16ab14248a8", - "notes": [], - "params": { - "flowRate": 716.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 150.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -37.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A3" - }, - "result": { - "position": { - "x": 32.3, - "y": 74.15, - "z": 22.95 - }, - "volume": 150.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "moveToWell", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "d70bae5f325b508b99b8dd3e0138d96f", - "notes": [], - "params": { - "forceDirect": false, - "labwareId": "UUID", - "pipetteId": "UUID", - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -3.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A3" - }, - "result": { - "position": { - "x": 32.3, - "y": 74.15, - "z": 56.95 - } - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "dispense", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "672debfdee77318ffff1b2cff21ccab7", - "notes": [], - "params": { - "flowRate": 716.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 150.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": 0.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 342.38, - "y": 181.24, - "z": 50.459999999999994 - }, - "volume": 150.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "moveToWell", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "66093f66f5aa96e91c39f0e3c01e46cd", - "notes": [], - "params": { - "forceDirect": false, - "labwareId": "UUID", - "pipetteId": "UUID", - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": 20.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 342.38, - "y": 181.24, - "z": 70.46 - } - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "waitForDuration", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "347c5121278c2810e041c94cff7db2e3", - "notes": [], - "params": { - "seconds": 30.0 - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "aspirate", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "cee3f5ea84293d86b1ba85b9e338813b", - "notes": [], - "params": { - "flowRate": 107.39999999999999, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 150.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -13.949999999999996 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 342.38, - "y": 181.24, - "z": 36.510000000000005 - }, - "volume": 150.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "aspirate", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "fac47a9fd8d07b04483f636217bf69d1", - "notes": [], - "params": { - "flowRate": 107.39999999999999, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 10.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -14.149999999999999 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 342.38, - "y": 181.24, - "z": 36.31 - }, - "volume": 10.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "moveToWell", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "9d8ac359ccb52841dfb3e4df5a1f4a18", - "notes": [], - "params": { - "forceDirect": false, - "labwareId": "UUID", - "pipetteId": "UUID", - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -3.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 342.38, - "y": 181.24, - "z": 47.459999999999994 - } - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "dispense", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "585038ade3637a88d64e4608c4bb5ee4", - "notes": [], - "params": { - "flowRate": 716.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 160.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -3.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 178.38, - "y": 363.78, - "z": 28.400000000000002 - }, - "volume": 160.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "waitForDuration", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "ba28d9ce128a5b4b3489322a661b3651", - "notes": [], - "params": { - "seconds": 2.0 - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "blowout", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "3d2f711f065ca932e8230b8aa3aae811", - "notes": [], - "params": { - "flowRate": 716.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -3.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 178.38, - "y": 363.78, - "z": 28.400000000000002 - } - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "dropTip", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "9e9947e94d00cd7af2fbd93da9f0a80b", - "notes": [], - "params": { - "alternateDropLocation": true, - "labwareId": "UUID", - "pipetteId": "UUID", - "wellLocation": { - "offset": { - "x": 0, - "y": 0, - "z": 0 - }, - "origin": "default" - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 359.25, - "y": 364.0, - "z": 40.0 - } - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "waitForDuration", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "aceac2839f511dd95c277edef67d8fb7", - "notes": [], - "params": { - "seconds": 120.0 - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "comment", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "ca67c1c88c455a5aba1959cd1caf8f2a", - "notes": [], - "params": { - "message": "\n\n----------ADDING ELUTION BUFFER----------\n" - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "moveLabware", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "5eca5b4fc517fd179f00584d01536884", - "notes": [], - "params": { - "labwareId": "UUID", - "newLocation": { - "slotName": "D2" - }, - "strategy": "usingGripper" - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "pickUpTip", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "32bbdbea3b7c9013fa48ade4fe3324fb", - "notes": [], - "params": { - "labwareId": "UUID", - "pipetteId": "UUID", - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": 0.0 - }, - "origin": "top" - }, - "wellName": "A9" - }, - "result": { - "position": { - "x": 86.38, - "y": 181.38, - "z": 99.0 - }, - "tipDiameter": 5.58, - "tipLength": 47.849999999999994, - "tipVolume": 50.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "aspirate", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "5a315c4cd14cb77926724b952a7fbead", - "notes": [], - "params": { - "flowRate": 35.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 19.5, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -37.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A6" - }, - "result": { - "position": { - "x": 59.3, - "y": 74.15, - "z": 22.95 - }, - "volume": 19.5 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "dispense", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "17ec51833aa27af96c25ee6505183966", - "notes": [], - "params": { - "flowRate": 57.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 19.5, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -13.95 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 178.38, - "y": 74.24, - "z": 2.05 - }, - "volume": 19.5 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "aspirate", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "9f24e834779a37078144cf3156cd3db8", - "notes": [], - "params": { - "flowRate": 35.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 15.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -14.15 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 178.38, - "y": 74.24, - "z": 1.849999999999999 - }, - "volume": 15.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "dispense", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "f33cf695ecd2531478a68d2802e148f7", - "notes": [], - "params": { - "flowRate": 114.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 15.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -8.95 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 178.38, - "y": 74.24, - "z": 7.05 - }, - "volume": 15.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "aspirate", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "c429821867b899d5c77969f6fbaacc9f", - "notes": [], - "params": { - "flowRate": 35.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 15.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -14.15 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 178.38, - "y": 74.24, - "z": 1.849999999999999 - }, - "volume": 15.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "dispense", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "74b9adbac35a2a3a642e07a860b1c426", - "notes": [], - "params": { - "flowRate": 114.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 15.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -8.95 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 178.38, - "y": 74.24, - "z": 7.05 - }, - "volume": 15.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "aspirate", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "24733fb2d4d18cf9508abd39983f3d38", - "notes": [], - "params": { - "flowRate": 35.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 15.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -14.15 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 178.38, - "y": 74.24, - "z": 1.849999999999999 - }, - "volume": 15.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "dispense", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "a3ec8da86c6e0a867d6969d1297611a9", - "notes": [], - "params": { - "flowRate": 114.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 15.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -8.95 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 178.38, - "y": 74.24, - "z": 7.05 - }, - "volume": 15.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "aspirate", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "404694e8951bb51adca17738330711d3", - "notes": [], - "params": { - "flowRate": 35.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 15.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -14.15 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 178.38, - "y": 74.24, - "z": 1.849999999999999 - }, - "volume": 15.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "dispense", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "11d60469d4437e94279c257149f593da", - "notes": [], - "params": { - "flowRate": 114.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 15.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -8.95 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 178.38, - "y": 74.24, - "z": 7.05 - }, - "volume": 15.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "aspirate", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "fdf47b41ab2d95b5f5814f29947d2a3e", - "notes": [], - "params": { - "flowRate": 35.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 15.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -14.15 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 178.38, - "y": 74.24, - "z": 1.849999999999999 - }, - "volume": 15.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "dispense", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "a7ba68c365c32dd9f9bf5efa42268ead", - "notes": [], - "params": { - "flowRate": 114.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 15.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -8.95 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 178.38, - "y": 74.24, - "z": 7.05 - }, - "volume": 15.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "aspirate", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "7d92a9f1e96414b17520cfc4d0a39df4", - "notes": [], - "params": { - "flowRate": 35.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 15.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -14.15 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 178.38, - "y": 74.24, - "z": 1.849999999999999 - }, - "volume": 15.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "dispense", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "c01967532b5744a89d4ae61978d6e3f3", - "notes": [], - "params": { - "flowRate": 114.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 15.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -8.95 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 178.38, - "y": 74.24, - "z": 7.05 - }, - "volume": 15.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "aspirate", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "ea1d41201d32add509fd9ec69cce92a4", - "notes": [], - "params": { - "flowRate": 35.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 15.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -14.15 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 178.38, - "y": 74.24, - "z": 1.849999999999999 - }, - "volume": 15.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "dispense", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "87f5ad959bf21caa837f7be34cb2acbc", - "notes": [], - "params": { - "flowRate": 114.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 15.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -8.95 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 178.38, - "y": 74.24, - "z": 7.05 - }, - "volume": 15.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "aspirate", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "53644c8767ea650bb4a2978c1694951f", - "notes": [], - "params": { - "flowRate": 52.5, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 14.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -14.25 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 178.38, - "y": 74.24, - "z": 1.7499999999999993 - }, - "volume": 14.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "dispense", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "3a1e1283bc33c1ffc45046a245e7deff", - "notes": [], - "params": { - "flowRate": 85.5, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 14.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -14.25 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 178.38, - "y": 74.24, - "z": 1.7499999999999993 - }, - "volume": 14.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "aspirate", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "613dc6176dd81f519d7271bb1af5bc5a", - "notes": [], - "params": { - "flowRate": 52.5, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 14.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -14.25 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 178.38, - "y": 74.24, - "z": 1.7499999999999993 - }, - "volume": 14.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "dispense", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "2ea46cbed07c4b44ee476894708bc003", - "notes": [], - "params": { - "flowRate": 85.5, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 14.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -14.25 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 178.38, - "y": 74.24, - "z": 1.7499999999999993 - }, - "volume": 14.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "aspirate", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "c3aa22272f7134a79ddffd9136359c37", - "notes": [], - "params": { - "flowRate": 52.5, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 14.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -14.25 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 178.38, - "y": 74.24, - "z": 1.7499999999999993 - }, - "volume": 14.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "dispense", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "6a81677c234f3abe6904d8aa3abab446", - "notes": [], - "params": { - "flowRate": 85.5, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 14.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -14.25 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 178.38, - "y": 74.24, - "z": 1.7499999999999993 - }, - "volume": 14.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "aspirate", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "705582b79c3d98faa603533ac103d651", - "notes": [], - "params": { - "flowRate": 52.5, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 14.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -14.25 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 178.38, - "y": 74.24, - "z": 1.7499999999999993 - }, - "volume": 14.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "dispense", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "f25fb64699eac468c88e4806f5bb3916", - "notes": [], - "params": { - "flowRate": 85.5, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 14.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -14.25 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 178.38, - "y": 74.24, - "z": 1.7499999999999993 - }, - "volume": 14.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "aspirate", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "b9b463d95c4eefeb5814e9982affce67", - "notes": [], - "params": { - "flowRate": 52.5, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 14.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -14.25 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 178.38, - "y": 74.24, - "z": 1.7499999999999993 - }, - "volume": 14.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "dispense", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "7d467408fd88a0136fb6c7456c7cc94a", - "notes": [], - "params": { - "flowRate": 85.5, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 14.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -14.25 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 178.38, - "y": 74.24, - "z": 1.7499999999999993 - }, - "volume": 14.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "moveToWell", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "fea7c6fb5de7e3dcd4bf4957de66f05e", - "notes": [], - "params": { - "forceDirect": false, - "labwareId": "UUID", - "pipetteId": "UUID", - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": 0.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 178.38, - "y": 74.24, - "z": 16.0 - } - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "waitForDuration", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "cc92fcbd002554e2ce0855f343a3a68e", - "notes": [], - "params": { - "seconds": 1.0 - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "blowout", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "017af87721e992b5157fb78be01033b2", - "notes": [], - "params": { - "flowRate": 57.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": 0.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 178.38, - "y": 74.24, - "z": 16.0 - } - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "touchTip", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "d61eb6e9d9b3183f747f70ce29828ff7", - "notes": [], - "params": { - "labwareId": "UUID", - "pipetteId": "UUID", - "radius": 1.0, - "speed": 60.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -1.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 178.38, - "y": 74.24, - "z": 15.0 - } - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "dropTip", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "ddda43ec173555ea3b16b98ad6d2c39e", - "notes": [], - "params": { - "alternateDropLocation": true, - "labwareId": "UUID", - "pipetteId": "UUID", - "wellLocation": { - "offset": { - "x": 0, - "y": 0, - "z": 0 - }, - "origin": "default" - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 412.25, - "y": 364.0, - "z": 40.0 - } - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "moveLabware", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "0367269519dab3d917ff70e4c2eb3ac5", - "notes": [], - "params": { - "labwareId": "UUID", - "newLocation": { - "moduleId": "UUID" - }, - "strategy": "usingGripper" - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "waitForDuration", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "c92eb88b6abd64aee972dffe5e25327a", - "notes": [], - "params": { - "seconds": 120.0 - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "comment", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "fff71392918a3b8f2f00bdefa4a53134", - "notes": [], - "params": { - "message": "\n\n----------TRANSFERRING ELUTE----------\n" - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "pickUpTip", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "1211dc66481f76a8e1dd9cc3a16fc306", - "notes": [], - "params": { - "labwareId": "UUID", - "pipetteId": "UUID", - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": 0.0 - }, - "origin": "top" - }, - "wellName": "A10" - }, - "result": { - "position": { - "x": 95.38, - "y": 181.38, - "z": 99.0 - }, - "tipDiameter": 5.58, - "tipLength": 47.849999999999994, - "tipVolume": 50.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "aspirate", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "02042c1eb009e7757551c7d6a5e2493f", - "notes": [], - "params": { - "flowRate": 7.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 17.5, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -14.149999999999999 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 342.38, - "y": 181.24, - "z": 36.31 - }, - "volume": 17.5 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "dispense", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "f88f2a7423ac5f5070ba46bd9989126c", - "notes": [], - "params": { - "flowRate": 57.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 17.5, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -13.95 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": -5.624999999999998, - "y": 356.2, - "z": 2.3100000000000014 - }, - "volume": 17.5 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "moveToWell", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "03a1b309d898cee327e0b3dd131a7507", - "notes": [], - "params": { - "forceDirect": false, - "labwareId": "UUID", - "pipetteId": "UUID", - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": 0.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 342.38, - "y": 181.24, - "z": 50.459999999999994 - } - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "blowout", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "674540c74c2dc67ded15c1b937ef28fb", - "notes": [], - "params": { - "flowRate": 57.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": 0.0 - }, - "origin": "top", - "volumeOffset": 0.0 - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": -5.624999999999998, - "y": 356.2, - "z": 16.26 - } - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "dropTip", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "6498d67c1416ebe7d0b8c12d8b8d3c87", - "notes": [], - "params": { - "alternateDropLocation": true, - "labwareId": "UUID", - "pipetteId": "UUID", - "wellLocation": { - "offset": { - "x": 0, - "y": 0, - "z": 0 - }, - "origin": "default" - }, - "wellName": "A1" - }, - "result": { - "position": { - "x": 359.25, - "y": 364.0, - "z": 40.0 - } - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - } - ], - "config": { - "apiVersion": [ - 2, - 15 - ], - "protocolType": "python" - }, - "createdAt": "TIMESTAMP", - "errors": [], - "files": [ - { - "name": "pl_langone_ribo_pt1_ramp.py", - "role": "main" - } - ], - "labware": [ - { - "definitionUri": "opentrons/opentrons_1_trash_3200ml_fixed/1", - "id": "UUID", - "loadName": "opentrons_1_trash_3200ml_fixed", - "location": { - "slotName": "A3" - } - }, - { - "definitionUri": "opentrons/opentrons_96_wellplate_200ul_pcr_full_skirt/2", - "id": "UUID", - "loadName": "opentrons_96_wellplate_200ul_pcr_full_skirt", - "location": "offDeck" - }, - { - "definitionUri": "opentrons/opentrons_96_wellplate_200ul_pcr_full_skirt/2", - "id": "UUID", - "loadName": "opentrons_96_wellplate_200ul_pcr_full_skirt", - "location": { - "moduleId": "UUID" - } - }, - { - "definitionUri": "opentrons/opentrons_96_wellplate_200ul_pcr_full_skirt/2", - "id": "UUID", - "loadName": "opentrons_96_wellplate_200ul_pcr_full_skirt", - "location": { - "moduleId": "UUID" - } - }, - { - "definitionUri": "opentrons/nest_96_wellplate_2ml_deep/2", - "id": "UUID", - "loadName": "nest_96_wellplate_2ml_deep", - "location": { - "moduleId": "UUID" - } - }, - { - "definitionUri": "opentrons/nest_96_wellplate_2ml_deep/2", - "displayName": "8", - "id": "UUID", - "loadName": "nest_96_wellplate_2ml_deep", - "location": { - "moduleId": "UUID" - } - }, - { - "definitionUri": "opentrons/nest_12_reservoir_15ml/1", - "id": "UUID", - "loadName": "nest_12_reservoir_15ml", - "location": { - "slotName": "A2" - } - }, - { - "definitionUri": "opentrons/opentrons_flex_96_tiprack_50ul/1", - "id": "UUID", - "loadName": "opentrons_flex_96_tiprack_50ul", - "location": { - "slotName": "C1" - } - }, - { - "definitionUri": "opentrons/opentrons_flex_96_tiprack_50ul/1", - "id": "UUID", - "loadName": "opentrons_flex_96_tiprack_50ul", - "location": { - "slotName": "C2" - } - }, - { - "definitionUri": "opentrons/opentrons_flex_96_tiprack_200ul/1", - "id": "UUID", - "loadName": "opentrons_flex_96_tiprack_200ul", - "location": { - "slotName": "B2" - } - }, - { - "definitionUri": "opentrons/opentrons_flex_96_tiprack_200ul/1", - "id": "UUID", - "loadName": "opentrons_flex_96_tiprack_200ul", - "location": { - "slotName": "B3" - } - } - ], - "liquids": [ - { - "description": "DB1/DP1", - "displayColor": "#7EFF42", - "displayName": "DB1/DP1", - "id": "UUID" - }, - { - "description": "RDB/RDE", - "displayColor": "#50D5FF", - "displayName": "RDB/RDE", - "id": "UUID" - }, - { - "description": "PRB/PRE", - "displayColor": "#FF4F4F", - "displayName": "PRB/PRE", - "id": "UUID" - }, - { - "description": "EPH3", - "displayColor": "#B925FF", - "displayName": "EPH3", - "id": "UUID" - }, - { - "description": "FSA/RVT", - "displayColor": "#FF9900", - "displayName": "FSA/RVT", - "id": "UUID" - }, - { - "description": "SMM", - "displayColor": "#0019FF", - "displayName": "SMM", - "id": "UUID" - }, - { - "description": "BEADS", - "displayColor": "#007AFF", - "displayName": "BEADS", - "id": "UUID" - }, - { - "description": "ETHANOL", - "displayColor": "#FF0076", - "displayName": "ETHANOL", - "id": "UUID" - }, - { - "description": "ELUTION BUFFER", - "displayColor": "#00FFBC", - "displayName": "ELUTION BUFFER", - "id": "UUID" - }, - { - "description": "RSB", - "displayColor": "#00AAFF", - "displayName": "RSB", - "id": "UUID" - }, - { - "description": "Sample", - "displayColor": "#008000", - "displayName": "Sample", - "id": "UUID" - }, - { - "description": "Mastermix", - "displayColor": "#008000", - "displayName": "Mastermix", - "id": "UUID" - }, - { - "description": "Water", - "displayColor": "#A52A2A", - "displayName": "Water", - "id": "UUID" - }, - { - "description": "DNA", - "displayColor": "#A52A2A", - "displayName": "DNA", - "id": "UUID" - } - ], - "metadata": { - "author": "Rami Farawi ", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[4b9e0f93d9][OT2_S_v2_20_8_None_PARTIAL_COLUMN_HappyPathMixedTipRacks].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[4b9e0f93d9][OT2_S_v2_20_8_None_PARTIAL_COLUMN_HappyPathMixedTipRacks].json index 858286887b6..2c0e359ce5f 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[4b9e0f93d9][OT2_S_v2_20_8_None_PARTIAL_COLUMN_HappyPathMixedTipRacks].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[4b9e0f93d9][OT2_S_v2_20_8_None_PARTIAL_COLUMN_HappyPathMixedTipRacks].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -19714,6 +19715,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "description": "OT-2 protocol with 1ch and 8ch pipette partial/single tip configurations. Mixing tipracks and using separate tipracks. ", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[4cb705bdbf][Flex_X_v2_16_NO_PIPETTES_MM_MagneticModuleInFlexProtocol].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[4cb705bdbf][Flex_X_v2_16_NO_PIPETTES_MM_MagneticModuleInFlexProtocol].json index d810bd75c88..dd52de35f74 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[4cb705bdbf][Flex_X_v2_16_NO_PIPETTES_MM_MagneticModuleInFlexProtocol].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[4cb705bdbf][Flex_X_v2_16_NO_PIPETTES_MM_MagneticModuleInFlexProtocol].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -103,6 +104,7 @@ } ], "labware": [], + "liquidClasses": [], "liquids": [], "metadata": { "author": "Derek Maggio ", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[4ea9d66206][Flex_X_v2_20_96_and_8_Overrides_InvalidConfigs_Override_eight_partial_column_no_end].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[4ea9d66206][Flex_X_v2_20_96_and_8_Overrides_InvalidConfigs_Override_eight_partial_column_no_end].json index 90bfa119fb7..25f1fc6f062 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[4ea9d66206][Flex_X_v2_20_96_and_8_Overrides_InvalidConfigs_Override_eight_partial_column_no_end].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[4ea9d66206][Flex_X_v2_20_96_and_8_Overrides_InvalidConfigs_Override_eight_partial_column_no_end].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -1241,6 +1242,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "description": "oooo", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[4f50c02c81][Flex_S_v2_19_AMPure_XP_48x].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[4f50c02c81][Flex_S_v2_19_AMPure_XP_48x].json index 3af042768f6..01407d2b421 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[4f50c02c81][Flex_S_v2_19_AMPure_XP_48x].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[4f50c02c81][Flex_S_v2_19_AMPure_XP_48x].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -4031,6 +4032,11 @@ }, "schemaVersion": 2, "stackingOffsetWithLabware": { + "evotips_flex_96_tiprack_adapter": { + "x": 0, + "y": 0, + "z": 102 + }, "opentrons_96_deep_well_adapter": { "x": 0, "y": 0, @@ -15813,9 +15819,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -16213,9 +16219,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -16613,9 +16619,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -19715,9 +19721,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -20115,9 +20121,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -20515,9 +20521,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -22430,9 +22436,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -22830,9 +22836,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -23230,9 +23236,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -24131,9 +24137,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -24495,9 +24501,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -24859,9 +24865,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -26373,9 +26379,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -26569,9 +26575,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -26765,9 +26771,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -28213,6 +28219,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "AMPure Beads", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[4fadc166c0][Flex_X_v2_18_NO_PIPETTES_Overrides_BadTypesInRTP_Override_wrong_type_in_variable_name].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[4fadc166c0][Flex_X_v2_18_NO_PIPETTES_Overrides_BadTypesInRTP_Override_wrong_type_in_variable_name].json index 843078fa552..0e9136c426d 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[4fadc166c0][Flex_X_v2_18_NO_PIPETTES_Overrides_BadTypesInRTP_Override_wrong_type_in_variable_name].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[4fadc166c0][Flex_X_v2_18_NO_PIPETTES_Overrides_BadTypesInRTP_Override_wrong_type_in_variable_name].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [], "config": { "apiVersion": [ @@ -42,6 +43,7 @@ } ], "labware": [], + "liquidClasses": [], "liquids": [], "metadata": { "protocolName": "Description Too Long 2.18" diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[50d7be4518][pl_Zymo_Quick_RNA_Cells_Flex_multi].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[50d7be4518][pl_Zymo_Quick_RNA_Cells_Flex_multi].json index dfc888c15b5..00eb89d9c69 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[50d7be4518][pl_Zymo_Quick_RNA_Cells_Flex_multi].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[50d7be4518][pl_Zymo_Quick_RNA_Cells_Flex_multi].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -828,6 +829,11 @@ }, "schemaVersion": 2, "stackingOffsetWithLabware": { + "evotips_flex_96_tiprack_adapter": { + "x": 0, + "y": 0, + "z": 102 + }, "opentrons_96_deep_well_adapter": { "x": 0, "y": 0, @@ -14861,9 +14867,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -15727,9 +15733,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -16593,9 +16599,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -17459,9 +17465,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -19680,9 +19686,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -20546,9 +20552,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -21412,9 +21418,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -22278,9 +22284,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -23420,6 +23426,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "Cells in DNA/ RNA Shield", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[51a761307d][Flex_X_v2_18_NO_PIPETTES_Overrides_DefaultOutOfRangeRTP_Override_default_greater_than_maximum].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[51a761307d][Flex_X_v2_18_NO_PIPETTES_Overrides_DefaultOutOfRangeRTP_Override_default_greater_than_maximum].json index d2955132ff2..c702bc58d97 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[51a761307d][Flex_X_v2_18_NO_PIPETTES_Overrides_DefaultOutOfRangeRTP_Override_default_greater_than_maximum].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[51a761307d][Flex_X_v2_18_NO_PIPETTES_Overrides_DefaultOutOfRangeRTP_Override_default_greater_than_maximum].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [], "config": { "apiVersion": [ @@ -42,6 +43,7 @@ } ], "labware": [], + "liquidClasses": [], "liquids": [], "metadata": { "protocolName": "Default not in range" diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[51fc977577][OT2_S_v6_P300M_P20S_MixTransferManyLiquids].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[51fc977577][OT2_S_v6_P300M_P20S_MixTransferManyLiquids].json index 2b447932025..edb78f79ace 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[51fc977577][OT2_S_v6_P300M_P20S_MixTransferManyLiquids].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[51fc977577][OT2_S_v6_P300M_P20S_MixTransferManyLiquids].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -3389,9 +3390,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -3424,8 +3425,8 @@ "volume": 100.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 1.0 }, "origin": "bottom", @@ -3458,8 +3459,8 @@ "volume": 100.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -3491,9 +3492,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -3521,9 +3522,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -3556,8 +3557,8 @@ "volume": 100.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 1.0 }, "origin": "bottom", @@ -3590,8 +3591,8 @@ "volume": 100.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -3623,9 +3624,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -3653,9 +3654,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -3688,8 +3689,8 @@ "volume": 150.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 1.0 }, "origin": "bottom", @@ -3722,8 +3723,8 @@ "volume": 150.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -3755,9 +3756,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -3785,9 +3786,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -3820,8 +3821,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 1.0 }, "origin": "bottom", @@ -3854,8 +3855,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -3887,9 +3888,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -3917,9 +3918,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -3952,8 +3953,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 1.0 }, "origin": "bottom", @@ -3986,8 +3987,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -4019,9 +4020,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -4049,9 +4050,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -4084,8 +4085,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 1.0 }, "origin": "bottom", @@ -4118,8 +4119,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -4151,9 +4152,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -4181,9 +4182,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -4216,8 +4217,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 1.0 }, "origin": "bottom", @@ -4250,8 +4251,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -4283,9 +4284,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -4313,9 +4314,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -4348,8 +4349,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 1.0 }, "origin": "bottom", @@ -4382,8 +4383,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -4415,9 +4416,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -4445,9 +4446,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -4480,8 +4481,8 @@ "volume": 300.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -4514,8 +4515,8 @@ "volume": 300.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -4548,8 +4549,8 @@ "volume": 300.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -4582,8 +4583,8 @@ "volume": 300.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -4616,8 +4617,8 @@ "volume": 300.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -4650,8 +4651,8 @@ "volume": 300.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -4684,8 +4685,8 @@ "volume": 300.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -4718,8 +4719,8 @@ "volume": 300.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -4752,8 +4753,8 @@ "volume": 300.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -4786,8 +4787,8 @@ "volume": 300.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -4820,8 +4821,8 @@ "volume": 300.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -4854,8 +4855,8 @@ "volume": 300.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -4888,8 +4889,8 @@ "volume": 300.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -4922,8 +4923,8 @@ "volume": 300.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -4956,8 +4957,8 @@ "volume": 300.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -4990,8 +4991,8 @@ "volume": 300.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -5024,8 +5025,8 @@ "volume": 300.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -5058,8 +5059,8 @@ "volume": 300.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -5092,8 +5093,8 @@ "volume": 300.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -5126,8 +5127,8 @@ "volume": 300.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -5159,9 +5160,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -5189,9 +5190,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -5224,8 +5225,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 1.0 }, "origin": "bottom", @@ -5258,8 +5259,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -5291,9 +5292,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -5321,9 +5322,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -5356,8 +5357,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 1.0 }, "origin": "bottom", @@ -5390,8 +5391,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -5423,9 +5424,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -5453,9 +5454,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -5488,8 +5489,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 1.0 }, "origin": "bottom", @@ -5522,8 +5523,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -5555,9 +5556,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -5585,9 +5586,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -5620,8 +5621,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 1.0 }, "origin": "bottom", @@ -5654,8 +5655,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -5687,9 +5688,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -5717,9 +5718,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -5752,8 +5753,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 1.0 }, "origin": "bottom", @@ -5786,8 +5787,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -5819,9 +5820,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -5849,9 +5850,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -5884,8 +5885,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 1.0 }, "origin": "bottom", @@ -5918,8 +5919,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -5951,9 +5952,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -6064,6 +6065,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[53db9bf516][Flex_X_v2_20_96_and_8_Overrides_InvalidConfigs_Override_eight_partial_column_bottom_left].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[53db9bf516][Flex_X_v2_20_96_and_8_Overrides_InvalidConfigs_Override_eight_partial_column_bottom_left].json index 0aaa562c15c..e514b696c6a 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[53db9bf516][Flex_X_v2_20_96_and_8_Overrides_InvalidConfigs_Override_eight_partial_column_bottom_left].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[53db9bf516][Flex_X_v2_20_96_and_8_Overrides_InvalidConfigs_Override_eight_partial_column_bottom_left].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -1284,6 +1285,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "description": "oooo", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[549cc904ac][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_c3_right_edge].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[549cc904ac][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_c3_right_edge].json index 952985449d9..aeac8222289 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[549cc904ac][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_c3_right_edge].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[549cc904ac][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_c3_right_edge].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -1249,6 +1250,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "description": "oooo", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[54b0b509cd][OT2_X_v2_20_8_Overrides_InvalidConfigs_Override_eight_partial_column_no_end].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[54b0b509cd][OT2_X_v2_20_8_Overrides_InvalidConfigs_Override_eight_partial_column_no_end].json index d28023877a0..c639bf70fc2 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[54b0b509cd][OT2_X_v2_20_8_Overrides_InvalidConfigs_Override_eight_partial_column_no_end].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[54b0b509cd][OT2_X_v2_20_8_Overrides_InvalidConfigs_Override_eight_partial_column_no_end].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -2395,6 +2396,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "description": "oooo", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[54f717cfd1][OT2_S_v2_16_P300S_None_verifyNoFloatingPointErrorInPipetting].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[54f717cfd1][OT2_S_v2_16_P300S_None_verifyNoFloatingPointErrorInPipetting].json index 9cad51f6d80..69e890d57fd 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[54f717cfd1][OT2_S_v2_16_P300S_None_verifyNoFloatingPointErrorInPipetting].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[54f717cfd1][OT2_S_v2_16_P300S_None_verifyNoFloatingPointErrorInPipetting].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -1894,6 +1895,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": {}, "modules": [], diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[555b2fff00][Flex_S_v2_19_Illumina_DNA_Prep_48x].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[555b2fff00][Flex_S_v2_19_Illumina_DNA_Prep_48x].json index c5c5f1a2e67..22587a32c81 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[555b2fff00][Flex_S_v2_19_Illumina_DNA_Prep_48x].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[555b2fff00][Flex_S_v2_19_Illumina_DNA_Prep_48x].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -5204,6 +5205,11 @@ }, "schemaVersion": 2, "stackingOffsetWithLabware": { + "evotips_flex_96_tiprack_adapter": { + "x": 0, + "y": 0, + "z": 102 + }, "opentrons_96_deep_well_adapter": { "x": 0, "y": 0, @@ -14066,9 +14072,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -14198,9 +14204,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -14330,9 +14336,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -14909,9 +14915,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -15341,9 +15347,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -15773,9 +15779,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -19116,9 +19122,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -19548,9 +19554,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -19980,9 +19986,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -22136,9 +22142,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -22568,9 +22574,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -23000,9 +23006,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -26343,9 +26349,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -26775,9 +26781,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -27207,9 +27213,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -27587,9 +27593,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -27939,9 +27945,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -28291,9 +28297,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -36139,9 +36145,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -36539,9 +36545,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -36939,9 +36945,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -39945,9 +39951,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -40345,9 +40351,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -40745,9 +40751,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -42564,9 +42570,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -42964,9 +42970,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -43364,9 +43370,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -43744,9 +43750,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -44096,9 +44102,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -44448,9 +44454,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -46021,9 +46027,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -46285,9 +46291,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -46549,9 +46555,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -46844,9 +46850,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -47008,9 +47014,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -47172,9 +47178,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -49707,6 +49713,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "CleanupBead Beads", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[58750bf5fb][Flex_X_v2_16_NO_PIPETTES_TrashBinInStagingAreaCol4].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[58750bf5fb][Flex_X_v2_16_NO_PIPETTES_TrashBinInStagingAreaCol4].json index 7c04e4274de..d11ea9fe7d6 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[58750bf5fb][Flex_X_v2_16_NO_PIPETTES_TrashBinInStagingAreaCol4].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[58750bf5fb][Flex_X_v2_16_NO_PIPETTES_TrashBinInStagingAreaCol4].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -55,6 +56,7 @@ } ], "labware": [], + "liquidClasses": [], "liquids": [], "metadata": { "author": "Derek Maggio ", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[59b00713a7][Flex_S_v2_18_ligseq].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[59b00713a7][Flex_S_v2_18_ligseq].json index 3646ae2d522..737dc286bb5 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[59b00713a7][Flex_S_v2_18_ligseq].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[59b00713a7][Flex_S_v2_18_ligseq].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -2422,6 +2423,11 @@ }, "schemaVersion": 2, "stackingOffsetWithLabware": { + "evotips_flex_96_tiprack_adapter": { + "x": 0, + "y": 0, + "z": 102 + }, "opentrons_96_deep_well_adapter": { "x": 0, "y": 0, @@ -22844,6 +22850,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "AMPure Beads", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[5c57add326][pl_Omega_HDQ_DNA_Bacteria_Flex_96_channel].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[5c57add326][pl_Omega_HDQ_DNA_Bacteria_Flex_96_channel].json index e608af8c173..e504f260861 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[5c57add326][pl_Omega_HDQ_DNA_Bacteria_Flex_96_channel].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[5c57add326][pl_Omega_HDQ_DNA_Bacteria_Flex_96_channel].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -828,6 +829,11 @@ }, "schemaVersion": 2, "stackingOffsetWithLabware": { + "evotips_flex_96_tiprack_adapter": { + "x": 0, + "y": 0, + "z": 102 + }, "opentrons_96_deep_well_adapter": { "x": 0, "y": 0, @@ -2107,6 +2113,11 @@ }, "schemaVersion": 2, "stackingOffsetWithLabware": { + "evotips_flex_96_tiprack_adapter": { + "x": 0, + "y": 0, + "z": 102 + }, "opentrons_96_deep_well_adapter": { "x": 0, "y": 0, @@ -7978,6 +7989,11 @@ }, "schemaVersion": 2, "stackingOffsetWithLabware": { + "evotips_flex_96_tiprack_adapter": { + "x": 0, + "y": 0, + "z": 102 + }, "opentrons_96_deep_well_adapter": { "x": 0, "y": 0, @@ -12713,6 +12729,11 @@ }, "schemaVersion": 2, "stackingOffsetWithLabware": { + "evotips_flex_96_tiprack_adapter": { + "x": 0, + "y": 0, + "z": 102 + }, "opentrons_96_deep_well_adapter": { "x": 0, "y": 0, @@ -15720,6 +15741,11 @@ }, "schemaVersion": 2, "stackingOffsetWithLabware": { + "evotips_flex_96_tiprack_adapter": { + "x": 0, + "y": 0, + "z": 102 + }, "opentrons_96_deep_well_adapter": { "x": 0, "y": 0, @@ -18727,6 +18753,11 @@ }, "schemaVersion": 2, "stackingOffsetWithLabware": { + "evotips_flex_96_tiprack_adapter": { + "x": 0, + "y": 0, + "z": 102 + }, "opentrons_96_deep_well_adapter": { "x": 0, "y": 0, @@ -21734,6 +21765,11 @@ }, "schemaVersion": 2, "stackingOffsetWithLabware": { + "evotips_flex_96_tiprack_adapter": { + "x": 0, + "y": 0, + "z": 102 + }, "opentrons_96_deep_well_adapter": { "x": 0, "y": 0, @@ -34069,9 +34105,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -34081,7 +34117,7 @@ "position": { "x": 14.38, "y": 395.38, - "z": 90.88 + "z": 42.12400000000001 } }, "startedAt": "TIMESTAMP", @@ -34373,9 +34409,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -34385,7 +34421,7 @@ "position": { "x": 14.38, "y": 395.38, - "z": 90.88 + "z": 42.12400000000001 } }, "startedAt": "TIMESTAMP", @@ -39647,9 +39683,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -39659,7 +39695,7 @@ "position": { "x": 14.38, "y": 395.38, - "z": 90.88 + "z": 42.12400000000001 } }, "startedAt": "TIMESTAMP", @@ -41607,9 +41643,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -41619,7 +41655,7 @@ "position": { "x": 14.38, "y": 395.38, - "z": 90.88 + "z": 42.12400000000001 } }, "startedAt": "TIMESTAMP", @@ -41879,9 +41915,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -41891,7 +41927,7 @@ "position": { "x": 14.38, "y": 395.38, - "z": 90.88 + "z": 42.12400000000001 } }, "startedAt": "TIMESTAMP", @@ -42171,9 +42207,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -42183,7 +42219,7 @@ "position": { "x": 14.38, "y": 395.38, - "z": 90.88 + "z": 42.12400000000001 } }, "startedAt": "TIMESTAMP", @@ -42442,9 +42478,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -42454,7 +42490,7 @@ "position": { "x": 14.38, "y": 395.38, - "z": 90.88 + "z": 42.12400000000001 } }, "startedAt": "TIMESTAMP", @@ -42734,9 +42770,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -42746,7 +42782,7 @@ "position": { "x": 14.38, "y": 395.38, - "z": 90.88 + "z": 42.12400000000001 } }, "startedAt": "TIMESTAMP", @@ -43005,9 +43041,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -43017,7 +43053,7 @@ "position": { "x": 14.38, "y": 395.38, - "z": 90.88 + "z": 42.12400000000001 } }, "startedAt": "TIMESTAMP", @@ -43297,9 +43333,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -43309,7 +43345,7 @@ "position": { "x": 14.38, "y": 395.38, - "z": 90.88 + "z": 42.12400000000001 } }, "startedAt": "TIMESTAMP", @@ -43568,9 +43604,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -43580,7 +43616,7 @@ "position": { "x": 14.38, "y": 395.38, - "z": 90.88 + "z": 42.12400000000001 } }, "startedAt": "TIMESTAMP", @@ -45762,9 +45798,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -45774,7 +45810,7 @@ "position": { "x": 178.38, "y": 395.38, - "z": 90.88 + "z": 42.12400000000001 } }, "startedAt": "TIMESTAMP", @@ -46020,9 +46056,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -46032,7 +46068,7 @@ "position": { "x": 178.38, "y": 395.38, - "z": 90.88 + "z": 42.12400000000001 } }, "startedAt": "TIMESTAMP", @@ -46193,6 +46229,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "Sample Resuspended in PBS", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[5e958b7c98][Flex_X_v2_16_P300MGen2_None_OT2PipetteInFlexProtocol].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[5e958b7c98][Flex_X_v2_16_P300MGen2_None_OT2PipetteInFlexProtocol].json index c76b2aca7f9..accbbbbda1e 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[5e958b7c98][Flex_X_v2_16_P300MGen2_None_OT2PipetteInFlexProtocol].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[5e958b7c98][Flex_X_v2_16_P300MGen2_None_OT2PipetteInFlexProtocol].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -1258,6 +1259,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "author": "Derek Maggio ", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[5edb9b4de3][Flex_X_v2_20_96_and_8_Overrides_InvalidConfigs_Override_ninety_six_partial_column_2].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[5edb9b4de3][Flex_X_v2_20_96_and_8_Overrides_InvalidConfigs_Override_ninety_six_partial_column_2].json index a107fa87e60..122bbadc03d 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[5edb9b4de3][Flex_X_v2_20_96_and_8_Overrides_InvalidConfigs_Override_ninety_six_partial_column_2].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[5edb9b4de3][Flex_X_v2_20_96_and_8_Overrides_InvalidConfigs_Override_ninety_six_partial_column_2].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -1241,6 +1242,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "description": "oooo", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[60015c6e65][OT2_X_v2_18_None_None_duplicateRTPVariableName].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[60015c6e65][OT2_X_v2_18_None_None_duplicateRTPVariableName].json index 86d3274f412..87a9f4d3b02 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[60015c6e65][OT2_X_v2_18_None_None_duplicateRTPVariableName].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[60015c6e65][OT2_X_v2_18_None_None_duplicateRTPVariableName].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [], "config": { "apiVersion": [ @@ -42,6 +43,7 @@ } ], "labware": [], + "liquidClasses": [], "liquids": [], "metadata": { "protocolName": "Multiple RTP Variables with Same Name" diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[604023f7f1][Flex_X_v2_16_NO_PIPETTES_TM_ModuleInStagingAreaCol3].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[604023f7f1][Flex_X_v2_16_NO_PIPETTES_TM_ModuleInStagingAreaCol3].json index 0de0eff0022..ac1235fe4a1 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[604023f7f1][Flex_X_v2_16_NO_PIPETTES_TM_ModuleInStagingAreaCol3].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[604023f7f1][Flex_X_v2_16_NO_PIPETTES_TM_ModuleInStagingAreaCol3].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -197,6 +198,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "author": "Derek Maggio ", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[60c1d39463][Flex_X_v2_18_NO_PIPETTES_Overrides_DefaultChoiceNoMatchChoice_Override_int_default_no_matching_choices].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[60c1d39463][Flex_X_v2_18_NO_PIPETTES_Overrides_DefaultChoiceNoMatchChoice_Override_int_default_no_matching_choices].json index 726906c04d4..f119f6dd2c6 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[60c1d39463][Flex_X_v2_18_NO_PIPETTES_Overrides_DefaultChoiceNoMatchChoice_Override_int_default_no_matching_choices].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[60c1d39463][Flex_X_v2_18_NO_PIPETTES_Overrides_DefaultChoiceNoMatchChoice_Override_int_default_no_matching_choices].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [], "config": { "apiVersion": [ @@ -42,6 +43,7 @@ } ], "labware": [], + "liquidClasses": [], "liquids": [], "metadata": { "protocolName": "default choice does not match a choice" diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6122c72996][Flex_X_v2_20_96_and_8_Overrides_InvalidConfigs_Override_ninety_six_partial_column_1].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6122c72996][Flex_X_v2_20_96_and_8_Overrides_InvalidConfigs_Override_ninety_six_partial_column_1].json index 1dcac6e453a..341397530c7 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6122c72996][Flex_X_v2_20_96_and_8_Overrides_InvalidConfigs_Override_ninety_six_partial_column_1].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6122c72996][Flex_X_v2_20_96_and_8_Overrides_InvalidConfigs_Override_ninety_six_partial_column_1].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -1241,6 +1242,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "description": "oooo", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6126498df7][Flex_X_v2_16_NO_PIPETTES_TM_ModuleInStagingAreaCol4].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6126498df7][Flex_X_v2_16_NO_PIPETTES_TM_ModuleInStagingAreaCol4].json index d8409d8db46..a2736f68c26 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6126498df7][Flex_X_v2_16_NO_PIPETTES_TM_ModuleInStagingAreaCol4].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6126498df7][Flex_X_v2_16_NO_PIPETTES_TM_ModuleInStagingAreaCol4].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -55,6 +56,7 @@ } ], "labware": [], + "liquidClasses": [], "liquids": [], "metadata": { "author": "Derek Maggio ", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[61619d5498][Flex_S_v2_18_NO_PIPETTES_GoldenRTP].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[61619d5498][Flex_S_v2_18_NO_PIPETTES_GoldenRTP].json index afa5bb0b4d2..6ed42554132 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[61619d5498][Flex_S_v2_18_NO_PIPETTES_GoldenRTP].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[61619d5498][Flex_S_v2_18_NO_PIPETTES_GoldenRTP].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -295,6 +296,7 @@ } ], "labware": [], + "liquidClasses": [], "liquids": [], "metadata": { "protocolName": "Golden RTP Examples" diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[618f29898f][pl_Flex_customizable_serial_dilution_upload].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[618f29898f][pl_Flex_customizable_serial_dilution_upload].json index 385da3c78a4..1a2e4840a95 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[618f29898f][pl_Flex_customizable_serial_dilution_upload].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[618f29898f][pl_Flex_customizable_serial_dilution_upload].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -5308,9 +5309,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -10112,9 +10113,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -10310,9 +10311,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -10386,6 +10387,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "Diluent liquid is filled in the reservoir", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[63ea171b47][pl_M_N_Nucleomag_DNA_Flex_multi].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[63ea171b47][pl_M_N_Nucleomag_DNA_Flex_multi].json index 5681dc28194..4b86c4eccdc 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[63ea171b47][pl_M_N_Nucleomag_DNA_Flex_multi].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[63ea171b47][pl_M_N_Nucleomag_DNA_Flex_multi].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -828,6 +829,11 @@ }, "schemaVersion": 2, "stackingOffsetWithLabware": { + "evotips_flex_96_tiprack_adapter": { + "x": 0, + "y": 0, + "z": 102 + }, "opentrons_96_deep_well_adapter": { "x": 0, "y": 0, @@ -12986,9 +12992,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -13827,9 +13833,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -14668,9 +14674,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -15331,9 +15337,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -15689,9 +15695,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -16386,6 +16392,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "Cell Pellet", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[675d2c2562][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_south_east].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[675d2c2562][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_south_east].json index 1bf35620512..77e2166f737 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[675d2c2562][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_south_east].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[675d2c2562][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_south_east].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -1481,6 +1482,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "description": "oooo", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[68614da0b3][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_east].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[68614da0b3][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_east].json index b2ec113fe4e..df66e371d7b 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[68614da0b3][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_east].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[68614da0b3][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_east].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -1481,6 +1482,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "description": "oooo", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6ad5590adf][Flex_X_v2_18_NO_PIPETTES_Overrides_BadTypesInRTP_Override_wrong_type_in_unit].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6ad5590adf][Flex_X_v2_18_NO_PIPETTES_Overrides_BadTypesInRTP_Override_wrong_type_in_unit].json index e545da56bd4..a57c328ac0d 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6ad5590adf][Flex_X_v2_18_NO_PIPETTES_Overrides_BadTypesInRTP_Override_wrong_type_in_unit].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6ad5590adf][Flex_X_v2_18_NO_PIPETTES_Overrides_BadTypesInRTP_Override_wrong_type_in_unit].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [], "config": { "apiVersion": [ @@ -42,6 +43,7 @@ } ], "labware": [], + "liquidClasses": [], "liquids": [], "metadata": { "protocolName": "Description Too Long 2.18" diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6c20d6c570][Flex_S_v2_20_96_None_COLUMN_HappyPath].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6c20d6c570][Flex_S_v2_20_96_None_COLUMN_HappyPath].json index 19ac0d4e0f7..e4961b84c9f 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6c20d6c570][Flex_S_v2_20_96_None_COLUMN_HappyPath].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6c20d6c570][Flex_S_v2_20_96_None_COLUMN_HappyPath].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -6263,6 +6264,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "description": "96 channel pipette and a COLUMN partial tip configuration.", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6cee20a957][OT2_S_v2_12_NO_PIPETTES_Python310SyntaxRobotAnalysisOnlyError].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6cee20a957][OT2_S_v2_12_NO_PIPETTES_Python310SyntaxRobotAnalysisOnlyError].json index 8f88134625a..b7a95b5bf1d 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6cee20a957][OT2_S_v2_12_NO_PIPETTES_Python310SyntaxRobotAnalysisOnlyError].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6cee20a957][OT2_S_v2_12_NO_PIPETTES_Python310SyntaxRobotAnalysisOnlyError].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -94,6 +95,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "apiLevel": "2.12", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6e2f6f10c5][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_consolidate_destination_collision].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6e2f6f10c5][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_consolidate_destination_collision].json index cf3e8bf4aa3..e8590ac1760 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6e2f6f10c5][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_consolidate_destination_collision].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6e2f6f10c5][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_consolidate_destination_collision].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -3946,6 +3947,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "description": "oooo", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6e34343cfc][Flex_S_v2_15_P1000_96_GRIP_HS_MB_TM_MagMaxRNAExtraction].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6e34343cfc][Flex_S_v2_15_P1000_96_GRIP_HS_MB_TM_MagMaxRNAExtraction].json index 66877246558..c232b2db8c6 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6e34343cfc][Flex_S_v2_15_P1000_96_GRIP_HS_MB_TM_MagMaxRNAExtraction].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6e34343cfc][Flex_S_v2_15_P1000_96_GRIP_HS_MB_TM_MagMaxRNAExtraction].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -760,6 +761,11 @@ }, "schemaVersion": 2, "stackingOffsetWithLabware": { + "evotips_flex_96_tiprack_adapter": { + "x": 0, + "y": 0, + "z": 102 + }, "opentrons_96_deep_well_adapter": { "x": 0, "y": 0, @@ -2038,6 +2044,11 @@ }, "schemaVersion": 2, "stackingOffsetWithLabware": { + "evotips_flex_96_tiprack_adapter": { + "x": 0, + "y": 0, + "z": 102 + }, "opentrons_96_deep_well_adapter": { "x": 0, "y": 0, @@ -5016,6 +5027,11 @@ }, "schemaVersion": 2, "stackingOffsetWithLabware": { + "evotips_flex_96_tiprack_adapter": { + "x": 0, + "y": 0, + "z": 102 + }, "opentrons_96_deep_well_adapter": { "x": 0, "y": 0, @@ -6294,6 +6310,11 @@ }, "schemaVersion": 2, "stackingOffsetWithLabware": { + "evotips_flex_96_tiprack_adapter": { + "x": 0, + "y": 0, + "z": 102 + }, "opentrons_96_deep_well_adapter": { "x": 0, "y": 0, @@ -7572,6 +7593,11 @@ }, "schemaVersion": 2, "stackingOffsetWithLabware": { + "evotips_flex_96_tiprack_adapter": { + "x": 0, + "y": 0, + "z": 102 + }, "opentrons_96_deep_well_adapter": { "x": 0, "y": 0, @@ -16290,9 +16316,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -16302,7 +16328,7 @@ "position": { "x": 14.38, "y": 395.38, - "z": 98.33 + "z": 74.99000000000001 } }, "startedAt": "TIMESTAMP", @@ -18264,9 +18290,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -18276,7 +18302,7 @@ "position": { "x": 14.38, "y": 395.38, - "z": 98.33 + "z": 74.99000000000001 } }, "startedAt": "TIMESTAMP", @@ -18674,9 +18700,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -18686,7 +18712,7 @@ "position": { "x": 14.38, "y": 395.38, - "z": 98.33 + "z": 74.99000000000001 } }, "startedAt": "TIMESTAMP", @@ -18962,9 +18988,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -18974,7 +19000,7 @@ "position": { "x": 14.38, "y": 395.38, - "z": 98.33 + "z": 74.99000000000001 } }, "startedAt": "TIMESTAMP", @@ -19258,9 +19284,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -19270,7 +19296,7 @@ "position": { "x": 14.38, "y": 395.38, - "z": 98.33 + "z": 74.99000000000001 } }, "startedAt": "TIMESTAMP", @@ -19546,9 +19572,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -19558,7 +19584,7 @@ "position": { "x": 14.38, "y": 395.38, - "z": 98.33 + "z": 74.99000000000001 } }, "startedAt": "TIMESTAMP", @@ -19842,9 +19868,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -19854,7 +19880,7 @@ "position": { "x": 14.38, "y": 395.38, - "z": 98.33 + "z": 74.99000000000001 } }, "startedAt": "TIMESTAMP", @@ -22276,9 +22302,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -22288,7 +22314,7 @@ "position": { "x": 14.38, "y": 395.38, - "z": 98.33 + "z": 74.99000000000001 } }, "startedAt": "TIMESTAMP", @@ -23708,9 +23734,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -23720,7 +23746,7 @@ "position": { "x": 14.38, "y": 395.38, - "z": 98.33 + "z": 74.99000000000001 } }, "startedAt": "TIMESTAMP", @@ -24004,9 +24030,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -24016,7 +24042,7 @@ "position": { "x": 14.38, "y": 395.38, - "z": 98.33 + "z": 74.99000000000001 } }, "startedAt": "TIMESTAMP", @@ -24292,9 +24318,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -24304,7 +24330,7 @@ "position": { "x": 14.38, "y": 395.38, - "z": 98.33 + "z": 74.99000000000001 } }, "startedAt": "TIMESTAMP", @@ -24588,9 +24614,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -24600,7 +24626,7 @@ "position": { "x": 14.38, "y": 395.38, - "z": 98.33 + "z": 74.99000000000001 } }, "startedAt": "TIMESTAMP", @@ -24876,9 +24902,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -24888,7 +24914,7 @@ "position": { "x": 14.38, "y": 395.38, - "z": 98.33 + "z": 74.99000000000001 } }, "startedAt": "TIMESTAMP", @@ -25172,9 +25198,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -25184,7 +25210,7 @@ "position": { "x": 14.38, "y": 395.38, - "z": 98.33 + "z": 74.99000000000001 } }, "startedAt": "TIMESTAMP", @@ -26579,9 +26605,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -26591,7 +26617,7 @@ "position": { "x": 178.38, "y": 395.38, - "z": 98.33 + "z": 74.99000000000001 } }, "startedAt": "TIMESTAMP", @@ -26891,9 +26917,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -26903,7 +26929,7 @@ "position": { "x": 178.38, "y": 395.38, - "z": 98.33 + "z": 74.99000000000001 } }, "startedAt": "TIMESTAMP", @@ -27845,9 +27871,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -27857,7 +27883,7 @@ "position": { "x": 14.38, "y": 395.38, - "z": 98.33 + "z": 74.99000000000001 } }, "startedAt": "TIMESTAMP", @@ -30765,9 +30791,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -30777,7 +30803,7 @@ "position": { "x": 14.38, "y": 395.38, - "z": 98.33 + "z": 74.99000000000001 } }, "startedAt": "TIMESTAMP", @@ -32739,9 +32765,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -32751,7 +32777,7 @@ "position": { "x": 14.38, "y": 395.38, - "z": 98.33 + "z": 74.99000000000001 } }, "startedAt": "TIMESTAMP", @@ -33149,9 +33175,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -33161,7 +33187,7 @@ "position": { "x": 14.38, "y": 395.38, - "z": 98.33 + "z": 74.99000000000001 } }, "startedAt": "TIMESTAMP", @@ -33437,9 +33463,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -33449,7 +33475,7 @@ "position": { "x": 14.38, "y": 395.38, - "z": 98.33 + "z": 74.99000000000001 } }, "startedAt": "TIMESTAMP", @@ -33733,9 +33759,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -33745,7 +33771,7 @@ "position": { "x": 14.38, "y": 395.38, - "z": 98.33 + "z": 74.99000000000001 } }, "startedAt": "TIMESTAMP", @@ -34021,9 +34047,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -34033,7 +34059,7 @@ "position": { "x": 14.38, "y": 395.38, - "z": 98.33 + "z": 74.99000000000001 } }, "startedAt": "TIMESTAMP", @@ -34317,9 +34343,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -34329,7 +34355,7 @@ "position": { "x": 14.38, "y": 395.38, - "z": 98.33 + "z": 74.99000000000001 } }, "startedAt": "TIMESTAMP", @@ -36751,9 +36777,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -36763,7 +36789,7 @@ "position": { "x": 14.38, "y": 395.38, - "z": 98.33 + "z": 74.99000000000001 } }, "startedAt": "TIMESTAMP", @@ -38183,9 +38209,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -38195,7 +38221,7 @@ "position": { "x": 14.38, "y": 395.38, - "z": 98.33 + "z": 74.99000000000001 } }, "startedAt": "TIMESTAMP", @@ -38479,9 +38505,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -38491,7 +38517,7 @@ "position": { "x": 14.38, "y": 395.38, - "z": 98.33 + "z": 74.99000000000001 } }, "startedAt": "TIMESTAMP", @@ -38767,9 +38793,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -38779,7 +38805,7 @@ "position": { "x": 14.38, "y": 395.38, - "z": 98.33 + "z": 74.99000000000001 } }, "startedAt": "TIMESTAMP", @@ -39063,9 +39089,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -39075,7 +39101,7 @@ "position": { "x": 14.38, "y": 395.38, - "z": 98.33 + "z": 74.99000000000001 } }, "startedAt": "TIMESTAMP", @@ -39351,9 +39377,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -39363,7 +39389,7 @@ "position": { "x": 14.38, "y": 395.38, - "z": 98.33 + "z": 74.99000000000001 } }, "startedAt": "TIMESTAMP", @@ -39647,9 +39673,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -39659,7 +39685,7 @@ "position": { "x": 14.38, "y": 395.38, - "z": 98.33 + "z": 74.99000000000001 } }, "startedAt": "TIMESTAMP", @@ -41054,9 +41080,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -41066,7 +41092,7 @@ "position": { "x": 178.38, "y": 395.38, - "z": 98.33 + "z": 74.99000000000001 } }, "startedAt": "TIMESTAMP", @@ -41366,9 +41392,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -41378,7 +41404,7 @@ "position": { "x": 178.38, "y": 395.38, - "z": 98.33 + "z": 74.99000000000001 } }, "startedAt": "TIMESTAMP", @@ -42320,9 +42346,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -42332,7 +42358,7 @@ "position": { "x": 14.38, "y": 395.38, - "z": 98.33 + "z": 74.99000000000001 } }, "startedAt": "TIMESTAMP", @@ -45240,9 +45266,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -45252,7 +45278,7 @@ "position": { "x": 14.38, "y": 395.38, - "z": 98.33 + "z": 74.99000000000001 } }, "startedAt": "TIMESTAMP", @@ -47214,9 +47240,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -47226,7 +47252,7 @@ "position": { "x": 14.38, "y": 395.38, - "z": 98.33 + "z": 74.99000000000001 } }, "startedAt": "TIMESTAMP", @@ -47624,9 +47650,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -47636,7 +47662,7 @@ "position": { "x": 14.38, "y": 395.38, - "z": 98.33 + "z": 74.99000000000001 } }, "startedAt": "TIMESTAMP", @@ -47912,9 +47938,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -47924,7 +47950,7 @@ "position": { "x": 14.38, "y": 395.38, - "z": 98.33 + "z": 74.99000000000001 } }, "startedAt": "TIMESTAMP", @@ -48208,9 +48234,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -48220,7 +48246,7 @@ "position": { "x": 14.38, "y": 395.38, - "z": 98.33 + "z": 74.99000000000001 } }, "startedAt": "TIMESTAMP", @@ -48496,9 +48522,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -48508,7 +48534,7 @@ "position": { "x": 14.38, "y": 395.38, - "z": 98.33 + "z": 74.99000000000001 } }, "startedAt": "TIMESTAMP", @@ -48792,9 +48818,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -48804,7 +48830,7 @@ "position": { "x": 14.38, "y": 395.38, - "z": 98.33 + "z": 74.99000000000001 } }, "startedAt": "TIMESTAMP", @@ -51226,9 +51252,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -51238,7 +51264,7 @@ "position": { "x": 14.38, "y": 395.38, - "z": 98.33 + "z": 74.99000000000001 } }, "startedAt": "TIMESTAMP", @@ -52658,9 +52684,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -52670,7 +52696,7 @@ "position": { "x": 14.38, "y": 395.38, - "z": 98.33 + "z": 74.99000000000001 } }, "startedAt": "TIMESTAMP", @@ -52954,9 +52980,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -52966,7 +52992,7 @@ "position": { "x": 14.38, "y": 395.38, - "z": 98.33 + "z": 74.99000000000001 } }, "startedAt": "TIMESTAMP", @@ -53242,9 +53268,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -53254,7 +53280,7 @@ "position": { "x": 14.38, "y": 395.38, - "z": 98.33 + "z": 74.99000000000001 } }, "startedAt": "TIMESTAMP", @@ -53538,9 +53564,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -53550,7 +53576,7 @@ "position": { "x": 14.38, "y": 395.38, - "z": 98.33 + "z": 74.99000000000001 } }, "startedAt": "TIMESTAMP", @@ -53826,9 +53852,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -53838,7 +53864,7 @@ "position": { "x": 14.38, "y": 395.38, - "z": 98.33 + "z": 74.99000000000001 } }, "startedAt": "TIMESTAMP", @@ -54122,9 +54148,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -54134,7 +54160,7 @@ "position": { "x": 14.38, "y": 395.38, - "z": 98.33 + "z": 74.99000000000001 } }, "startedAt": "TIMESTAMP", @@ -55529,9 +55555,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -55541,7 +55567,7 @@ "position": { "x": 178.38, "y": 395.38, - "z": 98.33 + "z": 74.99000000000001 } }, "startedAt": "TIMESTAMP", @@ -55841,9 +55867,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -55853,7 +55879,7 @@ "position": { "x": 178.38, "y": 395.38, - "z": 98.33 + "z": 74.99000000000001 } }, "startedAt": "TIMESTAMP", @@ -56795,9 +56821,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -56807,7 +56833,7 @@ "position": { "x": 14.38, "y": 395.38, - "z": 98.33 + "z": 74.99000000000001 } }, "startedAt": "TIMESTAMP", @@ -56935,6 +56961,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "author": "Opentrons Engineering ", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6e5128f107][OT2_X_v2_16_None_None_HS_HeaterShakerConflictWithTrashBin1].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6e5128f107][OT2_X_v2_16_None_None_HS_HeaterShakerConflictWithTrashBin1].json index 63567ca7c96..39393aae057 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6e5128f107][OT2_X_v2_16_None_None_HS_HeaterShakerConflictWithTrashBin1].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6e5128f107][OT2_X_v2_16_None_None_HS_HeaterShakerConflictWithTrashBin1].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -512,6 +513,7 @@ } ], "labware": [], + "liquidClasses": [], "liquids": [], "metadata": { "protocolName": "Heater-shaker conflict OT-2" diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6e744cbb48][Flex_X_v2_18_NO_PIPETTES_Overrides_DefaultChoiceNoMatchChoice_Override_str_default_no_matching_choices].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6e744cbb48][Flex_X_v2_18_NO_PIPETTES_Overrides_DefaultChoiceNoMatchChoice_Override_str_default_no_matching_choices].json index cae3345ff13..2f0492db119 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6e744cbb48][Flex_X_v2_18_NO_PIPETTES_Overrides_DefaultChoiceNoMatchChoice_Override_str_default_no_matching_choices].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6e744cbb48][Flex_X_v2_18_NO_PIPETTES_Overrides_DefaultChoiceNoMatchChoice_Override_str_default_no_matching_choices].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [], "config": { "apiVersion": [ @@ -42,6 +43,7 @@ } ], "labware": [], + "liquidClasses": [], "liquids": [], "metadata": { "protocolName": "default choice does not match a choice" diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6f246e1cd8][Flex_S_2_18_P1000M_P50M_GRIP_HS_TM_MB_TC_IlluminaDNAEnrichmentV4].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6f246e1cd8][Flex_S_2_18_P1000M_P50M_GRIP_HS_TM_MB_TC_IlluminaDNAEnrichmentV4].json index d67ff04865b..a6b46f54c00 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6f246e1cd8][Flex_S_2_18_P1000M_P50M_GRIP_HS_TM_MB_TC_IlluminaDNAEnrichmentV4].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6f246e1cd8][Flex_S_2_18_P1000M_P50M_GRIP_HS_TM_MB_TC_IlluminaDNAEnrichmentV4].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -1945,6 +1946,11 @@ }, "schemaVersion": 2, "stackingOffsetWithLabware": { + "evotips_flex_96_tiprack_adapter": { + "x": 0, + "y": 0, + "z": 102 + }, "opentrons_96_deep_well_adapter": { "x": 0, "y": 0, @@ -12216,9 +12222,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -12380,9 +12386,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -12544,9 +12550,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -13538,9 +13544,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -14504,9 +14510,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -15470,9 +15476,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -16023,9 +16029,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -16399,9 +16405,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -16775,9 +16781,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -16983,9 +16989,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -17115,9 +17121,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -17247,9 +17253,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -17768,9 +17774,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -18124,9 +18130,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -18480,9 +18486,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -18674,9 +18680,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -18806,9 +18812,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -18938,9 +18944,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -19459,9 +19465,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -19815,9 +19821,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -20171,9 +20177,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -20365,9 +20371,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -20497,9 +20503,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -20629,9 +20635,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -21150,9 +21156,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -21506,9 +21512,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -21862,9 +21868,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -22056,9 +22062,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -22188,9 +22194,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -22320,9 +22326,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -22557,9 +22563,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -22721,9 +22727,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -22885,9 +22891,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -23331,9 +23337,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -23687,9 +23693,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -24043,9 +24049,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -24331,9 +24337,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -24605,9 +24611,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -24879,9 +24885,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -25067,9 +25073,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -25199,9 +25205,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -25331,9 +25337,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -25694,9 +25700,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -25858,9 +25864,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -26022,9 +26028,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -26889,9 +26895,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -27742,9 +27748,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -28595,9 +28601,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -28783,9 +28789,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -28915,9 +28921,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -29047,9 +29053,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -29914,9 +29920,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -30767,9 +30773,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -31620,9 +31626,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -32165,9 +32171,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -32329,9 +32335,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -32493,9 +32499,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -33405,9 +33411,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -34303,9 +34309,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -35201,9 +35207,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -35708,9 +35714,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -36094,9 +36100,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -36480,9 +36486,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -36896,9 +36902,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -37298,9 +37304,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -37700,9 +37706,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -38114,9 +38120,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -38500,9 +38506,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -38886,9 +38892,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -39302,9 +39308,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -39704,9 +39710,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -40106,9 +40112,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -40520,9 +40526,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -40906,9 +40912,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -41292,9 +41298,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -41594,9 +41600,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -41868,9 +41874,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -42142,9 +42148,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -43006,9 +43012,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -43839,9 +43845,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -44672,9 +44678,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -44957,9 +44963,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -45121,9 +45127,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -45285,9 +45291,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -45393,6 +45399,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "author": "Opentrons ", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6f3e297a11][OT2_S_v2_3_P300S_None_MM1_MM2_TM_Mix].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6f3e297a11][OT2_S_v2_3_P300S_None_MM1_MM2_TM_Mix].json index 80a9f7d117a..c37d22a7b4b 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6f3e297a11][OT2_S_v2_3_P300S_None_MM1_MM2_TM_Mix].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6f3e297a11][OT2_S_v2_3_P300S_None_MM1_MM2_TM_Mix].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -3100,9 +3101,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -3114,9 +3115,9 @@ "y": 0.0, "z": 0.0 }, - "tipDiameter": 0, + "tipDiameter": 0.0, "tipLength": 51.099999999999994, - "tipVolume": 300 + "tipVolume": 300.0 }, "startedAt": "TIMESTAMP", "status": "succeeded" @@ -3135,9 +3136,9 @@ "volume": 100.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -3169,9 +3170,9 @@ "volume": 100.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -3203,9 +3204,9 @@ "volume": 100.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -3237,9 +3238,9 @@ "volume": 100.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -3271,9 +3272,9 @@ "volume": 100.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -3305,9 +3306,9 @@ "volume": 100.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -3339,9 +3340,9 @@ "volume": 100.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -3373,9 +3374,9 @@ "volume": 100.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -3435,6 +3436,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "apiLevel": "2.3" diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6f84e60cb0][OT2_S_v2_16_P300M_P20S_HS_TC_TM_aspirateDispenseMix0Volume].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6f84e60cb0][OT2_S_v2_16_P300M_P20S_HS_TC_TM_aspirateDispenseMix0Volume].json index ad8638a9e6d..03aae1235b6 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6f84e60cb0][OT2_S_v2_16_P300M_P20S_HS_TC_TM_aspirateDispenseMix0Volume].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6f84e60cb0][OT2_S_v2_16_P300M_P20S_HS_TC_TM_aspirateDispenseMix0Volume].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -2849,6 +2850,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "H₂O", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[70b873c24b][pl_SamplePrep_MS_Digest_Flex_upto96].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[70b873c24b][pl_SamplePrep_MS_Digest_Flex_upto96].json index cbd7839e9ad..9afcc19539c 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[70b873c24b][pl_SamplePrep_MS_Digest_Flex_upto96].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[70b873c24b][pl_SamplePrep_MS_Digest_Flex_upto96].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -1599,6 +1600,11 @@ }, "schemaVersion": 2, "stackingOffsetWithLabware": { + "evotips_flex_96_tiprack_adapter": { + "x": 0, + "y": 0, + "z": 102 + }, "opentrons_96_deep_well_adapter": { "x": 0, "y": 0, @@ -6168,9 +6174,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -6465,9 +6471,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -6762,9 +6768,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -7059,9 +7065,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -7356,9 +7362,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -7653,9 +7659,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -7950,9 +7956,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -8247,9 +8253,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -8544,9 +8550,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -8841,9 +8847,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -9138,9 +9144,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -9435,9 +9441,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -9732,9 +9738,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -10029,9 +10035,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -10326,9 +10332,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -10623,9 +10629,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -10920,9 +10926,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -11217,9 +11223,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -11514,9 +11520,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -11811,9 +11817,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -12108,9 +12114,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -12405,9 +12411,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -12702,9 +12708,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -12999,9 +13005,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -13657,9 +13663,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -13954,9 +13960,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -14251,9 +14257,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -14548,9 +14554,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -14845,9 +14851,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -15142,9 +15148,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -15439,9 +15445,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -15736,9 +15742,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -16033,9 +16039,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -16330,9 +16336,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -16627,9 +16633,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -16924,9 +16930,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -17221,9 +17227,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -17518,9 +17524,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -17815,9 +17821,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -18112,9 +18118,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -18409,9 +18415,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -18706,9 +18712,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -19003,9 +19009,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -19300,9 +19306,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -19597,9 +19603,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -19894,9 +19900,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -20191,9 +20197,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -20488,9 +20494,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -21146,9 +21152,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -21443,9 +21449,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -21740,9 +21746,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -22037,9 +22043,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -22334,9 +22340,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -22631,9 +22637,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -22928,9 +22934,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -23225,9 +23231,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -23522,9 +23528,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -23819,9 +23825,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -24116,9 +24122,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -24413,9 +24419,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -24710,9 +24716,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -25007,9 +25013,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -25304,9 +25310,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -25601,9 +25607,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -25898,9 +25904,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -26195,9 +26201,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -26492,9 +26498,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -26789,9 +26795,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -27086,9 +27092,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -27383,9 +27389,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -27680,9 +27686,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -27977,9 +27983,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -28635,9 +28641,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -28932,9 +28938,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -29229,9 +29235,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -29526,9 +29532,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -29823,9 +29829,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -30120,9 +30126,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -30417,9 +30423,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -30714,9 +30720,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -31011,9 +31017,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -31308,9 +31314,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -31605,9 +31611,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -31902,9 +31908,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -32199,9 +32205,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -32496,9 +32502,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -32793,9 +32799,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -33090,9 +33096,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -33387,9 +33393,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -33684,9 +33690,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -33981,9 +33987,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -34278,9 +34284,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -34575,9 +34581,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -34872,9 +34878,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -35169,9 +35175,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -35466,9 +35472,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -38524,9 +38530,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -41564,9 +41570,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -42037,9 +42043,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -42510,9 +42516,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -42983,9 +42989,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -43456,9 +43462,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -43929,9 +43935,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -44402,9 +44408,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -44875,9 +44881,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -45348,9 +45354,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -45821,9 +45827,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -46294,9 +46300,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -46767,9 +46773,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -47240,9 +47246,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -50433,9 +50439,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -50906,9 +50912,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -51379,9 +51385,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -51852,9 +51858,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -52325,9 +52331,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -52798,9 +52804,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -53271,9 +53277,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -53744,9 +53750,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -54217,9 +54223,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -54690,9 +54696,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -55163,9 +55169,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -55636,9 +55642,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -56109,9 +56115,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -59163,9 +59169,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -59636,9 +59642,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -60109,9 +60115,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -60582,9 +60588,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -61055,9 +61061,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -61528,9 +61534,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -62001,9 +62007,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -62474,9 +62480,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -62947,9 +62953,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -63420,9 +63426,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -63893,9 +63899,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -64366,9 +64372,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -64839,9 +64845,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -65064,6 +65070,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "100 mM ABC in MS grade water, volume per well", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[733c9cdf62][Flex_S_v2_20_8_None_PARTIAL_COLUMN_HappyPath].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[733c9cdf62][Flex_S_v2_20_8_None_PARTIAL_COLUMN_HappyPath].json index 1b70c59e4b6..0eabefc61c9 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[733c9cdf62][Flex_S_v2_20_8_None_PARTIAL_COLUMN_HappyPath].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[733c9cdf62][Flex_S_v2_20_8_None_PARTIAL_COLUMN_HappyPath].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -5042,6 +5043,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "description": "Tip Rack South Clearance for the 8 Channel pipette and a SINGLE partial tip configuration.", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[7583faec7c][OT2_X_v2_20_8_Overrides_InvalidConfigs_Override_return_tip_error].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[7583faec7c][OT2_X_v2_20_8_Overrides_InvalidConfigs_Override_return_tip_error].json index 30ddffb8e03..be40cf92526 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[7583faec7c][OT2_X_v2_20_8_Overrides_InvalidConfigs_Override_return_tip_error].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[7583faec7c][OT2_X_v2_20_8_Overrides_InvalidConfigs_Override_return_tip_error].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -4920,6 +4921,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "description": "oooo", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[770ebdcd29][pl_KAPA_Library_Quant_48_v8].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[770ebdcd29][pl_KAPA_Library_Quant_48_v8].json index bc24730fad8..504f3f1a239 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[770ebdcd29][pl_KAPA_Library_Quant_48_v8].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[770ebdcd29][pl_KAPA_Library_Quant_48_v8].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -12913,9 +12914,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -15069,9 +15070,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -16024,9 +16025,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -16979,9 +16980,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -17948,9 +17949,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -18903,9 +18904,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -19858,9 +19859,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -20548,9 +20549,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -21144,9 +21145,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -21740,9 +21741,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -22776,9 +22777,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -23770,9 +23771,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -24764,9 +24765,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -26945,9 +26946,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -31929,6 +31930,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "Dilution Buffer", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[79e61426a2][Flex_S_v2_18_AMPure_XP_48x].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[79e61426a2][Flex_S_v2_18_AMPure_XP_48x].json index 19cf70d2edb..744032008ce 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[79e61426a2][Flex_S_v2_18_AMPure_XP_48x].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[79e61426a2][Flex_S_v2_18_AMPure_XP_48x].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -4031,6 +4032,11 @@ }, "schemaVersion": 2, "stackingOffsetWithLabware": { + "evotips_flex_96_tiprack_adapter": { + "x": 0, + "y": 0, + "z": 102 + }, "opentrons_96_deep_well_adapter": { "x": 0, "y": 0, @@ -15813,9 +15819,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -16213,9 +16219,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -16613,9 +16619,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -19715,9 +19721,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -20115,9 +20121,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -20515,9 +20521,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -22430,9 +22436,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -22830,9 +22836,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -23230,9 +23236,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -24131,9 +24137,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -24495,9 +24501,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -24859,9 +24865,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -26373,9 +26379,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -26569,9 +26575,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -26765,9 +26771,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -28213,6 +28219,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "AMPure Beads", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[7d06568bfe][Flex_X_v2_18_NO_PIPETTES_Overrides_BadTypesInRTP_Override_wrong_type_in_choice_display_name].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[7d06568bfe][Flex_X_v2_18_NO_PIPETTES_Overrides_BadTypesInRTP_Override_wrong_type_in_choice_display_name].json index bd4f009a701..43927f6a145 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[7d06568bfe][Flex_X_v2_18_NO_PIPETTES_Overrides_BadTypesInRTP_Override_wrong_type_in_choice_display_name].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[7d06568bfe][Flex_X_v2_18_NO_PIPETTES_Overrides_BadTypesInRTP_Override_wrong_type_in_choice_display_name].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [], "config": { "apiVersion": [ @@ -42,6 +43,7 @@ } ], "labware": [], + "liquidClasses": [], "liquids": [], "metadata": { "protocolName": "Description Too Long 2.18" diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[7e7a90041b][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_west_column].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[7e7a90041b][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_west_column].json index df37cc2db4b..485e79b56ad 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[7e7a90041b][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_west_column].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[7e7a90041b][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_west_column].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -1481,6 +1482,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "description": "oooo", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[7ebefe4580][pl_QIAseq_FX_24x_Normalizer_Workflow_B].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[7ebefe4580][pl_QIAseq_FX_24x_Normalizer_Workflow_B].json index 47ce454e920..c567c61d095 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[7ebefe4580][pl_QIAseq_FX_24x_Normalizer_Workflow_B].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[7ebefe4580][pl_QIAseq_FX_24x_Normalizer_Workflow_B].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -856,6 +857,11 @@ }, "schemaVersion": 2, "stackingOffsetWithLabware": { + "evotips_flex_96_tiprack_adapter": { + "x": 0, + "y": 0, + "z": 102 + }, "opentrons_96_deep_well_adapter": { "x": 0, "y": 0, @@ -75905,6 +75911,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "AMPure Beads", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[7f2ef0eaff][Flex_X_v2_18_NO_PIPETTES_Overrides_DefaultChoiceNoMatchChoice_Override_float_default_no_matching_choices].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[7f2ef0eaff][Flex_X_v2_18_NO_PIPETTES_Overrides_DefaultChoiceNoMatchChoice_Override_float_default_no_matching_choices].json index 0c559ae74b3..b4b14a04d6d 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[7f2ef0eaff][Flex_X_v2_18_NO_PIPETTES_Overrides_DefaultChoiceNoMatchChoice_Override_float_default_no_matching_choices].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[7f2ef0eaff][Flex_X_v2_18_NO_PIPETTES_Overrides_DefaultChoiceNoMatchChoice_Override_float_default_no_matching_choices].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [], "config": { "apiVersion": [ @@ -42,6 +43,7 @@ } ], "labware": [], + "liquidClasses": [], "liquids": [], "metadata": { "protocolName": "default choice does not match a choice" diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[82e9853b34][Flex_X_v2_16_NO_PIPETTES_TrashBinInCol2].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[82e9853b34][Flex_X_v2_16_NO_PIPETTES_TrashBinInCol2].json index 44584111a12..deb12bf8743 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[82e9853b34][Flex_X_v2_16_NO_PIPETTES_TrashBinInCol2].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[82e9853b34][Flex_X_v2_16_NO_PIPETTES_TrashBinInCol2].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -55,6 +56,7 @@ } ], "labware": [], + "liquidClasses": [], "liquids": [], "metadata": { "author": "Derek Maggio ", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[8455adcea9][OT2_S_v2_12_P300M_P20S_FailOnRun].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[8455adcea9][OT2_S_v2_12_P300M_P20S_FailOnRun].json index 63aed19f5f3..78518efc2cd 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[8455adcea9][OT2_S_v2_12_P300M_P20S_FailOnRun].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[8455adcea9][OT2_S_v2_12_P300M_P20S_FailOnRun].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -2666,6 +2667,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "apiLevel": "2.12", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[84f684cbc4][Flex_S_v2_18_IDT_xGen_EZ_48x].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[84f684cbc4][Flex_S_v2_18_IDT_xGen_EZ_48x].json index 80ce54abbcb..b4e7172cb6c 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[84f684cbc4][Flex_S_v2_18_IDT_xGen_EZ_48x].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[84f684cbc4][Flex_S_v2_18_IDT_xGen_EZ_48x].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -5143,6 +5144,11 @@ }, "schemaVersion": 2, "stackingOffsetWithLabware": { + "evotips_flex_96_tiprack_adapter": { + "x": 0, + "y": 0, + "z": 102 + }, "opentrons_96_deep_well_adapter": { "x": 0, "y": 0, @@ -26879,9 +26885,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -27279,9 +27285,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -27679,9 +27685,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -30782,9 +30788,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -31279,9 +31285,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -31776,9 +31782,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -33692,9 +33698,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -34189,9 +34195,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -34686,9 +34692,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -35066,9 +35072,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -35418,9 +35424,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -35770,9 +35776,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -36120,9 +36126,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -36384,9 +36390,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -36648,9 +36654,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -37023,9 +37029,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -37267,9 +37273,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -37511,9 +37517,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -47537,9 +47543,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -48034,9 +48040,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -48531,9 +48537,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -51538,9 +51544,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -52035,9 +52041,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -52532,9 +52538,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -54352,9 +54358,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -54849,9 +54855,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -55346,9 +55352,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -55726,9 +55732,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -56078,9 +56084,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -56430,9 +56436,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -58003,9 +58009,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -58267,9 +58273,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -58531,9 +58537,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -58906,9 +58912,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -59150,9 +59156,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -59394,9 +59400,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -59919,6 +59925,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "author": "Opentrons ", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[8860ee702c][OT2_S_v2_14_P300M_P20S_HS_TC_TM_SmokeTestV3].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[8860ee702c][OT2_S_v2_14_P300M_P20S_HS_TC_TM_SmokeTestV3].json index 9f3a0d8a1fb..71d5d247e8d 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[8860ee702c][OT2_S_v2_14_P300M_P20S_HS_TC_TM_SmokeTestV3].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[8860ee702c][OT2_S_v2_14_P300M_P20S_HS_TC_TM_SmokeTestV3].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -9159,9 +9160,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -13168,9 +13169,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -13523,9 +13524,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -13877,9 +13878,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -14155,9 +14156,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -14254,9 +14255,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -14372,6 +14373,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "H₂O", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[88abcfdbca][pl_Zymo_Quick_RNA_Cells_Flex_96_Channel].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[88abcfdbca][pl_Zymo_Quick_RNA_Cells_Flex_96_Channel].json index 3cc6db1a5cd..1438f0c6e8d 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[88abcfdbca][pl_Zymo_Quick_RNA_Cells_Flex_96_Channel].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[88abcfdbca][pl_Zymo_Quick_RNA_Cells_Flex_96_Channel].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -828,6 +829,11 @@ }, "schemaVersion": 2, "stackingOffsetWithLabware": { + "evotips_flex_96_tiprack_adapter": { + "x": 0, + "y": 0, + "z": 102 + }, "opentrons_96_deep_well_adapter": { "x": 0, "y": 0, @@ -6713,6 +6719,11 @@ }, "schemaVersion": 2, "stackingOffsetWithLabware": { + "evotips_flex_96_tiprack_adapter": { + "x": 0, + "y": 0, + "z": 102 + }, "opentrons_96_deep_well_adapter": { "x": 0, "y": 0, @@ -9720,6 +9731,11 @@ }, "schemaVersion": 2, "stackingOffsetWithLabware": { + "evotips_flex_96_tiprack_adapter": { + "x": 0, + "y": 0, + "z": 102 + }, "opentrons_96_deep_well_adapter": { "x": 0, "y": 0, @@ -14455,6 +14471,11 @@ }, "schemaVersion": 2, "stackingOffsetWithLabware": { + "evotips_flex_96_tiprack_adapter": { + "x": 0, + "y": 0, + "z": 102 + }, "opentrons_96_deep_well_adapter": { "x": 0, "y": 0, @@ -17462,6 +17483,11 @@ }, "schemaVersion": 2, "stackingOffsetWithLabware": { + "evotips_flex_96_tiprack_adapter": { + "x": 0, + "y": 0, + "z": 102 + }, "opentrons_96_deep_well_adapter": { "x": 0, "y": 0, @@ -20469,6 +20495,11 @@ }, "schemaVersion": 2, "stackingOffsetWithLabware": { + "evotips_flex_96_tiprack_adapter": { + "x": 0, + "y": 0, + "z": 102 + }, "opentrons_96_deep_well_adapter": { "x": 0, "y": 0, @@ -26383,6 +26414,11 @@ }, "schemaVersion": 2, "stackingOffsetWithLabware": { + "evotips_flex_96_tiprack_adapter": { + "x": 0, + "y": 0, + "z": 102 + }, "opentrons_96_deep_well_adapter": { "x": 0, "y": 0, @@ -35657,9 +35693,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -35669,7 +35705,7 @@ "position": { "x": 14.38, "y": 395.38, - "z": 90.88 + "z": 42.12400000000001 } }, "startedAt": "TIMESTAMP", @@ -37911,9 +37947,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -37923,7 +37959,7 @@ "position": { "x": 14.38, "y": 395.38, - "z": 90.88 + "z": 42.12400000000001 } }, "startedAt": "TIMESTAMP", @@ -38287,9 +38323,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -38299,7 +38335,7 @@ "position": { "x": 14.38, "y": 395.38, - "z": 90.88 + "z": 42.12400000000001 } }, "startedAt": "TIMESTAMP", @@ -38467,9 +38503,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -38479,7 +38515,7 @@ "position": { "x": 14.38, "y": 395.38, - "z": 90.88 + "z": 42.12400000000001 } }, "startedAt": "TIMESTAMP", @@ -38783,9 +38819,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -38795,7 +38831,7 @@ "position": { "x": 14.38, "y": 395.38, - "z": 90.88 + "z": 42.12400000000001 } }, "startedAt": "TIMESTAMP", @@ -38963,9 +38999,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -38975,7 +39011,7 @@ "position": { "x": 14.38, "y": 395.38, - "z": 90.88 + "z": 42.12400000000001 } }, "startedAt": "TIMESTAMP", @@ -39279,9 +39315,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -39291,7 +39327,7 @@ "position": { "x": 14.38, "y": 395.38, - "z": 90.88 + "z": 42.12400000000001 } }, "startedAt": "TIMESTAMP", @@ -39459,9 +39495,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -39471,7 +39507,7 @@ "position": { "x": 14.38, "y": 395.38, - "z": 90.88 + "z": 42.12400000000001 } }, "startedAt": "TIMESTAMP", @@ -39775,9 +39811,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -39787,7 +39823,7 @@ "position": { "x": 14.38, "y": 395.38, - "z": 90.88 + "z": 42.12400000000001 } }, "startedAt": "TIMESTAMP", @@ -44404,9 +44440,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -44416,7 +44452,7 @@ "position": { "x": 14.38, "y": 395.38, - "z": 90.88 + "z": 42.12400000000001 } }, "startedAt": "TIMESTAMP", @@ -44582,9 +44618,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -44594,7 +44630,7 @@ "position": { "x": 14.38, "y": 395.38, - "z": 90.88 + "z": 42.12400000000001 } }, "startedAt": "TIMESTAMP", @@ -44928,9 +44964,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -44940,7 +44976,7 @@ "position": { "x": 14.38, "y": 395.38, - "z": 90.88 + "z": 42.12400000000001 } }, "startedAt": "TIMESTAMP", @@ -45108,9 +45144,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -45120,7 +45156,7 @@ "position": { "x": 14.38, "y": 395.38, - "z": 90.88 + "z": 42.12400000000001 } }, "startedAt": "TIMESTAMP", @@ -45424,9 +45460,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -45436,7 +45472,7 @@ "position": { "x": 14.38, "y": 395.38, - "z": 90.88 + "z": 42.12400000000001 } }, "startedAt": "TIMESTAMP", @@ -45604,9 +45640,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -45616,7 +45652,7 @@ "position": { "x": 14.38, "y": 395.38, - "z": 90.88 + "z": 42.12400000000001 } }, "startedAt": "TIMESTAMP", @@ -45920,9 +45956,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -45932,7 +45968,7 @@ "position": { "x": 14.38, "y": 395.38, - "z": 90.88 + "z": 42.12400000000001 } }, "startedAt": "TIMESTAMP", @@ -46100,9 +46136,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -46112,7 +46148,7 @@ "position": { "x": 14.38, "y": 395.38, - "z": 90.88 + "z": 42.12400000000001 } }, "startedAt": "TIMESTAMP", @@ -46416,9 +46452,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -46428,7 +46464,7 @@ "position": { "x": 14.38, "y": 395.38, - "z": 90.88 + "z": 42.12400000000001 } }, "startedAt": "TIMESTAMP", @@ -48597,9 +48633,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -48609,7 +48645,7 @@ "position": { "x": 178.38, "y": 395.38, - "z": 90.88 + "z": 42.12400000000001 } }, "startedAt": "TIMESTAMP", @@ -49004,9 +49040,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -49016,7 +49052,7 @@ "position": { "x": 178.38, "y": 395.38, - "z": 90.88 + "z": 42.12400000000001 } }, "startedAt": "TIMESTAMP", @@ -49169,6 +49205,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "Sample Volume in Shield", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[88de2fb78f][OT2_X_v6_P20S_P300M_HS_HSCollision].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[88de2fb78f][OT2_X_v6_P20S_P300M_HS_HSCollision].json index 5d219d91f72..3e65e6d11ca 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[88de2fb78f][OT2_X_v6_P20S_P300M_HS_HSCollision].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[88de2fb78f][OT2_X_v6_P20S_P300M_HS_HSCollision].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -5373,6 +5374,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[89a255db0b][OT2_X_v2_18_None_None_StrRTPwith_unit].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[89a255db0b][OT2_X_v2_18_None_None_StrRTPwith_unit].json index 2b9cd2584d3..fb3e1ff9524 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[89a255db0b][OT2_X_v2_18_None_None_StrRTPwith_unit].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[89a255db0b][OT2_X_v2_18_None_None_StrRTPwith_unit].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [], "config": { "apiVersion": [ @@ -42,6 +43,7 @@ } ], "labware": [], + "liquidClasses": [], "liquids": [], "metadata": { "protocolName": "Str RTP with unit" diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[89a8226c4e][Flex_X_v2_16_P1000_96_TC_PartialTipPickupThermocyclerLidConflict].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[89a8226c4e][Flex_X_v2_16_P1000_96_TC_PartialTipPickupThermocyclerLidConflict].json index 47511dff64f..a8dc215c6e1 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[89a8226c4e][Flex_X_v2_16_P1000_96_TC_PartialTipPickupThermocyclerLidConflict].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[89a8226c4e][Flex_X_v2_16_P1000_96_TC_PartialTipPickupThermocyclerLidConflict].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -4997,6 +4998,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": {}, "modules": [ diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[8a663305c4][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_south].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[8a663305c4][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_south].json index e2fadc01642..c90d4eae5e8 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[8a663305c4][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_south].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[8a663305c4][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_south].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -1481,6 +1482,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "description": "oooo", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[8b07e799f6][Flex_X_v2_20_96_and_8_Overrides_InvalidConfigs_Override_drop_tip_with_location].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[8b07e799f6][Flex_X_v2_20_96_and_8_Overrides_InvalidConfigs_Override_drop_tip_with_location].json index 919f1980537..546aa74d915 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[8b07e799f6][Flex_X_v2_20_96_and_8_Overrides_InvalidConfigs_Override_drop_tip_with_location].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[8b07e799f6][Flex_X_v2_20_96_and_8_Overrides_InvalidConfigs_Override_drop_tip_with_location].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -3766,6 +3767,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "description": "oooo", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[8e1f35ed6c][pl_NiNTA_Flex_96well_PlatePrep_final].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[8e1f35ed6c][pl_NiNTA_Flex_96well_PlatePrep_final].json index b8dd13f5f42..23a738dea13 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[8e1f35ed6c][pl_NiNTA_Flex_96well_PlatePrep_final].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[8e1f35ed6c][pl_NiNTA_Flex_96well_PlatePrep_final].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -304,6 +305,11 @@ }, "schemaVersion": 2, "stackingOffsetWithLabware": { + "evotips_flex_96_tiprack_adapter": { + "x": 0, + "y": 0, + "z": 102 + }, "opentrons_96_deep_well_adapter": { "x": 0, "y": 0, @@ -1583,6 +1589,11 @@ }, "schemaVersion": 2, "stackingOffsetWithLabware": { + "evotips_flex_96_tiprack_adapter": { + "x": 0, + "y": 0, + "z": 102 + }, "opentrons_96_deep_well_adapter": { "x": 0, "y": 0, @@ -2862,6 +2873,11 @@ }, "schemaVersion": 2, "stackingOffsetWithLabware": { + "evotips_flex_96_tiprack_adapter": { + "x": 0, + "y": 0, + "z": 102 + }, "opentrons_96_deep_well_adapter": { "x": 0, "y": 0, @@ -5194,6 +5210,11 @@ }, "schemaVersion": 2, "stackingOffsetWithLabware": { + "evotips_flex_96_tiprack_adapter": { + "x": 0, + "y": 0, + "z": 102 + }, "opentrons_96_deep_well_adapter": { "x": 0, "y": 0, @@ -10097,9 +10118,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -13393,9 +13414,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -16673,9 +16694,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -18345,9 +18366,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -18477,6 +18498,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "Equilibration Buffer", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[8fcfd2ced0][Flex_S_v2_16_P1000_96_TC_PartialTipPickupColumn].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[8fcfd2ced0][Flex_S_v2_16_P1000_96_TC_PartialTipPickupColumn].json index 1d83bf0706f..69045761942 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[8fcfd2ced0][Flex_S_v2_16_P1000_96_TC_PartialTipPickupColumn].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[8fcfd2ced0][Flex_S_v2_16_P1000_96_TC_PartialTipPickupColumn].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -3727,6 +3728,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": {}, "modules": [], diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[918747b2f9][pl_Illumina_DNA_Prep_48x_v8].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[918747b2f9][pl_Illumina_DNA_Prep_48x_v8].json index 349fbd62034..4e22ae14eb6 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[918747b2f9][pl_Illumina_DNA_Prep_48x_v8].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[918747b2f9][pl_Illumina_DNA_Prep_48x_v8].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -5204,6 +5205,11 @@ }, "schemaVersion": 2, "stackingOffsetWithLabware": { + "evotips_flex_96_tiprack_adapter": { + "x": 0, + "y": 0, + "z": 102 + }, "opentrons_96_deep_well_adapter": { "x": 0, "y": 0, @@ -14066,9 +14072,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -14198,9 +14204,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -14330,9 +14336,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -14909,9 +14915,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -15341,9 +15347,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -15773,9 +15779,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -19116,9 +19122,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -19548,9 +19554,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -19980,9 +19986,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -22136,9 +22142,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -22568,9 +22574,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -23000,9 +23006,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -26343,9 +26349,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -26775,9 +26781,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -27207,9 +27213,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -27587,9 +27593,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -27939,9 +27945,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -28291,9 +28297,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -38623,9 +38629,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -39023,9 +39029,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -39423,9 +39429,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -42387,9 +42393,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -42787,9 +42793,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -43187,9 +43193,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -44964,9 +44970,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -45364,9 +45370,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -45764,9 +45770,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -46144,9 +46150,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -46496,9 +46502,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -46848,9 +46854,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -48489,9 +48495,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -48821,9 +48827,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -49153,9 +49159,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -49448,9 +49454,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -49612,9 +49618,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -49776,9 +49782,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -52167,6 +52173,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "CleanupBead Beads", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[93b724671e][OT2_X_v2_20_8_Overrides_InvalidConfigs_Override_eight_partial_column_bottom_right].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[93b724671e][OT2_X_v2_20_8_Overrides_InvalidConfigs_Override_eight_partial_column_bottom_right].json index 2c3d142321b..85f4f9f5ea7 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[93b724671e][OT2_X_v2_20_8_Overrides_InvalidConfigs_Override_eight_partial_column_bottom_right].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[93b724671e][OT2_X_v2_20_8_Overrides_InvalidConfigs_Override_eight_partial_column_bottom_right].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -2468,6 +2469,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "description": "oooo", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[94913d2988][OT2_S_v3_P300SGen1_None_Gen1PipetteSimple].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[94913d2988][OT2_S_v3_P300SGen1_None_Gen1PipetteSimple].json index 8c086d8fdff..8055e83d1b7 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[94913d2988][OT2_S_v3_P300SGen1_None_Gen1PipetteSimple].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[94913d2988][OT2_S_v3_P300SGen1_None_Gen1PipetteSimple].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -3778,9 +3779,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -3792,9 +3793,9 @@ "y": 0.0, "z": 0.0 }, - "tipDiameter": 0, + "tipDiameter": 0.0, "tipLength": 51.83, - "tipVolume": 300 + "tipVolume": 300.0 }, "startedAt": "TIMESTAMP", "status": "succeeded" @@ -3813,9 +3814,9 @@ "volume": 100.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -3847,9 +3848,9 @@ "volume": 100.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -3880,9 +3881,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -3910,9 +3911,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -3924,9 +3925,9 @@ "y": 0.0, "z": 0.0 }, - "tipDiameter": 0, + "tipDiameter": 0.0, "tipLength": 51.83, - "tipVolume": 300 + "tipVolume": 300.0 }, "startedAt": "TIMESTAMP", "status": "succeeded" @@ -3945,9 +3946,9 @@ "volume": 100.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -3979,9 +3980,9 @@ "volume": 100.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -4012,9 +4013,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -4042,9 +4043,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -4056,9 +4057,9 @@ "y": 0.0, "z": 0.0 }, - "tipDiameter": 0, + "tipDiameter": 0.0, "tipLength": 51.83, - "tipVolume": 300 + "tipVolume": 300.0 }, "startedAt": "TIMESTAMP", "status": "succeeded" @@ -4077,9 +4078,9 @@ "volume": 100.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -4111,9 +4112,9 @@ "volume": 100.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -4144,9 +4145,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -4174,9 +4175,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -4188,9 +4189,9 @@ "y": 0.0, "z": 0.0 }, - "tipDiameter": 0, + "tipDiameter": 0.0, "tipLength": 51.83, - "tipVolume": 300 + "tipVolume": 300.0 }, "startedAt": "TIMESTAMP", "status": "succeeded" @@ -4209,9 +4210,9 @@ "volume": 100.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -4243,9 +4244,9 @@ "volume": 100.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -4276,9 +4277,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -4306,9 +4307,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -4320,9 +4321,9 @@ "y": 0.0, "z": 0.0 }, - "tipDiameter": 0, + "tipDiameter": 0.0, "tipLength": 51.83, - "tipVolume": 300 + "tipVolume": 300.0 }, "startedAt": "TIMESTAMP", "status": "succeeded" @@ -4341,9 +4342,9 @@ "volume": 100.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -4375,9 +4376,9 @@ "volume": 100.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -4408,9 +4409,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -4438,9 +4439,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -4452,9 +4453,9 @@ "y": 0.0, "z": 0.0 }, - "tipDiameter": 0, + "tipDiameter": 0.0, "tipLength": 51.83, - "tipVolume": 300 + "tipVolume": 300.0 }, "startedAt": "TIMESTAMP", "status": "succeeded" @@ -4473,9 +4474,9 @@ "volume": 100.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -4507,9 +4508,9 @@ "volume": 100.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -4540,9 +4541,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -4570,9 +4571,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -4584,9 +4585,9 @@ "y": 0.0, "z": 0.0 }, - "tipDiameter": 0, + "tipDiameter": 0.0, "tipLength": 51.83, - "tipVolume": 300 + "tipVolume": 300.0 }, "startedAt": "TIMESTAMP", "status": "succeeded" @@ -4605,9 +4606,9 @@ "volume": 100.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -4639,9 +4640,9 @@ "volume": 100.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -4672,9 +4673,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -4702,9 +4703,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -4716,9 +4717,9 @@ "y": 0.0, "z": 0.0 }, - "tipDiameter": 0, + "tipDiameter": 0.0, "tipLength": 51.83, - "tipVolume": 300 + "tipVolume": 300.0 }, "startedAt": "TIMESTAMP", "status": "succeeded" @@ -4737,9 +4738,9 @@ "volume": 100.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -4771,9 +4772,9 @@ "volume": 100.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -4804,9 +4805,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -4834,9 +4835,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -4848,9 +4849,9 @@ "y": 0.0, "z": 0.0 }, - "tipDiameter": 0, + "tipDiameter": 0.0, "tipLength": 51.83, - "tipVolume": 300 + "tipVolume": 300.0 }, "startedAt": "TIMESTAMP", "status": "succeeded" @@ -4869,9 +4870,9 @@ "volume": 100.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -4903,9 +4904,9 @@ "volume": 100.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -4936,9 +4937,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -4966,9 +4967,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -4980,9 +4981,9 @@ "y": 0.0, "z": 0.0 }, - "tipDiameter": 0, + "tipDiameter": 0.0, "tipLength": 51.83, - "tipVolume": 300 + "tipVolume": 300.0 }, "startedAt": "TIMESTAMP", "status": "succeeded" @@ -5001,9 +5002,9 @@ "volume": 100.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -5035,9 +5036,9 @@ "volume": 100.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -5068,9 +5069,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -5098,9 +5099,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -5112,9 +5113,9 @@ "y": 0.0, "z": 0.0 }, - "tipDiameter": 0, + "tipDiameter": 0.0, "tipLength": 51.83, - "tipVolume": 300 + "tipVolume": 300.0 }, "startedAt": "TIMESTAMP", "status": "succeeded" @@ -5133,9 +5134,9 @@ "volume": 100.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -5167,9 +5168,9 @@ "volume": 100.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -5200,9 +5201,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -5230,9 +5231,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -5244,9 +5245,9 @@ "y": 0.0, "z": 0.0 }, - "tipDiameter": 0, + "tipDiameter": 0.0, "tipLength": 51.83, - "tipVolume": 300 + "tipVolume": 300.0 }, "startedAt": "TIMESTAMP", "status": "succeeded" @@ -5265,9 +5266,9 @@ "volume": 100.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -5299,9 +5300,9 @@ "volume": 100.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -5332,9 +5333,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -5362,9 +5363,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -5376,9 +5377,9 @@ "y": 0.0, "z": 0.0 }, - "tipDiameter": 0, + "tipDiameter": 0.0, "tipLength": 51.83, - "tipVolume": 300 + "tipVolume": 300.0 }, "startedAt": "TIMESTAMP", "status": "succeeded" @@ -5397,9 +5398,9 @@ "volume": 100.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -5431,9 +5432,9 @@ "volume": 100.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -5464,9 +5465,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -5494,9 +5495,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -5508,9 +5509,9 @@ "y": 0.0, "z": 0.0 }, - "tipDiameter": 0, + "tipDiameter": 0.0, "tipLength": 51.83, - "tipVolume": 300 + "tipVolume": 300.0 }, "startedAt": "TIMESTAMP", "status": "succeeded" @@ -5529,9 +5530,9 @@ "volume": 100.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -5563,9 +5564,9 @@ "volume": 100.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -5596,9 +5597,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -5626,9 +5627,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -5640,9 +5641,9 @@ "y": 0.0, "z": 0.0 }, - "tipDiameter": 0, + "tipDiameter": 0.0, "tipLength": 51.83, - "tipVolume": 300 + "tipVolume": 300.0 }, "startedAt": "TIMESTAMP", "status": "succeeded" @@ -5661,9 +5662,9 @@ "volume": 100.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -5695,9 +5696,9 @@ "volume": 100.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -5728,9 +5729,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -5758,9 +5759,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -5772,9 +5773,9 @@ "y": 0.0, "z": 0.0 }, - "tipDiameter": 0, + "tipDiameter": 0.0, "tipLength": 51.83, - "tipVolume": 300 + "tipVolume": 300.0 }, "startedAt": "TIMESTAMP", "status": "succeeded" @@ -5793,9 +5794,9 @@ "volume": 100.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -5827,9 +5828,9 @@ "volume": 100.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -5860,9 +5861,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -5890,9 +5891,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -5904,9 +5905,9 @@ "y": 0.0, "z": 0.0 }, - "tipDiameter": 0, + "tipDiameter": 0.0, "tipLength": 51.83, - "tipVolume": 300 + "tipVolume": 300.0 }, "startedAt": "TIMESTAMP", "status": "succeeded" @@ -5925,9 +5926,9 @@ "volume": 100.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -5959,9 +5960,9 @@ "volume": 100.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -5992,9 +5993,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -6022,9 +6023,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -6036,9 +6037,9 @@ "y": 0.0, "z": 0.0 }, - "tipDiameter": 0, + "tipDiameter": 0.0, "tipLength": 51.83, - "tipVolume": 300 + "tipVolume": 300.0 }, "startedAt": "TIMESTAMP", "status": "succeeded" @@ -6057,9 +6058,9 @@ "volume": 100.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -6091,9 +6092,9 @@ "volume": 100.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -6124,9 +6125,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -6154,9 +6155,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -6168,9 +6169,9 @@ "y": 0.0, "z": 0.0 }, - "tipDiameter": 0, + "tipDiameter": 0.0, "tipLength": 51.83, - "tipVolume": 300 + "tipVolume": 300.0 }, "startedAt": "TIMESTAMP", "status": "succeeded" @@ -6189,9 +6190,9 @@ "volume": 100.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -6223,9 +6224,9 @@ "volume": 100.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -6256,9 +6257,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -6286,9 +6287,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -6300,9 +6301,9 @@ "y": 0.0, "z": 0.0 }, - "tipDiameter": 0, + "tipDiameter": 0.0, "tipLength": 51.83, - "tipVolume": 300 + "tipVolume": 300.0 }, "startedAt": "TIMESTAMP", "status": "succeeded" @@ -6321,9 +6322,9 @@ "volume": 100.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -6355,9 +6356,9 @@ "volume": 100.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -6388,9 +6389,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -6474,6 +6475,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "author": "", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[95da6fbef2][Flex_S_2_15_P1000M_GRIP_HS_TM_MB_OmegaHDQDNAExtractionBacteria].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[95da6fbef2][Flex_S_2_15_P1000M_GRIP_HS_TM_MB_OmegaHDQDNAExtractionBacteria].json index 85ee931590d..38e6643aa88 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[95da6fbef2][Flex_S_2_15_P1000M_GRIP_HS_TM_MB_OmegaHDQDNAExtractionBacteria].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[95da6fbef2][Flex_S_2_15_P1000M_GRIP_HS_TM_MB_OmegaHDQDNAExtractionBacteria].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -828,6 +829,11 @@ }, "schemaVersion": 2, "stackingOffsetWithLabware": { + "evotips_flex_96_tiprack_adapter": { + "x": 0, + "y": 0, + "z": 102 + }, "opentrons_96_deep_well_adapter": { "x": 0, "y": 0, @@ -2107,6 +2113,11 @@ }, "schemaVersion": 2, "stackingOffsetWithLabware": { + "evotips_flex_96_tiprack_adapter": { + "x": 0, + "y": 0, + "z": 102 + }, "opentrons_96_deep_well_adapter": { "x": 0, "y": 0, @@ -6250,6 +6261,11 @@ }, "schemaVersion": 2, "stackingOffsetWithLabware": { + "evotips_flex_96_tiprack_adapter": { + "x": 0, + "y": 0, + "z": 102 + }, "opentrons_96_deep_well_adapter": { "x": 0, "y": 0, @@ -7529,6 +7545,11 @@ }, "schemaVersion": 2, "stackingOffsetWithLabware": { + "evotips_flex_96_tiprack_adapter": { + "x": 0, + "y": 0, + "z": 102 + }, "opentrons_96_deep_well_adapter": { "x": 0, "y": 0, @@ -8808,6 +8829,11 @@ }, "schemaVersion": 2, "stackingOffsetWithLabware": { + "evotips_flex_96_tiprack_adapter": { + "x": 0, + "y": 0, + "z": 102 + }, "opentrons_96_deep_well_adapter": { "x": 0, "y": 0, @@ -10087,6 +10113,11 @@ }, "schemaVersion": 2, "stackingOffsetWithLabware": { + "evotips_flex_96_tiprack_adapter": { + "x": 0, + "y": 0, + "z": 102 + }, "opentrons_96_deep_well_adapter": { "x": 0, "y": 0, @@ -11366,6 +11397,11 @@ }, "schemaVersion": 2, "stackingOffsetWithLabware": { + "evotips_flex_96_tiprack_adapter": { + "x": 0, + "y": 0, + "z": 102 + }, "opentrons_96_deep_well_adapter": { "x": 0, "y": 0, @@ -20245,9 +20281,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -20257,7 +20293,7 @@ "position": { "x": 14.38, "y": 395.38, - "z": 90.88 + "z": 42.12400000000001 } }, "startedAt": "TIMESTAMP", @@ -20549,9 +20585,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -20561,7 +20597,7 @@ "position": { "x": 14.38, "y": 395.38, - "z": 90.88 + "z": 42.12400000000001 } }, "startedAt": "TIMESTAMP", @@ -25823,9 +25859,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -25835,7 +25871,7 @@ "position": { "x": 14.38, "y": 395.38, - "z": 90.88 + "z": 42.12400000000001 } }, "startedAt": "TIMESTAMP", @@ -27783,9 +27819,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -27795,7 +27831,7 @@ "position": { "x": 14.38, "y": 395.38, - "z": 90.88 + "z": 42.12400000000001 } }, "startedAt": "TIMESTAMP", @@ -28055,9 +28091,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -28067,7 +28103,7 @@ "position": { "x": 14.38, "y": 395.38, - "z": 90.88 + "z": 42.12400000000001 } }, "startedAt": "TIMESTAMP", @@ -28347,9 +28383,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -28359,7 +28395,7 @@ "position": { "x": 14.38, "y": 395.38, - "z": 90.88 + "z": 42.12400000000001 } }, "startedAt": "TIMESTAMP", @@ -28604,9 +28640,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -28616,7 +28652,7 @@ "position": { "x": 14.38, "y": 395.38, - "z": 90.88 + "z": 42.12400000000001 } }, "startedAt": "TIMESTAMP", @@ -28880,9 +28916,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -28892,7 +28928,7 @@ "position": { "x": 14.38, "y": 395.38, - "z": 90.88 + "z": 42.12400000000001 } }, "startedAt": "TIMESTAMP", @@ -29137,9 +29173,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -29149,7 +29185,7 @@ "position": { "x": 14.38, "y": 395.38, - "z": 90.88 + "z": 42.12400000000001 } }, "startedAt": "TIMESTAMP", @@ -29413,9 +29449,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -29425,7 +29461,7 @@ "position": { "x": 14.38, "y": 395.38, - "z": 90.88 + "z": 42.12400000000001 } }, "startedAt": "TIMESTAMP", @@ -29670,9 +29706,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -29682,7 +29718,7 @@ "position": { "x": 14.38, "y": 395.38, - "z": 90.88 + "z": 42.12400000000001 } }, "startedAt": "TIMESTAMP", @@ -31848,9 +31884,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -31860,7 +31896,7 @@ "position": { "x": 178.38, "y": 395.38, - "z": 90.88 + "z": 42.12400000000001 } }, "startedAt": "TIMESTAMP", @@ -32092,9 +32128,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -32104,7 +32140,7 @@ "position": { "x": 178.38, "y": 395.38, - "z": 90.88 + "z": 42.12400000000001 } }, "startedAt": "TIMESTAMP", @@ -32273,6 +32309,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "author": "Zach Galluzzo ", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[9618a6623c][OT2_X_v2_11_P300S_TC1_TC2_ThermocyclerMoamError].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[9618a6623c][OT2_X_v2_11_P300S_TC1_TC2_ThermocyclerMoamError].json index cbad73a3a2d..407236c5c35 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[9618a6623c][OT2_X_v2_11_P300S_TC1_TC2_ThermocyclerMoamError].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[9618a6623c][OT2_X_v2_11_P300S_TC1_TC2_ThermocyclerMoamError].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -2775,6 +2776,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "apiLevel": "2.11" diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[973fa979e6][Flex_S_v2_16_NO_PIPETTES_TC_verifyThermocyclerLoadedSlots].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[973fa979e6][Flex_S_v2_16_NO_PIPETTES_TC_verifyThermocyclerLoadedSlots].json index cbf301b89e7..51ed5bbe694 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[973fa979e6][Flex_S_v2_16_NO_PIPETTES_TC_verifyThermocyclerLoadedSlots].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[973fa979e6][Flex_S_v2_16_NO_PIPETTES_TC_verifyThermocyclerLoadedSlots].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -171,6 +172,7 @@ } ], "labware": [], + "liquidClasses": [], "liquids": [], "metadata": {}, "modules": [ diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[9bcb0a3f13][pl_normalization_with_csv].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[9bcb0a3f13][pl_normalization_with_csv].json index b0f0b8ac0bd..ccab060d501 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[9bcb0a3f13][pl_normalization_with_csv].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[9bcb0a3f13][pl_normalization_with_csv].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -6024,6 +6025,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "author": "Krishna Soma ", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[9e56ee92f6][Flex_X_v2_16_P1000_96_GRIP_DropLabwareIntoTrashBin].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[9e56ee92f6][Flex_X_v2_16_P1000_96_GRIP_DropLabwareIntoTrashBin].json index 8d4e3a960dd..6263cb86172 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[9e56ee92f6][Flex_X_v2_16_P1000_96_GRIP_DropLabwareIntoTrashBin].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[9e56ee92f6][Flex_X_v2_16_P1000_96_GRIP_DropLabwareIntoTrashBin].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -1435,6 +1436,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "author": "Derek Maggio ", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a01a35c14a][Flex_X_v2_16_NO_PIPETTES_TrashBinInStagingAreaCol3].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a01a35c14a][Flex_X_v2_16_NO_PIPETTES_TrashBinInStagingAreaCol3].json index 5a508d84d58..ddc34e08100 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a01a35c14a][Flex_X_v2_16_NO_PIPETTES_TrashBinInStagingAreaCol3].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a01a35c14a][Flex_X_v2_16_NO_PIPETTES_TrashBinInStagingAreaCol3].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -153,6 +154,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "author": "Derek Maggio ", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a06502b2dc][Flex_X_v2_18_NO_PIPETTES_Overrides_BadTypesInRTP_Override_wrong_type_in_description].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a06502b2dc][Flex_X_v2_18_NO_PIPETTES_Overrides_BadTypesInRTP_Override_wrong_type_in_description].json index 7808bbc2d03..64da6dc2027 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a06502b2dc][Flex_X_v2_18_NO_PIPETTES_Overrides_BadTypesInRTP_Override_wrong_type_in_description].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a06502b2dc][Flex_X_v2_18_NO_PIPETTES_Overrides_BadTypesInRTP_Override_wrong_type_in_description].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [], "config": { "apiVersion": [ @@ -42,6 +43,7 @@ } ], "labware": [], + "liquidClasses": [], "liquids": [], "metadata": { "protocolName": "Description Too Long 2.18" diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a08c261369][Flex_S_v2_16_P1000_96_GRIP_DeckConfiguration1NoModulesNoFixtures].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a08c261369][Flex_S_v2_16_P1000_96_GRIP_DeckConfiguration1NoModulesNoFixtures].json index f951219fdff..f8b7ae9a636 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a08c261369][Flex_S_v2_16_P1000_96_GRIP_DeckConfiguration1NoModulesNoFixtures].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a08c261369][Flex_S_v2_16_P1000_96_GRIP_DeckConfiguration1NoModulesNoFixtures].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -8987,9 +8988,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -8999,7 +9000,7 @@ "position": { "x": 342.38, "y": 181.38, - "z": 90.88 + "z": 42.12400000000001 } }, "startedAt": "TIMESTAMP", @@ -9119,9 +9120,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -9131,7 +9132,7 @@ "position": { "x": 178.38, "y": 74.38, - "z": 90.88 + "z": 42.12400000000001 } }, "startedAt": "TIMESTAMP", @@ -9319,9 +9320,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -9331,7 +9332,7 @@ "position": { "x": 342.38, "y": 181.38, - "z": 90.88 + "z": 42.12400000000001 } }, "startedAt": "TIMESTAMP", @@ -9451,9 +9452,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -9463,7 +9464,7 @@ "position": { "x": 178.38, "y": 74.38, - "z": 90.88 + "z": 42.12400000000001 } }, "startedAt": "TIMESTAMP", @@ -9575,6 +9576,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "High Quality H₂O", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a0b755a1a1][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_north_west].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a0b755a1a1][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_north_west].json index eca34fc28c3..5113dbf3b85 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a0b755a1a1][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_north_west].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a0b755a1a1][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_north_west].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -1481,6 +1482,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "description": "oooo", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a0dad2eb8e][pl_SamplePrep_MS_Cleanup_Flex_upto96].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a0dad2eb8e][pl_SamplePrep_MS_Cleanup_Flex_upto96].json index 965ca7d3ead..a42786e59e3 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a0dad2eb8e][pl_SamplePrep_MS_Cleanup_Flex_upto96].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a0dad2eb8e][pl_SamplePrep_MS_Cleanup_Flex_upto96].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -2123,6 +2124,11 @@ }, "schemaVersion": 2, "stackingOffsetWithLabware": { + "evotips_flex_96_tiprack_adapter": { + "x": 0, + "y": 0, + "z": 102 + }, "opentrons_96_deep_well_adapter": { "x": 0, "y": 0, @@ -3651,6 +3657,11 @@ }, "schemaVersion": 2, "stackingOffsetWithLabware": { + "evotips_flex_96_tiprack_adapter": { + "x": 0, + "y": 0, + "z": 102 + }, "opentrons_96_deep_well_adapter": { "x": 0, "y": 0, @@ -14231,9 +14242,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -14729,9 +14740,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -15227,9 +15238,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -15725,9 +15736,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -16223,9 +16234,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -16721,9 +16732,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -17219,9 +17230,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -17717,9 +17728,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -18215,9 +18226,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -18713,9 +18724,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -19211,9 +19222,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -19709,9 +19720,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -26505,9 +26516,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -30169,9 +30180,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -30554,9 +30565,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -30784,9 +30795,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -31014,9 +31025,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -31244,9 +31255,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -31474,9 +31485,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -31704,9 +31715,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -31934,9 +31945,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -32164,9 +32175,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -32394,9 +32405,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -32624,9 +32635,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -32854,9 +32865,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -33084,9 +33095,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -33314,9 +33325,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -33544,9 +33555,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -33774,9 +33785,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -34004,9 +34015,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -34234,9 +34245,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -34464,9 +34475,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -34694,9 +34705,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -34924,9 +34935,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -35154,9 +35165,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -35384,9 +35395,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -35614,9 +35625,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -35844,9 +35855,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -39540,9 +39551,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -39925,9 +39936,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -40155,9 +40166,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -40385,9 +40396,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -40615,9 +40626,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -40845,9 +40856,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -41075,9 +41086,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -41305,9 +41316,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -41535,9 +41546,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -41765,9 +41776,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -41995,9 +42006,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -42225,9 +42236,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -42455,9 +42466,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -42685,9 +42696,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -42915,9 +42926,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -43145,9 +43156,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -43375,9 +43386,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -43605,9 +43616,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -43835,9 +43846,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -44065,9 +44076,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -44295,9 +44306,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -44525,9 +44536,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -44755,9 +44766,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -44985,9 +44996,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -45215,9 +45226,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -47485,9 +47496,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -47790,9 +47801,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -48002,9 +48013,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -48214,9 +48225,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -48426,9 +48437,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -48638,9 +48649,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -48850,9 +48861,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -49062,9 +49073,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -49274,9 +49285,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -49486,9 +49497,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -49698,9 +49709,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -49910,9 +49921,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -50122,9 +50133,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -50265,6 +50276,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "Digested Protein samples, volume per well", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a3dfca7f0c][Flex_S_v2_19_Illumina_DNA_PCR_Free].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a3dfca7f0c][Flex_S_v2_19_Illumina_DNA_PCR_Free].json index 1c3e57b481a..6da5b62e4ec 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a3dfca7f0c][Flex_S_v2_19_Illumina_DNA_PCR_Free].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a3dfca7f0c][Flex_S_v2_19_Illumina_DNA_PCR_Free].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -6157,6 +6158,11 @@ }, "schemaVersion": 2, "stackingOffsetWithLabware": { + "evotips_flex_96_tiprack_adapter": { + "x": 0, + "y": 0, + "z": 102 + }, "opentrons_96_deep_well_adapter": { "x": 0, "y": 0, @@ -27030,6 +27036,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "DNA sample of known quantity", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a437534569][Flex_S_v2_19_kapahyperplus].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a437534569][Flex_S_v2_19_kapahyperplus].json index 42781ff6ea1..814c874d188 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a437534569][Flex_S_v2_19_kapahyperplus].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a437534569][Flex_S_v2_19_kapahyperplus].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -6865,6 +6866,11 @@ }, "schemaVersion": 2, "stackingOffsetWithLabware": { + "evotips_flex_96_tiprack_adapter": { + "x": 0, + "y": 0, + "z": 102 + }, "opentrons_96_deep_well_adapter": { "x": 0, "y": 0, @@ -29144,6 +29150,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "DNA sample of known quantity", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a46928c103][pl_Nanopore_Genomic_Ligation_v5_Final].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a46928c103][pl_Nanopore_Genomic_Ligation_v5_Final].json index be8c1a00d13..8faa3209168 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a46928c103][pl_Nanopore_Genomic_Ligation_v5_Final].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a46928c103][pl_Nanopore_Genomic_Ligation_v5_Final].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -2422,6 +2423,11 @@ }, "schemaVersion": 2, "stackingOffsetWithLabware": { + "evotips_flex_96_tiprack_adapter": { + "x": 0, + "y": 0, + "z": 102 + }, "opentrons_96_deep_well_adapter": { "x": 0, "y": 0, @@ -22844,6 +22850,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "AMPure Beads", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a4d3b3a2d3][pl_96_ch_demo_rtp_with_hs].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a4d3b3a2d3][pl_96_ch_demo_rtp_with_hs].json index 8a0a8a6a2ee..1d06001caa0 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a4d3b3a2d3][pl_96_ch_demo_rtp_with_hs].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a4d3b3a2d3][pl_96_ch_demo_rtp_with_hs].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -4523,6 +4524,11 @@ }, "schemaVersion": 2, "stackingOffsetWithLabware": { + "evotips_flex_96_tiprack_adapter": { + "x": 0, + "y": 0, + "z": 102 + }, "opentrons_96_deep_well_adapter": { "x": 0, "y": 0, @@ -5802,6 +5808,11 @@ }, "schemaVersion": 2, "stackingOffsetWithLabware": { + "evotips_flex_96_tiprack_adapter": { + "x": 0, + "y": 0, + "z": 102 + }, "opentrons_96_deep_well_adapter": { "x": 0, "y": 0, @@ -10078,9 +10089,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -10090,7 +10101,7 @@ "position": { "x": 14.38, "y": 395.38, - "z": 90.88 + "z": 42.12400000000001 } }, "startedAt": "TIMESTAMP", @@ -10420,9 +10431,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -10432,7 +10443,7 @@ "position": { "x": 14.38, "y": 395.38, - "z": 90.88 + "z": 42.12400000000001 } }, "startedAt": "TIMESTAMP", @@ -10766,9 +10777,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -10778,7 +10789,7 @@ "position": { "x": 178.38, "y": 395.38, - "z": 98.33 + "z": 74.99000000000001 } }, "startedAt": "TIMESTAMP", @@ -10998,9 +11009,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -11010,7 +11021,7 @@ "position": { "x": 14.38, "y": 395.38, - "z": 90.88 + "z": 42.12400000000001 } }, "startedAt": "TIMESTAMP", @@ -11340,9 +11351,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -11352,7 +11363,7 @@ "position": { "x": 14.38, "y": 395.38, - "z": 90.88 + "z": 42.12400000000001 } }, "startedAt": "TIMESTAMP", @@ -11686,9 +11697,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -11698,7 +11709,7 @@ "position": { "x": 178.38, "y": 395.38, - "z": 98.33 + "z": 74.99000000000001 } }, "startedAt": "TIMESTAMP", @@ -12036,9 +12047,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -12048,7 +12059,7 @@ "position": { "x": 342.38, "y": 395.38, - "z": 98.42 + "z": 75.26 } }, "startedAt": "TIMESTAMP", @@ -12365,9 +12376,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -12377,7 +12388,7 @@ "position": { "x": 342.38, "y": 395.38, - "z": 98.42 + "z": 75.26 } }, "startedAt": "TIMESTAMP", @@ -12698,9 +12709,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -12710,7 +12721,7 @@ "position": { "x": 342.38, "y": 395.38, - "z": 98.42 + "z": 75.26 } }, "startedAt": "TIMESTAMP", @@ -16593,6 +16604,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "generic", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a66d700ed6][OT2_S_v2_13_P300M_P20S_HS_TC_TM_SmokeTestV3].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a66d700ed6][OT2_S_v2_13_P300M_P20S_HS_TC_TM_SmokeTestV3].json index 321a04e20ac..6248d78b0ad 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a66d700ed6][OT2_S_v2_13_P300M_P20S_HS_TC_TM_SmokeTestV3].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a66d700ed6][OT2_S_v2_13_P300M_P20S_HS_TC_TM_SmokeTestV3].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -8483,9 +8484,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -8497,9 +8498,9 @@ "y": 0.0, "z": 0.0 }, - "tipDiameter": 0, + "tipDiameter": 0.0, "tipLength": 30.950000000000003, - "tipVolume": 20 + "tipVolume": 20.0 }, "startedAt": "TIMESTAMP", "status": "succeeded" @@ -8518,9 +8519,9 @@ "volume": 19.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -8552,9 +8553,9 @@ "volume": 18.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -8585,9 +8586,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -8618,9 +8619,9 @@ "volume": 19.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -8652,9 +8653,9 @@ "volume": 18.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -8685,9 +8686,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -8718,9 +8719,9 @@ "volume": 19.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -8752,9 +8753,9 @@ "volume": 18.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -8785,9 +8786,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -8818,9 +8819,9 @@ "volume": 19.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -8852,9 +8853,9 @@ "volume": 18.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -8885,9 +8886,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -8918,9 +8919,9 @@ "volume": 19.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -8952,9 +8953,9 @@ "volume": 18.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -8985,9 +8986,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -9017,9 +9018,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -9047,9 +9048,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -9061,9 +9062,9 @@ "y": 0.0, "z": 0.0 }, - "tipDiameter": 0, + "tipDiameter": 0.0, "tipLength": 30.950000000000003, - "tipVolume": 20 + "tipVolume": 20.0 }, "startedAt": "TIMESTAMP", "status": "succeeded" @@ -9082,9 +9083,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -9116,9 +9117,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -9150,9 +9151,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -9184,9 +9185,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -9218,9 +9219,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -9252,9 +9253,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -9286,9 +9287,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -9335,9 +9336,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -9369,9 +9370,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -9403,9 +9404,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -9451,9 +9452,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -9484,9 +9485,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -9518,9 +9519,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -9552,9 +9553,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -9586,9 +9587,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -9620,9 +9621,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -9654,9 +9655,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -9688,9 +9689,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -9737,9 +9738,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -9771,9 +9772,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -9805,9 +9806,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -9853,9 +9854,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -9886,9 +9887,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -9920,9 +9921,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -9954,9 +9955,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -9988,9 +9989,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -10022,9 +10023,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -10056,9 +10057,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -10090,9 +10091,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -10139,9 +10140,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -10173,9 +10174,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -10207,9 +10208,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -10255,9 +10256,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -10288,9 +10289,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -10322,9 +10323,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -10356,9 +10357,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -10390,9 +10391,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -10424,9 +10425,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -10458,9 +10459,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -10492,9 +10493,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -10541,9 +10542,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -10575,9 +10576,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -10609,9 +10610,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -10657,9 +10658,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -10690,9 +10691,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -10724,9 +10725,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -10758,9 +10759,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -10792,9 +10793,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -10826,9 +10827,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -10860,9 +10861,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -10894,9 +10895,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -10943,9 +10944,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -10977,9 +10978,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -11011,9 +11012,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -11059,9 +11060,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -11092,9 +11093,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -11126,9 +11127,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -11160,9 +11161,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -11194,9 +11195,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -11228,9 +11229,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -11262,9 +11263,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -11296,9 +11297,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -11345,9 +11346,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -11379,9 +11380,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -11413,9 +11414,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -11461,9 +11462,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -11494,9 +11495,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -11528,9 +11529,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -11562,9 +11563,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -11596,9 +11597,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -11630,9 +11631,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -11664,9 +11665,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -11698,9 +11699,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -11747,9 +11748,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -11781,9 +11782,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -11815,9 +11816,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -11863,9 +11864,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -11896,9 +11897,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -11930,9 +11931,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -11964,9 +11965,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -11998,9 +11999,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -12032,9 +12033,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -12066,9 +12067,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -12100,9 +12101,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -12149,9 +12150,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -12183,9 +12184,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -12217,9 +12218,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -12265,9 +12266,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -12298,9 +12299,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -12332,9 +12333,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -12366,9 +12367,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -12400,9 +12401,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -12434,9 +12435,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -12468,9 +12469,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -12502,9 +12503,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -12551,9 +12552,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -12585,9 +12586,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -12619,9 +12620,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -12667,9 +12668,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -12700,9 +12701,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -12734,9 +12735,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -12767,9 +12768,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -12800,9 +12801,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -12834,9 +12835,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -12867,9 +12868,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -12900,9 +12901,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -12934,9 +12935,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -12967,9 +12968,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -12999,9 +13000,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -13029,9 +13030,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -13043,9 +13044,9 @@ "y": 0.0, "z": 0.0 }, - "tipDiameter": 0, + "tipDiameter": 0.0, "tipLength": 30.950000000000003, - "tipVolume": 20 + "tipVolume": 20.0 }, "startedAt": "TIMESTAMP", "status": "succeeded" @@ -13064,9 +13065,9 @@ "volume": 5.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -13098,9 +13099,9 @@ "volume": 10.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -13147,9 +13148,9 @@ "volume": 5.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -13210,9 +13211,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -13257,9 +13258,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -13495,9 +13496,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -13509,9 +13510,9 @@ "y": 0.0, "z": 0.0 }, - "tipDiameter": 0, + "tipDiameter": 0.0, "tipLength": 30.950000000000003, - "tipVolume": 20 + "tipVolume": 20.0 }, "startedAt": "TIMESTAMP", "status": "succeeded" @@ -13530,9 +13531,9 @@ "volume": 15.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -13564,9 +13565,9 @@ "volume": 15.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -13597,9 +13598,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -13627,9 +13628,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -13641,9 +13642,9 @@ "y": 0.0, "z": 0.0 }, - "tipDiameter": 0, + "tipDiameter": 0.0, "tipLength": 51.099999999999994, - "tipVolume": 300 + "tipVolume": 300.0 }, "startedAt": "TIMESTAMP", "status": "succeeded" @@ -13662,9 +13663,9 @@ "volume": 50.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -13696,9 +13697,9 @@ "volume": 50.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -13773,9 +13774,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -13787,9 +13788,9 @@ "y": 0.0, "z": 0.0 }, - "tipDiameter": 0, + "tipDiameter": 0.0, "tipLength": 30.950000000000003, - "tipVolume": 20 + "tipVolume": 20.0 }, "startedAt": "TIMESTAMP", "status": "succeeded" @@ -13808,9 +13809,9 @@ "volume": 10.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -13842,9 +13843,9 @@ "volume": 10.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -13875,9 +13876,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -13907,9 +13908,9 @@ "volume": 75.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -13941,9 +13942,9 @@ "volume": 60.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -13974,9 +13975,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -14092,6 +14093,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "author": "Opentrons Engineering ", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a8528e52b4][Flex_S_v2_20_96_None_SINGLE_HappyPathSouthSide].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a8528e52b4][Flex_S_v2_20_96_None_SINGLE_HappyPathSouthSide].json index f4e89bf46a3..aa07f2c7074 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a8528e52b4][Flex_S_v2_20_96_None_SINGLE_HappyPathSouthSide].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a8528e52b4][Flex_S_v2_20_96_None_SINGLE_HappyPathSouthSide].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -9431,6 +9432,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "description": "Unsafe protocol ❗❗❗❗❗❗❗❗❗❗❗ will collide with tube.", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a8a5ad823d][pl_cherrypicking_flex_v3].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a8a5ad823d][pl_cherrypicking_flex_v3].json index 6afef67d006..9be279fa95e 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a8a5ad823d][pl_cherrypicking_flex_v3].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a8a5ad823d][pl_cherrypicking_flex_v3].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -1815,6 +1816,11 @@ }, "schemaVersion": 2, "stackingOffsetWithLabware": { + "evotips_flex_96_tiprack_adapter": { + "x": 0, + "y": 0, + "z": 102 + }, "opentrons_96_deep_well_adapter": { "x": 0, "y": 0, @@ -9977,9 +9983,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -10109,9 +10115,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -10241,9 +10247,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -10373,6 +10379,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "author": "Krishna Soma ", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a94f22054f][Flex_S_v2_18_kapahyperplus].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a94f22054f][Flex_S_v2_18_kapahyperplus].json index 01ce458ff53..784df7d0aa1 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a94f22054f][Flex_S_v2_18_kapahyperplus].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a94f22054f][Flex_S_v2_18_kapahyperplus].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -6865,6 +6866,11 @@ }, "schemaVersion": 2, "stackingOffsetWithLabware": { + "evotips_flex_96_tiprack_adapter": { + "x": 0, + "y": 0, + "z": 102 + }, "opentrons_96_deep_well_adapter": { "x": 0, "y": 0, @@ -29144,6 +29150,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "DNA sample of known quantity", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a9557d762c][Flex_X_v2_16_NO_PIPETTES_AccessToFixedTrashProp].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a9557d762c][Flex_X_v2_16_NO_PIPETTES_AccessToFixedTrashProp].json index 60a0f1c77a3..f9d80664de4 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a9557d762c][Flex_X_v2_16_NO_PIPETTES_AccessToFixedTrashProp].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a9557d762c][Flex_X_v2_16_NO_PIPETTES_AccessToFixedTrashProp].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -55,6 +56,7 @@ } ], "labware": [], + "liquidClasses": [], "liquids": [], "metadata": { "author": "Derek Maggio ", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[aa61eee0bf][pl_sigdx_part2].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[aa61eee0bf][pl_sigdx_part2].json index 8e14d013357..6c5410c683b 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[aa61eee0bf][pl_sigdx_part2].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[aa61eee0bf][pl_sigdx_part2].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -762,6 +763,11 @@ }, "schemaVersion": 2, "stackingOffsetWithLabware": { + "evotips_flex_96_tiprack_adapter": { + "x": 0, + "y": 0, + "z": 102 + }, "opentrons_96_deep_well_adapter": { "x": 0, "y": 0, @@ -2622,6 +2628,11 @@ }, "schemaVersion": 2, "stackingOffsetWithLabware": { + "evotips_flex_96_tiprack_adapter": { + "x": 0, + "y": 0, + "z": 102 + }, "opentrons_96_deep_well_adapter": { "x": 0, "y": 0, @@ -3900,6 +3911,11 @@ }, "schemaVersion": 2, "stackingOffsetWithLabware": { + "evotips_flex_96_tiprack_adapter": { + "x": 0, + "y": 0, + "z": 102 + }, "opentrons_96_deep_well_adapter": { "x": 0, "y": 0, @@ -46385,6 +46401,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "Beads", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ac5a46e74b][pl_langone_pt2_ribo].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ac5a46e74b][pl_langone_pt2_ribo].json index b4324589435..f4bafb81e72 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ac5a46e74b][pl_langone_pt2_ribo].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ac5a46e74b][pl_langone_pt2_ribo].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -8493,6 +8494,11 @@ }, "schemaVersion": 2, "stackingOffsetWithLabware": { + "evotips_flex_96_tiprack_adapter": { + "x": 0, + "y": 0, + "z": 102 + }, "opentrons_96_deep_well_adapter": { "x": 0, "y": 0, @@ -17931,9 +17937,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -18613,9 +18619,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -19291,9 +19297,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -21619,9 +21625,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -21923,9 +21929,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -22327,9 +22333,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -22717,9 +22723,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -23822,9 +23828,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -24082,9 +24088,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -24308,9 +24314,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -24905,9 +24911,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -27546,9 +27552,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -27850,9 +27856,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -28222,9 +28228,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -28580,9 +28586,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -29685,9 +29691,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -29945,9 +29951,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -30083,6 +30089,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "ATL4", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ac886d7768][Flex_S_v2_15_P1000_96_GRIP_HS_MB_TC_TM_IDTXgen96Part1to3].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ac886d7768][Flex_S_v2_15_P1000_96_GRIP_HS_MB_TC_TM_IDTXgen96Part1to3].json index ef9acd1b1a3..bba8b7b6850 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ac886d7768][Flex_S_v2_15_P1000_96_GRIP_HS_MB_TC_TM_IDTXgen96Part1to3].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ac886d7768][Flex_S_v2_15_P1000_96_GRIP_HS_MB_TC_TM_IDTXgen96Part1to3].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -1505,6 +1506,11 @@ }, "schemaVersion": 2, "stackingOffsetWithLabware": { + "evotips_flex_96_tiprack_adapter": { + "x": 0, + "y": 0, + "z": 102 + }, "opentrons_96_deep_well_adapter": { "x": 0, "y": 0, @@ -4018,6 +4024,11 @@ }, "schemaVersion": 2, "stackingOffsetWithLabware": { + "evotips_flex_96_tiprack_adapter": { + "x": 0, + "y": 0, + "z": 102 + }, "opentrons_96_deep_well_adapter": { "x": 0, "y": 0, @@ -7697,6 +7708,11 @@ }, "schemaVersion": 2, "stackingOffsetWithLabware": { + "evotips_flex_96_tiprack_adapter": { + "x": 0, + "y": 0, + "z": 102 + }, "opentrons_96_deep_well_adapter": { "x": 0, "y": 0, @@ -10202,6 +10218,11 @@ }, "schemaVersion": 2, "stackingOffsetWithLabware": { + "evotips_flex_96_tiprack_adapter": { + "x": 0, + "y": 0, + "z": 102 + }, "opentrons_96_deep_well_adapter": { "x": 0, "y": 0, @@ -11492,9 +11513,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -11504,7 +11525,7 @@ "position": { "x": 342.38, "y": 181.38, - "z": 98.42 + "z": 75.26 } }, "startedAt": "TIMESTAMP", @@ -12062,9 +12083,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -12074,7 +12095,7 @@ "position": { "x": 342.38, "y": 181.38, - "z": 98.42 + "z": 75.26 } }, "startedAt": "TIMESTAMP", @@ -12962,9 +12983,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -12974,7 +12995,7 @@ "position": { "x": 342.38, "y": 288.38, - "z": 98.33 + "z": 74.99000000000001 } }, "startedAt": "TIMESTAMP", @@ -13390,9 +13411,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -13402,7 +13423,7 @@ "position": { "x": 342.38, "y": 288.38, - "z": 98.33 + "z": 74.99000000000001 } }, "startedAt": "TIMESTAMP", @@ -13806,9 +13827,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -13818,7 +13839,7 @@ "position": { "x": 342.38, "y": 288.38, - "z": 98.33 + "z": 74.99000000000001 } }, "startedAt": "TIMESTAMP", @@ -14206,9 +14227,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -14218,7 +14239,7 @@ "position": { "x": 342.38, "y": 288.38, - "z": 98.33 + "z": 74.99000000000001 } }, "startedAt": "TIMESTAMP", @@ -14622,9 +14643,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -14634,7 +14655,7 @@ "position": { "x": 342.38, "y": 288.38, - "z": 98.33 + "z": 74.99000000000001 } }, "startedAt": "TIMESTAMP", @@ -15022,9 +15043,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -15034,7 +15055,7 @@ "position": { "x": 342.38, "y": 288.38, - "z": 98.33 + "z": 74.99000000000001 } }, "startedAt": "TIMESTAMP", @@ -15276,6 +15297,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "author": "Opentrons ", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[acefe91275][OT2_X_v2_20_8_Overrides_InvalidConfigs_Override_eight_partial_column_bottom_left].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[acefe91275][OT2_X_v2_20_8_Overrides_InvalidConfigs_Override_eight_partial_column_bottom_left].json index c8389b97d75..752268901b8 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[acefe91275][OT2_X_v2_20_8_Overrides_InvalidConfigs_Override_eight_partial_column_bottom_left].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[acefe91275][OT2_X_v2_20_8_Overrides_InvalidConfigs_Override_eight_partial_column_bottom_left].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -2438,6 +2439,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "description": "oooo", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ad627dcedf][OT2_S_v6_P300M_P20S_HS_Smoke620release].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ad627dcedf][OT2_S_v6_P300M_P20S_HS_Smoke620release].json index 3a44acf987c..da74d2b109e 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ad627dcedf][OT2_S_v6_P300M_P20S_HS_Smoke620release].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ad627dcedf][OT2_S_v6_P300M_P20S_HS_Smoke620release].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -5891,9 +5892,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -5926,8 +5927,8 @@ "volume": 50.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 1.0 }, "origin": "bottom", @@ -5960,8 +5961,8 @@ "volume": 50.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -5993,9 +5994,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -6023,9 +6024,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -6058,8 +6059,8 @@ "volume": 18.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 1.0 }, "origin": "bottom", @@ -6092,8 +6093,8 @@ "volume": 18.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -6125,9 +6126,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -6155,9 +6156,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -6190,8 +6191,8 @@ "volume": 18.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 1.0 }, "origin": "bottom", @@ -6224,8 +6225,8 @@ "volume": 18.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -6257,9 +6258,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -6287,9 +6288,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -6322,8 +6323,8 @@ "volume": 18.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 1.0 }, "origin": "bottom", @@ -6356,8 +6357,8 @@ "volume": 18.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -6389,9 +6390,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -6419,9 +6420,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -6454,8 +6455,8 @@ "volume": 18.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 1.0 }, "origin": "bottom", @@ -6488,8 +6489,8 @@ "volume": 18.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -6521,9 +6522,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -6667,9 +6668,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -6702,8 +6703,8 @@ "volume": 50.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 1.0 }, "origin": "bottom", @@ -6736,8 +6737,8 @@ "volume": 50.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -6769,9 +6770,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -6799,9 +6800,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -6834,8 +6835,8 @@ "volume": 10.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 1.0 }, "origin": "bottom", @@ -6868,8 +6869,8 @@ "volume": 10.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -6901,9 +6902,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -6931,9 +6932,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -6966,8 +6967,8 @@ "volume": 10.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 1.0 }, "origin": "bottom", @@ -7000,8 +7001,8 @@ "volume": 10.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -7033,9 +7034,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -7063,9 +7064,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -7098,8 +7099,8 @@ "volume": 10.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 1.0 }, "origin": "bottom", @@ -7132,8 +7133,8 @@ "volume": 10.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -7165,9 +7166,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -7195,9 +7196,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -7230,8 +7231,8 @@ "volume": 10.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 1.0 }, "origin": "bottom", @@ -7264,8 +7265,8 @@ "volume": 10.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -7297,9 +7298,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -7327,9 +7328,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -7362,8 +7363,8 @@ "volume": 7.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -7396,8 +7397,8 @@ "volume": 7.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -7430,8 +7431,8 @@ "volume": 7.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -7464,8 +7465,8 @@ "volume": 7.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -7498,8 +7499,8 @@ "volume": 7.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -7532,8 +7533,8 @@ "volume": 7.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -7566,8 +7567,8 @@ "volume": 7.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -7600,8 +7601,8 @@ "volume": 7.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -7634,8 +7635,8 @@ "volume": 7.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -7668,8 +7669,8 @@ "volume": 7.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -7701,9 +7702,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -7731,9 +7732,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -7766,8 +7767,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 1.0 }, "origin": "bottom", @@ -7800,8 +7801,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -7834,8 +7835,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 1.0 }, "origin": "bottom", @@ -7868,8 +7869,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -7902,8 +7903,8 @@ "volume": 10.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 1.0 }, "origin": "bottom", @@ -7936,8 +7937,8 @@ "volume": 10.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -7970,8 +7971,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 1.0 }, "origin": "bottom", @@ -8004,8 +8005,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -8038,8 +8039,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 1.0 }, "origin": "bottom", @@ -8072,8 +8073,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -8106,8 +8107,8 @@ "volume": 10.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 1.0 }, "origin": "bottom", @@ -8140,8 +8141,8 @@ "volume": 10.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -8174,8 +8175,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 1.0 }, "origin": "bottom", @@ -8208,8 +8209,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -8242,8 +8243,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 1.0 }, "origin": "bottom", @@ -8276,8 +8277,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -8310,8 +8311,8 @@ "volume": 10.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 1.0 }, "origin": "bottom", @@ -8344,8 +8345,8 @@ "volume": 10.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -8378,8 +8379,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 1.0 }, "origin": "bottom", @@ -8412,8 +8413,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -8446,8 +8447,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 1.0 }, "origin": "bottom", @@ -8480,8 +8481,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -8514,8 +8515,8 @@ "volume": 10.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 1.0 }, "origin": "bottom", @@ -8548,8 +8549,8 @@ "volume": 10.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -8582,8 +8583,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 1.0 }, "origin": "bottom", @@ -8616,8 +8617,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -8650,8 +8651,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 1.0 }, "origin": "bottom", @@ -8684,8 +8685,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -8718,8 +8719,8 @@ "volume": 10.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 1.0 }, "origin": "bottom", @@ -8752,8 +8753,8 @@ "volume": 10.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -8786,8 +8787,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 1.0 }, "origin": "bottom", @@ -8820,8 +8821,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -8854,8 +8855,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 1.0 }, "origin": "bottom", @@ -8888,8 +8889,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -8922,8 +8923,8 @@ "volume": 10.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 1.0 }, "origin": "bottom", @@ -8956,8 +8957,8 @@ "volume": 10.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -8990,8 +8991,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 1.0 }, "origin": "bottom", @@ -9024,8 +9025,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -9058,8 +9059,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 1.0 }, "origin": "bottom", @@ -9092,8 +9093,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -9126,8 +9127,8 @@ "volume": 10.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 1.0 }, "origin": "bottom", @@ -9160,8 +9161,8 @@ "volume": 10.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -9194,8 +9195,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 1.0 }, "origin": "bottom", @@ -9228,8 +9229,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -9262,8 +9263,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 1.0 }, "origin": "bottom", @@ -9296,8 +9297,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -9330,8 +9331,8 @@ "volume": 10.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 1.0 }, "origin": "bottom", @@ -9364,8 +9365,8 @@ "volume": 10.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -9397,9 +9398,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -9492,6 +9493,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ad9184067d][Flex_X_v2_18_NO_PIPETTES_ReservedWord].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ad9184067d][Flex_X_v2_18_NO_PIPETTES_ReservedWord].json index d3338855040..3940f782426 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ad9184067d][Flex_X_v2_18_NO_PIPETTES_ReservedWord].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ad9184067d][Flex_X_v2_18_NO_PIPETTES_ReservedWord].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [], "config": { "apiVersion": [ @@ -42,6 +43,7 @@ } ], "labware": [], + "liquidClasses": [], "liquids": [], "metadata": { "protocolName": "Default not in range" diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[adc0621263][Flex_X_v2_16_P1000_96_TC_pipetteCollisionWithThermocyclerLid].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[adc0621263][Flex_X_v2_16_P1000_96_TC_pipetteCollisionWithThermocyclerLid].json index f86080f047c..2a342fcb3b8 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[adc0621263][Flex_X_v2_16_P1000_96_TC_pipetteCollisionWithThermocyclerLid].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[adc0621263][Flex_X_v2_16_P1000_96_TC_pipetteCollisionWithThermocyclerLid].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -6240,6 +6241,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": {}, "modules": [ diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[afd5d372a9][Flex_X_v2_20_96_and_8_Overrides_InvalidConfigs_Override_return_tip_error].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[afd5d372a9][Flex_X_v2_20_96_and_8_Overrides_InvalidConfigs_Override_return_tip_error].json index a5b5bdb65cc..6fe758b1dcf 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[afd5d372a9][Flex_X_v2_20_96_and_8_Overrides_InvalidConfigs_Override_return_tip_error].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[afd5d372a9][Flex_X_v2_20_96_and_8_Overrides_InvalidConfigs_Override_return_tip_error].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -3766,6 +3767,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "description": "oooo", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[b0ce7dde5d][Flex_X_v2_16_P1000_96_TC_PartialTipPickupTryToReturnTip].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[b0ce7dde5d][Flex_X_v2_16_P1000_96_TC_PartialTipPickupTryToReturnTip].json index 5b0df3b070c..9149f86b3eb 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[b0ce7dde5d][Flex_X_v2_16_P1000_96_TC_PartialTipPickupTryToReturnTip].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[b0ce7dde5d][Flex_X_v2_16_P1000_96_TC_PartialTipPickupTryToReturnTip].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -3706,6 +3707,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": {}, "modules": [], diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[b48407ff98][pl_cherrypicking_csv_airgap].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[b48407ff98][pl_cherrypicking_csv_airgap].json index 4433e026fd1..96dab168388 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[b48407ff98][pl_cherrypicking_csv_airgap].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[b48407ff98][pl_cherrypicking_csv_airgap].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -28815,6 +28816,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "Samples", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[b777168ac1][Flex_S_v2_19_Illumina_Stranded_total_RNA_Ribo_Zero].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[b777168ac1][Flex_S_v2_19_Illumina_Stranded_total_RNA_Ribo_Zero].json index 7005e6011ab..7fa95382fb8 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[b777168ac1][Flex_S_v2_19_Illumina_Stranded_total_RNA_Ribo_Zero].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[b777168ac1][Flex_S_v2_19_Illumina_Stranded_total_RNA_Ribo_Zero].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -3282,6 +3283,11 @@ }, "schemaVersion": 2, "stackingOffsetWithLabware": { + "evotips_flex_96_tiprack_adapter": { + "x": 0, + "y": 0, + "z": 102 + }, "opentrons_96_deep_well_adapter": { "x": 0, "y": 0, @@ -4492,6 +4498,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "author": "Dandra Howell ", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[b79134ab8a][OT2_X_v2_20_8_Overrides_InvalidConfigs_Override_drop_tip_with_location].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[b79134ab8a][OT2_X_v2_20_8_Overrides_InvalidConfigs_Override_drop_tip_with_location].json index b3637624ed4..d55cf157f22 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[b79134ab8a][OT2_X_v2_20_8_Overrides_InvalidConfigs_Override_drop_tip_with_location].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[b79134ab8a][OT2_X_v2_20_8_Overrides_InvalidConfigs_Override_drop_tip_with_location].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -4920,6 +4921,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "description": "oooo", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[b806f07be9][Flex_X_v2_18_NO_PIPETTES_Overrides_BadTypesInRTP_Override_wrong_type_in_choice_value].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[b806f07be9][Flex_X_v2_18_NO_PIPETTES_Overrides_BadTypesInRTP_Override_wrong_type_in_choice_value].json index 6b2391f6118..cb3c1622c42 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[b806f07be9][Flex_X_v2_18_NO_PIPETTES_Overrides_BadTypesInRTP_Override_wrong_type_in_choice_value].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[b806f07be9][Flex_X_v2_18_NO_PIPETTES_Overrides_BadTypesInRTP_Override_wrong_type_in_choice_value].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [], "config": { "apiVersion": [ @@ -42,6 +43,7 @@ } ], "labware": [], + "liquidClasses": [], "liquids": [], "metadata": { "protocolName": "Description Too Long 2.18" diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[b91d31eaa2][pl_MagMax_RNA_Cells_Flex_96_Channel].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[b91d31eaa2][pl_MagMax_RNA_Cells_Flex_96_Channel].json index 4bcec7cf7de..fed709fc6e2 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[b91d31eaa2][pl_MagMax_RNA_Cells_Flex_96_Channel].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[b91d31eaa2][pl_MagMax_RNA_Cells_Flex_96_Channel].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -828,6 +829,11 @@ }, "schemaVersion": 2, "stackingOffsetWithLabware": { + "evotips_flex_96_tiprack_adapter": { + "x": 0, + "y": 0, + "z": 102 + }, "opentrons_96_deep_well_adapter": { "x": 0, "y": 0, @@ -6713,6 +6719,11 @@ }, "schemaVersion": 2, "stackingOffsetWithLabware": { + "evotips_flex_96_tiprack_adapter": { + "x": 0, + "y": 0, + "z": 102 + }, "opentrons_96_deep_well_adapter": { "x": 0, "y": 0, @@ -8023,6 +8034,11 @@ }, "schemaVersion": 2, "stackingOffsetWithLabware": { + "evotips_flex_96_tiprack_adapter": { + "x": 0, + "y": 0, + "z": 102 + }, "opentrons_96_deep_well_adapter": { "x": 0, "y": 0, @@ -11030,6 +11046,11 @@ }, "schemaVersion": 2, "stackingOffsetWithLabware": { + "evotips_flex_96_tiprack_adapter": { + "x": 0, + "y": 0, + "z": 102 + }, "opentrons_96_deep_well_adapter": { "x": 0, "y": 0, @@ -14037,6 +14058,11 @@ }, "schemaVersion": 2, "stackingOffsetWithLabware": { + "evotips_flex_96_tiprack_adapter": { + "x": 0, + "y": 0, + "z": 102 + }, "opentrons_96_deep_well_adapter": { "x": 0, "y": 0, @@ -17044,6 +17070,11 @@ }, "schemaVersion": 2, "stackingOffsetWithLabware": { + "evotips_flex_96_tiprack_adapter": { + "x": 0, + "y": 0, + "z": 102 + }, "opentrons_96_deep_well_adapter": { "x": 0, "y": 0, @@ -33276,9 +33307,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -33288,7 +33319,7 @@ "position": { "x": 14.38, "y": 395.38, - "z": 98.33 + "z": 74.99000000000001 } }, "startedAt": "TIMESTAMP", @@ -33640,9 +33671,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -33652,7 +33683,7 @@ "position": { "x": 14.38, "y": 395.38, - "z": 98.33 + "z": 74.99000000000001 } }, "startedAt": "TIMESTAMP", @@ -33880,9 +33911,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -33892,7 +33923,7 @@ "position": { "x": 14.38, "y": 395.38, - "z": 98.33 + "z": 74.99000000000001 } }, "startedAt": "TIMESTAMP", @@ -34166,9 +34197,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -34178,7 +34209,7 @@ "position": { "x": 14.38, "y": 395.38, - "z": 98.33 + "z": 74.99000000000001 } }, "startedAt": "TIMESTAMP", @@ -34406,9 +34437,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -34418,7 +34449,7 @@ "position": { "x": 14.38, "y": 395.38, - "z": 98.33 + "z": 74.99000000000001 } }, "startedAt": "TIMESTAMP", @@ -34692,9 +34723,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -34704,7 +34735,7 @@ "position": { "x": 14.38, "y": 395.38, - "z": 98.33 + "z": 74.99000000000001 } }, "startedAt": "TIMESTAMP", @@ -37130,9 +37161,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -37142,7 +37173,7 @@ "position": { "x": 14.38, "y": 395.38, - "z": 98.33 + "z": 74.99000000000001 } }, "startedAt": "TIMESTAMP", @@ -37792,9 +37823,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -37804,7 +37835,7 @@ "position": { "x": 14.38, "y": 395.38, - "z": 98.33 + "z": 74.99000000000001 } }, "startedAt": "TIMESTAMP", @@ -38138,9 +38169,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -38150,7 +38181,7 @@ "position": { "x": 14.38, "y": 395.38, - "z": 98.33 + "z": 74.99000000000001 } }, "startedAt": "TIMESTAMP", @@ -38378,9 +38409,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -38390,7 +38421,7 @@ "position": { "x": 14.38, "y": 395.38, - "z": 98.33 + "z": 74.99000000000001 } }, "startedAt": "TIMESTAMP", @@ -38664,9 +38695,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -38676,7 +38707,7 @@ "position": { "x": 14.38, "y": 395.38, - "z": 98.33 + "z": 74.99000000000001 } }, "startedAt": "TIMESTAMP", @@ -38904,9 +38935,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -38916,7 +38947,7 @@ "position": { "x": 14.38, "y": 395.38, - "z": 98.33 + "z": 74.99000000000001 } }, "startedAt": "TIMESTAMP", @@ -39190,9 +39221,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -39202,7 +39233,7 @@ "position": { "x": 14.38, "y": 395.38, - "z": 98.33 + "z": 74.99000000000001 } }, "startedAt": "TIMESTAMP", @@ -39430,9 +39461,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -39442,7 +39473,7 @@ "position": { "x": 14.38, "y": 395.38, - "z": 98.33 + "z": 74.99000000000001 } }, "startedAt": "TIMESTAMP", @@ -39716,9 +39747,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -39728,7 +39759,7 @@ "position": { "x": 14.38, "y": 395.38, - "z": 98.33 + "z": 74.99000000000001 } }, "startedAt": "TIMESTAMP", @@ -40718,9 +40749,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -40730,7 +40761,7 @@ "position": { "x": 178.38, "y": 395.38, - "z": 98.33 + "z": 74.99000000000001 } }, "startedAt": "TIMESTAMP", @@ -41064,9 +41095,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -41076,7 +41107,7 @@ "position": { "x": 178.38, "y": 395.38, - "z": 98.33 + "z": 74.99000000000001 } }, "startedAt": "TIMESTAMP", @@ -41229,6 +41260,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "Magnetic Beads", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[baf79d9b4a][Flex_S_v2_15_P1000S_None_SimpleNormalizeLongRight].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[baf79d9b4a][Flex_S_v2_15_P1000S_None_SimpleNormalizeLongRight].json index 5460d2d1fd7..6ca11baef34 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[baf79d9b4a][Flex_S_v2_15_P1000S_None_SimpleNormalizeLongRight].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[baf79d9b4a][Flex_S_v2_15_P1000S_None_SimpleNormalizeLongRight].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -17323,9 +17324,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -17497,9 +17498,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -17629,9 +17630,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -17761,9 +17762,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -17893,9 +17894,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -18025,9 +18026,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -18157,9 +18158,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -18289,9 +18290,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -18421,9 +18422,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -18553,9 +18554,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -18685,9 +18686,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -18817,9 +18818,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -18949,9 +18950,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -19081,9 +19082,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -19213,9 +19214,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -19345,9 +19346,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -19477,9 +19478,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -19609,9 +19610,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -19741,9 +19742,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -19873,9 +19874,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -20005,9 +20006,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -20137,9 +20138,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -20269,9 +20270,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -20401,9 +20402,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -20533,9 +20534,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -20665,9 +20666,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -20797,9 +20798,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -20929,9 +20930,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -21061,9 +21062,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -21193,9 +21194,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -21325,9 +21326,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -21457,9 +21458,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -21589,9 +21590,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -21721,9 +21722,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -21853,9 +21854,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -21985,9 +21986,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -22117,9 +22118,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -22249,9 +22250,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -22381,9 +22382,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -22513,9 +22514,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -22645,9 +22646,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -22777,9 +22778,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -22909,9 +22910,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -23041,9 +23042,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -23173,9 +23174,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -23305,9 +23306,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -23437,9 +23438,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -23569,9 +23570,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -23701,9 +23702,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -23833,9 +23834,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -23965,9 +23966,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -24097,9 +24098,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -24229,9 +24230,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -24361,9 +24362,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -24493,9 +24494,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -24625,9 +24626,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -24757,9 +24758,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -24889,9 +24890,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -25021,9 +25022,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -25153,9 +25154,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -25285,9 +25286,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -25417,9 +25418,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -25549,9 +25550,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -25681,9 +25682,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -25813,9 +25814,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -25945,9 +25946,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -26077,9 +26078,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -26209,9 +26210,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -26341,9 +26342,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -26473,9 +26474,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -26605,9 +26606,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -26737,9 +26738,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -26869,9 +26870,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -27001,9 +27002,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -27133,9 +27134,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -27265,9 +27266,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -27397,9 +27398,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -27529,9 +27530,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -27661,9 +27662,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -27793,9 +27794,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -27925,9 +27926,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -28057,9 +28058,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -28189,9 +28190,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -28321,9 +28322,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -28453,9 +28454,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -28585,9 +28586,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -28717,9 +28718,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -28849,9 +28850,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -28981,9 +28982,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -29113,9 +29114,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -29245,9 +29246,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -29377,9 +29378,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -29509,9 +29510,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -29641,9 +29642,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -29773,9 +29774,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -29905,9 +29906,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -36471,9 +36472,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -36645,9 +36646,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -36777,9 +36778,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -36909,9 +36910,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -37041,9 +37042,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -37173,9 +37174,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -37305,9 +37306,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -37437,9 +37438,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -37569,9 +37570,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -37701,9 +37702,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -37833,9 +37834,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -37965,9 +37966,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -38097,9 +38098,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -38229,9 +38230,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -38361,9 +38362,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -38493,9 +38494,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -38625,9 +38626,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -38757,9 +38758,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -38889,9 +38890,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -39021,9 +39022,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -39153,9 +39154,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -39285,9 +39286,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -39417,9 +39418,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -39549,9 +39550,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -39681,9 +39682,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -39813,9 +39814,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -39945,9 +39946,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -40077,9 +40078,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -40209,9 +40210,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -40341,9 +40342,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -40473,9 +40474,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -40605,9 +40606,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -40737,9 +40738,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -40869,9 +40870,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -41001,9 +41002,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -41133,9 +41134,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -41265,9 +41266,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -41397,9 +41398,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -41529,9 +41530,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -41661,9 +41662,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -41793,9 +41794,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -41925,9 +41926,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -42057,9 +42058,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -42189,9 +42190,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -42321,9 +42322,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -42453,9 +42454,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -42585,9 +42586,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -42717,9 +42718,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -42849,9 +42850,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -42981,9 +42982,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -43113,9 +43114,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -43245,9 +43246,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -43377,9 +43378,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -43509,9 +43510,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -43641,9 +43642,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -43773,9 +43774,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -43905,9 +43906,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -44037,9 +44038,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -44169,9 +44170,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -44301,9 +44302,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -44433,9 +44434,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -44565,9 +44566,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -44697,9 +44698,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -44829,9 +44830,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -44961,9 +44962,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -45093,9 +45094,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -45225,9 +45226,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -45357,9 +45358,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -45489,9 +45490,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -45621,9 +45622,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -45753,9 +45754,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -45885,9 +45886,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -46017,9 +46018,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -46149,9 +46150,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -46281,9 +46282,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -46413,9 +46414,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -46545,9 +46546,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -46677,9 +46678,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -46809,9 +46810,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -46941,9 +46942,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -47073,9 +47074,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -47205,9 +47206,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -47337,9 +47338,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -47469,9 +47470,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -47601,9 +47602,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -47733,9 +47734,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -47865,9 +47866,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -47997,9 +47998,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -48129,9 +48130,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -48261,9 +48262,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -48393,9 +48394,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -48525,9 +48526,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -48657,9 +48658,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -48789,9 +48790,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -48921,9 +48922,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -49053,9 +49054,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -55619,9 +55620,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -55793,9 +55794,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -55925,9 +55926,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -56057,9 +56058,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -56189,9 +56190,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -56321,9 +56322,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -56453,9 +56454,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -56585,9 +56586,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -56717,9 +56718,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -56849,9 +56850,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -56981,9 +56982,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -57113,9 +57114,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -57245,9 +57246,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -57377,9 +57378,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -57509,9 +57510,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -57641,9 +57642,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -57773,9 +57774,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -57905,9 +57906,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -58037,9 +58038,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -58169,9 +58170,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -58301,9 +58302,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -58433,9 +58434,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -58565,9 +58566,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -58697,9 +58698,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -58829,9 +58830,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -58961,9 +58962,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -59093,9 +59094,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -59225,9 +59226,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -59357,9 +59358,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -59489,9 +59490,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -59621,9 +59622,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -59753,9 +59754,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -59885,9 +59886,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -60017,9 +60018,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -60149,9 +60150,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -60281,9 +60282,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -60413,9 +60414,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -60545,9 +60546,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -60677,9 +60678,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -60809,9 +60810,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -60941,9 +60942,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -61073,9 +61074,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -61205,9 +61206,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -61337,9 +61338,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -61469,9 +61470,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -61601,9 +61602,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -61733,9 +61734,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -61865,9 +61866,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -61997,9 +61998,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -62129,9 +62130,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -62261,9 +62262,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -62393,9 +62394,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -62525,9 +62526,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -62657,9 +62658,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -62789,9 +62790,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -62921,9 +62922,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -63053,9 +63054,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -63185,9 +63186,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -63317,9 +63318,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -63449,9 +63450,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -63581,9 +63582,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -63713,9 +63714,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -63845,9 +63846,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -63977,9 +63978,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -64109,9 +64110,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -64241,9 +64242,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -64373,9 +64374,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -64505,9 +64506,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -64637,9 +64638,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -64769,9 +64770,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -64901,9 +64902,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -65033,9 +65034,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -65165,9 +65166,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -65297,9 +65298,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -65429,9 +65430,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -65561,9 +65562,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -65693,9 +65694,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -65825,9 +65826,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -65957,9 +65958,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -66089,9 +66090,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -66221,9 +66222,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -66353,9 +66354,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -66485,9 +66486,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -66617,9 +66618,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -66749,9 +66750,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -66881,9 +66882,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -67013,9 +67014,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -67145,9 +67146,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -67277,9 +67278,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -67409,9 +67410,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -67541,9 +67542,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -67673,9 +67674,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -67805,9 +67806,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -67937,9 +67938,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -68069,9 +68070,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -68201,9 +68202,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -74767,9 +74768,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -74941,9 +74942,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -75073,9 +75074,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -75205,9 +75206,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -75337,9 +75338,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -75469,9 +75470,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -75601,9 +75602,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -75733,9 +75734,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -75865,9 +75866,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -75997,9 +75998,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -76129,9 +76130,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -76261,9 +76262,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -76393,9 +76394,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -76525,9 +76526,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -76657,9 +76658,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -76789,9 +76790,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -76921,9 +76922,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -77053,9 +77054,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -77185,9 +77186,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -77317,9 +77318,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -77449,9 +77450,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -77581,9 +77582,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -77713,9 +77714,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -77845,9 +77846,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -77977,9 +77978,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -78109,9 +78110,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -78241,9 +78242,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -78373,9 +78374,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -78505,9 +78506,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -78637,9 +78638,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -78769,9 +78770,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -78901,9 +78902,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -79033,9 +79034,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -79165,9 +79166,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -79297,9 +79298,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -79429,9 +79430,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -79561,9 +79562,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -79693,9 +79694,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -79825,9 +79826,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -79957,9 +79958,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -80089,9 +80090,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -80221,9 +80222,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -80353,9 +80354,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -80485,9 +80486,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -80617,9 +80618,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -80749,9 +80750,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -80881,9 +80882,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -81013,9 +81014,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -81145,9 +81146,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -81277,9 +81278,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -81409,9 +81410,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -81541,9 +81542,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -81673,9 +81674,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -81805,9 +81806,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -81937,9 +81938,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -82069,9 +82070,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -82201,9 +82202,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -82333,9 +82334,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -82465,9 +82466,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -82597,9 +82598,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -82729,9 +82730,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -82861,9 +82862,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -82993,9 +82994,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -83125,9 +83126,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -83257,9 +83258,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -83389,9 +83390,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -83521,9 +83522,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -83653,9 +83654,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -83785,9 +83786,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -83917,9 +83918,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -84049,9 +84050,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -84181,9 +84182,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -84313,9 +84314,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -84445,9 +84446,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -84577,9 +84578,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -84709,9 +84710,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -84841,9 +84842,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -84973,9 +84974,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -85105,9 +85106,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -85237,9 +85238,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -85369,9 +85370,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -85501,9 +85502,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -85633,9 +85634,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -85765,9 +85766,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -85897,9 +85898,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -86029,9 +86030,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -86161,9 +86162,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -86293,9 +86294,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -86425,9 +86426,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -86557,9 +86558,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -86689,9 +86690,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -86821,9 +86822,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -86953,9 +86954,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -87085,9 +87086,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -87217,9 +87218,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -87349,9 +87350,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -93915,9 +93916,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -94089,9 +94090,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -94221,9 +94222,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -94353,9 +94354,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -94485,9 +94486,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -94617,9 +94618,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -94749,9 +94750,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -94881,9 +94882,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -95013,9 +95014,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -95145,9 +95146,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -95277,9 +95278,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -95409,9 +95410,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -95541,9 +95542,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -95673,9 +95674,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -95805,9 +95806,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -95937,9 +95938,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -96069,9 +96070,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -96201,9 +96202,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -96333,9 +96334,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -96465,9 +96466,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -96597,9 +96598,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -96729,9 +96730,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -96861,9 +96862,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -96993,9 +96994,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -97125,9 +97126,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -97257,9 +97258,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -97389,9 +97390,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -97521,9 +97522,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -97653,9 +97654,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -97785,9 +97786,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -97917,9 +97918,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -98049,9 +98050,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -98181,9 +98182,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -98313,9 +98314,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -98445,9 +98446,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -98577,9 +98578,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -98709,9 +98710,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -98841,9 +98842,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -98973,9 +98974,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -99105,9 +99106,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -99237,9 +99238,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -99369,9 +99370,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -99501,9 +99502,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -99633,9 +99634,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -99765,9 +99766,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -99897,9 +99898,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -100029,9 +100030,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -100161,9 +100162,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -100293,9 +100294,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -100425,9 +100426,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -100557,9 +100558,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -100689,9 +100690,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -100821,9 +100822,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -100953,9 +100954,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -101085,9 +101086,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -101217,9 +101218,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -101349,9 +101350,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -101481,9 +101482,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -101613,9 +101614,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -101745,9 +101746,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -101877,9 +101878,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -102009,9 +102010,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -102141,9 +102142,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -102273,9 +102274,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -102405,9 +102406,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -102537,9 +102538,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -102669,9 +102670,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -102801,9 +102802,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -102933,9 +102934,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -103065,9 +103066,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -103197,9 +103198,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -103329,9 +103330,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -103461,9 +103462,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -103593,9 +103594,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -103725,9 +103726,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -103857,9 +103858,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -103989,9 +103990,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -104121,9 +104122,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -104253,9 +104254,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -104385,9 +104386,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -104517,9 +104518,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -104649,9 +104650,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -104781,9 +104782,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -104913,9 +104914,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -105045,9 +105046,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -105177,9 +105178,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -105309,9 +105310,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -105441,9 +105442,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -105573,9 +105574,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -105705,9 +105706,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -105837,9 +105838,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -105969,9 +105970,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -106101,9 +106102,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -106233,9 +106234,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -106365,9 +106366,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -106497,9 +106498,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -113063,9 +113064,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -113237,9 +113238,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -113369,9 +113370,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -113501,9 +113502,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -113633,9 +113634,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -113765,9 +113766,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -113897,9 +113898,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -114029,9 +114030,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -114161,9 +114162,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -114293,9 +114294,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -114425,9 +114426,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -114557,9 +114558,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -114689,9 +114690,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -114821,9 +114822,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -114953,9 +114954,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -115085,9 +115086,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -115217,9 +115218,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -115349,9 +115350,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -115481,9 +115482,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -115613,9 +115614,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -115745,9 +115746,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -115877,9 +115878,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -116009,9 +116010,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -116141,9 +116142,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -116273,9 +116274,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -116405,9 +116406,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -116537,9 +116538,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -116669,9 +116670,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -116801,9 +116802,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -116933,9 +116934,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -117065,9 +117066,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -117197,9 +117198,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -117329,9 +117330,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -117461,9 +117462,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -117593,9 +117594,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -117725,9 +117726,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -117857,9 +117858,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -117989,9 +117990,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -118121,9 +118122,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -118253,9 +118254,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -118385,9 +118386,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -118517,9 +118518,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -118649,9 +118650,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -118781,9 +118782,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -118913,9 +118914,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -119045,9 +119046,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -119177,9 +119178,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -119309,9 +119310,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -119441,9 +119442,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -119573,9 +119574,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -119705,9 +119706,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -119837,9 +119838,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -119969,9 +119970,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -120101,9 +120102,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -120233,9 +120234,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -120365,9 +120366,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -120497,9 +120498,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -120629,9 +120630,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -120761,9 +120762,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -120893,9 +120894,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -121025,9 +121026,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -121157,9 +121158,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -121289,9 +121290,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -121421,9 +121422,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -121553,9 +121554,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -121685,9 +121686,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -121817,9 +121818,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -121949,9 +121950,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -122081,9 +122082,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -122213,9 +122214,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -122345,9 +122346,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -122477,9 +122478,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -122609,9 +122610,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -122741,9 +122742,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -122873,9 +122874,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -123005,9 +123006,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -123137,9 +123138,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -123269,9 +123270,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -123401,9 +123402,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -123533,9 +123534,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -123665,9 +123666,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -123797,9 +123798,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -123929,9 +123930,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -124061,9 +124062,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -124193,9 +124194,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -124325,9 +124326,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -124457,9 +124458,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -124589,9 +124590,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -124721,9 +124722,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -124853,9 +124854,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -124985,9 +124986,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -125117,9 +125118,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -125249,9 +125250,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -125381,9 +125382,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -125513,9 +125514,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -125645,9 +125646,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -125769,6 +125770,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "author": "Opentrons Engineering ", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[bc049301c1][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_transfer_destination_collision].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[bc049301c1][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_transfer_destination_collision].json index 290674f3bd6..588a958e032 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[bc049301c1][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_transfer_destination_collision].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[bc049301c1][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_transfer_destination_collision].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -3912,6 +3913,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "description": "oooo", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c0435f08da][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_north].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c0435f08da][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_north].json index 67a07aa1297..6caf8361175 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c0435f08da][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_north].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c0435f08da][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_north].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -1481,6 +1482,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "description": "oooo", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c064d0de2c][OT2_S_v6_P1000S_None_SimpleTransfer].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c064d0de2c][OT2_S_v6_P1000S_None_SimpleTransfer].json index 14cc53aba17..3bca981777a 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c064d0de2c][OT2_S_v6_P1000S_None_SimpleTransfer].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c064d0de2c][OT2_S_v6_P1000S_None_SimpleTransfer].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -1730,9 +1731,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -1765,8 +1766,8 @@ "volume": 400.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 1.0 }, "origin": "bottom", @@ -1799,8 +1800,8 @@ "volume": 400.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -1833,8 +1834,8 @@ "volume": 400.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 1.0 }, "origin": "bottom", @@ -1867,8 +1868,8 @@ "volume": 400.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -1900,9 +1901,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -1977,6 +1978,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c195291f84][OT2_S_v2_17_NO_PIPETTES_TC_VerifyThermocyclerLoadedSlots].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c195291f84][OT2_S_v2_17_NO_PIPETTES_TC_VerifyThermocyclerLoadedSlots].json index d6eb8a28124..2409fd654ad 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c195291f84][OT2_S_v2_17_NO_PIPETTES_TC_VerifyThermocyclerLoadedSlots].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c195291f84][OT2_S_v2_17_NO_PIPETTES_TC_VerifyThermocyclerLoadedSlots].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -145,6 +146,7 @@ } ], "labware": [], + "liquidClasses": [], "liquids": [], "metadata": {}, "modules": [ diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c1c04baffd][Flex_S_v2_17_NO_PIPETTES_TC_verifyThermocyclerLoadedSlots].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c1c04baffd][Flex_S_v2_17_NO_PIPETTES_TC_verifyThermocyclerLoadedSlots].json index f59969368ab..f90f20fca53 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c1c04baffd][Flex_S_v2_17_NO_PIPETTES_TC_verifyThermocyclerLoadedSlots].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c1c04baffd][Flex_S_v2_17_NO_PIPETTES_TC_verifyThermocyclerLoadedSlots].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -171,6 +172,7 @@ } ], "labware": [], + "liquidClasses": [], "liquids": [], "metadata": {}, "modules": [ diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c3098303ad][OT2_S_v2_15_NO_PIPETTES_TC_VerifyThermocyclerLoadedSlots].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c3098303ad][OT2_S_v2_15_NO_PIPETTES_TC_VerifyThermocyclerLoadedSlots].json index 4c6c38162b3..71660ebb915 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c3098303ad][OT2_S_v2_15_NO_PIPETTES_TC_VerifyThermocyclerLoadedSlots].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c3098303ad][OT2_S_v2_15_NO_PIPETTES_TC_VerifyThermocyclerLoadedSlots].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -154,6 +155,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": {}, "modules": [ diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c343dfb5a0][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_bottom_right_edge].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c343dfb5a0][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_bottom_right_edge].json index 4665f21b62e..eeef20742b5 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c343dfb5a0][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_bottom_right_edge].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c343dfb5a0][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_bottom_right_edge].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -1249,6 +1250,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "description": "oooo", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c745e5824a][Flex_S_v2_16_P1000_96_GRIP_DeckConfiguration1NoModules].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c745e5824a][Flex_S_v2_16_P1000_96_GRIP_DeckConfiguration1NoModules].json index aadd38b4eaa..ee6ea8de744 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c745e5824a][Flex_S_v2_16_P1000_96_GRIP_DeckConfiguration1NoModules].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c745e5824a][Flex_S_v2_16_P1000_96_GRIP_DeckConfiguration1NoModules].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -12315,6 +12316,7 @@ "location": "offDeck" } ], + "liquidClasses": [], "liquids": [ { "description": "High Quality H₂O", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c821e64fad][OT2_S_v2_13_P300M_P20S_MM_TC_TM_Smoke620Release].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c821e64fad][OT2_S_v2_13_P300M_P20S_MM_TC_TM_Smoke620Release].json index 27e9d4f2c51..3d8d850d34e 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c821e64fad][OT2_S_v2_13_P300M_P20S_MM_TC_TM_Smoke620Release].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c821e64fad][OT2_S_v2_13_P300M_P20S_MM_TC_TM_Smoke620Release].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -7752,9 +7753,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -7766,9 +7767,9 @@ "y": 0.0, "z": 0.0 }, - "tipDiameter": 0, + "tipDiameter": 0.0, "tipLength": 30.950000000000003, - "tipVolume": 20 + "tipVolume": 20.0 }, "startedAt": "TIMESTAMP", "status": "succeeded" @@ -7787,9 +7788,9 @@ "volume": 19.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -7821,9 +7822,9 @@ "volume": 18.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -7854,9 +7855,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -7887,9 +7888,9 @@ "volume": 19.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -7921,9 +7922,9 @@ "volume": 18.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -7954,9 +7955,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -7987,9 +7988,9 @@ "volume": 19.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -8021,9 +8022,9 @@ "volume": 18.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -8054,9 +8055,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -8087,9 +8088,9 @@ "volume": 19.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -8121,9 +8122,9 @@ "volume": 18.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -8154,9 +8155,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -8187,9 +8188,9 @@ "volume": 19.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -8221,9 +8222,9 @@ "volume": 18.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -8254,9 +8255,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -8287,9 +8288,9 @@ "volume": 19.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -8321,9 +8322,9 @@ "volume": 18.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -8354,9 +8355,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -8387,9 +8388,9 @@ "volume": 19.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -8421,9 +8422,9 @@ "volume": 18.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -8454,9 +8455,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -8487,9 +8488,9 @@ "volume": 19.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -8521,9 +8522,9 @@ "volume": 18.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -8554,9 +8555,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -8587,9 +8588,9 @@ "volume": 19.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -8621,9 +8622,9 @@ "volume": 18.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -8654,9 +8655,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -8687,9 +8688,9 @@ "volume": 19.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -8721,9 +8722,9 @@ "volume": 18.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -8754,9 +8755,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -8787,9 +8788,9 @@ "volume": 19.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -8821,9 +8822,9 @@ "volume": 18.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -8854,9 +8855,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -8887,9 +8888,9 @@ "volume": 19.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -8921,9 +8922,9 @@ "volume": 18.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -8954,9 +8955,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -8987,9 +8988,9 @@ "volume": 19.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -9021,9 +9022,9 @@ "volume": 18.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -9054,9 +9055,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -9087,9 +9088,9 @@ "volume": 19.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -9121,9 +9122,9 @@ "volume": 18.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -9154,9 +9155,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -9187,9 +9188,9 @@ "volume": 19.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -9221,9 +9222,9 @@ "volume": 18.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -9254,9 +9255,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -9287,9 +9288,9 @@ "volume": 19.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -9321,9 +9322,9 @@ "volume": 18.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -9354,9 +9355,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -9387,9 +9388,9 @@ "volume": 19.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -9421,9 +9422,9 @@ "volume": 18.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -9454,9 +9455,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -9486,9 +9487,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -10161,9 +10162,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -10175,9 +10176,9 @@ "y": 0.0, "z": 0.0 }, - "tipDiameter": 0, + "tipDiameter": 0.0, "tipLength": 30.950000000000003, - "tipVolume": 20 + "tipVolume": 20.0 }, "startedAt": "TIMESTAMP", "status": "succeeded" @@ -10196,9 +10197,9 @@ "volume": 18.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -10230,9 +10231,9 @@ "volume": 18.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -10263,9 +10264,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -10293,9 +10294,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -10307,9 +10308,9 @@ "y": 0.0, "z": 0.0 }, - "tipDiameter": 0, + "tipDiameter": 0.0, "tipLength": 30.950000000000003, - "tipVolume": 20 + "tipVolume": 20.0 }, "startedAt": "TIMESTAMP", "status": "succeeded" @@ -10328,9 +10329,9 @@ "volume": 15.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -10362,9 +10363,9 @@ "volume": 15.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -10395,9 +10396,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -10425,9 +10426,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -10439,9 +10440,9 @@ "y": 0.0, "z": 0.0 }, - "tipDiameter": 0, + "tipDiameter": 0.0, "tipLength": 30.950000000000003, - "tipVolume": 20 + "tipVolume": 20.0 }, "startedAt": "TIMESTAMP", "status": "succeeded" @@ -10460,9 +10461,9 @@ "volume": 10.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -10494,9 +10495,9 @@ "volume": 10.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -10527,9 +10528,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -10557,9 +10558,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -10571,9 +10572,9 @@ "y": 0.0, "z": 0.0 }, - "tipDiameter": 0, + "tipDiameter": 0.0, "tipLength": 51.099999999999994, - "tipVolume": 300 + "tipVolume": 300.0 }, "startedAt": "TIMESTAMP", "status": "succeeded" @@ -10592,9 +10593,9 @@ "volume": 75.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -10626,9 +10627,9 @@ "volume": 60.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -10659,9 +10660,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -10777,6 +10778,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "apiLevel": "2.13", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c8d2ca0089][Flex_S_v2_18_QIASeq_FX_48x].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c8d2ca0089][Flex_S_v2_18_QIASeq_FX_48x].json index 131c7514649..c7cc6586b5d 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c8d2ca0089][Flex_S_v2_18_QIASeq_FX_48x].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c8d2ca0089][Flex_S_v2_18_QIASeq_FX_48x].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -5143,6 +5144,11 @@ }, "schemaVersion": 2, "stackingOffsetWithLabware": { + "evotips_flex_96_tiprack_adapter": { + "x": 0, + "y": 0, + "z": 102 + }, "opentrons_96_deep_well_adapter": { "x": 0, "y": 0, @@ -20182,9 +20188,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -20582,9 +20588,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -20982,9 +20988,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -23989,9 +23995,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -24486,9 +24492,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -24983,9 +24989,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -26803,9 +26809,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -27300,9 +27306,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -27797,9 +27803,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -27955,9 +27961,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -28085,9 +28091,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -28215,9 +28221,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -28497,9 +28503,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -28693,9 +28699,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -28889,9 +28895,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -29264,9 +29270,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -29508,9 +29514,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -29752,9 +29758,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -35049,9 +35055,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -35546,9 +35552,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -36043,9 +36049,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -37849,9 +37855,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -38332,9 +38338,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -38815,9 +38821,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -41808,9 +41814,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -42291,9 +42297,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -42774,9 +42780,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -42932,9 +42938,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -43062,9 +43068,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -43192,9 +43198,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -44731,9 +44737,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -44961,9 +44967,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -45191,9 +45197,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -56009,9 +56015,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -56506,9 +56512,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -57003,9 +57009,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -58823,9 +58829,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -59320,9 +59326,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -59817,9 +59823,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -62860,9 +62866,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -63357,9 +63363,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -63854,9 +63860,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -64012,9 +64018,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -64142,9 +64148,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -64272,9 +64278,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -64588,9 +64594,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -64818,9 +64824,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -65048,9 +65054,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -65423,9 +65429,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -65667,9 +65673,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -65911,9 +65917,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -66156,6 +66162,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "author": "Opentrons ", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c9e6e3d59d][OT2_X_v4_P300M_P20S_MM_TC1_TM_e2eTests].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c9e6e3d59d][OT2_X_v4_P300M_P20S_MM_TC1_TM_e2eTests].json index 5efbff81ebc..e361a770403 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c9e6e3d59d][OT2_X_v4_P300M_P20S_MM_TC1_TM_e2eTests].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c9e6e3d59d][OT2_X_v4_P300M_P20S_MM_TC1_TM_e2eTests].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -6991,9 +6992,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -7005,9 +7006,9 @@ "y": 0.0, "z": 0.0 }, - "tipDiameter": 0, + "tipDiameter": 0.0, "tipLength": 51.099999999999994, - "tipVolume": 300 + "tipVolume": 300.0 }, "startedAt": "TIMESTAMP", "status": "succeeded" @@ -7051,9 +7052,9 @@ "volume": 1000.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -7184,6 +7185,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "author": "NN MM", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[cb5adc3d23][OT2_S_v6_P20S_P300M_TransferReTransferLiquid].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[cb5adc3d23][OT2_S_v6_P20S_P300M_TransferReTransferLiquid].json index 1759b7b244f..a2fa133144c 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[cb5adc3d23][OT2_S_v6_P20S_P300M_TransferReTransferLiquid].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[cb5adc3d23][OT2_S_v6_P20S_P300M_TransferReTransferLiquid].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -4160,9 +4161,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -4195,8 +4196,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 1.0 }, "origin": "bottom", @@ -4229,8 +4230,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -4262,9 +4263,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -4292,9 +4293,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -4327,8 +4328,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 1.0 }, "origin": "bottom", @@ -4361,8 +4362,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -4394,9 +4395,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -4424,9 +4425,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -4459,8 +4460,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 1.0 }, "origin": "bottom", @@ -4493,8 +4494,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -4526,9 +4527,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -4556,9 +4557,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -4591,8 +4592,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 1.0 }, "origin": "bottom", @@ -4625,8 +4626,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -4658,9 +4659,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -4688,9 +4689,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -4723,8 +4724,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 1.0 }, "origin": "bottom", @@ -4757,8 +4758,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -4790,9 +4791,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -4820,9 +4821,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -4855,8 +4856,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 1.0 }, "origin": "bottom", @@ -4889,8 +4890,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -4922,9 +4923,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -4952,9 +4953,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -4987,8 +4988,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 1.0 }, "origin": "bottom", @@ -5021,8 +5022,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -5054,9 +5055,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -5084,9 +5085,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -5119,8 +5120,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 1.0 }, "origin": "bottom", @@ -5153,8 +5154,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -5186,9 +5187,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -5216,9 +5217,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -5251,8 +5252,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 1.0 }, "origin": "bottom", @@ -5285,8 +5286,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -5318,9 +5319,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -5348,9 +5349,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -5383,8 +5384,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 1.0 }, "origin": "bottom", @@ -5417,8 +5418,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -5450,9 +5451,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -5480,9 +5481,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -5515,8 +5516,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 1.0 }, "origin": "bottom", @@ -5549,8 +5550,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -5582,9 +5583,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -5612,9 +5613,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -5647,8 +5648,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 1.0 }, "origin": "bottom", @@ -5681,8 +5682,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -5714,9 +5715,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -5744,9 +5745,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -5779,8 +5780,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 1.0 }, "origin": "bottom", @@ -5813,8 +5814,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -5846,9 +5847,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -5876,9 +5877,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -5911,8 +5912,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 1.0 }, "origin": "bottom", @@ -5945,8 +5946,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -5978,9 +5979,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -6008,9 +6009,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -6043,8 +6044,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 1.0 }, "origin": "bottom", @@ -6077,8 +6078,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -6110,9 +6111,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -6140,9 +6141,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -6175,8 +6176,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 1.0 }, "origin": "bottom", @@ -6209,8 +6210,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -6242,9 +6243,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -6272,9 +6273,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -6307,8 +6308,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 1.0 }, "origin": "bottom", @@ -6341,8 +6342,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -6374,9 +6375,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -6404,9 +6405,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -6439,8 +6440,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 1.0 }, "origin": "bottom", @@ -6473,8 +6474,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -6506,9 +6507,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -6536,9 +6537,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -6571,8 +6572,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 1.0 }, "origin": "bottom", @@ -6605,8 +6606,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -6638,9 +6639,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -6668,9 +6669,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -6703,8 +6704,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 1.0 }, "origin": "bottom", @@ -6737,8 +6738,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -6770,9 +6771,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -6800,9 +6801,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -6835,8 +6836,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 1.0 }, "origin": "bottom", @@ -6869,8 +6870,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -6902,9 +6903,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -6932,9 +6933,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -6967,8 +6968,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 1.0 }, "origin": "bottom", @@ -7001,8 +7002,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -7034,9 +7035,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -7064,9 +7065,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -7099,8 +7100,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 1.0 }, "origin": "bottom", @@ -7133,8 +7134,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -7166,9 +7167,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -7196,9 +7197,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -7231,8 +7232,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 1.0 }, "origin": "bottom", @@ -7265,8 +7266,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -7298,9 +7299,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -7328,9 +7329,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -7363,8 +7364,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 1.0 }, "origin": "bottom", @@ -7397,8 +7398,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -7430,9 +7431,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -7460,9 +7461,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -7495,8 +7496,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 1.0 }, "origin": "bottom", @@ -7529,8 +7530,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -7562,9 +7563,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -7592,9 +7593,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -7627,8 +7628,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 1.0 }, "origin": "bottom", @@ -7661,8 +7662,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -7694,9 +7695,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -7724,9 +7725,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -7759,8 +7760,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 1.0 }, "origin": "bottom", @@ -7793,8 +7794,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -7826,9 +7827,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -7856,9 +7857,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -7891,8 +7892,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 1.0 }, "origin": "bottom", @@ -7925,8 +7926,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -7958,9 +7959,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -7988,9 +7989,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -8023,8 +8024,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 1.0 }, "origin": "bottom", @@ -8057,8 +8058,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -8090,9 +8091,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -8120,9 +8121,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -8155,8 +8156,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 1.0 }, "origin": "bottom", @@ -8189,8 +8190,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -8222,9 +8223,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -8252,9 +8253,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -8287,8 +8288,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 1.0 }, "origin": "bottom", @@ -8321,8 +8322,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -8354,9 +8355,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -8384,9 +8385,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -8419,8 +8420,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 1.0 }, "origin": "bottom", @@ -8453,8 +8454,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -8486,9 +8487,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -8516,9 +8517,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -8551,8 +8552,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 1.0 }, "origin": "bottom", @@ -8585,8 +8586,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -8618,9 +8619,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -8648,9 +8649,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -8683,8 +8684,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 1.0 }, "origin": "bottom", @@ -8717,8 +8718,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -8750,9 +8751,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -8780,9 +8781,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -8815,8 +8816,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 1.0 }, "origin": "bottom", @@ -8849,8 +8850,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -8882,9 +8883,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -8912,9 +8913,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -8947,8 +8948,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 1.0 }, "origin": "bottom", @@ -8981,8 +8982,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -9014,9 +9015,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -9044,9 +9045,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -9079,8 +9080,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 1.0 }, "origin": "bottom", @@ -9113,8 +9114,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -9146,9 +9147,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -9176,9 +9177,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -9211,8 +9212,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 1.0 }, "origin": "bottom", @@ -9245,8 +9246,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -9278,9 +9279,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -9308,9 +9309,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -9343,8 +9344,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 1.0 }, "origin": "bottom", @@ -9377,8 +9378,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -9410,9 +9411,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -9440,9 +9441,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -9475,8 +9476,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 1.0 }, "origin": "bottom", @@ -9509,8 +9510,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -9542,9 +9543,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -9572,9 +9573,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -9607,8 +9608,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 1.0 }, "origin": "bottom", @@ -9641,8 +9642,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -9674,9 +9675,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -9704,9 +9705,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -9739,8 +9740,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 1.0 }, "origin": "bottom", @@ -9773,8 +9774,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -9806,9 +9807,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -9836,9 +9837,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -9871,8 +9872,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 1.0 }, "origin": "bottom", @@ -9905,8 +9906,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -9938,9 +9939,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -9968,9 +9969,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -10003,8 +10004,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 1.0 }, "origin": "bottom", @@ -10037,8 +10038,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -10070,9 +10071,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -10100,9 +10101,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -10135,8 +10136,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 1.0 }, "origin": "bottom", @@ -10169,8 +10170,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -10202,9 +10203,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -10232,9 +10233,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -10267,8 +10268,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 1.0 }, "origin": "bottom", @@ -10301,8 +10302,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -10334,9 +10335,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -10364,9 +10365,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -10399,8 +10400,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 1.0 }, "origin": "bottom", @@ -10433,8 +10434,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -10466,9 +10467,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -10496,9 +10497,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -10531,8 +10532,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 1.0 }, "origin": "bottom", @@ -10565,8 +10566,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -10598,9 +10599,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -10628,9 +10629,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -10663,8 +10664,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 1.0 }, "origin": "bottom", @@ -10697,8 +10698,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -10730,9 +10731,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -10760,9 +10761,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -10795,8 +10796,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 1.0 }, "origin": "bottom", @@ -10829,8 +10830,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -10862,9 +10863,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -10892,9 +10893,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -10927,8 +10928,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 1.0 }, "origin": "bottom", @@ -10961,8 +10962,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -10994,9 +10995,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -11024,9 +11025,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -11059,8 +11060,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 1.0 }, "origin": "bottom", @@ -11093,8 +11094,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -11126,9 +11127,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -11156,9 +11157,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -11191,8 +11192,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 1.0 }, "origin": "bottom", @@ -11225,8 +11226,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -11258,9 +11259,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -11288,9 +11289,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -11323,8 +11324,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 1.0 }, "origin": "bottom", @@ -11357,8 +11358,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -11390,9 +11391,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -11420,9 +11421,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -11455,8 +11456,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 1.0 }, "origin": "bottom", @@ -11489,8 +11490,8 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -11522,9 +11523,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -11552,9 +11553,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -11587,8 +11588,8 @@ "volume": 10.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 1.0 }, "origin": "bottom", @@ -11621,8 +11622,8 @@ "volume": 10.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -11654,9 +11655,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -11684,9 +11685,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -11719,8 +11720,8 @@ "volume": 10.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 1.0 }, "origin": "bottom", @@ -11753,8 +11754,8 @@ "volume": 10.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -11786,9 +11787,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -11816,9 +11817,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -11851,8 +11852,8 @@ "volume": 10.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 1.0 }, "origin": "bottom", @@ -11885,8 +11886,8 @@ "volume": 10.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -11918,9 +11919,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -11948,9 +11949,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -11983,8 +11984,8 @@ "volume": 10.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 1.0 }, "origin": "bottom", @@ -12017,8 +12018,8 @@ "volume": 10.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -12050,9 +12051,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -12080,9 +12081,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -12115,8 +12116,8 @@ "volume": 10.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 1.0 }, "origin": "bottom", @@ -12149,8 +12150,8 @@ "volume": 10.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -12182,9 +12183,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -12212,9 +12213,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -12247,8 +12248,8 @@ "volume": 10.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 1.0 }, "origin": "bottom", @@ -12281,8 +12282,8 @@ "volume": 10.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -12314,9 +12315,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -12344,9 +12345,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -12379,8 +12380,8 @@ "volume": 10.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 1.0 }, "origin": "bottom", @@ -12413,8 +12414,8 @@ "volume": 10.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -12446,9 +12447,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -12476,9 +12477,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -12511,8 +12512,8 @@ "volume": 10.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 1.0 }, "origin": "bottom", @@ -12545,8 +12546,8 @@ "volume": 10.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -12578,9 +12579,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -12608,9 +12609,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -12643,8 +12644,8 @@ "volume": 10.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 1.0 }, "origin": "bottom", @@ -12677,8 +12678,8 @@ "volume": 10.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -12710,9 +12711,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -12796,6 +12797,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[cc26c104b4][Flex_S_v2_20_8_None_SINGLE_HappyPath].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[cc26c104b4][Flex_S_v2_20_8_None_SINGLE_HappyPath].json index 4ad4434ab42..00720d8483b 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[cc26c104b4][Flex_S_v2_20_8_None_SINGLE_HappyPath].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[cc26c104b4][Flex_S_v2_20_8_None_SINGLE_HappyPath].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -7179,6 +7180,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "description": "Tip Rack South Clearance for the 8 Channel pipette and a SINGLE partial tip configuration.", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[cecd51c8ee][pl_ExpressPlex_96_final].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[cecd51c8ee][pl_ExpressPlex_96_final].json index 4e8f71a17c1..93967eed2d8 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[cecd51c8ee][pl_ExpressPlex_96_final].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[cecd51c8ee][pl_ExpressPlex_96_final].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -12925,9 +12926,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -12937,7 +12938,7 @@ "position": { "x": 14.38, "y": 181.38, - "z": 98.42 + "z": 75.26 } }, "startedAt": "TIMESTAMP", @@ -13085,9 +13086,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -13097,7 +13098,7 @@ "position": { "x": 178.38, "y": 181.38, - "z": 98.42 + "z": 75.26 } }, "startedAt": "TIMESTAMP", @@ -13346,6 +13347,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "Index Plate color", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d0154b1493][Flex_X_v2_20_96_and_8_Overrides_InvalidConfigs_Override_eight_partial_column_bottom_right].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d0154b1493][Flex_X_v2_20_96_and_8_Overrides_InvalidConfigs_Override_eight_partial_column_bottom_right].json index 1149640d8b1..376f1a9c3d3 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d0154b1493][Flex_X_v2_20_96_and_8_Overrides_InvalidConfigs_Override_eight_partial_column_bottom_right].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d0154b1493][Flex_X_v2_20_96_and_8_Overrides_InvalidConfigs_Override_eight_partial_column_bottom_right].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -1314,6 +1315,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "description": "oooo", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d14738bdfe][pl_Zymo_Magbead_DNA_Cells_Flex_multi].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d14738bdfe][pl_Zymo_Magbead_DNA_Cells_Flex_multi].json index 6e02fa8a3f3..c8f00ac080e 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d14738bdfe][pl_Zymo_Magbead_DNA_Cells_Flex_multi].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d14738bdfe][pl_Zymo_Magbead_DNA_Cells_Flex_multi].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -828,6 +829,11 @@ }, "schemaVersion": 2, "stackingOffsetWithLabware": { + "evotips_flex_96_tiprack_adapter": { + "x": 0, + "y": 0, + "z": 102 + }, "opentrons_96_deep_well_adapter": { "x": 0, "y": 0, @@ -13202,9 +13208,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -14574,9 +14580,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -15341,9 +15347,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -16108,9 +16114,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -16875,9 +16881,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -18032,6 +18038,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "Samples", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d29d74d7fb][pl_QIASeq_FX_48x_v8].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d29d74d7fb][pl_QIASeq_FX_48x_v8].json index 9d35aba10fc..a9306a86942 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d29d74d7fb][pl_QIASeq_FX_48x_v8].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d29d74d7fb][pl_QIASeq_FX_48x_v8].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -5143,6 +5144,11 @@ }, "schemaVersion": 2, "stackingOffsetWithLabware": { + "evotips_flex_96_tiprack_adapter": { + "x": 0, + "y": 0, + "z": 102 + }, "opentrons_96_deep_well_adapter": { "x": 0, "y": 0, @@ -17016,9 +17022,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -18317,9 +18323,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -19618,9 +19624,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -20149,9 +20155,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -20549,9 +20555,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -20949,9 +20955,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -23914,9 +23920,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -24411,9 +24417,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -24908,9 +24914,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -26686,9 +26692,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -27183,9 +27189,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -27680,9 +27686,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -27838,9 +27844,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -27968,9 +27974,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -28098,9 +28104,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -28516,9 +28522,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -28848,9 +28854,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -29180,9 +29186,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -29555,9 +29561,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -29799,9 +29805,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -30043,9 +30049,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -35340,9 +35346,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -35837,9 +35843,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -36334,9 +36340,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -36831,9 +36837,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -38595,9 +38601,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -39078,9 +39084,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -40748,9 +40754,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -42512,9 +42518,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -42995,9 +43001,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -43478,9 +43484,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -43636,9 +43642,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -43766,9 +43772,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -43896,9 +43902,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -45571,9 +45577,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -45937,9 +45943,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -46303,9 +46309,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -52635,9 +52641,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -53936,9 +53942,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -56460,9 +56466,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -57088,9 +57094,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -57585,9 +57591,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -58082,9 +58088,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -59860,9 +59866,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -60357,9 +60363,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -60854,9 +60860,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -63855,9 +63861,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -64352,9 +64358,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -64849,9 +64855,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -65007,9 +65013,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -65137,9 +65143,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -65267,9 +65273,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -65719,9 +65725,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -66085,9 +66091,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -66451,9 +66457,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -68049,9 +68055,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -68293,9 +68299,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -68537,9 +68543,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -70949,6 +70955,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "CleanupBead Beads", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d2c818bf00][Flex_S_v2_20_P50_LPD].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d2c818bf00][Flex_S_v2_20_P50_LPD].json index 52e87c76f46..28841e807c1 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d2c818bf00][Flex_S_v2_20_P50_LPD].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d2c818bf00][Flex_S_v2_20_P50_LPD].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -5113,6 +5114,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "Test this wet!!!", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d391213095][Flex_S_v2_15_P1000_96_GRIP_HS_TM_QuickZymoMagbeadRNAExtractionCellsOrBacteria].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d391213095][Flex_S_v2_15_P1000_96_GRIP_HS_TM_QuickZymoMagbeadRNAExtractionCellsOrBacteria].json index 6f5f1f09b83..5a32f12c4fb 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d391213095][Flex_S_v2_15_P1000_96_GRIP_HS_TM_QuickZymoMagbeadRNAExtractionCellsOrBacteria].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d391213095][Flex_S_v2_15_P1000_96_GRIP_HS_TM_QuickZymoMagbeadRNAExtractionCellsOrBacteria].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -828,6 +829,11 @@ }, "schemaVersion": 2, "stackingOffsetWithLabware": { + "evotips_flex_96_tiprack_adapter": { + "x": 0, + "y": 0, + "z": 102 + }, "opentrons_96_deep_well_adapter": { "x": 0, "y": 0, @@ -4971,6 +4977,11 @@ }, "schemaVersion": 2, "stackingOffsetWithLabware": { + "evotips_flex_96_tiprack_adapter": { + "x": 0, + "y": 0, + "z": 102 + }, "opentrons_96_deep_well_adapter": { "x": 0, "y": 0, @@ -6250,6 +6261,11 @@ }, "schemaVersion": 2, "stackingOffsetWithLabware": { + "evotips_flex_96_tiprack_adapter": { + "x": 0, + "y": 0, + "z": 102 + }, "opentrons_96_deep_well_adapter": { "x": 0, "y": 0, @@ -7529,6 +7545,11 @@ }, "schemaVersion": 2, "stackingOffsetWithLabware": { + "evotips_flex_96_tiprack_adapter": { + "x": 0, + "y": 0, + "z": 102 + }, "opentrons_96_deep_well_adapter": { "x": 0, "y": 0, @@ -8808,6 +8829,11 @@ }, "schemaVersion": 2, "stackingOffsetWithLabware": { + "evotips_flex_96_tiprack_adapter": { + "x": 0, + "y": 0, + "z": 102 + }, "opentrons_96_deep_well_adapter": { "x": 0, "y": 0, @@ -10087,6 +10113,11 @@ }, "schemaVersion": 2, "stackingOffsetWithLabware": { + "evotips_flex_96_tiprack_adapter": { + "x": 0, + "y": 0, + "z": 102 + }, "opentrons_96_deep_well_adapter": { "x": 0, "y": 0, @@ -11366,6 +11397,11 @@ }, "schemaVersion": 2, "stackingOffsetWithLabware": { + "evotips_flex_96_tiprack_adapter": { + "x": 0, + "y": 0, + "z": 102 + }, "opentrons_96_deep_well_adapter": { "x": 0, "y": 0, @@ -15578,9 +15614,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -15590,7 +15626,7 @@ "position": { "x": 14.38, "y": 395.38, - "z": 90.88 + "z": 42.12400000000001 } }, "startedAt": "TIMESTAMP", @@ -19170,9 +19206,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -19182,7 +19218,7 @@ "position": { "x": 14.38, "y": 395.38, - "z": 90.88 + "z": 42.12400000000001 } }, "startedAt": "TIMESTAMP", @@ -19439,9 +19475,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -19451,7 +19487,7 @@ "position": { "x": 14.38, "y": 395.38, - "z": 90.88 + "z": 42.12400000000001 } }, "startedAt": "TIMESTAMP", @@ -21975,9 +22011,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -21987,7 +22023,7 @@ "position": { "x": 14.38, "y": 395.38, - "z": 90.88 + "z": 42.12400000000001 } }, "startedAt": "TIMESTAMP", @@ -22258,9 +22294,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -22270,7 +22306,7 @@ "position": { "x": 14.38, "y": 395.38, - "z": 90.88 + "z": 42.12400000000001 } }, "startedAt": "TIMESTAMP", @@ -22518,9 +22554,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -22530,7 +22566,7 @@ "position": { "x": 14.38, "y": 395.38, - "z": 90.88 + "z": 42.12400000000001 } }, "startedAt": "TIMESTAMP", @@ -22775,9 +22811,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -22787,7 +22823,7 @@ "position": { "x": 14.38, "y": 395.38, - "z": 90.88 + "z": 42.12400000000001 } }, "startedAt": "TIMESTAMP", @@ -23019,9 +23055,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -23031,7 +23067,7 @@ "position": { "x": 14.38, "y": 395.38, - "z": 90.88 + "z": 42.12400000000001 } }, "startedAt": "TIMESTAMP", @@ -23276,9 +23312,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -23288,7 +23324,7 @@ "position": { "x": 14.38, "y": 395.38, - "z": 90.88 + "z": 42.12400000000001 } }, "startedAt": "TIMESTAMP", @@ -23520,9 +23556,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -23532,7 +23568,7 @@ "position": { "x": 14.38, "y": 395.38, - "z": 90.88 + "z": 42.12400000000001 } }, "startedAt": "TIMESTAMP", @@ -23777,9 +23813,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -23789,7 +23825,7 @@ "position": { "x": 14.38, "y": 395.38, - "z": 90.88 + "z": 42.12400000000001 } }, "startedAt": "TIMESTAMP", @@ -24021,9 +24057,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -24033,7 +24069,7 @@ "position": { "x": 14.38, "y": 395.38, - "z": 90.88 + "z": 42.12400000000001 } }, "startedAt": "TIMESTAMP", @@ -24278,9 +24314,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -24290,7 +24326,7 @@ "position": { "x": 14.38, "y": 395.38, - "z": 90.88 + "z": 42.12400000000001 } }, "startedAt": "TIMESTAMP", @@ -24769,9 +24805,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -24781,7 +24817,7 @@ "position": { "x": 178.38, "y": 395.38, - "z": 90.88 + "z": 42.12400000000001 } }, "startedAt": "TIMESTAMP", @@ -25027,9 +25063,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -25039,7 +25075,7 @@ "position": { "x": 178.38, "y": 395.38, - "z": 90.88 + "z": 42.12400000000001 } }, "startedAt": "TIMESTAMP", @@ -25208,6 +25244,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "author": "Zach Galluzzo ", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d3b28ea1d7][pl_Zymo_Magbead_DNA_Cells_Flex_96_channel].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d3b28ea1d7][pl_Zymo_Magbead_DNA_Cells_Flex_96_channel].json index f0d2d744031..2ee45c0609a 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d3b28ea1d7][pl_Zymo_Magbead_DNA_Cells_Flex_96_channel].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d3b28ea1d7][pl_Zymo_Magbead_DNA_Cells_Flex_96_channel].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -828,6 +829,11 @@ }, "schemaVersion": 2, "stackingOffsetWithLabware": { + "evotips_flex_96_tiprack_adapter": { + "x": 0, + "y": 0, + "z": 102 + }, "opentrons_96_deep_well_adapter": { "x": 0, "y": 0, @@ -4971,6 +4977,11 @@ }, "schemaVersion": 2, "stackingOffsetWithLabware": { + "evotips_flex_96_tiprack_adapter": { + "x": 0, + "y": 0, + "z": 102 + }, "opentrons_96_deep_well_adapter": { "x": 0, "y": 0, @@ -9706,6 +9717,11 @@ }, "schemaVersion": 2, "stackingOffsetWithLabware": { + "evotips_flex_96_tiprack_adapter": { + "x": 0, + "y": 0, + "z": 102 + }, "opentrons_96_deep_well_adapter": { "x": 0, "y": 0, @@ -14441,6 +14457,11 @@ }, "schemaVersion": 2, "stackingOffsetWithLabware": { + "evotips_flex_96_tiprack_adapter": { + "x": 0, + "y": 0, + "z": 102 + }, "opentrons_96_deep_well_adapter": { "x": 0, "y": 0, @@ -17448,6 +17469,11 @@ }, "schemaVersion": 2, "stackingOffsetWithLabware": { + "evotips_flex_96_tiprack_adapter": { + "x": 0, + "y": 0, + "z": 102 + }, "opentrons_96_deep_well_adapter": { "x": 0, "y": 0, @@ -20455,6 +20481,11 @@ }, "schemaVersion": 2, "stackingOffsetWithLabware": { + "evotips_flex_96_tiprack_adapter": { + "x": 0, + "y": 0, + "z": 102 + }, "opentrons_96_deep_well_adapter": { "x": 0, "y": 0, @@ -23462,6 +23493,11 @@ }, "schemaVersion": 2, "stackingOffsetWithLabware": { + "evotips_flex_96_tiprack_adapter": { + "x": 0, + "y": 0, + "z": 102 + }, "opentrons_96_deep_well_adapter": { "x": 0, "y": 0, @@ -31130,9 +31166,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -31142,7 +31178,7 @@ "position": { "x": 14.38, "y": 395.38, - "z": 90.88 + "z": 42.12400000000001 } }, "startedAt": "TIMESTAMP", @@ -34722,9 +34758,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -34734,7 +34770,7 @@ "position": { "x": 14.38, "y": 395.38, - "z": 90.88 + "z": 42.12400000000001 } }, "startedAt": "TIMESTAMP", @@ -34991,9 +35027,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -35003,7 +35039,7 @@ "position": { "x": 14.38, "y": 395.38, - "z": 90.88 + "z": 42.12400000000001 } }, "startedAt": "TIMESTAMP", @@ -37527,9 +37563,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -37539,7 +37575,7 @@ "position": { "x": 14.38, "y": 395.38, - "z": 90.88 + "z": 42.12400000000001 } }, "startedAt": "TIMESTAMP", @@ -37810,9 +37846,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -37822,7 +37858,7 @@ "position": { "x": 14.38, "y": 395.38, - "z": 90.88 + "z": 42.12400000000001 } }, "startedAt": "TIMESTAMP", @@ -38070,9 +38106,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -38082,7 +38118,7 @@ "position": { "x": 14.38, "y": 395.38, - "z": 90.88 + "z": 42.12400000000001 } }, "startedAt": "TIMESTAMP", @@ -38341,9 +38377,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -38353,7 +38389,7 @@ "position": { "x": 14.38, "y": 395.38, - "z": 90.88 + "z": 42.12400000000001 } }, "startedAt": "TIMESTAMP", @@ -38601,9 +38637,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -38613,7 +38649,7 @@ "position": { "x": 14.38, "y": 395.38, - "z": 90.88 + "z": 42.12400000000001 } }, "startedAt": "TIMESTAMP", @@ -38872,9 +38908,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -38884,7 +38920,7 @@ "position": { "x": 14.38, "y": 395.38, - "z": 90.88 + "z": 42.12400000000001 } }, "startedAt": "TIMESTAMP", @@ -39132,9 +39168,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -39144,7 +39180,7 @@ "position": { "x": 14.38, "y": 395.38, - "z": 90.88 + "z": 42.12400000000001 } }, "startedAt": "TIMESTAMP", @@ -39403,9 +39439,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -39415,7 +39451,7 @@ "position": { "x": 14.38, "y": 395.38, - "z": 90.88 + "z": 42.12400000000001 } }, "startedAt": "TIMESTAMP", @@ -39663,9 +39699,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -39675,7 +39711,7 @@ "position": { "x": 14.38, "y": 395.38, - "z": 90.88 + "z": 42.12400000000001 } }, "startedAt": "TIMESTAMP", @@ -39934,9 +39970,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -39946,7 +39982,7 @@ "position": { "x": 14.38, "y": 395.38, - "z": 90.88 + "z": 42.12400000000001 } }, "startedAt": "TIMESTAMP", @@ -40441,9 +40477,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -40453,7 +40489,7 @@ "position": { "x": 178.38, "y": 395.38, - "z": 90.88 + "z": 42.12400000000001 } }, "startedAt": "TIMESTAMP", @@ -40713,9 +40749,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -40725,7 +40761,7 @@ "position": { "x": 178.38, "y": 395.38, - "z": 90.88 + "z": 42.12400000000001 } }, "startedAt": "TIMESTAMP", @@ -40886,6 +40922,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "Lysis Buffer", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d48bc4f0c9][OT2_S_v2_17_P300M_P20S_HS_TC_TM_SmokeTestV3].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d48bc4f0c9][OT2_S_v2_17_P300M_P20S_HS_TC_TM_SmokeTestV3].json index 3056b873a74..c7cb1821806 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d48bc4f0c9][OT2_S_v2_17_P300M_P20S_HS_TC_TM_SmokeTestV3].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d48bc4f0c9][OT2_S_v2_17_P300M_P20S_HS_TC_TM_SmokeTestV3].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -16182,9 +16183,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -16537,9 +16538,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -17430,6 +17431,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "H₂O", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d6026e11c5][OT2_X_v2_7_P300S_TwinningError].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d6026e11c5][OT2_X_v2_7_P300S_TwinningError].json index 026977dbcc6..3f8d27fbb6e 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d6026e11c5][OT2_X_v2_7_P300S_TwinningError].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d6026e11c5][OT2_X_v2_7_P300S_TwinningError].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -2638,9 +2639,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -2652,9 +2653,9 @@ "y": 0.0, "z": 0.0 }, - "tipDiameter": 0, + "tipDiameter": 0.0, "tipLength": 51.099999999999994, - "tipVolume": 300 + "tipVolume": 300.0 }, "startedAt": "TIMESTAMP", "status": "succeeded" @@ -2673,9 +2674,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -2707,9 +2708,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -2740,9 +2741,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -2836,6 +2837,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "apiLevel": "2.7", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d61739e6a3][Flex_S_v2_19_IDT_xGen_EZ_48x].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d61739e6a3][Flex_S_v2_19_IDT_xGen_EZ_48x].json index 99ccd21cc19..a8f00e1a0f1 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d61739e6a3][Flex_S_v2_19_IDT_xGen_EZ_48x].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d61739e6a3][Flex_S_v2_19_IDT_xGen_EZ_48x].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -5143,6 +5144,11 @@ }, "schemaVersion": 2, "stackingOffsetWithLabware": { + "evotips_flex_96_tiprack_adapter": { + "x": 0, + "y": 0, + "z": 102 + }, "opentrons_96_deep_well_adapter": { "x": 0, "y": 0, @@ -26879,9 +26885,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -27279,9 +27285,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -27679,9 +27685,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -30782,9 +30788,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -31279,9 +31285,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -31776,9 +31782,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -33692,9 +33698,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -34189,9 +34195,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -34686,9 +34692,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -35066,9 +35072,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -35418,9 +35424,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -35770,9 +35776,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -36120,9 +36126,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -36384,9 +36390,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -36648,9 +36654,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -37023,9 +37029,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -37267,9 +37273,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -37511,9 +37517,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -47537,9 +47543,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -48034,9 +48040,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -48531,9 +48537,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -51538,9 +51544,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -52035,9 +52041,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -52532,9 +52538,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -54352,9 +54358,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -54849,9 +54855,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -55346,9 +55352,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -55726,9 +55732,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -56078,9 +56084,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -56430,9 +56436,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -58003,9 +58009,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -58267,9 +58273,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -58531,9 +58537,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -58906,9 +58912,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -59150,9 +59156,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -59394,9 +59400,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -59919,6 +59925,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "author": "Opentrons ", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d6389183c0][pl_AMPure_XP_48x_v8].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d6389183c0][pl_AMPure_XP_48x_v8].json index 6b342319f31..a19d8ed0c76 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d6389183c0][pl_AMPure_XP_48x_v8].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d6389183c0][pl_AMPure_XP_48x_v8].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -4031,6 +4032,11 @@ }, "schemaVersion": 2, "stackingOffsetWithLabware": { + "evotips_flex_96_tiprack_adapter": { + "x": 0, + "y": 0, + "z": 102 + }, "opentrons_96_deep_well_adapter": { "x": 0, "y": 0, @@ -15813,9 +15819,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -16213,9 +16219,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -16613,9 +16619,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -19715,9 +19721,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -20115,9 +20121,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -20515,9 +20521,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -22430,9 +22436,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -22830,9 +22836,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -23230,9 +23236,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -24131,9 +24137,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -24495,9 +24501,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -24859,9 +24865,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -26373,9 +26379,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -26569,9 +26575,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -26765,9 +26771,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -28213,6 +28219,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "AMPure Beads", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d7e862d601][OT2_S_v2_18_None_None_duplicateChoiceValue].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d7e862d601][OT2_S_v2_18_None_None_duplicateChoiceValue].json index 7ea850030fd..da230dfc11c 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d7e862d601][OT2_S_v2_18_None_None_duplicateChoiceValue].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d7e862d601][OT2_S_v2_18_None_None_duplicateChoiceValue].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -43,6 +44,7 @@ } ], "labware": [], + "liquidClasses": [], "liquids": [], "metadata": { "protocolName": "Duplicate choice value" diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d8cb88b3b2][Flex_S_v2_16_P1000_96_TC_PartialTipPickupSingle].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d8cb88b3b2][Flex_S_v2_16_P1000_96_TC_PartialTipPickupSingle].json index 1e9b318abf5..1587a2ce11d 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d8cb88b3b2][Flex_S_v2_16_P1000_96_TC_PartialTipPickupSingle].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d8cb88b3b2][Flex_S_v2_16_P1000_96_TC_PartialTipPickupSingle].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -3591,6 +3592,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": {}, "modules": [], diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d979799443][OT2_S_v2_20_8_None_SINGLE_HappyPath].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d979799443][OT2_S_v2_20_8_None_SINGLE_HappyPath].json index 65c2da26059..30710e984f2 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d979799443][OT2_S_v2_20_8_None_SINGLE_HappyPath].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d979799443][OT2_S_v2_20_8_None_SINGLE_HappyPath].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -8241,6 +8242,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "description": "OT2 8 Channel pipette and a SINGLE partial tip configuration.", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[da326082e1][pl_Hyperplus_tiptracking_V4_final].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[da326082e1][pl_Hyperplus_tiptracking_V4_final].json index 8b7cf7214ac..b3678fd89b2 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[da326082e1][pl_Hyperplus_tiptracking_V4_final].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[da326082e1][pl_Hyperplus_tiptracking_V4_final].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -6865,6 +6866,11 @@ }, "schemaVersion": 2, "stackingOffsetWithLabware": { + "evotips_flex_96_tiprack_adapter": { + "x": 0, + "y": 0, + "z": 102 + }, "opentrons_96_deep_well_adapter": { "x": 0, "y": 0, @@ -29144,6 +29150,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "DNA sample of known quantity", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[dabb7872d8][Flex_S_v2_19_P1000_96_GRIP_HS_MB_TC_TM_Smoke].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[dabb7872d8][Flex_S_v2_19_P1000_96_GRIP_HS_MB_TC_TM_Smoke].json index b262ea72c0f..b1857c2cf42 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[dabb7872d8][Flex_S_v2_19_P1000_96_GRIP_HS_MB_TC_TM_Smoke].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[dabb7872d8][Flex_S_v2_19_P1000_96_GRIP_HS_MB_TC_TM_Smoke].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -10341,9 +10342,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -10353,7 +10354,7 @@ "position": { "x": 178.38, "y": 395.38, - "z": 90.88 + "z": 42.12400000000001 } }, "startedAt": "TIMESTAMP", @@ -11134,9 +11135,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -11146,7 +11147,7 @@ "position": { "x": 178.38, "y": 395.38, - "z": 90.88 + "z": 42.12400000000001 } }, "startedAt": "TIMESTAMP", @@ -13022,9 +13023,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -13034,7 +13035,7 @@ "position": { "x": 178.38, "y": 395.38, - "z": 90.88 + "z": 42.12400000000001 } }, "startedAt": "TIMESTAMP", @@ -13164,6 +13165,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "High Quality H₂O", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[db1fae41ec][Flex_X_v2_20_96_and_8_Overrides_InvalidConfigs_Override_ninety_six_partial_column_3].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[db1fae41ec][Flex_X_v2_20_96_and_8_Overrides_InvalidConfigs_Override_ninety_six_partial_column_3].json index beb0aa09c29..dd86269548a 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[db1fae41ec][Flex_X_v2_20_96_and_8_Overrides_InvalidConfigs_Override_ninety_six_partial_column_3].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[db1fae41ec][Flex_X_v2_20_96_and_8_Overrides_InvalidConfigs_Override_ninety_six_partial_column_3].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -1241,6 +1242,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "description": "oooo", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[dbba7a71a8][OT2_S_v2_16_NO_PIPETTES_TC_VerifyThermocyclerLoadedSlots].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[dbba7a71a8][OT2_S_v2_16_NO_PIPETTES_TC_VerifyThermocyclerLoadedSlots].json index 2288dccf926..34f2475222d 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[dbba7a71a8][OT2_S_v2_16_NO_PIPETTES_TC_VerifyThermocyclerLoadedSlots].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[dbba7a71a8][OT2_S_v2_16_NO_PIPETTES_TC_VerifyThermocyclerLoadedSlots].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -145,6 +146,7 @@ } ], "labware": [], + "liquidClasses": [], "liquids": [], "metadata": {}, "modules": [ diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[de4249ddfb][Flex_X_v2_16_NO_PIPETTES_TC_TrashBinAndThermocyclerConflict].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[de4249ddfb][Flex_X_v2_16_NO_PIPETTES_TC_TrashBinAndThermocyclerConflict].json index 0353b26aed1..9ea60fdbe89 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[de4249ddfb][Flex_X_v2_16_NO_PIPETTES_TC_TrashBinAndThermocyclerConflict].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[de4249ddfb][Flex_X_v2_16_NO_PIPETTES_TC_TrashBinAndThermocyclerConflict].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -171,6 +172,7 @@ } ], "labware": [], + "liquidClasses": [], "liquids": [], "metadata": { "protocolName": "Thermocycler conflict 1" diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e0b0133ffb][pl_Illumina_Stranded_total_RNA_Ribo_Zero_protocol].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e0b0133ffb][pl_Illumina_Stranded_total_RNA_Ribo_Zero_protocol].json index b22e56cb8ed..590ca209392 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e0b0133ffb][pl_Illumina_Stranded_total_RNA_Ribo_Zero_protocol].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e0b0133ffb][pl_Illumina_Stranded_total_RNA_Ribo_Zero_protocol].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -3282,6 +3283,11 @@ }, "schemaVersion": 2, "stackingOffsetWithLabware": { + "evotips_flex_96_tiprack_adapter": { + "x": 0, + "y": 0, + "z": 102 + }, "opentrons_96_deep_well_adapter": { "x": 0, "y": 0, @@ -39234,6 +39240,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "author": "Dandra Howell ", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e18bdd6f5d][Flex_S_2_15_P1000M_P50M_GRIP_HS_TM_MB_TC_KAPALibraryQuantv4_8].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e18bdd6f5d][Flex_S_2_15_P1000M_P50M_GRIP_HS_TM_MB_TC_KAPALibraryQuantv4_8].json index 6a1c9e67b51..9c502809377 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e18bdd6f5d][Flex_S_2_15_P1000M_P50M_GRIP_HS_TM_MB_TC_KAPALibraryQuantv4_8].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e18bdd6f5d][Flex_S_2_15_P1000M_P50M_GRIP_HS_TM_MB_TC_KAPALibraryQuantv4_8].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -18673,9 +18674,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -19089,9 +19090,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -19491,9 +19492,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -19893,9 +19894,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -20295,9 +20296,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -20697,9 +20698,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -21099,9 +21100,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -21560,9 +21561,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -21962,9 +21963,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -22364,9 +22365,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -22766,9 +22767,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -23168,9 +23169,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -23570,9 +23571,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -24597,9 +24598,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -25189,9 +25190,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -25781,9 +25782,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -26331,9 +26332,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -26881,9 +26882,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -27431,9 +27432,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -27981,9 +27982,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -28531,9 +28532,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -29563,9 +29564,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -30553,9 +30554,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -31543,9 +31544,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -32533,9 +32534,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -33523,9 +33524,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -34513,9 +34514,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -34667,6 +34668,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "author": "Opentrons ", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e42e36e3ca][OT2_X_v2_13_None_None_PythonSyntaxError].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e42e36e3ca][OT2_X_v2_13_None_None_PythonSyntaxError].json index bd05f58334f..954ce38e892 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e42e36e3ca][OT2_X_v2_13_None_None_PythonSyntaxError].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e42e36e3ca][OT2_X_v2_13_None_None_PythonSyntaxError].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [], "config": { "apiVersion": [ @@ -45,6 +46,7 @@ } ], "labware": [], + "liquidClasses": [], "liquids": [], "metadata": { "apiLevel": "2.13", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e4660ca6df][OT2_S_v4_P300S_None_MM_TM_TM_MOAMTemps].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e4660ca6df][OT2_S_v4_P300S_None_MM_TM_TM_MOAMTemps].json index 44fbc26f5b6..03af5582afa 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e4660ca6df][OT2_S_v4_P300S_None_MM_TM_TM_MOAMTemps].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e4660ca6df][OT2_S_v4_P300S_None_MM_TM_TM_MOAMTemps].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -2419,9 +2420,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -2433,9 +2434,9 @@ "y": 0.0, "z": 0.0 }, - "tipDiameter": 0, + "tipDiameter": 0.0, "tipLength": 51.099999999999994, - "tipVolume": 300 + "tipVolume": 300.0 }, "startedAt": "TIMESTAMP", "status": "succeeded" @@ -2454,9 +2455,9 @@ "volume": 100.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -2488,9 +2489,9 @@ "volume": 100.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -2521,9 +2522,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -2580,6 +2581,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "author": "AA BB", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e496fec176][Flex_X_v2_18_NO_PIPETTES_Overrides_BadTypesInRTP_Override_wrong_type_in_default].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e496fec176][Flex_X_v2_18_NO_PIPETTES_Overrides_BadTypesInRTP_Override_wrong_type_in_default].json index 013da0c0d7d..63d7284587d 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e496fec176][Flex_X_v2_18_NO_PIPETTES_Overrides_BadTypesInRTP_Override_wrong_type_in_default].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e496fec176][Flex_X_v2_18_NO_PIPETTES_Overrides_BadTypesInRTP_Override_wrong_type_in_default].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [], "config": { "apiVersion": [ @@ -42,6 +43,7 @@ } ], "labware": [], + "liquidClasses": [], "liquids": [], "metadata": { "protocolName": "Description Too Long 2.18" diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e4b5b30b2e][Flex_S_v2_18_Illumina_DNA_PCR_Free].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e4b5b30b2e][Flex_S_v2_18_Illumina_DNA_PCR_Free].json index 7f0ba6fd654..4459fe971fd 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e4b5b30b2e][Flex_S_v2_18_Illumina_DNA_PCR_Free].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e4b5b30b2e][Flex_S_v2_18_Illumina_DNA_PCR_Free].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -6157,6 +6158,11 @@ }, "schemaVersion": 2, "stackingOffsetWithLabware": { + "evotips_flex_96_tiprack_adapter": { + "x": 0, + "y": 0, + "z": 102 + }, "opentrons_96_deep_well_adapter": { "x": 0, "y": 0, @@ -27030,6 +27036,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "DNA sample of known quantity", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e71b031f47][pl_Illumina_DNA_PCR_Free].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e71b031f47][pl_Illumina_DNA_PCR_Free].json index a0e23ed018b..cffdb1bb54c 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e71b031f47][pl_Illumina_DNA_PCR_Free].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e71b031f47][pl_Illumina_DNA_PCR_Free].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -6157,6 +6158,11 @@ }, "schemaVersion": 2, "stackingOffsetWithLabware": { + "evotips_flex_96_tiprack_adapter": { + "x": 0, + "y": 0, + "z": 102 + }, "opentrons_96_deep_well_adapter": { "x": 0, "y": 0, @@ -27030,6 +27036,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "DNA sample of known quantity", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e84e23a4ea][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_top_edge].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e84e23a4ea][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_top_edge].json index 3ab5889bbf7..b7124e15878 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e84e23a4ea][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_top_edge].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e84e23a4ea][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_top_edge].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -1249,6 +1250,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "description": "oooo", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e8f451df45][Flex_S_v2_20_96_None_Column3_SINGLE_].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e8f451df45][Flex_S_v2_20_96_None_Column3_SINGLE_].json index ca6f70d1692..44a1b40bdff 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e8f451df45][Flex_S_v2_20_96_None_Column3_SINGLE_].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e8f451df45][Flex_S_v2_20_96_None_Column3_SINGLE_].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -26843,6 +26844,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": {}, "modules": [], diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ed1e64c539][Flex_X_v2_16_NO_PIPETTES_TM_ModuleInCol2].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ed1e64c539][Flex_X_v2_16_NO_PIPETTES_TM_ModuleInCol2].json index 368bbe05d9b..b3a25267695 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ed1e64c539][Flex_X_v2_16_NO_PIPETTES_TM_ModuleInCol2].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ed1e64c539][Flex_X_v2_16_NO_PIPETTES_TM_ModuleInCol2].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -103,6 +104,7 @@ } ], "labware": [], + "liquidClasses": [], "liquids": [], "metadata": { "author": "Derek Maggio ", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ed26635ff7][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_consolidate_source_collision].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ed26635ff7][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_consolidate_source_collision].json index 61a7e9595ff..5b9053fbea9 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ed26635ff7][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_consolidate_source_collision].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ed26635ff7][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_consolidate_source_collision].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -3878,6 +3879,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "description": "oooo", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ed2f3800b6][Flex_S_2_18_P1000M_P50M_GRIP_HS_TM_MB_TC_IlluminaDNAPrep24xV4_7].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ed2f3800b6][Flex_S_2_18_P1000M_P50M_GRIP_HS_TM_MB_TC_IlluminaDNAPrep24xV4_7].json index 00f911388c0..0dbe3c3fa57 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ed2f3800b6][Flex_S_2_18_P1000M_P50M_GRIP_HS_TM_MB_TC_IlluminaDNAPrep24xV4_7].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ed2f3800b6][Flex_S_2_18_P1000M_P50M_GRIP_HS_TM_MB_TC_IlluminaDNAPrep24xV4_7].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -12766,9 +12767,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -13449,9 +13450,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -14132,9 +14133,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -15239,9 +15240,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -16158,9 +16159,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -17077,9 +17078,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -17709,9 +17710,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -18065,9 +18066,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -18421,9 +18422,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -18930,9 +18931,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -19377,9 +19378,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -19824,9 +19825,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -20256,9 +20257,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -20612,9 +20613,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -20968,9 +20969,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -21477,9 +21478,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -21924,9 +21925,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -22371,9 +22372,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -22803,9 +22804,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -23159,9 +23160,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -23515,9 +23516,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -24024,9 +24025,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -24471,9 +24472,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -24918,9 +24919,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -25350,9 +25351,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -25706,9 +25707,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -26062,9 +26063,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -26220,9 +26221,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -26350,9 +26351,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -26480,9 +26481,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -27410,9 +27411,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -28264,9 +28265,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -29118,9 +29119,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -29627,9 +29628,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -29999,9 +30000,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -30371,9 +30372,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -31567,9 +31568,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -32375,9 +32376,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -33183,9 +33184,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -33767,9 +33768,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -34153,9 +34154,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -34539,9 +34540,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -35631,9 +35632,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -36045,9 +36046,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -36431,9 +36432,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -36817,9 +36818,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -37909,9 +37910,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -38323,9 +38324,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -38709,9 +38710,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -39095,9 +39096,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -39253,9 +39254,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -39383,9 +39384,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -39513,9 +39514,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -40049,9 +40050,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -40334,9 +40335,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -40498,9 +40499,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -40662,9 +40663,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -40778,6 +40779,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "author": "Opentrons ", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f0efddcd7d][Flex_X_v2_16_P1000_96_DropTipsWithNoTrash].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f0efddcd7d][Flex_X_v2_16_P1000_96_DropTipsWithNoTrash].json index 4bcefec1199..f7d8523d4ed 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f0efddcd7d][Flex_X_v2_16_P1000_96_DropTipsWithNoTrash].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f0efddcd7d][Flex_X_v2_16_P1000_96_DropTipsWithNoTrash].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -1448,6 +1449,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "author": "Derek Maggio ", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f24bb0b4d9][Flex_S_v2_15_P1000_96_GRIP_HS_MB_TC_TM_IlluminaDNAPrep96PART3].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f24bb0b4d9][Flex_S_v2_15_P1000_96_GRIP_HS_MB_TC_TM_IlluminaDNAPrep96PART3].json index d1feceae4d0..77c0392213f 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f24bb0b4d9][Flex_S_v2_15_P1000_96_GRIP_HS_MB_TC_TM_IlluminaDNAPrep96PART3].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f24bb0b4d9][Flex_S_v2_15_P1000_96_GRIP_HS_MB_TC_TM_IlluminaDNAPrep96PART3].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -4869,6 +4870,11 @@ }, "schemaVersion": 2, "stackingOffsetWithLabware": { + "evotips_flex_96_tiprack_adapter": { + "x": 0, + "y": 0, + "z": 102 + }, "opentrons_96_deep_well_adapter": { "x": 0, "y": 0, @@ -6147,6 +6153,11 @@ }, "schemaVersion": 2, "stackingOffsetWithLabware": { + "evotips_flex_96_tiprack_adapter": { + "x": 0, + "y": 0, + "z": 102 + }, "opentrons_96_deep_well_adapter": { "x": 0, "y": 0, @@ -9942,6 +9953,11 @@ }, "schemaVersion": 2, "stackingOffsetWithLabware": { + "evotips_flex_96_tiprack_adapter": { + "x": 0, + "y": 0, + "z": 102 + }, "opentrons_96_deep_well_adapter": { "x": 0, "y": 0, @@ -11220,6 +11236,11 @@ }, "schemaVersion": 2, "stackingOffsetWithLabware": { + "evotips_flex_96_tiprack_adapter": { + "x": 0, + "y": 0, + "z": 102 + }, "opentrons_96_deep_well_adapter": { "x": 0, "y": 0, @@ -12454,9 +12475,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -12466,7 +12487,7 @@ "position": { "x": 178.38, "y": 74.38, - "z": 98.33 + "z": 74.99000000000001 } }, "startedAt": "TIMESTAMP", @@ -13228,9 +13249,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -13240,7 +13261,7 @@ "position": { "x": 178.38, "y": 288.38, - "z": 98.33 + "z": 74.99000000000001 } }, "startedAt": "TIMESTAMP", @@ -13731,9 +13752,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -13743,7 +13764,7 @@ "position": { "x": 178.38, "y": 288.38, - "z": 98.33 + "z": 74.99000000000001 } }, "startedAt": "TIMESTAMP", @@ -16527,9 +16548,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -16539,7 +16560,7 @@ "position": { "x": 178.38, "y": 74.38, - "z": 98.33 + "z": 74.99000000000001 } }, "startedAt": "TIMESTAMP", @@ -16927,9 +16948,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -16939,7 +16960,7 @@ "position": { "x": 178.38, "y": 74.38, - "z": 98.33 + "z": 74.99000000000001 } }, "startedAt": "TIMESTAMP", @@ -17343,9 +17364,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -17355,7 +17376,7 @@ "position": { "x": 178.38, "y": 74.38, - "z": 98.33 + "z": 74.99000000000001 } }, "startedAt": "TIMESTAMP", @@ -17743,9 +17764,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -17755,7 +17776,7 @@ "position": { "x": 178.38, "y": 74.38, - "z": 98.33 + "z": 74.99000000000001 } }, "startedAt": "TIMESTAMP", @@ -18009,6 +18030,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "author": "Opentrons ", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f301704f56][OT2_S_v6_P300M_P300S_HS_HS_NormalUseWithTransfer].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f301704f56][OT2_S_v6_P300M_P300S_HS_HS_NormalUseWithTransfer].json index 4e89581c149..3e8b4c50dea 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f301704f56][OT2_S_v6_P300M_P300S_HS_HS_NormalUseWithTransfer].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f301704f56][OT2_S_v6_P300M_P300S_HS_HS_NormalUseWithTransfer].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -4737,9 +4738,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -4772,8 +4773,8 @@ "volume": 50.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 1.0 }, "origin": "bottom", @@ -4806,8 +4807,8 @@ "volume": 50.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -4839,9 +4840,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -4869,9 +4870,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -4904,8 +4905,8 @@ "volume": 75.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 1.0 }, "origin": "bottom", @@ -4938,8 +4939,8 @@ "volume": 75.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -4971,9 +4972,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -5001,9 +5002,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -5036,8 +5037,8 @@ "volume": 75.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 1.0 }, "origin": "bottom", @@ -5070,8 +5071,8 @@ "volume": 75.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -5103,9 +5104,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -5133,9 +5134,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -5168,8 +5169,8 @@ "volume": 75.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 1.0 }, "origin": "bottom", @@ -5202,8 +5203,8 @@ "volume": 75.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -5235,9 +5236,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -5265,9 +5266,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -5300,8 +5301,8 @@ "volume": 75.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 1.0 }, "origin": "bottom", @@ -5334,8 +5335,8 @@ "volume": 75.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -5367,9 +5368,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -5513,9 +5514,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -5548,8 +5549,8 @@ "volume": 50.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 1.0 }, "origin": "bottom", @@ -5582,8 +5583,8 @@ "volume": 50.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -5615,9 +5616,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -5645,9 +5646,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -5680,8 +5681,8 @@ "volume": 70.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 1.0 }, "origin": "bottom", @@ -5714,8 +5715,8 @@ "volume": 70.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -5747,9 +5748,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -5777,9 +5778,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -5812,8 +5813,8 @@ "volume": 70.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 1.0 }, "origin": "bottom", @@ -5846,8 +5847,8 @@ "volume": 70.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -5879,9 +5880,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -5909,9 +5910,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -5944,8 +5945,8 @@ "volume": 70.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 1.0 }, "origin": "bottom", @@ -5978,8 +5979,8 @@ "volume": 70.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -6011,9 +6012,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -6041,9 +6042,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -6076,8 +6077,8 @@ "volume": 70.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 1.0 }, "origin": "bottom", @@ -6110,8 +6111,8 @@ "volume": 70.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -6143,9 +6144,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -6173,9 +6174,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -6208,8 +6209,8 @@ "volume": 300.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -6242,8 +6243,8 @@ "volume": 300.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -6276,8 +6277,8 @@ "volume": 300.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -6310,8 +6311,8 @@ "volume": 300.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -6344,8 +6345,8 @@ "volume": 300.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -6378,8 +6379,8 @@ "volume": 300.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -6412,8 +6413,8 @@ "volume": 300.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -6446,8 +6447,8 @@ "volume": 300.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -6480,8 +6481,8 @@ "volume": 300.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -6514,8 +6515,8 @@ "volume": 300.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -6547,9 +6548,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -6577,9 +6578,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -6612,8 +6613,8 @@ "volume": 270.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 1.0 }, "origin": "bottom", @@ -6646,8 +6647,8 @@ "volume": 50.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -6680,8 +6681,8 @@ "volume": 50.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -6714,8 +6715,8 @@ "volume": 50.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -6748,8 +6749,8 @@ "volume": 50.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -6782,8 +6783,8 @@ "volume": 50.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -6815,8 +6816,8 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.0 }, "origin": "bottom", @@ -6848,8 +6849,8 @@ "volume": 170.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 1.0 }, "origin": "bottom", @@ -6882,8 +6883,8 @@ "volume": 50.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -6916,8 +6917,8 @@ "volume": 50.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -6950,8 +6951,8 @@ "volume": 50.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.5 }, "origin": "bottom", @@ -6983,8 +6984,8 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, + "x": 0.0, + "y": 0.0, "z": 0.0 }, "origin": "bottom", @@ -7015,9 +7016,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -7101,6 +7102,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f345e8e33a][OT2_S_v4_P300M_P20S_MM_TM_TC1_PD40].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f345e8e33a][OT2_S_v4_P300M_P20S_MM_TM_TC1_PD40].json index ab9fd95e4c0..2b339c70d69 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f345e8e33a][OT2_S_v4_P300M_P20S_MM_TM_TC1_PD40].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f345e8e33a][OT2_S_v4_P300M_P20S_MM_TM_TC1_PD40].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -6991,9 +6992,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -7005,9 +7006,9 @@ "y": 0.0, "z": 0.0 }, - "tipDiameter": 0, + "tipDiameter": 0.0, "tipLength": 51.099999999999994, - "tipVolume": 300 + "tipVolume": 300.0 }, "startedAt": "TIMESTAMP", "status": "succeeded" @@ -7026,9 +7027,9 @@ "volume": 100.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -7060,9 +7061,9 @@ "volume": 100.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -7093,9 +7094,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -7168,9 +7169,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -7182,9 +7183,9 @@ "y": 0.0, "z": 0.0 }, - "tipDiameter": 0, + "tipDiameter": 0.0, "tipLength": 30.950000000000003, - "tipVolume": 20 + "tipVolume": 20.0 }, "startedAt": "TIMESTAMP", "status": "succeeded" @@ -7203,9 +7204,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -7237,9 +7238,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -7271,9 +7272,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -7305,9 +7306,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -7339,9 +7340,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -7373,9 +7374,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -7407,9 +7408,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -7441,9 +7442,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -7475,9 +7476,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -7509,9 +7510,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -7542,9 +7543,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -7572,9 +7573,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -7586,9 +7587,9 @@ "y": 0.0, "z": 0.0 }, - "tipDiameter": 0, + "tipDiameter": 0.0, "tipLength": 30.950000000000003, - "tipVolume": 20 + "tipVolume": 20.0 }, "startedAt": "TIMESTAMP", "status": "succeeded" @@ -7607,9 +7608,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -7641,9 +7642,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -7675,9 +7676,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -7709,9 +7710,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -7743,9 +7744,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -7777,9 +7778,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -7811,9 +7812,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -7845,9 +7846,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -7879,9 +7880,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -7913,9 +7914,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -7946,9 +7947,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -7976,9 +7977,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -7990,9 +7991,9 @@ "y": 0.0, "z": 0.0 }, - "tipDiameter": 0, + "tipDiameter": 0.0, "tipLength": 30.950000000000003, - "tipVolume": 20 + "tipVolume": 20.0 }, "startedAt": "TIMESTAMP", "status": "succeeded" @@ -8011,9 +8012,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -8045,9 +8046,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -8079,9 +8080,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -8113,9 +8114,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -8147,9 +8148,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -8181,9 +8182,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -8215,9 +8216,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -8249,9 +8250,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -8283,9 +8284,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -8317,9 +8318,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -8350,9 +8351,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -8380,9 +8381,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -8394,9 +8395,9 @@ "y": 0.0, "z": 0.0 }, - "tipDiameter": 0, + "tipDiameter": 0.0, "tipLength": 30.950000000000003, - "tipVolume": 20 + "tipVolume": 20.0 }, "startedAt": "TIMESTAMP", "status": "succeeded" @@ -8415,9 +8416,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -8449,9 +8450,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -8483,9 +8484,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -8517,9 +8518,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -8551,9 +8552,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -8585,9 +8586,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -8619,9 +8620,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -8653,9 +8654,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -8687,9 +8688,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -8721,9 +8722,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -8754,9 +8755,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -8784,9 +8785,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -8798,9 +8799,9 @@ "y": 0.0, "z": 0.0 }, - "tipDiameter": 0, + "tipDiameter": 0.0, "tipLength": 30.950000000000003, - "tipVolume": 20 + "tipVolume": 20.0 }, "startedAt": "TIMESTAMP", "status": "succeeded" @@ -8819,9 +8820,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -8853,9 +8854,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -8887,9 +8888,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -8921,9 +8922,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -8955,9 +8956,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -8989,9 +8990,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -9023,9 +9024,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -9057,9 +9058,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -9091,9 +9092,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -9125,9 +9126,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -9158,9 +9159,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -9188,9 +9189,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -9202,9 +9203,9 @@ "y": 0.0, "z": 0.0 }, - "tipDiameter": 0, + "tipDiameter": 0.0, "tipLength": 30.950000000000003, - "tipVolume": 20 + "tipVolume": 20.0 }, "startedAt": "TIMESTAMP", "status": "succeeded" @@ -9223,9 +9224,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -9257,9 +9258,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -9291,9 +9292,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -9325,9 +9326,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -9359,9 +9360,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -9393,9 +9394,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -9427,9 +9428,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -9461,9 +9462,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -9495,9 +9496,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -9529,9 +9530,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -9562,9 +9563,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -9592,9 +9593,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -9606,9 +9607,9 @@ "y": 0.0, "z": 0.0 }, - "tipDiameter": 0, + "tipDiameter": 0.0, "tipLength": 30.950000000000003, - "tipVolume": 20 + "tipVolume": 20.0 }, "startedAt": "TIMESTAMP", "status": "succeeded" @@ -9627,9 +9628,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -9661,9 +9662,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -9695,9 +9696,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -9729,9 +9730,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -9763,9 +9764,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -9797,9 +9798,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -9831,9 +9832,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -9865,9 +9866,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -9899,9 +9900,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -9933,9 +9934,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -9966,9 +9967,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -9996,9 +9997,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -10010,9 +10011,9 @@ "y": 0.0, "z": 0.0 }, - "tipDiameter": 0, + "tipDiameter": 0.0, "tipLength": 30.950000000000003, - "tipVolume": 20 + "tipVolume": 20.0 }, "startedAt": "TIMESTAMP", "status": "succeeded" @@ -10031,9 +10032,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -10065,9 +10066,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -10099,9 +10100,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -10133,9 +10134,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -10167,9 +10168,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -10201,9 +10202,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -10235,9 +10236,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -10269,9 +10270,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -10303,9 +10304,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -10337,9 +10338,9 @@ "volume": 20.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -10370,9 +10371,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -10400,9 +10401,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top" }, @@ -10414,9 +10415,9 @@ "y": 0.0, "z": 0.0 }, - "tipDiameter": 0, + "tipDiameter": 0.0, "tipLength": 51.099999999999994, - "tipVolume": 300 + "tipVolume": 300.0 }, "startedAt": "TIMESTAMP", "status": "succeeded" @@ -10435,9 +10436,9 @@ "volume": 100.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -10469,9 +10470,9 @@ "volume": 100.0, "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "top", "volumeOffset": 0.0 @@ -10502,9 +10503,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -10615,6 +10616,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "author": "NN MM", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f37bb0ec36][OT2_S_v2_16_NO_PIPETTES_verifyDoesNotDeadlock].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f37bb0ec36][OT2_S_v2_16_NO_PIPETTES_verifyDoesNotDeadlock].json index b12618b009e..494cd46f04d 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f37bb0ec36][OT2_S_v2_16_NO_PIPETTES_verifyDoesNotDeadlock].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f37bb0ec36][OT2_S_v2_16_NO_PIPETTES_verifyDoesNotDeadlock].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -29,6 +30,7 @@ } ], "labware": [], + "liquidClasses": [], "liquids": [], "metadata": {}, "modules": [], diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f51172f73b][Flex_S_v2_16_P1000_96_GRIP_HS_MB_TC_TM_Smoke].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f51172f73b][Flex_S_v2_16_P1000_96_GRIP_HS_MB_TC_TM_Smoke].json index f8f121ce092..38ce1d02116 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f51172f73b][Flex_S_v2_16_P1000_96_GRIP_HS_MB_TC_TM_Smoke].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f51172f73b][Flex_S_v2_16_P1000_96_GRIP_HS_MB_TC_TM_Smoke].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -10838,9 +10839,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -10850,7 +10851,7 @@ "position": { "x": 178.38, "y": 395.38, - "z": 90.88 + "z": 42.12400000000001 } }, "startedAt": "TIMESTAMP", @@ -11355,9 +11356,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -11367,7 +11368,7 @@ "position": { "x": 178.38, "y": 395.38, - "z": 90.88 + "z": 42.12400000000001 } }, "startedAt": "TIMESTAMP", @@ -15386,6 +15387,7 @@ "location": "offDeck" } ], + "liquidClasses": [], "liquids": [ { "description": "High Quality H₂O", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f5f3b9c5bb][Flex_X_v2_16_P1000_96_TM_ModuleAndWasteChuteConflict].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f5f3b9c5bb][Flex_X_v2_16_P1000_96_TM_ModuleAndWasteChuteConflict].json index d452cf7ab52..48d7508e005 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f5f3b9c5bb][Flex_X_v2_16_P1000_96_TM_ModuleAndWasteChuteConflict].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f5f3b9c5bb][Flex_X_v2_16_P1000_96_TM_ModuleAndWasteChuteConflict].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -1339,6 +1340,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "author": "Derek Maggio ", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f639acc89d][Flex_S_v2_15_NO_PIPETTES_TC_verifyThermocyclerLoadedSlots].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f639acc89d][Flex_S_v2_15_NO_PIPETTES_TC_verifyThermocyclerLoadedSlots].json index 2c598934321..8ebd28d3748 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f639acc89d][Flex_S_v2_15_NO_PIPETTES_TC_verifyThermocyclerLoadedSlots].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f639acc89d][Flex_S_v2_15_NO_PIPETTES_TC_verifyThermocyclerLoadedSlots].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -180,6 +181,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": {}, "modules": [ diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f6c1ddbb32][pl_ExpressPlex_Pooling_Final].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f6c1ddbb32][pl_ExpressPlex_Pooling_Final].json index 8ca9a88cdbf..6ca5fe984f2 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f6c1ddbb32][pl_ExpressPlex_Pooling_Final].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f6c1ddbb32][pl_ExpressPlex_Pooling_Final].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -21490,9 +21491,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -21682,9 +21683,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -21874,9 +21875,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -22066,9 +22067,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -22258,9 +22259,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -22450,9 +22451,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -22642,9 +22643,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -22834,9 +22835,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -23026,9 +23027,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -23218,9 +23219,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -23410,9 +23411,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -23602,9 +23603,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -23794,9 +23795,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -23986,9 +23987,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -24178,9 +24179,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -24370,9 +24371,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -24562,9 +24563,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -24754,9 +24755,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -24946,9 +24947,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -25138,9 +25139,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -25330,9 +25331,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -25522,9 +25523,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -25714,9 +25715,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -25906,9 +25907,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -26098,9 +26099,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -26290,9 +26291,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -26482,9 +26483,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -26674,9 +26675,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -26866,9 +26867,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -27058,9 +27059,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -27250,9 +27251,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -27442,9 +27443,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -27634,9 +27635,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -27826,9 +27827,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -28018,9 +28019,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -28210,9 +28211,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -28402,9 +28403,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -28594,9 +28595,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -28786,9 +28787,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -28978,9 +28979,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -29170,9 +29171,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -29362,9 +29363,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -29554,9 +29555,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -29746,9 +29747,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -29938,9 +29939,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -30130,9 +30131,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -30322,9 +30323,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -30514,9 +30515,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -30823,9 +30824,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -31015,9 +31016,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -31207,9 +31208,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -31399,9 +31400,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -31591,9 +31592,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -31783,9 +31784,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -31975,9 +31976,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -32167,9 +32168,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -32359,9 +32360,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -32551,9 +32552,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -32743,9 +32744,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -32935,9 +32936,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -33127,9 +33128,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -33319,9 +33320,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -33511,9 +33512,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -33703,9 +33704,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -33895,9 +33896,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -34087,9 +34088,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -34279,9 +34280,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -34471,9 +34472,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -34663,9 +34664,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -34855,9 +34856,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -35047,9 +35048,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -35239,9 +35240,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -35431,9 +35432,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -35623,9 +35624,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -35815,9 +35816,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -36007,9 +36008,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -36199,9 +36200,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -36391,9 +36392,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -36583,9 +36584,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -36775,9 +36776,9 @@ "pipetteId": "UUID", "wellLocation": { "offset": { - "x": 0, - "y": 0, - "z": 0 + "x": 0.0, + "y": 0.0, + "z": 0.0 }, "origin": "default" }, @@ -36949,6 +36950,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "Amplified Libraries_1", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f7085d7134][Flex_X_v2_16_P1000_96_TC_pipetteCollisionWithThermocyclerLidClips].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f7085d7134][Flex_X_v2_16_P1000_96_TC_pipetteCollisionWithThermocyclerLidClips].json index 04d54b06b4e..81a870ec2c9 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f7085d7134][Flex_X_v2_16_P1000_96_TC_pipetteCollisionWithThermocyclerLidClips].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f7085d7134][Flex_X_v2_16_P1000_96_TC_pipetteCollisionWithThermocyclerLidClips].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -1385,6 +1386,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": {}, "modules": [ diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f834b97da1][Flex_S_v2_16_P1000_96_GRIP_HS_MB_TC_TM_DeckConfiguration1].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f834b97da1][Flex_S_v2_16_P1000_96_GRIP_HS_MB_TC_TM_DeckConfiguration1].json index 3152a671909..6d3153af4eb 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f834b97da1][Flex_S_v2_16_P1000_96_GRIP_HS_MB_TC_TM_DeckConfiguration1].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f834b97da1][Flex_S_v2_16_P1000_96_GRIP_HS_MB_TC_TM_DeckConfiguration1].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -14192,6 +14193,7 @@ "location": "offDeck" } ], + "liquidClasses": [], "liquids": [ { "description": "High Quality H₂O", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f86713b4d4][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_distribute_source_collision].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f86713b4d4][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_distribute_source_collision].json index 09e15f48097..044d6d802f4 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f86713b4d4][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_distribute_source_collision].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f86713b4d4][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_distribute_source_collision].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -3878,6 +3879,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "description": "oooo", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f88b7d6e30][Flex_X_v2_18_NO_PIPETTES_Overrides_BadTypesInRTP_Override_wrong_type_in_display_name].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f88b7d6e30][Flex_X_v2_18_NO_PIPETTES_Overrides_BadTypesInRTP_Override_wrong_type_in_display_name].json index 1652972327b..e181f59e6cd 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f88b7d6e30][Flex_X_v2_18_NO_PIPETTES_Overrides_BadTypesInRTP_Override_wrong_type_in_display_name].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f88b7d6e30][Flex_X_v2_18_NO_PIPETTES_Overrides_BadTypesInRTP_Override_wrong_type_in_display_name].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [], "config": { "apiVersion": [ @@ -42,6 +43,7 @@ } ], "labware": [], + "liquidClasses": [], "liquids": [], "metadata": { "protocolName": "Description Too Long 2.18" diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[fc60ef9cbd][OT2_S_v2_16_P300M_P20S_HS_TC_TM_dispense_changes].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[fc60ef9cbd][OT2_S_v2_16_P300M_P20S_HS_TC_TM_dispense_changes].json index 13f15c638d0..da344f2c829 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[fc60ef9cbd][OT2_S_v2_16_P300M_P20S_HS_TC_TM_dispense_changes].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[fc60ef9cbd][OT2_S_v2_16_P300M_P20S_HS_TC_TM_dispense_changes].json @@ -1,4 +1,5 @@ { + "commandAnnotations": [], "commands": [ { "commandType": "home", @@ -3105,6 +3106,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "author": "Opentrons Engineering ", diff --git a/api-client/src/runs/getRunLoadedLabwareDefintions.ts b/api-client/src/runs/getRunLoadedLabwareDefintions.ts new file mode 100644 index 00000000000..d96e7facf8f --- /dev/null +++ b/api-client/src/runs/getRunLoadedLabwareDefintions.ts @@ -0,0 +1,17 @@ +import { GET, request } from '../request' + +import type { ResponsePromise } from '../request' +import type { HostConfig } from '../types' +import type { RunLoadedLabwareDefinitions } from './types' + +export function getRunLoadedLabwareDefintions( + config: HostConfig, + runId: string +): ResponsePromise { + return request( + GET, + `runs/${runId}/loaded_labware_definitions`, + null, + config + ) +} diff --git a/api-client/src/runs/index.ts b/api-client/src/runs/index.ts index fff1f303543..cbbe54999fa 100644 --- a/api-client/src/runs/index.ts +++ b/api-client/src/runs/index.ts @@ -16,6 +16,7 @@ export * from './createLabwareDefinition' export * from './constants' export * from './updateErrorRecoveryPolicy' export * from './getErrorRecoveryPolicy' +export * from './getRunLoadedLabwareDefintions' export * from './types' export type { CreateRunData } from './createRun' diff --git a/api-client/src/runs/types.ts b/api-client/src/runs/types.ts index e41626b6448..ea24c040ebc 100644 --- a/api-client/src/runs/types.ts +++ b/api-client/src/runs/types.ts @@ -9,6 +9,9 @@ import type { RunTimeParameter, NozzleLayoutConfig, OnDeckLabwareLocation, + LabwareDefinition1, + LabwareDefinition2, + LabwareDefinition3, } from '@opentrons/shared-data' import type { ResourceLink, ErrorDetails } from '../types' export * from './commands/types' @@ -86,6 +89,10 @@ export interface LabwareOffset { vector: VectorOffset } +export interface RunLoadedLabwareDefinitions { + data: Array +} + export interface Run { data: RunData } diff --git a/api/Pipfile b/api/Pipfile index 950a39b9f71..39b6170566a 100755 --- a/api/Pipfile +++ b/api/Pipfile @@ -5,13 +5,15 @@ name = "pypi" [packages] jsonschema = "==4.17.3" -pydantic = "==1.10.12" +pydantic = "==2.9.0" +pydantic-settings = "==2.4.0" anyio = "==3.7.1" opentrons-shared-data = { editable = true, path = "../shared-data/python" } opentrons = { editable = true, path = "." } opentrons-hardware = { editable = true, path = "./../hardware", extras=["FLEX"] } +performance-metrics = {file = "../performance-metrics", editable = true} numpy = "==1.22.3" -packaging = "==21.3" +packaging = "==22.0" pyusb = "==1.2.1" [dev-packages] @@ -21,7 +23,7 @@ pyusb = "==1.2.1" atomicwrites = { version = "==1.4.0", markers="sys_platform=='win32'" } colorama = { version = "==0.4.4", markers="sys_platform=='win32'" } coverage = "==7.4.1" -mypy = "==1.8.0" +mypy = "==1.11.0" numpydoc = "==0.9.1" pytest = "==7.4.4" pytest-asyncio = "~=0.23.0" @@ -50,4 +52,3 @@ 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 f423cb7e2ca..27a195a29e3 100644 --- a/api/Pipfile.lock +++ b/api/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "f931b3b78087f32b1e1d8b344f4d01258b4fbb62cf22d59910f90041a72da0b5" + "sha256": "6c57732ab68828247e656cadaa071cc60bb7ee56cdc89b9f26c56d0f17aec7a6" }, "pipfile-spec": 6, "requires": {}, @@ -19,8 +19,17 @@ "sha256:25816a9eef030c774beaee22189a24e29bc43f81cebe574ef723851eaf89ddee", "sha256:9651e1373873c75786101330e302e114f85b6e8b5ad70b491497c8b3609a8449" ], + "markers": "python_version >= '3.8'", "version": "==0.3.1" }, + "annotated-types": { + "hashes": [ + "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", + "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89" + ], + "markers": "python_version >= '3.8'", + "version": "==0.7.0" + }, "anyio": { "hashes": [ "sha256:44a3c9aba0f5defa43261a8b3efb97891f2bd7d804e0e1f56419befa1adfc780", @@ -32,11 +41,11 @@ }, "attrs": { "hashes": [ - "sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30", - "sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1" + "sha256:5cfb1b9148b5b086569baec03f20d7b6bf3bcacc9a42bebf87ffaaca362f6346", + "sha256:81921eb96de3191c8258c199618104dd27ac608d9366f5e35d011eae1867ede2" ], "markers": "python_version >= '3.7'", - "version": "==23.2.0" + "version": "==24.2.0" }, "click": { "hashes": [ @@ -48,19 +57,19 @@ }, "exceptiongroup": { "hashes": [ - "sha256:5258b9ed329c5bbdd31a309f53cbfb0b155341807f6ff7606a1e801a891b29ad", - "sha256:a4785e48b045528f5bfe627b6ad554ff32def154f42372786903b7abcfe1aa16" + "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b", + "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc" ], "markers": "python_version < '3.11'", - "version": "==1.2.1" + "version": "==1.2.2" }, "idna": { "hashes": [ - "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc", - "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0" + "sha256:050b4e5baadcd44d760cedbd2b8e639f2ff89bbc7a5730fcc662954303377aac", + "sha256:d838c2c0ed6fced7693d5e8ab8e734d5f8fda53a039c0164afb0b82e771e3603" ], - "markers": "python_version >= '3.5'", - "version": "==3.7" + "markers": "python_version >= '3.6'", + "version": "==3.8" }, "jsonschema": { "hashes": [ @@ -179,63 +188,129 @@ }, "packaging": { "hashes": [ - "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb", - "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522" + "sha256:2198ec20bd4c017b8f9717e00f0c8714076fc2fd93816750ab48e2c41de2cfd3", + "sha256:957e2148ba0e1a3b282772e791ef1d8083648bc131c8ab0c1feba110ce1146c3" ], "index": "pypi", - "markers": "python_version >= '3.6'", - "version": "==21.3" + "markers": "python_version >= '3.7'", + "version": "==22.0" + }, + "performance-metrics": { + "editable": true, + "file": "../performance-metrics" }, "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" + "sha256:c7a8a9fdf7d100afa49647eae340e2d23efa382466a8d177efcd1381e9be5598", + "sha256:f66a7073abd93214a20c5f7b32d56843137a7a2e70d02111f3be287035c45370" ], "index": "pypi", - "markers": "python_version >= '3.7'", - "version": "==1.10.12" + "markers": "python_version >= '3.8'", + "version": "==2.9.0" + }, + "pydantic-core": { + "hashes": [ + "sha256:0102e49ac7d2df3379ef8d658d3bc59d3d769b0bdb17da189b75efa861fc07b4", + "sha256:0123655fedacf035ab10c23450163c2f65a4174f2bb034b188240a6cf06bb123", + "sha256:043ef8469f72609c4c3a5e06a07a1f713d53df4d53112c6d49207c0bd3c3bd9b", + "sha256:0448b81c3dfcde439551bb04a9f41d7627f676b12701865c8a2574bcea034437", + "sha256:05b366fb8fe3d8683b11ac35fa08947d7b92be78ec64e3277d03bd7f9b7cda79", + "sha256:07049ec9306ec64e955b2e7c40c8d77dd78ea89adb97a2013d0b6e055c5ee4c5", + "sha256:084414ffe9a85a52940b49631321d636dadf3576c30259607b75516d131fecd0", + "sha256:086c5db95157dc84c63ff9d96ebb8856f47ce113c86b61065a066f8efbe80acf", + "sha256:12625e69b1199e94b0ae1c9a95d000484ce9f0182f9965a26572f054b1537e44", + "sha256:16b25a4a120a2bb7dab51b81e3d9f3cde4f9a4456566c403ed29ac81bf49744f", + "sha256:19f1352fe4b248cae22a89268720fc74e83f008057a652894f08fa931e77dced", + "sha256:1a2ab4f410f4b886de53b6bddf5dd6f337915a29dd9f22f20f3099659536b2f6", + "sha256:1c7b81beaf7c7ebde978377dc53679c6cba0e946426fc7ade54251dfe24a7604", + "sha256:1cf842265a3a820ebc6388b963ead065f5ce8f2068ac4e1c713ef77a67b71f7c", + "sha256:1eb37f7d6a8001c0f86dc8ff2ee8d08291a536d76e49e78cda8587bb54d8b329", + "sha256:23af245b8f2f4ee9e2c99cb3f93d0e22fb5c16df3f2f643f5a8da5caff12a653", + "sha256:257d6a410a0d8aeb50b4283dea39bb79b14303e0fab0f2b9d617701331ed1515", + "sha256:276ae78153a94b664e700ac362587c73b84399bd1145e135287513442e7dfbc7", + "sha256:2b1a195efd347ede8bcf723e932300292eb13a9d2a3c1f84eb8f37cbbc905b7f", + "sha256:329a721253c7e4cbd7aad4a377745fbcc0607f9d72a3cc2102dd40519be75ed2", + "sha256:358331e21a897151e54d58e08d0219acf98ebb14c567267a87e971f3d2a3be59", + "sha256:3649bd3ae6a8ebea7dc381afb7f3c6db237fc7cebd05c8ac36ca8a4187b03b30", + "sha256:3713dc093d5048bfaedbba7a8dbc53e74c44a140d45ede020dc347dda18daf3f", + "sha256:3ef71ec876fcc4d3bbf2ae81961959e8d62f8d74a83d116668409c224012e3af", + "sha256:41ae8537ad371ec018e3c5da0eb3f3e40ee1011eb9be1da7f965357c4623c501", + "sha256:4a801c5e1e13272e0909c520708122496647d1279d252c9e6e07dac216accc41", + "sha256:4c83c64d05ffbbe12d4e8498ab72bdb05bcc1026340a4a597dc647a13c1605ec", + "sha256:4cebb9794f67266d65e7e4cbe5dcf063e29fc7b81c79dc9475bd476d9534150e", + "sha256:5668b3173bb0b2e65020b60d83f5910a7224027232c9f5dc05a71a1deac9f960", + "sha256:56e6a12ec8d7679f41b3750ffa426d22b44ef97be226a9bab00a03365f217b2b", + "sha256:582871902e1902b3c8e9b2c347f32a792a07094110c1bca6c2ea89b90150caac", + "sha256:5c8aa40f6ca803f95b1c1c5aeaee6237b9e879e4dfb46ad713229a63651a95fb", + "sha256:5d813fd871b3d5c3005157622ee102e8908ad6011ec915a18bd8fde673c4360e", + "sha256:5dd0ec5f514ed40e49bf961d49cf1bc2c72e9b50f29a163b2cc9030c6742aa73", + "sha256:5f3cf3721eaf8741cffaf092487f1ca80831202ce91672776b02b875580e174a", + "sha256:6294907eaaccf71c076abdd1c7954e272efa39bb043161b4b8aa1cd76a16ce43", + "sha256:64d094ea1aa97c6ded4748d40886076a931a8bf6f61b6e43e4a1041769c39dd2", + "sha256:6650a7bbe17a2717167e3e23c186849bae5cef35d38949549f1c116031b2b3aa", + "sha256:67b6655311b00581914aba481729971b88bb8bc7996206590700a3ac85e457b8", + "sha256:6b06c5d4e8701ac2ba99a2ef835e4e1b187d41095a9c619c5b185c9068ed2a49", + "sha256:6ce883906810b4c3bd90e0ada1f9e808d9ecf1c5f0b60c6b8831d6100bcc7dd6", + "sha256:6db09153d8438425e98cdc9a289c5fade04a5d2128faff8f227c459da21b9703", + "sha256:6f80fba4af0cb1d2344869d56430e304a51396b70d46b91a55ed4959993c0589", + "sha256:743e5811b0c377eb830150d675b0847a74a44d4ad5ab8845923d5b3a756d8100", + "sha256:753294d42fb072aa1775bfe1a2ba1012427376718fa4c72de52005a3d2a22178", + "sha256:7568f682c06f10f30ef643a1e8eec4afeecdafde5c4af1b574c6df079e96f96c", + "sha256:7706e15cdbf42f8fab1e6425247dfa98f4a6f8c63746c995d6a2017f78e619ae", + "sha256:785e7f517ebb9890813d31cb5d328fa5eda825bb205065cde760b3150e4de1f7", + "sha256:7a05c0240f6c711eb381ac392de987ee974fa9336071fb697768dfdb151345ce", + "sha256:7ce7eaf9a98680b4312b7cebcdd9352531c43db00fca586115845df388f3c465", + "sha256:7ce8e26b86a91e305858e018afc7a6e932f17428b1eaa60154bd1f7ee888b5f8", + "sha256:7d0324a35ab436c9d768753cbc3c47a865a2cbc0757066cb864747baa61f6ece", + "sha256:7e9b24cca4037a561422bf5dc52b38d390fb61f7bfff64053ce1b72f6938e6b2", + "sha256:810ca06cca91de9107718dc83d9ac4d2e86efd6c02cba49a190abcaf33fb0472", + "sha256:820f6ee5c06bc868335e3b6e42d7ef41f50dfb3ea32fbd523ab679d10d8741c0", + "sha256:82764c0bd697159fe9947ad59b6db6d7329e88505c8f98990eb07e84cc0a5d81", + "sha256:8ae65fdfb8a841556b52935dfd4c3f79132dc5253b12c0061b96415208f4d622", + "sha256:8d5b0ff3218858859910295df6953d7bafac3a48d5cd18f4e3ed9999efd2245f", + "sha256:95d6bf449a1ac81de562d65d180af5d8c19672793c81877a2eda8fde5d08f2fd", + "sha256:964c7aa318da542cdcc60d4a648377ffe1a2ef0eb1e996026c7f74507b720a78", + "sha256:96ef39add33ff58cd4c112cbac076726b96b98bb8f1e7f7595288dcfb2f10b57", + "sha256:a6612c2a844043e4d10a8324c54cdff0042c558eef30bd705770793d70b224aa", + "sha256:a8031074a397a5925d06b590121f8339d34a5a74cfe6970f8a1124eb8b83f4ac", + "sha256:aab9e522efff3993a9e98ab14263d4e20211e62da088298089a03056980a3e69", + "sha256:ae579143826c6f05a361d9546446c432a165ecf1c0b720bbfd81152645cb897d", + "sha256:ae90b9e50fe1bd115b24785e962b51130340408156d34d67b5f8f3fa6540938e", + "sha256:b18cf68255a476b927910c6873d9ed00da692bb293c5b10b282bd48a0afe3ae2", + "sha256:b7efb12e5071ad8d5b547487bdad489fbd4a5a35a0fc36a1941517a6ad7f23e0", + "sha256:c4d9f15ffe68bcd3898b0ad7233af01b15c57d91cd1667f8d868e0eacbfe3f87", + "sha256:c53100c8ee5a1e102766abde2158077d8c374bee0639201f11d3032e3555dfbc", + "sha256:c57e493a0faea1e4c38f860d6862ba6832723396c884fbf938ff5e9b224200e2", + "sha256:c8319e0bd6a7b45ad76166cc3d5d6a36c97d0c82a196f478c3ee5346566eebfd", + "sha256:caffda619099cfd4f63d48462f6aadbecee3ad9603b4b88b60cb821c1b258576", + "sha256:cc0c316fba3ce72ac3ab7902a888b9dc4979162d320823679da270c2d9ad0cad", + "sha256:cdd02a08205dc90238669f082747612cb3c82bd2c717adc60f9b9ecadb540f80", + "sha256:d50ac34835c6a4a0d456b5db559b82047403c4317b3bc73b3455fefdbdc54b0a", + "sha256:d6b9dd6aa03c812017411734e496c44fef29b43dba1e3dd1fa7361bbacfc1354", + "sha256:da3131ef2b940b99106f29dfbc30d9505643f766704e14c5d5e504e6a480c35e", + "sha256:da43cbe593e3c87d07108d0ebd73771dc414488f1f91ed2e204b0370b94b37ac", + "sha256:dd59638025160056687d598b054b64a79183f8065eae0d3f5ca523cde9943940", + "sha256:e1895e949f8849bc2757c0dbac28422a04be031204df46a56ab34bcf98507342", + "sha256:e1a79ad49f346aa1a2921f31e8dbbab4d64484823e813a002679eaa46cba39e1", + "sha256:e460475719721d59cd54a350c1f71c797c763212c836bf48585478c5514d2854", + "sha256:e64ffaf8f6e17ca15eb48344d86a7a741454526f3a3fa56bc493ad9d7ec63936", + "sha256:e6e3ccebdbd6e53474b0bb7ab8b88e83c0cfe91484b25e058e581348ee5a01a5", + "sha256:e758d271ed0286d146cf7c04c539a5169a888dd0b57026be621547e756af55bc", + "sha256:f087879f1ffde024dd2788a30d55acd67959dcf6c431e9d3682d1c491a0eb474", + "sha256:f477d26183e94eaafc60b983ab25af2a809a1b48ce4debb57b343f671b7a90b6", + "sha256:fc535cb898ef88333cf317777ecdfe0faac1c2a3187ef7eb061b6f7ecf7e6bae" + ], + "markers": "python_version >= '3.8'", + "version": "==2.23.2" }, - "pyparsing": { + "pydantic-settings": { "hashes": [ - "sha256:a1bac0ce561155ecc3ed78ca94d3c9378656ad4c94c1270de543f621420f94ad", - "sha256:f9db75911801ed778fe61bb643079ff86601aca99fcae6345aa67292038fb742" + "sha256:bb6849dc067f1687574c12a639e231f3a6feeed0a12d710c1382045c5db1c315", + "sha256:ed81c3a0f46392b4d7c0a565c05884e6e54b3456e6f0fe4d8814981172dc9a88" ], - "markers": "python_full_version >= '3.6.8'", - "version": "==3.1.2" + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==2.4.0" }, "pyrsistent": { "hashes": [ @@ -290,6 +365,14 @@ "markers": "python_version >= '3.7'", "version": "==4.2.2" }, + "python-dotenv": { + "hashes": [ + "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca", + "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a" + ], + "markers": "python_version >= '3.8'", + "version": "==1.0.1" + }, "pyusb": { "hashes": [ "sha256:2b4c7cb86dbadf044dfb9d3a4ff69fd217013dbe78a792177a3feb172449ea36", @@ -301,11 +384,11 @@ }, "setuptools": { "hashes": [ - "sha256:937a48c7cdb7a21eb53cd7f9b59e525503aa8abaf3584c730dc5f7a5bec3a650", - "sha256:a58a8fde0541dab0419750bcc521fbdf8585f6e5cb41909df3a472ef7b81ca95" + "sha256:5f4c08aa4d3ebcb57a50c33b1b07e94315d7fc7230f7115e47fc99776c8ce308", + "sha256:95b40ed940a1c67eb70fc099094bd6e99c6ee7c23aa2306f4d2697ba7916f9c6" ], "markers": "python_version >= '3.8'", - "version": "==70.1.1" + "version": "==74.1.2" }, "sniffio": { "hashes": [ @@ -320,9 +403,17 @@ "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8" ], - "markers": "python_version >= '3.8'", + "markers": "python_version < '3.13'", "version": "==4.12.2" }, + "tzdata": { + "hashes": [ + "sha256:2674120f8d891909751c38abcdfd386ac0a5a1127954fbc332af6b5ceae07efd", + "sha256:9068bc196136463f5245e51efda838afa15aaeca9903f49050dfa2679db4d252" + ], + "markers": "python_version >= '3.9'", + "version": "==2024.1" + }, "wrapt": { "hashes": [ "sha256:0d2691979e93d06a95a26257adb7bfd0c93818e89b1406f5a28f36e0d8c1e1fc", @@ -419,19 +510,19 @@ }, "attrs": { "hashes": [ - "sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30", - "sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1" + "sha256:5cfb1b9148b5b086569baec03f20d7b6bf3bcacc9a42bebf87ffaaca362f6346", + "sha256:81921eb96de3191c8258c199618104dd27ac608d9366f5e35d011eae1867ede2" ], "markers": "python_version >= '3.7'", - "version": "==23.2.0" + "version": "==24.2.0" }, "babel": { "hashes": [ - "sha256:08706bdad8d0a3413266ab61bd6c34d0c28d6e1e7badf40a2cebe67644e2e1fb", - "sha256:8daf0e265d05768bc6c7a314cf1321e9a123afc328cc635c18622a2f30a04413" + "sha256:368b5b98b37c06b7daf6696391c3240c938b37767d4584413e8438c5c435fa8b", + "sha256:d1f3554ca26605fe173f3de0c65f750f5a42f924499bf134de6423582298e316" ], "markers": "python_version >= '3.8'", - "version": "==2.15.0" + "version": "==2.16.0" }, "backports.tarfile": { "hashes": [ @@ -473,11 +564,11 @@ }, "certifi": { "hashes": [ - "sha256:3cd43f1c6fa7dedc5899d69d3ad0398fd018ad1a17fba83ddaf78aa46c747516", - "sha256:ddc6c8ce995e6987e7faf5e3f1b02b302836a0e5d98ece18392cb1a36c72ad56" + "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8", + "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9" ], "markers": "python_version >= '3.6'", - "version": "==2024.6.2" + "version": "==2024.8.30" }, "charset-normalizer": { "hashes": [ @@ -730,11 +821,11 @@ }, "exceptiongroup": { "hashes": [ - "sha256:5258b9ed329c5bbdd31a309f53cbfb0b155341807f6ff7606a1e801a891b29ad", - "sha256:a4785e48b045528f5bfe627b6ad554ff32def154f42372786903b7abcfe1aa16" + "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b", + "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc" ], "markers": "python_version < '3.11'", - "version": "==1.2.1" + "version": "==1.2.2" }, "execnet": { "hashes": [ @@ -782,51 +873,51 @@ }, "fonttools": { "hashes": [ - "sha256:099634631b9dd271d4a835d2b2a9e042ccc94ecdf7e2dd9f7f34f7daf333358d", - "sha256:0c555e039d268445172b909b1b6bdcba42ada1cf4a60e367d68702e3f87e5f64", - "sha256:1e677bfb2b4bd0e5e99e0f7283e65e47a9814b0486cb64a41adf9ef110e078f2", - "sha256:2367d47816cc9783a28645bc1dac07f8ffc93e0f015e8c9fc674a5b76a6da6e4", - "sha256:28d072169fe8275fb1a0d35e3233f6df36a7e8474e56cb790a7258ad822b6fd6", - "sha256:31f0e3147375002aae30696dd1dc596636abbd22fca09d2e730ecde0baad1d6b", - "sha256:3e0ad3c6ea4bd6a289d958a1eb922767233f00982cf0fe42b177657c86c80a8f", - "sha256:45b4afb069039f0366a43a5d454bc54eea942bfb66b3fc3e9a2c07ef4d617380", - "sha256:4a2a6ba400d386e904fd05db81f73bee0008af37799a7586deaa4aef8cd5971e", - "sha256:4f520d9ac5b938e6494f58a25c77564beca7d0199ecf726e1bd3d56872c59749", - "sha256:52a6e0a7a0bf611c19bc8ec8f7592bdae79c8296c70eb05917fd831354699b20", - "sha256:5a4788036201c908079e89ae3f5399b33bf45b9ea4514913f4dbbe4fac08efe0", - "sha256:6b4f04b1fbc01a3569d63359f2227c89ab294550de277fd09d8fca6185669fa4", - "sha256:715b41c3e231f7334cbe79dfc698213dcb7211520ec7a3bc2ba20c8515e8a3b5", - "sha256:73121a9b7ff93ada888aaee3985a88495489cc027894458cb1a736660bdfb206", - "sha256:74ae2441731a05b44d5988d3ac2cf784d3ee0a535dbed257cbfff4be8bb49eb9", - "sha256:7d6166192dcd925c78a91d599b48960e0a46fe565391c79fe6de481ac44d20ac", - "sha256:7f193f060391a455920d61684a70017ef5284ccbe6023bb056e15e5ac3de11d1", - "sha256:907fa0b662dd8fc1d7c661b90782ce81afb510fc4b7aa6ae7304d6c094b27bce", - "sha256:93156dd7f90ae0a1b0e8871032a07ef3178f553f0c70c386025a808f3a63b1f4", - "sha256:93bc9e5aaa06ff928d751dc6be889ff3e7d2aa393ab873bc7f6396a99f6fbb12", - "sha256:95db0c6581a54b47c30860d013977b8a14febc206c8b5ff562f9fe32738a8aca", - "sha256:973d030180eca8255b1bce6ffc09ef38a05dcec0e8320cc9b7bcaa65346f341d", - "sha256:9cd7a6beec6495d1dffb1033d50a3f82dfece23e9eb3c20cd3c2444d27514068", - "sha256:9fe9096a60113e1d755e9e6bda15ef7e03391ee0554d22829aa506cdf946f796", - "sha256:a209d2e624ba492df4f3bfad5996d1f76f03069c6133c60cd04f9a9e715595ec", - "sha256:a239afa1126b6a619130909c8404070e2b473dd2b7fc4aacacd2e763f8597fea", - "sha256:ba9f09ff17f947392a855e3455a846f9855f6cf6bec33e9a427d3c1d254c712f", - "sha256:bb7273789f69b565d88e97e9e1da602b4ee7ba733caf35a6c2affd4334d4f005", - "sha256:bd5bc124fae781a4422f61b98d1d7faa47985f663a64770b78f13d2c072410c2", - "sha256:bff98816cb144fb7b85e4b5ba3888a33b56ecef075b0e95b95bcd0a5fbf20f06", - "sha256:c4ee5a24e281fbd8261c6ab29faa7fd9a87a12e8c0eed485b705236c65999109", - "sha256:c93ed66d32de1559b6fc348838c7572d5c0ac1e4a258e76763a5caddd8944002", - "sha256:d1a24f51a3305362b94681120c508758a88f207fa0a681c16b5a4172e9e6c7a9", - "sha256:d8f191a17369bd53a5557a5ee4bab91d5330ca3aefcdf17fab9a497b0e7cff7a", - "sha256:daaef7390e632283051e3cf3e16aff2b68b247e99aea916f64e578c0449c9c68", - "sha256:e40013572bfb843d6794a3ce076c29ef4efd15937ab833f520117f8eccc84fd6", - "sha256:eceef49f457253000e6a2d0f7bd08ff4e9fe96ec4ffce2dbcb32e34d9c1b8161", - "sha256:ee595d7ba9bba130b2bec555a40aafa60c26ce68ed0cf509983e0f12d88674fd", - "sha256:ef50ec31649fbc3acf6afd261ed89d09eb909b97cc289d80476166df8438524d", - "sha256:fa1f3e34373aa16045484b4d9d352d4c6b5f9f77ac77a178252ccbc851e8b2ee", - "sha256:fca66d9ff2ac89b03f5aa17e0b21a97c21f3491c46b583bb131eb32c7bab33af" + "sha256:02569e9a810f9d11f4ae82c391ebc6fb5730d95a0657d24d754ed7763fb2d122", + "sha256:0679a30b59d74b6242909945429dbddb08496935b82f91ea9bf6ad240ec23397", + "sha256:10f5e6c3510b79ea27bb1ebfcc67048cde9ec67afa87c7dd7efa5c700491ac7f", + "sha256:2af40ae9cdcb204fc1d8f26b190aa16534fcd4f0df756268df674a270eab575d", + "sha256:32f029c095ad66c425b0ee85553d0dc326d45d7059dbc227330fc29b43e8ba60", + "sha256:35250099b0cfb32d799fb5d6c651220a642fe2e3c7d2560490e6f1d3f9ae9169", + "sha256:3b3c8ebafbee8d9002bd8f1195d09ed2bd9ff134ddec37ee8f6a6375e6a4f0e8", + "sha256:4824c198f714ab5559c5be10fd1adf876712aa7989882a4ec887bf1ef3e00e31", + "sha256:5ff7e5e9bad94e3a70c5cd2fa27f20b9bb9385e10cddab567b85ce5d306ea923", + "sha256:651390c3b26b0c7d1f4407cad281ee7a5a85a31a110cbac5269de72a51551ba2", + "sha256:6e08f572625a1ee682115223eabebc4c6a2035a6917eac6f60350aba297ccadb", + "sha256:6ed170b5e17da0264b9f6fae86073be3db15fa1bd74061c8331022bca6d09bab", + "sha256:73379d3ffdeecb376640cd8ed03e9d2d0e568c9d1a4e9b16504a834ebadc2dfb", + "sha256:75a157d8d26c06e64ace9df037ee93a4938a4606a38cb7ffaf6635e60e253b7a", + "sha256:791b31ebbc05197d7aa096bbc7bd76d591f05905d2fd908bf103af4488e60670", + "sha256:7b6b35e52ddc8fb0db562133894e6ef5b4e54e1283dff606fda3eed938c36fc8", + "sha256:84ec3fb43befb54be490147b4a922b5314e16372a643004f182babee9f9c3407", + "sha256:8959a59de5af6d2bec27489e98ef25a397cfa1774b375d5787509c06659b3671", + "sha256:9dfdae43b7996af46ff9da520998a32b105c7f098aeea06b2226b30e74fbba88", + "sha256:9e6ceba2a01b448e36754983d376064730690401da1dd104ddb543519470a15f", + "sha256:9efd176f874cb6402e607e4cc9b4a9cd584d82fc34a4b0c811970b32ba62501f", + "sha256:a1c7c5aa18dd3b17995898b4a9b5929d69ef6ae2af5b96d585ff4005033d82f0", + "sha256:aae7bd54187e8bf7fd69f8ab87b2885253d3575163ad4d669a262fe97f0136cb", + "sha256:b21952c092ffd827504de7e66b62aba26fdb5f9d1e435c52477e6486e9d128b2", + "sha256:b96cd370a61f4d083c9c0053bf634279b094308d52fdc2dd9a22d8372fdd590d", + "sha256:becc5d7cb89c7b7afa8321b6bb3dbee0eec2b57855c90b3e9bf5fb816671fa7c", + "sha256:bee32ea8765e859670c4447b0817514ca79054463b6b79784b08a8df3a4d78e3", + "sha256:c6e7170d675d12eac12ad1a981d90f118c06cf680b42a2d74c6c931e54b50719", + "sha256:c818c058404eb2bba05e728d38049438afd649e3c409796723dfc17cd3f08749", + "sha256:c8696544c964500aa9439efb6761947393b70b17ef4e82d73277413f291260a4", + "sha256:c9cd19cf4fe0595ebdd1d4915882b9440c3a6d30b008f3cc7587c1da7b95be5f", + "sha256:d4d0096cb1ac7a77b3b41cd78c9b6bc4a400550e21dc7a92f2b5ab53ed74eb02", + "sha256:d92d3c2a1b39631a6131c2fa25b5406855f97969b068e7e08413325bc0afba58", + "sha256:da33440b1413bad53a8674393c5d29ce64d8c1a15ef8a77c642ffd900d07bfe1", + "sha256:e013aae589c1c12505da64a7d8d023e584987e51e62006e1bb30d72f26522c41", + "sha256:e128778a8e9bc11159ce5447f76766cefbd876f44bd79aff030287254e4752c4", + "sha256:e54f1bba2f655924c1138bbc7fa91abd61f45c68bd65ab5ed985942712864bbb", + "sha256:e5b708073ea3d684235648786f5f6153a48dc8762cdfe5563c57e80787c29fbb", + "sha256:e8bf06b94694251861ba7fdeea15c8ec0967f84c3d4143ae9daf42bbc7717fe3", + "sha256:f08df60fbd8d289152079a65da4e66a447efc1d5d5a4d3f299cdd39e3b2e4a7d", + "sha256:f1f8758a2ad110bd6432203a344269f445a2907dc24ef6bccfd0ac4e14e0d71d", + "sha256:f677ce218976496a587ab17140da141557beb91d2a5c1a14212c994093f2eae2" ], "markers": "python_version >= '3.8'", - "version": "==4.53.0" + "version": "==4.53.1" }, "gprof2dot": { "hashes": [ @@ -847,11 +938,11 @@ }, "idna": { "hashes": [ - "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc", - "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0" + "sha256:050b4e5baadcd44d760cedbd2b8e639f2ff89bbc7a5730fcc662954303377aac", + "sha256:d838c2c0ed6fced7693d5e8ab8e734d5f8fda53a039c0164afb0b82e771e3603" ], - "markers": "python_version >= '3.5'", - "version": "==3.7" + "markers": "python_version >= '3.6'", + "version": "==3.8" }, "imagesize": { "hashes": [ @@ -863,11 +954,11 @@ }, "importlib-metadata": { "hashes": [ - "sha256:509ecb2ab77071db5137c655e24ceb3eee66e7bbc6574165d0d114d9fc4bbe68", - "sha256:ffef94b0b66046dd8ea2d619b701fe978d9264d38f3998bc4c27ec3b146a87c8" + "sha256:66f342cc6ac9818fc6ff340576acd24d65ba0b3efabb2b4ac08b598965a4a2f1", + "sha256:9a547d3bc3608b025f93d403fdd1aae741c24fbb8314df4b155675742ce303c5" ], "markers": "python_version >= '3.8'", - "version": "==7.2.1" + "version": "==8.4.0" }, "iniconfig": { "hashes": [ @@ -887,19 +978,19 @@ }, "jaraco.context": { "hashes": [ - "sha256:3e16388f7da43d384a1a7cd3452e72e14732ac9fe459678773a3608a812bf266", - "sha256:c2f67165ce1f9be20f32f650f25d8edfc1646a8aeee48ae06fb35f90763576d2" + "sha256:9bae4ea555cf0b14938dc0aee7c9f32ed303aa20a3b73e7dc80111628792d1b3", + "sha256:f797fc481b490edb305122c9181830a3a5b76d84ef6d1aef2fb9b47ab956f9e4" ], "markers": "python_version >= '3.8'", - "version": "==5.3.0" + "version": "==6.0.1" }, "jaraco.functools": { "hashes": [ - "sha256:3b24ccb921d6b593bdceb56ce14799204f473976e2a9d4b15b04d0f2c2326664", - "sha256:d33fa765374c0611b52f8b3a795f8900869aa88c84769d4d1746cd68fb28c3e8" + "sha256:3460c74cd0d32bf82b9576bbb3527c4364d5b27a21f5158a62aed6c4b42e23f5", + "sha256:c9d16a3ed4ccb5a889ad8e0b7a343401ee5b2a71cee6ed192d3f68bc351e94e3" ], "markers": "python_version >= '3.8'", - "version": "==4.0.1" + "version": "==4.0.2" }, "jinja2": { "hashes": [ @@ -910,132 +1001,133 @@ "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:2458681cdefc0dbc0b7eb6cf75d0b98e59f9ad9b2d4edd319d18f68bdca95e50", - "sha256:daaffd42dbda25ddafb1ad5fec4024e5bbcfe424597ca1ca452b299861e49f1b" + "sha256:8d85a1ea5d6db8515b59e1c5d1d1678b03cf7fc8b8dcfb1651e8c4a524eb42ef", + "sha256:8d963da00ccdf06e356acd9bf3b743208878751032d8599c6cc89eb51310ffae" ], "markers": "python_version >= '3.8'", - "version": "==25.2.1" + "version": "==25.3.0" }, "kiwisolver": { "hashes": [ - "sha256:00bd361b903dc4bbf4eb165f24d1acbee754fce22ded24c3d56eec268658a5cf", - "sha256:040c1aebeda72197ef477a906782b5ab0d387642e93bda547336b8957c61022e", - "sha256:05703cf211d585109fcd72207a31bb170a0f22144d68298dc5e61b3c946518af", - "sha256:06f54715b7737c2fecdbf140d1afb11a33d59508a47bf11bb38ecf21dc9ab79f", - "sha256:0dc9db8e79f0036e8173c466d21ef18e1befc02de8bf8aa8dc0813a6dc8a7046", - "sha256:0f114aa76dc1b8f636d077979c0ac22e7cd8f3493abbab152f20eb8d3cda71f3", - "sha256:11863aa14a51fd6ec28688d76f1735f8f69ab1fabf388851a595d0721af042f5", - "sha256:11c7de8f692fc99816e8ac50d1d1aef4f75126eefc33ac79aac02c099fd3db71", - "sha256:11d011a7574eb3b82bcc9c1a1d35c1d7075677fdd15de527d91b46bd35e935ee", - "sha256:146d14bebb7f1dc4d5fbf74f8a6cb15ac42baadee8912eb84ac0b3b2a3dc6ac3", - "sha256:15568384086b6df3c65353820a4473575dbad192e35010f622c6ce3eebd57af9", - "sha256:19df6e621f6d8b4b9c4d45f40a66839294ff2bb235e64d2178f7522d9170ac5b", - "sha256:1b04139c4236a0f3aff534479b58f6f849a8b351e1314826c2d230849ed48985", - "sha256:210ef2c3a1f03272649aff1ef992df2e724748918c4bc2d5a90352849eb40bea", - "sha256:2270953c0d8cdab5d422bee7d2007f043473f9d2999631c86a223c9db56cbd16", - "sha256:2400873bccc260b6ae184b2b8a4fec0e4082d30648eadb7c3d9a13405d861e89", - "sha256:2a40773c71d7ccdd3798f6489aaac9eee213d566850a9533f8d26332d626b82c", - "sha256:2c5674c4e74d939b9d91dda0fae10597ac7521768fec9e399c70a1f27e2ea2d9", - "sha256:3195782b26fc03aa9c6913d5bad5aeb864bdc372924c093b0f1cebad603dd712", - "sha256:31a82d498054cac9f6d0b53d02bb85811185bcb477d4b60144f915f3b3126342", - "sha256:32d5cf40c4f7c7b3ca500f8985eb3fb3a7dfc023215e876f207956b5ea26632a", - "sha256:346f5343b9e3f00b8db8ba359350eb124b98c99efd0b408728ac6ebf38173958", - "sha256:378a214a1e3bbf5ac4a8708304318b4f890da88c9e6a07699c4ae7174c09a68d", - "sha256:39b42c68602539407884cf70d6a480a469b93b81b7701378ba5e2328660c847a", - "sha256:3a2b053a0ab7a3960c98725cfb0bf5b48ba82f64ec95fe06f1d06c99b552e130", - "sha256:3aba7311af82e335dd1e36ffff68aaca609ca6290c2cb6d821a39aa075d8e3ff", - "sha256:3cd32d6c13807e5c66a7cbb79f90b553642f296ae4518a60d8d76243b0ad2898", - "sha256:3edd2fa14e68c9be82c5b16689e8d63d89fe927e56debd6e1dbce7a26a17f81b", - "sha256:4c380469bd3f970ef677bf2bcba2b6b0b4d5c75e7a020fb863ef75084efad66f", - "sha256:4e66e81a5779b65ac21764c295087de82235597a2293d18d943f8e9e32746265", - "sha256:53abb58632235cd154176ced1ae8f0d29a6657aa1aa9decf50b899b755bc2b93", - "sha256:5794cf59533bc3f1b1c821f7206a3617999db9fbefc345360aafe2e067514929", - "sha256:59415f46a37f7f2efeec758353dd2eae1b07640d8ca0f0c42548ec4125492635", - "sha256:59ec7b7c7e1a61061850d53aaf8e93db63dce0c936db1fda2658b70e4a1be709", - "sha256:59edc41b24031bc25108e210c0def6f6c2191210492a972d585a06ff246bb79b", - "sha256:5a580c91d686376f0f7c295357595c5a026e6cbc3d77b7c36e290201e7c11ecb", - "sha256:5b94529f9b2591b7af5f3e0e730a4e0a41ea174af35a4fd067775f9bdfeee01a", - "sha256:5c7b3b3a728dc6faf3fc372ef24f21d1e3cee2ac3e9596691d746e5a536de920", - "sha256:5c90ae8c8d32e472be041e76f9d2f2dbff4d0b0be8bd4041770eddb18cf49a4e", - "sha256:5e7139af55d1688f8b960ee9ad5adafc4ac17c1c473fe07133ac092310d76544", - "sha256:5ff5cf3571589b6d13bfbfd6bcd7a3f659e42f96b5fd1c4830c4cf21d4f5ef45", - "sha256:620ced262a86244e2be10a676b646f29c34537d0d9cc8eb26c08f53d98013390", - "sha256:6512cb89e334e4700febbffaaa52761b65b4f5a3cf33f960213d5656cea36a77", - "sha256:6c08e1312a9cf1074d17b17728d3dfce2a5125b2d791527f33ffbe805200a355", - "sha256:6c3bd3cde54cafb87d74d8db50b909705c62b17c2099b8f2e25b461882e544ff", - "sha256:6ef7afcd2d281494c0a9101d5c571970708ad911d028137cd558f02b851c08b4", - "sha256:7269d9e5f1084a653d575c7ec012ff57f0c042258bf5db0954bf551c158466e7", - "sha256:72d40b33e834371fd330fb1472ca19d9b8327acb79a5821d4008391db8e29f20", - "sha256:74d1b44c6cfc897df648cc9fdaa09bc3e7679926e6f96df05775d4fb3946571c", - "sha256:74db36e14a7d1ce0986fa104f7d5637aea5c82ca6326ed0ec5694280942d1162", - "sha256:763773d53f07244148ccac5b084da5adb90bfaee39c197554f01b286cf869228", - "sha256:76c6a5964640638cdeaa0c359382e5703e9293030fe730018ca06bc2010c4437", - "sha256:76d9289ed3f7501012e05abb8358bbb129149dbd173f1f57a1bf1c22d19ab7cc", - "sha256:7931d8f1f67c4be9ba1dd9c451fb0eeca1a25b89e4d3f89e828fe12a519b782a", - "sha256:7b8b454bac16428b22560d0a1cf0a09875339cab69df61d7805bf48919415901", - "sha256:7e5bab140c309cb3a6ce373a9e71eb7e4873c70c2dda01df6820474f9889d6d4", - "sha256:83d78376d0d4fd884e2c114d0621624b73d2aba4e2788182d286309ebdeed770", - "sha256:852542f9481f4a62dbb5dd99e8ab7aedfeb8fb6342349a181d4036877410f525", - "sha256:85267bd1aa8880a9c88a8cb71e18d3d64d2751a790e6ca6c27b8ccc724bcd5ad", - "sha256:88a2df29d4724b9237fc0c6eaf2a1adae0cdc0b3e9f4d8e7dc54b16812d2d81a", - "sha256:88b9f257ca61b838b6f8094a62418421f87ac2a1069f7e896c36a7d86b5d4c29", - "sha256:8ab3919a9997ab7ef2fbbed0cc99bb28d3c13e6d4b1ad36e97e482558a91be90", - "sha256:92dea1ffe3714fa8eb6a314d2b3c773208d865a0e0d35e713ec54eea08a66250", - "sha256:9407b6a5f0d675e8a827ad8742e1d6b49d9c1a1da5d952a67d50ef5f4170b18d", - "sha256:9408acf3270c4b6baad483865191e3e582b638b1654a007c62e3efe96f09a9a3", - "sha256:955e8513d07a283056b1396e9a57ceddbd272d9252c14f154d450d227606eb54", - "sha256:9db8ea4c388fdb0f780fe91346fd438657ea602d58348753d9fb265ce1bca67f", - "sha256:9eaa8b117dc8337728e834b9c6e2611f10c79e38f65157c4c38e9400286f5cb1", - "sha256:a51a263952b1429e429ff236d2f5a21c5125437861baeed77f5e1cc2d2c7c6da", - "sha256:a6aa6315319a052b4ee378aa171959c898a6183f15c1e541821c5c59beaa0238", - "sha256:aa12042de0171fad672b6c59df69106d20d5596e4f87b5e8f76df757a7c399aa", - "sha256:aaf7be1207676ac608a50cd08f102f6742dbfc70e8d60c4db1c6897f62f71523", - "sha256:b0157420efcb803e71d1b28e2c287518b8808b7cf1ab8af36718fd0a2c453eb0", - "sha256:b3f7e75f3015df442238cca659f8baa5f42ce2a8582727981cbfa15fee0ee205", - "sha256:b9098e0049e88c6a24ff64545cdfc50807818ba6c1b739cae221bbbcbc58aad3", - "sha256:ba55dce0a9b8ff59495ddd050a0225d58bd0983d09f87cfe2b6aec4f2c1234e4", - "sha256:bb86433b1cfe686da83ce32a9d3a8dd308e85c76b60896d58f082136f10bffac", - "sha256:bbea0db94288e29afcc4c28afbf3a7ccaf2d7e027489c449cf7e8f83c6346eb9", - "sha256:bbf1d63eef84b2e8c89011b7f2235b1e0bf7dacc11cac9431fc6468e99ac77fb", - "sha256:c7940c1dc63eb37a67721b10d703247552416f719c4188c54e04334321351ced", - "sha256:c9bf3325c47b11b2e51bca0824ea217c7cd84491d8ac4eefd1e409705ef092bd", - "sha256:cdc8a402aaee9a798b50d8b827d7ecf75edc5fb35ea0f91f213ff927c15f4ff0", - "sha256:ceec1a6bc6cab1d6ff5d06592a91a692f90ec7505d6463a88a52cc0eb58545da", - "sha256:cfe6ab8da05c01ba6fbea630377b5da2cd9bcbc6338510116b01c1bc939a2c18", - "sha256:d099e745a512f7e3bbe7249ca835f4d357c586d78d79ae8f1dcd4d8adeb9bda9", - "sha256:d0ef46024e6a3d79c01ff13801cb19d0cad7fd859b15037aec74315540acc276", - "sha256:d2e5a98f0ec99beb3c10e13b387f8db39106d53993f498b295f0c914328b1333", - "sha256:da4cfb373035def307905d05041c1d06d8936452fe89d464743ae7fb8371078b", - "sha256:da802a19d6e15dffe4b0c24b38b3af68e6c1a68e6e1d8f30148c83864f3881db", - "sha256:dced8146011d2bc2e883f9bd68618b8247387f4bbec46d7392b3c3b032640126", - "sha256:dfdd7c0b105af050eb3d64997809dc21da247cf44e63dc73ff0fd20b96be55a9", - "sha256:e368f200bbc2e4f905b8e71eb38b3c04333bddaa6a2464a6355487b02bb7fb09", - "sha256:e391b1f0a8a5a10ab3b9bb6afcfd74f2175f24f8975fb87ecae700d1503cdee0", - "sha256:e57e563a57fb22a142da34f38acc2fc1a5c864bc29ca1517a88abc963e60d6ec", - "sha256:e5d706eba36b4c4d5bc6c6377bb6568098765e990cfc21ee16d13963fab7b3e7", - "sha256:ec20916e7b4cbfb1f12380e46486ec4bcbaa91a9c448b97023fde0d5bbf9e4ff", - "sha256:f1d072c2eb0ad60d4c183f3fb44ac6f73fb7a8f16a2694a91f988275cbf352f9", - "sha256:f846c260f483d1fd217fe5ed7c173fb109efa6b1fc8381c8b7552c5781756192", - "sha256:f91de7223d4c7b793867797bacd1ee53bfe7359bd70d27b7b58a04efbb9436c8", - "sha256:faae4860798c31530dd184046a900e652c95513796ef51a12bc086710c2eec4d", - "sha256:fc579bf0f502e54926519451b920e875f433aceb4624a3646b3252b5caa9e0b6", - "sha256:fcc700eadbbccbf6bc1bcb9dbe0786b4b1cb91ca0dcda336eef5c2beed37b797", - "sha256:fd32ea360bcbb92d28933fc05ed09bffcb1704ba3fc7942e81db0fd4f81a7892", - "sha256:fdb7adb641a0d13bdcd4ef48e062363d8a9ad4a182ac7647ec88f695e719ae9f" + "sha256:073a36c8273647592ea332e816e75ef8da5c303236ec0167196793eb1e34657a", + "sha256:08471d4d86cbaec61f86b217dd938a83d85e03785f51121e791a6e6689a3be95", + "sha256:0c18ec74c0472de033e1bebb2911c3c310eef5649133dd0bedf2a169a1b269e5", + "sha256:0c6c43471bc764fad4bc99c5c2d6d16a676b1abf844ca7c8702bdae92df01ee0", + "sha256:10849fb2c1ecbfae45a693c070e0320a91b35dd4bcf58172c023b994283a124d", + "sha256:18077b53dc3bb490e330669a99920c5e6a496889ae8c63b58fbc57c3d7f33a18", + "sha256:18e0cca3e008e17fe9b164b55735a325140a5a35faad8de92dd80265cd5eb80b", + "sha256:22f499f6157236c19f4bbbd472fa55b063db77a16cd74d49afe28992dff8c258", + "sha256:2a8781ac3edc42ea4b90bc23e7d37b665d89423818e26eb6df90698aa2287c95", + "sha256:2e6039dcbe79a8e0f044f1c39db1986a1b8071051efba3ee4d74f5b365f5226e", + "sha256:34ea1de54beef1c104422d210c47c7d2a4999bdecf42c7b5718fbe59a4cac383", + "sha256:3ab58c12a2cd0fc769089e6d38466c46d7f76aced0a1f54c77652446733d2d02", + "sha256:3abc5b19d24af4b77d1598a585b8a719beb8569a71568b66f4ebe1fb0449460b", + "sha256:3bf1ed55088f214ba6427484c59553123fdd9b218a42bbc8c6496d6754b1e523", + "sha256:3ce6b2b0231bda412463e152fc18335ba32faf4e8c23a754ad50ffa70e4091ee", + "sha256:3da53da805b71e41053dc670f9a820d1157aae77b6b944e08024d17bcd51ef88", + "sha256:3f9362ecfca44c863569d3d3c033dbe8ba452ff8eed6f6b5806382741a1334bd", + "sha256:409afdfe1e2e90e6ee7fc896f3df9a7fec8e793e58bfa0d052c8a82f99c37abb", + "sha256:40fa14dbd66b8b8f470d5fc79c089a66185619d31645f9b0773b88b19f7223c4", + "sha256:4322872d5772cae7369f8351da1edf255a604ea7087fe295411397d0cfd9655e", + "sha256:44756f9fd339de0fb6ee4f8c1696cfd19b2422e0d70b4cefc1cc7f1f64045a8c", + "sha256:46707a10836894b559e04b0fd143e343945c97fd170d69a2d26d640b4e297935", + "sha256:48b571ecd8bae15702e4f22d3ff6a0f13e54d3d00cd25216d5e7f658242065ee", + "sha256:48be928f59a1f5c8207154f935334d374e79f2b5d212826307d072595ad76a2e", + "sha256:4bfa75a048c056a411f9705856abfc872558e33c055d80af6a380e3658766038", + "sha256:4c00336b9dd5ad96d0a558fd18a8b6f711b7449acce4c157e7343ba92dd0cf3d", + "sha256:4c26ed10c4f6fa6ddb329a5120ba3b6db349ca192ae211e882970bfc9d91420b", + "sha256:4d05d81ecb47d11e7f8932bd8b61b720bf0b41199358f3f5e36d38e28f0532c5", + "sha256:4e77f2126c3e0b0d055f44513ed349038ac180371ed9b52fe96a32aa071a5107", + "sha256:5337ec7809bcd0f424c6b705ecf97941c46279cf5ed92311782c7c9c2026f07f", + "sha256:5360cc32706dab3931f738d3079652d20982511f7c0ac5711483e6eab08efff2", + "sha256:58370b1ffbd35407444d57057b57da5d6549d2d854fa30249771775c63b5fe17", + "sha256:58cb20602b18f86f83a5c87d3ee1c766a79c0d452f8def86d925e6c60fbf7bfb", + "sha256:599b5c873c63a1f6ed7eead644a8a380cfbdf5db91dcb6f85707aaab213b1674", + "sha256:5b7dfa3b546da08a9f622bb6becdb14b3e24aaa30adba66749d38f3cc7ea9706", + "sha256:5b9c3f4ee0b9a439d2415012bd1b1cc2df59e4d6a9939f4d669241d30b414327", + "sha256:5d34eb8494bea691a1a450141ebb5385e4b69d38bb8403b5146ad279f4b30fa3", + "sha256:5d5abf8f8ec1f4e22882273c423e16cae834c36856cac348cfbfa68e01c40f3a", + "sha256:5e3bc157fed2a4c02ec468de4ecd12a6e22818d4f09cde2c31ee3226ffbefab2", + "sha256:612a10bdae23404a72941a0fc8fa2660c6ea1217c4ce0dbcab8a8f6543ea9e7f", + "sha256:657a05857bda581c3656bfc3b20e353c232e9193eb167766ad2dc58b56504948", + "sha256:65e720d2ab2b53f1f72fb5da5fb477455905ce2c88aaa671ff0a447c2c80e8e3", + "sha256:693902d433cf585133699972b6d7c42a8b9f8f826ebcaf0132ff55200afc599e", + "sha256:6af936f79086a89b3680a280c47ea90b4df7047b5bdf3aa5c524bbedddb9e545", + "sha256:71bb308552200fb2c195e35ef05de12f0c878c07fc91c270eb3d6e41698c3bcc", + "sha256:764202cc7e70f767dab49e8df52c7455e8de0df5d858fa801a11aa0d882ccf3f", + "sha256:76c8094ac20ec259471ac53e774623eb62e6e1f56cd8690c67ce6ce4fcb05650", + "sha256:78a42513018c41c2ffd262eb676442315cbfe3c44eed82385c2ed043bc63210a", + "sha256:79849239c39b5e1fd906556c474d9b0439ea6792b637511f3fe3a41158d89ca8", + "sha256:7ab9ccab2b5bd5702ab0803676a580fffa2aa178c2badc5557a84cc943fcf750", + "sha256:7bbfcb7165ce3d54a3dfbe731e470f65739c4c1f85bb1018ee912bae139e263b", + "sha256:7c06a4c7cf15ec739ce0e5971b26c93638730090add60e183530d70848ebdd34", + "sha256:801fa7802e5cfabe3ab0c81a34c323a319b097dfb5004be950482d882f3d7225", + "sha256:803b8e1459341c1bb56d1c5c010406d5edec8a0713a0945851290a7930679b51", + "sha256:82a5c2f4b87c26bb1a0ef3d16b5c4753434633b83d365cc0ddf2770c93829e3c", + "sha256:84ec80df401cfee1457063732d90022f93951944b5b58975d34ab56bb150dfb3", + "sha256:8705f17dfeb43139a692298cb6637ee2e59c0194538153e83e9ee0c75c2eddde", + "sha256:88a9ca9c710d598fd75ee5de59d5bda2684d9db36a9f50b6125eaea3969c2599", + "sha256:88f17c5ffa8e9462fb79f62746428dd57b46eb931698e42e990ad63103f35e6c", + "sha256:8a3ec5aa8e38fc4c8af308917ce12c536f1c88452ce554027e55b22cbbfbff76", + "sha256:8a9c83f75223d5e48b0bc9cb1bf2776cf01563e00ade8775ffe13b0b6e1af3a6", + "sha256:8b01aac285f91ca889c800042c35ad3b239e704b150cfd3382adfc9dcc780e39", + "sha256:8d53103597a252fb3ab8b5845af04c7a26d5e7ea8122303dd7a021176a87e8b9", + "sha256:8e045731a5416357638d1700927529e2b8ab304811671f665b225f8bf8d8f933", + "sha256:8f0ea6da6d393d8b2e187e6a5e3fb81f5862010a40c3945e2c6d12ae45cfb2ad", + "sha256:90da3b5f694b85231cf93586dad5e90e2d71b9428f9aad96952c99055582f520", + "sha256:913983ad2deb14e66d83c28b632fd35ba2b825031f2fa4ca29675e665dfecbe1", + "sha256:9242795d174daa40105c1d86aba618e8eab7bf96ba8c3ee614da8302a9f95503", + "sha256:929e294c1ac1e9f615c62a4e4313ca1823ba37326c164ec720a803287c4c499b", + "sha256:933d4de052939d90afbe6e9d5273ae05fb836cc86c15b686edd4b3560cc0ee36", + "sha256:942216596dc64ddb25adb215c3c783215b23626f8d84e8eff8d6d45c3f29f75a", + "sha256:94252291e3fe68001b1dd747b4c0b3be12582839b95ad4d1b641924d68fd4643", + "sha256:9893ff81bd7107f7b685d3017cc6583daadb4fc26e4a888350df530e41980a60", + "sha256:9e838bba3a3bac0fe06d849d29772eb1afb9745a59710762e4ba3f4cb8424483", + "sha256:a0f64a48bb81af7450e641e3fe0b0394d7381e342805479178b3d335d60ca7cf", + "sha256:a17f6a29cf8935e587cc8a4dbfc8368c55edc645283db0ce9801016f83526c2d", + "sha256:a1ecf0ac1c518487d9d23b1cd7139a6a65bc460cd101ab01f1be82ecf09794b6", + "sha256:a79ae34384df2b615eefca647a2873842ac3b596418032bef9a7283675962644", + "sha256:a91b5f9f1205845d488c928e8570dcb62b893372f63b8b6e98b863ebd2368ff2", + "sha256:aa0abdf853e09aff551db11fce173e2177d00786c688203f52c87ad7fcd91ef9", + "sha256:ac542bf38a8a4be2dc6b15248d36315ccc65f0743f7b1a76688ffb6b5129a5c2", + "sha256:ad42ba922c67c5f219097b28fae965e10045ddf145d2928bfac2eb2e17673640", + "sha256:aeb3531b196ef6f11776c21674dba836aeea9d5bd1cf630f869e3d90b16cfade", + "sha256:b38ac83d5f04b15e515fd86f312479d950d05ce2368d5413d46c088dda7de90a", + "sha256:b7d755065e4e866a8086c9bdada157133ff466476a2ad7861828e17b6026e22c", + "sha256:bd3de6481f4ed8b734da5df134cd5a6a64fe32124fe83dde1e5b5f29fe30b1e6", + "sha256:bfa1acfa0c54932d5607e19a2c24646fb4c1ae2694437789129cf099789a3b00", + "sha256:c619b101e6de2222c1fcb0531e1b17bbffbe54294bfba43ea0d411d428618c27", + "sha256:ce8be0466f4c0d585cdb6c1e2ed07232221df101a4c6f28821d2aa754ca2d9e2", + "sha256:cf0438b42121a66a3a667de17e779330fc0f20b0d97d59d2f2121e182b0505e4", + "sha256:cf8bcc23ceb5a1b624572a1623b9f79d2c3b337c8c455405ef231933a10da379", + "sha256:d2b0e12a42fb4e72d509fc994713d099cbb15ebf1103545e8a45f14da2dfca54", + "sha256:d83db7cde68459fc803052a55ace60bea2bae361fc3b7a6d5da07e11954e4b09", + "sha256:dda56c24d869b1193fcc763f1284b9126550eaf84b88bbc7256e15028f19188a", + "sha256:dea0bf229319828467d7fca8c7c189780aa9ff679c94539eed7532ebe33ed37c", + "sha256:e1631290ee9271dffe3062d2634c3ecac02c83890ada077d225e081aca8aab89", + "sha256:e28c7fea2196bf4c2f8d46a0415c77a1c480cc0724722f23d7410ffe9842c407", + "sha256:e2e6c39bd7b9372b0be21456caab138e8e69cc0fc1190a9dfa92bd45a1e6e904", + "sha256:e33e8fbd440c917106b237ef1a2f1449dfbb9b6f6e1ce17c94cd6a1e0d438376", + "sha256:e8df2eb9b2bac43ef8b082e06f750350fbbaf2887534a5be97f6cf07b19d9583", + "sha256:e968b84db54f9d42046cf154e02911e39c0435c9801681e3fc9ce8a3c4130278", + "sha256:eb542fe7933aa09d8d8f9d9097ef37532a7df6497819d16efe4359890a2f417a", + "sha256:edcfc407e4eb17e037bca59be0e85a2031a2ac87e4fed26d3e9df88b4165f92d", + "sha256:eee3ea935c3d227d49b4eb85660ff631556841f6e567f0f7bda972df6c2c9935", + "sha256:ef97b8df011141c9b0f6caf23b29379f87dd13183c978a30a3c546d2c47314cb", + "sha256:f106407dda69ae456dd1227966bf445b157ccc80ba0dff3802bb63f30b74e895", + "sha256:f3160309af4396e0ed04db259c3ccbfdc3621b5559b5453075e5de555e1f3a1b", + "sha256:f32d6edbc638cde7652bd690c3e728b25332acbadd7cad670cc4a02558d9c417", + "sha256:f37cfe618a117e50d8c240555331160d73d0411422b59b5ee217843d7b693608", + "sha256:f4c9aee212bc89d4e13f58be11a56cc8036cabad119259d12ace14b34476fd07", + "sha256:f4d742cb7af1c28303a51b7a27aaee540e71bb8e24f68c736f6f2ffc82f2bf05", + "sha256:f5a8b53bdc0b3961f8b6125e198617c40aeed638b387913bf1ce78afb1b0be2a", + "sha256:f816dd2277f8d63d79f9c8473a79fe54047bc0467754962840782c575522224d", + "sha256:f9a9e8a507420fe35992ee9ecb302dab68550dedc0da9e2880dd88071c5fb052" ], - "markers": "python_version >= '3.7'", - "version": "==1.4.5" + "markers": "python_version >= '3.8'", + "version": "==1.4.7" }, "markdown-it-py": { "hashes": [ @@ -1172,45 +1264,45 @@ }, "more-itertools": { "hashes": [ - "sha256:e5d93ef411224fbcef366a6e8ddc4c5781bc6359d43412a65dd5964e46111463", - "sha256:ea6a02e24a9161e51faad17a8782b92a0df82c12c1c8886fec7f0c3fa1a1b320" + "sha256:037b0d3203ce90cca8ab1defbbdac29d5f993fc20131f3664dc8d6acfa872aef", + "sha256:5482bfef7849c25dc3c6dd53a6173ae4795da2a41a80faea6700d9f5846c5da6" ], "markers": "python_version >= '3.8'", - "version": "==10.3.0" + "version": "==10.5.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" + "sha256:0bea2a0e71c2a375c9fa0ede3d98324214d67b3cbbfcbd55ac8f750f85a414e3", + "sha256:104e9c1620c2675420abd1f6c44bab7dd33cc85aea751c985006e83dcd001095", + "sha256:14f9294528b5f5cf96c721f231c9f5b2733164e02c1c018ed1a0eff8a18005ac", + "sha256:1a5d8d8dd8613a3e2be3eae829ee891b6b2de6302f24766ff06cb2875f5be9c6", + "sha256:1d44c1e44a8be986b54b09f15f2c1a66368eb43861b4e82573026e04c48a9e20", + "sha256:25bcfa75b9b5a5f8d67147a54ea97ed63a653995a82798221cca2a315c0238c1", + "sha256:35ce88b8ed3a759634cb4eb646d002c4cef0a38f20565ee82b5023558eb90c00", + "sha256:56913ec8c7638b0091ef4da6fcc9136896914a9d60d54670a75880c3e5b99ace", + "sha256:65f190a6349dec29c8d1a1cd4aa71284177aee5949e0502e6379b42873eddbe7", + "sha256:6801319fe76c3f3a3833f2b5af7bd2c17bb93c00026a2a1b924e6762f5b19e13", + "sha256:72596a79bbfb195fd41405cffa18210af3811beb91ff946dbcb7368240eed6be", + "sha256:93743608c7348772fdc717af4aeee1997293a1ad04bc0ea6efa15bf65385c538", + "sha256:940bfff7283c267ae6522ef926a7887305945f716a7704d3344d6d07f02df850", + "sha256:96f8dbc2c85046c81bcddc246232d500ad729cb720da4e20fce3b542cab91287", + "sha256:98790025861cb2c3db8c2f5ad10fc8c336ed2a55f4daf1b8b3f877826b6ff2eb", + "sha256:a3824187c99b893f90c845bab405a585d1ced4ff55421fdf5c84cb7710995229", + "sha256:a83ec98ae12d51c252be61521aa5731f5512231d0b738b4cb2498344f0b840cd", + "sha256:becc9111ca572b04e7e77131bc708480cc88a911adf3d0239f974c034b78085c", + "sha256:c1a184c64521dc549324ec6ef7cbaa6b351912be9cb5edb803c2808a0d7e85ac", + "sha256:c7b73a856522417beb78e0fb6d33ef89474e7a622db2653bc1285af36e2e3e3d", + "sha256:cea3d0fb69637944dd321f41bc896e11d0fb0b0aa531d887a6da70f6e7473aba", + "sha256:d2b3d36baac48e40e3064d2901f2fbd2a2d6880ec6ce6358825c85031d7c0d4d", + "sha256:d7b54c27783991399046837df5c7c9d325d921394757d09dbcbf96aee4649fe9", + "sha256:d8e2e43977f0e09f149ea69fd0556623919f816764e26d74da0c8a7b48f3e18a", + "sha256:dbe286303241fea8c2ea5466f6e0e6a046a135a7e7609167b07fd4e7baf151bf", + "sha256:f006e955718ecd8d159cee9932b64fba8f86ee6f7728ca3ac66c3a54b0062abe", + "sha256:f2268d9fcd9686b61ab64f077be7ffbc6fbcdfb4103e5dd0cc5eaab53a8886c2" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==1.8.0" + "version": "==1.11.0" }, "mypy-extensions": { "hashes": [ @@ -1222,24 +1314,24 @@ }, "nh3": { "hashes": [ - "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" + "sha256:0411beb0589eacb6734f28d5497ca2ed379eafab8ad8c84b31bb5c34072b7164", + "sha256:14c5a72e9fe82aea5fe3072116ad4661af5cf8e8ff8fc5ad3450f123e4925e86", + "sha256:19aaba96e0f795bd0a6c56291495ff59364f4300d4a39b29a0abc9cb3774a84b", + "sha256:34c03fa78e328c691f982b7c03d4423bdfd7da69cd707fe572f544cf74ac23ad", + "sha256:36c95d4b70530b320b365659bb5034341316e6a9b30f0b25fa9c9eff4c27a204", + "sha256:3a157ab149e591bb638a55c8c6bcb8cdb559c8b12c13a8affaba6cedfe51713a", + "sha256:42c64511469005058cd17cc1537578eac40ae9f7200bedcfd1fc1a05f4f8c200", + "sha256:5f36b271dae35c465ef5e9090e1fdaba4a60a56f0bb0ba03e0932a66f28b9189", + "sha256:6955369e4d9f48f41e3f238a9e60f9410645db7e07435e62c6a9ea6135a4907f", + "sha256:7b7c2a3c9eb1a827d42539aa64091640bd275b81e097cd1d8d82ef91ffa2e811", + "sha256:8ce0f819d2f1933953fca255db2471ad58184a60508f03e6285e5114b6254844", + "sha256:94a166927e53972a9698af9542ace4e38b9de50c34352b962f4d9a7d4c927af4", + "sha256:a7f1b5b2c15866f2db413a3649a8fe4fd7b428ae58be2c0f6bca5eefd53ca2be", + "sha256:c8b3a1cebcba9b3669ed1a84cc65bf005728d2f0bc1ed2a6594a992e817f3a50", + "sha256:de3ceed6e661954871d6cd78b410213bdcb136f79aafe22aa7182e028b8c7307", + "sha256:f0eca9ca8628dbb4e916ae2491d72957fdd35f7a5d326b7032a345f111ac07fe" + ], + "version": "==0.2.18" }, "numpy": { "hashes": [ @@ -1275,19 +1367,14 @@ "index": "pypi", "version": "==0.9.1" }, - "opentrons-shared-data": { - "editable": true, - "markers": "python_version >= '3.10'", - "path": "../shared-data/python" - }, "packaging": { "hashes": [ - "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb", - "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522" + "sha256:2198ec20bd4c017b8f9717e00f0c8714076fc2fd93816750ab48e2c41de2cfd3", + "sha256:957e2148ba0e1a3b282772e791ef1d8083648bc131c8ab0c1feba110ce1146c3" ], "index": "pypi", - "markers": "python_version >= '3.6'", - "version": "==21.3" + "markers": "python_version >= '3.7'", + "version": "==22.0" }, "pathspec": { "hashes": [ @@ -1297,84 +1384,91 @@ "markers": "python_version >= '3.8'", "version": "==0.12.1" }, - "performance-metrics": { - "editable": true, - "file": "../performance-metrics" - }, "pillow": { "hashes": [ - "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" + "sha256:02a2be69f9c9b8c1e97cf2713e789d4e398c751ecfd9967c18d0ce304efbf885", + "sha256:030abdbe43ee02e0de642aee345efa443740aa4d828bfe8e2eb11922ea6a21ea", + "sha256:06b2f7898047ae93fad74467ec3d28fe84f7831370e3c258afa533f81ef7f3df", + "sha256:0755ffd4a0c6f267cccbae2e9903d95477ca2f77c4fcf3a3a09570001856c8a5", + "sha256:0a9ec697746f268507404647e531e92889890a087e03681a3606d9b920fbee3c", + "sha256:0ae24a547e8b711ccaaf99c9ae3cd975470e1a30caa80a6aaee9a2f19c05701d", + "sha256:134ace6dc392116566980ee7436477d844520a26a4b1bd4053f6f47d096997fd", + "sha256:166c1cd4d24309b30d61f79f4a9114b7b2313d7450912277855ff5dfd7cd4a06", + "sha256:1b5dea9831a90e9d0721ec417a80d4cbd7022093ac38a568db2dd78363b00908", + "sha256:1d846aea995ad352d4bdcc847535bd56e0fd88d36829d2c90be880ef1ee4668a", + "sha256:1ef61f5dd14c300786318482456481463b9d6b91ebe5ef12f405afbba77ed0be", + "sha256:297e388da6e248c98bc4a02e018966af0c5f92dfacf5a5ca22fa01cb3179bca0", + "sha256:298478fe4f77a4408895605f3482b6cc6222c018b2ce565c2b6b9c354ac3229b", + "sha256:29dbdc4207642ea6aad70fbde1a9338753d33fb23ed6956e706936706f52dd80", + "sha256:2db98790afc70118bd0255c2eeb465e9767ecf1f3c25f9a1abb8ffc8cfd1fe0a", + "sha256:32cda9e3d601a52baccb2856b8ea1fc213c90b340c542dcef77140dfa3278a9e", + "sha256:37fb69d905be665f68f28a8bba3c6d3223c8efe1edf14cc4cfa06c241f8c81d9", + "sha256:416d3a5d0e8cfe4f27f574362435bc9bae57f679a7158e0096ad2beb427b8696", + "sha256:43efea75eb06b95d1631cb784aa40156177bf9dd5b4b03ff38979e048258bc6b", + "sha256:4b35b21b819ac1dbd1233317adeecd63495f6babf21b7b2512d244ff6c6ce309", + "sha256:4d9667937cfa347525b319ae34375c37b9ee6b525440f3ef48542fcf66f2731e", + "sha256:5161eef006d335e46895297f642341111945e2c1c899eb406882a6c61a4357ab", + "sha256:543f3dc61c18dafb755773efc89aae60d06b6596a63914107f75459cf984164d", + "sha256:551d3fd6e9dc15e4c1eb6fc4ba2b39c0c7933fa113b220057a34f4bb3268a060", + "sha256:59291fb29317122398786c2d44427bbd1a6d7ff54017075b22be9d21aa59bd8d", + "sha256:5b001114dd152cfd6b23befeb28d7aee43553e2402c9f159807bf55f33af8a8d", + "sha256:5b4815f2e65b30f5fbae9dfffa8636d992d49705723fe86a3661806e069352d4", + "sha256:5dc6761a6efc781e6a1544206f22c80c3af4c8cf461206d46a1e6006e4429ff3", + "sha256:5e84b6cc6a4a3d76c153a6b19270b3526a5a8ed6b09501d3af891daa2a9de7d6", + "sha256:6209bb41dc692ddfee4942517c19ee81b86c864b626dbfca272ec0f7cff5d9fb", + "sha256:673655af3eadf4df6b5457033f086e90299fdd7a47983a13827acf7459c15d94", + "sha256:6c762a5b0997f5659a5ef2266abc1d8851ad7749ad9a6a5506eb23d314e4f46b", + "sha256:7086cc1d5eebb91ad24ded9f58bec6c688e9f0ed7eb3dbbf1e4800280a896496", + "sha256:73664fe514b34c8f02452ffb73b7a92c6774e39a647087f83d67f010eb9a0cf0", + "sha256:76a911dfe51a36041f2e756b00f96ed84677cdeb75d25c767f296c1c1eda1319", + "sha256:780c072c2e11c9b2c7ca37f9a2ee8ba66f44367ac3e5c7832afcfe5104fd6d1b", + "sha256:7928ecbf1ece13956b95d9cbcfc77137652b02763ba384d9ab508099a2eca856", + "sha256:7970285ab628a3779aecc35823296a7869f889b8329c16ad5a71e4901a3dc4ef", + "sha256:7a8d4bade9952ea9a77d0c3e49cbd8b2890a399422258a77f357b9cc9be8d680", + "sha256:7c1ee6f42250df403c5f103cbd2768a28fe1a0ea1f0f03fe151c8741e1469c8b", + "sha256:7dfecdbad5c301d7b5bde160150b4db4c659cee2b69589705b6f8a0c509d9f42", + "sha256:812f7342b0eee081eaec84d91423d1b4650bb9828eb53d8511bcef8ce5aecf1e", + "sha256:866b6942a92f56300012f5fbac71f2d610312ee65e22f1aa2609e491284e5597", + "sha256:86dcb5a1eb778d8b25659d5e4341269e8590ad6b4e8b44d9f4b07f8d136c414a", + "sha256:87dd88ded2e6d74d31e1e0a99a726a6765cda32d00ba72dc37f0651f306daaa8", + "sha256:8bc1a764ed8c957a2e9cacf97c8b2b053b70307cf2996aafd70e91a082e70df3", + "sha256:8d4d5063501b6dd4024b8ac2f04962d661222d120381272deea52e3fc52d3736", + "sha256:8f0aef4ef59694b12cadee839e2ba6afeab89c0f39a3adc02ed51d109117b8da", + "sha256:930044bb7679ab003b14023138b50181899da3f25de50e9dbee23b61b4de2126", + "sha256:950be4d8ba92aca4b2bb0741285a46bfae3ca699ef913ec8416c1b78eadd64cd", + "sha256:961a7293b2457b405967af9c77dcaa43cc1a8cd50d23c532e62d48ab6cdd56f5", + "sha256:9b885f89040bb8c4a1573566bbb2f44f5c505ef6e74cec7ab9068c900047f04b", + "sha256:9f4727572e2918acaa9077c919cbbeb73bd2b3ebcfe033b72f858fc9fbef0026", + "sha256:a02364621fe369e06200d4a16558e056fe2805d3468350df3aef21e00d26214b", + "sha256:a985e028fc183bf12a77a8bbf36318db4238a3ded7fa9df1b9a133f1cb79f8fc", + "sha256:ac1452d2fbe4978c2eec89fb5a23b8387aba707ac72810d9490118817d9c0b46", + "sha256:b15e02e9bb4c21e39876698abf233c8c579127986f8207200bc8a8f6bb27acf2", + "sha256:b2724fdb354a868ddf9a880cb84d102da914e99119211ef7ecbdc613b8c96b3c", + "sha256:bbc527b519bd3aa9d7f429d152fea69f9ad37c95f0b02aebddff592688998abe", + "sha256:bcd5e41a859bf2e84fdc42f4edb7d9aba0a13d29a2abadccafad99de3feff984", + "sha256:bd2880a07482090a3bcb01f4265f1936a903d70bc740bfcb1fd4e8a2ffe5cf5a", + "sha256:bee197b30783295d2eb680b311af15a20a8b24024a19c3a26431ff83eb8d1f70", + "sha256:bf2342ac639c4cf38799a44950bbc2dfcb685f052b9e262f446482afaf4bffca", + "sha256:c76e5786951e72ed3686e122d14c5d7012f16c8303a674d18cdcd6d89557fc5b", + "sha256:cbed61494057c0f83b83eb3a310f0bf774b09513307c434d4366ed64f4128a91", + "sha256:cfdd747216947628af7b259d274771d84db2268ca062dd5faf373639d00113a3", + "sha256:d7480af14364494365e89d6fddc510a13e5a2c3584cb19ef65415ca57252fb84", + "sha256:dbc6ae66518ab3c5847659e9988c3b60dc94ffb48ef9168656e0019a93dbf8a1", + "sha256:dc3e2db6ba09ffd7d02ae9141cfa0ae23393ee7687248d46a7507b75d610f4f5", + "sha256:dfe91cb65544a1321e631e696759491ae04a2ea11d36715eca01ce07284738be", + "sha256:e4d49b85c4348ea0b31ea63bc75a9f3857869174e2bf17e7aba02945cd218e6f", + "sha256:e4db64794ccdf6cb83a59d73405f63adbe2a1887012e308828596100a0b2f6cc", + "sha256:e553cad5179a66ba15bb18b353a19020e73a7921296a7979c4a2b7f6a5cd57f9", + "sha256:e88d5e6ad0d026fba7bdab8c3f225a69f063f116462c49892b0149e21b6c0a0e", + "sha256:ecd85a8d3e79cd7158dec1c9e5808e821feea088e2f69a974db5edf84dc53141", + "sha256:f5b92f4d70791b4a67157321c4e8225d60b119c5cc9aee8ecf153aace4aad4ef", + "sha256:f5f0c3e969c8f12dd2bb7e0b15d5c468b51e5017e01e2e867335c81903046a22", + "sha256:f7baece4ce06bade126fb84b8af1c33439a76d8a6fd818970215e0560ca28c27", + "sha256:ff25afb18123cea58a591ea0244b92eb1e61a1fd497bf6d6384f09bc3262ec3e", + "sha256:ff337c552345e95702c5fde3158acb0625111017d0e5f24bf3acdb9cc16b90d1" ], "markers": "python_version >= '3.8'", - "version": "==10.3.0" + "version": "==10.4.0" }, "pkginfo": { "hashes": [ @@ -1416,49 +1510,6 @@ "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", @@ -1485,49 +1536,11 @@ }, "pyparsing": { "hashes": [ - "sha256:a1bac0ce561155ecc3ed78ca94d3c9378656ad4c94c1270de543f621420f94ad", - "sha256:f9db75911801ed778fe61bb643079ff86601aca99fcae6345aa67292038fb742" + "sha256:a6a7ee4235a3f944aa1fa2249307708f893fe5717dc603503c6c7969c070fb7c", + "sha256:f86ec8d1a83f11977c9a6ea7598e8c27fc5cddfa5b07ea2241edbbde1d7bc032" ], "markers": "python_full_version >= '3.6.8'", - "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" + "version": "==3.1.4" }, "pytest": { "hashes": [ @@ -1540,12 +1553,12 @@ }, "pytest-asyncio": { "hashes": [ - "sha256:009b48127fbe44518a547bddd25611551b0e43ccdbf1e67d12479f569832c20b", - "sha256:5f5c72948f4c49e7db4f29f2521d4031f1c27f86e57b046126654083d4770268" + "sha256:50265d892689a5faefb84df80819d1ecef566eb3549cf915dfb33569359d1ce2", + "sha256:759b10b33a6dc61cce40a8bd5205e302978bbbcc00e279a8b61d9a6a3c82e4d3" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==0.23.7" + "version": "==0.23.8" }, "pytest-cov": { "hashes": [ @@ -1596,7 +1609,7 @@ "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.9.0.post0" }, "readme-renderer": { @@ -1633,18 +1646,18 @@ }, "rich": { "hashes": [ - "sha256:4edbae314f59eb482f54e9e30bf00d33350aaa94f4bfcd4e9e3110e64d0d7222", - "sha256:9be308cb1fe2f1f57d67ce99e95af38a1e2bc71ad9813b0e247cf7ffbcc3a432" + "sha256:2e85306a063b9492dffc86278197a60cbece75bcb766022f3436f567cae11bdc", + "sha256:a5ac1f1cd448ade0d59cc3356f7db7a7ccda2c8cbae9c7a90c28ff463d3e91f4" ], "markers": "python_full_version >= '3.7.0'", - "version": "==13.7.1" + "version": "==13.8.0" }, "six": { "hashes": [ "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.16.0" }, "snowballstemmer": { @@ -1695,27 +1708,27 @@ }, "sphinxcontrib-applehelp": { "hashes": [ - "sha256:c40a4f96f3776c4393d933412053962fac2b84f4c99a7982ba42e09576a70619", - "sha256:cb61eb0ec1b61f349e5cc36b2028e9e7ca765be05e49641c97241274753067b4" + "sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1", + "sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5" ], "markers": "python_version >= '3.9'", - "version": "==1.0.8" + "version": "==2.0.0" }, "sphinxcontrib-devhelp": { "hashes": [ - "sha256:6485d09629944511c893fa11355bda18b742b83a2b181f9a009f7e500595c90f", - "sha256:9893fd3f90506bc4b97bdb977ceb8fbd823989f4316b28c3841ec128544372d3" + "sha256:411f5d96d445d1d73bb5d52133377b4248ec79db5c793ce7dbe59e074b4dd1ad", + "sha256:aefb8b83854e4b0998877524d1029fd3e6879210422ee3780459e28a1f03a8a2" ], "markers": "python_version >= '3.9'", - "version": "==1.0.6" + "version": "==2.0.0" }, "sphinxcontrib-htmlhelp": { "hashes": [ - "sha256:0dc87637d5de53dd5eec3a6a01753b1ccf99494bd756aafecd74b4fa9e729015", - "sha256:393f04f112b4d2f53d93448d4bce35842f62b307ccdc549ec1585e950bc35e04" + "sha256:166759820b47002d22914d64a075ce08f4c46818e17cfc9470a9786b759b19f8", + "sha256:c9e2916ace8aad64cc13a0d233ee22317f2b9025b9cf3295249fa985cc7082e9" ], "markers": "python_version >= '3.9'", - "version": "==2.0.5" + "version": "==2.1.0" }, "sphinxcontrib-jsmath": { "hashes": [ @@ -1727,19 +1740,19 @@ }, "sphinxcontrib-qthelp": { "hashes": [ - "sha256:053dedc38823a80a7209a80860b16b722e9e0209e32fea98c90e4e6624588ed6", - "sha256:e2ae3b5c492d58fcbd73281fbd27e34b8393ec34a073c792642cd8e529288182" + "sha256:4fe7d0ac8fc171045be623aba3e2a8f613f8682731f9153bb2e40ece16b9bbab", + "sha256:b18a828cdba941ccd6ee8445dbe72ffa3ef8cbe7505d8cd1fa0d42d3f2d5f3eb" ], "markers": "python_version >= '3.9'", - "version": "==1.0.7" + "version": "==2.0.0" }, "sphinxcontrib-serializinghtml": { "hashes": [ - "sha256:326369b8df80a7d2d8d7f99aa5ac577f51ea51556ed974e7716cfd4fca3f6cb7", - "sha256:93f3f5dc458b91b192fe10c397e324f262cf163d79f3282c158e8436a2c4511f" + "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331", + "sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d" ], "markers": "python_version >= '3.9'", - "version": "==1.1.10" + "version": "==2.0.0" }, "sphinxext-opengraph": { "hashes": [ @@ -1798,7 +1811,7 @@ "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8" ], - "markers": "python_version >= '3.8'", + "markers": "python_version < '3.13'", "version": "==4.12.2" }, "urllib3": { @@ -1820,11 +1833,11 @@ }, "zipp": { "hashes": [ - "sha256:bf1dcf6450f873a13e952a29504887c89e6de7506209e5b1bcc3460135d4de19", - "sha256:f091755f667055f2d02b32c53771a7a6c8b47e1fdbc4b72a8b9072b3eef8015c" + "sha256:9960cd8967c8f85a56f920d5d507274e74f9ff813a0ab8889a5b5be2daf44064", + "sha256:c22b14cc4763c5a5b04134207736c107db42e9d3ef2d9779d465f5f1bcba572b" ], "markers": "python_version >= '3.8'", - "version": "==3.19.2" + "version": "==3.20.1" } } } diff --git a/api/docs/v2/conf.py b/api/docs/v2/conf.py index 708d800ce3e..c8d1e8e7f97 100644 --- a/api/docs/v2/conf.py +++ b/api/docs/v2/conf.py @@ -99,7 +99,7 @@ # use rst_prolog to hold the subsitution # update the apiLevel value whenever a new minor version is released rst_prolog = f""" -.. |apiLevel| replace:: 2.20 +.. |apiLevel| replace:: 2.21 .. |release| replace:: {release} """ diff --git a/api/docs/v2/modules/thermocycler.rst b/api/docs/v2/modules/thermocycler.rst index 9322e0a96f0..17d57e84292 100644 --- a/api/docs/v2/modules/thermocycler.rst +++ b/api/docs/v2/modules/thermocycler.rst @@ -15,7 +15,7 @@ The examples in this section will use a Thermocycler Module GEN2 loaded as follo .. code-block:: python tc_mod = protocol.load_module(module_name="thermocyclerModuleV2") - plate = tc_mod.load_labware(name="nest_96_wellplate_100ul_pcr_full_skirt") + plate = tc_mod.load_labware(name="opentrons_96_wellplate_200ul_pcr_full_skirt") .. versionadded:: 2.13 @@ -139,6 +139,70 @@ However, this code would generate 60 lines in the protocol's run log, while exec .. versionadded:: 2.0 +Auto-sealing Lids +================= + +Starting in robot software version 8.2.0, you can use the Opentrons Tough PCR Auto-sealing Lid to reduce evaporation on the Thermocycler. The auto-sealing lids are designed for automated use with the Flex Gripper, although you can move them manually if needed. They also work with the Opentrons Flex Deck Riser adapter, which keeps lids away from the unsterilized deck and provides better access for the gripper. + +Use the following API load names for the auto-sealing lid and deck riser: + +.. list-table:: + :header-rows: 1 + + * - Labware + - API load name + * - Opentrons Tough PCR Auto-sealing Lid + - ``opentrons_tough_pcr_auto_sealing_lid`` + * - Opentrons Flex Deck Riser + - ``opentrons_flex_deck_riser`` + +Load the riser directly onto the deck with :py:meth:`.ProtocolContext.load_adapter`. Load the auto-sealing lid onto a compatible location (the deck, the riser, or another lid) with the appropriate ``load_labware()`` method. You can create a stack of up to five auto-sealing lids. If you try to stack more than five lids, the API will raise an error. + +Setting up the riser and preparing a lid to use on the Thermocycler generally consists of the following steps: + + 1. Load the riser on the deck. + 2. Load the lids onto the adapter. + 3. Load or move a PCR plate onto the Thermocycler. + 4. Move a lid onto the PCR plate. + 5. Close the Thermocycler. + +The following code sample shows how to perform these steps, using the riser and three auto-sealing lids. In a full protocol, you would likely have additional steps, such as pipetting to or from the PCR plate. + +.. code-block:: python + + # load riser + riser = protocol.load_adapter( + load_name="opentrons_flex_deck_riser", location="A2" + ) + + # load three lids + lid_1 = riser.load_labware("opentrons_tough_pcr_auto_sealing_lid") + lid_2 = lid_1.load_labware("opentrons_tough_pcr_auto_sealing_lid") + lid_3 = lid_2.load_labware("opentrons_tough_pcr_auto_sealing_lid") + + # load plate on Thermocycler + plate = protocol.load_labware( + load_name="opentrons_96_wellplate_200ul_pcr_full_skirt", location=tc_mod + ) + + # move lid to PCR plate + protocol.move_labware(labware=lid_3, new_location=plate, use_gripper=True) + + # close Thermocycler + tc_mod.close_lid() + +.. warning:: + When using the auto-sealing lids, `do not` affix a rubber automation seal to the inside of the Thermocycler lid. The Thermocycler will not close properly. + +When you're finished with a lid, use the gripper to dispose of it in either the waste chute or a trash bin:: + + tc_mod.open_lid() + protocol.move_labware(labware=lid_3, new_location=trash, use_gripper=True) + +.. versionadded:: 2.16 + :py:class:`.TrashBin` and :py:class:`.WasteChute` objects can accept lids. + +You can then move the PCR plate off of the Thermocycler. The Flex Gripper can't move a plate that has a lid on top of it. Always move the lid first, then the plate. Changes with the GEN2 Thermocycler Module ========================================= diff --git a/api/docs/v2/new_examples.rst b/api/docs/v2/new_examples.rst index 1aae3b633d0..42dbf92fd8d 100644 --- a/api/docs/v2/new_examples.rst +++ b/api/docs/v2/new_examples.rst @@ -284,7 +284,7 @@ When used in a protocol, loops automate repetitive steps such as aspirating and # etc... # range() starts at 0 and stops before 8, creating a range of 0-7 for i in range(8): - pipette.distribute(200, reservoir.wells()[i], plate.rows()[i]) + pipette.distribute(20, reservoir.wells()[i], plate.rows()[i]) .. tab:: OT-2 @@ -315,7 +315,7 @@ When used in a protocol, loops automate repetitive steps such as aspirating and # etc... # range() starts at 0 and stops before 8, creating a range of 0-7 for i in range(8): - p300.distribute(200, reservoir.wells()[i], plate.rows()[i]) + p300.distribute(20, reservoir.wells()[i], plate.rows()[i]) Notice here how Python's :py:class:`range` class (e.g., ``range(8)``) determines how many times the code loops. Also, in Python, a range of numbers is *exclusive* of the end value and counting starts at 0, not 1. For the Corning 96-well plate used here, this means well A1=0, B1=1, C1=2, and so on to the last well in the row, which is H1=7. diff --git a/api/docs/v2/new_protocol_api.rst b/api/docs/v2/new_protocol_api.rst index 2ce4c39e3cc..d7428799fe0 100644 --- a/api/docs/v2/new_protocol_api.rst +++ b/api/docs/v2/new_protocol_api.rst @@ -28,7 +28,7 @@ Labware ======= .. autoclass:: opentrons.protocol_api.Labware :members: - :exclude-members: next_tip, use_tips, previous_tip, return_tips + :exclude-members: next_tip, use_tips, previous_tip, return_tips, load_empty, load_liquid, load_liquid_by_well .. The trailing ()s at the end of TrashBin and WasteChute here hide the __init__() diff --git a/api/docs/v2/parameters/use_case_sample_count.rst b/api/docs/v2/parameters/use_case_sample_count.rst index 15933752592..d7ce6529e48 100644 --- a/api/docs/v2/parameters/use_case_sample_count.rst +++ b/api/docs/v2/parameters/use_case_sample_count.rst @@ -166,7 +166,7 @@ Now we'll bring sample count into consideration as we :ref:`load the liquids `. Use the load name ``absorbanceReaderV1`` with :py:meth:`.ProtocolContext.load_module` to add an Absorbance Plate Reader to a protocol. +- :ref:`Liquid presence detection ` now only checks on the first aspiration of the :py:meth:`.mix` cycle. +- Improved the run log output of :py:meth:`.ThermocyclerContext.execute_profile`. + Version 2.20 ------------ diff --git a/api/pytest.ini b/api/pytest.ini index a8e3bbb1933..78115d41057 100644 --- a/api/pytest.ini +++ b/api/pytest.ini @@ -5,3 +5,14 @@ markers = ot3_only: Test only functions using the OT3 hardware addopts = --color=yes --strict-markers asyncio_mode = auto + +filterwarnings = + # TODO this should be looked into being removed upon updating the Decoy library. The purpose of this warning is to + # catch missing attributes, but it raises for any property referenced in a test which accounts for about ~250 warnings + # which aren't serving any useful purpose and obscure other warnings. + ignore::decoy.warnings.MissingSpecAttributeWarning + # Pydantic's shims for its legacy v1 methods (e.g. `BaseModel.construct()`) + # are not type-checked properly. Forbid them, so we're forced to use their newer + # v2 replacements which are type-checked (e.g. ``BaseModel.model_construct()`) + error::pydantic.PydanticDeprecatedSince20 + diff --git a/api/release-notes-internal.md b/api/release-notes-internal.md index 7a3d81e5cbf..bc5398781e4 100644 --- a/api/release-notes-internal.md +++ b/api/release-notes-internal.md @@ -2,6 +2,22 @@ For more details about this release, please see the full [technical change log][ [technical change log]: https://github.com/Opentrons/opentrons/releases +## Internal Release 2.3.0-alpha.2 + +This internal release, pulled from the `edge` branch, contains features being developed for 8.3.0. It's for internal testing only. + +## Internal Release 2.3.0-alpha.1 + +This internal release, pulled from the `edge` branch, contains features being developed for 8.3.0. It's for internal testing only. + +## Internal Release 2.3.0-alpha.0 + +This internal release, pulled from the `edge` branch, contains features being developed for evo tip functionality. It's for internal testing only. + +## Internal Release 2.2.0-alpha.1 + +This internal release, pulled from the `edge` branch, contains features being developed for 8.2.0. It's for internal testing only. + ## Internal Release 2.2.0-alpha.0 This internal release, pulled from the `edge` branch, contains features being developed for 8.2.0. It's for internal testing only. diff --git a/api/release-notes.md b/api/release-notes.md index 1fdd9a033d9..acd5a164868 100644 --- a/api/release-notes.md +++ b/api/release-notes.md @@ -8,6 +8,17 @@ By installing and using Opentrons software, you agree to the Opentrons End-User --- +## Opentrons Robot Software Changes in 8.3.0 + +Welcome to the v8.3.0 release of the Opentrons robot software! This release includes improvements to error recovery on the Flex, as well as beta features for our commercial partners. + + +### Improved Features + +- Improvements to the Flex error recovery feature help protocols recover from detected stalls and collisions, saving you valuable time and resources. + +--- + ## Opentrons Robot Software Changes in 8.2.0 Welcome to the v8.2.0 release of the Opentrons robot software! This release adds support for the Opentrons Absorbance Plate Reader Module. diff --git a/api/setup.py b/api/setup.py index 92f06b49bef..a85fc83f114 100755 --- a/api/setup.py +++ b/api/setup.py @@ -59,9 +59,11 @@ def get_version(): f"opentrons-shared-data=={VERSION}", "aionotify==0.3.1", "anyio>=3.6.1,<4.0.0", + # todo(mm, 2024-12-14): investigate ref resolution problems caused by jsonschema>=4.18.1. "jsonschema>=3.0.1,<4.18.0", "numpy>=1.20.0,<2", - "pydantic>=1.10.9,<2.0.0", + "pydantic>=2.0.0,<3", + "pydantic-settings>=2,<3", "pyserial>=3.5", "typing-extensions>=4.0.0,<5", "click>=8.0.0,<9", diff --git a/api/src/opentrons/calibration_storage/deck_configuration.py b/api/src/opentrons/calibration_storage/deck_configuration.py index a627fce73c9..857c2c22d3f 100644 --- a/api/src/opentrons/calibration_storage/deck_configuration.py +++ b/api/src/opentrons/calibration_storage/deck_configuration.py @@ -10,7 +10,7 @@ class _CutoutFixturePlacementModel(pydantic.BaseModel): cutoutId: str cutoutFixtureId: str - opentronsModuleSerialNumber: Optional[str] + opentronsModuleSerialNumber: Optional[str] = None class _DeckConfigurationModel(pydantic.BaseModel): @@ -24,9 +24,9 @@ def serialize_deck_configuration( cutout_fixture_placements: List[CutoutFixturePlacement], last_modified: datetime ) -> bytes: """Serialize a deck configuration for storing on the filesystem.""" - data = _DeckConfigurationModel.construct( + data = _DeckConfigurationModel.model_construct( cutoutFixtures=[ - _CutoutFixturePlacementModel.construct( + _CutoutFixturePlacementModel.model_construct( cutoutId=e.cutout_id, cutoutFixtureId=e.cutout_fixture_id, opentronsModuleSerialNumber=e.opentrons_module_serial_number, diff --git a/api/src/opentrons/calibration_storage/file_operators.py b/api/src/opentrons/calibration_storage/file_operators.py index 70c16297ecd..bf80a034d54 100644 --- a/api/src/opentrons/calibration_storage/file_operators.py +++ b/api/src/opentrons/calibration_storage/file_operators.py @@ -103,7 +103,7 @@ def save_to_file( directory_path.mkdir(parents=True, exist_ok=True) file_path = directory_path / f"{file_name}.json" json_data = ( - data.json() + data.model_dump_json() if isinstance(data, pydantic.BaseModel) else json.dumps(data, cls=encoder) ) @@ -112,7 +112,7 @@ def save_to_file( def serialize_pydantic_model(data: pydantic.BaseModel) -> bytes: """Safely serialize data from a Pydantic model into a form suitable for storing on disk.""" - return data.json(by_alias=True).encode("utf-8") + return data.model_dump_json(by_alias=True).encode("utf-8") _ModelT = typing.TypeVar("_ModelT", bound=pydantic.BaseModel) @@ -133,7 +133,7 @@ def deserialize_pydantic_model( Returns `None` if the file is missing or corrupt. """ try: - return model.parse_raw(serialized) + return model.model_validate_json(serialized) except json.JSONDecodeError: _log.warning("Data is not valid JSON.", exc_info=True) return None diff --git a/api/src/opentrons/calibration_storage/helpers.py b/api/src/opentrons/calibration_storage/helpers.py index 1d271add9dd..db11dac3453 100644 --- a/api/src/opentrons/calibration_storage/helpers.py +++ b/api/src/opentrons/calibration_storage/helpers.py @@ -31,7 +31,9 @@ def convert_to_dict(obj: Any) -> Dict[str, Any]: # https://github.com/python/mypy/issues/6568 # Unfortunately, since it's not currently supported I have an # assert check instead. - assert is_dataclass(obj), "This function is intended for dataclasses only" + assert is_dataclass(obj) and not isinstance( + obj, type + ), "This function is intended for dataclasses only" return asdict(obj, dict_factory=dict_filter_none) diff --git a/api/src/opentrons/calibration_storage/ot2/models/v1.py b/api/src/opentrons/calibration_storage/ot2/models/v1.py index 922922415c8..23836e67c98 100644 --- a/api/src/opentrons/calibration_storage/ot2/models/v1.py +++ b/api/src/opentrons/calibration_storage/ot2/models/v1.py @@ -1,7 +1,7 @@ import typing from typing_extensions import Literal -from pydantic import BaseModel, Field, validator +from pydantic import field_validator, BaseModel, Field, PlainSerializer from datetime import datetime from opentrons_shared_data.pipette.types import LabwareUri @@ -9,11 +9,16 @@ from opentrons.types import Point from opentrons.calibration_storage import types +DatetimeType = typing.Annotated[ + datetime, + PlainSerializer(lambda x: x.isoformat(), when_used="json"), +] + class CalibrationStatus(BaseModel): markedBad: bool = False source: typing.Optional[types.SourceType] = None - markedAt: typing.Optional[datetime] = None + markedAt: typing.Optional[DatetimeType] = None # Schemas used to store the data types @@ -22,7 +27,7 @@ class CalibrationStatus(BaseModel): # they are currently saved in on the OT-2 to avoid a large migration. class TipLengthModel(BaseModel): tipLength: float = Field(..., description="Tip length data found from calibration.") - lastModified: datetime = Field( + lastModified: DatetimeType = Field( ..., description="The last time this tip length was calibrated." ) source: types.SourceType = Field( @@ -40,22 +45,19 @@ class TipLengthModel(BaseModel): ..., description="The tiprack hash associated with the tip length data." ) - @validator("tipLength") + @field_validator("tipLength") + @classmethod def ensure_tip_length_positive(cls, tipLength: float) -> float: if tipLength < 0.0: raise ValueError("Tip Length must be a positive number") return tipLength - class Config: - json_encoders = {datetime: lambda obj: obj.isoformat()} - json_decoders = {datetime: lambda obj: datetime.fromisoformat(obj)} - class DeckCalibrationModel(BaseModel): attitude: types.AttitudeMatrix = Field( ..., description="Attitude matrix for deck found from calibration." ) - last_modified: typing.Optional[datetime] = Field( + last_modified: typing.Optional[DatetimeType] = Field( default=None, description="The last time this deck was calibrated." ) source: types.SourceType = Field( @@ -72,10 +74,6 @@ class DeckCalibrationModel(BaseModel): description="The status of the calibration data.", ) - class Config: - json_encoders = {datetime: lambda obj: obj.isoformat()} - json_decoders = {datetime: lambda obj: datetime.fromisoformat(obj)} - class InstrumentOffsetModel(BaseModel): offset: Point = Field(..., description="Instrument offset found from calibration.") @@ -83,7 +81,7 @@ class InstrumentOffsetModel(BaseModel): uri: str = Field( ..., description="The URI of the labware used for instrument offset" ) - last_modified: datetime = Field( + last_modified: DatetimeType = Field( ..., description="The last time this instrument was calibrated." ) source: types.SourceType = Field( @@ -94,10 +92,6 @@ class InstrumentOffsetModel(BaseModel): description="The status of the calibration data.", ) - class Config: - json_encoders = {datetime: lambda obj: obj.isoformat()} - json_decoders = {datetime: lambda obj: datetime.fromisoformat(obj)} - # TODO(lc 09-19-2022) We need to refactor the calibration endpoints # so that we only need to use one data model schema. This model is a @@ -112,7 +106,7 @@ class PipetteOffsetCalibration(BaseModel): uri: str = Field( ..., description="The URI of the labware used for instrument offset" ) - last_modified: datetime = Field( + last_modified: DatetimeType = Field( ..., description="The last time this instrument was calibrated." ) source: types.SourceType = Field( @@ -123,10 +117,6 @@ class PipetteOffsetCalibration(BaseModel): description="The status of the calibration data.", ) - class Config: - json_encoders = {datetime: lambda obj: obj.isoformat()} - json_decoders = {datetime: lambda obj: datetime.fromisoformat(obj)} - # TODO(lc 09-19-2022) We need to refactor the calibration endpoints # so that we only need to use one data model schema. This model is a @@ -137,7 +127,7 @@ class TipLengthCalibration(BaseModel): ..., description="The tiprack hash associated with this tip length data." ) tipLength: float = Field(..., description="Tip length data found from calibration.") - lastModified: datetime = Field( + lastModified: DatetimeType = Field( ..., description="The last time this tip length was calibrated." ) source: types.SourceType = Field( @@ -151,12 +141,9 @@ class TipLengthCalibration(BaseModel): ..., description="The tiprack URI associated with the tip length data." ) - @validator("tipLength") + @field_validator("tipLength") + @classmethod def ensure_tip_length_positive(cls, tipLength: float) -> float: if tipLength < 0.0: raise ValueError("Tip Length must be a positive number") return tipLength - - class Config: - json_encoders = {datetime: lambda obj: obj.isoformat()} - json_decoders = {datetime: lambda obj: datetime.fromisoformat(obj)} diff --git a/api/src/opentrons/calibration_storage/ot2/tip_length.py b/api/src/opentrons/calibration_storage/ot2/tip_length.py index a0bcdcabf9d..979916bd85e 100644 --- a/api/src/opentrons/calibration_storage/ot2/tip_length.py +++ b/api/src/opentrons/calibration_storage/ot2/tip_length.py @@ -24,13 +24,14 @@ def _convert_tip_length_model_to_dict( - to_dict: typing.Dict[LabwareUri, v1.TipLengthModel] + to_dict: typing.Dict[LabwareUri, v1.TipLengthModel], ) -> typing.Dict[LabwareUri, typing.Any]: + # TODO[pydantic]: supported in Pydantic V2 # This is a workaround since pydantic doesn't have a nice way to # add encoders when converting to a dict. dict_of_tip_lengths = {} for key, item in to_dict.items(): - dict_of_tip_lengths[key] = json.loads(item.json()) + dict_of_tip_lengths[key] = json.loads(item.model_dump_json()) return dict_of_tip_lengths @@ -175,12 +176,14 @@ def delete_tip_length_calibration( io.save_to_file(tip_length_dir, pipette_id, dict_of_tip_lengths) else: io.delete_file(tip_length_dir / f"{pipette_id}.json") - elif tiprack_hash and any(tiprack_hash in v.dict() for v in tip_lengths.values()): + elif tiprack_hash and any( + tiprack_hash in v.model_dump() for v in tip_lengths.values() + ): # NOTE this is for backwards compatibilty only # TODO delete this check once the tip_length DELETE router # no longer depends on a tiprack hash for k, v in tip_lengths.items(): - if tiprack_hash in v.dict(): + if tiprack_hash in v.model_dump(): tip_lengths.pop(k) if tip_lengths: dict_of_tip_lengths = _convert_tip_length_model_to_dict(tip_lengths) diff --git a/api/src/opentrons/calibration_storage/ot3/models/v1.py b/api/src/opentrons/calibration_storage/ot3/models/v1.py index 55e028465c7..b895eac4c89 100644 --- a/api/src/opentrons/calibration_storage/ot3/models/v1.py +++ b/api/src/opentrons/calibration_storage/ot3/models/v1.py @@ -3,7 +3,7 @@ from typing_extensions import Literal from opentrons.hardware_control.modules.types import ModuleType from opentrons.hardware_control.types import OT3Mount -from pydantic import BaseModel, Field, validator +from pydantic import field_validator, BaseModel, Field, PlainSerializer from datetime import datetime from opentrons_shared_data.pipette.types import LabwareUri @@ -12,15 +12,21 @@ from opentrons.calibration_storage import types +DatetimeType = typing.Annotated[ + datetime, + PlainSerializer(lambda x: x.isoformat(), when_used="json"), +] + + class CalibrationStatus(BaseModel): markedBad: bool = False source: typing.Optional[types.SourceType] = None - markedAt: typing.Optional[datetime] = None + markedAt: typing.Optional[DatetimeType] = None class TipLengthModel(BaseModel): tipLength: float = Field(..., description="Tip length data found from calibration.") - lastModified: datetime = Field( + lastModified: DatetimeType = Field( ..., description="The last time this tip length was calibrated." ) uri: typing.Union[LabwareUri, Literal[""]] = Field( @@ -34,22 +40,19 @@ class TipLengthModel(BaseModel): description="The status of the calibration data.", ) - @validator("tipLength") + @field_validator("tipLength") + @classmethod def ensure_tip_length_positive(cls, tipLength: float) -> float: if tipLength < 0.0: raise ValueError("Tip Length must be a positive number") return tipLength - class Config: - json_encoders = {datetime: lambda obj: obj.isoformat()} - json_decoders = {datetime: lambda obj: datetime.fromisoformat(obj)} - class BeltCalibrationModel(BaseModel): attitude: types.AttitudeMatrix = Field( ..., description="Attitude matrix for belts found from calibration." ) - lastModified: datetime = Field( + lastModified: DatetimeType = Field( ..., description="The last time this deck was calibrated." ) source: types.SourceType = Field( @@ -63,14 +66,10 @@ class BeltCalibrationModel(BaseModel): description="The status of the calibration data.", ) - class Config: - json_encoders = {datetime: lambda obj: obj.isoformat()} - json_decoders = {datetime: lambda obj: datetime.fromisoformat(obj)} - class InstrumentOffsetModel(BaseModel): offset: Point = Field(..., description="Instrument offset found from calibration.") - lastModified: datetime = Field( + lastModified: DatetimeType = Field( ..., description="The last time this instrument was calibrated." ) source: types.SourceType = Field( @@ -81,10 +80,6 @@ class InstrumentOffsetModel(BaseModel): description="The status of the calibration data.", ) - class Config: - json_encoders = {datetime: lambda obj: obj.isoformat()} - json_decoders = {datetime: lambda obj: datetime.fromisoformat(obj)} - class ModuleOffsetModel(BaseModel): offset: Point = Field(..., description="Module offset found from calibration.") @@ -99,7 +94,7 @@ class ModuleOffsetModel(BaseModel): ..., description="The unique id of the instrument used to calibrate this module.", ) - lastModified: datetime = Field( + lastModified: DatetimeType = Field( ..., description="The last time this module was calibrated." ) source: types.SourceType = Field( @@ -109,7 +104,3 @@ class ModuleOffsetModel(BaseModel): default_factory=CalibrationStatus, description="The status of the calibration data.", ) - - class Config: - json_encoders = {datetime: lambda obj: obj.isoformat()} - json_decoders = {datetime: lambda obj: datetime.fromisoformat(obj)} diff --git a/api/src/opentrons/cli/analyze.py b/api/src/opentrons/cli/analyze.py index 8489da83d68..1ce161a96a7 100644 --- a/api/src/opentrons/cli/analyze.py +++ b/api/src/opentrons/cli/analyze.py @@ -53,9 +53,11 @@ LoadedPipette, LoadedModule, Liquid, + LiquidClassRecordWithId, StateSummary, ) from opentrons.protocol_engine.protocol_engine import code_in_error_tree +from opentrons.protocol_engine.types import CommandAnnotation from opentrons_shared_data.robot.types import RobotType @@ -333,8 +335,10 @@ async def _do_analyze( wells=[], hasEverEnteredErrorRecovery=False, files=[], + liquidClasses=[], ), parameters=[], + command_annotations=[], ) return analysis return await orchestrator.run(deck_configuration=[]) @@ -378,16 +382,20 @@ async def _analyze( else: result = AnalysisResult.OK - results = AnalyzeResults.construct( + results = AnalyzeResults.model_construct( createdAt=datetime.now(tz=timezone.utc), files=[ - ProtocolFile.construct(name=f.path.name, role=f.role) + ProtocolFile.model_construct(name=f.path.name, role=f.role) for f in protocol_source.files ], config=( - JsonConfig.construct(schemaVersion=protocol_source.config.schema_version) + JsonConfig.model_construct( + schemaVersion=protocol_source.config.schema_version + ) if isinstance(protocol_source.config, JsonProtocolConfig) - else PythonConfig.construct(apiVersion=protocol_source.config.api_version) + else PythonConfig.model_construct( + apiVersion=protocol_source.config.api_version + ) ), result=result, metadata=protocol_source.metadata, @@ -399,20 +407,22 @@ async def _analyze( pipettes=analysis.state_summary.pipettes, modules=analysis.state_summary.modules, liquids=analysis.state_summary.liquids, + commandAnnotations=analysis.command_annotations, + liquidClasses=analysis.state_summary.liquidClasses, ) _call_for_output_of_kind( "json", outputs, lambda to_file: to_file.write( - results.json(exclude_none=True).encode("utf-8"), + results.model_dump_json(exclude_none=True).encode("utf-8"), ), ) _call_for_output_of_kind( "human-json", outputs, lambda to_file: to_file.write( - results.json(exclude_none=True, indent=2).encode("utf-8") + results.model_dump_json(exclude_none=True, indent=2).encode("utf-8") ), ) if check: @@ -486,4 +496,6 @@ class AnalyzeResults(BaseModel): pipettes: List[LoadedPipette] modules: List[LoadedModule] liquids: List[Liquid] + liquidClasses: List[LiquidClassRecordWithId] errors: List[ErrorOccurrence] + commandAnnotations: List[CommandAnnotation] diff --git a/api/src/opentrons/config/defaults_ot3.py b/api/src/opentrons/config/defaults_ot3.py index 55565745d3a..53fab18392c 100644 --- a/api/src/opentrons/config/defaults_ot3.py +++ b/api/src/opentrons/config/defaults_ot3.py @@ -75,6 +75,7 @@ DEFAULT_GRIPPER_MOUNT_OFFSET: Final[Offset] = (84.55, -12.75, 93.85) DEFAULT_SAFE_HOME_DISTANCE: Final = 5 DEFAULT_CALIBRATION_AXIS_MAX_SPEED: Final = 30 +DEFAULT_EMULSIFYING_PIPETTE_AXIS_MAX_SPEED: Final = 90 DEFAULT_MAX_SPEEDS: Final[ByGantryLoad[Dict[OT3AxisKind, float]]] = ByGantryLoad( high_throughput={ diff --git a/api/src/opentrons/drivers/asyncio/communication/__init__.py b/api/src/opentrons/drivers/asyncio/communication/__init__.py index e936c0f16ee..5302c2e8e23 100644 --- a/api/src/opentrons/drivers/asyncio/communication/__init__.py +++ b/api/src/opentrons/drivers/asyncio/communication/__init__.py @@ -4,6 +4,7 @@ NoResponse, AlarmResponse, ErrorResponse, + UnhandledGcode, ) from .async_serial import AsyncSerial @@ -15,4 +16,5 @@ "NoResponse", "AlarmResponse", "ErrorResponse", + "UnhandledGcode", ] diff --git a/api/src/opentrons/drivers/asyncio/communication/errors.py b/api/src/opentrons/drivers/asyncio/communication/errors.py index 519263e90ec..48f66356319 100644 --- a/api/src/opentrons/drivers/asyncio/communication/errors.py +++ b/api/src/opentrons/drivers/asyncio/communication/errors.py @@ -1,23 +1,30 @@ """Errors raised by serial connection.""" +from enum import Enum + + +class ErrorCodes(Enum): + UNHANDLED_GCODE = "ERR003" + + class SerialException(Exception): """Base serial exception""" - def __init__(self, port: str, description: str): + def __init__(self, port: str, description: str) -> None: super().__init__(f"{port}: {description}") self.port = port self.description = description class NoResponse(SerialException): - def __init__(self, port: str, command: str): + def __init__(self, port: str, command: str) -> None: super().__init__(port=port, description=f"No response to '{command}'") self.command = command class FailedCommand(SerialException): - def __init__(self, port: str, response: str): + def __init__(self, port: str, response: str) -> None: super().__init__( port=port, description=f"'Received error response '{response}'" ) @@ -30,3 +37,9 @@ class AlarmResponse(FailedCommand): class ErrorResponse(FailedCommand): pass + + +class UnhandledGcode(ErrorResponse): + def __init__(self, port: str, response: str, command: str) -> None: + self.command = command + super().__init__(port, response) diff --git a/api/src/opentrons/drivers/asyncio/communication/serial_connection.py b/api/src/opentrons/drivers/asyncio/communication/serial_connection.py index f2868efb383..f925cfe8680 100644 --- a/api/src/opentrons/drivers/asyncio/communication/serial_connection.py +++ b/api/src/opentrons/drivers/asyncio/communication/serial_connection.py @@ -6,7 +6,7 @@ from opentrons.drivers.command_builder import CommandBuilder -from .errors import NoResponse, AlarmResponse, ErrorResponse +from .errors import NoResponse, AlarmResponse, ErrorResponse, UnhandledGcode, ErrorCodes from .async_serial import AsyncSerial log = logging.getLogger(__name__) @@ -199,7 +199,7 @@ async def _send_data(self, data: str, retries: int = 0) -> str: str_response = self.process_raw_response( command=data, response=response.decode() ) - self.raise_on_error(response=str_response) + self.raise_on_error(response=str_response, request=data) return str_response log.info(f"{self.name}: retry number {retry}/{retries}") @@ -232,11 +232,12 @@ def name(self) -> str: def send_data_lock(self) -> asyncio.Lock: return self._send_data_lock - def raise_on_error(self, response: str) -> None: + def raise_on_error(self, response: str, request: str) -> None: """ Raise an error if the response contains an error Args: + gcode: the requesting gocde response: response Returns: None @@ -248,8 +249,13 @@ def raise_on_error(self, response: str) -> None: if self._alarm_keyword in lower: raise AlarmResponse(port=self._port, response=response) - if self._error_keyword in lower: - raise ErrorResponse(port=self._port, response=response) + if self._error_keyword.lower() in lower: + if ErrorCodes.UNHANDLED_GCODE.value.lower() in lower: + raise UnhandledGcode( + port=self._port, response=response, command=request + ) + else: + raise ErrorResponse(port=self._port, response=response) async def on_retry(self) -> None: """ @@ -292,6 +298,7 @@ async def create( alarm_keyword: Optional[str] = None, reset_buffer_before_write: bool = False, async_error_ack: Optional[str] = None, + number_of_retries: int = 0, ) -> AsyncResponseSerialConnection: """ Create a connection. @@ -334,6 +341,7 @@ async def create( error_keyword=error_keyword or "err", alarm_keyword=alarm_keyword or "alarm", async_error_ack=async_error_ack or "async", + number_of_retries=number_of_retries, ) def __init__( @@ -346,6 +354,7 @@ def __init__( error_keyword: str, alarm_keyword: str, async_error_ack: str, + number_of_retries: int = 0, ) -> None: """ Constructor @@ -377,6 +386,7 @@ def __init__( self._name = name self._ack = ack.encode() self._retry_wait_time_seconds = retry_wait_time_seconds + self._number_of_retries = number_of_retries self._error_keyword = error_keyword.lower() self._alarm_keyword = alarm_keyword.lower() self._async_error_ack = async_error_ack.lower() @@ -397,7 +407,9 @@ async def send_command( Raises: SerialException """ return await self.send_data( - data=command.build(), retries=retries, timeout=timeout + data=command.build(), + retries=retries or self._number_of_retries, + timeout=timeout, ) async def send_data( @@ -418,7 +430,9 @@ async def send_data( async with super().send_data_lock, self._serial.timeout_override( "timeout", timeout ): - return await self._send_data(data=data, retries=retries) + return await self._send_data( + data=data, retries=retries or self._number_of_retries + ) async def _send_data(self, data: str, retries: int = 0) -> str: """ @@ -433,6 +447,7 @@ async def _send_data(self, data: str, retries: int = 0) -> str: Raises: SerialException """ data_encode = data.encode() + retries = retries or self._number_of_retries for retry in range(retries + 1): log.debug(f"{self._name}: Write -> {data_encode!r}") @@ -454,7 +469,7 @@ async def _send_data(self, data: str, retries: int = 0) -> str: str_response = self.process_raw_response( command=data, response=ackless_response.decode() ) - self.raise_on_error(response=str_response) + self.raise_on_error(response=str_response, request=data) if self._ack in response[-1]: # Remove ack from response @@ -462,7 +477,7 @@ async def _send_data(self, data: str, retries: int = 0) -> str: str_response = self.process_raw_response( command=data, response=ackless_response.decode() ) - self.raise_on_error(response=str_response) + self.raise_on_error(response=str_response, request=data) return str_response log.info(f"{self._name}: retry number {retry}/{retries}") diff --git a/api/src/opentrons/drivers/command_builder.py b/api/src/opentrons/drivers/command_builder.py index 99ac5c7890c..ea90a12b946 100644 --- a/api/src/opentrons/drivers/command_builder.py +++ b/api/src/opentrons/drivers/command_builder.py @@ -6,7 +6,7 @@ class CommandBuilder: """Class used to build GCODE commands.""" - def __init__(self, terminator: str) -> None: + def __init__(self, terminator: str = "\n") -> None: """ Construct a command builder. @@ -17,7 +17,7 @@ def __init__(self, terminator: str) -> None: self._elements: List[str] = [] def add_float( - self, prefix: str, value: float, precision: Optional[int] + self, prefix: str, value: float, precision: Optional[int] = None ) -> CommandBuilder: """ Add a float value. diff --git a/api/src/opentrons/drivers/flex_stacker/__init__.py b/api/src/opentrons/drivers/flex_stacker/__init__.py new file mode 100644 index 00000000000..cd4866c179a --- /dev/null +++ b/api/src/opentrons/drivers/flex_stacker/__init__.py @@ -0,0 +1,9 @@ +from .abstract import AbstractStackerDriver +from .driver import FlexStackerDriver +from .simulator import SimulatingDriver + +__all__ = [ + "AbstractStackerDriver", + "FlexStackerDriver", + "SimulatingDriver", +] diff --git a/api/src/opentrons/drivers/flex_stacker/abstract.py b/api/src/opentrons/drivers/flex_stacker/abstract.py new file mode 100644 index 00000000000..5ba3cdcb026 --- /dev/null +++ b/api/src/opentrons/drivers/flex_stacker/abstract.py @@ -0,0 +1,89 @@ +from typing import Protocol + +from .types import ( + StackerAxis, + PlatformStatus, + Direction, + MoveParams, + StackerInfo, + LEDColor, +) + + +class AbstractStackerDriver(Protocol): + """Protocol for the Stacker driver.""" + + async def connect(self) -> None: + """Connect to stacker.""" + ... + + async def disconnect(self) -> None: + """Disconnect from stacker.""" + ... + + async def is_connected(self) -> bool: + """Check connection to stacker.""" + ... + + async def update_firmware(self, firmware_file_path: str) -> None: + """Updates the firmware on the device.""" + ... + + async def get_device_info(self) -> StackerInfo: + """Get Device Info.""" + ... + + async def set_serial_number(self, sn: str) -> bool: + """Set Serial Number.""" + ... + + async def stop_motors(self) -> bool: + """Stop all motor movement.""" + ... + + async def get_limit_switch(self, axis: StackerAxis, direction: Direction) -> bool: + """Get limit switch status. + + :return: True if limit switch is triggered, False otherwise + """ + ... + + async def get_platform_sensor(self, direction: Direction) -> bool: + """Get platform sensor status. + + :return: True if platform is present, False otherwise + """ + ... + + async def get_platform_status(self) -> PlatformStatus: + """Get platform status.""" + ... + + async def get_hopper_door_closed(self) -> bool: + """Get whether or not door is closed. + + :return: True if door is closed, False otherwise + """ + ... + + async def move_in_mm( + self, axis: StackerAxis, distance: float, params: MoveParams | None = None + ) -> bool: + """Move axis.""" + ... + + async def move_to_limit_switch( + self, axis: StackerAxis, direction: Direction, params: MoveParams | None = None + ) -> bool: + """Move until limit switch is triggered.""" + ... + + async def home_axis(self, axis: StackerAxis, direction: Direction) -> bool: + """Home axis.""" + ... + + async def set_led( + self, power: float, color: LEDColor | None = None, external: bool | None = None + ) -> bool: + """Set LED color of status bar.""" + ... diff --git a/api/src/opentrons/drivers/flex_stacker/driver.py b/api/src/opentrons/drivers/flex_stacker/driver.py new file mode 100644 index 00000000000..83671023772 --- /dev/null +++ b/api/src/opentrons/drivers/flex_stacker/driver.py @@ -0,0 +1,260 @@ +import asyncio +import re +from typing import Optional + +from opentrons.drivers.command_builder import CommandBuilder +from opentrons.drivers.asyncio.communication import AsyncResponseSerialConnection + +from .abstract import AbstractStackerDriver +from .types import ( + GCODE, + StackerAxis, + PlatformStatus, + Direction, + StackerInfo, + HardwareRevision, + MoveParams, + LimitSwitchStatus, + LEDColor, +) + + +FS_BAUDRATE = 115200 +DEFAULT_FS_TIMEOUT = 40 +FS_ACK = "OK\n" +FS_ERROR_KEYWORD = "err" +FS_ASYNC_ERROR_ACK = "async" +DEFAULT_COMMAND_RETRIES = 0 +GCODE_ROUNDING_PRECISION = 2 + + +class FlexStackerDriver(AbstractStackerDriver): + """FLEX Stacker driver.""" + + @classmethod + def parse_device_info(cls, response: str) -> StackerInfo: + """Parse stacker info.""" + # TODO: Validate serial number format once established + _RE = re.compile( + f"^{GCODE.DEVICE_INFO} FW:(?P\\S+) HW:Opentrons-flex-stacker-(?P\\S+) SerialNo:(?P\\S+)$" + ) + m = _RE.match(response) + if not m: + raise ValueError(f"Incorrect Response for device info: {response}") + return StackerInfo( + m.group("fw"), HardwareRevision(m.group("hw")), m.group("sn") + ) + + @classmethod + def parse_limit_switch_status(cls, response: str) -> LimitSwitchStatus: + """Parse limit switch statuses.""" + field_names = LimitSwitchStatus.get_fields() + pattern = r"\s".join([rf"{name}:(?P<{name}>\d)" for name in field_names]) + _RE = re.compile(f"^{GCODE.GET_LIMIT_SWITCH} {pattern}$") + m = _RE.match(response) + if not m: + raise ValueError(f"Incorrect Response for limit switch status: {response}") + return LimitSwitchStatus(*(bool(int(m.group(name))) for name in field_names)) + + @classmethod + def parse_platform_sensor_status(cls, response: str) -> PlatformStatus: + """Parse platform statuses.""" + field_names = PlatformStatus.get_fields() + pattern = r"\s".join([rf"{name}:(?P<{name}>\d)" for name in field_names]) + _RE = re.compile(f"^{GCODE.GET_PLATFORM_SENSOR} {pattern}$") + m = _RE.match(response) + if not m: + raise ValueError(f"Incorrect Response for platform status: {response}") + return PlatformStatus(*(bool(int(m.group(name))) for name in field_names)) + + @classmethod + def parse_door_closed(cls, response: str) -> bool: + """Parse door closed.""" + _RE = re.compile(r"^M122 D:(\d)$") + match = _RE.match(response) + if not match: + raise ValueError(f"Incorrect Response for door closed: {response}") + return bool(int(match.group(1))) + + @classmethod + def append_move_params( + cls, command: CommandBuilder, params: MoveParams | None + ) -> CommandBuilder: + """Append move params.""" + if params is not None: + if params.max_speed is not None: + command.add_float("V", params.max_speed, GCODE_ROUNDING_PRECISION) + if params.acceleration is not None: + command.add_float("A", params.acceleration, GCODE_ROUNDING_PRECISION) + if params.max_speed_discont is not None: + command.add_float( + "D", params.max_speed_discont, GCODE_ROUNDING_PRECISION + ) + return command + + @classmethod + async def create( + cls, port: str, loop: Optional[asyncio.AbstractEventLoop] + ) -> "FlexStackerDriver": + """Create a FLEX Stacker driver.""" + connection = await AsyncResponseSerialConnection.create( + port=port, + baud_rate=FS_BAUDRATE, + timeout=DEFAULT_FS_TIMEOUT, + number_of_retries=DEFAULT_COMMAND_RETRIES, + ack=FS_ACK, + loop=loop, + error_keyword=FS_ERROR_KEYWORD, + async_error_ack=FS_ASYNC_ERROR_ACK, + ) + return cls(connection) + + def __init__(self, connection: AsyncResponseSerialConnection) -> None: + """ + Constructor + + Args: + connection: Connection to the FLEX Stacker + """ + self._connection = connection + + async def connect(self) -> None: + """Connect to stacker.""" + await self._connection.open() + + async def disconnect(self) -> None: + """Disconnect from stacker.""" + await self._connection.close() + + async def is_connected(self) -> bool: + """Check connection to stacker.""" + return await self._connection.is_open() + + async def get_device_info(self) -> StackerInfo: + """Get Device Info.""" + response = await self._connection.send_command( + GCODE.DEVICE_INFO.build_command() + ) + await self._connection.send_command(GCODE.GET_RESET_REASON.build_command()) + return self.parse_device_info(response) + + async def set_serial_number(self, sn: str) -> bool: + """Set Serial Number.""" + # TODO: validate the serial number format + resp = await self._connection.send_command( + GCODE.SET_SERIAL_NUMBER.build_command().add_element(sn) + ) + if not re.match(rf"^{GCODE.SET_SERIAL_NUMBER}$", resp): + raise ValueError(f"Incorrect Response for set serial number: {resp}") + return True + + async def stop_motors(self) -> bool: + """Stop all motor movement.""" + resp = await self._connection.send_command(GCODE.STOP_MOTORS.build_command()) + if not re.match(rf"^{GCODE.STOP_MOTORS}$", resp): + raise ValueError(f"Incorrect Response for stop motors: {resp}") + return True + + async def get_limit_switch(self, axis: StackerAxis, direction: Direction) -> bool: + """Get limit switch status. + + :return: True if limit switch is triggered, False otherwise + """ + response = await self.get_limit_switches_status() + return response.get(axis, direction) + + async def get_limit_switches_status(self) -> LimitSwitchStatus: + """Get limit switch statuses for all axes.""" + response = await self._connection.send_command( + GCODE.GET_LIMIT_SWITCH.build_command() + ) + return self.parse_limit_switch_status(response) + + async def get_platform_sensor(self, direction: Direction) -> bool: + """Get platform sensor at one direction.""" + response = await self.get_platform_status() + return response.get(direction) + + async def get_platform_status(self) -> PlatformStatus: + """Get platform sensor status. + + :return: True if platform is detected, False otherwise + """ + response = await self._connection.send_command( + GCODE.GET_PLATFORM_SENSOR.build_command() + ) + return self.parse_platform_sensor_status(response) + + async def get_hopper_door_closed(self) -> bool: + """Get whether or not door is closed. + + :return: True if door is closed, False otherwise + """ + response = await self._connection.send_command( + GCODE.GET_DOOR_SWITCH.build_command() + ) + return self.parse_door_closed(response) + + async def move_in_mm( + self, axis: StackerAxis, distance: float, params: MoveParams | None = None + ) -> bool: + """Move axis.""" + command = self.append_move_params( + GCODE.MOVE_TO.build_command().add_float( + axis.name, distance, GCODE_ROUNDING_PRECISION + ), + params, + ) + resp = await self._connection.send_command(command) + if not re.match(rf"^{GCODE.MOVE_TO}$", resp): + raise ValueError(f"Incorrect Response for move to: {resp}") + return True + + async def move_to_limit_switch( + self, axis: StackerAxis, direction: Direction, params: MoveParams | None = None + ) -> bool: + """Move until limit switch is triggered.""" + command = self.append_move_params( + GCODE.MOVE_TO_SWITCH.build_command().add_int(axis.name, direction.value), + params, + ) + resp = await self._connection.send_command(command) + if not re.match(rf"^{GCODE.MOVE_TO_SWITCH}$", resp): + raise ValueError(f"Incorrect Response for move to switch: {resp}") + return True + + async def home_axis(self, axis: StackerAxis, direction: Direction) -> bool: + """Home axis.""" + resp = await self._connection.send_command( + GCODE.HOME_AXIS.build_command().add_int(axis.name, direction.value) + ) + if not re.match(rf"^{GCODE.HOME_AXIS}$", resp): + raise ValueError(f"Incorrect Response for home axis: {resp}") + return True + + async def set_led( + self, power: float, color: LEDColor | None = None, external: bool | None = None + ) -> bool: + """Set LED color. + + :param power: Power of the LED (0-1.0), 0 is off, 1 is full power + :param color: Color of the LED + :param external: True if external LED, False if internal LED + """ + power = max(0, min(power, 1.0)) + command = GCODE.SET_LED.build_command().add_float( + "P", power, GCODE_ROUNDING_PRECISION + ) + if color is not None: + command.add_int("C", color.value) + if external is not None: + command.add_int("E", external) + resp = await self._connection.send_command(command) + if not re.match(rf"^{GCODE.SET_LED}$", resp): + raise ValueError(f"Incorrect Response for set led: {resp}") + return True + + async def update_firmware(self, firmware_file_path: str) -> None: + """Updates the firmware on the device.""" + # TODO: Implement firmware update + pass diff --git a/api/src/opentrons/drivers/flex_stacker/simulator.py b/api/src/opentrons/drivers/flex_stacker/simulator.py new file mode 100644 index 00000000000..1e0b59b19de --- /dev/null +++ b/api/src/opentrons/drivers/flex_stacker/simulator.py @@ -0,0 +1,109 @@ +from typing import Optional + +from opentrons.util.async_helpers import ensure_yield + +from .abstract import AbstractStackerDriver +from .types import ( + StackerAxis, + PlatformStatus, + Direction, + StackerInfo, + HardwareRevision, + MoveParams, + LimitSwitchStatus, +) + + +class SimulatingDriver(AbstractStackerDriver): + """FLEX Stacker driver simulator.""" + + def __init__(self, serial_number: Optional[str] = None) -> None: + self._sn = serial_number or "dummySerialFS" + self._limit_switch_status = LimitSwitchStatus(False, False, False, False, False) + self._platform_sensor_status = PlatformStatus(False, False) + self._door_closed = True + + def set_limit_switch(self, status: LimitSwitchStatus) -> bool: + self._limit_switch_status = status + return True + + def set_platform_sensor(self, status: PlatformStatus) -> bool: + self._platform_sensor_status = status + return True + + def set_door_closed(self, door_closed: bool) -> bool: + self._door_closed = door_closed + return True + + @ensure_yield + async def connect(self) -> None: + """Connect to stacker.""" + pass + + @ensure_yield + async def disconnect(self) -> None: + """Disconnect from stacker.""" + pass + + @ensure_yield + async def is_connected(self) -> bool: + """Check connection to stacker.""" + return True + + @ensure_yield + async def get_device_info(self) -> StackerInfo: + """Get Device Info.""" + return StackerInfo(fw="stacker-fw", hw=HardwareRevision.EVT, sn=self._sn) + + @ensure_yield + async def set_serial_number(self, sn: str) -> bool: + """Set Serial Number.""" + return True + + @ensure_yield + async def stop_motor(self) -> bool: + """Stop motor movement.""" + return True + + @ensure_yield + async def get_limit_switch(self, axis: StackerAxis, direction: Direction) -> bool: + """Get limit switch status. + + :return: True if limit switch is triggered, False otherwise + """ + return self._limit_switch_status.get(axis, direction) + + @ensure_yield + async def get_limit_switches_status(self) -> LimitSwitchStatus: + """Get limit switch statuses for all axes.""" + return self._limit_switch_status + + @ensure_yield + async def get_platform_sensor_status(self) -> PlatformStatus: + """Get platform sensor status. + + :return: True if platform is detected, False otherwise + """ + return self._platform_sensor_status + + @ensure_yield + async def get_hopper_door_closed(self) -> bool: + """Get whether or not door is closed. + + :return: True if door is closed, False otherwise + """ + return self._door_closed + + @ensure_yield + async def move_in_mm( + self, axis: StackerAxis, distance: float, params: MoveParams | None = None + ) -> bool: + """Move axis.""" + return True + + @ensure_yield + async def move_to_limit_switch( + self, axis: StackerAxis, direction: Direction, params: MoveParams | None = None + ) -> bool: + """Move until limit switch is triggered.""" + return True diff --git a/api/src/opentrons/drivers/flex_stacker/types.py b/api/src/opentrons/drivers/flex_stacker/types.py new file mode 100644 index 00000000000..4035aaaa755 --- /dev/null +++ b/api/src/opentrons/drivers/flex_stacker/types.py @@ -0,0 +1,138 @@ +from enum import Enum +from dataclasses import dataclass, fields +from typing import List + +from opentrons.drivers.command_builder import CommandBuilder + + +class GCODE(str, Enum): + + MOVE_TO = "G0" + MOVE_TO_SWITCH = "G5" + HOME_AXIS = "G28" + STOP_MOTORS = "M0" + GET_RESET_REASON = "M114" + DEVICE_INFO = "M115" + GET_LIMIT_SWITCH = "M119" + SET_LED = "M200" + GET_PLATFORM_SENSOR = "M121" + GET_DOOR_SWITCH = "M122" + SET_SERIAL_NUMBER = "M996" + ENTER_BOOTLOADER = "dfu" + + def build_command(self) -> CommandBuilder: + """Build command.""" + return CommandBuilder().add_gcode(self) + + +STACKER_VID = 0x483 +STACKER_PID = 0xEF24 +STACKER_FREQ = 115200 + + +class HardwareRevision(Enum): + """Hardware Revision.""" + + NFF = "nff" + EVT = "a1" + + +@dataclass +class StackerInfo: + """Stacker Info.""" + + fw: str + hw: HardwareRevision + sn: str + + +class StackerAxis(Enum): + """Stacker Axis.""" + + X = "X" + Z = "Z" + L = "L" + + def __str__(self) -> str: + """Name.""" + return self.name + + +class LEDColor(Enum): + """Stacker LED Color.""" + + WHITE = 0 + RED = 1 + GREEN = 2 + BLUE = 3 + + +class Direction(Enum): + """Direction.""" + + RETRACT = 0 # negative + EXTENT = 1 # positive + + def __str__(self) -> str: + """Convert to tag for clear logging.""" + return "negative" if self == Direction.RETRACT else "positive" + + def opposite(self) -> "Direction": + """Get opposite direction.""" + return Direction.EXTENT if self == Direction.RETRACT else Direction.RETRACT + + def distance(self, distance: float) -> float: + """Get signed distance, where retract direction is negative.""" + return distance * -1 if self == Direction.RETRACT else distance + + +@dataclass +class LimitSwitchStatus: + """Stacker Limit Switch Statuses.""" + + XE: bool + XR: bool + ZE: bool + ZR: bool + LR: bool + + @classmethod + def get_fields(cls) -> List[str]: + """Get fields.""" + return [f.name for f in fields(cls)] + + def get(self, axis: StackerAxis, direction: Direction) -> bool: + """Get limit switch status.""" + if axis == StackerAxis.X: + return self.XE if direction == Direction.EXTENT else self.XR + if axis == StackerAxis.Z: + return self.ZE if direction == Direction.EXTENT else self.ZR + if direction == Direction.EXTENT: + raise ValueError("Latch does not have extent limit switch") + return self.LR + + +@dataclass +class PlatformStatus: + """Stacker Platform Statuses.""" + + E: bool + R: bool + + @classmethod + def get_fields(cls) -> List[str]: + """Get fields.""" + return [f.name for f in fields(cls)] + + def get(self, direction: Direction) -> bool: + """Get platform status.""" + return self.E if direction == Direction.EXTENT else self.R + + +@dataclass +class MoveParams: + """Move Parameters.""" + + max_speed: float | None = None + acceleration: float | None = None + max_speed_discont: float | None = None diff --git a/api/src/opentrons/drivers/heater_shaker/driver.py b/api/src/opentrons/drivers/heater_shaker/driver.py index 624e81d51d5..6ac4698330b 100644 --- a/api/src/opentrons/drivers/heater_shaker/driver.py +++ b/api/src/opentrons/drivers/heater_shaker/driver.py @@ -6,7 +6,10 @@ from typing import Optional, Dict from opentrons.drivers import utils from opentrons.drivers.command_builder import CommandBuilder -from opentrons.drivers.asyncio.communication import AsyncResponseSerialConnection +from opentrons.drivers.asyncio.communication import ( + AsyncResponseSerialConnection, + UnhandledGcode, +) from opentrons.drivers.heater_shaker.abstract import AbstractHeaterShakerDriver from opentrons.drivers.types import Temperature, RPM, HeaterShakerLabwareLatchStatus @@ -23,6 +26,7 @@ class GCODE(str, Enum): CLOSE_LABWARE_LATCH = "M243" GET_LABWARE_LATCH_STATE = "M241" DEACTIVATE_HEATER = "M106" + GET_RESET_REASON = "M114" HS_BAUDRATE = 115200 @@ -166,12 +170,23 @@ async def home(self) -> None: async def get_device_info(self) -> Dict[str, str]: """Send get-device-info command""" - c = CommandBuilder(terminator=HS_COMMAND_TERMINATOR).add_gcode( + device_info = CommandBuilder(terminator=HS_COMMAND_TERMINATOR).add_gcode( gcode=GCODE.GET_VERSION ) response = await self._connection.send_command( - command=c, retries=DEFAULT_COMMAND_RETRIES + command=device_info, retries=DEFAULT_COMMAND_RETRIES + ) + + reset_reason = CommandBuilder(terminator=HS_COMMAND_TERMINATOR).add_gcode( + gcode=GCODE.GET_RESET_REASON ) + try: + await self._connection.send_command( + command=reset_reason, retries=DEFAULT_COMMAND_RETRIES + ) + except UnhandledGcode: + pass + return utils.parse_hs_device_information(device_info_string=response) async def enter_programming_mode(self) -> None: diff --git a/api/src/opentrons/drivers/temp_deck/driver.py b/api/src/opentrons/drivers/temp_deck/driver.py index 6cb385f460f..fee495896e4 100644 --- a/api/src/opentrons/drivers/temp_deck/driver.py +++ b/api/src/opentrons/drivers/temp_deck/driver.py @@ -17,7 +17,7 @@ from opentrons.drivers import utils from opentrons.drivers.types import Temperature from opentrons.drivers.command_builder import CommandBuilder -from opentrons.drivers.asyncio.communication import SerialConnection +from opentrons.drivers.asyncio.communication import SerialConnection, UnhandledGcode from opentrons.drivers.temp_deck.abstract import AbstractTempDeckDriver log = logging.getLogger(__name__) @@ -31,6 +31,7 @@ class GCODE(str, Enum): GET_TEMP = "M105" SET_TEMP = "M104" DEVICE_INFO = "M115" + GET_RESET_REASON = "M114" DISENGAGE = "M18" PROGRAMMING_MODE = "dfu" @@ -154,10 +155,19 @@ async def get_device_info(self) -> Dict[str, str]: Example input from Temp-Deck's serial response: "serial:aa11bb22 model:aa11bb22 version:aa11bb22" """ - c = CommandBuilder(terminator=TEMP_DECK_COMMAND_TERMINATOR).add_gcode( + device_info = CommandBuilder(terminator=TEMP_DECK_COMMAND_TERMINATOR).add_gcode( gcode=GCODE.DEVICE_INFO ) - response = await self._send_command(command=c) + response = await self._send_command(command=device_info) + + reset_reason = CommandBuilder( + terminator=TEMP_DECK_COMMAND_TERMINATOR + ).add_gcode(gcode=GCODE.GET_RESET_REASON) + try: + await self._send_command(command=reset_reason) + except UnhandledGcode: + pass + return utils.parse_device_information(device_info_string=response) async def enter_programming_mode(self) -> None: diff --git a/api/src/opentrons/drivers/thermocycler/driver.py b/api/src/opentrons/drivers/thermocycler/driver.py index a3e6340c4f5..16090a2ed40 100644 --- a/api/src/opentrons/drivers/thermocycler/driver.py +++ b/api/src/opentrons/drivers/thermocycler/driver.py @@ -11,6 +11,7 @@ SerialConnection, AsyncResponseSerialConnection, AsyncSerial, + UnhandledGcode, ) from opentrons.drivers.thermocycler.abstract import AbstractThermocyclerDriver from opentrons.drivers.types import Temperature, PlateTemperature, ThermocyclerLidStatus @@ -33,6 +34,7 @@ class GCODE(str, Enum): DEACTIVATE_LID = "M108" DEACTIVATE_BLOCK = "M14" DEVICE_INFO = "M115" + GET_RESET_REASON = "M114" ENTER_PROGRAMMING = "dfu" @@ -94,7 +96,7 @@ async def create( name=port, ack=TC_GEN2_SERIAL_ACK, retry_wait_time_seconds=0.1, - error_keyword="error", + error_keyword="err", alarm_keyword="alarm", ) @@ -292,12 +294,13 @@ async def deactivate_block(self) -> None: async def get_device_info(self) -> Dict[str, str]: """Send get device info command""" - c = CommandBuilder(terminator=TC_COMMAND_TERMINATOR).add_gcode( + device_info = CommandBuilder(terminator=TC_COMMAND_TERMINATOR).add_gcode( gcode=GCODE.DEVICE_INFO ) response = await self._connection.send_command( - command=c, retries=DEFAULT_COMMAND_RETRIES + command=device_info, retries=DEFAULT_COMMAND_RETRIES ) + return utils.parse_device_information(device_info_string=response) async def enter_programming_mode(self) -> None: @@ -353,6 +356,17 @@ async def get_device_info(self) -> Dict[str, str]: response = await self._connection.send_command( command=c, retries=DEFAULT_COMMAND_RETRIES ) + + reset_reason = CommandBuilder(terminator=TC_COMMAND_TERMINATOR).add_gcode( + gcode=GCODE.GET_RESET_REASON + ) + try: + await self._connection.send_command( + command=reset_reason, retries=DEFAULT_COMMAND_RETRIES + ) + except UnhandledGcode: + pass + return utils.parse_hs_device_information(device_info_string=response) async def enter_programming_mode(self) -> None: diff --git a/api/src/opentrons/execute.py b/api/src/opentrons/execute.py index a9b3562d82b..998d6bc6597 100644 --- a/api/src/opentrons/execute.py +++ b/api/src/opentrons/execute.py @@ -560,7 +560,9 @@ def _create_live_context_pe( # Non-async would use call_soon_threadsafe(), which makes the waiting harder. async def add_all_extra_labware() -> None: for labware_definition_dict in extra_labware.values(): - labware_definition = LabwareDefinition.parse_obj(labware_definition_dict) + labware_definition = LabwareDefinition.model_validate( + labware_definition_dict + ) pe.add_labware_definition(labware_definition) # Add extra_labware to ProtocolEngine, being careful not to modify ProtocolEngine from this diff --git a/api/src/opentrons/hardware_control/__init__.py b/api/src/opentrons/hardware_control/__init__.py index d575a2eada5..b49f1462249 100644 --- a/api/src/opentrons/hardware_control/__init__.py +++ b/api/src/opentrons/hardware_control/__init__.py @@ -38,8 +38,7 @@ ] HardwareControlAPI = Union[OT2HardwareControlAPI, OT3HardwareControlAPI] -# this type ignore is because of https://github.com/python/mypy/issues/13437 -ThreadManagedHardware = ThreadManager[HardwareControlAPI] # type: ignore[misc] +ThreadManagedHardware = ThreadManager[HardwareControlAPI] SyncHardwareAPI = SynchronousAdapter[HardwareControlAPI] __all__ = [ diff --git a/api/src/opentrons/hardware_control/api.py b/api/src/opentrons/hardware_control/api.py index ec019ef2f1d..175c89dda7e 100644 --- a/api/src/opentrons/hardware_control/api.py +++ b/api/src/opentrons/hardware_control/api.py @@ -169,6 +169,16 @@ def _update_door_state(self, door_state: DoorState) -> None: def _reset_last_mount(self) -> None: self._last_moved_mount = None + def get_deck_from_machine( + self, machine_pos: Dict[Axis, float] + ) -> Dict[Axis, float]: + return deck_from_machine( + machine_pos=machine_pos, + attitude=self._robot_calibration.deck_calibration.attitude, + offset=top_types.Point(0, 0, 0), + robot_type=cast(RobotType, "OT-2 Standard"), + ) + @classmethod async def build_hardware_controller( # noqa: C901 cls, @@ -657,11 +667,8 @@ async def home(self, axes: Optional[List[Axis]] = None) -> None: async with self._motion_lock: if smoothie_gantry: smoothie_pos.update(await self._backend.home(smoothie_gantry)) - self._current_position = deck_from_machine( - machine_pos=self._axis_map_from_string_map(smoothie_pos), - attitude=self._robot_calibration.deck_calibration.attitude, - offset=top_types.Point(0, 0, 0), - robot_type=cast(RobotType, "OT-2 Standard"), + self._current_position = self.get_deck_from_machine( + self._axis_map_from_string_map(smoothie_pos) ) for plunger in plungers: await self._do_plunger_home(axis=plunger, acquire_lock=False) @@ -703,11 +710,8 @@ async def current_position( async with self._motion_lock: if refresh: smoothie_pos = await self._backend.update_position() - self._current_position = deck_from_machine( - machine_pos=self._axis_map_from_string_map(smoothie_pos), - attitude=self._robot_calibration.deck_calibration.attitude, - offset=top_types.Point(0, 0, 0), - robot_type=cast(RobotType, "OT-2 Standard"), + self._current_position = self.get_deck_from_machine( + self._axis_map_from_string_map(smoothie_pos) ) if mount == top_types.Mount.RIGHT: offset = top_types.Point(0, 0, 0) @@ -774,6 +778,7 @@ async def move_axes( position: Mapping[Axis, float], speed: Optional[float] = None, max_speeds: Optional[Dict[Axis, float]] = None, + expect_stalls: bool = False, ) -> None: """Moves the effectors of the specified axis to the specified position. The effector of the x,y axis is the center of the carriage. @@ -917,6 +922,16 @@ def engaged_axes(self) -> Dict[Axis, bool]: async def disengage_axes(self, which: List[Axis]) -> None: await self._backend.disengage_axes([ot2_axis_to_string(ax) for ax in which]) + def axis_is_present(self, axis: Axis) -> bool: + is_ot2 = axis in Axis.ot2_axes() + if not is_ot2: + return False + if axis in Axis.pipette_axes(): + mount = Axis.to_ot2_mount(axis) + if self.attached_pipettes.get(mount) is None: + return False + return True + @ExecutionManagerProvider.wait_for_running async def _fast_home(self, axes: Sequence[str], margin: float) -> Dict[str, float]: converted_axes = "".join(axes) @@ -938,11 +953,8 @@ async def retract_axis(self, axis: Axis, margin: float = 10) -> None: async with self._motion_lock: smoothie_pos = await self._fast_home(smoothie_ax, margin) - self._current_position = deck_from_machine( - machine_pos=self._axis_map_from_string_map(smoothie_pos), - attitude=self._robot_calibration.deck_calibration.attitude, - offset=top_types.Point(0, 0, 0), - robot_type=cast(RobotType, "OT-2 Standard"), + self._current_position = self.get_deck_from_machine( + self._axis_map_from_string_map(smoothie_pos) ) # Gantry/frame (i.e. not pipette) config API @@ -1237,7 +1249,10 @@ async def pick_up_tip( await self.prepare_for_aspirate(mount) async def tip_drop_moves( - self, mount: top_types.Mount, home_after: bool = True + self, + mount: top_types.Mount, + home_after: bool = True, + ignore_plunger: bool = False, ) -> None: spec, _ = self.plan_check_drop_tip(mount, home_after) @@ -1256,11 +1271,8 @@ async def tip_drop_moves( axes=[ot2_axis_to_string(ax) for ax in move.home_axes], margin=move.home_after_safety_margin, ) - self._current_position = deck_from_machine( - machine_pos=self._axis_map_from_string_map(smoothie_pos), - attitude=self._robot_calibration.deck_calibration.attitude, - offset=top_types.Point(0, 0, 0), - robot_type=cast(RobotType, "OT-2 Standard"), + self._current_position = self.get_deck_from_machine( + self._axis_map_from_string_map(smoothie_pos) ) for shake in spec.shake_moves: diff --git a/api/src/opentrons/hardware_control/backends/flex_protocol.py b/api/src/opentrons/hardware_control/backends/flex_protocol.py index 466e7890026..ef38af631e7 100644 --- a/api/src/opentrons/hardware_control/backends/flex_protocol.py +++ b/api/src/opentrons/hardware_control/backends/flex_protocol.py @@ -36,10 +36,9 @@ HepaFanState, HepaUVState, StatusBarState, + PipetteSensorResponseQueue, ) from opentrons.hardware_control.module_control import AttachedModulesControl -from opentrons_hardware.firmware_bindings.constants import SensorId -from opentrons_hardware.sensors.types import SensorDataType from ..dev_types import OT3AttachedInstruments from .types import HWStopCondition @@ -60,6 +59,14 @@ def restore_system_constraints(self) -> AsyncIterator[None]: def grab_pressure(self, channels: int, mount: OT3Mount) -> AsyncIterator[None]: ... + def set_pressure_sensor_available( + self, pipette_axis: Axis, available: bool + ) -> None: + ... + + def get_pressure_sensor_available(self, pipette_axis: Axis) -> bool: + ... + def update_constraints_for_gantry_load(self, gantry_load: GantryLoad) -> None: ... @@ -70,7 +77,11 @@ def update_constraints_for_calibration_with_gantry_load( ... def update_constraints_for_plunger_acceleration( - self, mount: OT3Mount, acceleration: float, gantry_load: GantryLoad + self, + mount: OT3Mount, + acceleration: float, + gantry_load: GantryLoad, + high_speed_pipette: bool = False, ) -> None: ... @@ -154,11 +165,10 @@ async def liquid_probe( threshold_pascals: float, plunger_impulse_time: float, num_baseline_reads: int, + z_offset_for_plunger_prep: float, probe: InstrumentProbeType = InstrumentProbeType.PRIMARY, force_both_sensors: bool = False, - response_queue: Optional[ - asyncio.Queue[Dict[SensorId, List[SensorDataType]]] - ] = None, + response_queue: Optional[PipetteSensorResponseQueue] = None, ) -> float: ... @@ -221,7 +231,7 @@ async def get_jaw_state(self) -> GripperJawState: ... async def tip_action( - self, origin: Dict[Axis, float], targets: List[Tuple[Dict[Axis, float], float]] + self, origin: float, targets: List[Tuple[float, float]] ) -> None: ... diff --git a/api/src/opentrons/hardware_control/backends/ot3controller.py b/api/src/opentrons/hardware_control/backends/ot3controller.py index 48787e86933..84ffbffd8da 100644 --- a/api/src/opentrons/hardware_control/backends/ot3controller.py +++ b/api/src/opentrons/hardware_control/backends/ot3controller.py @@ -104,7 +104,6 @@ ErrorCode, SensorId, ) -from opentrons_hardware.sensors.types import SensorDataType from opentrons_hardware.firmware_bindings.messages.message_definitions import ( StopRequest, ) @@ -142,6 +141,10 @@ EstopState, HardwareEventHandler, HardwareEventUnsubscriber, + PipetteSensorId, + PipetteSensorType, + PipetteSensorData, + PipetteSensorResponseQueue, ) from opentrons.hardware_control.errors import ( InvalidPipetteName, @@ -197,6 +200,7 @@ PipetteLiquidNotFoundError, CommunicationError, PythonException, + UnsupportedHardwareCommand, ) from .subsystem_manager import SubsystemManager @@ -211,6 +215,7 @@ from .types import HWStopCondition from .flex_protocol import FlexBackend from .status_bar_state import StatusBarStateController +from opentrons_hardware.sensors.types import SensorDataType log = logging.getLogger(__name__) @@ -362,6 +367,7 @@ def __init__( self._configuration.motion_settings, GantryLoad.LOW_THROUGHPUT ) ) + self._pressure_sensor_available: Dict[NodeId, bool] = {} @asynccontextmanager async def restore_system_constraints(self) -> AsyncIterator[None]: @@ -380,6 +386,16 @@ async def grab_pressure( async with grab_pressure(channels, tool, self._messenger): yield + def set_pressure_sensor_available( + self, pipette_axis: Axis, available: bool + ) -> None: + pip_node = axis_to_node(pipette_axis) + self._pressure_sensor_available[pip_node] = available + + def get_pressure_sensor_available(self, pipette_axis: Axis) -> bool: + pip_node = axis_to_node(pipette_axis) + return self._pressure_sensor_available[pip_node] + def update_constraints_for_calibration_with_gantry_load( self, gantry_load: GantryLoad, @@ -399,10 +415,18 @@ def update_constraints_for_gantry_load(self, gantry_load: GantryLoad) -> None: ) def update_constraints_for_plunger_acceleration( - self, mount: OT3Mount, acceleration: float, gantry_load: GantryLoad + self, + mount: OT3Mount, + acceleration: float, + gantry_load: GantryLoad, + high_speed_pipette: bool = False, ) -> None: new_constraints = get_system_constraints_for_plunger_acceleration( - self._configuration.motion_settings, gantry_load, mount, acceleration + self._configuration.motion_settings, + gantry_load, + mount, + acceleration, + high_speed_pipette, ) self._move_manager.update_constraints(new_constraints) @@ -597,30 +621,104 @@ async def update_encoder_position(self) -> OT3AxisMap[float]: return axis_convert(self._encoder_position, 0.0) def _handle_motor_status_response( - self, - response: NodeMap[MotorPositionStatus], + self, response: NodeMap[MotorPositionStatus], handle_gear_move: bool = False ) -> None: for axis, pos in response.items(): - self._position.update({axis: pos.motor_position}) - self._encoder_position.update({axis: pos.encoder_position}) - # TODO (FPS 6-01-2023): Remove this once the Feature Flag to ignore stall detection is removed. - # This check will latch the motor status for an axis at "true" if it was ever set to true. - # To account for the case where a motor axis has its power reset, we also depend on the - # "encoder_ok" flag staying set (it will only be False if the motor axis has not been - # homed since a power cycle) - motor_ok_latch = ( - (not self._feature_flags.stall_detection_enabled) - and ((axis in self._motor_status) and self._motor_status[axis].motor_ok) - and self._motor_status[axis].encoder_ok - ) - self._motor_status.update( - { - axis: MotorStatus( - motor_ok=(pos.motor_ok or motor_ok_latch), - encoder_ok=pos.encoder_ok, + if handle_gear_move and axis == NodeId.pipette_left: + self._gear_motor_position = {axis: pos.motor_position} + else: + self._position.update({axis: pos.motor_position}) + self._encoder_position.update({axis: pos.encoder_position}) + # TODO (FPS 6-01-2023): Remove this once the Feature Flag to ignore stall detection is removed. + # This check will latch the motor status for an axis at "true" if it was ever set to true. + # To account for the case where a motor axis has its power reset, we also depend on the + # "encoder_ok" flag staying set (it will only be False if the motor axis has not been + # homed since a power cycle) + motor_ok_latch = ( + (not self._feature_flags.stall_detection_enabled) + and ( + (axis in self._motor_status) + and self._motor_status[axis].motor_ok ) - } + and self._motor_status[axis].encoder_ok + ) + self._motor_status.update( + { + axis: MotorStatus( + motor_ok=(pos.motor_ok or motor_ok_latch), + encoder_ok=pos.encoder_ok, + ) + } + ) + + def _build_move_node_axis_runner( + self, + origin: Dict[Axis, float], + target: Dict[Axis, float], + speed: float, + stop_condition: HWStopCondition, + nodes_in_moves_only: bool, + ) -> Tuple[Optional[MoveGroupRunner], bool]: + if not target: + return None, False + move_target = MoveTarget.build(position=target, max_speed=speed) + try: + _, movelist = self._move_manager.plan_motion( + origin=origin, target_list=[move_target] ) + except ZeroLengthMoveError as zme: + log.debug(f"Not moving because move was zero length {str(zme)}") + return None, False + moves = movelist[0] + log.debug( + f"move: machine coordinates {target} from origin: machine coordinates {origin} at speed: {speed} requires {moves}" + ) + + ordered_nodes = self._motor_nodes() + if nodes_in_moves_only: + moving_axes = { + axis_to_node(ax) for move in moves for ax in move.unit_vector.keys() + } + ordered_nodes = ordered_nodes.intersection(moving_axes) + + move_group, _ = create_move_group( + origin, moves, ordered_nodes, MoveStopCondition[stop_condition.name] + ) + return ( + MoveGroupRunner( + move_groups=[move_group], + ignore_stalls=True + if not self._feature_flags.stall_detection_enabled + else False, + ), + False, + ) + + def _build_move_gear_axis_runner( + self, + possible_q_axis_origin: Optional[float], + possible_q_axis_target: Optional[float], + speed: float, + nodes_in_moves_only: bool, + ) -> Tuple[Optional[MoveGroupRunner], bool]: + if possible_q_axis_origin is None or possible_q_axis_target is None: + return None, True + tip_motor_move_group = self._build_tip_action_group( + possible_q_axis_origin, [(possible_q_axis_target, speed)] + ) + if nodes_in_moves_only: + ordered_nodes = self._motor_nodes() + + ordered_nodes.intersection({axis_to_node(Axis.Q)}) + return ( + MoveGroupRunner( + move_groups=[tip_motor_move_group], + ignore_stalls=True + if not self._feature_flags.stall_detection_enabled + else False, + ), + True, + ) @requires_update @requires_estop @@ -648,40 +746,52 @@ async def move( Returns: None """ - move_target = MoveTarget.build(position=target, max_speed=speed) - try: - _, movelist = self._move_manager.plan_motion( - origin=origin, target_list=[move_target] - ) - except ZeroLengthMoveError as zme: - log.debug(f"Not moving because move was zero length {str(zme)}") - return - moves = movelist[0] - log.info(f"move: machine {target} from {origin} requires {moves}") + possible_q_axis_origin = origin.pop(Axis.Q, None) + possible_q_axis_target = target.pop(Axis.Q, None) - ordered_nodes = self._motor_nodes() - if nodes_in_moves_only: - moving_axes = { - axis_to_node(ax) for move in moves for ax in move.unit_vector.keys() - } - ordered_nodes = ordered_nodes.intersection(moving_axes) - - group = create_move_group( - origin, moves, ordered_nodes, MoveStopCondition[stop_condition.name] + maybe_runners = ( + self._build_move_node_axis_runner( + origin, target, speed, stop_condition, nodes_in_moves_only + ), + self._build_move_gear_axis_runner( + possible_q_axis_origin, + possible_q_axis_target, + speed, + nodes_in_moves_only, + ), ) - move_group, _ = group - runner = MoveGroupRunner( - move_groups=[move_group], - ignore_stalls=True - if not self._feature_flags.stall_detection_enabled - else False, + log.debug(f"The move groups are {maybe_runners}.") + + gather_moving_nodes = set() + all_moving_nodes = set() + for runner, _ in maybe_runners: + if runner: + for n in runner.all_nodes(): + gather_moving_nodes.add(n) + for n in runner.all_moving_nodes(): + all_moving_nodes.add(n) + + pipettes_moving = moving_pipettes_in_move_group( + gather_moving_nodes, all_moving_nodes ) - pipettes_moving = moving_pipettes_in_move_group(move_group) - - async with self._monitor_overpressure(pipettes_moving): + async def _runner_coroutine( + runner: MoveGroupRunner, is_gear_move: bool + ) -> Tuple[Dict[NodeId, MotorPositionStatus], bool]: positions = await runner.run(can_messenger=self._messenger) - self._handle_motor_status_response(positions) + return positions, is_gear_move + + coros = [ + _runner_coroutine(runner, is_gear_move) + for runner, is_gear_move in maybe_runners + if runner + ] + checked_moving_pipettes = self._pipettes_to_monitor_pressure(pipettes_moving) + async with self._monitor_overpressure(checked_moving_pipettes): + all_positions = await asyncio.gather(*coros) + + for positions, handle_gear_move in all_positions: + self._handle_motor_status_response(positions, handle_gear_move) def _get_axis_home_distance(self, axis: Axis) -> float: if self.check_motor_status([axis]): @@ -786,7 +896,8 @@ async def home( moving_pipettes = [ axis_to_node(ax) for ax in checked_axes if ax in Axis.pipette_axes() ] - async with self._monitor_overpressure(moving_pipettes): + checked_moving_pipettes = self._pipettes_to_monitor_pressure(moving_pipettes) + async with self._monitor_overpressure(checked_moving_pipettes): positions = await asyncio.gather(*coros) # TODO(CM): default gear motor homing routine to have some acceleration if Axis.Q in checked_axes: @@ -796,10 +907,14 @@ async def home( Axis.to_kind(Axis.Q) ], ) + for position in positions: self._handle_motor_status_response(position) return axis_convert(self._position, 0.0) + def _pipettes_to_monitor_pressure(self, pipettes: List[NodeId]) -> List[NodeId]: + return [pip for pip in pipettes if self._pressure_sensor_available[pip]] + def _filter_move_group(self, move_group: MoveGroup) -> MoveGroup: new_group: MoveGroup = [] for step in move_group: @@ -840,17 +955,23 @@ async def home_tip_motors( self._gear_motor_position = {} raise e - async def tip_action( - self, origin: Dict[Axis, float], targets: List[Tuple[Dict[Axis, float], float]] - ) -> None: + def _build_tip_action_group( + self, origin: float, targets: List[Tuple[float, float]] + ) -> MoveGroup: move_targets = [ - MoveTarget.build(target_pos, speed) for target_pos, speed in targets + MoveTarget.build({Axis.Q: target_pos}, speed) + for target_pos, speed in targets ] _, moves = self._move_manager.plan_motion( - origin=origin, target_list=move_targets + origin={Axis.Q: origin}, target_list=move_targets ) - move_group = create_tip_action_group(moves[0], [NodeId.pipette_left], "clamp") + return create_tip_action_group(moves[0], [NodeId.pipette_left], "clamp") + + async def tip_action( + self, origin: float, targets: List[Tuple[float, float]] + ) -> None: + move_group = self._build_tip_action_group(origin, targets) runner = MoveGroupRunner( move_groups=[move_group], ignore_stalls=True @@ -915,10 +1036,12 @@ def _lookup_serial_key(pipette_name: FirmwarePipetteName) -> str: lookup_name = { FirmwarePipetteName.p1000_single: "P1KS", FirmwarePipetteName.p1000_multi: "P1KM", + FirmwarePipetteName.p1000_multi_em: "P1KP", FirmwarePipetteName.p50_single: "P50S", FirmwarePipetteName.p50_multi: "P50M", FirmwarePipetteName.p1000_96: "P1KH", FirmwarePipetteName.p50_96: "P50H", + FirmwarePipetteName.p200_96: "P2HH", } return lookup_name[pipette_name] @@ -949,6 +1072,7 @@ def _build_attached_pip( converted_name.pipette_type, converted_name.pipette_channels, converted_name.pipette_version, + converted_name.oem_type, ), "id": OT3Controller._combine_serial_number(attached), } @@ -1370,14 +1494,39 @@ async def liquid_probe( threshold_pascals: float, plunger_impulse_time: float, num_baseline_reads: int, + z_offset_for_plunger_prep: float, probe: InstrumentProbeType = InstrumentProbeType.PRIMARY, force_both_sensors: bool = False, - response_queue: Optional[ - asyncio.Queue[Dict[SensorId, List[SensorDataType]]] - ] = None, + response_queue: Optional[PipetteSensorResponseQueue] = None, ) -> float: head_node = axis_to_node(Axis.by_mount(mount)) tool = sensor_node_for_pipette(OT3Mount(mount.value)) + if tool not in self._pipettes_to_monitor_pressure([tool]): + raise UnsupportedHardwareCommand( + "Liquid Presence Detection not available on this pipette." + ) + + if response_queue is None: + response_capture: Optional[ + Callable[[Dict[SensorId, List[SensorDataType]]], None] + ] = None + else: + + def response_capture(data: Dict[SensorId, List[SensorDataType]]) -> None: + response_queue.put_nowait( + { + PipetteSensorId(sensor_id.value): [ + PipetteSensorData( + sensor_type=PipetteSensorType(packet.sensor_type.value), + _as_int=packet.to_int, + _as_float=packet.to_float(), + ) + for packet in packets + ] + for sensor_id, packets in data.items() + } + ) + positions = await liquid_probe( messenger=self._messenger, tool=tool, @@ -1388,9 +1537,10 @@ async def liquid_probe( threshold_pascals=threshold_pascals, plunger_impulse_time=plunger_impulse_time, num_baseline_reads=num_baseline_reads, + z_offset_for_plunger_prep=z_offset_for_plunger_prep, sensor_id=sensor_id_for_instrument(probe), force_both_sensors=force_both_sensors, - response_queue=response_queue, + emplace_data=response_capture, ) for node, point in positions.items(): self._position.update({node: point.motor_position}) diff --git a/api/src/opentrons/hardware_control/backends/ot3simulator.py b/api/src/opentrons/hardware_control/backends/ot3simulator.py index 017c90c45b3..b7466386be6 100644 --- a/api/src/opentrons/hardware_control/backends/ot3simulator.py +++ b/api/src/opentrons/hardware_control/backends/ot3simulator.py @@ -45,6 +45,7 @@ EstopPhysicalStatus, HardwareEventHandler, HardwareEventUnsubscriber, + PipetteSensorResponseQueue, ) from opentrons_shared_data.pipette.types import PipetteName, PipetteModel @@ -62,9 +63,9 @@ ) from opentrons.util.async_helpers import ensure_yield from .types import HWStopCondition -from .flex_protocol import FlexBackend -from opentrons_hardware.firmware_bindings.constants import SensorId -from opentrons_hardware.sensors.types import SensorDataType +from .flex_protocol import ( + FlexBackend, +) log = logging.getLogger(__name__) @@ -235,7 +236,11 @@ def update_constraints_for_calibration_with_gantry_load( self._sim_gantry_load = gantry_load def update_constraints_for_plunger_acceleration( - self, mount: OT3Mount, acceleration: float, gantry_load: GantryLoad + self, + mount: OT3Mount, + acceleration: float, + gantry_load: GantryLoad, + high_speed_pipette: bool = False, ) -> None: self._sim_gantry_load = gantry_load @@ -348,11 +353,10 @@ async def liquid_probe( threshold_pascals: float, plunger_impulse_time: float, num_baseline_reads: int, + z_offset_for_plunger_prep: float, probe: InstrumentProbeType = InstrumentProbeType.PRIMARY, force_both_sensors: bool = False, - response_queue: Optional[ - asyncio.Queue[Dict[SensorId, List[SensorDataType]]] - ] = None, + response_queue: Optional[PipetteSensorResponseQueue] = None, ) -> float: z_axis = Axis.by_mount(mount) pos = self._position @@ -439,10 +443,12 @@ async def get_jaw_state(self) -> GripperJawState: return self._sim_jaw_state async def tip_action( - self, origin: Dict[Axis, float], targets: List[Tuple[Dict[Axis, float], float]] + self, origin: float, targets: List[Tuple[float, float]] ) -> None: self._gear_motor_position.update( - coalesce_move_segments(origin, [target[0] for target in targets]) + coalesce_move_segments( + {Axis.Q: origin}, [{Axis.Q: target[0]} for target in targets] + ) ) await asyncio.sleep(0) @@ -505,6 +511,7 @@ def _attached_pipette_to_mount( converted_name.pipette_type, converted_name.pipette_channels, converted_name.pipette_version, + converted_name.oem_type, ), "id": None, } @@ -527,6 +534,7 @@ def _attached_pipette_to_mount( converted_name.pipette_type, converted_name.pipette_channels, converted_name.pipette_version, + converted_name.oem_type, ), "id": init_instr["id"], } @@ -538,6 +546,7 @@ def _attached_pipette_to_mount( converted_name.pipette_type, converted_name.pipette_channels, converted_name.pipette_version, + converted_name.oem_type, ), "id": None, } diff --git a/api/src/opentrons/hardware_control/backends/ot3utils.py b/api/src/opentrons/hardware_control/backends/ot3utils.py index 167f16f5cb8..cb8f5e95e71 100644 --- a/api/src/opentrons/hardware_control/backends/ot3utils.py +++ b/api/src/opentrons/hardware_control/backends/ot3utils.py @@ -2,7 +2,10 @@ from typing import Dict, Iterable, List, Set, Tuple, TypeVar, cast, Sequence, Optional from typing_extensions import Literal from logging import getLogger -from opentrons.config.defaults_ot3 import DEFAULT_CALIBRATION_AXIS_MAX_SPEED +from opentrons.config.defaults_ot3 import ( + DEFAULT_CALIBRATION_AXIS_MAX_SPEED, + DEFAULT_EMULSIFYING_PIPETTE_AXIS_MAX_SPEED, +) from opentrons.config.types import OT3MotionSettings, OT3CurrentSettings, GantryLoad from opentrons.hardware_control.types import ( Axis, @@ -281,12 +284,22 @@ def get_system_constraints_for_plunger_acceleration( gantry_load: GantryLoad, mount: OT3Mount, acceleration: float, + high_speed_pipette: bool = False, ) -> "SystemConstraints[Axis]": old_constraints = config.by_gantry_load(gantry_load) new_constraints = {} axis_kinds = set([k for _, v in old_constraints.items() for k in v.keys()]) + + def _get_axis_max_speed(ax: Axis) -> float: + if ax == Axis.of_main_tool_actuator(mount) and high_speed_pipette: + _max_speed = float(DEFAULT_EMULSIFYING_PIPETTE_AXIS_MAX_SPEED) + else: + _max_speed = old_constraints["default_max_speed"][axis_kind] + return _max_speed + for axis_kind in axis_kinds: for axis in Axis.of_kind(axis_kind): + _default_max_speed = _get_axis_max_speed(axis) if axis == Axis.of_main_tool_actuator(mount): _accel = acceleration else: @@ -295,7 +308,32 @@ def get_system_constraints_for_plunger_acceleration( _accel, old_constraints["max_speed_discontinuity"][axis_kind], old_constraints["direction_change_speed_discontinuity"][axis_kind], - old_constraints["default_max_speed"][axis_kind], + _default_max_speed, + ) + return new_constraints + + +def get_system_constraints_for_emulsifying_pipette( + config: OT3MotionSettings, + gantry_load: GantryLoad, + mount: OT3Mount, +) -> "SystemConstraints[Axis]": + old_constraints = config.by_gantry_load(gantry_load) + new_constraints = {} + axis_kinds = set([k for _, v in old_constraints.items() for k in v.keys()]) + for axis_kind in axis_kinds: + for axis in Axis.of_kind(axis_kind): + if axis == Axis.of_main_tool_actuator(mount): + _max_speed = float(DEFAULT_EMULSIFYING_PIPETTE_AXIS_MAX_SPEED) + else: + _max_speed = old_constraints["default_max_speed"][axis_kind] + new_constraints[axis] = AxisConstraints.build( + max_acceleration=old_constraints["acceleration"][axis_kind], + max_speed_discont=old_constraints["max_speed_discontinuity"][axis_kind], + max_direction_change_speed_discont=old_constraints[ + "direction_change_speed_discontinuity" + ][axis_kind], + max_speed=_max_speed, ) return new_constraints @@ -498,10 +536,10 @@ def create_gripper_jaw_hold_group(encoder_position_um: int) -> MoveGroup: return move_group -def moving_pipettes_in_move_group(group: MoveGroup) -> List[NodeId]: +def moving_pipettes_in_move_group( + all_nodes: Set[NodeId], moving_nodes: Set[NodeId] +) -> List[NodeId]: """Utility function to get which pipette nodes are moving either in z or their plunger.""" - all_nodes = [node for step in group for node, _ in step.items()] - moving_nodes = moving_axes_in_move_group(group) pipettes_moving: List[NodeId] = [ k for k in moving_nodes if k in [NodeId.pipette_left, NodeId.pipette_right] ] @@ -512,16 +550,6 @@ def moving_pipettes_in_move_group(group: MoveGroup) -> List[NodeId]: return pipettes_moving -def moving_axes_in_move_group(group: MoveGroup) -> Set[NodeId]: - """Utility function to get only the moving nodes in a move group.""" - ret: Set[NodeId] = set() - for step in group: - for node, node_step in step.items(): - if node_step.is_moving_step(): - ret.add(node) - return ret - - AxisMapPayload = TypeVar("AxisMapPayload") diff --git a/api/src/opentrons/hardware_control/dev_types.py b/api/src/opentrons/hardware_control/dev_types.py index a6773cb9184..981e95e114e 100644 --- a/api/src/opentrons/hardware_control/dev_types.py +++ b/api/src/opentrons/hardware_control/dev_types.py @@ -20,6 +20,7 @@ PipetteConfigurations, SupportedTipsDefinition, PipetteBoundingBoxOffsetDefinition, + AvailableSensorDefinition, ) from opentrons_shared_data.gripper import ( GripperModel, @@ -100,6 +101,9 @@ class PipetteDict(InstrumentDict): pipette_bounding_box_offsets: PipetteBoundingBoxOffsetDefinition current_nozzle_map: NozzleMap lld_settings: Optional[Dict[str, Dict[str, float]]] + plunger_positions: Dict[str, float] + shaft_ul_per_mm: float + available_sensors: AvailableSensorDefinition class PipetteStateDict(TypedDict): diff --git a/api/src/opentrons/hardware_control/emulation/heater_shaker.py b/api/src/opentrons/hardware_control/emulation/heater_shaker.py index a465de86312..b172cb9ee16 100644 --- a/api/src/opentrons/hardware_control/emulation/heater_shaker.py +++ b/api/src/opentrons/hardware_control/emulation/heater_shaker.py @@ -45,6 +45,7 @@ def __init__(self, parser: Parser, settings: HeaterShakerSettings) -> None: GCODE.HOME.value: self._home, GCODE.ENTER_BOOTLOADER.value: self._enter_bootloader, GCODE.GET_VERSION.value: self._get_version, + GCODE.GET_RESET_REASON.value: self._get_reset_reason, GCODE.OPEN_LABWARE_LATCH.value: self._open_labware_latch, GCODE.CLOSE_LABWARE_LATCH.value: self._close_labware_latch, GCODE.GET_LABWARE_LATCH_STATE.value: self._get_labware_latch_state, @@ -126,6 +127,9 @@ def _get_version(self, command: Command) -> str: f"SerialNo:{self._settings.serial_number}" ) + def _get_reset_reason(self, command: Command) -> str: + return "M114 Last Reset Reason: 01" + def _open_labware_latch(self, command: Command) -> str: self._latch_status = HeaterShakerLabwareLatchStatus.IDLE_OPEN return "M242" diff --git a/api/src/opentrons/hardware_control/emulation/module_server/client.py b/api/src/opentrons/hardware_control/emulation/module_server/client.py index 4108fe76069..5adcde0f267 100644 --- a/api/src/opentrons/hardware_control/emulation/module_server/client.py +++ b/api/src/opentrons/hardware_control/emulation/module_server/client.py @@ -66,7 +66,7 @@ async def read(self) -> Message: """Read a message from the module server.""" try: b = await self._reader.readuntil(MessageDelimiter) - m: Message = Message.parse_raw(b) + m: Message = Message.model_validate_json(b) return m except LimitOverrunError as e: raise ModuleServerClientError(str(e)) diff --git a/api/src/opentrons/hardware_control/emulation/module_server/server.py b/api/src/opentrons/hardware_control/emulation/module_server/server.py index 5a3d696eb7b..36878c342e3 100644 --- a/api/src/opentrons/hardware_control/emulation/module_server/server.py +++ b/api/src/opentrons/hardware_control/emulation/module_server/server.py @@ -53,7 +53,9 @@ def on_server_connected( self._connections[identifier] = connection for c in self._clients: c.write( - Message(status="connected", connections=[connection]).json().encode() + Message(status="connected", connections=[connection]) + .model_dump_json() + .encode() ) c.write(b"\n") @@ -72,7 +74,7 @@ def on_server_disconnected(self, identifier: str) -> None: for c in self._clients: c.write( Message(status="disconnected", connections=[connection]) - .json() + .model_dump_json() .encode() ) c.write(MessageDelimiter) @@ -95,7 +97,7 @@ async def _handle_connection( # A client connected. Send a dump of all connected modules. m = Message(status="dump", connections=list(self._connections.values())) - writer.write(m.json().encode()) + writer.write(m.model_dump_json().encode()) writer.write(MessageDelimiter) self._clients.add(writer) diff --git a/api/src/opentrons/hardware_control/emulation/settings.py b/api/src/opentrons/hardware_control/emulation/settings.py index 538b0281808..dd4da4dfc54 100644 --- a/api/src/opentrons/hardware_control/emulation/settings.py +++ b/api/src/opentrons/hardware_control/emulation/settings.py @@ -1,7 +1,8 @@ from typing import List from opentrons.hardware_control.emulation.types import ModuleType from opentrons.hardware_control.emulation.util import TEMPERATURE_ROOM -from pydantic import BaseSettings, BaseModel +from pydantic import BaseModel +from pydantic_settings import BaseSettings, SettingsConfigDict class PipetteSettings(BaseModel): @@ -113,8 +114,6 @@ class Settings(BaseSettings): emulator_port=9003, driver_port=9998 ) magdeck_proxy: ProxySettings = ProxySettings(emulator_port=9004, driver_port=9999) - - class Config: - env_prefix = "OT_EMULATOR_" + model_config = SettingsConfigDict(env_prefix="OT_EMULATOR_") module_server: ModuleServerSettings = ModuleServerSettings() diff --git a/api/src/opentrons/hardware_control/instruments/ot2/instrument_calibration.py b/api/src/opentrons/hardware_control/instruments/ot2/instrument_calibration.py index e093763dcd1..b3b82b22421 100644 --- a/api/src/opentrons/hardware_control/instruments/ot2/instrument_calibration.py +++ b/api/src/opentrons/hardware_control/instruments/ot2/instrument_calibration.py @@ -123,7 +123,8 @@ def load_tip_length_for_pipette( ) -> TipLengthCalibration: if isinstance(tiprack, LabwareDefinition): tiprack = typing.cast( - "TypeDictLabwareDef", tiprack.dict(exclude_none=True, exclude_unset=True) + "TypeDictLabwareDef", + tiprack.model_dump(exclude_none=True, exclude_unset=True), ) tip_length_data = calibration_storage.load_tip_length_calibration( diff --git a/api/src/opentrons/hardware_control/instruments/ot2/pipette.py b/api/src/opentrons/hardware_control/instruments/ot2/pipette.py index 7fc15c4c2d3..2205da161f1 100644 --- a/api/src/opentrons/hardware_control/instruments/ot2/pipette.py +++ b/api/src/opentrons/hardware_control/instruments/ot2/pipette.py @@ -28,7 +28,7 @@ CommandPreconditionViolated, ) from opentrons_shared_data.pipette.ul_per_mm import ( - piecewise_volume_conversion, + calculate_ul_per_mm, PIPETTING_FUNCTION_FALLBACK_VERSION, PIPETTING_FUNCTION_LATEST_VERSION, ) @@ -56,6 +56,7 @@ UlPerMmAction, PipetteName, PipetteModel, + PipetteOEMType, ) from opentrons.hardware_control.dev_types import InstrumentHardwareConfigs @@ -96,7 +97,7 @@ def __init__( use_old_aspiration_functions: bool = False, ) -> None: self._config = config - self._config_as_dict = config.dict() + self._config_as_dict = config.model_dump() self._pipette_offset = pipette_offset_cal self._pipette_type = self._config.pipette_type self._pipette_version = self._config.version @@ -112,17 +113,20 @@ def __init__( pipette_type=config.pipette_type, pipette_channels=config.channels, pipette_generation=config.display_category, + oem_type=PipetteOEMType.OT, ) self._acting_as = self._pipette_name self._pipette_model = PipetteModelVersionType( pipette_type=config.pipette_type, pipette_channels=config.channels, pipette_version=config.version, + oem_type=PipetteOEMType.OT, ) self._valid_nozzle_maps = load_pipette_data.load_valid_nozzle_maps( self._pipette_model.pipette_type, self._pipette_model.pipette_channels, self._pipette_model.pipette_version, + PipetteOEMType.OT, ) self._nozzle_offset = self._config.nozzle_offset self._nozzle_manager = ( @@ -189,7 +193,7 @@ def act_as(self, name: PipetteNameType) -> None: ], f"{self.name} is not back-compatible with {name}" liquid_model = load_pipette_data.load_liquid_model( - name.pipette_type, name.pipette_channels, name.get_version() + name.pipette_type, name.pipette_channels, name.get_version(), name.oem_type ) # TODO need to grab name config here to deal with act as test self._liquid_class.max_volume = liquid_model["default"].max_volume @@ -273,15 +277,16 @@ def update_config_item( self._config, elements, liquid_class ) # Update the cached dict representation - self._config_as_dict = self._config.dict() + self._config_as_dict = self._config.model_dump() def reload_configurations(self) -> None: self._config = load_pipette_data.load_definition( self._pipette_model.pipette_type, self._pipette_model.pipette_channels, self._pipette_model.pipette_version, + self._pipette_model.oem_type, ) - self._config_as_dict = self._config.dict() + self._config_as_dict = self._config.model_dump() def reset_state(self) -> None: self._current_volume = 0.0 @@ -584,21 +589,9 @@ def get_nominal_tip_overlap_dictionary_by_configuration( # want this to unbounded. @functools.lru_cache(maxsize=100) def ul_per_mm(self, ul: float, action: UlPerMmAction) -> float: - if action == "aspirate": - fallback = self._active_tip_settings.aspirate.default[ - PIPETTING_FUNCTION_FALLBACK_VERSION - ] - sequence = self._active_tip_settings.aspirate.default.get( - self._pipetting_function_version, fallback - ) - else: - fallback = self._active_tip_settings.dispense.default[ - PIPETTING_FUNCTION_FALLBACK_VERSION - ] - sequence = self._active_tip_settings.dispense.default.get( - self._pipetting_function_version, fallback - ) - return piecewise_volume_conversion(ul, sequence) + return calculate_ul_per_mm( + ul, action, self._active_tip_settings, self._pipetting_function_version + ) def __str__(self) -> str: return "{} current volume {}ul critical point: {} at {}".format( @@ -668,8 +661,8 @@ def _reload_and_check_skip( # Same config, good enough return attached_instr, True else: - newdict = new_config.dict() - olddict = attached_instr.config.dict() + newdict = new_config.model_dump() + olddict = attached_instr.config.model_dump() changed: Set[str] = set() for k in newdict.keys(): if newdict[k] != olddict[k]: diff --git a/api/src/opentrons/hardware_control/instruments/ot2/pipette_handler.py b/api/src/opentrons/hardware_control/instruments/ot2/pipette_handler.py index 931c99fd4c6..9da14196d52 100644 --- a/api/src/opentrons/hardware_control/instruments/ot2/pipette_handler.py +++ b/api/src/opentrons/hardware_control/instruments/ot2/pipette_handler.py @@ -230,7 +230,7 @@ def get_attached_instrument(self, mount: MountType) -> PipetteDict: result["current_nozzle_map"] = instr.nozzle_manager.current_configuration result["min_volume"] = instr.liquid_class.min_volume result["max_volume"] = instr.liquid_class.max_volume - result["channels"] = instr.channels + result["channels"] = instr._max_channels.value result["has_tip"] = instr.has_tip result["tip_length"] = instr.current_tip_length result["aspirate_speed"] = self.plunger_speed( @@ -260,6 +260,13 @@ def get_attached_instrument(self, mount: MountType) -> PipetteDict: "pipette_bounding_box_offsets" ] = instr.config.pipette_bounding_box_offsets result["lld_settings"] = instr.config.lld_settings + result["plunger_positions"] = { + "top": instr.plunger_positions.top, + "bottom": instr.plunger_positions.bottom, + "blow_out": instr.plunger_positions.blow_out, + "drop_tip": instr.plunger_positions.drop_tip, + } + result["shaft_ul_per_mm"] = instr.config.shaft_ul_per_mm return cast(PipetteDict, result) @property diff --git a/api/src/opentrons/hardware_control/instruments/ot3/gripper.py b/api/src/opentrons/hardware_control/instruments/ot3/gripper.py index ba49ea7d5e7..bd70547ee45 100644 --- a/api/src/opentrons/hardware_control/instruments/ot3/gripper.py +++ b/api/src/opentrons/hardware_control/instruments/ot3/gripper.py @@ -318,8 +318,8 @@ def _reload_gripper( # Same config, good enough return attached_instr, True else: - newdict = new_config.dict() - olddict = attached_instr.config.dict() + newdict = new_config.model_dump() + olddict = attached_instr.config.model_dump() changed: Set[str] = set() for k in newdict.keys(): if newdict[k] != olddict[k]: diff --git a/api/src/opentrons/hardware_control/instruments/ot3/pipette.py b/api/src/opentrons/hardware_control/instruments/ot3/pipette.py index 109747ea1b9..bd7671d745d 100644 --- a/api/src/opentrons/hardware_control/instruments/ot3/pipette.py +++ b/api/src/opentrons/hardware_control/instruments/ot3/pipette.py @@ -27,7 +27,7 @@ InvalidInstrumentData, ) from opentrons_shared_data.pipette.ul_per_mm import ( - piecewise_volume_conversion, + calculate_ul_per_mm, PIPETTING_FUNCTION_FALLBACK_VERSION, PIPETTING_FUNCTION_LATEST_VERSION, ) @@ -41,6 +41,8 @@ UlPerMmAction, PipetteName, PipetteModel, + Quirks, + PipetteOEMType, ) from opentrons_shared_data.pipette import ( load_data as load_pipette_data, @@ -78,7 +80,7 @@ def __init__( use_old_aspiration_functions: bool = False, ) -> None: self._config = config - self._config_as_dict = config.dict() + self._config_as_dict = config.model_dump() self._plunger_motor_current = config.plunger_motor_configurations self._pick_up_configurations = config.pick_up_tip_configurations self._plunger_homing_configurations = config.plunger_homing_configurations @@ -92,22 +94,26 @@ def __init__( self._liquid_class_name = pip_types.LiquidClasses.default self._liquid_class = self._config.liquid_properties[self._liquid_class_name] + oem = PipetteOEMType.get_oem_from_quirks(config.quirks) # TODO (lc 12-05-2022) figure out how we can safely deprecate "name" and "model" self._pipette_name = PipetteNameType( pipette_type=config.pipette_type, pipette_channels=config.channels, pipette_generation=config.display_category, + oem_type=oem, ) self._acting_as = self._pipette_name self._pipette_model = PipetteModelVersionType( pipette_type=config.pipette_type, pipette_channels=config.channels, pipette_version=config.version, + oem_type=oem, ) self._valid_nozzle_maps = load_pipette_data.load_valid_nozzle_maps( self._pipette_model.pipette_type, self._pipette_model.pipette_channels, self._pipette_model.pipette_version, + self._pipette_model.oem_type, ) self._nozzle_offset = self._config.nozzle_offset self._nozzle_manager = ( @@ -225,6 +231,9 @@ def active_tip_settings(self) -> SupportedTipsDefinition: def push_out_volume(self) -> float: return self._active_tip_settings.default_push_out_volume + def is_high_speed_pipette(self) -> bool: + return Quirks.highSpeed in self._config.quirks + def act_as(self, name: PipetteName) -> None: """Reconfigure to act as ``name``. ``name`` must be either the actual name of the pipette, or a name in its back-compatibility @@ -246,8 +255,9 @@ def reload_configurations(self) -> None: self._pipette_model.pipette_type, self._pipette_model.pipette_channels, self._pipette_model.pipette_version, + self._pipette_model.oem_type, ) - self._config_as_dict = self._config.dict() + self._config_as_dict = self._config.model_dump() def reset_state(self) -> None: self._current_volume = 0.0 @@ -529,23 +539,13 @@ def tip_presence_responses(self) -> int: # want this to unbounded. @functools.lru_cache(maxsize=100) def ul_per_mm(self, ul: float, action: UlPerMmAction) -> float: - if action == "aspirate": - fallback = self._active_tip_settings.aspirate.default[ - PIPETTING_FUNCTION_FALLBACK_VERSION - ] - sequence = self._active_tip_settings.aspirate.default.get( - self._pipetting_function_version, fallback - ) - elif action == "blowout": - return self._config.shaft_ul_per_mm - else: - fallback = self._active_tip_settings.dispense.default[ - PIPETTING_FUNCTION_FALLBACK_VERSION - ] - sequence = self._active_tip_settings.dispense.default.get( - self._pipetting_function_version, fallback - ) - return piecewise_volume_conversion(ul, sequence) + return calculate_ul_per_mm( + ul, + action, + self._active_tip_settings, + self._pipetting_function_version, + self._config.shaft_ul_per_mm, + ) def __str__(self) -> str: return "{} current volume {}ul critical point: {} at {}".format( @@ -585,6 +585,7 @@ def as_dict(self) -> "Pipette.DictType": "versioned_tip_overlap": self.tip_overlap, "back_compat_names": self._config.pipette_backcompat_names, "supported_tips": self.liquid_class.supported_tips, + "shaft_ul_per_mm": self._config.shaft_ul_per_mm, } ) return self._config_as_dict @@ -775,8 +776,8 @@ def _reload_and_check_skip( # Same config, good enough return attached_instr, True else: - newdict = new_config.dict() - olddict = attached_instr.config.dict() + newdict = new_config.model_dump() + olddict = attached_instr.config.model_dump() changed: Set[str] = set() for k in newdict.keys(): if newdict[k] != olddict[k]: diff --git a/api/src/opentrons/hardware_control/instruments/ot3/pipette_handler.py b/api/src/opentrons/hardware_control/instruments/ot3/pipette_handler.py index f64078fcbff..ef081b95a62 100644 --- a/api/src/opentrons/hardware_control/instruments/ot3/pipette_handler.py +++ b/api/src/opentrons/hardware_control/instruments/ot3/pipette_handler.py @@ -237,6 +237,7 @@ def get_attached_instrument(self, mount: OT3Mount) -> PipetteDict: "back_compat_names", "supported_tips", "lld_settings", + "available_sensors", ] instr_dict = instr.as_dict() @@ -248,7 +249,7 @@ def get_attached_instrument(self, mount: OT3Mount) -> PipetteDict: result["current_nozzle_map"] = instr.nozzle_manager.current_configuration result["min_volume"] = instr.liquid_class.min_volume result["max_volume"] = instr.liquid_class.max_volume - result["channels"] = instr._max_channels + result["channels"] = instr._max_channels.value result["has_tip"] = instr.has_tip result["tip_length"] = instr.current_tip_length result["aspirate_speed"] = self.plunger_speed( @@ -282,6 +283,14 @@ def get_attached_instrument(self, mount: OT3Mount) -> PipetteDict: "pipette_bounding_box_offsets" ] = instr.config.pipette_bounding_box_offsets result["lld_settings"] = instr.config.lld_settings + result["plunger_positions"] = { + "top": instr.plunger_positions.top, + "bottom": instr.plunger_positions.bottom, + "blow_out": instr.plunger_positions.blow_out, + "drop_tip": instr.plunger_positions.drop_tip, + } + result["shaft_ul_per_mm"] = instr.config.shaft_ul_per_mm + result["available_sensors"] = instr.config.available_sensors return cast(PipetteDict, result) @property diff --git a/api/src/opentrons/hardware_control/modules/mod_abc.py b/api/src/opentrons/hardware_control/modules/mod_abc.py index b07c6156a88..ebc0da2fa13 100644 --- a/api/src/opentrons/hardware_control/modules/mod_abc.py +++ b/api/src/opentrons/hardware_control/modules/mod_abc.py @@ -2,7 +2,7 @@ import asyncio import logging import re -from typing import ClassVar, Mapping, Optional, TypeVar, cast +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 @@ -31,7 +31,7 @@ def parse_fw_version(version: str) -> Version: raise InvalidVersion() except InvalidVersion: device_version = parse("v0.0.0") - return cast(Version, device_version) + return device_version class AbstractModule(abc.ABC): diff --git a/api/src/opentrons/hardware_control/motion_utilities.py b/api/src/opentrons/hardware_control/motion_utilities.py index 15604dfd360..dd59437f7dc 100644 --- a/api/src/opentrons/hardware_control/motion_utilities.py +++ b/api/src/opentrons/hardware_control/motion_utilities.py @@ -1,4 +1,6 @@ """Utilities for calculating motion correctly.""" +from logging import getLogger + from functools import lru_cache from typing import Callable, Dict, Union, Optional, cast from collections import OrderedDict @@ -11,6 +13,7 @@ from .types import Axis, OT3Mount +log = getLogger(__name__) # TODO: The offset_for_mount function should be defined with an overload # set, as with other functions in this module. Unfortunately, mypy < 0.920 @@ -36,6 +39,19 @@ # ) -> Point: # ... +EMPTY_ORDERED_DICT = OrderedDict( + ( + (Axis.X, 0.0), + (Axis.Y, 0.0), + (Axis.Z_L, 0.0), + (Axis.Z_R, 0.0), + (Axis.Z_G, 0.0), + (Axis.P_L, 0.0), + (Axis.P_R, 0.0), + (Axis.Q, 0.0), + ) +) + @lru_cache(4) def offset_for_mount( @@ -68,6 +84,7 @@ def target_position_from_absolute( ) primary_cp = get_critical_point(mount) primary_z = Axis.by_mount(mount) + target_position = OrderedDict( ( (Axis.X, abs_position.x - offset.x - primary_cp.x), @@ -97,6 +114,57 @@ def target_position_from_relative( return target_position +def target_axis_map_from_absolute( + primary_mount: Union[OT3Mount, Mount], + axis_map: Dict[Axis, float], + get_critical_point: Callable[[Union[Mount, OT3Mount]], Point], + left_mount_offset: Point, + right_mount_offset: Point, + gripper_mount_offset: Optional[Point] = None, +) -> "OrderedDict[Axis, float]": + """Create an absolute target position for all specified machine axes.""" + keys_for_target_position = list(axis_map.keys()) + + offset = offset_for_mount( + primary_mount, left_mount_offset, right_mount_offset, gripper_mount_offset + ) + primary_cp = get_critical_point(primary_mount) + primary_z = Axis.by_mount(primary_mount) + target_position = OrderedDict() + + if Axis.X in keys_for_target_position: + target_position[Axis.X] = axis_map[Axis.X] - offset.x - primary_cp.x + if Axis.Y in keys_for_target_position: + target_position[Axis.Y] = axis_map[Axis.Y] - offset.y - primary_cp.y + if primary_z in keys_for_target_position: + # Since this function is intended to be used in conjunction with `API.move_axes` + # we must leave out the carriage offset subtraction from the target position as + # `move_axes` already does this calculation. + target_position[primary_z] = axis_map[primary_z] - primary_cp.z + + target_position.update( + {ax: val for ax, val in axis_map.items() if ax not in Axis.gantry_axes()} + ) + return target_position + + +def target_axis_map_from_relative( + axis_map: Dict[Axis, float], + current_position: Dict[Axis, float], +) -> "OrderedDict[Axis, float]": + """Create a target position for all specified machine axes.""" + target_position = OrderedDict( + ( + (ax, current_position[ax] + axis_map[ax]) + for ax in EMPTY_ORDERED_DICT.keys() + if ax in axis_map.keys() + ) + ) + log.info(f"Current position {current_position} and axis map delta {axis_map}") + log.info(f"Relative move target {target_position}") + return target_position + + def target_position_from_plunger( mount: Union[Mount, OT3Mount], delta: float, diff --git a/api/src/opentrons/hardware_control/nozzle_manager.py b/api/src/opentrons/hardware_control/nozzle_manager.py index bf42476f7ee..a761b9bcbe4 100644 --- a/api/src/opentrons/hardware_control/nozzle_manager.py +++ b/api/src/opentrons/hardware_control/nozzle_manager.py @@ -1,11 +1,13 @@ from typing import Dict, List, Optional, Any, Sequence, Iterator, Tuple, cast from dataclasses import dataclass from collections import OrderedDict -from enum import Enum from itertools import chain from opentrons.hardware_control.types import CriticalPoint -from opentrons.types import Point +from opentrons.types import ( + Point, + NozzleConfigurationType, +) from opentrons_shared_data.pipette.pipette_definition import ( PipetteGeometryDefinition, PipetteRowDefinition, @@ -41,43 +43,6 @@ def _row_col_indices_for_nozzle( ) -class NozzleConfigurationType(Enum): - """ - Nozzle Configuration Type. - - Represents the current nozzle - configuration stored in NozzleMap - """ - - COLUMN = "COLUMN" - ROW = "ROW" - SINGLE = "SINGLE" - FULL = "FULL" - SUBRECT = "SUBRECT" - - @classmethod - def determine_nozzle_configuration( - cls, - physical_rows: "OrderedDict[str, List[str]]", - current_rows: "OrderedDict[str, List[str]]", - physical_cols: "OrderedDict[str, List[str]]", - current_cols: "OrderedDict[str, List[str]]", - ) -> "NozzleConfigurationType": - """ - Determine the nozzle configuration based on the starting and - ending nozzle. - """ - if physical_rows == current_rows and physical_cols == current_cols: - return NozzleConfigurationType.FULL - if len(current_rows) == 1 and len(current_cols) == 1: - return NozzleConfigurationType.SINGLE - if len(current_rows) == 1: - return NozzleConfigurationType.ROW - if len(current_cols) == 1: - return NozzleConfigurationType.COLUMN - return NozzleConfigurationType.SUBRECT - - @dataclass class NozzleMap: """ @@ -113,6 +78,28 @@ class NozzleMap: full_instrument_rows: Dict[str, List[str]] #: A map of all the rows of an instrument + @classmethod + def determine_nozzle_configuration( + cls, + physical_rows: "OrderedDict[str, List[str]]", + current_rows: "OrderedDict[str, List[str]]", + physical_cols: "OrderedDict[str, List[str]]", + current_cols: "OrderedDict[str, List[str]]", + ) -> "NozzleConfigurationType": + """ + Determine the nozzle configuration based on the starting and + ending nozzle. + """ + if physical_rows == current_rows and physical_cols == current_cols: + return NozzleConfigurationType.FULL + if len(current_rows) == 1 and len(current_cols) == 1: + return NozzleConfigurationType.SINGLE + if len(current_rows) == 1: + return NozzleConfigurationType.ROW + if len(current_cols) == 1: + return NozzleConfigurationType.COLUMN + return NozzleConfigurationType.SUBRECT + def __str__(self) -> str: return f"back_left_nozzle: {self.back_left} front_right_nozzle: {self.front_right} configuration: {self.configuration}" @@ -216,6 +203,16 @@ def tip_count(self) -> int: """The total number of active nozzles in the configuration, and thus the number of tips that will be picked up.""" return len(self.map_store) + @property + def physical_nozzle_count(self) -> int: + """The number of physical nozzles, regardless of configuration.""" + return len(self.full_instrument_map_store) + + @property + def active_nozzles(self) -> list[str]: + """An unstructured list of all nozzles active in the configuration.""" + return list(self.map_store.keys()) + @classmethod def build( # noqa: C901 cls, @@ -274,7 +271,7 @@ def build( # noqa: C901 ) if ( - NozzleConfigurationType.determine_nozzle_configuration( + cls.determine_nozzle_configuration( physical_rows, rows, physical_columns, columns ) != NozzleConfigurationType.FULL @@ -289,6 +286,7 @@ def build( # noqa: C901 if valid_nozzle_maps.maps[map_key] == list(map_store.keys()): validated_map_key = map_key break + if validated_map_key is None: raise IncompatibleNozzleConfiguration( "Attempted Nozzle Configuration does not match any approved map layout for the current pipette." @@ -302,7 +300,7 @@ def build( # noqa: C901 full_instrument_map_store=physical_nozzles, full_instrument_rows=physical_rows, columns=columns, - configuration=NozzleConfigurationType.determine_nozzle_configuration( + configuration=cls.determine_nozzle_configuration( physical_rows, rows, physical_columns, columns ), ) diff --git a/api/src/opentrons/hardware_control/ot3_calibration.py b/api/src/opentrons/hardware_control/ot3_calibration.py index b0ebcd027ce..9303add23d6 100644 --- a/api/src/opentrons/hardware_control/ot3_calibration.py +++ b/api/src/opentrons/hardware_control/ot3_calibration.py @@ -968,7 +968,7 @@ def load_attitude_matrix(to_default: bool = True) -> DeckCalibration: return DeckCalibration( attitude=apply_machine_transform(calibration_data.attitude), source=calibration_data.source, - status=types.CalibrationStatus(**calibration_data.status.dict()), + status=types.CalibrationStatus(**calibration_data.status.model_dump()), belt_attitude=calibration_data.attitude, last_modified=calibration_data.lastModified, pipette_calibrated_with=calibration_data.pipetteCalibratedWith, diff --git a/api/src/opentrons/hardware_control/ot3api.py b/api/src/opentrons/hardware_control/ot3api.py index 7f28d861a2c..038843e23ac 100644 --- a/api/src/opentrons/hardware_control/ot3api.py +++ b/api/src/opentrons/hardware_control/ot3api.py @@ -32,6 +32,7 @@ ) from opentrons_shared_data.pipette import ( pipette_load_name_conversions as pipette_load_name, + pipette_definition, ) from opentrons_shared_data.robot.types import RobotType @@ -45,7 +46,6 @@ LiquidProbeSettings, ) from opentrons.drivers.rpi_drivers.types import USBPort, PortGroup -from opentrons.hardware_control.nozzle_manager import NozzleConfigurationType from opentrons_shared_data.errors.exceptions import ( EnumeratedError, PythonException, @@ -98,6 +98,7 @@ EstopState, HardwareFeatureFlags, FailedTipStateCheck, + PipetteSensorResponseQueue, ) from .errors import ( UpdateOngoingError, @@ -143,8 +144,6 @@ from .backends.flex_protocol import FlexBackend from .backends.ot3simulator import OT3Simulator from .backends.errors import SubsystemUpdating -from opentrons_hardware.firmware_bindings.constants import SensorId -from opentrons_hardware.sensors.types import SensorDataType mod_log = logging.getLogger(__name__) @@ -299,8 +298,11 @@ async def set_system_constraints_for_calibration(self) -> None: async def set_system_constraints_for_plunger_acceleration( self, mount: OT3Mount, acceleration: float ) -> None: + high_speed_pipette = self._pipette_handler.get_pipette( + mount + ).is_high_speed_pipette() self._backend.update_constraints_for_plunger_acceleration( - mount, acceleration, self._gantry_load + mount, acceleration, self._gantry_load, high_speed_pipette ) @contextlib.asynccontextmanager @@ -353,7 +355,9 @@ def _update_estop_state(self, event: HardwareEvent) -> "List[Future[None]]": def _reset_last_mount(self) -> None: self._last_moved_mount = None - def _deck_from_machine(self, machine_pos: Dict[Axis, float]) -> Dict[Axis, float]: + def get_deck_from_machine( + self, machine_pos: Dict[Axis, float] + ) -> Dict[Axis, float]: return deck_from_machine( machine_pos=machine_pos, attitude=self._robot_calibration.deck_calibration.attitude, @@ -633,10 +637,31 @@ async def cache_pipette( self._feature_flags.use_old_aspiration_functions, ) self._pipette_handler.hardware_instruments[mount] = p + + if config is not None: + self._set_pressure_sensor_available(mount, instrument_config=config) + # TODO (lc 12-5-2022) Properly support backwards compatibility # when applicable return skipped + def get_pressure_sensor_available(self, mount: OT3Mount) -> bool: + pip_axis = Axis.of_main_tool_actuator(mount) + return self._backend.get_pressure_sensor_available(pip_axis) + + def _set_pressure_sensor_available( + self, + mount: OT3Mount, + instrument_config: pipette_definition.PipetteConfigurations, + ) -> None: + pressure_sensor_available = ( + "pressure" in instrument_config.available_sensors.sensors + ) + pip_axis = Axis.of_main_tool_actuator(mount) + self._backend.set_pressure_sensor_available( + pipette_axis=pip_axis, available=pressure_sensor_available + ) + async def cache_gripper(self, instrument_data: AttachedGripper) -> bool: """Set up gripper based on scanned information.""" grip_cal = load_gripper_calibration_offset(instrument_data.get("id")) @@ -775,6 +800,8 @@ async def _update_position_estimation( """ Function to update motor estimation for a set of axes """ + await self._backend.update_motor_status() + if axes is None: axes = [ax for ax in Axis] @@ -943,8 +970,8 @@ async def home_gear_motors(self) -> None: ): # move toward home until a safe distance await self._backend.tip_action( - origin={Axis.Q: current_pos_float}, - targets=[({Axis.Q: self._config.safe_home_distance}, 400)], + origin=current_pos_float, + targets=[(self._config.safe_home_distance, 400)], ) # update current position @@ -1021,14 +1048,14 @@ async def _refresh_jaw_state(self) -> None: async def _cache_current_position(self) -> Dict[Axis, float]: """Cache current position from backend and return in absolute deck coords.""" - self._current_position = self._deck_from_machine( + self._current_position = self.get_deck_from_machine( await self._backend.update_position() ) return self._current_position async def _cache_encoder_position(self) -> Dict[Axis, float]: """Cache encoder position from backend and return in absolute deck coords.""" - self._encoder_position = self._deck_from_machine( + self._encoder_position = self.get_deck_from_machine( await self._backend.update_encoder_position() ) if self.has_gripper(): @@ -1162,7 +1189,7 @@ async def move_to( speed: Optional[float] = None, critical_point: Optional[CriticalPoint] = None, max_speeds: Union[None, Dict[Axis, float], OT3AxisMap[float]] = None, - _expect_stalls: bool = False, + expect_stalls: bool = False, ) -> None: """Move the critical point of the specified mount to a location relative to the deck, at the specified speed.""" @@ -1206,7 +1233,7 @@ async def move_to( target_position, speed=speed, max_speeds=checked_max, - expect_stalls=_expect_stalls, + expect_stalls=expect_stalls, ) async def move_axes( # noqa: C901 @@ -1214,6 +1241,7 @@ async def move_axes( # noqa: C901 position: Mapping[Axis, float], speed: Optional[float] = None, max_speeds: Optional[Dict[Axis, float]] = None, + expect_stalls: bool = False, ) -> None: """Moves the effectors of the specified axis to the specified position. The effector of the x,y axis is the center of the carriage. @@ -1228,7 +1256,9 @@ async def move_axes( # noqa: C901 message=f"{axis} is not present", detail={"axis": str(axis)} ) + self._log.info(f"Attempting to move {position} with speed {speed}.") if not self._backend.check_encoder_status(list(position.keys())): + self._log.info("Calling home in move_axes") await self.home() self._assert_motor_ok(list(position.keys())) @@ -1267,7 +1297,11 @@ async def move_axes( # noqa: C901 if axis not in absolute_positions: absolute_positions[axis] = position_value - await self._move(target_position=absolute_positions, speed=speed) + await self._move( + target_position=absolute_positions, + speed=speed, + expect_stalls=expect_stalls, + ) async def move_rel( self, @@ -1277,7 +1311,7 @@ async def move_rel( max_speeds: Union[None, Dict[Axis, float], OT3AxisMap[float]] = None, check_bounds: MotionChecks = MotionChecks.NONE, fail_on_not_homed: bool = False, - _expect_stalls: bool = False, + expect_stalls: bool = False, ) -> None: """Move the critical point of the specified mount by a specified displacement in a specified direction, at the specified speed.""" @@ -1319,7 +1353,7 @@ async def move_rel( speed=speed, max_speeds=checked_max, check_bounds=check_bounds, - expect_stalls=_expect_stalls, + expect_stalls=expect_stalls, ) async def _cache_and_maybe_retract_mount(self, mount: OT3Mount) -> None: @@ -1439,6 +1473,10 @@ async def _move( check_motion_bounds(to_check, target_position, bounds, check_bounds) self._log.info(f"Move: deck {target_position} becomes machine {machine_pos}") origin = await self._backend.update_position() + + if self._gantry_load == GantryLoad.HIGH_THROUGHPUT: + origin[Axis.Q] = self._backend.gear_motor_position or 0.0 + async with contextlib.AsyncExitStack() as stack: if acquire_lock: await stack.enter_async_context(self._motion_lock) @@ -1640,7 +1678,12 @@ async def disengage_axes(self, which: List[Axis]) -> None: await self._backend.disengage_axes(which) async def engage_axes(self, which: List[Axis]) -> None: - await self._backend.engage_axes(which) + await self._backend.engage_axes( + [axis for axis in which if self._backend.axis_is_present(axis)] + ) + + def axis_is_present(self, axis: Axis) -> bool: + return self._backend.axis_is_present(axis) async def get_limit_switches(self) -> Dict[Axis, bool]: res = await self._backend.get_limit_switches() @@ -1826,7 +1869,7 @@ async def tip_pickup_moves( if ( self.gantry_load == GantryLoad.HIGH_THROUGHPUT and instrument.nozzle_manager.current_configuration.configuration - == NozzleConfigurationType.FULL + == top_types.NozzleConfigurationType.FULL ): spec = self._pipette_handler.plan_ht_pick_up_tip( instrument.nozzle_manager.current_configuration.tip_count @@ -2156,8 +2199,8 @@ async def _high_throughput_check_tip(self) -> AsyncIterator[None]: # only move tip motors if they are not already below the sensor if tip_motor_pos_float < tip_presence_check_target: await self._backend.tip_action( - origin={Axis.Q: tip_motor_pos_float}, - targets=[({Axis.Q: tip_presence_check_target}, 400)], + origin=tip_motor_pos_float, + targets=[(tip_presence_check_target, 400)], ) try: yield @@ -2228,11 +2271,11 @@ async def _tip_motor_action( gear_origin_float = self._backend.gear_motor_position or 0.0 move_targets = [ - ({Axis.Q: move_segment.distance}, move_segment.speed or 400) + (move_segment.distance, move_segment.speed or 400) for move_segment in pipette_spec ] await self._backend.tip_action( - origin={Axis.Q: gear_origin_float}, targets=move_targets + origin=gear_origin_float, targets=move_targets ) await self.home_gear_motors() @@ -2282,11 +2325,16 @@ def set_working_volume( instrument.working_volume = tip_volume async def tip_drop_moves( - self, mount: Union[top_types.Mount, OT3Mount], home_after: bool = False + self, + mount: Union[top_types.Mount, OT3Mount], + home_after: bool = False, + ignore_plunger: bool = False, ) -> None: realmount = OT3Mount.from_mount(mount) - - await self._move_to_plunger_bottom(realmount, rate=1.0, check_current_vol=False) + if ignore_plunger is False: + await self._move_to_plunger_bottom( + realmount, rate=1.0, check_current_vol=False + ) if self.gantry_load == GantryLoad.HIGH_THROUGHPUT: spec = self._pipette_handler.plan_ht_drop_tip() @@ -2557,7 +2605,7 @@ def get_instrument_max_height( mount: Union[top_types.Mount, OT3Mount], critical_point: Optional[CriticalPoint] = None, ) -> float: - carriage_pos = self._deck_from_machine(self._backend.home_position()) + carriage_pos = self.get_deck_from_machine(self._backend.home_position()) pos_at_home = self._effector_pos_from_carriage_pos( OT3Mount.from_mount(mount), carriage_pos, critical_point ) @@ -2639,10 +2687,9 @@ async def _liquid_probe_pass( probe_settings: LiquidProbeSettings, probe: InstrumentProbeType, p_travel: float, + z_offset_for_plunger_prep: float, force_both_sensors: bool = False, - response_queue: Optional[ - asyncio.Queue[Dict[SensorId, List[SensorDataType]]] - ] = None, + response_queue: Optional[PipetteSensorResponseQueue] = None, ) -> float: plunger_direction = -1 if probe_settings.aspirate_while_sensing else 1 end_z = await self._backend.liquid_probe( @@ -2653,13 +2700,14 @@ async def _liquid_probe_pass( probe_settings.sensor_threshold_pascals, probe_settings.plunger_impulse_time, probe_settings.samples_for_baselining, + z_offset_for_plunger_prep, probe=probe, force_both_sensors=force_both_sensors, response_queue=response_queue, ) machine_pos = await self._backend.update_position() machine_pos[Axis.by_mount(mount)] = end_z - deck_end_z = self._deck_from_machine(machine_pos)[Axis.by_mount(mount)] + deck_end_z = self.get_deck_from_machine(machine_pos)[Axis.by_mount(mount)] offset = offset_for_mount( mount, top_types.Point(*self._config.left_mount_offset), @@ -2676,9 +2724,7 @@ async def liquid_probe( # noqa: C901 probe_settings: Optional[LiquidProbeSettings] = None, probe: Optional[InstrumentProbeType] = None, force_both_sensors: bool = False, - response_queue: Optional[ - asyncio.Queue[Dict[SensorId, List[SensorDataType]]] - ] = None, + response_queue: Optional[PipetteSensorResponseQueue] = None, ) -> float: """Search for and return liquid level height. @@ -2804,6 +2850,7 @@ async def prep_plunger_for_probe_move( probe_settings, checked_probe, plunger_travel_mm + sensor_baseline_plunger_move_mm, + z_offset_for_plunger_prep, force_both_sensors, response_queue, ) diff --git a/api/src/opentrons/hardware_control/protocols/gripper_controller.py b/api/src/opentrons/hardware_control/protocols/gripper_controller.py index fc81325193c..1b81f4ab460 100644 --- a/api/src/opentrons/hardware_control/protocols/gripper_controller.py +++ b/api/src/opentrons/hardware_control/protocols/gripper_controller.py @@ -14,6 +14,9 @@ async def grip( ) -> None: ... + async def home_gripper_jaw(self) -> None: + ... + async def ungrip(self, force_newtons: Optional[float] = None) -> None: """Release gripped object. diff --git a/api/src/opentrons/hardware_control/protocols/hardware_manager.py b/api/src/opentrons/hardware_control/protocols/hardware_manager.py index ee0228ae3b8..d2bfd94a06b 100644 --- a/api/src/opentrons/hardware_control/protocols/hardware_manager.py +++ b/api/src/opentrons/hardware_control/protocols/hardware_manager.py @@ -1,7 +1,7 @@ from typing import Dict, Optional from typing_extensions import Protocol -from ..types import SubSystem, SubSystemState +from ..types import SubSystem, SubSystemState, Axis class HardwareManager(Protocol): @@ -45,3 +45,7 @@ def attached_subsystems(self) -> Dict[SubSystem, SubSystemState]: async def get_serial_number(self) -> Optional[str]: """Get the robot serial number, if provisioned. If not provisioned, will be None.""" ... + + def axis_is_present(self, axis: Axis) -> bool: + """Get whether a motor axis is present on the machine.""" + ... diff --git a/api/src/opentrons/hardware_control/protocols/liquid_handler.py b/api/src/opentrons/hardware_control/protocols/liquid_handler.py index 8707fc33024..090b7dfec93 100644 --- a/api/src/opentrons/hardware_control/protocols/liquid_handler.py +++ b/api/src/opentrons/hardware_control/protocols/liquid_handler.py @@ -1,6 +1,8 @@ from typing import Optional from typing_extensions import Protocol +from opentrons.types import Point +from opentrons.hardware_control.types import CriticalPoint from .types import MountArgType, CalibrationType, ConfigType from .instrument_configurer import InstrumentConfigurer @@ -16,6 +18,22 @@ class LiquidHandler( Calibratable[CalibrationType], Protocol[CalibrationType, MountArgType, ConfigType], ): + def critical_point_for( + self, + mount: MountArgType, + cp_override: Optional[CriticalPoint] = None, + ) -> Point: + """ + Determine the current critical point for the specified mount. + + :param mount: A robot mount that the instrument is on. + :param cp_override: The critical point override to use. + + If no critical point override is specified, the robot defaults to nozzle location `A1` or the mount critical point. + :return: Point. + """ + ... + async def update_nozzle_configuration_for_mount( self, mount: MountArgType, @@ -165,7 +183,10 @@ async def pick_up_tip( ... async def tip_drop_moves( - self, mount: MountArgType, home_after: bool = True + self, + mount: MountArgType, + home_after: bool = True, + ignore_plunger: bool = False, ) -> None: ... diff --git a/api/src/opentrons/hardware_control/protocols/motion_controller.py b/api/src/opentrons/hardware_control/protocols/motion_controller.py index daaf166f283..77f78506506 100644 --- a/api/src/opentrons/hardware_control/protocols/motion_controller.py +++ b/api/src/opentrons/hardware_control/protocols/motion_controller.py @@ -9,6 +9,12 @@ class MotionController(Protocol[MountArgType]): """Protocol specifying fundamental motion controls.""" + def get_deck_from_machine( + self, machine_pos: Dict[Axis, float] + ) -> Dict[Axis, float]: + """Convert machine coordinates to deck coordinates.""" + ... + async def halt(self, disengage_before_stopping: bool = False) -> None: """Immediately stop motion. @@ -165,6 +171,7 @@ async def move_axes( position: Mapping[Axis, float], speed: Optional[float] = None, max_speeds: Optional[Dict[Axis, float]] = None, + expect_stalls: bool = False, ) -> None: """Moves the effectors of the specified axis to the specified position. The effector of the x,y axis is the center of the carriage. diff --git a/api/src/opentrons/hardware_control/robot_calibration.py b/api/src/opentrons/hardware_control/robot_calibration.py index 270344fff2f..8ecf6b67be6 100644 --- a/api/src/opentrons/hardware_control/robot_calibration.py +++ b/api/src/opentrons/hardware_control/robot_calibration.py @@ -154,7 +154,7 @@ def load_attitude_matrix() -> DeckCalibration: return DeckCalibration( attitude=calibration_data.attitude, source=calibration_data.source, - status=types.CalibrationStatus(**calibration_data.status.dict()), + status=types.CalibrationStatus(**calibration_data.status.model_dump()), last_modified=calibration_data.last_modified, pipette_calibrated_with=calibration_data.pipette_calibrated_with, tiprack=calibration_data.tiprack, diff --git a/api/src/opentrons/hardware_control/types.py b/api/src/opentrons/hardware_control/types.py index bc32431d2a5..4e3bb875498 100644 --- a/api/src/opentrons/hardware_control/types.py +++ b/api/src/opentrons/hardware_control/types.py @@ -1,3 +1,4 @@ +from asyncio import Queue import enum import logging from dataclasses import dataclass @@ -712,3 +713,63 @@ def __init__( super().__init__( f"Expected tip state {expected_state}, but received {actual_state}." ) + + +@enum.unique +class PipetteSensorId(int, enum.Enum): + """Sensor IDs available. + + Not to be confused with SensorType. This is the ID value that separate + two or more of the same type of sensor within a system. + + Note that this is a copy of an enum defined in opentrons_hardware.firmware_bindings.constants. That version + is authoritative; this version is here because this data is exposed above the hardware control layer and + therefore needs a typing source here so that we don't create a dependency on the internal hardware package. + """ + + S0 = 0x0 + S1 = 0x1 + UNUSED = 0x2 + BOTH = 0x3 + + +@enum.unique +class PipetteSensorType(int, enum.Enum): + """Sensor types available. + + Note that this is a copy of an enum defined in opentrons_hardware.firmware_bindings.constants. That version + is authoritative; this version is here because this data is exposed above the hardware control layer and + therefore needs a typing source here so that we don't create a dependency on the internal hardware package. + """ + + tip = 0x00 + capacitive = 0x01 + environment = 0x02 + pressure = 0x03 + pressure_temperature = 0x04 + humidity = 0x05 + temperature = 0x06 + + +@dataclass(frozen=True) +class PipetteSensorData: + """Sensor data from a monitored sensor. + + Note that this is a copy of an enum defined in opentrons_hardware.firmware_bindings.constants. That version + is authoritative; this version is here because this data is exposed above the hardware control layer and + therefore needs a typing source here so that we don't create a dependency on the internal hardware package. + """ + + sensor_type: PipetteSensorType + _as_int: int + _as_float: float + + def to_float(self) -> float: + return self._as_float + + @property + def to_int(self) -> int: + return self._as_int + + +PipetteSensorResponseQueue = Queue[Dict[PipetteSensorId, List[PipetteSensorData]]] diff --git a/api/src/opentrons/legacy_commands/commands.py b/api/src/opentrons/legacy_commands/commands.py index 68b6f1a0595..fbbb14d7fc4 100755 --- a/api/src/opentrons/legacy_commands/commands.py +++ b/api/src/opentrons/legacy_commands/commands.py @@ -299,3 +299,40 @@ def move_to_disposal_location( "name": command_types.MOVE_TO_DISPOSAL_LOCATION, "payload": {"instrument": instrument, "location": location, "text": text}, } + + +def seal( + instrument: InstrumentContext, + location: Well, +) -> command_types.SealCommand: + location_text = stringify_location(location) + text = f"Sealing to {location_text}" + return { + "name": command_types.SEAL, + "payload": {"instrument": instrument, "location": location, "text": text}, + } + + +def unseal( + instrument: InstrumentContext, + location: Well, +) -> command_types.UnsealCommand: + location_text = stringify_location(location) + text = f"Unsealing from {location_text}" + return { + "name": command_types.UNSEAL, + "payload": {"instrument": instrument, "location": location, "text": text}, + } + + +def resin_tip_dispense( + instrument: InstrumentContext, + flow_rate: float | None, +) -> command_types.PressurizeCommand: + if flow_rate is None: + flow_rate = 10 # The Protocol Engine default for Resin Tip Dispense + text = f"Pressurize pipette to dispense from resin tip at {flow_rate}uL/s." + return { + "name": command_types.PRESSURIZE, + "payload": {"instrument": instrument, "text": text}, + } diff --git a/api/src/opentrons/legacy_commands/types.py b/api/src/opentrons/legacy_commands/types.py index 5aaa72b8e09..61302985c2c 100755 --- a/api/src/opentrons/legacy_commands/types.py +++ b/api/src/opentrons/legacy_commands/types.py @@ -43,6 +43,10 @@ RETURN_TIP: Final = "command.RETURN_TIP" MOVE_TO: Final = "command.MOVE_TO" MOVE_TO_DISPOSAL_LOCATION: Final = "command.MOVE_TO_DISPOSAL_LOCATION" +SEAL: Final = "command.SEAL" +UNSEAL: Final = "command.UNSEAL" +PRESSURIZE: Final = "command.PRESSURIZE" + # Modules # @@ -535,11 +539,40 @@ class MoveLabwareCommandPayload(TextOnlyPayload): pass +class SealCommandPayload(TextOnlyPayload): + instrument: InstrumentContext + location: Union[None, Location, Well] + + +class UnsealCommandPayload(TextOnlyPayload): + instrument: InstrumentContext + location: Union[None, Location, Well] + + +class PressurizeCommandPayload(TextOnlyPayload): + instrument: InstrumentContext + + class MoveLabwareCommand(TypedDict): name: Literal["command.MOVE_LABWARE"] payload: MoveLabwareCommandPayload +class SealCommand(TypedDict): + name: Literal["command.SEAL"] + payload: SealCommandPayload + + +class UnsealCommand(TypedDict): + name: Literal["command.UNSEAL"] + payload: UnsealCommandPayload + + +class PressurizeCommand(TypedDict): + name: Literal["command.PRESSURIZE"] + payload: PressurizeCommandPayload + + Command = Union[ DropTipCommand, DropTipInDisposalLocationCommand, @@ -588,6 +621,9 @@ class MoveLabwareCommand(TypedDict): MoveToCommand, MoveToDisposalLocationCommand, MoveLabwareCommand, + SealCommand, + UnsealCommand, + PressurizeCommand, ] @@ -637,6 +673,9 @@ class MoveLabwareCommand(TypedDict): MoveToCommandPayload, MoveToDisposalLocationCommandPayload, MoveLabwareCommandPayload, + SealCommandPayload, + UnsealCommandPayload, + PressurizeCommandPayload, ] diff --git a/api/src/opentrons/protocol_api/__init__.py b/api/src/opentrons/protocol_api/__init__.py index 2f35bb46764..41a061f5a94 100644 --- a/api/src/opentrons/protocol_api/__init__.py +++ b/api/src/opentrons/protocol_api/__init__.py @@ -30,7 +30,16 @@ ) from .disposal_locations import TrashBin, WasteChute from ._liquid import Liquid, LiquidClass -from ._types import OFF_DECK +from ._types import ( + OFF_DECK, + PLUNGER_BLOWOUT, + PLUNGER_TOP, + PLUNGER_BOTTOM, + PLUNGER_DROPTIP, + ASPIRATE_ACTION, + DISPENSE_ACTION, + BLOWOUT_ACTION, +) from ._nozzle_layout import ( COLUMN, PARTIAL_COLUMN, @@ -69,12 +78,22 @@ "Liquid", "LiquidClass", "Parameters", + # Partial Tip types "COLUMN", "PARTIAL_COLUMN", "SINGLE", "ROW", "ALL", + # Deck location types "OFF_DECK", + # Pipette plunger types + "PLUNGER_BLOWOUT", + "PLUNGER_TOP", + "PLUNGER_BOTTOM", + "PLUNGER_DROPTIP", + "ASPIRATE_ACTION", + "DISPENSE_ACTION", + "BLOWOUT_ACTION", "RuntimeParameterRequiredError", "CSVParameter", # For internal Opentrons use only: diff --git a/api/src/opentrons/protocol_api/_liquid.py b/api/src/opentrons/protocol_api/_liquid.py index 75e2c6fb6f2..12c9a140ce3 100644 --- a/api/src/opentrons/protocol_api/_liquid.py +++ b/api/src/opentrons/protocol_api/_liquid.py @@ -1,13 +1,15 @@ +from __future__ import annotations + from dataclasses import dataclass -from typing import Optional, Sequence +from typing import Optional, Dict from opentrons_shared_data.liquid_classes.liquid_class_definition import ( LiquidClassSchemaV1, - AspirateProperties, - SingleDispenseProperties, - MultiDispenseProperties, - ByPipetteSetting, - ByTipTypeSetting, +) + +from ._liquid_properties import ( + TransferProperties, + build_transfer_properties, ) @@ -29,46 +31,29 @@ class Liquid: display_color: Optional[str] -# TODO (spp, 2024-10-17): create PAPI-equivalent types for all the properties -# and have validation on value updates with user-facing error messages -@dataclass -class TransferProperties: - _aspirate: AspirateProperties - _dispense: SingleDispenseProperties - _multi_dispense: Optional[MultiDispenseProperties] - - @property - def aspirate(self) -> AspirateProperties: - """Aspirate properties.""" - return self._aspirate - - @property - def dispense(self) -> SingleDispenseProperties: - """Single dispense properties.""" - return self._dispense - - @property - def multi_dispense(self) -> Optional[MultiDispenseProperties]: - """Multi dispense properties.""" - return self._multi_dispense - - @dataclass class LiquidClass: """A data class that contains properties of a specific class of liquids.""" _name: str _display_name: str - _by_pipette_setting: Sequence[ByPipetteSetting] + _by_pipette_setting: Dict[str, Dict[str, TransferProperties]] @classmethod def create(cls, liquid_class_definition: LiquidClassSchemaV1) -> "LiquidClass": """Liquid class factory method.""" + by_pipette_settings: Dict[str, Dict[str, TransferProperties]] = {} + for by_pipette in liquid_class_definition.byPipette: + tip_settings: Dict[str, TransferProperties] = {} + for tip_type in by_pipette.byTipType: + tip_settings[tip_type.tiprack] = build_transfer_properties(tip_type) + by_pipette_settings[by_pipette.pipetteModel] = tip_settings + return cls( _name=liquid_class_definition.liquidClassName, _display_name=liquid_class_definition.displayName, - _by_pipette_setting=liquid_class_definition.byPipette, + _by_pipette_setting=by_pipette_settings, ) @property @@ -81,26 +66,16 @@ def display_name(self) -> str: def get_for(self, pipette: str, tiprack: str) -> TransferProperties: """Get liquid class transfer properties for the specified pipette and tip.""" - settings_for_pipette: Sequence[ByPipetteSetting] = [ - pip_setting - for pip_setting in self._by_pipette_setting - if pip_setting.pipetteModel == pipette - ] - if len(settings_for_pipette) == 0: + try: + settings_for_pipette = self._by_pipette_setting[pipette] + except KeyError: raise ValueError( f"No properties found for {pipette} in {self._name} liquid class" ) - settings_for_tip: Sequence[ByTipTypeSetting] = [ - tip_setting - for tip_setting in settings_for_pipette[0].byTipType - if tip_setting.tiprack == tiprack - ] - if len(settings_for_tip) == 0: + try: + transfer_properties = settings_for_pipette[tiprack] + except KeyError: raise ValueError( f"No properties found for {tiprack} in {self._name} liquid class" ) - return TransferProperties( - _aspirate=settings_for_tip[0].aspirate, - _dispense=settings_for_tip[0].singleDispense, - _multi_dispense=settings_for_tip[0].multiDispense, - ) + return transfer_properties diff --git a/api/src/opentrons/protocol_api/_liquid_properties.py b/api/src/opentrons/protocol_api/_liquid_properties.py new file mode 100644 index 00000000000..dc848cfb7e2 --- /dev/null +++ b/api/src/opentrons/protocol_api/_liquid_properties.py @@ -0,0 +1,754 @@ +from dataclasses import dataclass +from numpy import interp +from typing import Optional, Dict, Sequence, Tuple, List + +from opentrons_shared_data.liquid_classes.liquid_class_definition import ( + AspirateProperties as SharedDataAspirateProperties, + SingleDispenseProperties as SharedDataSingleDispenseProperties, + MultiDispenseProperties as SharedDataMultiDispenseProperties, + DelayProperties as SharedDataDelayProperties, + DelayParams as SharedDataDelayParams, + TouchTipProperties as SharedDataTouchTipProperties, + LiquidClassTouchTipParams as SharedDataTouchTipParams, + MixProperties as SharedDataMixProperties, + MixParams as SharedDataMixParams, + BlowoutProperties as SharedDataBlowoutProperties, + BlowoutParams as SharedDataBlowoutParams, + ByTipTypeSetting as SharedByTipTypeSetting, + Submerge as SharedDataSubmerge, + RetractAspirate as SharedDataRetractAspirate, + RetractDispense as SharedDataRetractDispense, + BlowoutLocation, + PositionReference, + Coordinate, +) + +from . import validation + + +class LiquidHandlingPropertyByVolume: + def __init__(self, by_volume_property: Sequence[Tuple[float, float]]) -> None: + self._properties_by_volume: Dict[float, float] = { + float(volume): value for volume, value in by_volume_property + } + # Volumes need to be sorted for proper interpolation of non-defined volumes, and the + # corresponding values need to be in the same order for them to be interpolated correctly + self._sorted_volumes: Tuple[float, ...] = () + self._sorted_values: Tuple[float, ...] = () + self._sort_volume_and_values() + + def as_dict(self) -> Dict[float, float]: + """Get a dictionary representation of all set volumes and values along with the default.""" + return self._properties_by_volume + + def as_list_of_tuples(self) -> List[Tuple[float, float]]: + """Get as list of tuples.""" + return list(self._properties_by_volume.items()) + + def get_for_volume(self, volume: float) -> float: + """Get a value by volume for this property. Volumes not defined will be interpolated between set volumes.""" + validated_volume = validation.ensure_positive_float(volume) + if len(self._properties_by_volume) == 0: + raise ValueError( + "No properties found for any volumes. Cannot interpolate for the given volume." + ) + try: + return self._properties_by_volume[validated_volume] + except KeyError: + # If volume is not defined in dictionary, do a piecewise interpolation with existing sorted values + return float( + interp(validated_volume, self._sorted_volumes, self._sorted_values) + ) + + def set_for_volume(self, volume: float, value: float) -> None: + """Add a new volume and value for the property for the interpolation curve.""" + validated_volume = validation.ensure_positive_float(volume) + self._properties_by_volume[validated_volume] = value + self._sort_volume_and_values() + + def delete_for_volume(self, volume: float) -> None: + """Remove an existing volume and value from the property.""" + try: + del self._properties_by_volume[volume] + except KeyError: + raise KeyError(f"No value set for volume {volume} uL") + self._sort_volume_and_values() + + def _sort_volume_and_values(self) -> None: + """Sort volume in increasing order along with corresponding values in matching order.""" + self._sorted_volumes, self._sorted_values = ( + zip(*sorted(self._properties_by_volume.items())) + if len(self._properties_by_volume) > 0 + else [(), ()] + ) + + +@dataclass +class DelayProperties: + + _enabled: bool + _duration: Optional[float] + + @property + def enabled(self) -> bool: + return self._enabled + + @enabled.setter + def enabled(self, enable: bool) -> None: + validated_enable = validation.ensure_boolean(enable) + if validated_enable and self._duration is None: + raise ValueError("duration must be set before enabling delay.") + self._enabled = validated_enable + + @property + def duration(self) -> Optional[float]: + return self._duration + + @duration.setter + def duration(self, new_duration: float) -> None: + validated_duration = validation.ensure_positive_float(new_duration) + self._duration = validated_duration + + def as_shared_data_model(self) -> SharedDataDelayProperties: + return SharedDataDelayProperties( + enable=self._enabled, + params=SharedDataDelayParams(duration=self.duration) + if self.duration is not None + else None, + ) + + +@dataclass +class TouchTipProperties: + + _enabled: bool + _z_offset: Optional[float] + _mm_to_edge: Optional[float] + _speed: Optional[float] + + @property + def enabled(self) -> bool: + return self._enabled + + @enabled.setter + def enabled(self, enable: bool) -> None: + validated_enable = validation.ensure_boolean(enable) + if validated_enable and ( + self._z_offset is None or self._mm_to_edge is None or self._speed is None + ): + raise ValueError( + "z_offset, mm_to_edge and speed must be set before enabling touch tip." + ) + self._enabled = validated_enable + + @property + def z_offset(self) -> Optional[float]: + return self._z_offset + + @z_offset.setter + def z_offset(self, new_offset: float) -> None: + validated_offset = validation.ensure_float(new_offset) + self._z_offset = validated_offset + + @property + def mm_to_edge(self) -> Optional[float]: + return self._mm_to_edge + + @mm_to_edge.setter + def mm_to_edge(self, new_mm: float) -> None: + validated_mm = validation.ensure_float(new_mm) + self._z_offset = validated_mm + + @property + def speed(self) -> Optional[float]: + return self._speed + + @speed.setter + def speed(self, new_speed: float) -> None: + validated_speed = validation.ensure_positive_float(new_speed) + self._speed = validated_speed + + def _get_shared_data_params(self) -> Optional[SharedDataTouchTipParams]: + """Get the touch tip params in schema v1 shape.""" + if ( + self._z_offset is not None + and self._mm_to_edge is not None + and self._speed is not None + ): + return SharedDataTouchTipParams( + zOffset=self._z_offset, + mmToEdge=self._mm_to_edge, + speed=self._speed, + ) + else: + return None + + def as_shared_data_model(self) -> SharedDataTouchTipProperties: + return SharedDataTouchTipProperties( + enable=self._enabled, + params=self._get_shared_data_params(), + ) + + +@dataclass +class MixProperties: + + _enabled: bool + _repetitions: Optional[int] + _volume: Optional[float] + + @property + def enabled(self) -> bool: + return self._enabled + + @enabled.setter + def enabled(self, enable: bool) -> None: + validated_enable = validation.ensure_boolean(enable) + if validated_enable and (self._repetitions is None or self._volume is None): + raise ValueError("repetitions and volume must be set before enabling mix.") + self._enabled = validated_enable + + @property + def repetitions(self) -> Optional[int]: + return self._repetitions + + @repetitions.setter + def repetitions(self, new_repetitions: int) -> None: + validated_repetitions = validation.ensure_positive_int(new_repetitions) + self._repetitions = validated_repetitions + + @property + def volume(self) -> Optional[float]: + return self._volume + + @volume.setter + def volume(self, new_volume: float) -> None: + validated_volume = validation.ensure_positive_float(new_volume) + self._volume = validated_volume + + def _get_shared_data_params(self) -> Optional[SharedDataMixParams]: + """Get the mix params in schema v1 shape.""" + if self._repetitions is not None and self._volume is not None: + return SharedDataMixParams( + repetitions=self._repetitions, + volume=self._volume, + ) + else: + return None + + def as_shared_data_model(self) -> SharedDataMixProperties: + return SharedDataMixProperties( + enable=self._enabled, + params=self._get_shared_data_params(), + ) + + +@dataclass +class BlowoutProperties: + + _enabled: bool + _location: Optional[BlowoutLocation] + _flow_rate: Optional[float] + + @property + def enabled(self) -> bool: + return self._enabled + + @enabled.setter + def enabled(self, enable: bool) -> None: + validated_enable = validation.ensure_boolean(enable) + if validated_enable and (self._location is None or self._flow_rate is None): + raise ValueError( + "location and flow_rate must be set before enabling blowout." + ) + self._enabled = validated_enable + + @property + def location(self) -> Optional[BlowoutLocation]: + return self._location + + @location.setter + def location(self, new_location: str) -> None: + self._location = BlowoutLocation(new_location) + + @property + def flow_rate(self) -> Optional[float]: + return self._flow_rate + + @flow_rate.setter + def flow_rate(self, new_flow_rate: float) -> None: + validated_flow_rate = validation.ensure_positive_float(new_flow_rate) + self._flow_rate = validated_flow_rate + + def _get_shared_data_params(self) -> Optional[SharedDataBlowoutParams]: + """Get the mix params in schema v1 shape.""" + if self._location is not None and self._flow_rate is not None: + return SharedDataBlowoutParams( + location=self._location, + flowRate=self._flow_rate, + ) + else: + return None + + def as_shared_data_model(self) -> SharedDataBlowoutProperties: + return SharedDataBlowoutProperties( + enable=self._enabled, + params=self._get_shared_data_params(), + ) + + +@dataclass +class SubmergeRetractCommon: + + _position_reference: PositionReference + _offset: Coordinate + _speed: float + _delay: DelayProperties + + @property + def position_reference(self) -> PositionReference: + return self._position_reference + + @position_reference.setter + def position_reference(self, new_position: str) -> None: + self._position_reference = PositionReference(new_position) + + @property + def offset(self) -> Coordinate: + return self._offset + + @offset.setter + def offset(self, new_offset: Sequence[float]) -> None: + x, y, z = validation.validate_coordinates(new_offset) + self._offset = Coordinate(x=x, y=y, z=z) + + @property + def speed(self) -> float: + return self._speed + + @speed.setter + def speed(self, new_speed: float) -> None: + validated_speed = validation.ensure_positive_float(new_speed) + self._speed = validated_speed + + @property + def delay(self) -> DelayProperties: + return self._delay + + +@dataclass +class Submerge(SubmergeRetractCommon): + ... + + def as_shared_data_model(self) -> SharedDataSubmerge: + return SharedDataSubmerge( + positionReference=self._position_reference, + offset=self._offset, + speed=self._speed, + delay=self._delay.as_shared_data_model(), + ) + + +@dataclass +class RetractAspirate(SubmergeRetractCommon): + + _air_gap_by_volume: LiquidHandlingPropertyByVolume + _touch_tip: TouchTipProperties + + @property + def air_gap_by_volume(self) -> LiquidHandlingPropertyByVolume: + return self._air_gap_by_volume + + @property + def touch_tip(self) -> TouchTipProperties: + return self._touch_tip + + def as_shared_data_model(self) -> SharedDataRetractAspirate: + return SharedDataRetractAspirate( + positionReference=self._position_reference, + offset=self._offset, + speed=self._speed, + airGapByVolume=self._air_gap_by_volume.as_list_of_tuples(), + touchTip=self._touch_tip.as_shared_data_model(), + delay=self._delay.as_shared_data_model(), + ) + + +@dataclass +class RetractDispense(SubmergeRetractCommon): + + _air_gap_by_volume: LiquidHandlingPropertyByVolume + _touch_tip: TouchTipProperties + _blowout: BlowoutProperties + + @property + def air_gap_by_volume(self) -> LiquidHandlingPropertyByVolume: + return self._air_gap_by_volume + + @property + def touch_tip(self) -> TouchTipProperties: + return self._touch_tip + + @property + def blowout(self) -> BlowoutProperties: + return self._blowout + + def as_shared_data_model(self) -> SharedDataRetractDispense: + return SharedDataRetractDispense( + positionReference=self._position_reference, + offset=self._offset, + speed=self._speed, + airGapByVolume=self._air_gap_by_volume.as_list_of_tuples(), + blowout=self._blowout.as_shared_data_model(), + touchTip=self._touch_tip.as_shared_data_model(), + delay=self._delay.as_shared_data_model(), + ) + + +@dataclass +class BaseLiquidHandlingProperties: + + _submerge: Submerge + _position_reference: PositionReference + _offset: Coordinate + _flow_rate_by_volume: LiquidHandlingPropertyByVolume + _correction_by_volume: LiquidHandlingPropertyByVolume + _delay: DelayProperties + + @property + def submerge(self) -> Submerge: + return self._submerge + + @property + def position_reference(self) -> PositionReference: + return self._position_reference + + @position_reference.setter + def position_reference(self, new_position: str) -> None: + self._position_reference = PositionReference(new_position) + + @property + def offset(self) -> Coordinate: + return self._offset + + @offset.setter + def offset(self, new_offset: Sequence[float]) -> None: + x, y, z = validation.validate_coordinates(new_offset) + self._offset = Coordinate(x=x, y=y, z=z) + + @property + def flow_rate_by_volume(self) -> LiquidHandlingPropertyByVolume: + return self._flow_rate_by_volume + + @property + def correction_by_volume(self) -> LiquidHandlingPropertyByVolume: + return self._correction_by_volume + + @property + def delay(self) -> DelayProperties: + return self._delay + + +@dataclass +class AspirateProperties(BaseLiquidHandlingProperties): + + _retract: RetractAspirate + _pre_wet: bool + _mix: MixProperties + + @property + def pre_wet(self) -> bool: + return self._pre_wet + + @pre_wet.setter + def pre_wet(self, new_setting: bool) -> None: + validated_setting = validation.ensure_boolean(new_setting) + self._pre_wet = validated_setting + + @property + def retract(self) -> RetractAspirate: + return self._retract + + @property + def mix(self) -> MixProperties: + return self._mix + + def as_shared_data_model(self) -> SharedDataAspirateProperties: + return SharedDataAspirateProperties( + submerge=self._submerge.as_shared_data_model(), + retract=self._retract.as_shared_data_model(), + positionReference=self._position_reference, + offset=self._offset, + flowRateByVolume=self._flow_rate_by_volume.as_list_of_tuples(), + preWet=self._pre_wet, + mix=self._mix.as_shared_data_model(), + delay=self._delay.as_shared_data_model(), + correctionByVolume=self._correction_by_volume.as_list_of_tuples(), + ) + + +@dataclass +class SingleDispenseProperties(BaseLiquidHandlingProperties): + + _retract: RetractDispense + _push_out_by_volume: LiquidHandlingPropertyByVolume + _mix: MixProperties + + @property + def push_out_by_volume(self) -> LiquidHandlingPropertyByVolume: + return self._push_out_by_volume + + @property + def retract(self) -> RetractDispense: + return self._retract + + @property + def mix(self) -> MixProperties: + return self._mix + + def as_shared_data_model(self) -> SharedDataSingleDispenseProperties: + return SharedDataSingleDispenseProperties( + submerge=self._submerge.as_shared_data_model(), + retract=self._retract.as_shared_data_model(), + positionReference=self._position_reference, + offset=self._offset, + flowRateByVolume=self._flow_rate_by_volume.as_list_of_tuples(), + mix=self._mix.as_shared_data_model(), + pushOutByVolume=self._push_out_by_volume.as_list_of_tuples(), + delay=self._delay.as_shared_data_model(), + correctionByVolume=self._correction_by_volume.as_list_of_tuples(), + ) + + +@dataclass +class MultiDispenseProperties(BaseLiquidHandlingProperties): + + _retract: RetractDispense + _conditioning_by_volume: LiquidHandlingPropertyByVolume + _disposal_by_volume: LiquidHandlingPropertyByVolume + + @property + def retract(self) -> RetractDispense: + return self._retract + + @property + def conditioning_by_volume(self) -> LiquidHandlingPropertyByVolume: + return self._conditioning_by_volume + + @property + def disposal_by_volume(self) -> LiquidHandlingPropertyByVolume: + return self._disposal_by_volume + + def as_shared_data_model(self) -> SharedDataMultiDispenseProperties: + return SharedDataMultiDispenseProperties( + submerge=self._submerge.as_shared_data_model(), + retract=self._retract.as_shared_data_model(), + positionReference=self._position_reference, + offset=self._offset, + flowRateByVolume=self._flow_rate_by_volume.as_list_of_tuples(), + conditioningByVolume=self._conditioning_by_volume.as_list_of_tuples(), + disposalByVolume=self._disposal_by_volume.as_list_of_tuples(), + delay=self._delay.as_shared_data_model(), + correctionByVolume=self._correction_by_volume.as_list_of_tuples(), + ) + + +@dataclass +class TransferProperties: + _aspirate: AspirateProperties + _dispense: SingleDispenseProperties + _multi_dispense: Optional[MultiDispenseProperties] + + @property + def aspirate(self) -> AspirateProperties: + """Aspirate properties.""" + return self._aspirate + + @property + def dispense(self) -> SingleDispenseProperties: + """Single dispense properties.""" + return self._dispense + + @property + def multi_dispense(self) -> Optional[MultiDispenseProperties]: + """Multi dispense properties.""" + return self._multi_dispense + + +def _build_delay_properties( + delay_properties: SharedDataDelayProperties, +) -> DelayProperties: + if delay_properties.params is not None: + duration = delay_properties.params.duration + else: + duration = None + return DelayProperties(_enabled=delay_properties.enable, _duration=duration) + + +def _build_touch_tip_properties( + touch_tip_properties: SharedDataTouchTipProperties, +) -> TouchTipProperties: + if touch_tip_properties.params is not None: + z_offset = touch_tip_properties.params.zOffset + mm_to_edge = touch_tip_properties.params.mmToEdge + speed = touch_tip_properties.params.speed + else: + z_offset = None + mm_to_edge = None + speed = None + return TouchTipProperties( + _enabled=touch_tip_properties.enable, + _z_offset=z_offset, + _mm_to_edge=mm_to_edge, + _speed=speed, + ) + + +def _build_mix_properties( + mix_properties: SharedDataMixProperties, +) -> MixProperties: + if mix_properties.params is not None: + repetitions = mix_properties.params.repetitions + volume = mix_properties.params.volume + else: + repetitions = None + volume = None + return MixProperties( + _enabled=mix_properties.enable, _repetitions=repetitions, _volume=volume + ) + + +def _build_blowout_properties( + blowout_properties: SharedDataBlowoutProperties, +) -> BlowoutProperties: + if blowout_properties.params is not None: + location = blowout_properties.params.location + flow_rate = blowout_properties.params.flowRate + else: + location = None + flow_rate = None + return BlowoutProperties( + _enabled=blowout_properties.enable, _location=location, _flow_rate=flow_rate + ) + + +def _build_submerge( + submerge_properties: SharedDataSubmerge, +) -> Submerge: + return Submerge( + _position_reference=submerge_properties.positionReference, + _offset=submerge_properties.offset, + _speed=submerge_properties.speed, + _delay=_build_delay_properties(submerge_properties.delay), + ) + + +def _build_retract_aspirate( + retract_aspirate: SharedDataRetractAspirate, +) -> RetractAspirate: + return RetractAspirate( + _position_reference=retract_aspirate.positionReference, + _offset=retract_aspirate.offset, + _speed=retract_aspirate.speed, + _air_gap_by_volume=LiquidHandlingPropertyByVolume( + retract_aspirate.airGapByVolume + ), + _touch_tip=_build_touch_tip_properties(retract_aspirate.touchTip), + _delay=_build_delay_properties(retract_aspirate.delay), + ) + + +def _build_retract_dispense( + retract_dispense: SharedDataRetractDispense, +) -> RetractDispense: + return RetractDispense( + _position_reference=retract_dispense.positionReference, + _offset=retract_dispense.offset, + _speed=retract_dispense.speed, + _air_gap_by_volume=LiquidHandlingPropertyByVolume( + retract_dispense.airGapByVolume + ), + _blowout=_build_blowout_properties(retract_dispense.blowout), + _touch_tip=_build_touch_tip_properties(retract_dispense.touchTip), + _delay=_build_delay_properties(retract_dispense.delay), + ) + + +def build_aspirate_properties( + aspirate_properties: SharedDataAspirateProperties, +) -> AspirateProperties: + return AspirateProperties( + _submerge=_build_submerge(aspirate_properties.submerge), + _retract=_build_retract_aspirate(aspirate_properties.retract), + _position_reference=aspirate_properties.positionReference, + _offset=aspirate_properties.offset, + _flow_rate_by_volume=LiquidHandlingPropertyByVolume( + aspirate_properties.flowRateByVolume + ), + _correction_by_volume=LiquidHandlingPropertyByVolume( + aspirate_properties.correctionByVolume + ), + _pre_wet=aspirate_properties.preWet, + _mix=_build_mix_properties(aspirate_properties.mix), + _delay=_build_delay_properties(aspirate_properties.delay), + ) + + +def build_single_dispense_properties( + single_dispense_properties: SharedDataSingleDispenseProperties, +) -> SingleDispenseProperties: + return SingleDispenseProperties( + _submerge=_build_submerge(single_dispense_properties.submerge), + _retract=_build_retract_dispense(single_dispense_properties.retract), + _position_reference=single_dispense_properties.positionReference, + _offset=single_dispense_properties.offset, + _flow_rate_by_volume=LiquidHandlingPropertyByVolume( + single_dispense_properties.flowRateByVolume + ), + _correction_by_volume=LiquidHandlingPropertyByVolume( + single_dispense_properties.correctionByVolume + ), + _mix=_build_mix_properties(single_dispense_properties.mix), + _push_out_by_volume=LiquidHandlingPropertyByVolume( + single_dispense_properties.pushOutByVolume + ), + _delay=_build_delay_properties(single_dispense_properties.delay), + ) + + +def build_multi_dispense_properties( + multi_dispense_properties: Optional[SharedDataMultiDispenseProperties], +) -> Optional[MultiDispenseProperties]: + if multi_dispense_properties is None: + return None + return MultiDispenseProperties( + _submerge=_build_submerge(multi_dispense_properties.submerge), + _retract=_build_retract_dispense(multi_dispense_properties.retract), + _position_reference=multi_dispense_properties.positionReference, + _offset=multi_dispense_properties.offset, + _flow_rate_by_volume=LiquidHandlingPropertyByVolume( + multi_dispense_properties.flowRateByVolume + ), + _correction_by_volume=LiquidHandlingPropertyByVolume( + multi_dispense_properties.correctionByVolume + ), + _conditioning_by_volume=LiquidHandlingPropertyByVolume( + multi_dispense_properties.conditioningByVolume + ), + _disposal_by_volume=LiquidHandlingPropertyByVolume( + multi_dispense_properties.disposalByVolume + ), + _delay=_build_delay_properties(multi_dispense_properties.delay), + ) + + +def build_transfer_properties( + by_tip_type_setting: SharedByTipTypeSetting, +) -> TransferProperties: + return TransferProperties( + _aspirate=build_aspirate_properties(by_tip_type_setting.aspirate), + _dispense=build_single_dispense_properties(by_tip_type_setting.singleDispense), + _multi_dispense=build_multi_dispense_properties( + by_tip_type_setting.multiDispense + ), + ) diff --git a/api/src/opentrons/protocol_api/_types.py b/api/src/opentrons/protocol_api/_types.py index 9890e29c2bc..0e73405b3b7 100644 --- a/api/src/opentrons/protocol_api/_types.py +++ b/api/src/opentrons/protocol_api/_types.py @@ -17,3 +17,27 @@ class OffDeckType(enum.Enum): See :ref:`off-deck-location` for details on using ``OFF_DECK`` with :py:obj:`ProtocolContext.move_labware()`. """ + + +class PlungerPositionTypes(enum.Enum): + PLUNGER_TOP = "top" + PLUNGER_BOTTOM = "bottom" + PLUNGER_BLOWOUT = "blow_out" + PLUNGER_DROPTIP = "drop_tip" + + +PLUNGER_TOP: Final = PlungerPositionTypes.PLUNGER_TOP +PLUNGER_BOTTOM: Final = PlungerPositionTypes.PLUNGER_BOTTOM +PLUNGER_BLOWOUT: Final = PlungerPositionTypes.PLUNGER_BLOWOUT +PLUNGER_DROPTIP: Final = PlungerPositionTypes.PLUNGER_DROPTIP + + +class PipetteActionTypes(enum.Enum): + ASPIRATE_ACTION = "aspirate" + DISPENSE_ACTION = "dispense" + BLOWOUT_ACTION = "blowout" + + +ASPIRATE_ACTION: Final = PipetteActionTypes.ASPIRATE_ACTION +DISPENSE_ACTION: Final = PipetteActionTypes.DISPENSE_ACTION +BLOWOUT_ACTION: Final = PipetteActionTypes.BLOWOUT_ACTION diff --git a/api/src/opentrons/protocol_api/core/common.py b/api/src/opentrons/protocol_api/core/common.py index 5a63abb46b3..3aff2523a1f 100644 --- a/api/src/opentrons/protocol_api/core/common.py +++ b/api/src/opentrons/protocol_api/core/common.py @@ -14,6 +14,7 @@ ) from .protocol import AbstractProtocol from .well import AbstractWellCore +from .robot import AbstractRobot WellCore = AbstractWellCore @@ -26,4 +27,5 @@ HeaterShakerCore = AbstractHeaterShakerCore MagneticBlockCore = AbstractMagneticBlockCore AbsorbanceReaderCore = AbstractAbsorbanceReaderCore +RobotCore = AbstractRobot ProtocolCore = AbstractProtocol[InstrumentCore, LabwareCore, ModuleCore] diff --git a/api/src/opentrons/protocol_api/core/engine/instrument.py b/api/src/opentrons/protocol_api/core/engine/instrument.py index 825d45bfded..cd6548202ff 100644 --- a/api/src/opentrons/protocol_api/core/engine/instrument.py +++ b/api/src/opentrons/protocol_api/core/engine/instrument.py @@ -1,13 +1,14 @@ """ProtocolEngine-based InstrumentContext core implementation.""" -from __future__ import annotations -from typing import Optional, TYPE_CHECKING, cast, Union -from opentrons.protocols.api_support.types import APIVersion +from __future__ import annotations -from opentrons.types import Location, Mount +from typing import Optional, TYPE_CHECKING, cast, Union, List +from opentrons.types import Location, Mount, NozzleConfigurationType, NozzleMapInterface from opentrons.hardware_control import SyncHardwareAPI from opentrons.hardware_control.dev_types import PipetteDict from opentrons.protocols.api_support.util import FlowRates, find_value_for_api_version +from opentrons.protocols.api_support.types import APIVersion +from opentrons.protocols.advanced_control.transfers.common import TransferTipPolicyV2 from opentrons.protocol_engine import commands as cmd from opentrons.protocol_engine import ( DeckPoint, @@ -26,26 +27,29 @@ PRIMARY_NOZZLE_LITERAL, NozzleLayoutConfigurationType, AddressableOffsetVector, + LiquidClassRecord, ) from opentrons.protocol_engine.errors.exceptions import TipNotAttachedError from opentrons.protocol_engine.clients import SyncClient as EngineClient from opentrons.protocols.api_support.definitions import MAX_SUPPORTED_VERSION from opentrons_shared_data.pipette.types import PipetteNameType +from opentrons_shared_data.errors.exceptions import ( + UnsupportedHardwareCommand, +) from opentrons.protocol_api._nozzle_layout import NozzleLayout -from opentrons.hardware_control.nozzle_manager import NozzleConfigurationType -from opentrons.hardware_control.nozzle_manager import NozzleMap from . import overlap_versions, pipette_movement_conflict -from ..instrument import AbstractInstrument from .well import WellCore - +from ..instrument import AbstractInstrument from ...disposal_locations import TrashBin, WasteChute if TYPE_CHECKING: from .protocol import ProtocolCore - + from opentrons.protocol_api._liquid import LiquidClass _DISPENSE_VOLUME_VALIDATION_ADDED_IN = APIVersion(2, 17) +_RESIN_TIP_DEFAULT_VOLUME = 400 +_RESIN_TIP_DEFAULT_FLOW_RATE = 10.0 class InstrumentCore(AbstractInstrument[WellCore]): @@ -86,6 +90,13 @@ def __init__( self._liquid_presence_detection = bool( self._engine_client.state.pipettes.get_liquid_presence_detection(pipette_id) ) + if ( + self._liquid_presence_detection + and not self._pressure_supported_by_pipette() + ): + raise UnsupportedHardwareCommand( + "Pressure sensor not available for this pipette" + ) @property def pipette_id(self) -> str: @@ -104,6 +115,19 @@ def set_default_speed(self, speed: float) -> None: pipette_id=self._pipette_id, speed=speed ) + def air_gap_in_place(self, volume: float, flow_rate: float) -> None: + """Aspirate a given volume of air from the current location of the pipette. + + Args: + volume: The volume of air to aspirate, in microliters. + folw_rate: The flow rate of air into the pipette, in microliters/s + """ + self._engine_client.execute_command( + cmd.AirGapInPlaceParams( + pipetteId=self._pipette_id, volume=volume, flowRate=flow_rate + ) + ) + def aspirate( self, location: Location, @@ -656,6 +680,113 @@ def move_to( location=location, mount=self.get_mount() ) + def resin_tip_seal( + self, location: Location, well_core: WellCore, in_place: Optional[bool] = False + ) -> None: + labware_id = well_core.labware_id + well_name = well_core.get_name() + well_location = ( + self._engine_client.state.geometry.get_relative_pick_up_tip_well_location( + labware_id=labware_id, + well_name=well_name, + absolute_point=location.point, + ) + ) + + self._engine_client.execute_command( + cmd.EvotipSealPipetteParams( + pipetteId=self._pipette_id, + labwareId=labware_id, + wellName=well_name, + wellLocation=well_location, + ) + ) + + def resin_tip_unseal(self, location: Location, well_core: WellCore) -> None: + well_name = well_core.get_name() + labware_id = well_core.labware_id + + if location is not None: + relative_well_location = ( + self._engine_client.state.geometry.get_relative_well_location( + labware_id=labware_id, + well_name=well_name, + absolute_point=location.point, + ) + ) + + well_location = DropTipWellLocation( + origin=DropTipWellOrigin(relative_well_location.origin.value), + offset=relative_well_location.offset, + ) + else: + well_location = DropTipWellLocation() + + pipette_movement_conflict.check_safe_for_pipette_movement( + engine_state=self._engine_client.state, + pipette_id=self._pipette_id, + labware_id=labware_id, + well_name=well_name, + well_location=well_location, + ) + self._engine_client.execute_command( + cmd.EvotipUnsealPipetteParams( + pipetteId=self._pipette_id, + labwareId=labware_id, + wellName=well_name, + wellLocation=well_location, + ) + ) + + self._protocol_core.set_last_location(location=location, mount=self.get_mount()) + + def resin_tip_dispense( + self, + location: Location, + well_core: WellCore, + volume: Optional[float] = None, + flow_rate: Optional[float] = None, + ) -> None: + """ + Args: + volume: The volume of liquid to dispense, in microliters. Defaults to 400uL. + location: The exact location to dispense to. + well_core: The well to dispense to, if applicable. + flow_rate: The flow rate in µL/s to dispense at. Defaults to 10.0uL/S. + """ + if isinstance(location, (TrashBin, WasteChute)): + raise ValueError("Trash Bin and Waste Chute have no Wells.") + well_name = well_core.get_name() + labware_id = well_core.labware_id + if volume is None: + volume = _RESIN_TIP_DEFAULT_VOLUME + if flow_rate is None: + flow_rate = _RESIN_TIP_DEFAULT_FLOW_RATE + + well_location = self._engine_client.state.geometry.get_relative_liquid_handling_well_location( + labware_id=labware_id, + well_name=well_name, + absolute_point=location.point, + is_meniscus=None, + ) + pipette_movement_conflict.check_safe_for_pipette_movement( + engine_state=self._engine_client.state, + pipette_id=self._pipette_id, + labware_id=labware_id, + well_name=well_name, + well_location=well_location, + ) + self._engine_client.execute_command( + cmd.EvotipDispenseParams( + pipetteId=self._pipette_id, + labwareId=labware_id, + wellName=well_name, + wellLocation=well_location, + volume=volume, + flowRate=flow_rate, + ) + ) + def get_mount(self) -> Mount: """Get the mount the pipette is attached to.""" return self._engine_client.state.pipettes.get( @@ -724,7 +855,7 @@ def get_active_channels(self) -> int: self._pipette_id ) - def get_nozzle_map(self) -> NozzleMap: + def get_nozzle_map(self) -> NozzleMapInterface: return self._engine_client.state.tips.get_pipette_nozzle_map(self._pipette_id) def has_tip(self) -> bool: @@ -842,11 +973,55 @@ def configure_nozzle_layout( ) ) + def load_liquid_class( + self, + liquid_class: LiquidClass, + pipette_load_name: str, + tiprack_uri: str, + ) -> str: + """Load a liquid class into the engine and return its ID.""" + transfer_props = liquid_class.get_for( + pipette=pipette_load_name, tiprack=tiprack_uri + ) + + liquid_class_record = LiquidClassRecord( + liquidClassName=liquid_class.name, + pipetteModel=self.get_model(), # TODO: verify this is the correct 'model' to use + tiprack=tiprack_uri, + aspirate=transfer_props.aspirate.as_shared_data_model(), + singleDispense=transfer_props.dispense.as_shared_data_model(), + multiDispense=transfer_props.multi_dispense.as_shared_data_model() + if transfer_props.multi_dispense + else None, + ) + result = self._engine_client.execute_command_without_recovery( + cmd.LoadLiquidClassParams( + liquidClassRecord=liquid_class_record, + ) + ) + return result.liquidClassId + + def transfer_liquid( + self, + liquid_class_id: str, + volume: float, + source: List[WellCore], + dest: List[WellCore], + new_tip: TransferTipPolicyV2, + trash_location: Union[WellCore, Location, TrashBin, WasteChute], + ) -> None: + """Execute transfer using liquid class properties.""" + 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.execute_command(cmd.HomeParams(axes=[z_axis])) + def _pressure_supported_by_pipette(self) -> bool: + return self._engine_client.state.pipettes.get_pipette_supports_pressure( + self.pipette_id + ) + def detect_liquid_presence(self, well_core: WellCore, loc: Location) -> bool: labware_id = well_core.labware_id well_name = well_core.get_name() @@ -922,3 +1097,9 @@ def liquid_probe_without_recovery( self._protocol_core.set_last_location(location=loc, mount=self.get_mount()) return result.z_position + + def nozzle_configuration_valid_for_lld(self) -> bool: + """Check if the nozzle configuration currently supports LLD.""" + return self._engine_client.state.pipettes.get_nozzle_configuration_supports_lld( + self.pipette_id + ) diff --git a/api/src/opentrons/protocol_api/core/engine/labware.py b/api/src/opentrons/protocol_api/core/engine/labware.py index f7f4b4cdca6..d462401927f 100644 --- a/api/src/opentrons/protocol_api/core/engine/labware.py +++ b/api/src/opentrons/protocol_api/core/engine/labware.py @@ -1,5 +1,6 @@ """ProtocolEngine-based Labware core implementations.""" -from typing import List, Optional, cast + +from typing import List, Optional, cast, Dict from opentrons_shared_data.labware.types import ( LabwareParameters as LabwareParametersDict, @@ -19,11 +20,12 @@ LabwareOffsetCreate, LabwareOffsetVector, ) -from opentrons.types import DeckSlotName, Point, StagingSlotName -from opentrons.hardware_control.nozzle_manager import NozzleMap +from opentrons.types import DeckSlotName, NozzleMapInterface, Point, StagingSlotName +from ..._liquid import Liquid from ..labware import AbstractLabware, LabwareLoadParams + from .well import WellCore @@ -90,12 +92,14 @@ def get_name(self) -> str: def get_definition(self) -> LabwareDefinitionDict: """Get the labware's definition as a plain dictionary.""" - return cast(LabwareDefinitionDict, self._definition.dict(exclude_none=True)) + return cast( + LabwareDefinitionDict, self._definition.model_dump(exclude_none=True) + ) def get_parameters(self) -> LabwareParametersDict: return cast( LabwareParametersDict, - self._definition.parameters.dict(exclude_none=True), + self._definition.parameters.model_dump(exclude_none=True), ) def get_quirks(self) -> List[str]: @@ -116,7 +120,7 @@ def set_calibration(self, delta: Point) -> None: details={"kind": "labware-not-in-slot"}, ) - request = LabwareOffsetCreate.construct( + request = LabwareOffsetCreate.model_construct( definitionUri=self.get_uri(), location=offset_location, vector=LabwareOffsetVector(x=delta.x, y=delta.y, z=delta.z), @@ -162,7 +166,7 @@ def get_next_tip( self, num_tips: int, starting_tip: Optional[WellCore], - nozzle_map: Optional[NozzleMap], + nozzle_map: Optional[NozzleMapInterface], ) -> Optional[str]: return self._engine_client.state.tips.get_next_tip( labware_id=self._labware_id, @@ -203,3 +207,21 @@ def get_deck_slot(self) -> Optional[DeckSlotName]: LocationIsStagingSlotError, ): return None + + def load_liquid(self, volumes: Dict[str, float], liquid: Liquid) -> None: + """Load liquid into wells of the labware.""" + self._engine_client.execute_command( + cmd.LoadLiquidParams( + labwareId=self._labware_id, liquidId=liquid._id, volumeByWell=volumes + ) + ) + + def load_empty(self, wells: List[str]) -> None: + """Mark wells of the labware as empty.""" + self._engine_client.execute_command( + cmd.LoadLiquidParams( + labwareId=self._labware_id, + liquidId="EMPTY", + volumeByWell={well: 0.0 for well in wells}, + ) + ) diff --git a/api/src/opentrons/protocol_api/core/engine/protocol.py b/api/src/opentrons/protocol_api/core/engine/protocol.py index 7c681b232a5..ece431b0d1e 100644 --- a/api/src/opentrons/protocol_api/core/engine/protocol.py +++ b/api/src/opentrons/protocol_api/core/engine/protocol.py @@ -6,6 +6,7 @@ from opentrons.protocol_engine import commands as cmd from opentrons.protocol_engine.commands import LoadModuleResult + from opentrons_shared_data.deck.types import DeckDefinitionV5, SlotDefV3 from opentrons_shared_data.labware.labware_definition import LabwareDefinition from opentrons_shared_data.labware.types import LabwareDefinition as LabwareDefDict @@ -63,6 +64,7 @@ from ..labware import LabwareLoadParams from .labware import LabwareCore from .instrument import InstrumentCore +from .robot import RobotCore from .module_core import ( ModuleCore, TemperatureModuleCore, @@ -82,7 +84,9 @@ class ProtocolCore( AbstractProtocol[ - InstrumentCore, LabwareCore, Union[ModuleCore, NonConnectedModuleCore] + InstrumentCore, + LabwareCore, + Union[ModuleCore, NonConnectedModuleCore], ] ): """Protocol API core using a ProtocolEngine. @@ -189,7 +193,7 @@ def add_labware_definition( ) -> LabwareLoadParams: """Add a labware definition to the set of loadable definitions.""" uri = self._engine_client.add_labware_definition( - LabwareDefinition.parse_obj(definition) + LabwareDefinition.model_validate(definition) ) return LabwareLoadParams.from_uri(uri) @@ -229,6 +233,9 @@ def load_labware( ) # FIXME(jbl, 2023-08-14) validating after loading the object issue validation.ensure_definition_is_labware(load_result.definition) + validation.ensure_definition_is_not_lid_after_api_version( + self.api_version, load_result.definition + ) # FIXME(mm, 2023-02-21): # @@ -318,6 +325,52 @@ def load_adapter( return labware_core + def load_lid( + self, + load_name: str, + location: LabwareCore, + namespace: Optional[str], + version: Optional[int], + ) -> LabwareCore: + """Load an individual lid using its identifying parameters. Must be loaded on an existing Labware.""" + load_location = self._convert_labware_location(location=location) + custom_labware_params = ( + self._engine_client.state.labware.find_custom_labware_load_params() + ) + namespace, version = load_labware_params.resolve( + load_name, namespace, version, custom_labware_params + ) + load_result = self._engine_client.execute_command_without_recovery( + cmd.LoadLidParams( + loadName=load_name, + location=load_location, + namespace=namespace, + version=version, + ) + ) + # FIXME(chb, 2024-12-06) validating after loading the object issue + validation.ensure_definition_is_lid(load_result.definition) + + deck_conflict.check( + engine_state=self._engine_client.state, + new_labware_id=load_result.labwareId, + existing_disposal_locations=self._disposal_locations, + # TODO: We can now fetch these IDs from engine too. + # See comment in self.load_labware(). + # + # Wrapping .keys() in list() is just to make Decoy verification easier. + existing_labware_ids=list(self._labware_cores_by_id.keys()), + existing_module_ids=list(self._module_cores_by_id.keys()), + ) + + labware_core = LabwareCore( + labware_id=load_result.labwareId, + engine_client=self._engine_client, + ) + + self._labware_cores_by_id[labware_core.labware_id] = labware_core + return labware_core + def move_labware( self, labware_core: LabwareCore, @@ -422,6 +475,8 @@ def load_module( raise InvalidModuleLocationError(deck_slot, model.name) robot_type = self._engine_client.state.config.robot_type + # todo(mm, 2024-12-03): This might be possible to remove: + # Protocol Engine will normalize the deck slot itself. normalized_deck_slot = deck_slot.to_equivalent_for_robot_type(robot_type) result = self._engine_client.execute_command_without_recovery( @@ -502,6 +557,12 @@ def _get_module_core( load_module_result=load_module_result, model=model ) + def load_robot(self) -> RobotCore: + """Load a robot core into the RobotContext.""" + return RobotCore( + engine_client=self._engine_client, sync_hardware_api=self._sync_hardware + ) + def load_instrument( self, instrument_name: PipetteNameType, @@ -632,6 +693,72 @@ def set_last_location( self._last_location = location self._last_mount = mount + def load_lid_stack( + self, + load_name: str, + location: Union[DeckSlotName, StagingSlotName, LabwareCore], + quantity: int, + namespace: Optional[str], + version: Optional[int], + ) -> LabwareCore: + """Load a Stack of Lids to a given location, creating a Lid Stack.""" + if quantity < 1: + raise ValueError( + "When loading a lid stack quantity cannot be less than one." + ) + if isinstance(location, DeckSlotName) or isinstance(location, StagingSlotName): + load_location = self._convert_labware_location(location=location) + else: + if isinstance(location, LabwareCore): + load_location = self._convert_labware_location(location=location) + else: + raise ValueError( + "Expected type of Labware Location for lid stack must be Labware, not Legacy Labware or Well." + ) + + custom_labware_params = ( + self._engine_client.state.labware.find_custom_labware_load_params() + ) + namespace, version = load_labware_params.resolve( + load_name, namespace, version, custom_labware_params + ) + + load_result = self._engine_client.execute_command_without_recovery( + cmd.LoadLidStackParams( + loadName=load_name, + location=load_location, + namespace=namespace, + version=version, + quantity=quantity, + ) + ) + + # FIXME(CHB, 2024-12-04) just like load labware and load adapter we have a validating after loading the object issue + validation.ensure_definition_is_lid(load_result.definition) + + deck_conflict.check( + engine_state=self._engine_client.state, + new_labware_id=load_result.stackLabwareId, + existing_disposal_locations=self._disposal_locations, + # TODO (spp, 2023-11-27): We've been using IDs from _labware_cores_by_id + # and _module_cores_by_id instead of getting the lists directly from engine + # because of the chance of engine carrying labware IDs from LPC too. + # But with https://github.com/Opentrons/opentrons/pull/13943, + # & LPC in maintenance runs, we can now rely on engine state for these IDs too. + # Wrapping .keys() in list() is just to make Decoy verification easier. + existing_labware_ids=list(self._labware_cores_by_id.keys()), + existing_module_ids=list(self._module_cores_by_id.keys()), + ) + + labware_core = LabwareCore( + labware_id=load_result.stackLabwareId, + engine_client=self._engine_client, + ) + + self._labware_cores_by_id[labware_core.labware_id] = labware_core + + return labware_core + def get_deck_definition(self) -> DeckDefinitionV5: """Get the geometry definition of the robot's deck.""" return self._engine_client.state.labware.get_deck_definition() @@ -725,9 +852,7 @@ def define_liquid( _id=liquid.id, name=liquid.displayName, description=liquid.description, - display_color=( - liquid.displayColor.__root__ if liquid.displayColor else None - ), + display_color=(liquid.displayColor.root if liquid.displayColor else None), ) def define_liquid_class(self, name: str) -> LiquidClass: diff --git a/api/src/opentrons/protocol_api/core/engine/robot.py b/api/src/opentrons/protocol_api/core/engine/robot.py new file mode 100644 index 00000000000..0418afcbb95 --- /dev/null +++ b/api/src/opentrons/protocol_api/core/engine/robot.py @@ -0,0 +1,139 @@ +from typing import Optional, Dict, Union +from opentrons.hardware_control import SyncHardwareAPI + +from opentrons.types import Mount, MountType, Point, AxisType, AxisMapType +from opentrons_shared_data.pipette import types as pip_types +from opentrons.protocol_api._types import PipetteActionTypes, PlungerPositionTypes +from opentrons.protocol_engine import commands as cmd +from opentrons.protocol_engine.clients import SyncClient as EngineClient +from opentrons.protocol_engine.types import DeckPoint, MotorAxis + +from opentrons.protocol_api.core.robot import AbstractRobot + + +_AXIS_TYPE_TO_MOTOR_AXIS = { + AxisType.X: MotorAxis.X, + AxisType.Y: MotorAxis.Y, + AxisType.P_L: MotorAxis.LEFT_PLUNGER, + AxisType.P_R: MotorAxis.RIGHT_PLUNGER, + AxisType.Z_L: MotorAxis.LEFT_Z, + AxisType.Z_R: MotorAxis.RIGHT_Z, + AxisType.Z_G: MotorAxis.EXTENSION_Z, + AxisType.G: MotorAxis.EXTENSION_JAW, + AxisType.Q: MotorAxis.AXIS_96_CHANNEL_CAM, +} + + +class RobotCore(AbstractRobot): + """Robot API core using a ProtocolEngine. + + Args: + engine_client: A client to the ProtocolEngine that is executing the protocol. + api_version: The Python Protocol API versionat which this core is operating. + sync_hardware: A SynchronousAdapter-wrapped Hardware Control API. + """ + + def __init__( + self, engine_client: EngineClient, sync_hardware_api: SyncHardwareAPI + ) -> None: + self._engine_client = engine_client + self._sync_hardware_api = sync_hardware_api + + def _convert_to_engine_mount(self, axis_map: AxisMapType) -> Dict[MotorAxis, float]: + return {_AXIS_TYPE_TO_MOTOR_AXIS[ax]: dist for ax, dist in axis_map.items()} + + def get_pipette_type_from_engine( + self, mount: Union[Mount, str] + ) -> Optional[pip_types.PipetteNameType]: + """Get the pipette attached to the given mount.""" + if isinstance(mount, Mount): + engine_mount = MountType[mount.name] + else: + if mount.lower() == "right": + engine_mount = MountType.RIGHT + else: + engine_mount = MountType.LEFT + maybe_pipette = self._engine_client.state.pipettes.get_by_mount(engine_mount) + return maybe_pipette.pipetteName if maybe_pipette else None + + def get_plunger_position_from_name( + self, mount: Mount, position_name: PlungerPositionTypes + ) -> float: + engine_mount = MountType[mount.name] + maybe_pipette = self._engine_client.state.pipettes.get_by_mount(engine_mount) + if not maybe_pipette: + return 0.0 + return self._engine_client.state.pipettes.lookup_plunger_position_name( + maybe_pipette.id, position_name.value + ) + + def get_plunger_position_from_volume( + self, mount: Mount, volume: float, action: PipetteActionTypes, robot_type: str + ) -> float: + engine_mount = MountType[mount.name] + maybe_pipette = self._engine_client.state.pipettes.get_by_mount(engine_mount) + if not maybe_pipette: + raise RuntimeError( + f"Cannot load plunger position as no pipette is attached to {mount}" + ) + convert_volume = ( + self._engine_client.state.pipettes.lookup_volume_to_mm_conversion( + maybe_pipette.id, volume, action.value + ) + ) + plunger_bottom = ( + self._engine_client.state.pipettes.lookup_plunger_position_name( + maybe_pipette.id, "bottom" + ) + ) + mm = volume / convert_volume + if robot_type == "OT-2 Standard": + position = plunger_bottom + mm + else: + position = plunger_bottom - mm + return round(position, 6) + + def move_to(self, mount: Mount, destination: Point, speed: Optional[float]) -> None: + engine_mount = MountType[mount.name] + engine_destination = DeckPoint( + x=destination.x, y=destination.y, z=destination.z + ) + self._engine_client.execute_command( + cmd.robot.MoveToParams( + mount=engine_mount, destination=engine_destination, speed=speed + ) + ) + + def move_axes_to( + self, + axis_map: AxisMapType, + critical_point: Optional[AxisMapType], + speed: Optional[float], + ) -> None: + axis_engine_map = self._convert_to_engine_mount(axis_map) + if critical_point: + critical_point_engine = self._convert_to_engine_mount(critical_point) + else: + critical_point_engine = None + + self._engine_client.execute_command( + cmd.robot.MoveAxesToParams( + axis_map=axis_engine_map, + critical_point=critical_point_engine, + speed=speed, + ) + ) + + def move_axes_relative(self, axis_map: AxisMapType, speed: Optional[float]) -> None: + axis_engine_map = self._convert_to_engine_mount(axis_map) + self._engine_client.execute_command( + cmd.robot.MoveAxesRelativeParams(axis_map=axis_engine_map, speed=speed) + ) + + def release_grip(self) -> None: + self._engine_client.execute_command(cmd.robot.openGripperJawParams()) + + def close_gripper(self, force: Optional[float] = None) -> None: + self._engine_client.execute_command( + cmd.robot.closeGripperJawParams(force=force) + ) diff --git a/api/src/opentrons/protocol_api/core/engine/well.py b/api/src/opentrons/protocol_api/core/engine/well.py index 6743a8a39c5..34616d9eb55 100644 --- a/api/src/opentrons/protocol_api/core/engine/well.py +++ b/api/src/opentrons/protocol_api/core/engine/well.py @@ -130,7 +130,10 @@ def load_liquid( liquid: Liquid, volume: float, ) -> None: - """Load liquid into a well.""" + """Load liquid into a well. + + If the well is known to be empty, use ``load_empty()`` instead of calling this with a 0.0 volume. + """ self._engine_client.execute_command( cmd.LoadLiquidParams( labwareId=self._labware_id, diff --git a/api/src/opentrons/protocol_api/core/instrument.py b/api/src/opentrons/protocol_api/core/instrument.py index 7d1816e1044..7f0fa4d72a7 100644 --- a/api/src/opentrons/protocol_api/core/instrument.py +++ b/api/src/opentrons/protocol_api/core/instrument.py @@ -3,14 +3,14 @@ from __future__ import annotations from abc import abstractmethod, ABC -from typing import Any, Generic, Optional, TypeVar, Union +from typing import Any, Generic, Optional, TypeVar, Union, List from opentrons import types from opentrons.hardware_control.dev_types import PipetteDict from opentrons.protocols.api_support.util import FlowRates +from opentrons.protocols.advanced_control.transfers.common import TransferTipPolicyV2 from opentrons.protocol_api._nozzle_layout import NozzleLayout -from opentrons.hardware_control.nozzle_manager import NozzleMap - +from opentrons.protocol_api._liquid import LiquidClass from ..disposal_locations import TrashBin, WasteChute from .well import WellCoreType @@ -24,6 +24,14 @@ def get_default_speed(self) -> float: def set_default_speed(self, speed: float) -> None: ... + @abstractmethod + def air_gap_in_place(self, volume: float, flow_rate: float) -> None: + """Aspirate a given volume of air from the current location of the pipette. + Args: + volume: The volume of air to aspirate, in microliters. + flow_rate: The flow rate of air into the pipette, in microliters. + """ + @abstractmethod def aspirate( self, @@ -172,6 +180,33 @@ def move_to( ) -> None: ... + @abstractmethod + def resin_tip_seal( + self, + location: types.Location, + well_core: WellCoreType, + in_place: Optional[bool] = False, + ) -> None: + ... + + @abstractmethod + def resin_tip_unseal( + self, + location: types.Location, + well_core: WellCoreType, + ) -> None: + ... + + @abstractmethod + def resin_tip_dispense( + self, + location: types.Location, + well_core: WellCoreType, + volume: Optional[float] = None, + flow_rate: Optional[float] = None, + ) -> None: + ... + @abstractmethod def get_mount(self) -> types.Mount: ... @@ -222,7 +257,7 @@ def get_active_channels(self) -> int: ... @abstractmethod - def get_nozzle_map(self) -> NozzleMap: + def get_nozzle_map(self) -> types.NozzleMapInterface: ... @abstractmethod @@ -253,6 +288,10 @@ def get_blow_out_flow_rate(self, rate: float = 1.0) -> float: def get_liquid_presence_detection(self) -> bool: ... + @abstractmethod + def _pressure_supported_by_pipette(self) -> bool: + ... + @abstractmethod def set_liquid_presence_detection(self, enable: bool) -> None: ... @@ -298,6 +337,32 @@ def configure_nozzle_layout( """ ... + @abstractmethod + def load_liquid_class( + self, + liquid_class: LiquidClass, + pipette_load_name: str, + tiprack_uri: str, + ) -> str: + """Load the liquid class properties of given pipette and tiprack into the engine. + + Returns: ID of the liquid class record + """ + ... + + @abstractmethod + def transfer_liquid( + self, + liquid_class_id: str, + volume: float, + source: List[WellCoreType], + dest: List[WellCoreType], + new_tip: TransferTipPolicyV2, + trash_location: Union[WellCoreType, types.Location, TrashBin, WasteChute], + ) -> None: + """Transfer a liquid from source to dest according to liquid class properties.""" + ... + @abstractmethod def is_tip_tracking_available(self) -> bool: """Return whether auto tip tracking is available for the pipette's current nozzle configuration.""" @@ -327,5 +392,9 @@ def liquid_probe_without_recovery( """Do a liquid probe to find the level of the liquid in the well.""" ... + @abstractmethod + def nozzle_configuration_valid_for_lld(self) -> bool: + """Check if the nozzle configuration currently supports LLD.""" + InstrumentCoreType = TypeVar("InstrumentCoreType", bound=AbstractInstrument[Any]) diff --git a/api/src/opentrons/protocol_api/core/labware.py b/api/src/opentrons/protocol_api/core/labware.py index 691a764e8d3..8bb5c66eb90 100644 --- a/api/src/opentrons/protocol_api/core/labware.py +++ b/api/src/opentrons/protocol_api/core/labware.py @@ -1,8 +1,9 @@ """The interface that implements InstrumentContext.""" + from __future__ import annotations from abc import ABC, abstractmethod -from typing import Any, Generic, List, NamedTuple, Optional, TypeVar +from typing import Any, Generic, List, NamedTuple, Optional, TypeVar, Dict from opentrons_shared_data.labware.types import ( LabwareUri, @@ -10,8 +11,8 @@ LabwareDefinition as LabwareDefinitionDict, ) -from opentrons.types import DeckSlotName, Point -from opentrons.hardware_control.nozzle_manager import NozzleMap +from opentrons.types import DeckSlotName, Point, NozzleMapInterface +from .._liquid import Liquid from .well import WellCoreType @@ -118,7 +119,7 @@ def get_next_tip( self, num_tips: int, starting_tip: Optional[WellCoreType], - nozzle_map: Optional[NozzleMap], + nozzle_map: Optional[NozzleMapInterface], ) -> Optional[str]: """Get the name of the next available tip(s) in the rack, if available.""" @@ -134,5 +135,13 @@ def get_well_core(self, well_name: str) -> WellCoreType: def get_deck_slot(self) -> Optional[DeckSlotName]: """Get the deck slot the labware or its parent is in, if any.""" + @abstractmethod + def load_liquid(self, volumes: Dict[str, float], liquid: Liquid) -> None: + """Load liquid into wells of the labware.""" + + @abstractmethod + def load_empty(self, wells: List[str]) -> None: + """Mark wells of the labware as empty.""" + LabwareCoreType = TypeVar("LabwareCoreType", bound=AbstractLabware[Any]) 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 ed1e0d607c9..20d0b862e53 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 @@ -1,12 +1,13 @@ from __future__ import annotations import logging -from typing import TYPE_CHECKING, Optional, Union +from typing import TYPE_CHECKING, Optional, Union, List from opentrons import types from opentrons.hardware_control import CriticalPoint from opentrons.hardware_control.dev_types import PipetteDict from opentrons.protocol_api.core.common import WellCore +from opentrons.protocols.advanced_control.transfers.common import TransferTipPolicyV2 from opentrons.protocols.api_support import instrument as instrument_support from opentrons.protocols.api_support.definitions import MAX_SUPPORTED_VERSION from opentrons.protocols.api_support.labware_like import LabwareLike @@ -19,7 +20,7 @@ ) from opentrons.protocols.geometry import planning from opentrons.protocol_api._nozzle_layout import NozzleLayout -from opentrons.hardware_control.nozzle_manager import NozzleMap +from opentrons.protocol_api._liquid import LiquidClass from ...disposal_locations import TrashBin, WasteChute from ..instrument import AbstractInstrument @@ -72,6 +73,9 @@ def set_default_speed(self, speed: float) -> None: """Sets the speed at which the robot's gantry moves.""" self._default_speed = speed + def air_gap_in_place(self, volume: float, flow_rate: float) -> None: + assert False, "Air gap tracking only available in API version 2.22 and later" + def aspirate( self, location: types.Location, @@ -304,6 +308,30 @@ def drop_tip_in_disposal_location( ) -> None: raise APIVersionError(api_element="Dropping tips in a trash bin or waste chute") + def resin_tip_seal( + self, + location: types.Location, + well_core: WellCore, + in_place: Optional[bool] = False, + ) -> None: + raise APIVersionError(api_element="Sealing resin tips.") + + def resin_tip_unseal( + self, + location: types.Location, + well_core: WellCore, + ) -> None: + raise APIVersionError(api_element="Unsealing resin tips.") + + def resin_tip_dispense( + self, + location: types.Location, + well_core: WellCore, + volume: Optional[float] = None, + flow_rate: Optional[float] = None, + ) -> None: + raise APIVersionError(api_element="Dispensing liquid from resin tips.") + def home(self) -> None: """Home the mount""" self._protocol_interface.get_hardware().home_z( @@ -396,6 +424,32 @@ def move_to( location=location, mount=location_cache_mount ) + def evotip_seal( + self, + location: types.Location, + well_core: LegacyWellCore, + in_place: Optional[bool] = False, + ) -> None: + """This will never be called because it was added in API 2.22.""" + assert False, "evotip_seal only supported in API 2.22 & later" + + def evotip_unseal( + self, location: types.Location, well_core: WellCore, home_after: Optional[bool] + ) -> None: + """This will never be called because it was added in API 2.22.""" + assert False, "evotip_unseal only supported in API 2.22 & later" + + def evotip_dispense( + self, + location: types.Location, + well_core: WellCore, + volume: Optional[float] = None, + flow_rate: Optional[float] = None, + push_out: Optional[float] = None, + ) -> None: + """This will never be called because it was added in API 2.22.""" + assert False, "evotip_dispense only supported in API 2.22 & later" + def get_mount(self) -> types.Mount: """Get the mount this pipette is attached to.""" return self._mount @@ -552,11 +606,34 @@ def configure_nozzle_layout( """This will never be called because it was added in API 2.16.""" pass + def load_liquid_class( + self, + liquid_class: LiquidClass, + pipette_load_name: str, + tiprack_uri: str, + ) -> str: + """This will never be called because it was added in ..""" + # TODO(spp, 2024-11-20): update the docstring and error to include API version + assert False, "load_liquid_class is not supported in legacy context" + + def transfer_liquid( + self, + liquid_class_id: str, + volume: float, + source: List[LegacyWellCore], + dest: List[LegacyWellCore], + new_tip: TransferTipPolicyV2, + trash_location: Union[LegacyWellCore, types.Location, TrashBin, WasteChute], + ) -> None: + """This will never be called because it was added in ..""" + # TODO(spp, 2024-11-20): update the docstring and error to include API version + assert False, "transfer_liquid is not supported in legacy context" + def get_active_channels(self) -> int: """This will never be called because it was added in API 2.16.""" assert False, "get_active_channels only supported in API 2.16 & later" - def get_nozzle_map(self) -> NozzleMap: + def get_nozzle_map(self) -> types.NozzleMapInterface: """This will never be called because it was added in API 2.18.""" assert False, "get_nozzle_map only supported in API 2.18 & later" @@ -583,3 +660,10 @@ def liquid_probe_without_recovery( ) -> float: """This will never be called because it was added in API 2.20.""" assert False, "liquid_probe_without_recovery only supported in API 2.20 & later" + + def _pressure_supported_by_pipette(self) -> bool: + return False + + def nozzle_configuration_valid_for_lld(self) -> bool: + """Check if the nozzle configuration currently supports LLD.""" + return False diff --git a/api/src/opentrons/protocol_api/core/legacy/legacy_labware_core.py b/api/src/opentrons/protocol_api/core/legacy/legacy_labware_core.py index 06411765d51..3957edb106c 100644 --- a/api/src/opentrons/protocol_api/core/legacy/legacy_labware_core.py +++ b/api/src/opentrons/protocol_api/core/legacy/legacy_labware_core.py @@ -1,13 +1,14 @@ -from typing import List, Optional +from typing import List, Optional, Dict from opentrons.calibration_storage import helpers from opentrons.protocols.geometry.labware_geometry import LabwareGeometry from opentrons.protocols.api_support.tip_tracker import TipTracker -from opentrons.types import DeckSlotName, Location, Point -from opentrons.hardware_control.nozzle_manager import NozzleMap +from opentrons.types import DeckSlotName, Location, Point, NozzleMapInterface + from opentrons_shared_data.labware.types import LabwareParameters, LabwareDefinition +from ..._liquid import Liquid from ..labware import AbstractLabware, LabwareLoadParams from .legacy_well_core import LegacyWellCore from .well_geometry import WellGeometry @@ -162,7 +163,7 @@ def get_next_tip( self, num_tips: int, starting_tip: Optional[LegacyWellCore], - nozzle_map: Optional[NozzleMap], + nozzle_map: Optional[NozzleMapInterface], ) -> Optional[str]: if nozzle_map is not None: raise ValueError( @@ -220,3 +221,11 @@ def get_deck_slot(self) -> Optional[DeckSlotName]: """Get the deck slot the labware is in, if in a deck slot.""" slot = self._geometry.parent.labware.first_parent() return DeckSlotName.from_primitive(slot) if slot is not None else None + + def load_liquid(self, volumes: Dict[str, float], liquid: Liquid) -> None: + """Load liquid into wells of the labware.""" + assert False, "load_liquid only supported in API version 2.22 & later" + + def load_empty(self, wells: List[str]) -> None: + """Mark wells of the labware as empty.""" + assert False, "load_empty only supported in API version 2.22 & later" 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 eac5a9109fa..8adadbe1ecf 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 @@ -6,7 +6,13 @@ from opentrons_shared_data.pipette.types import PipetteNameType from opentrons_shared_data.robot.types import RobotType -from opentrons.types import DeckSlotName, StagingSlotName, Location, Mount, Point +from opentrons.types import ( + DeckSlotName, + StagingSlotName, + Location, + Mount, + Point, +) from opentrons.util.broker import Broker from opentrons.hardware_control import SyncHardwareAPI from opentrons.hardware_control.modules import AbstractModule, ModuleModel, ModuleType @@ -267,6 +273,20 @@ def load_adapter( """Load an adapter using its identifying parameters""" raise APIVersionError(api_element="Loading adapter") + def load_lid( + self, + load_name: str, + location: LegacyLabwareCore, + namespace: Optional[str], + version: Optional[int], + ) -> LegacyLabwareCore: + """Load an individual lid labware using its identifying parameters. Must be loaded on a labware.""" + raise APIVersionError(api_element="Loading lid") + + def load_robot(self) -> None: # type: ignore + """Load an adapter using its identifying parameters""" + raise APIVersionError(api_element="Loading robot") + def move_labware( self, labware_core: LegacyLabwareCore, @@ -474,6 +494,17 @@ def set_last_location( self._last_location = location self._last_mount = mount + def load_lid_stack( + self, + load_name: str, + location: Union[DeckSlotName, StagingSlotName, LegacyLabwareCore], + quantity: int, + namespace: Optional[str], + version: Optional[int], + ) -> LegacyLabwareCore: + """Load a Stack of Lids to a given location, creating a Lid Stack.""" + raise APIVersionError(api_element="Lid stack") + def get_module_cores(self) -> List[legacy_module_core.LegacyModuleCore]: """Get loaded module cores.""" return self._module_cores diff --git a/api/src/opentrons/protocol_api/core/legacy/legacy_robot_core.py b/api/src/opentrons/protocol_api/core/legacy/legacy_robot_core.py new file mode 100644 index 00000000000..e69de29bb2d 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 55bde6c0a75..54c43a90f8c 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 @@ -1,12 +1,13 @@ from __future__ import annotations import logging -from typing import TYPE_CHECKING, Optional, Union +from typing import TYPE_CHECKING, Optional, Union, List from opentrons import types from opentrons.hardware_control.dev_types import PipetteDict from opentrons.hardware_control.types import HardwareAction from opentrons.protocol_api.core.common import WellCore +from opentrons.protocols.advanced_control.transfers.common import TransferTipPolicyV2 from opentrons.protocols.api_support import instrument as instrument_support from opentrons.protocols.api_support.labware_like import LabwareLike from opentrons.protocols.api_support.types import APIVersion @@ -24,7 +25,7 @@ from ...disposal_locations import TrashBin, WasteChute from opentrons.protocol_api._nozzle_layout import NozzleLayout -from opentrons.hardware_control.nozzle_manager import NozzleMap +from opentrons.protocol_api._liquid import LiquidClass from ..instrument import AbstractInstrument @@ -83,6 +84,9 @@ def get_default_speed(self) -> float: def set_default_speed(self, speed: float) -> None: self._default_speed = speed + def air_gap_in_place(self, volume: float, flow_rate: float) -> None: + assert False, "Air gap tracking only available in API version 2.22 and later" + def aspirate( self, location: types.Location, @@ -272,6 +276,30 @@ def drop_tip_in_disposal_location( ) -> None: raise APIVersionError(api_element="Dropping tips in a trash bin or waste chute") + def resin_tip_seal( + self, + location: types.Location, + well_core: WellCore, + in_place: Optional[bool] = False, + ) -> None: + raise APIVersionError(api_element="Sealing resin tips.") + + def resin_tip_unseal( + self, + location: types.Location, + well_core: WellCore, + ) -> None: + raise APIVersionError(api_element="Unsealing resin tips.") + + def resin_tip_dispense( + self, + location: types.Location, + well_core: WellCore, + volume: Optional[float] = None, + flow_rate: Optional[float] = None, + ) -> None: + raise APIVersionError(api_element="Dispensing liquid from resin tips.") + def home(self) -> None: self._protocol_interface.set_last_location(None) @@ -470,11 +498,34 @@ def configure_nozzle_layout( """This will never be called because it was added in API 2.15.""" pass + def load_liquid_class( + self, + liquid_class: LiquidClass, + pipette_load_name: str, + tiprack_uri: str, + ) -> str: + """This will never be called because it was added in ..""" + # TODO(spp, 2024-11-20): update the docstring and error to include API version + assert False, "load_liquid_class is not supported in legacy context" + + def transfer_liquid( + self, + liquid_class_id: str, + volume: float, + source: List[LegacyWellCore], + dest: List[LegacyWellCore], + new_tip: TransferTipPolicyV2, + trash_location: Union[LegacyWellCore, types.Location, TrashBin, WasteChute], + ) -> None: + """Transfer a liquid from source to dest according to liquid class properties.""" + # TODO(spp, 2024-11-20): update the docstring and error to include API version + assert False, "transfer_liquid is not supported in legacy context" + def get_active_channels(self) -> int: """This will never be called because it was added in API 2.16.""" assert False, "get_active_channels only supported in API 2.16 & later" - def get_nozzle_map(self) -> NozzleMap: + def get_nozzle_map(self) -> types.NozzleMapInterface: """This will never be called because it was added in API 2.18.""" assert False, "get_nozzle_map only supported in API 2.18 & later" @@ -501,3 +552,10 @@ def liquid_probe_without_recovery( ) -> float: """This will never be called because it was added in API 2.20.""" assert False, "liquid_probe_without_recovery only supported in API 2.20 & later" + + def _pressure_supported_by_pipette(self) -> bool: + return False + + def nozzle_configuration_valid_for_lld(self) -> bool: + """Check if the nozzle configuration currently supports LLD.""" + return False diff --git a/api/src/opentrons/protocol_api/core/protocol.py b/api/src/opentrons/protocol_api/core/protocol.py index f79ab987157..27d41b921b0 100644 --- a/api/src/opentrons/protocol_api/core/protocol.py +++ b/api/src/opentrons/protocol_api/core/protocol.py @@ -10,7 +10,13 @@ from opentrons_shared_data.labware.types import LabwareDefinition from opentrons_shared_data.robot.types import RobotType -from opentrons.types import DeckSlotName, StagingSlotName, Location, Mount, Point +from opentrons.types import ( + DeckSlotName, + StagingSlotName, + Location, + Mount, + Point, +) from opentrons.hardware_control import SyncHardwareAPI from opentrons.hardware_control.modules.types import ModuleModel from opentrons.protocols.api_support.util import AxisMaxSpeeds @@ -19,6 +25,7 @@ from .labware import LabwareCoreType, LabwareLoadParams from .module import ModuleCoreType from .._liquid import Liquid, LiquidClass +from .robot import AbstractRobot from .._types import OffDeckType from ..disposal_locations import TrashBin, WasteChute @@ -93,6 +100,17 @@ def load_adapter( """Load an adapter using its identifying parameters""" ... + @abstractmethod + def load_lid( + self, + load_name: str, + location: LabwareCoreType, + namespace: Optional[str], + version: Optional[int], + ) -> LabwareCoreType: + """Load an individual lid labware using its identifying parameters. Must be loaded on a labware.""" + ... + @abstractmethod def move_labware( self, @@ -190,6 +208,17 @@ def set_last_location( ) -> None: ... + @abstractmethod + def load_lid_stack( + self, + load_name: str, + location: Union[DeckSlotName, StagingSlotName, LabwareCoreType], + quantity: int, + namespace: Optional[str], + version: Optional[int], + ) -> LabwareCoreType: + ... + @abstractmethod def get_deck_definition(self) -> DeckDefinitionV5: """Get the geometry definition of the robot's deck.""" @@ -257,3 +286,7 @@ def get_labware_location( self, labware_core: LabwareCoreType ) -> Union[str, LabwareCoreType, ModuleCoreType, OffDeckType]: """Get labware parent location.""" + + @abstractmethod + def load_robot(self) -> AbstractRobot: + """Load a Robot Core context into a protocol""" diff --git a/api/src/opentrons/protocol_api/core/robot.py b/api/src/opentrons/protocol_api/core/robot.py new file mode 100644 index 00000000000..f65ddbbd7bb --- /dev/null +++ b/api/src/opentrons/protocol_api/core/robot.py @@ -0,0 +1,51 @@ +from abc import abstractmethod, ABC +from typing import Optional, Union + +from opentrons.types import AxisMapType, Mount, Point +from opentrons_shared_data.pipette.types import PipetteNameType +from opentrons.protocol_api._types import PlungerPositionTypes, PipetteActionTypes + + +class AbstractRobot(ABC): + @abstractmethod + def get_pipette_type_from_engine( + self, mount: Union[Mount, str] + ) -> Optional[PipetteNameType]: + ... + + @abstractmethod + def get_plunger_position_from_volume( + self, mount: Mount, volume: float, action: PipetteActionTypes, robot_type: str + ) -> float: + ... + + @abstractmethod + def get_plunger_position_from_name( + self, mount: Mount, position_name: PlungerPositionTypes + ) -> float: + ... + + @abstractmethod + def move_to(self, mount: Mount, destination: Point, speed: Optional[float]) -> None: + ... + + @abstractmethod + def move_axes_to( + self, + axis_map: AxisMapType, + critical_point: Optional[AxisMapType], + speed: Optional[float], + ) -> None: + ... + + @abstractmethod + def move_axes_relative(self, axis_map: AxisMapType, speed: Optional[float]) -> None: + ... + + @abstractmethod + def release_grip(self) -> None: + ... + + @abstractmethod + def close_gripper(self, force: Optional[float] = None) -> None: + ... diff --git a/api/src/opentrons/protocol_api/instrument_context.py b/api/src/opentrons/protocol_api/instrument_context.py index 880626b53c9..bc2e072b671 100644 --- a/api/src/opentrons/protocol_api/instrument_context.py +++ b/api/src/opentrons/protocol_api/instrument_context.py @@ -2,12 +2,14 @@ import logging from contextlib import ExitStack from typing import Any, List, Optional, Sequence, Union, cast, Dict -from opentrons.protocol_engine.errors.exceptions import TipNotAttachedError from opentrons_shared_data.errors.exceptions import ( CommandPreconditionViolated, CommandParameterLimitViolated, UnexpectedTipRemovalError, + UnsupportedHardwareCommand, ) +from opentrons_shared_data.robot.types import RobotTypeEnum + from opentrons.legacy_broker import LegacyBroker from opentrons.hardware_control.dev_types import PipetteDict from opentrons import types @@ -15,8 +17,7 @@ from opentrons.legacy_commands import publisher from opentrons.protocols.advanced_control.mix import mix_from_kwargs -from opentrons.protocols.advanced_control import transfers - +from opentrons.protocols.advanced_control.transfers import transfer as v1_transfer from opentrons.protocols.api_support.deck_type import NoTrashDefinedError from opentrons.protocols.api_support.types import APIVersion from opentrons.protocols.api_support import instrument @@ -28,7 +29,6 @@ APIVersionError, UnsupportedAPIError, ) -from opentrons.hardware_control.nozzle_manager import NozzleConfigurationType from .core.common import InstrumentCore, ProtocolCore from .core.engine import ENGINE_CORE_API_VERSION @@ -36,10 +36,13 @@ from .config import Clearances from .disposal_locations import TrashBin, WasteChute from ._nozzle_layout import NozzleLayout +from ._liquid import LiquidClass from . import labware, validation - - -AdvancedLiquidHandling = transfers.AdvancedLiquidHandling +from ..config import feature_flags +from ..protocols.advanced_control.transfers.common import ( + TransferTipPolicyV2, + TransferTipPolicyV2Type, +) _DEFAULT_ASPIRATE_CLEARANCE = 1.0 _DEFAULT_DISPENSE_CLEARANCE = 1.0 @@ -60,6 +63,10 @@ """The version after which offsets for deck configured trash containers and changes to alternating tip drop behavior were introduced.""" _PARTIAL_NOZZLE_CONFIGURATION_SINGLE_ROW_PARTIAL_COLUMN_ADDED_IN = APIVersion(2, 20) """The version after which partial nozzle configurations of single, row, and partial column layouts became available.""" +_AIR_GAP_TRACKING_ADDED_IN = APIVersion(2, 22) +"""The version after which air gaps should be implemented with a separate call instead of an aspirate for better liquid volume tracking.""" + +AdvancedLiquidHandling = v1_transfer.AdvancedLiquidHandling class InstrumentContext(publisher.CommandPublisher): @@ -257,9 +264,10 @@ def aspirate( self.api_version >= APIVersion(2, 20) and well is not None and self.liquid_presence_detection - and self._96_tip_config_valid() + and self._core.nozzle_configuration_valid_for_lld() and self._core.get_current_volume() == 0 ): + self._raise_if_pressure_not_supported_by_pipette() self.require_liquid_presence(well=well) with publisher.publish_context( @@ -513,6 +521,8 @@ def mix( ``pipette.mix(1, location=wellplate['A1'])`` is a valid call, but ``pipette.mix(1, wellplate['A1'])`` is not. + .. versionchanged:: 2.21 + Does not repeatedly check for liquid presence. """ _log.debug( "mixing {}uL with {} repetitions in {} at rate={}".format( @@ -753,7 +763,11 @@ def air_gap( ``pipette.air_gap(height=2)``. If you call ``air_gap`` with a single, unnamed argument, it will always be interpreted as a volume. - + .. TODO: restore this as a note block for 2.22 docs + Before API version 2.22, this function was implemented as an aspirate, and + dispensing into a well would add the air gap volume to the liquid tracked in + the well. At or above API version 2.22, air gap volume is not counted as liquid + when dispensing into a well. """ if not self._core.has_tip(): raise UnexpectedTipRemovalError("air_gap", self.name, self.mount) @@ -765,7 +779,12 @@ def air_gap( raise RuntimeError("No previous Well cached to perform air gap") target = loc.labware.as_well().top(height) self.move_to(target, publish=False) - self.aspirate(volume) + if self.api_version >= _AIR_GAP_TRACKING_ADDED_IN: + c_vol = self._core.get_available_volume() if volume is None else volume + flow_rate = self._core.get_aspirate_flow_rate() + self._core.air_gap_in_place(c_vol, flow_rate) + else: + self.aspirate(volume) return self @publisher.publish(command=cmds.return_tip) @@ -934,7 +953,7 @@ def pick_up_tip( # noqa: C901 if location is None: if ( nozzle_map is not None - and nozzle_map.configuration != NozzleConfigurationType.FULL + and nozzle_map.configuration != types.NozzleConfigurationType.FULL and self.starting_tip is not None ): # Disallowing this avoids concerning the system with the direction @@ -1206,7 +1225,6 @@ def home_plunger(self) -> InstrumentContext: self._core.home_plunger() return self - # TODO (spp, 2024-03-08): verify if ok to & change source & dest types to AdvancedLiquidHandling @publisher.publish(command=cmds.distribute) @requires_version(2, 0) def distribute( @@ -1246,7 +1264,6 @@ def distribute( return self.transfer(volume, source, dest, **kwargs) - # TODO (spp, 2024-03-08): verify if ok to & change source & dest types to AdvancedLiquidHandling @publisher.publish(command=cmds.consolidate) @requires_version(2, 0) def consolidate( @@ -1396,9 +1413,9 @@ def transfer( # noqa: C901 mix_strategy, mix_opts = mix_from_kwargs(kwargs) if trash: - drop_tip = transfers.DropTipStrategy.TRASH + drop_tip = v1_transfer.DropTipStrategy.TRASH else: - drop_tip = transfers.DropTipStrategy.RETURN + drop_tip = v1_transfer.DropTipStrategy.RETURN new_tip = kwargs.get("new_tip") if isinstance(new_tip, str): @@ -1420,19 +1437,19 @@ def transfer( # noqa: C901 if blow_out and not blowout_location: if self.current_volume: - blow_out_strategy = transfers.BlowOutStrategy.SOURCE + blow_out_strategy = v1_transfer.BlowOutStrategy.SOURCE else: - blow_out_strategy = transfers.BlowOutStrategy.TRASH + blow_out_strategy = v1_transfer.BlowOutStrategy.TRASH elif blow_out and blowout_location: if blowout_location == "source well": - blow_out_strategy = transfers.BlowOutStrategy.SOURCE + blow_out_strategy = v1_transfer.BlowOutStrategy.SOURCE elif blowout_location == "destination well": - blow_out_strategy = transfers.BlowOutStrategy.DEST + blow_out_strategy = v1_transfer.BlowOutStrategy.DEST elif blowout_location == "trash": - blow_out_strategy = transfers.BlowOutStrategy.TRASH + blow_out_strategy = v1_transfer.BlowOutStrategy.TRASH if new_tip != types.TransferTipPolicy.NEVER: - tr, next_tip = labware.next_available_tip( + _, next_tip = labware.next_available_tip( self.starting_tip, self.tip_racks, active_channels, @@ -1444,9 +1461,9 @@ def transfer( # noqa: C901 touch_tip = None if kwargs.get("touch_tip"): - touch_tip = transfers.TouchTipStrategy.ALWAYS + touch_tip = v1_transfer.TouchTipStrategy.ALWAYS - default_args = transfers.Transfer() + default_args = v1_transfer.Transfer() disposal = kwargs.get("disposal_volume") if disposal is None: @@ -1459,7 +1476,7 @@ def transfer( # noqa: C901 f"working volume, {max_volume}uL" ) - transfer_args = transfers.Transfer( + transfer_args = v1_transfer.Transfer( new_tip=new_tip or default_args.new_tip, air_gap=air_gap, carryover=kwargs.get("carryover") or default_args.carryover, @@ -1472,10 +1489,10 @@ def transfer( # noqa: C901 blow_out_strategy=blow_out_strategy or default_args.blow_out_strategy, touch_tip_strategy=(touch_tip or default_args.touch_tip_strategy), ) - transfer_options = transfers.TransferOptions( + transfer_options = v1_transfer.TransferOptions( transfer=transfer_args, mix=mix_opts ) - plan = transfers.TransferPlan( + plan = v1_transfer.TransferPlan( volume, source, dest, @@ -1488,10 +1505,113 @@ def transfer( # noqa: C901 self._execute_transfer(plan) return self - def _execute_transfer(self, plan: transfers.TransferPlan) -> None: + def _execute_transfer(self, plan: v1_transfer.TransferPlan) -> None: for cmd in plan: getattr(self, cmd["method"])(*cmd["args"], **cmd["kwargs"]) + def transfer_liquid( + self, + liquid_class: LiquidClass, + volume: float, + source: Union[ + labware.Well, Sequence[labware.Well], Sequence[Sequence[labware.Well]] + ], + dest: Union[ + labware.Well, Sequence[labware.Well], Sequence[Sequence[labware.Well]] + ], + new_tip: TransferTipPolicyV2Type = "once", + tip_drop_location: Optional[ + Union[types.Location, labware.Well, TrashBin, WasteChute] + ] = None, # Maybe call this 'tip_drop_location' which is similar to PD + ) -> InstrumentContext: + """Transfer liquid from source to dest using the specified liquid class properties. + + TODO: Add args description. + """ + if not feature_flags.allow_liquid_classes( + robot_type=RobotTypeEnum.robot_literal_to_enum( + self._protocol_core.robot_type + ) + ): + raise NotImplementedError("This method is not implemented.") + + flat_sources_list = validation.ensure_valid_flat_wells_list_for_transfer_v2( + source + ) + flat_dests_list = validation.ensure_valid_flat_wells_list_for_transfer_v2(dest) + for well in flat_sources_list + flat_dests_list: + instrument.validate_takes_liquid( + location=well.top(), + reject_module=True, + reject_adapter=True, + ) + if len(flat_sources_list) != len(flat_dests_list): + raise ValueError( + "Sources and destinations should be of the same length in order to perform a transfer." + " To transfer liquid from one source to many destinations, use 'distribute_liquid'," + " to transfer liquid onto one destinations from many sources, use 'consolidate_liquid'." + ) + + valid_new_tip = validation.ensure_new_tip_policy(new_tip) + if valid_new_tip == TransferTipPolicyV2.NEVER: + if self._last_tip_picked_up_from is None: + raise RuntimeError( + "Pipette has no tip attached to perform transfer." + " Either do a pick_up_tip beforehand or specify a new_tip parameter" + " of 'once' or 'always'." + ) + else: + tiprack = self._last_tip_picked_up_from.parent + else: + tiprack, well = labware.next_available_tip( + starting_tip=self.starting_tip, + tip_racks=self.tip_racks, + channels=self.active_channels, + nozzle_map=self._core.get_nozzle_map(), + ) + if self.current_volume != 0: + raise RuntimeError( + "A transfer on a liquid class cannot start with liquid already in the tip." + " Ensure that all previously aspirated liquid is dispensed before starting" + " a new transfer." + ) + + _trash_location: Union[types.Location, labware.Well, TrashBin, WasteChute] + if tip_drop_location is None: + saved_trash = self.trash_container + if isinstance(saved_trash, labware.Labware): + _trash_location = saved_trash.wells()[0] + else: + _trash_location = saved_trash + else: + _trash_location = tip_drop_location + + checked_trash_location = ( + validation.ensure_valid_tip_drop_location_for_transfer_v2( + tip_drop_location=_trash_location + ) + ) + liquid_class_id = self._core.load_liquid_class( + liquid_class=liquid_class, + pipette_load_name=self.name, + tiprack_uri=tiprack.uri, + ) + + self._core.transfer_liquid( + liquid_class_id=liquid_class_id, + volume=volume, + source=[well._core for well in flat_sources_list], + dest=[well._core for well in flat_dests_list], + new_tip=valid_new_tip, + trash_location=( + checked_trash_location._core + if isinstance(checked_trash_location, labware.Well) + else checked_trash_location + ), + ) + + return self + @requires_version(2, 0) def delay(self, *args: Any, **kwargs: Any) -> None: """ @@ -1592,6 +1712,147 @@ def move_to( return self + @requires_version(2, 22) + def resin_tip_seal( + self, + location: Union[labware.Well, labware.Labware], + ) -> InstrumentContext: + """Seal resin tips onto the pipette. + + The location provided should contain resin tips. Sealing the + tip will perform a `pick up` action but there will be no tip tracking + associated with the pipette. + + :param location: A location containing resin tips, must be a Labware or a Well. + + :type location: :py:class:`~.types.Location` + """ + if isinstance(location, labware.Labware): + well = location.wells()[0] + else: + well = location + + with publisher.publish_context( + broker=self.broker, + command=cmds.seal( + instrument=self, + location=well, + ), + ): + self._core.resin_tip_seal( + location=well.top(), well_core=well._core, in_place=False + ) + return self + + @requires_version(2, 22) + def resin_tip_unseal( + self, + location: Union[labware.Well, labware.Labware], + ) -> InstrumentContext: + """Release resin tips from the pipette. + + The location provided should be a valid location to drop resin tips. + + :param location: A location containing that can accept tips. + + :type location: :py:class:`~.types.Location` + + :param home_after: + Whether to home the pipette after dropping the tip. If not specified + defaults to ``True`` on a Flex. The plunger will not home on an unseal. + + When ``False``, the pipette does not home its plunger. This can save a few + seconds, but is not recommended. Homing helps the robot track the pipette's + position. + + """ + if isinstance(location, labware.Labware): + well = location.wells()[0] + else: + well = location + + with publisher.publish_context( + broker=self.broker, + command=cmds.unseal( + instrument=self, + location=well, + ), + ): + self._core.resin_tip_unseal(location=well.top(), well_core=well._core) + + return self + + @requires_version(2, 22) + def resin_tip_dispense( + self, + location: types.Location, + volume: Optional[float] = None, + rate: Optional[float] = None, + ) -> InstrumentContext: + """Dispense a volume from resin tips into a labware. + + The location provided should contain resin tips labware as well as a + receptical for dispensed liquid. Dispensing from tip will perform a + `dispense` action of the specified volume at a desired flow rate. + + :param location: A location containing resin tips. + :type location: :py:class:`~.types.Location` + + :param volume: Will default to maximum, recommended to use the default. + The volume, in µL, that the pipette will prepare to handle. + :type volume: float + + :param rate: Will default to 10.0, recommended to use the default. How quickly + a pipette dispenses liquid. The speed in µL/s is calculated as + ``rate`` multiplied by :py:attr:`flow_rate.dispense`. + :type rate: float + + """ + well: Optional[labware.Well] = None + last_location = self._get_last_location_by_api_version() + + try: + target = validation.validate_location( + location=location, last_location=last_location + ) + except validation.NoLocationError as e: + raise RuntimeError( + "If dispense is called without an explicit location, another" + " method that moves to a location (such as move_to or " + "aspirate) must previously have been called so the robot " + "knows where it is." + ) from e + + if isinstance(target, validation.WellTarget): + well = target.well + if target.location: + move_to_location = target.location + elif well.parent._core.is_fixed_trash(): + move_to_location = target.well.top() + else: + move_to_location = target.well.bottom( + z=self._well_bottom_clearances.dispense + ) + else: + raise RuntimeError( + "A well must be specified when using `resin_tip_dispense`." + ) + + with publisher.publish_context( + broker=self.broker, + command=cmds.resin_tip_dispense( + instrument=self, + flow_rate=rate, + ), + ): + self._core.resin_tip_dispense( + move_to_location, + well_core=well._core, + volume=volume, + flow_rate=rate, + ) + return self + @requires_version(2, 18) def _retract( self, @@ -1694,6 +1955,8 @@ def liquid_presence_detection(self) -> bool: @liquid_presence_detection.setter @requires_version(2, 20) def liquid_presence_detection(self, enable: bool) -> None: + if enable: + self._raise_if_pressure_not_supported_by_pipette() self._core.set_liquid_presence_detection(enable) @property @@ -1870,19 +2133,6 @@ def _get_last_location_by_api_version(self) -> Optional[types.Location]: else: return self._protocol_core.get_last_location() - def _96_tip_config_valid(self) -> bool: - n_map = self._core.get_nozzle_map() - channels = self._core.get_active_channels() - if channels == 96: - if ( - n_map.back_left != n_map.full_instrument_back_left - and n_map.front_right != n_map.full_instrument_front_right - ): - raise TipNotAttachedError( - "Either the front right or the back left nozzle must have a tip attached to do LLD." - ) - return True - def __repr__(self) -> str: return "<{}: {} in {}>".format( self.__class__.__name__, @@ -2143,8 +2393,8 @@ def detect_liquid_presence(self, well: labware.Well) -> bool: .. note:: The pressure sensors for the Flex 8-channel pipette are on channels 1 and 8 (positions A1 and H1). For the Flex 96-channel pipette, the pressure sensors are on channels 1 and 96 (positions A1 and H12). Other channels on multi-channel pipettes do not have sensors and cannot detect liquid. """ + self._raise_if_pressure_not_supported_by_pipette() loc = well.top() - self._96_tip_config_valid() return self._core.detect_liquid_presence(well._core, loc) @requires_version(2, 20) @@ -2156,8 +2406,8 @@ def require_liquid_presence(self, well: labware.Well) -> None: .. note:: The pressure sensors for the Flex 8-channel pipette are on channels 1 and 8 (positions A1 and H1). For the Flex 96-channel pipette, the pressure sensors are on channels 1 and 96 (positions A1 and H12). Other channels on multi-channel pipettes do not have sensors and cannot detect liquid. """ + self._raise_if_pressure_not_supported_by_pipette() loc = well.top() - self._96_tip_config_valid() self._core.liquid_probe_with_recovery(well._core, loc) @requires_version(2, 20) @@ -2170,9 +2420,8 @@ def measure_liquid_height(self, well: labware.Well) -> float: This is intended for Opentrons internal use only and is not a guaranteed API. """ - + self._raise_if_pressure_not_supported_by_pipette() loc = well.top() - self._96_tip_config_valid() height = self._core.liquid_probe_without_recovery(well._core, loc) return height @@ -2192,6 +2441,12 @@ def _raise_if_configuration_not_supported_by_pipette( ) # SINGLE, QUADRANT and ALL are supported by all pipettes + def _raise_if_pressure_not_supported_by_pipette(self) -> None: + if not self._core._pressure_supported_by_pipette(): + raise UnsupportedHardwareCommand( + "Pressure sensor not available for this pipette" + ) + def _handle_aspirate_target( self, target: validation.ValidTarget ) -> tuple[types.Location, Optional[labware.Well], Optional[bool]]: diff --git a/api/src/opentrons/protocol_api/labware.py b/api/src/opentrons/protocol_api/labware.py index 0e8a17d07d3..98ec5c7308d 100644 --- a/api/src/opentrons/protocol_api/labware.py +++ b/api/src/opentrons/protocol_api/labware.py @@ -1,4 +1,4 @@ -""" opentrons.protocol_api.labware: classes and functions for labware handling +"""opentrons.protocol_api.labware: classes and functions for labware handling This module provides things like :py:class:`Labware`, and :py:class:`Well` to encapsulate labware instances used in protocols @@ -13,18 +13,29 @@ import logging from itertools import dropwhile -from typing import TYPE_CHECKING, Any, List, Dict, Optional, Union, Tuple, cast +from typing import ( + TYPE_CHECKING, + Any, + List, + Dict, + Optional, + Union, + Tuple, + cast, + Sequence, + Mapping, +) from opentrons_shared_data.labware.types import LabwareDefinition, LabwareParameters -from opentrons.types import Location, Point +from opentrons.types import Location, Point, NozzleMapInterface from opentrons.protocols.api_support.types import APIVersion from opentrons.protocols.api_support.util import ( requires_version, APIVersionError, UnsupportedAPIError, ) -from opentrons.hardware_control.nozzle_manager import NozzleMap + # TODO(mc, 2022-09-02): re-exports provided for backwards compatibility # remove when their usage is no longer needed @@ -104,8 +115,23 @@ def parent(self) -> Labware: @property @requires_version(2, 0) def has_tip(self) -> bool: - """Whether this well contains a tip. Always ``False`` if the parent labware - isn't a tip rack.""" + """Whether this well contains an unused tip. + + From API v2.2 on: + + - Returns ``False`` if: + + - the well has no tip present, or + - the well has a tip that's been used by the protocol previously + + - Returns ``True`` if the well has an unused tip. + + Before API v2.2: + + - Returns ``True`` as long as the well has a tip, even if it is used. + + Always ``False`` if the parent labware isn't a tip rack. + """ return self._core.has_tip() @has_tip.setter @@ -280,6 +306,10 @@ def load_liquid(self, liquid: Liquid, volume: float) -> None: :param Liquid liquid: The liquid to load into the well. :param float volume: The volume of liquid to load, in µL. + + .. TODO: flag as deprecated in 2.22 docs + In API version 2.22 and later, use :py:meth:`~Labware.load_liquid`, :py:meth:`~Labware.load_liquid_by_well`, + or :py:meth:`~Labware.load_empty` to load liquid into a well. """ self._core.load_liquid( liquid=liquid, @@ -529,6 +559,7 @@ def load_labware( self, name: str, label: Optional[str] = None, + lid: Optional[str] = None, namespace: Optional[str] = None, version: Optional[int] = None, ) -> Labware: @@ -558,6 +589,20 @@ def load_labware( self._core_map.add(labware_core, labware) + if lid is not None: + if self._api_version < validation.LID_STACK_VERSION_GATE: + raise APIVersionError( + api_element="Loading a Lid on a Labware", + until_version="2.23", + current_version=f"{self._api_version}", + ) + self._protocol_core.load_lid( + load_name=lid, + location=labware_core, + namespace=namespace, + version=version, + ) + return labware @requires_version(2, 15) @@ -582,6 +627,65 @@ def load_labware_from_definition( label=label, ) + @requires_version(2, 23) + def load_lid_stack( + self, + load_name: str, + quantity: int, + namespace: Optional[str] = None, + version: Optional[int] = None, + ) -> Labware: + """ + Load a stack of Lids onto a valid Deck Location or Adapter. + + :param str load_name: A string to use for looking up a lid definition. + You can find the ``load_name`` for any standard lid on the Opentrons + `Labware Library `_. + :param int quantity: The quantity of lids to be loaded in the stack. + :param str namespace: The namespace that the lid labware definition belongs to. + If unspecified, the API will automatically search two namespaces: + + - ``"opentrons"``, to load standard Opentrons labware definitions. + - ``"custom_beta"``, to load custom labware definitions created with the + `Custom Labware Creator `__. + + You might need to specify an explicit ``namespace`` if you have a custom + definition whose ``load_name`` is the same as an Opentrons-verified + definition, and you want to explicitly choose one or the other. + + :param version: The version of the labware definition. You should normally + leave this unspecified to let ``load_lid_stack()`` choose a version + automatically. + + :return: The initialized and loaded labware object representing the Lid Stack. + """ + if self._api_version < validation.LID_STACK_VERSION_GATE: + raise APIVersionError( + api_element="Loading a Lid Stack", + until_version="2.23", + current_version=f"{self._api_version}", + ) + + load_location = self._core + + load_name = validation.ensure_lowercase_name(load_name) + + result = self._protocol_core.load_lid_stack( + load_name=load_name, + location=load_location, + quantity=quantity, + namespace=namespace, + version=version, + ) + + labware = Labware( + core=result, + api_version=self._api_version, + protocol_core=self._protocol_core, + core_map=self._core_map, + ) + return labware + def set_calibration(self, delta: Point) -> None: """ An internal, deprecated method used for updating the labware offset. @@ -932,7 +1036,7 @@ def next_tip( num_tips: int = 1, starting_tip: Optional[Well] = None, *, - nozzle_map: Optional[NozzleMap] = None, + nozzle_map: Optional[NozzleMapInterface] = None, ) -> Optional[Well]: """ Find the next valid well for pick-up. @@ -1105,6 +1209,141 @@ def reset(self) -> None: """ self._core.reset_tips() + @requires_version(2, 22) + def load_liquid( + self, wells: Sequence[Union[str, Well]], volume: float, liquid: Liquid + ) -> None: + """Mark several wells as containing the same amount of liquid. + + This method should be called at the beginning of a protocol, soon after loading the labware and before + liquid handling operations begin. It is a base of information for liquid tracking functionality. If a well in a labware + has not been named in a call to :py:meth:`~Labware.load_empty`, :py:meth:`~Labware.load_liquid`, or + :py:meth:`~Labware.load_liquid_by_well`, the volume it contains is unknown and the well's liquid will not be tracked. + + For example, to load 10µL of a liquid named ``water`` (defined with :py:meth:`~ProtocolContext.define_liquid`) + into all the wells of a labware, you could call ``labware.load_liquid(labware.wells(), 10, water)``. + + If you want to load different volumes of liquid into different wells, use :py:meth:`~Labware.load_liquid_by_well`. + + If you want to mark the well as containing no liquid, use :py:meth:`~Labware.load_empty`. + + :param wells: The wells to load the liquid into. + :type wells: List of well names or list of Well objects, for instance from :py:meth:`~Labware.wells`. + + :param volume: The volume of liquid to load into each well, in 10µL. + :type volume: float + + :param liquid: The liquid to load into each well, previously defined by :py:meth:`~ProtocolContext.define_liquid` + :type liquid: Liquid + """ + well_names: List[str] = [] + for well in wells: + if isinstance(well, str): + if well not in self.wells_by_name(): + raise KeyError( + f"{well} is not a well in labware {self.name}. The elements of wells should name wells in this labware." + ) + well_names.append(well) + elif isinstance(well, Well): + if well.parent is not self: + raise KeyError( + f"{well.well_name} is not a well in labware {self.name}. The elements of wells should be wells of this labware." + ) + well_names.append(well.well_name) + else: + raise TypeError( + f"Unexpected type for element {repr(well)}. The elements of wells should be Well instances or well names." + ) + if not isinstance(volume, (float, int)): + raise TypeError( + f"Unexpected type for volume {repr(volume)}. Volume should be a number in microliters." + ) + self._core.load_liquid({well_name: volume for well_name in well_names}, liquid) + + @requires_version(2, 22) + def load_liquid_by_well( + self, volumes: Mapping[Union[str, Well], float], liquid: Liquid + ) -> None: + """Mark several wells as containing unique volumes of liquid. + + This method should be called at the beginning of a protocol, soon after loading the labware and before + liquid handling operations begin. It is a base of information for liquid tracking functionality. If a well in a labware + has not been named in a call to :py:meth:`~Labware.load_empty`, :py:meth:`~Labware.load_liquid`, or + :py:meth:`~Labware.load_liquid_by_well`, the volume it contains is unknown and the well's liquid will not be tracked. + + For example, to load a decreasing amount of of a liquid named ``water`` (defined with :py:meth:`~ProtocolContext.define_liquid`) + into each successive well of a row, you could call + ``labware.load_liquid_by_well({'A1': 1000, 'A2': 950, 'A3': 900, ..., 'A12': 600}, water)`` + + If you want to load the same volume of a liquid into multiple wells, it is often easier to use :py:meth:`~Labware.load_liquid`. + + If you want to mark the well as containing no liquid, use :py:meth:`~Labware.load_empty`. + + :param volumes: A dictionary of well names (or :py:class:`Well` objects, for instance from ``labware['A1']``) + :type wells: Dict[Union[str, Well], float] + + :param liquid: The liquid to load into each well, previously defined by :py:meth:`~ProtocolContext.define_liquid` + :type liquid: Liquid + """ + verified_volumes: Dict[str, float] = {} + for well, volume in volumes.items(): + if isinstance(well, str): + if well not in self.wells_by_name(): + raise KeyError( + f"{well} is not a well in {self.name}. The keys of volumes should name wells in this labware" + ) + verified_volumes[well] = volume + elif isinstance(well, Well): + if well.parent is not self: + raise KeyError( + f"{well.well_name} is not a well in {self.name}. The keys of volumes should be wells of this labware" + ) + verified_volumes[well.well_name] = volume + else: + raise TypeError( + f"Unexpected type for well name {repr(well)}. The keys of volumes should be Well instances or well names." + ) + if not isinstance(volume, (float, int)): + raise TypeError( + f"Unexpected type for volume {repr(volume)}. The values of volumes should be numbers in microliters." + ) + self._core.load_liquid(verified_volumes, liquid) + + @requires_version(2, 22) + def load_empty(self, wells: Sequence[Union[Well, str]]) -> None: + """Mark several wells as empty. + + This method should be called at the beginning of a protocol, soon after loading the labware and before liquid handling + operations begin. It is a base of information for liquid tracking functionality. If a well in a labware has not been named + in a call to :py:meth:`Labware.load_empty`, :py:meth:`Labware.load_liquid`, or :py:meth:`Labware.load_liquid_by_well`, the + volume it contains is unknown and the well's liquid will not be tracked. + + For instance, to mark all wells in the labware as empty, you can call ``labware.load_empty(labware.wells())``. + + :param wells: The list of wells to mark empty. To mark all wells as empty, pass ``labware.wells()``. You can also specify + wells by their names (for instance, ``labware.load_empty(['A1', 'A2'])``). + :type wells: Union[List[Well], List[str]] + """ + well_names: List[str] = [] + for well in wells: + if isinstance(well, str): + if well not in self.wells_by_name(): + raise KeyError( + f"{well} is not a well in {self.name}. The elements of wells should name wells in this labware." + ) + well_names.append(well) + elif isinstance(well, Well): + if well.parent is not self: + raise KeyError( + f"{well.well_name} is not a well in {self.name}. The elements of wells should be wells of this labware." + ) + well_names.append(well.well_name) + else: + raise TypeError( + f"Unexpected type for well name {repr(well)}. The elements of wells should be Well instances or well names." + ) + self._core.load_empty(well_names) + # TODO(mc, 2022-11-09): implementation detail, move to core def split_tipracks(tip_racks: List[Labware]) -> Tuple[Labware, List[Labware]]: @@ -1121,7 +1360,7 @@ def select_tiprack_from_list( num_channels: int, starting_point: Optional[Well] = None, *, - nozzle_map: Optional[NozzleMap] = None, + nozzle_map: Optional[NozzleMapInterface] = None, ) -> Tuple[Labware, Well]: try: first, rest = split_tipracks(tip_racks) @@ -1159,7 +1398,7 @@ def next_available_tip( tip_racks: List[Labware], channels: int, *, - nozzle_map: Optional[NozzleMap] = None, + nozzle_map: Optional[NozzleMapInterface] = None, ) -> Tuple[Labware, Well]: start = starting_tip if start is None: diff --git a/api/src/opentrons/protocol_api/module_contexts.py b/api/src/opentrons/protocol_api/module_contexts.py index 8890981e32a..614bb4f53c7 100644 --- a/api/src/opentrons/protocol_api/module_contexts.py +++ b/api/src/opentrons/protocol_api/module_contexts.py @@ -125,6 +125,7 @@ def load_labware( namespace: Optional[str] = None, version: Optional[int] = None, adapter: Optional[str] = None, + lid: Optional[str] = None, ) -> Labware: """Load a labware onto the module using its load parameters. @@ -180,6 +181,19 @@ def load_labware( version=version, location=load_location, ) + if lid is not None: + if self._api_version < validation.LID_STACK_VERSION_GATE: + raise APIVersionError( + api_element="Loading a lid on a Labware", + until_version="2.23", + current_version=f"{self._api_version}", + ) + self._protocol_core.load_lid( + load_name=lid, + location=labware_core, + namespace=namespace, + version=version, + ) if isinstance(self._core, LegacyModuleCore): labware = self._core.add_labware_core(cast(LegacyLabwareCore, labware_core)) @@ -608,7 +622,7 @@ def set_lid_temperature(self, temperature: float) -> None: .. note:: The Thermocycler will proceed to the next command immediately after - ``temperature`` has been reached. + ``temperature`` is reached. """ self._core.set_target_lid_temperature(celsius=temperature) @@ -625,28 +639,18 @@ def execute_profile( """Execute a Thermocycler profile, defined as a cycle of ``steps``, for a given number of ``repetitions``. - :param steps: List of unique steps that make up a single cycle. - Each list item should be a dictionary that maps to - the parameters of the :py:meth:`set_block_temperature` - method with a ``temperature`` key, and either or both of + :param steps: List of steps that make up a single cycle. + Each list item should be a dictionary that maps to the parameters + of the :py:meth:`set_block_temperature` method. The dictionary's + keys must be ``temperature`` and one or both of ``hold_time_seconds`` and ``hold_time_minutes``. :param repetitions: The number of times to repeat the cycled steps. :param block_max_volume: The greatest volume of liquid contained in any individual well of the loaded labware, in µL. If not specified, the default is 25 µL. - .. note:: - - Unlike with :py:meth:`set_block_temperature`, either or both of - ``hold_time_minutes`` and ``hold_time_seconds`` must be defined - and for each step. - - .. note:: - - Before API Version 2.21, Thermocycler profiles run with this command - would be listed in the app as having a number of repetitions equal to - their step count. At or above API Version 2.21, the structure of the - Thermocycler cycles is preserved. + .. versionchanged:: 2.21 + Fixed run log listing number of steps instead of number of repetitions. """ repetitions = validation.ensure_thermocycler_repetition_count(repetitions) diff --git a/api/src/opentrons/protocol_api/protocol_context.py b/api/src/opentrons/protocol_api/protocol_context.py index 840edba5081..b9f96e4d536 100644 --- a/api/src/opentrons/protocol_api/protocol_context.py +++ b/api/src/opentrons/protocol_api/protocol_context.py @@ -186,7 +186,14 @@ def __init__( self._commands: List[str] = [] self._params: Parameters = Parameters() self._unsubscribe_commands: Optional[Callable[[], None]] = None - self._robot = RobotContext(self._core) + try: + self._robot: Optional[RobotContext] = RobotContext( + core=self._core.load_robot(), + protocol_core=self._core, + api_version=self._api_version, + ) + except APIVersionError: + self._robot = None self.clear_commands() @property @@ -212,12 +219,14 @@ def api_version(self) -> APIVersion: return self._api_version @property - @requires_version(2, 21) + @requires_version(2, 22) def robot(self) -> RobotContext: """The :py:class:`.RobotContext` for the protocol. :meta private: """ + if self._core.robot_type != "OT-3 Standard" or not self._robot: + raise RobotTypeError("The RobotContext is only available on Flex robots.") return self._robot @property @@ -229,7 +238,9 @@ def _hw_manager(self) -> HardwareManager: "This function will be deprecated in later versions." "Please use with caution." ) - return self._robot.hardware + if self._robot: + return self._robot.hardware + return HardwareManager(hardware=self._core.get_hardware()) @property @requires_version(2, 0) @@ -388,6 +399,7 @@ def load_labware( namespace: Optional[str] = None, version: Optional[int] = None, adapter: Optional[str] = None, + lid: Optional[str] = None, ) -> Labware: """Load a labware onto a location. @@ -432,6 +444,10 @@ def load_labware( values as the ``load_name`` parameter of :py:meth:`.load_adapter`. The adapter will use the same namespace as the labware, and the API will choose the adapter's version automatically. + :param lid: A lid to load the on top of the main labware. Accepts the same + values as the ``load_name`` parameter of :py:meth:`.load_lid_stack`. The + lid will use the same namespace as the labware, and the API will + choose the lid's version automatically. .. versionadded:: 2.15 """ @@ -472,6 +488,20 @@ def load_labware( version=version, ) + if lid is not None: + if self._api_version < validation.LID_STACK_VERSION_GATE: + raise APIVersionError( + api_element="Loading a Lid on a Labware", + until_version="2.23", + current_version=f"{self._api_version}", + ) + self._core.load_lid( + load_name=lid, + location=labware_core, + namespace=namespace, + version=version, + ) + labware = Labware( core=labware_core, api_version=self._api_version, @@ -956,7 +986,10 @@ def load_instrument( mount, checked_instrument_name ) - is_96_channel = checked_instrument_name == PipetteNameType.P1000_96 + is_96_channel = checked_instrument_name in [ + PipetteNameType.P1000_96, + PipetteNameType.P200_96, + ] tip_racks = tip_racks or [] @@ -1320,6 +1353,94 @@ def door_closed(self) -> bool: """Returns ``True`` if the front door of the robot is closed.""" return self._core.door_closed() + @requires_version(2, 23) + def load_lid_stack( + self, + load_name: str, + location: Union[DeckLocation, Labware], + quantity: int, + adapter: Optional[str] = None, + namespace: Optional[str] = None, + version: Optional[int] = None, + ) -> Labware: + """ + Load a stack of Lids onto a valid Deck Location or Adapter. + + :param str load_name: A string to use for looking up a lid definition. + You can find the ``load_name`` for any standard lid on the Opentrons + `Labware Library `_. + :param location: Either a :ref:`deck slot `, + like ``1``, ``"1"``, or ``"D1"``, or the a valid Opentrons Adapter. + :param int quantity: The quantity of lids to be loaded in the stack. + :param adapter: An adapter to load the lid stack on top of. Accepts the same + values as the ``load_name`` parameter of :py:meth:`.load_adapter`. The + adapter will use the same namespace as the lid labware, and the API will + choose the adapter's version automatically. + :param str namespace: The namespace that the lid labware definition belongs to. + If unspecified, the API will automatically search two namespaces: + + - ``"opentrons"``, to load standard Opentrons labware definitions. + - ``"custom_beta"``, to load custom labware definitions created with the + `Custom Labware Creator `__. + + You might need to specify an explicit ``namespace`` if you have a custom + definition whose ``load_name`` is the same as an Opentrons-verified + definition, and you want to explicitly choose one or the other. + + :param version: The version of the labware definition. You should normally + leave this unspecified to let ``load_lid_stack()`` choose a version + automatically. + + :return: The initialized and loaded labware object representing the Lid Stack. + """ + if self._api_version < validation.LID_STACK_VERSION_GATE: + raise APIVersionError( + api_element="Loading a Lid Stack", + until_version="2.23", + current_version=f"{self._api_version}", + ) + + load_location: Union[DeckSlotName, StagingSlotName, LabwareCore] + if isinstance(location, Labware): + load_location = location._core + else: + load_location = validation.ensure_and_convert_deck_slot( + location, self._api_version, self._core.robot_type + ) + + if adapter is not None: + if isinstance(load_location, DeckSlotName) or isinstance( + load_location, StagingSlotName + ): + loaded_adapter = self.load_adapter( + load_name=adapter, + location=load_location.value, + namespace=namespace, + ) + load_location = loaded_adapter._core + else: + raise ValueError( + "Location cannot be a Labware or Adapter when the 'adapter' field is not None." + ) + + load_name = validation.ensure_lowercase_name(load_name) + + result = self._core.load_lid_stack( + load_name=load_name, + location=load_location, + quantity=quantity, + namespace=namespace, + version=version, + ) + + labware = Labware( + core=result, + api_version=self._api_version, + protocol_core=self._core, + core_map=self._core_map, + ) + return labware + def _create_module_context( module_core: Union[ModuleCore, NonConnectedModuleCore], diff --git a/api/src/opentrons/protocol_api/robot_context.py b/api/src/opentrons/protocol_api/robot_context.py index 01a443cd743..df14b8bb7c5 100644 --- a/api/src/opentrons/protocol_api/robot_context.py +++ b/api/src/opentrons/protocol_api/robot_context.py @@ -1,11 +1,25 @@ -from typing import NamedTuple, Union, Dict, Optional +from typing import NamedTuple, Union, Optional -from opentrons.types import Mount, DeckLocation, Point +from opentrons.types import ( + Mount, + DeckLocation, + Location, + Point, + AxisMapType, + AxisType, + StringAxisMap, +) from opentrons.legacy_commands import publisher -from opentrons.hardware_control import SyncHardwareAPI, types as hw_types +from opentrons.hardware_control import SyncHardwareAPI +from opentrons.protocols.api_support.util import requires_version +from opentrons.protocols.api_support.types import APIVersion +from opentrons_shared_data.pipette.types import PipetteNameType -from ._types import OffDeckType -from .core.common import ProtocolCore +from . import validation +from .core.common import ProtocolCore, RobotCore +from .module_contexts import ModuleContext +from .labware import Labware +from ._types import PipetteActionTypes, PlungerPositionTypes class HardwareManager(NamedTuple): @@ -34,56 +48,214 @@ class RobotContext(publisher.CommandPublisher): """ - def __init__(self, core: ProtocolCore) -> None: - self._hardware = HardwareManager(hardware=core.get_hardware()) + def __init__( + self, core: RobotCore, protocol_core: ProtocolCore, api_version: APIVersion + ) -> None: + self._hardware = HardwareManager(hardware=protocol_core.get_hardware()) + self._core = core + self._protocol_core = protocol_core + self._api_version = api_version + + @property + @requires_version(2, 22) + def api_version(self) -> APIVersion: + return self._api_version @property def hardware(self) -> HardwareManager: + # TODO this hardware attribute should be deprecated + # in version 3.0+ as we will only support exposed robot + # context commands. return self._hardware + @requires_version(2, 22) def move_to( self, mount: Union[Mount, str], - destination: Point, - velocity: float, + destination: Location, + speed: Optional[float] = None, ) -> None: - raise NotImplementedError() + """ + Move a specified mount to a destination location on the deck. + + :param mount: The mount of the instrument you wish to move. + This can either be an instance of :py:class:`.types.Mount` or one + of the strings ``"left"``, ``"right"``, ``"extension"``, ``"gripper"``. Note + that the gripper mount can be referred to either as ``"extension"`` or ``"gripper"``. + :type mount: types.Mount or str + :param Location destination: + :param speed: + """ + mount = validation.ensure_instrument_mount(mount) + self._core.move_to(mount, destination.point, speed) + @requires_version(2, 22) def move_axes_to( self, - abs_axis_map: Dict[hw_types.Axis, hw_types.AxisMapValue], - velocity: float, - critical_point: Optional[hw_types.CriticalPoint], + axis_map: Union[AxisMapType, StringAxisMap], + critical_point: Optional[Union[AxisMapType, StringAxisMap]] = None, + speed: Optional[float] = None, ) -> None: - raise NotImplementedError() + """ + Move a set of axes to an absolute position on the deck. + :param axis_map: A dictionary mapping axes to an absolute position on the deck in mm. + :param critical_point: The critical point to move the axes with. It should only + specify the gantry axes (i.e. `x`, `y`, `z`). + :param float speed: The maximum speed with which you want to move all the axes + in the axis map. + """ + instrument_on_left = self._core.get_pipette_type_from_engine(Mount.LEFT) + is_96_channel = instrument_on_left == PipetteNameType.P1000_96 + axis_map = validation.ensure_axis_map_type( + axis_map, self._protocol_core.robot_type, is_96_channel + ) + if critical_point: + critical_point = validation.ensure_axis_map_type( + critical_point, self._protocol_core.robot_type, is_96_channel + ) + validation.ensure_only_gantry_axis_map_type( + critical_point, self._protocol_core.robot_type + ) + else: + critical_point = None + self._core.move_axes_to(axis_map, critical_point, speed) + + @requires_version(2, 22) def move_axes_relative( - self, rel_axis_map: Dict[hw_types.Axis, hw_types.AxisMapValue], velocity: float + self, + axis_map: Union[AxisMapType, StringAxisMap], + speed: Optional[float] = None, ) -> None: - raise NotImplementedError() + """ + Move a set of axes to a relative position on the deck. + + :param axis_map: A dictionary mapping axes to relative movements in mm. + :type mount: types.Mount or str - def close_gripper_jaw(self, force: float) -> None: - raise NotImplementedError() + :param float speed: The maximum speed with which you want to move all the axes + in the axis map. + """ + instrument_on_left = self._core.get_pipette_type_from_engine(Mount.LEFT) + is_96_channel = instrument_on_left == PipetteNameType.P1000_96 + + axis_map = validation.ensure_axis_map_type( + axis_map, self._protocol_core.robot_type, is_96_channel + ) + self._core.move_axes_relative(axis_map, speed) + + def close_gripper_jaw(self, force: Optional[float] = None) -> None: + """Command the gripper closed with some force.""" + self._core.close_gripper(force) def open_gripper_jaw(self) -> None: - raise NotImplementedError() + """Command the gripper open.""" + self._core.release_grip() def axis_coordinates_for( - self, mount: Union[Mount, str], location: Union[DeckLocation, OffDeckType] - ) -> None: - raise NotImplementedError() + self, + mount: Union[Mount, str], + location: Union[Location, ModuleContext, DeckLocation], + ) -> AxisMapType: + """ + Build a :py:class:`.types.AxisMapType` from a location to be compatible with + either :py:meth:`.RobotContext.move_axes_to` or :py:meth:`.RobotContext.move_axes_relative`. + You must provide only one of `location`, `slot`, or `module` to build + the axis map. + + :param mount: The mount of the instrument you wish create an axis map for. + This can either be an instance of :py:class:`.types.Mount` or one + of the strings ``"left"``, ``"right"``, ``"extension"``, ``"gripper"``. Note + that the gripper mount can be referred to either as ``"extension"`` or ``"gripper"``. + :type mount: types.Mount or str + :param location: The location to format an axis map for. + :type location: `Well`, `ModuleContext`, `DeckLocation` or `OffDeckType` + """ + mount = validation.ensure_instrument_mount(mount) + + mount_axis = AxisType.axis_for_mount(mount) + if location: + loc: Union[Point, Labware, None] + if isinstance(location, ModuleContext): + loc = location.labware + if not loc: + raise ValueError(f"There must be a labware on {location}") + top_of_labware = loc.wells()[0].top() + loc = top_of_labware.point + return {mount_axis: loc.z, AxisType.X: loc.x, AxisType.Y: loc.y} + elif location is DeckLocation and not isinstance(location, Location): + slot_name = validation.ensure_and_convert_deck_slot( + location, + api_version=self._api_version, + robot_type=self._protocol_core.robot_type, + ) + loc = self._protocol_core.get_slot_center(slot_name) + return {mount_axis: loc.z, AxisType.X: loc.x, AxisType.Y: loc.y} + elif isinstance(location, Location): + assert isinstance(location, Location) + loc = location.point + return {mount_axis: loc.z, AxisType.X: loc.x, AxisType.Y: loc.y} + else: + raise ValueError( + "Location parameter must be a Module, Deck Location, or Location type." + ) + else: + raise TypeError("You must specify a location to move to.") def plunger_coordinates_for_volume( - self, mount: Union[Mount, str], volume: float - ) -> None: - raise NotImplementedError() + self, mount: Union[Mount, str], volume: float, action: PipetteActionTypes + ) -> AxisMapType: + """ + Build a :py:class:`.types.AxisMapType` for a pipette plunger motor from volume. + + """ + pipette_name = self._core.get_pipette_type_from_engine(mount) + if not pipette_name: + raise ValueError( + f"Expected a pipette to be attached to provided mount {mount}" + ) + mount = validation.ensure_mount_for_pipette(mount, pipette_name) + pipette_axis = AxisType.plunger_axis_for_mount(mount) + + pipette_position = self._core.get_plunger_position_from_volume( + mount, volume, action, self._protocol_core.robot_type + ) + return {pipette_axis: pipette_position} def plunger_coordinates_for_named_position( - self, mount: Union[Mount, str], position_name: str - ) -> None: - raise NotImplementedError() + self, mount: Union[Mount, str], position_name: PlungerPositionTypes + ) -> AxisMapType: + """ + Build a :py:class:`.types.AxisMapType` for a pipette plunger motor from position_name. - def build_axis_map( - self, axis_map: Dict[hw_types.Axis, hw_types.AxisMapValue] - ) -> None: - raise NotImplementedError() + """ + pipette_name = self._core.get_pipette_type_from_engine(mount) + if not pipette_name: + raise ValueError( + f"Expected a pipette to be attached to provided mount {mount}" + ) + mount = validation.ensure_mount_for_pipette(mount, pipette_name) + pipette_axis = AxisType.plunger_axis_for_mount(mount) + pipette_position = self._core.get_plunger_position_from_name( + mount, position_name + ) + return {pipette_axis: pipette_position} + + def build_axis_map(self, axis_map: StringAxisMap) -> AxisMapType: + """Take in a :py:class:`.types.StringAxisMap` and output a :py:class:`.types.AxisMapType`. + A :py:class:`.types.StringAxisMap` is allowed to contain any of the following strings: + ``"x"``, ``"y"``, "``z_l"``, "``z_r"``, "``z_g"``, ``"q"``. + + An example of a valid axis map could be: + + {"x": 1, "y": 2} or {"Z_L": 100} + + Note that capitalization does not matter. + + """ + instrument_on_left = self._core.get_pipette_type_from_engine(Mount.LEFT) + is_96_channel = instrument_on_left == PipetteNameType.P1000_96 + + return validation.ensure_axis_map_type( + axis_map, self._protocol_core.robot_type, is_96_channel + ) diff --git a/api/src/opentrons/protocol_api/validation.py b/api/src/opentrons/protocol_api/validation.py index 630211e9ac6..e734a98e818 100644 --- a/api/src/opentrons/protocol_api/validation.py +++ b/api/src/opentrons/protocol_api/validation.py @@ -11,7 +11,7 @@ NamedTuple, TYPE_CHECKING, ) - +from math import isinf, isnan from typing_extensions import TypeGuard from opentrons_shared_data.labware.labware_definition import LabwareRole @@ -21,7 +21,16 @@ from opentrons.protocols.api_support.types import APIVersion, ThermocyclerStep from opentrons.protocols.api_support.util import APIVersionError from opentrons.protocols.models import LabwareDefinition -from opentrons.types import Mount, DeckSlotName, StagingSlotName, Location +from opentrons.protocols.advanced_control.transfers.common import TransferTipPolicyV2 +from opentrons.types import ( + Mount, + DeckSlotName, + StagingSlotName, + Location, + AxisType, + AxisMapType, + StringAxisMap, +) from opentrons.hardware_control.modules.types import ( ModuleModel, MagneticModuleModel, @@ -44,6 +53,9 @@ # The first APIVersion where Python protocols can specify staging deck slots (e.g. "D4") _STAGING_DECK_SLOT_VERSION_GATE = APIVersion(2, 16) +# The first APIVersion where Python protocols can load lids as stacks and treat them as attributes of a parent labware. +LID_STACK_VERSION_GATE = APIVersion(2, 23) + # Mapping of public Python Protocol API pipette load names # to names used by the internal Opentrons system _PIPETTE_NAMES_MAP = { @@ -63,7 +75,9 @@ "flex_8channel_50": PipetteNameType.P50_MULTI_FLEX, "flex_1channel_1000": PipetteNameType.P1000_SINGLE_FLEX, "flex_8channel_1000": PipetteNameType.P1000_MULTI_FLEX, + "flex_8channel_1000_em": PipetteNameType.P1000_MULTI_EM, "flex_96channel_1000": PipetteNameType.P1000_96, + "flex_96channel_200": PipetteNameType.P200_96, } @@ -75,6 +89,14 @@ class PipetteMountTypeError(TypeError): """An error raised when an invalid mount type is used for loading pipettes.""" +class InstrumentMountTypeError(TypeError): + """An error raised when an invalid mount type is used for any available instruments.""" + + +class IncorrectAxisError(TypeError): + """An error raised when an invalid axis key is provided in an axis map.""" + + class LabwareDefinitionIsNotAdapterError(ValueError): """An error raised when an adapter is attempted to be loaded as a labware.""" @@ -95,7 +117,7 @@ def ensure_mount_for_pipette( mount: Union[str, Mount, None], pipette: PipetteNameType ) -> Mount: """Ensure that an input value represents a valid mount, and is valid for the given pipette.""" - if pipette == PipetteNameType.P1000_96: + if pipette in [PipetteNameType.P1000_96, PipetteNameType.P200_96]: # Always validate the raw mount input, even if the pipette is a 96-channel and we're not going # to use the mount value. if mount is not None: @@ -146,6 +168,25 @@ def _ensure_mount(mount: Union[str, Mount]) -> Mount: ) +def ensure_instrument_mount(mount: Union[str, Mount]) -> Mount: + """Ensure that an input value represents a valid Mount for all instruments.""" + if isinstance(mount, Mount): + return mount + + if isinstance(mount, str): + if mount == "gripper": + # TODO (lc 08-02-2024) We should decide on the user facing name for + # the gripper mount axis. + mount = "extension" + try: + return Mount[mount.upper()] + except KeyError as e: + raise InstrumentMountTypeError( + "If mount is specified as a string, it must be 'left', 'right', 'gripper', or 'extension';" + f" instead, {mount} was given." + ) from e + + def ensure_pipette_name(pipette_name: str) -> PipetteNameType: """Ensure that an input value represents a valid pipette name.""" pipette_name = ensure_lowercase_name(pipette_name) @@ -158,6 +199,79 @@ def ensure_pipette_name(pipette_name: str) -> PipetteNameType: ) from None +def _check_ot2_axis_type( + robot_type: RobotType, axis_map_keys: Union[List[str], List[AxisType]] +) -> None: + if robot_type == "OT-2 Standard" and isinstance(axis_map_keys[0], AxisType): + if any(k not in AxisType.ot2_axes() for k in axis_map_keys): + raise IncorrectAxisError( + f"An OT-2 Robot only accepts the following axes {AxisType.ot2_axes()}" + ) + if robot_type == "OT-2 Standard" and isinstance(axis_map_keys[0], str): + if any(k.upper() not in [axis.value for axis in AxisType.ot2_axes()] for k in axis_map_keys): # type: ignore [union-attr] + raise IncorrectAxisError( + f"An OT-2 Robot only accepts the following axes {AxisType.ot2_axes()}" + ) + + +def _check_96_channel_axis_type( + is_96_channel: bool, axis_map_keys: Union[List[str], List[AxisType]] +) -> None: + if is_96_channel and any( + key_variation in axis_map_keys for key_variation in ["Z_R", "z_r", AxisType.Z_R] + ): + raise IncorrectAxisError( + "A 96 channel is attached. You cannot move the `Z_R` mount." + ) + if not is_96_channel and any( + key_variation in axis_map_keys for key_variation in ["Q", "q", AxisType.Q] + ): + raise IncorrectAxisError( + "A 96 channel is not attached. The clamp `Q` motor does not exist." + ) + + +def ensure_axis_map_type( + axis_map: Union[AxisMapType, StringAxisMap], + robot_type: RobotType, + is_96_channel: bool = False, +) -> AxisMapType: + """Ensure that the axis map provided is in the correct shape and contains the correct keys.""" + axis_map_keys: Union[List[str], List[AxisType]] = list(axis_map.keys()) # type: ignore + key_type = set(type(k) for k in axis_map_keys) + + if len(key_type) > 1: + raise IncorrectAxisError( + "Please provide an `axis_map` with only string or only AxisType keys." + ) + _check_ot2_axis_type(robot_type, axis_map_keys) + _check_96_channel_axis_type(is_96_channel, axis_map_keys) + + if all(isinstance(k, AxisType) for k in axis_map_keys): + return_map: AxisMapType = axis_map # type: ignore + return return_map + try: + return {AxisType[k.upper()]: v for k, v in axis_map.items()} # type: ignore [union-attr] + except KeyError as e: + raise IncorrectAxisError(f"{e} is not a supported `AxisMapType`") + + +def ensure_only_gantry_axis_map_type( + axis_map: AxisMapType, robot_type: RobotType +) -> None: + """Ensure that the axis map provided is in the correct shape and matches the gantry axes for the robot.""" + if robot_type == "OT-2 Standard": + if any(k not in AxisType.ot2_gantry_axes() for k in axis_map.keys()): + raise IncorrectAxisError( + f"A critical point only accepts OT-2 gantry axes which are {AxisType.ot2_gantry_axes()}" + ) + else: + if any(k not in AxisType.flex_gantry_axes() for k in axis_map.keys()): + raise IncorrectAxisError( + f"A critical point only accepts Flex gantry axes which are {AxisType.flex_gantry_axes()}" + ) + + # TODO(jbl 11-17-2023) this function's original purpose was ensure a valid deck slot for a given robot type # With deck configuration, the shape of this should change to better represent it checking if a deck slot # (and maybe any addressable area) being valid for that deck configuration @@ -253,6 +367,27 @@ def ensure_definition_is_labware(definition: LabwareDefinition) -> None: ) +def ensure_definition_is_lid(definition: LabwareDefinition) -> None: + """Ensure that one of the definition's allowed roles is `lid` or that that field is empty.""" + if LabwareRole.lid not in definition.allowedRoles: + raise LabwareDefinitionIsNotLabwareError( + f"Labware {definition.parameters.loadName} is not a lid." + ) + + +def ensure_definition_is_not_lid_after_api_version( + api_version: APIVersion, definition: LabwareDefinition +) -> None: + """Ensure that one of the definition's allowed roles is not `lid` or that the API Version is below the release where lid loading was seperated.""" + if ( + LabwareRole.lid in definition.allowedRoles + and api_version >= LID_STACK_VERSION_GATE + ): + raise APIVersionError( + f"Labware Lids cannot be loaded like standard labware in Protocols written with an API version greater than {LID_STACK_VERSION_GATE}." + ) + + _MODULE_ALIASES: Dict[str, ModuleModel] = { "magdeck": MagneticModuleModel.MAGNETIC_V1, "magnetic module": MagneticModuleModel.MAGNETIC_V1, @@ -483,3 +618,127 @@ def validate_location( if well is not None else PointTarget(location=target_location, in_place=in_place) ) + + +def ensure_boolean(value: bool) -> bool: + """Ensure value is a boolean.""" + if not isinstance(value, bool): + raise ValueError("Value must be a boolean.") + return value + + +def ensure_float(value: Union[int, float]) -> float: + """Ensure value is a float (or an integer) and return it as a float.""" + if not isinstance(value, (int, float)): + raise ValueError("Value must be a floating point number.") + return float(value) + + +def ensure_positive_float(value: Union[int, float]) -> float: + """Ensure value is a positive and real float value.""" + float_value = ensure_float(value) + if isnan(float_value) or isinf(float_value): + raise ValueError("Value must be a defined, non-infinite number.") + if float_value < 0: + raise ValueError("Value must be a positive float.") + return float_value + + +def ensure_positive_int(value: int) -> int: + """Ensure value is a positive integer.""" + if not isinstance(value, int): + raise ValueError("Value must be an integer.") + if value < 0: + raise ValueError("Value must be a positive integer.") + return value + + +def validate_coordinates(value: Sequence[float]) -> Tuple[float, float, float]: + """Ensure value is a valid sequence of 3 floats and return a tuple of 3 floats.""" + if len(value) != 3: + raise ValueError("Coordinates must be a sequence of exactly three numbers") + if not all(isinstance(v, (float, int)) for v in value): + raise ValueError("All values in coordinates must be floats.") + return float(value[0]), float(value[1]), float(value[2]) + + +def ensure_new_tip_policy(value: str) -> TransferTipPolicyV2: + """Ensure that new_tip value is a valid TransferTipPolicy value.""" + try: + return TransferTipPolicyV2(value.lower()) + except ValueError: + raise ValueError( + f"'{value}' is invalid value for 'new_tip'." + f" Acceptable value is either 'never', 'once', 'always' or 'per source'." + ) + + +def _verify_each_list_element_is_valid_location(locations: Sequence[Well]) -> None: + from .labware import Well + + for loc in locations: + if not isinstance(loc, Well): + raise ValueError( + f"'{loc}' is not a valid location for transfer." + f" Location should be a well instance." + ) + + +def ensure_valid_flat_wells_list_for_transfer_v2( + target: Union[Well, Sequence[Well], Sequence[Sequence[Well]]], +) -> List[Well]: + """Ensure that the given target(s) for a liquid transfer are valid and in a flat list.""" + from .labware import Well + + if isinstance(target, Well): + return [target] + + if isinstance(target, (list, tuple)): + if len(target) == 0: + raise ValueError("No target well(s) specified for transfer.") + if isinstance(target[0], (list, tuple)): + for sub_sequence in target: + _verify_each_list_element_is_valid_location(sub_sequence) + return [loc for sub_sequence in target for loc in sub_sequence] + else: + _verify_each_list_element_is_valid_location(target) + return list(target) + else: + raise ValueError( + f"'{target}' is not a valid location for transfer." + f" Location should be a well instance, or a 1-dimensional or" + f" 2-dimensional sequence of well instances." + ) + + +def ensure_valid_tip_drop_location_for_transfer_v2( + tip_drop_location: Union[Location, Well, TrashBin, WasteChute] +) -> Union[Location, Well, TrashBin, WasteChute]: + """Ensure that the tip drop location is valid for v2 transfer.""" + from .labware import Well + + if ( + isinstance(tip_drop_location, Well) + or isinstance(tip_drop_location, TrashBin) + or isinstance(tip_drop_location, WasteChute) + ): + return tip_drop_location + elif isinstance(tip_drop_location, Location): + _, maybe_well = tip_drop_location.labware.get_parent_labware_and_well() + + if maybe_well is None: + raise TypeError( + "If a location is specified as a `types.Location`" + " (for instance, as the result of a call to `Well.top()`)," + " it must be a location relative to a well," + " since that is where a tip is dropped." + " However, the given location doesn't refer to any well." + ) + return tip_drop_location + else: + raise TypeError( + f"If specified, location should be an instance of" + f" `types.Location` (e.g. the return value from `Well.top()`)" + f" or `Well` (e.g. `reservoir.wells()[0]`) or an instance of `TrashBin` or `WasteChute`." + f" However, it is '{tip_drop_location}'." + ) diff --git a/api/src/opentrons/protocol_engine/__init__.py b/api/src/opentrons/protocol_engine/__init__.py index 25599189916..7efaef7199d 100644 --- a/api/src/opentrons/protocol_engine/__init__.py +++ b/api/src/opentrons/protocol_engine/__init__.py @@ -57,6 +57,8 @@ ModuleModel, ModuleDefinition, Liquid, + LiquidClassRecord, + LiquidClassRecordWithId, AllNozzleLayoutConfiguration, SingleNozzleLayoutConfiguration, RowNozzleLayoutConfiguration, @@ -122,6 +124,8 @@ "ModuleModel", "ModuleDefinition", "Liquid", + "LiquidClassRecord", + "LiquidClassRecordWithId", "AllNozzleLayoutConfiguration", "SingleNozzleLayoutConfiguration", "RowNozzleLayoutConfiguration", diff --git a/api/src/opentrons/protocol_engine/actions/actions.py b/api/src/opentrons/protocol_engine/actions/actions.py index 15b04048699..a9dcc3e7dc3 100644 --- a/api/src/opentrons/protocol_engine/actions/actions.py +++ b/api/src/opentrons/protocol_engine/actions/actions.py @@ -27,7 +27,6 @@ ModuleDefinition, Liquid, DeckConfigurationType, - AddressableAreaLocation, ) @@ -235,12 +234,12 @@ class SetDeckConfigurationAction: class AddAddressableAreaAction: """Add a single addressable area to state. - This differs from the deck configuration in ProvideDeckConfigurationAction which + This differs from the deck configuration in SetDeckConfigurationAction which sends over a mapping of cutout fixtures. This action will only load one addressable area and that should be pre-validated before being sent via the action. """ - addressable_area: AddressableAreaLocation + addressable_area_name: str @dataclasses.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 3460c13d463..4d04353b271 100644 --- a/api/src/opentrons/protocol_engine/clients/sync_client.py +++ b/api/src/opentrons/protocol_engine/clients/sync_client.py @@ -77,6 +77,18 @@ def execute_command_without_recovery( ) -> commands.LoadPipetteResult: pass + @overload + def execute_command_without_recovery( + self, params: commands.LoadLidStackParams + ) -> commands.LoadLidStackResult: + pass + + @overload + def execute_command_without_recovery( + self, params: commands.LoadLidParams + ) -> commands.LoadLidResult: + pass + @overload def execute_command_without_recovery( self, params: commands.LiquidProbeParams @@ -89,6 +101,12 @@ def execute_command_without_recovery( ) -> commands.TryLiquidProbeResult: pass + @overload + def execute_command_without_recovery( + self, params: commands.LoadLiquidClassParams + ) -> commands.LoadLiquidClassResult: + pass + def execute_command_without_recovery( self, params: commands.CommandParams ) -> commands.CommandResult: diff --git a/api/src/opentrons/protocol_engine/commands/__init__.py b/api/src/opentrons/protocol_engine/commands/__init__.py index 649bb4b6507..6f6c08c35e3 100644 --- a/api/src/opentrons/protocol_engine/commands/__init__.py +++ b/api/src/opentrons/protocol_engine/commands/__init__.py @@ -20,6 +20,7 @@ from . import thermocycler from . import calibration from . import unsafe +from . import robot from .hash_command_params import hash_protocol_command_params from .generate_command_schema import generate_command_schema @@ -34,13 +35,23 @@ from .command_unions import ( Command, + CommandAdapter, CommandParams, CommandCreate, + CommandCreateAdapter, CommandResult, CommandType, CommandDefinedErrorData, ) +from .air_gap_in_place import ( + AirGapInPlace, + AirGapInPlaceParams, + AirGapInPlaceCreate, + AirGapInPlaceResult, + AirGapInPlaceCommandType, +) + from .aspirate import ( Aspirate, AspirateParams, @@ -138,6 +149,15 @@ LoadLiquidImplementation, ) +from .load_liquid_class import ( + LoadLiquidClass, + LoadLiquidClassParams, + LoadLiquidClassCreate, + LoadLiquidClassResult, + LoadLiquidClassCommandType, + LoadLiquidClassImplementation, +) + from .load_module import ( LoadModule, LoadModuleParams, @@ -154,6 +174,22 @@ LoadPipetteCommandType, ) +from .load_lid_stack import ( + LoadLidStack, + LoadLidStackParams, + LoadLidStackCreate, + LoadLidStackResult, + LoadLidStackCommandType, +) + +from .load_lid import ( + LoadLid, + LoadLidParams, + LoadLidCreate, + LoadLidResult, + LoadLidCommandType, +) + from .move_labware import ( MoveLabware, MoveLabwareParams, @@ -323,6 +359,14 @@ VerifyTipPresenceCommandType, ) +from .get_next_tip import ( + GetNextTip, + GetNextTipCreate, + GetNextTipParams, + GetNextTipResult, + GetNextTipCommandType, +) + from .liquid_probe import ( LiquidProbe, LiquidProbeParams, @@ -336,11 +380,35 @@ TryLiquidProbeCommandType, ) +from .evotip_seal_pipette import ( + EvotipSealPipette, + EvotipSealPipetteParams, + EvotipSealPipetteCreate, + EvotipSealPipetteResult, + EvotipSealPipetteCommandType, +) +from .evotip_unseal_pipette import ( + EvotipUnsealPipette, + EvotipUnsealPipetteParams, + EvotipUnsealPipetteCreate, + EvotipUnsealPipetteResult, + EvotipUnsealPipetteCommandType, +) +from .evotip_dispense import ( + EvotipDispense, + EvotipDispenseParams, + EvotipDispenseCreate, + EvotipDispenseResult, + EvotipDispenseCommandType, +) + __all__ = [ # command type unions "Command", + "CommandAdapter", "CommandParams", "CommandCreate", + "CommandCreateAdapter", "CommandResult", "CommandType", "CommandPrivateResult", @@ -355,6 +423,12 @@ "hash_protocol_command_params", # command schema generation "generate_command_schema", + # air gap command models + "AirGapInPlace", + "AirGapInPlaceCreate", + "AirGapInPlaceParams", + "AirGapInPlaceResult", + "AirGapInPlaceCommandType", # aspirate command models "Aspirate", "AspirateCreate", @@ -440,6 +514,20 @@ "LoadPipetteResult", "LoadPipetteCommandType", "LoadPipettePrivateResult", + # load lid stack command models + "LoadLidStack", + "LoadLidStackCreate", + "LoadLidStackParams", + "LoadLidStackResult", + "LoadLidStackCommandType", + "LoadLidStackPrivateResult", + # load lid command models + "LoadLid", + "LoadLidCreate", + "LoadLidParams", + "LoadLidResult", + "LoadLidCommandType", + "LoadLidPrivateResult", # move labware command models "MoveLabware", "MoveLabwareCreate", @@ -538,6 +626,14 @@ "LoadLiquidParams", "LoadLiquidResult", "LoadLiquidCommandType", + # load liquid class command models + "LoadLiquidClass", + "LoadLiquidClassParams", + "LoadLiquidClassCreate", + "LoadLiquidClassResult", + "LoadLiquidClassImplementation", + "LoadLiquidClassCommandType", + # hardware control command models # hardware module command bundles "absorbance_reader", "heater_shaker", @@ -548,6 +644,7 @@ "calibration", # unsafe command bundle "unsafe", + "robot", # configure pipette volume command bundle "ConfigureForVolume", "ConfigureForVolumeCreate", @@ -578,6 +675,12 @@ "VerifyTipPresenceParams", "VerifyTipPresenceResult", "VerifyTipPresenceCommandType", + # get next tip command bundle + "GetNextTip", + "GetNextTipCreate", + "GetNextTipParams", + "GetNextTipResult", + "GetNextTipCommandType", # liquid probe command bundle "LiquidProbe", "LiquidProbeParams", @@ -589,4 +692,22 @@ "TryLiquidProbeCreate", "TryLiquidProbeResult", "TryLiquidProbeCommandType", + # evotip seal command bundle + "EvotipSealPipette", + "EvotipSealPipetteParams", + "EvotipSealPipetteCreate", + "EvotipSealPipetteResult", + "EvotipSealPipetteCommandType", + # evotip unseal command bundle + "EvotipUnsealPipette", + "EvotipUnsealPipetteParams", + "EvotipUnsealPipetteCreate", + "EvotipUnsealPipetteResult", + "EvotipUnsealPipetteCommandType", + # evotip dispense command bundle + "EvotipDispense", + "EvotipDispenseParams", + "EvotipDispenseCreate", + "EvotipDispenseResult", + "EvotipDispenseCommandType", ] diff --git a/api/src/opentrons/protocol_engine/commands/absorbance_reader/close_lid.py b/api/src/opentrons/protocol_engine/commands/absorbance_reader/close_lid.py index 069c2803b22..fd6e279d659 100644 --- a/api/src/opentrons/protocol_engine/commands/absorbance_reader/close_lid.py +++ b/api/src/opentrons/protocol_engine/commands/absorbance_reader/close_lid.py @@ -60,7 +60,6 @@ async def execute(self, params: CloseLidParams) -> SuccessData[CloseLidResult]: hardware_lid_status = AbsorbanceReaderLidStatus.OFF if not self._state_view.config.use_virtual_modules: abs_reader = self._equipment.get_module_hardware_api(mod_substate.module_id) - if abs_reader is not None: hardware_lid_status = await abs_reader.get_current_lid_status() else: @@ -95,7 +94,6 @@ async def execute(self, params: CloseLidParams) -> SuccessData[CloseLidResult]: deck_slot=self._state_view.modules.get_location( params.moduleId ).slotName, - deck_type=self._state_view.config.deck_type, model=absorbance_model, ) ) @@ -134,7 +132,7 @@ class CloseLid(BaseCommand[CloseLidParams, CloseLidResult, ErrorOccurrence]): commandType: CloseLidCommandType = "absorbanceReader/closeLid" params: CloseLidParams - result: Optional[CloseLidResult] + result: Optional[CloseLidResult] = None _ImplementationCls: Type[CloseLidImpl] = CloseLidImpl diff --git a/api/src/opentrons/protocol_engine/commands/absorbance_reader/initialize.py b/api/src/opentrons/protocol_engine/commands/absorbance_reader/initialize.py index 458225ad1bb..5fa810455ec 100644 --- a/api/src/opentrons/protocol_engine/commands/absorbance_reader/initialize.py +++ b/api/src/opentrons/protocol_engine/commands/absorbance_reader/initialize.py @@ -1,9 +1,10 @@ """Command models to initialize an Absorbance Reader.""" from __future__ import annotations -from typing import List, Optional, Literal, TYPE_CHECKING +from typing import List, Optional, Literal, TYPE_CHECKING, Any from typing_extensions import Type from pydantic import BaseModel, Field +from pydantic.json_schema import SkipJsonSchema from opentrons.drivers.types import ABSMeasurementMode from opentrons.protocol_engine.types import ABSMeasureMode @@ -11,6 +12,7 @@ from ..command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, SuccessData from ...errors.error_occurrence import ErrorOccurrence from ...errors import InvalidWavelengthError +from ...state import update_types if TYPE_CHECKING: from opentrons.protocol_engine.state.state import StateView @@ -20,6 +22,10 @@ InitializeCommandType = Literal["absorbanceReader/initialize"] +def _remove_default(s: dict[str, Any]) -> None: + s.pop("default", None) + + class InitializeParams(BaseModel): """Input parameters to initialize an absorbance reading.""" @@ -28,8 +34,10 @@ class InitializeParams(BaseModel): ..., description="Initialize single or multi measurement mode." ) sampleWavelengths: List[int] = Field(..., description="Sample wavelengths in nm.") - referenceWavelength: Optional[int] = Field( - None, description="Optional reference wavelength in nm." + referenceWavelength: int | SkipJsonSchema[None] = Field( + None, + description="Optional reference wavelength in nm.", + json_schema_extra=_remove_default, ) @@ -53,6 +61,7 @@ def __init__( async def execute(self, params: InitializeParams) -> SuccessData[InitializeResult]: """Initiate a single absorbance measurement.""" + state_update = update_types.StateUpdate() abs_reader_substate = self._state_view.modules.get_absorbance_reader_substate( module_id=params.moduleId ) @@ -113,17 +122,22 @@ async def execute(self, params: InitializeParams) -> SuccessData[InitializeResul reference_wavelength=params.referenceWavelength, ) - return SuccessData( - public=InitializeResult(), + state_update.initialize_absorbance_reader( + abs_reader_substate.module_id, + params.measureMode, + params.sampleWavelengths, + params.referenceWavelength, ) + return SuccessData(public=InitializeResult(), state_update=state_update) + class Initialize(BaseCommand[InitializeParams, InitializeResult, ErrorOccurrence]): """A command to initialize an Absorbance Reader.""" commandType: InitializeCommandType = "absorbanceReader/initialize" params: InitializeParams - result: Optional[InitializeResult] + result: Optional[InitializeResult] = None _ImplementationCls: Type[InitializeImpl] = InitializeImpl diff --git a/api/src/opentrons/protocol_engine/commands/absorbance_reader/open_lid.py b/api/src/opentrons/protocol_engine/commands/absorbance_reader/open_lid.py index 1ad56413f9a..3fa965e33b3 100644 --- a/api/src/opentrons/protocol_engine/commands/absorbance_reader/open_lid.py +++ b/api/src/opentrons/protocol_engine/commands/absorbance_reader/open_lid.py @@ -91,7 +91,6 @@ async def execute(self, params: OpenLidParams) -> SuccessData[OpenLidResult]: deck_slot=self._state_view.modules.get_location( params.moduleId ).slotName, - deck_type=self._state_view.config.deck_type, model=absorbance_model, ) ) @@ -134,7 +133,7 @@ class OpenLid(BaseCommand[OpenLidParams, OpenLidResult, ErrorOccurrence]): commandType: OpenLidCommandType = "absorbanceReader/openLid" params: OpenLidParams - result: Optional[OpenLidResult] + result: Optional[OpenLidResult] = None _ImplementationCls: Type[OpenLidImpl] = OpenLidImpl diff --git a/api/src/opentrons/protocol_engine/commands/absorbance_reader/read.py b/api/src/opentrons/protocol_engine/commands/absorbance_reader/read.py index 8e8926089f1..a4a3aa58a5e 100644 --- a/api/src/opentrons/protocol_engine/commands/absorbance_reader/read.py +++ b/api/src/opentrons/protocol_engine/commands/absorbance_reader/read.py @@ -1,10 +1,11 @@ """Command models to read absorbance.""" from __future__ import annotations from datetime import datetime -from typing import Optional, Dict, TYPE_CHECKING, List -from typing_extensions import Literal, Type +from typing import Optional, Dict, TYPE_CHECKING, List, Any +from typing_extensions import Literal, Type from pydantic import BaseModel, Field +from pydantic.json_schema import SkipJsonSchema from ..command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, SuccessData from ...errors import CannotPerformModuleAction, StorageLimitReachedError @@ -16,12 +17,17 @@ MAXIMUM_CSV_FILE_LIMIT, ) from ...resources import FileProvider +from ...state import update_types if TYPE_CHECKING: from opentrons.protocol_engine.state.state import StateView from opentrons.protocol_engine.execution import EquipmentHandler +def _remove_default(s: dict[str, Any]) -> None: + s.pop("default", None) + + ReadAbsorbanceCommandType = Literal["absorbanceReader/read"] @@ -29,9 +35,10 @@ class ReadAbsorbanceParams(BaseModel): """Input parameters for an absorbance reading.""" moduleId: str = Field(..., description="Unique ID of the Absorbance Reader.") - fileName: Optional[str] = Field( + fileName: str | SkipJsonSchema[None] = Field( None, description="Optional file name to use when storing the results of a measurement.", + json_schema_extra=_remove_default, ) @@ -67,6 +74,7 @@ async def execute( # noqa: C901 self, params: ReadAbsorbanceParams ) -> SuccessData[ReadAbsorbanceResult]: """Initiate an absorbance measurement.""" + state_update = update_types.StateUpdate() abs_reader_substate = self._state_view.modules.get_absorbance_reader_substate( module_id=params.moduleId ) @@ -117,7 +125,9 @@ async def execute( # noqa: C901 ) asbsorbance_result[wavelength] = converted_values transform_results.append( - ReadData.construct(wavelength=wavelength, data=converted_values) + ReadData.model_construct( + wavelength=wavelength, data=converted_values + ) ) # Handle the virtual module case for data creation (all zeroes) elif self._state_view.config.use_virtual_modules: @@ -131,22 +141,27 @@ async def execute( # noqa: C901 ) asbsorbance_result[wavelength] = converted_values transform_results.append( - ReadData.construct(wavelength=wavelength, data=converted_values) + ReadData.model_construct( + wavelength=wavelength, data=converted_values + ) ) else: raise CannotPerformModuleAction( "Plate Reader data cannot be requested with a module that has not been initialized." ) + state_update.set_absorbance_reader_data( + module_id=abs_reader_substate.module_id, read_result=asbsorbance_result + ) # TODO (cb, 10-17-2024): FILE PROVIDER - Some day we may want to break the file provider behavior into a seperate API function. # When this happens, we probably will to have the change the command results handler we utilize to track file IDs in engine. # Today, the action handler for the FileStore looks for a ReadAbsorbanceResult command action, this will need to be delinked. # Begin interfacing with the file provider if the user provided a filename - file_ids = [] + file_ids: list[str] = [] if params.fileName is not None: # Create the Plate Reader Transform - plate_read_result = PlateReaderData.construct( + plate_read_result = PlateReaderData.model_construct( read_results=transform_results, reference_wavelength=abs_reader_substate.reference_wavelength, start_time=start_time, @@ -167,15 +182,26 @@ async def execute( # noqa: C901 ) file_ids.append(file_id) + state_update.files_added = update_types.FilesAddedUpdate( + file_ids=file_ids + ) # Return success data to api return SuccessData( public=ReadAbsorbanceResult( - data=asbsorbance_result, fileIds=file_ids + data=asbsorbance_result, + fileIds=file_ids, ), + state_update=state_update, ) + state_update.files_added = update_types.FilesAddedUpdate(file_ids=file_ids) + return SuccessData( - public=ReadAbsorbanceResult(data=asbsorbance_result, fileIds=file_ids), + public=ReadAbsorbanceResult( + data=asbsorbance_result, + fileIds=file_ids, + ), + state_update=state_update, ) @@ -186,7 +212,7 @@ class ReadAbsorbance( commandType: ReadAbsorbanceCommandType = "absorbanceReader/read" params: ReadAbsorbanceParams - result: Optional[ReadAbsorbanceResult] + result: Optional[ReadAbsorbanceResult] = None _ImplementationCls: Type[ReadAbsorbanceImpl] = ReadAbsorbanceImpl diff --git a/api/src/opentrons/protocol_engine/commands/air_gap_in_place.py b/api/src/opentrons/protocol_engine/commands/air_gap_in_place.py new file mode 100644 index 00000000000..f8c6bcf859b --- /dev/null +++ b/api/src/opentrons/protocol_engine/commands/air_gap_in_place.py @@ -0,0 +1,160 @@ +"""AirGap in place command request, result, and implementation models.""" + +from __future__ import annotations +from typing import TYPE_CHECKING, Optional, Type, Union +from typing_extensions import Literal + +from opentrons_shared_data.errors.exceptions import PipetteOverpressureError + +from opentrons.hardware_control import HardwareControlAPI + +from .pipetting_common import ( + PipetteIdMixin, + AspirateVolumeMixin, + FlowRateMixin, + BaseLiquidHandlingResult, + OverpressureError, +) +from .command import ( + AbstractCommandImpl, + BaseCommand, + BaseCommandCreate, + SuccessData, + DefinedErrorData, +) +from ..errors.error_occurrence import ErrorOccurrence +from ..errors.exceptions import PipetteNotReadyToAspirateError +from ..state.update_types import StateUpdate +from ..types import AspiratedFluid, FluidKind + +if TYPE_CHECKING: + from ..execution import PipettingHandler, GantryMover + from ..resources import ModelUtils + from ..state.state import StateView + from ..notes import CommandNoteAdder + +AirGapInPlaceCommandType = Literal["airGapInPlace"] + + +class AirGapInPlaceParams(PipetteIdMixin, AspirateVolumeMixin, FlowRateMixin): + """Payload required to air gap in place.""" + + pass + + +class AirGapInPlaceResult(BaseLiquidHandlingResult): + """Result data from the execution of a AirGapInPlace command.""" + + pass + + +_ExecuteReturn = Union[ + SuccessData[AirGapInPlaceResult], + DefinedErrorData[OverpressureError], +] + + +class AirGapInPlaceImplementation( + AbstractCommandImpl[AirGapInPlaceParams, _ExecuteReturn] +): + """AirGapInPlace command implementation.""" + + def __init__( + self, + pipetting: PipettingHandler, + hardware_api: HardwareControlAPI, + state_view: StateView, + command_note_adder: CommandNoteAdder, + model_utils: ModelUtils, + gantry_mover: GantryMover, + **kwargs: object, + ) -> None: + self._pipetting = pipetting + self._state_view = state_view + self._hardware_api = hardware_api + self._command_note_adder = command_note_adder + self._model_utils = model_utils + self._gantry_mover = gantry_mover + + async def execute(self, params: AirGapInPlaceParams) -> _ExecuteReturn: + """Air gap without moving the pipette. + + Raises: + TipNotAttachedError: if no tip is attached to the pipette. + PipetteNotReadyToAirGapError: pipette plunger is not ready. + """ + ready_to_aspirate = self._pipetting.get_is_ready_to_aspirate( + pipette_id=params.pipetteId, + ) + + if not ready_to_aspirate: + raise PipetteNotReadyToAspirateError( + "Pipette cannot air gap in place because of a previous blow out." + " The first aspirate following a blow-out must be from a specific well" + " so the plunger can be reset in a known safe position." + ) + + state_update = StateUpdate() + + try: + current_position = await self._gantry_mover.get_position(params.pipetteId) + volume = await self._pipetting.aspirate_in_place( + pipette_id=params.pipetteId, + volume=params.volume, + flow_rate=params.flowRate, + command_note_adder=self._command_note_adder, + ) + except PipetteOverpressureError as e: + return DefinedErrorData( + public=OverpressureError( + id=self._model_utils.generate_id(), + createdAt=self._model_utils.get_timestamp(), + wrappedErrors=[ + ErrorOccurrence.from_failed( + id=self._model_utils.generate_id(), + createdAt=self._model_utils.get_timestamp(), + error=e, + ) + ], + errorInfo=( + { + "retryLocation": ( + current_position.x, + current_position.y, + current_position.z, + ) + } + ), + ), + state_update=state_update, + ) + else: + state_update.set_fluid_aspirated( + pipette_id=params.pipetteId, + fluid=AspiratedFluid(kind=FluidKind.AIR, volume=volume), + ) + return SuccessData( + public=AirGapInPlaceResult(volume=volume), + state_update=state_update, + ) + + +class AirGapInPlace( + BaseCommand[AirGapInPlaceParams, AirGapInPlaceResult, OverpressureError] +): + """AirGapInPlace command model.""" + + commandType: AirGapInPlaceCommandType = "airGapInPlace" + params: AirGapInPlaceParams + result: Optional[AirGapInPlaceResult] = None + + _ImplementationCls: Type[AirGapInPlaceImplementation] = AirGapInPlaceImplementation + + +class AirGapInPlaceCreate(BaseCommandCreate[AirGapInPlaceParams]): + """AirGapInPlace command request model.""" + + commandType: AirGapInPlaceCommandType = "airGapInPlace" + params: AirGapInPlaceParams + + _CommandCls: Type[AirGapInPlace] = AirGapInPlace diff --git a/api/src/opentrons/protocol_engine/commands/aspirate.py b/api/src/opentrons/protocol_engine/commands/aspirate.py index 00d57a93e9a..9664d733b8a 100644 --- a/api/src/opentrons/protocol_engine/commands/aspirate.py +++ b/api/src/opentrons/protocol_engine/commands/aspirate.py @@ -1,7 +1,7 @@ """Aspirate command request, result, and implementation models.""" + from __future__ import annotations from typing import TYPE_CHECKING, Optional, Type, Union -from opentrons_shared_data.errors.exceptions import PipetteOverpressureError from typing_extensions import Literal from .pipetting_common import ( @@ -9,9 +9,15 @@ PipetteIdMixin, AspirateVolumeMixin, FlowRateMixin, - LiquidHandlingWellLocationMixin, BaseLiquidHandlingResult, + aspirate_in_place, + prepare_for_aspirate, +) +from .movement_common import ( + LiquidHandlingWellLocationMixin, DestinationPositionResult, + StallOrCollisionError, + move_to_well, ) from .command import ( AbstractCommandImpl, @@ -20,12 +26,15 @@ DefinedErrorData, SuccessData, ) -from ..errors.error_occurrence import ErrorOccurrence from opentrons.hardware_control import HardwareControlAPI from ..state.update_types import StateUpdate, CLEAR -from ..types import WellLocation, WellOrigin, CurrentWell, DeckPoint +from ..types import ( + WellLocation, + WellOrigin, + CurrentWell, +) if TYPE_CHECKING: from ..execution import MovementHandler, PipettingHandler @@ -53,7 +62,7 @@ class AspirateResult(BaseLiquidHandlingResult, DestinationPositionResult): _ExecuteReturn = Union[ SuccessData[AspirateResult], - DefinedErrorData[OverpressureError], + DefinedErrorData[OverpressureError] | DefinedErrorData[StallOrCollisionError], ] @@ -86,23 +95,51 @@ async def execute(self, params: AspirateParams) -> _ExecuteReturn: pipette_id = params.pipetteId labware_id = params.labwareId well_name = params.wellName + well_location = params.wellLocation + + state_update = StateUpdate() + + final_location = self._state_view.geometry.get_well_position( + labware_id=labware_id, + well_name=well_name, + well_location=well_location, + operation_volume=-params.volume, + pipette_id=pipette_id, + ) ready_to_aspirate = self._pipetting.get_is_ready_to_aspirate( pipette_id=pipette_id ) current_well = None - state_update = StateUpdate() if not ready_to_aspirate: - await self._movement.move_to_well( + move_result = await move_to_well( + movement=self._movement, + model_utils=self._model_utils, pipette_id=pipette_id, labware_id=labware_id, well_name=well_name, well_location=WellLocation(origin=WellOrigin.TOP), ) + state_update.append(move_result.state_update) + if isinstance(move_result, DefinedErrorData): + return DefinedErrorData(move_result.public, state_update=state_update) - await self._pipetting.prepare_for_aspirate(pipette_id=pipette_id) + prepare_result = await prepare_for_aspirate( + pipette_id=pipette_id, + pipetting=self._pipetting, + model_utils=self._model_utils, + # Note that the retryLocation is the final location, inside the liquid, + # because that's where we'd want the client to try re-aspirating if this + # command fails and the run enters error recovery. + location_if_error={"retryLocation": final_location}, + ) + state_update.append(prepare_result.state_update) + if isinstance(prepare_result, DefinedErrorData): + return DefinedErrorData( + public=prepare_result.public, state_update=state_update + ) # set our current deck location to the well now that we've made # an intermediate move for the "prepare for aspirate" step @@ -112,71 +149,84 @@ async def execute(self, params: AspirateParams) -> _ExecuteReturn: well_name=well_name, ) - position = await self._movement.move_to_well( + move_result = await move_to_well( + movement=self._movement, + model_utils=self._model_utils, pipette_id=pipette_id, labware_id=labware_id, well_name=well_name, - well_location=params.wellLocation, + well_location=well_location, current_well=current_well, operation_volume=-params.volume, ) - deck_point = DeckPoint.construct(x=position.x, y=position.y, z=position.z) - state_update.set_pipette_location( + state_update.append(move_result.state_update) + if isinstance(move_result, DefinedErrorData): + return DefinedErrorData( + public=move_result.public, state_update=state_update + ) + + aspirate_result = await aspirate_in_place( pipette_id=pipette_id, - new_labware_id=labware_id, - new_well_name=well_name, - new_deck_point=deck_point, + volume=params.volume, + flow_rate=params.flowRate, + location_if_error={ + "retryLocation": ( + move_result.public.position.x, + move_result.public.position.y, + move_result.public.position.z, + ) + }, + command_note_adder=self._command_note_adder, + pipetting=self._pipetting, + model_utils=self._model_utils, ) - - try: - volume_aspirated = await self._pipetting.aspirate_in_place( - pipette_id=pipette_id, - volume=params.volume, - flow_rate=params.flowRate, - command_note_adder=self._command_note_adder, - ) - except PipetteOverpressureError as e: + state_update.append(aspirate_result.state_update) + if isinstance(aspirate_result, DefinedErrorData): state_update.set_liquid_operated( labware_id=labware_id, - well_name=well_name, + well_names=self._state_view.geometry.get_wells_covered_by_pipette_with_active_well( + labware_id, + well_name, + params.pipetteId, + ), volume_added=CLEAR, ) return DefinedErrorData( - public=OverpressureError( - id=self._model_utils.generate_id(), - createdAt=self._model_utils.get_timestamp(), - wrappedErrors=[ - ErrorOccurrence.from_failed( - id=self._model_utils.generate_id(), - createdAt=self._model_utils.get_timestamp(), - error=e, - ) - ], - errorInfo={"retryLocation": (position.x, position.y, position.z)}, - ), - state_update=state_update, - ) - else: - state_update.set_liquid_operated( - labware_id=labware_id, - well_name=well_name, - volume_added=-volume_aspirated, - ) - return SuccessData( - public=AspirateResult( - volume=volume_aspirated, - position=deck_point, - ), - state_update=state_update, + public=aspirate_result.public, state_update=state_update ) + state_update.set_liquid_operated( + labware_id=labware_id, + well_names=self._state_view.geometry.get_wells_covered_by_pipette_with_active_well( + labware_id, well_name, pipette_id + ), + volume_added=-aspirate_result.public.volume + * self._state_view.geometry.get_nozzles_per_well( + labware_id, + well_name, + params.pipetteId, + ), + ) -class Aspirate(BaseCommand[AspirateParams, AspirateResult, OverpressureError]): + return SuccessData( + public=AspirateResult( + volume=aspirate_result.public.volume, + position=move_result.public.position, + ), + state_update=state_update, + ) + + +class Aspirate( + BaseCommand[ + AspirateParams, AspirateResult, OverpressureError | StallOrCollisionError + ] +): """Aspirate command model.""" commandType: AspirateCommandType = "aspirate" params: AspirateParams - result: Optional[AspirateResult] + result: Optional[AspirateResult] = None _ImplementationCls: Type[AspirateImplementation] = AspirateImplementation diff --git a/api/src/opentrons/protocol_engine/commands/aspirate_in_place.py b/api/src/opentrons/protocol_engine/commands/aspirate_in_place.py index 3bca160e50b..434924928d7 100644 --- a/api/src/opentrons/protocol_engine/commands/aspirate_in_place.py +++ b/api/src/opentrons/protocol_engine/commands/aspirate_in_place.py @@ -4,8 +4,6 @@ from typing import TYPE_CHECKING, Optional, Type, Union from typing_extensions import Literal -from opentrons_shared_data.errors.exceptions import PipetteOverpressureError - from opentrons.hardware_control import HardwareControlAPI from .pipetting_common import ( @@ -14,6 +12,7 @@ FlowRateMixin, BaseLiquidHandlingResult, OverpressureError, + aspirate_in_place, ) from .command import ( AbstractCommandImpl, @@ -22,9 +21,8 @@ SuccessData, DefinedErrorData, ) -from ..errors.error_occurrence import ErrorOccurrence from ..errors.exceptions import PipetteNotReadyToAspirateError -from ..state.update_types import StateUpdate, CLEAR +from ..state.update_types import CLEAR from ..types import CurrentWell if TYPE_CHECKING: @@ -83,8 +81,6 @@ async def execute(self, params: AspirateInPlaceParams) -> _ExecuteReturn: TipNotAttachedError: if no tip is attached to the pipette. PipetteNotReadyToAspirateError: pipette plunger is not ready. """ - state_update = StateUpdate() - ready_to_aspirate = self._pipetting.get_is_ready_to_aspirate( pipette_id=params.pipetteId, ) @@ -95,63 +91,71 @@ async def execute(self, params: AspirateInPlaceParams) -> _ExecuteReturn: " so the plunger can be reset in a known safe position." ) - current_location = self._state_view.pipettes.get_current_location() current_position = await self._gantry_mover.get_position(params.pipetteId) + current_location = self._state_view.pipettes.get_current_location() - try: - volume = await self._pipetting.aspirate_in_place( - pipette_id=params.pipetteId, - volume=params.volume, - flow_rate=params.flowRate, - command_note_adder=self._command_note_adder, - ) - except PipetteOverpressureError as e: + result = await aspirate_in_place( + pipette_id=params.pipetteId, + volume=params.volume, + flow_rate=params.flowRate, + location_if_error={ + "retryLocation": ( + current_position.x, + current_position.y, + current_position.z, + ) + }, + command_note_adder=self._command_note_adder, + pipetting=self._pipetting, + model_utils=self._model_utils, + ) + if isinstance(result, DefinedErrorData): if ( isinstance(current_location, CurrentWell) and current_location.pipette_id == params.pipetteId ): - state_update.set_liquid_operated( - labware_id=current_location.labware_id, - well_name=current_location.well_name, - volume_added=CLEAR, - ) - return DefinedErrorData( - public=OverpressureError( - id=self._model_utils.generate_id(), - createdAt=self._model_utils.get_timestamp(), - wrappedErrors=[ - ErrorOccurrence.from_failed( - id=self._model_utils.generate_id(), - createdAt=self._model_utils.get_timestamp(), - error=e, - ) - ], - errorInfo=( - { - "retryLocation": ( - current_position.x, - current_position.y, - current_position.z, - ) - } + return DefinedErrorData( + public=result.public, + state_update=result.state_update.set_liquid_operated( + labware_id=current_location.labware_id, + well_names=self._state_view.geometry.get_wells_covered_by_pipette_with_active_well( + current_location.labware_id, + current_location.well_name, + params.pipetteId, + ), + volume_added=CLEAR, ), - ), - state_update=state_update, - ) + state_update_if_false_positive=result.state_update_if_false_positive, + ) + else: + return result else: if ( isinstance(current_location, CurrentWell) and current_location.pipette_id == params.pipetteId ): - state_update.set_liquid_operated( - labware_id=current_location.labware_id, - well_name=current_location.well_name, - volume_added=-volume, + return SuccessData( + public=AspirateInPlaceResult(volume=result.public.volume), + state_update=result.state_update.set_liquid_operated( + labware_id=current_location.labware_id, + well_names=self._state_view.geometry.get_wells_covered_by_pipette_with_active_well( + current_location.labware_id, + current_location.well_name, + params.pipetteId, + ), + volume_added=-result.public.volume + * self._state_view.geometry.get_nozzles_per_well( + current_location.labware_id, + current_location.well_name, + params.pipetteId, + ), + ), + ) + else: + return SuccessData( + public=AspirateInPlaceResult(volume=result.public.volume), + state_update=result.state_update, ) - return SuccessData( - public=AspirateInPlaceResult(volume=volume), - state_update=state_update, - ) class AspirateInPlace( @@ -161,7 +165,7 @@ class AspirateInPlace( commandType: AspirateInPlaceCommandType = "aspirateInPlace" params: AspirateInPlaceParams - result: Optional[AspirateInPlaceResult] + result: Optional[AspirateInPlaceResult] = None _ImplementationCls: Type[ AspirateInPlaceImplementation diff --git a/api/src/opentrons/protocol_engine/commands/blow_out.py b/api/src/opentrons/protocol_engine/commands/blow_out.py index e13378b5541..07582781a3a 100644 --- a/api/src/opentrons/protocol_engine/commands/blow_out.py +++ b/api/src/opentrons/protocol_engine/commands/blow_out.py @@ -1,18 +1,21 @@ """Blow-out command request, result, and implementation models.""" + from __future__ import annotations from typing import TYPE_CHECKING, Optional, Type, Union -from opentrons_shared_data.errors.exceptions import PipetteOverpressureError from typing_extensions import Literal -from ..state.update_types import StateUpdate -from ..types import DeckPoint from .pipetting_common import ( OverpressureError, PipetteIdMixin, FlowRateMixin, + blow_out_in_place, +) +from .movement_common import ( WellLocationMixin, DestinationPositionResult, + move_to_well, + StallOrCollisionError, ) from .command import ( AbstractCommandImpl, @@ -21,7 +24,7 @@ DefinedErrorData, SuccessData, ) -from ..errors.error_occurrence import ErrorOccurrence +from ..state.update_types import StateUpdate from opentrons.hardware_control import HardwareControlAPI @@ -49,7 +52,7 @@ class BlowOutResult(DestinationPositionResult): _ExecuteReturn = Union[ SuccessData[BlowOutResult], - DefinedErrorData[OverpressureError], + DefinedErrorData[OverpressureError] | DefinedErrorData[StallOrCollisionError], ] @@ -73,59 +76,61 @@ def __init__( async def execute(self, params: BlowOutParams) -> _ExecuteReturn: """Move to and blow-out the requested well.""" - state_update = StateUpdate() - - x, y, z = await self._movement.move_to_well( + move_result = await move_to_well( + movement=self._movement, + model_utils=self._model_utils, pipette_id=params.pipetteId, labware_id=params.labwareId, well_name=params.wellName, well_location=params.wellLocation, ) - deck_point = DeckPoint.construct(x=x, y=y, z=z) - state_update.set_pipette_location( + if isinstance(move_result, DefinedErrorData): + return move_result + blow_out_result = await blow_out_in_place( pipette_id=params.pipetteId, - new_labware_id=params.labwareId, - new_well_name=params.wellName, - new_deck_point=deck_point, + flow_rate=params.flowRate, + location_if_error={ + "retryLocation": ( + move_result.public.position.x, + move_result.public.position.y, + move_result.public.position.z, + ) + }, + pipetting=self._pipetting, + model_utils=self._model_utils, ) - try: - await self._pipetting.blow_out_in_place( - pipette_id=params.pipetteId, flow_rate=params.flowRate - ) - except PipetteOverpressureError as e: + if isinstance(blow_out_result, DefinedErrorData): return DefinedErrorData( - public=OverpressureError( - id=self._model_utils.generate_id(), - createdAt=self._model_utils.get_timestamp(), - wrappedErrors=[ - ErrorOccurrence.from_failed( - id=self._model_utils.generate_id(), - createdAt=self._model_utils.get_timestamp(), - error=e, - ) - ], - errorInfo={ - "retryLocation": ( - x, - y, - z, - ) - }, + public=blow_out_result.public, + state_update=StateUpdate.reduce( + move_result.state_update, blow_out_result.state_update + ), + state_update_if_false_positive=StateUpdate.reduce( + move_result.state_update, + blow_out_result.state_update_if_false_positive, ), ) else: return SuccessData( - public=BlowOutResult(position=deck_point), - state_update=state_update, + public=BlowOutResult(position=move_result.public.position), + state_update=StateUpdate.reduce( + move_result.state_update, blow_out_result.state_update + ), ) -class BlowOut(BaseCommand[BlowOutParams, BlowOutResult, ErrorOccurrence]): +class BlowOut( + BaseCommand[ + BlowOutParams, + BlowOutResult, + OverpressureError | StallOrCollisionError, + ] +): """Blow-out command model.""" commandType: BlowOutCommandType = "blowout" params: BlowOutParams - result: Optional[BlowOutResult] + result: Optional[BlowOutResult] = None _ImplementationCls: Type[BlowOutImplementation] = BlowOutImplementation diff --git a/api/src/opentrons/protocol_engine/commands/blow_out_in_place.py b/api/src/opentrons/protocol_engine/commands/blow_out_in_place.py index 0b9aaec77b2..527c921e499 100644 --- a/api/src/opentrons/protocol_engine/commands/blow_out_in_place.py +++ b/api/src/opentrons/protocol_engine/commands/blow_out_in_place.py @@ -2,7 +2,6 @@ from __future__ import annotations from typing import TYPE_CHECKING, Optional, Type, Union -from opentrons_shared_data.errors.exceptions import PipetteOverpressureError from typing_extensions import Literal from pydantic import BaseModel @@ -10,6 +9,7 @@ OverpressureError, PipetteIdMixin, FlowRateMixin, + blow_out_in_place, ) from .command import ( AbstractCommandImpl, @@ -72,36 +72,25 @@ def __init__( async def execute(self, params: BlowOutInPlaceParams) -> _ExecuteReturn: """Blow-out without moving the pipette.""" - try: - current_position = await self._gantry_mover.get_position(params.pipetteId) - await self._pipetting.blow_out_in_place( - pipette_id=params.pipetteId, flow_rate=params.flowRate - ) - except PipetteOverpressureError as e: - return DefinedErrorData( - public=OverpressureError( - id=self._model_utils.generate_id(), - createdAt=self._model_utils.get_timestamp(), - wrappedErrors=[ - ErrorOccurrence.from_failed( - id=self._model_utils.generate_id(), - createdAt=self._model_utils.get_timestamp(), - error=e, - ) - ], - errorInfo={ - "retryLocation": ( - current_position.x, - current_position.y, - current_position.z, - ) - }, - ), - ) - else: - return SuccessData( - public=BlowOutInPlaceResult(), - ) + current_position = await self._gantry_mover.get_position(params.pipetteId) + result = await blow_out_in_place( + pipette_id=params.pipetteId, + flow_rate=params.flowRate, + location_if_error={ + "retryLocation": ( + current_position.x, + current_position.y, + current_position.z, + ) + }, + pipetting=self._pipetting, + model_utils=self._model_utils, + ) + if isinstance(result, DefinedErrorData): + return result + return SuccessData( + public=BlowOutInPlaceResult(), state_update=result.state_update + ) class BlowOutInPlace( @@ -111,7 +100,7 @@ class BlowOutInPlace( commandType: BlowOutInPlaceCommandType = "blowOutInPlace" params: BlowOutInPlaceParams - result: Optional[BlowOutInPlaceResult] + result: Optional[BlowOutInPlaceResult] = None _ImplementationCls: Type[ BlowOutInPlaceImplementation diff --git a/api/src/opentrons/protocol_engine/commands/calibration/calibrate_gripper.py b/api/src/opentrons/protocol_engine/commands/calibration/calibrate_gripper.py index 0333a171077..25ab19e2cd4 100644 --- a/api/src/opentrons/protocol_engine/commands/calibration/calibrate_gripper.py +++ b/api/src/opentrons/protocol_engine/commands/calibration/calibrate_gripper.py @@ -1,10 +1,11 @@ """Models and implementation for the calibrateGripper command.""" from enum import Enum -from typing import Optional, Type +from typing import Optional, Type, Any from typing_extensions import Literal from pydantic import BaseModel, Field +from pydantic.json_schema import SkipJsonSchema from opentrons.types import Point from opentrons.hardware_control import HardwareControlAPI @@ -22,6 +23,10 @@ CalibrateGripperCommandType = Literal["calibration/calibrateGripper"] +def _remove_default(s: dict[str, Any]) -> None: + s.pop("default", None) + + class CalibrateGripperParamsJaw(Enum): # noqa: D101 FRONT = "front" REAR = "rear" @@ -39,7 +44,7 @@ class CalibrateGripperParams(BaseModel): ), ) - otherJawOffset: Optional[Vec3f] = Field( + otherJawOffset: Vec3f | SkipJsonSchema[None] = Field( None, description=( "If an offset for the other probe is already found, then specifying it here" @@ -48,6 +53,7 @@ class CalibrateGripperParams(BaseModel): " If this param is not specified then the command will only find and return" " the offset for the specified probe." ), + json_schema_extra=_remove_default, ) @@ -62,11 +68,12 @@ class CalibrateGripperResult(BaseModel): ), ) - savedCalibration: Optional[GripperCalibrationOffset] = Field( + savedCalibration: GripperCalibrationOffset | SkipJsonSchema[None] = Field( None, description=( "Gripper calibration result data, when `otherJawOffset` is provided." ), + json_schema_extra=_remove_default, ) @@ -118,8 +125,8 @@ async def execute( calibration_data = result return SuccessData( - public=CalibrateGripperResult.construct( - jawOffset=Vec3f.construct( + public=CalibrateGripperResult.model_construct( + jawOffset=Vec3f.model_construct( x=probe_offset.x, y=probe_offset.y, z=probe_offset.z ), savedCalibration=calibration_data, @@ -143,7 +150,7 @@ class CalibrateGripper( commandType: CalibrateGripperCommandType = "calibration/calibrateGripper" params: CalibrateGripperParams - result: Optional[CalibrateGripperResult] + result: Optional[CalibrateGripperResult] = None _ImplementationCls: Type[ CalibrateGripperImplementation diff --git a/api/src/opentrons/protocol_engine/commands/calibration/calibrate_module.py b/api/src/opentrons/protocol_engine/commands/calibration/calibrate_module.py index f488e8eab97..e203dcc19be 100644 --- a/api/src/opentrons/protocol_engine/commands/calibration/calibrate_module.py +++ b/api/src/opentrons/protocol_engine/commands/calibration/calibrate_module.py @@ -101,7 +101,7 @@ class CalibrateModule( commandType: CalibrateModuleCommandType = "calibration/calibrateModule" params: CalibrateModuleParams - result: Optional[CalibrateModuleResult] + result: Optional[CalibrateModuleResult] = None _ImplementationCls: Type[ CalibrateModuleImplementation diff --git a/api/src/opentrons/protocol_engine/commands/calibration/calibrate_pipette.py b/api/src/opentrons/protocol_engine/commands/calibration/calibrate_pipette.py index fbe754f6389..cb0eb93876c 100644 --- a/api/src/opentrons/protocol_engine/commands/calibration/calibrate_pipette.py +++ b/api/src/opentrons/protocol_engine/commands/calibration/calibrate_pipette.py @@ -65,8 +65,8 @@ async def execute( await ot3_api.save_instrument_offset(mount=ot3_mount, delta=pipette_offset) return SuccessData( - public=CalibratePipetteResult.construct( - pipetteOffset=InstrumentOffsetVector.construct( + public=CalibratePipetteResult.model_construct( + pipetteOffset=InstrumentOffsetVector.model_construct( x=pipette_offset.x, y=pipette_offset.y, z=pipette_offset.z ) ), @@ -80,7 +80,7 @@ class CalibratePipette( commandType: CalibratePipetteCommandType = "calibration/calibratePipette" params: CalibratePipetteParams - result: Optional[CalibratePipetteResult] + result: Optional[CalibratePipetteResult] = None _ImplementationCls: Type[ CalibratePipetteImplementation diff --git a/api/src/opentrons/protocol_engine/commands/calibration/move_to_maintenance_position.py b/api/src/opentrons/protocol_engine/commands/calibration/move_to_maintenance_position.py index afb178cae99..ca18d70a265 100644 --- a/api/src/opentrons/protocol_engine/commands/calibration/move_to_maintenance_position.py +++ b/api/src/opentrons/protocol_engine/commands/calibration/move_to_maintenance_position.py @@ -136,7 +136,7 @@ class MoveToMaintenancePosition( "calibration/moveToMaintenancePosition" ) params: MoveToMaintenancePositionParams - result: Optional[MoveToMaintenancePositionResult] + result: Optional[MoveToMaintenancePositionResult] = None _ImplementationCls: Type[ MoveToMaintenancePositionImplementation diff --git a/api/src/opentrons/protocol_engine/commands/command.py b/api/src/opentrons/protocol_engine/commands/command.py index bea6bed084d..38d1512905e 100644 --- a/api/src/opentrons/protocol_engine/commands/command.py +++ b/api/src/opentrons/protocol_engine/commands/command.py @@ -1,12 +1,11 @@ """Base command data model and type definitions.""" - from __future__ import annotations import dataclasses from abc import ABC, abstractmethod from datetime import datetime -from enum import Enum +import enum from typing import ( TYPE_CHECKING, Generic, @@ -15,10 +14,12 @@ List, Type, Union, + Any, + Dict, ) from pydantic import BaseModel, Field -from pydantic.generics import GenericModel +from pydantic.json_schema import SkipJsonSchema from opentrons.hardware_control import HardwareControlAPI from opentrons.protocol_engine.state.update_types import StateUpdate @@ -41,7 +42,7 @@ _ErrorT_co = TypeVar("_ErrorT_co", bound=ErrorOccurrence, covariant=True) -class CommandStatus(str, Enum): +class CommandStatus(str, enum.Enum): """Command execution status.""" QUEUED = "queued" @@ -50,7 +51,7 @@ class CommandStatus(str, Enum): FAILED = "failed" -class CommandIntent(str, Enum): +class CommandIntent(str, enum.Enum): """Run intent for a given command. Props: @@ -63,8 +64,12 @@ class CommandIntent(str, Enum): FIXIT = "fixit" +def _pop_default(s: Dict[str, Any]) -> None: + s.pop("default", None) + + class BaseCommandCreate( - GenericModel, + BaseModel, # These type parameters need to be invariant because our fields are mutable. Generic[_ParamsT], ): @@ -82,7 +87,7 @@ class BaseCommandCreate( ), ) params: _ParamsT = Field(..., description="Command execution data payload") - intent: Optional[CommandIntent] = Field( + intent: CommandIntent | SkipJsonSchema[None] = Field( None, description=( "The reason the command was added. If not specified or `protocol`," @@ -95,14 +100,16 @@ class BaseCommandCreate( "Use setup commands for activities like pre-run calibration checks" " and module setup, like pre-heating." ), + json_schema_extra=_pop_default, ) - key: Optional[str] = Field( + key: str | SkipJsonSchema[None] = Field( None, description=( "A key value, unique in this run, that can be used to track" " the same logical command across multiple runs of the same protocol." " If a value is not provided, one will be generated." ), + json_schema_extra=_pop_default, ) @@ -144,8 +151,65 @@ class DefinedErrorData(Generic[_ErrorT_co]): ) +_ExecuteReturnT_co = TypeVar( + "_ExecuteReturnT_co", + bound=Union[ + SuccessData[BaseModel], + DefinedErrorData[ErrorOccurrence], + ], + covariant=True, +) + + +class AbstractCommandImpl( + ABC, + Generic[_ParamsT_contra, _ExecuteReturnT_co], +): + """Abstract command creation and execution implementation. + + A given command request should map to a specific command implementation, + which defines how to execute the command and map data from execution into the + result model. + """ + + def __init__( + self, + state_view: StateView, + hardware_api: HardwareControlAPI, + equipment: execution.EquipmentHandler, + file_provider: execution.FileProvider, + movement: execution.MovementHandler, + gantry_mover: execution.GantryMover, + labware_movement: execution.LabwareMovementHandler, + pipetting: execution.PipettingHandler, + tip_handler: execution.TipHandler, + run_control: execution.RunControlHandler, + rail_lights: execution.RailLightsHandler, + model_utils: ModelUtils, + status_bar: execution.StatusBarHandler, + command_note_adder: CommandNoteAdder, + ) -> None: + """Initialize the command implementation with execution handlers.""" + pass + + @abstractmethod + async def execute(self, params: _ParamsT_contra) -> _ExecuteReturnT_co: + """Execute the command, mapping data from execution into a response model. + + This should either: + + - Return a `SuccessData`, if the command completed normally. + - Return a `DefinedErrorData`, if the command failed with a "defined error." + Defined errors are errors that are documented as part of the robot's public + API. + - Raise an exception, if the command failed with any other error + (in other words, an undefined error). + """ + ... + + class BaseCommand( - GenericModel, + BaseModel, # These type parameters need to be invariant because our fields are mutable. Generic[_ParamsT, _ResultT, _ErrorT], ): @@ -242,60 +306,3 @@ class BaseCommand( ], ] ] - - -_ExecuteReturnT_co = TypeVar( - "_ExecuteReturnT_co", - bound=Union[ - SuccessData[BaseModel], - DefinedErrorData[ErrorOccurrence], - ], - covariant=True, -) - - -class AbstractCommandImpl( - ABC, - Generic[_ParamsT_contra, _ExecuteReturnT_co], -): - """Abstract command creation and execution implementation. - - A given command request should map to a specific command implementation, - which defines how to execute the command and map data from execution into the - result model. - """ - - def __init__( - self, - state_view: StateView, - hardware_api: HardwareControlAPI, - equipment: execution.EquipmentHandler, - file_provider: execution.FileProvider, - movement: execution.MovementHandler, - gantry_mover: execution.GantryMover, - labware_movement: execution.LabwareMovementHandler, - pipetting: execution.PipettingHandler, - tip_handler: execution.TipHandler, - run_control: execution.RunControlHandler, - rail_lights: execution.RailLightsHandler, - model_utils: ModelUtils, - status_bar: execution.StatusBarHandler, - command_note_adder: CommandNoteAdder, - ) -> None: - """Initialize the command implementation with execution handlers.""" - pass - - @abstractmethod - async def execute(self, params: _ParamsT_contra) -> _ExecuteReturnT_co: - """Execute the command, mapping data from execution into a response model. - - This should either: - - - Return a `SuccessData`, if the command completed normally. - - Return a `DefinedErrorData`, if the command failed with a "defined error." - Defined errors are errors that are documented as part of the robot's public - API. - - Raise an exception, if the command failed with any other error - (in other words, an undefined error). - """ - ... diff --git a/api/src/opentrons/protocol_engine/commands/command_unions.py b/api/src/opentrons/protocol_engine/commands/command_unions.py index 0e0a4cf3112..d5d1b0a3fc9 100644 --- a/api/src/opentrons/protocol_engine/commands/command_unions.py +++ b/api/src/opentrons/protocol_engine/commands/command_unions.py @@ -3,7 +3,7 @@ from collections.abc import Collection from typing import Annotated, Type, Union, get_type_hints -from pydantic import Field +from pydantic import Field, TypeAdapter from opentrons.util.get_union_elements import get_union_elements @@ -13,6 +13,7 @@ LiquidNotFoundError, TipPhysicallyAttachedError, ) +from .movement_common import StallOrCollisionError from . import absorbance_reader from . import heater_shaker @@ -22,6 +23,7 @@ from . import calibration from . import unsafe +from . import robot from .set_rail_lights import ( SetRailLights, @@ -31,6 +33,14 @@ SetRailLightsResult, ) +from .air_gap_in_place import ( + AirGapInPlace, + AirGapInPlaceParams, + AirGapInPlaceCreate, + AirGapInPlaceResult, + AirGapInPlaceCommandType, +) + from .aspirate import ( Aspirate, AspirateParams, @@ -127,6 +137,14 @@ LoadLiquidCommandType, ) +from .load_liquid_class import ( + LoadLiquidClass, + LoadLiquidClassParams, + LoadLiquidClassCreate, + LoadLiquidClassResult, + LoadLiquidClassCommandType, +) + from .load_module import ( LoadModule, LoadModuleParams, @@ -143,6 +161,22 @@ LoadPipetteCommandType, ) +from .load_lid_stack import ( + LoadLidStack, + LoadLidStackParams, + LoadLidStackCreate, + LoadLidStackResult, + LoadLidStackCommandType, +) + +from .load_lid import ( + LoadLid, + LoadLidParams, + LoadLidCreate, + LoadLidResult, + LoadLidCommandType, +) + from .move_labware import ( GripperMovementError, MoveLabware, @@ -305,6 +339,14 @@ GetTipPresenceCommandType, ) +from .get_next_tip import ( + GetNextTip, + GetNextTipCreate, + GetNextTipParams, + GetNextTipResult, + GetNextTipCommandType, +) + from .liquid_probe import ( LiquidProbe, LiquidProbeParams, @@ -318,8 +360,33 @@ TryLiquidProbeCommandType, ) +from .evotip_seal_pipette import ( + EvotipSealPipette, + EvotipSealPipetteParams, + EvotipSealPipetteCreate, + EvotipSealPipetteResult, + EvotipSealPipetteCommandType, +) + +from .evotip_dispense import ( + EvotipDispense, + EvotipDispenseParams, + EvotipDispenseCreate, + EvotipDispenseResult, + EvotipDispenseCommandType, +) + +from .evotip_unseal_pipette import ( + EvotipUnsealPipette, + EvotipUnsealPipetteParams, + EvotipUnsealPipetteCreate, + EvotipUnsealPipetteResult, + EvotipUnsealPipetteCommandType, +) + Command = Annotated[ Union[ + AirGapInPlace, Aspirate, AspirateInPlace, Comment, @@ -337,8 +404,11 @@ LoadLabware, ReloadLabware, LoadLiquid, + LoadLiquidClass, LoadModule, LoadPipette, + LoadLidStack, + LoadLid, MoveLabware, MoveRelative, MoveToCoordinates, @@ -355,8 +425,12 @@ SetStatusBar, VerifyTipPresence, GetTipPresence, + GetNextTip, LiquidProbe, TryLiquidProbe, + EvotipSealPipette, + EvotipDispense, + EvotipUnsealPipette, heater_shaker.WaitForTemperature, heater_shaker.SetTargetTemperature, heater_shaker.DeactivateHeater, @@ -393,11 +467,17 @@ unsafe.UnsafeEngageAxes, unsafe.UnsafeUngripLabware, unsafe.UnsafePlaceLabware, + robot.MoveTo, + robot.MoveAxesRelative, + robot.MoveAxesTo, + robot.openGripperJaw, + robot.closeGripperJaw, ], Field(discriminator="commandType"), ] CommandParams = Union[ + AirGapInPlaceParams, AspirateParams, AspirateInPlaceParams, CommentParams, @@ -413,8 +493,11 @@ HomeParams, RetractAxisParams, LoadLabwareParams, + LoadLidStackParams, + LoadLidParams, ReloadLabwareParams, LoadLiquidParams, + LoadLiquidClassParams, LoadModuleParams, LoadPipetteParams, MoveLabwareParams, @@ -433,8 +516,12 @@ SetStatusBarParams, VerifyTipPresenceParams, GetTipPresenceParams, + GetNextTipParams, LiquidProbeParams, TryLiquidProbeParams, + EvotipSealPipetteParams, + EvotipDispenseParams, + EvotipUnsealPipetteParams, heater_shaker.WaitForTemperatureParams, heater_shaker.SetTargetTemperatureParams, heater_shaker.DeactivateHeaterParams, @@ -471,9 +558,15 @@ unsafe.UnsafeEngageAxesParams, unsafe.UnsafeUngripLabwareParams, unsafe.UnsafePlaceLabwareParams, + robot.MoveAxesRelativeParams, + robot.MoveAxesToParams, + robot.MoveToParams, + robot.openGripperJawParams, + robot.closeGripperJawParams, ] CommandType = Union[ + AirGapInPlaceCommandType, AspirateCommandType, AspirateInPlaceCommandType, CommentCommandType, @@ -491,8 +584,11 @@ LoadLabwareCommandType, ReloadLabwareCommandType, LoadLiquidCommandType, + LoadLiquidClassCommandType, LoadModuleCommandType, LoadPipetteCommandType, + LoadLidStackCommandType, + LoadLidCommandType, MoveLabwareCommandType, MoveRelativeCommandType, MoveToCoordinatesCommandType, @@ -509,8 +605,12 @@ SetStatusBarCommandType, VerifyTipPresenceCommandType, GetTipPresenceCommandType, + GetNextTipCommandType, LiquidProbeCommandType, TryLiquidProbeCommandType, + EvotipSealPipetteCommandType, + EvotipDispenseCommandType, + EvotipUnsealPipetteCommandType, heater_shaker.WaitForTemperatureCommandType, heater_shaker.SetTargetTemperatureCommandType, heater_shaker.DeactivateHeaterCommandType, @@ -547,10 +647,16 @@ unsafe.UnsafeEngageAxesCommandType, unsafe.UnsafeUngripLabwareCommandType, unsafe.UnsafePlaceLabwareCommandType, + robot.MoveAxesRelativeCommandType, + robot.MoveAxesToCommandType, + robot.MoveToCommandType, + robot.openGripperJawCommandType, + robot.closeGripperJawCommandType, ] CommandCreate = Annotated[ Union[ + AirGapInPlaceCreate, AspirateCreate, AspirateInPlaceCreate, CommentCreate, @@ -568,8 +674,11 @@ LoadLabwareCreate, ReloadLabwareCreate, LoadLiquidCreate, + LoadLiquidClassCreate, LoadModuleCreate, LoadPipetteCreate, + LoadLidStackCreate, + LoadLidCreate, MoveLabwareCreate, MoveRelativeCreate, MoveToCoordinatesCreate, @@ -586,8 +695,12 @@ SetStatusBarCreate, VerifyTipPresenceCreate, GetTipPresenceCreate, + GetNextTipCreate, LiquidProbeCreate, TryLiquidProbeCreate, + EvotipSealPipetteCreate, + EvotipDispenseCreate, + EvotipUnsealPipetteCreate, heater_shaker.WaitForTemperatureCreate, heater_shaker.SetTargetTemperatureCreate, heater_shaker.DeactivateHeaterCreate, @@ -624,11 +737,24 @@ unsafe.UnsafeEngageAxesCreate, unsafe.UnsafeUngripLabwareCreate, unsafe.UnsafePlaceLabwareCreate, + robot.MoveAxesRelativeCreate, + robot.MoveAxesToCreate, + robot.MoveToCreate, + robot.openGripperJawCreate, + robot.closeGripperJawCreate, ], Field(discriminator="commandType"), ] +# Each time a TypeAdapter is instantiated, it will construct a new validator and +# serializer. To improve performance, TypeAdapters are instantiated once. +# See https://docs.pydantic.dev/latest/concepts/performance/#typeadapter-instantiated-once +CommandCreateAdapter: TypeAdapter[CommandCreate] = TypeAdapter(CommandCreate) + +CommandAdapter: TypeAdapter[Command] = TypeAdapter(Command) + CommandResult = Union[ + AirGapInPlaceResult, AspirateResult, AspirateInPlaceResult, CommentResult, @@ -646,8 +772,11 @@ LoadLabwareResult, ReloadLabwareResult, LoadLiquidResult, + LoadLiquidClassResult, LoadModuleResult, LoadPipetteResult, + LoadLidStackResult, + LoadLidResult, MoveLabwareResult, MoveRelativeResult, MoveToCoordinatesResult, @@ -664,8 +793,12 @@ SetStatusBarResult, VerifyTipPresenceResult, GetTipPresenceResult, + GetNextTipResult, LiquidProbeResult, TryLiquidProbeResult, + EvotipSealPipetteResult, + EvotipDispenseResult, + EvotipUnsealPipetteResult, heater_shaker.WaitForTemperatureResult, heater_shaker.SetTargetTemperatureResult, heater_shaker.DeactivateHeaterResult, @@ -702,6 +835,11 @@ unsafe.UnsafeEngageAxesResult, unsafe.UnsafeUngripLabwareResult, unsafe.UnsafePlaceLabwareResult, + robot.MoveAxesRelativeResult, + robot.MoveAxesToResult, + robot.MoveToResult, + robot.openGripperJawResult, + robot.closeGripperJawResult, ] @@ -712,6 +850,7 @@ DefinedErrorData[OverpressureError], DefinedErrorData[LiquidNotFoundError], DefinedErrorData[GripperMovementError], + DefinedErrorData[StallOrCollisionError], ] diff --git a/api/src/opentrons/protocol_engine/commands/comment.py b/api/src/opentrons/protocol_engine/commands/comment.py index 5cd0b0c3113..f14a9a9992c 100644 --- a/api/src/opentrons/protocol_engine/commands/comment.py +++ b/api/src/opentrons/protocol_engine/commands/comment.py @@ -43,7 +43,7 @@ class Comment(BaseCommand[CommentParams, CommentResult, ErrorOccurrence]): commandType: CommentCommandType = "comment" params: CommentParams - result: Optional[CommentResult] + result: Optional[CommentResult] = None _ImplementationCls: Type[CommentImplementation] = CommentImplementation diff --git a/api/src/opentrons/protocol_engine/commands/configure_for_volume.py b/api/src/opentrons/protocol_engine/commands/configure_for_volume.py index 1c8aa21f491..50e1e7546bc 100644 --- a/api/src/opentrons/protocol_engine/commands/configure_for_volume.py +++ b/api/src/opentrons/protocol_engine/commands/configure_for_volume.py @@ -1,7 +1,9 @@ """Configure for volume command request, result, and implementation models.""" from __future__ import annotations +from typing import TYPE_CHECKING, Optional, Type, Any + from pydantic import BaseModel, Field -from typing import TYPE_CHECKING, Optional, Type +from pydantic.json_schema import SkipJsonSchema from typing_extensions import Literal from .pipetting_common import PipetteIdMixin @@ -16,6 +18,10 @@ ConfigureForVolumeCommandType = Literal["configureForVolume"] +def _remove_default(s: dict[str, Any]) -> None: + s.pop("default", None) + + class ConfigureForVolumeParams(PipetteIdMixin): """Parameters required to configure volume for a specific pipette.""" @@ -25,12 +31,13 @@ class ConfigureForVolumeParams(PipetteIdMixin): "than a pipette-specific maximum volume.", ge=0, ) - tipOverlapNotAfterVersion: Optional[str] = Field( + tipOverlapNotAfterVersion: str | SkipJsonSchema[None] = Field( None, description="A version of tip overlap data to not exceed. The highest-versioned " "tip overlap data that does not exceed this version will be used. Versions are " "expressed as vN where N is an integer, counting up from v0. If None, the current " "highest version will be used.", + json_schema_extra=_remove_default, ) @@ -81,7 +88,7 @@ class ConfigureForVolume( commandType: ConfigureForVolumeCommandType = "configureForVolume" params: ConfigureForVolumeParams - result: Optional[ConfigureForVolumeResult] + result: Optional[ConfigureForVolumeResult] = None _ImplementationCls: Type[ ConfigureForVolumeImplementation diff --git a/api/src/opentrons/protocol_engine/commands/configure_nozzle_layout.py b/api/src/opentrons/protocol_engine/commands/configure_nozzle_layout.py index f78839773ec..072307a0609 100644 --- a/api/src/opentrons/protocol_engine/commands/configure_nozzle_layout.py +++ b/api/src/opentrons/protocol_engine/commands/configure_nozzle_layout.py @@ -61,9 +61,11 @@ async def execute( self, params: ConfigureNozzleLayoutParams ) -> SuccessData[ConfigureNozzleLayoutResult]: """Check that requested pipette can support the requested nozzle layout.""" - primary_nozzle = params.configurationParams.dict().get("primaryNozzle") - front_right_nozzle = params.configurationParams.dict().get("frontRightNozzle") - back_left_nozzle = params.configurationParams.dict().get("backLeftNozzle") + primary_nozzle = params.configurationParams.model_dump().get("primaryNozzle") + front_right_nozzle = params.configurationParams.model_dump().get( + "frontRightNozzle" + ) + back_left_nozzle = params.configurationParams.model_dump().get("backLeftNozzle") nozzle_params = await self._tip_handler.available_for_nozzle_layout( pipette_id=params.pipetteId, style=params.configurationParams.style, @@ -97,7 +99,7 @@ class ConfigureNozzleLayout( commandType: ConfigureNozzleLayoutCommandType = "configureNozzleLayout" params: ConfigureNozzleLayoutParams - result: Optional[ConfigureNozzleLayoutResult] + result: Optional[ConfigureNozzleLayoutResult] = None _ImplementationCls: Type[ ConfigureNozzleLayoutImplementation diff --git a/api/src/opentrons/protocol_engine/commands/custom.py b/api/src/opentrons/protocol_engine/commands/custom.py index 1bdf07084be..b15b5cdb8d3 100644 --- a/api/src/opentrons/protocol_engine/commands/custom.py +++ b/api/src/opentrons/protocol_engine/commands/custom.py @@ -10,7 +10,7 @@ If you are implementing a custom command, you should probably put your own disambiguation identifier in the payload. """ -from pydantic import BaseModel, Extra +from pydantic import ConfigDict, BaseModel, SerializeAsAny from typing import Optional, Type from typing_extensions import Literal @@ -24,19 +24,13 @@ class CustomParams(BaseModel): """Payload used by a custom command.""" - class Config: - """Allow arbitrary fields.""" - - extra = Extra.allow + model_config = ConfigDict(extra="allow") class CustomResult(BaseModel): """Result data from a custom command.""" - class Config: - """Allow arbitrary fields.""" - - extra = Extra.allow + model_config = ConfigDict(extra="allow") class CustomImplementation( @@ -50,7 +44,7 @@ class CustomImplementation( async def execute(self, params: CustomParams) -> SuccessData[CustomResult]: """A custom command does nothing when executed directly.""" return SuccessData( - public=CustomResult.construct(), + public=CustomResult.model_construct(), ) @@ -58,8 +52,8 @@ class Custom(BaseCommand[CustomParams, CustomResult, ErrorOccurrence]): """Custom command model.""" commandType: CustomCommandType = "custom" - params: CustomParams - result: Optional[CustomResult] + params: SerializeAsAny[CustomParams] + result: Optional[CustomResult] = None _ImplementationCls: Type[CustomImplementation] = CustomImplementation diff --git a/api/src/opentrons/protocol_engine/commands/dispense.py b/api/src/opentrons/protocol_engine/commands/dispense.py index a7fee20c762..8ad2365ccb5 100644 --- a/api/src/opentrons/protocol_engine/commands/dispense.py +++ b/api/src/opentrons/protocol_engine/commands/dispense.py @@ -1,22 +1,27 @@ """Dispense command request, result, and implementation models.""" + from __future__ import annotations -from typing import TYPE_CHECKING, Optional, Type, Union +from typing import TYPE_CHECKING, Optional, Type, Union, Any from typing_extensions import Literal -from opentrons_shared_data.errors.exceptions import PipetteOverpressureError from pydantic import Field +from pydantic.json_schema import SkipJsonSchema -from ..types import DeckPoint from ..state.update_types import StateUpdate, CLEAR from .pipetting_common import ( PipetteIdMixin, DispenseVolumeMixin, FlowRateMixin, - LiquidHandlingWellLocationMixin, BaseLiquidHandlingResult, - DestinationPositionResult, OverpressureError, + dispense_in_place, +) +from .movement_common import ( + LiquidHandlingWellLocationMixin, + DestinationPositionResult, + StallOrCollisionError, + move_to_well, ) from .command import ( AbstractCommandImpl, @@ -25,7 +30,6 @@ DefinedErrorData, SuccessData, ) -from ..errors.error_occurrence import ErrorOccurrence if TYPE_CHECKING: from ..execution import MovementHandler, PipettingHandler @@ -36,14 +40,19 @@ DispenseCommandType = Literal["dispense"] +def _remove_default(s: dict[str, Any]) -> None: + s.pop("default", None) + + class DispenseParams( PipetteIdMixin, DispenseVolumeMixin, FlowRateMixin, LiquidHandlingWellLocationMixin ): """Payload required to dispense to a specific well.""" - pushOut: Optional[float] = Field( + pushOut: float | SkipJsonSchema[None] = Field( None, description="push the plunger a small amount farther than necessary for accurate low-volume dispensing", + json_schema_extra=_remove_default, ) @@ -55,7 +64,7 @@ class DispenseResult(BaseLiquidHandlingResult, DestinationPositionResult): _ExecuteReturn = Union[ SuccessData[DispenseResult], - DefinedErrorData[OverpressureError], + DefinedErrorData[OverpressureError] | DefinedErrorData[StallOrCollisionError], ] @@ -77,7 +86,6 @@ def __init__( async def execute(self, params: DispenseParams) -> _ExecuteReturn: """Move to and dispense to the requested well.""" - state_update = StateUpdate() well_location = params.wellLocation labware_id = params.labwareId well_name = params.wellName @@ -85,66 +93,92 @@ async def execute(self, params: DispenseParams) -> _ExecuteReturn: # TODO(pbm, 10-15-24): call self._state_view.geometry.validate_dispense_volume_into_well() - position = await self._movement.move_to_well( + move_result = await move_to_well( + movement=self._movement, + model_utils=self._model_utils, pipette_id=params.pipetteId, labware_id=labware_id, well_name=well_name, well_location=well_location, ) - deck_point = DeckPoint.construct(x=position.x, y=position.y, z=position.z) - state_update.set_pipette_location( + if isinstance(move_result, DefinedErrorData): + return move_result + dispense_result = await dispense_in_place( pipette_id=params.pipetteId, - new_labware_id=labware_id, - new_well_name=well_name, - new_deck_point=deck_point, + volume=volume, + flow_rate=params.flowRate, + push_out=params.pushOut, + location_if_error={ + "retryLocation": ( + move_result.public.position.x, + move_result.public.position.y, + move_result.public.position.z, + ) + }, + pipetting=self._pipetting, + model_utils=self._model_utils, ) - try: - volume = await self._pipetting.dispense_in_place( - pipette_id=params.pipetteId, - volume=volume, - flow_rate=params.flowRate, - push_out=params.pushOut, - ) - except PipetteOverpressureError as e: - state_update.set_liquid_operated( - labware_id=labware_id, - well_name=well_name, - volume_added=CLEAR, - ) + if isinstance(dispense_result, DefinedErrorData): return DefinedErrorData( - public=OverpressureError( - id=self._model_utils.generate_id(), - createdAt=self._model_utils.get_timestamp(), - wrappedErrors=[ - ErrorOccurrence.from_failed( - id=self._model_utils.generate_id(), - createdAt=self._model_utils.get_timestamp(), - error=e, - ) - ], - errorInfo={"retryLocation": (position.x, position.y, position.z)}, + public=dispense_result.public, + state_update=( + StateUpdate.reduce( + move_result.state_update, dispense_result.state_update + ).set_liquid_operated( + labware_id=labware_id, + well_names=self._state_view.geometry.get_wells_covered_by_pipette_with_active_well( + labware_id, well_name, params.pipetteId + ), + volume_added=CLEAR, + ) + ), + state_update_if_false_positive=StateUpdate.reduce( + move_result.state_update, + dispense_result.state_update_if_false_positive, ), - state_update=state_update, ) else: - state_update.set_liquid_operated( - labware_id=labware_id, - well_name=well_name, - volume_added=volume, + volume_added = ( + self._state_view.pipettes.get_liquid_dispensed_by_ejecting_volume( + pipette_id=params.pipetteId, volume=dispense_result.public.volume + ) ) + if volume_added is not None: + volume_added *= self._state_view.geometry.get_nozzles_per_well( + labware_id, well_name, params.pipetteId + ) return SuccessData( - public=DispenseResult(volume=volume, position=deck_point), - state_update=state_update, + public=DispenseResult( + volume=dispense_result.public.volume, + position=move_result.public.position, + ), + state_update=( + StateUpdate.reduce( + move_result.state_update, dispense_result.state_update + ).set_liquid_operated( + labware_id=labware_id, + well_names=self._state_view.geometry.get_wells_covered_by_pipette_with_active_well( + labware_id, well_name, params.pipetteId + ), + volume_added=volume_added + if volume_added is not None + else CLEAR, + ) + ), ) -class Dispense(BaseCommand[DispenseParams, DispenseResult, OverpressureError]): +class Dispense( + BaseCommand[ + DispenseParams, DispenseResult, OverpressureError | StallOrCollisionError + ] +): """Dispense command model.""" commandType: DispenseCommandType = "dispense" params: DispenseParams - result: Optional[DispenseResult] + result: Optional[DispenseResult] = None _ImplementationCls: Type[DispenseImplementation] = DispenseImplementation diff --git a/api/src/opentrons/protocol_engine/commands/dispense_in_place.py b/api/src/opentrons/protocol_engine/commands/dispense_in_place.py index 7935f7f72e9..117aa011a84 100644 --- a/api/src/opentrons/protocol_engine/commands/dispense_in_place.py +++ b/api/src/opentrons/protocol_engine/commands/dispense_in_place.py @@ -1,10 +1,10 @@ """Dispense-in-place command request, result, and implementation models.""" + from __future__ import annotations -from typing import TYPE_CHECKING, Optional, Type, Union +from typing import TYPE_CHECKING, Optional, Type, Union, Any from typing_extensions import Literal from pydantic import Field - -from opentrons_shared_data.errors.exceptions import PipetteOverpressureError +from pydantic.json_schema import SkipJsonSchema from .pipetting_common import ( PipetteIdMixin, @@ -12,6 +12,7 @@ FlowRateMixin, BaseLiquidHandlingResult, OverpressureError, + dispense_in_place, ) from .command import ( AbstractCommandImpl, @@ -20,8 +21,7 @@ SuccessData, DefinedErrorData, ) -from ..errors.error_occurrence import ErrorOccurrence -from ..state.update_types import StateUpdate, CLEAR +from ..state.update_types import CLEAR from ..types import CurrentWell if TYPE_CHECKING: @@ -33,12 +33,17 @@ DispenseInPlaceCommandType = Literal["dispenseInPlace"] +def _remove_default(s: dict[str, Any]) -> None: + s.pop("default", None) + + class DispenseInPlaceParams(PipetteIdMixin, DispenseVolumeMixin, FlowRateMixin): """Payload required to dispense in place.""" - pushOut: Optional[float] = Field( + pushOut: float | SkipJsonSchema[None] = Field( None, description="push the plunger a small amount farther than necessary for accurate low-volume dispensing", + json_schema_extra=_remove_default, ) @@ -74,63 +79,78 @@ def __init__( async def execute(self, params: DispenseInPlaceParams) -> _ExecuteReturn: """Dispense without moving the pipette.""" - state_update = StateUpdate() current_location = self._state_view.pipettes.get_current_location() current_position = await self._gantry_mover.get_position(params.pipetteId) - try: - volume = await self._pipetting.dispense_in_place( - pipette_id=params.pipetteId, - volume=params.volume, - flow_rate=params.flowRate, - push_out=params.pushOut, - ) - except PipetteOverpressureError as e: + result = await dispense_in_place( + pipette_id=params.pipetteId, + volume=params.volume, + flow_rate=params.flowRate, + push_out=params.pushOut, + location_if_error={ + "retryLocation": ( + current_position.x, + current_position.y, + current_position.z, + ) + }, + pipetting=self._pipetting, + model_utils=self._model_utils, + ) + if isinstance(result, DefinedErrorData): if ( isinstance(current_location, CurrentWell) and current_location.pipette_id == params.pipetteId ): - state_update.set_liquid_operated( - labware_id=current_location.labware_id, - well_name=current_location.well_name, - volume_added=CLEAR, - ) - return DefinedErrorData( - public=OverpressureError( - id=self._model_utils.generate_id(), - createdAt=self._model_utils.get_timestamp(), - wrappedErrors=[ - ErrorOccurrence.from_failed( - id=self._model_utils.generate_id(), - createdAt=self._model_utils.get_timestamp(), - error=e, - ) - ], - errorInfo=( - { - "retryLocation": ( - current_position.x, - current_position.y, - current_position.z, - ) - } + return DefinedErrorData( + public=result.public, + state_update=result.state_update.set_liquid_operated( + labware_id=current_location.labware_id, + well_names=self._state_view.geometry.get_wells_covered_by_pipette_with_active_well( + current_location.labware_id, + current_location.well_name, + params.pipetteId, + ), + volume_added=CLEAR, ), - ), - state_update=state_update, - ) + state_update_if_false_positive=result.state_update_if_false_positive, + ) + else: + return result else: if ( isinstance(current_location, CurrentWell) and current_location.pipette_id == params.pipetteId ): - state_update.set_liquid_operated( - labware_id=current_location.labware_id, - well_name=current_location.well_name, - volume_added=volume, + volume_added = ( + self._state_view.pipettes.get_liquid_dispensed_by_ejecting_volume( + pipette_id=params.pipetteId, volume=result.public.volume + ) + ) + if volume_added is not None: + volume_added *= self._state_view.geometry.get_nozzles_per_well( + current_location.labware_id, + current_location.well_name, + params.pipetteId, + ) + return SuccessData( + public=DispenseInPlaceResult(volume=result.public.volume), + state_update=result.state_update.set_liquid_operated( + labware_id=current_location.labware_id, + well_names=self._state_view.geometry.get_wells_covered_by_pipette_with_active_well( + current_location.labware_id, + current_location.well_name, + params.pipetteId, + ), + volume_added=volume_added + if volume_added is not None + else CLEAR, + ), + ) + else: + return SuccessData( + public=DispenseInPlaceResult(volume=result.public.volume), + state_update=result.state_update, ) - return SuccessData( - public=DispenseInPlaceResult(volume=volume), - state_update=state_update, - ) class DispenseInPlace( @@ -140,7 +160,7 @@ class DispenseInPlace( commandType: DispenseInPlaceCommandType = "dispenseInPlace" params: DispenseInPlaceParams - result: Optional[DispenseInPlaceResult] + result: Optional[DispenseInPlaceResult] = None _ImplementationCls: Type[ DispenseInPlaceImplementation diff --git a/api/src/opentrons/protocol_engine/commands/drop_tip.py b/api/src/opentrons/protocol_engine/commands/drop_tip.py index 36b7a7c22c2..2c393064eb6 100644 --- a/api/src/opentrons/protocol_engine/commands/drop_tip.py +++ b/api/src/opentrons/protocol_engine/commands/drop_tip.py @@ -1,20 +1,27 @@ """Drop tip command request, result, and implementation models.""" + from __future__ import annotations +from typing import TYPE_CHECKING, Optional, Type, Any from pydantic import Field -from typing import TYPE_CHECKING, Optional, Type +from pydantic.json_schema import SkipJsonSchema + from typing_extensions import Literal from opentrons.protocol_engine.errors.exceptions import TipAttachedError from opentrons.protocol_engine.resources.model_utils import ModelUtils -from ..state import update_types -from ..types import DropTipWellLocation, DeckPoint +from ..state.update_types import StateUpdate +from ..types import DropTipWellLocation from .pipetting_common import ( PipetteIdMixin, - DestinationPositionResult, TipPhysicallyAttachedError, ) +from .movement_common import ( + DestinationPositionResult, + move_to_well, + StallOrCollisionError, +) from .command import ( AbstractCommandImpl, BaseCommand, @@ -32,6 +39,10 @@ DropTipCommandType = Literal["dropTip"] +def _remove_default(s: dict[str, Any]) -> None: + s.pop("default", None) + + class DropTipParams(PipetteIdMixin): """Payload required to drop a tip in a specific well.""" @@ -41,15 +52,16 @@ class DropTipParams(PipetteIdMixin): default_factory=DropTipWellLocation, description="Relative well location at which to drop the tip.", ) - homeAfter: Optional[bool] = Field( + homeAfter: bool | SkipJsonSchema[None] = Field( None, description=( "Whether to home this pipette's plunger after dropping the tip." " You should normally leave this unspecified to let the robot choose" " a safe default depending on its hardware." ), + json_schema_extra=_remove_default, ) - alternateDropLocation: Optional[bool] = Field( + alternateDropLocation: bool | SkipJsonSchema[None] = Field( False, description=( "Whether to alternate location where tip is dropped within the labware." @@ -58,6 +70,7 @@ class DropTipParams(PipetteIdMixin): " labware well." " If False, the tip will be dropped at the top center of the well." ), + json_schema_extra=_remove_default, ) @@ -68,7 +81,9 @@ class DropTipResult(DestinationPositionResult): _ExecuteReturn = ( - SuccessData[DropTipResult] | DefinedErrorData[TipPhysicallyAttachedError] + SuccessData[DropTipResult] + | DefinedErrorData[TipPhysicallyAttachedError] + | DefinedErrorData[StallOrCollisionError] ) @@ -95,8 +110,6 @@ async def execute(self, params: DropTipParams) -> _ExecuteReturn: well_name = params.wellName home_after = params.homeAfter - state_update = update_types.StateUpdate() - if params.alternateDropLocation: well_location = self._state_view.geometry.get_next_tip_drop_location( labware_id=labware_id, @@ -116,19 +129,16 @@ async def execute(self, params: DropTipParams) -> _ExecuteReturn: partially_configured=is_partially_configured, ) - position = await self._movement_handler.move_to_well( + move_result = await move_to_well( + movement=self._movement_handler, + model_utils=self._model_utils, pipette_id=pipette_id, labware_id=labware_id, well_name=well_name, well_location=tip_drop_location, ) - deck_point = DeckPoint.construct(x=position.x, y=position.y, z=position.z) - state_update.set_pipette_location( - pipette_id=pipette_id, - new_labware_id=labware_id, - new_well_name=well_name, - new_deck_point=deck_point, - ) + if isinstance(move_result, DefinedErrorData): + return move_result try: await self._tip_handler.drop_tip( @@ -145,33 +155,44 @@ async def execute(self, params: DropTipParams) -> _ExecuteReturn: error=exception, ) ], - errorInfo={"retryLocation": position}, - ) - state_update_if_false_positive = update_types.StateUpdate() - state_update_if_false_positive.update_pipette_tip_state( - pipette_id=params.pipetteId, tip_geometry=None + errorInfo={ + "retryLocation": ( + move_result.public.position.x, + move_result.public.position.y, + move_result.public.position.z, + ) + }, ) return DefinedErrorData( public=error, - state_update=state_update, - state_update_if_false_positive=state_update_if_false_positive, + state_update=StateUpdate.reduce( + StateUpdate(), move_result.state_update + ).set_fluid_unknown(pipette_id=pipette_id), + state_update_if_false_positive=move_result.state_update.update_pipette_tip_state( + pipette_id=params.pipetteId, tip_geometry=None + ), ) else: - state_update.update_pipette_tip_state( - pipette_id=params.pipetteId, tip_geometry=None - ) return SuccessData( - public=DropTipResult(position=deck_point), - state_update=state_update, + public=DropTipResult(position=move_result.public.position), + state_update=move_result.state_update.set_fluid_unknown( + pipette_id=pipette_id + ).update_pipette_tip_state( + pipette_id=params.pipetteId, tip_geometry=None + ), ) -class DropTip(BaseCommand[DropTipParams, DropTipResult, TipPhysicallyAttachedError]): +class DropTip( + BaseCommand[ + DropTipParams, DropTipResult, TipPhysicallyAttachedError | StallOrCollisionError + ] +): """Drop tip command model.""" commandType: DropTipCommandType = "dropTip" params: DropTipParams - result: Optional[DropTipResult] + result: Optional[DropTipResult] = None _ImplementationCls: Type[DropTipImplementation] = DropTipImplementation diff --git a/api/src/opentrons/protocol_engine/commands/drop_tip_in_place.py b/api/src/opentrons/protocol_engine/commands/drop_tip_in_place.py index b0390632d6a..60eb33625d6 100644 --- a/api/src/opentrons/protocol_engine/commands/drop_tip_in_place.py +++ b/api/src/opentrons/protocol_engine/commands/drop_tip_in_place.py @@ -1,9 +1,17 @@ """Drop tip in place command request, result, and implementation models.""" from __future__ import annotations + +from typing import TYPE_CHECKING, Optional, Type, Any, Union + from pydantic import Field, BaseModel -from typing import TYPE_CHECKING, Optional, Type +from pydantic.json_schema import SkipJsonSchema from typing_extensions import Literal +from opentrons_shared_data.errors.exceptions import ( + PipetteOverpressureError, + StallOrCollisionDetectedError, +) + from .command import ( AbstractCommandImpl, BaseCommand, @@ -11,7 +19,12 @@ DefinedErrorData, SuccessData, ) -from .pipetting_common import PipetteIdMixin, TipPhysicallyAttachedError +from .movement_common import StallOrCollisionError +from .pipetting_common import ( + PipetteIdMixin, + TipPhysicallyAttachedError, + OverpressureError, +) from ..errors.exceptions import TipAttachedError from ..errors.error_occurrence import ErrorOccurrence from ..resources.model_utils import ModelUtils @@ -24,16 +37,21 @@ DropTipInPlaceCommandType = Literal["dropTipInPlace"] +def _remove_default(s: dict[str, Any]) -> None: + s.pop("default", None) + + class DropTipInPlaceParams(PipetteIdMixin): """Payload required to drop a tip in place.""" - homeAfter: Optional[bool] = Field( + homeAfter: bool | SkipJsonSchema[None] = Field( None, description=( "Whether to home this pipette's plunger after dropping the tip." " You should normally leave this unspecified to let the robot choose" " a safe default depending on its hardware." ), + json_schema_extra=_remove_default, ) @@ -43,9 +61,12 @@ class DropTipInPlaceResult(BaseModel): pass -_ExecuteReturn = ( - SuccessData[DropTipInPlaceResult] | DefinedErrorData[TipPhysicallyAttachedError] -) +_ExecuteReturn = Union[ + SuccessData[DropTipInPlaceResult] + | DefinedErrorData[TipPhysicallyAttachedError] + | DefinedErrorData[OverpressureError] + | DefinedErrorData[StallOrCollisionError] +] class DropTipInPlaceImplementation( @@ -79,6 +100,7 @@ async def execute(self, params: DropTipInPlaceParams) -> _ExecuteReturn: state_update_if_false_positive.update_pipette_tip_state( pipette_id=params.pipetteId, tip_geometry=None ) + state_update.set_fluid_unknown(pipette_id=params.pipetteId) error = TipPhysicallyAttachedError( id=self._model_utils.generate_id(), createdAt=self._model_utils.get_timestamp(), @@ -96,7 +118,52 @@ async def execute(self, params: DropTipInPlaceParams) -> _ExecuteReturn: state_update=state_update, state_update_if_false_positive=state_update_if_false_positive, ) + except PipetteOverpressureError as exception: + state_update_if_false_positive = update_types.StateUpdate() + state_update_if_false_positive.update_pipette_tip_state( + pipette_id=params.pipetteId, tip_geometry=None + ) + state_update.set_fluid_unknown(pipette_id=params.pipetteId) + return DefinedErrorData( + public=OverpressureError( + id=self._model_utils.generate_id(), + createdAt=self._model_utils.get_timestamp(), + wrappedErrors=[ + ErrorOccurrence.from_failed( + id=self._model_utils.generate_id(), + createdAt=self._model_utils.get_timestamp(), + error=exception, + ) + ], + errorInfo={"retryLocation": retry_location}, + ), + state_update=state_update, + state_update_if_false_positive=state_update_if_false_positive, + ) + except StallOrCollisionDetectedError as exception: + state_update_if_false_positive = update_types.StateUpdate() + state_update_if_false_positive.update_pipette_tip_state( + pipette_id=params.pipetteId, tip_geometry=None + ) + state_update.set_fluid_unknown(pipette_id=params.pipetteId) + return DefinedErrorData( + public=StallOrCollisionError( + id=self._model_utils.generate_id(), + createdAt=self._model_utils.get_timestamp(), + wrappedErrors=[ + ErrorOccurrence.from_failed( + id=self._model_utils.generate_id(), + createdAt=self._model_utils.get_timestamp(), + error=exception, + ) + ], + errorInfo={"retryLocation": retry_location}, + ), + state_update=state_update, + state_update_if_false_positive=state_update_if_false_positive, + ) else: + state_update.set_fluid_unknown(pipette_id=params.pipetteId) state_update.update_pipette_tip_state( pipette_id=params.pipetteId, tip_geometry=None ) @@ -104,13 +171,17 @@ async def execute(self, params: DropTipInPlaceParams) -> _ExecuteReturn: class DropTipInPlace( - BaseCommand[DropTipInPlaceParams, DropTipInPlaceResult, TipPhysicallyAttachedError] + BaseCommand[ + DropTipInPlaceParams, + DropTipInPlaceResult, + TipPhysicallyAttachedError | OverpressureError | StallOrCollisionError, + ] ): """Drop tip in place command model.""" commandType: DropTipInPlaceCommandType = "dropTipInPlace" params: DropTipInPlaceParams - result: Optional[DropTipInPlaceResult] + result: Optional[DropTipInPlaceResult] = None _ImplementationCls: Type[ DropTipInPlaceImplementation diff --git a/api/src/opentrons/protocol_engine/commands/evotip_dispense.py b/api/src/opentrons/protocol_engine/commands/evotip_dispense.py new file mode 100644 index 00000000000..e0053262295 --- /dev/null +++ b/api/src/opentrons/protocol_engine/commands/evotip_dispense.py @@ -0,0 +1,156 @@ +"""Evotip Dispense-in-place command request, result, and implementation models.""" + +from __future__ import annotations +from typing import TYPE_CHECKING, Optional, Type, Union +from typing_extensions import Literal + +from opentrons.protocol_engine.errors import UnsupportedLabwareForActionError +from .pipetting_common import ( + PipetteIdMixin, + FlowRateMixin, + DispenseVolumeMixin, + BaseLiquidHandlingResult, + dispense_in_place, +) +from .movement_common import ( + LiquidHandlingWellLocationMixin, + StallOrCollisionError, + move_to_well, +) + +from .command import ( + AbstractCommandImpl, + BaseCommand, + BaseCommandCreate, + SuccessData, + DefinedErrorData, +) +from ..state.update_types import StateUpdate +from ..resources import labware_validation +from ..errors import ProtocolEngineError + +if TYPE_CHECKING: + from ..execution import PipettingHandler, GantryMover, MovementHandler + from ..resources import ModelUtils + from ..state.state import StateView + + +EvotipDispenseCommandType = Literal["evotipDispense"] + + +class EvotipDispenseParams( + PipetteIdMixin, DispenseVolumeMixin, FlowRateMixin, LiquidHandlingWellLocationMixin +): + """Payload required to dispense in place.""" + + pass + + +class EvotipDispenseResult(BaseLiquidHandlingResult): + """Result data from the execution of a DispenseInPlace command.""" + + pass + + +_ExecuteReturn = Union[ + SuccessData[EvotipDispenseResult], + DefinedErrorData[StallOrCollisionError], +] + + +class EvotipDispenseImplementation( + AbstractCommandImpl[EvotipDispenseParams, _ExecuteReturn] +): + """DispenseInPlace command implementation.""" + + def __init__( + self, + pipetting: PipettingHandler, + state_view: StateView, + gantry_mover: GantryMover, + model_utils: ModelUtils, + movement: MovementHandler, + **kwargs: object, + ) -> None: + self._pipetting = pipetting + self._state_view = state_view + self._gantry_mover = gantry_mover + self._model_utils = model_utils + self._movement = movement + + async def execute(self, params: EvotipDispenseParams) -> _ExecuteReturn: + """Move to and dispense to the requested well.""" + well_location = params.wellLocation + labware_id = params.labwareId + well_name = params.wellName + + labware_definition = self._state_view.labware.get_definition(params.labwareId) + if not labware_validation.is_evotips(labware_definition.parameters.loadName): + raise UnsupportedLabwareForActionError( + f"Cannot use command: `EvotipDispense` with labware: {labware_definition.parameters.loadName}" + ) + move_result = await move_to_well( + movement=self._movement, + model_utils=self._model_utils, + pipette_id=params.pipetteId, + labware_id=labware_id, + well_name=well_name, + well_location=well_location, + ) + if isinstance(move_result, DefinedErrorData): + return move_result + + current_position = await self._gantry_mover.get_position(params.pipetteId) + result = await dispense_in_place( + pipette_id=params.pipetteId, + volume=params.volume, + flow_rate=params.flowRate, + push_out=None, + location_if_error={ + "retryLocation": ( + current_position.x, + current_position.y, + current_position.z, + ) + }, + pipetting=self._pipetting, + model_utils=self._model_utils, + ) + if isinstance(result, DefinedErrorData): + # TODO (chb, 2025-01-29): Remove this and the OverpressureError returns once disabled for this function + raise ProtocolEngineError( + message="Overpressure Error during Resin Tip Dispense Command." + ) + return SuccessData( + public=EvotipDispenseResult(volume=result.public.volume), + state_update=StateUpdate.reduce( + move_result.state_update, result.state_update + ), + ) + + +class EvotipDispense( + BaseCommand[ + EvotipDispenseParams, + EvotipDispenseResult, + StallOrCollisionError, + ] +): + """DispenseInPlace command model.""" + + commandType: EvotipDispenseCommandType = "evotipDispense" + params: EvotipDispenseParams + result: Optional[EvotipDispenseResult] = None + + _ImplementationCls: Type[ + EvotipDispenseImplementation + ] = EvotipDispenseImplementation + + +class EvotipDispenseCreate(BaseCommandCreate[EvotipDispenseParams]): + """DispenseInPlace command request model.""" + + commandType: EvotipDispenseCommandType = "evotipDispense" + params: EvotipDispenseParams + + _CommandCls: Type[EvotipDispense] = EvotipDispense diff --git a/api/src/opentrons/protocol_engine/commands/evotip_seal_pipette.py b/api/src/opentrons/protocol_engine/commands/evotip_seal_pipette.py new file mode 100644 index 00000000000..d357ad66e2f --- /dev/null +++ b/api/src/opentrons/protocol_engine/commands/evotip_seal_pipette.py @@ -0,0 +1,331 @@ +"""Seal evotip resin tip command request, result, and implementation models.""" + +from __future__ import annotations +from pydantic import Field, BaseModel +from typing import TYPE_CHECKING, Optional, Type, Union +from opentrons.types import MountType +from opentrons.protocol_engine.types import MotorAxis +from typing_extensions import Literal + +from opentrons.protocol_engine.errors import UnsupportedLabwareForActionError +from ..resources import ModelUtils, labware_validation +from ..types import PickUpTipWellLocation, FluidKind, AspiratedFluid +from .pipetting_common import ( + PipetteIdMixin, +) +from .movement_common import ( + DestinationPositionResult, + StallOrCollisionError, + move_to_well, +) +from .command import ( + AbstractCommandImpl, + BaseCommand, + BaseCommandCreate, + DefinedErrorData, + SuccessData, +) + +from opentrons.hardware_control import HardwareControlAPI +from opentrons.hardware_control.types import Axis +from ..state.update_types import StateUpdate + +if TYPE_CHECKING: + from ..state.state import StateView + from ..execution import ( + MovementHandler, + TipHandler, + GantryMover, + PipettingHandler, + ) + + +EvotipSealPipetteCommandType = Literal["evotipSealPipette"] +_PREP_DISTANCE_DEFAULT = 8.25 +_PRESS_DISTANCE_DEFAULT = 3.5 +_EJECTOR_PUSH_MM_DEFAULT = 7.0 +_SAFE_TOP_VOLUME = 400 + + +class TipPickUpParams(BaseModel): + """Payload used to specify press-tip parameters for a seal command.""" + + prepDistance: float = Field( + default=0, description="The distance to move down to fit the tips on." + ) + pressDistance: float = Field( + default=0, description="The distance to press on tips." + ) + ejectorPushMm: float = Field( + default=0, + description="The distance to back off to ensure that the tip presence sensors are not triggered.", + ) + + +class EvotipSealPipetteParams(PipetteIdMixin): + """Payload needed to seal resin tips to a pipette.""" + + labwareId: str = Field(..., description="Identifier of labware to use.") + wellName: str = Field(..., description="Name of well to use in labware.") + wellLocation: PickUpTipWellLocation = Field( + default_factory=PickUpTipWellLocation, + description="Relative well location at which to pick up the tip.", + ) + tipPickUpParams: Optional[TipPickUpParams] = Field( + default=None, description="Specific parameters for " + ) + + +class EvotipSealPipetteResult(DestinationPositionResult): + """Result data from the execution of a EvotipSealPipette.""" + + tipVolume: float = Field( + 0, + description="Maximum volume of liquid that the picked up tip can hold, in µL.", + ge=0, + ) + + tipLength: float = Field( + 0, + description="The length of the tip in mm.", + ge=0, + ) + + tipDiameter: float = Field( + 0, + description="The diameter of the tip in mm.", + ge=0, + ) + + +_ExecuteReturn = Union[ + SuccessData[EvotipSealPipetteResult], + DefinedErrorData[StallOrCollisionError], +] + + +class EvotipSealPipetteImplementation( + AbstractCommandImpl[EvotipSealPipetteParams, _ExecuteReturn] +): + """Evotip seal pipette command implementation.""" + + def __init__( + self, + state_view: StateView, + tip_handler: TipHandler, + model_utils: ModelUtils, + movement: MovementHandler, + hardware_api: HardwareControlAPI, + gantry_mover: GantryMover, + pipetting: PipettingHandler, + **kwargs: object, + ) -> None: + self._state_view = state_view + self._tip_handler = tip_handler + self._model_utils = model_utils + self._movement = movement + self._gantry_mover = gantry_mover + self._pipetting = pipetting + self._hardware_api = hardware_api + + async def relative_pickup_tip( + self, + tip_pick_up_params: TipPickUpParams, + mount: MountType, + ) -> None: + """A relative press-fit pick up command using gantry moves.""" + prep_distance = tip_pick_up_params.prepDistance + press_distance = tip_pick_up_params.pressDistance + retract_distance = -1 * (prep_distance + press_distance) + + mount_axis = MotorAxis.LEFT_Z if mount == MountType.LEFT else MotorAxis.RIGHT_Z + + # TODO chb, 2025-01-29): Factor out the movement constants and relocate this logic into the hardware controller + await self._gantry_mover.move_axes( + axis_map={mount_axis: prep_distance}, speed=10, relative_move=True + ) + + # Drive mount down for press-fit + await self._gantry_mover.move_axes( + axis_map={mount_axis: press_distance}, + speed=10.0, + relative_move=True, + expect_stalls=True, + ) + # retract cam : 11.05 + await self._gantry_mover.move_axes( + axis_map={mount_axis: retract_distance}, speed=5.5, relative_move=True + ) + + async def cam_action_relative_pickup_tip( + self, + tip_pick_up_params: TipPickUpParams, + mount: MountType, + ) -> None: + """A cam action pick up command using gantry moves.""" + prep_distance = tip_pick_up_params.prepDistance + press_distance = tip_pick_up_params.pressDistance + ejector_push_mm = tip_pick_up_params.ejectorPushMm + retract_distance = -1 * (prep_distance + press_distance) + + mount_axis = MotorAxis.LEFT_Z if mount == MountType.LEFT else MotorAxis.RIGHT_Z + + # TODO chb, 2025-01-29): Factor out the movement constants and relocate this logic into the hardware controller + await self._gantry_mover.move_axes( + axis_map={mount_axis: -6}, speed=10, relative_move=True + ) + + # Drive Q down 3mm at fast speed - look into the pick up tip fuinction to find slow and fast: 10.0 + await self._gantry_mover.move_axes( + axis_map={MotorAxis.AXIS_96_CHANNEL_CAM: prep_distance}, + speed=10.0, + relative_move=True, + ) + # 2.8mm at slow speed - cam action pickup speed: 5.5 + await self._gantry_mover.move_axes( + axis_map={MotorAxis.AXIS_96_CHANNEL_CAM: press_distance}, + speed=5.5, + relative_move=True, + ) + # retract cam : 11.05 + await self._gantry_mover.move_axes( + axis_map={MotorAxis.AXIS_96_CHANNEL_CAM: retract_distance}, + speed=5.5, + relative_move=True, + ) + + # Lower tip presence + await self._gantry_mover.move_axes( + axis_map={mount_axis: 2}, speed=10, relative_move=True + ) + await self._gantry_mover.move_axes( + axis_map={MotorAxis.AXIS_96_CHANNEL_CAM: ejector_push_mm}, + speed=5.5, + relative_move=True, + ) + await self._gantry_mover.move_axes( + axis_map={MotorAxis.AXIS_96_CHANNEL_CAM: -1 * ejector_push_mm}, + speed=5.5, + relative_move=True, + ) + + async def execute( + self, params: EvotipSealPipetteParams + ) -> Union[SuccessData[EvotipSealPipetteResult], _ExecuteReturn]: + """Move to and pick up a tip using the requested pipette.""" + pipette_id = params.pipetteId + labware_id = params.labwareId + well_name = params.wellName + + labware_definition = self._state_view.labware.get_definition(params.labwareId) + if not labware_validation.is_evotips(labware_definition.parameters.loadName): + raise UnsupportedLabwareForActionError( + f"Cannot use command: `EvotipSealPipette` with labware: {labware_definition.parameters.loadName}" + ) + + well_location = self._state_view.geometry.convert_pick_up_tip_well_location( + well_location=params.wellLocation + ) + move_result = await move_to_well( + movement=self._movement, + model_utils=self._model_utils, + pipette_id=pipette_id, + labware_id=labware_id, + well_name=well_name, + well_location=well_location, + ) + if isinstance(move_result, DefinedErrorData): + return move_result + + # Aspirate to move plunger to a maximum volume position per pipette type + tip_geometry = self._state_view.geometry.get_nominal_tip_geometry( + pipette_id, labware_id, well_name + ) + if self._state_view.pipettes.get_mount(pipette_id) == MountType.LEFT: + await self._hardware_api.home(axes=[Axis.P_L]) + else: + await self._hardware_api.home(axes=[Axis.P_R]) + + # Begin relative pickup steps for the resin tips + + channels = self._state_view.tips.get_pipette_active_channels(pipette_id) + mount = self._state_view.pipettes.get_mount(pipette_id) + tip_pick_up_params = params.tipPickUpParams + if tip_pick_up_params is None: + tip_pick_up_params = TipPickUpParams( + prepDistance=_PREP_DISTANCE_DEFAULT, + pressDistance=_PRESS_DISTANCE_DEFAULT, + ejectorPushMm=_EJECTOR_PUSH_MM_DEFAULT, + ) + + if channels != 96: + await self.relative_pickup_tip( + tip_pick_up_params=tip_pick_up_params, + mount=mount, + ) + elif channels == 96: + await self.cam_action_relative_pickup_tip( + tip_pick_up_params=tip_pick_up_params, + mount=mount, + ) + else: + tip_geometry = await self._tip_handler.pick_up_tip( + pipette_id=pipette_id, + labware_id=labware_id, + well_name=well_name, + do_not_ignore_tip_presence=True, + ) + + # cache_tip + if self._state_view.config.use_virtual_pipettes is False: + self._tip_handler.cache_tip(pipette_id, tip_geometry) + hw_instr = self._hardware_api.hardware_instruments[mount.to_hw_mount()] + if hw_instr is not None: + hw_instr.set_current_volume(_SAFE_TOP_VOLUME) + + state_update = StateUpdate() + state_update.update_pipette_tip_state( + pipette_id=pipette_id, + tip_geometry=tip_geometry, + ) + + state_update.set_fluid_aspirated( + pipette_id=pipette_id, + fluid=AspiratedFluid(kind=FluidKind.LIQUID, volume=_SAFE_TOP_VOLUME), + ) + return SuccessData( + public=EvotipSealPipetteResult( + tipVolume=tip_geometry.volume, + tipLength=tip_geometry.length, + tipDiameter=tip_geometry.diameter, + position=move_result.public.position, + ), + state_update=state_update, + ) + + +class EvotipSealPipette( + BaseCommand[ + EvotipSealPipetteParams, + EvotipSealPipetteResult, + StallOrCollisionError, + ] +): + """Seal evotip resin tip command model.""" + + commandType: EvotipSealPipetteCommandType = "evotipSealPipette" + params: EvotipSealPipetteParams + result: Optional[EvotipSealPipetteResult] = None + + _ImplementationCls: Type[ + EvotipSealPipetteImplementation + ] = EvotipSealPipetteImplementation + + +class EvotipSealPipetteCreate(BaseCommandCreate[EvotipSealPipetteParams]): + """Seal evotip resin tip command creation request model.""" + + commandType: EvotipSealPipetteCommandType = "evotipSealPipette" + params: EvotipSealPipetteParams + + _CommandCls: Type[EvotipSealPipette] = EvotipSealPipette diff --git a/api/src/opentrons/protocol_engine/commands/evotip_unseal_pipette.py b/api/src/opentrons/protocol_engine/commands/evotip_unseal_pipette.py new file mode 100644 index 00000000000..e8cde34fd5c --- /dev/null +++ b/api/src/opentrons/protocol_engine/commands/evotip_unseal_pipette.py @@ -0,0 +1,160 @@ +"""Unseal evotip resin tip command request, result, and implementation models.""" + +from __future__ import annotations + +from pydantic import Field +from typing import TYPE_CHECKING, Optional, Type +from typing_extensions import Literal + +from opentrons.protocol_engine.resources.model_utils import ModelUtils +from opentrons.protocol_engine.errors import UnsupportedLabwareForActionError +from opentrons.protocol_engine.types import MotorAxis +from opentrons.types import MountType + +from ..types import DropTipWellLocation +from .pipetting_common import ( + PipetteIdMixin, +) +from .movement_common import ( + DestinationPositionResult, + move_to_well, + StallOrCollisionError, +) +from .command import ( + AbstractCommandImpl, + BaseCommand, + BaseCommandCreate, + DefinedErrorData, + SuccessData, +) +from ..resources import labware_validation + +if TYPE_CHECKING: + from ..state.state import StateView + from ..execution import MovementHandler, TipHandler, GantryMover + + +EvotipUnsealPipetteCommandType = Literal["evotipUnsealPipette"] + + +class EvotipUnsealPipetteParams(PipetteIdMixin): + """Payload required to drop a tip in a specific well.""" + + labwareId: str = Field(..., description="Identifier of labware to use.") + wellName: str = Field(..., description="Name of well to use in labware.") + wellLocation: DropTipWellLocation = Field( + default_factory=DropTipWellLocation, + description="Relative well location at which to drop the tip.", + ) + + +class EvotipUnsealPipetteResult(DestinationPositionResult): + """Result data from the execution of a DropTip command.""" + + pass + + +_ExecuteReturn = ( + SuccessData[EvotipUnsealPipetteResult] | DefinedErrorData[StallOrCollisionError] +) + + +class EvotipUnsealPipetteImplementation( + AbstractCommandImpl[EvotipUnsealPipetteParams, _ExecuteReturn] +): + """Drop tip command implementation.""" + + def __init__( + self, + state_view: StateView, + tip_handler: TipHandler, + movement: MovementHandler, + model_utils: ModelUtils, + gantry_mover: GantryMover, + **kwargs: object, + ) -> None: + self._state_view = state_view + self._tip_handler = tip_handler + self._movement_handler = movement + self._model_utils = model_utils + self._gantry_mover = gantry_mover + + async def execute(self, params: EvotipUnsealPipetteParams) -> _ExecuteReturn: + """Move to and drop a tip using the requested pipette.""" + pipette_id = params.pipetteId + labware_id = params.labwareId + well_name = params.wellName + + well_location = params.wellLocation + labware_definition = self._state_view.labware.get_definition(params.labwareId) + if not labware_validation.is_evotips(labware_definition.parameters.loadName): + raise UnsupportedLabwareForActionError( + f"Cannot use command: `EvotipUnsealPipette` with labware: {labware_definition.parameters.loadName}" + ) + is_partially_configured = self._state_view.pipettes.get_is_partially_configured( + pipette_id=pipette_id + ) + tip_drop_location = self._state_view.geometry.get_checked_tip_drop_location( + pipette_id=pipette_id, + labware_id=labware_id, + well_location=well_location, + partially_configured=is_partially_configured, + ) + + move_result = await move_to_well( + movement=self._movement_handler, + model_utils=self._model_utils, + pipette_id=pipette_id, + labware_id=labware_id, + well_name=well_name, + well_location=tip_drop_location, + ) + if isinstance(move_result, DefinedErrorData): + return move_result + + # Move to an appropriate position + mount = self._state_view.pipettes.get_mount(pipette_id) + + mount_axis = MotorAxis.LEFT_Z if mount == MountType.LEFT else MotorAxis.RIGHT_Z + await self._gantry_mover.move_axes( + axis_map={mount_axis: -14}, speed=10, relative_move=True + ) + + await self._tip_handler.drop_tip( + pipette_id=pipette_id, + home_after=None, + do_not_ignore_tip_presence=False, + ignore_plunger=True, + ) + + return SuccessData( + public=EvotipUnsealPipetteResult(position=move_result.public.position), + state_update=move_result.state_update.set_fluid_unknown( + pipette_id=pipette_id + ).update_pipette_tip_state(pipette_id=params.pipetteId, tip_geometry=None), + ) + + +class EvotipUnsealPipette( + BaseCommand[ + EvotipUnsealPipetteParams, EvotipUnsealPipetteResult, StallOrCollisionError + ] +): + """Evotip unseal command model.""" + + commandType: EvotipUnsealPipetteCommandType = "evotipUnsealPipette" + params: EvotipUnsealPipetteParams + result: Optional[EvotipUnsealPipetteResult] = None + + _ImplementationCls: Type[ + EvotipUnsealPipetteImplementation + ] = EvotipUnsealPipetteImplementation + + +class EvotipUnsealPipetteCreate(BaseCommandCreate[EvotipUnsealPipetteParams]): + """Evotip unseal command creation request model.""" + + commandType: EvotipUnsealPipetteCommandType = "evotipUnsealPipette" + params: EvotipUnsealPipetteParams + + _CommandCls: Type[EvotipUnsealPipette] = EvotipUnsealPipette diff --git a/api/src/opentrons/protocol_engine/commands/generate_command_schema.py b/api/src/opentrons/protocol_engine/commands/generate_command_schema.py index acc0923bcdf..938244b74e8 100644 --- a/api/src/opentrons/protocol_engine/commands/generate_command_schema.py +++ b/api/src/opentrons/protocol_engine/commands/generate_command_schema.py @@ -1,24 +1,17 @@ """Generate a JSON schema against which all create commands statically validate.""" + import json -import pydantic import argparse import sys -from opentrons.protocol_engine.commands.command_unions import CommandCreate - - -class CreateCommandUnion(pydantic.BaseModel): - """Model that validates a union of all CommandCreate models.""" - - __root__: CommandCreate +from opentrons.protocol_engine.commands.command_unions import CommandCreateAdapter def generate_command_schema(version: str) -> str: """Generate a JSON Schema that all valid create commands can validate against.""" - raw_json_schema = CreateCommandUnion.schema_json() - schema_as_dict = json.loads(raw_json_schema) + schema_as_dict = CommandCreateAdapter.json_schema(mode="validation") schema_as_dict["$id"] = f"opentronsCommandSchemaV{version}" schema_as_dict["$schema"] = "http://json-schema.org/draft-07/schema#" - return json.dumps(schema_as_dict, indent=2) + return json.dumps(schema_as_dict, indent=2, sort_keys=True) if __name__ == "__main__": diff --git a/api/src/opentrons/protocol_engine/commands/get_next_tip.py b/api/src/opentrons/protocol_engine/commands/get_next_tip.py new file mode 100644 index 00000000000..9e0cf3b50cf --- /dev/null +++ b/api/src/opentrons/protocol_engine/commands/get_next_tip.py @@ -0,0 +1,134 @@ +"""Get next tip command request, result, and implementation models.""" + +from __future__ import annotations +from pydantic import BaseModel, Field +from typing import TYPE_CHECKING, Any, Optional, Type, List, Literal, Union + +from pydantic.json_schema import SkipJsonSchema + +from opentrons.types import NozzleConfigurationType + +from ..errors import ErrorOccurrence +from ..types import NextTipInfo, NoTipAvailable, NoTipReason +from .pipetting_common import PipetteIdMixin + +from .command import ( + AbstractCommandImpl, + BaseCommand, + BaseCommandCreate, + SuccessData, +) + +if TYPE_CHECKING: + from ..state.state import StateView + + +def _remove_default(s: dict[str, Any]) -> None: + s.pop("default", None) + + +GetNextTipCommandType = Literal["getNextTip"] + + +class GetNextTipParams(PipetteIdMixin): + """Payload needed to resolve the next available tip.""" + + labwareIds: List[str] = Field( + ..., + description="Labware ID(s) of tip racks to resolve next available tip(s) from" + " Labware IDs will be resolved sequentially", + ) + startingTipWell: str | SkipJsonSchema[None] = Field( + None, + description="Name of starting tip rack 'well'." + " This only applies to the first tip rack in the list provided in labwareIDs", + json_schema_extra=_remove_default, + ) + + +class GetNextTipResult(BaseModel): + """Result data from the execution of a GetNextTip.""" + + nextTipInfo: Union[NextTipInfo, NoTipAvailable] = Field( + ..., + description="Labware ID and well name of next available tip for a pipette," + " or information why no tip could be resolved.", + ) + + +class GetNextTipImplementation( + AbstractCommandImpl[GetNextTipParams, SuccessData[GetNextTipResult]] +): + """Get next tip command implementation.""" + + def __init__( + self, + state_view: StateView, + **kwargs: object, + ) -> None: + self._state_view = state_view + + async def execute(self, params: GetNextTipParams) -> SuccessData[GetNextTipResult]: + """Get the next available tip for the requested pipette.""" + pipette_id = params.pipetteId + starting_tip_name = params.startingTipWell + + num_tips = self._state_view.tips.get_pipette_active_channels(pipette_id) + nozzle_map = self._state_view.tips.get_pipette_nozzle_map(pipette_id) + + if ( + starting_tip_name is not None + and nozzle_map.configuration != NozzleConfigurationType.FULL + ): + # This is to match the behavior found in PAPI, but also because we don't have logic to automatically find + # the next tip with partial configuration and a starting tip. This will never work for a 96-channel due to + # x-axis overlap, but could eventually work with 8-channel if we better define starting tip USED or CLEAN + # state when starting a protocol to prevent accidental tip pick-up with starting non-full tip racks. + return SuccessData( + public=GetNextTipResult( + nextTipInfo=NoTipAvailable( + noTipReason=NoTipReason.STARTING_TIP_WITH_PARTIAL, + message="Cannot automatically resolve next tip with starting tip and partial tip configuration.", + ) + ) + ) + + next_tip: Union[NextTipInfo, NoTipAvailable] + for labware_id in params.labwareIds: + well_name = self._state_view.tips.get_next_tip( + labware_id=labware_id, + num_tips=num_tips, + starting_tip_name=starting_tip_name, + nozzle_map=nozzle_map, + ) + if well_name is not None: + next_tip = NextTipInfo(labwareId=labware_id, tipStartingWell=well_name) + break + # After the first tip rack is exhausted, starting tip no longer applies + starting_tip_name = None + else: + next_tip = NoTipAvailable( + noTipReason=NoTipReason.NO_AVAILABLE_TIPS, + message="No available tips for given pipette, nozzle configuration and provided tip racks.", + ) + + return SuccessData(public=GetNextTipResult(nextTipInfo=next_tip)) + + +class GetNextTip(BaseCommand[GetNextTipParams, GetNextTipResult, ErrorOccurrence]): + """Get next tip command model.""" + + commandType: GetNextTipCommandType = "getNextTip" + params: GetNextTipParams + result: Optional[GetNextTipResult] = None + + _ImplementationCls: Type[GetNextTipImplementation] = GetNextTipImplementation + + +class GetNextTipCreate(BaseCommandCreate[GetNextTipParams]): + """Get next tip command creation request model.""" + + commandType: GetNextTipCommandType = "getNextTip" + params: GetNextTipParams + + _CommandCls: Type[GetNextTip] = GetNextTip diff --git a/api/src/opentrons/protocol_engine/commands/get_tip_presence.py b/api/src/opentrons/protocol_engine/commands/get_tip_presence.py index 6bbe5fa2fe3..05f9e1052c1 100644 --- a/api/src/opentrons/protocol_engine/commands/get_tip_presence.py +++ b/api/src/opentrons/protocol_engine/commands/get_tip_presence.py @@ -71,7 +71,7 @@ class GetTipPresence( commandType: GetTipPresenceCommandType = "getTipPresence" params: GetTipPresenceParams - result: Optional[GetTipPresenceResult] + result: Optional[GetTipPresenceResult] = None _ImplementationCls: Type[ GetTipPresenceImplementation diff --git a/api/src/opentrons/protocol_engine/commands/heater_shaker/close_labware_latch.py b/api/src/opentrons/protocol_engine/commands/heater_shaker/close_labware_latch.py index 2151fb05877..211b374be7e 100644 --- a/api/src/opentrons/protocol_engine/commands/heater_shaker/close_labware_latch.py +++ b/api/src/opentrons/protocol_engine/commands/heater_shaker/close_labware_latch.py @@ -69,7 +69,7 @@ class CloseLabwareLatch( commandType: CloseLabwareLatchCommandType = "heaterShaker/closeLabwareLatch" params: CloseLabwareLatchParams - result: Optional[CloseLabwareLatchResult] + result: Optional[CloseLabwareLatchResult] = None _ImplementationCls: Type[CloseLabwareLatchImpl] = CloseLabwareLatchImpl diff --git a/api/src/opentrons/protocol_engine/commands/heater_shaker/deactivate_heater.py b/api/src/opentrons/protocol_engine/commands/heater_shaker/deactivate_heater.py index 3932f1d6490..6ac76f020d3 100644 --- a/api/src/opentrons/protocol_engine/commands/heater_shaker/deactivate_heater.py +++ b/api/src/opentrons/protocol_engine/commands/heater_shaker/deactivate_heater.py @@ -68,7 +68,7 @@ class DeactivateHeater( commandType: DeactivateHeaterCommandType = "heaterShaker/deactivateHeater" params: DeactivateHeaterParams - result: Optional[DeactivateHeaterResult] + result: Optional[DeactivateHeaterResult] = None _ImplementationCls: Type[DeactivateHeaterImpl] = DeactivateHeaterImpl diff --git a/api/src/opentrons/protocol_engine/commands/heater_shaker/deactivate_shaker.py b/api/src/opentrons/protocol_engine/commands/heater_shaker/deactivate_shaker.py index b259745ea3d..e7e50b07fee 100644 --- a/api/src/opentrons/protocol_engine/commands/heater_shaker/deactivate_shaker.py +++ b/api/src/opentrons/protocol_engine/commands/heater_shaker/deactivate_shaker.py @@ -70,7 +70,7 @@ class DeactivateShaker( commandType: DeactivateShakerCommandType = "heaterShaker/deactivateShaker" params: DeactivateShakerParams - result: Optional[DeactivateShakerResult] + result: Optional[DeactivateShakerResult] = None _ImplementationCls: Type[DeactivateShakerImpl] = DeactivateShakerImpl diff --git a/api/src/opentrons/protocol_engine/commands/heater_shaker/open_labware_latch.py b/api/src/opentrons/protocol_engine/commands/heater_shaker/open_labware_latch.py index 9c3a9d8ae7d..8bb869c47ee 100644 --- a/api/src/opentrons/protocol_engine/commands/heater_shaker/open_labware_latch.py +++ b/api/src/opentrons/protocol_engine/commands/heater_shaker/open_labware_latch.py @@ -96,7 +96,7 @@ class OpenLabwareLatch( commandType: OpenLabwareLatchCommandType = "heaterShaker/openLabwareLatch" params: OpenLabwareLatchParams - result: Optional[OpenLabwareLatchResult] + result: Optional[OpenLabwareLatchResult] = None _ImplementationCls: Type[OpenLabwareLatchImpl] = OpenLabwareLatchImpl diff --git a/api/src/opentrons/protocol_engine/commands/heater_shaker/set_and_wait_for_shake_speed.py b/api/src/opentrons/protocol_engine/commands/heater_shaker/set_and_wait_for_shake_speed.py index 8828195c658..2bcf1d48ba3 100644 --- a/api/src/opentrons/protocol_engine/commands/heater_shaker/set_and_wait_for_shake_speed.py +++ b/api/src/opentrons/protocol_engine/commands/heater_shaker/set_and_wait_for_shake_speed.py @@ -109,7 +109,7 @@ class SetAndWaitForShakeSpeed( "heaterShaker/setAndWaitForShakeSpeed" ) params: SetAndWaitForShakeSpeedParams - result: Optional[SetAndWaitForShakeSpeedResult] + result: Optional[SetAndWaitForShakeSpeedResult] = None _ImplementationCls: Type[SetAndWaitForShakeSpeedImpl] = SetAndWaitForShakeSpeedImpl diff --git a/api/src/opentrons/protocol_engine/commands/heater_shaker/set_target_temperature.py b/api/src/opentrons/protocol_engine/commands/heater_shaker/set_target_temperature.py index fa29390b910..f0b0533aca3 100644 --- a/api/src/opentrons/protocol_engine/commands/heater_shaker/set_target_temperature.py +++ b/api/src/opentrons/protocol_engine/commands/heater_shaker/set_target_temperature.py @@ -76,7 +76,7 @@ class SetTargetTemperature( commandType: SetTargetTemperatureCommandType = "heaterShaker/setTargetTemperature" params: SetTargetTemperatureParams - result: Optional[SetTargetTemperatureResult] + result: Optional[SetTargetTemperatureResult] = None _ImplementationCls: Type[SetTargetTemperatureImpl] = SetTargetTemperatureImpl diff --git a/api/src/opentrons/protocol_engine/commands/heater_shaker/wait_for_temperature.py b/api/src/opentrons/protocol_engine/commands/heater_shaker/wait_for_temperature.py index bb440a2674c..676fbcd4bfb 100644 --- a/api/src/opentrons/protocol_engine/commands/heater_shaker/wait_for_temperature.py +++ b/api/src/opentrons/protocol_engine/commands/heater_shaker/wait_for_temperature.py @@ -1,9 +1,10 @@ """Command models to wait for a Heater-Shaker Module's target temperature.""" from __future__ import annotations -from typing import Optional, TYPE_CHECKING -from typing_extensions import Literal, Type +from typing import Optional, TYPE_CHECKING, Any +from typing_extensions import Literal, Type from pydantic import BaseModel, Field +from pydantic.json_schema import SkipJsonSchema from ..command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, SuccessData from ...errors.error_occurrence import ErrorOccurrence @@ -16,11 +17,15 @@ WaitForTemperatureCommandType = Literal["heaterShaker/waitForTemperature"] +def _remove_default(s: dict[str, Any]) -> None: + s.pop("default", None) + + class WaitForTemperatureParams(BaseModel): """Input parameters to wait for a Heater-Shaker's target temperature.""" moduleId: str = Field(..., description="Unique ID of the Heater-Shaker Module.") - celsius: Optional[float] = Field( + celsius: float | SkipJsonSchema[None] = Field( None, description="Target temperature in °C. If not specified, will " "default to the module's target temperature. " @@ -28,6 +33,7 @@ class WaitForTemperatureParams(BaseModel): "could lead to unpredictable behavior and hence is not " "recommended for use. This parameter can be removed in a " "future version without prior notice.", + json_schema_extra=_remove_default, ) @@ -82,7 +88,7 @@ class WaitForTemperature( commandType: WaitForTemperatureCommandType = "heaterShaker/waitForTemperature" params: WaitForTemperatureParams - result: Optional[WaitForTemperatureResult] + result: Optional[WaitForTemperatureResult] = None _ImplementationCls: Type[WaitForTemperatureImpl] = WaitForTemperatureImpl diff --git a/api/src/opentrons/protocol_engine/commands/home.py b/api/src/opentrons/protocol_engine/commands/home.py index 7b82f90e1fd..f096740958f 100644 --- a/api/src/opentrons/protocol_engine/commands/home.py +++ b/api/src/opentrons/protocol_engine/commands/home.py @@ -1,7 +1,10 @@ """Home command payload, result, and implementation models.""" from __future__ import annotations +from typing import TYPE_CHECKING, Optional, List, Type, Any + from pydantic import BaseModel, Field -from typing import TYPE_CHECKING, Optional, List, Type +from pydantic.json_schema import SkipJsonSchema + from typing_extensions import Literal from opentrons.types import MountType @@ -17,24 +20,30 @@ HomeCommandType = Literal["home"] +def _remove_default(s: dict[str, Any]) -> None: + s.pop("default", None) + + class HomeParams(BaseModel): """Payload required for a Home command.""" - axes: Optional[List[MotorAxis]] = Field( + axes: List[MotorAxis] | SkipJsonSchema[None] = Field( None, description=( "Axes to return to their home positions. If omitted," " will home all motors. Extra axes may be implicitly homed" " to ensure accurate homing of the explicitly specified axes." ), + json_schema_extra=_remove_default, ) - skipIfMountPositionOk: Optional[MountType] = Field( + skipIfMountPositionOk: MountType | SkipJsonSchema[None] = Field( None, description=( "If this parameter is provided, the gantry will only be homed if the" " specified mount has an invalid position. If omitted, the homing action" " will be executed unconditionally." ), + json_schema_extra=_remove_default, ) @@ -77,7 +86,7 @@ class Home(BaseCommand[HomeParams, HomeResult, ErrorOccurrence]): commandType: HomeCommandType = "home" params: HomeParams - result: Optional[HomeResult] + result: Optional[HomeResult] = None _ImplementationCls: Type[HomeImplementation] = HomeImplementation diff --git a/api/src/opentrons/protocol_engine/commands/liquid_probe.py b/api/src/opentrons/protocol_engine/commands/liquid_probe.py index f78cd5bb55c..53cc3f77abd 100644 --- a/api/src/opentrons/protocol_engine/commands/liquid_probe.py +++ b/api/src/opentrons/protocol_engine/commands/liquid_probe.py @@ -1,10 +1,11 @@ """The liquidProbe and tryLiquidProbe commands.""" from __future__ import annotations -from typing import TYPE_CHECKING, NamedTuple, Optional, Type, Union -from typing_extensions import Literal +from typing import TYPE_CHECKING, NamedTuple, Optional, Type, Union, Any +from typing_extensions import Literal from pydantic import Field +from pydantic.json_schema import SkipJsonSchema from opentrons.protocol_engine.state import update_types from opentrons.protocol_engine.errors.exceptions import ( @@ -12,18 +13,26 @@ PipetteNotReadyToAspirateError, TipNotEmptyError, IncompleteLabwareDefinitionError, + TipNotAttachedError, ) from opentrons.types import MountType from opentrons_shared_data.errors.exceptions import ( PipetteLiquidNotFoundError, + UnsupportedHardwareCommand, + PipetteOverpressureError, ) from ..types import DeckPoint from .pipetting_common import ( LiquidNotFoundError, PipetteIdMixin, + OverpressureError, +) +from .movement_common import ( WellLocationMixin, DestinationPositionResult, + StallOrCollisionError, + move_to_well, ) from .command import ( AbstractCommandImpl, @@ -36,11 +45,15 @@ from ..errors.error_occurrence import ErrorOccurrence if TYPE_CHECKING: - from ..execution import MovementHandler, PipettingHandler + from ..execution import MovementHandler, PipettingHandler, GantryMover from ..resources import ModelUtils from ..state.state import StateView +def _remove_default(s: dict[str, Any]) -> None: + s.pop("default", None) + + LiquidProbeCommandType = Literal["liquidProbe"] TryLiquidProbeCommandType = Literal["tryLiquidProbe"] @@ -76,43 +89,64 @@ class LiquidProbeResult(DestinationPositionResult): class TryLiquidProbeResult(DestinationPositionResult): """Result data from the execution of a `tryLiquidProbe` command.""" - z_position: Optional[float] = Field( + z_position: float | SkipJsonSchema[None] = Field( ..., description=( "The Z coordinate, in mm, of the found liquid in deck space." " If no liquid was found, `null` or omitted." ), + json_schema_extra=_remove_default, ) _LiquidProbeExecuteReturn = Union[ SuccessData[LiquidProbeResult], - DefinedErrorData[LiquidNotFoundError], + DefinedErrorData[LiquidNotFoundError] + | DefinedErrorData[StallOrCollisionError] + | DefinedErrorData[OverpressureError], ] -_TryLiquidProbeExecuteReturn = SuccessData[TryLiquidProbeResult] +_TryLiquidProbeExecuteReturn = ( + SuccessData[TryLiquidProbeResult] + | DefinedErrorData[StallOrCollisionError] + | DefinedErrorData[OverpressureError] +) class _ExecuteCommonResult(NamedTuple): # If the probe succeeded, the z_pos that it returned. # Or, if the probe found no liquid, the error representing that, # so calling code can propagate those details up. - z_pos_or_error: float | PipetteLiquidNotFoundError + z_pos_or_error: float | PipetteLiquidNotFoundError | PipetteOverpressureError state_update: update_types.StateUpdate deck_point: DeckPoint -async def _execute_common( +async def _execute_common( # noqa: C901 state_view: StateView, movement: MovementHandler, + gantry_mover: GantryMover, pipetting: PipettingHandler, + model_utils: ModelUtils, params: _CommonParams, -) -> _ExecuteCommonResult: +) -> _ExecuteCommonResult | DefinedErrorData[StallOrCollisionError] | DefinedErrorData[ + OverpressureError +]: pipette_id = params.pipetteId labware_id = params.labwareId well_name = params.wellName + if ( + "pressure" + not in state_view.pipettes.get_config(pipette_id).available_sensors.sensors + ): + raise UnsupportedHardwareCommand( + "Pressure sensor not available for this pipette" + ) - state_update = update_types.StateUpdate() + if not state_view.pipettes.get_nozzle_configuration_supports_lld(pipette_id): + raise TipNotAttachedError( + "Either the front right or back left nozzle must have a tip attached to probe liquid height." + ) # May raise TipNotAttachedError. aspirated_volume = state_view.pipettes.get_aspirated_volume(pipette_id) @@ -137,21 +171,18 @@ async def _execute_common( ) # liquid_probe process start position - position = await movement.move_to_well( + move_result = await move_to_well( + movement=movement, + model_utils=model_utils, pipette_id=pipette_id, labware_id=labware_id, well_name=well_name, well_location=params.wellLocation, ) - deck_point = DeckPoint.construct(x=position.x, y=position.y, z=position.z) - state_update.set_pipette_location( - pipette_id=pipette_id, - new_labware_id=labware_id, - new_well_name=well_name, - new_deck_point=deck_point, - ) - + if isinstance(move_result, DefinedErrorData): + return move_result try: + current_position = await gantry_mover.get_position(params.pipetteId) z_pos = await pipetting.liquid_probe_in_place( pipette_id=pipette_id, labware_id=labware_id, @@ -160,11 +191,42 @@ async def _execute_common( ) except PipetteLiquidNotFoundError as exception: return _ExecuteCommonResult( - z_pos_or_error=exception, state_update=state_update, deck_point=deck_point + z_pos_or_error=exception, + state_update=move_result.state_update, + deck_point=move_result.public.position, + ) + except PipetteOverpressureError as e: + return DefinedErrorData( + public=OverpressureError( + id=model_utils.generate_id(), + createdAt=model_utils.get_timestamp(), + wrappedErrors=[ + ErrorOccurrence.from_failed( + id=model_utils.generate_id(), + createdAt=model_utils.get_timestamp(), + error=e, + ) + ], + errorInfo=( + { + # This is here bc its not optional in the type but we are not using the retry location for this case + "retryLocation": ( + current_position.x, + current_position.y, + current_position.z, + ) + } + ), + ), + state_update=move_result.state_update.set_fluid_unknown( + pipette_id=pipette_id + ), ) else: return _ExecuteCommonResult( - z_pos_or_error=z_pos, state_update=state_update, deck_point=deck_point + z_pos_or_error=z_pos, + state_update=move_result.state_update, + deck_point=move_result.public.position, ) @@ -177,12 +239,14 @@ def __init__( self, state_view: StateView, movement: MovementHandler, + gantry_mover: GantryMover, pipetting: PipettingHandler, model_utils: ModelUtils, **kwargs: object, ) -> None: self._state_view = state_view self._movement = movement + self._gantry_mover = gantry_mover self._pipetting = pipetting self._model_utils = model_utils @@ -202,10 +266,20 @@ async def execute(self, params: _CommonParams) -> _LiquidProbeExecuteReturn: MustHomeError: as an undefined error, if the plunger is not in a valid position. """ - z_pos_or_error, state_update, deck_point = await _execute_common( - self._state_view, self._movement, self._pipetting, params + result = await _execute_common( + state_view=self._state_view, + movement=self._movement, + gantry_mover=self._gantry_mover, + pipetting=self._pipetting, + model_utils=self._model_utils, + params=params, ) - if isinstance(z_pos_or_error, PipetteLiquidNotFoundError): + if isinstance(result, DefinedErrorData): + return result + z_pos_or_error, state_update, deck_point = result + if isinstance( + z_pos_or_error, (PipetteLiquidNotFoundError, PipetteOverpressureError) + ): state_update.set_liquid_probed( labware_id=params.labwareId, well_name=params.wellName, @@ -262,12 +336,14 @@ def __init__( self, state_view: StateView, movement: MovementHandler, + gantry_mover: GantryMover, pipetting: PipettingHandler, model_utils: ModelUtils, **kwargs: object, ) -> None: self._state_view = state_view self._movement = movement + self._gantry_mover = gantry_mover self._pipetting = pipetting self._model_utils = model_utils @@ -278,11 +354,21 @@ async def execute(self, params: _CommonParams) -> _TryLiquidProbeExecuteReturn: found, `tryLiquidProbe` returns a success result with `z_position=null` instead of a defined error. """ - z_pos_or_error, state_update, deck_point = await _execute_common( - self._state_view, self._movement, self._pipetting, params + result = await _execute_common( + state_view=self._state_view, + movement=self._movement, + gantry_mover=self._gantry_mover, + pipetting=self._pipetting, + model_utils=self._model_utils, + params=params, ) + if isinstance(result, DefinedErrorData): + return result + z_pos_or_error, state_update, deck_point = result - if isinstance(z_pos_or_error, PipetteLiquidNotFoundError): + if isinstance( + z_pos_or_error, (PipetteLiquidNotFoundError, PipetteOverpressureError) + ): z_pos = None well_volume: float | update_types.ClearType = update_types.CLEAR else: @@ -312,25 +398,33 @@ async def execute(self, params: _CommonParams) -> _TryLiquidProbeExecuteReturn: class LiquidProbe( - BaseCommand[LiquidProbeParams, LiquidProbeResult, LiquidNotFoundError] + BaseCommand[ + LiquidProbeParams, + LiquidProbeResult, + LiquidNotFoundError | StallOrCollisionError | OverpressureError, + ] ): """The model for a full `liquidProbe` command.""" commandType: LiquidProbeCommandType = "liquidProbe" params: LiquidProbeParams - result: Optional[LiquidProbeResult] + result: Optional[LiquidProbeResult] = None _ImplementationCls: Type[LiquidProbeImplementation] = LiquidProbeImplementation class TryLiquidProbe( - BaseCommand[TryLiquidProbeParams, TryLiquidProbeResult, ErrorOccurrence] + BaseCommand[ + TryLiquidProbeParams, + TryLiquidProbeResult, + StallOrCollisionError | OverpressureError, + ] ): """The model for a full `tryLiquidProbe` command.""" commandType: TryLiquidProbeCommandType = "tryLiquidProbe" params: TryLiquidProbeParams - result: Optional[TryLiquidProbeResult] + result: Optional[TryLiquidProbeResult] = None _ImplementationCls: Type[ TryLiquidProbeImplementation diff --git a/api/src/opentrons/protocol_engine/commands/load_labware.py b/api/src/opentrons/protocol_engine/commands/load_labware.py index fb97f5d2c87..d965c1116ad 100644 --- a/api/src/opentrons/protocol_engine/commands/load_labware.py +++ b/api/src/opentrons/protocol_engine/commands/load_labware.py @@ -1,7 +1,9 @@ """Load labware command request, result, and implementation models.""" from __future__ import annotations +from typing import TYPE_CHECKING, Optional, Type, Any + from pydantic import BaseModel, Field -from typing import TYPE_CHECKING, Optional, Type +from pydantic.json_schema import SkipJsonSchema from typing_extensions import Literal from opentrons_shared_data.labware.labware_definition import LabwareDefinition @@ -29,6 +31,10 @@ LoadLabwareCommandType = Literal["loadLabware"] +def _remove_default(s: dict[str, Any]) -> None: + s.pop("default", None) + + class LoadLabwareParams(BaseModel): """Payload required to load a labware into a slot.""" @@ -48,18 +54,20 @@ class LoadLabwareParams(BaseModel): ..., description="The labware definition version.", ) - labwareId: Optional[str] = Field( + labwareId: str | SkipJsonSchema[None] = Field( None, description="An optional ID to assign to this labware. If None, an ID " "will be generated.", + json_schema_extra=_remove_default, ) - displayName: Optional[str] = Field( + displayName: str | SkipJsonSchema[None] = Field( None, description="An optional user-specified display name " "or label for this labware.", # NOTE: v4/5 JSON protocols will always have a displayName which will be the # user-specified label OR the displayName property of the labware's definition. # TODO: Make sure v6 JSON protocols don't do that. + json_schema_extra=_remove_default, ) @@ -104,6 +112,8 @@ async def execute( self, params: LoadLabwareParams ) -> SuccessData[LoadLabwareResult]: """Load definition and calibration data necessary for a labware.""" + state_update = StateUpdate() + # TODO (tz, 8-15-2023): extend column validation to column 1 when working # on https://opentrons.atlassian.net/browse/RSS-258 and completing # https://opentrons.atlassian.net/browse/RSS-255 @@ -128,10 +138,12 @@ async def execute( self._state_view.addressable_areas.raise_if_area_not_in_deck_configuration( area_name ) + state_update.set_addressable_area_used(area_name) elif isinstance(params.location, DeckSlotLocation): self._state_view.addressable_areas.raise_if_area_not_in_deck_configuration( params.location.slotName.id ) + state_update.set_addressable_area_used(params.location.slotName.id) verified_location = self._state_view.geometry.ensure_location_not_occupied( params.location @@ -144,8 +156,6 @@ async def execute( labware_id=params.labwareId, ) - state_update = StateUpdate() - state_update.set_loaded_labware( labware_id=loaded_labware.labware_id, offset_id=loaded_labware.offsetId, @@ -162,6 +172,19 @@ async def execute( top_labware_definition=loaded_labware.definition, bottom_labware_id=verified_location.labwareId, ) + # Validate load location is valid for lids + if ( + labware_validation.validate_definition_is_lid( + definition=loaded_labware.definition + ) + and loaded_labware.definition.compatibleParentLabware is not None + and self._state_view.labware.get_load_name(verified_location.labwareId) + not in loaded_labware.definition.compatibleParentLabware + ): + raise ValueError( + f"Labware Lid {params.loadName} may not be loaded on parent labware {self._state_view.labware.get_display_name(verified_location.labwareId)}." + ) + # Validate labware for the absorbance reader elif isinstance(params.location, ModuleLocation): module = self._state_view.modules.get(params.location.moduleId) @@ -170,6 +193,10 @@ async def execute( loaded_labware.definition ) + self._state_view.labware.raise_if_labware_cannot_be_ondeck( + location=params.location, labware_definition=loaded_labware.definition + ) + return SuccessData( public=LoadLabwareResult( labwareId=loaded_labware.labware_id, @@ -185,7 +212,7 @@ class LoadLabware(BaseCommand[LoadLabwareParams, LoadLabwareResult, ErrorOccurre commandType: LoadLabwareCommandType = "loadLabware" params: LoadLabwareParams - result: Optional[LoadLabwareResult] + result: Optional[LoadLabwareResult] = None _ImplementationCls: Type[LoadLabwareImplementation] = LoadLabwareImplementation diff --git a/api/src/opentrons/protocol_engine/commands/load_lid.py b/api/src/opentrons/protocol_engine/commands/load_lid.py new file mode 100644 index 00000000000..c348ffbc3ea --- /dev/null +++ b/api/src/opentrons/protocol_engine/commands/load_lid.py @@ -0,0 +1,146 @@ +"""Load lid command request, result, and implementation models.""" +from __future__ import annotations +from pydantic import BaseModel, Field +from typing import TYPE_CHECKING, Optional, Type +from typing_extensions import Literal + +from opentrons_shared_data.labware.labware_definition import LabwareDefinition + +from ..errors import LabwareCannotBeStackedError, LabwareIsNotAllowedInLocationError +from ..resources import labware_validation +from ..types import ( + LabwareLocation, + OnLabwareLocation, +) + +from .command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, SuccessData +from ..errors.error_occurrence import ErrorOccurrence +from ..state.update_types import StateUpdate + +if TYPE_CHECKING: + from ..state.state import StateView + from ..execution import EquipmentHandler + + +LoadLidCommandType = Literal["loadLid"] + + +class LoadLidParams(BaseModel): + """Payload required to load a lid onto a labware.""" + + location: LabwareLocation = Field( + ..., + description="Labware the lid should be loaded onto.", + ) + loadName: str = Field( + ..., + description="Name used to reference a lid labware definition.", + ) + namespace: str = Field( + ..., + description="The namespace the lid labware definition belongs to.", + ) + version: int = Field( + ..., + description="The lid labware definition version.", + ) + + +class LoadLidResult(BaseModel): + """Result data from the execution of a LoadLabware command.""" + + labwareId: str = Field( + ..., + description="An ID to reference this lid labware in subsequent commands.", + ) + definition: LabwareDefinition = Field( + ..., + description="The full definition data for this lid labware.", + ) + + +class LoadLidImplementation( + AbstractCommandImpl[LoadLidParams, SuccessData[LoadLidResult]] +): + """Load lid command implementation.""" + + def __init__( + self, equipment: EquipmentHandler, state_view: StateView, **kwargs: object + ) -> None: + self._equipment = equipment + self._state_view = state_view + + async def execute(self, params: LoadLidParams) -> SuccessData[LoadLidResult]: + """Load definition and calibration data necessary for a lid.""" + if not isinstance(params.location, OnLabwareLocation): + raise LabwareIsNotAllowedInLocationError( + "Lid Labware is only allowed to be loaded on top of a labware. Try `load_lid_stack(...)` to load lids without parent labware." + ) + + verified_location = self._state_view.geometry.ensure_location_not_occupied( + params.location + ) + loaded_labware = await self._equipment.load_labware( + load_name=params.loadName, + namespace=params.namespace, + version=params.version, + location=verified_location, + labware_id=None, + ) + + # TODO(chb 2024-12-12) these validation checks happen after the labware is loaded, because they rely on + # on the definition. In practice this will not cause any issues since they will raise protocol ending + # exception, but for correctness should be refactored to do this check beforehand. + if not labware_validation.validate_definition_is_lid(loaded_labware.definition): + raise LabwareCannotBeStackedError( + f"Labware {params.loadName} is not a Lid and cannot be loaded onto {self._state_view.labware.get_display_name(params.location.labwareId)}." + ) + + state_update = StateUpdate() + + # In the case of lids being loaded on top of other labware, set the parent labware's lid + state_update.set_lid( + parent_labware_id=params.location.labwareId, + lid_id=loaded_labware.labware_id, + ) + + state_update.set_loaded_labware( + labware_id=loaded_labware.labware_id, + offset_id=loaded_labware.offsetId, + definition=loaded_labware.definition, + location=verified_location, + display_name=None, + ) + + if isinstance(verified_location, OnLabwareLocation): + self._state_view.labware.raise_if_labware_cannot_be_stacked( + top_labware_definition=loaded_labware.definition, + bottom_labware_id=verified_location.labwareId, + ) + + return SuccessData( + public=LoadLidResult( + labwareId=loaded_labware.labware_id, + definition=loaded_labware.definition, + ), + state_update=state_update, + ) + + +class LoadLid(BaseCommand[LoadLidParams, LoadLidResult, ErrorOccurrence]): + """Load lid command resource model.""" + + commandType: LoadLidCommandType = "loadLid" + params: LoadLidParams + result: Optional[LoadLidResult] = None + + _ImplementationCls: Type[LoadLidImplementation] = LoadLidImplementation + + +class LoadLidCreate(BaseCommandCreate[LoadLidParams]): + """Load lid command creation request.""" + + commandType: LoadLidCommandType = "loadLid" + params: LoadLidParams + + _CommandCls: Type[LoadLid] = LoadLid diff --git a/api/src/opentrons/protocol_engine/commands/load_lid_stack.py b/api/src/opentrons/protocol_engine/commands/load_lid_stack.py new file mode 100644 index 00000000000..1a19831b1f9 --- /dev/null +++ b/api/src/opentrons/protocol_engine/commands/load_lid_stack.py @@ -0,0 +1,189 @@ +"""Load lid stack command request, result, and implementation models.""" +from __future__ import annotations +from pydantic import BaseModel, Field +from typing import TYPE_CHECKING, Optional, Type, List +from typing_extensions import Literal + +from opentrons_shared_data.labware.labware_definition import LabwareDefinition + +from ..errors import LabwareIsNotAllowedInLocationError, ProtocolEngineError +from ..resources import fixture_validation, labware_validation +from ..types import ( + LabwareLocation, + OnLabwareLocation, + DeckSlotLocation, + AddressableAreaLocation, +) + +from .command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, SuccessData +from ..errors.error_occurrence import ErrorOccurrence +from ..state.update_types import StateUpdate + +if TYPE_CHECKING: + from ..state.state import StateView + from ..execution import EquipmentHandler + + +LoadLidStackCommandType = Literal["loadLidStack"] + +_LID_STACK_PE_LABWARE = "protocol_engine_lid_stack_object" +_LID_STACK_PE_NAMESPACE = "opentrons" +_LID_STACK_PE_VERSION = 1 + + +class LoadLidStackParams(BaseModel): + """Payload required to load a lid stack onto a location.""" + + location: LabwareLocation = Field( + ..., + description="Location the lid stack should be loaded into.", + ) + loadName: str = Field( + ..., + description="Name used to reference a lid labware definition.", + ) + namespace: str = Field( + ..., + description="The namespace the lid labware definition belongs to.", + ) + version: int = Field( + ..., + description="The lid labware definition version.", + ) + quantity: int = Field( + ..., + description="The quantity of lids to load.", + ) + + +class LoadLidStackResult(BaseModel): + """Result data from the execution of a LoadLidStack command.""" + + stackLabwareId: str = Field( + ..., + description="An ID to reference the Protocol Engine Labware Lid Stack in subsequent commands.", + ) + labwareIds: List[str] = Field( + ..., + description="A list of lid labware IDs to reference the lids in this stack by. The first ID is the bottom of the stack.", + ) + definition: LabwareDefinition = Field( + ..., + description="The full definition data for this lid labware.", + ) + location: LabwareLocation = Field( + ..., description="The Location that the stack of lid labware has been loaded." + ) + + +class LoadLidStackImplementation( + AbstractCommandImpl[LoadLidStackParams, SuccessData[LoadLidStackResult]] +): + """Load lid stack command implementation.""" + + def __init__( + self, equipment: EquipmentHandler, state_view: StateView, **kwargs: object + ) -> None: + self._equipment = equipment + self._state_view = state_view + + async def execute( + self, params: LoadLidStackParams + ) -> SuccessData[LoadLidStackResult]: + """Load definition and calibration data necessary for a lid stack.""" + if isinstance(params.location, AddressableAreaLocation): + area_name = params.location.addressableAreaName + if not ( + fixture_validation.is_deck_slot(params.location.addressableAreaName) + or fixture_validation.is_abs_reader(params.location.addressableAreaName) + ): + raise LabwareIsNotAllowedInLocationError( + f"Cannot load {params.loadName} onto addressable area {area_name}" + ) + self._state_view.addressable_areas.raise_if_area_not_in_deck_configuration( + area_name + ) + elif isinstance(params.location, DeckSlotLocation): + self._state_view.addressable_areas.raise_if_area_not_in_deck_configuration( + params.location.slotName.id + ) + + verified_location = self._state_view.geometry.ensure_location_not_occupied( + params.location + ) + + lid_stack_object = await self._equipment.load_labware( + load_name=_LID_STACK_PE_LABWARE, + namespace=_LID_STACK_PE_NAMESPACE, + version=_LID_STACK_PE_VERSION, + location=verified_location, + labware_id=None, + ) + if not labware_validation.validate_definition_is_system( + lid_stack_object.definition + ): + raise ProtocolEngineError( + message="Lid Stack Labware Object Labware Definition does not contain required allowed role 'system'." + ) + + loaded_lid_labwares = await self._equipment.load_lids( + load_name=params.loadName, + namespace=params.namespace, + version=params.version, + location=OnLabwareLocation(labwareId=lid_stack_object.labware_id), + quantity=params.quantity, + ) + loaded_lid_locations_by_id = {} + load_location = OnLabwareLocation(labwareId=lid_stack_object.labware_id) + for loaded_lid in loaded_lid_labwares: + loaded_lid_locations_by_id[loaded_lid.labware_id] = load_location + load_location = OnLabwareLocation(labwareId=loaded_lid.labware_id) + + state_update = StateUpdate() + state_update.set_loaded_lid_stack( + stack_id=lid_stack_object.labware_id, + stack_object_definition=lid_stack_object.definition, + stack_location=verified_location, + labware_ids=list(loaded_lid_locations_by_id.keys()), + labware_definition=loaded_lid_labwares[0].definition, + locations=loaded_lid_locations_by_id, + ) + + if isinstance(verified_location, OnLabwareLocation): + self._state_view.labware.raise_if_labware_cannot_be_stacked( + top_labware_definition=loaded_lid_labwares[ + params.quantity - 1 + ].definition, + bottom_labware_id=verified_location.labwareId, + ) + + return SuccessData( + public=LoadLidStackResult( + stackLabwareId=lid_stack_object.labware_id, + labwareIds=list(loaded_lid_locations_by_id.keys()), + definition=loaded_lid_labwares[0].definition, + location=params.location, + ), + state_update=state_update, + ) + + +class LoadLidStack( + BaseCommand[LoadLidStackParams, LoadLidStackResult, ErrorOccurrence] +): + """Load lid stack command resource model.""" + + commandType: LoadLidStackCommandType = "loadLidStack" + params: LoadLidStackParams + result: Optional[LoadLidStackResult] = None + + _ImplementationCls: Type[LoadLidStackImplementation] = LoadLidStackImplementation + + +class LoadLidStackCreate(BaseCommandCreate[LoadLidStackParams]): + """Load lid stack command creation request.""" + + commandType: LoadLidStackCommandType = "loadLidStack" + params: LoadLidStackParams + + _CommandCls: Type[LoadLidStack] = LoadLidStack diff --git a/api/src/opentrons/protocol_engine/commands/load_liquid.py b/api/src/opentrons/protocol_engine/commands/load_liquid.py index 5dd4737410e..a1ed1e8401c 100644 --- a/api/src/opentrons/protocol_engine/commands/load_liquid.py +++ b/api/src/opentrons/protocol_engine/commands/load_liquid.py @@ -5,6 +5,8 @@ from typing_extensions import Literal from opentrons.protocol_engine.state.update_types import StateUpdate +from opentrons.protocol_engine.types import LiquidId +from opentrons.protocol_engine.errors import InvalidLiquidError from .command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, SuccessData from ..errors.error_occurrence import ErrorOccurrence @@ -19,9 +21,9 @@ class LoadLiquidParams(BaseModel): """Payload required to load a liquid into a well.""" - liquidId: str = Field( + liquidId: LiquidId = Field( ..., - description="Unique identifier of the liquid to load.", + description="Unique identifier of the liquid to load. If this is the sentinel value EMPTY, all values of volumeByWell must be 0.", ) labwareId: str = Field( ..., @@ -29,7 +31,7 @@ class LoadLiquidParams(BaseModel): ) volumeByWell: Dict[str, float] = Field( ..., - description="Volume of liquid, in µL, loaded into each well by name, in this labware.", + description="Volume of liquid, in µL, loaded into each well by name, in this labware. If the liquid id is the sentinel value EMPTY, all volumes must be 0.", ) @@ -57,6 +59,12 @@ async def execute(self, params: LoadLiquidParams) -> SuccessData[LoadLiquidResul self._state_view.labware.validate_liquid_allowed_in_labware( labware_id=params.labwareId, wells=params.volumeByWell ) + if params.liquidId == "EMPTY": + for well_name, volume in params.volumeByWell.items(): + if volume != 0.0: + raise InvalidLiquidError( + 'loadLiquid commands that specify the special liquid "EMPTY" must set volume to be 0.0, but the volume for {well_name} is {volume}' + ) state_update = StateUpdate() state_update.set_liquid_loaded( @@ -73,7 +81,7 @@ class LoadLiquid(BaseCommand[LoadLiquidParams, LoadLiquidResult, ErrorOccurrence commandType: LoadLiquidCommandType = "loadLiquid" params: LoadLiquidParams - result: Optional[LoadLiquidResult] + result: Optional[LoadLiquidResult] = None _ImplementationCls: Type[LoadLiquidImplementation] = LoadLiquidImplementation diff --git a/api/src/opentrons/protocol_engine/commands/load_liquid_class.py b/api/src/opentrons/protocol_engine/commands/load_liquid_class.py new file mode 100644 index 00000000000..2dfcb089414 --- /dev/null +++ b/api/src/opentrons/protocol_engine/commands/load_liquid_class.py @@ -0,0 +1,144 @@ +"""LoadLiquidClass stores the liquid class settings used for a transfer into the Protocol Engine.""" +from __future__ import annotations + +from typing import Optional, Type, TYPE_CHECKING, Any + +from typing_extensions import Literal +from pydantic import BaseModel, Field +from pydantic.json_schema import SkipJsonSchema + +from .command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, SuccessData +from ..errors import LiquidClassDoesNotExistError +from ..errors.error_occurrence import ErrorOccurrence +from ..errors.exceptions import LiquidClassRedefinitionError +from ..state.update_types import LiquidClassLoadedUpdate, StateUpdate +from ..types import LiquidClassRecord + +if TYPE_CHECKING: + from ..state.state import StateView + from ..resources import ModelUtils + +LoadLiquidClassCommandType = Literal["loadLiquidClass"] + + +def _remove_default(s: dict[str, Any]) -> None: + s.pop("default", None) + + +class LoadLiquidClassParams(BaseModel): + """The liquid class transfer properties to store.""" + + liquidClassId: str | SkipJsonSchema[None] = Field( + None, + description="Unique identifier for the liquid class to store. " + "If you do not supply a liquidClassId, we will generate one.", + json_schema_extra=_remove_default, + ) + liquidClassRecord: LiquidClassRecord = Field( + ..., + description="The liquid class to store.", + ) + + +class LoadLiquidClassResult(BaseModel): + """Result from execution of LoadLiquidClass command.""" + + liquidClassId: str = Field( + ..., + description="The ID for the liquid class that was loaded, either the one you " + "supplied or the one we generated.", + ) + + +class LoadLiquidClassImplementation( + AbstractCommandImpl[LoadLiquidClassParams, SuccessData[LoadLiquidClassResult]] +): + """Load Liquid Class command implementation.""" + + def __init__( + self, state_view: StateView, model_utils: ModelUtils, **kwargs: object + ) -> None: + self._state_view = state_view + self._model_utils = model_utils + + async def execute( + self, params: LoadLiquidClassParams + ) -> SuccessData[LoadLiquidClassResult]: + """Store the liquid class in the Protocol Engine.""" + liquid_class_id: Optional[str] + already_loaded = False + + if params.liquidClassId: + liquid_class_id = params.liquidClassId + if self._liquid_class_id_already_loaded( + liquid_class_id, params.liquidClassRecord + ): + already_loaded = True + else: + liquid_class_id = ( + self._state_view.liquid_classes.get_id_for_liquid_class_record( + params.liquidClassRecord + ) # if liquidClassRecord was already loaded, reuse the existing ID + ) + if liquid_class_id: + already_loaded = True + else: + liquid_class_id = self._model_utils.generate_id() + + if already_loaded: + state_update = StateUpdate() # liquid class already loaded, do nothing + else: + state_update = StateUpdate( + liquid_class_loaded=LiquidClassLoadedUpdate( + liquid_class_id=liquid_class_id, + liquid_class_record=params.liquidClassRecord, + ) + ) + + return SuccessData( + public=LoadLiquidClassResult(liquidClassId=liquid_class_id), + state_update=state_update, + ) + + def _liquid_class_id_already_loaded( + self, liquid_class_id: str, liquid_class_record: LiquidClassRecord + ) -> bool: + """Check if the liquid_class_id has already been loaded. + + If it has, make sure that liquid_class_record matches the previously loaded definition. + """ + try: + existing_liquid_class_record = self._state_view.liquid_classes.get( + liquid_class_id + ) + except LiquidClassDoesNotExistError: + return False + + if liquid_class_record != existing_liquid_class_record: + raise LiquidClassRedefinitionError( + f"Liquid class {liquid_class_id} conflicts with previously loaded definition." + ) + return True + + +class LoadLiquidClass( + BaseCommand[LoadLiquidClassParams, LoadLiquidClassResult, ErrorOccurrence] +): + """Load Liquid Class command resource model.""" + + commandType: LoadLiquidClassCommandType = "loadLiquidClass" + params: LoadLiquidClassParams + result: Optional[LoadLiquidClassResult] = None + + _ImplementationCls: Type[ + LoadLiquidClassImplementation + ] = LoadLiquidClassImplementation + + +class LoadLiquidClassCreate(BaseCommandCreate[LoadLiquidClassParams]): + """Load Liquid Class command creation request.""" + + commandType: LoadLiquidClassCommandType = "loadLiquidClass" + params: LoadLiquidClassParams + + _CommandCls: Type[LoadLiquidClass] = LoadLiquidClass diff --git a/api/src/opentrons/protocol_engine/commands/load_module.py b/api/src/opentrons/protocol_engine/commands/load_module.py index a44212f9bf5..a0f4736a3a4 100644 --- a/api/src/opentrons/protocol_engine/commands/load_module.py +++ b/api/src/opentrons/protocol_engine/commands/load_module.py @@ -1,8 +1,11 @@ """Implementation, request models, and response models for the load module command.""" from __future__ import annotations -from typing import TYPE_CHECKING, Optional, Type +from typing import TYPE_CHECKING, Optional, Type, Any from typing_extensions import Literal from pydantic import BaseModel, Field +from pydantic.json_schema import SkipJsonSchema + +from opentrons.protocol_engine.state.update_types import StateUpdate from .command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, SuccessData from ..errors.error_occurrence import ErrorOccurrence @@ -25,6 +28,10 @@ LoadModuleCommandType = Literal["loadModule"] +def _remove_default(s: dict[str, Any]) -> None: + s.pop("default", None) + + class LoadModuleParams(BaseModel): """Payload required to load a module.""" @@ -57,12 +64,13 @@ class LoadModuleParams(BaseModel): ), ) - moduleId: Optional[str] = Field( + moduleId: str | SkipJsonSchema[None] = Field( None, description=( "An optional ID to assign to this module." " If None, an ID will be generated." ), + json_schema_extra=_remove_default, ) @@ -75,7 +83,7 @@ class LoadModuleResult(BaseModel): # TODO(mm, 2023-04-13): Remove this field. Jira RSS-221. definition: ModuleDefinition = Field( - deprecated=True, + json_schema_extra={"deprecated": True}, description=( "The definition of the connected module." " This field is an implementation detail. We might change or remove it without warning." @@ -116,26 +124,35 @@ def __init__( async def execute(self, params: LoadModuleParams) -> SuccessData[LoadModuleResult]: """Check that the requested module is attached and assign its identifier.""" + state_update = StateUpdate() + module_type = params.model.as_type() self._ensure_module_location(params.location.slotName, module_type) + # todo(mm, 2024-12-03): Theoretically, we should be able to deal with + # addressable areas and deck configurations the same way between OT-2 and Flex. + # Can this be simplified? 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, + addressable_area_provided_by_module = ( + self._state_view.modules.ensure_and_convert_module_fixture_location( + deck_slot=params.location.slotName, + model=params.model, + ) ) self._state_view.addressable_areas.raise_if_area_not_in_deck_configuration( - addressable_area + addressable_area_provided_by_module ) verified_location = self._state_view.geometry.ensure_location_not_occupied( params.location ) + state_update.set_addressable_area_used( + addressable_area_name=params.location.slotName.id + ) if params.model == ModuleModel.MAGNETIC_BLOCK_V1: loaded_module = await self._equipment.load_magnetic_block( @@ -157,11 +174,15 @@ async def execute(self, params: LoadModuleParams) -> SuccessData[LoadModuleResul model=loaded_module.definition.model, definition=loaded_module.definition, ), + state_update=state_update, ) def _ensure_module_location( self, slot: DeckSlotName, module_type: ModuleType ) -> None: + # todo(mm, 2024-12-03): Theoretically, we should be able to deal with + # addressable areas and deck configurations the same way between OT-2 and Flex. + # Can this be simplified? 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"] @@ -173,7 +194,7 @@ def _ensure_module_location( 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, + self._state_view.labware.get_deck_definition(), ) cutout_id = ( self._state_view.addressable_areas.get_cutout_id_by_deck_slot_name(slot) @@ -189,7 +210,7 @@ class LoadModule(BaseCommand[LoadModuleParams, LoadModuleResult, ErrorOccurrence commandType: LoadModuleCommandType = "loadModule" params: LoadModuleParams - result: Optional[LoadModuleResult] + result: Optional[LoadModuleResult] = None _ImplementationCls: Type[LoadModuleImplementation] = LoadModuleImplementation diff --git a/api/src/opentrons/protocol_engine/commands/load_pipette.py b/api/src/opentrons/protocol_engine/commands/load_pipette.py index 7d09bf33028..812299a6da1 100644 --- a/api/src/opentrons/protocol_engine/commands/load_pipette.py +++ b/api/src/opentrons/protocol_engine/commands/load_pipette.py @@ -1,20 +1,23 @@ """Load pipette command request, result, and implementation models.""" from __future__ import annotations +from typing import TYPE_CHECKING, Optional, Type, Any + +from pydantic import BaseModel, Field +from pydantic.json_schema import SkipJsonSchema +from typing_extensions import Literal -from opentrons.protocol_engine.state.update_types import StateUpdate from opentrons_shared_data.pipette.pipette_load_name_conversions import ( convert_to_pipette_name_type, ) from opentrons_shared_data.pipette.types import PipetteGenerationType from opentrons_shared_data.robot import user_facing_robot_type from opentrons_shared_data.robot.types import RobotTypeEnum -from pydantic import BaseModel, Field -from typing import TYPE_CHECKING, Optional, Type -from typing_extensions import Literal + from opentrons_shared_data.pipette.types import PipetteNameType from opentrons.types import MountType +from opentrons.protocol_engine.state.update_types import StateUpdate from .command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, SuccessData from ..errors.error_occurrence import ErrorOccurrence from ..errors import InvalidSpecificationForRobotTypeError, InvalidLoadPipetteSpecsError @@ -27,6 +30,10 @@ LoadPipetteCommandType = Literal["loadPipette"] +def _remove_default(s: dict[str, Any]) -> None: + s.pop("default", None) + + class LoadPipetteParams(BaseModel): """Payload needed to load a pipette on to a mount.""" @@ -38,21 +45,24 @@ class LoadPipetteParams(BaseModel): ..., description="The mount the pipette should be present on.", ) - pipetteId: Optional[str] = Field( + pipetteId: str | SkipJsonSchema[None] = Field( None, description="An optional ID to assign to this pipette. If None, an ID " "will be generated.", + json_schema_extra=_remove_default, ) - tipOverlapNotAfterVersion: Optional[str] = Field( + tipOverlapNotAfterVersion: str | SkipJsonSchema[None] = Field( None, description="A version of tip overlap data to not exceed. The highest-versioned " "tip overlap data that does not exceed this version will be used. Versions are " "expressed as vN where N is an integer, counting up from v0. If None, the current " "highest version will be used.", + json_schema_extra=_remove_default, ) - liquidPresenceDetection: Optional[bool] = Field( + liquidPresenceDetection: bool | SkipJsonSchema[None] = Field( None, description="Enable liquid presence detection for this pipette. Defaults to False.", + json_schema_extra=_remove_default, ) @@ -127,6 +137,7 @@ async def execute( serial_number=loaded_pipette.serial_number, config=loaded_pipette.static_config, ) + state_update.set_fluid_unknown(pipette_id=loaded_pipette.pipette_id) return SuccessData( public=LoadPipetteResult(pipetteId=loaded_pipette.pipette_id), @@ -139,7 +150,7 @@ class LoadPipette(BaseCommand[LoadPipetteParams, LoadPipetteResult, ErrorOccurre commandType: LoadPipetteCommandType = "loadPipette" params: LoadPipetteParams - result: Optional[LoadPipetteResult] + result: Optional[LoadPipetteResult] = None _ImplementationCls: Type[LoadPipetteImplementation] = LoadPipetteImplementation diff --git a/api/src/opentrons/protocol_engine/commands/magnetic_module/disengage.py b/api/src/opentrons/protocol_engine/commands/magnetic_module/disengage.py index c20b18e481d..d0c7abef5a4 100644 --- a/api/src/opentrons/protocol_engine/commands/magnetic_module/disengage.py +++ b/api/src/opentrons/protocol_engine/commands/magnetic_module/disengage.py @@ -83,7 +83,7 @@ class Disengage(BaseCommand[DisengageParams, DisengageResult, ErrorOccurrence]): commandType: DisengageCommandType = "magneticModule/disengage" params: DisengageParams - result: Optional[DisengageResult] + result: Optional[DisengageResult] = None _ImplementationCls: Type[DisengageImplementation] = DisengageImplementation diff --git a/api/src/opentrons/protocol_engine/commands/magnetic_module/engage.py b/api/src/opentrons/protocol_engine/commands/magnetic_module/engage.py index 62f4e24eef4..6686d88edba 100644 --- a/api/src/opentrons/protocol_engine/commands/magnetic_module/engage.py +++ b/api/src/opentrons/protocol_engine/commands/magnetic_module/engage.py @@ -105,7 +105,7 @@ class Engage(BaseCommand[EngageParams, EngageResult, ErrorOccurrence]): commandType: EngageCommandType = "magneticModule/engage" params: EngageParams - result: Optional[EngageResult] + result: Optional[EngageResult] = None _ImplementationCls: Type[EngageImplementation] = EngageImplementation diff --git a/api/src/opentrons/protocol_engine/commands/move_labware.py b/api/src/opentrons/protocol_engine/commands/move_labware.py index 09cdc08561c..5b3d751bbfb 100644 --- a/api/src/opentrons/protocol_engine/commands/move_labware.py +++ b/api/src/opentrons/protocol_engine/commands/move_labware.py @@ -1,14 +1,17 @@ """Models and implementation for the ``moveLabware`` command.""" from __future__ import annotations +from typing import TYPE_CHECKING, Optional, Type, Any + +from pydantic.json_schema import SkipJsonSchema +from pydantic import BaseModel, Field +from typing_extensions import Literal + from opentrons_shared_data.errors.exceptions import ( FailedGripperPickupError, LabwareDroppedError, StallOrCollisionDetectedError, ) -from pydantic import BaseModel, Field -from typing import TYPE_CHECKING, Optional, Type -from typing_extensions import Literal from opentrons.protocol_engine.resources.model_utils import ModelUtils from opentrons.types import Point @@ -49,6 +52,10 @@ MoveLabwareCommandType = Literal["moveLabware"] +def _remove_default(s: dict[str, Any]) -> None: + s.pop("default", None) + + # Extra buffer on top of minimum distance to move to the right _TRASH_CHUTE_DROP_BUFFER_MM = 8 @@ -63,15 +70,17 @@ class MoveLabwareParams(BaseModel): description="Whether to use the gripper to perform the labware movement" " or to perform a manual movement with an option to pause.", ) - pickUpOffset: Optional[LabwareOffsetVector] = Field( + pickUpOffset: LabwareOffsetVector | SkipJsonSchema[None] = Field( None, description="Offset to use when picking up labware. " "Experimental param, subject to change", + json_schema_extra=_remove_default, ) - dropOffset: Optional[LabwareOffsetVector] = Field( + dropOffset: LabwareOffsetVector | SkipJsonSchema[None] = Field( None, description="Offset to use when dropping off labware. " "Experimental param, subject to change", + json_schema_extra=_remove_default, ) @@ -156,6 +165,7 @@ async def execute(self, params: MoveLabwareParams) -> _ExecuteReturn: # noqa: C self._state_view.addressable_areas.raise_if_area_not_in_deck_configuration( area_name ) + state_update.set_addressable_area_used(addressable_area_name=area_name) if fixture_validation.is_gripper_waste_chute(area_name): # When dropping off labware in the waste chute, some bigger pieces @@ -201,6 +211,9 @@ async def execute(self, params: MoveLabwareParams) -> _ExecuteReturn: # noqa: C self._state_view.addressable_areas.raise_if_area_not_in_deck_configuration( params.newLocation.slotName.id ) + state_update.set_addressable_area_used( + addressable_area_name=params.newLocation.slotName.id + ) available_new_location = self._state_view.geometry.ensure_location_not_occupied( location=params.newLocation @@ -210,6 +223,15 @@ async def execute(self, params: MoveLabwareParams) -> _ExecuteReturn: # noqa: C self._state_view.labware.raise_if_labware_has_labware_on_top( labware_id=params.labwareId ) + + if isinstance(available_new_location, DeckSlotLocation): + self._state_view.labware.raise_if_labware_cannot_be_ondeck( + location=available_new_location, + labware_definition=self._state_view.labware.get_definition( + params.labwareId + ), + ) + if isinstance(available_new_location, OnLabwareLocation): self._state_view.labware.raise_if_labware_has_labware_on_top( available_new_location.labwareId @@ -355,7 +377,7 @@ class MoveLabware( commandType: MoveLabwareCommandType = "moveLabware" params: MoveLabwareParams - result: Optional[MoveLabwareResult] + result: Optional[MoveLabwareResult] = None _ImplementationCls: Type[MoveLabwareImplementation] = MoveLabwareImplementation diff --git a/api/src/opentrons/protocol_engine/commands/move_relative.py b/api/src/opentrons/protocol_engine/commands/move_relative.py index 9133725727d..d54f9470184 100644 --- a/api/src/opentrons/protocol_engine/commands/move_relative.py +++ b/api/src/opentrons/protocol_engine/commands/move_relative.py @@ -5,14 +5,23 @@ from typing_extensions import Literal -from ..state import update_types -from ..types import MovementAxis, DeckPoint -from .command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, SuccessData -from ..errors.error_occurrence import ErrorOccurrence -from .pipetting_common import DestinationPositionResult +from ..types import MovementAxis +from .command import ( + AbstractCommandImpl, + BaseCommand, + BaseCommandCreate, + SuccessData, + DefinedErrorData, +) +from .movement_common import ( + DestinationPositionResult, + move_relative, + StallOrCollisionError, +) if TYPE_CHECKING: from ..execution import MovementHandler + from ..resources.model_utils import ModelUtils MoveRelativeCommandType = Literal["moveRelative"] @@ -39,46 +48,47 @@ class MoveRelativeResult(DestinationPositionResult): class MoveRelativeImplementation( - AbstractCommandImpl[MoveRelativeParams, SuccessData[MoveRelativeResult]] + AbstractCommandImpl[ + MoveRelativeParams, + SuccessData[MoveRelativeResult] | DefinedErrorData[StallOrCollisionError], + ] ): """Move relative command implementation.""" - def __init__(self, movement: MovementHandler, **kwargs: object) -> None: + def __init__( + self, movement: MovementHandler, model_utils: ModelUtils, **kwargs: object + ) -> None: self._movement = movement + self._model_utils = model_utils async def execute( self, params: MoveRelativeParams - ) -> SuccessData[MoveRelativeResult]: + ) -> SuccessData[MoveRelativeResult] | DefinedErrorData[StallOrCollisionError]: """Move (jog) a given pipette a relative distance.""" - state_update = update_types.StateUpdate() - - x, y, z = await self._movement.move_relative( + result = await move_relative( + movement=self._movement, + model_utils=self._model_utils, pipette_id=params.pipetteId, axis=params.axis, distance=params.distance, ) - deck_point = DeckPoint.construct(x=x, y=y, z=z) - state_update.pipette_location = update_types.PipetteLocationUpdate( - pipette_id=params.pipetteId, - # TODO(jbl 2023-02-14): Need to investigate whether move relative should clear current location - new_location=update_types.NO_CHANGE, - new_deck_point=deck_point, - ) - - return SuccessData( - public=MoveRelativeResult(position=deck_point), - state_update=state_update, - ) + if isinstance(result, DefinedErrorData): + return result + else: + return SuccessData( + public=MoveRelativeResult(position=result.public.position), + state_update=result.state_update, + ) class MoveRelative( - BaseCommand[MoveRelativeParams, MoveRelativeResult, ErrorOccurrence] + BaseCommand[MoveRelativeParams, MoveRelativeResult, StallOrCollisionError] ): """Command to move (jog) a given pipette a relative distance.""" commandType: MoveRelativeCommandType = "moveRelative" params: MoveRelativeParams - result: Optional[MoveRelativeResult] + result: Optional[MoveRelativeResult] = None _ImplementationCls: Type[MoveRelativeImplementation] = MoveRelativeImplementation diff --git a/api/src/opentrons/protocol_engine/commands/move_to_addressable_area.py b/api/src/opentrons/protocol_engine/commands/move_to_addressable_area.py index 8247f54a266..2ac788b33cf 100644 --- a/api/src/opentrons/protocol_engine/commands/move_to_addressable_area.py +++ b/api/src/opentrons/protocol_engine/commands/move_to_addressable_area.py @@ -7,20 +7,29 @@ from opentrons_shared_data.pipette.types import PipetteNameType from ..errors import LocationNotAccessibleByPipetteError -from ..state import update_types -from ..types import DeckPoint, AddressableOffsetVector +from ..types import AddressableOffsetVector from ..resources import fixture_validation from .pipetting_common import ( PipetteIdMixin, +) +from .movement_common import ( MovementMixin, DestinationPositionResult, + move_to_addressable_area, + StallOrCollisionError, +) +from .command import ( + AbstractCommandImpl, + BaseCommand, + BaseCommandCreate, + SuccessData, + DefinedErrorData, ) -from .command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, SuccessData -from ..errors.error_occurrence import ErrorOccurrence if TYPE_CHECKING: from ..execution import MovementHandler from ..state.state import StateView + from ..resources.model_utils import ModelUtils MoveToAddressableAreaCommandType = Literal["moveToAddressableArea"] @@ -74,25 +83,29 @@ class MoveToAddressableAreaResult(DestinationPositionResult): pass +_ExecuteReturn = ( + SuccessData[MoveToAddressableAreaResult] | DefinedErrorData[StallOrCollisionError] +) + + class MoveToAddressableAreaImplementation( - AbstractCommandImpl[ - MoveToAddressableAreaParams, SuccessData[MoveToAddressableAreaResult] - ] + AbstractCommandImpl[MoveToAddressableAreaParams, _ExecuteReturn] ): """Move to addressable area command implementation.""" def __init__( - self, movement: MovementHandler, state_view: StateView, **kwargs: object + self, + movement: MovementHandler, + state_view: StateView, + model_utils: ModelUtils, + **kwargs: object, ) -> None: self._movement = movement self._state_view = state_view + self._model_utils = model_utils - async def execute( - self, params: MoveToAddressableAreaParams - ) -> SuccessData[MoveToAddressableAreaResult]: + async def execute(self, params: MoveToAddressableAreaParams) -> _ExecuteReturn: """Move the requested pipette to the requested addressable area.""" - state_update = update_types.StateUpdate() - self._state_view.addressable_areas.raise_if_area_not_in_deck_configuration( params.addressableAreaName ) @@ -115,7 +128,9 @@ async def execute( f"Cannot move pipette to staging slot {params.addressableAreaName}" ) - x, y, z = await self._movement.move_to_addressable_area( + result = await move_to_addressable_area( + movement=self._movement, + model_utils=self._model_utils, pipette_id=params.pipetteId, addressable_area_name=params.addressableAreaName, offset=params.offset, @@ -125,29 +140,27 @@ async def execute( stay_at_highest_possible_z=params.stayAtHighestPossibleZ, highest_possible_z_extra_offset=extra_z_offset, ) - deck_point = DeckPoint.construct(x=x, y=y, z=z) - state_update.set_pipette_location( - pipette_id=params.pipetteId, - new_addressable_area_name=params.addressableAreaName, - new_deck_point=deck_point, - ) - - return SuccessData( - public=MoveToAddressableAreaResult(position=DeckPoint(x=x, y=y, z=z)), - state_update=state_update, - ) + if isinstance(result, DefinedErrorData): + return result + else: + return SuccessData( + public=MoveToAddressableAreaResult(position=result.public.position), + state_update=result.state_update, + ) class MoveToAddressableArea( BaseCommand[ - MoveToAddressableAreaParams, MoveToAddressableAreaResult, ErrorOccurrence + MoveToAddressableAreaParams, + MoveToAddressableAreaResult, + StallOrCollisionError, ] ): """Move to addressable area command model.""" commandType: MoveToAddressableAreaCommandType = "moveToAddressableArea" params: MoveToAddressableAreaParams - result: Optional[MoveToAddressableAreaResult] + result: Optional[MoveToAddressableAreaResult] = None _ImplementationCls: Type[ MoveToAddressableAreaImplementation diff --git a/api/src/opentrons/protocol_engine/commands/move_to_addressable_area_for_drop_tip.py b/api/src/opentrons/protocol_engine/commands/move_to_addressable_area_for_drop_tip.py index 1c151f1e605..f4afcd5d1ff 100644 --- a/api/src/opentrons/protocol_engine/commands/move_to_addressable_area_for_drop_tip.py +++ b/api/src/opentrons/protocol_engine/commands/move_to_addressable_area_for_drop_tip.py @@ -1,28 +1,43 @@ """Move to addressable area for drop tip command request, result, and implementation models.""" from __future__ import annotations -from pydantic import Field -from typing import TYPE_CHECKING, Optional, Type +from typing import TYPE_CHECKING, Optional, Type, Any from typing_extensions import Literal +from pydantic import Field +from pydantic.json_schema import SkipJsonSchema + from ..errors import LocationNotAccessibleByPipetteError -from ..state import update_types -from ..types import DeckPoint, AddressableOffsetVector +from ..types import AddressableOffsetVector from ..resources import fixture_validation from .pipetting_common import ( PipetteIdMixin, +) +from .movement_common import ( MovementMixin, DestinationPositionResult, + move_to_addressable_area, + StallOrCollisionError, +) +from .command import ( + AbstractCommandImpl, + BaseCommand, + BaseCommandCreate, + SuccessData, + DefinedErrorData, ) -from .command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, SuccessData -from ..errors.error_occurrence import ErrorOccurrence if TYPE_CHECKING: from ..execution import MovementHandler from ..state.state import StateView + from ..resources.model_utils import ModelUtils MoveToAddressableAreaForDropTipCommandType = Literal["moveToAddressableAreaForDropTip"] +def _remove_default(s: dict[str, Any]) -> None: + s.pop("default", None) + + class MoveToAddressableAreaForDropTipParams(PipetteIdMixin, MovementMixin): """Payload required to move a pipette to a specific addressable area. @@ -56,7 +71,7 @@ class MoveToAddressableAreaForDropTipParams(PipetteIdMixin, MovementMixin): AddressableOffsetVector(x=0, y=0, z=0), description="Relative offset of addressable area to move pipette's critical point.", ) - alternateDropLocation: Optional[bool] = Field( + alternateDropLocation: bool | SkipJsonSchema[None] = Field( False, description=( "Whether to alternate location where tip is dropped within the addressable area." @@ -65,8 +80,9 @@ class MoveToAddressableAreaForDropTipParams(PipetteIdMixin, MovementMixin): " labware well." " If False, the tip will be dropped at the top center of the area." ), + json_schema_extra=_remove_default, ) - ignoreTipConfiguration: Optional[bool] = Field( + ignoreTipConfiguration: bool | SkipJsonSchema[None] = Field( True, description=( "Whether to utilize the critical point of the tip configuraiton when moving to an addressable area." @@ -74,6 +90,7 @@ class MoveToAddressableAreaForDropTipParams(PipetteIdMixin, MovementMixin): " as the critical point for movement." " If False, this command will use the critical point provided by the current tip configuration." ), + json_schema_extra=_remove_default, ) @@ -83,26 +100,32 @@ class MoveToAddressableAreaForDropTipResult(DestinationPositionResult): pass +_ExecuteReturn = ( + SuccessData[MoveToAddressableAreaForDropTipResult] + | DefinedErrorData[StallOrCollisionError] +) + + class MoveToAddressableAreaForDropTipImplementation( - AbstractCommandImpl[ - MoveToAddressableAreaForDropTipParams, - SuccessData[MoveToAddressableAreaForDropTipResult], - ] + AbstractCommandImpl[MoveToAddressableAreaForDropTipParams, _ExecuteReturn] ): """Move to addressable area for drop tip command implementation.""" def __init__( - self, movement: MovementHandler, state_view: StateView, **kwargs: object + self, + movement: MovementHandler, + state_view: StateView, + model_utils: ModelUtils, + **kwargs: object, ) -> None: self._movement = movement self._state_view = state_view + self._model_utils = model_utils async def execute( self, params: MoveToAddressableAreaForDropTipParams - ) -> SuccessData[MoveToAddressableAreaForDropTipResult]: + ) -> _ExecuteReturn: """Move the requested pipette to the requested addressable area in preperation of a drop tip.""" - state_update = update_types.StateUpdate() - self._state_view.addressable_areas.raise_if_area_not_in_deck_configuration( params.addressableAreaName ) @@ -120,7 +143,9 @@ async def execute( else: offset = params.offset - x, y, z = await self._movement.move_to_addressable_area( + result = await move_to_addressable_area( + movement=self._movement, + model_utils=self._model_utils, pipette_id=params.pipetteId, addressable_area_name=params.addressableAreaName, offset=offset, @@ -129,26 +154,22 @@ async def execute( speed=params.speed, ignore_tip_configuration=params.ignoreTipConfiguration, ) - deck_point = DeckPoint.construct(x=x, y=y, z=z) - state_update.set_pipette_location( - pipette_id=params.pipetteId, - new_addressable_area_name=params.addressableAreaName, - new_deck_point=deck_point, - ) - - return SuccessData( - public=MoveToAddressableAreaForDropTipResult( - position=DeckPoint(x=x, y=y, z=z) - ), - state_update=state_update, - ) + if isinstance(result, DefinedErrorData): + return result + else: + return SuccessData( + public=MoveToAddressableAreaForDropTipResult( + position=result.public.position, + ), + state_update=result.state_update, + ) class MoveToAddressableAreaForDropTip( BaseCommand[ MoveToAddressableAreaForDropTipParams, MoveToAddressableAreaForDropTipResult, - ErrorOccurrence, + StallOrCollisionError, ] ): """Move to addressable area for drop tip command model.""" @@ -157,7 +178,7 @@ class MoveToAddressableAreaForDropTip( "moveToAddressableAreaForDropTip" ) params: MoveToAddressableAreaForDropTipParams - result: Optional[MoveToAddressableAreaForDropTipResult] + result: Optional[MoveToAddressableAreaForDropTipResult] = None _ImplementationCls: Type[ MoveToAddressableAreaForDropTipImplementation diff --git a/api/src/opentrons/protocol_engine/commands/move_to_coordinates.py b/api/src/opentrons/protocol_engine/commands/move_to_coordinates.py index d7a0919d238..3493f5d6ea6 100644 --- a/api/src/opentrons/protocol_engine/commands/move_to_coordinates.py +++ b/api/src/opentrons/protocol_engine/commands/move_to_coordinates.py @@ -6,14 +6,25 @@ from typing_extensions import Literal -from ..state import update_types from ..types import DeckPoint -from .pipetting_common import PipetteIdMixin, MovementMixin, DestinationPositionResult -from .command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, SuccessData -from ..errors.error_occurrence import ErrorOccurrence +from .pipetting_common import PipetteIdMixin +from .movement_common import ( + MovementMixin, + DestinationPositionResult, + move_to_coordinates, + StallOrCollisionError, +) +from .command import ( + AbstractCommandImpl, + BaseCommand, + BaseCommandCreate, + SuccessData, + DefinedErrorData, +) if TYPE_CHECKING: from ..execution import MovementHandler + from ..resources.model_utils import ModelUtils MoveToCoordinatesCommandType = Literal["moveToCoordinates"] @@ -34,50 +45,53 @@ class MoveToCoordinatesResult(DestinationPositionResult): pass +_ExecuteReturn = ( + SuccessData[MoveToCoordinatesResult] | DefinedErrorData[StallOrCollisionError] +) + + class MoveToCoordinatesImplementation( - AbstractCommandImpl[MoveToCoordinatesParams, SuccessData[MoveToCoordinatesResult]] + AbstractCommandImpl[MoveToCoordinatesParams, _ExecuteReturn] ): """Move to coordinates command implementation.""" def __init__( self, movement: MovementHandler, + model_utils: ModelUtils, **kwargs: object, ) -> None: self._movement = movement + self._model_utils = model_utils - async def execute( - self, params: MoveToCoordinatesParams - ) -> SuccessData[MoveToCoordinatesResult]: + async def execute(self, params: MoveToCoordinatesParams) -> _ExecuteReturn: """Move the requested pipette to the requested coordinates.""" - state_update = update_types.StateUpdate() - - x, y, z = await self._movement.move_to_coordinates( + result = await move_to_coordinates( + movement=self._movement, + model_utils=self._model_utils, pipette_id=params.pipetteId, deck_coordinates=params.coordinates, direct=params.forceDirect, additional_min_travel_z=params.minimumZHeight, speed=params.speed, ) - deck_point = DeckPoint.construct(x=x, y=y, z=z) - state_update.pipette_location = update_types.PipetteLocationUpdate( - pipette_id=params.pipetteId, new_location=None, new_deck_point=deck_point - ) - - return SuccessData( - public=MoveToCoordinatesResult(position=DeckPoint(x=x, y=y, z=z)), - state_update=state_update, - ) + if isinstance(result, DefinedErrorData): + return result + else: + return SuccessData( + public=MoveToCoordinatesResult(position=result.public.position), + state_update=result.state_update, + ) class MoveToCoordinates( - BaseCommand[MoveToCoordinatesParams, MoveToCoordinatesResult, ErrorOccurrence] + BaseCommand[MoveToCoordinatesParams, MoveToCoordinatesResult, StallOrCollisionError] ): """Move to well command model.""" commandType: MoveToCoordinatesCommandType = "moveToCoordinates" params: MoveToCoordinatesParams - result: Optional[MoveToCoordinatesResult] + result: Optional[MoveToCoordinatesResult] = None _ImplementationCls: Type[ MoveToCoordinatesImplementation diff --git a/api/src/opentrons/protocol_engine/commands/move_to_well.py b/api/src/opentrons/protocol_engine/commands/move_to_well.py index 49ab10111a4..73ebfbff638 100644 --- a/api/src/opentrons/protocol_engine/commands/move_to_well.py +++ b/api/src/opentrons/protocol_engine/commands/move_to_well.py @@ -1,23 +1,32 @@ """Move to well command request, result, and implementation models.""" + from __future__ import annotations from typing import TYPE_CHECKING, Optional, Type from typing_extensions import Literal -from ..types import DeckPoint from .pipetting_common import ( PipetteIdMixin, +) +from .movement_common import ( WellLocationMixin, MovementMixin, DestinationPositionResult, + StallOrCollisionError, + move_to_well, +) +from .command import ( + AbstractCommandImpl, + BaseCommand, + BaseCommandCreate, + SuccessData, + DefinedErrorData, ) -from .command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, SuccessData -from ..errors.error_occurrence import ErrorOccurrence -from ..state import update_types from ..errors import LabwareIsTipRackError if TYPE_CHECKING: from ..execution import MovementHandler from ..state.state import StateView + from ..resources.model_utils import ModelUtils MoveToWellCommandType = Literal["moveToWell"] @@ -35,25 +44,33 @@ class MoveToWellResult(DestinationPositionResult): class MoveToWellImplementation( - AbstractCommandImpl[MoveToWellParams, SuccessData[MoveToWellResult]] + AbstractCommandImpl[ + MoveToWellParams, + SuccessData[MoveToWellResult] | DefinedErrorData[StallOrCollisionError], + ] ): """Move to well command implementation.""" def __init__( - self, state_view: StateView, movement: MovementHandler, **kwargs: object + self, + state_view: StateView, + movement: MovementHandler, + model_utils: ModelUtils, + **kwargs: object, ) -> None: self._state_view = state_view self._movement = movement + self._model_utils = model_utils - async def execute(self, params: MoveToWellParams) -> SuccessData[MoveToWellResult]: + async def execute( + self, params: MoveToWellParams + ) -> SuccessData[MoveToWellResult] | DefinedErrorData[StallOrCollisionError]: """Move the requested pipette to the requested well.""" pipette_id = params.pipetteId labware_id = params.labwareId well_name = params.wellName well_location = params.wellLocation - state_update = update_types.StateUpdate() - if ( self._state_view.labware.is_tiprack(labware_id) and well_location.volumeOffset @@ -62,7 +79,9 @@ async def execute(self, params: MoveToWellParams) -> SuccessData[MoveToWellResul "Cannot specify a WellLocation with a volumeOffset with movement to a tip rack" ) - x, y, z = await self._movement.move_to_well( + move_result = await move_to_well( + model_utils=self._model_utils, + movement=self._movement, pipette_id=pipette_id, labware_id=labware_id, well_name=well_name, @@ -71,26 +90,23 @@ async def execute(self, params: MoveToWellParams) -> SuccessData[MoveToWellResul minimum_z_height=params.minimumZHeight, speed=params.speed, ) - deck_point = DeckPoint.construct(x=x, y=y, z=z) - state_update.set_pipette_location( - pipette_id=pipette_id, - new_labware_id=labware_id, - new_well_name=well_name, - new_deck_point=deck_point, - ) - - return SuccessData( - public=MoveToWellResult(position=deck_point), - state_update=state_update, - ) + if isinstance(move_result, DefinedErrorData): + return move_result + else: + return SuccessData( + public=MoveToWellResult(position=move_result.public.position), + state_update=move_result.state_update, + ) -class MoveToWell(BaseCommand[MoveToWellParams, MoveToWellResult, ErrorOccurrence]): +class MoveToWell( + BaseCommand[MoveToWellParams, MoveToWellResult, StallOrCollisionError] +): """Move to well command model.""" commandType: MoveToWellCommandType = "moveToWell" params: MoveToWellParams - result: Optional[MoveToWellResult] + result: Optional[MoveToWellResult] = None _ImplementationCls: Type[MoveToWellImplementation] = MoveToWellImplementation diff --git a/api/src/opentrons/protocol_engine/commands/movement_common.py b/api/src/opentrons/protocol_engine/commands/movement_common.py new file mode 100644 index 00000000000..babf70b29d9 --- /dev/null +++ b/api/src/opentrons/protocol_engine/commands/movement_common.py @@ -0,0 +1,338 @@ +"""Common movement base models.""" + +from __future__ import annotations + +from typing import Optional, Union, TYPE_CHECKING, Literal, Any + +from pydantic import BaseModel, Field +from pydantic.json_schema import SkipJsonSchema + +from opentrons_shared_data.errors import ErrorCodes +from opentrons_shared_data.errors.exceptions import StallOrCollisionDetectedError +from ..errors import ErrorOccurrence +from ..types import ( + WellLocation, + LiquidHandlingWellLocation, + DeckPoint, + CurrentWell, + MovementAxis, + AddressableOffsetVector, +) +from ..state.update_types import StateUpdate, PipetteLocationUpdate +from .command import SuccessData, DefinedErrorData + + +if TYPE_CHECKING: + from ..execution.movement import MovementHandler + from ..resources.model_utils import ModelUtils + + +def _remove_default(s: dict[str, Any]) -> None: + s.pop("default", None) + + +class WellLocationMixin(BaseModel): + """Mixin for command requests that take a location that's somewhere in a well.""" + + labwareId: str = Field( + ..., + description="Identifier of labware to use.", + ) + wellName: str = Field( + ..., + description="Name of well to use in labware.", + ) + wellLocation: WellLocation = Field( + default_factory=WellLocation, + description="Relative well location at which to perform the operation", + ) + + +class LiquidHandlingWellLocationMixin(BaseModel): + """Mixin for command requests that take a location that's somewhere in a well.""" + + labwareId: str = Field( + ..., + description="Identifier of labware to use.", + ) + wellName: str = Field( + ..., + description="Name of well to use in labware.", + ) + wellLocation: LiquidHandlingWellLocation = Field( + default_factory=LiquidHandlingWellLocation, + description="Relative well location at which to perform the operation", + ) + + +class MovementMixin(BaseModel): + """Mixin for command requests that move a pipette.""" + + minimumZHeight: float | SkipJsonSchema[None] = Field( + None, + description=( + "Optional minimal Z margin in mm." + " If this is larger than the API's default safe Z margin," + " it will make the arc higher. If it's smaller, it will have no effect." + ), + json_schema_extra=_remove_default, + ) + + forceDirect: bool = Field( + False, + description=( + "If true, moving from one labware/well to another" + " will not arc to the default safe z," + " but instead will move directly to the specified location." + " This will also force the `minimumZHeight` param to be ignored." + " A 'direct' movement is in X/Y/Z simultaneously." + ), + ) + + speed: float | SkipJsonSchema[None] = Field( + None, + description=( + "Override the travel speed in mm/s." + " This controls the straight linear speed of motion." + ), + json_schema_extra=_remove_default, + ) + + +class StallOrCollisionError(ErrorOccurrence): + """Returned when the machine detects that axis encoders are reading a different position than expected. + + All axes are stopped at the point where the error was encountered. + + The next thing to move the machine must account for the robot not having a valid estimate + of its position. It should be a `home` or `unsafe/updatePositionEstimators`. + """ + + isDefined: bool = True + errorType: Literal["stallOrCollision"] = "stallOrCollision" + + errorCode: str = ErrorCodes.STALL_OR_COLLISION_DETECTED.value.code + detail: str = ErrorCodes.STALL_OR_COLLISION_DETECTED.value.detail + + +class DestinationPositionResult(BaseModel): + """Mixin for command results that move a pipette.""" + + # todo(mm, 2024-08-02): Consider deprecating or redefining this. + # + # This is here because opentrons.protocol_engine needed it for internal bookkeeping + # and, at the time, we didn't have a way to do that without adding this to the + # public command results. Its usefulness to callers outside + # opentrons.protocol_engine is questionable because they would need to know which + # critical point is in play, and I think that can change depending on obscure + # things like labware quirks. + position: DeckPoint = Field( + DeckPoint(x=0, y=0, z=0), + description=( + "The (x,y,z) coordinates of the pipette's critical point in deck space" + " after the move was completed." + ), + ) + + +MoveToWellOperationReturn = ( + SuccessData[DestinationPositionResult] | DefinedErrorData[StallOrCollisionError] +) + + +async def move_to_well( + movement: MovementHandler, + model_utils: ModelUtils, + pipette_id: str, + labware_id: str, + well_name: str, + well_location: Optional[Union[WellLocation, LiquidHandlingWellLocation]] = None, + current_well: Optional[CurrentWell] = None, + force_direct: bool = False, + minimum_z_height: Optional[float] = None, + speed: Optional[float] = None, + operation_volume: Optional[float] = None, +) -> MoveToWellOperationReturn: + """Execute a move to well microoperation.""" + try: + position = await movement.move_to_well( + pipette_id=pipette_id, + labware_id=labware_id, + well_name=well_name, + well_location=well_location, + current_well=current_well, + force_direct=force_direct, + minimum_z_height=minimum_z_height, + speed=speed, + operation_volume=operation_volume, + ) + except StallOrCollisionDetectedError as e: + return DefinedErrorData( + public=StallOrCollisionError( + id=model_utils.generate_id(), + createdAt=model_utils.get_timestamp(), + wrappedErrors=[ + ErrorOccurrence.from_failed( + id=model_utils.generate_id(), + createdAt=model_utils.get_timestamp(), + error=e, + ) + ], + ), + state_update=StateUpdate().clear_all_pipette_locations(), + ) + else: + deck_point = DeckPoint.model_construct(x=position.x, y=position.y, z=position.z) + return SuccessData( + public=DestinationPositionResult( + position=deck_point, + ), + state_update=StateUpdate().set_pipette_location( + pipette_id=pipette_id, + new_labware_id=labware_id, + new_well_name=well_name, + new_deck_point=deck_point, + ), + ) + + +async def move_relative( + movement: MovementHandler, + model_utils: ModelUtils, + pipette_id: str, + axis: MovementAxis, + distance: float, +) -> SuccessData[DestinationPositionResult] | DefinedErrorData[StallOrCollisionError]: + """Move by a fixed displacement from the current position.""" + try: + position = await movement.move_relative(pipette_id, axis, distance) + except StallOrCollisionDetectedError as e: + return DefinedErrorData( + public=StallOrCollisionError( + id=model_utils.generate_id(), + createdAt=model_utils.get_timestamp(), + wrappedErrors=[ + ErrorOccurrence.from_failed( + id=model_utils.generate_id(), + createdAt=model_utils.get_timestamp(), + error=e, + ) + ], + ), + state_update=StateUpdate().clear_all_pipette_locations(), + ) + else: + deck_point = DeckPoint.model_construct(x=position.x, y=position.y, z=position.z) + return SuccessData( + public=DestinationPositionResult( + position=deck_point, + ), + state_update=StateUpdate().set_pipette_location( + pipette_id=pipette_id, new_deck_point=deck_point + ), + ) + + +async def move_to_addressable_area( + movement: MovementHandler, + model_utils: ModelUtils, + pipette_id: str, + addressable_area_name: str, + offset: AddressableOffsetVector, + force_direct: bool = False, + minimum_z_height: float | None = None, + speed: float | None = None, + stay_at_highest_possible_z: bool = False, + ignore_tip_configuration: bool | None = True, + highest_possible_z_extra_offset: float | None = None, +) -> SuccessData[DestinationPositionResult] | DefinedErrorData[StallOrCollisionError]: + """Move to an addressable area identified by name.""" + try: + x, y, z = await movement.move_to_addressable_area( + pipette_id=pipette_id, + addressable_area_name=addressable_area_name, + offset=offset, + force_direct=force_direct, + minimum_z_height=minimum_z_height, + speed=speed, + stay_at_highest_possible_z=stay_at_highest_possible_z, + ignore_tip_configuration=ignore_tip_configuration, + highest_possible_z_extra_offset=highest_possible_z_extra_offset, + ) + except StallOrCollisionDetectedError as e: + return DefinedErrorData( + public=StallOrCollisionError( + id=model_utils.generate_id(), + createdAt=model_utils.get_timestamp(), + wrappedErrors=[ + ErrorOccurrence.from_failed( + id=model_utils.generate_id(), + createdAt=model_utils.get_timestamp(), + error=e, + ) + ], + ), + state_update=StateUpdate() + .clear_all_pipette_locations() + .set_addressable_area_used(addressable_area_name=addressable_area_name), + ) + else: + deck_point = DeckPoint.model_construct(x=x, y=y, z=z) + return SuccessData( + public=DestinationPositionResult(position=deck_point), + state_update=StateUpdate() + .set_pipette_location( + pipette_id=pipette_id, + new_addressable_area_name=addressable_area_name, + new_deck_point=deck_point, + ) + .set_addressable_area_used(addressable_area_name=addressable_area_name), + ) + + +async def move_to_coordinates( + movement: MovementHandler, + model_utils: ModelUtils, + pipette_id: str, + deck_coordinates: DeckPoint, + direct: bool, + additional_min_travel_z: float | None, + speed: float | None = None, +) -> SuccessData[DestinationPositionResult] | DefinedErrorData[StallOrCollisionError]: + """Move to a set of coordinates.""" + try: + x, y, z = await movement.move_to_coordinates( + pipette_id=pipette_id, + deck_coordinates=deck_coordinates, + direct=direct, + additional_min_travel_z=additional_min_travel_z, + speed=speed, + ) + except StallOrCollisionDetectedError as e: + return DefinedErrorData( + public=StallOrCollisionError( + id=model_utils.generate_id(), + createdAt=model_utils.get_timestamp(), + wrappedErrors=[ + ErrorOccurrence.from_failed( + id=model_utils.generate_id(), + createdAt=model_utils.get_timestamp(), + error=e, + ) + ], + ), + state_update=StateUpdate().clear_all_pipette_locations(), + ) + else: + deck_point = DeckPoint.model_construct(x=x, y=y, z=z) + + return SuccessData( + public=DestinationPositionResult(position=DeckPoint(x=x, y=y, z=z)), + state_update=StateUpdate( + pipette_location=PipetteLocationUpdate( + pipette_id=pipette_id, + new_location=None, + new_deck_point=deck_point, + ) + ), + ) diff --git a/api/src/opentrons/protocol_engine/commands/pick_up_tip.py b/api/src/opentrons/protocol_engine/commands/pick_up_tip.py index 1c434e54f51..e5612bb3cdc 100644 --- a/api/src/opentrons/protocol_engine/commands/pick_up_tip.py +++ b/api/src/opentrons/protocol_engine/commands/pick_up_tip.py @@ -1,4 +1,5 @@ """Pick up tip command request, result, and implementation models.""" + from __future__ import annotations from opentrons_shared_data.errors import ErrorCodes from pydantic import Field @@ -9,10 +10,14 @@ from ..errors import ErrorOccurrence, PickUpTipTipNotAttachedError from ..resources import ModelUtils from ..state import update_types -from ..types import PickUpTipWellLocation, DeckPoint +from ..types import PickUpTipWellLocation from .pipetting_common import ( PipetteIdMixin, +) +from .movement_common import ( DestinationPositionResult, + StallOrCollisionError, + move_to_well, ) from .command import ( AbstractCommandImpl, @@ -87,7 +92,8 @@ class TipPhysicallyMissingError(ErrorOccurrence): _ExecuteReturn = Union[ SuccessData[PickUpTipResult], - DefinedErrorData[TipPhysicallyMissingError], + DefinedErrorData[TipPhysicallyMissingError] + | DefinedErrorData[StallOrCollisionError], ] @@ -115,24 +121,19 @@ async def execute( labware_id = params.labwareId well_name = params.wellName - state_update = update_types.StateUpdate() - well_location = self._state_view.geometry.convert_pick_up_tip_well_location( well_location=params.wellLocation ) - position = await self._movement.move_to_well( + move_result = await move_to_well( + movement=self._movement, + model_utils=self._model_utils, pipette_id=pipette_id, labware_id=labware_id, well_name=well_name, well_location=well_location, ) - deck_point = DeckPoint.construct(x=position.x, y=position.y, z=position.z) - state_update.set_pipette_location( - pipette_id=pipette_id, - new_labware_id=labware_id, - new_well_name=well_name, - new_deck_point=deck_point, - ) + if isinstance(move_result, DefinedErrorData): + return move_result try: tip_geometry = await self._tip_handler.pick_up_tip( @@ -141,13 +142,27 @@ async def execute( well_name=well_name, ) except PickUpTipTipNotAttachedError as e: - state_update_if_false_positive = update_types.StateUpdate() - state_update_if_false_positive.update_pipette_tip_state( - pipette_id=pipette_id, - tip_geometry=e.tip_geometry, + state_update_if_false_positive = ( + update_types.StateUpdate.reduce( + update_types.StateUpdate(), move_result.state_update + ) + .update_pipette_tip_state( + pipette_id=pipette_id, + tip_geometry=e.tip_geometry, + ) + .set_fluid_empty(pipette_id=pipette_id) + .mark_tips_as_used( + pipette_id=pipette_id, labware_id=labware_id, well_name=well_name + ) ) - state_update.mark_tips_as_used( - pipette_id=pipette_id, labware_id=labware_id, well_name=well_name + state_update = ( + update_types.StateUpdate.reduce( + update_types.StateUpdate(), move_result.state_update + ) + .mark_tips_as_used( + pipette_id=pipette_id, labware_id=labware_id, well_name=well_name + ) + .set_fluid_unknown(pipette_id=pipette_id) ) return DefinedErrorData( public=TipPhysicallyMissingError( @@ -165,32 +180,39 @@ async def execute( state_update_if_false_positive=state_update_if_false_positive, ) else: - state_update.update_pipette_tip_state( - pipette_id=pipette_id, - tip_geometry=tip_geometry, - ) - state_update.mark_tips_as_used( - pipette_id=pipette_id, labware_id=labware_id, well_name=well_name + state_update = ( + move_result.state_update.update_pipette_tip_state( + pipette_id=pipette_id, + tip_geometry=tip_geometry, + ) + .mark_tips_as_used( + pipette_id=pipette_id, labware_id=labware_id, well_name=well_name + ) + .set_fluid_empty(pipette_id=pipette_id) ) return SuccessData( public=PickUpTipResult( tipVolume=tip_geometry.volume, tipLength=tip_geometry.length, tipDiameter=tip_geometry.diameter, - position=deck_point, + position=move_result.public.position, ), state_update=state_update, ) class PickUpTip( - BaseCommand[PickUpTipParams, PickUpTipResult, TipPhysicallyMissingError] + BaseCommand[ + PickUpTipParams, + PickUpTipResult, + TipPhysicallyMissingError | StallOrCollisionError, + ] ): """Pick up tip command model.""" commandType: PickUpTipCommandType = "pickUpTip" params: PickUpTipParams - result: Optional[PickUpTipResult] + result: Optional[PickUpTipResult] = None _ImplementationCls: Type[PickUpTipImplementation] = PickUpTipImplementation diff --git a/api/src/opentrons/protocol_engine/commands/pipetting_common.py b/api/src/opentrons/protocol_engine/commands/pipetting_common.py index ef53b585e87..c373642a02e 100644 --- a/api/src/opentrons/protocol_engine/commands/pipetting_common.py +++ b/api/src/opentrons/protocol_engine/commands/pipetting_common.py @@ -1,11 +1,23 @@ """Common pipetting command base models.""" -from opentrons_shared_data.errors import ErrorCodes + +from __future__ import annotations +from typing import Literal, Tuple, TYPE_CHECKING + +from typing_extensions import TypedDict from pydantic import BaseModel, Field -from typing import Literal, Optional, Tuple, TypedDict +from opentrons_shared_data.errors import ErrorCodes from opentrons.protocol_engine.errors.error_occurrence import ErrorOccurrence +from opentrons.protocol_engine.types import AspiratedFluid, FluidKind +from opentrons_shared_data.errors.exceptions import PipetteOverpressureError +from .command import DefinedErrorData, SuccessData +from opentrons.protocol_engine.state.update_types import StateUpdate -from ..types import WellLocation, LiquidHandlingWellLocation, DeckPoint + +if TYPE_CHECKING: + from ..execution.pipetting import PipettingHandler + from ..resources import ModelUtils + from ..notes import CommandNoteAdder class PipetteIdMixin(BaseModel): @@ -51,72 +63,6 @@ class FlowRateMixin(BaseModel): ) -class WellLocationMixin(BaseModel): - """Mixin for command requests that take a location that's somewhere in a well.""" - - labwareId: str = Field( - ..., - description="Identifier of labware to use.", - ) - wellName: str = Field( - ..., - description="Name of well to use in labware.", - ) - wellLocation: WellLocation = Field( - default_factory=WellLocation, - description="Relative well location at which to perform the operation", - ) - - -class LiquidHandlingWellLocationMixin(BaseModel): - """Mixin for command requests that take a location that's somewhere in a well.""" - - labwareId: str = Field( - ..., - description="Identifier of labware to use.", - ) - wellName: str = Field( - ..., - description="Name of well to use in labware.", - ) - wellLocation: LiquidHandlingWellLocation = Field( - default_factory=LiquidHandlingWellLocation, - description="Relative well location at which to perform the operation", - ) - - -class MovementMixin(BaseModel): - """Mixin for command requests that move a pipette.""" - - minimumZHeight: Optional[float] = Field( - None, - description=( - "Optional minimal Z margin in mm." - " If this is larger than the API's default safe Z margin," - " it will make the arc higher. If it's smaller, it will have no effect." - ), - ) - - forceDirect: bool = Field( - False, - description=( - "If true, moving from one labware/well to another" - " will not arc to the default safe z," - " but instead will move directly to the specified location." - " This will also force the `minimumZHeight` param to be ignored." - " A 'direct' movement is in X/Y/Z simultaneously." - ), - ) - - speed: Optional[float] = Field( - None, - description=( - "Override the travel speed in mm/s." - " This controls the straight linear speed of motion." - ), - ) - - class BaseLiquidHandlingResult(BaseModel): """Base properties of a liquid handling result.""" @@ -127,24 +73,8 @@ class BaseLiquidHandlingResult(BaseModel): ) -class DestinationPositionResult(BaseModel): - """Mixin for command results that move a pipette.""" - - # todo(mm, 2024-08-02): Consider deprecating or redefining this. - # - # This is here because opentrons.protocol_engine needed it for internal bookkeeping - # and, at the time, we didn't have a way to do that without adding this to the - # public command results. Its usefulness to callers outside - # opentrons.protocol_engine is questionable because they would need to know which - # critical point is in play, and I think that can change depending on obscure - # things like labware quirks. - position: DeckPoint = Field( - DeckPoint(x=0, y=0, z=0), - description=( - "The (x,y,z) coordinates of the pipette's critical point in deck space" - " after the move was completed." - ), - ) +class EmptyResult(BaseModel): + """A result with no data.""" class ErrorLocationInfo(TypedDict): @@ -208,3 +138,155 @@ class TipPhysicallyAttachedError(ErrorOccurrence): detail: str = ErrorCodes.TIP_DROP_FAILED.value.detail errorInfo: ErrorLocationInfo + + +async def prepare_for_aspirate( + pipette_id: str, + pipetting: PipettingHandler, + model_utils: ModelUtils, + location_if_error: ErrorLocationInfo, +) -> SuccessData[EmptyResult] | DefinedErrorData[OverpressureError]: + """Execute pipetting.prepare_for_aspirate, handle errors, and marshal success.""" + try: + await pipetting.prepare_for_aspirate(pipette_id) + except PipetteOverpressureError as e: + return DefinedErrorData( + public=OverpressureError( + id=model_utils.generate_id(), + createdAt=model_utils.get_timestamp(), + wrappedErrors=[ + ErrorOccurrence.from_failed( + id=model_utils.generate_id(), + createdAt=model_utils.get_timestamp(), + error=e, + ) + ], + errorInfo=location_if_error, + ), + state_update=StateUpdate().set_fluid_unknown(pipette_id=pipette_id), + ) + else: + return SuccessData( + public=EmptyResult(), + state_update=StateUpdate().set_fluid_empty(pipette_id=pipette_id), + ) + + +async def aspirate_in_place( + pipette_id: str, + volume: float, + flow_rate: float, + location_if_error: ErrorLocationInfo, + command_note_adder: CommandNoteAdder, + pipetting: PipettingHandler, + model_utils: ModelUtils, +) -> SuccessData[BaseLiquidHandlingResult] | DefinedErrorData[OverpressureError]: + """Execute an aspirate in place microoperation.""" + try: + volume_aspirated = await pipetting.aspirate_in_place( + pipette_id=pipette_id, + volume=volume, + flow_rate=flow_rate, + command_note_adder=command_note_adder, + ) + except PipetteOverpressureError as e: + return DefinedErrorData( + public=OverpressureError( + id=model_utils.generate_id(), + createdAt=model_utils.get_timestamp(), + wrappedErrors=[ + ErrorOccurrence.from_failed( + id=model_utils.generate_id(), + createdAt=model_utils.get_timestamp(), + error=e, + ) + ], + errorInfo=location_if_error, + ), + state_update=StateUpdate().set_fluid_unknown(pipette_id=pipette_id), + ) + else: + return SuccessData( + public=BaseLiquidHandlingResult( + volume=volume_aspirated, + ), + state_update=StateUpdate().set_fluid_aspirated( + pipette_id=pipette_id, + fluid=AspiratedFluid(kind=FluidKind.LIQUID, volume=volume_aspirated), + ), + ) + + +async def dispense_in_place( + pipette_id: str, + volume: float, + flow_rate: float, + push_out: float | None, + location_if_error: ErrorLocationInfo, + pipetting: PipettingHandler, + model_utils: ModelUtils, +) -> SuccessData[BaseLiquidHandlingResult] | DefinedErrorData[OverpressureError]: + """Dispense-in-place as a microoperation.""" + try: + volume = await pipetting.dispense_in_place( + pipette_id=pipette_id, + volume=volume, + flow_rate=flow_rate, + push_out=push_out, + ) + except PipetteOverpressureError as e: + return DefinedErrorData( + public=OverpressureError( + id=model_utils.generate_id(), + createdAt=model_utils.get_timestamp(), + wrappedErrors=[ + ErrorOccurrence.from_failed( + id=model_utils.generate_id(), + createdAt=model_utils.get_timestamp(), + error=e, + ) + ], + errorInfo=location_if_error, + ), + state_update=StateUpdate().set_fluid_unknown(pipette_id=pipette_id), + ) + else: + return SuccessData( + public=BaseLiquidHandlingResult(volume=volume), + state_update=StateUpdate().set_fluid_ejected( + pipette_id=pipette_id, volume=volume + ), + ) + + +async def blow_out_in_place( + pipette_id: str, + flow_rate: float, + location_if_error: ErrorLocationInfo, + pipetting: PipettingHandler, + model_utils: ModelUtils, +) -> SuccessData[EmptyResult] | DefinedErrorData[OverpressureError]: + """Execute a blow-out-in-place micro-operation.""" + try: + await pipetting.blow_out_in_place(pipette_id=pipette_id, flow_rate=flow_rate) + except PipetteOverpressureError as e: + return DefinedErrorData( + public=OverpressureError( + id=model_utils.generate_id(), + createdAt=model_utils.get_timestamp(), + wrappedErrors=[ + ErrorOccurrence.from_failed( + id=model_utils.generate_id(), + createdAt=model_utils.get_timestamp(), + error=e, + ) + ], + errorInfo=location_if_error, + ), + state_update=StateUpdate().set_fluid_unknown(pipette_id=pipette_id), + ) + else: + return SuccessData( + public=EmptyResult(), + state_update=StateUpdate().set_fluid_empty(pipette_id=pipette_id), + ) diff --git a/api/src/opentrons/protocol_engine/commands/prepare_to_aspirate.py b/api/src/opentrons/protocol_engine/commands/prepare_to_aspirate.py index 01012be1d7f..beaf6e1ca0c 100644 --- a/api/src/opentrons/protocol_engine/commands/prepare_to_aspirate.py +++ b/api/src/opentrons/protocol_engine/commands/prepare_to_aspirate.py @@ -1,15 +1,11 @@ """Prepare to aspirate command request, result, and implementation models.""" from __future__ import annotations -from opentrons_shared_data.errors.exceptions import PipetteOverpressureError from pydantic import BaseModel from typing import TYPE_CHECKING, Optional, Type, Union from typing_extensions import Literal -from .pipetting_common import ( - OverpressureError, - PipetteIdMixin, -) +from .pipetting_common import OverpressureError, PipetteIdMixin, prepare_for_aspirate from .command import ( AbstractCommandImpl, BaseCommand, @@ -61,39 +57,34 @@ def __init__( self._model_utils = model_utils self._gantry_mover = gantry_mover + def _transform_result( + self, result: SuccessData[BaseModel] + ) -> SuccessData[PrepareToAspirateResult]: + return SuccessData( + public=PrepareToAspirateResult(), state_update=result.state_update + ) + async def execute(self, params: PrepareToAspirateParams) -> _ExecuteReturn: """Prepare the pipette to aspirate.""" current_position = await self._gantry_mover.get_position(params.pipetteId) - try: - await self._pipetting_handler.prepare_for_aspirate( - pipette_id=params.pipetteId, - ) - except PipetteOverpressureError as e: - return DefinedErrorData( - public=OverpressureError( - id=self._model_utils.generate_id(), - createdAt=self._model_utils.get_timestamp(), - wrappedErrors=[ - ErrorOccurrence.from_failed( - id=self._model_utils.generate_id(), - createdAt=self._model_utils.get_timestamp(), - error=e, - ) - ], - errorInfo=( - { - "retryLocation": ( - current_position.x, - current_position.y, - current_position.z, - ) - } - ), - ), - ) + prepare_result = await prepare_for_aspirate( + pipette_id=params.pipetteId, + pipetting=self._pipetting_handler, + model_utils=self._model_utils, + location_if_error={ + "retryLocation": ( + current_position.x, + current_position.y, + current_position.z, + ) + }, + ) + if isinstance(prepare_result, DefinedErrorData): + return prepare_result else: return SuccessData( public=PrepareToAspirateResult(), + state_update=prepare_result.state_update, ) @@ -104,7 +95,7 @@ class PrepareToAspirate( commandType: PrepareToAspirateCommandType = "prepareToAspirate" params: PrepareToAspirateParams - result: Optional[PrepareToAspirateResult] + result: Optional[PrepareToAspirateResult] = None _ImplementationCls: Type[ PrepareToAspirateImplementation diff --git a/api/src/opentrons/protocol_engine/commands/reload_labware.py b/api/src/opentrons/protocol_engine/commands/reload_labware.py index 60230a1c6dd..07d70957cdb 100644 --- a/api/src/opentrons/protocol_engine/commands/reload_labware.py +++ b/api/src/opentrons/protocol_engine/commands/reload_labware.py @@ -89,7 +89,7 @@ class ReloadLabware( commandType: ReloadLabwareCommandType = "reloadLabware" params: ReloadLabwareParams - result: Optional[ReloadLabwareResult] + result: Optional[ReloadLabwareResult] = None _ImplementationCls: Type[ReloadLabwareImplementation] = ReloadLabwareImplementation diff --git a/api/src/opentrons/protocol_engine/commands/retract_axis.py b/api/src/opentrons/protocol_engine/commands/retract_axis.py index 49020eb517e..19c7653793f 100644 --- a/api/src/opentrons/protocol_engine/commands/retract_axis.py +++ b/api/src/opentrons/protocol_engine/commands/retract_axis.py @@ -61,7 +61,7 @@ class RetractAxis(BaseCommand[RetractAxisParams, RetractAxisResult, ErrorOccurre commandType: RetractAxisCommandType = "retractAxis" params: RetractAxisParams - result: Optional[RetractAxisResult] + result: Optional[RetractAxisResult] = None _ImplementationCls: Type[RetractAxisImplementation] = RetractAxisImplementation diff --git a/api/src/opentrons/protocol_engine/commands/robot/__init__.py b/api/src/opentrons/protocol_engine/commands/robot/__init__.py index ee78c1d4044..048fecd09fe 100644 --- a/api/src/opentrons/protocol_engine/commands/robot/__init__.py +++ b/api/src/opentrons/protocol_engine/commands/robot/__init__.py @@ -1 +1,70 @@ """Robot movement commands.""" + +from .move_to import ( + MoveTo, + MoveToCreate, + MoveToParams, + MoveToResult, + MoveToCommandType, +) +from .move_axes_to import ( + MoveAxesTo, + MoveAxesToCreate, + MoveAxesToParams, + MoveAxesToResult, + MoveAxesToCommandType, +) +from .move_axes_relative import ( + MoveAxesRelative, + MoveAxesRelativeCreate, + MoveAxesRelativeParams, + MoveAxesRelativeResult, + MoveAxesRelativeCommandType, +) +from .open_gripper_jaw import ( + openGripperJaw, + openGripperJawCreate, + openGripperJawParams, + openGripperJawResult, + openGripperJawCommandType, +) +from .close_gripper_jaw import ( + closeGripperJaw, + closeGripperJawCreate, + closeGripperJawParams, + closeGripperJawResult, + closeGripperJawCommandType, +) + +__all__ = [ + # robot/moveTo + "MoveTo", + "MoveToCreate", + "MoveToParams", + "MoveToResult", + "MoveToCommandType", + # robot/moveAxesTo + "MoveAxesTo", + "MoveAxesToCreate", + "MoveAxesToParams", + "MoveAxesToResult", + "MoveAxesToCommandType", + # robot/moveAxesRelative + "MoveAxesRelative", + "MoveAxesRelativeCreate", + "MoveAxesRelativeParams", + "MoveAxesRelativeResult", + "MoveAxesRelativeCommandType", + # robot/openGripperJaw + "openGripperJaw", + "openGripperJawCreate", + "openGripperJawParams", + "openGripperJawResult", + "openGripperJawCommandType", + # robot/closeGripperJaw + "closeGripperJaw", + "closeGripperJawCreate", + "closeGripperJawParams", + "closeGripperJawResult", + "closeGripperJawCommandType", +] diff --git a/api/src/opentrons/protocol_engine/commands/robot/close_gripper_jaw.py b/api/src/opentrons/protocol_engine/commands/robot/close_gripper_jaw.py new file mode 100644 index 00000000000..5ff11891a1b --- /dev/null +++ b/api/src/opentrons/protocol_engine/commands/robot/close_gripper_jaw.py @@ -0,0 +1,86 @@ +"""Command models for opening a gripper jaw.""" +from __future__ import annotations +from typing import Literal, Type, Optional, Any + +from pydantic import BaseModel, Field +from pydantic.json_schema import SkipJsonSchema + +from opentrons.hardware_control import HardwareControlAPI +from opentrons.protocol_engine.resources import ensure_ot3_hardware + +from ..command import ( + AbstractCommandImpl, + BaseCommand, + BaseCommandCreate, + SuccessData, +) +from opentrons.protocol_engine.errors.error_occurrence import ErrorOccurrence + + +closeGripperJawCommandType = Literal["robot/closeGripperJaw"] + + +def _remove_default(s: dict[str, Any]) -> None: + s.pop("default", None) + + +class closeGripperJawParams(BaseModel): + """Payload required to close a gripper.""" + + force: float | SkipJsonSchema[None] = Field( + default=None, + description="The force the gripper should use to hold the jaws, falls to default if none is provided.", + json_schema_extra=_remove_default, + ) + + +class closeGripperJawResult(BaseModel): + """Result data from the execution of a closeGripperJaw command.""" + + pass + + +class closeGripperJawImplementation( + AbstractCommandImpl[closeGripperJawParams, SuccessData[closeGripperJawResult]] +): + """closeGripperJaw command implementation.""" + + def __init__( + self, + hardware_api: HardwareControlAPI, + **kwargs: object, + ) -> None: + self._hardware_api = hardware_api + + async def execute( + self, params: closeGripperJawParams + ) -> SuccessData[closeGripperJawResult]: + """Release the gripper.""" + ot3_hardware_api = ensure_ot3_hardware(self._hardware_api) + await ot3_hardware_api.grip(force_newtons=params.force) + return SuccessData( + public=closeGripperJawResult(), + ) + + +class closeGripperJaw( + BaseCommand[closeGripperJawParams, closeGripperJawResult, ErrorOccurrence] +): + """closeGripperJaw command model.""" + + commandType: closeGripperJawCommandType = "robot/closeGripperJaw" + params: closeGripperJawParams + result: Optional[closeGripperJawResult] = None + + _ImplementationCls: Type[ + closeGripperJawImplementation + ] = closeGripperJawImplementation + + +class closeGripperJawCreate(BaseCommandCreate[closeGripperJawParams]): + """closeGripperJaw command request model.""" + + commandType: closeGripperJawCommandType = "robot/closeGripperJaw" + params: closeGripperJawParams + + _CommandCls: Type[closeGripperJaw] = closeGripperJaw diff --git a/api/src/opentrons/protocol_engine/commands/robot/common.py b/api/src/opentrons/protocol_engine/commands/robot/common.py new file mode 100644 index 00000000000..1cd0b0d17b3 --- /dev/null +++ b/api/src/opentrons/protocol_engine/commands/robot/common.py @@ -0,0 +1,18 @@ +"""Shared result types for robot API commands.""" +from pydantic import BaseModel, Field + +from typing import Dict +from opentrons.protocol_engine.types import MotorAxis + + +MotorAxisMapType = Dict[MotorAxis, float] +default_position = {ax: 0.0 for ax in MotorAxis} + + +class DestinationRobotPositionResult(BaseModel): + """The result dictionary of `MotorAxis` type.""" + + position: MotorAxisMapType = Field( + default=default_position, + description="The position of all axes on the robot. If no mount was provided, the last moved mount is used to determine the location.", + ) diff --git a/api/src/opentrons/protocol_engine/commands/robot/move_axes_relative.py b/api/src/opentrons/protocol_engine/commands/robot/move_axes_relative.py new file mode 100644 index 00000000000..c334e4017ce --- /dev/null +++ b/api/src/opentrons/protocol_engine/commands/robot/move_axes_relative.py @@ -0,0 +1,101 @@ +"""Command models for moving any robot axis relative.""" + +from __future__ import annotations +from typing import Literal, Type, Optional, TYPE_CHECKING, Any + +from pydantic import BaseModel, Field +from pydantic.json_schema import SkipJsonSchema +from opentrons.hardware_control import HardwareControlAPI +from opentrons.protocol_engine.resources import ensure_ot3_hardware + +from .common import MotorAxisMapType, DestinationRobotPositionResult + +from ..command import ( + AbstractCommandImpl, + BaseCommand, + BaseCommandCreate, + SuccessData, +) +from ...errors.error_occurrence import ErrorOccurrence + +if TYPE_CHECKING: + from opentrons.protocol_engine.execution import GantryMover + + +MoveAxesRelativeCommandType = Literal["robot/moveAxesRelative"] + + +def _remove_default(s: dict[str, Any]) -> None: + s.pop("default", None) + + +class MoveAxesRelativeParams(BaseModel): + """Payload required to move axes relative to position.""" + + axis_map: MotorAxisMapType = Field( + ..., description="A dictionary mapping axes to relative movements in mm." + ) + speed: float | SkipJsonSchema[None] = Field( + default=None, + description="The max velocity to move the axes at. Will fall to hardware defaults if none provided.", + json_schema_extra=_remove_default, + ) + + +class MoveAxesRelativeResult(DestinationRobotPositionResult): + """Result data from the execution of a MoveAxesRelative command.""" + + pass + + +class MoveAxesRelativeImplementation( + AbstractCommandImpl[MoveAxesRelativeParams, SuccessData[MoveAxesRelativeResult]] +): + """MoveAxesRelative command implementation.""" + + def __init__( + self, + gantry_mover: GantryMover, + hardware_api: HardwareControlAPI, + **kwargs: object, + ) -> None: + self._gantry_mover = gantry_mover + self._hardware_api = hardware_api + + async def execute( + self, params: MoveAxesRelativeParams + ) -> SuccessData[MoveAxesRelativeResult]: + """Move the axes on a flex a relative distance.""" + # TODO (lc 08-16-2024) implement `move_axes` for OT 2 hardware controller + # and then we can remove this validation. + ensure_ot3_hardware(self._hardware_api) + + current_position = await self._gantry_mover.move_axes( + axis_map=params.axis_map, speed=params.speed, relative_move=True + ) + return SuccessData( + public=MoveAxesRelativeResult(position=current_position), + ) + + +class MoveAxesRelative( + BaseCommand[MoveAxesRelativeParams, MoveAxesRelativeResult, ErrorOccurrence] +): + """MoveAxesRelative command model.""" + + commandType: MoveAxesRelativeCommandType = "robot/moveAxesRelative" + params: MoveAxesRelativeParams + result: Optional[MoveAxesRelativeResult] = None + + _ImplementationCls: Type[ + MoveAxesRelativeImplementation + ] = MoveAxesRelativeImplementation + + +class MoveAxesRelativeCreate(BaseCommandCreate[MoveAxesRelativeParams]): + """MoveAxesRelative command request model.""" + + commandType: MoveAxesRelativeCommandType = "robot/moveAxesRelative" + params: MoveAxesRelativeParams + + _CommandCls: Type[MoveAxesRelative] = MoveAxesRelative diff --git a/api/src/opentrons/protocol_engine/commands/robot/move_axes_to.py b/api/src/opentrons/protocol_engine/commands/robot/move_axes_to.py new file mode 100644 index 00000000000..81c4462af57 --- /dev/null +++ b/api/src/opentrons/protocol_engine/commands/robot/move_axes_to.py @@ -0,0 +1,100 @@ +"""Command models for moving any robot axis to an absolute position.""" +from __future__ import annotations +from typing import Literal, Optional, Type, TYPE_CHECKING, Any + +from pydantic import Field, BaseModel +from pydantic.json_schema import SkipJsonSchema + +from opentrons.hardware_control import HardwareControlAPI +from opentrons.protocol_engine.resources import ensure_ot3_hardware + +from .common import MotorAxisMapType, DestinationRobotPositionResult +from ..command import ( + AbstractCommandImpl, + BaseCommand, + BaseCommandCreate, + SuccessData, +) +from ...errors.error_occurrence import ErrorOccurrence + +if TYPE_CHECKING: + from opentrons.protocol_engine.execution import GantryMover + + +MoveAxesToCommandType = Literal["robot/moveAxesTo"] + + +def _remove_default(s: dict[str, Any]) -> None: + s.pop("default", None) + + +class MoveAxesToParams(BaseModel): + """Payload required to move axes to absolute position.""" + + axis_map: MotorAxisMapType = Field( + ..., description="The specified axes to move to an absolute deck position with." + ) + critical_point: MotorAxisMapType | SkipJsonSchema[None] = Field( + default=None, + description="The critical point to move the mount with.", + json_schema_extra=_remove_default, + ) + speed: float | SkipJsonSchema[None] = Field( + default=None, + description="The max velocity to move the axes at. Will fall to hardware defaults if none provided.", + json_schema_extra=_remove_default, + ) + + +class MoveAxesToResult(DestinationRobotPositionResult): + """Result data from the execution of a MoveAxesTo command.""" + + pass + + +class MoveAxesToImplementation( + AbstractCommandImpl[MoveAxesToParams, SuccessData[MoveAxesToResult]] +): + """MoveAxesTo command implementation.""" + + def __init__( + self, + gantry_mover: GantryMover, + hardware_api: HardwareControlAPI, + **kwargs: object, + ) -> None: + self._gantry_mover = gantry_mover + self._hardware_api = hardware_api + + async def execute(self, params: MoveAxesToParams) -> SuccessData[MoveAxesToResult]: + """Move the axes on a flex an absolute distance.""" + # TODO (lc 08-16-2024) implement `move_axes` for OT 2 hardware controller + # and then we can remove this validation. + ensure_ot3_hardware(self._hardware_api) + current_position = await self._gantry_mover.move_axes( + axis_map=params.axis_map, + speed=params.speed, + critical_point=params.critical_point, + ) + return SuccessData( + public=MoveAxesToResult(position=current_position), + ) + + +class MoveAxesTo(BaseCommand[MoveAxesToParams, MoveAxesToResult, ErrorOccurrence]): + """MoveAxesTo command model.""" + + commandType: MoveAxesToCommandType = "robot/moveAxesTo" + params: MoveAxesToParams + result: Optional[MoveAxesToResult] = None + + _ImplementationCls: Type[MoveAxesToImplementation] = MoveAxesToImplementation + + +class MoveAxesToCreate(BaseCommandCreate[MoveAxesToParams]): + """MoveAxesTo command request model.""" + + commandType: MoveAxesToCommandType = "robot/moveAxesTo" + params: MoveAxesToParams + + _CommandCls: Type[MoveAxesTo] = MoveAxesTo diff --git a/api/src/opentrons/protocol_engine/commands/robot/move_to.py b/api/src/opentrons/protocol_engine/commands/robot/move_to.py new file mode 100644 index 00000000000..e0b5365c048 --- /dev/null +++ b/api/src/opentrons/protocol_engine/commands/robot/move_to.py @@ -0,0 +1,94 @@ +"""Command models for moving any robot mount to a destination point.""" +from __future__ import annotations +from typing import Literal, Type, Optional, TYPE_CHECKING, Any + +from pydantic import BaseModel, Field +from pydantic.json_schema import SkipJsonSchema + +from opentrons.types import MountType + +from ..movement_common import DestinationPositionResult +from ..command import ( + AbstractCommandImpl, + BaseCommand, + BaseCommandCreate, + SuccessData, +) +from opentrons.protocol_engine.types import DeckPoint +from opentrons.protocol_engine.errors.error_occurrence import ErrorOccurrence + + +if TYPE_CHECKING: + from opentrons.protocol_engine.execution import MovementHandler + + +MoveToCommandType = Literal["robot/moveTo"] + + +def _remove_default(s: dict[str, Any]) -> None: + s.pop("default", None) + + +class MoveToParams(BaseModel): + """Payload required to move to a destination position.""" + + mount: MountType = Field( + ..., + description="The mount to move to the destination point.", + ) + destination: DeckPoint = Field( + ..., + description="X, Y and Z coordinates in mm from deck's origin location (left-front-bottom corner of work space)", + ) + speed: float | SkipJsonSchema[None] = Field( + default=None, + description="The max velocity to move the axes at. Will fall to hardware defaults if none provided.", + json_schema_extra=_remove_default, + ) + + +class MoveToResult(DestinationPositionResult): + """Result data from the execution of a MoveTo command.""" + + pass + + +class MoveToImplementation( + AbstractCommandImpl[MoveToParams, SuccessData[MoveToResult]] +): + """MoveTo command implementation.""" + + def __init__( + self, + movement: MovementHandler, + **kwargs: object, + ) -> None: + self._movement = movement + + async def execute(self, params: MoveToParams) -> SuccessData[MoveToResult]: + """Move to a given destination on a flex.""" + x, y, z = await self._movement.move_mount_to( + mount=params.mount, destination=params.destination, speed=params.speed + ) + return SuccessData( + public=MoveToResult(position=DeckPoint(x=x, y=y, z=z)), + ) + + +class MoveTo(BaseCommand[MoveToParams, MoveToResult, ErrorOccurrence]): + """MoveTo command model.""" + + commandType: MoveToCommandType = "robot/moveTo" + params: MoveToParams + result: Optional[MoveToResult] = None + + _ImplementationCls: Type[MoveToImplementation] = MoveToImplementation + + +class MoveToCreate(BaseCommandCreate[MoveToParams]): + """MoveTo command request model.""" + + commandType: MoveToCommandType = "robot/moveTo" + params: MoveToParams + + _CommandCls: Type[MoveTo] = MoveTo diff --git a/api/src/opentrons/protocol_engine/commands/robot/open_gripper_jaw.py b/api/src/opentrons/protocol_engine/commands/robot/open_gripper_jaw.py new file mode 100644 index 00000000000..83b58647394 --- /dev/null +++ b/api/src/opentrons/protocol_engine/commands/robot/open_gripper_jaw.py @@ -0,0 +1,77 @@ +"""Command models for opening a gripper jaw.""" +from __future__ import annotations +from typing import Literal, Type, Optional +from opentrons.hardware_control import HardwareControlAPI +from opentrons.protocol_engine.resources import ensure_ot3_hardware + +from pydantic import BaseModel + +from ..command import ( + AbstractCommandImpl, + BaseCommand, + BaseCommandCreate, + SuccessData, +) +from opentrons.protocol_engine.errors.error_occurrence import ErrorOccurrence + + +openGripperJawCommandType = Literal["robot/openGripperJaw"] + + +class openGripperJawParams(BaseModel): + """Payload required to release a gripper.""" + + pass + + +class openGripperJawResult(BaseModel): + """Result data from the execution of a openGripperJaw command.""" + + pass + + +class openGripperJawImplementation( + AbstractCommandImpl[openGripperJawParams, SuccessData[openGripperJawResult]] +): + """openGripperJaw command implementation.""" + + def __init__( + self, + hardware_api: HardwareControlAPI, + **kwargs: object, + ) -> None: + self._hardware_api = hardware_api + + async def execute( + self, params: openGripperJawParams + ) -> SuccessData[openGripperJawResult]: + """Release the gripper.""" + ot3_hardware_api = ensure_ot3_hardware(self._hardware_api) + + await ot3_hardware_api.home_gripper_jaw() + return SuccessData( + public=openGripperJawResult(), + ) + + +class openGripperJaw( + BaseCommand[openGripperJawParams, openGripperJawResult, ErrorOccurrence] +): + """openGripperJaw command model.""" + + commandType: openGripperJawCommandType = "robot/openGripperJaw" + params: openGripperJawParams + result: Optional[openGripperJawResult] = None + + _ImplementationCls: Type[ + openGripperJawImplementation + ] = openGripperJawImplementation + + +class openGripperJawCreate(BaseCommandCreate[openGripperJawParams]): + """openGripperJaw command request model.""" + + commandType: openGripperJawCommandType = "robot/openGripperJaw" + params: openGripperJawParams + + _CommandCls: Type[openGripperJaw] = openGripperJaw diff --git a/api/src/opentrons/protocol_engine/commands/save_position.py b/api/src/opentrons/protocol_engine/commands/save_position.py index 4bc208d1421..6a1d22c4687 100644 --- a/api/src/opentrons/protocol_engine/commands/save_position.py +++ b/api/src/opentrons/protocol_engine/commands/save_position.py @@ -1,8 +1,10 @@ """Save pipette position command request, result, and implementation models.""" from __future__ import annotations +from typing import TYPE_CHECKING, Optional, Type, Any + from pydantic import BaseModel, Field -from typing import TYPE_CHECKING, Optional, Type +from pydantic.json_schema import SkipJsonSchema from typing_extensions import Literal from ..types import DeckPoint @@ -16,19 +18,26 @@ SavePositionCommandType = Literal["savePosition"] +def _remove_default(s: dict[str, Any]) -> None: + s.pop("default", None) + + class SavePositionParams(BaseModel): """Payload needed to save a pipette's current position.""" pipetteId: str = Field( ..., description="Unique identifier of the pipette in question." ) - positionId: Optional[str] = Field( + positionId: str | SkipJsonSchema[None] = Field( None, description="An optional ID to assign to this command instance. " "Auto-assigned if not defined.", + json_schema_extra=_remove_default, ) - failOnNotHomed: Optional[bool] = Field( - True, descrption="Require all axes to be homed before saving position." + failOnNotHomed: bool | SkipJsonSchema[None] = Field( + True, + description="Require all axes to be homed before saving position.", + json_schema_extra=_remove_default, ) @@ -86,7 +95,7 @@ class SavePosition( commandType: SavePositionCommandType = "savePosition" params: SavePositionParams - result: Optional[SavePositionResult] + result: Optional[SavePositionResult] = None _ImplementationCls: Type[SavePositionImplementation] = SavePositionImplementation diff --git a/api/src/opentrons/protocol_engine/commands/set_rail_lights.py b/api/src/opentrons/protocol_engine/commands/set_rail_lights.py index 09254dbe966..7ca3929695b 100644 --- a/api/src/opentrons/protocol_engine/commands/set_rail_lights.py +++ b/api/src/opentrons/protocol_engine/commands/set_rail_lights.py @@ -53,7 +53,7 @@ class SetRailLights( commandType: SetRailLightsCommandType = "setRailLights" params: SetRailLightsParams - result: Optional[SetRailLightsResult] + result: Optional[SetRailLightsResult] = None _ImplementationCls: Type[SetRailLightsImplementation] = SetRailLightsImplementation diff --git a/api/src/opentrons/protocol_engine/commands/set_status_bar.py b/api/src/opentrons/protocol_engine/commands/set_status_bar.py index 2e1483f6d93..62ca9b7682a 100644 --- a/api/src/opentrons/protocol_engine/commands/set_status_bar.py +++ b/api/src/opentrons/protocol_engine/commands/set_status_bar.py @@ -75,7 +75,7 @@ class SetStatusBar( commandType: SetStatusBarCommandType = "setStatusBar" params: SetStatusBarParams - result: Optional[SetStatusBarResult] + result: Optional[SetStatusBarResult] = None _ImplementationCls: Type[SetStatusBarImplementation] = SetStatusBarImplementation diff --git a/api/src/opentrons/protocol_engine/commands/temperature_module/deactivate.py b/api/src/opentrons/protocol_engine/commands/temperature_module/deactivate.py index e56c98e6e30..e1514c91f30 100644 --- a/api/src/opentrons/protocol_engine/commands/temperature_module/deactivate.py +++ b/api/src/opentrons/protocol_engine/commands/temperature_module/deactivate.py @@ -72,7 +72,7 @@ class DeactivateTemperature( commandType: DeactivateTemperatureCommandType = "temperatureModule/deactivate" params: DeactivateTemperatureParams - result: Optional[DeactivateTemperatureResult] + result: Optional[DeactivateTemperatureResult] = None _ImplementationCls: Type[DeactivateTemperatureImpl] = DeactivateTemperatureImpl diff --git a/api/src/opentrons/protocol_engine/commands/temperature_module/set_target_temperature.py b/api/src/opentrons/protocol_engine/commands/temperature_module/set_target_temperature.py index 6d65bf47bb0..b0aa7bd5cd0 100644 --- a/api/src/opentrons/protocol_engine/commands/temperature_module/set_target_temperature.py +++ b/api/src/opentrons/protocol_engine/commands/temperature_module/set_target_temperature.py @@ -81,7 +81,7 @@ class SetTargetTemperature( "temperatureModule/setTargetTemperature" ) params: SetTargetTemperatureParams - result: Optional[SetTargetTemperatureResult] + result: Optional[SetTargetTemperatureResult] = None _ImplementationCls: Type[SetTargetTemperatureImpl] = SetTargetTemperatureImpl diff --git a/api/src/opentrons/protocol_engine/commands/temperature_module/wait_for_temperature.py b/api/src/opentrons/protocol_engine/commands/temperature_module/wait_for_temperature.py index fa7784de141..5f3f052d91b 100644 --- a/api/src/opentrons/protocol_engine/commands/temperature_module/wait_for_temperature.py +++ b/api/src/opentrons/protocol_engine/commands/temperature_module/wait_for_temperature.py @@ -1,9 +1,10 @@ """Command models to wait for target temperature of a Temperature Module.""" from __future__ import annotations -from typing import Optional, TYPE_CHECKING -from typing_extensions import Literal, Type +from typing import Optional, TYPE_CHECKING, Any +from typing_extensions import Literal, Type from pydantic import BaseModel, Field +from pydantic.json_schema import SkipJsonSchema from ..command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, SuccessData from ...errors.error_occurrence import ErrorOccurrence @@ -15,11 +16,15 @@ WaitForTemperatureCommandType = Literal["temperatureModule/waitForTemperature"] +def _remove_default(s: dict[str, Any]) -> None: + s.pop("default", None) + + class WaitForTemperatureParams(BaseModel): """Input parameters to wait for a Temperature Module's target temperature.""" moduleId: str = Field(..., description="Unique ID of the Temperature Module.") - celsius: Optional[float] = Field( + celsius: float | SkipJsonSchema[None] = Field( None, description="Target temperature in °C. If not specified, will " "default to the module's target temperature. " @@ -27,6 +32,7 @@ class WaitForTemperatureParams(BaseModel): "could lead to unpredictable behavior and hence is not " "recommended for use. This parameter can be removed in a " "future version without prior notice.", + json_schema_extra=_remove_default, ) @@ -84,7 +90,7 @@ class WaitForTemperature( commandType: WaitForTemperatureCommandType = "temperatureModule/waitForTemperature" params: WaitForTemperatureParams - result: Optional[WaitForTemperatureResult] + result: Optional[WaitForTemperatureResult] = None _ImplementationCls: Type[WaitForTemperatureImpl] = WaitForTemperatureImpl diff --git a/api/src/opentrons/protocol_engine/commands/thermocycler/close_lid.py b/api/src/opentrons/protocol_engine/commands/thermocycler/close_lid.py index 578a5d6b7a7..c5171c82da8 100644 --- a/api/src/opentrons/protocol_engine/commands/thermocycler/close_lid.py +++ b/api/src/opentrons/protocol_engine/commands/thermocycler/close_lid.py @@ -73,7 +73,7 @@ class CloseLid(BaseCommand[CloseLidParams, CloseLidResult, ErrorOccurrence]): commandType: CloseLidCommandType = "thermocycler/closeLid" params: CloseLidParams - result: Optional[CloseLidResult] + result: Optional[CloseLidResult] = None _ImplementationCls: Type[CloseLidImpl] = CloseLidImpl diff --git a/api/src/opentrons/protocol_engine/commands/thermocycler/deactivate_block.py b/api/src/opentrons/protocol_engine/commands/thermocycler/deactivate_block.py index 67199577d53..fb49f031ac4 100644 --- a/api/src/opentrons/protocol_engine/commands/thermocycler/deactivate_block.py +++ b/api/src/opentrons/protocol_engine/commands/thermocycler/deactivate_block.py @@ -66,7 +66,7 @@ class DeactivateBlock( commandType: DeactivateBlockCommandType = "thermocycler/deactivateBlock" params: DeactivateBlockParams - result: Optional[DeactivateBlockResult] + result: Optional[DeactivateBlockResult] = None _ImplementationCls: Type[DeactivateBlockImpl] = DeactivateBlockImpl diff --git a/api/src/opentrons/protocol_engine/commands/thermocycler/deactivate_lid.py b/api/src/opentrons/protocol_engine/commands/thermocycler/deactivate_lid.py index 9c3541efb12..f92065c88e9 100644 --- a/api/src/opentrons/protocol_engine/commands/thermocycler/deactivate_lid.py +++ b/api/src/opentrons/protocol_engine/commands/thermocycler/deactivate_lid.py @@ -66,7 +66,7 @@ class DeactivateLid( commandType: DeactivateLidCommandType = "thermocycler/deactivateLid" params: DeactivateLidParams - result: Optional[DeactivateLidResult] + result: Optional[DeactivateLidResult] = None _ImplementationCls: Type[DeactivateLidImpl] = DeactivateLidImpl diff --git a/api/src/opentrons/protocol_engine/commands/thermocycler/open_lid.py b/api/src/opentrons/protocol_engine/commands/thermocycler/open_lid.py index 3df32d1ec99..6eedb9f4c60 100644 --- a/api/src/opentrons/protocol_engine/commands/thermocycler/open_lid.py +++ b/api/src/opentrons/protocol_engine/commands/thermocycler/open_lid.py @@ -73,7 +73,7 @@ class OpenLid(BaseCommand[OpenLidParams, OpenLidResult, ErrorOccurrence]): commandType: OpenLidCommandType = "thermocycler/openLid" params: OpenLidParams - result: Optional[OpenLidResult] + result: Optional[OpenLidResult] = None _ImplementationCls: Type[OpenLidImpl] = OpenLidImpl diff --git a/api/src/opentrons/protocol_engine/commands/thermocycler/run_extended_profile.py b/api/src/opentrons/protocol_engine/commands/thermocycler/run_extended_profile.py index 6f63aed8fe3..ec294d5965c 100644 --- a/api/src/opentrons/protocol_engine/commands/thermocycler/run_extended_profile.py +++ b/api/src/opentrons/protocol_engine/commands/thermocycler/run_extended_profile.py @@ -1,9 +1,10 @@ """Command models to execute a Thermocycler profile.""" from __future__ import annotations -from typing import List, Optional, TYPE_CHECKING, overload, Union +from typing import List, Optional, TYPE_CHECKING, overload, Union, Any from typing_extensions import Literal, Type from pydantic import BaseModel, Field +from pydantic.json_schema import SkipJsonSchema from opentrons.hardware_control.modules.types import ThermocyclerStep, ThermocyclerCycle @@ -21,6 +22,10 @@ RunExtendedProfileCommandType = Literal["thermocycler/runExtendedProfile"] +def _remove_default(s: dict[str, Any]) -> None: + s.pop("default", None) + + class ProfileStep(BaseModel): """An individual step in a Thermocycler extended profile.""" @@ -45,10 +50,11 @@ class RunExtendedProfileParams(BaseModel): ..., description="Elements of the profile. Each can be either a step or a cycle.", ) - blockMaxVolumeUl: Optional[float] = Field( + blockMaxVolumeUl: float | SkipJsonSchema[None] = Field( None, description="Amount of liquid in uL of the most-full well" " in labware loaded onto the thermocycler.", + json_schema_extra=_remove_default, ) @@ -151,7 +157,7 @@ class RunExtendedProfile( commandType: RunExtendedProfileCommandType = "thermocycler/runExtendedProfile" params: RunExtendedProfileParams - result: Optional[RunExtendedProfileResult] + result: Optional[RunExtendedProfileResult] = None _ImplementationCls: Type[RunExtendedProfileImpl] = RunExtendedProfileImpl diff --git a/api/src/opentrons/protocol_engine/commands/thermocycler/run_profile.py b/api/src/opentrons/protocol_engine/commands/thermocycler/run_profile.py index 02aa7ad93e2..fee6ed82bb3 100644 --- a/api/src/opentrons/protocol_engine/commands/thermocycler/run_profile.py +++ b/api/src/opentrons/protocol_engine/commands/thermocycler/run_profile.py @@ -1,9 +1,10 @@ """Command models to execute a Thermocycler profile.""" from __future__ import annotations -from typing import List, Optional, TYPE_CHECKING +from typing import List, Optional, TYPE_CHECKING, Any from typing_extensions import Literal, Type from pydantic import BaseModel, Field +from pydantic.json_schema import SkipJsonSchema from opentrons.hardware_control.modules.types import ThermocyclerStep @@ -18,6 +19,10 @@ RunProfileCommandType = Literal["thermocycler/runProfile"] +def _remove_default(s: dict[str, Any]) -> None: + s.pop("default", None) + + class RunProfileStepParams(BaseModel): """Input parameters for an individual Thermocycler profile step.""" @@ -35,10 +40,11 @@ class RunProfileParams(BaseModel): ..., description="Array of profile steps with target temperature and temperature hold time.", ) - blockMaxVolumeUl: Optional[float] = Field( + blockMaxVolumeUl: float | SkipJsonSchema[None] = Field( None, description="Amount of liquid in uL of the most-full well" " in labware loaded onto the thermocycler.", + json_schema_extra=_remove_default, ) @@ -104,7 +110,7 @@ class RunProfile(BaseCommand[RunProfileParams, RunProfileResult, ErrorOccurrence commandType: RunProfileCommandType = "thermocycler/runProfile" params: RunProfileParams - result: Optional[RunProfileResult] + result: Optional[RunProfileResult] = None _ImplementationCls: Type[RunProfileImpl] = RunProfileImpl diff --git a/api/src/opentrons/protocol_engine/commands/thermocycler/set_target_block_temperature.py b/api/src/opentrons/protocol_engine/commands/thermocycler/set_target_block_temperature.py index b69bb15ea90..f1884e8ee9e 100644 --- a/api/src/opentrons/protocol_engine/commands/thermocycler/set_target_block_temperature.py +++ b/api/src/opentrons/protocol_engine/commands/thermocycler/set_target_block_temperature.py @@ -1,9 +1,10 @@ """Command models to start heating a Thermocycler's block.""" from __future__ import annotations -from typing import Optional, TYPE_CHECKING +from typing import Optional, TYPE_CHECKING, Any from typing_extensions import Literal, Type from pydantic import BaseModel, Field +from pydantic.json_schema import SkipJsonSchema from ..command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, SuccessData from ...errors.error_occurrence import ErrorOccurrence @@ -16,21 +17,27 @@ SetTargetBlockTemperatureCommandType = Literal["thermocycler/setTargetBlockTemperature"] +def _remove_default(s: dict[str, Any]) -> None: + s.pop("default", None) + + class SetTargetBlockTemperatureParams(BaseModel): """Input parameters to set a Thermocycler's target block temperature.""" moduleId: str = Field(..., description="Unique ID of the Thermocycler Module.") celsius: float = Field(..., description="Target temperature in °C.") - blockMaxVolumeUl: Optional[float] = Field( + blockMaxVolumeUl: float | SkipJsonSchema[None] = Field( None, description="Amount of liquid in uL of the most-full well" " in labware loaded onto the thermocycler.", + json_schema_extra=_remove_default, ) - holdTimeSeconds: Optional[float] = Field( + holdTimeSeconds: float | SkipJsonSchema[None] = Field( None, description="Amount of time, in seconds, to hold the temperature for." " If specified, a waitForBlockTemperature command will block until" " the given hold time has elapsed.", + json_schema_extra=_remove_default, ) @@ -113,7 +120,7 @@ class SetTargetBlockTemperature( "thermocycler/setTargetBlockTemperature" ) params: SetTargetBlockTemperatureParams - result: Optional[SetTargetBlockTemperatureResult] + result: Optional[SetTargetBlockTemperatureResult] = None _ImplementationCls: Type[ SetTargetBlockTemperatureImpl diff --git a/api/src/opentrons/protocol_engine/commands/thermocycler/set_target_lid_temperature.py b/api/src/opentrons/protocol_engine/commands/thermocycler/set_target_lid_temperature.py index 37217e047ae..a1af8941ee9 100644 --- a/api/src/opentrons/protocol_engine/commands/thermocycler/set_target_lid_temperature.py +++ b/api/src/opentrons/protocol_engine/commands/thermocycler/set_target_lid_temperature.py @@ -84,7 +84,7 @@ class SetTargetLidTemperature( "thermocycler/setTargetLidTemperature" ) params: SetTargetLidTemperatureParams - result: Optional[SetTargetLidTemperatureResult] + result: Optional[SetTargetLidTemperatureResult] = None _ImplementationCls: Type[SetTargetLidTemperatureImpl] = SetTargetLidTemperatureImpl diff --git a/api/src/opentrons/protocol_engine/commands/thermocycler/wait_for_block_temperature.py b/api/src/opentrons/protocol_engine/commands/thermocycler/wait_for_block_temperature.py index 8e8c9b1a4ec..388d1af13d9 100644 --- a/api/src/opentrons/protocol_engine/commands/thermocycler/wait_for_block_temperature.py +++ b/api/src/opentrons/protocol_engine/commands/thermocycler/wait_for_block_temperature.py @@ -77,7 +77,7 @@ class WaitForBlockTemperature( "thermocycler/waitForBlockTemperature" ) params: WaitForBlockTemperatureParams - result: Optional[WaitForBlockTemperatureResult] + result: Optional[WaitForBlockTemperatureResult] = None _ImplementationCls: Type[WaitForBlockTemperatureImpl] = WaitForBlockTemperatureImpl diff --git a/api/src/opentrons/protocol_engine/commands/thermocycler/wait_for_lid_temperature.py b/api/src/opentrons/protocol_engine/commands/thermocycler/wait_for_lid_temperature.py index 95e5fbc4f0a..233e4ff5a8c 100644 --- a/api/src/opentrons/protocol_engine/commands/thermocycler/wait_for_lid_temperature.py +++ b/api/src/opentrons/protocol_engine/commands/thermocycler/wait_for_lid_temperature.py @@ -75,7 +75,7 @@ class WaitForLidTemperature( commandType: WaitForLidTemperatureCommandType = "thermocycler/waitForLidTemperature" params: WaitForLidTemperatureParams - result: Optional[WaitForLidTemperatureResult] + result: Optional[WaitForLidTemperatureResult] = None _ImplementationCls: Type[WaitForLidTemperatureImpl] = WaitForLidTemperatureImpl diff --git a/api/src/opentrons/protocol_engine/commands/touch_tip.py b/api/src/opentrons/protocol_engine/commands/touch_tip.py index 48c947abcbd..d4591bf1d27 100644 --- a/api/src/opentrons/protocol_engine/commands/touch_tip.py +++ b/api/src/opentrons/protocol_engine/commands/touch_tip.py @@ -1,29 +1,50 @@ """Touch tip command request, result, and implementation models.""" + from __future__ import annotations -from pydantic import Field -from typing import TYPE_CHECKING, Optional, Type +from typing import TYPE_CHECKING, Optional, Type, Any + from typing_extensions import Literal +from pydantic import Field +from pydantic.json_schema import SkipJsonSchema -from opentrons.protocol_engine.state import update_types +from opentrons.types import Point -from ..errors import TouchTipDisabledError, LabwareIsTipRackError +from ..errors import ( + TouchTipDisabledError, + TouchTipIncompatibleArgumentsError, + LabwareIsTipRackError, +) from ..types import DeckPoint -from .command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, SuccessData -from ..errors.error_occurrence import ErrorOccurrence +from .command import ( + AbstractCommandImpl, + BaseCommand, + BaseCommandCreate, + SuccessData, + DefinedErrorData, +) from .pipetting_common import ( PipetteIdMixin, +) +from .movement_common import ( WellLocationMixin, DestinationPositionResult, + StallOrCollisionError, + move_to_well, ) if TYPE_CHECKING: from ..execution import MovementHandler, GantryMover from ..state.state import StateView + from ..resources.model_utils import ModelUtils TouchTipCommandType = Literal["touchTip"] +def _remove_default(s: dict[str, Any]) -> None: + s.pop("default", None) + + class TouchTipParams(PipetteIdMixin, WellLocationMixin): """Payload needed to touch a pipette tip the sides of a specific well.""" @@ -34,12 +55,20 @@ class TouchTipParams(PipetteIdMixin, WellLocationMixin): ), ) - speed: Optional[float] = Field( + mmFromEdge: float | SkipJsonSchema[None] = Field( + None, + description="Offset away from the the well edge, in millimeters." + "Incompatible when a radius is included as a non 1.0 value.", + json_schema_extra=_remove_default, + ) + + speed: float | SkipJsonSchema[None] = Field( None, description=( "Override the travel speed in mm/s." " This controls the straight linear speed of motion." ), + json_schema_extra=_remove_default, ) @@ -50,7 +79,10 @@ class TouchTipResult(DestinationPositionResult): class TouchTipImplementation( - AbstractCommandImpl[TouchTipParams, SuccessData[TouchTipResult]] + AbstractCommandImpl[ + TouchTipParams, + SuccessData[TouchTipResult] | DefinedErrorData[StallOrCollisionError], + ] ): """Touch tip command implementation.""" @@ -59,19 +91,26 @@ def __init__( state_view: StateView, movement: MovementHandler, gantry_mover: GantryMover, + model_utils: ModelUtils, **kwargs: object, ) -> None: self._state_view = state_view self._movement = movement self._gantry_mover = gantry_mover + self._model_utils = model_utils - async def execute(self, params: TouchTipParams) -> SuccessData[TouchTipResult]: + async def execute( + self, params: TouchTipParams + ) -> SuccessData[TouchTipResult] | DefinedErrorData[StallOrCollisionError]: """Touch tip to sides of a well using the requested pipette.""" pipette_id = params.pipetteId labware_id = params.labwareId well_name = params.wellName - state_update = update_types.StateUpdate() + if params.radius != 1.0 and params.mmFromEdge is not None: + raise TouchTipIncompatibleArgumentsError( + "Cannot use mmFromEdge with a radius that is not 1.0" + ) if self._state_view.labware.get_has_quirk(labware_id, "touchTipDisabled"): raise TouchTipDisabledError( @@ -81,23 +120,33 @@ async def execute(self, params: TouchTipParams) -> SuccessData[TouchTipResult]: if self._state_view.labware.is_tiprack(labware_id): raise LabwareIsTipRackError("Cannot touch tip on tip rack") - center_point = await self._movement.move_to_well( + center_result = await move_to_well( + movement=self._movement, + model_utils=self._model_utils, pipette_id=pipette_id, labware_id=labware_id, well_name=well_name, well_location=params.wellLocation, ) + if isinstance(center_result, DefinedErrorData): + return center_result touch_speed = self._state_view.pipettes.get_movement_speed( pipette_id, params.speed ) + mm_from_edge = params.mmFromEdge if params.mmFromEdge is not None else 0 touch_waypoints = self._state_view.motion.get_touch_tip_waypoints( pipette_id=pipette_id, labware_id=labware_id, well_name=well_name, radius=params.radius, - center_point=center_point, + mm_from_edge=mm_from_edge, + center_point=Point( + center_result.public.position.x, + center_result.public.position.y, + center_result.public.position.z, + ), ) final_point = await self._gantry_mover.move_to( @@ -105,10 +154,10 @@ async def execute(self, params: TouchTipParams) -> SuccessData[TouchTipResult]: waypoints=touch_waypoints, speed=touch_speed, ) - final_deck_point = DeckPoint.construct( + final_deck_point = DeckPoint.model_construct( x=final_point.x, y=final_point.y, z=final_point.z ) - state_update.set_pipette_location( + state_update = center_result.state_update.set_pipette_location( pipette_id=pipette_id, new_labware_id=labware_id, new_well_name=well_name, @@ -121,12 +170,12 @@ async def execute(self, params: TouchTipParams) -> SuccessData[TouchTipResult]: ) -class TouchTip(BaseCommand[TouchTipParams, TouchTipResult, ErrorOccurrence]): +class TouchTip(BaseCommand[TouchTipParams, TouchTipResult, StallOrCollisionError]): """Touch up tip command model.""" commandType: TouchTipCommandType = "touchTip" params: TouchTipParams - result: Optional[TouchTipResult] + result: Optional[TouchTipResult] = None _ImplementationCls: Type[TouchTipImplementation] = TouchTipImplementation diff --git a/api/src/opentrons/protocol_engine/commands/unsafe/unsafe_blow_out_in_place.py b/api/src/opentrons/protocol_engine/commands/unsafe/unsafe_blow_out_in_place.py index 4738b7c9b97..bd48c3c1c8b 100644 --- a/api/src/opentrons/protocol_engine/commands/unsafe/unsafe_blow_out_in_place.py +++ b/api/src/opentrons/protocol_engine/commands/unsafe/unsafe_blow_out_in_place.py @@ -10,6 +10,7 @@ from ..pipetting_common import PipetteIdMixin, FlowRateMixin from ...resources import ensure_ot3_hardware from ...errors.error_occurrence import ErrorOccurrence +from ...state import update_types from opentrons.hardware_control import HardwareControlAPI from opentrons.hardware_control.types import Axis @@ -66,9 +67,11 @@ async def execute( await self._pipetting.blow_out_in_place( pipette_id=params.pipetteId, flow_rate=params.flowRate ) + state_update = update_types.StateUpdate() + state_update.set_fluid_empty(pipette_id=params.pipetteId) return SuccessData( - public=UnsafeBlowOutInPlaceResult(), + public=UnsafeBlowOutInPlaceResult(), state_update=state_update ) @@ -79,7 +82,7 @@ class UnsafeBlowOutInPlace( commandType: UnsafeBlowOutInPlaceCommandType = "unsafe/blowOutInPlace" params: UnsafeBlowOutInPlaceParams - result: Optional[UnsafeBlowOutInPlaceResult] + result: Optional[UnsafeBlowOutInPlaceResult] = None _ImplementationCls: Type[ UnsafeBlowOutInPlaceImplementation diff --git a/api/src/opentrons/protocol_engine/commands/unsafe/unsafe_drop_tip_in_place.py b/api/src/opentrons/protocol_engine/commands/unsafe/unsafe_drop_tip_in_place.py index ff749711cfb..d6b0652a4c0 100644 --- a/api/src/opentrons/protocol_engine/commands/unsafe/unsafe_drop_tip_in_place.py +++ b/api/src/opentrons/protocol_engine/commands/unsafe/unsafe_drop_tip_in_place.py @@ -1,13 +1,16 @@ """Command models to drop tip in place while plunger positions are unknown.""" + from __future__ import annotations -from opentrons.protocol_engine.state.update_types import StateUpdate +from typing import TYPE_CHECKING, Optional, Type, Any + from pydantic import Field, BaseModel -from typing import TYPE_CHECKING, Optional, Type +from pydantic.json_schema import SkipJsonSchema from typing_extensions import Literal from opentrons.hardware_control import HardwareControlAPI from opentrons.hardware_control.types import Axis +from opentrons.protocol_engine.state.update_types import StateUpdate from ..pipetting_common import PipetteIdMixin from ..command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, SuccessData from ...errors.error_occurrence import ErrorOccurrence @@ -21,16 +24,21 @@ UnsafeDropTipInPlaceCommandType = Literal["unsafe/dropTipInPlace"] +def _remove_default(s: dict[str, Any]) -> None: + s.pop("default", None) + + class UnsafeDropTipInPlaceParams(PipetteIdMixin): """Payload required to drop a tip in place even if the plunger position is not known.""" - homeAfter: Optional[bool] = Field( + homeAfter: bool | SkipJsonSchema[None] = Field( None, description=( "Whether to home this pipette's plunger after dropping the tip." " You should normally leave this unspecified to let the robot choose" " a safe default depending on its hardware." ), + json_schema_extra=_remove_default, ) @@ -77,6 +85,7 @@ async def execute( state_update.update_pipette_tip_state( pipette_id=params.pipetteId, tip_geometry=None ) + state_update.set_fluid_unknown(pipette_id=params.pipetteId) return SuccessData( public=UnsafeDropTipInPlaceResult(), state_update=state_update @@ -90,7 +99,7 @@ class UnsafeDropTipInPlace( commandType: UnsafeDropTipInPlaceCommandType = "unsafe/dropTipInPlace" params: UnsafeDropTipInPlaceParams - result: Optional[UnsafeDropTipInPlaceResult] + result: Optional[UnsafeDropTipInPlaceResult] = None _ImplementationCls: Type[ UnsafeDropTipInPlaceImplementation diff --git a/api/src/opentrons/protocol_engine/commands/unsafe/unsafe_engage_axes.py b/api/src/opentrons/protocol_engine/commands/unsafe/unsafe_engage_axes.py index 02bc22b0396..c38bc1ed481 100644 --- a/api/src/opentrons/protocol_engine/commands/unsafe/unsafe_engage_axes.py +++ b/api/src/opentrons/protocol_engine/commands/unsafe/unsafe_engage_axes.py @@ -52,10 +52,7 @@ async def execute( """Enable exes.""" ot3_hardware_api = ensure_ot3_hardware(self._hardware_api) await ot3_hardware_api.engage_axes( - [ - self._gantry_mover.motor_axis_to_hardware_axis(axis) - for axis in params.axes - ] + self._gantry_mover.motor_axes_to_present_hardware_axes(params.axes) ) return SuccessData( public=UnsafeEngageAxesResult(), @@ -69,7 +66,7 @@ class UnsafeEngageAxes( commandType: UnsafeEngageAxesCommandType = "unsafe/engageAxes" params: UnsafeEngageAxesParams - result: Optional[UnsafeEngageAxesResult] + result: Optional[UnsafeEngageAxesResult] = None _ImplementationCls: Type[ UnsafeEngageAxesImplementation diff --git a/api/src/opentrons/protocol_engine/commands/unsafe/unsafe_place_labware.py b/api/src/opentrons/protocol_engine/commands/unsafe/unsafe_place_labware.py index c69cea29243..cd105867524 100644 --- a/api/src/opentrons/protocol_engine/commands/unsafe/unsafe_place_labware.py +++ b/api/src/opentrons/protocol_engine/commands/unsafe/unsafe_place_labware.py @@ -192,7 +192,7 @@ class UnsafePlaceLabware( commandType: UnsafePlaceLabwareCommandType = "unsafe/placeLabware" params: UnsafePlaceLabwareParams - result: Optional[UnsafePlaceLabwareResult] + result: Optional[UnsafePlaceLabwareResult] = None _ImplementationCls: Type[ UnsafePlaceLabwareImplementation diff --git a/api/src/opentrons/protocol_engine/commands/unsafe/unsafe_ungrip_labware.py b/api/src/opentrons/protocol_engine/commands/unsafe/unsafe_ungrip_labware.py index 6f8f5b71fce..dd7248a1932 100644 --- a/api/src/opentrons/protocol_engine/commands/unsafe/unsafe_ungrip_labware.py +++ b/api/src/opentrons/protocol_engine/commands/unsafe/unsafe_ungrip_labware.py @@ -61,7 +61,7 @@ class UnsafeUngripLabware( commandType: UnsafeUngripLabwareCommandType = "unsafe/ungripLabware" params: UnsafeUngripLabwareParams - result: Optional[UnsafeUngripLabwareResult] + result: Optional[UnsafeUngripLabwareResult] = None _ImplementationCls: Type[ UnsafeUngripLabwareImplementation diff --git a/api/src/opentrons/protocol_engine/commands/unsafe/update_position_estimators.py b/api/src/opentrons/protocol_engine/commands/unsafe/update_position_estimators.py index ff06b6c22ed..0f6e4c5d7eb 100644 --- a/api/src/opentrons/protocol_engine/commands/unsafe/update_position_estimators.py +++ b/api/src/opentrons/protocol_engine/commands/unsafe/update_position_estimators.py @@ -58,10 +58,7 @@ async def execute( """Update axis position estimators from their encoders.""" ot3_hardware_api = ensure_ot3_hardware(self._hardware_api) await ot3_hardware_api.update_axis_position_estimations( - [ - self._gantry_mover.motor_axis_to_hardware_axis(axis) - for axis in params.axes - ] + self._gantry_mover.motor_axes_to_present_hardware_axes(params.axes) ) return SuccessData( public=UpdatePositionEstimatorsResult(), @@ -77,7 +74,7 @@ class UpdatePositionEstimators( commandType: UpdatePositionEstimatorsCommandType = "unsafe/updatePositionEstimators" params: UpdatePositionEstimatorsParams - result: Optional[UpdatePositionEstimatorsResult] + result: Optional[UpdatePositionEstimatorsResult] = None _ImplementationCls: Type[ UpdatePositionEstimatorsImplementation diff --git a/api/src/opentrons/protocol_engine/commands/verify_tip_presence.py b/api/src/opentrons/protocol_engine/commands/verify_tip_presence.py index e0412022e85..dc6f451e3e4 100644 --- a/api/src/opentrons/protocol_engine/commands/verify_tip_presence.py +++ b/api/src/opentrons/protocol_engine/commands/verify_tip_presence.py @@ -1,8 +1,9 @@ """Verify tip presence command request, result and implementation models.""" from __future__ import annotations +from typing import TYPE_CHECKING, Optional, Type, Any from pydantic import Field, BaseModel -from typing import TYPE_CHECKING, Optional, Type +from pydantic.json_schema import SkipJsonSchema from typing_extensions import Literal from .pipetting_common import PipetteIdMixin @@ -18,14 +19,20 @@ VerifyTipPresenceCommandType = Literal["verifyTipPresence"] +def _remove_default(s: dict[str, Any]) -> None: + s.pop("default", None) + + class VerifyTipPresenceParams(PipetteIdMixin): """Payload required for a VerifyTipPresence command.""" expectedState: TipPresenceStatus = Field( ..., description="The expected tip presence status on the pipette." ) - followSingularSensor: Optional[InstrumentSensorId] = Field( - default=None, description="The sensor id to follow if the other can be ignored." + followSingularSensor: InstrumentSensorId | SkipJsonSchema[None] = Field( + default=None, + description="The sensor id to follow if the other can be ignored.", + json_schema_extra=_remove_default, ) @@ -77,7 +84,7 @@ class VerifyTipPresence( commandType: VerifyTipPresenceCommandType = "verifyTipPresence" params: VerifyTipPresenceParams - result: Optional[VerifyTipPresenceResult] + result: Optional[VerifyTipPresenceResult] = None _ImplementationCls: Type[ VerifyTipPresenceImplementation diff --git a/api/src/opentrons/protocol_engine/commands/wait_for_duration.py b/api/src/opentrons/protocol_engine/commands/wait_for_duration.py index 04f8693386e..26a4372c8ca 100644 --- a/api/src/opentrons/protocol_engine/commands/wait_for_duration.py +++ b/api/src/opentrons/protocol_engine/commands/wait_for_duration.py @@ -1,7 +1,9 @@ """Wait for duration command request, result, and implementation models.""" from __future__ import annotations +from typing import TYPE_CHECKING, Optional, Type, Any + from pydantic import BaseModel, Field -from typing import TYPE_CHECKING, Optional, Type +from pydantic.json_schema import SkipJsonSchema from typing_extensions import Literal from .command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, SuccessData @@ -14,13 +16,18 @@ WaitForDurationCommandType = Literal["waitForDuration"] +def _remove_default(s: dict[str, Any]) -> None: + s.pop("default", None) + + class WaitForDurationParams(BaseModel): """Payload required to pause the protocol.""" seconds: float = Field(..., description="Duration, in seconds, to wait for.") - message: Optional[str] = Field( + message: str | SkipJsonSchema[None] = Field( None, description="A user-facing message associated with the pause", + json_schema_extra=_remove_default, ) @@ -53,7 +60,7 @@ class WaitForDuration( commandType: WaitForDurationCommandType = "waitForDuration" params: WaitForDurationParams - result: Optional[WaitForDurationResult] + result: Optional[WaitForDurationResult] = None _ImplementationCls: Type[ WaitForDurationImplementation diff --git a/api/src/opentrons/protocol_engine/commands/wait_for_resume.py b/api/src/opentrons/protocol_engine/commands/wait_for_resume.py index f5066d52521..28458aa3721 100644 --- a/api/src/opentrons/protocol_engine/commands/wait_for_resume.py +++ b/api/src/opentrons/protocol_engine/commands/wait_for_resume.py @@ -1,7 +1,9 @@ """Wait for resume command request, result, and implementation models.""" from __future__ import annotations +from typing import TYPE_CHECKING, Optional, Type, Any + from pydantic import BaseModel, Field -from typing import TYPE_CHECKING, Optional, Type +from pydantic.json_schema import SkipJsonSchema from typing_extensions import Literal from .command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, SuccessData @@ -16,12 +18,17 @@ WaitForResumeCommandType = Literal["waitForResume", "pause"] +def _remove_default(s: dict[str, Any]) -> None: + s.pop("default", None) + + class WaitForResumeParams(BaseModel): """Payload required to pause the protocol.""" - message: Optional[str] = Field( + message: str | SkipJsonSchema[None] = Field( None, description="A user-facing message associated with the pause", + json_schema_extra=_remove_default, ) @@ -54,7 +61,7 @@ class WaitForResume( commandType: WaitForResumeCommandType = "waitForResume" params: WaitForResumeParams - result: Optional[WaitForResumeResult] + result: Optional[WaitForResumeResult] = None _ImplementationCls: Type[WaitForResumeImplementation] = WaitForResumeImplementation diff --git a/api/src/opentrons/protocol_engine/errors/__init__.py b/api/src/opentrons/protocol_engine/errors/__init__.py index 0db3d7ebdf2..d3dcc5abaac 100644 --- a/api/src/opentrons/protocol_engine/errors/__init__.py +++ b/api/src/opentrons/protocol_engine/errors/__init__.py @@ -11,6 +11,7 @@ PickUpTipTipNotAttachedError, TipAttachedError, CommandDoesNotExistError, + UnsupportedLabwareForActionError, LabwareNotLoadedError, LabwareNotLoadedOnModuleError, LabwareNotLoadedOnLabwareError, @@ -18,12 +19,14 @@ LiquidDoesNotExistError, LabwareDefinitionDoesNotExistError, LabwareCannotBeStackedError, + LabwareCannotSitOnDeckError, LabwareIsInStackError, LabwareOffsetDoesNotExistError, LabwareIsNotTipRackError, LabwareIsTipRackError, LabwareIsAdapterError, TouchTipDisabledError, + TouchTipIncompatibleArgumentsError, WellDoesNotExistError, PipetteNotLoadedError, ModuleNotLoadedError, @@ -78,6 +81,9 @@ OperationLocationNotInWellError, InvalidDispenseVolumeError, StorageLimitReachedError, + InvalidLiquidError, + LiquidClassDoesNotExistError, + LiquidClassRedefinitionError, ) from .error_occurrence import ErrorOccurrence, ProtocolCommandFailedError @@ -99,14 +105,17 @@ "LabwareNotLoadedOnLabwareError", "LabwareNotOnDeckError", "LiquidDoesNotExistError", + "UnsupportedLabwareForActionError", "LabwareDefinitionDoesNotExistError", "LabwareCannotBeStackedError", + "LabwareCannotSitOnDeckError", "LabwareIsInStackError", "LabwareOffsetDoesNotExistError", "LabwareIsNotTipRackError", "LabwareIsTipRackError", "LabwareIsAdapterError", "TouchTipDisabledError", + "TouchTipIncompatibleArgumentsError", "WellDoesNotExistError", "PipetteNotLoadedError", "ModuleNotLoadedError", @@ -138,6 +147,7 @@ "InvalidTargetSpeedError", "InvalidBlockVolumeError", "InvalidHoldTimeError", + "InvalidLiquidError", "InvalidWavelengthError", "CannotPerformModuleAction", "ResumeFromRecoveryNotAllowedError", @@ -164,4 +174,6 @@ "OperationLocationNotInWellError", "InvalidDispenseVolumeError", "StorageLimitReachedError", + "LiquidClassDoesNotExistError", + "LiquidClassRedefinitionError", ] diff --git a/api/src/opentrons/protocol_engine/errors/error_occurrence.py b/api/src/opentrons/protocol_engine/errors/error_occurrence.py index 4141befe9b8..002596d0172 100644 --- a/api/src/opentrons/protocol_engine/errors/error_occurrence.py +++ b/api/src/opentrons/protocol_engine/errors/error_occurrence.py @@ -4,7 +4,7 @@ from datetime import datetime from textwrap import dedent from typing import Any, Dict, Mapping, List, Type, Union, Optional, Sequence -from pydantic import BaseModel, Field +from pydantic import BaseModel, Field, ConfigDict from opentrons_shared_data.errors.codes import ErrorCodes from .exceptions import ProtocolEngineError from opentrons_shared_data.errors.exceptions import EnumeratedError @@ -29,7 +29,7 @@ def from_failed( wrappedErrors = [ cls.from_failed(id, createdAt, err) for err in error.wrapping ] - return cls.construct( + return cls.model_construct( id=id, createdAt=createdAt, errorType=type(error).__name__, @@ -39,6 +39,21 @@ def from_failed( wrappedErrors=wrappedErrors, ) + @staticmethod + def schema_extra(schema: Dict[str, Any], model: object) -> None: + """Append the schema to make the errorCode appear required. + + `errorCode`, `wrappedErrors`, and `errorInfo` have defaults because they are not included in earlier + versions of this model, _and_ this model is loaded directly from + the on-robot store. That means that, without a default, it will + fail to parse. Once a default is defined, the automated schema will + mark this as a non-required field, which is misleading as this is + a response from the server to the client and it will always have an + errorCode defined. This hack is required because it informs the client + that it does not, in fact, have to account for a missing errorCode, wrappedError, or errorInfo. + """ + schema["required"].extend(["errorCode", "wrappedErrors", "errorInfo"]) + id: str = Field(..., description="Unique identifier of this error occurrence.") createdAt: datetime = Field(..., description="When the error occurred.") @@ -145,23 +160,7 @@ def from_failed( default=[], description="Errors that may have caused this one." ) - class Config: - """Customize configuration for this model.""" - - @staticmethod - def schema_extra(schema: Dict[str, Any], model: object) -> None: - """Append the schema to make the errorCode appear required. - - `errorCode`, `wrappedErrors`, and `errorInfo` have defaults because they are not included in earlier - versions of this model, _and_ this model is loaded directly from - the on-robot store. That means that, without a default, it will - fail to parse. Once a default is defined, the automated schema will - mark this as a non-required field, which is misleading as this is - a response from the server to the client and it will always have an - errorCode defined. This hack is required because it informs the client - that it does not, in fact, have to account for a missing errorCode, wrappedError, or errorInfo. - """ - schema["required"].extend(["errorCode", "wrappedErrors", "errorInfo"]) + model_config = ConfigDict(json_schema_extra=schema_extra) # TODO (tz, 7-12-23): move this to exceptions.py when we stop relaying on ErrorOccurrence. @@ -180,4 +179,4 @@ def __init__( self.original_error = original_error -ErrorOccurrence.update_forward_refs() +ErrorOccurrence.model_rebuild() diff --git a/api/src/opentrons/protocol_engine/errors/exceptions.py b/api/src/opentrons/protocol_engine/errors/exceptions.py index 43efc7b05b0..d8c96d8dd31 100644 --- a/api/src/opentrons/protocol_engine/errors/exceptions.py +++ b/api/src/opentrons/protocol_engine/errors/exceptions.py @@ -244,6 +244,19 @@ def __init__( super().__init__(ErrorCodes.GENERAL_ERROR, message, details, wrapping) +class InvalidLiquidError(ProtocolEngineError): + """Raised when attempting to add a liquid with an invalid property.""" + + def __init__( + self, + message: Optional[str] = None, + details: Optional[Dict[str, Any]] = None, + wrapping: Optional[Sequence[EnumeratedError]] = None, + ) -> None: + """Build an InvalidLiquidError.""" + super().__init__(ErrorCodes.INVALID_PROTOCOL_DATA, message, details, wrapping) + + class LabwareDefinitionDoesNotExistError(ProtocolEngineError): """Raised when referencing a labware definition that does not exist.""" @@ -270,6 +283,19 @@ def __init__( super().__init__(ErrorCodes.GENERAL_ERROR, message, details, wrapping) +class LabwareCannotSitOnDeckError(ProtocolEngineError): + """Raised when a labware is incompatible with a deck slot.""" + + def __init__( + self, + message: Optional[str] = None, + details: Optional[Dict[str, Any]] = None, + wrapping: Optional[Sequence[EnumeratedError]] = None, + ) -> None: + """Build a LabwareCannotSitOnDeckError.""" + super().__init__(ErrorCodes.GENERAL_ERROR, message, details, wrapping) + + class LabwareIsInStackError(ProtocolEngineError): """Raised when trying to move to or physically interact with a labware that has another labware on top.""" @@ -348,6 +374,32 @@ def __init__( super().__init__(ErrorCodes.GENERAL_ERROR, message, details, wrapping) +class TouchTipIncompatibleArgumentsError(ProtocolEngineError): + """Raised when touch tip is used with both a custom radius and a mmFromEdge argument.""" + + def __init__( + self, + message: Optional[str] = None, + details: Optional[Dict[str, Any]] = None, + wrapping: Optional[Sequence[EnumeratedError]] = None, + ) -> None: + """Build a TouchTipIncompatibleArgumentsError.""" + super().__init__(ErrorCodes.GENERAL_ERROR, message, details, wrapping) + + +class UnsupportedLabwareForActionError(ProtocolEngineError): + """Raised when trying to use an unsupported labware for a command.""" + + def __init__( + self, + message: Optional[str] = None, + details: Optional[Dict[str, Any]] = None, + wrapping: Optional[Sequence[EnumeratedError]] = None, + ) -> None: + """Build a UnsupportedLabwareForActionError.""" + super().__init__(ErrorCodes.GENERAL_ERROR, message, details, wrapping) + + class WellDoesNotExistError(ProtocolEngineError): """Raised when referencing a well that does not exist.""" @@ -1155,3 +1207,27 @@ def __init__( ) -> None: """Build an StorageLimitReached.""" super().__init__(ErrorCodes.GENERAL_ERROR, message, detail, wrapping) + + +class LiquidClassDoesNotExistError(ProtocolEngineError): + """Raised when referencing a liquid class that has not been loaded.""" + + def __init__( + self, + message: Optional[str] = None, + details: Optional[Dict[str, Any]] = None, + wrapping: Optional[Sequence[EnumeratedError]] = None, + ) -> None: + super().__init__(ErrorCodes.GENERAL_ERROR, message, details, wrapping) + + +class LiquidClassRedefinitionError(ProtocolEngineError): + """Raised when attempting to load a liquid class that conflicts with a liquid class already loaded.""" + + def __init__( + self, + message: Optional[str] = None, + details: Optional[Dict[str, Any]] = None, + wrapping: Optional[Sequence[EnumeratedError]] = None, + ) -> None: + super().__init__(ErrorCodes.GENERAL_ERROR, message, details, wrapping) diff --git a/api/src/opentrons/protocol_engine/execution/command_executor.py b/api/src/opentrons/protocol_engine/execution/command_executor.py index b6c686e0b11..47184d94ef2 100644 --- a/api/src/opentrons/protocol_engine/execution/command_executor.py +++ b/api/src/opentrons/protocol_engine/execution/command_executor.py @@ -188,7 +188,7 @@ async def execute(self, command_id: str) -> None: "completedAt": self._model_utils.get_timestamp(), "notes": note_tracker.get_notes(), } - succeeded_command = running_command.copy(update=update) + succeeded_command = running_command.model_copy(update=update) self._action_dispatcher.dispatch( SucceedCommandAction( command=succeeded_command, diff --git a/api/src/opentrons/protocol_engine/execution/equipment.py b/api/src/opentrons/protocol_engine/execution/equipment.py index 792bd583b88..3d26b355741 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, Union +from typing import Optional, overload, Union, List from opentrons_shared_data.pipette.types import PipetteNameType @@ -152,10 +152,6 @@ async def load_labware( Returns: A LoadedLabwareData object. """ - labware_id = ( - labware_id if labware_id is not None else self._model_utils.generate_id() - ) - definition_uri = uri_from_details( load_name=load_name, namespace=namespace, @@ -172,6 +168,10 @@ async def load_labware( version=version, ) + labware_id = ( + labware_id if labware_id is not None else self._model_utils.generate_id() + ) + # Allow propagation of ModuleNotLoadedError. offset_id = self.find_applicable_labware_offset_id( labware_definition_uri=definition_uri, @@ -379,6 +379,74 @@ async def load_module( definition=attached_module.definition, ) + async def load_lids( + self, + load_name: str, + namespace: str, + version: int, + location: LabwareLocation, + quantity: int, + ) -> List[LoadedLabwareData]: + """Load one or many lid labware by assigning an identifier and pulling required data. + + Args: + load_name: The lid labware's load name. + namespace: The lid labware's namespace. + version: The lid labware's version. + location: The deck location at which lid(s) will be placed. + labware_ids: An optional list of identifiers to assign the labware. If None, + an identifier will be generated. + + Raises: + ModuleNotLoadedError: If `location` references a module ID + that doesn't point to a valid loaded module. + + Returns: + A list of LoadedLabwareData objects. + """ + definition_uri = uri_from_details( + load_name=load_name, + namespace=namespace, + version=version, + ) + try: + # Try to use existing definition in state. + definition = self._state_store.labware.get_definition_by_uri(definition_uri) + except LabwareDefinitionDoesNotExistError: + definition = await self._labware_data_provider.get_labware_definition( + load_name=load_name, + namespace=namespace, + version=version, + ) + + stack_limit = definition.stackLimit if definition.stackLimit is not None else 1 + if quantity > stack_limit: + raise ValueError( + f"Requested quantity {quantity} is greater than the stack limit of {stack_limit} provided by definition for {load_name}." + ) + + # Allow propagation of ModuleNotLoadedError. + if ( + isinstance(location, DeckSlotLocation) + and definition.parameters.isDeckSlotCompatible is not None + and not definition.parameters.isDeckSlotCompatible + ): + raise ValueError( + f"Lid Labware {load_name} cannot be loaded onto a Deck Slot." + ) + + load_labware_data_list = [] + for i in range(quantity): + load_labware_data_list.append( + LoadedLabwareData( + labware_id=self._model_utils.generate_id(), + definition=definition, + offsetId=None, + ) + ) + + return load_labware_data_list + async def configure_for_volume( self, pipette_id: str, volume: float, tip_overlap_version: Optional[str] ) -> LoadedConfigureForVolumeData: diff --git a/api/src/opentrons/protocol_engine/execution/gantry_mover.py b/api/src/opentrons/protocol_engine/execution/gantry_mover.py index 8b33e43f437..ca90d0a12cb 100644 --- a/api/src/opentrons/protocol_engine/execution/gantry_mover.py +++ b/api/src/opentrons/protocol_engine/execution/gantry_mover.py @@ -1,11 +1,18 @@ """Gantry movement wrapper for hardware and simulation based movement.""" -from typing import Optional, List, Dict +from logging import getLogger +from opentrons.config.types import OT3Config +from functools import partial +from typing import Optional, List, Dict, Tuple from typing_extensions import Protocol as TypingProtocol -from opentrons.types import Point, Mount +from opentrons.types import Point, Mount, MountType from opentrons.hardware_control import HardwareControlAPI -from opentrons.hardware_control.types import Axis as HardwareAxis +from opentrons.hardware_control.types import Axis as HardwareAxis, CriticalPoint +from opentrons.hardware_control.motion_utilities import ( + target_axis_map_from_relative, + target_axis_map_from_absolute, +) from opentrons_shared_data.errors.exceptions import PositionUnknownError from opentrons.motion_planning import Waypoint @@ -14,6 +21,8 @@ from ..types import MotorAxis, CurrentWell from ..errors import MustHomeError, InvalidAxisForRobotType +log = getLogger(__name__) + _MOTOR_AXIS_TO_HARDWARE_AXIS: Dict[MotorAxis, HardwareAxis] = { MotorAxis.X: HardwareAxis.X, @@ -24,8 +33,38 @@ MotorAxis.RIGHT_PLUNGER: HardwareAxis.C, MotorAxis.EXTENSION_Z: HardwareAxis.Z_G, MotorAxis.EXTENSION_JAW: HardwareAxis.G, + MotorAxis.AXIS_96_CHANNEL_CAM: HardwareAxis.Q, +} + +_MOTOR_AXIS_TO_HARDWARE_MOUNT: Dict[MotorAxis, Mount] = { + MotorAxis.LEFT_Z: Mount.LEFT, + MotorAxis.RIGHT_Z: Mount.RIGHT, + MotorAxis.EXTENSION_Z: Mount.EXTENSION, +} + +_HARDWARE_MOUNT_MOTOR_AXIS_TO: Dict[Mount, MotorAxis] = { + Mount.LEFT: MotorAxis.LEFT_Z, + Mount.RIGHT: MotorAxis.RIGHT_Z, + Mount.EXTENSION: MotorAxis.EXTENSION_Z, +} + +_HARDWARE_AXIS_TO_MOTOR_AXIS: Dict[HardwareAxis, MotorAxis] = { + HardwareAxis.X: MotorAxis.X, + HardwareAxis.Y: MotorAxis.Y, + HardwareAxis.Z: MotorAxis.LEFT_Z, + HardwareAxis.A: MotorAxis.RIGHT_Z, + HardwareAxis.B: MotorAxis.LEFT_PLUNGER, + HardwareAxis.C: MotorAxis.RIGHT_PLUNGER, + HardwareAxis.P_L: MotorAxis.LEFT_PLUNGER, + HardwareAxis.P_R: MotorAxis.RIGHT_PLUNGER, + HardwareAxis.Z_L: MotorAxis.LEFT_Z, + HardwareAxis.Z_R: MotorAxis.RIGHT_Z, + HardwareAxis.Z_G: MotorAxis.EXTENSION_Z, + HardwareAxis.G: MotorAxis.EXTENSION_JAW, + HardwareAxis.Q: MotorAxis.AXIS_96_CHANNEL_CAM, } + # The height of the bottom of the pipette nozzle at home position without any tips. # We rely on this being the same for every OT-3 pipette. # @@ -36,6 +75,8 @@ # That OT3Simulator return value is what Protocol Engine uses for simulation when Protocol Engine # is configured to not virtualize pipettes, so this number should match it. VIRTUAL_MAX_OT3_HEIGHT = 248.0 +# This number was found by using the longest pipette's P1000V2 default configuration values. +VIRTUAL_MAX_OT2_HEIGHT = 268.14 class GantryMover(TypingProtocol): @@ -50,16 +91,46 @@ async def get_position( """Get the current position of the gantry.""" ... + async def get_position_from_mount( + self, + mount: Mount, + critical_point: Optional[CriticalPoint] = None, + fail_on_not_homed: bool = False, + ) -> Point: + """Get the current position of the gantry based on the given mount.""" + ... + def get_max_travel_z(self, pipette_id: str) -> float: """Get the maximum allowed z-height for pipette movement.""" ... + def get_max_travel_z_from_mount(self, mount: MountType) -> float: + """Get the maximum allowed z-height for mount movement.""" + ... + + async def move_axes( + self, + axis_map: Dict[MotorAxis, float], + critical_point: Optional[Dict[MotorAxis, float]] = None, + speed: Optional[float] = None, + relative_move: bool = False, + expect_stalls: bool = False, + ) -> Dict[MotorAxis, float]: + """Move a set of axes a given distance.""" + ... + async def move_to( self, pipette_id: str, waypoints: List[Waypoint], speed: Optional[float] ) -> Point: """Move the hardware gantry to a waypoint.""" ... + async def move_mount_to( + self, mount: Mount, waypoints: List[Waypoint], speed: Optional[float] + ) -> Point: + """Move the provided hardware mount to a waypoint.""" + ... + async def move_relative( self, pipette_id: str, @@ -85,6 +156,16 @@ def motor_axis_to_hardware_axis(self, motor_axis: MotorAxis) -> HardwareAxis: """Transform an engine motor axis into a hardware axis.""" ... + def pick_mount_from_axis_map(self, axis_map: Dict[MotorAxis, float]) -> Mount: + """Find a mount axis in the axis_map if it exists otherwise default to left mount.""" + ... + + def motor_axes_to_present_hardware_axes( + self, motor_axes: List[MotorAxis] + ) -> List[HardwareAxis]: + """Transform a list of engine axes into a list of hardware axes, filtering out non-present axes.""" + ... + class HardwareGantryMover(GantryMover): """Hardware API based gantry movement handler.""" @@ -93,10 +174,70 @@ def __init__(self, hardware_api: HardwareControlAPI, state_view: StateView) -> N self._hardware_api = hardware_api self._state_view = state_view + def motor_axes_to_present_hardware_axes( + self, motor_axes: List[MotorAxis] + ) -> List[HardwareAxis]: + """Get hardware axes from engine axes while filtering out non-present axes.""" + return [ + self.motor_axis_to_hardware_axis(motor_axis) + for motor_axis in motor_axes + if self._hardware_api.axis_is_present( + self.motor_axis_to_hardware_axis(motor_axis) + ) + ] + def motor_axis_to_hardware_axis(self, motor_axis: MotorAxis) -> HardwareAxis: """Transform an engine motor axis into a hardware axis.""" return _MOTOR_AXIS_TO_HARDWARE_AXIS[motor_axis] + def _hardware_axis_to_motor_axis(self, motor_axis: HardwareAxis) -> MotorAxis: + """Transform an hardware axis into a engine motor axis.""" + return _HARDWARE_AXIS_TO_MOTOR_AXIS[motor_axis] + + def _convert_axis_map_for_hw( + self, axis_map: Dict[MotorAxis, float] + ) -> Dict[HardwareAxis, float]: + """Transform an engine motor axis map to a hardware axis map.""" + return {_MOTOR_AXIS_TO_HARDWARE_AXIS[ax]: dist for ax, dist in axis_map.items()} + + def _critical_point_for( + self, mount: Mount, cp_override: Optional[Dict[MotorAxis, float]] = None + ) -> Point: + if cp_override: + return Point( + x=cp_override[MotorAxis.X], + y=cp_override[MotorAxis.Y], + z=cp_override[_HARDWARE_MOUNT_MOTOR_AXIS_TO[mount]], + ) + else: + return self._hardware_api.critical_point_for(mount) + + def _get_gantry_offsets_for_robot_type( + self, + ) -> Tuple[Point, Point, Optional[Point]]: + if isinstance(self._hardware_api.config, OT3Config): + return ( + Point(*self._hardware_api.config.left_mount_offset), + Point(*self._hardware_api.config.right_mount_offset), + Point(*self._hardware_api.config.gripper_mount_offset), + ) + else: + return ( + Point(*self._hardware_api.config.left_mount_offset), + Point(0, 0, 0), + None, + ) + + def pick_mount_from_axis_map(self, axis_map: Dict[MotorAxis, float]) -> Mount: + """Find a mount axis in the axis_map if it exists otherwise default to left mount.""" + found_mount = Mount.LEFT + mounts = list(_MOTOR_AXIS_TO_HARDWARE_MOUNT.keys()) + for k in axis_map.keys(): + if k in mounts: + found_mount = _MOTOR_AXIS_TO_HARDWARE_MOUNT[k] + break + return found_mount + async def get_position( self, pipette_id: str, @@ -114,12 +255,33 @@ async def get_position( pipette_id=pipette_id, current_location=current_well, ) + point = await self.get_position_from_mount( + mount=pipette_location.mount.to_hw_mount(), + critical_point=pipette_location.critical_point, + fail_on_not_homed=fail_on_not_homed, + ) + return point + + async def get_position_from_mount( + self, + mount: Mount, + critical_point: Optional[CriticalPoint] = None, + fail_on_not_homed: bool = False, + ) -> Point: + """Get the current position of the gantry based on the mount. + + Args: + mount: The mount to get the position for. + critical_point: Optional parameter for getting instrument location data, effects critical point. + fail_on_not_homed: Raise PositionUnknownError if gantry position is not known. + """ try: - return await self._hardware_api.gantry_position( - mount=pipette_location.mount.to_hw_mount(), - critical_point=pipette_location.critical_point, + point = await self._hardware_api.gantry_position( + mount=mount, + critical_point=critical_point, fail_on_not_homed=fail_on_not_homed, ) + return point except PositionUnknownError as e: raise MustHomeError(message=str(e), wrapping=[e]) @@ -129,8 +291,16 @@ def get_max_travel_z(self, pipette_id: str) -> float: Args: pipette_id: Pipette ID to get max travel z-height for. """ - hw_mount = self._state_view.pipettes.get_mount(pipette_id).to_hw_mount() - return self._hardware_api.get_instrument_max_height(mount=hw_mount) + mount = self._state_view.pipettes.get_mount(pipette_id) + return self.get_max_travel_z_from_mount(mount=mount) + + def get_max_travel_z_from_mount(self, mount: MountType) -> float: + """Get the maximum allowed z-height for any mount movement. + + Args: + mount: Mount to get max travel z-height for. + """ + return self._hardware_api.get_instrument_max_height(mount=mount.to_hw_mount()) async def move_to( self, pipette_id: str, waypoints: List[Waypoint], speed: Optional[float] @@ -150,6 +320,91 @@ async def move_to( return waypoints[-1].position + async def move_mount_to( + self, mount: Mount, waypoints: List[Waypoint], speed: Optional[float] + ) -> Point: + """Move the given hardware mount to a waypoint.""" + assert len(waypoints) > 0, "Must have at least one waypoint" + for waypoint in waypoints: + log.info(f"The current waypoint moving is {waypoint}") + await self._hardware_api.move_to( + mount=mount, + abs_position=waypoint.position, + critical_point=waypoint.critical_point, + speed=speed, + ) + + return waypoints[-1].position + + async def move_axes( + self, + axis_map: Dict[MotorAxis, float], + critical_point: Optional[Dict[MotorAxis, float]] = None, + speed: Optional[float] = None, + relative_move: bool = False, + expect_stalls: bool = False, + ) -> Dict[MotorAxis, float]: + """Move a set of axes a given distance. + + Args: + axis_map: The mapping of axes to command. + critical_point: A critical point override for axes + speed: Optional speed parameter for the move. + relative_move: Whether the axis map needs to be converted from a relative to absolute move. + expect_stalls: Whether it is expected that the move triggers a stall error. + """ + try: + pos_hw = self._convert_axis_map_for_hw(axis_map) + mount = self.pick_mount_from_axis_map(axis_map) + if relative_move: + current_position = await self._hardware_api.current_position( + mount, refresh=True + ) + log.info(f"The current position of the robot is: {current_position}.") + converted_current_position_deck = ( + self._hardware_api.get_deck_from_machine(current_position) + ) + log.info(f"The current position of the robot is: {current_position}.") + + pos_hw = target_axis_map_from_relative(pos_hw, current_position) + log.info( + f"The absolute position is: {pos_hw} and hw pos map is {pos_hw}." + ) + log.info(f"The calculated move {pos_hw} and {mount}") + ( + left_offset, + right_offset, + gripper_offset, + ) = self._get_gantry_offsets_for_robot_type() + absolute_pos = target_axis_map_from_absolute( + mount, + pos_hw, + partial(self._critical_point_for, cp_override=critical_point), + left_mount_offset=left_offset, + right_mount_offset=right_offset, + gripper_mount_offset=gripper_offset, + ) + log.info(f"The prepped abs {absolute_pos}") + await self._hardware_api.move_axes( + position=absolute_pos, + speed=speed, + expect_stalls=expect_stalls, + ) + + except PositionUnknownError as e: + raise MustHomeError(message=str(e), wrapping=[e]) + + current_position = await self._hardware_api.current_position( + mount, refresh=True + ) + converted_current_position_deck = self._hardware_api.get_deck_from_machine( + current_position + ) + return { + self._hardware_axis_to_motor_axis(ax): pos + for ax, pos in converted_current_position_deck.items() + } + async def move_relative( self, pipette_id: str, @@ -239,6 +494,16 @@ def motor_axis_to_hardware_axis(self, motor_axis: MotorAxis) -> HardwareAxis: """Transform an engine motor axis into a hardware axis.""" return _MOTOR_AXIS_TO_HARDWARE_AXIS[motor_axis] + def pick_mount_from_axis_map(self, axis_map: Dict[MotorAxis, float]) -> Mount: + """Find a mount axis in the axis_map if it exists otherwise default to left mount.""" + found_mount = Mount.LEFT + mounts = list(_MOTOR_AXIS_TO_HARDWARE_MOUNT.keys()) + for k in axis_map.keys(): + if k in mounts: + found_mount = _MOTOR_AXIS_TO_HARDWARE_MOUNT[k] + break + return found_mount + async def get_position( self, pipette_id: str, @@ -261,6 +526,31 @@ async def get_position( origin = Point(x=0, y=0, z=0) return origin + async def get_position_from_mount( + self, + mount: Mount, + critical_point: Optional[CriticalPoint] = None, + fail_on_not_homed: bool = False, + ) -> Point: + """Get the current position of the gantry based on the mount. + + Args: + mount: The mount to get the position for. + critical_point: Optional parameter for getting instrument location data, effects critical point. + fail_on_not_homed: Raise PositionUnknownError if gantry position is not known. + """ + pipette = self._state_view.pipettes.get_by_mount(MountType[mount.name]) + origin_deck_point = ( + self._state_view.pipettes.get_deck_point(pipette.id) if pipette else None + ) + if origin_deck_point is not None: + origin = Point( + x=origin_deck_point.x, y=origin_deck_point.y, z=origin_deck_point.z + ) + else: + origin = Point(x=0, y=0, z=0) + return origin + def get_max_travel_z(self, pipette_id: str) -> float: """Get the maximum allowed z-height for pipette movement. @@ -278,6 +568,69 @@ def get_max_travel_z(self, pipette_id: str) -> float: tip_length = tip.length if tip is not None else 0 return instrument_height - tip_length + def get_max_travel_z_from_mount(self, mount: MountType) -> float: + """Get the maximum allowed z-height for mount.""" + pipette = self._state_view.pipettes.get_by_mount(mount) + if self._state_view.config.robot_type == "OT-2 Standard": + instrument_height = ( + self._state_view.pipettes.get_instrument_max_height_ot2(pipette.id) + if pipette + else VIRTUAL_MAX_OT2_HEIGHT + ) + else: + instrument_height = VIRTUAL_MAX_OT3_HEIGHT + if pipette: + tip = self._state_view.pipettes.get_attached_tip(pipette_id=pipette.id) + tip_length = tip.length if tip is not None else 0.0 + else: + tip_length = 0.0 + return instrument_height - tip_length + + async def move_axes( + self, + axis_map: Dict[MotorAxis, float], + critical_point: Optional[Dict[MotorAxis, float]] = None, + speed: Optional[float] = None, + relative_move: bool = False, + expect_stalls: bool = False, + ) -> Dict[MotorAxis, float]: + """Move the give axes map. No-op in virtual implementation.""" + mount = self.pick_mount_from_axis_map(axis_map) + current_position = await self.get_position_from_mount(mount) + updated_position = {} + if relative_move: + updated_position[MotorAxis.X] = ( + axis_map.get(MotorAxis.X, 0.0) + current_position[0] + ) + updated_position[MotorAxis.Y] = ( + axis_map.get(MotorAxis.Y, 0.0) + current_position[1] + ) + if mount == Mount.RIGHT: + updated_position[MotorAxis.RIGHT_Z] = ( + axis_map.get(MotorAxis.RIGHT_Z, 0.0) + current_position[2] + ) + elif mount == Mount.EXTENSION: + updated_position[MotorAxis.EXTENSION_Z] = ( + axis_map.get(MotorAxis.EXTENSION_Z, 0.0) + current_position[2] + ) + else: + updated_position[MotorAxis.LEFT_Z] = ( + axis_map.get(MotorAxis.LEFT_Z, 0.0) + current_position[2] + ) + else: + critical_point = critical_point or {} + updated_position = { + ax: pos - critical_point.get(ax, 0.0) for ax, pos in axis_map.items() + } + return updated_position + + async def move_mount_to( + self, mount: Mount, waypoints: List[Waypoint], speed: Optional[float] + ) -> Point: + """Move the hardware mount to a waypoint. No-op in virtual implementation.""" + assert len(waypoints) > 0, "Must have at least one waypoint" + return waypoints[-1].position + async def move_to( self, pipette_id: str, waypoints: List[Waypoint], speed: Optional[float] ) -> Point: @@ -313,6 +666,14 @@ async def prepare_for_mount_movement(self, mount: Mount) -> None: """Retract the 'idle' mount if necessary.""" pass + def motor_axes_to_present_hardware_axes( + self, motor_axes: List[MotorAxis] + ) -> List[HardwareAxis]: + """Get present hardware axes from a list of engine axes. In simulation, all axes are present.""" + return [ + self.motor_axis_to_hardware_axis(motor_axis) for motor_axis in motor_axes + ] + def create_gantry_mover( state_view: StateView, hardware_api: HardwareControlAPI diff --git a/api/src/opentrons/protocol_engine/execution/hardware_stopper.py b/api/src/opentrons/protocol_engine/execution/hardware_stopper.py index 81d4f10d94d..0312f622ac9 100644 --- a/api/src/opentrons/protocol_engine/execution/hardware_stopper.py +++ b/api/src/opentrons/protocol_engine/execution/hardware_stopper.py @@ -65,7 +65,7 @@ async def _home_everything_except_plungers(self) -> None: axes=[MotorAxis.X, MotorAxis.Y, MotorAxis.LEFT_Z, MotorAxis.RIGHT_Z] ) - async def _drop_tip(self) -> None: + async def _try_to_drop_tips(self) -> None: """Drop currently attached tip, if any, into trash after a run cancel.""" attached_tips = self._state_store.pipettes.get_all_attached_tips() @@ -134,9 +134,9 @@ async def do_stop_and_recover( PostRunHardwareState.HOME_THEN_DISENGAGE, ) if drop_tips_after_run: - await self._drop_tip() - await self._hardware_api.stop(home_after=home_after_stop) - else: - await self._hardware_api.stop(home_after=False) - if home_after_stop: - await self._home_everything_except_plungers() + await self._try_to_drop_tips() + + await self._hardware_api.stop(home_after=False) + + if home_after_stop: + await self._home_everything_except_plungers() diff --git a/api/src/opentrons/protocol_engine/execution/movement.py b/api/src/opentrons/protocol_engine/execution/movement.py index 7681a1ce07a..be8bbbb8de2 100644 --- a/api/src/opentrons/protocol_engine/execution/movement.py +++ b/api/src/opentrons/protocol_engine/execution/movement.py @@ -149,6 +149,33 @@ async def move_to_well( return final_point + async def move_mount_to( + self, mount: MountType, destination: DeckPoint, speed: Optional[float] = None + ) -> Point: + """Move mount to a specific location on the deck.""" + hw_mount = mount.to_hw_mount() + await self._gantry_mover.prepare_for_mount_movement(hw_mount) + origin = await self._gantry_mover.get_position_from_mount(mount=hw_mount) + max_travel_z = self._gantry_mover.get_max_travel_z_from_mount(mount=mount) + + # calculate the movement's waypoints + waypoints = self._state_store.motion.get_movement_waypoints_to_coords( + origin=origin, + dest=Point(x=destination.x, y=destination.y, z=destination.z), + max_travel_z=max_travel_z, + direct=False, + additional_min_travel_z=None, + ) + + # move through the waypoints + final_point = await self._gantry_mover.move_mount_to( + mount=hw_mount, + waypoints=waypoints, + speed=speed, + ) + + return final_point + async def move_to_addressable_area( self, pipette_id: str, diff --git a/api/src/opentrons/protocol_engine/execution/pipetting.py b/api/src/opentrons/protocol_engine/execution/pipetting.py index 2964f02d183..10d613e4dcf 100644 --- a/api/src/opentrons/protocol_engine/execution/pipetting.py +++ b/api/src/opentrons/protocol_engine/execution/pipetting.py @@ -91,7 +91,11 @@ def get_is_ready_to_aspirate(self, pipette_id: str) -> bool: ) async def prepare_for_aspirate(self, pipette_id: str) -> None: - """Prepare for pipette aspiration.""" + """Prepare for pipette aspiration. + + Raises: + PipetteOverpressureError, propagated as-is from the hardware controller. + """ hw_mount = self._state_view.pipettes.get_mount(pipette_id).to_hw_mount() await self._hardware_api.prepare_for_aspirate(mount=hw_mount) diff --git a/api/src/opentrons/protocol_engine/execution/tip_handler.py b/api/src/opentrons/protocol_engine/execution/tip_handler.py index dde67ece007..d27925e00fe 100644 --- a/api/src/opentrons/protocol_engine/execution/tip_handler.py +++ b/api/src/opentrons/protocol_engine/execution/tip_handler.py @@ -1,11 +1,12 @@ """Tip pickup and drop procedures.""" + from typing import Optional, Dict from typing_extensions import Protocol as TypingProtocol from opentrons.hardware_control import HardwareControlAPI from opentrons.hardware_control.types import FailedTipStateCheck, InstrumentProbeType from opentrons.protocol_engine.errors.exceptions import PickUpTipTipNotAttachedError -from opentrons.types import Mount +from opentrons.types import Mount, NozzleConfigurationType from opentrons_shared_data.errors.exceptions import ( CommandPreconditionViolated, @@ -23,9 +24,6 @@ ProtocolEngineError, ) -from opentrons.hardware_control.nozzle_manager import NozzleConfigurationType - - PRIMARY_NOZZLE_TO_ENDING_NOZZLE_MAP = { "A1": {"COLUMN": "H1", "ROW": "A12"}, "H1": {"COLUMN": "A1", "ROW": "H12"}, @@ -64,6 +62,7 @@ async def pick_up_tip( pipette_id: str, labware_id: str, well_name: str, + do_not_ignore_tip_presence: bool = True, ) -> TipGeometry: """Pick up the named tip. @@ -77,7 +76,13 @@ async def pick_up_tip( """ ... - async def drop_tip(self, pipette_id: str, home_after: Optional[bool]) -> None: + async def drop_tip( + self, + pipette_id: str, + home_after: Optional[bool], + do_not_ignore_tip_presence: bool = True, + ignore_plunger: bool = False, + ) -> None: """Drop the attached tip into the current location. Pipette should be in place over the destination prior to calling this method. @@ -232,6 +237,7 @@ async def pick_up_tip( pipette_id: str, labware_id: str, well_name: str, + do_not_ignore_tip_presence: bool = True, ) -> TipGeometry: """See documentation on abstract base class.""" hw_mount = self._get_hw_mount(pipette_id) @@ -255,10 +261,11 @@ async def pick_up_tip( await self._hardware_api.tip_pickup_moves( mount=hw_mount, presses=None, increment=None ) - try: - await self.verify_tip_presence(pipette_id, TipPresenceStatus.PRESENT) - except TipNotAttachedError as e: - raise PickUpTipTipNotAttachedError(tip_geometry=tip_geometry) from e + if do_not_ignore_tip_presence: + try: + await self.verify_tip_presence(pipette_id, TipPresenceStatus.PRESENT) + except TipNotAttachedError as e: + raise PickUpTipTipNotAttachedError(tip_geometry=tip_geometry) from e self.cache_tip(pipette_id, tip_geometry) @@ -266,7 +273,13 @@ async def pick_up_tip( return tip_geometry - async def drop_tip(self, pipette_id: str, home_after: Optional[bool]) -> None: + async def drop_tip( + self, + pipette_id: str, + home_after: Optional[bool], + do_not_ignore_tip_presence: bool = True, + ignore_plunger: bool = False, + ) -> None: """See documentation on abstract base class.""" hw_mount = self._get_hw_mount(pipette_id) @@ -277,10 +290,13 @@ async def drop_tip(self, pipette_id: str, home_after: Optional[bool]) -> None: else: kwargs = {} - await self._hardware_api.tip_drop_moves(mount=hw_mount, **kwargs) + await self._hardware_api.tip_drop_moves( + mount=hw_mount, ignore_plunger=ignore_plunger, **kwargs + ) - # Allow TipNotAttachedError to propagate. - await self.verify_tip_presence(pipette_id, TipPresenceStatus.ABSENT) + if do_not_ignore_tip_presence: + # Allow TipNotAttachedError to propagate. + await self.verify_tip_presence(pipette_id, TipPresenceStatus.ABSENT) self.remove_tip(pipette_id) @@ -326,8 +342,8 @@ async def verify_tip_presence( follow_singular_sensor: Optional[InstrumentProbeType] = None, ) -> None: """See documentation on abstract base class.""" - nozzle_configuration = ( - self._state_view.pipettes.state.nozzle_configuration_by_id[pipette_id] + nozzle_configuration = self._state_view.pipettes.get_nozzle_configuration( + pipette_id=pipette_id ) # Configuration metrics by which tip presence checking is ignored @@ -385,6 +401,7 @@ async def pick_up_tip( pipette_id: str, labware_id: str, well_name: str, + do_not_ignore_tip_presence: bool = True, ) -> TipGeometry: """Pick up a tip at the current location using a virtual pipette. @@ -426,6 +443,8 @@ async def drop_tip( self, pipette_id: str, home_after: Optional[bool], + do_not_ignore_tip_presence: bool = True, + ignore_plunger: bool = False, ) -> None: """Pick up a tip at the current location using a virtual pipette. diff --git a/api/src/opentrons/protocol_engine/notes/notes.py b/api/src/opentrons/protocol_engine/notes/notes.py index 8c349d167cd..2ec71d90b55 100644 --- a/api/src/opentrons/protocol_engine/notes/notes.py +++ b/api/src/opentrons/protocol_engine/notes/notes.py @@ -35,7 +35,7 @@ def make_error_recovery_debug_note(type: "ErrorRecoveryType") -> CommandNote: This is intended to be read by developers and support people, not computers. """ message = f"Handling this command failure with {type.name}." - return CommandNote.construct( + return CommandNote.model_construct( noteKind="debugErrorRecovery", shortMessage=message, longMessage=message, diff --git a/api/src/opentrons/protocol_engine/protocol_engine.py b/api/src/opentrons/protocol_engine/protocol_engine.py index df9a00fe131..92d992016cd 100644 --- a/api/src/opentrons/protocol_engine/protocol_engine.py +++ b/api/src/opentrons/protocol_engine/protocol_engine.py @@ -30,7 +30,6 @@ HexColor, PostRunHardwareState, DeckConfigurationType, - AddressableAreaLocation, ) from .execution import ( QueueWorker, @@ -427,7 +426,7 @@ async def finish( post_run_hardware_state: The state in which to leave the gantry and motors in after the run is over. """ - if self._state_store.commands.state.stopped_by_estop: + if self._state_store.commands.get_is_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 waitForDuration. @@ -565,15 +564,17 @@ def add_liquid( description=(description or ""), displayColor=color, ) + validated_liquid = self._state_store.liquid.validate_liquid_allowed( + liquid=liquid + ) - self._action_dispatcher.dispatch(AddLiquidAction(liquid=liquid)) - return liquid + self._action_dispatcher.dispatch(AddLiquidAction(liquid=validated_liquid)) + return validated_liquid def add_addressable_area(self, addressable_area_name: str) -> None: """Add an addressable area to state.""" - area = AddressableAreaLocation(addressableAreaName=addressable_area_name) self._action_dispatcher.dispatch( - AddAddressableAreaAction(addressable_area=area) + AddAddressableAreaAction(addressable_area_name) ) def reset_tips(self, labware_id: str) -> None: diff --git a/api/src/opentrons/protocol_engine/resources/labware_data_provider.py b/api/src/opentrons/protocol_engine/resources/labware_data_provider.py index 0b08720d4e9..8d5cdfc7899 100644 --- a/api/src/opentrons/protocol_engine/resources/labware_data_provider.py +++ b/api/src/opentrons/protocol_engine/resources/labware_data_provider.py @@ -44,7 +44,7 @@ async def get_labware_definition( def _get_labware_definition_sync( load_name: str, namespace: str, version: int ) -> LabwareDefinition: - return LabwareDefinition.parse_obj( + return LabwareDefinition.model_validate( get_labware_definition(load_name, namespace, version) ) diff --git a/api/src/opentrons/protocol_engine/resources/labware_validation.py b/api/src/opentrons/protocol_engine/resources/labware_validation.py index 090723ffb7e..b33650e65be 100644 --- a/api/src/opentrons/protocol_engine/resources/labware_validation.py +++ b/api/src/opentrons/protocol_engine/resources/labware_validation.py @@ -14,6 +14,11 @@ def is_absorbance_reader_lid(load_name: str) -> bool: return load_name == "opentrons_flex_lid_absorbance_plate_reader_module" +def is_evotips(load_name: str) -> bool: + """Check if a labware is an evotips tiprack.""" + return load_name == "evotips_opentrons_96_labware" + + def validate_definition_is_labware(definition: LabwareDefinition) -> bool: """Validate that one of the definition's allowed roles is `labware`. @@ -32,6 +37,11 @@ def validate_definition_is_lid(definition: LabwareDefinition) -> bool: return LabwareRole.lid in definition.allowedRoles +def validate_definition_is_system(definition: LabwareDefinition) -> bool: + """Validate that one of the definition's allowed roles is `system`.""" + return LabwareRole.system in definition.allowedRoles + + def validate_labware_can_be_stacked( top_labware_definition: LabwareDefinition, below_labware_load_name: str ) -> bool: @@ -39,6 +49,14 @@ def validate_labware_can_be_stacked( return below_labware_load_name in top_labware_definition.stackingOffsetWithLabware +def validate_labware_can_be_ondeck(definition: LabwareDefinition) -> bool: + """Validate that the labware being loaded onto the deck can sit in a slot.""" + return ( + definition.parameters.quirks is None + or "stackingOnly" not in definition.parameters.quirks + ) + + def validate_gripper_compatible(definition: LabwareDefinition) -> bool: """Validate that the labware definition does not have a quirk disallowing movement with gripper.""" return ( diff --git a/api/src/opentrons/protocol_engine/resources/module_data_provider.py b/api/src/opentrons/protocol_engine/resources/module_data_provider.py index a12b85ee5b3..3ee7b5d6bd9 100644 --- a/api/src/opentrons/protocol_engine/resources/module_data_provider.py +++ b/api/src/opentrons/protocol_engine/resources/module_data_provider.py @@ -22,7 +22,7 @@ class ModuleDataProvider: def get_definition(model: ModuleModel) -> ModuleDefinition: """Get the module definition.""" data = load_definition(model_or_loadname=model.value, version="3") - return ModuleDefinition.parse_obj(data) + return ModuleDefinition.model_validate(data) @staticmethod def load_module_calibrations() -> Dict[str, ModuleOffsetData]: diff --git a/api/src/opentrons/protocol_engine/resources/pipette_data_provider.py b/api/src/opentrons/protocol_engine/resources/pipette_data_provider.py index d3998c69bd1..ee721c88f2c 100644 --- a/api/src/opentrons/protocol_engine/resources/pipette_data_provider.py +++ b/api/src/opentrons/protocol_engine/resources/pipette_data_provider.py @@ -67,6 +67,9 @@ class LoadedStaticPipetteData: back_left_corner_offset: Point front_right_corner_offset: Point pipette_lld_settings: Optional[Dict[str, Dict[str, float]]] + plunger_positions: Dict[str, float] + shaft_ul_per_mm: float + available_sensors: pipette_definition.AvailableSensorDefinition class VirtualPipetteDataProvider: @@ -95,6 +98,7 @@ def configure_virtual_pipette_nozzle_layout( config.pipette_type, config.channels, config.version, + pip_types.PipetteOEMType.OT, ) new_nozzle_manager = NozzleConfigurationManager.build_from_config( config, valid_nozzle_maps @@ -127,6 +131,7 @@ def configure_virtual_pipette_for_volume( pipette_model.pipette_type, pipette_model.pipette_channels, pipette_model.pipette_version, + pipette_model.oem_type, ) liquid_class = pipette_definition.liquid_class_for_volume_between_default_and_defaultlowvolume( @@ -160,6 +165,7 @@ def _get_virtual_pipette_full_config_by_model_string( pipette_model.pipette_type, pipette_model.pipette_channels, pipette_model.pipette_version, + pipette_model.oem_type, ) def _get_virtual_pipette_static_config_by_model( # noqa: C901 @@ -176,6 +182,7 @@ def _get_virtual_pipette_static_config_by_model( # noqa: C901 pipette_model.pipette_type, pipette_model.pipette_channels, pipette_model.pipette_version, + pipette_model.oem_type, ) try: tip_type = pip_types.PipetteTipType( @@ -192,6 +199,7 @@ def _get_virtual_pipette_static_config_by_model( # noqa: C901 pipette_model.pipette_type, pipette_model.pipette_channels, pipette_model.pipette_version, + pipette_model.oem_type, ) if pipette_id not in self._nozzle_manager_layout_by_id: nozzle_manager = NozzleConfigurationManager.build_from_config( @@ -252,6 +260,7 @@ def _get_virtual_pipette_static_config_by_model( # noqa: C901 pip_back_left = config.pipette_bounding_box_offsets.back_left_corner pip_front_right = config.pipette_bounding_box_offsets.front_right_corner + plunger_positions = config.plunger_positions_configurations[liquid_class] return LoadedStaticPipetteData( model=str(pipette_model), display_name=config.display_name, @@ -280,6 +289,15 @@ def _get_virtual_pipette_static_config_by_model( # noqa: C901 pip_front_right[0], pip_front_right[1], pip_front_right[2] ), pipette_lld_settings=config.lld_settings, + plunger_positions={ + "top": plunger_positions.top, + "bottom": plunger_positions.bottom, + "blow_out": plunger_positions.blow_out, + "drop_tip": plunger_positions.drop_tip, + }, + shaft_ul_per_mm=config.shaft_ul_per_mm, + available_sensors=config.available_sensors + or pipette_definition.AvailableSensorDefinition(sensors=[]), ) def get_virtual_pipette_static_config( @@ -298,6 +316,11 @@ def get_pipette_static_config( """Get the config for a pipette, given the state/config object from the HW API.""" back_left_offset = pipette_dict["pipette_bounding_box_offsets"].back_left_corner front_right_offset = pipette_dict["pipette_bounding_box_offsets"].front_right_corner + available_sensors = ( + pipette_dict["available_sensors"] + if "available_sensors" in pipette_dict.keys() + else pipette_definition.AvailableSensorDefinition(sensors=[]) + ) return LoadedStaticPipetteData( model=pipette_dict["model"], display_name=pipette_dict["display_name"], @@ -327,6 +350,9 @@ def get_pipette_static_config( front_right_offset[0], front_right_offset[1], front_right_offset[2] ), pipette_lld_settings=pipette_dict["lld_settings"], + plunger_positions=pipette_dict["plunger_positions"], + shaft_ul_per_mm=pipette_dict["shaft_ul_per_mm"], + available_sensors=available_sensors, ) diff --git a/api/src/opentrons/protocol_engine/slot_standardization.py b/api/src/opentrons/protocol_engine/slot_standardization.py index b600258bbf0..d940517eebe 100644 --- a/api/src/opentrons/protocol_engine/slot_standardization.py +++ b/api/src/opentrons/protocol_engine/slot_standardization.py @@ -35,9 +35,9 @@ def standardize_labware_offset( original: LabwareOffsetCreate, robot_type: RobotType ) -> LabwareOffsetCreate: """Convert the deck slot in the given `LabwareOffsetCreate` to match the given robot type.""" - return original.copy( + return original.model_copy( update={ - "location": original.location.copy( + "location": original.location.model_copy( update={ "slotName": original.location.slotName.to_equivalent_for_robot_type( robot_type @@ -70,40 +70,40 @@ def standardize_command( def _standardize_load_labware( original: commands.LoadLabwareCreate, robot_type: RobotType ) -> commands.LoadLabwareCreate: - params = original.params.copy( + params = original.params.model_copy( update={ "location": _standardize_labware_location( original.params.location, robot_type ) } ) - return original.copy(update={"params": params}) + return original.model_copy(update={"params": params}) def _standardize_load_module( original: commands.LoadModuleCreate, robot_type: RobotType ) -> commands.LoadModuleCreate: - params = original.params.copy( + params = original.params.model_copy( update={ "location": _standardize_deck_slot_location( original.params.location, robot_type ) } ) - return original.copy(update={"params": params}) + return original.model_copy(update={"params": params}) def _standardize_move_labware( original: commands.MoveLabwareCreate, robot_type: RobotType ) -> commands.MoveLabwareCreate: - params = original.params.copy( + params = original.params.model_copy( update={ "newLocation": _standardize_labware_location( original.params.newLocation, robot_type ) } ) - return original.copy(update={"params": params}) + return original.model_copy(update={"params": params}) _standardize_command_functions: Dict[ @@ -135,6 +135,6 @@ def _standardize_labware_location( def _standardize_deck_slot_location( original: DeckSlotLocation, robot_type: RobotType ) -> DeckSlotLocation: - return original.copy( + return original.model_copy( update={"slotName": original.slotName.to_equivalent_for_robot_type(robot_type)} ) diff --git a/api/src/opentrons/protocol_engine/state/_move_types.py b/api/src/opentrons/protocol_engine/state/_move_types.py index b8dcb28bd8d..94201ffead9 100644 --- a/api/src/opentrons/protocol_engine/state/_move_types.py +++ b/api/src/opentrons/protocol_engine/state/_move_types.py @@ -53,15 +53,19 @@ def get_move_type_to_well( def get_edge_point_list( - center: Point, x_radius: float, y_radius: float, edge_path_type: EdgePathType + center: Point, + x_radius: float, + y_radius: float, + mm_from_edge: float, + edge_path_type: EdgePathType, ) -> List[Point]: """Get list of edge points dependent on edge path type.""" edges = EdgeList( - right=center + Point(x=x_radius, y=0, z=0), - left=center + Point(x=-x_radius, y=0, z=0), + right=center + Point(x=x_radius - mm_from_edge, y=0, z=0), + left=center + Point(x=-x_radius + mm_from_edge, y=0, z=0), center=center, - forward=center + Point(x=0, y=y_radius, z=0), - back=center + Point(x=0, y=-y_radius, z=0), + forward=center + Point(x=0, y=y_radius - mm_from_edge, z=0), + back=center + Point(x=0, y=-y_radius + mm_from_edge, z=0), ) if edge_path_type == EdgePathType.LEFT: diff --git a/api/src/opentrons/protocol_engine/state/_well_math.py b/api/src/opentrons/protocol_engine/state/_well_math.py new file mode 100644 index 00000000000..2d0998580f5 --- /dev/null +++ b/api/src/opentrons/protocol_engine/state/_well_math.py @@ -0,0 +1,193 @@ +"""Utilities for doing coverage math on wells.""" + +from typing import Iterator +from opentrons_shared_data.errors.exceptions import ( + InvalidStoredData, + InvalidProtocolData, +) + +from opentrons.hardware_control.nozzle_manager import NozzleMap + + +def wells_covered_by_pipette_configuration( + nozzle_map: NozzleMap, + target_well: str, + labware_wells_by_column: list[list[str]], +) -> Iterator[str]: + """Compute the wells covered by a pipette nozzle configuration.""" + if len(labware_wells_by_column) >= 12 and len(labware_wells_by_column[0]) >= 8: + yield from wells_covered_dense( + nozzle_map, + target_well, + labware_wells_by_column, + ) + elif len(labware_wells_by_column) < 12 and len(labware_wells_by_column[0]) < 8: + yield from wells_covered_sparse( + nozzle_map, target_well, labware_wells_by_column + ) + else: + raise InvalidStoredData( + "Labware of non-SBS and non-reservoir format cannot be handled" + ) + + +def row_col_ordinals_from_column_major_map( + target_well: str, column_major_wells: list[list[str]] +) -> tuple[int, int]: + """Turn a well name into the index of its row and column (in that order) within the labware.""" + for column_index, column in enumerate(column_major_wells): + if target_well in column: + return column.index(target_well), column_index + raise InvalidStoredData(f"Well name {target_well} is not present in labware") + + +def wells_covered_dense( # noqa: C901 + nozzle_map: NozzleMap, target_well: str, target_wells_by_column: list[list[str]] +) -> Iterator[str]: + """Get the list of wells covered by a nozzle map on an SBS format labware with a specified multiplier of 96 into the number of wells. + + This will handle the offsetting of the nozzle map into higher-density well plates. For instance, a full column config target at A1 of a + 96 plate would cover wells A1, B1, C1, D1, E1, F1, G1, H1, and use downsample_factor 1.0 (96*1 = 96). A full column config target on a + 384 plate would cover wells A1, C1, E1, G1, I1, K1, M1, O1 and use downsample_factor 4.0 (96*4 = 384), while a full column config + targeting B1 would cover wells B1, D1, F1, H1, J1, L1, N1, P1 - still using downsample_factor 4.0, with the offset gathered from the + target well. + + The function may also handle sub-96 regular labware with fractional downsample factors, but that's physically improbable and it's not + tested. If you have a regular labware with fewer than 96 wells that is still regularly-spaced and has little enough space between well + walls that it's reasonable to use with multiple channels, you probably want wells_covered_trough. + """ + target_row_index, target_column_index = row_col_ordinals_from_column_major_map( + target_well, target_wells_by_column + ) + column_downsample = len(target_wells_by_column) // 12 + row_downsample = len(target_wells_by_column[0]) // 8 + if column_downsample < 1 or row_downsample < 1: + raise InvalidStoredData( + "This labware cannot be used wells_covered_dense because it is less dense than an SBS 96 standard" + ) + + for nozzle_column in range(len(nozzle_map.columns)): + target_column_offset = nozzle_column * column_downsample + for nozzle_row in range(len(nozzle_map.rows)): + target_row_offset = nozzle_row * row_downsample + if nozzle_map.starting_nozzle == "A1": + if ( + target_column_index + target_column_offset + < len(target_wells_by_column) + ) and ( + target_row_index + target_row_offset + < len(target_wells_by_column[target_column_index]) + ): + yield target_wells_by_column[ + target_column_index + target_column_offset + ][target_row_index + target_row_offset] + elif nozzle_map.starting_nozzle == "A12": + if (target_column_index - target_column_offset >= 0) and ( + target_row_index + target_row_offset + < len(target_wells_by_column[target_column_index]) + ): + yield target_wells_by_column[ + target_column_index - target_column_offset + ][target_row_index + target_row_offset] + elif nozzle_map.starting_nozzle == "H1": + if ( + target_column_index + target_column_offset + < len(target_wells_by_column) + ) and (target_row_index - target_row_offset >= 0): + yield target_wells_by_column[ + target_column_index + target_column_offset + ][target_row_index - target_row_offset] + elif nozzle_map.starting_nozzle == "H12": + if (target_column_index - target_column_offset >= 0) and ( + target_row_index - target_row_offset >= 0 + ): + yield target_wells_by_column[ + target_column_index - target_column_offset + ][target_row_index - target_row_offset] + else: + raise InvalidProtocolData( + f"A pipette nozzle configuration may not having a starting nozzle of {nozzle_map.starting_nozzle}" + ) + + +def wells_covered_sparse( # noqa: C901 + nozzle_map: NozzleMap, target_well: str, target_wells_by_column: list[list[str]] +) -> Iterator[str]: + """Get the list of wells covered by a nozzle map on a column-oriented reservoir. + + This function handles reservoirs whose wells span multiple rows and columns - the most common case is something like a + 12-well reservoir, whose wells are the height of an SBS column and the width of an SBS row, or a 1-well reservoir whose well + is the size of an SBS active area. + """ + target_row_index, target_column_index = row_col_ordinals_from_column_major_map( + target_well, target_wells_by_column + ) + column_upsample = 12 // len(target_wells_by_column) + row_upsample = 8 // len(target_wells_by_column[0]) + if column_upsample < 1 or row_upsample < 1: + raise InvalidStoredData( + "This labware cannot be used with wells_covered_sparse because it is more dense than an SBS 96 standard." + ) + for nozzle_column in range(max(1, len(nozzle_map.columns) // column_upsample)): + for nozzle_row in range(max(1, len(nozzle_map.rows) // row_upsample)): + if nozzle_map.starting_nozzle == "A1": + if ( + target_column_index + nozzle_column < len(target_wells_by_column) + ) and ( + target_row_index + nozzle_row + < len(target_wells_by_column[target_column_index]) + ): + yield target_wells_by_column[target_column_index + nozzle_column][ + target_row_index + nozzle_row + ] + elif nozzle_map.starting_nozzle == "A12": + if (target_column_index - nozzle_column >= 0) and ( + target_row_index + nozzle_row + < len(target_wells_by_column[target_column_index]) + ): + yield target_wells_by_column[target_column_index - nozzle_column][ + target_row_index + nozzle_row + ] + elif nozzle_map.starting_nozzle == "H1": + if ( + target_column_index + nozzle_column + < len(target_wells_by_column[target_column_index]) + ) and (target_row_index - nozzle_row >= 0): + yield target_wells_by_column[target_column_index + nozzle_column][ + target_row_index - nozzle_row + ] + elif nozzle_map.starting_nozzle == "H12": + if (target_column_index - nozzle_column >= 0) and ( + target_row_index - nozzle_row >= 0 + ): + yield target_wells_by_column[target_column_index - nozzle_column][ + target_row_index - nozzle_row + ] + else: + raise InvalidProtocolData( + f"A pipette nozzle configuration may not having a starting nozzle of {nozzle_map.starting_nozzle}" + ) + + +def nozzles_per_well( + nozzle_map: NozzleMap, target_well: str, target_wells_by_column: list[list[str]] +) -> int: + """Get the number of nozzles that will interact with each well in the labware. + + For instance, if this is an SBS 96 or more dense, there is always 1 nozzle per well + that is interacted with (and some wells may not be interacted with at all). If this is + a 12-column reservoir, then all active nozzles in each column of the configuration will + interact with each well; so an 8-channel full config would have 8 nozzles per well, + and a 96 channel with a rectangle config from A1 to D12 would have 4 nozzles per well. + """ + _, target_column_index = row_col_ordinals_from_column_major_map( + target_well, target_wells_by_column + ) + # labware as or more dense than a 96 plate will only ever have 1 nozzle per well (and some wells won't be touched) + if len(target_wells_by_column) >= len(nozzle_map.columns) and len( + target_wells_by_column[target_column_index] + ) >= len(nozzle_map.rows): + return 1 + return max(1, len(nozzle_map.columns) // len(target_wells_by_column)) * max( + 1, len(nozzle_map.rows) // len(target_wells_by_column[target_column_index]) + ) diff --git a/api/src/opentrons/protocol_engine/state/addressable_areas.py b/api/src/opentrons/protocol_engine/state/addressable_areas.py index afd076380f7..16898ccb4ed 100644 --- a/api/src/opentrons/protocol_engine/state/addressable_areas.py +++ b/api/src/opentrons/protocol_engine/state/addressable_areas.py @@ -1,7 +1,7 @@ """Basic addressable area data state and store.""" from dataclasses import dataclass from functools import cached_property -from typing import Dict, List, Optional, Set, Union +from typing import Dict, List, Optional, Set from opentrons_shared_data.robot.types import RobotType, RobotDefinition from opentrons_shared_data.deck.types import ( @@ -12,14 +12,6 @@ from opentrons.types import Point, DeckSlotName -from ..commands import ( - Command, - LoadLabwareResult, - LoadModuleResult, - MoveLabwareResult, - MoveToAddressableAreaResult, - MoveToAddressableAreaForDropTipResult, -) from ..errors import ( IncompatibleAddressableAreaError, AreaNotInDeckConfigurationError, @@ -29,19 +21,18 @@ ) from ..resources import deck_configuration_provider from ..types import ( - DeckSlotLocation, - AddressableAreaLocation, AddressableArea, PotentialCutoutFixture, DeckConfigurationType, Dimensions, ) +from ..actions.get_state_update import get_state_updates from ..actions import ( Action, - SucceedCommandAction, SetDeckConfigurationAction, AddAddressableAreaAction, ) +from . import update_types from .config import Config from ._abstract_store import HasState, HandlesActions @@ -193,10 +184,14 @@ def __init__( def handle_action(self, action: Action) -> None: """Modify state in reaction to an action.""" - if isinstance(action, SucceedCommandAction): - self._handle_command(action.command) - elif isinstance(action, AddAddressableAreaAction): - self._check_location_is_addressable_area(action.addressable_area) + for state_update in get_state_updates(action): + if state_update.addressable_area_used != update_types.NO_CHANGE: + self._add_addressable_area( + state_update.addressable_area_used.addressable_area_name + ) + + if isinstance(action, AddAddressableAreaAction): + self._add_addressable_area(action.addressable_area_name) elif isinstance(action, SetDeckConfigurationAction): current_state = self._state if ( @@ -211,28 +206,6 @@ def handle_action(self, action: Action) -> None: ) ) - def _handle_command(self, command: Command) -> None: - """Modify state in reaction to a command.""" - if isinstance(command.result, LoadLabwareResult): - location = command.params.location - if isinstance(location, (DeckSlotLocation, AddressableAreaLocation)): - self._check_location_is_addressable_area(location) - - elif isinstance(command.result, MoveLabwareResult): - location = command.params.newLocation - if isinstance(location, (DeckSlotLocation, AddressableAreaLocation)): - self._check_location_is_addressable_area(location) - - elif isinstance(command.result, LoadModuleResult): - self._check_location_is_addressable_area(command.params.location) - - elif isinstance( - command.result, - (MoveToAddressableAreaResult, MoveToAddressableAreaForDropTipResult), - ): - addressable_area_name = command.params.addressableAreaName - self._check_location_is_addressable_area(addressable_area_name) - @staticmethod def _get_addressable_areas_from_deck_configuration( deck_config: DeckConfigurationType, deck_definition: DeckDefinitionV5 @@ -260,16 +233,7 @@ def _get_addressable_areas_from_deck_configuration( ) return {area.area_name: area for area in addressable_areas} - def _check_location_is_addressable_area( - self, location: Union[DeckSlotLocation, AddressableAreaLocation, str] - ) -> None: - if isinstance(location, DeckSlotLocation): - addressable_area_name = location.slotName.id - elif isinstance(location, AddressableAreaLocation): - addressable_area_name = location.addressableAreaName - else: - addressable_area_name = location - + def _add_addressable_area(self, addressable_area_name: str) -> None: if addressable_area_name not in self._state.loaded_addressable_areas_by_name: cutout_id = self._validate_addressable_area_for_simulation( addressable_area_name @@ -323,7 +287,7 @@ def _validate_addressable_area_for_simulation( return cutout_id -class AddressableAreaView(HasState[AddressableAreaState]): +class AddressableAreaView: """Read-only addressable area state view.""" _state: AddressableAreaState @@ -345,8 +309,8 @@ def deck_extents(self) -> Point: @cached_property def mount_offsets(self) -> Dict[str, Point]: """The left and right mount offsets of the robot.""" - left_offset = self.state.robot_definition["mountOffsets"]["left"] - right_offset = self.state.robot_definition["mountOffsets"]["right"] + left_offset = self._state.robot_definition["mountOffsets"]["left"] + right_offset = self._state.robot_definition["mountOffsets"]["right"] return { "left": Point(x=left_offset[0], y=left_offset[1], z=left_offset[2]), "right": Point(x=right_offset[0], y=right_offset[1], z=right_offset[2]), @@ -355,10 +319,10 @@ def mount_offsets(self) -> Dict[str, Point]: @cached_property def padding_offsets(self) -> Dict[str, float]: """The padding offsets to be applied to the deck extents of the robot.""" - rear_offset = self.state.robot_definition["paddingOffsets"]["rear"] - front_offset = self.state.robot_definition["paddingOffsets"]["front"] - left_side_offset = self.state.robot_definition["paddingOffsets"]["leftSide"] - right_side_offset = self.state.robot_definition["paddingOffsets"]["rightSide"] + rear_offset = self._state.robot_definition["paddingOffsets"]["rear"] + front_offset = self._state.robot_definition["paddingOffsets"]["front"] + left_side_offset = self._state.robot_definition["paddingOffsets"]["leftSide"] + right_side_offset = self._state.robot_definition["paddingOffsets"]["rightSide"] return { "rear": rear_offset, "front": front_offset, @@ -420,12 +384,12 @@ def _check_if_area_is_compatible_with_potential_fixtures( _get_conflicting_addressable_areas_error_string( self._state.potential_cutout_fixtures_by_cutout_id[cutout_id], self._state.loaded_addressable_areas_by_name, - self.state.deck_definition, + self._state.deck_definition, ) ) area_display_name = ( deck_configuration_provider.get_addressable_area_display_name( - area_name, self.state.deck_definition + area_name, self._state.deck_definition ) ) raise IncompatibleAddressableAreaError( @@ -504,7 +468,7 @@ def get_addressable_area_offsets_from_cutout( addressable_area_name: str, ) -> Point: """Get the offset form cutout fixture of an addressable area.""" - for addressable_area in self.state.deck_definition["locations"][ + for addressable_area in self._state.deck_definition["locations"][ "addressableAreas" ]: if addressable_area["id"] == addressable_area_name: @@ -568,7 +532,7 @@ def get_fixture_by_deck_slot_name( self, slot_name: DeckSlotName ) -> Optional[CutoutFixture]: """Get the Cutout Fixture currently loaded where a specific Deck Slot would be.""" - deck_config = self.state.deck_configuration + deck_config = self._state.deck_configuration if deck_config: slot_cutout_id = DECK_SLOT_TO_CUTOUT_MAP[slot_name] slot_cutout_fixture = None @@ -581,7 +545,7 @@ def get_fixture_by_deck_slot_name( if cutout_id == slot_cutout_id: slot_cutout_fixture = ( deck_configuration_provider.get_cutout_fixture( - cutout_fixture_id, self.state.deck_definition + cutout_fixture_id, self._state.deck_definition ) ) return slot_cutout_fixture @@ -605,7 +569,7 @@ 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 + 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 diff --git a/api/src/opentrons/protocol_engine/state/command_history.py b/api/src/opentrons/protocol_engine/state/command_history.py index d555764e54e..0879a7cd130 100644 --- a/api/src/opentrons/protocol_engine/state/command_history.py +++ b/api/src/opentrons/protocol_engine/state/command_history.py @@ -24,6 +24,9 @@ class CommandHistory: _all_command_ids: List[str] """All command IDs, in insertion order.""" + _all_failed_command_ids: List[str] + """All failed command IDs, in insertion order.""" + _all_command_ids_but_fixit_command_ids: List[str] """All command IDs besides fixit command intents, in insertion order.""" @@ -47,6 +50,7 @@ class CommandHistory: def __init__(self) -> None: self._all_command_ids = [] + self._all_failed_command_ids = [] self._all_command_ids_but_fixit_command_ids = [] self._queued_command_ids = OrderedSet() self._queued_setup_command_ids = OrderedSet() @@ -101,6 +105,13 @@ def get_all_commands(self) -> List[Command]: for command_id in self._all_command_ids ] + def get_all_failed_commands(self) -> List[Command]: + """Get all failed commands.""" + return [ + self._commands_by_id[command_id].command + for command_id in self._all_failed_command_ids + ] + def get_filtered_command_ids(self, include_fixit_commands: bool) -> List[str]: """Get all fixit command IDs.""" if include_fixit_commands: @@ -242,6 +253,7 @@ def set_command_failed(self, command: Command) -> None: self._remove_queue_id(command.id) self._remove_setup_queue_id(command.id) self._set_most_recently_completed_command_id(command.id) + self._all_failed_command_ids.append(command.id) def _add(self, command_id: str, command_entry: CommandEntry) -> None: """Create or update a command entry.""" diff --git a/api/src/opentrons/protocol_engine/state/commands.py b/api/src/opentrons/protocol_engine/state/commands.py index 4d2009aae80..dd8ec108687 100644 --- a/api/src/opentrons/protocol_engine/state/commands.py +++ b/api/src/opentrons/protocol_engine/state/commands.py @@ -1,4 +1,5 @@ """Protocol engine commands sub-state.""" + from __future__ import annotations import enum @@ -228,9 +229,6 @@ class CommandState: This value can be used to generate future hashes. """ - failed_command_errors: List[ErrorOccurrence] - """List of command errors that occurred during run execution.""" - has_entered_error_recovery: bool """Whether the run has entered error recovery.""" @@ -269,7 +267,6 @@ def __init__( run_started_at=None, latest_protocol_command_hash=None, stopped_by_estop=False, - failed_command_errors=[], error_recovery_policy=error_recovery_policy, has_entered_error_recovery=False, ) @@ -308,7 +305,7 @@ def _handle_queue_command_action(self, action: QueueCommandAction) -> None: # TODO(mc, 2021-06-22): mypy has trouble with this automatic # request > command mapping, figure out how to type precisely # (or wait for a future mypy version that can figure it out). - queued_command = action.request._CommandCls.construct( + queued_command = action.request._CommandCls.model_construct( id=action.command_id, key=( action.request.key @@ -330,7 +327,7 @@ def _handle_queue_command_action(self, action: QueueCommandAction) -> None: def _handle_run_command_action(self, action: RunCommandAction) -> None: prev_entry = self._state.command_history.get(action.command_id) - running_command = prev_entry.command.copy( + running_command = prev_entry.command.model_copy( update={ "status": CommandStatus.RUNNING, "startedAt": action.started_at, @@ -366,7 +363,6 @@ def _handle_fail_command_action(self, action: FailCommandAction) -> None: notes=action.notes, ) self._state.failed_command = self._state.command_history.get(action.command_id) - self._state.failed_command_errors.append(public_error_occurrence) if ( prev_entry.command.intent in (CommandIntent.PROTOCOL, None) @@ -511,7 +507,10 @@ def _handle_door_change_action(self, action: DoorChangeAction) -> None: pass case QueueStatus.RUNNING | QueueStatus.PAUSED: self._state.queue_status = QueueStatus.PAUSED - case QueueStatus.AWAITING_RECOVERY | QueueStatus.AWAITING_RECOVERY_PAUSED: + case ( + QueueStatus.AWAITING_RECOVERY + | QueueStatus.AWAITING_RECOVERY_PAUSED + ): self._state.queue_status = QueueStatus.AWAITING_RECOVERY_PAUSED elif action.door_state == DoorState.CLOSED: self._state.is_door_blocking = False @@ -530,7 +529,7 @@ def _update_to_failed( notes: Optional[List[CommandNote]], ) -> None: prev_entry = self._state.command_history.get(command_id) - failed_command = prev_entry.command.copy( + failed_command = prev_entry.command.model_copy( update={ "completedAt": failed_at, "status": CommandStatus.FAILED, @@ -584,7 +583,7 @@ def _map_finish_exception_to_error_occurrence( ) -class CommandView(HasState[CommandState]): +class CommandView: """Read-only command state view.""" _state: CommandState @@ -684,7 +683,7 @@ def get_error(self) -> Optional[ErrorOccurrence]: finish_error = self._state.finish_error if run_error and finish_error: - combined_error = ErrorOccurrence.construct( + combined_error = ErrorOccurrence( id=finish_error.id, createdAt=finish_error.createdAt, errorType="RunAndFinishFailed", @@ -706,7 +705,12 @@ def get_error(self) -> Optional[ErrorOccurrence]: def get_all_errors(self) -> List[ErrorOccurrence]: """Get the run's full error list, if there was none, returns an empty list.""" - return self._state.failed_command_errors + failed_commands = self._state.command_history.get_all_failed_commands() + return [ + command_error.error + for command_error in failed_commands + if command_error.error is not None + ] def get_has_entered_recovery_mode(self) -> bool: """Get whether the run has entered recovery mode.""" @@ -916,7 +920,7 @@ def raise_fatal_command_error(self) -> None: fatal error of the overall run coming from anywhere in the Python script, including in between commands. """ - failed_command = self.state.failed_command + failed_command = self._state.failed_command if ( failed_command and failed_command.command.error @@ -932,12 +936,16 @@ def get_error_recovery_type(self, command_id: str) -> ErrorRecoveryType: The command ID is assumed to point to a failed command. """ - return self.state.command_error_recovery_types[command_id] + 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 + def get_is_stopped_by_estop(self) -> bool: + """Return whether the engine was stopped specifically by an E-stop.""" + return self._state.stopped_by_estop + def has_been_played(self) -> bool: """Get whether engine has started.""" return self._state.run_started_at is not None diff --git a/api/src/opentrons/protocol_engine/state/files.py b/api/src/opentrons/protocol_engine/state/files.py index 655d038df34..bd54d58a4f8 100644 --- a/api/src/opentrons/protocol_engine/state/files.py +++ b/api/src/opentrons/protocol_engine/state/files.py @@ -2,12 +2,11 @@ from dataclasses import dataclass from typing import List +from opentrons.protocol_engine.actions.get_state_update import get_state_updates +from opentrons.protocol_engine.state import update_types + from ._abstract_store import HasState, HandlesActions -from ..actions import Action, SucceedCommandAction -from ..commands import ( - Command, - absorbance_reader, -) +from ..actions import Action @dataclass @@ -28,16 +27,15 @@ def __init__(self) -> None: def handle_action(self, action: Action) -> None: """Modify state in reaction to an action.""" - if isinstance(action, SucceedCommandAction): - self._handle_command(action.command) + for state_update in get_state_updates(action): + self._handle_state_update(state_update) - def _handle_command(self, command: Command) -> None: - if isinstance(command.result, absorbance_reader.ReadAbsorbanceResult): - if command.result.fileIds is not None: - self._state.file_ids.extend(command.result.fileIds) + def _handle_state_update(self, state_update: update_types.StateUpdate) -> None: + if state_update.files_added != update_types.NO_CHANGE: + self._state.file_ids.extend(state_update.files_added.file_ids) -class FileView(HasState[FileState]): +class FileView: """Read-only engine created file state view.""" _state: FileState diff --git a/api/src/opentrons/protocol_engine/state/fluid_stack.py b/api/src/opentrons/protocol_engine/state/fluid_stack.py new file mode 100644 index 00000000000..95465e531b2 --- /dev/null +++ b/api/src/opentrons/protocol_engine/state/fluid_stack.py @@ -0,0 +1,138 @@ +"""Implements fluid stack tracking for pipettes. + +Inside a pipette's tip, there can be a mix of kinds of fluids - here, "fluid" means "liquid" (i.e. a protocol-relevant +working liquid that is aspirated or dispensed from wells) or "air" (i.e. because there was an air gap). Since sometimes +you want air gaps in different places - physically-below liquid to prevent dripping, physically-above liquid to provide +extra room to push the plunger - we need to support some notion of at least phsyical ordinal position of air and liquid, +and we do so as a logical stack because that's physically relevant. +""" +from logging import getLogger +from numpy import isclose +from ..types import AspiratedFluid, FluidKind + +_LOG = getLogger(__name__) + + +class FluidStack: + """A FluidStack data structure is a list of AspiratedFluids, with stack-style (last-in-first-out) ordering. + + The front of the list is the physical-top of the liquid stack (logical-bottom of the stack data structure) + and the back of the list is the physical-bottom of the liquid stack (logical-top of the stack data structure). + The state is internal and the interaction surface is the methods. This is a mutating API. + """ + + _FluidStack = list[AspiratedFluid] + + _fluid_stack: _FluidStack + + def __init__(self, _fluid_stack: _FluidStack | None = None) -> None: + """Build a FluidStack. + + The argument is provided for testing and shouldn't be generally used. + """ + self._fluid_stack = _fluid_stack or [] + + def add_fluid(self, new: AspiratedFluid) -> None: + """Add fluid to a stack. + + If the new fluid is of a different kind than what's on the physical-bottom of the stack, add a new record. + If the new fluid is of the same kind as what's on the physical-bottom of the stack, add the new volume to + the same record. + """ + if len(self._fluid_stack) == 0 or self._fluid_stack[-1].kind != new.kind: + # this is a new kind of fluid, append the record + self._fluid_stack.append(new) + else: + # this is more of the same kind of fluid, add the volumes + old_fluid = self._fluid_stack.pop(-1) + self._fluid_stack.append( + AspiratedFluid(kind=new.kind, volume=old_fluid.volume + new.volume) + ) + + def _alter_fluid_records( + self, remove: int, new_last: AspiratedFluid | None + ) -> None: + if remove >= len(self._fluid_stack) or len(self._fluid_stack) == 0: + self._fluid_stack = [] + return + if remove != 0: + removed = self._fluid_stack[:-remove] + else: + removed = self._fluid_stack + if new_last: + removed[-1] = new_last + self._fluid_stack = removed + + def remove_fluid(self, volume: float) -> None: + """Remove a specific amount of fluid from the physical-bottom of the stack. + + This will consume records that are wholly included in the provided volume and alter the remaining + final records (if any) to decrement the amount of volume removed from it. + + This function is designed to be used inside pipette store action handlers, which are generally not + exception-safe, and therefore swallows and logs errors. + """ + self._fluid_stack_iterator = reversed(self._fluid_stack) + removed_elements: list[AspiratedFluid] = [] + while volume > 0: + try: + last_stack_element = next(self._fluid_stack_iterator) + except StopIteration: + _LOG.error( + f"Attempting to remove more fluid than present, {volume}uL left over" + ) + self._alter_fluid_records(len(removed_elements), None) + return + if last_stack_element.volume < volume: + removed_elements.append(last_stack_element) + volume -= last_stack_element.volume + elif isclose(last_stack_element.volume, volume): + self._alter_fluid_records(len(removed_elements) + 1, None) + return + else: + self._alter_fluid_records( + len(removed_elements), + AspiratedFluid( + kind=last_stack_element.kind, + volume=last_stack_element.volume - volume, + ), + ) + return + + _LOG.error(f"Failed to handle removing {volume}uL from {self._fluid_stack}") + + def aspirated_volume(self, kind: FluidKind | None = None) -> float: + """Measure the total amount of fluid (optionally filtered by kind) in the stack.""" + volume = 0.0 + for el in self._fluid_stack: + if kind is not None and el.kind != kind: + continue + volume += el.volume + return volume + + def liquid_part_of_dispense_volume(self, volume: float) -> float: + """Get the amount of liquid in the specified volume starting at the physical-bottom of the stack.""" + liquid_volume = 0.0 + for el in reversed(self._fluid_stack): + if el.kind == FluidKind.LIQUID: + liquid_volume += min(volume, el.volume) + volume -= min(el.volume, volume) + if isclose(volume, 0.0): + return liquid_volume + return liquid_volume + + def __eq__(self, other: object) -> bool: + """Equality.""" + if isinstance(other, type(self)): + return other._fluid_stack == self._fluid_stack + return False + + def __repr__(self) -> str: + """String representation of a fluid stack.""" + if self._fluid_stack: + stringified_stack = ( + f'(top) {", ".join([str(item) for item in self._fluid_stack])} (bottom)' + ) + else: + stringified_stack = "empty" + return f"<{self.__class__.__name__}: {stringified_stack}>" diff --git a/api/src/opentrons/protocol_engine/state/frustum_helpers.py b/api/src/opentrons/protocol_engine/state/frustum_helpers.py index dfdb0eec56f..b28fb936be7 100644 --- a/api/src/opentrons/protocol_engine/state/frustum_helpers.py +++ b/api/src/opentrons/protocol_engine/state/frustum_helpers.py @@ -82,19 +82,12 @@ def _circular_frustum_polynomial_roots( def _volume_from_height_circular( - target_height: float, - total_frustum_height: float, - bottom_radius: float, - top_radius: float, + target_height: float, segment: ConicalFrustum ) -> float: """Find the volume given a height within a circular frustum.""" - a, b, c = _circular_frustum_polynomial_roots( - bottom_radius=bottom_radius, - top_radius=top_radius, - total_frustum_height=total_frustum_height, - ) - volume = a * (target_height**3) + b * (target_height**2) + c * target_height - return volume + heights = segment.height_to_volume_table.keys() + best_fit_height = min(heights, key=lambda x: abs(x - target_height)) + return segment.height_to_volume_table[best_fit_height] def _volume_from_height_rectangular( @@ -138,26 +131,12 @@ def _volume_from_height_squared_cone( def _height_from_volume_circular( - volume: float, - total_frustum_height: float, - bottom_radius: float, - top_radius: float, + target_volume: float, segment: ConicalFrustum ) -> float: - """Find the height given a volume within a circular frustum.""" - a, b, c = _circular_frustum_polynomial_roots( - bottom_radius=bottom_radius, - top_radius=top_radius, - total_frustum_height=total_frustum_height, - ) - d = volume * -1 - x_intercept_roots = (a, b, c, d) - - height_from_volume_roots = roots(x_intercept_roots) - height = _reject_unacceptable_heights( - potential_heights=list(height_from_volume_roots), - max_height=total_frustum_height, - ) - return height + """Find the height given a volume within a squared cone segment.""" + volumes = segment.volume_to_height_table.keys() + best_fit_volume = min(volumes, key=lambda x: abs(x - target_volume)) + return segment.volume_to_height_table[best_fit_volume] def _height_from_volume_rectangular( @@ -220,28 +199,38 @@ def _get_segment_capacity(segment: WellSegment) -> float: section_height = segment.topHeight - segment.bottomHeight match segment: case SphericalSegment(): - return _volume_from_height_spherical( - target_height=segment.topHeight, - radius_of_curvature=segment.radiusOfCurvature, + return ( + _volume_from_height_spherical( + target_height=segment.topHeight, + radius_of_curvature=segment.radiusOfCurvature, + ) + * segment.count ) case CuboidalFrustum(): - return _volume_from_height_rectangular( - target_height=section_height, - bottom_length=segment.bottomYDimension, - bottom_width=segment.bottomXDimension, - top_length=segment.topYDimension, - top_width=segment.topXDimension, - total_frustum_height=section_height, + return ( + _volume_from_height_rectangular( + target_height=section_height, + bottom_length=segment.bottomYDimension, + bottom_width=segment.bottomXDimension, + top_length=segment.topYDimension, + top_width=segment.topXDimension, + total_frustum_height=section_height, + ) + * segment.count ) case ConicalFrustum(): - return _volume_from_height_circular( - target_height=section_height, - total_frustum_height=section_height, - bottom_radius=(segment.bottomDiameter / 2), - top_radius=(segment.topDiameter / 2), + return ( + _volume_from_height_circular( + target_height=section_height, + segment=segment, + ) + * segment.count ) case SquaredConeSegment(): - return _volume_from_height_squared_cone(section_height, segment) + return ( + _volume_from_height_squared_cone(section_height, segment) + * segment.count + ) case _: # TODO: implement volume calculations for truncated circular and rounded rectangular segments raise NotImplementedError( @@ -272,6 +261,7 @@ def height_at_volume_within_section( section_height: float, ) -> float: """Calculate a height within a bounded section according to geometry.""" + target_volume_relative = target_volume_relative / section.count match section: case SphericalSegment(): return _height_from_volume_spherical( @@ -280,12 +270,7 @@ def height_at_volume_within_section( radius_of_curvature=section.radiusOfCurvature, ) case ConicalFrustum(): - return _height_from_volume_circular( - volume=target_volume_relative, - top_radius=(section.bottomDiameter / 2), - bottom_radius=(section.topDiameter / 2), - total_frustum_height=section_height, - ) + return _height_from_volume_circular(target_volume_relative, section) case CuboidalFrustum(): return _height_from_volume_rectangular( volume=target_volume_relative, @@ -311,28 +296,37 @@ def volume_at_height_within_section( """Calculate a volume within a bounded section according to geometry.""" match section: case SphericalSegment(): - return _volume_from_height_spherical( - target_height=target_height_relative, - radius_of_curvature=section.radiusOfCurvature, + return ( + _volume_from_height_spherical( + target_height=target_height_relative, + radius_of_curvature=section.radiusOfCurvature, + ) + * section.count ) case ConicalFrustum(): - return _volume_from_height_circular( - target_height=target_height_relative, - total_frustum_height=section_height, - bottom_radius=(section.bottomDiameter / 2), - top_radius=(section.topDiameter / 2), + return ( + _volume_from_height_circular( + target_height=target_height_relative, segment=section + ) + * section.count ) case CuboidalFrustum(): - return _volume_from_height_rectangular( - target_height=target_height_relative, - total_frustum_height=section_height, - bottom_width=section.bottomXDimension, - bottom_length=section.bottomYDimension, - top_width=section.topXDimension, - top_length=section.topYDimension, + return ( + _volume_from_height_rectangular( + target_height=target_height_relative, + total_frustum_height=section_height, + bottom_width=section.bottomXDimension, + bottom_length=section.bottomYDimension, + top_width=section.topXDimension, + top_length=section.topYDimension, + ) + * section.count ) case SquaredConeSegment(): - return _volume_from_height_squared_cone(target_height_relative, section) + return ( + _volume_from_height_squared_cone(target_height_relative, section) + * section.count + ) case _: # TODO(cm): this would be the NEST-96 2uL wells referenced in EXEC-712 # we need to input the math attached to that issue @@ -402,7 +396,7 @@ def _find_height_in_partial_frustum( if ( bottom_section_volume < target_volume - < (bottom_section_volume + section_volume) + <= (bottom_section_volume + section_volume) ): relative_target_volume = target_volume - bottom_section_volume section_height = section.topHeight - section.bottomHeight diff --git a/api/src/opentrons/protocol_engine/state/geometry.py b/api/src/opentrons/protocol_engine/state/geometry.py index 8120dc7a0ad..e0d9cb1afa1 100644 --- a/api/src/opentrons/protocol_engine/state/geometry.py +++ b/api/src/opentrons/protocol_engine/state/geometry.py @@ -1,4 +1,5 @@ """Geometry state getters.""" + import enum from numpy import array, dot, double as npdouble from numpy.typing import NDArray @@ -8,6 +9,7 @@ from opentrons.types import Point, DeckSlotName, StagingSlotName, MountType +from opentrons_shared_data.errors.exceptions import InvalidStoredData from opentrons_shared_data.labware.constants import WELL_NAME_PATTERN from opentrons_shared_data.deck.types import CutoutFixture from opentrons_shared_data.pipette import PIPETTE_X_SPAN @@ -61,6 +63,7 @@ find_volume_at_well_height, find_height_at_well_volume, ) +from ._well_math import wells_covered_by_pipette_configuration, nozzles_per_well SLOT_WIDTH = 128 @@ -486,7 +489,7 @@ def get_well_position( well_depth=well_depth, operation_volume=operation_volume, ) - offset = offset.copy(update={"z": offset.z + offset_adjustment}) + offset = offset.model_copy(update={"z": offset.z + offset_adjustment}) self.validate_well_position( well_location=well_location, z_offset=offset.z, pipette_id=pipette_id ) @@ -1559,3 +1562,46 @@ def validate_dispense_volume_into_well( raise errors.InvalidDispenseVolumeError( f"Attempting to dispense {volume}µL of liquid into a well that can only hold {well_volumetric_capacity}µL (well {well_name} in labware_id: {labware_id})" ) + + def get_wells_covered_by_pipette_with_active_well( + self, labware_id: str, target_well_name: str, pipette_id: str + ) -> list[str]: + """Get a flat list of wells that are covered by a pipette when moved to a specified well. + + When you move a pipette in a multichannel configuration to a specific well - the target well - + the pipette will operate on other wells as well. + + For instance, a pipette with a COLUMN configuration with well A1 of an SBS standard labware target + will also "cover", under this definition, wells B1-H1. That same pipette, when C5 is the target well, will "cover" + wells C5-H5. + + This math only works, and may only be applied, if one of the following is true: + - The pipette is in a SINGLE configuration + - The pipette is in a non-SINGLE configuration, and the labware is an SBS-format 96 or 384 well plate (and is so + marked in its definition's parameters.format key, as 96Standard or 384Standard) + + If all of the following do not apply, regardless of the nozzle configuration of the pipette this function will + return only the labware covered by the primary well. + """ + pipette_nozzle_map = self._pipettes.get_nozzle_configuration(pipette_id) + labware_columns = [ + column for column in self._labware.get_definition(labware_id).ordering + ] + try: + return list( + wells_covered_by_pipette_configuration( + pipette_nozzle_map, target_well_name, labware_columns + ) + ) + except InvalidStoredData: + return [target_well_name] + + def get_nozzles_per_well( + self, labware_id: str, target_well_name: str, pipette_id: str + ) -> int: + """Get the number of nozzles that will interact with each well.""" + return nozzles_per_well( + self._pipettes.get_nozzle_configuration(pipette_id), + target_well_name, + self._labware.get_definition(labware_id).ordering, + ) diff --git a/api/src/opentrons/protocol_engine/state/labware.py b/api/src/opentrons/protocol_engine/state/labware.py index 419e9974d5c..60ae8344930 100644 --- a/api/src/opentrons/protocol_engine/state/labware.py +++ b/api/src/opentrons/protocol_engine/state/labware.py @@ -131,7 +131,7 @@ def __init__( for fixed_labware in deck_fixed_labware } labware_by_id = { - fixed_labware.labware_id: LoadedLabware.construct( + fixed_labware.labware_id: LoadedLabware.model_construct( id=fixed_labware.labware_id, location=fixed_labware.location, loadName=fixed_labware.definition.parameters.loadName, @@ -156,10 +156,12 @@ def handle_action(self, action: Action) -> None: """Modify state in reaction to an action.""" for state_update in get_state_updates(action): self._add_loaded_labware(state_update) + self._add_loaded_lid_stack(state_update) self._set_labware_location(state_update) + self._set_labware_lid(state_update) if isinstance(action, AddLabwareOffsetAction): - labware_offset = LabwareOffset.construct( + labware_offset = LabwareOffset.model_construct( id=action.labware_offset_id, createdAt=action.created_at, definitionUri=action.request.definitionUri, @@ -212,7 +214,7 @@ def _add_loaded_labware(self, state_update: update_types.StateUpdate) -> None: self._state.labware_by_id[ loaded_labware_update.labware_id - ] = LoadedLabware.construct( + ] = LoadedLabware.model_construct( id=loaded_labware_update.labware_id, location=location, loadName=loaded_labware_update.definition.parameters.loadName, @@ -221,6 +223,63 @@ def _add_loaded_labware(self, state_update: update_types.StateUpdate) -> None: displayName=display_name, ) + def _add_loaded_lid_stack(self, state_update: update_types.StateUpdate) -> None: + loaded_lid_stack_update = state_update.loaded_lid_stack + if loaded_lid_stack_update != update_types.NO_CHANGE: + # Add the stack object + stack_definition_uri = uri_from_details( + namespace=loaded_lid_stack_update.stack_object_definition.namespace, + load_name=loaded_lid_stack_update.stack_object_definition.parameters.loadName, + version=loaded_lid_stack_update.stack_object_definition.version, + ) + self.state.definitions_by_uri[ + stack_definition_uri + ] = loaded_lid_stack_update.stack_object_definition + self._state.labware_by_id[ + loaded_lid_stack_update.stack_id + ] = LoadedLabware.construct( + id=loaded_lid_stack_update.stack_id, + location=loaded_lid_stack_update.stack_location, + loadName=loaded_lid_stack_update.stack_object_definition.parameters.loadName, + definitionUri=stack_definition_uri, + offsetId=None, + displayName=None, + ) + + # Add the Lids on top of the stack object + for i in range(len(loaded_lid_stack_update.labware_ids)): + definition_uri = uri_from_details( + namespace=loaded_lid_stack_update.definition.namespace, + load_name=loaded_lid_stack_update.definition.parameters.loadName, + version=loaded_lid_stack_update.definition.version, + ) + + self._state.definitions_by_uri[ + definition_uri + ] = loaded_lid_stack_update.definition + + location = loaded_lid_stack_update.new_locations_by_id[ + loaded_lid_stack_update.labware_ids[i] + ] + + self._state.labware_by_id[ + loaded_lid_stack_update.labware_ids[i] + ] = LoadedLabware.construct( + id=loaded_lid_stack_update.labware_ids[i], + location=location, + loadName=loaded_lid_stack_update.definition.parameters.loadName, + definitionUri=definition_uri, + offsetId=None, + displayName=None, + ) + + def _set_labware_lid(self, state_update: update_types.StateUpdate) -> None: + labware_lid_update = state_update.labware_lid + if labware_lid_update != update_types.NO_CHANGE: + parent_labware_id = labware_lid_update.parent_labware_id + lid_id = labware_lid_update.lid_id + self._state.labware_by_id[parent_labware_id].lid_id = lid_id + def _set_labware_location(self, state_update: update_types.StateUpdate) -> None: labware_location_update = state_update.labware_location if labware_location_update != update_types.NO_CHANGE: @@ -244,7 +303,7 @@ def _set_labware_location(self, state_update: update_types.StateUpdate) -> None: self._state.labware_by_id[labware_id].location = new_location -class LabwareView(HasState[LabwareState]): +class LabwareView: """Read-only labware state view.""" _state: LabwareState @@ -268,7 +327,7 @@ def get(self, labware_id: str) -> LoadedLabware: def get_id_by_module(self, module_id: str) -> str: """Return the ID of the labware loaded on the given module.""" - for labware_id, labware in self.state.labware_by_id.items(): + for labware_id, labware in self._state.labware_by_id.items(): if ( isinstance(labware.location, ModuleLocation) and labware.location.moduleId == module_id @@ -281,7 +340,7 @@ def get_id_by_module(self, module_id: str) -> str: def get_id_by_labware(self, labware_id: str) -> str: """Return the ID of the labware loaded on the given labware.""" - for labware in self.state.labware_by_id.values(): + for labware in self._state.labware_by_id.values(): if ( isinstance(labware.location, OnLabwareLocation) and labware.location.labwareId == labware_id @@ -441,21 +500,7 @@ def get_labware_stacking_maximum(self, labware: LabwareDefinition) -> int: If not defined within a labware, defaults to one. """ - stacking_quirks = { - "stackingMaxFive": 5, - "stackingMaxFour": 4, - "stackingMaxThree": 3, - "stackingMaxTwo": 2, - "stackingMaxOne": 1, - "stackingMaxZero": 0, - } - for quirk in stacking_quirks.keys(): - if ( - labware.parameters.quirks is not None - and quirk in labware.parameters.quirks - ): - return stacking_quirks[quirk] - return 1 + return labware.stackLimit if labware.stackLimit is not None else 1 def get_should_center_pipette_on_target_well(self, labware_id: str) -> bool: """True if a pipette moving to a well of this labware should center its body on the target. @@ -479,7 +524,6 @@ def get_well_definition( will be used. """ definition = self.get_definition(labware_id) - if well_name is None: well_name = definition.ordering[0][0] @@ -815,6 +859,11 @@ def raise_if_labware_inaccessible_by_pipette(self, labware_id: str) -> None: return self.raise_if_labware_inaccessible_by_pipette( labware_location.labwareId ) + elif labware.lid_id is not None: + raise errors.LocationNotAccessibleByPipetteError( + f"Cannot move pipette to {labware.loadName} " + "because labware is currently covered by a lid." + ) elif isinstance(labware_location, AddressableAreaLocation): if fixture_validation.is_staging_slot(labware_location.addressableAreaName): raise errors.LocationNotAccessibleByPipetteError( @@ -837,6 +886,19 @@ def raise_if_labware_in_location( f"Labware {labware.loadName} is already present at {location}." ) + def raise_if_labware_cannot_be_ondeck( + self, + location: LabwareLocation, + labware_definition: LabwareDefinition, + ) -> None: + """Raise an error if the labware cannot be in the specified location.""" + if isinstance( + location, (DeckSlotLocation, AddressableAreaLocation) + ) and not labware_validation.validate_labware_can_be_ondeck(labware_definition): + raise errors.LabwareCannotSitOnDeckError( + f"{labware_definition.parameters.loadName} cannot sit in a slot by itself." + ) + def raise_if_labware_incompatible_with_plate_reader( self, labware_definition: LabwareDefinition, @@ -998,11 +1060,15 @@ def get_child_gripper_offsets( return None else: return LabwareMovementOffsetData( - pickUpOffset=cast( - LabwareOffsetVector, parsed_offsets[offset_key].pickUpOffset + pickUpOffset=LabwareOffsetVector.model_construct( + x=parsed_offsets[offset_key].pickUpOffset.x, + y=parsed_offsets[offset_key].pickUpOffset.y, + z=parsed_offsets[offset_key].pickUpOffset.z, ), - dropOffset=cast( - LabwareOffsetVector, parsed_offsets[offset_key].dropOffset + dropOffset=LabwareOffsetVector.model_construct( + x=parsed_offsets[offset_key].dropOffset.x, + y=parsed_offsets[offset_key].dropOffset.y, + z=parsed_offsets[offset_key].dropOffset.z, ), ) diff --git a/api/src/opentrons/protocol_engine/state/liquid_classes.py b/api/src/opentrons/protocol_engine/state/liquid_classes.py new file mode 100644 index 00000000000..4010c7be821 --- /dev/null +++ b/api/src/opentrons/protocol_engine/state/liquid_classes.py @@ -0,0 +1,82 @@ +"""A data store of liquid classes.""" + +from __future__ import annotations + +import dataclasses +from typing import Dict +from typing_extensions import Optional + +from .. import errors +from ..actions import Action, get_state_updates +from ..types import LiquidClassRecord +from . import update_types +from ._abstract_store import HasState, HandlesActions + + +@dataclasses.dataclass +class LiquidClassState: + """Our state is a bidirectional mapping between IDs <-> LiquidClassRecords.""" + + # We use the bidirectional map to see if we've already assigned an ID to a liquid class when the + # engine is asked to store a new liquid class. + liquid_class_record_by_id: Dict[str, LiquidClassRecord] + liquid_class_record_to_id: Dict[LiquidClassRecord, str] + + +class LiquidClassStore(HasState[LiquidClassState], HandlesActions): + """Container for LiquidClassState.""" + + _state: LiquidClassState + + def __init__(self) -> None: + self._state = LiquidClassState( + liquid_class_record_by_id={}, + liquid_class_record_to_id={}, + ) + + def handle_action(self, action: Action) -> None: + """Update the state in response to the action.""" + for state_update in get_state_updates(action): + if state_update.liquid_class_loaded != update_types.NO_CHANGE: + self._handle_liquid_class_loaded_update( + state_update.liquid_class_loaded + ) + + def _handle_liquid_class_loaded_update( + self, state_update: update_types.LiquidClassLoadedUpdate + ) -> None: + # We're just a data store. All the validation and ID generation happens in the command implementation. + self._state.liquid_class_record_by_id[ + state_update.liquid_class_id + ] = state_update.liquid_class_record + self._state.liquid_class_record_to_id[ + state_update.liquid_class_record + ] = state_update.liquid_class_id + + +class LiquidClassView: + """Read-only view of the LiquidClassState.""" + + _state: LiquidClassState + + def __init__(self, state: LiquidClassState) -> None: + self._state = state + + def get(self, liquid_class_id: str) -> LiquidClassRecord: + """Get the LiquidClassRecord with the given identifier.""" + try: + return self._state.liquid_class_record_by_id[liquid_class_id] + except KeyError as e: + raise errors.LiquidClassDoesNotExistError( + f"Liquid class ID {liquid_class_id} not found." + ) from e + + def get_id_for_liquid_class_record( + self, liquid_class_record: LiquidClassRecord + ) -> Optional[str]: + """See if the given LiquidClassRecord if already in the store, and if so, return its identifier.""" + return self._state.liquid_class_record_to_id.get(liquid_class_record) + + def get_all(self) -> Dict[str, LiquidClassRecord]: + """Get all the LiquidClassRecords in the store.""" + return self._state.liquid_class_record_by_id.copy() diff --git a/api/src/opentrons/protocol_engine/state/liquids.py b/api/src/opentrons/protocol_engine/state/liquids.py index 9394e4261b1..034e0c4030b 100644 --- a/api/src/opentrons/protocol_engine/state/liquids.py +++ b/api/src/opentrons/protocol_engine/state/liquids.py @@ -1,11 +1,11 @@ """Basic liquid data state and store.""" from dataclasses import dataclass from typing import Dict, List -from opentrons.protocol_engine.types import Liquid +from opentrons.protocol_engine.types import Liquid, LiquidId from ._abstract_store import HasState, HandlesActions from ..actions import Action, AddLiquidAction -from ..errors import LiquidDoesNotExistError +from ..errors import LiquidDoesNotExistError, InvalidLiquidError @dataclass @@ -34,7 +34,7 @@ def _add_liquid(self, action: AddLiquidAction) -> None: self._state.liquids_by_id[action.liquid.id] = action.liquid -class LiquidView(HasState[LiquidState]): +class LiquidView: """Read-only liquid state view.""" _state: LiquidState @@ -51,11 +51,23 @@ def get_all(self) -> List[Liquid]: """Get all protocol liquids.""" return list(self._state.liquids_by_id.values()) - def validate_liquid_id(self, liquid_id: str) -> str: + def validate_liquid_id(self, liquid_id: LiquidId) -> LiquidId: """Check if liquid_id exists in liquids.""" + is_empty = liquid_id == "EMPTY" + if is_empty: + return liquid_id has_liquid = liquid_id in self._state.liquids_by_id if not has_liquid: raise LiquidDoesNotExistError( f"Supplied liquidId: {liquid_id} does not exist in the loaded liquids." ) return liquid_id + + def validate_liquid_allowed(self, liquid: Liquid) -> Liquid: + """Validate that a liquid is legal to load.""" + is_empty = liquid.id == "EMPTY" + if is_empty: + raise InvalidLiquidError( + message='Protocols may not define a liquid with the special id "EMPTY".' + ) + return liquid diff --git a/api/src/opentrons/protocol_engine/state/modules.py b/api/src/opentrons/protocol_engine/state/modules.py index f2f9dc5e8e4..ce75473265e 100644 --- a/api/src/opentrons/protocol_engine/state/modules.py +++ b/api/src/opentrons/protocol_engine/state/modules.py @@ -35,6 +35,7 @@ AbsorbanceReaderMeasureMode, ) from opentrons.types import DeckSlotName, MountType, StagingSlotName +from .update_types import AbsorbanceReaderStateUpdate from ..errors import ModuleNotConnectedError from ..types import ( @@ -63,7 +64,6 @@ heater_shaker, temperature_module, thermocycler, - absorbance_reader, ) from ..actions import ( Action, @@ -296,40 +296,10 @@ def _handle_command(self, command: Command) -> None: ): self._handle_thermocycler_module_commands(command) - if isinstance( - command.result, - ( - absorbance_reader.InitializeResult, - absorbance_reader.ReadAbsorbanceResult, - ), - ): - self._handle_absorbance_reader_commands(command) - def _handle_state_update(self, state_update: update_types.StateUpdate) -> None: - if state_update.absorbance_reader_lid != update_types.NO_CHANGE: - module_id = state_update.absorbance_reader_lid.module_id - is_lid_on = state_update.absorbance_reader_lid.is_lid_on - - # Get current values: - absorbance_reader_substate = self._state.substate_by_module_id[module_id] - assert isinstance( - absorbance_reader_substate, AbsorbanceReaderSubState - ), f"{module_id} is not an absorbance plate reader." - configured = absorbance_reader_substate.configured - measure_mode = absorbance_reader_substate.measure_mode - configured_wavelengths = absorbance_reader_substate.configured_wavelengths - reference_wavelength = absorbance_reader_substate.reference_wavelength - data = absorbance_reader_substate.data - - self._state.substate_by_module_id[module_id] = AbsorbanceReaderSubState( - module_id=AbsorbanceReaderId(module_id), - configured=configured, - measured=True, - is_lid_on=is_lid_on, - measure_mode=measure_mode, - configured_wavelengths=configured_wavelengths, - reference_wavelength=reference_wavelength, - data=data, + if state_update.absorbance_reader_state_update != update_types.NO_CHANGE: + self._handle_absorbance_reader_commands( + state_update.absorbance_reader_state_update ) def _add_module_substate( @@ -589,50 +559,61 @@ def _handle_thermocycler_module_commands( ) def _handle_absorbance_reader_commands( - self, - command: Union[ - absorbance_reader.Initialize, - absorbance_reader.ReadAbsorbance, - ], + self, absorbance_reader_state_update: AbsorbanceReaderStateUpdate ) -> None: - module_id = command.params.moduleId + # Get current values: + module_id = absorbance_reader_state_update.module_id absorbance_reader_substate = self._state.substate_by_module_id[module_id] assert isinstance( absorbance_reader_substate, AbsorbanceReaderSubState ), f"{module_id} is not an absorbance plate reader." - - # Get current values + is_lid_on = absorbance_reader_substate.is_lid_on + measured = True configured = absorbance_reader_substate.configured measure_mode = absorbance_reader_substate.measure_mode configured_wavelengths = absorbance_reader_substate.configured_wavelengths reference_wavelength = absorbance_reader_substate.reference_wavelength - is_lid_on = absorbance_reader_substate.is_lid_on - - if isinstance(command.result, absorbance_reader.InitializeResult): - self._state.substate_by_module_id[module_id] = AbsorbanceReaderSubState( - module_id=AbsorbanceReaderId(module_id), - configured=True, - measured=False, - is_lid_on=is_lid_on, - measure_mode=AbsorbanceReaderMeasureMode(command.params.measureMode), - configured_wavelengths=command.params.sampleWavelengths, - reference_wavelength=command.params.referenceWavelength, - data=None, + data = absorbance_reader_substate.data + if ( + absorbance_reader_state_update.absorbance_reader_lid + != update_types.NO_CHANGE + ): + is_lid_on = absorbance_reader_state_update.absorbance_reader_lid.is_lid_on + elif ( + absorbance_reader_state_update.initialize_absorbance_reader_update + != update_types.NO_CHANGE + ): + configured = True + measured = False + is_lid_on = is_lid_on + measure_mode = AbsorbanceReaderMeasureMode( + absorbance_reader_state_update.initialize_absorbance_reader_update.measure_mode ) - elif isinstance(command.result, absorbance_reader.ReadAbsorbanceResult): - self._state.substate_by_module_id[module_id] = AbsorbanceReaderSubState( - module_id=AbsorbanceReaderId(module_id), - configured=configured, - measured=True, - is_lid_on=is_lid_on, - measure_mode=measure_mode, - configured_wavelengths=configured_wavelengths, - reference_wavelength=reference_wavelength, - data=command.result.data, + configured_wavelengths = ( + absorbance_reader_state_update.initialize_absorbance_reader_update.sample_wave_lengths + ) + reference_wavelength = ( + absorbance_reader_state_update.initialize_absorbance_reader_update.reference_wave_length ) + data = None + elif ( + absorbance_reader_state_update.absorbance_reader_data + != update_types.NO_CHANGE + ): + data = absorbance_reader_state_update.absorbance_reader_data.read_result + self._state.substate_by_module_id[module_id] = AbsorbanceReaderSubState( + module_id=AbsorbanceReaderId(module_id), + configured=configured, + measured=measured, + is_lid_on=is_lid_on, + measure_mode=measure_mode, + configured_wavelengths=configured_wavelengths, + reference_wavelength=reference_wavelength, + data=data, + ) -class ModuleView(HasState[ModuleState]): +class ModuleView: """Read-only view of computed module state.""" _state: ModuleState @@ -654,7 +635,7 @@ def get(self, module_id: str) -> LoadedModule: DeckSlotLocation(slotName=slot_name) if slot_name is not None else None ) - return LoadedModule.construct( + return LoadedModule.model_construct( id=module_id, location=location, model=attached_module.definition.model, @@ -860,8 +841,8 @@ def get_nominal_offset_to_child( Labware Position Check offset. """ if ( - self.state.deck_type == DeckType.OT2_STANDARD - or self.state.deck_type == DeckType.OT2_SHORT_TRASH + 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 @@ -908,7 +889,7 @@ def get_nominal_offset_to_child( "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 + location, module.model ) module_addressable_area_position = ( addressable_areas.get_addressable_area_offsets_from_cutout( @@ -1281,13 +1262,14 @@ def convert_absorbance_reader_data_points( 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. """ + deck_type = self._state.deck_type + 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." diff --git a/api/src/opentrons/protocol_engine/state/motion.py b/api/src/opentrons/protocol_engine/state/motion.py index 0863c42a0c1..855025d01b6 100644 --- a/api/src/opentrons/protocol_engine/state/motion.py +++ b/api/src/opentrons/protocol_engine/state/motion.py @@ -327,6 +327,7 @@ def get_touch_tip_waypoints( labware_id: str, well_name: str, center_point: Point, + mm_from_edge: float = 0, radius: float = 1.0, ) -> List[motion_planning.Waypoint]: """Get a list of touch points for a touch tip operation.""" @@ -346,7 +347,11 @@ def get_touch_tip_waypoints( ) positions = _move_types.get_edge_point_list( - center_point, x_offset, y_offset, edge_path_type + center=center_point, + x_radius=x_offset, + y_radius=y_offset, + mm_from_edge=mm_from_edge, + edge_path_type=edge_path_type, ) critical_point: Optional[CriticalPoint] = None diff --git a/api/src/opentrons/protocol_engine/state/pipettes.py b/api/src/opentrons/protocol_engine/state/pipettes.py index bb90e067ec6..fc7b1da20ac 100644 --- a/api/src/opentrons/protocol_engine/state/pipettes.py +++ b/api/src/opentrons/protocol_engine/state/pipettes.py @@ -1,28 +1,33 @@ """Basic pipette data state and store.""" + from __future__ import annotations import dataclasses +from logging import getLogger from typing import ( Dict, List, Mapping, Optional, Tuple, - Union, + cast, ) +from typing_extensions import assert_never + from opentrons_shared_data.pipette import pipette_definition +from opentrons_shared_data.pipette.ul_per_mm import calculate_ul_per_mm +from opentrons_shared_data.pipette.types import UlPerMmAction + from opentrons.config.defaults_ot2 import Z_RETRACT_DISTANCE from opentrons.hardware_control.dev_types import PipetteDict from opentrons.hardware_control import CriticalPoint from opentrons.hardware_control.nozzle_manager import ( - NozzleConfigurationType, NozzleMap, ) -from opentrons.types import MountType, Mount as HwMount, Point +from opentrons.types import MountType, Mount as HwMount, Point, NozzleConfigurationType -from . import update_types -from .. import commands +from . import update_types, fluid_stack from .. import errors from ..types import ( LoadedPipette, @@ -36,13 +41,13 @@ ) from ..actions import ( Action, - FailCommandAction, SetPipetteMovementSpeedAction, - SucceedCommandAction, get_state_updates, ) from ._abstract_store import HasState, HandlesActions +LOG = getLogger(__name__) + @dataclasses.dataclass(frozen=True) class HardwarePipette: @@ -98,6 +103,9 @@ class StaticPipetteConfig: bounding_nozzle_offsets: BoundingNozzlesOffsets default_nozzle_map: NozzleMap # todo(mm, 2024-10-14): unused, remove? lld_settings: Optional[Dict[str, Dict[str, float]]] + plunger_positions: Dict[str, float] + shaft_ul_per_mm: float + available_sensors: pipette_definition.AvailableSensorDefinition @dataclasses.dataclass @@ -108,7 +116,7 @@ class PipetteState: # attributes are populated at the appropriate times. Refactor to a # single dict-of-many-things instead of many dicts-of-single-things. pipettes_by_id: Dict[str, LoadedPipette] - aspirated_volume_by_id: Dict[str, Optional[float]] + pipette_contents_by_id: Dict[str, Optional[fluid_stack.FluidStack]] current_location: Optional[CurrentPipetteLocation] current_deck_point: CurrentDeckPoint attached_tip_by_id: Dict[str, Optional[TipGeometry]] @@ -128,7 +136,7 @@ def __init__(self) -> None: """Initialize a PipetteStore and its state.""" self._state = PipetteState( pipettes_by_id={}, - aspirated_volume_by_id={}, + pipette_contents_by_id={}, attached_tip_by_id={}, current_location=None, current_deck_point=CurrentDeckPoint(mount=None, deck_point=None), @@ -147,11 +155,9 @@ def handle_action(self, action: Action) -> None: self._update_pipette_config(state_update) self._update_pipette_nozzle_map(state_update) self._update_tip_state(state_update) + self._update_volumes(state_update) - if isinstance(action, (SucceedCommandAction, FailCommandAction)): - self._update_volumes(action) - - elif isinstance(action, SetPipetteMovementSpeedAction): + if isinstance(action, SetPipetteMovementSpeedAction): self._state.movement_speed_by_id[action.pipette_id] = action.speed def _set_load_pipette(self, state_update: update_types.StateUpdate) -> None: @@ -166,7 +172,6 @@ def _set_load_pipette(self, state_update: update_types.StateUpdate) -> None: self._state.liquid_presence_detection_by_id[pipette_id] = ( state_update.loaded_pipette.liquid_presence_detection or False ) - self._state.aspirated_volume_by_id[pipette_id] = None self._state.movement_speed_by_id[pipette_id] = None self._state.attached_tip_by_id[pipette_id] = None @@ -177,7 +182,6 @@ def _update_tip_state(self, state_update: update_types.StateUpdate) -> None: attached_tip = state_update.pipette_tip_state.tip_geometry self._state.attached_tip_by_id[pipette_id] = attached_tip - self._state.aspirated_volume_by_id[pipette_id] = 0 static_config = self._state.static_config_by_id.get(pipette_id) if static_config: @@ -204,7 +208,6 @@ def _update_tip_state(self, state_update: update_types.StateUpdate) -> None: else: pipette_id = state_update.pipette_tip_state.pipette_id - self._state.aspirated_volume_by_id[pipette_id] = None self._state.attached_tip_by_id[pipette_id] = None static_config = self._state.static_config_by_id.get(pipette_id) @@ -292,6 +295,9 @@ def _update_pipette_config(self, state_update: update_types.StateUpdate) -> None ), default_nozzle_map=config.nozzle_map, lld_settings=config.pipette_lld_settings, + plunger_positions=config.plunger_positions, + shaft_ul_per_mm=config.shaft_ul_per_mm, + available_sensors=config.available_sensors, ) self._state.flow_rates_by_id[ state_update.pipette_config.pipette_id @@ -308,54 +314,48 @@ def _update_pipette_nozzle_map( state_update.pipette_nozzle_map.pipette_id ] = state_update.pipette_nozzle_map.nozzle_map - def _update_volumes( - self, action: Union[SucceedCommandAction, FailCommandAction] + def _update_volumes(self, state_update: update_types.StateUpdate) -> None: + if state_update.pipette_aspirated_fluid == update_types.NO_CHANGE: + return + if state_update.pipette_aspirated_fluid.type == "aspirated": + self._update_aspirated(state_update.pipette_aspirated_fluid) + elif state_update.pipette_aspirated_fluid.type == "ejected": + self._update_ejected(state_update.pipette_aspirated_fluid) + elif state_update.pipette_aspirated_fluid.type == "empty": + self._update_empty(state_update.pipette_aspirated_fluid) + elif state_update.pipette_aspirated_fluid.type == "unknown": + self._update_unknown(state_update.pipette_aspirated_fluid) + else: + assert_never(state_update.pipette_aspirated_fluid.type) + + def _update_aspirated( + self, update: update_types.PipetteAspiratedFluidUpdate ) -> None: - # todo(mm, 2024-10-10): Port these isinstance checks to StateUpdate. - # https://opentrons.atlassian.net/browse/EXEC-754 + if self._state.pipette_contents_by_id[update.pipette_id] is None: + self._state.pipette_contents_by_id[ + update.pipette_id + ] = fluid_stack.FluidStack() - if isinstance(action, SucceedCommandAction) and isinstance( - action.command.result, - (commands.AspirateResult, commands.AspirateInPlaceResult), - ): - pipette_id = action.command.params.pipetteId - previous_volume = self._state.aspirated_volume_by_id[pipette_id] or 0 - # PipetteHandler will have clamped action.command.result.volume for us, so - # next_volume should always be in bounds. - next_volume = previous_volume + action.command.result.volume + self._fluid_stack_log_if_empty(update.pipette_id).add_fluid(update.fluid) - self._state.aspirated_volume_by_id[pipette_id] = next_volume + def _update_ejected(self, update: update_types.PipetteEjectedFluidUpdate) -> None: + self._fluid_stack_log_if_empty(update.pipette_id).remove_fluid(update.volume) - elif isinstance(action, SucceedCommandAction) and isinstance( - action.command.result, - (commands.DispenseResult, commands.DispenseInPlaceResult), - ): - pipette_id = action.command.params.pipetteId - previous_volume = self._state.aspirated_volume_by_id[pipette_id] or 0 - # PipetteHandler will have clamped action.command.result.volume for us, so - # next_volume should always be in bounds. - next_volume = previous_volume - action.command.result.volume - self._state.aspirated_volume_by_id[pipette_id] = next_volume - - elif isinstance(action, SucceedCommandAction) and isinstance( - action.command.result, - ( - commands.BlowOutResult, - commands.BlowOutInPlaceResult, - commands.unsafe.UnsafeBlowOutInPlaceResult, - ), - ): - pipette_id = action.command.params.pipetteId - self._state.aspirated_volume_by_id[pipette_id] = None + def _update_empty(self, update: update_types.PipetteEmptyFluidUpdate) -> None: + self._state.pipette_contents_by_id[update.pipette_id] = fluid_stack.FluidStack() - elif isinstance(action, SucceedCommandAction) and isinstance( - action.command.result, commands.PrepareToAspirateResult - ): - pipette_id = action.command.params.pipetteId - self._state.aspirated_volume_by_id[pipette_id] = 0 + def _update_unknown(self, update: update_types.PipetteUnknownFluidUpdate) -> None: + self._state.pipette_contents_by_id[update.pipette_id] = None + def _fluid_stack_log_if_empty(self, pipette_id: str) -> fluid_stack.FluidStack: + stack = self._state.pipette_contents_by_id[pipette_id] + if stack is None: + LOG.error("Pipette state tried to alter an unknown-contents pipette") + return fluid_stack.FluidStack() + return stack -class PipetteView(HasState[PipetteState]): + +class PipetteView: """Read-only view of computed pipettes state.""" _state: PipetteState @@ -457,6 +457,10 @@ def get_all_attached_tips(self) -> List[Tuple[str, TipGeometry]]: def get_aspirated_volume(self, pipette_id: str) -> Optional[float]: """Get the currently aspirated volume of a pipette by ID. + This is the volume currently displaced by the plunger relative to its bottom position, + regardless of whether that volume likely contains liquid or air. This makes it the right + function to call to know how much more volume the plunger may displace. + Returns: The volume the pipette has aspirated. None, after blow-out and the plunger is in an unsafe position. @@ -468,13 +472,50 @@ def get_aspirated_volume(self, pipette_id: str) -> Optional[float]: self.validate_tip_state(pipette_id, True) try: - return self._state.aspirated_volume_by_id[pipette_id] + stack = self._state.pipette_contents_by_id[pipette_id] + if stack is None: + return None + return stack.aspirated_volume() except KeyError as e: raise errors.PipetteNotLoadedError( f"Pipette {pipette_id} not found; unable to get current volume." ) from e + def get_liquid_dispensed_by_ejecting_volume( + self, pipette_id: str, volume: float + ) -> Optional[float]: + """Get the amount of liquid (not air) that will be dispensed if the pipette ejects a specified volume. + + For instance, if the pipette contains, in vertical order, + 10 ul air + 80 ul liquid + 5 ul air + + then dispensing 10ul would result in 5ul of liquid; dispensing 85 ul would result in 80ul liquid; dispensing + 95ul would result in 80ul liquid. + + Returns: + The volume of liquid that would be dispensed by the requested volume. + None, after blow-out or when the plunger is in an unsafe position. + + Raises: + PipetteNotLoadedError: pipette ID does not exist. + TipnotAttachedError: No tip is attached to the pipette. + """ + self.validate_tip_state(pipette_id, True) + + try: + stack = self._state.pipette_contents_by_id[pipette_id] + if stack is None: + return None + return stack.liquid_part_of_dispense_volume(volume) + + except KeyError as e: + raise errors.PipetteNotLoadedError( + f"Pipette {pipette_id} not found; unable to get current liquid volume." + ) from e + def get_working_volume(self, pipette_id: str) -> float: """Get the working maximum volume of a pipette by ID. @@ -641,6 +682,10 @@ def get_primary_nozzle(self, pipette_id: str) -> str: nozzle_map = self._state.nozzle_configuration_by_id[pipette_id] return nozzle_map.starting_nozzle + def get_nozzle_configuration(self, pipette_id: str) -> NozzleMap: + """Get the nozzle map of the pipette.""" + return self._state.nozzle_configuration_by_id[pipette_id] + def _get_critical_point_offset_without_tip( self, pipette_id: str, critical_point: Optional[CriticalPoint] ) -> Point: @@ -723,6 +768,13 @@ def get_pipette_bounds_at_specified_move_to_position( pip_front_left_bound, ) + def get_pipette_supports_pressure(self, pipette_id: str) -> bool: + """Return if this pipette supports a pressure sensor.""" + return ( + "pressure" + in self._state.static_config_by_id[pipette_id].available_sensors.sensors + ) + def get_liquid_presence_detection(self, pipette_id: str) -> bool: """Determine if liquid presence detection is enabled for this pipette.""" try: @@ -731,3 +783,42 @@ def get_liquid_presence_detection(self, pipette_id: str) -> bool: raise errors.PipetteNotLoadedError( f"Pipette {pipette_id} not found; unable to determine if pipette liquid presence detection enabled." ) from e + + def get_nozzle_configuration_supports_lld(self, pipette_id: str) -> bool: + """Determine if the current partial tip configuration supports LLD.""" + nozzle_map = self.get_nozzle_configuration(pipette_id) + if ( + nozzle_map.physical_nozzle_count == 96 + and nozzle_map.back_left != nozzle_map.full_instrument_back_left + and nozzle_map.front_right != nozzle_map.full_instrument_front_right + ): + return False + return True + + def lookup_volume_to_mm_conversion( + self, pipette_id: str, volume: float, action: str + ) -> float: + """Get the volumn to mm conversion for a pipette.""" + try: + lookup_volume = self.get_working_volume(pipette_id) + except errors.TipNotAttachedError: + lookup_volume = self.get_maximum_volume(pipette_id) + + pipette_config = self.get_config(pipette_id) + lookup_table_from_config = pipette_config.tip_configuration_lookup_table + try: + tip_settings = lookup_table_from_config[lookup_volume] + except KeyError: + tip_settings = list(lookup_table_from_config.values())[0] + return calculate_ul_per_mm( + volume, + cast(UlPerMmAction, action), + tip_settings, + shaft_ul_per_mm=pipette_config.shaft_ul_per_mm, + ) + + def lookup_plunger_position_name( + self, pipette_id: str, position_name: str + ) -> float: + """Get the plunger position provided for the given pipette id.""" + return self.get_config(pipette_id).plunger_positions[position_name] diff --git a/api/src/opentrons/protocol_engine/state/state.py b/api/src/opentrons/protocol_engine/state/state.py index 6743e1f44fc..5ff12b739f3 100644 --- a/api/src/opentrons/protocol_engine/state/state.py +++ b/api/src/opentrons/protocol_engine/state/state.py @@ -9,7 +9,7 @@ from opentrons_shared_data.robot.types import RobotDefinition from opentrons.protocol_engine.error_recovery_policy import ErrorRecoveryPolicy -from opentrons.protocol_engine.types import ModuleOffsetData +from opentrons.protocol_engine.types import LiquidClassRecordWithId, ModuleOffsetData from opentrons.util.change_notifier import ChangeNotifier from ..resources import DeckFixedLabware @@ -25,6 +25,7 @@ from .pipettes import PipetteState, PipetteStore, PipetteView from .modules import ModuleState, ModuleStore, ModuleView from .liquids import LiquidState, LiquidView, LiquidStore +from .liquid_classes import LiquidClassState, LiquidClassStore, LiquidClassView from .tips import TipState, TipView, TipStore from .wells import WellState, WellView, WellStore from .geometry import GeometryView @@ -49,6 +50,7 @@ class State: pipettes: PipetteState modules: ModuleState liquids: LiquidState + liquid_classes: LiquidClassState tips: TipState wells: WellState files: FileState @@ -64,6 +66,7 @@ class StateView(HasState[State]): _pipettes: PipetteView _modules: ModuleView _liquid: LiquidView + _liquid_classes: LiquidClassView _tips: TipView _wells: WellView _geometry: GeometryView @@ -101,6 +104,11 @@ def liquid(self) -> LiquidView: """Get state view selectors for liquid state.""" return self._liquid + @property + def liquid_classes(self) -> LiquidClassView: + """Get state view selectors for liquid class state.""" + return self._liquid_classes + @property def tips(self) -> TipView: """Get state view selectors for tip state.""" @@ -135,7 +143,7 @@ def get_summary(self) -> StateSummary: """Get protocol run data.""" error = self._commands.get_error() # TODO maybe add summary here for AA - return StateSummary.construct( + return StateSummary.model_construct( status=self._commands.get_status(), errors=[] if error is None else [error], pipettes=self._pipettes.get_all(), @@ -148,6 +156,12 @@ def get_summary(self) -> StateSummary: wells=self._wells.get_all(), hasEverEnteredErrorRecovery=self._commands.get_has_entered_recovery_mode(), files=self._state.files.file_ids, + liquidClasses=[ + LiquidClassRecordWithId( + liquidClassId=liquid_class_id, **dict(liquid_class_record) + ) + for liquid_class_id, liquid_class_record in self._liquid_classes.get_all().items() + ], ) @@ -213,6 +227,7 @@ def __init__( module_calibration_offsets=module_calibration_offsets, ) self._liquid_store = LiquidStore() + self._liquid_class_store = LiquidClassStore() self._tip_store = TipStore() self._well_store = WellStore() self._file_store = FileStore() @@ -224,6 +239,7 @@ def __init__( self._labware_store, self._module_store, self._liquid_store, + self._liquid_class_store, self._tip_store, self._well_store, self._file_store, @@ -342,6 +358,7 @@ def _get_next_state(self) -> State: pipettes=self._pipette_store.state, modules=self._module_store.state, liquids=self._liquid_store.state, + liquid_classes=self._liquid_class_store.state, tips=self._tip_store.state, wells=self._well_store.state, files=self._file_store.state, @@ -359,6 +376,7 @@ def _initialize_state(self) -> None: self._pipettes = PipetteView(state.pipettes) self._modules = ModuleView(state.modules) self._liquid = LiquidView(state.liquids) + self._liquid_classes = LiquidClassView(state.liquid_classes) self._tips = TipView(state.tips) self._wells = WellView(state.wells) self._files = FileView(state.files) @@ -391,6 +409,7 @@ def _update_state_views(self) -> None: self._pipettes._state = next_state.pipettes self._modules._state = next_state.modules self._liquid._state = next_state.liquids + self._liquid_classes._state = next_state.liquid_classes self._tips._state = next_state.tips self._wells._state = next_state.wells self._change_notifier.notify() diff --git a/api/src/opentrons/protocol_engine/state/state_summary.py b/api/src/opentrons/protocol_engine/state/state_summary.py index 7e47ccbbb37..64f8e2b2737 100644 --- a/api/src/opentrons/protocol_engine/state/state_summary.py +++ b/api/src/opentrons/protocol_engine/state/state_summary.py @@ -11,6 +11,7 @@ LoadedModule, LoadedPipette, Liquid, + LiquidClassRecordWithId, WellInfoSummary, ) @@ -27,8 +28,9 @@ class StateSummary(BaseModel): pipettes: List[LoadedPipette] modules: List[LoadedModule] labwareOffsets: List[LabwareOffset] - startedAt: Optional[datetime] - completedAt: Optional[datetime] + startedAt: Optional[datetime] = None + completedAt: Optional[datetime] = None liquids: List[Liquid] = Field(default_factory=list) wells: List[WellInfoSummary] = Field(default_factory=list) files: List[str] = Field(default_factory=list) + liquidClasses: List[LiquidClassRecordWithId] = Field(default_factory=list) diff --git a/api/src/opentrons/protocol_engine/state/tips.py b/api/src/opentrons/protocol_engine/state/tips.py index 1ac3e91f795..214e2a9bc07 100644 --- a/api/src/opentrons/protocol_engine/state/tips.py +++ b/api/src/opentrons/protocol_engine/state/tips.py @@ -1,11 +1,14 @@ """Tip state tracking.""" + from dataclasses import dataclass from enum import Enum from typing import Dict, Optional, List, Union +from opentrons.types import NozzleMapInterface from opentrons.protocol_engine.state import update_types from ._abstract_store import HasState, HandlesActions +from ._well_math import wells_covered_dense from ..actions import Action, ResetTipsAction, get_state_updates from opentrons.hardware_control.nozzle_manager import NozzleMap @@ -108,49 +111,15 @@ def _handle_state_update(self, state_update: update_types.StateUpdate) -> None: column for column in definition.ordering ] - def _set_used_tips( # noqa: C901 - self, pipette_id: str, well_name: str, labware_id: str - ) -> None: + def _set_used_tips(self, pipette_id: str, well_name: str, labware_id: str) -> None: columns = self._state.column_by_labware_id.get(labware_id, []) wells = self._state.tips_by_labware_id.get(labware_id, {}) nozzle_map = self._state.pipette_info_by_pipette_id[pipette_id].nozzle_map + for well in wells_covered_dense(nozzle_map, well_name, columns): + wells[well] = TipRackWellState.USED - # TODO (cb, 02-28-2024): Transition from using partial nozzle map to full instrument map for the set used logic - num_nozzle_cols = len(nozzle_map.columns) - num_nozzle_rows = len(nozzle_map.rows) - - critical_column = 0 - critical_row = 0 - for column in columns: - if well_name in column: - critical_row = column.index(well_name) - critical_column = columns.index(column) - for i in range(num_nozzle_cols): - for j in range(num_nozzle_rows): - if nozzle_map.starting_nozzle == "A1": - if (critical_column + i < len(columns)) and ( - critical_row + j < len(columns[critical_column]) - ): - well = columns[critical_column + i][critical_row + j] - wells[well] = TipRackWellState.USED - elif nozzle_map.starting_nozzle == "A12": - if (critical_column - i >= 0) and ( - critical_row + j < len(columns[critical_column]) - ): - well = columns[critical_column - i][critical_row + j] - wells[well] = TipRackWellState.USED - elif nozzle_map.starting_nozzle == "H1": - if (critical_column + i < len(columns)) and (critical_row - j >= 0): - well = columns[critical_column + i][critical_row - j] - wells[well] = TipRackWellState.USED - elif nozzle_map.starting_nozzle == "H12": - if (critical_column - i >= 0) and (critical_row - j >= 0): - well = columns[critical_column - i][critical_row - j] - wells[well] = TipRackWellState.USED - - -class TipView(HasState[TipState]): +class TipView: """Read-only tip state view.""" _state: TipState @@ -168,12 +137,13 @@ def get_next_tip( # noqa: C901 labware_id: str, num_tips: int, starting_tip_name: Optional[str], - nozzle_map: Optional[NozzleMap], + nozzle_map: Optional[NozzleMapInterface], ) -> Optional[str]: """Get the next available clean tip. Does not support use of a starting tip if the pipette used is in a partial configuration.""" wells = self._state.tips_by_labware_id.get(labware_id, {}) columns = self._state.column_by_labware_id.get(labware_id, []) + # TODO(sf): I'm pretty sure this can be replaced with wells_covered_96 but I'm not quite sure how def _identify_tip_cluster( active_columns: int, active_rows: int, @@ -224,10 +194,7 @@ def _validate_tip_cluster( return None else: # In the case of an 8ch pipette where a column has mixed state tips we may simply progress to the next column in our search - if ( - nozzle_map is not None - and len(nozzle_map.full_instrument_map_store) == 8 - ): + if nozzle_map is not None and nozzle_map.physical_nozzle_count == 8: return None # In the case of a 96ch we can attempt to index in by singular rows and columns assuming that indexed direction is safe @@ -357,7 +324,7 @@ def _cluster_search_H12(active_columns: int, active_rows: int) -> Optional[str]: return None if starting_tip_name is None and nozzle_map is not None and columns: - num_channels = len(nozzle_map.full_instrument_map_store) + num_channels = nozzle_map.physical_nozzle_count num_nozzle_cols = len(nozzle_map.columns) num_nozzle_rows = len(nozzle_map.rows) # Each pipette's cluster search is determined by the point of entry for a given pipette/configuration: diff --git a/api/src/opentrons/protocol_engine/state/update_types.py b/api/src/opentrons/protocol_engine/state/update_types.py index 3d062e00265..9f44fcfee11 100644 --- a/api/src/opentrons/protocol_engine/state/update_types.py +++ b/api/src/opentrons/protocol_engine/state/update_types.py @@ -1,14 +1,22 @@ """Structures to represent changes that commands want to make to engine state.""" - import dataclasses import enum import typing +from typing_extensions import Self from datetime import datetime from opentrons.hardware_control.nozzle_manager import NozzleMap from opentrons.protocol_engine.resources import pipette_data_provider -from opentrons.protocol_engine.types import DeckPoint, LabwareLocation, TipGeometry +from opentrons.protocol_engine.types import ( + DeckPoint, + LabwareLocation, + OnLabwareLocation, + TipGeometry, + AspiratedFluid, + LiquidClassRecord, + ABSMeasureMode, +) from opentrons.types import MountType from opentrons_shared_data.labware.labware_definition import LabwareDefinition from opentrons_shared_data.pipette.types import PipetteNameType @@ -92,7 +100,7 @@ class LabwareLocationUpdate: new_location: LabwareLocation """The labware's new location.""" - offset_id: typing.Optional[str] + offset_id: str | None """The ID of the labware's new offset, for its new location.""" @@ -106,12 +114,46 @@ class LoadedLabwareUpdate: new_location: LabwareLocation """The labware's initial location.""" - offset_id: typing.Optional[str] + offset_id: str | None """The ID of the labware's offset.""" - display_name: typing.Optional[str] + display_name: str | None + + definition: LabwareDefinition + + +@dataclasses.dataclass +class LoadedLidStackUpdate: + """An update that loads a new lid stack.""" + + stack_id: str + """The unique ID of the Lid Stack Object.""" + + stack_object_definition: LabwareDefinition + "The System-only Labware Definition of the Lid Stack Object" + + stack_location: LabwareLocation + "The initial location of the Lid Stack Object." + + labware_ids: typing.List[str] + """The unique IDs of the new lids.""" + + new_locations_by_id: typing.Dict[str, OnLabwareLocation] + """Each lid's initial location keyed by Labware ID.""" definition: LabwareDefinition + "The Labware Definition of the Lid Labware(s) loaded." + + +@dataclasses.dataclass +class LabwareLidUpdate: + """An update that identifies a lid on a given parent labware.""" + + parent_labware_id: str + """The unique ID of the parent labware.""" + + lid_id: str + """The unique IDs of the new lids.""" @dataclasses.dataclass @@ -127,7 +169,7 @@ class LoadPipetteUpdate: pipette_name: PipetteNameType mount: MountType - liquid_presence_detection: typing.Optional[bool] + liquid_presence_detection: bool | None @dataclasses.dataclass @@ -156,7 +198,7 @@ class PipetteTipStateUpdate: """Update pipette tip state.""" pipette_id: str - tip_geometry: typing.Optional[TipGeometry] + tip_geometry: TipGeometry | None @dataclasses.dataclass @@ -201,18 +243,101 @@ class LiquidOperatedUpdate: """An update from operating a liquid.""" labware_id: str - well_name: str + well_names: list[str] volume_added: float | ClearType +@dataclasses.dataclass +class PipetteAspiratedFluidUpdate: + """Represents the pipette aspirating something. Might be air or liquid from a well.""" + + pipette_id: str + fluid: AspiratedFluid + type: typing.Literal["aspirated"] = "aspirated" + + +@dataclasses.dataclass +class PipetteEjectedFluidUpdate: + """Represents the pipette pushing something out. Might be air or liquid.""" + + pipette_id: str + volume: float + type: typing.Literal["ejected"] = "ejected" + + +@dataclasses.dataclass +class PipetteUnknownFluidUpdate: + """Represents the amount of fluid in the pipette becoming unknown.""" + + pipette_id: str + type: typing.Literal["unknown"] = "unknown" + + +@dataclasses.dataclass +class PipetteEmptyFluidUpdate: + """Sets the pipette to be valid and empty.""" + + pipette_id: str + type: typing.Literal["empty"] = "empty" + + @dataclasses.dataclass class AbsorbanceReaderLidUpdate: """An update to an absorbance reader's lid location.""" - module_id: str is_lid_on: bool +@dataclasses.dataclass +class AbsorbanceReaderDataUpdate: + """An update to an absorbance reader's lid location.""" + + read_result: typing.Dict[int, typing.Dict[str, float]] + + +@dataclasses.dataclass(frozen=True) +class AbsorbanceReaderInitializeUpdate: + """An update to an absorbance reader's initialization.""" + + measure_mode: ABSMeasureMode + sample_wave_lengths: typing.List[int] + reference_wave_length: typing.Optional[int] + + +@dataclasses.dataclass +class AbsorbanceReaderStateUpdate: + """An update to the absorbance reader module state.""" + + module_id: str + absorbance_reader_lid: AbsorbanceReaderLidUpdate | NoChangeType = NO_CHANGE + absorbance_reader_data: AbsorbanceReaderDataUpdate | NoChangeType = NO_CHANGE + initialize_absorbance_reader_update: AbsorbanceReaderInitializeUpdate | NoChangeType = ( + NO_CHANGE + ) + + +@dataclasses.dataclass +class LiquidClassLoadedUpdate: + """The state update from loading a liquid class.""" + + liquid_class_id: str + liquid_class_record: LiquidClassRecord + + +@dataclasses.dataclass +class FilesAddedUpdate: + """An update that adds a new data file.""" + + file_ids: list[str] + + +@dataclasses.dataclass +class AddressableAreaUsedUpdate: + """An update that says an addressable area has been used.""" + + addressable_area_name: str + + @dataclasses.dataclass class StateUpdate: """Represents an update to perform on engine state.""" @@ -227,10 +352,22 @@ class StateUpdate: pipette_tip_state: PipetteTipStateUpdate | NoChangeType = NO_CHANGE + pipette_aspirated_fluid: ( + PipetteAspiratedFluidUpdate + | PipetteEjectedFluidUpdate + | PipetteUnknownFluidUpdate + | PipetteEmptyFluidUpdate + | NoChangeType + ) = NO_CHANGE + labware_location: LabwareLocationUpdate | NoChangeType = NO_CHANGE loaded_labware: LoadedLabwareUpdate | NoChangeType = NO_CHANGE + loaded_lid_stack: LoadedLidStackUpdate | NoChangeType = NO_CHANGE + + labware_lid: LabwareLidUpdate | NoChangeType = NO_CHANGE + tips_used: TipsUsedUpdate | NoChangeType = NO_CHANGE liquid_loaded: LiquidLoadedUpdate | NoChangeType = NO_CHANGE @@ -239,42 +376,81 @@ class StateUpdate: liquid_operated: LiquidOperatedUpdate | NoChangeType = NO_CHANGE - absorbance_reader_lid: AbsorbanceReaderLidUpdate | NoChangeType = NO_CHANGE + absorbance_reader_state_update: AbsorbanceReaderStateUpdate | NoChangeType = ( + NO_CHANGE + ) + + liquid_class_loaded: LiquidClassLoadedUpdate | NoChangeType = NO_CHANGE + + files_added: FilesAddedUpdate | NoChangeType = NO_CHANGE + + addressable_area_used: AddressableAreaUsedUpdate | NoChangeType = NO_CHANGE + + def append(self, other: Self) -> Self: + """Apply another `StateUpdate` "on top of" this one. + + This object is mutated in-place, taking values from `other`. + If an attribute in `other` is `NO_CHANGE`, the value in this object is kept. + """ + fields = dataclasses.fields(other) + for field in fields: + other_value = other.__dict__[field.name] + if other_value != NO_CHANGE: + self.__dict__[field.name] = other_value + return self + + @classmethod + def reduce(cls: typing.Type[Self], *args: Self) -> Self: + """Fuse multiple state updates into a single one. + + State updates that are later in the parameter list are preferred to those that are earlier; + NO_CHANGE is ignored. + """ + accumulator = cls() + for arg in args: + accumulator.append(arg) + return accumulator # These convenience functions let the caller avoid the boilerplate of constructing a - # complicated dataclass tree. + # complicated dataclass tree, and allow chaining. @typing.overload def set_pipette_location( - self, + self: Self, *, pipette_id: str, new_deck_point: DeckPoint + ) -> Self: + """Schedule a pipette's coordinates to be changed while preserving its logical location.""" + + @typing.overload + def set_pipette_location( + self: Self, *, pipette_id: str, new_labware_id: str, new_well_name: str, new_deck_point: DeckPoint, - ) -> None: + ) -> Self: """Schedule a pipette's location to be set to a well.""" @typing.overload def set_pipette_location( - self, + self: Self, *, pipette_id: str, new_addressable_area_name: str, new_deck_point: DeckPoint, - ) -> None: + ) -> Self: """Schedule a pipette's location to be set to an addressable area.""" pass def set_pipette_location( # noqa: D102 - self, + self: Self, *, pipette_id: str, new_labware_id: str | NoChangeType = NO_CHANGE, new_well_name: str | NoChangeType = NO_CHANGE, new_addressable_area_name: str | NoChangeType = NO_CHANGE, new_deck_point: DeckPoint, - ) -> None: + ) -> Self: if new_addressable_area_name != NO_CHANGE: self.pipette_location = PipetteLocationUpdate( pipette_id=pipette_id, @@ -283,43 +459,48 @@ def set_pipette_location( # noqa: D102 ), new_deck_point=new_deck_point, ) + elif new_labware_id == NO_CHANGE or new_well_name == NO_CHANGE: + self.pipette_location = PipetteLocationUpdate( + pipette_id=pipette_id, + new_location=NO_CHANGE, + new_deck_point=new_deck_point, + ) else: - # These asserts should always pass because of the overloads. - assert new_labware_id != NO_CHANGE - assert new_well_name != NO_CHANGE - self.pipette_location = PipetteLocationUpdate( pipette_id=pipette_id, new_location=Well(labware_id=new_labware_id, well_name=new_well_name), new_deck_point=new_deck_point, ) + return self - def clear_all_pipette_locations(self) -> None: + def clear_all_pipette_locations(self) -> Self: """Mark all pipettes as having an unknown location.""" self.pipette_location = CLEAR + return self def set_labware_location( - self, + self: Self, *, labware_id: str, new_location: LabwareLocation, new_offset_id: str | None, - ) -> None: + ) -> Self: """Set a labware's location. See `LabwareLocationUpdate`.""" self.labware_location = LabwareLocationUpdate( labware_id=labware_id, new_location=new_location, offset_id=new_offset_id, ) + return self def set_loaded_labware( - self, + self: Self, definition: LabwareDefinition, labware_id: str, - offset_id: typing.Optional[str], - display_name: typing.Optional[str], + offset_id: str | None, + display_name: str | None, location: LabwareLocation, - ) -> None: + ) -> Self: """Add a new labware to state. See `LoadedLabwareUpdate`.""" self.loaded_labware = LoadedLabwareUpdate( definition=definition, @@ -328,14 +509,47 @@ def set_loaded_labware( new_location=location, display_name=display_name, ) + return self + + def set_loaded_lid_stack( + self: Self, + stack_id: str, + stack_object_definition: LabwareDefinition, + stack_location: LabwareLocation, + labware_definition: LabwareDefinition, + labware_ids: typing.List[str], + locations: typing.Dict[str, OnLabwareLocation], + ) -> Self: + """Add a new lid stack to state. See `LoadedLidStackUpdate`.""" + self.loaded_lid_stack = LoadedLidStackUpdate( + stack_id=stack_id, + stack_object_definition=stack_object_definition, + stack_location=stack_location, + definition=labware_definition, + labware_ids=labware_ids, + new_locations_by_id=locations, + ) + return self + + def set_lid( + self: Self, + parent_labware_id: str, + lid_id: str, + ) -> Self: + """Update the labware parent of a loaded or moved lid. See `LabwareLidUpdate`.""" + self.labware_lid = LabwareLidUpdate( + parent_labware_id=parent_labware_id, + lid_id=lid_id, + ) + return self def set_load_pipette( - self, + self: Self, pipette_id: str, pipette_name: PipetteNameType, mount: MountType, - liquid_presence_detection: typing.Optional[bool], - ) -> None: + liquid_presence_detection: bool | None, + ) -> Self: """Add a new pipette to state. See `LoadPipetteUpdate`.""" self.loaded_pipette = LoadPipetteUpdate( pipette_id=pipette_id, @@ -343,61 +557,69 @@ def set_load_pipette( mount=mount, liquid_presence_detection=liquid_presence_detection, ) + return self def update_pipette_config( - self, + self: Self, pipette_id: str, config: pipette_data_provider.LoadedStaticPipetteData, serial_number: str, - ) -> None: + ) -> Self: """Update a pipette's config. See `PipetteConfigUpdate`.""" self.pipette_config = PipetteConfigUpdate( pipette_id=pipette_id, config=config, serial_number=serial_number ) + return self - def update_pipette_nozzle(self, pipette_id: str, nozzle_map: NozzleMap) -> None: + def update_pipette_nozzle( + self: Self, pipette_id: str, nozzle_map: NozzleMap + ) -> Self: """Update a pipette's nozzle map. See `PipetteNozzleMapUpdate`.""" self.pipette_nozzle_map = PipetteNozzleMapUpdate( pipette_id=pipette_id, nozzle_map=nozzle_map ) + return self def update_pipette_tip_state( - self, pipette_id: str, tip_geometry: typing.Optional[TipGeometry] - ) -> None: + self: Self, pipette_id: str, tip_geometry: TipGeometry | None + ) -> Self: """Update a pipette's tip state. See `PipetteTipStateUpdate`.""" self.pipette_tip_state = PipetteTipStateUpdate( pipette_id=pipette_id, tip_geometry=tip_geometry ) + return self def mark_tips_as_used( - self, pipette_id: str, labware_id: str, well_name: str - ) -> None: + self: Self, pipette_id: str, labware_id: str, well_name: str + ) -> Self: """Mark tips in a tip rack as used. See `TipsUsedUpdate`.""" self.tips_used = TipsUsedUpdate( pipette_id=pipette_id, labware_id=labware_id, well_name=well_name ) + return self def set_liquid_loaded( - self, + self: Self, labware_id: str, volumes: typing.Dict[str, float], last_loaded: datetime, - ) -> None: + ) -> Self: """Add liquid volumes to well state. See `LoadLiquidUpdate`.""" self.liquid_loaded = LiquidLoadedUpdate( labware_id=labware_id, volumes=volumes, last_loaded=last_loaded, ) + return self def set_liquid_probed( - self, + self: Self, labware_id: str, well_name: str, last_probed: datetime, height: float | ClearType, volume: float | ClearType, - ) -> None: + ) -> Self: """Add a liquid height and volume to well state. See `ProbeLiquidUpdate`.""" self.liquid_probed = LiquidProbedUpdate( labware_id=labware_id, @@ -406,19 +628,92 @@ def set_liquid_probed( volume=volume, last_probed=last_probed, ) + return self def set_liquid_operated( - self, labware_id: str, well_name: str, volume_added: float | ClearType - ) -> None: + self: Self, + labware_id: str, + well_names: list[str], + volume_added: float | ClearType, + ) -> Self: """Update liquid volumes in well state. See `OperateLiquidUpdate`.""" self.liquid_operated = LiquidOperatedUpdate( labware_id=labware_id, - well_name=well_name, + well_names=well_names, volume_added=volume_added, ) + return self - def set_absorbance_reader_lid(self, module_id: str, is_lid_on: bool) -> None: + def set_fluid_aspirated(self: Self, pipette_id: str, fluid: AspiratedFluid) -> Self: + """Update record of fluid held inside a pipette. See `PipetteAspiratedFluidUpdate`.""" + self.pipette_aspirated_fluid = PipetteAspiratedFluidUpdate( + type="aspirated", pipette_id=pipette_id, fluid=fluid + ) + return self + + def set_fluid_ejected(self: Self, pipette_id: str, volume: float) -> Self: + """Update record of fluid held inside a pipette. See `PipetteEjectedFluidUpdate`.""" + self.pipette_aspirated_fluid = PipetteEjectedFluidUpdate( + type="ejected", pipette_id=pipette_id, volume=volume + ) + return self + + def set_fluid_unknown(self: Self, pipette_id: str) -> Self: + """Update record of fluid held inside a pipette. See `PipetteUnknownFluidUpdate`.""" + self.pipette_aspirated_fluid = PipetteUnknownFluidUpdate( + type="unknown", pipette_id=pipette_id + ) + return self + + def set_fluid_empty(self: Self, pipette_id: str) -> Self: + """Update record fo fluid held inside a pipette. See `PipetteEmptyFluidUpdate`.""" + self.pipette_aspirated_fluid = PipetteEmptyFluidUpdate( + type="empty", pipette_id=pipette_id + ) + return self + + def set_absorbance_reader_lid(self: Self, module_id: str, is_lid_on: bool) -> Self: """Update an absorbance reader's lid location. See `AbsorbanceReaderLidUpdate`.""" - self.absorbance_reader_lid = AbsorbanceReaderLidUpdate( - module_id=module_id, is_lid_on=is_lid_on + assert self.absorbance_reader_state_update == NO_CHANGE + self.absorbance_reader_state_update = AbsorbanceReaderStateUpdate( + module_id=module_id, + absorbance_reader_lid=AbsorbanceReaderLidUpdate(is_lid_on=is_lid_on), + ) + return self + + def set_absorbance_reader_data( + self, module_id: str, read_result: typing.Dict[int, typing.Dict[str, float]] + ) -> Self: + """Update an absorbance reader's read data. See `AbsorbanceReaderReadDataUpdate`.""" + assert self.absorbance_reader_state_update == NO_CHANGE + self.absorbance_reader_state_update = AbsorbanceReaderStateUpdate( + module_id=module_id, + absorbance_reader_data=AbsorbanceReaderDataUpdate(read_result=read_result), + ) + return self + + def initialize_absorbance_reader( + self, + module_id: str, + measure_mode: ABSMeasureMode, + sample_wave_lengths: typing.List[int], + reference_wave_length: typing.Optional[int], + ) -> Self: + """Initialize absorbance reader.""" + assert self.absorbance_reader_state_update == NO_CHANGE + self.absorbance_reader_state_update = AbsorbanceReaderStateUpdate( + module_id=module_id, + initialize_absorbance_reader_update=AbsorbanceReaderInitializeUpdate( + measure_mode=measure_mode, + sample_wave_lengths=sample_wave_lengths, + reference_wave_length=reference_wave_length, + ), + ) + return self + + def set_addressable_area_used(self: Self, addressable_area_name: str) -> Self: + """Mark that an addressable area has been used. See `AddressableAreaUsedUpdate`.""" + self.addressable_area_used = AddressableAreaUsedUpdate( + addressable_area_name=addressable_area_name ) + return self diff --git a/api/src/opentrons/protocol_engine/state/wells.py b/api/src/opentrons/protocol_engine/state/wells.py index 5b4d3bb8d77..fdcb8322094 100644 --- a/api/src/opentrons/protocol_engine/state/wells.py +++ b/api/src/opentrons/protocol_engine/state/wells.py @@ -1,4 +1,5 @@ """Basic well data state and store.""" + from dataclasses import dataclass from typing import Dict, List, Union, Iterator, Optional, Tuple, overload, TypeVar @@ -54,7 +55,7 @@ def _handle_liquid_loaded_update( labware_id = state_update.labware_id if labware_id not in self._state.loaded_volumes: self._state.loaded_volumes[labware_id] = {} - for (well, volume) in state_update.volumes.items(): + for well, volume in state_update.volumes.items(): self._state.loaded_volumes[labware_id][well] = LoadedVolumeInfo( volume=_none_from_clear(volume), last_loaded=state_update.last_loaded, @@ -83,19 +84,28 @@ def _handle_liquid_probed_update( def _handle_liquid_operated_update( self, state_update: update_types.LiquidOperatedUpdate ) -> None: - labware_id = state_update.labware_id - well_name = state_update.well_name + for well_name in state_update.well_names: + self._handle_well_operated( + state_update.labware_id, well_name, state_update.volume_added + ) + + def _handle_well_operated( + self, + labware_id: str, + well_name: str, + volume_added: float | update_types.ClearType, + ) -> None: if ( labware_id in self._state.loaded_volumes and well_name in self._state.loaded_volumes[labware_id] ): - if state_update.volume_added is update_types.CLEAR: + if volume_added is update_types.CLEAR: del self._state.loaded_volumes[labware_id][well_name] else: prev_loaded_vol_info = self._state.loaded_volumes[labware_id][well_name] assert prev_loaded_vol_info.volume is not None self._state.loaded_volumes[labware_id][well_name] = LoadedVolumeInfo( - volume=prev_loaded_vol_info.volume + state_update.volume_added, + volume=prev_loaded_vol_info.volume + volume_added, last_loaded=prev_loaded_vol_info.last_loaded, operations_since_load=prev_loaded_vol_info.operations_since_load + 1, @@ -109,16 +119,14 @@ def _handle_liquid_operated_update( labware_id in self._state.probed_volumes and well_name in self._state.probed_volumes[labware_id] ): - if state_update.volume_added is update_types.CLEAR: + if volume_added is update_types.CLEAR: del self._state.probed_volumes[labware_id][well_name] else: prev_probed_vol_info = self._state.probed_volumes[labware_id][well_name] if prev_probed_vol_info.volume is None: new_vol_info: float | None = None else: - new_vol_info = ( - prev_probed_vol_info.volume + state_update.volume_added - ) + new_vol_info = prev_probed_vol_info.volume + volume_added self._state.probed_volumes[labware_id][well_name] = ProbedVolumeInfo( volume=new_vol_info, last_probed=prev_probed_vol_info.last_probed, @@ -127,7 +135,7 @@ def _handle_liquid_operated_update( ) -class WellView(HasState[WellState]): +class WellView: """Read-only well state view.""" _state: WellState @@ -214,7 +222,7 @@ def _volume_from_info(info: Optional[LoadedVolumeInfo]) -> Optional[float]: def _volume_from_info( - info: Union[ProbedVolumeInfo, LoadedVolumeInfo, None] + info: Union[ProbedVolumeInfo, LoadedVolumeInfo, None], ) -> Optional[float]: if info is None: return None diff --git a/api/src/opentrons/protocol_engine/types.py b/api/src/opentrons/protocol_engine/types.py index ea3a57945b2..9d596adbaa8 100644 --- a/api/src/opentrons/protocol_engine/types.py +++ b/api/src/opentrons/protocol_engine/types.py @@ -1,29 +1,30 @@ """Public protocol engine value types and models.""" from __future__ import annotations -import re from datetime import datetime from enum import Enum from dataclasses import dataclass from pathlib import Path +from typing import ( + Any, + Dict, + FrozenSet, + List, + Mapping, + NamedTuple, + Optional, + Tuple, + Union, +) + from pydantic import ( + ConfigDict, BaseModel, Field, + RootModel, StrictBool, StrictFloat, StrictInt, StrictStr, - validator, -) -from typing import ( - Optional, - Union, - List, - Dict, - Any, - NamedTuple, - Tuple, - FrozenSet, - Mapping, ) from typing_extensions import Literal, TypeGuard @@ -36,7 +37,9 @@ from opentrons.hardware_control.modules import ( ModuleType as ModuleType, ) - +from opentrons_shared_data.liquid_classes.liquid_class_definition import ( + ByTipTypeSetting, +) from opentrons_shared_data.pipette.types import ( # noqa: F401 # convenience re-export of LabwareUri type LabwareUri as LabwareUri, @@ -423,6 +426,21 @@ class TipGeometry: volume: float +class FluidKind(str, Enum): + """A kind of fluid that can be inside a pipette.""" + + LIQUID = "LIQUID" + AIR = "AIR" + + +@dataclass(frozen=True) +class AspiratedFluid: + """Fluid inside a pipette.""" + + kind: FluidKind + volume: float + + class MovementAxis(str, Enum): """Axis on which to issue a relative movement.""" @@ -442,6 +460,7 @@ class MotorAxis(str, Enum): RIGHT_PLUNGER = "rightPlunger" EXTENSION_Z = "extensionZ" EXTENSION_JAW = "extensionJaw" + AXIS_96_CHANNEL_CAM = "axis96ChannelCam" # TODO(mc, 2022-01-18): use opentrons_shared_data.module.types.ModuleModel @@ -535,7 +554,7 @@ class ModuleDimensions(BaseModel): bareOverallHeight: float overLabwareHeight: float - lidHeight: Optional[float] + lidHeight: Optional[float] = None class Vec3f(BaseModel): @@ -690,8 +709,8 @@ class LoadedModule(BaseModel): id: str model: ModuleModel - location: Optional[DeckSlotLocation] - serialNumber: Optional[str] + location: Optional[DeckSlotLocation] = None + serialNumber: Optional[str] = None class LabwareOffsetLocation(BaseModel): @@ -785,6 +804,10 @@ class LoadedLabware(BaseModel): location: LabwareLocation = Field( ..., description="The labware's current location." ) + lid_id: Optional[str] = Field( + None, + description=("Labware ID of a Lid currently loaded on top of the labware."), + ) offsetId: Optional[str] = Field( None, description=( @@ -800,17 +823,14 @@ class LoadedLabware(BaseModel): ) -class HexColor(BaseModel): +class HexColor(RootModel[str]): """Hex color representation.""" - __root__: str + root: str = Field(pattern=r"^#(?:[0-9a-fA-F]{3,4}){1,2}$") + - @validator("__root__") - def _color_is_a_valid_hex(cls, v: str) -> str: - match = re.search(r"^#(?:[0-9a-fA-F]{3,4}){1,2}$", v) - if not match: - raise ValueError("Color is not a valid hex color.") - return v +EmptyLiquidId = Literal["EMPTY"] +LiquidId = str | EmptyLiquidId class Liquid(BaseModel): @@ -819,7 +839,59 @@ class Liquid(BaseModel): id: str displayName: str description: str - displayColor: Optional[HexColor] + displayColor: Optional[HexColor] = None + + +class LiquidClassRecord(ByTipTypeSetting, frozen=True): + """LiquidClassRecord is our internal representation of an (immutable) liquid class. + + Conceptually, a liquid class record is the tuple (name, pipette, tip, transfer properties). + We consider two liquid classes to be the same if every entry in that tuple is the same; and liquid + classes are different if any entry in the tuple is different. + + This class defines the tuple via inheritance so that we can reuse the definitions from shared_data. + """ + + liquidClassName: str = Field( + ..., + description="Identifier for the liquid of this liquid class, e.g. glycerol50.", + ) + pipetteModel: str = Field( + ..., + description="Identifier for the pipette of this liquid class.", + ) + # The other fields like tiprack ID, aspirate properties, etc. are pulled in from ByTipTypeSetting. + + def __hash__(self) -> int: + """Hash function for LiquidClassRecord.""" + # Within the Protocol Engine, LiquidClassRecords are immutable, and we'd like to be able to + # look up LiquidClassRecords by value, which involves hashing. However, Pydantic does not + # generate a usable hash function if any of the subfields (like Coordinate) are not frozen. + # So we have to implement the hash function ourselves. + # Our strategy is to recursively convert this object into a list of (key, value) tuples. + def dict_to_tuple(d: dict[str, Any]) -> tuple[tuple[str, Any], ...]: + return tuple( + ( + field_name, + dict_to_tuple(value) + if isinstance(value, dict) + else tuple(value) + if isinstance(value, list) + else value, + ) + for field_name, value in d.items() + ) + + return hash(dict_to_tuple(self.model_dump())) + + +class LiquidClassRecordWithId(LiquidClassRecord, frozen=True): + """A LiquidClassRecord with its ID, for use in summary lists.""" + + liquidClassId: str = Field( + ..., + description="Unique identifier for this liquid class.", + ) class SpeedRange(NamedTuple): @@ -976,12 +1048,12 @@ class QuadrantNozzleLayoutConfiguration(BaseModel): ) frontRightNozzle: str = Field( ..., - regex=NOZZLE_NAME_REGEX, + pattern=NOZZLE_NAME_REGEX, description="The front right nozzle in your configuration.", ) backLeftNozzle: str = Field( ..., - regex=NOZZLE_NAME_REGEX, + pattern=NOZZLE_NAME_REGEX, description="The back left nozzle in your configuration.", ) @@ -1041,6 +1113,82 @@ def from_hw_state(cls, state: HwTipStateType) -> "TipPresenceStatus": }[state] +class NextTipInfo(BaseModel): + """Next available tip labware and well name data.""" + + labwareId: str = Field( + ..., + description="The labware ID of the tip rack where the next available tip(s) are located.", + ) + tipStartingWell: str = Field( + ..., description="The (starting) well name of the next available tip(s)." + ) + + +class NoTipReason(Enum): + """The cause of no tip being available for a pipette and tip rack(s).""" + + NO_AVAILABLE_TIPS = "noAvailableTips" + STARTING_TIP_WITH_PARTIAL = "startingTipWithPartial" + INCOMPATIBLE_CONFIGURATION = "incompatibleConfiguration" + + +class NoTipAvailable(BaseModel): + """No available next tip data.""" + + noTipReason: NoTipReason = Field( + ..., description="The reason why no next available tip could be provided." + ) + message: Optional[str] = Field( + None, description="Optional message explaining why a tip wasn't available." + ) + + +class BaseCommandAnnotation(BaseModel): + """Optional annotations for protocol engine commands.""" + + commandKeys: List[str] = Field( + ..., description="Command keys to which this annotation applies" + ) + annotationType: str = Field( + ..., description="The type of annotation (for machine parsing)" + ) + + +class SecondOrderCommandAnnotation(BaseCommandAnnotation): + """Annotates a group of atomic commands which were the direct result of a second order command. + + Examples of second order commands would be transfer, consolidate, mix, etc. + """ + + annotationType: Literal["secondOrderCommand"] = "secondOrderCommand" + params: Dict[str, Any] = Field( + ..., + description="Key value pairs of the parameters passed to the second order command that this annotates.", + ) + machineReadableName: str = Field( + ..., + description="The name of the second order command in the form that the generating software refers to it", + ) + userSpecifiedName: Optional[str] = Field( + None, description="The optional user-specified name of the second order command" + ) + userSpecifiedDescription: Optional[str] = Field( + None, + description="The optional user-specified description of the second order command", + ) + + +class CustomCommandAnnotation(BaseCommandAnnotation): + """Annotates a group of atomic commands in some manner that Opentrons software does not anticipate or originate.""" + + annotationType: Literal["custom"] = "custom" + model_config = ConfigDict(extra="allow") + + +CommandAnnotation = Union[SecondOrderCommandAnnotation, CustomCommandAnnotation] + + # 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_reader/extract_labware_definitions.py b/api/src/opentrons/protocol_reader/extract_labware_definitions.py index 2ecb64a8a39..88d7e256a07 100644 --- a/api/src/opentrons/protocol_reader/extract_labware_definitions.py +++ b/api/src/opentrons/protocol_reader/extract_labware_definitions.py @@ -41,7 +41,10 @@ async def extract_labware_definitions( async def _extract_from_labware_file(path: Path) -> LabwareDefinition: - return await anyio.to_thread.run_sync(LabwareDefinition.parse_file, path) + def _do_parse() -> LabwareDefinition: + return LabwareDefinition.model_validate_json(path.read_bytes()) + + return await anyio.to_thread.run_sync(_do_parse) async def _extract_from_json_protocol_file(path: Path) -> List[LabwareDefinition]: @@ -52,7 +55,7 @@ def extract_sync(path: Path) -> List[LabwareDefinition]: # which require this labwareDefinitions key. unvalidated_definitions = json_contents["labwareDefinitions"].values() validated_definitions = [ - LabwareDefinition.parse_obj(u) for u in unvalidated_definitions + LabwareDefinition.model_validate(u) for u in unvalidated_definitions ] return validated_definitions diff --git a/api/src/opentrons/protocol_reader/file_format_validator.py b/api/src/opentrons/protocol_reader/file_format_validator.py index df119ac3ffa..17969fc70fe 100644 --- a/api/src/opentrons/protocol_reader/file_format_validator.py +++ b/api/src/opentrons/protocol_reader/file_format_validator.py @@ -60,7 +60,7 @@ async def validate(files: Iterable[IdentifiedFile]) -> None: async def _validate_labware_definition(info: IdentifiedLabwareDefinition) -> None: def validate_sync() -> None: try: - LabwareDefinition.parse_obj(info.unvalidated_json) + LabwareDefinition.model_validate(info.unvalidated_json) except PydanticValidationError as e: raise FileFormatValidationError( message=f"{info.original_file.name} could not be read as a labware definition.", @@ -133,17 +133,17 @@ async def _validate_json_protocol(info: IdentifiedJsonMain) -> None: def validate_sync() -> None: if info.schema_version == 8: try: - JsonProtocolV8.parse_obj(info.unvalidated_json) + JsonProtocolV8.model_validate(info.unvalidated_json) except PydanticValidationError as pve: _handle_v8_json_protocol_validation_error(info, pve) else: try: if info.schema_version == 7: - JsonProtocolV7.parse_obj(info.unvalidated_json) + JsonProtocolV7.model_validate(info.unvalidated_json) elif info.schema_version == 6: - JsonProtocolV6.parse_obj(info.unvalidated_json) + JsonProtocolV6.model_validate(info.unvalidated_json) else: - JsonProtocolUpToV5.parse_obj(info.unvalidated_json) + JsonProtocolUpToV5.model_validate(info.unvalidated_json) except PydanticValidationError as e: raise FileFormatValidationError._generic_json_failure(info, e) from e diff --git a/api/src/opentrons/protocol_runner/json_file_reader.py b/api/src/opentrons/protocol_runner/json_file_reader.py index 488c28d273b..f318be1db5d 100644 --- a/api/src/opentrons/protocol_runner/json_file_reader.py +++ b/api/src/opentrons/protocol_runner/json_file_reader.py @@ -30,11 +30,17 @@ def read( }, ) if protocol_source.config.schema_version == 6: - return ProtocolSchemaV6.parse_file(protocol_source.main_file) + return ProtocolSchemaV6.model_validate_json( + protocol_source.main_file.read_bytes() + ) elif protocol_source.config.schema_version == 7: - return ProtocolSchemaV7.parse_file(protocol_source.main_file) + return ProtocolSchemaV7.model_validate_json( + protocol_source.main_file.read_bytes() + ) elif protocol_source.config.schema_version == 8: - return ProtocolSchemaV8.parse_file(protocol_source.main_file) + return ProtocolSchemaV8.model_validate_json( + protocol_source.main_file.read_bytes() + ) else: raise ProtocolFilesInvalidError( message=f"{name} is a JSON protocol v{protocol_source.config.schema_version} which this robot cannot execute", diff --git a/api/src/opentrons/protocol_runner/json_translator.py b/api/src/opentrons/protocol_runner/json_translator.py index 6c670baf97a..f20bb3464d8 100644 --- a/api/src/opentrons/protocol_runner/json_translator.py +++ b/api/src/opentrons/protocol_runner/json_translator.py @@ -1,6 +1,7 @@ """Translation of JSON protocol commands into ProtocolEngine commands.""" -from typing import cast, List, Union, Iterator -from pydantic import parse_obj_as, ValidationError as PydanticValidationError + +from typing import List, Union, Iterator +from pydantic import ValidationError as PydanticValidationError, TypeAdapter from opentrons_shared_data.pipette.types import PipetteNameType from opentrons_shared_data.protocol.models import ( @@ -10,6 +11,8 @@ protocol_schema_v7, ProtocolSchemaV8, protocol_schema_v8, + Location, + # CommandSchemaId, ) from opentrons_shared_data import command as command_schema from opentrons_shared_data.errors.exceptions import InvalidProtocolData, PythonException @@ -22,7 +25,7 @@ DeckSlotLocation, Liquid, ) -from opentrons.protocol_engine.types import HexColor +from opentrons.protocol_engine.types import HexColor, CommandAnnotation class CommandTranslatorError(Exception): @@ -31,6 +34,15 @@ class CommandTranslatorError(Exception): pass +# Each time a TypeAdapter is instantiated, it will construct a new validator and +# serializer. To improve performance, TypeAdapters are instantiated once. +# See https://docs.pydantic.dev/latest/concepts/performance/#typeadapter-instantiated-once +LabwareLocationAdapter: TypeAdapter[LabwareLocation] = TypeAdapter(LabwareLocation) +CommandAnnotationAdapter: TypeAdapter[CommandAnnotation] = TypeAdapter( + CommandAnnotation +) + + def _translate_labware_command( protocol: ProtocolSchemaV6, command: protocol_schema_v6.Command, @@ -41,6 +53,8 @@ def _translate_labware_command( assert labware_id is not None definition_id = protocol.labware[labware_id].definitionId assert definition_id is not None + + location = command.params.location labware_command = pe_commands.LoadLabwareCreate( params=pe_commands.LoadLabwareParams( labwareId=command.params.labwareId, @@ -48,10 +62,8 @@ def _translate_labware_command( version=protocol.labwareDefinitions[definition_id].version, namespace=protocol.labwareDefinitions[definition_id].namespace, loadName=protocol.labwareDefinitions[definition_id].parameters.loadName, - location=parse_obj_as( - # https://github.com/samuelcolvin/pydantic/issues/1847 - LabwareLocation, # type: ignore[arg-type] - command.params.location, + location=LabwareLocationAdapter.validate_python( + location.model_dump() if isinstance(location, Location) else location ), ), key=command.key, @@ -70,6 +82,7 @@ def _translate_v7_labware_command( assert command.params.namespace is not None assert command.params.loadName is not None + location = command.params.location labware_command = pe_commands.LoadLabwareCreate( params=pe_commands.LoadLabwareParams( labwareId=command.params.labwareId, @@ -77,10 +90,8 @@ def _translate_v7_labware_command( version=command.params.version, namespace=command.params.namespace, loadName=command.params.loadName, - location=parse_obj_as( - # https://github.com/samuelcolvin/pydantic/issues/1847 - LabwareLocation, # type: ignore[arg-type] - command.params.location, + location=LabwareLocationAdapter.validate_python( + location.model_dump() if isinstance(location, Location) else location ), ), key=command.key, @@ -98,10 +109,14 @@ def _translate_module_command( # load module command must contain module_id. modules cannot be None. assert module_id is not None assert modules is not None + + location = command.params.location translated_obj = pe_commands.LoadModuleCreate( params=pe_commands.LoadModuleParams( model=ModuleModel(modules[module_id].model), - location=DeckSlotLocation.parse_obj(command.params.location), + location=DeckSlotLocation.model_validate( + location.model_dump() if isinstance(location, Location) else location + ), moduleId=command.params.moduleId, ), key=command.key, @@ -117,10 +132,13 @@ def _translate_v7_module_command( # load module command must contain module_id. modules cannot be None. assert module_id is not None assert command.params.model is not None + location = command.params.location translated_obj = pe_commands.LoadModuleCreate( params=pe_commands.LoadModuleParams( model=ModuleModel(command.params.model), - location=DeckSlotLocation.parse_obj(command.params.location), + location=DeckSlotLocation.model_validate( + location.model_dump() if isinstance(location, Location) else location + ), moduleId=command.params.moduleId, ), key=command.key, @@ -171,9 +189,9 @@ def _translate_simple_command( protocol_schema_v6.Command, protocol_schema_v7.Command, protocol_schema_v8.Command, - ] + ], ) -> pe_commands.CommandCreate: - dict_command = command.dict(exclude_none=True) + dict_command = command.model_dump(exclude_none=True) # map deprecated `delay` commands to `waitForResume` / `waitForDuration` if dict_command["commandType"] == "delay": @@ -182,15 +200,7 @@ def _translate_simple_command( else: dict_command["commandType"] = "waitForDuration" - translated_obj = cast( - pe_commands.CommandCreate, - parse_obj_as( - # https://github.com/samuelcolvin/pydantic/issues/1847 - pe_commands.CommandCreate, # type: ignore[arg-type] - dict_command, - ), - ) - return translated_obj + return pe_commands.CommandCreateAdapter.validate_python(dict_command) class JsonTranslator: @@ -207,7 +217,7 @@ def translate_liquids( id=liquid_id, displayName=liquid.displayName, description=liquid.description, - displayColor=HexColor(__root__=liquid.displayColor) + displayColor=HexColor(liquid.displayColor) if liquid.displayColor is not None else None, ) @@ -284,3 +294,19 @@ def translate_all_commands() -> Iterator[pe_commands.CommandCreate]: ) return list(translate_all_commands()) + + def translate_command_annotations( + self, + protocol: Union[ProtocolSchemaV8, ProtocolSchemaV7, ProtocolSchemaV6], + ) -> List[CommandAnnotation]: + """Translate command annotations in json protocol schema v8.""" + if isinstance(protocol, (ProtocolSchemaV6, ProtocolSchemaV7)): + return [] + else: + command_annotations: List[CommandAnnotation] = [ + CommandAnnotationAdapter.validate_python( + command_annotation.model_dump(), + ) + for command_annotation in protocol.commandAnnotations + ] + return command_annotations diff --git a/api/src/opentrons/protocol_runner/legacy_command_mapper.py b/api/src/opentrons/protocol_runner/legacy_command_mapper.py index 27b1c7ea331..f7d50e539d8 100644 --- a/api/src/opentrons/protocol_runner/legacy_command_mapper.py +++ b/api/src/opentrons/protocol_runner/legacy_command_mapper.py @@ -177,11 +177,13 @@ def map_command( # noqa: C901 completed_command: pe_commands.Command if command_error is None: if isinstance(running_command, pe_commands.PickUpTip): - completed_command = running_command.copy( + completed_command = running_command.model_copy( update={ - "result": pe_commands.PickUpTipResult.construct( + "result": pe_commands.PickUpTipResult.model_construct( tipVolume=command["payload"]["location"].max_volume, # type: ignore[typeddict-item] - tipLength=command["payload"]["instrument"].hw_pipette["tip_length"], # type: ignore[typeddict-item] + tipLength=command["payload"]["instrument"].hw_pipette[ # type: ignore[typeddict-item] + "tip_length" + ], position=pe_types.DeckPoint(x=0, y=0, z=0), ), "status": pe_commands.CommandStatus.SUCCEEDED, @@ -190,9 +192,9 @@ def map_command( # noqa: C901 } ) elif isinstance(running_command, pe_commands.DropTip): - completed_command = running_command.copy( + completed_command = running_command.model_copy( update={ - "result": pe_commands.DropTipResult.construct( + "result": pe_commands.DropTipResult.model_construct( position=pe_types.DeckPoint(x=0, y=0, z=0) ), "status": pe_commands.CommandStatus.SUCCEEDED, @@ -201,9 +203,9 @@ def map_command( # noqa: C901 } ) elif isinstance(running_command, pe_commands.Aspirate): - completed_command = running_command.copy( + completed_command = running_command.model_copy( update={ - # Don't .construct() result, because we want to validate + # Don't .model_construct() result, because we want to validate # volume. "result": pe_commands.AspirateResult( volume=running_command.params.volume, @@ -215,9 +217,9 @@ def map_command( # noqa: C901 } ) elif isinstance(running_command, pe_commands.Dispense): - completed_command = running_command.copy( + completed_command = running_command.model_copy( update={ - # Don't .construct() result, because we want to validate + # Don't .model_construct() result, because we want to validate # volume. "result": pe_commands.DispenseResult( volume=running_command.params.volume, @@ -229,9 +231,9 @@ def map_command( # noqa: C901 } ) elif isinstance(running_command, pe_commands.BlowOut): - completed_command = running_command.copy( + completed_command = running_command.model_copy( update={ - "result": pe_commands.BlowOutResult.construct( + "result": pe_commands.BlowOutResult.model_construct( position=pe_types.DeckPoint(x=0, y=0, z=0) ), "status": pe_commands.CommandStatus.SUCCEEDED, @@ -240,18 +242,18 @@ def map_command( # noqa: C901 } ) elif isinstance(running_command, pe_commands.Comment): - completed_command = running_command.copy( + completed_command = running_command.model_copy( update={ - "result": pe_commands.CommentResult.construct(), + "result": pe_commands.CommentResult.model_construct(), "status": pe_commands.CommandStatus.SUCCEEDED, "completedAt": now, "notes": [], } ) elif isinstance(running_command, pe_commands.Custom): - completed_command = running_command.copy( + completed_command = running_command.model_copy( update={ - "result": pe_commands.CustomResult.construct(), + "result": pe_commands.CustomResult.model_construct(), "status": pe_commands.CommandStatus.SUCCEEDED, "completedAt": now, "notes": [], @@ -261,7 +263,7 @@ def map_command( # noqa: C901 # TODO(mm, 2024-06-13): This looks potentially wrong. # We're creating a `SUCCEEDED` command that does not have a `result`, # which is not normally possible. - completed_command = running_command.copy( + completed_command = running_command.model_copy( update={ "status": pe_commands.CommandStatus.SUCCEEDED, "completedAt": now, @@ -331,51 +333,51 @@ def _build_initial_command( elif command["name"] == legacy_command_types.BLOW_OUT: return self._build_blow_out(command=command, command_id=command_id, now=now) elif command["name"] == legacy_command_types.PAUSE: - wait_for_resume_running = pe_commands.WaitForResume.construct( + wait_for_resume_running = pe_commands.WaitForResume.model_construct( id=command_id, key=command_id, status=pe_commands.CommandStatus.RUNNING, createdAt=now, startedAt=now, - params=pe_commands.WaitForResumeParams.construct( + params=pe_commands.WaitForResumeParams.model_construct( message=command["payload"]["userMessage"], ), ) wait_for_resume_create: pe_commands.CommandCreate = ( - pe_commands.WaitForResumeCreate.construct( + pe_commands.WaitForResumeCreate.model_construct( key=wait_for_resume_running.key, params=wait_for_resume_running.params, ) ) return wait_for_resume_create, wait_for_resume_running elif command["name"] == legacy_command_types.COMMENT: - comment_running = pe_commands.Comment.construct( + comment_running = pe_commands.Comment.model_construct( id=command_id, key=command_id, status=pe_commands.CommandStatus.RUNNING, createdAt=now, startedAt=now, - params=pe_commands.CommentParams.construct( + params=pe_commands.CommentParams.model_construct( message=command["payload"]["text"], ), ) - comment_create = pe_commands.CommentCreate.construct( + comment_create = pe_commands.CommentCreate.model_construct( key=comment_running.key, params=comment_running.params ) return comment_create, comment_running else: - custom_running = pe_commands.Custom.construct( + custom_running = pe_commands.Custom.model_construct( id=command_id, key=command_id, status=pe_commands.CommandStatus.RUNNING, createdAt=now, startedAt=now, - params=LegacyCommandParams.construct( + params=LegacyCommandParams.model_construct( legacyCommandType=command["name"], legacyCommandText=command["payload"]["text"], ), ) - custom_create = pe_commands.CustomCreate.construct( + custom_create = pe_commands.CustomCreate.model_construct( key=custom_running.key, params=custom_running.params, ) @@ -396,19 +398,19 @@ def _build_drop_tip( labware_id = self._labware_id_by_slot[slot] pipette_id = self._pipette_id_by_mount[mount] - running = pe_commands.DropTip.construct( + running = pe_commands.DropTip.model_construct( id=command_id, key=command_id, status=pe_commands.CommandStatus.RUNNING, createdAt=now, startedAt=now, - params=pe_commands.DropTipParams.construct( + params=pe_commands.DropTipParams.model_construct( pipetteId=pipette_id, labwareId=labware_id, wellName=well_name, ), ) - create = pe_commands.DropTipCreate.construct( + create = pe_commands.DropTipCreate.model_construct( key=running.key, params=running.params, ) @@ -430,19 +432,19 @@ def _build_pick_up_tip( labware_id = self._labware_id_by_slot[slot] pipette_id = self._pipette_id_by_mount[mount] - running = pe_commands.PickUpTip.construct( + running = pe_commands.PickUpTip.model_construct( id=command_id, key=command_id, status=pe_commands.CommandStatus.RUNNING, createdAt=now, startedAt=now, - params=pe_commands.PickUpTipParams.construct( + params=pe_commands.PickUpTipParams.model_construct( pipetteId=pipette_id, labwareId=labware_id, wellName=well_name, ), ) - create = pe_commands.PickUpTipCreate.construct( + create = pe_commands.PickUpTipCreate.model_construct( key=running.key, params=running.params ) return create, running @@ -482,31 +484,31 @@ def _build_liquid_handling( # TODO(mm, 2024-03-22): I don't think this has been true since # https://github.com/Opentrons/opentrons/pull/14211. Can we just use # aspirate and dispense commands now? - move_to_well_running = pe_commands.MoveToWell.construct( + move_to_well_running = pe_commands.MoveToWell.model_construct( id=command_id, key=command_id, status=pe_commands.CommandStatus.RUNNING, createdAt=now, startedAt=now, - params=pe_commands.MoveToWellParams.construct( + params=pe_commands.MoveToWellParams.model_construct( pipetteId=pipette_id, labwareId=labware_id, wellName=well_name, ), ) - move_to_well_create = pe_commands.MoveToWellCreate.construct( + move_to_well_create = pe_commands.MoveToWellCreate.model_construct( key=move_to_well_running.key, params=move_to_well_running.params ) return move_to_well_create, move_to_well_running elif command["name"] == legacy_command_types.ASPIRATE: flow_rate = command["payload"]["rate"] * pipette.flow_rate.aspirate - aspirate_running = pe_commands.Aspirate.construct( + aspirate_running = pe_commands.Aspirate.model_construct( id=command_id, key=command_id, status=pe_commands.CommandStatus.RUNNING, createdAt=now, startedAt=now, - # Don't .construct() params, because we want to validate + # Don't .model_construct() params, because we want to validate # volume and flowRate. params=pe_commands.AspirateParams( pipetteId=pipette_id, @@ -516,19 +518,19 @@ def _build_liquid_handling( flowRate=flow_rate, ), ) - aspirate_create = pe_commands.AspirateCreate.construct( + aspirate_create = pe_commands.AspirateCreate.model_construct( key=aspirate_running.key, params=aspirate_running.params ) return aspirate_create, aspirate_running else: flow_rate = command["payload"]["rate"] * pipette.flow_rate.dispense - dispense_running = pe_commands.Dispense.construct( + dispense_running = pe_commands.Dispense.model_construct( id=command_id, key=command_id, status=pe_commands.CommandStatus.RUNNING, createdAt=now, startedAt=now, - # Don't .construct params, because we want to validate + # Don't .model_construct params, because we want to validate # volume and flowRate. params=pe_commands.DispenseParams( pipetteId=pipette_id, @@ -538,24 +540,24 @@ def _build_liquid_handling( flowRate=flow_rate, ), ) - dispense_create = pe_commands.DispenseCreate.construct( + dispense_create = pe_commands.DispenseCreate.model_construct( key=dispense_running.key, params=dispense_running.params ) return dispense_create, dispense_running else: - running = pe_commands.Custom.construct( + running = pe_commands.Custom.model_construct( id=command_id, key=command_id, status=pe_commands.CommandStatus.RUNNING, createdAt=now, startedAt=now, - params=LegacyCommandParams.construct( + params=LegacyCommandParams.model_construct( legacyCommandType=command["name"], legacyCommandText=command["payload"]["text"], ), ) - create = pe_commands.CustomCreate.construct( + create = pe_commands.CustomCreate.model_construct( key=running.key, params=running.params ) return create, running @@ -584,13 +586,13 @@ def _build_blow_out( well_name = well.well_name pipette_id = self._pipette_id_by_mount[mount] - blow_out_running = pe_commands.BlowOut.construct( + blow_out_running = pe_commands.BlowOut.model_construct( id=command_id, key=command_id, status=pe_commands.CommandStatus.RUNNING, createdAt=now, startedAt=now, - # Don't .construct() params, because we want to validate flowRate. + # Don't .model_construct() params, because we want to validate flowRate. params=pe_commands.BlowOutParams( pipetteId=pipette_id, labwareId=labware_id, @@ -598,7 +600,7 @@ def _build_blow_out( flowRate=flow_rate, ), ) - blow_out_create = pe_commands.BlowOutCreate.construct( + blow_out_create = pe_commands.BlowOutCreate.model_construct( key=blow_out_running.key, params=blow_out_running.params ) return blow_out_create, blow_out_running @@ -606,18 +608,18 @@ def _build_blow_out( # TODO:(jr, 15.08.2022): blow_out commands with no specified labware get filtered # into custom. Refactor this in followup legacy command mapping else: - custom_running = pe_commands.Custom.construct( + custom_running = pe_commands.Custom.model_construct( id=command_id, key=command_id, status=pe_commands.CommandStatus.RUNNING, createdAt=now, startedAt=now, - params=LegacyCommandParams.construct( + params=LegacyCommandParams.model_construct( legacyCommandType=command["name"], legacyCommandText=command["payload"]["text"], ), ) - custom_create = pe_commands.CustomCreate.construct( + custom_create = pe_commands.CustomCreate.model_construct( key=custom_running.key, params=custom_running.params ) return custom_create, custom_running @@ -631,23 +633,23 @@ def _map_labware_load( slot = labware_load_info.deck_slot location: pe_types.LabwareLocation if labware_load_info.on_module: - location = pe_types.ModuleLocation.construct( + location = pe_types.ModuleLocation.model_construct( moduleId=self._module_id_by_slot[slot] ) else: - location = pe_types.DeckSlotLocation.construct(slotName=slot) + location = pe_types.DeckSlotLocation.model_construct(slotName=slot) command_id = f"commands.LOAD_LABWARE-{count}" labware_id = f"labware-{count}" - succeeded_command = pe_commands.LoadLabware.construct( + succeeded_command = pe_commands.LoadLabware.model_construct( id=command_id, key=command_id, status=pe_commands.CommandStatus.SUCCEEDED, createdAt=now, startedAt=now, completedAt=now, - params=pe_commands.LoadLabwareParams.construct( + params=pe_commands.LoadLabwareParams.model_construct( location=location, loadName=labware_load_info.labware_load_name, namespace=labware_load_info.labware_namespace, @@ -655,9 +657,9 @@ def _map_labware_load( displayName=labware_load_info.labware_display_name, ), notes=[], - result=pe_commands.LoadLabwareResult.construct( + result=pe_commands.LoadLabwareResult.model_construct( labwareId=labware_id, - definition=LabwareDefinition.parse_obj( + definition=LabwareDefinition.model_validate( labware_load_info.labware_definition ), offsetId=labware_load_info.offset_id, @@ -666,7 +668,7 @@ def _map_labware_load( queue_action = pe_actions.QueueCommandAction( command_id=succeeded_command.id, created_at=succeeded_command.createdAt, - request=pe_commands.LoadLabwareCreate.construct( + request=pe_commands.LoadLabwareCreate.model_construct( key=succeeded_command.key, params=succeeded_command.params ), request_hash=None, @@ -714,19 +716,19 @@ def _map_instrument_load( pipette_id = f"pipette-{count}" mount = MountType(str(instrument_load_info.mount).lower()) - succeeded_command = pe_commands.LoadPipette.construct( + succeeded_command = pe_commands.LoadPipette.model_construct( id=command_id, key=command_id, status=pe_commands.CommandStatus.SUCCEEDED, createdAt=now, startedAt=now, completedAt=now, - params=pe_commands.LoadPipetteParams.construct( + params=pe_commands.LoadPipetteParams.model_construct( pipetteName=PipetteNameType(instrument_load_info.instrument_load_name), mount=mount, ), notes=[], - result=pe_commands.LoadPipetteResult.construct(pipetteId=pipette_id), + result=pe_commands.LoadPipetteResult.model_construct(pipetteId=pipette_id), ) serial = instrument_load_info.pipette_dict.get("pipette_id", None) or "" state_update = StateUpdate() @@ -749,7 +751,7 @@ def _map_instrument_load( queue_action = pe_actions.QueueCommandAction( command_id=succeeded_command.id, created_at=succeeded_command.createdAt, - request=pe_commands.LoadPipetteCreate.construct( + request=pe_commands.LoadPipetteCreate.model_construct( key=succeeded_command.key, params=succeeded_command.params ), request_hash=None, @@ -791,14 +793,14 @@ def _map_module_load( loaded_model ) or self._module_data_provider.get_definition(loaded_model) - succeeded_command = pe_commands.LoadModule.construct( + succeeded_command = pe_commands.LoadModule.model_construct( id=command_id, key=command_id, status=pe_commands.CommandStatus.SUCCEEDED, createdAt=now, startedAt=now, completedAt=now, - params=pe_commands.LoadModuleParams.construct( + params=pe_commands.LoadModuleParams.model_construct( model=requested_model, location=pe_types.DeckSlotLocation( slotName=module_load_info.deck_slot, @@ -806,7 +808,7 @@ def _map_module_load( moduleId=module_id, ), notes=[], - result=pe_commands.LoadModuleResult.construct( + result=pe_commands.LoadModuleResult.model_construct( moduleId=module_id, serialNumber=module_load_info.module_serial, definition=loaded_definition, @@ -816,7 +818,7 @@ def _map_module_load( queue_action = pe_actions.QueueCommandAction( command_id=succeeded_command.id, created_at=succeeded_command.createdAt, - request=pe_commands.LoadModuleCreate.construct( + request=pe_commands.LoadModuleCreate.model_construct( key=succeeded_command.key, params=succeeded_command.params ), request_hash=None, diff --git a/api/src/opentrons/protocol_runner/protocol_runner.py b/api/src/opentrons/protocol_runner/protocol_runner.py index aec2aae80df..ed540020d24 100644 --- a/api/src/opentrons/protocol_runner/protocol_runner.py +++ b/api/src/opentrons/protocol_runner/protocol_runner.py @@ -24,6 +24,7 @@ Command, commands as pe_commands, ) +from opentrons.protocol_engine.types import CommandAnnotation from opentrons.protocols.parse import PythonParseMode from opentrons.util.async_helpers import asyncio_yield from opentrons.util.broker import Broker @@ -56,6 +57,7 @@ class RunResult(NamedTuple): commands: List[Command] state_summary: StateSummary parameters: List[RunTimeParameter] + command_annotations: List[CommandAnnotation] class AbstractRunner(ABC): @@ -93,6 +95,11 @@ def run_time_parameters(self) -> List[RunTimeParameter]: """Parameter definitions defined by protocol, if any. Currently only for python protocols.""" return [] + @property + def command_annotations(self) -> List[CommandAnnotation]: + """Command annotations defined by protocol, if any. Currently only for json protocols.""" + return [] + def was_started(self) -> bool: """Whether the run has been started. @@ -177,7 +184,7 @@ def __init__( @property def run_time_parameters(self) -> List[RunTimeParameter]: - """Parameter definitions defined by protocol, if any. Will always be empty before execution.""" + """Parameter definitions defined by protocol, if any.""" if self._parameter_context is not None: return self._parameter_context.export_parameters_for_analysis() return [] @@ -278,7 +285,10 @@ async def run( # noqa: D102 commands = self._protocol_engine.state_view.commands.get_all() parameters = self.run_time_parameters return RunResult( - commands=commands, state_summary=run_data, parameters=parameters + commands=commands, + state_summary=run_data, + parameters=parameters, + command_annotations=[], ) @@ -313,6 +323,12 @@ def __init__( hardware_api.should_taskify_movement_execution(taskify=False) self._queued_commands: List[pe_commands.CommandCreate] = [] + self._command_annotations: List[CommandAnnotation] = [] + + @property + def command_annotations(self) -> List[CommandAnnotation]: + """Command annotations defined by protocol, if any.""" + return self._command_annotations async def load(self, protocol_source: ProtocolSource) -> None: """Load a JSONv6+ ProtocolSource into managed ProtocolEngine.""" @@ -355,6 +371,11 @@ async def load(self, protocol_source: ProtocolSource) -> None: ) await asyncio_yield() + self._command_annotations = await anyio.to_thread.run_sync( + self._json_translator.translate_command_annotations, + protocol, + ) + initial_home_command = pe_commands.HomeCreate( params=pe_commands.HomeParams(axes=None) ) @@ -382,7 +403,12 @@ async def run( # noqa: D102 run_data = self._protocol_engine.state_view.get_summary() commands = self._protocol_engine.state_view.commands.get_all() - return RunResult(commands=commands, state_summary=run_data, parameters=[]) + return RunResult( + commands=commands, + state_summary=run_data, + parameters=[], + command_annotations=self._command_annotations, + ) async def _add_and_execute_commands(self) -> None: for command_request in self._queued_commands: @@ -453,7 +479,12 @@ async def run( # noqa: D102 run_data = self._protocol_engine.state_view.get_summary() commands = self._protocol_engine.state_view.commands.get_all() - return RunResult(commands=commands, state_summary=run_data, parameters=[]) + return RunResult( + commands=commands, + state_summary=run_data, + parameters=[], + command_annotations=[], + ) AnyRunner = Union[PythonAndLegacyRunner, JsonRunner, LiveRunner] diff --git a/api/src/opentrons/protocol_runner/python_protocol_wrappers.py b/api/src/opentrons/protocol_runner/python_protocol_wrappers.py index f20012f1dfe..ce063013878 100644 --- a/api/src/opentrons/protocol_runner/python_protocol_wrappers.py +++ b/api/src/opentrons/protocol_runner/python_protocol_wrappers.py @@ -66,7 +66,7 @@ def read( namespace=lw.namespace, load_name=lw.parameters.loadName, version=lw.version, - ): cast(LabwareDefinitionTypedDict, lw.dict(exclude_none=True)) + ): cast(LabwareDefinitionTypedDict, lw.model_dump(exclude_none=True)) for lw in labware_definitions } data_file_paths = [ diff --git a/api/src/opentrons/protocol_runner/run_orchestrator.py b/api/src/opentrons/protocol_runner/run_orchestrator.py index 94e6fe48eb0..28266a9c485 100644 --- a/api/src/opentrons/protocol_runner/run_orchestrator.py +++ b/api/src/opentrons/protocol_runner/run_orchestrator.py @@ -1,11 +1,13 @@ """Engine/Runner provider.""" + from __future__ import annotations import enum -from typing import Optional, Union, List, Dict, AsyncGenerator +from typing import Optional, Union, List, Dict, AsyncGenerator, Mapping from anyio import move_on_after +from opentrons.types import NozzleMapInterface from opentrons_shared_data.labware.types import LabwareUri from opentrons_shared_data.labware.labware_definition import LabwareDefinition from opentrons_shared_data.errors import GeneralError @@ -14,7 +16,6 @@ from . import protocol_runner, RunResult, JsonRunner, PythonAndLegacyRunner from ..hardware_control import HardwareControlAPI from ..hardware_control.modules import AbstractModule as HardwareModuleAPI -from ..hardware_control.nozzle_manager import NozzleMap from ..protocol_engine import ( ProtocolEngine, CommandCreate, @@ -36,6 +37,7 @@ RunTimeParameter, PrimitiveRunTimeParamValuesType, CSVRuntimeParamPaths, + CommandAnnotation, ) from ..protocol_engine.error_recovery_policy import ErrorRecoveryPolicy @@ -253,6 +255,14 @@ def get_run_time_parameters(self) -> List[RunTimeParameter]: else self._protocol_runner.run_time_parameters ) + def get_command_annotations(self) -> List[CommandAnnotation]: + """Get the list of command annotations defined in the protocol, if any.""" + return ( + [] + if self._protocol_runner is None + else self._protocol_runner.command_annotations + ) + def get_current_command(self) -> Optional[CommandPointer]: """Get the "current" command, if any.""" return self._protocol_engine.state_view.commands.get_current() @@ -414,7 +424,7 @@ def get_deck_type(self) -> DeckType: """Get engine deck type.""" return self._protocol_engine.state_view.config.deck_type - def get_nozzle_maps(self) -> Dict[str, NozzleMap]: + def get_nozzle_maps(self) -> Mapping[str, NozzleMapInterface]: """Get current nozzle maps keyed by pipette id.""" return self._protocol_engine.state_view.tips.get_pipette_nozzle_maps() diff --git a/api/src/opentrons/protocols/advanced_control/common.py b/api/src/opentrons/protocols/advanced_control/common.py new file mode 100644 index 00000000000..09e7d7a4adc --- /dev/null +++ b/api/src/opentrons/protocols/advanced_control/common.py @@ -0,0 +1,38 @@ +"""Common resources for all advanced control functions.""" +import enum +from typing import NamedTuple, Optional + + +class MixStrategy(enum.Enum): + BOTH = enum.auto() + BEFORE = enum.auto() + AFTER = enum.auto() + NEVER = enum.auto() + + +class MixOpts(NamedTuple): + """ + Options to customize behavior of mix. + + These options will be passed to + :py:meth:`InstrumentContext.mix` when it is called during the + transfer. + """ + + repetitions: Optional[int] = None + volume: Optional[float] = None + rate: Optional[float] = None + + +MixOpts.repetitions.__doc__ = ":py:class:`int`" +MixOpts.volume.__doc__ = ":py:class:`float`" +MixOpts.rate.__doc__ = ":py:class:`float`" + + +class Mix(NamedTuple): + """ + Options to control mix behavior before aspirate and after dispense. + """ + + mix_before: MixOpts = MixOpts() + mix_after: MixOpts = MixOpts() diff --git a/api/src/opentrons/protocols/advanced_control/mix.py b/api/src/opentrons/protocols/advanced_control/mix.py index 587916e98bc..8ddc3035b48 100644 --- a/api/src/opentrons/protocols/advanced_control/mix.py +++ b/api/src/opentrons/protocols/advanced_control/mix.py @@ -1,6 +1,6 @@ from typing import Any, Dict, Tuple -from opentrons.protocols.advanced_control.transfers import MixStrategy, Mix +from .common import MixStrategy, Mix def mix_from_kwargs(top_kwargs: Dict[str, Any]) -> Tuple[MixStrategy, Mix]: diff --git a/api/src/opentrons/protocols/advanced_control/transfers.py b/api/src/opentrons/protocols/advanced_control/transfers.py deleted file mode 100644 index 5ad9dd64d24..00000000000 --- a/api/src/opentrons/protocols/advanced_control/transfers.py +++ /dev/null @@ -1,1047 +0,0 @@ -import enum -from typing import ( - Any, - Dict, - List, - Optional, - Union, - NamedTuple, - Callable, - Generator, - Iterator, - Iterable, - Sequence, - Tuple, - TypedDict, - TypeAlias, - TYPE_CHECKING, - TypeVar, -) -from opentrons.protocol_api.labware import Labware, Well -from opentrons import types -from opentrons.protocols.api_support.types import APIVersion -from opentrons.hardware_control.nozzle_manager import NozzleConfigurationType - - -AdvancedLiquidHandling = Union[ - Well, - types.Location, - Sequence[Union[Well, types.Location]], - Sequence[Sequence[Well]], -] - - -class TransferStep(TypedDict): - method: str - args: Optional[List[Any]] - kwargs: Optional[Dict[Any, Any]] - - -if TYPE_CHECKING: - from opentrons.protocol_api import InstrumentContext - -_PARTIAL_TIP_SUPPORT_ADDED = APIVersion(2, 18) -"""The version after which partial tip support and nozzle maps were made available.""" - - -class MixStrategy(enum.Enum): - BOTH = enum.auto() - BEFORE = enum.auto() - AFTER = enum.auto() - NEVER = enum.auto() - - -class DropTipStrategy(enum.Enum): - TRASH = enum.auto() - RETURN = enum.auto() - - -class TouchTipStrategy(enum.Enum): - NEVER = enum.auto() - ALWAYS = enum.auto() - - -class BlowOutStrategy(enum.Enum): - NONE = enum.auto() - TRASH = enum.auto() - DEST = enum.auto() - SOURCE = enum.auto() - CUSTOM_LOCATION = enum.auto() - - -class TransferMode(enum.Enum): - DISTRIBUTE = enum.auto() - CONSOLIDATE = enum.auto() - TRANSFER = enum.auto() - - -class Transfer(NamedTuple): - """ - Options pertaining to behavior of the transfer. - - """ - - new_tip: types.TransferTipPolicy = types.TransferTipPolicy.ONCE - air_gap: float = 0 - carryover: bool = True - gradient_function: Optional[Callable[[float], float]] = None - disposal_volume: float = 0 - mix_strategy: MixStrategy = MixStrategy.NEVER - drop_tip_strategy: DropTipStrategy = DropTipStrategy.TRASH - blow_out_strategy: BlowOutStrategy = BlowOutStrategy.NONE - touch_tip_strategy: TouchTipStrategy = TouchTipStrategy.NEVER - - -Transfer.new_tip.__doc__ = """ - Control when or if to pick up tip during a transfer - - :py:attr:`types.TransferTipPolicy.ALWAYS` - Drop and pick up a new tip after each dispense. - - :py:attr:`types.TransferTipPolicy.ONCE` - Pick up tip at the beginning of the transfer and use it - throughout the transfer. This would speed up the transfer. - - :py:attr:`types.TransferTipPolicy.NEVER` - Do not ever pick up or drop tip. The protocol should explicitly - pick up a tip before transfer and drop it afterwards. - - To customize where to drop tip, see :py:attr:`.drop_tip_strategy`. - To customize the behavior of pickup tip, see - :py:attr:`.TransferOptions.pick_up_tip`. - """ - -Transfer.air_gap.__doc__ = """ - Controls the volume (in uL) of air gap aspirated when moving to - dispense. - - Adding an air gap would slow down a transfer since less liquid will - now fit in the pipette but it prevents the loss of liquid while - moving between wells. - """ - -Transfer.carryover.__doc__ = """ - Controls whether volumes larger than pipette's max volume will be - split into smaller volumes. - """ - -Transfer.gradient_function.__doc__ = """ - Specify a nonlinear gradient for volumes. - - This should be a function that takes a single float between 0 and 1 - and returns a single float between 0 and 1. This function is used - to determine the path the transfer takes between the volume - gradient minimum and maximum if the transfer volume is specified as - a gradient. For instance, specifying the function as - - .. code-block:: python - - def gradient(a): - if a > 0.5: - return 1.0 - else: - return 0.0 - - would transfer the minimum volume of the gradient to the first half - of the target wells, and the maximum to the other half. - """ - -Transfer.disposal_volume.__doc__ = """ - The amount of liquid (in uL) to aspirate as a buffer. - - The remaining buffer will be blown out into the location specified - by :py:attr:`.blow_out_strategy`. - - This is useful to avoid under-pipetting but can waste reagent and - slow down transfer. - """ - -Transfer.mix_strategy.__doc__ = """ - If and when to mix during a transfer. - - :py:attr:`MixStrategy.NEVER` - Do not ever perform a mix during the transfer. - - :py:attr:`MixStrategy.BEFORE` - Mix before each aspirate. - - :py:attr:`MixStrategy.AFTER` - Mix after each dispense. - - :py:attr:`MixStrategy.BOTH` - Mix before each aspirate and after each dispense. - - To customize the mix behavior, see :py:attr:`.TransferOptions.mix` - """ - -Transfer.drop_tip_strategy.__doc__ = """ - Specifies the location to drop tip into. - - :py:attr:`DropTipStrategy.TRASH` - Drop the tip into the trash container. - - :py:attr:`DropTipStrategy.RETURN` - Return the tip to tiprack. - """ - -Transfer.blow_out_strategy.__doc__ = """ - Specifies the location to blow out the liquid in the pipette to. - - :py:attr:`BlowOutStrategy.TRASH` - Blow out to trash container. - - :py:attr:`BlowOutStrategy.SOURCE` - Blow out into the source well in order to dispense any leftover - liquid. - - :py:attr:`BlowOutStrategy.DEST` - Blow out into the destination well in order to dispense any leftover - liquid. - - :py:attr:`BlowOutStrategy.CUSTOM_LOCATION` - If using any other location to blow out to. Specify the location in - :py:attr:`.TransferOptions.blow_out`. - """ - -Transfer.touch_tip_strategy.__doc__ = """ - Controls whether to touch tip during the transfer - - This helps in getting rid of any droplets clinging to the pipette - tip at the cost of slowing down the transfer. - - :py:attr:`TouchTipStrategy.NEVER` - Do not touch tip ever during the transfer. - - :py:attr:`TouchTipStrategy.ALWAYS` - Touch tip after each aspirate. - - To customize the behavior of touch tips, see - :py:attr:`.TransferOptions.touch_tip`. - """ - - -class PickUpTipOpts(NamedTuple): - """ - Options to customize :py:attr:`.Transfer.new_tip`. - - These options will be passed to - :py:meth:`InstrumentContext.pick_up_tip` when it is called during - the transfer. - """ - - location: Optional[types.Location] = None - presses: Optional[int] = None - increment: Optional[int] = None - - -PickUpTipOpts.location.__doc__ = ":py:class:`types.Location`" -PickUpTipOpts.presses.__doc__ = ":py:class:`int`" -PickUpTipOpts.increment.__doc__ = ":py:class:`int`" - - -class MixOpts(NamedTuple): - """ - Options to customize behavior of mix. - - These options will be passed to - :py:meth:`InstrumentContext.mix` when it is called during the - transfer. - """ - - repetitions: Optional[int] = None - volume: Optional[float] = None - rate: Optional[float] = None - - -MixOpts.repetitions.__doc__ = ":py:class:`int`" -MixOpts.volume.__doc__ = ":py:class:`float`" -MixOpts.rate.__doc__ = ":py:class:`float`" - - -class Mix(NamedTuple): - """ - Options to control mix behavior before aspirate and after dispense. - """ - - mix_before: MixOpts = MixOpts() - mix_after: MixOpts = MixOpts() - - -Mix.mix_before.__doc__ = """ - Options applied to mix before aspirate. - See :py:class:`.Mix.MixOpts`. - """ - -Mix.mix_after.__doc__ = """ - Options applied to mix after dispense. See :py:class:`.Mix.MixOpts`. - """ - - -class BlowOutOpts(NamedTuple): - """ - Location where to blow out instead of the trash. - - This location will be passed to :py:meth:`InstrumentContext.blow_out` - when called during the transfer - """ - - location: Optional[Union[types.Location, Well]] = None - - -BlowOutOpts.location.__doc__ = ":py:class:`types.Location`" - - -class TouchTipOpts(NamedTuple): - """ - Options to customize touch tip. - - These options will be passed to - :py:meth:`InstrumentContext.touch_tip` when called during the - transfer. - """ - - radius: Optional[float] = None - v_offset: Optional[float] = None - speed: Optional[float] = None - - -TouchTipOpts.radius.__doc__ = ":py:class:`float`" -TouchTipOpts.v_offset.__doc__ = ":py:class:`float`" -TouchTipOpts.speed.__doc__ = ":py:class:`float`" - - -class AspirateOpts(NamedTuple): - """ - Option to customize aspirate rate. - - This option will be passed to :py:meth:`InstrumentContext.aspirate` - when called during the transfer. - """ - - rate: Optional[float] = 1.0 - - -AspirateOpts.rate.__doc__ = ":py:class:`float`" - - -class DispenseOpts(NamedTuple): - """ - Option to customize dispense rate. - - This option will be passed to :py:meth:`InstrumentContext.dispense` - when called during the transfer. - """ - - rate: Optional[float] = 1.0 - - -DispenseOpts.rate.__doc__ = ":py:class:`float`" - - -class TransferOptions(NamedTuple): - """ - All available options for a transfer, distribute or consolidate function - """ - - transfer: Transfer = Transfer() - pick_up_tip: PickUpTipOpts = PickUpTipOpts() - mix: Mix = Mix() - blow_out: BlowOutOpts = BlowOutOpts() - touch_tip: TouchTipOpts = TouchTipOpts() - aspirate: AspirateOpts = AspirateOpts() - dispense: DispenseOpts = DispenseOpts() - - -FormatDictArgs: TypeAlias = Union[ - PickUpTipOpts, MixOpts, BlowOutOpts, TouchTipOpts, AspirateOpts, DispenseOpts -] - - -TransferOptions.transfer.__doc__ = """ - Options pertaining to behavior of the transfer. - - For instance you can control how frequently to get a new tip using - :py:attr:`.Transfer.new_tip`. For documentation of all transfer options - see :py:class:`.Transfer`. - """ - -TransferOptions.pick_up_tip.__doc__ = """ - Options used when picking up a tip during transfer. - See :py:class:`.PickUpTipOpts`. - """ - -TransferOptions.mix.__doc__ = """ - Options to control mix behavior before aspirate and after dispense. - See :py:class:`.Mix`. - """ - -TransferOptions.blow_out.__doc__ = """ - Option to specify custom location for blow out. See - :py:class:`.BlowOutOpts`. - """ - -TransferOptions.touch_tip.__doc__ = """ - Options to customize touch tip. See - :py:class:`.TouchTipOpts`. - """ - -TransferOptions.aspirate.__doc__ = """ - Option to customize aspirate rate. See - :py:class:`.AspirateOpts`. - """ - -TransferOptions.dispense.__doc__ = """ - Option to customize dispense rate. See - :py:class:`.DispenseOpts`. - """ - - -class TransferPlan: - """Calculate and carry state for an arbitrary transfer - - This class encapsulates the logic around planning an M:N transfer. - - It handles calculations based on pipette channels, tip management, and all - the various little commands that can be involved in a transfer. It can be - iterated to resolve methods to call to execute the plan. - """ - - def __init__( - self, - volume: Union[float, Sequence[float]], - srcs: AdvancedLiquidHandling, - dsts: AdvancedLiquidHandling, - # todo(mm, 2021-03-10): - # Refactor to not need an InstrumentContext, so we can more - # easily test this class's logic on its own. - instr: "InstrumentContext", - max_volume: float, - api_version: APIVersion, - mode: str, - options: Optional[TransferOptions] = None, - ) -> None: - """Build the transfer plan. - - This method initializes the object and does the work of preparing the - transfer plan. Its arguments are as those of - :py:meth:`.InstrumentContext.transfer`. - """ - self._instr = instr - self._api_version = api_version - # Convert sources & dests into proper format - # CASES: - # i. if using multi-channel pipette, - # and the source or target is a row/column of Wells (i.e list of Wells) - # then avoid iterating through its Wells. - # ii. if using single channel pipettes, flatten a multi-dimensional - # list of Wells into a 1 dimensional list of Wells - pipette_configuration_type = NozzleConfigurationType.FULL - normalized_sources: List[Union[Well, types.Location]] - normalized_dests: List[Union[Well, types.Location]] - if self._api_version >= _PARTIAL_TIP_SUPPORT_ADDED: - pipette_configuration_type = ( - self._instr._core.get_nozzle_map().configuration - ) - if ( - self._instr.channels > 1 - and pipette_configuration_type == NozzleConfigurationType.FULL - ): - normalized_sources, normalized_dests = self._multichannel_transfer( - srcs, dsts - ) - else: - if isinstance(srcs, List): - if isinstance(srcs[0], List): - # Source is a List[List[Well]] - normalized_sources = [ - well for well_list in srcs for well in well_list - ] - else: - normalized_sources = srcs - elif isinstance(srcs, Well) or isinstance(srcs, types.Location): - normalized_sources = [srcs] - if isinstance(dsts, List): - if isinstance(dsts[0], List): - # Dest is a List[List[Well]] - normalized_dests = [ - well for well_list in dsts for well in well_list - ] - else: - normalized_dests = dsts - elif isinstance(dsts, Well) or isinstance(dsts, types.Location): - normalized_dests = [dsts] - - total_xfers = max(len(normalized_sources), len(normalized_dests)) - - self._volumes = self._create_volume_list(volume, total_xfers) - self._sources = normalized_sources - self._dests = normalized_dests - self._options = options or TransferOptions() - self._strategy = self._options.transfer - self._tip_opts = self._options.pick_up_tip - self._blow_opts = self._options.blow_out - self._touch_tip_opts = self._options.touch_tip - self._mix_before_opts = self._options.mix.mix_before - self._mix_after_opts = self._options.mix.mix_after - self._max_volume = max_volume - - self._mode = TransferMode[mode.upper()] - - def __iter__(self) -> Iterator[TransferStep]: - if self._strategy.new_tip == types.TransferTipPolicy.ONCE: - yield self._format_dict("pick_up_tip", kwargs=self._tip_opts) - yield from { - TransferMode.CONSOLIDATE: self._plan_consolidate, - TransferMode.DISTRIBUTE: self._plan_distribute, - TransferMode.TRANSFER: self._plan_transfer, - }[self._mode]() - if self._strategy.new_tip == types.TransferTipPolicy.ONCE: - if self._strategy.drop_tip_strategy == DropTipStrategy.RETURN: - yield self._format_dict("return_tip") - else: - yield self._format_dict("drop_tip") - - def _plan_transfer(self) -> Generator[TransferStep, None, None]: - """ - * **Source/ Dest:** Multiple sources to multiple destinations. - Src & dest should be equal length - - * **Volume:** Single volume or List of volumes is acceptable. This list - should be same length as sources/destinations - - * **Behavior with transfer options:** - - - New_tip: can be either NEVER or ONCE or ALWAYS - - Air_gap: if specified, will be performed after every aspirate - - Blow_out: can be performed after each dispense (after mix, before - touch_tip) at the location specified. If there is - liquid present in the tip (as in the case of nonzero - disposal volume), blow_out will be performed at either - user-defined location or (default) trash. - If no liquid is supposed to be present in the tip after - dispense, blow_out will be performed at dispense well - location (if blow out strategy is DEST) - - Touch_tip: can be performed after each aspirate and/or after - each dispense - - Mix: can be performed before aspirate and/or after dispense - if there is no disposal volume (i.e. can be performed - only when the tip is supposed to be empty) - - Considering all options, the sequence of actions is: - *New Tip -> Mix -> Aspirate (with disposal volume) -> Air gap -> - -> Touch tip -> Dispense air gap -> Dispense -> Mix if empty -> - -> Blow out -> Touch tip -> Drop tip* - """ - # reform source target lists - sources, dests = self._extend_source_target_lists(self._sources, self._dests) - self._check_valid_volume_parameters( - disposal_volume=self._strategy.disposal_volume, - air_gap=self._strategy.air_gap, - max_volume=self._instr.max_volume, - ) - plan_iter = self._expand_for_volume_constraints( - self._volumes, - zip(sources, dests), - self._instr.max_volume - - self._strategy.disposal_volume - - self._strategy.air_gap, - ) - for step_vol, (src, dest) in plan_iter: - if self._strategy.new_tip == types.TransferTipPolicy.ALWAYS: - yield self._format_dict("pick_up_tip", kwargs=self._tip_opts) - max_vol = ( - self._max_volume - - self._strategy.disposal_volume - - self._strategy.air_gap - ) - xferred_vol = 0.0 - while xferred_vol < step_vol: - # TODO: account for unequal length sources, dests - # TODO: ensure last transfer is > min_vol - vol = min(max_vol, step_vol - xferred_vol) - yield from self._aspirate_actions(vol, src) - yield from self._dispense_actions(vol=vol, dest=dest, src=src) - xferred_vol += vol - yield from self._new_tip_action() - - @staticmethod - def _extend_source_target_lists( - sources: List[Union[Well, types.Location]], - targets: List[Union[Well, types.Location]], - ) -> Tuple[List[Union[Well, types.Location]], List[Union[Well, types.Location]]]: - """Extend source or target list to match the length of the other""" - if len(sources) < len(targets): - if len(targets) % len(sources) != 0: - raise ValueError("Source and destination lists must be divisible") - sources = [ - source - for source in sources - for i in range(int(len(targets) / len(sources))) - ] - elif len(sources) > len(targets): - if len(sources) % len(targets) != 0: - raise ValueError("Source and destination lists must be divisible") - targets = [ - target - for target in targets - for i in range(int(len(sources) / len(targets))) - ] - return sources, targets - - def _plan_distribute(self) -> Generator[TransferStep, None, None]: - """ - * **Source/ Dest:** One source to many destinations - * **Volume:** Single volume or List of volumes is acceptable. This list - should be same length as destinations - * **Behavior with transfer options:** - - - New_tip: can be either NEVER or ONCE - (ALWAYS will fallback to ONCE) - - Air_gap: if specified, will be performed after every aspirate and - also in-between dispenses (to keep air gap while moving - between wells) - - Blow_out: can be performed at the end of distribute (after mix, - before touch_tip) at the location specified. If there - is liquid present in the tip, blow_out will be - performed at either user-defined location or (default) - trash. If no liquid is supposed to be present in the - tip at the end of distribute, blow_out will be - performed at the last well the liquid was dispensed to - (if strategy is DEST) - - Touch_tip: can be performed after each aspirate and/or after - every dispense - - Mix: can be performed before aspirate and/or after the last - dispense if there is no disposal volume (i.e. can be - performed only when the tip is supposed to be empty) - - Considering all options, the sequence of actions is: - - 1. Going from source to dest1: - *New Tip -> Mix -> Aspirate (with disposal volume) -> Air gap -> - -> Touch tip -> Dispense air gap -> Dispense -> Mix if empty -> - -> Blow out -> Touch tip -> Drop tip* - 2. Going from destn to destn+1: - *.. Dispense air gap -> Dispense -> Touch tip -> Air gap -> - .. Dispense air gap -> ...* - - """ - - self._check_valid_volume_parameters( - disposal_volume=self._strategy.disposal_volume, - air_gap=self._strategy.air_gap, - max_volume=self._instr.max_volume, - ) - - # TODO: decide whether default disposal vol for distribute should be - # pipette min_vol or should we leave it to being 0 by default and - # recommend users to specify a disposal vol when using distribute. - # First method keeps distribute consistent with current behavior while - # the other maintains consistency in default behaviors of all functions - plan_iter = self._expand_for_volume_constraints( - self._volumes, - self._dests, - # todo(mm, 2021-03-09): Is it right for this to be - # _instr_.max_volume? Does/should this take the tip maximum volume - # into account? - self._instr.max_volume - - self._strategy.disposal_volume - - self._strategy.air_gap, - ) - - done = False - current_xfer = next(plan_iter) - if self._strategy.new_tip == types.TransferTipPolicy.ALWAYS: - yield self._format_dict("pick_up_tip", kwargs=self._tip_opts) - while not done: - asp_grouped: List[Tuple[float, Well | types.Location]] = [] - try: - while ( - sum(a[0] for a in asp_grouped) - + self._strategy.disposal_volume - + self._strategy.air_gap - + current_xfer[0] - ) <= self._max_volume: - append_xfer = self._check_volume_not_zero( - self._api_version, current_xfer[0] - ) - if append_xfer: - asp_grouped.append(current_xfer) - current_xfer = next(plan_iter) - except StopIteration: - done = True - if not asp_grouped: - break - - yield from self._aspirate_actions( - sum(a[0] for a in asp_grouped) + self._strategy.disposal_volume, - self._sources[0], - ) - for step in asp_grouped: - - yield from self._dispense_actions( - vol=step[0], - src=self._sources[0], - dest=step[1], - is_disp_next=step is not asp_grouped[-1], - ) - yield from self._new_tip_action() - - Target = TypeVar("Target") - - @staticmethod - def _expand_for_volume_constraints( - volumes: Iterable[float], targets: Iterable[Target], max_volume: float - ) -> Generator[Tuple[float, "Target"], None, None]: - """Split a sequence of proposed transfers if necessary to keep each - transfer under the given max volume. - """ - # A final defense against an infinite loop. - # Raising a proper exception with a helpful message is left to calling code, - # because it has more context about what the user is trying to do. - assert max_volume > 0 - for volume, target in zip(volumes, targets): - while volume > max_volume * 2: - yield max_volume, target - volume -= max_volume - - if volume > max_volume: - volume /= 2 - yield volume, target - yield volume, target - - def _plan_consolidate(self) -> Generator[TransferStep, None, None]: - """ - * **Source/ Dest:** Many sources to one destination - * **Volume:** Single volume or List of volumes is acceptable. This list - should be same length as sources - * **Behavior with transfer options:** - - - New_tip: can be either NEVER or ONCE - (ALWAYS will fallback to ONCE) - - Air_gap: if specified, will be performed after every aspirate - so that the aspirated liquids do not mix inside the tip. - The air gap will be dispensed while dispensing the - liquid into the destination well. - - Blow_out: can be performed after a dispense (after mix, - before touch_tip) at the location specified. If there - is liquid present in the tip (which shouldn't happen - since consolidate doesn't take a disposal vol, yet), - blow_out will be performed at either user-defined - location or (default) trash. - If no liquid is supposed to be present in the tip after - dispense, blow_out will be performed at dispense well - loc (if blow out strategy is DEST) - - Touch_tip: can be performed after each aspirate and/or after - dispense - - Mix: can be performed before the first aspirate and/or after - dispense if there is no disposal volume (i.e. can be - performed only when the tip is supposed to be empty) - - Considering all options, the sequence of actions is: - 1. Going from source to dest1: - *New Tip -> Mix -> Aspirate (with disposal volume?) -> Air gap - -> Touch tip -> Dispense air gap -> Dispense -> Mix if empty -> - -> Blow out -> Touch tip -> Drop tip* - 2. Going from source(n) to source(n+1): - *.. Aspirate -> Air gap -> Touch tip ->.. - .. Aspirate -> .....* - """ - # TODO: verify if _check_valid_volume_parameters should be re-enabled here - # self._check_valid_volume_parameters( - # disposal_volume=self._strategy.disposal_volume, - # air_gap=self._strategy.air_gap, - # max_volume=self._instr.max_volume, - # ) - plan_iter = self._expand_for_volume_constraints( - # todo(mm, 2021-03-09): Is it right to use _instr.max_volume here? - # Why don't we account for tip max volume, disposal volume, or air - # gap? - self._volumes, - self._sources, - self._instr.max_volume, - ) - current_xfer = next(plan_iter) - if self._strategy.new_tip == types.TransferTipPolicy.ALWAYS: - yield self._format_dict("pick_up_tip", kwargs=self._tip_opts) - done = False - while not done: - asp_grouped: List[Tuple[float, Union[Well, types.Location]]] = [] - try: - while ( - sum([a[0] for a in asp_grouped]) - + self._strategy.disposal_volume - + self._strategy.air_gap * len(asp_grouped) - + current_xfer[0] - ) <= self._max_volume: - append_xfer = self._check_volume_not_zero( - self._api_version, current_xfer[0] - ) - if append_xfer: - asp_grouped.append(current_xfer) - current_xfer = next(plan_iter) - except StopIteration: - done = True - if not asp_grouped: - break - # Q: What accounts as disposal volume in a consolidate action? - # yield self._format_dict('aspirate', - # self._strategy.disposal_volume, loc) - for step in asp_grouped: - yield from self._aspirate_actions(step[0], step[1]) - yield from self._dispense_actions( - vol=sum([a[0] + self._strategy.air_gap for a in asp_grouped]) - - self._strategy.air_gap, - src=None, - dest=self._dests[0], - ) - yield from self._new_tip_action() - - def _aspirate_actions( - self, vol: float, loc: Union[Well, types.Location] - ) -> Generator[TransferStep, None, None]: - yield from self._before_aspirate(loc) - yield self._format_dict("aspirate", [vol, loc, self._options.aspirate.rate]) - yield from self._after_aspirate() - - def _dispense_actions( - self, - vol: float, - dest: Union[Well, types.Location], - src: Optional[Union[Well, types.Location]] = None, - is_disp_next: bool = False, - ) -> Generator[TransferStep, None, None]: - if self._strategy.air_gap: - vol += self._strategy.air_gap - yield self._format_dict("dispense", [vol, dest, self._options.dispense.rate]) - yield from self._after_dispense(dest=dest, src=src, is_disp_next=is_disp_next) - - def _before_aspirate( - self, loc: Union[Well, types.Location] - ) -> Generator[TransferStep, None, None]: - if ( - self._strategy.mix_strategy == MixStrategy.BEFORE - or self._strategy.mix_strategy == MixStrategy.BOTH - ): - if self._instr.current_volume == 0: - mix_before_opts = self._mix_before_opts._asdict() - mix_before_opts["location"] = loc - yield self._format_dict("mix", kwargs=mix_before_opts) - - def _after_aspirate(self) -> Generator[TransferStep, None, None]: - if self._strategy.air_gap: - yield self._format_dict("air_gap", [self._strategy.air_gap]) - if self._strategy.touch_tip_strategy == TouchTipStrategy.ALWAYS: - yield self._format_dict("touch_tip", kwargs=self._touch_tip_opts) - - def _after_dispense_trash(self) -> Generator[TransferStep, None, None]: - if isinstance(self._instr.trash_container, Labware): - yield self._format_dict( - "blow_out", [self._instr.trash_container.wells()[0]] - ) - else: - yield self._format_dict("blow_out", [self._instr.trash_container]) - - def _after_dispense_helper(self) -> Generator[TransferStep, None, None]: - # Used by distribute - if self._strategy.air_gap: - yield self._format_dict("air_gap", [self._strategy.air_gap]) - if self._strategy.touch_tip_strategy == TouchTipStrategy.ALWAYS: - yield self._format_dict("touch_tip", kwargs=self._touch_tip_opts) - - def _after_dispense( - self, - dest: Union[Well, types.Location], - src: Optional[Union[types.Location, Well]], - is_disp_next: bool = False, - ) -> Generator[TransferStep, None, None]: - # This sequence of actions is subject to change - if not is_disp_next: - # If the next command is an aspirate, we are switching - # between aspirate and dispense. - if self._instr.current_volume == 0: - # If we're empty, then this is when after mixes come into play - if ( - self._strategy.mix_strategy == MixStrategy.AFTER - or self._strategy.mix_strategy == MixStrategy.BOTH - ): - mix_after_opts = self._mix_after_opts._asdict() - mix_after_opts["location"] = dest - yield self._format_dict("mix", kwargs=mix_after_opts) - if self._strategy.touch_tip_strategy == TouchTipStrategy.ALWAYS: - yield self._format_dict("touch_tip", kwargs=self._touch_tip_opts) - - if self._strategy.blow_out_strategy == BlowOutStrategy.SOURCE: - yield self._format_dict("blow_out", [src]) - elif self._strategy.blow_out_strategy == BlowOutStrategy.DEST: - yield self._format_dict("blow_out", [dest]) - elif self._strategy.blow_out_strategy == BlowOutStrategy.CUSTOM_LOCATION: - yield self._format_dict("blow_out", kwargs=self._blow_opts) - elif ( - self._strategy.blow_out_strategy == BlowOutStrategy.TRASH - or self._strategy.disposal_volume - ): - yield from self._after_dispense_trash() - else: - yield from self._after_dispense_helper() - - def _new_tip_action(self) -> Generator[TransferStep, None, None]: - if self._strategy.new_tip == types.TransferTipPolicy.ALWAYS: - if self._strategy.drop_tip_strategy == DropTipStrategy.RETURN: - yield self._format_dict("return_tip") - else: - yield self._format_dict("drop_tip") - - def _format_dict( - self, - method: str, - args: Optional[List[Any]] = None, - kwargs: Optional[Union[Dict[Any, Any], FormatDictArgs]] = None, - ) -> TransferStep: - if kwargs: - if isinstance(kwargs, Dict): - params = {key: val for key, val in kwargs.items() if val} - else: - params = {key: val for key, val in kwargs._asdict().items() if val} - else: - params = {} - if not args: - args = [] - return {"method": method, "args": args, "kwargs": params} - - def _create_volume_list( - self, volume: Union[Union[float, int], Sequence[float]], total_xfers: int - ) -> List[float]: - if isinstance(volume, (float, int)): - return [float(volume)] * total_xfers - elif isinstance(volume, tuple): - return self._create_volume_gradient( - volume[0], volume[-1], total_xfers, self._strategy.gradient_function - ) - else: - if not isinstance(volume, List): - raise TypeError( - "Volume expected as a number or List or" - " tuple but got {}".format(volume) - ) - elif not len(volume) == total_xfers: - raise RuntimeError( - "List of volumes should be equal to number " "of transfers" - ) - return volume - - def _create_volume_gradient( - self, - min_v: float, - max_v: float, - total: int, - gradient: Optional[Callable[[float], float]] = None, - ) -> List[float]: - - diff_vol = max_v - min_v - - def _map_volume(i: int) -> float: - nonlocal diff_vol, total - rel_x = i / (total - 1) - rel_y = gradient(rel_x) if gradient else rel_x - return (rel_y * diff_vol) + min_v - - return [_map_volume(i) for i in range(total)] - - def _check_valid_volume_parameters( - self, disposal_volume: float, air_gap: float, max_volume: float - ) -> None: - if air_gap >= max_volume: - raise ValueError( - "The air gap must be less than the maximum volume of the pipette" - ) - elif disposal_volume >= max_volume: - raise ValueError( - "The disposal volume must be less than the maximum volume of the pipette" - ) - elif disposal_volume + air_gap >= max_volume: - raise ValueError( - "The sum of the air gap and disposal volume must be less than the maximum volume of the pipette" - ) - - def _check_valid_well_list( - self, well_list: List[Any], id: str, old_well_list: List[Any] - ) -> None: - if self._api_version >= APIVersion(2, 2) and len(well_list) < 1: - raise RuntimeError( - f"Invalid {id} for multichannel transfer: {old_well_list}" - ) - - @staticmethod - def _check_volume_not_zero(api_version: APIVersion, volume: float) -> bool: - # We should only be adding volumes to transfer plans if it is - # greater than zero to prevent extraneous robot movements. - if api_version < APIVersion(2, 8): - return True - elif volume > 0: - return True - return False - - def _multichannel_transfer( - self, s: AdvancedLiquidHandling, d: AdvancedLiquidHandling - ) -> Tuple[List[Union[Well, types.Location]], List[Union[Well, types.Location]]]: - # TODO: add a check for container being multi-channel compatible? - # Helper function for multi-channel use-case - assert ( - isinstance(s, Well) - or isinstance(s, types.Location) - or (isinstance(s, List) and isinstance(s[0], Well)) - or (isinstance(s, List) and isinstance(s[0], List)) - or (isinstance(s, List) and isinstance(s[0], types.Location)) - ), "Source should be a Well or List[Well] but is {}".format(s) - assert ( - isinstance(d, Well) - or isinstance(d, types.Location) - or (isinstance(d, List) and isinstance(d[0], Well)) - or (isinstance(d, List) and isinstance(d[0], List)) - or (isinstance(d, List) and isinstance(d[0], types.Location)) - ), "Target should be a Well or List[Well] but is {}".format(d) - - # TODO: Account for cases where a src/dest list has a non-first-row - # well (eg, 'B1') and would expect the robot/pipette to - # understand that it is referring to the whole first column - if isinstance(s, List) and isinstance(s[0], List): - # s is a List[List]]; flatten to 1D list - s = [well for list_elem in s for well in list_elem] - elif isinstance(s, Well) or isinstance(s, types.Location): - s = [s] - new_src = [] - for well in s: - if self._is_valid_row(well): - new_src.append(well) - self._check_valid_well_list(new_src, "source", s) - - if isinstance(d, List) and isinstance(d[0], List): - # s is a List[List]]; flatten to 1D list - d = [well for list_elem in d for well in list_elem] - elif isinstance(d, Well) or isinstance(d, types.Location): - d = [d] - new_dst = [] - for well in d: - if self._is_valid_row(well): - new_dst.append(well) - self._check_valid_well_list(new_dst, "target", d) - return new_src, new_dst - - def _is_valid_row(self, well: Union[Well, types.Location]) -> bool: - if isinstance(well, types.Location): - test_well = well.labware.as_well() - else: - test_well = well - - if self._api_version < APIVersion(2, 2): - return test_well in test_well.parent.rows()[0] - else: - # Allow the first 2 rows to be accessible to 384-well plates; - # otherwise, only the first row is accessible - if test_well.parent.parameters["format"] == "384Standard": - valid_wells = [ - well for row in test_well.parent.rows()[:2] for well in row - ] - return test_well in valid_wells - else: - return test_well in test_well.parent.rows()[0] diff --git a/api/src/opentrons/protocols/advanced_control/transfers/__init__.py b/api/src/opentrons/protocols/advanced_control/transfers/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/api/src/opentrons/protocols/advanced_control/transfers/common.py b/api/src/opentrons/protocols/advanced_control/transfers/common.py new file mode 100644 index 00000000000..c40a55beacd --- /dev/null +++ b/api/src/opentrons/protocols/advanced_control/transfers/common.py @@ -0,0 +1,56 @@ +"""Common functions between v1 transfer and liquid-class-based transfer.""" +import enum +from typing import Iterable, Generator, Tuple, TypeVar, Literal + + +class TransferTipPolicyV2(enum.Enum): + ONCE = "once" + NEVER = "never" + ALWAYS = "always" + PER_SOURCE = "per source" + + +TransferTipPolicyV2Type = Literal["once", "always", "per source", "never"] + +Target = TypeVar("Target") + + +def check_valid_volume_parameters( + disposal_volume: float, air_gap: float, max_volume: float +) -> None: + if air_gap >= max_volume: + raise ValueError( + "The air gap must be less than the maximum volume of the pipette" + ) + elif disposal_volume >= max_volume: + raise ValueError( + "The disposal volume must be less than the maximum volume of the pipette" + ) + elif disposal_volume + air_gap >= max_volume: + raise ValueError( + "The sum of the air gap and disposal volume must be less than" + " the maximum volume of the pipette" + ) + + +def expand_for_volume_constraints( + volumes: Iterable[float], + targets: Iterable[Target], + max_volume: float, +) -> Generator[Tuple[float, "Target"], None, None]: + """Split a sequence of proposed transfers if necessary to keep each + transfer under the given max volume. + """ + # A final defense against an infinite loop. + # Raising a proper exception with a helpful message is left to calling code, + # because it has more context about what the user is trying to do. + assert max_volume > 0 + for volume, target in zip(volumes, targets): + while volume > max_volume * 2: + yield max_volume, target + volume -= max_volume + + if volume > max_volume: + volume /= 2 + yield volume, target + yield volume, target diff --git a/api/src/opentrons/protocols/advanced_control/transfers/transfer.py b/api/src/opentrons/protocols/advanced_control/transfers/transfer.py new file mode 100644 index 00000000000..3f5f90ab550 --- /dev/null +++ b/api/src/opentrons/protocols/advanced_control/transfers/transfer.py @@ -0,0 +1,972 @@ +import enum +from typing import ( + Any, + Dict, + List, + Optional, + Union, + NamedTuple, + Callable, + Generator, + Iterator, + Sequence, + Tuple, + TypedDict, + TypeAlias, + TYPE_CHECKING, +) +from opentrons.protocol_api.labware import Labware, Well +from opentrons import types +from opentrons.protocols.api_support.types import APIVersion + +from . import common as tx_commons +from ..common import Mix, MixOpts, MixStrategy + +AdvancedLiquidHandling = Union[ + Well, + types.Location, + Sequence[Union[Well, types.Location]], + Sequence[Sequence[Well]], +] + + +class TransferStep(TypedDict): + method: str + args: Optional[List[Any]] + kwargs: Optional[Dict[Any, Any]] + + +if TYPE_CHECKING: + from opentrons.protocol_api import InstrumentContext + +_PARTIAL_TIP_SUPPORT_ADDED = APIVersion(2, 18) +"""The version after which partial tip support and nozzle maps were made available.""" + + +class DropTipStrategy(enum.Enum): + TRASH = enum.auto() + RETURN = enum.auto() + + +class TouchTipStrategy(enum.Enum): + NEVER = enum.auto() + ALWAYS = enum.auto() + + +class BlowOutStrategy(enum.Enum): + NONE = enum.auto() + TRASH = enum.auto() + DEST = enum.auto() + SOURCE = enum.auto() + CUSTOM_LOCATION = enum.auto() + + +class TransferMode(enum.Enum): + DISTRIBUTE = enum.auto() + CONSOLIDATE = enum.auto() + TRANSFER = enum.auto() + + +class Transfer(NamedTuple): + """ + Options pertaining to behavior of the transfer. + + """ + + new_tip: types.TransferTipPolicy = types.TransferTipPolicy.ONCE + air_gap: float = 0 + carryover: bool = True + gradient_function: Optional[Callable[[float], float]] = None + disposal_volume: float = 0 + mix_strategy: MixStrategy = MixStrategy.NEVER + drop_tip_strategy: DropTipStrategy = DropTipStrategy.TRASH + blow_out_strategy: BlowOutStrategy = BlowOutStrategy.NONE + touch_tip_strategy: TouchTipStrategy = TouchTipStrategy.NEVER + + +Transfer.new_tip.__doc__ = """ + Control when or if to pick up tip during a transfer + + :py:attr:`types.TransferTipPolicy.ALWAYS` + Drop and pick up a new tip after each dispense. + + :py:attr:`types.TransferTipPolicy.ONCE` + Pick up tip at the beginning of the transfer and use it + throughout the transfer. This would speed up the transfer. + + :py:attr:`types.TransferTipPolicy.NEVER` + Do not ever pick up or drop tip. The protocol should explicitly + pick up a tip before transfer and drop it afterwards. + + To customize where to drop tip, see :py:attr:`.drop_tip_strategy`. + To customize the behavior of pickup tip, see + :py:attr:`.TransferOptions.pick_up_tip`. + """ + +Transfer.air_gap.__doc__ = """ + Controls the volume (in uL) of air gap aspirated when moving to + dispense. + + Adding an air gap would slow down a transfer since less liquid will + now fit in the pipette but it prevents the loss of liquid while + moving between wells. + """ + +Transfer.carryover.__doc__ = """ + Controls whether volumes larger than pipette's max volume will be + split into smaller volumes. + """ + +Transfer.gradient_function.__doc__ = """ + Specify a nonlinear gradient for volumes. + + This should be a function that takes a single float between 0 and 1 + and returns a single float between 0 and 1. This function is used + to determine the path the transfer takes between the volume + gradient minimum and maximum if the transfer volume is specified as + a gradient. For instance, specifying the function as + + .. code-block:: python + + def gradient(a): + if a > 0.5: + return 1.0 + else: + return 0.0 + + would transfer the minimum volume of the gradient to the first half + of the target wells, and the maximum to the other half. + """ + +Transfer.disposal_volume.__doc__ = """ + The amount of liquid (in uL) to aspirate as a buffer. + + The remaining buffer will be blown out into the location specified + by :py:attr:`.blow_out_strategy`. + + This is useful to avoid under-pipetting but can waste reagent and + slow down transfer. + """ + +Transfer.mix_strategy.__doc__ = """ + If and when to mix during a transfer. + + :py:attr:`MixStrategy.NEVER` + Do not ever perform a mix during the transfer. + + :py:attr:`MixStrategy.BEFORE` + Mix before each aspirate. + + :py:attr:`MixStrategy.AFTER` + Mix after each dispense. + + :py:attr:`MixStrategy.BOTH` + Mix before each aspirate and after each dispense. + + To customize the mix behavior, see :py:attr:`.TransferOptions.mix` + """ + +Transfer.drop_tip_strategy.__doc__ = """ + Specifies the location to drop tip into. + + :py:attr:`DropTipStrategy.TRASH` + Drop the tip into the trash container. + + :py:attr:`DropTipStrategy.RETURN` + Return the tip to tiprack. + """ + +Transfer.blow_out_strategy.__doc__ = """ + Specifies the location to blow out the liquid in the pipette to. + + :py:attr:`BlowOutStrategy.TRASH` + Blow out to trash container. + + :py:attr:`BlowOutStrategy.SOURCE` + Blow out into the source well in order to dispense any leftover + liquid. + + :py:attr:`BlowOutStrategy.DEST` + Blow out into the destination well in order to dispense any leftover + liquid. + + :py:attr:`BlowOutStrategy.CUSTOM_LOCATION` + If using any other location to blow out to. Specify the location in + :py:attr:`.TransferOptions.blow_out`. + """ + +Transfer.touch_tip_strategy.__doc__ = """ + Controls whether to touch tip during the transfer + + This helps in getting rid of any droplets clinging to the pipette + tip at the cost of slowing down the transfer. + + :py:attr:`TouchTipStrategy.NEVER` + Do not touch tip ever during the transfer. + + :py:attr:`TouchTipStrategy.ALWAYS` + Touch tip after each aspirate. + + To customize the behavior of touch tips, see + :py:attr:`.TransferOptions.touch_tip`. + """ + + +class PickUpTipOpts(NamedTuple): + """ + Options to customize :py:attr:`.Transfer.new_tip`. + + These options will be passed to + :py:meth:`InstrumentContext.pick_up_tip` when it is called during + the transfer. + """ + + location: Optional[types.Location] = None + presses: Optional[int] = None + increment: Optional[int] = None + + +PickUpTipOpts.location.__doc__ = ":py:class:`types.Location`" +PickUpTipOpts.presses.__doc__ = ":py:class:`int`" +PickUpTipOpts.increment.__doc__ = ":py:class:`int`" + + +Mix.mix_before.__doc__ = """ + Options applied to mix before aspirate. + See :py:class:`.Mix.MixOpts`. + """ + +Mix.mix_after.__doc__ = """ + Options applied to mix after dispense. See :py:class:`.Mix.MixOpts`. + """ + + +class BlowOutOpts(NamedTuple): + """ + Location where to blow out instead of the trash. + + This location will be passed to :py:meth:`InstrumentContext.blow_out` + when called during the transfer + """ + + location: Optional[Union[types.Location, Well]] = None + + +BlowOutOpts.location.__doc__ = ":py:class:`types.Location`" + + +class TouchTipOpts(NamedTuple): + """ + Options to customize touch tip. + + These options will be passed to + :py:meth:`InstrumentContext.touch_tip` when called during the + transfer. + """ + + radius: Optional[float] = None + v_offset: Optional[float] = None + speed: Optional[float] = None + + +TouchTipOpts.radius.__doc__ = ":py:class:`float`" +TouchTipOpts.v_offset.__doc__ = ":py:class:`float`" +TouchTipOpts.speed.__doc__ = ":py:class:`float`" + + +class AspirateOpts(NamedTuple): + """ + Option to customize aspirate rate. + + This option will be passed to :py:meth:`InstrumentContext.aspirate` + when called during the transfer. + """ + + rate: Optional[float] = 1.0 + + +AspirateOpts.rate.__doc__ = ":py:class:`float`" + + +class DispenseOpts(NamedTuple): + """ + Option to customize dispense rate. + + This option will be passed to :py:meth:`InstrumentContext.dispense` + when called during the transfer. + """ + + rate: Optional[float] = 1.0 + + +DispenseOpts.rate.__doc__ = ":py:class:`float`" + + +class TransferOptions(NamedTuple): + """ + All available options for a transfer, distribute or consolidate function + """ + + transfer: Transfer = Transfer() + pick_up_tip: PickUpTipOpts = PickUpTipOpts() + mix: Mix = Mix() + blow_out: BlowOutOpts = BlowOutOpts() + touch_tip: TouchTipOpts = TouchTipOpts() + aspirate: AspirateOpts = AspirateOpts() + dispense: DispenseOpts = DispenseOpts() + + +FormatDictArgs: TypeAlias = Union[ + PickUpTipOpts, MixOpts, BlowOutOpts, TouchTipOpts, AspirateOpts, DispenseOpts +] + + +TransferOptions.transfer.__doc__ = """ + Options pertaining to behavior of the transfer. + + For instance you can control how frequently to get a new tip using + :py:attr:`.Transfer.new_tip`. For documentation of all transfer options + see :py:class:`.Transfer`. + """ + +TransferOptions.pick_up_tip.__doc__ = """ + Options used when picking up a tip during transfer. + See :py:class:`.PickUpTipOpts`. + """ + +TransferOptions.mix.__doc__ = """ + Options to control mix behavior before aspirate and after dispense. + See :py:class:`.Mix`. + """ + +TransferOptions.blow_out.__doc__ = """ + Option to specify custom location for blow out. See + :py:class:`.BlowOutOpts`. + """ + +TransferOptions.touch_tip.__doc__ = """ + Options to customize touch tip. See + :py:class:`.TouchTipOpts`. + """ + +TransferOptions.aspirate.__doc__ = """ + Option to customize aspirate rate. See + :py:class:`.AspirateOpts`. + """ + +TransferOptions.dispense.__doc__ = """ + Option to customize dispense rate. See + :py:class:`.DispenseOpts`. + """ + + +class TransferPlan: + """Calculate and carry state for an arbitrary transfer + + This class encapsulates the logic around planning an M:N transfer. + + It handles calculations based on pipette channels, tip management, and all + the various little commands that can be involved in a transfer. It can be + iterated to resolve methods to call to execute the plan. + """ + + def __init__( + self, + volume: Union[float, Sequence[float]], + srcs: AdvancedLiquidHandling, + dsts: AdvancedLiquidHandling, + # todo(mm, 2021-03-10): + # Refactor to not need an InstrumentContext, so we can more + # easily test this class's logic on its own. + instr: "InstrumentContext", + max_volume: float, + api_version: APIVersion, + mode: str, + options: Optional[TransferOptions] = None, + ) -> None: + """Build the transfer plan. + + This method initializes the object and does the work of preparing the + transfer plan. Its arguments are as those of + :py:meth:`.InstrumentContext.transfer`. + """ + self._instr = instr + self._api_version = api_version + # Convert sources & dests into proper format + # CASES: + # i. if using multi-channel pipette, + # and the source or target is a row/column of Wells (i.e list of Wells) + # then avoid iterating through its Wells. + # ii. if using single channel pipettes, flatten a multi-dimensional + # list of Wells into a 1 dimensional list of Wells + pipette_configuration_type = types.NozzleConfigurationType.FULL + normalized_sources: List[Union[Well, types.Location]] + normalized_dests: List[Union[Well, types.Location]] + if self._api_version >= _PARTIAL_TIP_SUPPORT_ADDED: + pipette_configuration_type = ( + self._instr._core.get_nozzle_map().configuration + ) + if ( + self._instr.channels > 1 + and pipette_configuration_type == types.NozzleConfigurationType.FULL + ): + normalized_sources, normalized_dests = self._multichannel_transfer( + srcs, dsts + ) + else: + if isinstance(srcs, List): + if isinstance(srcs[0], List): + # Source is a List[List[Well]] + normalized_sources = [ + well for well_list in srcs for well in well_list + ] + else: + normalized_sources = srcs + elif isinstance(srcs, Well) or isinstance(srcs, types.Location): + normalized_sources = [srcs] + if isinstance(dsts, List): + if isinstance(dsts[0], List): + # Dest is a List[List[Well]] + normalized_dests = [ + well for well_list in dsts for well in well_list + ] + else: + normalized_dests = dsts + elif isinstance(dsts, Well) or isinstance(dsts, types.Location): + normalized_dests = [dsts] + + total_xfers = max(len(normalized_sources), len(normalized_dests)) + + self._volumes = self._create_volume_list(volume, total_xfers) + self._sources = normalized_sources + self._dests = normalized_dests + self._options = options or TransferOptions() + self._strategy = self._options.transfer + self._tip_opts = self._options.pick_up_tip + self._blow_opts = self._options.blow_out + self._touch_tip_opts = self._options.touch_tip + self._mix_before_opts = self._options.mix.mix_before + self._mix_after_opts = self._options.mix.mix_after + self._max_volume = max_volume + + self._mode = TransferMode[mode.upper()] + + def __iter__(self) -> Iterator[TransferStep]: + if self._strategy.new_tip == types.TransferTipPolicy.ONCE: + yield self._format_dict("pick_up_tip", kwargs=self._tip_opts) + yield from { + TransferMode.CONSOLIDATE: self._plan_consolidate, + TransferMode.DISTRIBUTE: self._plan_distribute, + TransferMode.TRANSFER: self._plan_transfer, + }[self._mode]() + if self._strategy.new_tip == types.TransferTipPolicy.ONCE: + if self._strategy.drop_tip_strategy == DropTipStrategy.RETURN: + yield self._format_dict("return_tip") + else: + yield self._format_dict("drop_tip") + + def _plan_transfer(self) -> Generator[TransferStep, None, None]: + """ + * **Source/ Dest:** Multiple sources to multiple destinations. + Src & dest should be equal length + + * **Volume:** Single volume or List of volumes is acceptable. This list + should be same length as sources/destinations + + * **Behavior with transfer options:** + + - New_tip: can be either NEVER or ONCE or ALWAYS + - Air_gap: if specified, will be performed after every aspirate + - Blow_out: can be performed after each dispense (after mix, before + touch_tip) at the location specified. If there is + liquid present in the tip (as in the case of nonzero + disposal volume), blow_out will be performed at either + user-defined location or (default) trash. + If no liquid is supposed to be present in the tip after + dispense, blow_out will be performed at dispense well + location (if blow out strategy is DEST) + - Touch_tip: can be performed after each aspirate and/or after + each dispense + - Mix: can be performed before aspirate and/or after dispense + if there is no disposal volume (i.e. can be performed + only when the tip is supposed to be empty) + + Considering all options, the sequence of actions is: + *New Tip -> Mix -> Aspirate (with disposal volume) -> Air gap -> + -> Touch tip -> Dispense air gap -> Dispense -> Mix if empty -> + -> Blow out -> Touch tip -> Drop tip* + """ + # reform source target lists + sources, dests = self._extend_source_target_lists(self._sources, self._dests) + tx_commons.check_valid_volume_parameters( + disposal_volume=self._strategy.disposal_volume, + air_gap=self._strategy.air_gap, + max_volume=self._instr.max_volume, + ) + plan_iter = tx_commons.expand_for_volume_constraints( + self._volumes, + zip(sources, dests), + self._instr.max_volume + - self._strategy.disposal_volume + - self._strategy.air_gap, + ) + for step_vol, (src, dest) in plan_iter: + if self._strategy.new_tip == types.TransferTipPolicy.ALWAYS: + yield self._format_dict("pick_up_tip", kwargs=self._tip_opts) + max_vol = ( + self._max_volume + - self._strategy.disposal_volume + - self._strategy.air_gap + ) + xferred_vol = 0.0 + while xferred_vol < step_vol: + # TODO: account for unequal length sources, dests + # TODO: ensure last transfer is > min_vol + vol = min(max_vol, step_vol - xferred_vol) + yield from self._aspirate_actions(vol, src) + yield from self._dispense_actions(vol=vol, dest=dest, src=src) + xferred_vol += vol + yield from self._new_tip_action() + + @staticmethod + def _extend_source_target_lists( + sources: List[Union[Well, types.Location]], + targets: List[Union[Well, types.Location]], + ) -> Tuple[List[Union[Well, types.Location]], List[Union[Well, types.Location]]]: + """Extend source or target list to match the length of the other""" + if len(sources) < len(targets): + if len(targets) % len(sources) != 0: + raise ValueError("Source and destination lists must be divisible") + sources = [ + source + for source in sources + for i in range(int(len(targets) / len(sources))) + ] + elif len(sources) > len(targets): + if len(sources) % len(targets) != 0: + raise ValueError("Source and destination lists must be divisible") + targets = [ + target + for target in targets + for i in range(int(len(sources) / len(targets))) + ] + return sources, targets + + def _plan_distribute(self) -> Generator[TransferStep, None, None]: + """ + * **Source/ Dest:** One source to many destinations + * **Volume:** Single volume or List of volumes is acceptable. This list + should be same length as destinations + * **Behavior with transfer options:** + + - New_tip: can be either NEVER or ONCE + (ALWAYS will fallback to ONCE) + - Air_gap: if specified, will be performed after every aspirate and + also in-between dispenses (to keep air gap while moving + between wells) + - Blow_out: can be performed at the end of distribute (after mix, + before touch_tip) at the location specified. If there + is liquid present in the tip, blow_out will be + performed at either user-defined location or (default) + trash. If no liquid is supposed to be present in the + tip at the end of distribute, blow_out will be + performed at the last well the liquid was dispensed to + (if strategy is DEST) + - Touch_tip: can be performed after each aspirate and/or after + every dispense + - Mix: can be performed before aspirate and/or after the last + dispense if there is no disposal volume (i.e. can be + performed only when the tip is supposed to be empty) + + Considering all options, the sequence of actions is: + + 1. Going from source to dest1: + *New Tip -> Mix -> Aspirate (with disposal volume) -> Air gap -> + -> Touch tip -> Dispense air gap -> Dispense -> Mix if empty -> + -> Blow out -> Touch tip -> Drop tip* + 2. Going from destn to destn+1: + *.. Dispense air gap -> Dispense -> Touch tip -> Air gap -> + .. Dispense air gap -> ...* + + """ + + tx_commons.check_valid_volume_parameters( + disposal_volume=self._strategy.disposal_volume, + air_gap=self._strategy.air_gap, + max_volume=self._instr.max_volume, + ) + + # TODO: decide whether default disposal vol for distribute should be + # pipette min_vol or should we leave it to being 0 by default and + # recommend users to specify a disposal vol when using distribute. + # First method keeps distribute consistent with current behavior while + # the other maintains consistency in default behaviors of all functions + plan_iter = tx_commons.expand_for_volume_constraints( + self._volumes, + self._dests, + # todo(mm, 2021-03-09): Is it right for this to be + # _instr_.max_volume? Does/should this take the tip maximum volume + # into account? + self._instr.max_volume + - self._strategy.disposal_volume + - self._strategy.air_gap, + ) + + done = False + current_xfer = next(plan_iter) + if self._strategy.new_tip == types.TransferTipPolicy.ALWAYS: + yield self._format_dict("pick_up_tip", kwargs=self._tip_opts) + while not done: + asp_grouped: List[Tuple[float, Well | types.Location]] = [] + try: + while ( + sum(a[0] for a in asp_grouped) + + self._strategy.disposal_volume + + self._strategy.air_gap + + current_xfer[0] + ) <= self._max_volume: + append_xfer = self._check_volume_not_zero( + self._api_version, current_xfer[0] + ) + if append_xfer: + asp_grouped.append(current_xfer) + current_xfer = next(plan_iter) + except StopIteration: + done = True + if not asp_grouped: + break + + yield from self._aspirate_actions( + sum(a[0] for a in asp_grouped) + self._strategy.disposal_volume, + self._sources[0], + ) + for step in asp_grouped: + + yield from self._dispense_actions( + vol=step[0], + src=self._sources[0], + dest=step[1], + is_disp_next=step is not asp_grouped[-1], + ) + yield from self._new_tip_action() + + def _plan_consolidate(self) -> Generator[TransferStep, None, None]: + """ + * **Source/ Dest:** Many sources to one destination + * **Volume:** Single volume or List of volumes is acceptable. This list + should be same length as sources + * **Behavior with transfer options:** + + - New_tip: can be either NEVER or ONCE + (ALWAYS will fallback to ONCE) + - Air_gap: if specified, will be performed after every aspirate + so that the aspirated liquids do not mix inside the tip. + The air gap will be dispensed while dispensing the + liquid into the destination well. + - Blow_out: can be performed after a dispense (after mix, + before touch_tip) at the location specified. If there + is liquid present in the tip (which shouldn't happen + since consolidate doesn't take a disposal vol, yet), + blow_out will be performed at either user-defined + location or (default) trash. + If no liquid is supposed to be present in the tip after + dispense, blow_out will be performed at dispense well + loc (if blow out strategy is DEST) + - Touch_tip: can be performed after each aspirate and/or after + dispense + - Mix: can be performed before the first aspirate and/or after + dispense if there is no disposal volume (i.e. can be + performed only when the tip is supposed to be empty) + + Considering all options, the sequence of actions is: + 1. Going from source to dest1: + *New Tip -> Mix -> Aspirate (with disposal volume?) -> Air gap + -> Touch tip -> Dispense air gap -> Dispense -> Mix if empty -> + -> Blow out -> Touch tip -> Drop tip* + 2. Going from source(n) to source(n+1): + *.. Aspirate -> Air gap -> Touch tip ->.. + .. Aspirate -> .....* + """ + # TODO: verify if _check_valid_volume_parameters should be re-enabled here + # self._check_valid_volume_parameters( + # disposal_volume=self._strategy.disposal_volume, + # air_gap=self._strategy.air_gap, + # max_volume=self._instr.max_volume, + # ) + plan_iter = tx_commons.expand_for_volume_constraints( + # todo(mm, 2021-03-09): Is it right to use _instr.max_volume here? + # Why don't we account for tip max volume, disposal volume, or air + # gap? + self._volumes, + self._sources, + self._instr.max_volume, + ) + current_xfer = next(plan_iter) + if self._strategy.new_tip == types.TransferTipPolicy.ALWAYS: + yield self._format_dict("pick_up_tip", kwargs=self._tip_opts) + done = False + while not done: + asp_grouped: List[Tuple[float, Union[Well, types.Location]]] = [] + try: + while ( + sum([a[0] for a in asp_grouped]) + + self._strategy.disposal_volume + + self._strategy.air_gap * len(asp_grouped) + + current_xfer[0] + ) <= self._max_volume: + append_xfer = self._check_volume_not_zero( + self._api_version, current_xfer[0] + ) + if append_xfer: + asp_grouped.append(current_xfer) + current_xfer = next(plan_iter) + except StopIteration: + done = True + if not asp_grouped: + break + # Q: What accounts as disposal volume in a consolidate action? + # yield self._format_dict('aspirate', + # self._strategy.disposal_volume, loc) + for step in asp_grouped: + yield from self._aspirate_actions(step[0], step[1]) + yield from self._dispense_actions( + vol=sum([a[0] + self._strategy.air_gap for a in asp_grouped]) + - self._strategy.air_gap, + src=None, + dest=self._dests[0], + ) + yield from self._new_tip_action() + + def _aspirate_actions( + self, vol: float, loc: Union[Well, types.Location] + ) -> Generator[TransferStep, None, None]: + yield from self._before_aspirate(loc) + yield self._format_dict("aspirate", [vol, loc, self._options.aspirate.rate]) + yield from self._after_aspirate() + + def _dispense_actions( + self, + vol: float, + dest: Union[Well, types.Location], + src: Optional[Union[Well, types.Location]] = None, + is_disp_next: bool = False, + ) -> Generator[TransferStep, None, None]: + if self._strategy.air_gap: + vol += self._strategy.air_gap + yield self._format_dict("dispense", [vol, dest, self._options.dispense.rate]) + yield from self._after_dispense(dest=dest, src=src, is_disp_next=is_disp_next) + + def _before_aspirate( + self, loc: Union[Well, types.Location] + ) -> Generator[TransferStep, None, None]: + if ( + self._strategy.mix_strategy == MixStrategy.BEFORE + or self._strategy.mix_strategy == MixStrategy.BOTH + ): + if self._instr.current_volume == 0: + mix_before_opts = self._mix_before_opts._asdict() + mix_before_opts["location"] = loc + yield self._format_dict("mix", kwargs=mix_before_opts) + + def _after_aspirate(self) -> Generator[TransferStep, None, None]: + if self._strategy.air_gap: + yield self._format_dict("air_gap", [self._strategy.air_gap]) + if self._strategy.touch_tip_strategy == TouchTipStrategy.ALWAYS: + yield self._format_dict("touch_tip", kwargs=self._touch_tip_opts) + + def _after_dispense_trash(self) -> Generator[TransferStep, None, None]: + if isinstance(self._instr.trash_container, Labware): + yield self._format_dict( + "blow_out", [self._instr.trash_container.wells()[0]] + ) + else: + yield self._format_dict("blow_out", [self._instr.trash_container]) + + def _after_dispense_helper(self) -> Generator[TransferStep, None, None]: + # Used by distribute + if self._strategy.air_gap: + yield self._format_dict("air_gap", [self._strategy.air_gap]) + if self._strategy.touch_tip_strategy == TouchTipStrategy.ALWAYS: + yield self._format_dict("touch_tip", kwargs=self._touch_tip_opts) + + def _after_dispense( + self, + dest: Union[Well, types.Location], + src: Optional[Union[types.Location, Well]], + is_disp_next: bool = False, + ) -> Generator[TransferStep, None, None]: + # This sequence of actions is subject to change + if not is_disp_next: + # If the next command is an aspirate, we are switching + # between aspirate and dispense. + if self._instr.current_volume == 0: + # If we're empty, then this is when after mixes come into play + if ( + self._strategy.mix_strategy == MixStrategy.AFTER + or self._strategy.mix_strategy == MixStrategy.BOTH + ): + mix_after_opts = self._mix_after_opts._asdict() + mix_after_opts["location"] = dest + yield self._format_dict("mix", kwargs=mix_after_opts) + if self._strategy.touch_tip_strategy == TouchTipStrategy.ALWAYS: + yield self._format_dict("touch_tip", kwargs=self._touch_tip_opts) + + if self._strategy.blow_out_strategy == BlowOutStrategy.SOURCE: + yield self._format_dict("blow_out", [src]) + elif self._strategy.blow_out_strategy == BlowOutStrategy.DEST: + yield self._format_dict("blow_out", [dest]) + elif self._strategy.blow_out_strategy == BlowOutStrategy.CUSTOM_LOCATION: + yield self._format_dict("blow_out", kwargs=self._blow_opts) + elif ( + self._strategy.blow_out_strategy == BlowOutStrategy.TRASH + or self._strategy.disposal_volume + ): + yield from self._after_dispense_trash() + else: + yield from self._after_dispense_helper() + + def _new_tip_action(self) -> Generator[TransferStep, None, None]: + if self._strategy.new_tip == types.TransferTipPolicy.ALWAYS: + if self._strategy.drop_tip_strategy == DropTipStrategy.RETURN: + yield self._format_dict("return_tip") + else: + yield self._format_dict("drop_tip") + + def _format_dict( + self, + method: str, + args: Optional[List[Any]] = None, + kwargs: Optional[Union[Dict[Any, Any], FormatDictArgs]] = None, + ) -> TransferStep: + if kwargs: + if isinstance(kwargs, Dict): + params = {key: val for key, val in kwargs.items() if val} + else: + params = {key: val for key, val in kwargs._asdict().items() if val} + else: + params = {} + if not args: + args = [] + return {"method": method, "args": args, "kwargs": params} + + def _create_volume_list( + self, volume: Union[Union[float, int], Sequence[float]], total_xfers: int + ) -> List[float]: + if isinstance(volume, (float, int)): + return [float(volume)] * total_xfers + elif isinstance(volume, tuple): + return self._create_volume_gradient( + volume[0], volume[-1], total_xfers, self._strategy.gradient_function + ) + else: + if not isinstance(volume, List): + raise TypeError( + "Volume expected as a number or List or" + " tuple but got {}".format(volume) + ) + elif not len(volume) == total_xfers: + raise RuntimeError( + "List of volumes should be equal to number " "of transfers" + ) + return volume + + @staticmethod + def _create_volume_gradient( + min_v: float, + max_v: float, + total: int, + gradient: Optional[Callable[[float], float]] = None, + ) -> List[float]: + + diff_vol = max_v - min_v + + def _map_volume(i: int) -> float: + nonlocal diff_vol, total + rel_x = i / (total - 1) + rel_y = gradient(rel_x) if gradient else rel_x + return (rel_y * diff_vol) + min_v + + return [_map_volume(i) for i in range(total)] + + def _check_valid_well_list( + self, well_list: List[Any], id: str, old_well_list: List[Any] + ) -> None: + if self._api_version >= APIVersion(2, 2) and len(well_list) < 1: + raise RuntimeError( + f"Invalid {id} for multichannel transfer: {old_well_list}" + ) + + @staticmethod + def _check_volume_not_zero(api_version: APIVersion, volume: float) -> bool: + # We should only be adding volumes to transfer plans if it is + # greater than zero to prevent extraneous robot movements. + if api_version < APIVersion(2, 8): + return True + elif volume > 0: + return True + return False + + def _multichannel_transfer( + self, s: AdvancedLiquidHandling, d: AdvancedLiquidHandling + ) -> Tuple[List[Union[Well, types.Location]], List[Union[Well, types.Location]]]: + # TODO: add a check for container being multi-channel compatible? + # Helper function for multi-channel use-case + assert ( + isinstance(s, Well) + or isinstance(s, types.Location) + or (isinstance(s, List) and isinstance(s[0], Well)) + or (isinstance(s, List) and isinstance(s[0], List)) + or (isinstance(s, List) and isinstance(s[0], types.Location)) + ), "Source should be a Well or List[Well] but is {}".format(s) + assert ( + isinstance(d, Well) + or isinstance(d, types.Location) + or (isinstance(d, List) and isinstance(d[0], Well)) + or (isinstance(d, List) and isinstance(d[0], List)) + or (isinstance(d, List) and isinstance(d[0], types.Location)) + ), "Target should be a Well or List[Well] but is {}".format(d) + + # TODO: Account for cases where a src/dest list has a non-first-row + # well (eg, 'B1') and would expect the robot/pipette to + # understand that it is referring to the whole first column + if isinstance(s, List) and isinstance(s[0], List): + # s is a List[List]]; flatten to 1D list + s = [well for list_elem in s for well in list_elem] + elif isinstance(s, Well) or isinstance(s, types.Location): + s = [s] + new_src = [] + for well in s: + if self._is_valid_row(well): + new_src.append(well) + self._check_valid_well_list(new_src, "source", s) + + if isinstance(d, List) and isinstance(d[0], List): + # s is a List[List]]; flatten to 1D list + d = [well for list_elem in d for well in list_elem] + elif isinstance(d, Well) or isinstance(d, types.Location): + d = [d] + new_dst = [] + for well in d: + if self._is_valid_row(well): + new_dst.append(well) + self._check_valid_well_list(new_dst, "target", d) + return new_src, new_dst + + def _is_valid_row(self, well: Union[Well, types.Location]) -> bool: + if isinstance(well, types.Location): + test_well = well.labware.as_well() + else: + test_well = well + + if self._api_version < APIVersion(2, 2): + return test_well in test_well.parent.rows()[0] + else: + # Allow the first 2 rows to be accessible to 384-well plates; + # otherwise, only the first row is accessible + if test_well.parent.parameters["format"] == "384Standard": + valid_wells = [ + well for row in test_well.parent.rows()[:2] for well in row + ] + return test_well in valid_wells + else: + return test_well in test_well.parent.rows()[0] diff --git a/api/src/opentrons/protocols/api_support/definitions.py b/api/src/opentrons/protocols/api_support/definitions.py index ad692e03828..e2f6aee1a2a 100644 --- a/api/src/opentrons/protocols/api_support/definitions.py +++ b/api/src/opentrons/protocols/api_support/definitions.py @@ -1,6 +1,6 @@ from .types import APIVersion -MAX_SUPPORTED_VERSION = APIVersion(2, 21) +MAX_SUPPORTED_VERSION = APIVersion(2, 22) """The maximum supported protocol API version in this release.""" MIN_SUPPORTED_VERSION = APIVersion(2, 0) diff --git a/api/src/opentrons/protocols/api_support/instrument.py b/api/src/opentrons/protocols/api_support/instrument.py index 0137b43a4c8..3299b8512f9 100644 --- a/api/src/opentrons/protocols/api_support/instrument.py +++ b/api/src/opentrons/protocols/api_support/instrument.py @@ -73,7 +73,7 @@ def tip_length_for( VALID_PIP_TIPRACK_VOL = { - "FLEX": {"p50": [50], "p1000": [50, 200, 1000]}, + "FLEX": {"p50": [50], "p200": [50, 200], "p1000": [50, 200, 1000]}, "OT2": { "p10": [10, 20], "p20": [10, 20], diff --git a/api/src/opentrons/protocols/api_support/util.py b/api/src/opentrons/protocols/api_support/util.py index da4ceff7360..3438692de2f 100644 --- a/api/src/opentrons/protocols/api_support/util.py +++ b/api/src/opentrons/protocols/api_support/util.py @@ -391,3 +391,13 @@ def _check_version_wrapper(*args: Any, **kwargs: Any) -> Any: return cast(FuncT, _check_version_wrapper) return _set_version + + +class ModifiedList(list[str]): + def __contains__(self, item: object) -> bool: + if not isinstance(item, str): + return False + for name in self: + if name == item.replace("-", "_").lower(): + return True + return False diff --git a/api/src/opentrons/protocols/labware.py b/api/src/opentrons/protocols/labware.py index ed1b7d15219..ec05e3cbb72 100644 --- a/api/src/opentrons/protocols/labware.py +++ b/api/src/opentrons/protocols/labware.py @@ -2,13 +2,14 @@ import logging import json - +import os from pathlib import Path -from typing import Any, AnyStr, Dict, Optional, Union +from typing import Any, AnyStr, Dict, Optional, Union, List, Sequence, Literal import jsonschema # type: ignore from opentrons_shared_data import load_shared_data, get_shared_data_root +from opentrons.protocols.api_support.util import ModifiedList from opentrons.protocols.api_support.constants import ( OPENTRONS_NAMESPACE, CUSTOM_NAMESPACE, @@ -16,10 +17,30 @@ USER_DEFS_PATH, ) from opentrons_shared_data.labware.types import LabwareDefinition +from opentrons_shared_data.errors.exceptions import InvalidProtocolData MODULE_LOG = logging.getLogger(__name__) +LabwareProblem = Literal[ + "no-schema-id", "bad-schema-id", "schema-mismatch", "invalid-json" +] + + +class NotALabwareError(InvalidProtocolData): + def __init__( + self, problem: LabwareProblem, wrapping: Sequence[BaseException] + ) -> None: + messages: dict[LabwareProblem, str] = { + "no-schema-id": "No schema ID present in file", + "bad-schema-id": "Bad schema ID in file", + "invalid-json": "File does not contain valid JSON", + "schema-mismatch": "File does not match labware schema", + } + super().__init__( + message=messages[problem], detail={"kind": problem}, wrapping=wrapping + ) + def get_labware_definition( load_name: str, @@ -61,6 +82,29 @@ def get_labware_definition( return _get_standard_labware_definition(load_name, namespace, version) +def get_all_labware_definitions(schema_version: str = "2") -> List[str]: + """ + Return a list of standard and custom labware definitions with load_name + + name_space + version existing on the robot + """ + labware_list = ModifiedList() + + def _check_for_subdirectories(path: Union[str, Path, os.DirEntry[str]]) -> None: + with os.scandir(path) as top_path: + for sub_dir in top_path: + if sub_dir.is_dir(): + labware_list.append(sub_dir.name) + + # check for standard labware + _check_for_subdirectories( + get_shared_data_root() / STANDARD_DEFS_PATH / schema_version + ) + # check for custom labware + for namespace in os.scandir(USER_DEFS_PATH): + _check_for_subdirectories(namespace) + return labware_list + + def save_definition( labware_def: LabwareDefinition, force: bool = False, location: Optional[Path] = None ) -> None: @@ -102,7 +146,7 @@ def save_definition( json.dump(labware_def, f) -def verify_definition( +def verify_definition( # noqa: C901 contents: Union[AnyStr, LabwareDefinition, Dict[str, Any]] ) -> LabwareDefinition: """Verify that an input string is a labware definition and return it. @@ -114,14 +158,33 @@ def verify_definition( :raises jsonschema.ValidationError: If the definition is not valid. :returns: The parsed definition """ - schema_body = load_shared_data("labware/schemas/2.json").decode("utf-8") - labware_schema_v2 = json.loads(schema_body) + schemata_by_version = { + 2: json.loads(load_shared_data("labware/schemas/2.json").decode("utf-8")), + 3: json.loads(load_shared_data("labware/schemas/3.json").decode("utf-8")), + } if isinstance(contents, dict): to_return = contents else: - to_return = json.loads(contents) - jsonschema.validate(to_return, labware_schema_v2) + try: + to_return = json.loads(contents) + except json.JSONDecodeError as e: + raise NotALabwareError("invalid-json", [e]) from e + try: + schema_version = to_return["schemaVersion"] + except KeyError as e: + raise NotALabwareError("no-schema-id", [e]) from e + + try: + schema = schemata_by_version[schema_version] + except KeyError as e: + raise NotALabwareError("bad-schema-id", [e]) from e + + try: + jsonschema.validate(to_return, schema) + except jsonschema.ValidationError as e: + raise NotALabwareError("schema-mismatch", [e]) from e + # we can type ignore this because if it passes the jsonschema it has # the correct structure return to_return # type: ignore[return-value] @@ -176,7 +239,6 @@ def _get_labware_definition_from_bundle( def _get_standard_labware_definition( load_name: str, namespace: Optional[str] = None, version: Optional[int] = None ) -> LabwareDefinition: - if version is None: checked_version = 1 else: diff --git a/api/src/opentrons/protocols/models/json_protocol.py b/api/src/opentrons/protocols/models/json_protocol.py index 979d0192f62..ef2fec8823d 100644 --- a/api/src/opentrons/protocols/models/json_protocol.py +++ b/api/src/opentrons/protocols/models/json_protocol.py @@ -9,7 +9,7 @@ from typing import Any, Dict, List, Optional, Union -from pydantic import BaseModel, Extra, Field +from pydantic import ConfigDict, BaseModel, Field from typing_extensions import Literal from opentrons_shared_data.labware.labware_definition import LabwareDefinition @@ -75,8 +75,7 @@ class Metadata(BaseModel): Optional metadata about the protocol """ - class Config: - extra = Extra.allow + model_config = ConfigDict(extra="allow") protocolName: Optional[str] = Field( None, description="A short, human-readable name for the protocol" @@ -574,8 +573,7 @@ class Pipettes(BaseModel): Fields describing an individual pipette """ - class Config: - extra = Extra.allow + model_config = ConfigDict(extra="allow") mount: Literal["left", "right"] = Field( ..., description="Where the pipette is mounted" @@ -592,8 +590,7 @@ class Labware(BaseModel): Fields describing a single labware on the deck """ - class Config: - extra = Extra.allow + model_config = ConfigDict(extra="allow") slot: str = Field( ..., @@ -616,8 +613,7 @@ class Modules(BaseModel): Fields describing a single module on the deck """ - class Config: - extra = Extra.allow + model_config = ConfigDict(extra="allow") slot: str = Field( ..., diff --git a/api/src/opentrons/simulate.py b/api/src/opentrons/simulate.py index e565bab83e0..bed24c68731 100644 --- a/api/src/opentrons/simulate.py +++ b/api/src/opentrons/simulate.py @@ -829,7 +829,9 @@ def _create_live_context_pe( # Non-async would use call_soon_threadsafe(), which makes the waiting harder. async def add_all_extra_labware() -> None: for labware_definition_dict in extra_labware.values(): - labware_definition = LabwareDefinition.parse_obj(labware_definition_dict) + labware_definition = LabwareDefinition.model_validate( + labware_definition_dict + ) pe.add_labware_definition(labware_definition) # Add extra_labware to ProtocolEngine, being careful not to modify ProtocolEngine from this diff --git a/api/src/opentrons/types.py b/api/src/opentrons/types.py index 22611393f40..09e138513c1 100644 --- a/api/src/opentrons/types.py +++ b/api/src/opentrons/types.py @@ -1,7 +1,17 @@ from __future__ import annotations import enum from math import sqrt, isclose -from typing import TYPE_CHECKING, Any, NamedTuple, Iterator, Union, List, Optional +from typing import ( + TYPE_CHECKING, + Any, + NamedTuple, + Iterator, + Union, + List, + Optional, + Protocol, + Dict, +) from opentrons_shared_data.robot.types import RobotType @@ -150,7 +160,7 @@ def __iter__(self) -> Iterator[Union[Point, LabwareLike]]: point, labware = location some_function_taking_both(*location) """ - return iter((self._point, self._labware)) # type: ignore [arg-type] + return iter((self._point, self._labware)) def __eq__(self, other: object) -> bool: return ( @@ -253,6 +263,75 @@ class OT3MountType(str, enum.Enum): GRIPPER = "gripper" +class AxisType(enum.Enum): + X = "X" # gantry + Y = "Y" + Z_L = "Z_L" # left pipette mount Z + Z_R = "Z_R" # right pipette mount Z + Z_G = "Z_G" # gripper mount Z + P_L = "P_L" # left pipette plunger + P_R = "P_R" # right pipette plunger + Q = "Q" # hi-throughput pipette tiprack grab + G = "G" # gripper grab + + @classmethod + def axis_for_mount(cls, mount: Mount) -> "AxisType": + map_axis_to_mount = { + Mount.LEFT: cls.Z_L, + Mount.RIGHT: cls.Z_R, + Mount.EXTENSION: cls.Z_G, + } + return map_axis_to_mount[mount] + + @classmethod + def mount_for_axis(cls, axis: "AxisType") -> Mount: + map_mount_to_axis = { + cls.Z_L: Mount.LEFT, + cls.Z_R: Mount.RIGHT, + cls.Z_G: Mount.EXTENSION, + } + return map_mount_to_axis[axis] + + @classmethod + def plunger_axis_for_mount(cls, mount: Mount) -> "AxisType": + map_plunger_axis_mount = {Mount.LEFT: cls.P_L, Mount.RIGHT: cls.P_R} + return map_plunger_axis_mount[mount] + + @classmethod + def ot2_axes(cls) -> List["AxisType"]: + return [ + AxisType.X, + AxisType.Y, + AxisType.Z_L, + AxisType.Z_R, + AxisType.P_L, + AxisType.P_R, + ] + + @classmethod + def flex_gantry_axes(cls) -> List["AxisType"]: + return [ + AxisType.X, + AxisType.Y, + AxisType.Z_L, + AxisType.Z_R, + AxisType.Z_G, + ] + + @classmethod + def ot2_gantry_axes(cls) -> List["AxisType"]: + return [ + AxisType.X, + AxisType.Y, + AxisType.Z_L, + AxisType.Z_R, + ] + + +AxisMapType = Dict[AxisType, float] +StringAxisMap = Dict[str, float] + + # TODO(mc, 2020-11-09): this makes sense in shared-data or other common # model library # https://github.com/Opentrons/opentrons/pull/6943#discussion_r519029833 @@ -426,3 +505,84 @@ class TransferTipPolicy(enum.Enum): DeckLocation = Union[int, str] ALLOWED_PRIMARY_NOZZLES = ["A1", "H1", "A12", "H12"] + + +class NozzleConfigurationType(enum.Enum): + """Short names for types of nozzle configurations. + + Represents the current nozzle configuration stored in a NozzleMap. + """ + + COLUMN = "COLUMN" + ROW = "ROW" + SINGLE = "SINGLE" + FULL = "FULL" + SUBRECT = "SUBRECT" + + +class NozzleMapInterface(Protocol): + """ + A NozzleMap instance represents a specific configuration of active nozzles on a pipette. + + It exposes properties of the configuration like the configuration's front-right, front-left, + back-left and starting nozzles as well as a map of all the nozzles active in the configuration. + + Because NozzleMaps represent configurations directly, the properties of the NozzleMap may not + match the properties of the physical pipette. For instance, a NozzleMap for a single channel + configuration of an 8-channel pipette - say, A1 only - will have its front left, front right, + and active channels all be A1, while the physical configuration would have the front right + channel be H1. + """ + + @property + def starting_nozzle(self) -> str: + """The nozzle that automated operations that count nozzles should start at.""" + ... + + @property + def rows(self) -> dict[str, list[str]]: + """A map of all the rows active in this configuration.""" + ... + + @property + def columns(self) -> dict[str, list[str]]: + """A map of all the columns active in this configuration.""" + ... + + @property + def back_left(self) -> str: + """The backest, leftest (i.e. back if it's a column, left if it's a row) nozzle of the configuration. + + Note: This is the value relevant for this particular configuration, and it may not represent the back left nozzle + of the underlying physical pipette. For instance, the back-left nozzle of a configuration representing nozzles + D7 to H12 of a 96-channel pipette is D7, which is not the back-left nozzle of the physical pipette (A1). + """ + ... + + @property + def configuration(self) -> NozzleConfigurationType: + """The kind of configuration represented by this nozzle map.""" + ... + + @property + def front_right(self) -> str: + """The frontest, rightest (i.e. front if it's a column, right if it's a row) nozzle of the configuration. + + Note: This is the value relevant for this configuration, not the physical pipette. See the note on back_left. + """ + ... + + @property + def tip_count(self) -> int: + """The total number of active nozzles in the configuration, and thus the number of tips that will be picked up.""" + ... + + @property + def physical_nozzle_count(self) -> int: + """The number of actual physical nozzles on the pipette, regardless of configuration.""" + ... + + @property + def active_nozzles(self) -> list[str]: + """An unstructured list of all nozzles active in the configuration.""" + ... diff --git a/api/src/opentrons/util/entrypoint_util.py b/api/src/opentrons/util/entrypoint_util.py index 2da4cac874c..508f6769bc5 100644 --- a/api/src/opentrons/util/entrypoint_util.py +++ b/api/src/opentrons/util/entrypoint_util.py @@ -6,7 +6,6 @@ from dataclasses import dataclass import json import logging -from json import JSONDecodeError import pathlib import subprocess import sys @@ -21,8 +20,6 @@ TYPE_CHECKING, ) -from jsonschema import ValidationError # type: ignore - from opentrons.calibration_storage.deck_configuration import ( deserialize_deck_configuration, ) @@ -32,7 +29,7 @@ JUPYTER_NOTEBOOK_LABWARE_DIR, SystemArchitecture, ) -from opentrons.protocol_api import labware +from opentrons.protocols import labware from opentrons.calibration_storage import helpers from opentrons.protocol_engine.errors.error_occurrence import ( ErrorOccurrence as ProtocolEngineErrorOccurrence, @@ -79,7 +76,7 @@ def labware_from_paths( if child.is_file() and child.suffix.endswith("json"): try: defn = labware.verify_definition(child.read_bytes()) - except (ValidationError, JSONDecodeError): + except labware.NotALabwareError: log.info(f"{child}: invalid labware, ignoring") log.debug( f"{child}: labware invalid because of this exception.", diff --git a/api/src/opentrons/util/logging_config.py b/api/src/opentrons/util/logging_config.py index 6520bb912f6..f9a59799d9d 100644 --- a/api/src/opentrons/util/logging_config.py +++ b/api/src/opentrons/util/logging_config.py @@ -36,7 +36,7 @@ def _host_config(level_value: int) -> Dict[str, Any]: "class": "logging.handlers.RotatingFileHandler", "formatter": "basic", "filename": serial_log_filename, - "maxBytes": 5000000, + "maxBytes": 1000000, "level": logging.DEBUG, "backupCount": 3, }, diff --git a/api/tests/opentrons/calibration_storage/test_deck_attitude.py b/api/tests/opentrons/calibration_storage/test_deck_attitude.py index bbb832651d1..bce3ae02809 100644 --- a/api/tests/opentrons/calibration_storage/test_deck_attitude.py +++ b/api/tests/opentrons/calibration_storage/test_deck_attitude.py @@ -57,7 +57,7 @@ def test_save_ot2_deck_attitude(ot_config_tempdir: Any) -> None: "pip1", "mytiprack", ) - assert get_robot_deck_attitude() != {} + assert get_robot_deck_attitude() is not None def test_save_ot3_deck_attitude(ot_config_tempdir: Any) -> None: diff --git a/api/tests/opentrons/calibration_storage/test_file_operators.py b/api/tests/opentrons/calibration_storage/test_file_operators.py index 5a95f225fe3..ec25a2279c1 100644 --- a/api/tests/opentrons/calibration_storage/test_file_operators.py +++ b/api/tests/opentrons/calibration_storage/test_file_operators.py @@ -84,7 +84,7 @@ def test_deserialize_pydantic_model_valid() -> None: serialized = b'{"integer_field": 123, "! aliased field !": "abc"}' assert io.deserialize_pydantic_model( serialized, DummyModel - ) == DummyModel.construct(integer_field=123, aliased_field="abc") + ) == DummyModel.model_construct(integer_field=123, aliased_field="abc") def test_deserialize_pydantic_model_invalid_as_json() -> None: diff --git a/api/tests/opentrons/calibration_storage/test_tip_length_ot2.py b/api/tests/opentrons/calibration_storage/test_tip_length_ot2.py index df503241d75..51096866b5d 100644 --- a/api/tests/opentrons/calibration_storage/test_tip_length_ot2.py +++ b/api/tests/opentrons/calibration_storage/test_tip_length_ot2.py @@ -46,7 +46,7 @@ def starting_calibration_data( "tipLength": 27, "lastModified": inside_data.lastModified.isoformat(), "source": inside_data.source, - "status": inside_data.status.dict(), + "status": inside_data.status.model_dump(), "uri": "dummy_namespace/minimal_labware_def/1", } } diff --git a/api/tests/opentrons/conftest.py b/api/tests/opentrons/conftest.py index cf8fdd0e97c..20b8d3a4502 100755 --- a/api/tests/opentrons/conftest.py +++ b/api/tests/opentrons/conftest.py @@ -608,6 +608,7 @@ def minimal_labware_def() -> LabwareDefinition: "displayCategory": "other", "displayVolumeUnits": "mL", }, + "allowedRoles": ["labware"], "cornerOffsetFromSlot": {"x": 10, "y": 10, "z": 5}, "parameters": { "isTiprack": False, @@ -804,10 +805,10 @@ def minimal_liquid_class_def2() -> LiquidClassSchemaV1: namespace="test-fixture-2", byPipette=[ ByPipetteSetting( - pipetteModel="p20_single_gen2", + pipetteModel="flex_1channel_50", byTipType=[ ByTipTypeSetting( - tiprack="opentrons_96_tiprack_20ul", + tiprack="opentrons_flex_96_tiprack_50ul", aspirate=AspirateProperties( submerge=Submerge( positionReference=PositionReference.LIQUID_MENISCUS, @@ -821,13 +822,14 @@ def minimal_liquid_class_def2() -> LiquidClassSchemaV1: positionReference=PositionReference.WELL_TOP, offset=Coordinate(x=0, y=0, z=5), speed=100, - airGapByVolume={"default": 2, "5": 3, "10": 4}, + airGapByVolume=[(5.0, 3.0), (10.0, 4.0)], touchTip=TouchTipProperties(enable=False), delay=DelayProperties(enable=False), ), positionReference=PositionReference.WELL_BOTTOM, offset=Coordinate(x=0, y=0, z=-5), - flowRateByVolume={"default": 50, "10": 40, "20": 30}, + flowRateByVolume=[(10.0, 40.0), (20.0, 30.0)], + correctionByVolume=[(15.0, 1.5), (30.0, -5.0)], preWet=True, mix=MixProperties(enable=False), delay=DelayProperties( @@ -845,16 +847,17 @@ def minimal_liquid_class_def2() -> LiquidClassSchemaV1: positionReference=PositionReference.WELL_TOP, offset=Coordinate(x=0, y=0, z=5), speed=100, - airGapByVolume={"default": 2, "5": 3, "10": 4}, + airGapByVolume=[(5.0, 3.0), (10.0, 4.0)], blowout=BlowoutProperties(enable=False), touchTip=TouchTipProperties(enable=False), delay=DelayProperties(enable=False), ), positionReference=PositionReference.WELL_BOTTOM, offset=Coordinate(x=0, y=0, z=-5), - flowRateByVolume={"default": 50, "10": 40, "20": 30}, + flowRateByVolume=[(10.0, 40.0), (20.0, 30.0)], + correctionByVolume=[(15.0, -1.5), (30.0, 5.0)], mix=MixProperties(enable=False), - pushOutByVolume={"default": 5, "10": 7, "20": 10}, + pushOutByVolume=[(10.0, 7.0), (20.0, 10.0)], delay=DelayProperties(enable=False), ), multiDispense=None, diff --git a/api/tests/opentrons/drivers/asyncio/communication/test_serial_connection.py b/api/tests/opentrons/drivers/asyncio/communication/test_serial_connection.py index bd7dd73f613..e2290944fdf 100644 --- a/api/tests/opentrons/drivers/asyncio/communication/test_serial_connection.py +++ b/api/tests/opentrons/drivers/asyncio/communication/test_serial_connection.py @@ -13,6 +13,7 @@ NoResponse, AlarmResponse, ErrorResponse, + UnhandledGcode, ) @@ -149,25 +150,31 @@ async def test_send_command_response( @pytest.mark.parametrize( - argnames=["response", "exception_type"], + argnames=["response", "exception_type", "async_only"], argvalues=[ - ["error", ErrorResponse], - ["Error", ErrorResponse], - ["Error: was found.", ErrorResponse], - ["alarm", AlarmResponse], - ["ALARM", AlarmResponse], - ["This is an Alarm", AlarmResponse], - ["error:Alarm lock", AlarmResponse], - ["alarm:error", AlarmResponse], - ["ALARM: Hard limit -X", AlarmResponse], + ["error", ErrorResponse, False], + ["Error", ErrorResponse, False], + ["Error: was found.", ErrorResponse, False], + ["alarm", AlarmResponse, False], + ["ALARM", AlarmResponse, False], + ["This is an Alarm", AlarmResponse, False], + ["error:Alarm lock", AlarmResponse, False], + ["alarm:error", AlarmResponse, False], + ["ALARM: Hard limit -X", AlarmResponse, False], + ["ERR003:unhandled gcode OK ", UnhandledGcode, True], ], ) def test_raise_on_error( - subject: SerialKind, response: str, exception_type: Type[Exception] + subject: SerialKind, + response: str, + exception_type: Type[Exception], + async_only: bool, ) -> None: """It should raise an exception on error/alarm responses.""" + if isinstance(subject, SerialConnection) and async_only: + pytest.skip() with pytest.raises(expected_exception=exception_type, match=response): - subject.raise_on_error(response) + subject.raise_on_error(response, "fake request") async def test_on_retry(mock_serial_port: AsyncMock, subject: SerialKind) -> None: @@ -188,6 +195,7 @@ async def test_send_data_with_async_error_before( serial_error_response = f" {error_response} {ack}" encoded_error_response = serial_error_response.encode() successful_response = "G28" + data = "G28" serial_successful_response = f" {successful_response} {ack}" encoded_successful_response = serial_successful_response.encode() mock_serial_port.read_until.side_effect = [ @@ -195,7 +203,7 @@ async def test_send_data_with_async_error_before( encoded_successful_response, ] - response = await subject_raise_on_error_patched._send_data(data="G28") + response = await subject_raise_on_error_patched._send_data(data=data) assert response == successful_response mock_serial_port.read_until.assert_has_calls( @@ -206,8 +214,8 @@ async def test_send_data_with_async_error_before( ) subject_raise_on_error_patched.raise_on_error.assert_has_calls( # type: ignore[attr-defined] calls=[ - call(response=error_response), - call(response=successful_response), + call(response=error_response, request=data), + call(response=successful_response, request=data), ] ) @@ -222,6 +230,7 @@ async def test_send_data_with_async_error_after( serial_error_response = f" {error_response} {ack}" encoded_error_response = serial_error_response.encode() successful_response = "G28" + data = "G28" serial_successful_response = f" {successful_response} {ack}" encoded_successful_response = serial_successful_response.encode() mock_serial_port.read_until.side_effect = [ @@ -229,7 +238,7 @@ async def test_send_data_with_async_error_after( encoded_error_response, ] - response = await subject_raise_on_error_patched._send_data(data="G28") + response = await subject_raise_on_error_patched._send_data(data=data) assert response == successful_response mock_serial_port.read_until.assert_has_calls( @@ -239,6 +248,6 @@ async def test_send_data_with_async_error_after( ) subject_raise_on_error_patched.raise_on_error.assert_has_calls( # type: ignore[attr-defined] calls=[ - call(response=successful_response), + call(response=successful_response, request=data), ] ) diff --git a/api/tests/opentrons/drivers/flex_stacker/__init__.py b/api/tests/opentrons/drivers/flex_stacker/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/api/tests/opentrons/drivers/flex_stacker/test_driver.py b/api/tests/opentrons/drivers/flex_stacker/test_driver.py new file mode 100644 index 00000000000..aea2492cf9e --- /dev/null +++ b/api/tests/opentrons/drivers/flex_stacker/test_driver.py @@ -0,0 +1,257 @@ +import pytest +from mock import AsyncMock +from opentrons.drivers.asyncio.communication.serial_connection import ( + AsyncResponseSerialConnection, +) +from opentrons.drivers.flex_stacker.driver import FlexStackerDriver +from opentrons.drivers.flex_stacker import types + + +@pytest.fixture +def connection() -> AsyncMock: + return AsyncMock(spec=AsyncResponseSerialConnection) + + +@pytest.fixture +def subject(connection: AsyncMock) -> FlexStackerDriver: + connection.send_command.return_value = "" + return FlexStackerDriver(connection) + + +async def test_get_device_info( + subject: FlexStackerDriver, connection: AsyncMock +) -> None: + """It should send a get device info command""" + connection.send_command.return_value = ( + "M115 FW:0.0.1 HW:Opentrons-flex-stacker-a1 SerialNo:STCA120230605001" + ) + response = await subject.get_device_info() + assert response == types.StackerInfo( + fw="0.0.1", + hw=types.HardwareRevision.EVT, + sn="STCA120230605001", + ) + + device_info = types.GCODE.DEVICE_INFO.build_command() + reset_reason = types.GCODE.GET_RESET_REASON.build_command() + connection.send_command.assert_any_call(device_info) + connection.send_command.assert_called_with(reset_reason) + connection.reset_mock() + + # Test invalid response + connection.send_command.return_value = "M115 FW:0.0.1 SerialNo:STCA120230605001" + + # This should raise ValueError + with pytest.raises(ValueError): + response = await subject.get_device_info() + + device_info = types.GCODE.DEVICE_INFO.build_command() + reset_reason = types.GCODE.GET_RESET_REASON.build_command() + connection.send_command.assert_any_call(device_info) + connection.send_command.assert_called_with(reset_reason) + + +async def test_stop_motors(subject: FlexStackerDriver, connection: AsyncMock) -> None: + """It should send a stop motors command""" + connection.send_command.return_value = "M0" + response = await subject.stop_motors() + assert response + + stop_motors = types.GCODE.STOP_MOTORS.build_command() + connection.send_command.assert_any_call(stop_motors) + connection.reset_mock() + + # This should raise ValueError + with pytest.raises(ValueError): + await subject.get_device_info() + + +async def test_set_serial_number( + subject: FlexStackerDriver, connection: AsyncMock +) -> None: + """It should send a set serial number command""" + connection.send_command.return_value = "M996" + + serial_number = "Something" + response = await subject.set_serial_number(serial_number) + assert response + + set_serial_number = types.GCODE.SET_SERIAL_NUMBER.build_command().add_element( + serial_number + ) + connection.send_command.assert_any_call(set_serial_number) + connection.reset_mock() + + # Test invalid response + connection.send_command.return_value = "M9nn" + with pytest.raises(ValueError): + response = await subject.set_serial_number(serial_number) + + set_serial_number = types.GCODE.SET_SERIAL_NUMBER.build_command().add_element( + serial_number + ) + connection.send_command.assert_any_call(set_serial_number) + connection.reset_mock() + + +async def test_get_limit_switch( + subject: FlexStackerDriver, connection: AsyncMock +) -> None: + """It should send a get limit switch command and return the boolean of one.""" + connection.send_command.return_value = "M119 XE:1 XR:0 ZE:0 ZR:1 LR:1" + response = await subject.get_limit_switch( + types.StackerAxis.X, types.Direction.EXTENT + ) + assert response + + limit_switch_status = types.GCODE.GET_LIMIT_SWITCH.build_command() + connection.send_command.assert_any_call(limit_switch_status) + connection.reset_mock() + + +async def test_get_limit_switches_status( + subject: FlexStackerDriver, connection: AsyncMock +) -> None: + """It should send a get limit switch status and return LimitSwitchStatus.""" + connection.send_command.return_value = "M119 XE:1 XR:0 ZE:0 ZR:1 LR:1" + response = await subject.get_limit_switches_status() + assert response == types.LimitSwitchStatus( + XE=True, + XR=False, + ZE=False, + ZR=True, + LR=True, + ) + + limit_switch_status = types.GCODE.GET_LIMIT_SWITCH.build_command() + connection.send_command.assert_any_call(limit_switch_status) + connection.reset_mock() + + # Test invalid response + connection.send_command.return_value = "M119 XE:b XR:0 ZE:a ZR:1 LR:n" + with pytest.raises(ValueError): + response = await subject.get_limit_switches_status() + + limit_switch_status = types.GCODE.GET_LIMIT_SWITCH.build_command() + connection.send_command.assert_any_call(limit_switch_status) + + +async def test_get_platform_sensor( + subject: FlexStackerDriver, connection: AsyncMock +) -> None: + """It should send a get platform sensor command return status of specified sensor.""" + connection.send_command.return_value = "M121 E:1 R:1" + response = await subject.get_platform_sensor(types.Direction.EXTENT) + assert response + + platform_sensor = types.GCODE.GET_PLATFORM_SENSOR.build_command() + connection.send_command.assert_any_call(platform_sensor) + connection.reset_mock() + + +async def test_get_platform_status( + subject: FlexStackerDriver, connection: AsyncMock +) -> None: + """it should send a get platform sensors status.""" + connection.send_command.return_value = "M121 E:0 R:1" + response = await subject.get_platform_status() + assert response == types.PlatformStatus( + E=False, + R=True, + ) + + platform_status = types.GCODE.GET_PLATFORM_SENSOR.build_command() + connection.send_command.assert_any_call(platform_status) + connection.reset_mock() + + # Test invalid response + connection.send_command.return_value = "M121 E:0 R:1 something" + with pytest.raises(ValueError): + response = await subject.get_platform_status() + + platform_status = types.GCODE.GET_PLATFORM_SENSOR.build_command() + connection.send_command.assert_any_call(platform_status) + + +async def test_get_hopper_door_closed( + subject: FlexStackerDriver, connection: AsyncMock +) -> None: + """It should send a get door closed command.""" + connection.send_command.return_value = "M122 D:1" + response = await subject.get_hopper_door_closed() + assert response + + door_closed = types.GCODE.GET_DOOR_SWITCH.build_command() + connection.send_command.assert_any_call(door_closed) + connection.reset_mock() + + # Test door open + connection.send_command.return_value = "M122 D:0" + response = await subject.get_hopper_door_closed() + assert not response + + door_closed = types.GCODE.GET_DOOR_SWITCH.build_command() + connection.send_command.assert_any_call(door_closed) + connection.reset_mock() + + # Test invalid response + connection.send_command.return_value = "M122 78gybhjk" + + with pytest.raises(ValueError): + response = await subject.get_hopper_door_closed() + + door_closed = types.GCODE.GET_DOOR_SWITCH.build_command() + connection.send_command.assert_any_call(door_closed) + connection.reset_mock() + + +async def test_move_in_mm(subject: FlexStackerDriver, connection: AsyncMock) -> None: + """It should send a move to command""" + connection.send_command.return_value = "G0" + response = await subject.move_in_mm(types.StackerAxis.X, 10) + assert response + + move_to = types.GCODE.MOVE_TO.build_command().add_float("X", 10) + connection.send_command.assert_any_call(move_to) + connection.reset_mock() + + +async def test_move_to_switch( + subject: FlexStackerDriver, connection: AsyncMock +) -> None: + """It should send a move to switch command""" + connection.send_command.return_value = "G5" + axis = types.StackerAxis.X + direction = types.Direction.EXTENT + response = await subject.move_to_limit_switch(axis, direction) + assert response + + move_to = types.GCODE.MOVE_TO_SWITCH.build_command().add_int( + axis.name, direction.value + ) + connection.send_command.assert_any_call(move_to) + connection.reset_mock() + + +async def test_home_axis(subject: FlexStackerDriver, connection: AsyncMock) -> None: + """It should send a home axis command""" + connection.send_command.return_value = "G28" + axis = types.StackerAxis.X + direction = types.Direction.EXTENT + response = await subject.home_axis(axis, direction) + assert response + + move_to = types.GCODE.HOME_AXIS.build_command().add_int(axis.name, direction.value) + connection.send_command.assert_any_call(move_to) + connection.reset_mock() + + +async def test_set_led(subject: FlexStackerDriver, connection: AsyncMock) -> None: + """It should send a set led command""" + connection.send_command.return_value = "M200" + response = await subject.set_led(1, types.LEDColor.RED) + assert response + + set_led = types.GCODE.SET_LED.build_command().add_float("P", 1).add_int("C", 1) + connection.send_command.assert_any_call(set_led) + connection.reset_mock() diff --git a/api/tests/opentrons/drivers/heater_shaker/test_driver.py b/api/tests/opentrons/drivers/heater_shaker/test_driver.py index a1fadc34446..a3f5e1151b3 100644 --- a/api/tests/opentrons/drivers/heater_shaker/test_driver.py +++ b/api/tests/opentrons/drivers/heater_shaker/test_driver.py @@ -137,10 +137,14 @@ async def test_get_device_info( ) response = await subject.get_device_info() assert response == {"serial": "TC2101010A2", "model": "A", "version": "21.2.1"} - expected = CommandBuilder(terminator=driver.HS_COMMAND_TERMINATOR).add_gcode( + device_info = CommandBuilder(terminator=driver.HS_COMMAND_TERMINATOR).add_gcode( gcode="M115" ) - connection.send_command.assert_called_once_with(command=expected, retries=0) + reset_reason = CommandBuilder(terminator=driver.HS_COMMAND_TERMINATOR).add_gcode( + gcode="M114" + ) + connection.send_command.assert_any_call(command=device_info, retries=0) + connection.send_command.assert_called_with(command=reset_reason, retries=0) async def test_enter_bootloader( diff --git a/api/tests/opentrons/drivers/temp_deck/test_driver.py b/api/tests/opentrons/drivers/temp_deck/test_driver.py index df424aa397c..7b6e6a075d8 100644 --- a/api/tests/opentrons/drivers/temp_deck/test_driver.py +++ b/api/tests/opentrons/drivers/temp_deck/test_driver.py @@ -1,7 +1,10 @@ from mock import AsyncMock import pytest -from opentrons.drivers.asyncio.communication.serial_connection import SerialConnection +from opentrons.drivers.asyncio.communication.serial_connection import ( + SerialConnection, +) +from opentrons.drivers.asyncio.communication.errors import UnhandledGcode from opentrons.drivers.temp_deck.driver import ( TempDeckDriver, TEMP_DECK_COMMAND_TERMINATOR, @@ -59,15 +62,57 @@ async def test_get_temperature(driver: TempDeckDriver, connection: AsyncMock) -> assert response == Temperature(current=25, target=132) -async def test_get_device_info(driver: TempDeckDriver, connection: AsyncMock) -> None: +async def test_get_device_info_with_reset_reason( + driver: TempDeckDriver, connection: AsyncMock +) -> None: """It should send a get device info command and parse response""" connection.send_command.return_value = "serial:s model:m version:v" response = await driver.get_device_info() - expected = CommandBuilder(terminator=TEMP_DECK_COMMAND_TERMINATOR).add_gcode("M115") + device_info = CommandBuilder(terminator=TEMP_DECK_COMMAND_TERMINATOR).add_gcode( + "M115" + ) + reset_reason = CommandBuilder(terminator=TEMP_DECK_COMMAND_TERMINATOR).add_gcode( + "M114" + ) - connection.send_command.assert_called_once_with(command=expected, retries=3) + connection.send_command.assert_any_call(command=device_info, retries=3) + connection.send_command.assert_called_with(command=reset_reason, retries=3) + + assert response == {"serial": "s", "model": "m", "version": "v"} + + +async def test_get_device_info_no_reset_reason( + driver: TempDeckDriver, connection: AsyncMock +) -> None: + """It should send a get device info command and parse response""" + + async def fake_send_command( + command: CommandBuilder, retries: int = 0, timeout: float | None = None + ) -> str: + if command.build().startswith("M114"): + raise UnhandledGcode( + port="fake port", + response="ERR003: Unhandled Gcode", + command=command.build(), + ) + else: + return "serial:s model:m version:v" + + connection.send_command.side_effect = fake_send_command + + response = await driver.get_device_info() + + device_info = CommandBuilder(terminator=TEMP_DECK_COMMAND_TERMINATOR).add_gcode( + "M115" + ) + reset_reason = CommandBuilder(terminator=TEMP_DECK_COMMAND_TERMINATOR).add_gcode( + "M114" + ) + + connection.send_command.assert_any_call(command=device_info, retries=3) + connection.send_command.assert_called_with(command=reset_reason, retries=3) assert response == {"serial": "s", "model": "m", "version": "v"} diff --git a/api/tests/opentrons/drivers/thermocycler/test_driver.py b/api/tests/opentrons/drivers/thermocycler/test_driver.py index 0198e4e623f..2eca8a0de09 100644 --- a/api/tests/opentrons/drivers/thermocycler/test_driver.py +++ b/api/tests/opentrons/drivers/thermocycler/test_driver.py @@ -237,10 +237,9 @@ async def test_device_info( device_info = await subject.get_device_info() - expected = CommandBuilder(terminator=driver.TC_COMMAND_TERMINATOR).add_gcode( + get_device_info = CommandBuilder(terminator=driver.TC_COMMAND_TERMINATOR).add_gcode( gcode="M115" ) - connection.send_command.assert_called_once_with(command=expected, retries=3) - + connection.send_command.assert_any_call(command=get_device_info, retries=3) assert device_info == {"serial": "s", "model": "m", "version": "v"} diff --git a/api/tests/opentrons/drivers/thermocycler/test_gen2_driver.py b/api/tests/opentrons/drivers/thermocycler/test_gen2_driver.py index 47cf6d4ebe3..da5388c558e 100644 --- a/api/tests/opentrons/drivers/thermocycler/test_gen2_driver.py +++ b/api/tests/opentrons/drivers/thermocycler/test_gen2_driver.py @@ -225,11 +225,15 @@ async def test_device_info( device_info = await subject.get_device_info() - expected = CommandBuilder(terminator=driver.TC_COMMAND_TERMINATOR).add_gcode( + get_device_info = CommandBuilder(terminator=driver.TC_COMMAND_TERMINATOR).add_gcode( gcode="M115" ) + reset_reason = CommandBuilder(terminator=driver.TC_COMMAND_TERMINATOR).add_gcode( + gcode="M114" + ) - connection.send_command.assert_called_once_with(command=expected, retries=3) + connection.send_command.assert_any_call(command=get_device_info, retries=3) + connection.send_command.assert_called_with(command=reset_reason, retries=3) assert device_info == { "serial": "EMPTYSN", diff --git a/api/tests/opentrons/hardware_control/backends/test_ot3_controller.py b/api/tests/opentrons/hardware_control/backends/test_ot3_controller.py index 5ffee581de4..9bd87fe62ec 100644 --- a/api/tests/opentrons/hardware_control/backends/test_ot3_controller.py +++ b/api/tests/opentrons/hardware_control/backends/test_ot3_controller.py @@ -197,6 +197,7 @@ def mock_send_stop_threshold() -> Iterator[mock.AsyncMock]: @pytest.fixture def mock_move_group_run() -> Iterator[mock.AsyncMock]: + with mock.patch( "opentrons.hardware_control.backends.ot3controller.MoveGroupRunner.run", autospec=True, @@ -338,7 +339,7 @@ def fw_node_info() -> Dict[NodeId, DeviceInfoCache]: ] -def move_group_run_side_effect( +def move_group_run_side_effect_home( controller: OT3Controller, axes_to_home: List[Axis] ) -> Iterator[Dict[NodeId, MotorPositionStatus]]: """Return homed position for axis that is present and was commanded to home.""" @@ -366,13 +367,15 @@ async def test_home_execute( mock_present_devices: None, mock_check_overpressure: None, ) -> None: - config = {"run.side_effect": move_group_run_side_effect(controller, axes)} + config = {"run.side_effect": move_group_run_side_effect_home(controller, axes)} with mock.patch( # type: ignore [call-overload] "opentrons.hardware_control.backends.ot3controller.MoveGroupRunner", spec=MoveGroupRunner, **config ) as mock_runner: present_axes = set(ax for ax in axes if controller.axis_is_present(ax)) + controller.set_pressure_sensor_available(Axis.P_L, True) + controller.set_pressure_sensor_available(Axis.P_R, True) # nothing has been homed assert not controller._motor_status @@ -484,8 +487,10 @@ async def test_home_only_present_devices( homed_position = {} controller._position = starting_position + controller.set_pressure_sensor_available(Axis.P_L, True) + controller.set_pressure_sensor_available(Axis.P_R, True) - mock_move_group_run.side_effect = move_group_run_side_effect(controller, axes) + mock_move_group_run.side_effect = move_group_run_side_effect_home(controller, axes) # nothing has been homed assert not controller._motor_status @@ -728,6 +733,9 @@ async def test_liquid_probe( mock_move_group_run.side_effect = probe_move_group_run_side_effect( head_node, tool_node ) + controller._pipettes_to_monitor_pressure = mock.MagicMock( # type: ignore[method-assign] + return_value=[sensor_node_for_mount(mount)] + ) try: await controller.liquid_probe( mount=mount, @@ -737,6 +745,7 @@ async def test_liquid_probe( threshold_pascals=fake_liquid_settings.sensor_threshold_pascals, plunger_impulse_time=fake_liquid_settings.plunger_impulse_time, num_baseline_reads=fake_liquid_settings.samples_for_baselining, + z_offset_for_plunger_prep=2.0, ) except PipetteLiquidNotFoundError: # the move raises a liquid not found now since we don't call the move group and it doesn't @@ -1292,3 +1301,154 @@ def test_grip_error_detection( hard_max, hard_min, ) + + +def move_group_run_side_effect( + controller: OT3Controller, target_pos: Dict[Axis, float] +) -> Iterator[Dict[NodeId, MotorPositionStatus]]: + """Return homed position for axis that is present and was commanded to home.""" + motor_nodes = controller._motor_nodes() + target_nodes = {axis_to_node(ax): ax for ax in target_pos.keys() if ax != Axis.Q} + res = {} + for node in motor_nodes: + pos = 0.0 + if target_nodes.get(node): + pos = target_pos[target_nodes[node]] + res[node] = MotorPositionStatus(pos, pos, True, True, MoveCompleteAck(1)) + yield res + + +@pytest.mark.parametrize( + argnames=["origin_pos", "target_pos", "expected_pos", "gear_position"], + argvalues=[ + [ + { + Axis.X: 0, + Axis.Y: 0, + Axis.Z_L: 0, + Axis.Z_R: 0, + Axis.P_L: 0, + Axis.P_R: 0, + Axis.Z_G: 0, + Axis.G: 0, + Axis.Q: 0, + }, + {Axis.Q: 10}, + { + Axis.X: 0, + Axis.Y: 0, + Axis.Z_L: 0, + Axis.Z_R: 0, + Axis.P_L: 0, + Axis.P_R: 0, + Axis.Z_G: 0, + Axis.G: 0, + }, + 10, + ], + [ + { + Axis.X: 0, + Axis.Y: 0, + Axis.Z_L: 0, + Axis.Z_R: 0, + Axis.P_L: 0, + Axis.P_R: 0, + Axis.Z_G: 0, + Axis.G: 0, + }, + {Axis.Q: 10}, + { + Axis.X: 0, + Axis.Y: 0, + Axis.Z_L: 0, + Axis.Z_R: 0, + Axis.P_L: 0, + Axis.P_R: 0, + Axis.Z_G: 0, + Axis.G: 0, + }, + None, + ], + [ + { + Axis.X: 0, + Axis.Y: 0, + Axis.Z_L: 0, + Axis.Z_R: 0, + }, + { + Axis.X: 10, + Axis.Y: 10, + Axis.Z_L: 10, + }, + { + Axis.X: 10, + Axis.Y: 10, + Axis.Z_L: 10, + Axis.Z_R: 0, + Axis.P_L: 0, + Axis.P_R: 0, + Axis.Z_G: 0, + Axis.G: 0, + }, + None, + ], + ], +) +async def test_controller_move( + controller: OT3Controller, + mock_present_devices: mock.AsyncMock, + origin_pos: Dict[Axis, float], + target_pos: Dict[Axis, float], + expected_pos: Dict[Axis, float], + gear_position: Optional[float], +) -> None: + from copy import deepcopy + + controller.update_constraints_for_gantry_load(GantryLoad.HIGH_THROUGHPUT) + + run_target_pos = deepcopy(target_pos) + config = {"run.side_effect": move_group_run_side_effect(controller, run_target_pos)} + with mock.patch( # type: ignore [call-overload] + "opentrons.hardware_control.backends.ot3controller.MoveGroupRunner", + spec=MoveGroupRunner, + **config + ): + await controller.move(origin_pos, target_pos, 100) + position = await controller.update_position() + gear_position = controller.gear_motor_position + + assert position == expected_pos + assert gear_position == gear_position + + +@pytest.mark.parametrize( + argnames=["axes", "pipette_has_sensor"], + argvalues=[[[Axis.P_L, Axis.P_R], True], [[Axis.P_L, Axis.P_R], False]], +) +async def test_pressure_disable( + controller: OT3Controller, + axes: List[Axis], + mock_present_devices: None, + mock_check_overpressure: None, + pipette_has_sensor: bool, +) -> None: + config = {"run.side_effect": move_group_run_side_effect_home(controller, axes)} + with mock.patch( # type: ignore [call-overload] + "opentrons.hardware_control.backends.ot3controller.MoveGroupRunner", + spec=MoveGroupRunner, + **config + ): + with mock.patch.object(controller, "_monitor_overpressure") as monitor: + controller.set_pressure_sensor_available(Axis.P_L, pipette_has_sensor) + controller.set_pressure_sensor_available(Axis.P_R, True) + + await controller.home(axes, GantryLoad.LOW_THROUGHPUT) + + if pipette_has_sensor: + monitor.assert_called_once_with( + [NodeId.pipette_left, NodeId.pipette_right] + ) + else: + monitor.assert_called_once_with([NodeId.pipette_right]) diff --git a/api/tests/opentrons/hardware_control/backends/test_ot3_utils.py b/api/tests/opentrons/hardware_control/backends/test_ot3_utils.py index 0d081878dd1..d7125cfb027 100644 --- a/api/tests/opentrons/hardware_control/backends/test_ot3_utils.py +++ b/api/tests/opentrons/hardware_control/backends/test_ot3_utils.py @@ -1,12 +1,9 @@ import pytest from typing import List from opentrons_hardware.hardware_control.motion_planning import Move -from opentrons_hardware.hardware_control.motion import ( - create_step, -) from opentrons.hardware_control.backends import ot3utils from opentrons_hardware.firmware_bindings.constants import NodeId -from opentrons.hardware_control.types import Axis, OT3Mount +from opentrons.hardware_control.types import Axis, OT3Mount, OT3AxisKind from numpy import float64 as f64 from opentrons.config import defaults_ot3, types as conf_types @@ -39,31 +36,6 @@ def test_create_step() -> None: assert set(present_nodes) == set(step.keys()) -def test_get_moving_nodes() -> None: - """Test that we can filter out the nonmoving nodes.""" - # Create a dummy group where X has velocity but no accel, and Y has accel but no velocity. - present_nodes = [NodeId.gantry_x, NodeId.gantry_y, NodeId.head_l, NodeId.head_r] - move_group = [ - create_step( - distance={NodeId.gantry_x: f64(100), NodeId.gantry_y: f64(100)}, - velocity={NodeId.gantry_x: f64(100), NodeId.gantry_y: f64(0)}, - acceleration={NodeId.gantry_x: f64(0), NodeId.gantry_y: f64(100)}, - duration=f64(1), - present_nodes=present_nodes, - ) - ] - assert len(move_group[0]) == 4 - - print(move_group) - - moving_nodes = ot3utils.moving_axes_in_move_group(move_group) - assert len(moving_nodes) == 2 - assert NodeId.gantry_x in moving_nodes - assert NodeId.gantry_y in moving_nodes - assert NodeId.head_l not in moving_nodes - assert NodeId.head_r not in moving_nodes - - def test_filter_zero_duration_step() -> None: origin = { Axis.X: 0, @@ -123,6 +95,22 @@ def test_get_system_contraints_for_plunger() -> None: assert updated_contraints[axis].max_acceleration == set_acceleration +@pytest.mark.parametrize(["mount"], [[OT3Mount.LEFT], [OT3Mount.RIGHT]]) +def test_get_system_constraints_for_emulsifying_pipette(mount: OT3Mount) -> None: + set_max_speed = 90 + config = defaults_ot3.build_with_defaults({}) + pipette_ax = Axis.of_main_tool_actuator(mount) + default_pip_max_speed = config.motion_settings.default_max_speed[ + conf_types.GantryLoad.LOW_THROUGHPUT + ][OT3AxisKind.P] + updated_constraints = ot3utils.get_system_constraints_for_emulsifying_pipette( + config.motion_settings, conf_types.GantryLoad.LOW_THROUGHPUT, mount + ) + other_pipette = list(set(Axis.pipette_axes()) - {pipette_ax})[0] + assert updated_constraints[pipette_ax].max_speed == set_max_speed + assert updated_constraints[other_pipette].max_speed == default_pip_max_speed + + @pytest.mark.parametrize( ["moving", "expected"], [ @@ -154,15 +142,8 @@ def test_moving_pipettes_in_move_group( NodeId.gripper_g, NodeId.gripper_z, ] - move_group = [ - create_step( - distance={node: f64(100) for node in moving}, - velocity={node: f64(100) for node in moving}, - acceleration={node: f64(0) for node in moving}, - duration=f64(1), - present_nodes=present_nodes, - ) - ] - moving_pipettes = ot3utils.moving_pipettes_in_move_group(move_group) + moving_pipettes = ot3utils.moving_pipettes_in_move_group( + set(present_nodes), set(moving) + ) assert set(moving_pipettes) == set(expected) 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 6f9ad72c460..fd746ed9743 100644 --- a/api/tests/opentrons/hardware_control/instruments/test_instrument_calibration.py +++ b/api/tests/opentrons/hardware_control/instruments/test_instrument_calibration.py @@ -55,10 +55,10 @@ def tip_rack_dict() -> LabwareDefDict: @pytest.fixture def tip_rack_model() -> LabwareDefinition: """Get a tip rack Pydantic model definition value object.""" - return LabwareDefinition.construct( # type: ignore[call-arg] + return LabwareDefinition.model_construct( # type: ignore[call-arg] namespace="test", version=1, - parameters=Parameters.construct( # type: ignore[call-arg] + parameters=Parameters.model_construct( # type: ignore[call-arg] loadName="cool-labware", tipOverlap=None, # add a None value to validate serialization to dictionary ), diff --git a/api/tests/opentrons/hardware_control/instruments/test_nozzle_manager.py b/api/tests/opentrons/hardware_control/instruments/test_nozzle_manager.py index 5030bec31fe..066cdbc3caf 100644 --- a/api/tests/opentrons/hardware_control/instruments/test_nozzle_manager.py +++ b/api/tests/opentrons/hardware_control/instruments/test_nozzle_manager.py @@ -3,13 +3,14 @@ from opentrons.hardware_control import nozzle_manager -from opentrons.types import Point +from opentrons.types import Point, NozzleConfigurationType from opentrons_shared_data.pipette.load_data import load_definition from opentrons_shared_data.pipette.types import ( PipetteModelType, PipetteChannelType, PipetteVersionType, + PipetteOEMType, ) from opentrons_shared_data.pipette.pipette_definition import ( PipetteConfigurations, @@ -258,30 +259,24 @@ ], ) def test_single_pipettes_always_full( - pipette_details: Tuple[PipetteModelType, PipetteVersionType] + pipette_details: Tuple[PipetteModelType, PipetteVersionType], ) -> None: config = load_definition( - pipette_details[0], PipetteChannelType.SINGLE_CHANNEL, pipette_details[1] + pipette_details[0], + PipetteChannelType.SINGLE_CHANNEL, + pipette_details[1], + PipetteOEMType.OT, ) subject = nozzle_manager.NozzleConfigurationManager.build_from_config( config, ValidNozzleMaps(maps=A1) ) - assert ( - subject.current_configuration.configuration - == nozzle_manager.NozzleConfigurationType.FULL - ) + assert subject.current_configuration.configuration == NozzleConfigurationType.FULL subject.update_nozzle_configuration("A1", "A1", "A1") - assert ( - subject.current_configuration.configuration - == nozzle_manager.NozzleConfigurationType.FULL - ) + assert subject.current_configuration.configuration == NozzleConfigurationType.FULL subject.reset_to_default_configuration() - assert ( - subject.current_configuration.configuration - == nozzle_manager.NozzleConfigurationType.FULL - ) + assert subject.current_configuration.configuration == NozzleConfigurationType.FULL @pytest.mark.parametrize( @@ -295,10 +290,13 @@ def test_single_pipettes_always_full( ], ) def test_single_pipette_map_entries( - pipette_details: Tuple[PipetteModelType, PipetteVersionType] + pipette_details: Tuple[PipetteModelType, PipetteVersionType], ) -> None: config = load_definition( - pipette_details[0], PipetteChannelType.SINGLE_CHANNEL, pipette_details[1] + pipette_details[0], + PipetteChannelType.SINGLE_CHANNEL, + pipette_details[1], + PipetteOEMType.OT, ) subject = nozzle_manager.NozzleConfigurationManager.build_from_config( config, ValidNozzleMaps(maps=A1) @@ -332,10 +330,13 @@ def test_map_entries(nozzlemap: nozzle_manager.NozzleMap) -> None: ], ) def test_single_pipette_map_geometry( - pipette_details: Tuple[PipetteModelType, PipetteVersionType] + pipette_details: Tuple[PipetteModelType, PipetteVersionType], ) -> None: config = load_definition( - pipette_details[0], PipetteChannelType.SINGLE_CHANNEL, pipette_details[1] + pipette_details[0], + PipetteChannelType.SINGLE_CHANNEL, + pipette_details[1], + PipetteOEMType.OT, ) subject = nozzle_manager.NozzleConfigurationManager.build_from_config( config, ValidNozzleMaps(maps=A1) @@ -365,65 +366,56 @@ def test_map_geometry(nozzlemap: nozzle_manager.NozzleMap) -> None: ], ) def test_multi_config_identification( - pipette_details: Tuple[PipetteModelType, PipetteVersionType] + pipette_details: Tuple[PipetteModelType, PipetteVersionType], ) -> None: config = load_definition( - pipette_details[0], PipetteChannelType.EIGHT_CHANNEL, pipette_details[1] + pipette_details[0], + PipetteChannelType.EIGHT_CHANNEL, + pipette_details[1], + PipetteOEMType.OT, ) subject = nozzle_manager.NozzleConfigurationManager.build_from_config( config, ValidNozzleMaps(maps=EIGHT_CHANNEL_FULL | A1_D1 | A1 | H1), ) - assert ( - subject.current_configuration.configuration - == nozzle_manager.NozzleConfigurationType.FULL - ) + assert subject.current_configuration.configuration == NozzleConfigurationType.FULL subject.update_nozzle_configuration("A1", "H1", "A1") - assert ( - subject.current_configuration.configuration - == nozzle_manager.NozzleConfigurationType.FULL - ) + assert subject.current_configuration.configuration == NozzleConfigurationType.FULL subject.reset_to_default_configuration() - assert ( - subject.current_configuration.configuration - == nozzle_manager.NozzleConfigurationType.FULL - ) + assert subject.current_configuration.configuration == NozzleConfigurationType.FULL subject.update_nozzle_configuration("A1", "D1", "A1") assert ( cast( - nozzle_manager.NozzleConfigurationType, + NozzleConfigurationType, subject.current_configuration.configuration, ) - == nozzle_manager.NozzleConfigurationType.COLUMN + == NozzleConfigurationType.COLUMN ) subject.update_nozzle_configuration("A1", "A1", "A1") assert ( cast( - nozzle_manager.NozzleConfigurationType, + NozzleConfigurationType, subject.current_configuration.configuration, ) - == nozzle_manager.NozzleConfigurationType.SINGLE + == NozzleConfigurationType.SINGLE ) subject.update_nozzle_configuration("H1", "H1", "H1") assert ( cast( - nozzle_manager.NozzleConfigurationType, + NozzleConfigurationType, subject.current_configuration.configuration, ) - == nozzle_manager.NozzleConfigurationType.SINGLE + == NozzleConfigurationType.SINGLE ) subject.reset_to_default_configuration() - assert ( - subject.current_configuration.configuration - == nozzle_manager.NozzleConfigurationType.FULL - ) + assert subject.current_configuration.configuration == NozzleConfigurationType.FULL @pytest.mark.parametrize( @@ -437,10 +429,13 @@ def test_multi_config_identification( ], ) def test_multi_config_map_entries( - pipette_details: Tuple[PipetteModelType, PipetteVersionType] + pipette_details: Tuple[PipetteModelType, PipetteVersionType], ) -> None: config = load_definition( - pipette_details[0], PipetteChannelType.EIGHT_CHANNEL, pipette_details[1] + pipette_details[0], + PipetteChannelType.EIGHT_CHANNEL, + pipette_details[1], + PipetteOEMType.OT, ) subject = nozzle_manager.NozzleConfigurationManager.build_from_config( config, @@ -503,10 +498,13 @@ def assert_offset_in_center_of( ], ) def test_multi_config_geometry( - pipette_details: Tuple[PipetteModelType, PipetteVersionType] + pipette_details: Tuple[PipetteModelType, PipetteVersionType], ) -> None: config = load_definition( - pipette_details[0], PipetteChannelType.EIGHT_CHANNEL, pipette_details[1] + pipette_details[0], + PipetteChannelType.EIGHT_CHANNEL, + pipette_details[1], + PipetteOEMType.OT, ) subject = nozzle_manager.NozzleConfigurationManager.build_from_config( config, @@ -554,10 +552,13 @@ def test_map_geometry( "pipette_details", [(PipetteModelType.p1000, PipetteVersionType(major=3, minor=5))] ) def test_96_config_identification( - pipette_details: Tuple[PipetteModelType, PipetteVersionType] + pipette_details: Tuple[PipetteModelType, PipetteVersionType], ) -> None: config = load_definition( - pipette_details[0], PipetteChannelType.NINETY_SIX_CHANNEL, pipette_details[1] + pipette_details[0], + PipetteChannelType.NINETY_SIX_CHANNEL, + pipette_details[1], + PipetteOEMType.OT, ) subject = nozzle_manager.NozzleConfigurationManager.build_from_config( config, @@ -577,97 +578,91 @@ def test_96_config_identification( ), ) - assert ( - subject.current_configuration.configuration - == nozzle_manager.NozzleConfigurationType.FULL - ) + assert subject.current_configuration.configuration == NozzleConfigurationType.FULL subject.update_nozzle_configuration("A1", "H12") - assert ( - subject.current_configuration.configuration - == nozzle_manager.NozzleConfigurationType.FULL - ) + assert subject.current_configuration.configuration == NozzleConfigurationType.FULL subject.update_nozzle_configuration("A1", "H1") assert ( cast( - nozzle_manager.NozzleConfigurationType, + NozzleConfigurationType, subject.current_configuration.configuration, ) - == nozzle_manager.NozzleConfigurationType.COLUMN + == NozzleConfigurationType.COLUMN ) subject.update_nozzle_configuration("A12", "H12") assert ( cast( - nozzle_manager.NozzleConfigurationType, + NozzleConfigurationType, subject.current_configuration.configuration, ) - == nozzle_manager.NozzleConfigurationType.COLUMN + == NozzleConfigurationType.COLUMN ) subject.update_nozzle_configuration("A1", "A12") assert ( cast( - nozzle_manager.NozzleConfigurationType, + NozzleConfigurationType, subject.current_configuration.configuration, ) - == nozzle_manager.NozzleConfigurationType.ROW + == NozzleConfigurationType.ROW ) subject.update_nozzle_configuration("H1", "H12") assert ( cast( - nozzle_manager.NozzleConfigurationType, + NozzleConfigurationType, subject.current_configuration.configuration, ) - == nozzle_manager.NozzleConfigurationType.ROW + == NozzleConfigurationType.ROW ) subject.update_nozzle_configuration("E1", "H6") assert ( cast( - nozzle_manager.NozzleConfigurationType, + NozzleConfigurationType, subject.current_configuration.configuration, ) - == nozzle_manager.NozzleConfigurationType.SUBRECT + == NozzleConfigurationType.SUBRECT ) subject.update_nozzle_configuration("E7", "H12") assert ( cast( - nozzle_manager.NozzleConfigurationType, + NozzleConfigurationType, subject.current_configuration.configuration, ) - == nozzle_manager.NozzleConfigurationType.SUBRECT + == NozzleConfigurationType.SUBRECT ) subject.update_nozzle_configuration("A1", "B12") assert ( cast( - nozzle_manager.NozzleConfigurationType, + NozzleConfigurationType, subject.current_configuration.configuration, ) - == nozzle_manager.NozzleConfigurationType.SUBRECT + == NozzleConfigurationType.SUBRECT ) subject.update_nozzle_configuration("G1", "H12") assert ( cast( - nozzle_manager.NozzleConfigurationType, + NozzleConfigurationType, subject.current_configuration.configuration, ) - == nozzle_manager.NozzleConfigurationType.SUBRECT + == NozzleConfigurationType.SUBRECT ) subject.update_nozzle_configuration("A1", "H3") assert ( cast( - nozzle_manager.NozzleConfigurationType, + NozzleConfigurationType, subject.current_configuration.configuration, ) - == nozzle_manager.NozzleConfigurationType.SUBRECT + == NozzleConfigurationType.SUBRECT ) subject.update_nozzle_configuration("A10", "H12") assert ( cast( - nozzle_manager.NozzleConfigurationType, + NozzleConfigurationType, subject.current_configuration.configuration, ) - == nozzle_manager.NozzleConfigurationType.SUBRECT + == NozzleConfigurationType.SUBRECT ) @@ -675,10 +670,13 @@ def test_96_config_identification( "pipette_details", [(PipetteModelType.p1000, PipetteVersionType(major=3, minor=5))] ) def test_96_config_map_entries( - pipette_details: Tuple[PipetteModelType, PipetteVersionType] + pipette_details: Tuple[PipetteModelType, PipetteVersionType], ) -> None: config = load_definition( - pipette_details[0], PipetteChannelType.NINETY_SIX_CHANNEL, pipette_details[1] + pipette_details[0], + PipetteChannelType.NINETY_SIX_CHANNEL, + pipette_details[1], + PipetteOEMType.OT, ) subject = nozzle_manager.NozzleConfigurationManager.build_from_config( config, @@ -1012,10 +1010,13 @@ def _nozzles() -> Iterator[str]: "pipette_details", [(PipetteModelType.p1000, PipetteVersionType(major=3, minor=5))] ) def test_96_config_geometry( - pipette_details: Tuple[PipetteModelType, PipetteVersionType] + pipette_details: Tuple[PipetteModelType, PipetteVersionType], ) -> None: config = load_definition( - pipette_details[0], PipetteChannelType.NINETY_SIX_CHANNEL, pipette_details[1] + pipette_details[0], + PipetteChannelType.NINETY_SIX_CHANNEL, + pipette_details[1], + PipetteOEMType.OT, ) subject = nozzle_manager.NozzleConfigurationManager.build_from_config( config, diff --git a/api/tests/opentrons/hardware_control/test_gripper.py b/api/tests/opentrons/hardware_control/test_gripper.py index f084a11df89..fdce4f27d3d 100644 --- a/api/tests/opentrons/hardware_control/test_gripper.py +++ b/api/tests/opentrons/hardware_control/test_gripper.py @@ -104,8 +104,13 @@ def test_reload_instrument_cal_ot3_conf_changed( "fakeid123", jaw_max_offset=15, ) - new_conf = fake_gripper_conf.copy( - update={"grip_force_profile": {"default_grip_force": 1}} + new_conf = fake_gripper_conf.model_copy( + update={ + "grip_force_profile": fake_gripper_conf.grip_force_profile.model_copy( + update={"default_grip_force": 1} + ) + }, + deep=True, ) assert new_conf != old_gripper.config diff --git a/api/tests/opentrons/hardware_control/test_ot3_api.py b/api/tests/opentrons/hardware_control/test_ot3_api.py index 8d07999646e..cbb5838c266 100644 --- a/api/tests/opentrons/hardware_control/test_ot3_api.py +++ b/api/tests/opentrons/hardware_control/test_ot3_api.py @@ -59,14 +59,13 @@ EstopStateNotification, TipStateType, ) -from opentrons.hardware_control.nozzle_manager import NozzleConfigurationType from opentrons.hardware_control.errors import InvalidCriticalPoint from opentrons.hardware_control.ot3api import OT3API from opentrons.hardware_control import ThreadManager from opentrons.hardware_control.backends.ot3simulator import OT3Simulator from opentrons_hardware.firmware_bindings.constants import NodeId -from opentrons.types import Point, Mount +from opentrons.types import Point, Mount, NozzleConfigurationType from opentrons_hardware.hardware_control.motion_planning.types import Move @@ -83,6 +82,7 @@ PipetteChannelType, PipetteVersionType, LiquidClasses, + PipetteOEMType, ) from opentrons_shared_data.pipette import ( load_data as load_pipette_data, @@ -382,6 +382,7 @@ class PipetteLoadConfig(TypedDict): channels: Literal[1, 8, 96] version: Tuple[Literal[1, 2, 3], Literal[0, 1, 2, 3, 4, 5, 6]] model: PipetteModel + oem_type: PipetteOEMType class GripperLoadConfig(TypedDict): @@ -403,8 +404,24 @@ class GripperLoadConfig(TypedDict): ( ( [ - (OT3Mount.RIGHT, {"channels": 8, "version": (3, 3), "model": "p50"}), - (OT3Mount.LEFT, {"channels": 1, "version": (3, 3), "model": "p1000"}), + ( + OT3Mount.RIGHT, + { + "channels": 8, + "version": (3, 3), + "model": "p50", + "oem_type": PipetteOEMType.OT, + }, + ), + ( + OT3Mount.LEFT, + { + "channels": 1, + "version": (3, 3), + "model": "p1000", + "oem_type": PipetteOEMType.OT, + }, + ), ], GantryLoad.LOW_THROUGHPUT, ), @@ -414,34 +431,88 @@ class GripperLoadConfig(TypedDict): GantryLoad.LOW_THROUGHPUT, ), ( - [(OT3Mount.LEFT, {"channels": 8, "version": (3, 3), "model": "p1000"})], + [ + ( + OT3Mount.LEFT, + { + "channels": 8, + "version": (3, 3), + "model": "p1000", + "oem_type": "ot", + }, + ) + ], GantryLoad.LOW_THROUGHPUT, ), ( - [(OT3Mount.RIGHT, {"channels": 8, "version": (3, 3), "model": "p1000"})], + [ + ( + OT3Mount.RIGHT, + { + "channels": 8, + "version": (3, 3), + "model": "p1000", + "oem_type": "ot", + }, + ) + ], GantryLoad.LOW_THROUGHPUT, ), ( - [(OT3Mount.LEFT, {"channels": 96, "model": "p1000", "version": (3, 3)})], + [ + ( + OT3Mount.LEFT, + { + "channels": 96, + "model": "p1000", + "version": (3, 3), + "oem_type": "ot", + }, + ) + ], GantryLoad.HIGH_THROUGHPUT, ), ( [ - (OT3Mount.LEFT, {"channels": 1, "version": (3, 3), "model": "p1000"}), + ( + OT3Mount.LEFT, + { + "channels": 1, + "version": (3, 3), + "model": "p1000", + "oem_type": "ot", + }, + ), (OT3Mount.GRIPPER, {"model": GripperModel.v1, "id": "g12345"}), ], GantryLoad.LOW_THROUGHPUT, ), ( [ - (OT3Mount.RIGHT, {"channels": 8, "version": (3, 3), "model": "p1000"}), + ( + OT3Mount.RIGHT, + { + "channels": 8, + "version": (3, 3), + "model": "p1000", + "oem_type": "ot", + }, + ), (OT3Mount.GRIPPER, {"model": GripperModel.v1, "id": "g12345"}), ], GantryLoad.LOW_THROUGHPUT, ), ( [ - (OT3Mount.LEFT, {"channels": 96, "model": "p1000", "version": (3, 3)}), + ( + OT3Mount.LEFT, + { + "channels": 96, + "model": "p1000", + "version": (3, 3), + "oem_type": "ot", + }, + ), (OT3Mount.GRIPPER, {"model": GripperModel.v1, "id": "g12345"}), ], GantryLoad.HIGH_THROUGHPUT, @@ -464,6 +535,7 @@ async def test_gantry_load_transform( PipetteModelType(pair[1]["model"]), PipetteChannelType(pair[1]["channels"]), PipetteVersionType(*pair[1]["version"]), + PipetteOEMType(pair[1]["oem_type"]), ) instr_data = AttachedPipette(config=pipette_config, id="fakepip") await ot3_hardware.cache_pipette(pair[0], instr_data, None) @@ -558,9 +630,30 @@ def mock_verify_tip_presence( load_pipette_configs = [ - {OT3Mount.LEFT: {"channels": 1, "version": (3, 3), "model": "p1000"}}, - {OT3Mount.RIGHT: {"channels": 8, "version": (3, 3), "model": "p50"}}, - {OT3Mount.LEFT: {"channels": 96, "model": "p1000", "version": (3, 3)}}, + { + OT3Mount.LEFT: { + "channels": 1, + "version": (3, 3), + "model": "p1000", + "oem_type": PipetteOEMType.OT, + } + }, + { + OT3Mount.RIGHT: { + "channels": 8, + "version": (3, 3), + "model": "p50", + "oem_type": PipetteOEMType.OT, + } + }, + { + OT3Mount.LEFT: { + "channels": 96, + "model": "p1000", + "version": (3, 3), + "oem_type": PipetteOEMType.OT, + } + }, ] @@ -574,6 +667,7 @@ async def prepare_for_mock_blowout( PipetteModelType(configs["model"]), PipetteChannelType(configs["channels"]), PipetteVersionType(*configs["version"]), + PipetteOEMType(configs["oem_type"]), ) instr_data = AttachedPipette(config=pipette_config, id="fakepip") await ot3_hardware.cache_pipette(mount, instr_data, None) @@ -668,7 +762,11 @@ async def test_pickup_moves( @pytest.mark.parametrize("load_configs", load_pipette_configs) @given(blowout_volume=strategies.floats(min_value=0, max_value=10)) -@settings(suppress_health_check=[HealthCheck.function_scoped_fixture], max_examples=10) +@settings( + suppress_health_check=[HealthCheck.function_scoped_fixture], + max_examples=10, + deadline=400, +) @example(blowout_volume=0.0) async def test_blow_out_position( ot3_hardware: ThreadManager[OT3API], @@ -801,7 +899,10 @@ async def test_liquid_probe( ) -> None: instr_data = AttachedPipette( config=load_pipette_data.load_definition( - PipetteModelType("p1000"), PipetteChannelType(1), PipetteVersionType(3, 4) + PipetteModelType("p1000"), + PipetteChannelType(1), + PipetteVersionType(3, 4), + PipetteOEMType.OT, ), id="fakepip", ) @@ -855,6 +956,7 @@ async def test_liquid_probe( fake_settings_aspirate.sensor_threshold_pascals, fake_settings_aspirate.plunger_impulse_time, fake_settings_aspirate.samples_for_baselining, + probe_safe_reset_mm, probe=InstrumentProbeType.PRIMARY, force_both_sensors=False, response_queue=None, @@ -891,7 +993,10 @@ async def test_liquid_probe_plunger_moves( # when approaching its max z distance instr_data = AttachedPipette( config=load_pipette_data.load_definition( - PipetteModelType("p1000"), PipetteChannelType(1), PipetteVersionType(3, 4) + PipetteModelType("p1000"), + PipetteChannelType(1), + PipetteVersionType(3, 4), + PipetteOEMType.OT, ), id="fakepip", ) @@ -998,7 +1103,10 @@ async def test_liquid_probe_mount_moves( """Verify move targets for one singular liquid pass probe.""" instr_data = AttachedPipette( config=load_pipette_data.load_definition( - PipetteModelType("p1000"), PipetteChannelType(1), PipetteVersionType(3, 4) + PipetteModelType("p1000"), + PipetteChannelType(1), + PipetteVersionType(3, 4), + PipetteOEMType.OT, ), id="fakepip", ) @@ -1060,7 +1168,10 @@ async def test_multi_liquid_probe( ) -> None: instr_data = AttachedPipette( config=load_pipette_data.load_definition( - PipetteModelType("p1000"), PipetteChannelType(1), PipetteVersionType(3, 4) + PipetteModelType("p1000"), + PipetteChannelType(1), + PipetteVersionType(3, 4), + PipetteOEMType.OT, ), id="fakepip", ) @@ -1111,6 +1222,7 @@ async def test_multi_liquid_probe( fake_settings_aspirate.sensor_threshold_pascals, fake_settings_aspirate.plunger_impulse_time, fake_settings_aspirate.samples_for_baselining, + 2.0, probe=InstrumentProbeType.PRIMARY, force_both_sensors=False, response_queue=None, @@ -1126,7 +1238,10 @@ async def test_liquid_not_found( ) -> None: instr_data = AttachedPipette( config=load_pipette_data.load_definition( - PipetteModelType("p1000"), PipetteChannelType(1), PipetteVersionType(3, 4) + PipetteModelType("p1000"), + PipetteChannelType(1), + PipetteVersionType(3, 4), + PipetteOEMType.OT, ), id="fakepip", ) @@ -1146,6 +1261,7 @@ async def _fake_pos_update_and_raise( threshold_pascals: float, plunger_impulse_time: float, num_baseline_reads: int, + z_offset_for_plunger_prep: float, probe: InstrumentProbeType = InstrumentProbeType.PRIMARY, force_both_sensors: bool = False, response_queue: Optional[ @@ -1593,7 +1709,10 @@ async def test_home_plunger( mount = OT3Mount.LEFT instr_data = AttachedPipette( config=load_pipette_data.load_definition( - PipetteModelType("p1000"), PipetteChannelType(1), PipetteVersionType(3, 4) + PipetteModelType("p1000"), + PipetteChannelType(1), + PipetteVersionType(3, 4), + PipetteOEMType.OT, ), id="fakepip", ) @@ -1615,6 +1734,7 @@ async def test_prepare_for_aspirate( PipetteModelType("p1000"), PipetteChannelType(1), PipetteVersionType(3, 4), + PipetteOEMType.OT, ), id="fakepip", ) @@ -1649,6 +1769,7 @@ async def test_plunger_ready_to_aspirate_after_dispense( PipetteModelType("p1000"), PipetteChannelType(1), PipetteVersionType(3, 4), + PipetteOEMType.OT, ), id="fakepip", ) @@ -1674,7 +1795,10 @@ async def test_move_to_plunger_bottom( mount = OT3Mount.LEFT instr_data = AttachedPipette( config=load_pipette_data.load_definition( - PipetteModelType("p1000"), PipetteChannelType(1), PipetteVersionType(3, 4) + PipetteModelType("p1000"), + PipetteChannelType(1), + PipetteVersionType(3, 4), + PipetteOEMType.OT, ), id="fakepip", ) @@ -1771,7 +1895,9 @@ async def test_move_axes( await ot3_hardware.move_axes(position=input_position) mock_check_motor.return_value = True - mock_move.assert_called_once_with(target_position=expected_move_pos, speed=None) + mock_move.assert_called_once_with( + target_position=expected_move_pos, speed=None, expect_stalls=False + ) async def test_move_gripper_mount_without_gripper_attached( @@ -1789,14 +1915,14 @@ async def test_move_expect_stall_flag( expected = HWStopCondition.stall if expect_stalls else HWStopCondition.none - await ot3_hardware.move_to(Mount.LEFT, Point(0, 0, 0), _expect_stalls=expect_stalls) + await ot3_hardware.move_to(Mount.LEFT, Point(0, 0, 0), expect_stalls=expect_stalls) mock_backend_move.assert_called_once() _, _, _, condition = mock_backend_move.call_args_list[0][0] assert condition == expected mock_backend_move.reset_mock() await ot3_hardware.move_rel( - Mount.LEFT, Point(10, 0, 0), _expect_stalls=expect_stalls + Mount.LEFT, Point(10, 0, 0), expect_stalls=expect_stalls ) mock_backend_move.assert_called_once() _, _, _, condition = mock_backend_move.call_args_list[0][0] @@ -2026,12 +2152,10 @@ def set_mock_plunger_configs() -> None: assert len(tip_action.call_args_list) == 2 # first call should be "clamp", moving down first_target = tip_action.call_args_list[0][-1]["targets"][0][0] - assert list(first_target.keys()) == [Axis.Q] - assert first_target[Axis.Q] == 10 + assert first_target == 10 # next call should be "clamp", moving back up second_target = tip_action.call_args_list[1][-1]["targets"][0][0] - assert list(second_target.keys()) == [Axis.Q] - assert second_target[Axis.Q] < 10 + assert second_target < 10 # home should be called after tip_action is done assert len(mock_home_gear_motors.call_args_list) == 1 @@ -2126,6 +2250,7 @@ async def test_home_axis( PipetteModelType("p1000"), PipetteChannelType(1), PipetteVersionType(3, 3), + PipetteOEMType.OT, ) instr_data = AttachedPipette(config=pipette_config, id="fakepip") await ot3_hardware.cache_pipette(Axis.to_ot3_mount(axis), instr_data, None) diff --git a/api/tests/opentrons/hardware_control/test_pipette.py b/api/tests/opentrons/hardware_control/test_pipette.py index 25ac7b0298a..610fcc2a022 100644 --- a/api/tests/opentrons/hardware_control/test_pipette.py +++ b/api/tests/opentrons/hardware_control/test_pipette.py @@ -73,7 +73,10 @@ def _create_pipette( ) -> ot3_pipette.Pipette: return ot3_pipette.Pipette( load_pipette_data.load_definition( - model.pipette_type, model.pipette_channels, model.pipette_version + model.pipette_type, + model.pipette_channels, + model.pipette_version, + model.oem_type, ), calibration, id, diff --git a/api/tests/opentrons/protocol_api/core/engine/test_deck_conflict.py b/api/tests/opentrons/protocol_api/core/engine/test_deck_conflict.py index 05a09ac452c..208ac843b94 100644 --- a/api/tests/opentrons/protocol_api/core/engine/test_deck_conflict.py +++ b/api/tests/opentrons/protocol_api/core/engine/test_deck_conflict.py @@ -7,7 +7,6 @@ from opentrons_shared_data.robot.types import RobotType from opentrons.hardware_control import CriticalPoint -from opentrons.hardware_control.nozzle_manager import NozzleConfigurationType from opentrons.motion_planning import deck_conflict as wrapped_deck_conflict from opentrons.motion_planning import adjacent_slots_getters from opentrons.motion_planning.adjacent_slots_getters import _MixedTypeSlots @@ -31,7 +30,13 @@ from opentrons.protocol_engine.clients import SyncClient from opentrons.protocol_engine.errors import LabwareNotLoadedOnModuleError -from opentrons.types import DeckSlotName, Point, StagingSlotName, MountType +from opentrons.types import ( + DeckSlotName, + Point, + StagingSlotName, + MountType, + NozzleConfigurationType, +) from opentrons.protocol_engine.types import ( DeckType, 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 0ab9ac9da73..73f39006299 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 @@ -1,16 +1,20 @@ """Test for the ProtocolEngine-based instrument API core.""" + from typing import cast, Optional from opentrons_shared_data.errors.exceptions import PipetteLiquidNotFoundError import pytest from decoy import Decoy from decoy import errors +from opentrons_shared_data.liquid_classes.liquid_class_definition import ( + LiquidClassSchemaV1, +) from opentrons_shared_data.pipette.types import PipetteNameType from opentrons.hardware_control import SyncHardwareAPI from opentrons.hardware_control.dev_types import PipetteDict -from opentrons.hardware_control.nozzle_manager import NozzleConfigurationType +from opentrons.protocol_api._liquid_properties import TransferProperties from opentrons.protocol_engine import ( DeckPoint, LoadedPipette, @@ -36,6 +40,7 @@ SingleNozzleLayoutConfiguration, ColumnNozzleLayoutConfiguration, AddressableOffsetVector, + LiquidClassRecord, ) from opentrons.protocol_api.disposal_locations import ( TrashBin, @@ -43,6 +48,7 @@ DisposalOffset, ) from opentrons.protocol_api._nozzle_layout import NozzleLayout +from opentrons.protocol_api._liquid import LiquidClass from opentrons.protocol_api.core.engine import ( InstrumentCore, WellCore, @@ -51,7 +57,7 @@ ) from opentrons.protocols.api_support.definitions import MAX_SUPPORTED_VERSION from opentrons.protocols.api_support.types import APIVersion -from opentrons.types import Location, Mount, MountType, Point +from opentrons.types import Location, Mount, MountType, Point, NozzleConfigurationType from ... import versions_below, versions_at_or_above @@ -94,7 +100,7 @@ def subject( ) -> InstrumentCore: """Get a InstrumentCore test subject with its dependencies mocked out.""" decoy.when(mock_engine_client.state.pipettes.get("abc123")).then_return( - LoadedPipette.construct(mount=MountType.LEFT) # type: ignore[call-arg] + LoadedPipette.model_construct(mount=MountType.LEFT) # type: ignore[call-arg] ) decoy.when(mock_engine_client.state.pipettes.get_flow_rates("abc123")).then_return( @@ -125,7 +131,7 @@ def test_get_pipette_name( ) -> None: """It should get the pipette's load name.""" decoy.when(mock_engine_client.state.pipettes.get("abc123")).then_return( - LoadedPipette.construct(pipetteName=PipetteNameType.P300_SINGLE) # type: ignore[call-arg] + LoadedPipette.model_construct(pipetteName=PipetteNameType.P300_SINGLE) # type: ignore[call-arg] ) result = subject.get_pipette_name() @@ -138,7 +144,7 @@ def test_get_mount( ) -> None: """It should get the pipette's mount.""" decoy.when(mock_engine_client.state.pipettes.get("abc123")).then_return( - LoadedPipette.construct(mount=MountType.LEFT) # type: ignore[call-arg] + LoadedPipette.model_construct(mount=MountType.LEFT) # type: ignore[call-arg] ) result = subject.get_mount() @@ -156,7 +162,7 @@ def test_get_hardware_state( pipette_dict = cast(PipetteDict, {"display_name": "Cool Pipette", "has_tip": True}) decoy.when(mock_engine_client.state.pipettes.get("abc123")).then_return( - LoadedPipette.construct(mount=MountType.LEFT) # type: ignore[call-arg] + LoadedPipette.model_construct(mount=MountType.LEFT) # type: ignore[call-arg] ) decoy.when(mock_sync_hardware.get_attached_instrument(Mount.LEFT)).then_return( pipette_dict @@ -525,7 +531,7 @@ def test_aspirate_from_well( pipette_id="abc123", labware_id="123abc", well_name="my cool well", - well_location=WellLocation( + well_location=LiquidHandlingWellLocation( origin=WellOrigin.TOP, offset=WellOffset(x=3, y=2, z=1) ), ), @@ -823,7 +829,7 @@ def test_dispense_to_well( pipette_id="abc123", labware_id="123abc", well_name="my cool well", - well_location=WellLocation( + well_location=LiquidHandlingWellLocation( origin=WellOrigin.TOP, offset=WellOffset(x=3, y=2, z=1) ), ), @@ -1433,7 +1439,7 @@ def test_detect_liquid_presence( ) ) ).then_return( - cmd.TryLiquidProbeResult.construct( + cmd.TryLiquidProbeResult.model_construct( z_position=returned_from_engine, position=object(), # type: ignore[arg-type] ) @@ -1495,3 +1501,59 @@ def test_liquid_probe_with_recovery( ) ) ) + + +def test_load_liquid_class( + decoy: Decoy, + mock_engine_client: EngineClient, + subject: InstrumentCore, + minimal_liquid_class_def2: LiquidClassSchemaV1, +) -> None: + """It should send the load liquid class command to the engine.""" + sample_aspirate_data = minimal_liquid_class_def2.byPipette[0].byTipType[0].aspirate + sample_single_dispense_data = ( + minimal_liquid_class_def2.byPipette[0].byTipType[0].singleDispense + ) + sample_multi_dispense_data = ( + minimal_liquid_class_def2.byPipette[0].byTipType[0].multiDispense + ) + + test_liq_class = decoy.mock(cls=LiquidClass) + test_transfer_props = decoy.mock(cls=TransferProperties) + + decoy.when( + test_liq_class.get_for("flex_1channel_50", "opentrons_flex_96_tiprack_50ul") + ).then_return(test_transfer_props) + decoy.when(test_liq_class.name).then_return("water") + decoy.when( + mock_engine_client.state.pipettes.get_model_name(subject.pipette_id) + ).then_return("flex_1channel_50") + decoy.when(test_transfer_props.aspirate.as_shared_data_model()).then_return( + sample_aspirate_data + ) + decoy.when(test_transfer_props.dispense.as_shared_data_model()).then_return( + sample_single_dispense_data + ) + decoy.when(test_transfer_props.multi_dispense.as_shared_data_model()).then_return( # type: ignore[union-attr] + sample_multi_dispense_data + ) + decoy.when( + mock_engine_client.execute_command_without_recovery( + cmd.LoadLiquidClassParams( + liquidClassRecord=LiquidClassRecord( + liquidClassName="water", + pipetteModel="flex_1channel_50", + tiprack="opentrons_flex_96_tiprack_50ul", + aspirate=sample_aspirate_data, + singleDispense=sample_single_dispense_data, + multiDispense=sample_multi_dispense_data, + ) + ) + ) + ).then_return(cmd.LoadLiquidClassResult(liquidClassId="liquid-class-id")) + result = subject.load_liquid_class( + liquid_class=test_liq_class, + pipette_load_name="flex_1channel_50", + tiprack_uri="opentrons_flex_96_tiprack_50ul", + ) + assert result == "liquid-class-id" diff --git a/api/tests/opentrons/protocol_api/core/engine/test_labware_core.py b/api/tests/opentrons/protocol_api/core/engine/test_labware_core.py index 847c80d2125..beca8fe99d1 100644 --- a/api/tests/opentrons/protocol_api/core/engine/test_labware_core.py +++ b/api/tests/opentrons/protocol_api/core/engine/test_labware_core.py @@ -1,4 +1,5 @@ """Tests for opentrons.protocol_api.core.engine.LabwareCore.""" + from typing import cast import pytest @@ -25,7 +26,7 @@ LabwareOffsetLocation, LabwareOffsetVector, ) - +from opentrons.protocol_api._liquid import Liquid from opentrons.protocol_api.core.labware import LabwareLoadParams from opentrons.protocol_api.core.engine import LabwareCore, WellCore from opentrons.calibration_storage.helpers import uri_from_details @@ -34,7 +35,7 @@ @pytest.fixture def labware_definition() -> LabwareDefinition: """Get a LabwareDefinition value object to use in tests.""" - return LabwareDefinition.construct(ordering=[]) # type: ignore[call-arg] + return LabwareDefinition.model_construct(ordering=[]) # type: ignore[call-arg] @pytest.fixture @@ -58,10 +59,10 @@ def subject(mock_engine_client: EngineClient) -> LabwareCore: @pytest.mark.parametrize( "labware_definition", [ - LabwareDefinition.construct( # type: ignore[call-arg] + LabwareDefinition.model_construct( # type: ignore[call-arg] namespace="hello", version=42, - parameters=LabwareDefinitionParameters.construct(loadName="world"), # type: ignore[call-arg] + parameters=LabwareDefinitionParameters.model_construct(loadName="world"), # type: ignore[call-arg] ordering=[], ) ], @@ -75,12 +76,14 @@ def test_get_load_params(subject: LabwareCore) -> None: @pytest.mark.parametrize( "labware_definition", [ - LabwareDefinition.construct( # type: ignore[call-arg] + LabwareDefinition.model_construct( # type: ignore[call-arg] namespace="hello", version=42, - parameters=LabwareDefinitionParameters.construct(loadName="world"), # type: ignore[call-arg] + parameters=LabwareDefinitionParameters.model_construct(loadName="world"), # type: ignore[call-arg] ordering=[], - metadata=LabwareDefinitionMetadata.construct(displayName="what a cool labware"), # type: ignore[call-arg] + metadata=LabwareDefinitionMetadata.model_construct( # type: ignore[call-arg] + displayName="what a cool labware" + ), ) ], ) @@ -127,10 +130,10 @@ def test_set_calibration_succeeds_in_ok_location( @pytest.mark.parametrize( "labware_definition", [ - LabwareDefinition.construct( # type: ignore[call-arg] + LabwareDefinition.model_construct( # type: ignore[call-arg] namespace="hello", version=42, - parameters=LabwareDefinitionParameters.construct(loadName="world"), # type: ignore[call-arg] + parameters=LabwareDefinitionParameters.model_construct(loadName="world"), # type: ignore[call-arg] ordering=[], ) ], @@ -161,9 +164,9 @@ def test_set_calibration_fails_in_bad_location( @pytest.mark.parametrize( "labware_definition", [ - LabwareDefinition.construct( # type: ignore[call-arg] + LabwareDefinition.model_construct( # type: ignore[call-arg] namespace="hello", - parameters=LabwareDefinitionParameters.construct(loadName="world"), # type: ignore[call-arg] + parameters=LabwareDefinitionParameters.model_construct(loadName="world"), # type: ignore[call-arg] ordering=[], allowedRoles=[], stackingOffsetWithLabware={}, @@ -205,9 +208,9 @@ def test_get_user_display_name(decoy: Decoy, mock_engine_client: EngineClient) - @pytest.mark.parametrize( "labware_definition", [ - LabwareDefinition.construct( # type: ignore[call-arg] + LabwareDefinition.model_construct( # type: ignore[call-arg] ordering=[], - metadata=LabwareDefinitionMetadata.construct( # type: ignore[call-arg] + metadata=LabwareDefinitionMetadata.model_construct( # type: ignore[call-arg] displayName="Cool Display Name" ), ) @@ -223,8 +226,10 @@ def test_get_display_name(subject: LabwareCore) -> None: @pytest.mark.parametrize( "labware_definition", [ - LabwareDefinition.construct( # type: ignore[call-arg] - parameters=LabwareDefinitionParameters.construct(loadName="load-name"), # type: ignore[call-arg] + LabwareDefinition.model_construct( # type: ignore[call-arg] + parameters=LabwareDefinitionParameters.model_construct( # type: ignore[call-arg] + loadName="load-name" + ), ), ], ) @@ -251,9 +256,9 @@ def test_get_name_display_name(decoy: Decoy, mock_engine_client: EngineClient) - @pytest.mark.parametrize( "labware_definition", [ - LabwareDefinition.construct( # type: ignore[call-arg] + LabwareDefinition.model_construct( # type: ignore[call-arg] ordering=[], - parameters=LabwareDefinitionParameters.construct(isTiprack=True), # type: ignore[call-arg] + parameters=LabwareDefinitionParameters.model_construct(isTiprack=True), # type: ignore[call-arg] ) ], ) @@ -268,13 +273,13 @@ def test_is_tip_rack(subject: LabwareCore) -> None: argnames=["labware_definition", "expected_result"], argvalues=[ ( - LabwareDefinition.construct( # type: ignore[call-arg] + LabwareDefinition.model_construct( # type: ignore[call-arg] ordering=[], allowedRoles=[LabwareRole.adapter] ), True, ), ( - LabwareDefinition.construct( # type: ignore[call-arg] + LabwareDefinition.model_construct( # type: ignore[call-arg] ordering=[], allowedRoles=[LabwareRole.labware] ), False, @@ -291,7 +296,7 @@ def test_is_adapter(expected_result: bool, subject: LabwareCore) -> None: @pytest.mark.parametrize( "labware_definition", [ - LabwareDefinition.construct( # type: ignore[call-arg] + LabwareDefinition.model_construct( # type: ignore[call-arg] ordering=[["A1", "B1"], ["A2", "B2"]], ) ], @@ -351,9 +356,9 @@ def test_get_next_tip( @pytest.mark.parametrize( "labware_definition", [ - LabwareDefinition.construct( # type: ignore[call-arg] + LabwareDefinition.model_construct( # type: ignore[call-arg] ordering=[], - parameters=LabwareDefinitionParameters.construct(isTiprack=True), # type: ignore[call-arg] + parameters=LabwareDefinitionParameters.model_construct(isTiprack=True), # type: ignore[call-arg] ) ], ) @@ -368,10 +373,10 @@ def test_reset_tips( @pytest.mark.parametrize( "labware_definition", [ - LabwareDefinition.construct( # type: ignore[call-arg] + LabwareDefinition.model_construct( # type: ignore[call-arg] ordering=[], - parameters=LabwareDefinitionParameters.construct(isTiprack=False), # type: ignore[call-arg] - metadata=LabwareDefinitionMetadata.construct( # type: ignore[call-arg] + parameters=LabwareDefinitionParameters.model_construct(isTiprack=False), # type: ignore[call-arg] + metadata=LabwareDefinitionMetadata.model_construct( # type: ignore[call-arg] displayName="Cool Display Name" ), ) @@ -427,9 +432,9 @@ def test_get_calibrated_offset( @pytest.mark.parametrize( "labware_definition", [ - LabwareDefinition.construct( # type: ignore[call-arg] + LabwareDefinition.model_construct( # type: ignore[call-arg] ordering=[], - parameters=LabwareDefinitionParameters.construct(quirks=["quirk"]), # type: ignore[call-arg] + parameters=LabwareDefinitionParameters.model_construct(quirks=["quirk"]), # type: ignore[call-arg] ) ], ) @@ -455,3 +460,40 @@ def test_get_deck_slot( ).then_raise(LabwareNotOnDeckError("oh no")) assert subject.get_deck_slot() is None + + +def test_load_liquid( + decoy: Decoy, mock_engine_client: EngineClient, subject: LabwareCore +) -> None: + """It should pass loaded liquids to the engine.""" + mock_liquid = Liquid( + _id="liquid-id", name="water", description=None, display_color=None + ) + subject.load_liquid(volumes={"A1": 20, "B1": 30, "C1": 40}, liquid=mock_liquid) + + decoy.verify( + mock_engine_client.execute_command( + cmd.LoadLiquidParams( + labwareId="cool-labware", + liquidId="liquid-id", + volumeByWell={"A1": 20, "B1": 30, "C1": 40}, + ) + ), + times=1, + ) + + +def test_load_empty( + decoy: Decoy, mock_engine_client: EngineClient, subject: LabwareCore +) -> None: + """It should pass empty liquids to the engine.""" + subject.load_empty(wells=["A1", "B1", "C1"]) + decoy.verify( + mock_engine_client.execute_command( + cmd.LoadLiquidParams( + labwareId="cool-labware", + liquidId="EMPTY", + volumeByWell={"A1": 0.0, "B1": 0.0, "C1": 0.0}, + ) + ) + ) diff --git a/api/tests/opentrons/protocol_api/core/engine/test_module_core.py b/api/tests/opentrons/protocol_api/core/engine/test_module_core.py index f18a672afb8..a1310999d72 100644 --- a/api/tests/opentrons/protocol_api/core/engine/test_module_core.py +++ b/api/tests/opentrons/protocol_api/core/engine/test_module_core.py @@ -1,4 +1,5 @@ """Tests for opentrons.protocol_api.core.engine.ModuleCore.""" + import pytest import inspect from decoy import Decoy @@ -107,7 +108,7 @@ def test_get_display_name( decoy: Decoy, subject: ModuleCore, mock_engine_client: EngineClient ) -> None: """It should return the module display name.""" - module_definition = ModuleDefinition.construct( # type: ignore[call-arg] + module_definition = ModuleDefinition.model_construct( # type: ignore[call-arg] displayName="abra kadabra", ) decoy.when(mock_engine_client.state.modules.get_definition("1234")).then_return( 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 7b549fc035d..2889a47cea9 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 @@ -1,4 +1,5 @@ """Test for the ProtocolEngine-based protocol API core.""" + import inspect from typing import Optional, Type, cast, Tuple @@ -7,7 +8,6 @@ from opentrons_shared_data.liquid_classes.liquid_class_definition import ( LiquidClassSchemaV1, ) -from pytest_lazyfixture import lazy_fixture # type: ignore[import-untyped] from decoy import Decoy from opentrons_shared_data.deck import load as load_deck @@ -26,7 +26,7 @@ from opentrons.types import DeckSlotName, StagingSlotName, Mount, MountType, Point from opentrons.protocol_api import OFF_DECK from opentrons.hardware_control import SyncHardwareAPI, SynchronousAdapter -from opentrons.hardware_control.modules import AbstractModule, ModuleType +from opentrons.hardware_control.modules import AbstractModule from opentrons.hardware_control.modules.types import ( ModuleModel, TemperatureModuleModel, @@ -180,7 +180,7 @@ def subject( decoy.when( mock_engine_client.state.labware.get_definition("fixed-trash-123") ).then_return( - LabwareDefinition.construct(ordering=[["A1"]]) # type: ignore[call-arg] + LabwareDefinition.model_construct(ordering=[["A1"]]) # type: ignore[call-arg] ) return ProtocolCore( @@ -359,13 +359,13 @@ def test_load_labware( ).then_return( commands.LoadLabwareResult( labwareId="abc123", - definition=LabwareDefinition.construct(), # type: ignore[call-arg] + definition=LabwareDefinition.model_construct(), # type: ignore[call-arg] offsetId=None, ) ) decoy.when(mock_engine_client.state.labware.get_definition("abc123")).then_return( - LabwareDefinition.construct(ordering=[]) # type: ignore[call-arg] + LabwareDefinition.model_construct(ordering=[]) # type: ignore[call-arg] ) result = subject.load_labware( @@ -395,7 +395,7 @@ def test_load_labware( slot_name=DeckSlotName.SLOT_5, ) ).then_return( - LoadedLabware.construct(id="abc123") # type: ignore[call-arg] + LoadedLabware.model_construct(id="abc123") # type: ignore[call-arg] ) assert subject.get_slot_item(DeckSlotName.SLOT_5) is result @@ -433,13 +433,13 @@ def test_load_labware_on_staging_slot( ).then_return( commands.LoadLabwareResult( labwareId="abc123", - definition=LabwareDefinition.construct(), # type: ignore[call-arg] + definition=LabwareDefinition.model_construct(), # type: ignore[call-arg] offsetId=None, ) ) decoy.when(mock_engine_client.state.labware.get_definition("abc123")).then_return( - LabwareDefinition.construct(ordering=[]) # type: ignore[call-arg] + LabwareDefinition.model_construct(ordering=[]) # type: ignore[call-arg] ) result = subject.load_labware( @@ -469,7 +469,7 @@ def test_load_labware_on_staging_slot( slot_name=StagingSlotName.SLOT_B4, ) ).then_return( - LoadedLabware.construct(id="abc123") # type: ignore[call-arg] + LoadedLabware.model_construct(id="abc123") # type: ignore[call-arg] ) assert subject.get_slot_item(StagingSlotName.SLOT_B4) is result @@ -510,13 +510,13 @@ def test_load_labware_on_labware( ).then_return( commands.LoadLabwareResult( labwareId="abc123", - definition=LabwareDefinition.construct(), # type: ignore[call-arg] + definition=LabwareDefinition.model_construct(), # type: ignore[call-arg] offsetId=None, ) ) decoy.when(mock_engine_client.state.labware.get_definition("abc123")).then_return( - LabwareDefinition.construct(ordering=[]) # type: ignore[call-arg] + LabwareDefinition.model_construct(ordering=[]) # type: ignore[call-arg] ) decoy.when( @@ -580,13 +580,13 @@ def test_load_labware_off_deck( ).then_return( commands.LoadLabwareResult( labwareId="abc123", - definition=LabwareDefinition.construct(), # type: ignore[call-arg] + definition=LabwareDefinition.model_construct(), # type: ignore[call-arg] offsetId=None, ) ) decoy.when(mock_engine_client.state.labware.get_definition("abc123")).then_return( - LabwareDefinition.construct(ordering=[]) # type: ignore[call-arg] + LabwareDefinition.model_construct(ordering=[]) # type: ignore[call-arg] ) result = subject.load_labware( @@ -643,13 +643,13 @@ def test_load_adapter( ).then_return( commands.LoadLabwareResult( labwareId="abc123", - definition=LabwareDefinition.construct(), # type: ignore[call-arg] + definition=LabwareDefinition.model_construct(), # type: ignore[call-arg] offsetId=None, ) ) decoy.when(mock_engine_client.state.labware.get_definition("abc123")).then_return( - LabwareDefinition.construct(ordering=[]) # type: ignore[call-arg] + LabwareDefinition.model_construct(ordering=[]) # type: ignore[call-arg] ) result = subject.load_adapter( @@ -678,7 +678,7 @@ def test_load_adapter( slot_name=DeckSlotName.SLOT_5, ) ).then_return( - LoadedLabware.construct(id="abc123") # type: ignore[call-arg] + LoadedLabware.model_construct(id="abc123") # type: ignore[call-arg] ) assert subject.get_slot_item(DeckSlotName.SLOT_5) is result @@ -715,13 +715,13 @@ def test_load_adapter_on_staging_slot( ).then_return( commands.LoadLabwareResult( labwareId="abc123", - definition=LabwareDefinition.construct(), # type: ignore[call-arg] + definition=LabwareDefinition.model_construct(), # type: ignore[call-arg] offsetId=None, ) ) decoy.when(mock_engine_client.state.labware.get_definition("abc123")).then_return( - LabwareDefinition.construct(ordering=[]) # type: ignore[call-arg] + LabwareDefinition.model_construct(ordering=[]) # type: ignore[call-arg] ) result = subject.load_adapter( @@ -750,12 +750,160 @@ def test_load_adapter_on_staging_slot( slot_name=StagingSlotName.SLOT_B4, ) ).then_return( - LoadedLabware.construct(id="abc123") # type: ignore[call-arg] + LoadedLabware.model_construct(id="abc123") # type: ignore[call-arg] ) assert subject.get_slot_item(StagingSlotName.SLOT_B4) is result +def test_load_lid( + decoy: Decoy, + mock_engine_client: EngineClient, + subject: ProtocolCore, +) -> None: + """It should issue a LoadLid command.""" + mock_labware_core = decoy.mock(cls=LabwareCore) + decoy.when(mock_labware_core.labware_id).then_return("labware-id") + decoy.when( + mock_engine_client.state.labware.find_custom_labware_load_params() + ).then_return([EngineLabwareLoadParams("hello", "world", 654)]) + + decoy.when( + load_labware_params.resolve( + "some_labware", + "a_namespace", + 456, + [EngineLabwareLoadParams("hello", "world", 654)], + ) + ).then_return(("some_namespace", 9001)) + + decoy.when( + mock_engine_client.execute_command_without_recovery( + cmd.LoadLidParams( + location=OnLabwareLocation(labwareId="labware-id"), + loadName="some_labware", + namespace="some_namespace", + version=9001, + ) + ) + ).then_return( + commands.LoadLidResult( + labwareId="abc123", + definition=LabwareDefinition.model_construct(ordering=[]), # type: ignore[call-arg] + ) + ) + + decoy.when(mock_engine_client.state.labware.get_definition("abc123")).then_return( + LabwareDefinition.model_construct(ordering=[]) # type: ignore[call-arg] + ) + + result = subject.load_lid( + load_name="some_labware", + location=mock_labware_core, + namespace="a_namespace", + version=456, + ) + + assert isinstance(result, LabwareCore) + assert result.labware_id == "abc123" + assert subject.get_labware_cores() == [result] + + decoy.verify( + deck_conflict.check( + engine_state=mock_engine_client.state, + existing_labware_ids=[], + existing_module_ids=[], + existing_disposal_locations=[], + new_labware_id="abc123", + ) + ) + + decoy.when( + mock_engine_client.state.geometry.get_slot_item( + slot_name=DeckSlotName.SLOT_5, + ) + ).then_return( + LoadedLabware.model_construct(id="abc123") # type: ignore[call-arg] + ) + + assert subject.get_slot_item(DeckSlotName.SLOT_5) is result + + +def test_load_lid_stack( + decoy: Decoy, + mock_engine_client: EngineClient, + subject: ProtocolCore, +) -> None: + """It should issue a LoadLidStack command.""" + decoy.when( + mock_engine_client.state.labware.find_custom_labware_load_params() + ).then_return([EngineLabwareLoadParams("hello", "world", 654)]) + + decoy.when( + load_labware_params.resolve( + "some_labware", + "a_namespace", + 456, + [EngineLabwareLoadParams("hello", "world", 654)], + ) + ).then_return(("some_namespace", 9001)) + + decoy.when( + mock_engine_client.execute_command_without_recovery( + cmd.LoadLidStackParams( + location=DeckSlotLocation(slotName=DeckSlotName.SLOT_5), + loadName="some_labware", + namespace="some_namespace", + version=9001, + quantity=5, + ) + ) + ).then_return( + commands.LoadLidStackResult( + stackLabwareId="abc123", + labwareIds=["1", "2", "3", "4", "5"], + definition=LabwareDefinition.model_construct(), # type: ignore[call-arg] + location=DeckSlotLocation(slotName=DeckSlotName.SLOT_5), + ) + ) + + decoy.when(mock_engine_client.state.labware.get_definition("abc123")).then_return( + LabwareDefinition.model_construct(ordering=[]) # type: ignore[call-arg] + ) + + result = subject.load_lid_stack( + load_name="some_labware", + location=DeckSlotName.SLOT_5, + namespace="a_namespace", + version=456, + quantity=5, + ) + + assert isinstance(result, LabwareCore) + assert result.labware_id == "abc123" + assert subject.get_labware_cores() == [result] + + decoy.verify( + deck_conflict.check( + engine_state=mock_engine_client.state, + existing_labware_ids=[], + existing_module_ids=[], + existing_disposal_locations=[], + new_labware_id="abc123", + ) + ) + + decoy.when( + mock_engine_client.state.geometry.get_slot_item( + slot_name=DeckSlotName.SLOT_5, + ) + ).then_return( + LoadedLabware.model_construct(id="abc123") # type: ignore[call-arg] + ) + + assert subject.get_slot_item(DeckSlotName.SLOT_5) is result + + def test_load_trash_bin( decoy: Decoy, mock_engine_client: EngineClient, @@ -861,7 +1009,7 @@ def test_move_labware( decoy.when( mock_engine_client.state.labware.get_definition("labware-id") ).then_return( - LabwareDefinition.construct(ordering=[]) # type: ignore[call-arg] + LabwareDefinition.model_construct(ordering=[]) # type: ignore[call-arg] ) labware = LabwareCore(labware_id="labware-id", engine_client=mock_engine_client) subject.move_labware( @@ -904,7 +1052,7 @@ def test_move_labware_on_staging_slot( decoy.when( mock_engine_client.state.labware.get_definition("labware-id") ).then_return( - LabwareDefinition.construct(ordering=[]) # type: ignore[call-arg] + LabwareDefinition.model_construct(ordering=[]) # type: ignore[call-arg] ) labware = LabwareCore(labware_id="labware-id", engine_client=mock_engine_client) subject.move_labware( @@ -945,7 +1093,7 @@ def test_move_labware_on_non_connected_module( decoy.when( mock_engine_client.state.labware.get_definition("labware-id") ).then_return( - LabwareDefinition.construct(ordering=[]) # type: ignore[call-arg] + LabwareDefinition.model_construct(ordering=[]) # type: ignore[call-arg] ) labware = LabwareCore(labware_id="labware-id", engine_client=mock_engine_client) non_connected_module_core = NonConnectedModuleCore( @@ -991,7 +1139,7 @@ def test_move_labware_off_deck( decoy.when( mock_engine_client.state.labware.get_definition("labware-id") ).then_return( - LabwareDefinition.construct(ordering=[]) # type: ignore[call-arg] + LabwareDefinition.model_construct(ordering=[]) # type: ignore[call-arg] ) labware = LabwareCore(labware_id="labware-id", engine_client=mock_engine_client) @@ -1057,13 +1205,13 @@ def test_load_labware_on_module( ).then_return( commands.LoadLabwareResult( labwareId="abc123", - definition=LabwareDefinition.construct(), # type: ignore[call-arg] + definition=LabwareDefinition.model_construct(), # type: ignore[call-arg] offsetId=None, ) ) decoy.when(mock_engine_client.state.labware.get_definition("abc123")).then_return( - LabwareDefinition.construct(ordering=[]) # type: ignore[call-arg] + LabwareDefinition.model_construct(ordering=[]) # type: ignore[call-arg] ) module_core = ModuleCore( @@ -1134,13 +1282,13 @@ def test_load_labware_on_non_connected_module( ).then_return( commands.LoadLabwareResult( labwareId="abc123", - definition=LabwareDefinition.construct(), # type: ignore[call-arg] + definition=LabwareDefinition.model_construct(), # type: ignore[call-arg] offsetId=None, ) ) decoy.when(mock_engine_client.state.labware.get_definition("abc123")).then_return( - LabwareDefinition.construct(ordering=[]) # type: ignore[call-arg] + LabwareDefinition.model_construct(ordering=[]) # type: ignore[call-arg] ) non_connected_module_core = NonConnectedModuleCore( @@ -1186,7 +1334,7 @@ def test_add_labware_definition( """It should add a labware definition to the engine.""" decoy.when( mock_engine_client.add_labware_definition( - definition=LabwareDefinition.parse_obj(minimal_labware_def) + definition=LabwareDefinition.model_validate(minimal_labware_def) ) ).then_return(LabwareUri("hello/world/123")) @@ -1200,7 +1348,6 @@ def test_add_labware_definition( "requested_model", "engine_model", "expected_core_cls", - "deck_def", "slot_name", "robot_type", ), @@ -1209,7 +1356,6 @@ def test_add_labware_definition( TemperatureModuleModel.TEMPERATURE_V1, EngineModuleModel.TEMPERATURE_MODULE_V1, TemperatureModuleCore, - lazy_fixture("ot2_standard_deck_def"), DeckSlotName.SLOT_1, "OT-2 Standard", ), @@ -1217,7 +1363,6 @@ def test_add_labware_definition( TemperatureModuleModel.TEMPERATURE_V2, EngineModuleModel.TEMPERATURE_MODULE_V2, TemperatureModuleCore, - lazy_fixture("ot3_standard_deck_def"), DeckSlotName.SLOT_D1, "OT-3 Standard", ), @@ -1225,7 +1370,6 @@ def test_add_labware_definition( MagneticModuleModel.MAGNETIC_V1, EngineModuleModel.MAGNETIC_MODULE_V1, MagneticModuleCore, - lazy_fixture("ot2_standard_deck_def"), DeckSlotName.SLOT_1, "OT-2 Standard", ), @@ -1233,7 +1377,6 @@ def test_add_labware_definition( ThermocyclerModuleModel.THERMOCYCLER_V1, EngineModuleModel.THERMOCYCLER_MODULE_V1, ThermocyclerModuleCore, - lazy_fixture("ot2_standard_deck_def"), DeckSlotName.SLOT_7, "OT-2 Standard", ), @@ -1241,7 +1384,6 @@ def test_add_labware_definition( ThermocyclerModuleModel.THERMOCYCLER_V2, EngineModuleModel.THERMOCYCLER_MODULE_V2, ThermocyclerModuleCore, - lazy_fixture("ot3_standard_deck_def"), DeckSlotName.SLOT_B1, "OT-3 Standard", ), @@ -1249,7 +1391,6 @@ def test_add_labware_definition( HeaterShakerModuleModel.HEATER_SHAKER_V1, EngineModuleModel.HEATER_SHAKER_MODULE_V1, HeaterShakerModuleCore, - lazy_fixture("ot3_standard_deck_def"), DeckSlotName.SLOT_A1, "OT-3 Standard", ), @@ -1265,12 +1406,11 @@ def test_load_module( engine_model: EngineModuleModel, expected_core_cls: Type[ModuleCore], subject: ProtocolCore, - deck_def: DeckDefinitionV5, slot_name: DeckSlotName, robot_type: RobotType, ) -> None: """It should issue a load module engine command.""" - definition = ModuleDefinition.construct() # type: ignore[call-arg] + definition = ModuleDefinition.model_construct() # type: ignore[call-arg] mock_hw_mod_1 = decoy.mock(cls=AbstractModule) mock_hw_mod_2 = decoy.mock(cls=AbstractModule) @@ -1281,23 +1421,6 @@ def test_load_module( [mock_hw_mod_1, mock_hw_mod_2] ) - 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) decoy.when( @@ -1341,7 +1464,7 @@ def test_load_module( slot_name=slot_name, ) ).then_return( - LoadedModule.construct(id="abc123") # type: ignore[call-arg] + LoadedModule.model_construct(id="abc123") # type: ignore[call-arg] ) decoy.when(mock_engine_client.state.labware.get_id_by_module("abc123")).then_raise( LabwareNotLoadedOnModuleError("oh no") @@ -1356,34 +1479,13 @@ def test_load_module( def test_load_mag_block( decoy: Decoy, mock_engine_client: EngineClient, - mock_sync_hardware_api: SyncHardwareAPI, subject: ProtocolCore, - ot3_standard_deck_def: DeckDefinitionV5, ) -> None: """It should issue a load module engine command.""" - definition = ModuleDefinition.construct() # type: ignore[call-arg] + definition = ModuleDefinition.model_construct() # type: ignore[call-arg] decoy.when(mock_engine_client.state.config.robot_type).then_return("OT-3 Standard") - decoy.when(subject.get_slot_definition(DeckSlotName.SLOT_A2)).then_return( - cast( - SlotDefV3, - { - "compatibleModuleTypes": [ - ModuleType.from_model(MagneticBlockModel.MAGNETIC_BLOCK_V1) - ] - }, - ) - ) - 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.execute_command_without_recovery( cmd.LoadModuleParams( @@ -1425,7 +1527,7 @@ def test_load_mag_block( slot_name=DeckSlotName.SLOT_1, ) ).then_return( - LoadedModule.construct(id="abc123") # type: ignore[call-arg] + LoadedModule.model_construct(id="abc123") # type: ignore[call-arg] ) decoy.when(mock_engine_client.state.labware.get_id_by_module("abc123")).then_raise( LabwareNotLoadedOnModuleError("oh no") @@ -1436,18 +1538,16 @@ def test_load_mag_block( @pytest.mark.parametrize( - ("requested_model", "engine_model", "deck_def", "expected_slot"), + ("requested_model", "engine_model", "expected_slot"), [ ( ThermocyclerModuleModel.THERMOCYCLER_V1, EngineModuleModel.THERMOCYCLER_MODULE_V1, - lazy_fixture("ot3_standard_deck_def"), DeckSlotName.SLOT_B1, ), ( ThermocyclerModuleModel.THERMOCYCLER_V2, EngineModuleModel.THERMOCYCLER_MODULE_V2, - lazy_fixture("ot3_standard_deck_def"), DeckSlotName.SLOT_B1, ), ], @@ -1459,24 +1559,15 @@ def test_load_module_thermocycler_with_no_location( requested_model: ModuleModel, engine_model: EngineModuleModel, subject: ProtocolCore, - deck_def: DeckDefinitionV5, expected_slot: DeckSlotName, ) -> None: """It should issue a load module engine command with location at 7.""" - definition = ModuleDefinition.construct() # type: ignore[call-arg] + definition = ModuleDefinition.model_construct() # type: ignore[call-arg] mock_hw_mod = decoy.mock(cls=AbstractModule) 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( - 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.execute_command_without_recovery( @@ -1728,11 +1819,11 @@ def test_add_liquid( subject: ProtocolCore, ) -> None: """It should return the created liquid.""" - liquid = PE_Liquid.construct( + liquid = PE_Liquid.model_construct( id="water-id", displayName="water", description="water desc", - displayColor=HexColor(__root__="#fff"), + displayColor=HexColor("#fff"), ) expected_result = Liquid( @@ -1763,7 +1854,7 @@ def test_define_liquid_class( ) -> None: """It should create a LiquidClass and cache the definition.""" expected_liquid_class = LiquidClass( - _name="water1", _display_name="water 1", _by_pipette_setting=[] + _name="water1", _display_name="water 1", _by_pipette_setting={} ) decoy.when(liquid_classes.load_definition("water")).then_return( minimal_liquid_class_def1 diff --git a/api/tests/opentrons/protocol_api/core/engine/test_well_core.py b/api/tests/opentrons/protocol_api/core/engine/test_well_core.py index 31b562f7e81..6e1912f0aec 100644 --- a/api/tests/opentrons/protocol_api/core/engine/test_well_core.py +++ b/api/tests/opentrons/protocol_api/core/engine/test_well_core.py @@ -1,4 +1,5 @@ """Test for the ProtocolEngine-based well API core.""" + import inspect import pytest @@ -49,7 +50,7 @@ def api_version() -> APIVersion: @pytest.fixture def well_definition() -> WellDefinition: """Get a partial WellDefinition value object.""" - return WellDefinition.construct() # type: ignore[call-arg] + return WellDefinition.model_construct() # type: ignore[call-arg] @pytest.fixture @@ -93,7 +94,7 @@ def test_display_name( @pytest.mark.parametrize( "well_definition", - [WellDefinition.construct(totalLiquidVolume=101)], # type: ignore[call-arg] + [WellDefinition.model_construct(totalLiquidVolume=101)], # type: ignore[call-arg] ) def test_max_volume(subject: WellCore) -> None: """It should have a max volume.""" @@ -192,7 +193,7 @@ def test_load_liquid( @pytest.mark.parametrize( "well_definition", - [WellDefinition.construct(diameter=123.4)], # type: ignore[call-arg] + [WellDefinition.model_construct(diameter=123.4)], # type: ignore[call-arg] ) def test_diameter(subject: WellCore) -> None: """It should get the diameter.""" @@ -201,7 +202,7 @@ def test_diameter(subject: WellCore) -> None: @pytest.mark.parametrize( "well_definition", - [WellDefinition.construct(xDimension=567.8)], # type: ignore[call-arg] + [WellDefinition.model_construct(xDimension=567.8)], # type: ignore[call-arg] ) def test_length(subject: WellCore) -> None: """It should get the length.""" @@ -210,7 +211,7 @@ def test_length(subject: WellCore) -> None: @pytest.mark.parametrize( "well_definition", - [WellDefinition.construct(yDimension=987.6)], # type: ignore[call-arg] + [WellDefinition.model_construct(yDimension=987.6)], # type: ignore[call-arg] ) def test_width(subject: WellCore) -> None: """It should get the width.""" @@ -219,7 +220,7 @@ def test_width(subject: WellCore) -> None: @pytest.mark.parametrize( "well_definition", - [WellDefinition.construct(depth=42.0)], # type: ignore[call-arg] + [WellDefinition.model_construct(depth=42.0)], # type: ignore[call-arg] ) def test_depth(subject: WellCore) -> None: """It should get the depth.""" diff --git a/api/tests/opentrons/protocol_api/test_instrument_context.py b/api/tests/opentrons/protocol_api/test_instrument_context.py index 069330036ec..3f639aff922 100644 --- a/api/tests/opentrons/protocol_api/test_instrument_context.py +++ b/api/tests/opentrons/protocol_api/test_instrument_context.py @@ -1,4 +1,5 @@ """Tests for the InstrumentContext public interface.""" + import inspect import pytest from collections import OrderedDict @@ -9,14 +10,15 @@ from decoy import Decoy from pytest_lazyfixture import lazy_fixture # type: ignore[import-untyped] +from opentrons.config import feature_flags as ff from opentrons.protocol_engine.commands.pipetting_common import LiquidNotFoundError from opentrons.protocol_engine.errors.error_occurrence import ( ProtocolCommandFailedError, ) from opentrons.legacy_broker import LegacyBroker +from opentrons.protocols.advanced_control.transfers.common import TransferTipPolicyV2 -from opentrons.protocol_engine.errors.exceptions import TipNotAttachedError from tests.opentrons.protocol_api.partial_tip_configurations import ( PipetteReliantNozzleConfigSpec, PIPETTE_RELIANT_TEST_SPECS, @@ -27,11 +29,6 @@ INSTRUMENT_CORE_NOZZLE_LAYOUT_TEST_SPECS, ExpectedCoreArgs, ) -from tests.opentrons.protocol_engine.pipette_fixtures import ( - NINETY_SIX_COLS, - NINETY_SIX_MAP, - NINETY_SIX_ROWS, -) from opentrons.protocols.api_support import instrument as mock_instrument_support from opentrons.protocols.api_support.types import APIVersion from opentrons.protocols.api_support.util import ( @@ -47,6 +44,7 @@ Well, labware, validation as mock_validation, + LiquidClass, ) from opentrons.protocol_api.validation import WellTarget, PointTarget from opentrons.protocol_api.core.common import InstrumentCore, ProtocolCore @@ -56,12 +54,17 @@ from opentrons.hardware_control.nozzle_manager import NozzleMap from opentrons.protocol_api.disposal_locations import TrashBin, WasteChute -from opentrons_shared_data.pipette.pipette_definition import ValidNozzleMaps from opentrons.types import Location, Mount, Point +from opentrons_shared_data.pipette.pipette_definition import ValidNozzleMaps from opentrons_shared_data.errors.exceptions import ( CommandPreconditionViolated, ) +from opentrons_shared_data.liquid_classes.liquid_class_definition import ( + LiquidClassSchemaV1, +) +from opentrons_shared_data.robot.types import RobotTypeEnum, RobotType +from . import versions_at_or_above, versions_between @pytest.fixture(autouse=True) @@ -89,7 +92,7 @@ def mock_instrument_core(decoy: Decoy) -> InstrumentCore: """Get a mock instrument implementation core.""" instrument_core = decoy.mock(cls=InstrumentCore) decoy.when(instrument_core.get_mount()).then_return(Mount.LEFT) - + decoy.when(instrument_core._pressure_supported_by_pipette()).then_return(True) # we need to add this for the mock of liquid_presence detection to actually work # this replaces the mock with a a property again instrument_core._liquid_presence_detection = False # type: ignore[attr-defined] @@ -1489,60 +1492,12 @@ def test_measure_liquid_height( assert pcfe.value is errorToRaise -def test_96_tip_config_valid( - decoy: Decoy, mock_instrument_core: InstrumentCore, subject: InstrumentContext -) -> None: - """It should error when there's no tips on the correct corner nozzles.""" - nozzle_map = NozzleMap.build( - physical_nozzles=NINETY_SIX_MAP, - physical_rows=NINETY_SIX_ROWS, - physical_columns=NINETY_SIX_COLS, - starting_nozzle="A5", - back_left_nozzle="A5", - front_right_nozzle="H5", - valid_nozzle_maps=ValidNozzleMaps(maps={"Column12": NINETY_SIX_COLS["5"]}), - ) - decoy.when(mock_instrument_core.get_nozzle_map()).then_return(nozzle_map) - decoy.when(mock_instrument_core.get_active_channels()).then_return(96) - with pytest.raises(TipNotAttachedError): - subject._96_tip_config_valid() - - -def test_96_tip_config_invalid( - decoy: Decoy, mock_instrument_core: InstrumentCore, subject: InstrumentContext -) -> None: - """It should return True when there are tips on the correct corner nozzles.""" - nozzle_map = NozzleMap.build( - physical_nozzles=NINETY_SIX_MAP, - physical_rows=NINETY_SIX_ROWS, - physical_columns=NINETY_SIX_COLS, - starting_nozzle="A1", - back_left_nozzle="A1", - front_right_nozzle="H12", - valid_nozzle_maps=ValidNozzleMaps( - maps={ - "Full": sum( - [ - NINETY_SIX_ROWS["A"], - NINETY_SIX_ROWS["B"], - NINETY_SIX_ROWS["C"], - NINETY_SIX_ROWS["D"], - NINETY_SIX_ROWS["E"], - NINETY_SIX_ROWS["F"], - NINETY_SIX_ROWS["G"], - NINETY_SIX_ROWS["H"], - ], - [], - ) - } - ), - ) - decoy.when(mock_instrument_core.get_nozzle_map()).then_return(nozzle_map) - decoy.when(mock_instrument_core.get_active_channels()).then_return(96) - assert subject._96_tip_config_valid() is True - - -@pytest.mark.parametrize("api_version", [APIVersion(2, 21)]) +@pytest.mark.parametrize( + "api_version", + versions_between( + low_exclusive_bound=APIVersion(2, 13), high_inclusive_bound=APIVersion(2, 21) + ), +) def test_mix_no_lpd( decoy: Decoy, mock_instrument_core: InstrumentCore, @@ -1553,6 +1508,7 @@ def test_mix_no_lpd( mock_well = decoy.mock(cls=Well) bottom_location = Location(point=Point(1, 2, 3), labware=mock_well) + top_location = Location(point=Point(3, 2, 1), labware=None) input_location = Location(point=Point(2, 2, 2), labware=None) last_location = Location(point=Point(9, 9, 9), labware=None) @@ -1568,6 +1524,7 @@ def test_mix_no_lpd( mock_validation.validate_location(location=None, last_location=last_location) ).then_return(WellTarget(well=mock_well, location=None, in_place=False)) decoy.when(mock_well.bottom(z=1.0)).then_return(bottom_location) + decoy.when(mock_well.top()).then_return(top_location) decoy.when(mock_instrument_core.get_aspirate_flow_rate(1.23)).then_return(5.67) decoy.when(mock_instrument_core.get_dispense_flow_rate(1.23)).then_return(5.67) decoy.when(mock_instrument_core.has_tip()).then_return(True) @@ -1575,25 +1532,69 @@ def test_mix_no_lpd( subject.mix(repetitions=10, volume=10.0, location=input_location, rate=1.23) decoy.verify( - mock_instrument_core.aspirate(), # type: ignore[call-arg] - ignore_extra_args=True, - times=10, - ) - decoy.verify( - mock_instrument_core.dispense(), # type: ignore[call-arg] - ignore_extra_args=True, + mock_instrument_core.aspirate( + bottom_location, + mock_well._core, + 10.0, + 1.23, + 5.67, + False, + None, + ), times=10, ) + # Slight differences in dispense push-out logic for 2.14 and 2.15 api levels + if subject.api_version < APIVersion(2, 16): + decoy.verify( + mock_instrument_core.dispense( + bottom_location, + mock_well._core, + 10.0, + 1.23, + 5.67, + False, + None, + None, + ), + times=10, + ) + else: + decoy.verify( + mock_instrument_core.dispense( + bottom_location, + mock_well._core, + 10.0, + 1.23, + 5.67, + False, + 0.0, + None, + ), + times=9, + ) + decoy.verify( + mock_instrument_core.dispense( + bottom_location, + mock_well._core, + 10.0, + 1.23, + 5.67, + False, + None, + None, + ), + times=1, + ) + decoy.verify( - mock_instrument_core.liquid_probe_with_recovery(), # type: ignore[call-arg] - ignore_extra_args=True, + mock_instrument_core.liquid_probe_with_recovery(mock_well._core, top_location), times=0, ) @pytest.mark.ot3_only -@pytest.mark.parametrize("api_version", [APIVersion(2, 21)]) +@pytest.mark.parametrize("api_version", versions_at_or_above(APIVersion(2, 21))) def test_mix_with_lpd( decoy: Decoy, mock_instrument_core: InstrumentCore, @@ -1603,6 +1604,7 @@ def test_mix_with_lpd( """It should aspirate/dispense to a well several times and do 1 lpd.""" mock_well = decoy.mock(cls=Well) bottom_location = Location(point=Point(1, 2, 3), labware=mock_well) + top_location = Location(point=Point(3, 2, 1), labware=None) input_location = Location(point=Point(2, 2, 2), labware=None) last_location = Location(point=Point(9, 9, 9), labware=None) @@ -1618,26 +1620,387 @@ def test_mix_with_lpd( mock_validation.validate_location(location=None, last_location=last_location) ).then_return(WellTarget(well=mock_well, location=None, in_place=False)) decoy.when(mock_well.bottom(z=1.0)).then_return(bottom_location) + decoy.when(mock_well.top()).then_return(top_location) decoy.when(mock_instrument_core.get_aspirate_flow_rate(1.23)).then_return(5.67) decoy.when(mock_instrument_core.get_dispense_flow_rate(1.23)).then_return(5.67) decoy.when(mock_instrument_core.has_tip()).then_return(True) decoy.when(mock_instrument_core.get_current_volume()).then_return(0.0) + decoy.when(mock_instrument_core.nozzle_configuration_valid_for_lld()).then_return( + True + ) subject.liquid_presence_detection = True subject.mix(repetitions=10, volume=10.0, location=input_location, rate=1.23) decoy.verify( - mock_instrument_core.aspirate(), # type: ignore[call-arg] - ignore_extra_args=True, + mock_instrument_core.aspirate( + bottom_location, + mock_well._core, + 10.0, + 1.23, + 5.67, + False, + None, + ), times=10, ) decoy.verify( - mock_instrument_core.dispense(), # type: ignore[call-arg] - ignore_extra_args=True, - times=10, + mock_instrument_core.dispense( + bottom_location, + mock_well._core, + 10.0, + 1.23, + 5.67, + False, + 0.0, + None, + ), + times=9, + ) + decoy.verify( + mock_instrument_core.dispense( + bottom_location, + mock_well._core, + 10.0, + 1.23, + 5.67, + False, + None, + None, + ), + times=1, ) - decoy.verify( - mock_instrument_core.liquid_probe_with_recovery(), # type: ignore[call-arg] - ignore_extra_args=True, + mock_instrument_core.liquid_probe_with_recovery(mock_well._core, top_location), times=1, ) + + +@pytest.mark.parametrize( + "api_version", + versions_between( + low_exclusive_bound=APIVersion(2, 13), high_inclusive_bound=APIVersion(2, 21) + ), +) +def test_air_gap_uses_aspirate( + decoy: Decoy, + mock_instrument_core: InstrumentCore, + mock_protocol_core: ProtocolCore, + subject: InstrumentContext, + monkeypatch: pytest.MonkeyPatch, +) -> None: + """It should use its own aspirate function to aspirate air.""" + mock_well = decoy.mock(cls=Well) + top_location = Location(point=Point(9, 9, 14), labware=mock_well) + last_location = Location(point=Point(9, 9, 9), labware=mock_well) + mock_aspirate = decoy.mock(func=subject.aspirate) + mock_move_to = decoy.mock(func=subject.move_to) + monkeypatch.setattr(subject, "aspirate", mock_aspirate) + monkeypatch.setattr(subject, "move_to", mock_move_to) + + decoy.when(mock_instrument_core.has_tip()).then_return(True) + decoy.when(mock_protocol_core.get_last_location()).then_return(last_location) + decoy.when(mock_well.top(z=5.0)).then_return(top_location) + subject.air_gap(volume=10, height=5) + + decoy.verify(mock_move_to(top_location, publish=False)) + decoy.verify(mock_aspirate(10)) + + +@pytest.mark.parametrize("api_version", versions_at_or_above(APIVersion(2, 22))) +def test_air_gap_uses_air_gap( + decoy: Decoy, + mock_instrument_core: InstrumentCore, + mock_protocol_core: ProtocolCore, + subject: InstrumentContext, + monkeypatch: pytest.MonkeyPatch, +) -> None: + """It should use its own aspirate function to aspirate air.""" + mock_well = decoy.mock(cls=Well) + top_location = Location(point=Point(9, 9, 14), labware=mock_well) + last_location = Location(point=Point(9, 9, 9), labware=mock_well) + mock_move_to = decoy.mock(func=subject.move_to) + monkeypatch.setattr(subject, "move_to", mock_move_to) + + decoy.when(mock_instrument_core.has_tip()).then_return(True) + decoy.when(mock_protocol_core.get_last_location()).then_return(last_location) + decoy.when(mock_well.top(z=5.0)).then_return(top_location) + decoy.when(mock_instrument_core.get_aspirate_flow_rate()).then_return(11) + + subject.air_gap(volume=10, height=5) + + decoy.verify(mock_move_to(top_location, publish=False)) + decoy.verify(mock_instrument_core.air_gap_in_place(10, 11)) + + +@pytest.mark.parametrize("robot_type", ["OT-2 Standard", "OT-3 Standard"]) +def test_transfer_liquid_raises_for_invalid_locations( + decoy: Decoy, + mock_protocol_core: ProtocolCore, + subject: InstrumentContext, + mock_feature_flags: None, + robot_type: RobotType, + minimal_liquid_class_def2: LiquidClassSchemaV1, +) -> None: + """It should raise errors if source or destination is invalid.""" + test_liq_class = LiquidClass.create(minimal_liquid_class_def2) + mock_well = decoy.mock(cls=Well) + decoy.when(mock_protocol_core.robot_type).then_return(robot_type) + decoy.when( + ff.allow_liquid_classes(RobotTypeEnum.robot_literal_to_enum(robot_type)) + ).then_return(True) + decoy.when( + mock_validation.ensure_valid_flat_wells_list_for_transfer_v2([mock_well]) + ).then_raise(ValueError("Oh no")) + with pytest.raises(ValueError): + subject.transfer_liquid( + liquid_class=test_liq_class, + volume=10, + source=[mock_well], + dest=[[mock_well]], + ) + + +@pytest.mark.parametrize("robot_type", ["OT-2 Standard", "OT-3 Standard"]) +def test_transfer_liquid_raises_for_unequal_source_and_dest( + decoy: Decoy, + mock_protocol_core: ProtocolCore, + subject: InstrumentContext, + mock_feature_flags: None, + robot_type: RobotType, + minimal_liquid_class_def2: LiquidClassSchemaV1, +) -> None: + """It should raise errors if source and destination are not of same length.""" + test_liq_class = LiquidClass.create(minimal_liquid_class_def2) + mock_well = decoy.mock(cls=Well) + decoy.when(mock_protocol_core.robot_type).then_return(robot_type) + decoy.when( + ff.allow_liquid_classes(RobotTypeEnum.robot_literal_to_enum(robot_type)) + ).then_return(True) + decoy.when( + mock_validation.ensure_valid_flat_wells_list_for_transfer_v2(mock_well) + ).then_return([mock_well, mock_well]) + decoy.when( + mock_validation.ensure_valid_flat_wells_list_for_transfer_v2([mock_well]) + ).then_return([mock_well]) + with pytest.raises( + ValueError, match="Sources and destinations should be of the same length" + ): + subject.transfer_liquid( + liquid_class=test_liq_class, volume=10, source=mock_well, dest=[mock_well] + ) + + +@pytest.mark.parametrize("robot_type", ["OT-2 Standard", "OT-3 Standard"]) +def test_transfer_liquid_raises_for_non_liquid_handling_locations( + decoy: Decoy, + mock_protocol_core: ProtocolCore, + subject: InstrumentContext, + mock_feature_flags: None, + robot_type: RobotType, + minimal_liquid_class_def2: LiquidClassSchemaV1, +) -> None: + """It should raise errors if source and destination are not of same length.""" + test_liq_class = LiquidClass.create(minimal_liquid_class_def2) + mock_well = decoy.mock(cls=Well) + decoy.when(mock_protocol_core.robot_type).then_return(robot_type) + decoy.when( + ff.allow_liquid_classes(RobotTypeEnum.robot_literal_to_enum(robot_type)) + ).then_return(True) + decoy.when( + mock_validation.ensure_valid_flat_wells_list_for_transfer_v2([mock_well]) + ).then_return([mock_well]) + decoy.when( + mock_instrument_support.validate_takes_liquid( + mock_well.top(), reject_module=True, reject_adapter=True + ) + ).then_raise(ValueError("Uh oh")) + with pytest.raises(ValueError, match="Uh oh"): + subject.transfer_liquid( + liquid_class=test_liq_class, volume=10, source=[mock_well], dest=[mock_well] + ) + + +@pytest.mark.parametrize("robot_type", ["OT-2 Standard", "OT-3 Standard"]) +def test_transfer_liquid_raises_for_bad_tip_policy( + decoy: Decoy, + mock_protocol_core: ProtocolCore, + subject: InstrumentContext, + mock_feature_flags: None, + robot_type: RobotType, + minimal_liquid_class_def2: LiquidClassSchemaV1, +) -> None: + """It should raise errors if new_tip is invalid.""" + test_liq_class = LiquidClass.create(minimal_liquid_class_def2) + mock_well = decoy.mock(cls=Well) + decoy.when(mock_protocol_core.robot_type).then_return(robot_type) + decoy.when( + ff.allow_liquid_classes(RobotTypeEnum.robot_literal_to_enum(robot_type)) + ).then_return(True) + decoy.when( + mock_validation.ensure_valid_flat_wells_list_for_transfer_v2([mock_well]) + ).then_return([mock_well]) + decoy.when(mock_validation.ensure_new_tip_policy("once")).then_raise( + ValueError("Uh oh") + ) + with pytest.raises(ValueError, match="Uh oh"): + subject.transfer_liquid( + liquid_class=test_liq_class, + volume=10, + source=[mock_well], + dest=[mock_well], + new_tip="once", + ) + + +@pytest.mark.parametrize("robot_type", ["OT-2 Standard", "OT-3 Standard"]) +def test_transfer_liquid_raises_for_no_tip( + decoy: Decoy, + mock_protocol_core: ProtocolCore, + subject: InstrumentContext, + mock_feature_flags: None, + robot_type: RobotType, + minimal_liquid_class_def2: LiquidClassSchemaV1, +) -> None: + """It should raise errors if there is no tip attached.""" + test_liq_class = LiquidClass.create(minimal_liquid_class_def2) + mock_well = decoy.mock(cls=Well) + decoy.when(mock_protocol_core.robot_type).then_return(robot_type) + decoy.when( + ff.allow_liquid_classes(RobotTypeEnum.robot_literal_to_enum(robot_type)) + ).then_return(True) + decoy.when( + mock_validation.ensure_valid_flat_wells_list_for_transfer_v2([mock_well]) + ).then_return([mock_well]) + decoy.when(mock_validation.ensure_new_tip_policy("never")).then_return( + TransferTipPolicyV2.NEVER + ) + with pytest.raises(RuntimeError, match="Pipette has no tip"): + subject.transfer_liquid( + liquid_class=test_liq_class, + volume=10, + source=[mock_well], + dest=[mock_well], + new_tip="never", + ) + + +@pytest.mark.parametrize("robot_type", ["OT-2 Standard", "OT-3 Standard"]) +def test_transfer_liquid_raises_if_tip_has_liquid( + decoy: Decoy, + mock_protocol_core: ProtocolCore, + mock_instrument_core: InstrumentCore, + subject: InstrumentContext, + mock_feature_flags: None, + robot_type: RobotType, + minimal_liquid_class_def2: LiquidClassSchemaV1, +) -> None: + """It should raise errors if there is no tip attached.""" + test_liq_class = LiquidClass.create(minimal_liquid_class_def2) + mock_well = decoy.mock(cls=Well) + tip_racks = [decoy.mock(cls=Labware)] + + subject.starting_tip = None + subject.tip_racks = tip_racks + + decoy.when(mock_protocol_core.robot_type).then_return(robot_type) + decoy.when( + ff.allow_liquid_classes(RobotTypeEnum.robot_literal_to_enum(robot_type)) + ).then_return(True) + decoy.when( + mock_validation.ensure_valid_flat_wells_list_for_transfer_v2([mock_well]) + ).then_return([mock_well]) + decoy.when(mock_validation.ensure_new_tip_policy("never")).then_return( + TransferTipPolicyV2.ONCE + ) + decoy.when(mock_instrument_core.get_nozzle_map()).then_return(MOCK_MAP) + decoy.when(mock_instrument_core.get_active_channels()).then_return(2) + decoy.when( + labware.next_available_tip( + starting_tip=None, + tip_racks=tip_racks, + channels=2, + nozzle_map=MOCK_MAP, + ) + ).then_return((decoy.mock(cls=Labware), decoy.mock(cls=Well))) + decoy.when(mock_instrument_core.get_current_volume()).then_return(1000) + with pytest.raises(RuntimeError, match="liquid already in the tip"): + subject.transfer_liquid( + liquid_class=test_liq_class, + volume=10, + source=[mock_well], + dest=[mock_well], + new_tip="never", + ) + + +@pytest.mark.parametrize("robot_type", ["OT-2 Standard", "OT-3 Standard"]) +def test_transfer_liquid_delegates_to_engine_core( + decoy: Decoy, + mock_protocol_core: ProtocolCore, + mock_instrument_core: InstrumentCore, + subject: InstrumentContext, + mock_feature_flags: None, + robot_type: RobotType, + minimal_liquid_class_def2: LiquidClassSchemaV1, +) -> None: + """It should load liquid class into engine and delegate the transfer execution to core.""" + test_liq_class = LiquidClass.create(minimal_liquid_class_def2) + mock_well = decoy.mock(cls=Well) + tip_racks = [decoy.mock(cls=Labware)] + trash_location = Location(point=Point(1, 2, 3), labware=mock_well) + next_tiprack = decoy.mock(cls=Labware) + subject.starting_tip = None + subject.tip_racks = tip_racks + + decoy.when(mock_protocol_core.robot_type).then_return(robot_type) + decoy.when( + ff.allow_liquid_classes(RobotTypeEnum.robot_literal_to_enum(robot_type)) + ).then_return(True) + decoy.when( + mock_validation.ensure_valid_flat_wells_list_for_transfer_v2([mock_well]) + ).then_return([mock_well]) + decoy.when(mock_validation.ensure_new_tip_policy("never")).then_return( + TransferTipPolicyV2.ONCE + ) + decoy.when(mock_instrument_core.get_nozzle_map()).then_return(MOCK_MAP) + decoy.when(mock_instrument_core.get_active_channels()).then_return(2) + decoy.when( + labware.next_available_tip( + starting_tip=None, + tip_racks=tip_racks, + channels=2, + nozzle_map=MOCK_MAP, + ) + ).then_return((next_tiprack, decoy.mock(cls=Well))) + decoy.when(mock_instrument_core.get_current_volume()).then_return(0) + decoy.when( + mock_validation.ensure_valid_tip_drop_location_for_transfer_v2(trash_location) + ).then_return(trash_location.move(Point(1, 2, 3))) + decoy.when(next_tiprack.uri).then_return("tiprack-uri") + decoy.when(mock_instrument_core.get_pipette_name()).then_return("pipette-name") + decoy.when( + mock_instrument_core.load_liquid_class( + liquid_class=test_liq_class, + pipette_load_name="pipette-name", + tiprack_uri="tiprack-uri", + ) + ).then_return("liq-class-id") + + subject.transfer_liquid( + liquid_class=test_liq_class, + volume=10, + source=[mock_well], + dest=[mock_well], + new_tip="never", + tip_drop_location=trash_location, + ) + decoy.verify( + mock_instrument_core.transfer_liquid( + liquid_class_id="liq-class-id", + volume=10, + source=[mock_well._core], + dest=[mock_well._core], + new_tip=TransferTipPolicyV2.ONCE, + trash_location=trash_location.move(Point(1, 2, 3)), + ) + ) diff --git a/api/tests/opentrons/protocol_api/test_labware.py b/api/tests/opentrons/protocol_api/test_labware.py index 4610145162f..5e49cd29947 100644 --- a/api/tests/opentrons/protocol_api/test_labware.py +++ b/api/tests/opentrons/protocol_api/test_labware.py @@ -1,4 +1,5 @@ """Tests for the InstrumentContext public interface.""" + import inspect from typing import cast @@ -21,6 +22,7 @@ from opentrons.protocol_api.core.labware import LabwareLoadParams from opentrons.protocol_api.core.core_map import LoadedCoreMap from opentrons.protocol_api import TemperatureModuleContext +from opentrons.protocol_api._liquid import Liquid from opentrons.types import Point @@ -364,3 +366,318 @@ def test_separate_calibration_raises_on_high_api_version( """It should raise an error, on high API versions.""" with pytest.raises(UnsupportedAPIError): subject.separate_calibration + + +@pytest.mark.parametrize("api_version", versions_at_or_above(APIVersion(2, 22))) +def test_load_liquid_handles_valid_inputs( + decoy: Decoy, + mock_labware_core: LabwareCore, + api_version: APIVersion, + mock_protocol_core: ProtocolCore, + mock_map_core: LoadedCoreMap, +) -> None: + """It should load volumes for list of wells.""" + mock_well_core_1 = decoy.mock(cls=WellCore) + mock_well_core_2 = decoy.mock(cls=WellCore) + grid = well_grid.WellGrid( + columns_by_name={"1": ["A1", "B1"]}, + rows_by_name={"A": ["A1"], "B": ["B1"]}, + ) + decoy.when(mock_well_core_1.get_name()).then_return("A1") + decoy.when(mock_well_core_2.get_name()).then_return("B1") + + decoy.when(mock_labware_core.get_well_columns()).then_return([["A1", "B1"]]) + decoy.when(mock_labware_core.get_well_core("A1")).then_return(mock_well_core_1) + decoy.when(mock_labware_core.get_well_core("B1")).then_return(mock_well_core_2) + decoy.when(well_grid.create([["A1", "B1"]])).then_return(grid) + + subject = Labware( + core=mock_labware_core, + api_version=api_version, + protocol_core=mock_protocol_core, + core_map=mock_map_core, + ) + mock_liquid = decoy.mock(cls=Liquid) + + subject.load_liquid(["A1", subject["B1"]], 10, mock_liquid) + decoy.verify( + mock_labware_core.load_liquid( + { + "A1": 10, + "B1": 10, + }, + mock_liquid, + ) + ) + + +@pytest.mark.parametrize("api_version", versions_at_or_above(APIVersion(2, 22))) +def test_load_liquid_rejects_invalid_inputs( + decoy: Decoy, + mock_labware_core: LabwareCore, + api_version: APIVersion, + mock_protocol_core: ProtocolCore, + mock_map_core: LoadedCoreMap, +) -> None: + """It should require valid load inputs.""" + mock_well_core_1 = decoy.mock(cls=WellCore) + mock_well_core_2 = decoy.mock(cls=WellCore) + + grid = well_grid.WellGrid( + columns_by_name={"1": ["A1", "B1"]}, + rows_by_name={"A": ["A1"], "B": ["B1"]}, + ) + decoy.when(mock_well_core_1.get_name()).then_return("A1") + decoy.when(mock_well_core_2.get_name()).then_return("B1") + decoy.when(mock_labware_core.get_well_columns()).then_return([["A1", "B1"]]) + decoy.when(mock_labware_core.get_well_core("A1")).then_return(mock_well_core_1) + decoy.when(mock_labware_core.get_well_core("B1")).then_return(mock_well_core_2) + decoy.when(well_grid.create([["A1", "B1"]])).then_return(grid) + subject = Labware( + core=mock_labware_core, + api_version=api_version, + protocol_core=mock_protocol_core, + core_map=mock_map_core, + ) + + core_2 = decoy.mock(cls=LabwareCore) + mock_well_core_3 = decoy.mock(cls=WellCore) + grid_2 = well_grid.WellGrid( + columns_by_name={"1": ["A1"]}, rows_by_name={"A": ["A1"]} + ) + decoy.when(mock_well_core_3.get_name()).then_return("A1") + decoy.when(core_2.get_well_columns()).then_return([["A1", "B1"]]) + decoy.when(core_2.get_well_core("A1")).then_return(mock_well_core_1) + decoy.when(core_2.get_well_core("B1")).then_return(mock_well_core_2) + + decoy.when(well_grid.create([["A1"]])).then_return(grid_2) + other_labware = Labware( + core=core_2, + api_version=api_version, + protocol_core=mock_protocol_core, + core_map=mock_map_core, + ) + mock_liquid = decoy.mock(cls=Liquid) + with pytest.raises(KeyError): + subject.load_liquid(["A1", "C1"], 10, mock_liquid) + + with pytest.raises(KeyError): + subject.load_liquid([subject["A1"], other_labware["A1"]], 10, mock_liquid) + + with pytest.raises(TypeError): + subject.load_liquid([2], 10, mock_liquid) # type: ignore[list-item] + + with pytest.raises(TypeError): + subject.load_liquid(["A1"], "A1", mock_liquid) # type: ignore[arg-type] + mock_liquid = decoy.mock(cls=Liquid) + + subject.load_liquid(["A1", subject["B1"]], 10, mock_liquid) + decoy.verify( + mock_labware_core.load_liquid( + { + "A1": 10, + "B1": 10, + }, + mock_liquid, + ) + ) + + +@pytest.mark.parametrize("api_version", versions_at_or_above(APIVersion(2, 22))) +def test_load_liquid_by_well_handles_valid_inputs( + decoy: Decoy, + mock_labware_core: LabwareCore, + api_version: APIVersion, + mock_protocol_core: ProtocolCore, + mock_map_core: LoadedCoreMap, +) -> None: + """It should load liquids of different volumes in different wells.""" + mock_well_core_1 = decoy.mock(cls=WellCore) + mock_well_core_2 = decoy.mock(cls=WellCore) + grid = well_grid.WellGrid( + columns_by_name={"1": ["A1", "B1"]}, + rows_by_name={"A": ["A1"], "B": ["B1"]}, + ) + decoy.when(mock_well_core_1.get_name()).then_return("A1") + decoy.when(mock_well_core_2.get_name()).then_return("B1") + + decoy.when(mock_labware_core.get_well_columns()).then_return([["A1", "B1"]]) + decoy.when(mock_labware_core.get_well_core("A1")).then_return(mock_well_core_1) + decoy.when(mock_labware_core.get_well_core("B1")).then_return(mock_well_core_2) + decoy.when(well_grid.create([["A1", "B1"]])).then_return(grid) + decoy.when(mock_well_core_2.get_display_name()).then_return("well 2") + subject = Labware( + core=mock_labware_core, + api_version=api_version, + protocol_core=mock_protocol_core, + core_map=mock_map_core, + ) + mock_liquid = decoy.mock(cls=Liquid) + + subject.load_liquid_by_well({"A1": 10, subject["B1"]: 11}, mock_liquid) + decoy.verify( + mock_labware_core.load_liquid( + { + "A1": 10, + "B1": 11, + }, + mock_liquid, + ) + ) + + +@pytest.mark.parametrize("api_version", versions_at_or_above(APIVersion(2, 22))) +def test_load_liquid_by_well_rejects_invalid_inputs( + decoy: Decoy, + mock_labware_core: LabwareCore, + api_version: APIVersion, + mock_protocol_core: ProtocolCore, + mock_map_core: LoadedCoreMap, +) -> None: + """It should require valid well specs.""" + mock_well_core_1 = decoy.mock(cls=WellCore) + mock_well_core_2 = decoy.mock(cls=WellCore) + + grid = well_grid.WellGrid( + columns_by_name={"1": ["A1", "B1"]}, + rows_by_name={"A": ["A1"], "B": ["B1"]}, + ) + decoy.when(mock_well_core_1.get_name()).then_return("A1") + decoy.when(mock_well_core_2.get_name()).then_return("B1") + decoy.when(mock_well_core_1.get_display_name()).then_return("well 1") + decoy.when(mock_well_core_2.get_display_name()).then_return("well 2") + decoy.when(mock_well_core_1.get_top(z_offset=0.0)).then_return(Point(4, 5, 6)) + decoy.when(mock_well_core_1.get_top(z_offset=0.0)).then_return(Point(7, 8, 9)) + decoy.when(mock_labware_core.get_well_columns()).then_return([["A1", "B1"]]) + decoy.when(mock_labware_core.get_well_core("A1")).then_return(mock_well_core_1) + decoy.when(mock_labware_core.get_well_core("B1")).then_return(mock_well_core_2) + decoy.when(well_grid.create([["A1", "B1"]])).then_return(grid) + subject = Labware( + core=mock_labware_core, + api_version=api_version, + protocol_core=mock_protocol_core, + core_map=mock_map_core, + ) + core_2 = decoy.mock(cls=LabwareCore) + mock_well_core_3 = decoy.mock(cls=WellCore) + decoy.when(mock_well_core_3.get_display_name()).then_return("well 3") + grid_2 = well_grid.WellGrid( + columns_by_name={"1": ["A1"]}, rows_by_name={"A": ["A1"]} + ) + decoy.when(mock_well_core_3.get_name()).then_return("A1") + decoy.when(core_2.get_well_columns()).then_return([["A1"]]) + decoy.when(core_2.get_well_core("A1")).then_return(mock_well_core_3) + decoy.when(mock_well_core_3.get_top(z_offset=0.0)).then_return(Point(1, 2, 3)) + + decoy.when(well_grid.create([["A1"]])).then_return(grid_2) + other_labware = Labware( + core=core_2, + api_version=api_version, + protocol_core=mock_protocol_core, + core_map=mock_map_core, + ) + + mock_liquid = decoy.mock(cls=Liquid) + with pytest.raises(KeyError): + subject.load_liquid_by_well({"A1": 10, "C1": 11}, mock_liquid) + + with pytest.raises(KeyError): + subject.load_liquid_by_well( + {subject["A1"]: 10, other_labware["A1"]: 11}, mock_liquid + ) + + with pytest.raises(TypeError): + subject.load_liquid_by_well({2: 10}, mock_liquid) # type: ignore[dict-item] + + with pytest.raises(TypeError): + subject.load_liquid_by_well({"A1": "A3"}, mock_liquid) # type: ignore[dict-item] + + +@pytest.mark.parametrize("api_version", versions_at_or_above(APIVersion(2, 22))) +def test_load_empty_handles_valid_inputs( + decoy: Decoy, + mock_labware_core: LabwareCore, + api_version: APIVersion, + mock_protocol_core: ProtocolCore, + mock_map_core: LoadedCoreMap, +) -> None: + """It should load lists of wells as empty.""" + mock_well_core_1 = decoy.mock(cls=WellCore) + mock_well_core_2 = decoy.mock(cls=WellCore) + grid = well_grid.WellGrid( + columns_by_name={"1": ["A1", "B1"]}, + rows_by_name={"A": ["A1"], "B": ["B1"]}, + ) + decoy.when(mock_well_core_1.get_name()).then_return("A1") + decoy.when(mock_well_core_2.get_name()).then_return("B1") + + decoy.when(mock_labware_core.get_well_columns()).then_return([["A1", "B1"]]) + decoy.when(mock_labware_core.get_well_core("A1")).then_return(mock_well_core_1) + decoy.when(mock_labware_core.get_well_core("B1")).then_return(mock_well_core_2) + decoy.when(well_grid.create([["A1", "B1"]])).then_return(grid) + + subject = Labware( + core=mock_labware_core, + api_version=api_version, + protocol_core=mock_protocol_core, + core_map=mock_map_core, + ) + + subject.load_empty(["A1", subject["B1"]]) + decoy.verify(mock_labware_core.load_empty(["A1", "B1"])) + + +@pytest.mark.parametrize("api_version", versions_at_or_above(APIVersion(2, 22))) +def test_load_empty_rejects_invalid_inputs( + decoy: Decoy, + mock_labware_core: LabwareCore, + api_version: APIVersion, + mock_protocol_core: ProtocolCore, + mock_map_core: LoadedCoreMap, +) -> None: + """It should require valid well specs.""" + mock_well_core_1 = decoy.mock(cls=WellCore) + mock_well_core_2 = decoy.mock(cls=WellCore) + + grid = well_grid.WellGrid( + columns_by_name={"1": ["A1", "B1"]}, + rows_by_name={"A": ["A1"], "B": ["B1"]}, + ) + decoy.when(mock_well_core_1.get_name()).then_return("A1") + decoy.when(mock_well_core_2.get_name()).then_return("B1") + decoy.when(mock_labware_core.get_well_columns()).then_return([["A1", "B1"]]) + decoy.when(mock_labware_core.get_well_core("A1")).then_return(mock_well_core_1) + decoy.when(mock_labware_core.get_well_core("B1")).then_return(mock_well_core_2) + decoy.when(well_grid.create([["A1", "B1"]])).then_return(grid) + subject = Labware( + core=mock_labware_core, + api_version=api_version, + protocol_core=mock_protocol_core, + core_map=mock_map_core, + ) + + core_2 = decoy.mock(cls=LabwareCore) + mock_well_core_3 = decoy.mock(cls=WellCore) + grid_2 = well_grid.WellGrid( + columns_by_name={"1": ["A1"]}, rows_by_name={"A": ["A1"]} + ) + decoy.when(mock_well_core_3.get_name()).then_return("A1") + decoy.when(core_2.get_well_columns()).then_return([["A1", "B1"]]) + decoy.when(core_2.get_well_core("A1")).then_return(mock_well_core_1) + decoy.when(core_2.get_well_core("B1")).then_return(mock_well_core_2) + + decoy.when(well_grid.create([["A1"]])).then_return(grid_2) + other_labware = Labware( + core=core_2, + api_version=api_version, + protocol_core=mock_protocol_core, + core_map=mock_map_core, + ) + with pytest.raises(KeyError): + subject.load_empty(["A1", "C1"]) + + with pytest.raises(KeyError): + subject.load_empty([subject["A1"], other_labware["A1"]]) + + with pytest.raises(TypeError): + subject.load_empty([2]) # type: ignore[list-item] diff --git a/api/tests/opentrons/protocol_api/test_liquid_class.py b/api/tests/opentrons/protocol_api/test_liquid_class.py index 48f3788f496..7118080eda0 100644 --- a/api/tests/opentrons/protocol_api/test_liquid_class.py +++ b/api/tests/opentrons/protocol_api/test_liquid_class.py @@ -12,7 +12,7 @@ def test_create_liquid_class( ) -> None: """It should create a LiquidClass from provided definition.""" assert LiquidClass.create(minimal_liquid_class_def1) == LiquidClass( - _name="water1", _display_name="water 1", _by_pipette_setting=[] + _name="water1", _display_name="water 1", _by_pipette_setting={} ) @@ -21,8 +21,11 @@ def test_get_for_pipette_and_tip( ) -> None: """It should get the properties for the specified pipette and tip.""" liq_class = LiquidClass.create(minimal_liquid_class_def2) - result = liq_class.get_for("p20_single_gen2", "opentrons_96_tiprack_20ul") - assert result.aspirate.flowRateByVolume == {"default": 50, "10": 40, "20": 30} + result = liq_class.get_for("flex_1channel_50", "opentrons_flex_96_tiprack_50ul") + assert result.aspirate.flow_rate_by_volume.as_dict() == { + 10.0: 40.0, + 20.0: 30.0, + } def test_get_for_raises_for_incorrect_pipette_or_tip( @@ -32,7 +35,7 @@ def test_get_for_raises_for_incorrect_pipette_or_tip( liq_class = LiquidClass.create(minimal_liquid_class_def2) with pytest.raises(ValueError): - liq_class.get_for("p20_single_gen2", "no_such_tiprack") + liq_class.get_for("flex_1channel_50", "no_such_tiprack") with pytest.raises(ValueError): - liq_class.get_for("p300_single", "opentrons_96_tiprack_20ul") + liq_class.get_for("no_such_pipette", "opentrons_flex_96_tiprack_50ul") diff --git a/api/tests/opentrons/protocol_api/test_liquid_class_properties.py b/api/tests/opentrons/protocol_api/test_liquid_class_properties.py new file mode 100644 index 00000000000..94e6dd49205 --- /dev/null +++ b/api/tests/opentrons/protocol_api/test_liquid_class_properties.py @@ -0,0 +1,206 @@ +"""Tests for LiquidClass properties and related functions.""" +import pytest +from opentrons_shared_data import load_shared_data +from opentrons_shared_data.liquid_classes.liquid_class_definition import ( + LiquidClassSchemaV1, + Coordinate, +) + +from opentrons.protocol_api._liquid_properties import ( + build_aspirate_properties, + build_single_dispense_properties, + build_multi_dispense_properties, + LiquidHandlingPropertyByVolume, +) + + +def test_build_aspirate_settings() -> None: + """It should convert the shared data aspirate settings to the PAPI type.""" + fixture_data = load_shared_data("liquid-class/fixtures/1/fixture_glycerol50.json") + liquid_class_model = LiquidClassSchemaV1.model_validate_json(fixture_data) + aspirate_data = liquid_class_model.byPipette[0].byTipType[0].aspirate + + aspirate_properties = build_aspirate_properties(aspirate_data) + + assert aspirate_properties.submerge.position_reference.value == "liquid-meniscus" + assert aspirate_properties.submerge.offset == Coordinate(x=0, y=0, z=-5) + assert aspirate_properties.submerge.speed == 100 + assert aspirate_properties.submerge.delay.enabled is True + assert aspirate_properties.submerge.delay.duration == 1.5 + + assert aspirate_properties.retract.position_reference.value == "well-top" + assert aspirate_properties.retract.offset == Coordinate(x=0, y=0, z=5) + assert aspirate_properties.retract.speed == 100 + assert aspirate_properties.retract.air_gap_by_volume.as_dict() == { + 5.0: 3.0, + 10.0: 4.0, + } + assert aspirate_properties.retract.touch_tip.enabled is True + assert aspirate_properties.retract.touch_tip.z_offset == 2 + assert aspirate_properties.retract.touch_tip.mm_to_edge == 1 + assert aspirate_properties.retract.touch_tip.speed == 50 + assert aspirate_properties.retract.delay.enabled is True + assert aspirate_properties.retract.delay.duration == 1 + + assert aspirate_properties.position_reference.value == "well-bottom" + assert aspirate_properties.offset == Coordinate(x=0, y=0, z=-5) + assert aspirate_properties.flow_rate_by_volume.as_dict() == {10: 50.0} + assert aspirate_properties.correction_by_volume.as_dict() == { + 1.0: -2.5, + 10.0: 3, + } + assert aspirate_properties.pre_wet is True + assert aspirate_properties.mix.enabled is True + assert aspirate_properties.mix.repetitions == 3 + assert aspirate_properties.mix.volume == 15 + assert aspirate_properties.delay.enabled is True + assert aspirate_properties.delay.duration == 2 + assert aspirate_properties.as_shared_data_model() == aspirate_data + + +def test_build_single_dispense_settings() -> None: + """It should convert the shared data single dispense settings to the PAPI type.""" + fixture_data = load_shared_data("liquid-class/fixtures/1/fixture_glycerol50.json") + liquid_class_model = LiquidClassSchemaV1.model_validate_json(fixture_data) + single_dispense_data = liquid_class_model.byPipette[0].byTipType[0].singleDispense + + single_dispense_properties = build_single_dispense_properties(single_dispense_data) + + assert ( + single_dispense_properties.submerge.position_reference.value + == "liquid-meniscus" + ) + assert single_dispense_properties.submerge.offset == Coordinate(x=0, y=0, z=-5) + assert single_dispense_properties.submerge.speed == 100 + assert single_dispense_properties.submerge.delay.enabled is True + assert single_dispense_properties.submerge.delay.duration == 1.5 + + assert single_dispense_properties.retract.position_reference.value == "well-top" + assert single_dispense_properties.retract.offset == Coordinate(x=0, y=0, z=5) + assert single_dispense_properties.retract.speed == 100 + assert single_dispense_properties.retract.air_gap_by_volume.as_dict() == { + 5.0: 3.0, + 10.0: 4.0, + } + assert single_dispense_properties.retract.touch_tip.enabled is True + assert single_dispense_properties.retract.touch_tip.z_offset == 2 + assert single_dispense_properties.retract.touch_tip.mm_to_edge == 1 + assert single_dispense_properties.retract.touch_tip.speed == 50 + assert single_dispense_properties.retract.blowout.enabled is True + assert single_dispense_properties.retract.blowout.location is not None + assert single_dispense_properties.retract.blowout.location.value == "trash" + assert single_dispense_properties.retract.blowout.flow_rate == 100 + assert single_dispense_properties.retract.delay.enabled is True + assert single_dispense_properties.retract.delay.duration == 1 + + assert single_dispense_properties.position_reference.value == "well-bottom" + assert single_dispense_properties.offset == Coordinate(x=0, y=0, z=-5) + assert single_dispense_properties.flow_rate_by_volume.as_dict() == { + 10.0: 40.0, + 20.0: 30.0, + } + assert single_dispense_properties.correction_by_volume.as_dict() == { + 2.0: -1.5, + 20.0: 2, + } + assert single_dispense_properties.mix.enabled is True + assert single_dispense_properties.mix.repetitions == 3 + assert single_dispense_properties.mix.volume == 15 + assert single_dispense_properties.push_out_by_volume.as_dict() == { + 10.0: 7.0, + 20.0: 10.0, + } + assert single_dispense_properties.delay.enabled is True + assert single_dispense_properties.delay.duration == 2.5 + assert single_dispense_properties.as_shared_data_model() == single_dispense_data + + +def test_build_multi_dispense_settings() -> None: + """It should convert the shared data multi dispense settings to the PAPI type.""" + fixture_data = load_shared_data("liquid-class/fixtures/1/fixture_glycerol50.json") + liquid_class_model = LiquidClassSchemaV1.model_validate_json(fixture_data) + multi_dispense_data = liquid_class_model.byPipette[0].byTipType[0].multiDispense + + assert multi_dispense_data is not None + multi_dispense_properties = build_multi_dispense_properties(multi_dispense_data) + assert multi_dispense_properties is not None + + assert ( + multi_dispense_properties.submerge.position_reference.value == "liquid-meniscus" + ) + assert multi_dispense_properties.submerge.offset == Coordinate(x=0, y=0, z=-5) + assert multi_dispense_properties.submerge.speed == 100 + assert multi_dispense_properties.submerge.delay.enabled is True + assert multi_dispense_properties.submerge.delay.duration == 1.5 + + assert multi_dispense_properties.retract.position_reference.value == "well-top" + assert multi_dispense_properties.retract.offset == Coordinate(x=0, y=0, z=5) + assert multi_dispense_properties.retract.speed == 100 + assert multi_dispense_properties.retract.air_gap_by_volume.as_dict() == { + 5.0: 3.0, + 10.0: 4.0, + } + assert multi_dispense_properties.retract.touch_tip.enabled is True + assert multi_dispense_properties.retract.touch_tip.z_offset == 2 + assert multi_dispense_properties.retract.touch_tip.mm_to_edge == 1 + assert multi_dispense_properties.retract.touch_tip.speed == 50 + assert multi_dispense_properties.retract.blowout.enabled is False + assert multi_dispense_properties.retract.blowout.location is None + assert multi_dispense_properties.retract.blowout.flow_rate is None + assert multi_dispense_properties.retract.delay.enabled is True + assert multi_dispense_properties.retract.delay.duration == 1 + + assert multi_dispense_properties.position_reference.value == "well-bottom" + assert multi_dispense_properties.offset == Coordinate(x=0, y=0, z=-5) + assert multi_dispense_properties.flow_rate_by_volume.as_dict() == { + 10.0: 40.0, + 20.0: 30.0, + } + assert multi_dispense_properties.correction_by_volume.as_dict() == { + 3.0: -0.5, + 30.0: 1, + } + assert multi_dispense_properties.conditioning_by_volume.as_dict() == { + 5.0: 5.0, + } + assert multi_dispense_properties.disposal_by_volume.as_dict() == { + 5.0: 3.0, + } + assert multi_dispense_properties.delay.enabled is True + assert multi_dispense_properties.delay.duration == 1 + assert multi_dispense_properties.as_shared_data_model() == multi_dispense_data + + +def test_build_multi_dispense_settings_none( + minimal_liquid_class_def2: LiquidClassSchemaV1, +) -> None: + """It should return None if there are no multi dispense properties in the model.""" + transfer_settings = minimal_liquid_class_def2.byPipette[0].byTipType[0] + assert build_multi_dispense_properties(transfer_settings.multiDispense) is None + + +def test_liquid_handling_property_by_volume() -> None: + """It should create a class that can interpolate values and add and delete new points.""" + subject = LiquidHandlingPropertyByVolume([(5.0, 50.0), (10.0, 250.0)]) + assert subject.as_dict() == {5.0: 50, 10.0: 250} + assert subject.get_for_volume(7) == 130.0 + assert subject.as_list_of_tuples() == [(5.0, 50.0), (10.0, 250.0)] + + subject.set_for_volume(volume=7, value=175.5) + assert subject.as_dict() == { + 5.0: 50, + 10.0: 250, + 7.0: 175.5, + } + assert subject.get_for_volume(7) == 175.5 + + subject.delete_for_volume(7) + assert subject.as_dict() == {5.0: 50, 10.0: 250} + assert subject.get_for_volume(7) == 130.0 + + with pytest.raises(KeyError, match="No value set for volume"): + subject.delete_for_volume(7) + + # Test bounds + assert subject.get_for_volume(1) == 50.0 + assert subject.get_for_volume(1000) == 250.0 diff --git a/api/tests/opentrons/protocol_api/test_protocol_context.py b/api/tests/opentrons/protocol_api/test_protocol_context.py index 5e516a5b274..80728b7820c 100644 --- a/api/tests/opentrons/protocol_api/test_protocol_context.py +++ b/api/tests/opentrons/protocol_api/test_protocol_context.py @@ -742,6 +742,115 @@ def test_load_labware_on_adapter( decoy.verify(mock_core_map.add(mock_labware_core, result), times=1) +@pytest.mark.parametrize("api_version", [APIVersion(2, 23)]) +def test_load_labware_with_lid( + decoy: Decoy, + mock_core: ProtocolCore, + mock_core_map: LoadedCoreMap, + api_version: APIVersion, + subject: ProtocolContext, +) -> None: + """It should create a labware with a lid on it using its execution core.""" + mock_labware_core = decoy.mock(cls=LabwareCore) + mock_lid_core = decoy.mock(cls=LabwareCore) + + decoy.when(mock_validation.ensure_lowercase_name("UPPERCASE_LABWARE")).then_return( + "lowercase_labware" + ) + + decoy.when(mock_validation.ensure_lowercase_name("UPPERCASE_LID")).then_return( + "lowercase_lid" + ) + decoy.when(mock_core.robot_type).then_return("OT-3 Standard") + decoy.when( + mock_validation.ensure_and_convert_deck_slot(42, api_version, "OT-3 Standard") + ).then_return(DeckSlotName.SLOT_C1) + + decoy.when( + mock_core.load_labware( + load_name="lowercase_labware", + location=DeckSlotName.SLOT_C1, + label="some_display_name", + namespace="some_namespace", + version=1337, + ) + ).then_return(mock_labware_core) + decoy.when(mock_lid_core.get_well_columns()).then_return([]) + + decoy.when( + mock_core.load_lid( + load_name="lowercase_lid", + location=mock_labware_core, + namespace="some_namespace", + version=None, + ) + ).then_return(mock_lid_core) + + decoy.when(mock_labware_core.get_name()).then_return("Full Name") + decoy.when(mock_labware_core.get_display_name()).then_return("Display Name") + decoy.when(mock_labware_core.get_well_columns()).then_return([]) + + result = subject.load_labware( + load_name="UPPERCASE_LABWARE", + location=42, + label="some_display_name", + namespace="some_namespace", + version=1337, + lid="UPPERCASE_LID", + ) + + assert isinstance(result, Labware) + assert result.name == "Full Name" + + decoy.verify(mock_core_map.add(mock_labware_core, result), times=1) + + +@pytest.mark.parametrize("api_version", [APIVersion(2, 23)]) +def test_load_lid_stack( + decoy: Decoy, + mock_core: ProtocolCore, + mock_core_map: LoadedCoreMap, + api_version: APIVersion, + subject: ProtocolContext, +) -> None: + """It should create a labware with a lid on it using its execution core.""" + mock_lid_core = decoy.mock(cls=LabwareCore) + + decoy.when(mock_validation.ensure_lowercase_name("UPPERCASE_LID")).then_return( + "lowercase_lid" + ) + + decoy.when(mock_core.robot_type).then_return("OT-3 Standard") + decoy.when( + mock_validation.ensure_and_convert_deck_slot(42, api_version, "OT-3 Standard") + ).then_return(DeckSlotName.SLOT_C1) + + decoy.when( + mock_core.load_lid_stack( + load_name="lowercase_lid", + location=DeckSlotName.SLOT_C1, + quantity=5, + namespace="some_namespace", + version=1337, + ) + ).then_return(mock_lid_core) + + decoy.when(mock_lid_core.get_name()).then_return("STACK_OBJECT") + decoy.when(mock_lid_core.get_display_name()).then_return("") + decoy.when(mock_lid_core.get_well_columns()).then_return([]) + + result = subject.load_lid_stack( + load_name="UPPERCASE_LID", + location=42, + quantity=5, + namespace="some_namespace", + version=1337, + ) + + assert isinstance(result, Labware) + assert result.name == "STACK_OBJECT" + + def test_loaded_labware( decoy: Decoy, mock_core_map: LoadedCoreMap, @@ -1303,7 +1412,7 @@ def test_define_liquid_class( ) -> None: """It should create the liquid class definition.""" expected_liquid_class = LiquidClass( - _name="volatile_100", _display_name="volatile 100%", _by_pipette_setting=[] + _name="volatile_100", _display_name="volatile 100%", _by_pipette_setting={} ) decoy.when(mock_core.define_liquid_class("volatile_90")).then_return( expected_liquid_class diff --git a/api/tests/opentrons/protocol_api/test_robot_context.py b/api/tests/opentrons/protocol_api/test_robot_context.py new file mode 100644 index 00000000000..36b94c52b15 --- /dev/null +++ b/api/tests/opentrons/protocol_api/test_robot_context.py @@ -0,0 +1,256 @@ +"""Test the functionality of the `RobotContext`.""" +import pytest +from decoy import Decoy +from typing import Union, Optional + +from opentrons.types import ( + DeckLocation, + Mount, + Point, + Location, + DeckSlotName, + AxisType, + StringAxisMap, + AxisMapType, +) +from opentrons.protocols.api_support.types import APIVersion +from opentrons.protocol_api.core.common import ProtocolCore, RobotCore +from opentrons.protocol_api import RobotContext, ModuleContext +from opentrons.protocol_api.deck import Deck +from opentrons_shared_data.pipette.types import PipetteNameType + +from opentrons.protocol_api._types import PipetteActionTypes, PlungerPositionTypes + + +@pytest.fixture +def mock_core(decoy: Decoy) -> RobotCore: + """Get a mock module implementation core.""" + return decoy.mock(cls=RobotCore) + + +@pytest.fixture +def api_version() -> APIVersion: + """Get the API version to test at.""" + return APIVersion(2, 22) + + +@pytest.fixture +def mock_deck(decoy: Decoy) -> Deck: + """Get a mocked deck object.""" + deck = decoy.mock(cls=Deck) + decoy.when(deck.get_slot_center(DeckSlotName.SLOT_D1.value)).then_return( + Point(3, 3, 3) + ) + return deck + + +@pytest.fixture +def mock_protocol(decoy: Decoy, mock_deck: Deck, mock_core: RobotCore) -> ProtocolCore: + """Get a mock protocol implementation core without a 96 channel attached.""" + protocol_core = decoy.mock(cls=ProtocolCore) + decoy.when(protocol_core.robot_type).then_return("OT-3 Standard") + decoy.when(protocol_core.load_robot()).then_return(mock_core) + return protocol_core + + +@pytest.fixture +def subject( + decoy: Decoy, + mock_core: RobotCore, + mock_protocol: ProtocolCore, + api_version: APIVersion, +) -> RobotContext: + """Get a RobotContext test subject with its dependencies mocked out.""" + decoy.when(mock_core.get_pipette_type_from_engine(Mount.LEFT)).then_return( + PipetteNameType.P1000_SINGLE_FLEX + ) + decoy.when(mock_core.get_pipette_type_from_engine(Mount.RIGHT)).then_return( + PipetteNameType.P1000_SINGLE_FLEX + ) + return RobotContext( + core=mock_core, api_version=api_version, protocol_core=mock_protocol + ) + + +@pytest.mark.parametrize( + argnames=["mount", "destination", "speed"], + argvalues=[ + ("left", Location(point=Point(1, 2, 3), labware=None), None), + (Mount.RIGHT, Location(point=Point(1, 2, 3), labware=None), 100), + ], +) +def test_move_to( + decoy: Decoy, + subject: RobotContext, + mount: Union[str, Mount], + destination: Location, + speed: Optional[float], +) -> None: + """Test `RobotContext.move_to`.""" + subject.move_to(mount, destination, speed) + core_mount: Mount + if isinstance(mount, str): + core_mount = Mount.string_to_mount(mount) + else: + core_mount = mount + decoy.verify(subject._core.move_to(core_mount, destination.point, speed)) + + +@pytest.mark.parametrize( + argnames=[ + "axis_map", + "critical_point", + "expected_axis_map", + "expected_critical_point", + "speed", + ], + argvalues=[ + ( + {"x": 100, "Y": 50, "z_g": 80}, + {"x": 5, "Y": 5, "z_g": 5}, + {AxisType.X: 100, AxisType.Y: 50, AxisType.Z_G: 80}, + {AxisType.X: 5, AxisType.Y: 5, AxisType.Z_G: 5}, + None, + ), + ( + {"x": 5, "Y": 5}, + {"x": 5, "Y": 5}, + {AxisType.X: 5, AxisType.Y: 5}, + {AxisType.X: 5, AxisType.Y: 5}, + None, + ), + ], +) +def test_move_axes_to( + decoy: Decoy, + subject: RobotContext, + axis_map: Union[StringAxisMap, AxisMapType], + critical_point: Union[StringAxisMap, AxisMapType], + expected_axis_map: AxisMapType, + expected_critical_point: AxisMapType, + speed: Optional[float], +) -> None: + """Test `RobotContext.move_axes_to`.""" + subject.move_axes_to(axis_map, critical_point, speed) + decoy.verify( + subject._core.move_axes_to(expected_axis_map, expected_critical_point, speed) + ) + + +@pytest.mark.parametrize( + argnames=["axis_map", "converted_map", "speed"], + argvalues=[ + ( + {"x": 10, "Y": 10, "z_g": 10}, + {AxisType.X: 10, AxisType.Y: 10, AxisType.Z_G: 10}, + None, + ), + ({AxisType.P_L: 10}, {AxisType.P_L: 10}, 5), + ], +) +def test_move_axes_relative( + decoy: Decoy, + subject: RobotContext, + axis_map: Union[StringAxisMap, AxisMapType], + converted_map: AxisMapType, + speed: Optional[float], +) -> None: + """Test `RobotContext.move_axes_relative`.""" + subject.move_axes_relative(axis_map, speed) + decoy.verify(subject._core.move_axes_relative(converted_map, speed)) + + +@pytest.mark.parametrize( + argnames=["mount", "location_to_move", "expected_axis_map"], + argvalues=[ + ( + "left", + Location(point=Point(1, 2, 3), labware=None), + {AxisType.Z_L: 3, AxisType.X: 1, AxisType.Y: 2}, + ), + ( + Mount.EXTENSION, + Location(point=Point(1, 2, 3), labware=None), + {AxisType.Z_G: 3, AxisType.X: 1, AxisType.Y: 2}, + ), + ], +) +def test_get_axes_coordinates_for( + subject: RobotContext, + mount: Union[Mount, str], + location_to_move: Union[Location, ModuleContext, DeckLocation], + expected_axis_map: AxisMapType, +) -> None: + """Test `RobotContext.get_axis_coordinates_for`.""" + res = subject.axis_coordinates_for(mount, location_to_move) + assert res == expected_axis_map + + +@pytest.mark.parametrize( + argnames=["mount", "volume", "action", "expected_axis_map"], + argvalues=[ + (Mount.RIGHT, 200, PipetteActionTypes.ASPIRATE_ACTION, {AxisType.P_R: 100}), + (Mount.LEFT, 100, PipetteActionTypes.DISPENSE_ACTION, {AxisType.P_L: 100}), + ], +) +def test_plunger_coordinates_for_volume( + decoy: Decoy, + subject: RobotContext, + mount: Mount, + volume: float, + action: PipetteActionTypes, + expected_axis_map: AxisMapType, +) -> None: + """Test `RobotContext.plunger_coordinates_for_volume`.""" + decoy.when( + subject._core.get_plunger_position_from_volume( + mount, volume, action, "OT-3 Standard" + ) + ).then_return(100) + + result = subject.plunger_coordinates_for_volume(mount, volume, action) + assert result == expected_axis_map + + +@pytest.mark.parametrize( + argnames=["mount", "position_name", "expected_axis_map"], + argvalues=[ + (Mount.RIGHT, PlungerPositionTypes.PLUNGER_TOP, {AxisType.P_R: 3}), + ( + Mount.RIGHT, + PlungerPositionTypes.PLUNGER_BOTTOM, + {AxisType.P_R: 3}, + ), + ], +) +def test_plunger_coordinates_for_named_position( + decoy: Decoy, + subject: RobotContext, + mount: Mount, + position_name: PlungerPositionTypes, + expected_axis_map: AxisMapType, +) -> None: + """Test `RobotContext.plunger_coordinates_for_named_position`.""" + decoy.when( + subject._core.get_plunger_position_from_name(mount, position_name) + ).then_return(3) + result = subject.plunger_coordinates_for_named_position(mount, position_name) + assert result == expected_axis_map + + +def test_plunger_methods_raise_without_pipette( + mock_core: RobotCore, mock_protocol: ProtocolCore, api_version: APIVersion +) -> None: + """Test that `RobotContext` plunger functions raise without pipette attached.""" + subject = RobotContext( + core=mock_core, api_version=api_version, protocol_core=mock_protocol + ) + with pytest.raises(ValueError): + subject.plunger_coordinates_for_named_position( + Mount.LEFT, PlungerPositionTypes.PLUNGER_TOP + ) + + with pytest.raises(ValueError): + subject.plunger_coordinates_for_volume( + Mount.LEFT, 200, PipetteActionTypes.ASPIRATE_ACTION + ) diff --git a/api/tests/opentrons/protocol_api/test_validation.py b/api/tests/opentrons/protocol_api/test_validation.py index 2a2ed6375b0..ce12d1a8f53 100644 --- a/api/tests/opentrons/protocol_api/test_validation.py +++ b/api/tests/opentrons/protocol_api/test_validation.py @@ -1,11 +1,13 @@ """Tests for Protocol API input validation.""" -from typing import ContextManager, List, Type, Union, Optional, Dict, Any + +from typing import ContextManager, List, Type, Union, Optional, Dict, Sequence, Any from contextlib import nullcontext as do_not_raise from decoy import Decoy import pytest import re +from opentrons.protocols.advanced_control.transfers.common import TransferTipPolicyV2 from opentrons_shared_data.labware.labware_definition import ( LabwareRole, Parameters as LabwareDefinitionParameters, @@ -13,7 +15,16 @@ from opentrons_shared_data.pipette.types import PipetteNameType from opentrons_shared_data.robot.types import RobotType -from opentrons.types import Mount, DeckSlotName, StagingSlotName, Location, Point +from opentrons.types import ( + Mount, + DeckSlotName, + AxisType, + AxisMapType, + StringAxisMap, + StagingSlotName, + Location, + Point, +) from opentrons.hardware_control.modules.types import ( ModuleModel, MagneticModuleModel, @@ -25,7 +36,13 @@ from opentrons.protocols.models import LabwareDefinition from opentrons.protocols.api_support.types import APIVersion from opentrons.protocols.api_support.util import APIVersionError -from opentrons.protocol_api import validation as subject, Well, Labware +from opentrons.protocol_api import ( + validation as subject, + Well, + Labware, + TrashBin, + WasteChute, +) @pytest.mark.parametrize( @@ -207,7 +224,9 @@ def test_ensure_deck_slot_invalid( """It should raise an exception if given an invalid name.""" with pytest.raises(expected_error_type, match=expected_error_match): subject.ensure_and_convert_deck_slot( - input_value, input_api_version, input_robot_type # type: ignore[arg-type] + input_value, # type: ignore[arg-type] + input_api_version, + input_robot_type, ) @@ -227,23 +246,23 @@ def test_ensure_lowercase_name_invalid() -> None: ("definition", "expected_raise"), [ ( - LabwareDefinition.construct( # type: ignore[call-arg] + LabwareDefinition.model_construct( # type: ignore[call-arg] allowedRoles=[LabwareRole.labware], - parameters=LabwareDefinitionParameters.construct(loadName="Foo"), # type: ignore[call-arg] + parameters=LabwareDefinitionParameters.model_construct(loadName="Foo"), # type: ignore[call-arg] ), do_not_raise(), ), ( - LabwareDefinition.construct( # type: ignore[call-arg] + LabwareDefinition.model_construct( # type: ignore[call-arg] allowedRoles=[], - parameters=LabwareDefinitionParameters.construct(loadName="Foo"), # type: ignore[call-arg] + parameters=LabwareDefinitionParameters.model_construct(loadName="Foo"), # type: ignore[call-arg] ), do_not_raise(), ), ( - LabwareDefinition.construct( # type: ignore[call-arg] + LabwareDefinition.model_construct( # type: ignore[call-arg] allowedRoles=[LabwareRole.adapter], - parameters=LabwareDefinitionParameters.construct(loadName="Foo"), # type: ignore[call-arg] + parameters=LabwareDefinitionParameters.model_construct(loadName="Foo"), # type: ignore[call-arg] ), pytest.raises(subject.LabwareDefinitionIsNotLabwareError), ), @@ -261,23 +280,23 @@ def test_ensure_definition_is_labware( ("definition", "expected_raise"), [ ( - LabwareDefinition.construct( # type: ignore[call-arg] + LabwareDefinition.model_construct( # type: ignore[call-arg] allowedRoles=[LabwareRole.adapter], - parameters=LabwareDefinitionParameters.construct(loadName="Foo"), # type: ignore[call-arg] + parameters=LabwareDefinitionParameters.model_construct(loadName="Foo"), # type: ignore[call-arg] ), do_not_raise(), ), ( - LabwareDefinition.construct( # type: ignore[call-arg] + LabwareDefinition.model_construct( # type: ignore[call-arg] allowedRoles=[], - parameters=LabwareDefinitionParameters.construct(loadName="Foo"), # type: ignore[call-arg] + parameters=LabwareDefinitionParameters.model_construct(loadName="Foo"), # type: ignore[call-arg] ), pytest.raises(subject.LabwareDefinitionIsNotAdapterError), ), ( - LabwareDefinition.construct( # type: ignore[call-arg] + LabwareDefinition.model_construct( # type: ignore[call-arg] allowedRoles=[LabwareRole.labware], - parameters=LabwareDefinitionParameters.construct(loadName="Foo"), # type: ignore[call-arg] + parameters=LabwareDefinitionParameters.model_construct(loadName="Foo"), # type: ignore[call-arg] ), pytest.raises(subject.LabwareDefinitionIsNotAdapterError), ), @@ -456,7 +475,7 @@ def test_validate_well_no_location(decoy: Decoy) -> None: assert result == expected_result -def test_validate_coordinates(decoy: Decoy) -> None: +def test_validate_well_coordinates(decoy: Decoy) -> None: """Should return a WellTarget with no location.""" input_location = Location(point=Point(x=1, y=1, z=2), labware=None) expected_result = subject.PointTarget(location=input_location, in_place=False) @@ -531,7 +550,8 @@ def test_validate_with_wrong_location() -> None: """Should raise a LocationTypeError.""" with pytest.raises(subject.LocationTypeError): subject.validate_location( - location=42, last_location=None # type: ignore[arg-type] + location=42, # type: ignore[arg-type] + last_location=None, ) @@ -559,3 +579,298 @@ def test_validate_last_location_with_labware(decoy: Decoy) -> None: result = subject.validate_location(location=None, last_location=input_last_location) assert result == subject.PointTarget(location=input_last_location, in_place=True) + + +def test_ensure_boolean() -> None: + """It should return a boolean value.""" + assert subject.ensure_boolean(False) is False + + +@pytest.mark.parametrize("value", [0, "False", "f", 0.0]) +def test_ensure_boolean_raises(value: Union[str, int, float]) -> None: + """It should raise if the value is not a boolean.""" + with pytest.raises(ValueError, match="must be a boolean"): + subject.ensure_boolean(value) # type: ignore[arg-type] + + +@pytest.mark.parametrize("value", [-1.23, -1, 0, 0.0, 1, 1.23]) +def test_ensure_float(value: Union[int, float]) -> None: + """It should return a float value.""" + assert subject.ensure_float(value) == float(value) + + +def test_ensure_float_raises() -> None: + """It should raise if the value is not a float or an integer.""" + with pytest.raises(ValueError, match="must be a floating point"): + subject.ensure_float("1.23") # type: ignore[arg-type] + + +@pytest.mark.parametrize("value", [0, 0.1, 1, 1.0]) +def test_ensure_positive_float(value: Union[int, float]) -> None: + """It should return a positive float.""" + assert subject.ensure_positive_float(value) == float(value) + + +@pytest.mark.parametrize("value", [-1, -1.0, float("inf"), float("-inf"), float("nan")]) +def test_ensure_positive_float_raises(value: Union[int, float]) -> None: + """It should raise if value is not a positive float.""" + with pytest.raises(ValueError, match="(non-infinite|positive float)"): + subject.ensure_positive_float(value) + + +def test_ensure_positive_int() -> None: + """It should return a positive int.""" + assert subject.ensure_positive_int(42) == 42 + + +@pytest.mark.parametrize("value", [1.0, -1.0, -1]) +def test_ensure_positive_int_raises(value: Union[int, float]) -> None: + """It should raise if value is not a positive integer.""" + with pytest.raises(ValueError, match="integer"): + subject.ensure_positive_int(value) # type: ignore[arg-type] + + +def test_validate_coordinates() -> None: + """It should validate the coordinates and return them as a tuple.""" + assert subject.validate_coordinates([1, 2.0, 3.3]) == (1.0, 2.0, 3.3) + + +@pytest.mark.parametrize("value", [[1, 2.0], [1, 2.0, 3.3, 4.2], ["1", 2, 3]]) +def test_validate_coordinates_raises(value: Sequence[Union[int, float, str]]) -> None: + """It should raise if value is not a valid sequence of three numbers.""" + with pytest.raises(ValueError, match="(exactly three|must be floats)"): + subject.validate_coordinates(value) # type: ignore[arg-type] + + +@pytest.mark.parametrize( + argnames=["axis_map", "robot_type", "is_96_channel", "expected_axis_map"], + argvalues=[ + ( + {"x": 100, "Y": 50, "z_g": 80}, + "OT-3 Standard", + True, + {AxisType.X: 100, AxisType.Y: 50, AxisType.Z_G: 80}, + ), + ({"z_r": 80}, "OT-2 Standard", False, {AxisType.Z_R: 80}), + ( + {"Z_L": 19, "P_L": 20}, + "OT-2 Standard", + False, + {AxisType.Z_L: 19, AxisType.P_L: 20}, + ), + ({"Q": 5}, "OT-3 Standard", True, {AxisType.Q: 5}), + ], +) +def test_ensure_axis_map_type_success( + axis_map: Union[AxisMapType, StringAxisMap], + robot_type: RobotType, + is_96_channel: bool, + expected_axis_map: AxisMapType, +) -> None: + """Check that axis map type validation returns the correct shape.""" + res = subject.ensure_axis_map_type(axis_map, robot_type, is_96_channel) + assert res == expected_axis_map + + +@pytest.mark.parametrize( + argnames=["axis_map", "robot_type", "is_96_channel", "error_message"], + argvalues=[ + ( + {AxisType.X: 100, "y": 50}, + "OT-3 Standard", + True, + "Please provide an `axis_map` with only string or only AxisType keys", + ), + ( + {AxisType.Z_R: 60}, + "OT-3 Standard", + True, + "A 96 channel is attached. You cannot move the `Z_R` mount.", + ), + ( + {"Z_G": 19, "P_L": 20}, + "OT-2 Standard", + False, + "An OT-2 Robot only accepts the following axes ", + ), + ( + {"Q": 5}, + "OT-3 Standard", + False, + "A 96 channel is not attached. The clamp `Q` motor does not exist.", + ), + ], +) +def test_ensure_axis_map_type_failure( + axis_map: Union[AxisMapType, StringAxisMap], + robot_type: RobotType, + is_96_channel: bool, + error_message: str, +) -> None: + """Check that axis_map validation occurs for the given scenarios.""" + with pytest.raises(subject.IncorrectAxisError, match=error_message): + subject.ensure_axis_map_type(axis_map, robot_type, is_96_channel) + + +@pytest.mark.parametrize( + argnames=["axis_map", "robot_type", "error_message"], + argvalues=[ + ( + {AxisType.X: 100, AxisType.P_L: 50}, + "OT-3 Standard", + "A critical point only accepts Flex gantry axes which are ", + ), + ( + {AxisType.Z_G: 60}, + "OT-2 Standard", + "A critical point only accepts OT-2 gantry axes which are ", + ), + ], +) +def test_ensure_only_gantry_axis_map_type( + axis_map: AxisMapType, robot_type: RobotType, error_message: str +) -> None: + """Check that gantry axis_map validation occurs for the given scenarios.""" + with pytest.raises(subject.IncorrectAxisError, match=error_message): + subject.ensure_only_gantry_axis_map_type(axis_map, robot_type) + + +@pytest.mark.parametrize( + ["value", "expected_result"], + [ + ("once", TransferTipPolicyV2.ONCE), + ("NEVER", TransferTipPolicyV2.NEVER), + ("alWaYs", TransferTipPolicyV2.ALWAYS), + ("Per Source", TransferTipPolicyV2.PER_SOURCE), + ], +) +def test_ensure_new_tip_policy( + value: str, expected_result: TransferTipPolicyV2 +) -> None: + """It should return the expected tip policy.""" + assert subject.ensure_new_tip_policy(value) == expected_result + + +def test_ensure_new_tip_policy_raises() -> None: + """It should raise ValueError for invalid new_tip value.""" + with pytest.raises(ValueError, match="is invalid value for 'new_tip'"): + subject.ensure_new_tip_policy("blah") + + +@pytest.mark.parametrize( + ["target", "expected_raise"], + [ + ( + "a", + pytest.raises( + ValueError, match="'a' is not a valid location for transfer." + ), + ), + ( + ["a"], + pytest.raises( + ValueError, match="'a' is not a valid location for transfer." + ), + ), + ( + [("a",)], + pytest.raises( + ValueError, match="'a' is not a valid location for transfer." + ), + ), + ( + [], + pytest.raises( + ValueError, match="No target well\\(s\\) specified for transfer." + ), + ), + ], +) +def test_ensure_valid_flat_wells_list_raises_for_invalid_targets( + target: Any, + expected_raise: ContextManager[Any], +) -> None: + """It should raise an error if target location is invalid.""" + with expected_raise: + subject.ensure_valid_flat_wells_list_for_transfer_v2(target) + + +def test_ensure_valid_flat_wells_list_raises_for_mixed_targets(decoy: Decoy) -> None: + """It should raise appropriate error if target has mixed valid and invalid wells.""" + target1 = [decoy.mock(cls=Well), "a"] + with pytest.raises(ValueError, match="'a' is not a valid location for transfer."): + subject.ensure_valid_flat_wells_list_for_transfer_v2(target1) # type: ignore[arg-type] + + target2 = [[decoy.mock(cls=Well)], ["a"]] + with pytest.raises(ValueError, match="'a' is not a valid location for transfer."): + subject.ensure_valid_flat_wells_list_for_transfer_v2(target2) # type: ignore[arg-type] + + +def test_ensure_valid_flat_wells_list(decoy: Decoy) -> None: + """It should convert the locations to flat lists correctly.""" + target1 = decoy.mock(cls=Well) + target2 = decoy.mock(cls=Well) + + assert subject.ensure_valid_flat_wells_list_for_transfer_v2(target1) == [target1] + assert subject.ensure_valid_flat_wells_list_for_transfer_v2([target1, target2]) == [ + target1, + target2, + ] + assert subject.ensure_valid_flat_wells_list_for_transfer_v2( + [ + [target1, target1], + [target2, target2], + ] + ) == [target1, target1, target2, target2] + assert subject.ensure_valid_flat_wells_list_for_transfer_v2((target1, target2)) == [ + target1, + target2, + ] + assert subject.ensure_valid_flat_wells_list_for_transfer_v2( + ( + [target1, target1], + [target2, target2], + ) + ) == [target1, target1, target2, target2] + + +def test_ensure_valid_tip_drop_location_for_transfer_v2( + decoy: Decoy, +) -> None: + """It should check that the tip drop location is valid.""" + mock_well = decoy.mock(cls=Well) + mock_location = Location(point=Point(x=1, y=1, z=1), labware=mock_well) + mock_trash_bin = decoy.mock(cls=TrashBin) + mock_waste_chute = decoy.mock(cls=WasteChute) + assert ( + subject.ensure_valid_tip_drop_location_for_transfer_v2(mock_well) == mock_well + ) + assert ( + subject.ensure_valid_tip_drop_location_for_transfer_v2(mock_location) + == mock_location + ) + assert ( + subject.ensure_valid_tip_drop_location_for_transfer_v2(mock_trash_bin) + == mock_trash_bin + ) + assert ( + subject.ensure_valid_tip_drop_location_for_transfer_v2(mock_waste_chute) + == mock_waste_chute + ) + + +def test_ensure_valid_tip_drop_location_for_transfer_v2_raises(decoy: Decoy) -> None: + """It should raise an error for invalid tip drop locations.""" + with pytest.raises(TypeError, match="However, it is '\\['a'\\]'"): + subject.ensure_valid_tip_drop_location_for_transfer_v2(["a"]) # type: ignore[arg-type] + + mock_labware = decoy.mock(cls=Labware) + with pytest.raises(TypeError, match=f"However, it is '{mock_labware}'"): + subject.ensure_valid_tip_drop_location_for_transfer_v2(mock_labware) # type: ignore[arg-type] + + with pytest.raises( + TypeError, match="However, the given location doesn't refer to any well." + ): + subject.ensure_valid_tip_drop_location_for_transfer_v2( + Location(point=Point(x=1, y=1, z=1), labware=None) + ) diff --git a/api/tests/opentrons/protocol_api/test_well.py b/api/tests/opentrons/protocol_api/test_well.py index ef1eed84c62..c0ef530289b 100644 --- a/api/tests/opentrons/protocol_api/test_well.py +++ b/api/tests/opentrons/protocol_api/test_well.py @@ -1,4 +1,5 @@ """Tests for the InstrumentContext public interface.""" + import pytest from decoy import Decoy diff --git a/api/tests/opentrons/protocol_api_integration/conftest.py b/api/tests/opentrons/protocol_api_integration/conftest.py new file mode 100644 index 00000000000..fa98ccbb039 --- /dev/null +++ b/api/tests/opentrons/protocol_api_integration/conftest.py @@ -0,0 +1,28 @@ +"""Fixtures for protocol api integration tests.""" + +import pytest +from _pytest.fixtures import SubRequest +from typing import Generator + +from opentrons import simulate, protocol_api +from opentrons.protocol_api.core.engine import ENGINE_CORE_API_VERSION + + +@pytest.fixture +def simulated_protocol_context( + request: SubRequest, +) -> Generator[protocol_api.ProtocolContext, None, None]: + """Return a protocol context with requested version and robot.""" + version, robot_type = request.param + context = simulate.get_protocol_api(version=version, robot_type=robot_type) + try: + yield context + finally: + if context.api_version >= ENGINE_CORE_API_VERSION: + # TODO(jbl, 2024-11-14) this is a hack of a hack to close the hardware and the PE thread when a test is + # complete. At some point this should be replaced with a more holistic way of safely cleaning up these + # threads so they don't leak and cause tests to fail when `get_protocol_api` is called too many times. + simulate._LIVE_PROTOCOL_ENGINE_CONTEXTS.close() + else: + # If this is a non-PE context we need to clean up the hardware thread manually + context._hw_manager.hardware.clean_up() diff --git a/api/tests/opentrons/protocol_api_integration/test_liquid_classes.py b/api/tests/opentrons/protocol_api_integration/test_liquid_classes.py index 049edae5c0f..20bbd2b646c 100644 --- a/api/tests/opentrons/protocol_api_integration/test_liquid_classes.py +++ b/api/tests/opentrons/protocol_api_integration/test_liquid_classes.py @@ -3,58 +3,63 @@ from decoy import Decoy from opentrons_shared_data.robot.types import RobotTypeEnum -from opentrons import simulate +from opentrons.protocol_api import ProtocolContext from opentrons.config import feature_flags as ff -@pytest.mark.ot2_only +@pytest.mark.ot3_only +@pytest.mark.parametrize( + "simulated_protocol_context", [("2.20", "Flex")], indirect=True +) def test_liquid_class_creation_and_property_fetching( - decoy: Decoy, mock_feature_flags: None + decoy: Decoy, + mock_feature_flags: None, + simulated_protocol_context: ProtocolContext, ) -> None: """It should create the liquid class and provide access to its properties.""" - decoy.when(ff.allow_liquid_classes(RobotTypeEnum.OT2)).then_return(True) - protocol_context = simulate.get_protocol_api(version="2.20", robot_type="OT-2") - pipette_left = protocol_context.load_instrument("p20_single_gen2", mount="left") - pipette_right = protocol_context.load_instrument("p300_multi", mount="right") - tiprack = protocol_context.load_labware("opentrons_96_tiprack_20ul", "1") - - glycerol_50 = protocol_context.define_liquid_class("fixture_glycerol50") + decoy.when(ff.allow_liquid_classes(RobotTypeEnum.FLEX)).then_return(True) + pipette_load_name = "flex_8channel_50" + simulated_protocol_context.load_instrument(pipette_load_name, mount="left") + tiprack = simulated_protocol_context.load_labware( + "opentrons_flex_96_tiprack_50ul", "D1" + ) + water = simulated_protocol_context.define_liquid_class("water") - assert glycerol_50.name == "fixture_glycerol50" - assert glycerol_50.display_name == "Glycerol 50%" + assert water.name == "water" + assert water.display_name == "Water" - # TODO (spp, 2024-10-17): update this to use pipette's load name instead of pipette.name + # TODO (spp, 2024-10-17): update this to fetch pipette load name from instrument context assert ( - glycerol_50.get_for( - pipette_left.name, tiprack.load_name - ).dispense.flowRateByVolume["default"] + water.get_for( + pipette_load_name, tiprack.load_name + ).dispense.flow_rate_by_volume.get_for_volume(1) == 50 ) assert ( - glycerol_50.get_for( - pipette_left.name, tiprack.load_name - ).aspirate.submerge.speed + water.get_for(pipette_load_name, tiprack.load_name).aspirate.submerge.speed == 100 ) with pytest.raises( ValueError, - match="No properties found for p300_multi in fixture_glycerol50 liquid class", + match="No properties found for non-existent-pipette in water liquid class", ): - glycerol_50.get_for(pipette_right.name, tiprack.load_name) + water.get_for("non-existent-pipette", tiprack.load_name) with pytest.raises(AttributeError): - glycerol_50.name = "foo" # type: ignore + water.name = "foo" # type: ignore with pytest.raises(AttributeError): - glycerol_50.display_name = "bar" # type: ignore + water.display_name = "bar" # type: ignore with pytest.raises(ValueError, match="Liquid class definition not found"): - protocol_context.define_liquid_class("non-existent-liquid") + simulated_protocol_context.define_liquid_class("non-existent-liquid") -def test_liquid_class_feature_flag() -> None: +@pytest.mark.parametrize( + "simulated_protocol_context", [("2.20", "OT-2")], indirect=True +) +def test_liquid_class_feature_flag(simulated_protocol_context: ProtocolContext) -> None: """It should raise a not implemented error without the allowLiquidClass flag set.""" - protocol_context = simulate.get_protocol_api(version="2.20", robot_type="OT-2") with pytest.raises(NotImplementedError): - protocol_context.define_liquid_class("fixture_glycerol50") + simulated_protocol_context.define_liquid_class("water") diff --git a/api/tests/opentrons/protocol_api_integration/test_modules.py b/api/tests/opentrons/protocol_api_integration/test_modules.py index e8a26112d88..72ee8ed8c52 100644 --- a/api/tests/opentrons/protocol_api_integration/test_modules.py +++ b/api/tests/opentrons/protocol_api_integration/test_modules.py @@ -3,13 +3,17 @@ import typing import pytest -from opentrons import simulate, protocol_api +from opentrons import protocol_api -def test_absorbance_reader_labware_load_conflict() -> None: +@pytest.mark.parametrize( + "simulated_protocol_context", [("2.21", "Flex")], indirect=True +) +def test_absorbance_reader_labware_load_conflict( + simulated_protocol_context: protocol_api.ProtocolContext, +) -> None: """It should prevent loading a labware onto a closed absorbance reader.""" - protocol = simulate.get_protocol_api(version="2.21", robot_type="Flex") - module = protocol.load_module("absorbanceReaderV1", "A3") + module = simulated_protocol_context.load_module("absorbanceReaderV1", "A3") # The lid should be treated as initially closed. with pytest.raises(Exception): @@ -19,7 +23,7 @@ def test_absorbance_reader_labware_load_conflict() -> None: # Should not raise after opening the lid. labware_1 = module.load_labware("opentrons_96_wellplate_200ul_pcr_full_skirt") - protocol.move_labware(labware_1, protocol_api.OFF_DECK) + simulated_protocol_context.move_labware(labware_1, protocol_api.OFF_DECK) # Should raise after closing the lid again. module.close_lid() # type: ignore[union-attr] @@ -27,34 +31,44 @@ def test_absorbance_reader_labware_load_conflict() -> None: module.load_labware("opentrons_96_wellplate_200ul_pcr_full_skirt") -def test_absorbance_reader_labware_move_conflict() -> None: +@pytest.mark.parametrize( + "simulated_protocol_context", [("2.21", "Flex")], indirect=True +) +def test_absorbance_reader_labware_move_conflict( + simulated_protocol_context: protocol_api.ProtocolContext, +) -> None: """It should prevent moving a labware onto a closed absorbance reader.""" - protocol = simulate.get_protocol_api(version="2.21", robot_type="Flex") - module = protocol.load_module("absorbanceReaderV1", "A3") - labware = protocol.load_labware("opentrons_96_wellplate_200ul_pcr_full_skirt", "A1") + module = simulated_protocol_context.load_module("absorbanceReaderV1", "A3") + labware = simulated_protocol_context.load_labware( + "opentrons_96_wellplate_200ul_pcr_full_skirt", "A1" + ) with pytest.raises(Exception): # The lid should be treated as initially closed. - protocol.move_labware(labware, module, use_gripper=True) + simulated_protocol_context.move_labware(labware, module, use_gripper=True) module.open_lid() # type: ignore[union-attr] # Should not raise after opening the lid. - protocol.move_labware(labware, module, use_gripper=True) + simulated_protocol_context.move_labware(labware, module, use_gripper=True) - protocol.move_labware(labware, "A1", use_gripper=True) + simulated_protocol_context.move_labware(labware, "A1", use_gripper=True) # Should raise after closing the lid again. module.close_lid() # type: ignore[union-attr] with pytest.raises(Exception): - protocol.move_labware(labware, module, use_gripper=True) + simulated_protocol_context.move_labware(labware, module, use_gripper=True) -def test_absorbance_reader_read_preconditions() -> None: +@pytest.mark.parametrize( + "simulated_protocol_context", [("2.21", "Flex")], indirect=True +) +def test_absorbance_reader_read_preconditions( + simulated_protocol_context: protocol_api.ProtocolContext, +) -> None: """Test the preconditions for triggering an absorbance reader read.""" - protocol = simulate.get_protocol_api(version="2.21", robot_type="Flex") module = typing.cast( protocol_api.AbsorbanceReaderContext, - protocol.load_module("absorbanceReaderV1", "A3"), + simulated_protocol_context.load_module("absorbanceReaderV1", "A3"), ) with pytest.raises(Exception, match="initialize"): diff --git a/api/tests/opentrons/protocol_api_integration/test_pipette_movement_deck_conflicts.py b/api/tests/opentrons/protocol_api_integration/test_pipette_movement_deck_conflicts.py index cad2bffddf9..2b7fc11ca91 100644 --- a/api/tests/opentrons/protocol_api_integration/test_pipette_movement_deck_conflicts.py +++ b/api/tests/opentrons/protocol_api_integration/test_pipette_movement_deck_conflicts.py @@ -2,54 +2,59 @@ import pytest -from opentrons import simulate -from opentrons.protocol_api import COLUMN, ALL, SINGLE, ROW +from opentrons.protocol_api import COLUMN, ALL, SINGLE, ROW, ProtocolContext from opentrons.protocol_api.core.engine.pipette_movement_conflict import ( PartialTipMovementNotAllowedError, ) @pytest.mark.ot3_only -def test_deck_conflicts_for_96_ch_a12_column_configuration() -> None: +@pytest.mark.parametrize( + "simulated_protocol_context", [("2.16", "Flex")], indirect=True +) +def test_deck_conflicts_for_96_ch_a12_column_configuration( + simulated_protocol_context: ProtocolContext, +) -> None: """It should raise errors for the expected deck conflicts.""" - protocol_context = simulate.get_protocol_api(version="2.16", robot_type="Flex") - trash_labware = protocol_context.load_labware( + trash_labware = simulated_protocol_context.load_labware( "opentrons_1_trash_3200ml_fixed", "A3" ) - badly_placed_tiprack = protocol_context.load_labware( + badly_placed_tiprack = simulated_protocol_context.load_labware( "opentrons_flex_96_tiprack_50ul", "C2" ) - well_placed_tiprack = protocol_context.load_labware( + well_placed_tiprack = simulated_protocol_context.load_labware( "opentrons_flex_96_tiprack_50ul", "C1" ) - tiprack_on_adapter = protocol_context.load_labware( + tiprack_on_adapter = simulated_protocol_context.load_labware( "opentrons_flex_96_tiprack_50ul", "C3", adapter="opentrons_flex_96_tiprack_adapter", ) - thermocycler = protocol_context.load_module("thermocyclerModuleV2") - tc_adjacent_plate = protocol_context.load_labware( + thermocycler = simulated_protocol_context.load_module("thermocyclerModuleV2") + tc_adjacent_plate = simulated_protocol_context.load_labware( "opentrons_96_wellplate_200ul_pcr_full_skirt", "A2" ) accessible_plate = thermocycler.load_labware( "opentrons_96_wellplate_200ul_pcr_full_skirt" ) - instrument = protocol_context.load_instrument("flex_96channel_1000", mount="left") + instrument = simulated_protocol_context.load_instrument( + "flex_96channel_1000", mount="left" + ) instrument.trash_container = trash_labware # ############ SHORT LABWARE ################ # These labware should be to the west of tall labware to avoid any partial tip deck conflicts - badly_placed_labware = protocol_context.load_labware( + badly_placed_labware = simulated_protocol_context.load_labware( "nest_96_wellplate_200ul_flat", "D2" ) - well_placed_labware = protocol_context.load_labware( + well_placed_labware = simulated_protocol_context.load_labware( "nest_96_wellplate_200ul_flat", "D3" ) # ############ TALL LABWARE ############## - protocol_context.load_labware( + simulated_protocol_context.load_labware( "opentrons_10_tuberack_falcon_4x50ml_6x15ml_conical", "D1" ) @@ -104,24 +109,30 @@ def test_deck_conflicts_for_96_ch_a12_column_configuration() -> None: @pytest.mark.ot3_only -def test_close_shave_deck_conflicts_for_96_ch_a12_column_configuration() -> None: +@pytest.mark.parametrize( + "simulated_protocol_context", [("2.20", "Flex")], indirect=True +) +def test_close_shave_deck_conflicts_for_96_ch_a12_column_configuration( + simulated_protocol_context: ProtocolContext, +) -> None: """Shouldn't raise errors for "almost collision"s.""" - protocol_context = simulate.get_protocol_api(version="2.20", robot_type="Flex") - res12 = protocol_context.load_labware("nest_12_reservoir_15ml", "C3") + res12 = simulated_protocol_context.load_labware("nest_12_reservoir_15ml", "C3") # Mag block and tiprack adapter are very close to the destination reservoir labware - protocol_context.load_module("magneticBlockV1", "D2") - protocol_context.load_labware( + simulated_protocol_context.load_module("magneticBlockV1", "D2") + simulated_protocol_context.load_labware( "opentrons_flex_96_tiprack_200ul", "B3", adapter="opentrons_flex_96_tiprack_adapter", ) - tiprack_8 = protocol_context.load_labware("opentrons_flex_96_tiprack_200ul", "B2") - hs = protocol_context.load_module("heaterShakerModuleV1", "C1") + tiprack_8 = simulated_protocol_context.load_labware( + "opentrons_flex_96_tiprack_200ul", "B2" + ) + hs = simulated_protocol_context.load_module("heaterShakerModuleV1", "C1") hs_adapter = hs.load_adapter("opentrons_96_deep_well_adapter") deepwell = hs_adapter.load_labware("nest_96_wellplate_2ml_deep") - protocol_context.load_trash_bin("A3") - p1000_96 = protocol_context.load_instrument("flex_96channel_1000") + simulated_protocol_context.load_trash_bin("A3") + p1000_96 = simulated_protocol_context.load_instrument("flex_96channel_1000") p1000_96.configure_nozzle_layout(style=SINGLE, start="A12", tip_racks=[tiprack_8]) hs.close_labware_latch() # type: ignore[union-attr] @@ -135,16 +146,28 @@ def test_close_shave_deck_conflicts_for_96_ch_a12_column_configuration() -> None @pytest.mark.ot3_only -def test_deck_conflicts_for_96_ch_a1_column_configuration() -> None: +@pytest.mark.parametrize( + "simulated_protocol_context", [("2.16", "Flex")], indirect=True +) +def test_deck_conflicts_for_96_ch_a1_column_configuration( + simulated_protocol_context: ProtocolContext, +) -> None: """It should raise errors for expected deck conflicts.""" - protocol = simulate.get_protocol_api(version="2.16", robot_type="Flex") - instrument = protocol.load_instrument("flex_96channel_1000", mount="left") - trash_labware = protocol.load_labware("opentrons_1_trash_3200ml_fixed", "A3") + instrument = simulated_protocol_context.load_instrument( + "flex_96channel_1000", mount="left" + ) + trash_labware = simulated_protocol_context.load_labware( + "opentrons_1_trash_3200ml_fixed", "A3" + ) instrument.trash_container = trash_labware - badly_placed_tiprack = protocol.load_labware("opentrons_flex_96_tiprack_50ul", "C2") - well_placed_tiprack = protocol.load_labware("opentrons_flex_96_tiprack_50ul", "A1") - tiprack_on_adapter = protocol.load_labware( + badly_placed_tiprack = simulated_protocol_context.load_labware( + "opentrons_flex_96_tiprack_50ul", "C2" + ) + well_placed_tiprack = simulated_protocol_context.load_labware( + "opentrons_flex_96_tiprack_50ul", "A1" + ) + tiprack_on_adapter = simulated_protocol_context.load_labware( "opentrons_flex_96_tiprack_50ul", "C3", adapter="opentrons_flex_96_tiprack_adapter", @@ -152,11 +175,15 @@ def test_deck_conflicts_for_96_ch_a1_column_configuration() -> None: # ############ SHORT LABWARE ################ # These labware should be to the east of tall labware to avoid any partial tip deck conflicts - badly_placed_plate = protocol.load_labware("nest_96_wellplate_200ul_flat", "B1") - well_placed_plate = protocol.load_labware("nest_96_wellplate_200ul_flat", "B3") + badly_placed_plate = simulated_protocol_context.load_labware( + "nest_96_wellplate_200ul_flat", "B1" + ) + well_placed_plate = simulated_protocol_context.load_labware( + "nest_96_wellplate_200ul_flat", "B3" + ) # ############ TALL LABWARE ############### - my_tuberack = protocol.load_labware( + my_tuberack = simulated_protocol_context.load_labware( "opentrons_10_tuberack_falcon_4x50ml_6x15ml_conical", "B2" ) @@ -208,7 +235,7 @@ def test_deck_conflicts_for_96_ch_a1_column_configuration() -> None: instrument.drop_tip() instrument.trash_container = None # type: ignore - protocol.load_trash_bin("C1") + simulated_protocol_context.load_trash_bin("C1") # This doesn't raise an error because it now treats the trash bin as an addressable area # and the bounds check doesn't yet check moves to addressable areas. @@ -229,28 +256,38 @@ def test_deck_conflicts_for_96_ch_a1_column_configuration() -> None: @pytest.mark.ot3_only -def test_deck_conflicts_for_96_ch_and_reservoirs() -> None: +@pytest.mark.parametrize( + "simulated_protocol_context", [("2.20", "Flex")], indirect=True +) +def test_deck_conflicts_for_96_ch_and_reservoirs( + simulated_protocol_context: ProtocolContext, +) -> None: """It should raise errors for expected deck conflicts when moving to reservoirs. This test checks that the critical point of the pipette is taken into account, specifically when it differs from the primary nozzle. """ - protocol = simulate.get_protocol_api(version="2.20", robot_type="Flex") - instrument = protocol.load_instrument("flex_96channel_1000", mount="left") + instrument = simulated_protocol_context.load_instrument( + "flex_96channel_1000", mount="left" + ) # trash_labware = protocol.load_labware("opentrons_1_trash_3200ml_fixed", "A3") # instrument.trash_container = trash_labware - protocol.load_trash_bin("A3") - right_tiprack = protocol.load_labware("opentrons_flex_96_tiprack_50ul", "C3") - front_tiprack = protocol.load_labware("opentrons_flex_96_tiprack_50ul", "D2") + simulated_protocol_context.load_trash_bin("A3") + right_tiprack = simulated_protocol_context.load_labware( + "opentrons_flex_96_tiprack_50ul", "C3" + ) + front_tiprack = simulated_protocol_context.load_labware( + "opentrons_flex_96_tiprack_50ul", "D2" + ) # Tall deck item in B3 - protocol.load_labware( + simulated_protocol_context.load_labware( "opentrons_flex_96_tiprack_50ul", "B3", adapter="opentrons_flex_96_tiprack_adapter", ) # Tall deck item in B1 - protocol.load_labware( + simulated_protocol_context.load_labware( "opentrons_flex_96_tiprack_50ul", "B1", adapter="opentrons_flex_96_tiprack_adapter", @@ -258,8 +295,12 @@ def test_deck_conflicts_for_96_ch_and_reservoirs() -> None: # ############ RESERVOIRS ################ # These labware should be to the east of tall labware to avoid any partial tip deck conflicts - reservoir_1_well = protocol.load_labware("nest_1_reservoir_195ml", "C2") - reservoir_12_well = protocol.load_labware("nest_12_reservoir_15ml", "B2") + reservoir_1_well = simulated_protocol_context.load_labware( + "nest_1_reservoir_195ml", "C2" + ) + reservoir_12_well = simulated_protocol_context.load_labware( + "nest_12_reservoir_15ml", "B2" + ) # ########### Use COLUMN A1 Config ############# instrument.configure_nozzle_layout(style=COLUMN, start="A1") diff --git a/api/tests/opentrons/protocol_api_integration/test_trashes.py b/api/tests/opentrons/protocol_api_integration/test_trashes.py index 18dfa62170d..1166ba01c70 100644 --- a/api/tests/opentrons/protocol_api_integration/test_trashes.py +++ b/api/tests/opentrons/protocol_api_integration/test_trashes.py @@ -1,46 +1,42 @@ """Tests for the APIs around waste chutes and trash bins.""" -from opentrons import protocol_api, simulate +from opentrons import protocol_api from opentrons.protocols.api_support.types import APIVersion from opentrons.protocols.api_support.util import UnsupportedAPIError import contextlib from typing import ContextManager, Optional, Type -from typing_extensions import Literal import re import pytest @pytest.mark.parametrize( - ("version", "robot_type", "expected_trash_class"), + ("simulated_protocol_context", "expected_trash_class"), [ - ("2.13", "OT-2", protocol_api.Labware), - ("2.14", "OT-2", protocol_api.Labware), - ("2.15", "OT-2", protocol_api.Labware), + (("2.13", "OT-2"), protocol_api.Labware), + (("2.14", "OT-2"), protocol_api.Labware), + (("2.15", "OT-2"), protocol_api.Labware), pytest.param( - "2.15", - "Flex", + ("2.15", "Flex"), protocol_api.Labware, marks=pytest.mark.ot3_only, # Simulating a Flex protocol requires a Flex hardware API. ), pytest.param( - "2.16", - "OT-2", + ("2.16", "OT-2"), protocol_api.TrashBin, ), pytest.param( - "2.16", - "Flex", + ("2.16", "Flex"), None, marks=pytest.mark.ot3_only, # Simulating a Flex protocol requires a Flex hardware API. ), ], + indirect=["simulated_protocol_context"], ) def test_fixed_trash_presence( - robot_type: Literal["OT-2", "Flex"], - version: str, + simulated_protocol_context: protocol_api.ProtocolContext, expected_trash_class: Optional[Type[object]], ) -> None: """Test the presence of the fixed trash. @@ -49,9 +45,10 @@ def test_fixed_trash_presence( For those that do, ProtocolContext.fixed_trash and InstrumentContext.trash_container should point to it. The type of the object depends on the API version. """ - protocol = simulate.get_protocol_api(version=version, robot_type=robot_type) - instrument = protocol.load_instrument( - "p300_single_gen2" if robot_type == "OT-2" else "flex_1channel_50", + instrument = simulated_protocol_context.load_instrument( + "p300_single_gen2" + if simulated_protocol_context._core.robot_type == "OT-2 Standard" + else "flex_1channel_50", mount="left", ) @@ -59,46 +56,53 @@ def test_fixed_trash_presence( with pytest.raises( UnsupportedAPIError, match=re.escape( - "Error 4002 API_REMOVED (UnsupportedAPIError): Fixed Trash is not available after API version 2.16. You are currently using API version 2.16. Fixed trash is no longer supported on Flex protocols." + "Error 4002 API_REMOVED (UnsupportedAPIError): Fixed Trash is not available after API version 2.16." + " You are currently using API version 2.16. Fixed trash is no longer supported on Flex protocols." ), ): - protocol.fixed_trash + simulated_protocol_context.fixed_trash with pytest.raises(Exception, match="No trash container has been defined"): instrument.trash_container else: - assert isinstance(protocol.fixed_trash, expected_trash_class) - assert instrument.trash_container is protocol.fixed_trash + assert isinstance(simulated_protocol_context.fixed_trash, expected_trash_class) + assert instrument.trash_container is simulated_protocol_context.fixed_trash @pytest.mark.ot3_only # Simulating a Flex protocol requires a Flex hardware API. -def test_trash_search() -> None: +@pytest.mark.parametrize( + "simulated_protocol_context", [("2.16", "Flex")], indirect=True +) +def test_trash_search(simulated_protocol_context: protocol_api.ProtocolContext) -> None: """Test the automatic trash search for protocols without a fixed trash.""" - protocol = simulate.get_protocol_api(version="2.16", robot_type="Flex") - instrument = protocol.load_instrument("flex_1channel_50", mount="left") + instrument = simulated_protocol_context.load_instrument( + "flex_1channel_50", mount="left" + ) # By default, there should be no trash. with pytest.raises( UnsupportedAPIError, match=re.escape( - "Error 4002 API_REMOVED (UnsupportedAPIError): Fixed Trash is not available after API version 2.16. You are currently using API version 2.16. Fixed trash is no longer supported on Flex protocols." + "Error 4002 API_REMOVED (UnsupportedAPIError): Fixed Trash is not available after API version 2.16." + " You are currently using API version 2.16. Fixed trash is no longer supported on Flex protocols." ), ): - protocol.fixed_trash + simulated_protocol_context.fixed_trash with pytest.raises(Exception, match="No trash container has been defined"): instrument.trash_container - loaded_first = protocol.load_trash_bin("A1") - loaded_second = protocol.load_trash_bin("B1") + loaded_first = simulated_protocol_context.load_trash_bin("A1") + loaded_second = simulated_protocol_context.load_trash_bin("B1") # After loading some trashes, there should still be no protocol.fixed_trash... with pytest.raises( UnsupportedAPIError, match=re.escape( - "Error 4002 API_REMOVED (UnsupportedAPIError): Fixed Trash is not available after API version 2.16. You are currently using API version 2.16. Fixed trash is no longer supported on Flex protocols." + "Error 4002 API_REMOVED (UnsupportedAPIError): Fixed Trash is not available after API version 2.16." + " You are currently using API version 2.16. Fixed trash is no longer supported on Flex protocols." ), ): - protocol.fixed_trash + simulated_protocol_context.fixed_trash # ...but instrument.trash_container should automatically update to point to # the first trash that we loaded. assert instrument.trash_container is loaded_first @@ -109,40 +113,36 @@ def test_trash_search() -> None: @pytest.mark.parametrize( - ("version", "robot_type", "expect_load_to_succeed"), + ("simulated_protocol_context", "expect_load_to_succeed"), [ pytest.param( - "2.13", - "OT-2", + ("2.13", "OT-2"), False, # This xfail (the system does let you load a labware onto slot 12, and does not raise) # is surprising to me. It may be be a bug in old PAPI versions. marks=pytest.mark.xfail(strict=True, raises=pytest.fail.Exception), ), - ("2.14", "OT-2", False), - ("2.15", "OT-2", False), + (("2.14", "OT-2"), False), + (("2.15", "OT-2"), False), pytest.param( - "2.15", - "Flex", + ("2.15", "Flex"), False, marks=pytest.mark.ot3_only, # Simulating a Flex protocol requires a Flex hardware API. ), pytest.param( - "2.16", - "OT-2", + ("2.16", "OT-2"), False, ), pytest.param( - "2.16", - "Flex", + ("2.16", "Flex"), True, marks=pytest.mark.ot3_only, # Simulating a Flex protocol requires a Flex hardware API. ), ], + indirect=["simulated_protocol_context"], ) def test_fixed_trash_load_conflicts( - robot_type: Literal["Flex", "OT-2"], - version: str, + simulated_protocol_context: protocol_api.ProtocolContext, expect_load_to_succeed: bool, ) -> None: """Test loading something onto the location historically used for the fixed trash. @@ -150,14 +150,12 @@ def test_fixed_trash_load_conflicts( In configurations where there is a fixed trash, this should be disallowed. In configurations without a fixed trash, this should be allowed. """ - protocol = simulate.get_protocol_api(version=version, robot_type=robot_type) - if expect_load_to_succeed: expected_error: ContextManager[object] = contextlib.nullcontext() else: # If we're expecting an error, it'll be a LocationIsOccupied for 2.15 and below, otherwise # it will fail with an IncompatibleAddressableAreaError, since slot 12 will not be in the deck config - if APIVersion.from_string(version) < APIVersion(2, 16): + if simulated_protocol_context.api_version < APIVersion(2, 16): error_name = "LocationIsOccupiedError" else: error_name = "IncompatibleAddressableAreaError" @@ -169,4 +167,6 @@ def test_fixed_trash_load_conflicts( ) with expected_error: - protocol.load_labware("opentrons_96_wellplate_200ul_pcr_full_skirt", 12) + simulated_protocol_context.load_labware( + "opentrons_96_wellplate_200ul_pcr_full_skirt", 12 + ) diff --git a/api/tests/opentrons/protocol_api_old/test_context.py b/api/tests/opentrons/protocol_api_old/test_context.py index 098fbf9634b..2ceab400ab4 100644 --- a/api/tests/opentrons/protocol_api_old/test_context.py +++ b/api/tests/opentrons/protocol_api_old/test_context.py @@ -31,7 +31,8 @@ from opentrons.hardware_control.types import Axis, CriticalPoint from opentrons.protocol_api.core.legacy.deck import Deck from opentrons.hardware_control.modules import SimulatingModule -from opentrons.protocols.advanced_control import transfers as tf +from opentrons.protocols.advanced_control.common import MixStrategy, Mix, MixOpts +from opentrons.protocols.advanced_control.transfers import transfer as tf from opentrons.protocols.api_support.types import APIVersion @@ -852,15 +853,15 @@ def fake_execute_transfer(xfer_plan: tf.TransferPlan) -> None: carryover=True, gradient_function=None, disposal_volume=0, - mix_strategy=tf.MixStrategy.BOTH, + mix_strategy=MixStrategy.BOTH, drop_tip_strategy=tf.DropTipStrategy.TRASH, blow_out_strategy=tf.BlowOutStrategy.TRASH, touch_tip_strategy=tf.TouchTipStrategy.NEVER, ), pick_up_tip=tf.PickUpTipOpts(), - mix=tf.Mix( - mix_before=tf.MixOpts(repetitions=2, volume=10, rate=None), - mix_after=tf.MixOpts(repetitions=3, volume=20, rate=None), + mix=Mix( + mix_before=MixOpts(repetitions=2, volume=10, rate=None), + mix_after=MixOpts(repetitions=3, volume=20, rate=None), ), blow_out=tf.BlowOutOpts(), touch_tip=tf.TouchTipOpts(), @@ -889,15 +890,15 @@ def fake_execute_transfer(xfer_plan: tf.TransferPlan) -> None: carryover=True, gradient_function=None, disposal_volume=10, - mix_strategy=tf.MixStrategy.BEFORE, + mix_strategy=MixStrategy.BEFORE, drop_tip_strategy=tf.DropTipStrategy.RETURN, blow_out_strategy=tf.BlowOutStrategy.NONE, touch_tip_strategy=tf.TouchTipStrategy.ALWAYS, ), pick_up_tip=tf.PickUpTipOpts(), - mix=tf.Mix( - mix_before=tf.MixOpts(repetitions=2, volume=30, rate=None), - mix_after=tf.MixOpts(), + mix=Mix( + mix_before=MixOpts(repetitions=2, volume=30, rate=None), + mix_after=MixOpts(), ), blow_out=tf.BlowOutOpts(), touch_tip=tf.TouchTipOpts(), diff --git a/api/tests/opentrons/protocol_api_old/test_instrument.py b/api/tests/opentrons/protocol_api_old/test_instrument.py index 5f274f513b4..fbb98cdce24 100644 --- a/api/tests/opentrons/protocol_api_old/test_instrument.py +++ b/api/tests/opentrons/protocol_api_old/test_instrument.py @@ -4,7 +4,7 @@ from typing import Any, Callable, Dict from opentrons.types import Mount -from opentrons.protocols.advanced_control import transfers +from opentrons.protocols.advanced_control.transfers import transfer as v1_transfer from opentrons.protocols.api_support.types import APIVersion from opentrons.hardware_control import ThreadManagedHardware @@ -89,13 +89,13 @@ def test_blowout_location_invalid( @pytest.mark.parametrize( argnames="liquid_handling_command," "blowout_location," "expected_strat,", argvalues=[ - ["transfer", "destination well", transfers.BlowOutStrategy.DEST], - ["transfer", "source well", transfers.BlowOutStrategy.SOURCE], - ["transfer", "trash", transfers.BlowOutStrategy.TRASH], - ["consolidate", "destination well", transfers.BlowOutStrategy.DEST], - ["consolidate", "trash", transfers.BlowOutStrategy.TRASH], - ["distribute", "source well", transfers.BlowOutStrategy.SOURCE], - ["distribute", "trash", transfers.BlowOutStrategy.TRASH], + ["transfer", "destination well", v1_transfer.BlowOutStrategy.DEST], + ["transfer", "source well", v1_transfer.BlowOutStrategy.SOURCE], + ["transfer", "trash", v1_transfer.BlowOutStrategy.TRASH], + ["consolidate", "destination well", v1_transfer.BlowOutStrategy.DEST], + ["consolidate", "trash", v1_transfer.BlowOutStrategy.TRASH], + ["distribute", "source well", v1_transfer.BlowOutStrategy.SOURCE], + ["distribute", "trash", v1_transfer.BlowOutStrategy.TRASH], ], ) def test_valid_blowout_location( diff --git a/api/tests/opentrons/protocol_engine/clients/test_child_thread_transport.py b/api/tests/opentrons/protocol_engine/clients/test_child_thread_transport.py index 9cbd03c3ec8..700f11ff190 100644 --- a/api/tests/opentrons/protocol_engine/clients/test_child_thread_transport.py +++ b/api/tests/opentrons/protocol_engine/clients/test_child_thread_transport.py @@ -1,4 +1,5 @@ """Tests for am ChildThreadTransport.""" + import threading from asyncio import get_running_loop from datetime import datetime @@ -104,7 +105,7 @@ async def test_call_method( subject: ChildThreadTransport, ) -> None: """It should call a synchronous method in a thread-safe manner.""" - labware_def = LabwareDefinition.construct(namespace="hello") # type: ignore[call-arg] + labware_def = LabwareDefinition.model_construct(namespace="hello") # type: ignore[call-arg] labware_uri = LabwareUri("hello/world/123") calling_thread_id = None diff --git a/api/tests/opentrons/protocol_engine/clients/test_sync_client.py b/api/tests/opentrons/protocol_engine/clients/test_sync_client.py index 03d6912371c..628e23cc052 100644 --- a/api/tests/opentrons/protocol_engine/clients/test_sync_client.py +++ b/api/tests/opentrons/protocol_engine/clients/test_sync_client.py @@ -12,6 +12,7 @@ import pytest from decoy import Decoy + from opentrons_shared_data.labware.types import LabwareUri from opentrons_shared_data.labware.labware_definition import LabwareDefinition @@ -61,7 +62,7 @@ def test_execute_command_without_recovery( result_from_transport ) result_from_subject = subject.execute_command_without_recovery(params) - assert result_from_subject == result_from_transport + assert result_from_subject == result_from_transport # type: ignore[comparison-overlap] def test_add_labware_definition( @@ -70,7 +71,7 @@ def test_add_labware_definition( subject: SyncClient, ) -> None: """It should add a labware definition.""" - labware_definition = LabwareDefinition.construct(namespace="hello") # type: ignore[call-arg] + labware_definition = LabwareDefinition.model_construct(namespace="hello") # type: ignore[call-arg] expected_labware_uri = LabwareUri("hello/world/123") decoy.when( @@ -108,7 +109,7 @@ def test_add_liquid( subject: SyncClient, ) -> None: """It should add a liquid to engine state.""" - liquid = Liquid.construct(displayName="water") # type: ignore[call-arg] + liquid = Liquid.model_construct(displayName="water") # type: ignore[call-arg] decoy.when( transport.call_method( diff --git a/api/tests/opentrons/protocol_engine/commands/absorbance_reader/__init__.py b/api/tests/opentrons/protocol_engine/commands/absorbance_reader/__init__.py new file mode 100644 index 00000000000..ebb91ede166 --- /dev/null +++ b/api/tests/opentrons/protocol_engine/commands/absorbance_reader/__init__.py @@ -0,0 +1 @@ +"""Tests for absorbance reader commands.""" diff --git a/api/tests/opentrons/protocol_engine/commands/absorbance_reader/test_close_lid.py b/api/tests/opentrons/protocol_engine/commands/absorbance_reader/test_close_lid.py new file mode 100644 index 00000000000..c40a825d9c6 --- /dev/null +++ b/api/tests/opentrons/protocol_engine/commands/absorbance_reader/test_close_lid.py @@ -0,0 +1,221 @@ +"""Test absorbance reader initilize command.""" +import pytest +from decoy import Decoy + +from opentrons.drivers.types import ( + AbsorbanceReaderLidStatus, +) +from opentrons.hardware_control.modules import AbsorbanceReader +from opentrons.protocol_engine import ModuleModel, DeckSlotLocation +from opentrons.protocol_engine.errors import CannotPerformModuleAction + +from opentrons.protocol_engine.execution import EquipmentHandler, LabwareMovementHandler +from opentrons.protocol_engine.state import update_types +from opentrons.protocol_engine.state.state import StateView +from opentrons.protocol_engine.state.module_substates import ( + AbsorbanceReaderSubState, + AbsorbanceReaderId, +) +from opentrons.protocol_engine.commands.command import SuccessData +from opentrons.protocol_engine.commands.absorbance_reader import ( + CloseLidResult, + CloseLidParams, +) +from opentrons.protocol_engine.commands.absorbance_reader.close_lid import ( + CloseLidImpl, +) +from opentrons.protocol_engine.types import ( + LabwareMovementOffsetData, + LabwareOffsetVector, +) +from opentrons.types import DeckSlotName +from opentrons_shared_data.labware.labware_definition import ( + LabwareDefinition, + Parameters, +) + + +@pytest.fixture +def absorbance_def() -> LabwareDefinition: + """Get a tip rack Pydantic model definition value object.""" + return LabwareDefinition.model_construct( # type: ignore[call-arg] + namespace="test", + version=1, + parameters=Parameters.model_construct( # type: ignore[call-arg] + loadName="cool-labware", + tipOverlap=None, # add a None value to validate serialization to dictionary + ), + ) + + +@pytest.fixture +def subject( + state_view: StateView, + equipment: EquipmentHandler, + labware_movement: LabwareMovementHandler, +) -> CloseLidImpl: + """Subject fixture.""" + return CloseLidImpl( + state_view=state_view, equipment=equipment, labware_movement=labware_movement + ) + + +@pytest.mark.parametrize( + "hardware_lid_status", + (AbsorbanceReaderLidStatus.ON, AbsorbanceReaderLidStatus.OFF), +) +async def test_absorbance_reader_close_lid_implementation( + decoy: Decoy, + subject: CloseLidImpl, + state_view: StateView, + equipment: EquipmentHandler, + hardware_lid_status: AbsorbanceReaderLidStatus, + absorbance_def: LabwareDefinition, +) -> None: + """It should validate, find hardware module if not virtualized, and close lid.""" + params = CloseLidParams( + moduleId="unverified-module-id", + ) + + mabsorbance_module_substate = decoy.mock(cls=AbsorbanceReaderSubState) + absorbance_module_hw = decoy.mock(cls=AbsorbanceReader) + verified_module_id = AbsorbanceReaderId("module-id") + + decoy.when( + state_view.modules.get_absorbance_reader_substate("unverified-module-id") + ).then_return(mabsorbance_module_substate) + + decoy.when(mabsorbance_module_substate.module_id).then_return(verified_module_id) + + decoy.when(equipment.get_module_hardware_api(verified_module_id)).then_return( + absorbance_module_hw + ) + + decoy.when(await absorbance_module_hw.get_current_lid_status()).then_return( + hardware_lid_status + ) + decoy.when(state_view.modules.get_requested_model(params.moduleId)).then_return( + ModuleModel.ABSORBANCE_READER_V1 + ) + + decoy.when(state_view.labware.get_absorbance_reader_lid_definition()).then_return() + + decoy.when(state_view.modules.get_location(params.moduleId)).then_return( + DeckSlotLocation(slotName=DeckSlotName.SLOT_D3) + ) + decoy.when( + state_view.modules.ensure_and_convert_module_fixture_location( + DeckSlotName.SLOT_D3, ModuleModel.ABSORBANCE_READER_V1 + ) + ).then_return("absorbanceReaderV1D3") + + decoy.when(state_view.labware.get_absorbance_reader_lid_definition()).then_return( + absorbance_def + ) + decoy.when( + state_view.labware.get_child_gripper_offsets( + labware_definition=absorbance_def, + slot_name=None, + ) + ).then_return( + LabwareMovementOffsetData( + pickUpOffset=LabwareOffsetVector(x=0, y=0, z=0), + dropOffset=LabwareOffsetVector(x=0, y=0, z=0), + ) + ) + + result = await subject.execute(params=params) + + assert result == SuccessData( + public=CloseLidResult(), + state_update=update_types.StateUpdate( + absorbance_reader_state_update=update_types.AbsorbanceReaderStateUpdate( + module_id="module-id", + absorbance_reader_lid=update_types.AbsorbanceReaderLidUpdate( + is_lid_on=True + ), + ), + ), + ) + + +async def test_close_lid_raises_no_module( + decoy: Decoy, + state_view: StateView, + equipment: EquipmentHandler, + subject: CloseLidImpl, +) -> None: + """Should raise an error that the hardware module not found.""" + params = CloseLidParams( + moduleId="unverified-module-id", + ) + + mabsorbance_module_substate = decoy.mock(cls=AbsorbanceReaderSubState) + verified_module_id = AbsorbanceReaderId("module-id") + + decoy.when( + state_view.modules.get_absorbance_reader_substate("unverified-module-id") + ).then_return(mabsorbance_module_substate) + + decoy.when(mabsorbance_module_substate.module_id).then_return(verified_module_id) + decoy.when(state_view.config.use_virtual_modules).then_return(False) + decoy.when(equipment.get_module_hardware_api(verified_module_id)).then_return(None) + with pytest.raises(CannotPerformModuleAction): + await subject.execute(params=params) + + +async def test_close_lid_raises_no_gripper_offset( + decoy: Decoy, + state_view: StateView, + equipment: EquipmentHandler, + subject: CloseLidImpl, + absorbance_def: LabwareDefinition, +) -> None: + """Should raise an error that gripper offset not found.""" + params = CloseLidParams( + moduleId="unverified-module-id", + ) + + mabsorbance_module_substate = decoy.mock(cls=AbsorbanceReaderSubState) + absorbance_module_hw = decoy.mock(cls=AbsorbanceReader) + verified_module_id = AbsorbanceReaderId("module-id") + + decoy.when( + state_view.modules.get_absorbance_reader_substate("unverified-module-id") + ).then_return(mabsorbance_module_substate) + + decoy.when(mabsorbance_module_substate.module_id).then_return(verified_module_id) + + decoy.when(equipment.get_module_hardware_api(verified_module_id)).then_return( + absorbance_module_hw + ) + + decoy.when(await absorbance_module_hw.get_current_lid_status()).then_return( + AbsorbanceReaderLidStatus.OFF + ) + decoy.when(state_view.modules.get_requested_model(params.moduleId)).then_return( + ModuleModel.ABSORBANCE_READER_V1 + ) + + decoy.when(state_view.labware.get_absorbance_reader_lid_definition()).then_return() + + decoy.when(state_view.modules.get_location(params.moduleId)).then_return( + DeckSlotLocation(slotName=DeckSlotName.SLOT_D3) + ) + decoy.when( + state_view.modules.ensure_and_convert_module_fixture_location( + DeckSlotName.SLOT_D3, ModuleModel.ABSORBANCE_READER_V1 + ) + ).then_return("absorbanceReaderV1D3") + + decoy.when(state_view.labware.get_absorbance_reader_lid_definition()).then_return( + absorbance_def + ) + decoy.when( + state_view.labware.get_child_gripper_offsets( + labware_definition=absorbance_def, + slot_name=None, + ) + ).then_return(None) + with pytest.raises(ValueError): + await subject.execute(params=params) diff --git a/api/tests/opentrons/protocol_engine/commands/absorbance_reader/test_initialize.py b/api/tests/opentrons/protocol_engine/commands/absorbance_reader/test_initialize.py new file mode 100644 index 00000000000..efdd8908583 --- /dev/null +++ b/api/tests/opentrons/protocol_engine/commands/absorbance_reader/test_initialize.py @@ -0,0 +1,238 @@ +"""Test absorbance reader initilize command.""" +import pytest +from decoy import Decoy +from typing import List + +from opentrons.drivers.types import ABSMeasurementMode +from opentrons.hardware_control.modules import AbsorbanceReader +from opentrons.protocol_engine.errors import InvalidWavelengthError + +from opentrons.protocol_engine.execution import EquipmentHandler +from opentrons.protocol_engine.state import update_types +from opentrons.protocol_engine.state.state import StateView +from opentrons.protocol_engine.state.module_substates import ( + AbsorbanceReaderSubState, + AbsorbanceReaderId, +) +from opentrons.protocol_engine.commands.command import SuccessData +from opentrons.protocol_engine.commands.absorbance_reader import ( + InitializeParams, + InitializeResult, +) +from opentrons.protocol_engine.commands.absorbance_reader.initialize import ( + InitializeImpl, +) + + +@pytest.fixture +def subject( + state_view: StateView, + equipment: EquipmentHandler, +) -> InitializeImpl: + """Subject command implementation to test.""" + return InitializeImpl(state_view=state_view, equipment=equipment) + + +@pytest.mark.parametrize( + "input_sample_wave_length, input_measure_mode", [([1, 2], "multi"), ([1], "single")] +) +async def test_absorbance_reader_implementation( + decoy: Decoy, + input_sample_wave_length: List[int], + input_measure_mode: str, + subject: InitializeImpl, + state_view: StateView, + equipment: EquipmentHandler, +) -> None: + """It should validate, find hardware module if not virtualized, and disengage.""" + params = InitializeParams( + moduleId="unverified-module-id", + measureMode=input_measure_mode, # type: ignore[arg-type] + sampleWavelengths=input_sample_wave_length, + ) + + mabsorbance_module_substate = decoy.mock(cls=AbsorbanceReaderSubState) + absorbance_module_hw = decoy.mock(cls=AbsorbanceReader) + verified_module_id = AbsorbanceReaderId("module-id") + + decoy.when( + state_view.modules.get_absorbance_reader_substate("unverified-module-id") + ).then_return(mabsorbance_module_substate) + + decoy.when(mabsorbance_module_substate.module_id).then_return(verified_module_id) + + decoy.when(equipment.get_module_hardware_api(verified_module_id)).then_return( + absorbance_module_hw + ) + + decoy.when((absorbance_module_hw.supported_wavelengths)).then_return([1, 2]) + + result = await subject.execute(params=params) + + decoy.verify( + await absorbance_module_hw.set_sample_wavelength( + ABSMeasurementMode(params.measureMode), + params.sampleWavelengths, + reference_wavelength=params.referenceWavelength, + ), + times=1, + ) + assert result == SuccessData( + public=InitializeResult(), + state_update=update_types.StateUpdate( + absorbance_reader_state_update=update_types.AbsorbanceReaderStateUpdate( + module_id="module-id", + initialize_absorbance_reader_update=update_types.AbsorbanceReaderInitializeUpdate( + measure_mode=input_measure_mode, # type: ignore[arg-type] + sample_wave_lengths=input_sample_wave_length, + reference_wave_length=None, + ), + ) + ), + ) + + +@pytest.mark.parametrize( + "input_sample_wave_length, input_measure_mode", + [([1, 2, 3], "multi"), ([3], "single")], +) +async def test_initialize_raises_invalid_wave_length( + decoy: Decoy, + state_view: StateView, + equipment: EquipmentHandler, + subject: InitializeImpl, + input_sample_wave_length: List[int], + input_measure_mode: str, +) -> None: + """Should raise an InvalidWavelengthError error.""" + params = InitializeParams( + moduleId="unverified-module-id", + measureMode=input_measure_mode, # type: ignore[arg-type] + sampleWavelengths=input_sample_wave_length, + ) + + mabsorbance_module_substate = decoy.mock(cls=AbsorbanceReaderSubState) + verified_module_id = AbsorbanceReaderId("module-id") + absorbance_module_hw = decoy.mock(cls=AbsorbanceReader) + + decoy.when( + state_view.modules.get_absorbance_reader_substate("unverified-module-id") + ).then_return(mabsorbance_module_substate) + + decoy.when(mabsorbance_module_substate.module_id).then_return(verified_module_id) + + decoy.when(equipment.get_module_hardware_api(verified_module_id)).then_return( + absorbance_module_hw + ) + + decoy.when((absorbance_module_hw.supported_wavelengths)).then_return([1, 2]) + + with pytest.raises(InvalidWavelengthError): + await subject.execute(params=params) + + +@pytest.mark.parametrize( + "input_sample_wave_length, input_measure_mode", + [([], "multi"), ([], "single")], +) +async def test_initialize_raises_measure_mode_not_matching( + decoy: Decoy, + state_view: StateView, + equipment: EquipmentHandler, + subject: InitializeImpl, + input_sample_wave_length: List[int], + input_measure_mode: str, +) -> None: + """Should raise an error that the measure mode does not match sample wave.""" + params = InitializeParams( + moduleId="unverified-module-id", + measureMode=input_measure_mode, # type: ignore[arg-type] + sampleWavelengths=input_sample_wave_length, + ) + + mabsorbance_module_substate = decoy.mock(cls=AbsorbanceReaderSubState) + verified_module_id = AbsorbanceReaderId("module-id") + absorbance_module_hw = decoy.mock(cls=AbsorbanceReader) + + decoy.when( + state_view.modules.get_absorbance_reader_substate("unverified-module-id") + ).then_return(mabsorbance_module_substate) + + decoy.when(mabsorbance_module_substate.module_id).then_return(verified_module_id) + + decoy.when(equipment.get_module_hardware_api(verified_module_id)).then_return( + absorbance_module_hw + ) + + decoy.when((absorbance_module_hw.supported_wavelengths)).then_return([1, 2]) + + with pytest.raises(ValueError): + await subject.execute(params=params) + + +async def test_initialize_single_raises_reference_wave_length_not_matching( + decoy: Decoy, + state_view: StateView, + equipment: EquipmentHandler, + subject: InitializeImpl, +) -> None: + """Should raise an error that the measure mode does not match sample wave.""" + params = InitializeParams( + moduleId="unverified-module-id", + measureMode="single", + sampleWavelengths=[1], + referenceWavelength=3, + ) + + mabsorbance_module_substate = decoy.mock(cls=AbsorbanceReaderSubState) + verified_module_id = AbsorbanceReaderId("module-id") + absorbance_module_hw = decoy.mock(cls=AbsorbanceReader) + + decoy.when( + state_view.modules.get_absorbance_reader_substate("unverified-module-id") + ).then_return(mabsorbance_module_substate) + + decoy.when(mabsorbance_module_substate.module_id).then_return(verified_module_id) + + decoy.when(equipment.get_module_hardware_api(verified_module_id)).then_return( + absorbance_module_hw + ) + + decoy.when((absorbance_module_hw.supported_wavelengths)).then_return([1, 2]) + + with pytest.raises(InvalidWavelengthError): + await subject.execute(params=params) + + +async def test_initialize_multi_raises_no_reference_wave_length( + decoy: Decoy, + state_view: StateView, + equipment: EquipmentHandler, + subject: InitializeImpl, +) -> None: + """Should raise an error that the measure mode does not match sample wave.""" + params = InitializeParams( + moduleId="unverified-module-id", + measureMode="multi", + sampleWavelengths=[1, 2], + referenceWavelength=3, + ) + + mabsorbance_module_substate = decoy.mock(cls=AbsorbanceReaderSubState) + verified_module_id = AbsorbanceReaderId("module-id") + absorbance_module_hw = decoy.mock(cls=AbsorbanceReader) + + decoy.when( + state_view.modules.get_absorbance_reader_substate("unverified-module-id") + ).then_return(mabsorbance_module_substate) + + decoy.when(mabsorbance_module_substate.module_id).then_return(verified_module_id) + + decoy.when(equipment.get_module_hardware_api(verified_module_id)).then_return( + absorbance_module_hw + ) + + decoy.when((absorbance_module_hw.supported_wavelengths)).then_return([1, 2]) + + with pytest.raises(ValueError): + await subject.execute(params=params) diff --git a/api/tests/opentrons/protocol_engine/commands/absorbance_reader/test_open_lid.py b/api/tests/opentrons/protocol_engine/commands/absorbance_reader/test_open_lid.py new file mode 100644 index 00000000000..bc555a9bb18 --- /dev/null +++ b/api/tests/opentrons/protocol_engine/commands/absorbance_reader/test_open_lid.py @@ -0,0 +1,222 @@ +"""Test absorbance reader initilize command.""" +import pytest +from decoy import Decoy + +from opentrons.drivers.types import ( + AbsorbanceReaderLidStatus, +) +from opentrons.hardware_control.modules import AbsorbanceReader +from opentrons.protocol_engine import ModuleModel, DeckSlotLocation +from opentrons.protocol_engine.errors import CannotPerformModuleAction + +from opentrons.protocol_engine.execution import EquipmentHandler, LabwareMovementHandler +from opentrons.protocol_engine.state import update_types +from opentrons.protocol_engine.state.state import StateView +from opentrons.protocol_engine.state.module_substates import ( + AbsorbanceReaderSubState, + AbsorbanceReaderId, +) +from opentrons.protocol_engine.commands.command import SuccessData +from opentrons.protocol_engine.commands.absorbance_reader import ( + OpenLidResult, + OpenLidParams, +) +from opentrons.protocol_engine.commands.absorbance_reader.open_lid import ( + OpenLidImpl, +) +from opentrons.protocol_engine.types import ( + LabwareMovementOffsetData, + LabwareOffsetVector, +) +from opentrons.types import DeckSlotName +from opentrons_shared_data.labware.labware_definition import ( + LabwareDefinition, + Parameters, +) + + +@pytest.fixture +def absorbance_def() -> LabwareDefinition: + """Get a tip rack Pydantic model definition value object.""" + return LabwareDefinition.model_construct( # type: ignore[call-arg] + namespace="test", + version=1, + parameters=Parameters.model_construct( # type: ignore[call-arg] + loadName="cool-labware", + tipOverlap=None, # add a None value to validate serialization to dictionary + ), + ) + + +@pytest.fixture +def subject( + state_view: StateView, + equipment: EquipmentHandler, + labware_movement: LabwareMovementHandler, +) -> OpenLidImpl: + """Command implementation subject for testing.""" + return OpenLidImpl( + state_view=state_view, equipment=equipment, labware_movement=labware_movement + ) + + +@pytest.mark.parametrize( + "hardware_lid_status", + (AbsorbanceReaderLidStatus.ON, AbsorbanceReaderLidStatus.OFF), +) +async def test_absorbance_reader_implementation( + decoy: Decoy, + state_view: StateView, + equipment: EquipmentHandler, + labware_movement: LabwareMovementHandler, + hardware_lid_status: AbsorbanceReaderLidStatus, + absorbance_def: LabwareDefinition, + subject: OpenLidImpl, +) -> None: + """It should validate, find hardware module if not virtualized, and disengage.""" + params = OpenLidParams( + moduleId="unverified-module-id", + ) + + mabsorbance_module_substate = decoy.mock(cls=AbsorbanceReaderSubState) + absorbance_module_hw = decoy.mock(cls=AbsorbanceReader) + verified_module_id = AbsorbanceReaderId("module-id") + + decoy.when( + state_view.modules.get_absorbance_reader_substate("unverified-module-id") + ).then_return(mabsorbance_module_substate) + + decoy.when(mabsorbance_module_substate.module_id).then_return(verified_module_id) + + decoy.when(equipment.get_module_hardware_api(verified_module_id)).then_return( + absorbance_module_hw + ) + + decoy.when(await absorbance_module_hw.get_current_lid_status()).then_return( + hardware_lid_status + ) + decoy.when(state_view.modules.get_requested_model(params.moduleId)).then_return( + ModuleModel.ABSORBANCE_READER_V1 + ) + + decoy.when(state_view.labware.get_absorbance_reader_lid_definition()).then_return() + + decoy.when(state_view.modules.get_location(params.moduleId)).then_return( + DeckSlotLocation(slotName=DeckSlotName.SLOT_D3) + ) + decoy.when( + state_view.modules.ensure_and_convert_module_fixture_location( + DeckSlotName.SLOT_D3, ModuleModel.ABSORBANCE_READER_V1 + ) + ).then_return("absorbanceReaderV1D3") + + decoy.when(state_view.labware.get_absorbance_reader_lid_definition()).then_return( + absorbance_def + ) + decoy.when( + state_view.labware.get_child_gripper_offsets( + labware_definition=absorbance_def, + slot_name=None, + ) + ).then_return( + LabwareMovementOffsetData( + pickUpOffset=LabwareOffsetVector(x=0, y=0, z=0), + dropOffset=LabwareOffsetVector(x=0, y=0, z=0), + ) + ) + + result = await subject.execute(params=params) + + assert result == SuccessData( + public=OpenLidResult(), + state_update=update_types.StateUpdate( + absorbance_reader_state_update=update_types.AbsorbanceReaderStateUpdate( + module_id="module-id", + absorbance_reader_lid=update_types.AbsorbanceReaderLidUpdate( + is_lid_on=False + ), + ), + ), + ) + + +async def test_open_lid_raises_no_module( + decoy: Decoy, + state_view: StateView, + equipment: EquipmentHandler, + subject: OpenLidImpl, +) -> None: + """Should raise an error that the hardware module not found.""" + params = OpenLidParams( + moduleId="unverified-module-id", + ) + + mabsorbance_module_substate = decoy.mock(cls=AbsorbanceReaderSubState) + verified_module_id = AbsorbanceReaderId("module-id") + + decoy.when( + state_view.modules.get_absorbance_reader_substate("unverified-module-id") + ).then_return(mabsorbance_module_substate) + + decoy.when(mabsorbance_module_substate.module_id).then_return(verified_module_id) + decoy.when(state_view.config.use_virtual_modules).then_return(False) + decoy.when(equipment.get_module_hardware_api(verified_module_id)).then_return(None) + with pytest.raises(CannotPerformModuleAction): + await subject.execute(params=params) + + +async def test_open_lid_raises_no_gripper_offset( + decoy: Decoy, + state_view: StateView, + equipment: EquipmentHandler, + subject: OpenLidImpl, + absorbance_def: LabwareDefinition, +) -> None: + """Should raise an error that gripper offset not found.""" + params = OpenLidParams( + moduleId="unverified-module-id", + ) + + mabsorbance_module_substate = decoy.mock(cls=AbsorbanceReaderSubState) + absorbance_module_hw = decoy.mock(cls=AbsorbanceReader) + verified_module_id = AbsorbanceReaderId("module-id") + + decoy.when( + state_view.modules.get_absorbance_reader_substate("unverified-module-id") + ).then_return(mabsorbance_module_substate) + + decoy.when(mabsorbance_module_substate.module_id).then_return(verified_module_id) + + decoy.when(equipment.get_module_hardware_api(verified_module_id)).then_return( + absorbance_module_hw + ) + + decoy.when(await absorbance_module_hw.get_current_lid_status()).then_return( + AbsorbanceReaderLidStatus.OFF + ) + decoy.when(state_view.modules.get_requested_model(params.moduleId)).then_return( + ModuleModel.ABSORBANCE_READER_V1 + ) + + decoy.when(state_view.labware.get_absorbance_reader_lid_definition()).then_return() + + decoy.when(state_view.modules.get_location(params.moduleId)).then_return( + DeckSlotLocation(slotName=DeckSlotName.SLOT_D3) + ) + decoy.when( + state_view.modules.ensure_and_convert_module_fixture_location( + DeckSlotName.SLOT_D3, ModuleModel.ABSORBANCE_READER_V1 + ) + ).then_return("absorbanceReaderV1D3") + + decoy.when(state_view.labware.get_absorbance_reader_lid_definition()).then_return( + absorbance_def + ) + decoy.when( + state_view.labware.get_child_gripper_offsets( + labware_definition=absorbance_def, + slot_name=None, + ) + ).then_return(None) + with pytest.raises(ValueError): + await subject.execute(params=params) diff --git a/api/tests/opentrons/protocol_engine/commands/absorbance_reader/test_read.py b/api/tests/opentrons/protocol_engine/commands/absorbance_reader/test_read.py new file mode 100644 index 00000000000..6ba7619f219 --- /dev/null +++ b/api/tests/opentrons/protocol_engine/commands/absorbance_reader/test_read.py @@ -0,0 +1,176 @@ +"""Test absorbance reader initilize command.""" +import pytest +from decoy import Decoy + +from opentrons.drivers.types import ABSMeasurementMode, ABSMeasurementConfig +from opentrons.hardware_control.modules import AbsorbanceReader +from opentrons.protocol_engine.errors import ( + CannotPerformModuleAction, + StorageLimitReachedError, +) + +from opentrons.protocol_engine.execution import EquipmentHandler +from opentrons.protocol_engine.resources import FileProvider +from opentrons.protocol_engine.state import update_types +from opentrons.protocol_engine.state.state import StateView +from opentrons.protocol_engine.state.module_substates import ( + AbsorbanceReaderSubState, + AbsorbanceReaderId, +) +from opentrons.protocol_engine.commands.command import SuccessData +from opentrons.protocol_engine.commands.absorbance_reader import ( + ReadAbsorbanceResult, + ReadAbsorbanceParams, +) +from opentrons.protocol_engine.commands.absorbance_reader.read import ( + ReadAbsorbanceImpl, +) + + +async def test_absorbance_reader_implementation( + decoy: Decoy, + state_view: StateView, + equipment: EquipmentHandler, + file_provider: FileProvider, +) -> None: + """It should validate, find hardware module if not virtualized, and disengage.""" + subject = ReadAbsorbanceImpl( + state_view=state_view, equipment=equipment, file_provider=file_provider + ) + + params = ReadAbsorbanceParams( + moduleId="unverified-module-id", + ) + + mabsorbance_module_substate = decoy.mock(cls=AbsorbanceReaderSubState) + absorbance_module_hw = decoy.mock(cls=AbsorbanceReader) + verified_module_id = AbsorbanceReaderId("module-id") + asbsorbance_result = {1: {"A1": 1.2}} + + decoy.when( + state_view.modules.get_absorbance_reader_substate("unverified-module-id") + ).then_return(mabsorbance_module_substate) + + decoy.when(mabsorbance_module_substate.module_id).then_return(verified_module_id) + + decoy.when(equipment.get_module_hardware_api(verified_module_id)).then_return( + absorbance_module_hw + ) + + decoy.when(await absorbance_module_hw.start_measure()).then_return([[1.2, 1.3]]) + decoy.when(absorbance_module_hw._measurement_config).then_return( + ABSMeasurementConfig( + measure_mode=ABSMeasurementMode.SINGLE, + sample_wavelengths=[1, 2], + reference_wavelength=None, + ) + ) + decoy.when( + state_view.modules.convert_absorbance_reader_data_points([1.2, 1.3]) + ).then_return({"A1": 1.2}) + + result = await subject.execute(params=params) + + assert result == SuccessData( + public=ReadAbsorbanceResult( + data=asbsorbance_result, + fileIds=[], + ), + state_update=update_types.StateUpdate( + files_added=update_types.FilesAddedUpdate(file_ids=[]), + absorbance_reader_state_update=update_types.AbsorbanceReaderStateUpdate( + module_id="module-id", + absorbance_reader_data=update_types.AbsorbanceReaderDataUpdate( + read_result=asbsorbance_result + ), + ), + ), + ) + + +async def test_read_raises_cannot_preform_action( + decoy: Decoy, + state_view: StateView, + equipment: EquipmentHandler, + file_provider: FileProvider, +) -> None: + """It should raise CannotPerformModuleAction when not configured/lid is not on.""" + subject = ReadAbsorbanceImpl( + state_view=state_view, equipment=equipment, file_provider=file_provider + ) + + params = ReadAbsorbanceParams( + moduleId="unverified-module-id", + ) + + mabsorbance_module_substate = decoy.mock(cls=AbsorbanceReaderSubState) + absorbance_module_hw = decoy.mock(cls=AbsorbanceReader) + + verified_module_id = AbsorbanceReaderId("module-id") + + decoy.when( + state_view.modules.get_absorbance_reader_substate("unverified-module-id") + ).then_return(mabsorbance_module_substate) + + decoy.when(mabsorbance_module_substate.module_id).then_return(verified_module_id) + + decoy.when(equipment.get_module_hardware_api(verified_module_id)).then_return( + absorbance_module_hw + ) + + decoy.when(mabsorbance_module_substate.configured).then_return(False) + + with pytest.raises(CannotPerformModuleAction): + await subject.execute(params=params) + + decoy.when(mabsorbance_module_substate.configured).then_return(True) + + decoy.when(mabsorbance_module_substate.is_lid_on).then_return(False) + + with pytest.raises(CannotPerformModuleAction): + await subject.execute(params=params) + + +async def test_read_raises_storage_limit( + decoy: Decoy, + state_view: StateView, + equipment: EquipmentHandler, + file_provider: FileProvider, +) -> None: + """It should raise StorageLimitReachedError when not configured/lid is not on.""" + subject = ReadAbsorbanceImpl( + state_view=state_view, equipment=equipment, file_provider=file_provider + ) + + params = ReadAbsorbanceParams(moduleId="unverified-module-id", fileName="test") + + mabsorbance_module_substate = decoy.mock(cls=AbsorbanceReaderSubState) + absorbance_module_hw = decoy.mock(cls=AbsorbanceReader) + + verified_module_id = AbsorbanceReaderId("module-id") + + decoy.when( + state_view.modules.get_absorbance_reader_substate("unverified-module-id") + ).then_return(mabsorbance_module_substate) + + decoy.when(mabsorbance_module_substate.module_id).then_return(verified_module_id) + + decoy.when(equipment.get_module_hardware_api(verified_module_id)).then_return( + absorbance_module_hw + ) + decoy.when(await absorbance_module_hw.start_measure()).then_return([[1.2, 1.3]]) + + decoy.when(absorbance_module_hw._measurement_config).then_return( + ABSMeasurementConfig( + measure_mode=ABSMeasurementMode.SINGLE, + sample_wavelengths=[1, 2], + reference_wavelength=None, + ) + ) + decoy.when(mabsorbance_module_substate.configured_wavelengths).then_return( + [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11] + ) + + decoy.when(state_view.files.get_filecount()).then_return(390) + with pytest.raises(StorageLimitReachedError): + await subject.execute(params=params) diff --git a/api/tests/opentrons/protocol_engine/commands/conftest.py b/api/tests/opentrons/protocol_engine/commands/conftest.py index 1d27dea0536..cf2d36b092e 100644 --- a/api/tests/opentrons/protocol_engine/commands/conftest.py +++ b/api/tests/opentrons/protocol_engine/commands/conftest.py @@ -15,6 +15,7 @@ TipHandler, GantryMover, ) +from opentrons.protocol_engine.resources import FileProvider from opentrons.protocol_engine.resources.model_utils import ModelUtils from opentrons.protocol_engine.state.state import StateView @@ -83,3 +84,9 @@ def status_bar(decoy: Decoy) -> StatusBarHandler: def gantry_mover(decoy: Decoy) -> GantryMover: """Get a mocked out GantryMover.""" return decoy.mock(cls=GantryMover) + + +@pytest.fixture +def file_provider(decoy: Decoy) -> FileProvider: + """Get a mocked out StateView.""" + return decoy.mock(cls=FileProvider) diff --git a/api/tests/opentrons/protocol_engine/commands/robot/__init__.py b/api/tests/opentrons/protocol_engine/commands/robot/__init__.py new file mode 100644 index 00000000000..36375876456 --- /dev/null +++ b/api/tests/opentrons/protocol_engine/commands/robot/__init__.py @@ -0,0 +1 @@ +"""Tests for Robot Module commands.""" diff --git a/api/tests/opentrons/protocol_engine/commands/robot/test_close_gripper_jaw.py b/api/tests/opentrons/protocol_engine/commands/robot/test_close_gripper_jaw.py new file mode 100644 index 00000000000..c5ccd4bf48d --- /dev/null +++ b/api/tests/opentrons/protocol_engine/commands/robot/test_close_gripper_jaw.py @@ -0,0 +1,28 @@ +"""Test robot.open-gripper-jaw commands.""" +from decoy import Decoy + +from opentrons.hardware_control import OT3HardwareControlAPI + +from opentrons.protocol_engine.commands.command import SuccessData +from opentrons.protocol_engine.commands.robot.close_gripper_jaw import ( + closeGripperJawParams, + closeGripperJawResult, + closeGripperJawImplementation, +) + + +async def test_close_gripper_jaw_implementation( + decoy: Decoy, + ot3_hardware_api: OT3HardwareControlAPI, +) -> None: + """Test the `robot.closeGripperJaw` implementation.""" + subject = closeGripperJawImplementation( + hardware_api=ot3_hardware_api, + ) + + params = closeGripperJawParams(force=10) + + result = await subject.execute(params=params) + + assert result == SuccessData(public=closeGripperJawResult()) + decoy.verify(await ot3_hardware_api.grip(force_newtons=10)) diff --git a/api/tests/opentrons/protocol_engine/commands/robot/test_move_axes_relative_to.py b/api/tests/opentrons/protocol_engine/commands/robot/test_move_axes_relative_to.py new file mode 100644 index 00000000000..11c6e13b54f --- /dev/null +++ b/api/tests/opentrons/protocol_engine/commands/robot/test_move_axes_relative_to.py @@ -0,0 +1,52 @@ +"""Test robot.move-axes-relative commands.""" +from decoy import Decoy + +from opentrons.hardware_control import HardwareControlAPI + +from opentrons.protocol_engine.execution import GantryMover +from opentrons.protocol_engine.types import MotorAxis +from opentrons.hardware_control.protocols.types import FlexRobotType + +from opentrons.protocol_engine.commands.command import SuccessData +from opentrons.protocol_engine.commands.robot.move_axes_relative import ( + MoveAxesRelativeParams, + MoveAxesRelativeResult, + MoveAxesRelativeImplementation, +) + + +async def test_move_axes_to_implementation( + decoy: Decoy, + gantry_mover: GantryMover, + ot3_hardware_api: HardwareControlAPI, +) -> None: + """Test the `robot.moveAxesRelative` implementation. + + It should call `MovementHandler.move_mount_to` with the + correct coordinates. + """ + subject = MoveAxesRelativeImplementation( + gantry_mover=gantry_mover, + hardware_api=ot3_hardware_api, + ) + + params = MoveAxesRelativeParams( + axis_map={MotorAxis.X: 10, MotorAxis.Y: 10, MotorAxis.EXTENSION_Z: 20}, + speed=567.8, + ) + + # Flex shape + decoy.when(ot3_hardware_api.get_robot_type()).then_return(FlexRobotType) + decoy.when( + await gantry_mover.move_axes( + axis_map=params.axis_map, speed=params.speed, relative_move=True + ) + ).then_return({MotorAxis.X: 10, MotorAxis.Y: 10, MotorAxis.EXTENSION_Z: 20}) + + result = await subject.execute(params=params) + + assert result == SuccessData( + public=MoveAxesRelativeResult( + position={MotorAxis.X: 10, MotorAxis.Y: 10, MotorAxis.EXTENSION_Z: 20} + ) + ) diff --git a/api/tests/opentrons/protocol_engine/commands/robot/test_move_axes_to.py b/api/tests/opentrons/protocol_engine/commands/robot/test_move_axes_to.py new file mode 100644 index 00000000000..3caa8b03ec8 --- /dev/null +++ b/api/tests/opentrons/protocol_engine/commands/robot/test_move_axes_to.py @@ -0,0 +1,54 @@ +"""Test robot.move-axes-to commands.""" +from decoy import Decoy + +from opentrons.hardware_control import HardwareControlAPI + +from opentrons.protocol_engine.execution import GantryMover +from opentrons.protocol_engine.types import MotorAxis +from opentrons.hardware_control.protocols.types import FlexRobotType + +from opentrons.protocol_engine.commands.command import SuccessData +from opentrons.protocol_engine.commands.robot.move_axes_to import ( + MoveAxesToParams, + MoveAxesToResult, + MoveAxesToImplementation, +) + + +async def test_move_axes_to_implementation( + decoy: Decoy, + gantry_mover: GantryMover, + ot3_hardware_api: HardwareControlAPI, +) -> None: + """Test the `robot.moveAxesTo` implementation. + + It should call `MovementHandler.move_mount_to` with the + correct coordinates. + """ + subject = MoveAxesToImplementation( + gantry_mover=gantry_mover, + hardware_api=ot3_hardware_api, + ) + + params = MoveAxesToParams( + axis_map={MotorAxis.X: 10, MotorAxis.Y: 10, MotorAxis.EXTENSION_Z: 20}, + critical_point={MotorAxis.X: 1, MotorAxis.Y: 1, MotorAxis.EXTENSION_Z: 0}, + speed=567.8, + ) + + # Flex shape + decoy.when(ot3_hardware_api.get_robot_type()).then_return(FlexRobotType) + decoy.when( + await gantry_mover.move_axes( + axis_map=params.axis_map, + speed=params.speed, + critical_point=params.critical_point, + ) + ).then_return({MotorAxis.X: 10, MotorAxis.Y: 10, MotorAxis.EXTENSION_Z: 20}) + result = await subject.execute(params=params) + + assert result == SuccessData( + public=MoveAxesToResult( + position={MotorAxis.X: 10, MotorAxis.Y: 10, MotorAxis.EXTENSION_Z: 20} + ) + ) diff --git a/api/tests/opentrons/protocol_engine/commands/robot/test_move_to.py b/api/tests/opentrons/protocol_engine/commands/robot/test_move_to.py new file mode 100644 index 00000000000..28bd5b6df33 --- /dev/null +++ b/api/tests/opentrons/protocol_engine/commands/robot/test_move_to.py @@ -0,0 +1,47 @@ +"""Test robot.move-to commands.""" +from decoy import Decoy + +from opentrons.protocol_engine.execution import MovementHandler +from opentrons.protocol_engine.types import DeckPoint +from opentrons.types import Point, MountType + +from opentrons.protocol_engine.commands.command import SuccessData +from opentrons.protocol_engine.commands.robot.move_to import ( + MoveToParams, + MoveToResult, + MoveToImplementation, +) + + +async def test_move_to_implementation( + decoy: Decoy, + movement: MovementHandler, +) -> None: + """Test the `robot.moveTo` implementation. + + It should call `MovementHandler.move_mount_to` with the + correct coordinates. + """ + subject = MoveToImplementation( + movement=movement, + ) + + params = MoveToParams( + mount=MountType.LEFT, + destination=DeckPoint(x=1.11, y=2.22, z=3.33), + speed=567.8, + ) + + decoy.when( + await movement.move_mount_to( + mount=MountType.LEFT, + destination=DeckPoint(x=1.11, y=2.22, z=3.33), + speed=567.8, + ) + ).then_return(Point(x=4.44, y=5.55, z=6.66)) + + result = await subject.execute(params=params) + + assert result == SuccessData( + public=MoveToResult(position=DeckPoint(x=4.44, y=5.55, z=6.66)) + ) diff --git a/api/tests/opentrons/protocol_engine/commands/robot/test_open_gripper_jaw.py b/api/tests/opentrons/protocol_engine/commands/robot/test_open_gripper_jaw.py new file mode 100644 index 00000000000..6ded7932963 --- /dev/null +++ b/api/tests/opentrons/protocol_engine/commands/robot/test_open_gripper_jaw.py @@ -0,0 +1,28 @@ +"""Test robot.open-gripper-jaw commands.""" +from decoy import Decoy + +from opentrons.hardware_control import OT3HardwareControlAPI + +from opentrons.protocol_engine.commands.command import SuccessData +from opentrons.protocol_engine.commands.robot.open_gripper_jaw import ( + openGripperJawParams, + openGripperJawResult, + openGripperJawImplementation, +) + + +async def test_open_gripper_jaw_implementation( + decoy: Decoy, + ot3_hardware_api: OT3HardwareControlAPI, +) -> None: + """Test the `robot.openGripperJaw` implementation.""" + subject = openGripperJawImplementation( + hardware_api=ot3_hardware_api, + ) + + params = openGripperJawParams() + + result = await subject.execute(params=params) + + assert result == SuccessData(public=openGripperJawResult()) + decoy.verify(await ot3_hardware_api.home_gripper_jaw()) diff --git a/api/tests/opentrons/protocol_engine/commands/test_air_gap_in_place.py b/api/tests/opentrons/protocol_engine/commands/test_air_gap_in_place.py new file mode 100644 index 00000000000..b9d110fd9c2 --- /dev/null +++ b/api/tests/opentrons/protocol_engine/commands/test_air_gap_in_place.py @@ -0,0 +1,284 @@ +"""Test aspirate-in-place commands.""" +from datetime import datetime + +import pytest +from decoy import Decoy, matchers + +from opentrons_shared_data.errors.exceptions import PipetteOverpressureError + +from opentrons.types import Point +from opentrons.hardware_control import API as HardwareAPI + +from opentrons.protocol_engine.execution import PipettingHandler, GantryMover +from opentrons.protocol_engine.commands.air_gap_in_place import ( + AirGapInPlaceParams, + AirGapInPlaceResult, + AirGapInPlaceImplementation, +) +from opentrons.protocol_engine.commands.command import SuccessData, DefinedErrorData +from opentrons.protocol_engine.errors.exceptions import PipetteNotReadyToAspirateError +from opentrons.protocol_engine.notes import CommandNoteAdder +from opentrons.protocol_engine.resources import ModelUtils +from opentrons.protocol_engine.state.state import StateStore +from opentrons.protocol_engine.commands.pipetting_common import OverpressureError +from opentrons.protocol_engine.types import ( + CurrentWell, + CurrentPipetteLocation, + CurrentAddressableArea, + AspiratedFluid, + FluidKind, +) +from opentrons.protocol_engine.state import update_types + + +@pytest.fixture +def hardware_api(decoy: Decoy) -> HardwareAPI: + """Get a mock in the shape of a HardwareAPI.""" + return decoy.mock(cls=HardwareAPI) + + +@pytest.fixture +def state_store(decoy: Decoy) -> StateStore: + """Get a mock in the shape of a StateStore.""" + return decoy.mock(cls=StateStore) + + +@pytest.fixture +def pipetting(decoy: Decoy) -> PipettingHandler: + """Get a mock in the shape of a PipettingHandler.""" + return decoy.mock(cls=PipettingHandler) + + +@pytest.fixture +def subject( + pipetting: PipettingHandler, + state_store: StateStore, + hardware_api: HardwareAPI, + mock_command_note_adder: CommandNoteAdder, + model_utils: ModelUtils, + gantry_mover: GantryMover, +) -> AirGapInPlaceImplementation: + """Get the impelementation subject.""" + return AirGapInPlaceImplementation( + pipetting=pipetting, + hardware_api=hardware_api, + state_view=state_store, + command_note_adder=mock_command_note_adder, + model_utils=model_utils, + gantry_mover=gantry_mover, + ) + + +@pytest.mark.parametrize( + "location,stateupdateLabware,stateupdateWell", + [ + ( + CurrentWell( + pipette_id="pipette-id-abc", + labware_id="labware-id-1", + well_name="well-name-1", + ), + "labware-id-1", + "well-name-1", + ), + (None, None, None), + (CurrentAddressableArea("pipette-id-abc", "addressable-area-1"), None, None), + ], +) +async def test_air_gap_in_place_implementation( + decoy: Decoy, + pipetting: PipettingHandler, + state_store: StateStore, + hardware_api: HardwareAPI, + mock_command_note_adder: CommandNoteAdder, + subject: AirGapInPlaceImplementation, + location: CurrentPipetteLocation | None, + stateupdateLabware: str, + stateupdateWell: str, +) -> None: + """It should aspirate in place.""" + data = AirGapInPlaceParams( + pipetteId="pipette-id-abc", + volume=123, + flowRate=1.234, + ) + + decoy.when( + pipetting.get_is_ready_to_aspirate( + pipette_id="pipette-id-abc", + ) + ).then_return(True) + + decoy.when( + await pipetting.aspirate_in_place( + pipette_id="pipette-id-abc", + volume=123, + flow_rate=1.234, + command_note_adder=mock_command_note_adder, + ) + ).then_return(123) + + decoy.when(state_store.pipettes.get_current_location()).then_return(location) + + result = await subject.execute(params=data) + + if isinstance(location, CurrentWell): + assert result == SuccessData( + public=AirGapInPlaceResult(volume=123), + state_update=update_types.StateUpdate( + pipette_aspirated_fluid=update_types.PipetteAspiratedFluidUpdate( + pipette_id="pipette-id-abc", + fluid=AspiratedFluid(kind=FluidKind.AIR, volume=123), + ) + ), + ) + else: + assert result == SuccessData( + public=AirGapInPlaceResult(volume=123), + state_update=update_types.StateUpdate( + pipette_aspirated_fluid=update_types.PipetteAspiratedFluidUpdate( + pipette_id="pipette-id-abc", + fluid=AspiratedFluid(kind=FluidKind.AIR, volume=123), + ) + ), + ) + + +async def test_handle_air_gap_in_place_request_not_ready_to_aspirate( + decoy: Decoy, + pipetting: PipettingHandler, + state_store: StateStore, + hardware_api: HardwareAPI, + subject: AirGapInPlaceImplementation, +) -> None: + """Should raise an exception for not ready to aspirate.""" + data = AirGapInPlaceParams( + pipetteId="pipette-id-abc", + volume=123, + flowRate=1.234, + ) + + decoy.when( + pipetting.get_is_ready_to_aspirate( + pipette_id="pipette-id-abc", + ) + ).then_return(False) + + with pytest.raises( + PipetteNotReadyToAspirateError, + match="Pipette cannot air gap in place because of a previous blow out." + " The first aspirate following a blow-out must be from a specific well" + " so the plunger can be reset in a known safe position.", + ): + await subject.execute(params=data) + + +async def test_aspirate_raises_volume_error( + decoy: Decoy, + pipetting: PipettingHandler, + subject: AirGapInPlaceImplementation, + mock_command_note_adder: CommandNoteAdder, +) -> None: + """Should raise an assertion error for volume larger than working volume.""" + data = AirGapInPlaceParams( + pipetteId="abc", + volume=50, + flowRate=1.23, + ) + + decoy.when(pipetting.get_is_ready_to_aspirate(pipette_id="abc")).then_return(True) + + decoy.when( + await pipetting.aspirate_in_place( + pipette_id="abc", + volume=50, + flow_rate=1.23, + command_note_adder=mock_command_note_adder, + ) + ).then_raise(AssertionError("blah blah")) + + with pytest.raises(AssertionError): + await subject.execute(data) + + +@pytest.mark.parametrize( + "location,stateupdateLabware,stateupdateWell", + [ + ( + CurrentWell( + pipette_id="pipette-id", + labware_id="labware-id-1", + well_name="well-name-1", + ), + "labware-id-1", + "well-name-1", + ), + (None, None, None), + (CurrentAddressableArea("pipette-id", "addressable-area-1"), None, None), + ], +) +async def test_overpressure_error( + decoy: Decoy, + gantry_mover: GantryMover, + pipetting: PipettingHandler, + subject: AirGapInPlaceImplementation, + model_utils: ModelUtils, + mock_command_note_adder: CommandNoteAdder, + state_store: StateStore, + location: CurrentPipetteLocation | None, + stateupdateLabware: str, + stateupdateWell: str, +) -> None: + """It should return an overpressure error if the hardware API indicates that.""" + pipette_id = "pipette-id" + + position = Point(x=1, y=2, z=3) + + error_id = "error-id" + error_timestamp = datetime(year=2020, month=1, day=2) + + data = AirGapInPlaceParams( + pipetteId=pipette_id, + volume=50, + flowRate=1.23, + ) + + decoy.when(pipetting.get_is_ready_to_aspirate(pipette_id=pipette_id)).then_return( + True + ) + + decoy.when( + await pipetting.aspirate_in_place( + pipette_id=pipette_id, + volume=50, + flow_rate=1.23, + command_note_adder=mock_command_note_adder, + ), + ).then_raise(PipetteOverpressureError()) + + decoy.when(model_utils.generate_id()).then_return(error_id) + decoy.when(model_utils.get_timestamp()).then_return(error_timestamp) + decoy.when(await gantry_mover.get_position(pipette_id)).then_return(position) + decoy.when(state_store.pipettes.get_current_location()).then_return(location) + + result = await subject.execute(data) + + if isinstance(location, CurrentWell): + assert result == DefinedErrorData( + public=OverpressureError.model_construct( + id=error_id, + createdAt=error_timestamp, + wrappedErrors=[matchers.Anything()], + errorInfo={"retryLocation": (position.x, position.y, position.z)}, + ), + state_update=update_types.StateUpdate(), + ) + else: + assert result == DefinedErrorData( + public=OverpressureError.model_construct( + id=error_id, + createdAt=error_timestamp, + wrappedErrors=[matchers.Anything()], + errorInfo={"retryLocation": (position.x, position.y, position.z)}, + ), + ) diff --git a/api/tests/opentrons/protocol_engine/commands/test_aspirate.py b/api/tests/opentrons/protocol_engine/commands/test_aspirate.py index 102114b1cc8..4a8adbcdc76 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_aspirate.py +++ b/api/tests/opentrons/protocol_engine/commands/test_aspirate.py @@ -1,13 +1,18 @@ """Test aspirate commands.""" + from datetime import datetime -from opentrons_shared_data.errors.exceptions import PipetteOverpressureError +from opentrons_shared_data.errors.exceptions import ( + PipetteOverpressureError, + StallOrCollisionDetectedError, +) from decoy import matchers, Decoy import pytest from opentrons.protocol_engine.commands.pipetting_common import OverpressureError +from opentrons.protocol_engine.commands.movement_common import StallOrCollisionError from opentrons.protocol_engine.state import update_types -from opentrons.types import MountType, Point +from opentrons.types import Point from opentrons.protocol_engine import ( LiquidHandlingWellLocation, WellOrigin, @@ -29,7 +34,12 @@ PipettingHandler, ) from opentrons.protocol_engine.resources.model_utils import ModelUtils -from opentrons.protocol_engine.types import CurrentWell, LoadedPipette +from opentrons.protocol_engine.types import ( + CurrentWell, + AspiratedFluid, + FluidKind, + WellLocation, +) from opentrons.hardware_control import HardwareControlAPI from opentrons.protocol_engine.notes import CommandNoteAdder @@ -57,62 +67,88 @@ def subject( async def test_aspirate_implementation_no_prep( decoy: Decoy, state_view: StateView, - hardware_api: HardwareControlAPI, movement: MovementHandler, pipetting: PipettingHandler, subject: AspirateImplementation, mock_command_note_adder: CommandNoteAdder, ) -> None: """An Aspirate should have an execution implementation without preparing to aspirate.""" + pipette_id = "pipette-id" + labware_id = "labware-id" + well_name = "well-name" location = LiquidHandlingWellLocation( origin=WellOrigin.BOTTOM, offset=WellOffset(x=0, y=0, z=1) ) - - data = AspirateParams( - pipetteId="abc", - labwareId="123", - wellName="A3", + params = AspirateParams( + pipetteId=pipette_id, + labwareId=labware_id, + wellName=well_name, wellLocation=location, volume=50, flowRate=1.23, ) - decoy.when(pipetting.get_is_ready_to_aspirate(pipette_id="abc")).then_return(True) + decoy.when(pipetting.get_is_ready_to_aspirate(pipette_id=pipette_id)).then_return( + True + ) + + decoy.when( + state_view.geometry.get_nozzles_per_well( + labware_id=labware_id, + target_well_name=well_name, + pipette_id=pipette_id, + ) + ).then_return(2) + + decoy.when( + state_view.geometry.get_wells_covered_by_pipette_with_active_well( + labware_id, well_name, pipette_id + ) + ).then_return(["covered-well-1", "covered-well-2"]) decoy.when( await movement.move_to_well( - pipette_id="abc", - labware_id="123", - well_name="A3", + pipette_id=pipette_id, + labware_id=labware_id, + well_name=well_name, well_location=location, current_well=None, + force_direct=False, + minimum_z_height=None, + speed=None, operation_volume=-50, ), ).then_return(Point(x=1, y=2, z=3)) decoy.when( await pipetting.aspirate_in_place( - pipette_id="abc", + pipette_id=pipette_id, volume=50, flow_rate=1.23, command_note_adder=mock_command_note_adder, ), ).then_return(50) - result = await subject.execute(data) + result = await subject.execute(params) assert result == SuccessData( public=AspirateResult(volume=50, position=DeckPoint(x=1, y=2, z=3)), state_update=update_types.StateUpdate( pipette_location=update_types.PipetteLocationUpdate( - pipette_id="abc", - new_location=update_types.Well(labware_id="123", well_name="A3"), + pipette_id=pipette_id, + new_location=update_types.Well( + labware_id=labware_id, well_name=well_name + ), new_deck_point=DeckPoint(x=1, y=2, z=3), ), liquid_operated=update_types.LiquidOperatedUpdate( - labware_id="123", - well_name="A3", - volume_added=-50, + labware_id=labware_id, + well_names=["covered-well-1", "covered-well-2"], + volume_added=-100, + ), + pipette_aspirated_fluid=update_types.PipetteAspiratedFluidUpdate( + pipette_id=pipette_id, + fluid=AspiratedFluid(kind=FluidKind.LIQUID, volume=50), ), ), ) @@ -121,123 +157,172 @@ async def test_aspirate_implementation_no_prep( async def test_aspirate_implementation_with_prep( decoy: Decoy, state_view: StateView, - hardware_api: HardwareControlAPI, movement: MovementHandler, pipetting: PipettingHandler, mock_command_note_adder: CommandNoteAdder, subject: AspirateImplementation, ) -> None: """An Aspirate should have an execution implementation with preparing to aspirate.""" + pipette_id = "pipette-id" + labware_id = "labware-id" + well_name = "well-name" location = LiquidHandlingWellLocation( origin=WellOrigin.BOTTOM, offset=WellOffset(x=0, y=0, z=1) ) - - data = AspirateParams( - pipetteId="abc", - labwareId="123", - wellName="A3", + volume = 50 + flow_rate = 1.23 + params = AspirateParams( + pipetteId=pipette_id, + labwareId=labware_id, + wellName=well_name, wellLocation=location, - volume=50, - flowRate=1.23, + volume=volume, + flowRate=flow_rate, ) - decoy.when(pipetting.get_is_ready_to_aspirate(pipette_id="abc")).then_return(False) + decoy.when(pipetting.get_is_ready_to_aspirate(pipette_id=pipette_id)).then_return( + False + ) - decoy.when(state_view.pipettes.get(pipette_id="abc")).then_return( - LoadedPipette.construct( # type:ignore[call-arg] - mount=MountType.LEFT + decoy.when( + state_view.geometry.get_nozzles_per_well( + labware_id=labware_id, + target_well_name=well_name, + pipette_id=pipette_id, ) - ) + ).then_return(2) + + decoy.when( + state_view.geometry.get_wells_covered_by_pipette_with_active_well( + labware_id, well_name, pipette_id + ) + ).then_return(["covered-well-1", "covered-well-2"]) + + decoy.when( + await movement.move_to_well( + pipette_id=pipette_id, + labware_id=labware_id, + well_name=well_name, + well_location=WellLocation(origin=WellOrigin.TOP), + current_well=None, + force_direct=False, + minimum_z_height=None, + speed=None, + operation_volume=None, + ), + ).then_return(Point()) + decoy.when( await movement.move_to_well( - pipette_id="abc", - labware_id="123", - well_name="A3", + pipette_id=pipette_id, + labware_id=labware_id, + well_name=well_name, well_location=location, current_well=CurrentWell( - pipette_id="abc", - labware_id="123", - well_name="A3", + pipette_id=pipette_id, + labware_id=labware_id, + well_name=well_name, ), - operation_volume=-50, + force_direct=False, + minimum_z_height=None, + speed=None, + operation_volume=-volume, ), ).then_return(Point(x=1, y=2, z=3)) decoy.when( await pipetting.aspirate_in_place( - pipette_id="abc", - volume=50, - flow_rate=1.23, + pipette_id=pipette_id, + volume=volume, + flow_rate=flow_rate, command_note_adder=mock_command_note_adder, ), - ).then_return(50) + ).then_return(volume) - result = await subject.execute(data) + result = await subject.execute(params) assert result == SuccessData( public=AspirateResult(volume=50, position=DeckPoint(x=1, y=2, z=3)), state_update=update_types.StateUpdate( pipette_location=update_types.PipetteLocationUpdate( - pipette_id="abc", - new_location=update_types.Well(labware_id="123", well_name="A3"), + pipette_id=pipette_id, + new_location=update_types.Well( + labware_id=labware_id, well_name=well_name + ), new_deck_point=DeckPoint(x=1, y=2, z=3), ), liquid_operated=update_types.LiquidOperatedUpdate( - labware_id="123", - well_name="A3", - volume_added=-50, + labware_id=labware_id, + well_names=["covered-well-1", "covered-well-2"], + volume_added=-100, + ), + pipette_aspirated_fluid=update_types.PipetteAspiratedFluidUpdate( + pipette_id=pipette_id, + fluid=AspiratedFluid(kind=FluidKind.LIQUID, volume=50), ), ), ) - decoy.verify( - await movement.move_to_well( - pipette_id="abc", - labware_id="123", - well_name="A3", - well_location=LiquidHandlingWellLocation(origin=WellOrigin.TOP), - ), - await pipetting.prepare_for_aspirate(pipette_id="abc"), - ) - async def test_aspirate_raises_volume_error( decoy: Decoy, pipetting: PipettingHandler, movement: MovementHandler, mock_command_note_adder: CommandNoteAdder, + state_view: StateView, subject: AspirateImplementation, ) -> None: """Should raise an assertion error for volume larger than working volume.""" + pipette_id = "pipette-id" + labware_id = "labware-id" + well_name = "well-name" location = LiquidHandlingWellLocation( origin=WellOrigin.BOTTOM, offset=WellOffset(x=0, y=0, z=1) ) - - data = AspirateParams( - pipetteId="abc", - labwareId="123", - wellName="A3", + params = AspirateParams( + pipetteId=pipette_id, + labwareId=labware_id, + wellName=well_name, wellLocation=location, volume=50, flowRate=1.23, ) - decoy.when(pipetting.get_is_ready_to_aspirate(pipette_id="abc")).then_return(True) + decoy.when(pipetting.get_is_ready_to_aspirate(pipette_id=pipette_id)).then_return( + True + ) + + decoy.when( + state_view.geometry.get_nozzles_per_well( + labware_id=labware_id, + target_well_name=well_name, + pipette_id=pipette_id, + ) + ).then_return(2) + + decoy.when( + state_view.geometry.get_wells_covered_by_pipette_with_active_well( + labware_id, well_name, pipette_id + ) + ).then_return(["covered-well-1", "covered-well-2"]) decoy.when( await movement.move_to_well( - pipette_id="abc", - labware_id="123", - well_name="A3", + pipette_id=pipette_id, + labware_id=labware_id, + well_name=well_name, well_location=location, current_well=None, + force_direct=False, + minimum_z_height=None, + speed=None, operation_volume=-50, ), ).then_return(Point(1, 2, 3)) decoy.when( await pipetting.aspirate_in_place( - pipette_id="abc", + pipette_id=pipette_id, volume=50, flow_rate=1.23, command_note_adder=mock_command_note_adder, @@ -245,7 +330,7 @@ async def test_aspirate_raises_volume_error( ).then_raise(AssertionError("blah blah")) with pytest.raises(AssertionError): - await subject.execute(data) + await subject.execute(params) async def test_overpressure_error( @@ -255,6 +340,7 @@ async def test_overpressure_error( subject: AspirateImplementation, model_utils: ModelUtils, mock_command_note_adder: CommandNoteAdder, + state_view: StateView, ) -> None: """It should return an overpressure error if the hardware API indicates that.""" pipette_id = "pipette-id" @@ -269,7 +355,7 @@ async def test_overpressure_error( error_id = "error-id" error_timestamp = datetime(year=2020, month=1, day=2) - data = AspirateParams( + params = AspirateParams( pipetteId=pipette_id, labwareId=labware_id, wellName=well_name, @@ -278,6 +364,20 @@ async def test_overpressure_error( flowRate=1.23, ) + decoy.when( + state_view.geometry.get_nozzles_per_well( + labware_id=labware_id, + target_well_name=well_name, + pipette_id=pipette_id, + ) + ).then_return(2) + + decoy.when( + state_view.geometry.get_wells_covered_by_pipette_with_active_well( + labware_id, well_name, pipette_id + ) + ).then_return(["covered-well-1", "covered-well-2"]) + decoy.when(pipetting.get_is_ready_to_aspirate(pipette_id=pipette_id)).then_return( True ) @@ -289,6 +389,9 @@ async def test_overpressure_error( well_name=well_name, well_location=well_location, current_well=None, + force_direct=False, + minimum_z_height=None, + speed=None, operation_volume=-50, ), ).then_return(position) @@ -305,10 +408,10 @@ async def test_overpressure_error( decoy.when(model_utils.generate_id()).then_return(error_id) decoy.when(model_utils.get_timestamp()).then_return(error_timestamp) - result = await subject.execute(data) + result = await subject.execute(params) assert result == DefinedErrorData( - public=OverpressureError.construct( + public=OverpressureError.model_construct( id=error_id, createdAt=error_timestamp, wrappedErrors=[matchers.Anything()], @@ -324,9 +427,12 @@ async def test_overpressure_error( ), liquid_operated=update_types.LiquidOperatedUpdate( labware_id=labware_id, - well_name=well_name, + well_names=["covered-well-1", "covered-well-2"], volume_added=update_types.CLEAR, ), + pipette_aspirated_fluid=update_types.PipetteUnknownFluidUpdate( + pipette_id=pipette_id + ), ), ) @@ -341,56 +447,295 @@ async def test_aspirate_implementation_meniscus( mock_command_note_adder: CommandNoteAdder, ) -> None: """Aspirate should update WellVolumeOffset when called with WellOrigin.MENISCUS.""" + pipette_id = "pipette-id" + labware_id = "labware-id" + well_name = "well-name" location = LiquidHandlingWellLocation( origin=WellOrigin.MENISCUS, offset=WellOffset(x=0, y=0, z=-1), volumeOffset="operationVolume", ) - data = AspirateParams( - pipetteId="abc", - labwareId="123", - wellName="A3", + params = AspirateParams( + pipetteId=pipette_id, + labwareId=labware_id, + wellName=well_name, wellLocation=location, volume=50, flowRate=1.23, ) - decoy.when(pipetting.get_is_ready_to_aspirate(pipette_id="abc")).then_return(True) + decoy.when( + state_view.geometry.get_nozzles_per_well( + labware_id=labware_id, + target_well_name=well_name, + pipette_id=pipette_id, + ) + ).then_return(2) + + decoy.when( + state_view.geometry.get_wells_covered_by_pipette_with_active_well( + labware_id, well_name, pipette_id + ) + ).then_return(["covered-well-1", "covered-well-2"]) + + decoy.when(pipetting.get_is_ready_to_aspirate(pipette_id=pipette_id)).then_return( + True + ) decoy.when( await movement.move_to_well( - pipette_id="abc", - labware_id="123", - well_name="A3", + pipette_id=pipette_id, + labware_id=labware_id, + well_name=well_name, well_location=location, current_well=None, + force_direct=False, + minimum_z_height=None, + speed=None, operation_volume=-50, ), ).then_return(Point(x=1, y=2, z=3)) decoy.when( await pipetting.aspirate_in_place( - pipette_id="abc", + pipette_id=pipette_id, volume=50, flow_rate=1.23, command_note_adder=mock_command_note_adder, ), ).then_return(50) - result = await subject.execute(data) + result = await subject.execute(params) assert result == SuccessData( public=AspirateResult(volume=50, position=DeckPoint(x=1, y=2, z=3)), state_update=update_types.StateUpdate( pipette_location=update_types.PipetteLocationUpdate( - pipette_id="abc", - new_location=update_types.Well(labware_id="123", well_name="A3"), + pipette_id=pipette_id, + new_location=update_types.Well( + labware_id=labware_id, well_name=well_name + ), new_deck_point=DeckPoint(x=1, y=2, z=3), ), liquid_operated=update_types.LiquidOperatedUpdate( - labware_id="123", - well_name="A3", - volume_added=-50, + labware_id=labware_id, + well_names=["covered-well-1", "covered-well-2"], + volume_added=-100, + ), + pipette_aspirated_fluid=update_types.PipetteAspiratedFluidUpdate( + pipette_id=pipette_id, + fluid=AspiratedFluid(kind=FluidKind.LIQUID, volume=50), + ), + ), + ) + + +async def test_stall_during_final_movement( + decoy: Decoy, + movement: MovementHandler, + pipetting: PipettingHandler, + subject: AspirateImplementation, + model_utils: ModelUtils, + state_view: StateView, +) -> None: + """It should propagate a stall error that happens when moving to the final position.""" + pipette_id = "pipette-id" + labware_id = "labware-id" + well_name = "well-name" + well_location = LiquidHandlingWellLocation( + origin=WellOrigin.BOTTOM, offset=WellOffset(x=0, y=0, z=1) + ) + + error_id = "error-id" + error_timestamp = datetime(year=2020, month=1, day=2) + decoy.when(pipetting.get_is_ready_to_aspirate(pipette_id=pipette_id)).then_return( + True + ) + + params = AspirateParams( + pipetteId=pipette_id, + labwareId=labware_id, + wellName=well_name, + wellLocation=well_location, + volume=50, + flowRate=1.23, + ) + + decoy.when( + await movement.move_to_well( + pipette_id=pipette_id, + labware_id=labware_id, + well_name=well_name, + well_location=well_location, + current_well=None, + force_direct=False, + minimum_z_height=None, + speed=None, + operation_volume=-50, + ), + ).then_raise(StallOrCollisionDetectedError()) + + decoy.when(model_utils.generate_id()).then_return(error_id) + decoy.when(model_utils.get_timestamp()).then_return(error_timestamp) + + result = await subject.execute(params) + + assert result == DefinedErrorData( + public=StallOrCollisionError.model_construct( + id=error_id, + createdAt=error_timestamp, + wrappedErrors=[matchers.Anything()], + ), + state_update=update_types.StateUpdate(pipette_location=update_types.CLEAR), + ) + + +async def test_stall_during_preparation( + decoy: Decoy, + movement: MovementHandler, + pipetting: PipettingHandler, + subject: AspirateImplementation, + model_utils: ModelUtils, +) -> None: + """It should propagate a stall error that happens during the prepare-to-aspirate part.""" + pipette_id = "pipette-id" + labware_id = "labware-id" + well_name = "well-name" + well_location = LiquidHandlingWellLocation( + origin=WellOrigin.BOTTOM, offset=WellOffset(x=0, y=0, z=1) + ) + + error_id = "error-id" + error_timestamp = datetime(year=2020, month=1, day=2) + + params = AspirateParams( + pipetteId=pipette_id, + labwareId=labware_id, + wellName=well_name, + wellLocation=well_location, + volume=50, + flowRate=1.23, + ) + + decoy.when(pipetting.get_is_ready_to_aspirate(pipette_id=pipette_id)).then_return( + False + ) + + decoy.when( + await movement.move_to_well( + pipette_id=pipette_id, + labware_id=labware_id, + well_name=well_name, + well_location=WellLocation(origin=WellOrigin.TOP), + current_well=None, + force_direct=False, + minimum_z_height=None, + speed=None, + operation_volume=None, + ), + ).then_raise(StallOrCollisionDetectedError()) + decoy.when(model_utils.generate_id()).then_return(error_id) + decoy.when(model_utils.get_timestamp()).then_return(error_timestamp) + + result = await subject.execute(params) + assert result == DefinedErrorData( + public=StallOrCollisionError.model_construct( + id=error_id, createdAt=error_timestamp, wrappedErrors=[matchers.Anything()] + ), + state_update=update_types.StateUpdate( + pipette_location=update_types.CLEAR, + ), + state_update_if_false_positive=update_types.StateUpdate(), + ) + + +async def test_overpressure_during_preparation( + decoy: Decoy, + movement: MovementHandler, + pipetting: PipettingHandler, + subject: AspirateImplementation, + state_view: StateView, + model_utils: ModelUtils, +) -> None: + """It should propagate an overpressure error that happens during the prepare-to-aspirate part.""" + pipette_id = "pipette-id" + labware_id = "labware-id" + well_name = "well-name" + well_location = LiquidHandlingWellLocation( + origin=WellOrigin.BOTTOM, offset=WellOffset(x=0, y=0, z=1) + ) + + error_id = "error-id" + error_timestamp = datetime(year=2020, month=1, day=2) + + params = AspirateParams( + pipetteId=pipette_id, + labwareId=labware_id, + wellName=well_name, + wellLocation=well_location, + volume=50, + flowRate=1.23, + ) + + decoy.when(pipetting.get_is_ready_to_aspirate(pipette_id=pipette_id)).then_return( + False + ) + + retry_location = Point(1, 2, 3) + decoy.when( + state_view.geometry.get_well_position( + labware_id=labware_id, + well_name=well_name, + well_location=well_location, + operation_volume=-params.volume, + pipette_id=pipette_id, + ) + ).then_return(retry_location) + + prep_location = Point(4, 5, 6) + decoy.when( + await movement.move_to_well( + pipette_id=pipette_id, + labware_id=labware_id, + well_name=well_name, + well_location=WellLocation(origin=WellOrigin.TOP), + current_well=None, + force_direct=False, + minimum_z_height=None, + speed=None, + operation_volume=None, + ), + ).then_return(prep_location) + + decoy.when(await pipetting.prepare_for_aspirate(pipette_id)).then_raise( + PipetteOverpressureError() + ) + decoy.when(model_utils.generate_id()).then_return(error_id) + decoy.when(model_utils.get_timestamp()).then_return(error_timestamp) + + result = await subject.execute(params) + assert result == DefinedErrorData( + public=OverpressureError.model_construct( + id=error_id, + createdAt=error_timestamp, + wrappedErrors=[matchers.Anything()], + errorInfo={ + "retryLocation": (retry_location.x, retry_location.y, retry_location.z) + }, + ), + state_update=update_types.StateUpdate( + pipette_location=update_types.PipetteLocationUpdate( + pipette_id=pipette_id, + new_location=update_types.Well( + labware_id=labware_id, well_name=well_name + ), + new_deck_point=DeckPoint( + x=prep_location.x, y=prep_location.y, z=prep_location.z + ), + ), + pipette_aspirated_fluid=update_types.PipetteUnknownFluidUpdate( + pipette_id=pipette_id ), ), + state_update_if_false_positive=update_types.StateUpdate(), ) diff --git a/api/tests/opentrons/protocol_engine/commands/test_aspirate_in_place.py b/api/tests/opentrons/protocol_engine/commands/test_aspirate_in_place.py index 85d8f4fab84..5a7ca3ee940 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_aspirate_in_place.py +++ b/api/tests/opentrons/protocol_engine/commands/test_aspirate_in_place.py @@ -1,4 +1,5 @@ """Test aspirate-in-place commands.""" + from datetime import datetime import pytest @@ -25,6 +26,8 @@ CurrentWell, CurrentPipetteLocation, CurrentAddressableArea, + AspiratedFluid, + FluidKind, ) from opentrons.protocol_engine.state import update_types @@ -85,6 +88,7 @@ def subject( ) async def test_aspirate_in_place_implementation( decoy: Decoy, + gantry_mover: GantryMover, pipetting: PipettingHandler, state_store: StateStore, hardware_api: HardwareAPI, @@ -100,7 +104,19 @@ async def test_aspirate_in_place_implementation( volume=123, flowRate=1.234, ) + decoy.when( + state_store.geometry.get_nozzles_per_well( + labware_id=stateupdateLabware, + target_well_name=stateupdateWell, + pipette_id="pipette-id-abc", + ) + ).then_return(2) + decoy.when( + state_store.geometry.get_wells_covered_by_pipette_with_active_well( + stateupdateLabware, stateupdateWell, "pipette-id-abc" + ) + ).then_return(["A3", "A4"]) decoy.when( pipetting.get_is_ready_to_aspirate( pipette_id="pipette-id-abc", @@ -116,6 +132,10 @@ async def test_aspirate_in_place_implementation( ) ).then_return(123) + decoy.when(await gantry_mover.get_position("pipette-id-abc")).then_return( + Point(1, 2, 3) + ) + decoy.when(state_store.pipettes.get_current_location()).then_return(location) result = await subject.execute(params=data) @@ -126,19 +146,30 @@ async def test_aspirate_in_place_implementation( state_update=update_types.StateUpdate( liquid_operated=update_types.LiquidOperatedUpdate( labware_id=stateupdateLabware, - well_name=stateupdateWell, - volume_added=-123, - ) + well_names=["A3", "A4"], + volume_added=-246, + ), + pipette_aspirated_fluid=update_types.PipetteAspiratedFluidUpdate( + pipette_id="pipette-id-abc", + fluid=AspiratedFluid(kind=FluidKind.LIQUID, volume=123), + ), ), ) else: assert result == SuccessData( public=AspirateInPlaceResult(volume=123), + state_update=update_types.StateUpdate( + pipette_aspirated_fluid=update_types.PipetteAspiratedFluidUpdate( + pipette_id="pipette-id-abc", + fluid=AspiratedFluid(kind=FluidKind.LIQUID, volume=123), + ) + ), ) async def test_handle_aspirate_in_place_request_not_ready_to_aspirate( decoy: Decoy, + gantry_mover: GantryMover, pipetting: PipettingHandler, state_store: StateStore, hardware_api: HardwareAPI, @@ -150,7 +181,9 @@ async def test_handle_aspirate_in_place_request_not_ready_to_aspirate( volume=123, flowRate=1.234, ) - + decoy.when(await gantry_mover.get_position("pipette-id-abc")).then_return( + Point(1, 2, 3) + ) decoy.when( pipetting.get_is_ready_to_aspirate( pipette_id="pipette-id-abc", @@ -171,6 +204,7 @@ async def test_aspirate_raises_volume_error( pipetting: PipettingHandler, subject: AspirateInPlaceImplementation, mock_command_note_adder: CommandNoteAdder, + gantry_mover: GantryMover, ) -> None: """Should raise an assertion error for volume larger than working volume.""" data = AspirateInPlaceParams( @@ -178,7 +212,7 @@ async def test_aspirate_raises_volume_error( volume=50, flowRate=1.23, ) - + decoy.when(await gantry_mover.get_position("abc")).then_return(Point(x=1, y=2, z=3)) decoy.when(pipetting.get_is_ready_to_aspirate(pipette_id="abc")).then_return(True) decoy.when( @@ -229,7 +263,19 @@ async def test_overpressure_error( error_id = "error-id" error_timestamp = datetime(year=2020, month=1, day=2) + decoy.when( + state_store.geometry.get_nozzles_per_well( + labware_id=stateupdateLabware, + target_well_name=stateupdateWell, + pipette_id="pipette-id", + ) + ).then_return(2) + decoy.when( + state_store.geometry.get_wells_covered_by_pipette_with_active_well( + stateupdateLabware, stateupdateWell, "pipette-id" + ) + ).then_return(["A3", "A4"]) data = AspirateInPlaceParams( pipetteId=pipette_id, volume=50, @@ -258,7 +304,7 @@ async def test_overpressure_error( if isinstance(location, CurrentWell): assert result == DefinedErrorData( - public=OverpressureError.construct( + public=OverpressureError.model_construct( id=error_id, createdAt=error_timestamp, wrappedErrors=[matchers.Anything()], @@ -267,17 +313,25 @@ async def test_overpressure_error( state_update=update_types.StateUpdate( liquid_operated=update_types.LiquidOperatedUpdate( labware_id=stateupdateLabware, - well_name=stateupdateWell, + well_names=["A3", "A4"], volume_added=update_types.CLEAR, - ) + ), + pipette_aspirated_fluid=update_types.PipetteUnknownFluidUpdate( + pipette_id="pipette-id" + ), ), ) else: assert result == DefinedErrorData( - public=OverpressureError.construct( + public=OverpressureError.model_construct( id=error_id, createdAt=error_timestamp, wrappedErrors=[matchers.Anything()], errorInfo={"retryLocation": (position.x, position.y, position.z)}, ), + state_update=update_types.StateUpdate( + pipette_aspirated_fluid=update_types.PipetteUnknownFluidUpdate( + pipette_id="pipette-id" + ) + ), ) diff --git a/api/tests/opentrons/protocol_engine/commands/test_blow_out.py b/api/tests/opentrons/protocol_engine/commands/test_blow_out.py index 3e9aa6d82b8..7549141be5b 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_blow_out.py +++ b/api/tests/opentrons/protocol_engine/commands/test_blow_out.py @@ -1,8 +1,12 @@ """Test blow-out command.""" + from datetime import datetime + from decoy import Decoy, matchers +import pytest from opentrons.protocol_engine.commands.pipetting_common import OverpressureError +from opentrons.protocol_engine.commands.movement_common import StallOrCollisionError from opentrons.protocol_engine.resources.model_utils import ModelUtils from opentrons.types import Point from opentrons.protocol_engine import ( @@ -24,8 +28,10 @@ PipettingHandler, ) from opentrons.hardware_control import HardwareControlAPI -from opentrons_shared_data.errors.exceptions import PipetteOverpressureError -import pytest +from opentrons_shared_data.errors.exceptions import ( + PipetteOverpressureError, + StallOrCollisionDetectedError, +) @pytest.fixture @@ -69,6 +75,11 @@ async def test_blow_out_implementation( labware_id="labware-id", well_name="C6", well_location=location, + current_well=None, + force_direct=False, + minimum_z_height=None, + speed=None, + operation_volume=None, ) ).then_return(Point(x=1, y=2, z=3)) @@ -84,7 +95,10 @@ async def test_blow_out_implementation( well_name="C6", ), new_deck_point=DeckPoint(x=1, y=2, z=3), - ) + ), + pipette_aspirated_fluid=update_types.PipetteEmptyFluidUpdate( + pipette_id="pipette-id" + ), ), ) @@ -133,16 +147,98 @@ async def test_overpressure_error( labware_id="labware-id", well_name="C6", well_location=location, + current_well=None, + force_direct=False, + minimum_z_height=None, + speed=None, + operation_volume=None, ) ).then_return(Point(x=1, y=2, z=3)) result = await subject.execute(data) assert result == DefinedErrorData( - public=OverpressureError.construct( + public=OverpressureError.model_construct( id=error_id, createdAt=error_timestamp, wrappedErrors=[matchers.Anything()], errorInfo={"retryLocation": (1, 2, 3)}, ), + state_update=update_types.StateUpdate( + pipette_location=update_types.PipetteLocationUpdate( + pipette_id="pipette-id", + new_location=update_types.Well( + labware_id="labware-id", + well_name="C6", + ), + new_deck_point=DeckPoint(x=1, y=2, z=3), + ), + pipette_aspirated_fluid=update_types.PipetteUnknownFluidUpdate( + pipette_id="pipette-id" + ), + ), + state_update_if_false_positive=update_types.StateUpdate( + pipette_location=update_types.PipetteLocationUpdate( + pipette_id="pipette-id", + new_location=update_types.Well( + labware_id="labware-id", + well_name="C6", + ), + new_deck_point=DeckPoint(x=1, y=2, z=3), + ), + ), + ) + + +async def test_stall_error( + decoy: Decoy, + pipetting: PipettingHandler, + subject: BlowOutImplementation, + model_utils: ModelUtils, + movement: MovementHandler, +) -> None: + """It should return an overpressure error if the hardware API indicates that.""" + pipette_id = "pipette-id" + labware_id = "labware-id" + well_name = "C6" + error_id = "error-id" + error_timestamp = datetime(year=2020, month=1, day=2) + + location = WellLocation(origin=WellOrigin.BOTTOM, offset=WellOffset(x=0, y=0, z=1)) + + data = BlowOutParams( + pipetteId=pipette_id, + labwareId=labware_id, + wellName=well_name, + wellLocation=location, + flowRate=1.234, + ) + + decoy.when(model_utils.generate_id()).then_return(error_id) + decoy.when(model_utils.get_timestamp()).then_return(error_timestamp) + decoy.when( + await movement.move_to_well( + pipette_id=pipette_id, + labware_id=labware_id, + well_name=well_name, + well_location=location, + current_well=None, + force_direct=False, + minimum_z_height=None, + speed=None, + operation_volume=None, + ) + ).then_raise(StallOrCollisionDetectedError()) + + result = await subject.execute(data) + + assert result == DefinedErrorData( + public=StallOrCollisionError.model_construct( + id=error_id, + createdAt=error_timestamp, + wrappedErrors=[matchers.Anything()], + ), + state_update=update_types.StateUpdate( + pipette_location=update_types.CLEAR, + ), ) diff --git a/api/tests/opentrons/protocol_engine/commands/test_blow_out_in_place.py b/api/tests/opentrons/protocol_engine/commands/test_blow_out_in_place.py index 49eced0670b..50bee696c5a 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_blow_out_in_place.py +++ b/api/tests/opentrons/protocol_engine/commands/test_blow_out_in_place.py @@ -1,11 +1,14 @@ """Test blow-out-in-place commands.""" from datetime import datetime + +import pytest from decoy import Decoy, matchers from opentrons.protocol_engine.commands.pipetting_common import OverpressureError from opentrons.protocol_engine.execution.gantry_mover import GantryMover from opentrons.protocol_engine.resources.model_utils import ModelUtils from opentrons.protocol_engine.state.state import StateView +from opentrons.protocol_engine.state import update_types from opentrons.protocol_engine.commands.blow_out_in_place import ( BlowOutInPlaceParams, BlowOutInPlaceResult, @@ -18,7 +21,6 @@ from opentrons.hardware_control import HardwareControlAPI from opentrons.types import Point from opentrons_shared_data.errors.exceptions import PipetteOverpressureError -import pytest @pytest.fixture @@ -41,6 +43,7 @@ def subject( async def test_blow_out_in_place_implementation( decoy: Decoy, + gantry_mover: GantryMover, subject: BlowOutInPlaceImplementation, pipetting: PipettingHandler, ) -> None: @@ -49,10 +52,19 @@ async def test_blow_out_in_place_implementation( pipetteId="pipette-id", flowRate=1.234, ) + decoy.when(await gantry_mover.get_position("pipette-id")).then_return( + Point(1, 2, 3) + ) result = await subject.execute(data) - - assert result == SuccessData(public=BlowOutInPlaceResult()) + assert result == SuccessData( + public=BlowOutInPlaceResult(), + state_update=update_types.StateUpdate( + pipette_aspirated_fluid=update_types.PipetteEmptyFluidUpdate( + pipette_id="pipette-id" + ) + ), + ) decoy.verify( await pipetting.blow_out_in_place(pipette_id="pipette-id", flow_rate=1.234) @@ -94,10 +106,15 @@ async def test_overpressure_error( result = await subject.execute(data) assert result == DefinedErrorData( - public=OverpressureError.construct( + public=OverpressureError.model_construct( id=error_id, createdAt=error_timestamp, wrappedErrors=[matchers.Anything()], errorInfo={"retryLocation": (position.x, position.y, position.z)}, ), + state_update=update_types.StateUpdate( + pipette_aspirated_fluid=update_types.PipetteUnknownFluidUpdate( + pipette_id="pipette-id" + ) + ), ) diff --git a/api/tests/opentrons/protocol_engine/commands/test_configure_for_volume.py b/api/tests/opentrons/protocol_engine/commands/test_configure_for_volume.py index d237c9e6090..2d8685109ed 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_configure_for_volume.py +++ b/api/tests/opentrons/protocol_engine/commands/test_configure_for_volume.py @@ -22,10 +22,17 @@ ConfigureForVolumeImplementation, ) from opentrons_shared_data.pipette.types import PipetteNameType +from opentrons_shared_data.pipette.pipette_definition import AvailableSensorDefinition from ..pipette_fixtures import get_default_nozzle_map from opentrons.types import Point +@pytest.fixture +def available_sensors() -> AvailableSensorDefinition: + """Provide a list of sensors.""" + return AvailableSensorDefinition(sensors=["pressure", "capacitive", "environment"]) + + @pytest.mark.parametrize( "data", [ @@ -41,7 +48,10 @@ ], ) async def test_configure_for_volume_implementation( - decoy: Decoy, equipment: EquipmentHandler, data: ConfigureForVolumeParams + decoy: Decoy, + equipment: EquipmentHandler, + data: ConfigureForVolumeParams, + available_sensors: AvailableSensorDefinition, ) -> None: """A ConfigureForVolume command should have an execution implementation.""" subject = ConfigureForVolumeImplementation(equipment=equipment) @@ -63,6 +73,14 @@ async def test_configure_for_volume_implementation( back_left_corner_offset=Point(10, 20, 30), front_right_corner_offset=Point(40, 50, 60), pipette_lld_settings={}, + plunger_positions={ + "top": 0.0, + "bottom": 5.0, + "blow_out": 19.0, + "drop_tip": 20.0, + }, + shaft_ul_per_mm=5.0, + available_sensors=available_sensors, ) decoy.when( diff --git a/api/tests/opentrons/protocol_engine/commands/test_dispense.py b/api/tests/opentrons/protocol_engine/commands/test_dispense.py index a996e6915e8..5b60b61d4df 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_dispense.py +++ b/api/tests/opentrons/protocol_engine/commands/test_dispense.py @@ -1,10 +1,14 @@ """Test dispense commands.""" + from datetime import datetime import pytest from decoy import Decoy, matchers -from opentrons_shared_data.errors.exceptions import PipetteOverpressureError +from opentrons_shared_data.errors.exceptions import ( + PipetteOverpressureError, + StallOrCollisionDetectedError, +) from opentrons.protocol_engine import ( LiquidHandlingWellLocation, @@ -25,6 +29,7 @@ ) from opentrons.protocol_engine.resources.model_utils import ModelUtils from opentrons.protocol_engine.commands.pipetting_common import OverpressureError +from opentrons.protocol_engine.commands.movement_common import StallOrCollisionError @pytest.fixture @@ -48,6 +53,7 @@ async def test_dispense_implementation( movement: MovementHandler, pipetting: PipettingHandler, subject: DispenseImplementation, + state_view: StateView, ) -> None: """It should move to the target location and then dispense.""" well_location = LiquidHandlingWellLocation( @@ -69,14 +75,38 @@ async def test_dispense_implementation( labware_id="labware-id-abc123", well_name="A3", well_location=well_location, + current_well=None, + force_direct=False, + minimum_z_height=None, + speed=None, + operation_volume=None, ) ).then_return(Point(x=1, y=2, z=3)) + decoy.when( + state_view.geometry.get_nozzles_per_well( + labware_id="labware-id-abc123", + target_well_name="A3", + pipette_id="pipette-id-abc123", + ) + ).then_return(2) + + decoy.when( + state_view.geometry.get_wells_covered_by_pipette_with_active_well( + "labware-id-abc123", "A3", "pipette-id-abc123" + ) + ).then_return(["A3", "A4"]) + decoy.when( await pipetting.dispense_in_place( pipette_id="pipette-id-abc123", volume=50, flow_rate=1.23, push_out=None ) ).then_return(42) + decoy.when( + state_view.pipettes.get_liquid_dispensed_by_ejecting_volume( + pipette_id="pipette-id-abc123", volume=42 + ) + ).then_return(34) result = await subject.execute(data) @@ -89,12 +119,15 @@ async def test_dispense_implementation( labware_id="labware-id-abc123", well_name="A3", ), - new_deck_point=DeckPoint.construct(x=1, y=2, z=3), + new_deck_point=DeckPoint.model_construct(x=1, y=2, z=3), ), liquid_operated=update_types.LiquidOperatedUpdate( labware_id="labware-id-abc123", - well_name="A3", - volume_added=42, + well_names=["A3", "A4"], + volume_added=68, + ), + pipette_aspirated_fluid=update_types.PipetteEjectedFluidUpdate( + pipette_id="pipette-id-abc123", volume=42 ), ), ) @@ -106,6 +139,7 @@ async def test_overpressure_error( pipetting: PipettingHandler, subject: DispenseImplementation, model_utils: ModelUtils, + state_view: StateView, ) -> None: """It should return an overpressure error if the hardware API indicates that.""" pipette_id = "pipette-id" @@ -129,12 +163,31 @@ async def test_overpressure_error( flowRate=1.23, ) + decoy.when( + state_view.geometry.get_nozzles_per_well( + labware_id=labware_id, + target_well_name=well_name, + pipette_id=pipette_id, + ) + ).then_return(2) + + decoy.when( + state_view.geometry.get_wells_covered_by_pipette_with_active_well( + labware_id, well_name, pipette_id + ) + ).then_return(["A3", "A4"]) + decoy.when( await movement.move_to_well( pipette_id=pipette_id, labware_id=labware_id, well_name=well_name, well_location=well_location, + current_well=None, + force_direct=False, + minimum_z_height=None, + speed=None, + operation_volume=None, ), ).then_return(position) @@ -150,7 +203,7 @@ async def test_overpressure_error( result = await subject.execute(data) assert result == DefinedErrorData( - public=OverpressureError.construct( + public=OverpressureError.model_construct( id=error_id, createdAt=error_timestamp, wrappedErrors=[matchers.Anything()], @@ -163,12 +216,82 @@ async def test_overpressure_error( labware_id="labware-id", well_name="well-name", ), - new_deck_point=DeckPoint.construct(x=1, y=2, z=3), + new_deck_point=DeckPoint.model_construct(x=1, y=2, z=3), ), liquid_operated=update_types.LiquidOperatedUpdate( labware_id="labware-id", - well_name="well-name", + well_names=["A3", "A4"], volume_added=update_types.CLEAR, ), + pipette_aspirated_fluid=update_types.PipetteUnknownFluidUpdate( + pipette_id="pipette-id" + ), + ), + state_update_if_false_positive=update_types.StateUpdate( + pipette_location=update_types.PipetteLocationUpdate( + pipette_id="pipette-id", + new_location=update_types.Well( + labware_id="labware-id", + well_name="well-name", + ), + new_deck_point=DeckPoint.model_construct(x=1, y=2, z=3), + ), + ), + ) + + +async def test_stall_error( + decoy: Decoy, + movement: MovementHandler, + pipetting: PipettingHandler, + subject: DispenseImplementation, + model_utils: ModelUtils, + state_view: StateView, +) -> None: + """It should return a stall error if the hardware API indicates that.""" + pipette_id = "pipette-id" + labware_id = "labware-id" + well_name = "well-name" + well_location = LiquidHandlingWellLocation( + origin=WellOrigin.BOTTOM, offset=WellOffset(x=0, y=0, z=1) + ) + + error_id = "error-id" + error_timestamp = datetime(year=2020, month=1, day=2) + + data = DispenseParams( + pipetteId=pipette_id, + labwareId=labware_id, + wellName=well_name, + wellLocation=well_location, + volume=50, + flowRate=1.23, + ) + + decoy.when( + await movement.move_to_well( + pipette_id=pipette_id, + labware_id=labware_id, + well_name=well_name, + well_location=well_location, + current_well=None, + force_direct=False, + minimum_z_height=None, + speed=None, + operation_volume=None, + ), + ).then_raise(StallOrCollisionDetectedError()) + + decoy.when(model_utils.generate_id()).then_return(error_id) + decoy.when(model_utils.get_timestamp()).then_return(error_timestamp) + + result = await subject.execute(data) + + assert result == DefinedErrorData( + public=StallOrCollisionError.model_construct( + id=error_id, + createdAt=error_timestamp, + wrappedErrors=[matchers.Anything()], ), + state_update=update_types.StateUpdate(pipette_location=update_types.CLEAR), ) diff --git a/api/tests/opentrons/protocol_engine/commands/test_dispense_in_place.py b/api/tests/opentrons/protocol_engine/commands/test_dispense_in_place.py index 5e432bef80a..e9c715223de 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_dispense_in_place.py +++ b/api/tests/opentrons/protocol_engine/commands/test_dispense_in_place.py @@ -1,4 +1,5 @@ """Test dispense-in-place commands.""" + from datetime import datetime import pytest @@ -17,7 +18,7 @@ ) from opentrons.protocol_engine.commands.pipetting_common import OverpressureError from opentrons.protocol_engine.resources import ModelUtils -from opentrons.protocol_engine.state.state import StateStore +from opentrons.protocol_engine.state.state import StateView from opentrons.protocol_engine.types import ( CurrentWell, CurrentPipetteLocation, @@ -27,9 +28,19 @@ @pytest.fixture -def state_store(decoy: Decoy) -> StateStore: - """Get a mock in the shape of a StateStore.""" - return decoy.mock(cls=StateStore) +def subject( + pipetting: PipettingHandler, + state_view: StateView, + gantry_mover: GantryMover, + model_utils: ModelUtils, +) -> DispenseInPlaceImplementation: + """Build a command implementation.""" + return DispenseInPlaceImplementation( + pipetting=pipetting, + state_view=state_view, + gantry_mover=gantry_mover, + model_utils=model_utils, + ) @pytest.mark.parametrize( @@ -50,22 +61,15 @@ def state_store(decoy: Decoy) -> StateStore: ) async def test_dispense_in_place_implementation( decoy: Decoy, - pipetting: PipettingHandler, - state_store: StateStore, gantry_mover: GantryMover, - model_utils: ModelUtils, + pipetting: PipettingHandler, + state_view: StateView, + subject: DispenseInPlaceImplementation, location: CurrentPipetteLocation | None, stateupdateLabware: str, stateupdateWell: str, ) -> None: """It should dispense in place.""" - subject = DispenseInPlaceImplementation( - pipetting=pipetting, - state_view=state_store, - gantry_mover=gantry_mover, - model_utils=model_utils, - ) - data = DispenseInPlaceParams( pipetteId="pipette-id-abc", volume=123, @@ -78,7 +82,29 @@ async def test_dispense_in_place_implementation( ) ).then_return(42) - decoy.when(state_store.pipettes.get_current_location()).then_return(location) + decoy.when(state_view.pipettes.get_current_location()).then_return(location) + decoy.when( + state_view.pipettes.get_liquid_dispensed_by_ejecting_volume( + pipette_id="pipette-id-abc", volume=42 + ) + ).then_return(34) + + decoy.when( + state_view.geometry.get_nozzles_per_well( + labware_id=stateupdateLabware, + target_well_name=stateupdateWell, + pipette_id="pipette-id-abc", + ) + ).then_return(2) + + decoy.when( + state_view.geometry.get_wells_covered_by_pipette_with_active_well( + stateupdateLabware, stateupdateWell, "pipette-id-abc" + ) + ).then_return(["A3", "A4"]) + decoy.when(await gantry_mover.get_position("pipette-id-abc")).then_return( + Point(1, 2, 3) + ) result = await subject.execute(data) @@ -86,16 +112,24 @@ async def test_dispense_in_place_implementation( assert result == SuccessData( public=DispenseInPlaceResult(volume=42), state_update=update_types.StateUpdate( + pipette_aspirated_fluid=update_types.PipetteEjectedFluidUpdate( + pipette_id="pipette-id-abc", volume=42 + ), liquid_operated=update_types.LiquidOperatedUpdate( labware_id=stateupdateLabware, - well_name=stateupdateWell, - volume_added=42, - ) + well_names=["A3", "A4"], + volume_added=68, + ), ), ) else: assert result == SuccessData( public=DispenseInPlaceResult(volume=42), + state_update=update_types.StateUpdate( + pipette_aspirated_fluid=update_types.PipetteEjectedFluidUpdate( + pipette_id="pipette-id-abc", volume=42 + ) + ), ) @@ -119,20 +153,14 @@ async def test_overpressure_error( decoy: Decoy, gantry_mover: GantryMover, pipetting: PipettingHandler, - state_store: StateStore, + state_view: StateView, model_utils: ModelUtils, + subject: DispenseInPlaceImplementation, location: CurrentPipetteLocation | None, stateupdateLabware: str, stateupdateWell: str, ) -> None: """It should return an overpressure error if the hardware API indicates that.""" - subject = DispenseInPlaceImplementation( - pipetting=pipetting, - state_view=state_store, - gantry_mover=gantry_mover, - model_utils=model_utils, - ) - pipette_id = "pipette-id" position = Point(x=1, y=2, z=3) @@ -147,6 +175,20 @@ async def test_overpressure_error( pushOut=10, ) + decoy.when( + state_view.geometry.get_nozzles_per_well( + labware_id=stateupdateLabware, + target_well_name=stateupdateWell, + pipette_id="pipette-id", + ) + ).then_return(2) + + decoy.when( + state_view.geometry.get_wells_covered_by_pipette_with_active_well( + stateupdateLabware, stateupdateWell, "pipette-id" + ) + ).then_return(["A3", "A4"]) + decoy.when( await pipetting.dispense_in_place( pipette_id=pipette_id, @@ -159,13 +201,13 @@ async def test_overpressure_error( decoy.when(model_utils.generate_id()).then_return(error_id) decoy.when(model_utils.get_timestamp()).then_return(error_timestamp) decoy.when(await gantry_mover.get_position(pipette_id)).then_return(position) - decoy.when(state_store.pipettes.get_current_location()).then_return(location) + decoy.when(state_view.pipettes.get_current_location()).then_return(location) result = await subject.execute(data) if isinstance(location, CurrentWell): assert result == DefinedErrorData( - public=OverpressureError.construct( + public=OverpressureError.model_construct( id=error_id, createdAt=error_timestamp, wrappedErrors=[matchers.Anything()], @@ -174,17 +216,25 @@ async def test_overpressure_error( state_update=update_types.StateUpdate( liquid_operated=update_types.LiquidOperatedUpdate( labware_id=stateupdateLabware, - well_name=stateupdateWell, + well_names=["A3", "A4"], volume_added=update_types.CLEAR, - ) + ), + pipette_aspirated_fluid=update_types.PipetteUnknownFluidUpdate( + pipette_id="pipette-id" + ), ), ) else: assert result == DefinedErrorData( - public=OverpressureError.construct( + public=OverpressureError.model_construct( id=error_id, createdAt=error_timestamp, wrappedErrors=[matchers.Anything()], errorInfo={"retryLocation": (position.x, position.y, position.z)}, - ) + ), + state_update=update_types.StateUpdate( + pipette_aspirated_fluid=update_types.PipetteUnknownFluidUpdate( + pipette_id="pipette-id" + ) + ), ) diff --git a/api/tests/opentrons/protocol_engine/commands/test_drop_tip.py b/api/tests/opentrons/protocol_engine/commands/test_drop_tip.py index 64368de1ff2..430fa8dff32 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_drop_tip.py +++ b/api/tests/opentrons/protocol_engine/commands/test_drop_tip.py @@ -1,9 +1,12 @@ """Test drop tip commands.""" + from datetime import datetime import pytest from decoy import Decoy, matchers +from opentrons_shared_data.errors.exceptions import StallOrCollisionDetectedError + from opentrons.protocol_engine import ( DropTipWellLocation, DropTipWellOrigin, @@ -20,12 +23,14 @@ from opentrons.protocol_engine.commands.pipetting_common import ( TipPhysicallyAttachedError, ) +from opentrons.protocol_engine.commands.movement_common import StallOrCollisionError from opentrons.protocol_engine.errors.exceptions import TipAttachedError from opentrons.protocol_engine.resources.model_utils import ModelUtils from opentrons.protocol_engine.state import update_types from opentrons.protocol_engine.state.state import StateView from opentrons.protocol_engine.execution import MovementHandler, TipHandler + from opentrons.types import Point @@ -55,7 +60,7 @@ def mock_model_utils(decoy: Decoy) -> ModelUtils: def test_drop_tip_params_defaults() -> None: """A drop tip should use a `WellOrigin.DROP_TIP` by default.""" - default_params = DropTipParams.parse_obj( + default_params = DropTipParams.model_validate( {"pipetteId": "abc", "labwareId": "def", "wellName": "ghj"} ) @@ -66,7 +71,7 @@ def test_drop_tip_params_defaults() -> None: def test_drop_tip_params_default_origin() -> None: """A drop tip should drop a `WellOrigin.DROP_TIP` by default even if an offset is given.""" - default_params = DropTipParams.parse_obj( + default_params = DropTipParams.model_validate( { "pipetteId": "abc", "labwareId": "def", @@ -122,6 +127,11 @@ async def test_drop_tip_implementation( labware_id="123", well_name="A3", well_location=WellLocation(offset=WellOffset(x=4, y=5, z=6)), + current_well=None, + force_direct=False, + minimum_z_height=None, + speed=None, + operation_volume=None, ) ).then_return(Point(x=111, y=222, z=333)) @@ -141,6 +151,9 @@ async def test_drop_tip_implementation( pipette_tip_state=update_types.PipetteTipStateUpdate( pipette_id="abc", tip_geometry=None ), + pipette_aspirated_fluid=update_types.PipetteUnknownFluidUpdate( + pipette_id="abc" + ), ), ) @@ -200,6 +213,11 @@ async def test_drop_tip_with_alternating_locations( labware_id="123", well_name="A3", well_location=WellLocation(offset=WellOffset(x=4, y=5, z=6)), + current_well=None, + force_direct=False, + minimum_z_height=None, + speed=None, + operation_volume=None, ) ).then_return(Point(x=111, y=222, z=333)) @@ -218,6 +236,9 @@ async def test_drop_tip_with_alternating_locations( pipette_tip_state=update_types.PipetteTipStateUpdate( pipette_id="abc", tip_geometry=None ), + pipette_aspirated_fluid=update_types.PipetteUnknownFluidUpdate( + pipette_id="abc" + ), ), ) @@ -263,6 +284,11 @@ async def test_tip_attached_error( labware_id="123", well_name="A3", well_location=WellLocation(offset=WellOffset(x=4, y=5, z=6)), + current_well=None, + force_direct=False, + minimum_z_height=None, + speed=None, + operation_volume=None, ) ).then_return(Point(x=111, y=222, z=333)) decoy.when( @@ -277,7 +303,7 @@ async def test_tip_attached_error( result = await subject.execute(params) assert result == DefinedErrorData( - public=TipPhysicallyAttachedError.construct( + public=TipPhysicallyAttachedError.model_construct( id="error-id", createdAt=datetime(year=1, month=2, day=3), wrappedErrors=[matchers.Anything()], @@ -292,11 +318,90 @@ async def test_tip_attached_error( ), new_deck_point=DeckPoint(x=111, y=222, z=333), ), + pipette_aspirated_fluid=update_types.PipetteUnknownFluidUpdate( + pipette_id="abc" + ), ), state_update_if_false_positive=update_types.StateUpdate( pipette_tip_state=update_types.PipetteTipStateUpdate( pipette_id="abc", tip_geometry=None, - ) + ), + pipette_location=update_types.PipetteLocationUpdate( + pipette_id="abc", + new_location=update_types.Well( + labware_id="123", + well_name="A3", + ), + new_deck_point=DeckPoint(x=111, y=222, z=333), + ), + ), + ) + + +async def test_stall_error( + decoy: Decoy, + mock_state_view: StateView, + mock_movement_handler: MovementHandler, + mock_tip_handler: TipHandler, + mock_model_utils: ModelUtils, +) -> None: + """A DropTip command should have an execution implementation.""" + subject = DropTipImplementation( + state_view=mock_state_view, + movement=mock_movement_handler, + tip_handler=mock_tip_handler, + model_utils=mock_model_utils, + ) + + params = DropTipParams( + pipetteId="abc", + labwareId="123", + wellName="A3", + wellLocation=DropTipWellLocation(offset=WellOffset(x=1, y=2, z=3)), + ) + + decoy.when( + mock_state_view.pipettes.get_is_partially_configured(pipette_id="abc") + ).then_return(False) + + decoy.when( + mock_state_view.geometry.get_checked_tip_drop_location( + pipette_id="abc", + labware_id="123", + well_location=DropTipWellLocation(offset=WellOffset(x=1, y=2, z=3)), + partially_configured=False, + ) + ).then_return(WellLocation(offset=WellOffset(x=4, y=5, z=6))) + + decoy.when( + await mock_movement_handler.move_to_well( + pipette_id="abc", + labware_id="123", + well_name="A3", + well_location=WellLocation(offset=WellOffset(x=4, y=5, z=6)), + current_well=None, + force_direct=False, + minimum_z_height=None, + speed=None, + operation_volume=None, + ) + ).then_raise(StallOrCollisionDetectedError()) + + decoy.when(mock_model_utils.generate_id()).then_return("error-id") + decoy.when(mock_model_utils.get_timestamp()).then_return( + datetime(year=1, month=2, day=3) + ) + + result = await subject.execute(params) + + assert result == DefinedErrorData( + public=StallOrCollisionError.model_construct( + id="error-id", + createdAt=datetime(year=1, month=2, day=3), + wrappedErrors=[matchers.Anything()], + ), + state_update=update_types.StateUpdate( + pipette_location=update_types.CLEAR, ), ) diff --git a/api/tests/opentrons/protocol_engine/commands/test_drop_tip_in_place.py b/api/tests/opentrons/protocol_engine/commands/test_drop_tip_in_place.py index 807d702e2bc..8c4716cf380 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_drop_tip_in_place.py +++ b/api/tests/opentrons/protocol_engine/commands/test_drop_tip_in_place.py @@ -19,6 +19,7 @@ from opentrons.protocol_engine.state.update_types import ( PipetteTipStateUpdate, StateUpdate, + PipetteUnknownFluidUpdate, ) from opentrons.types import Point @@ -60,7 +61,10 @@ async def test_success( assert result == SuccessData( public=DropTipInPlaceResult(), state_update=StateUpdate( - pipette_tip_state=PipetteTipStateUpdate(pipette_id="abc", tip_geometry=None) + pipette_tip_state=PipetteTipStateUpdate( + pipette_id="abc", tip_geometry=None + ), + pipette_aspirated_fluid=PipetteUnknownFluidUpdate(pipette_id="abc"), ), ) @@ -100,14 +104,18 @@ async def test_tip_attached_error( result = await subject.execute(params) assert result == DefinedErrorData( - public=TipPhysicallyAttachedError.construct( + public=TipPhysicallyAttachedError.model_construct( id="error-id", createdAt=datetime(year=1, month=2, day=3), wrappedErrors=[matchers.Anything()], errorInfo={"retryLocation": (9, 8, 7)}, ), - state_update=StateUpdate(), + state_update=StateUpdate( + pipette_aspirated_fluid=PipetteUnknownFluidUpdate(pipette_id="abc") + ), state_update_if_false_positive=StateUpdate( - pipette_tip_state=PipetteTipStateUpdate(pipette_id="abc", tip_geometry=None) + pipette_tip_state=PipetteTipStateUpdate( + pipette_id="abc", tip_geometry=None + ), ), ) diff --git a/api/tests/opentrons/protocol_engine/commands/test_evotip_dispense.py b/api/tests/opentrons/protocol_engine/commands/test_evotip_dispense.py new file mode 100644 index 00000000000..f922caec41d --- /dev/null +++ b/api/tests/opentrons/protocol_engine/commands/test_evotip_dispense.py @@ -0,0 +1,128 @@ +"""Test evotip dispense in place commands.""" + +import pytest +from decoy import Decoy + +from opentrons.protocol_engine import ( + LiquidHandlingWellLocation, + WellOrigin, + WellOffset, + DeckPoint, +) +from opentrons.types import Point +from opentrons.protocol_engine.execution import ( + PipettingHandler, + GantryMover, + MovementHandler, +) + +from opentrons.protocols.models import LabwareDefinition +from opentrons.protocol_engine.commands.command import SuccessData +from opentrons.protocol_engine.commands.evotip_dispense import ( + EvotipDispenseParams, + EvotipDispenseResult, + EvotipDispenseImplementation, +) +from opentrons.protocol_engine.resources import ModelUtils +from opentrons.protocol_engine.state.state import StateView +from opentrons.protocol_engine.state import update_types + +from opentrons_shared_data.labware import load_definition + + +@pytest.fixture +def evotips_definition() -> LabwareDefinition: + """A fixturee of the evotips definition.""" + # TODO (chb 2025-01-29): When we migrate all labware to v3 we can clean this up + return LabwareDefinition.model_validate( + load_definition("evotips_opentrons_96_labware", 1) + ) + + +@pytest.fixture +def subject( + pipetting: PipettingHandler, + state_view: StateView, + gantry_mover: GantryMover, + model_utils: ModelUtils, + movement: MovementHandler, + **kwargs: object, +) -> EvotipDispenseImplementation: + """Build a command implementation.""" + return EvotipDispenseImplementation( + pipetting=pipetting, + state_view=state_view, + gantry_mover=gantry_mover, + model_utils=model_utils, + movement=movement, + ) + + +async def test_evotip_dispense_implementation( + decoy: Decoy, + movement: MovementHandler, + gantry_mover: GantryMover, + pipetting: PipettingHandler, + state_view: StateView, + subject: EvotipDispenseImplementation, + evotips_definition: LabwareDefinition, +) -> None: + """It should dispense in place.""" + well_location = LiquidHandlingWellLocation( + origin=WellOrigin.TOP, offset=WellOffset(x=0, y=0, z=0) + ) + + data = EvotipDispenseParams( + pipetteId="pipette-id-abc123", + labwareId="labware-id-abc123", + wellName="A3", + volume=100, + flowRate=456, + ) + + decoy.when( + await movement.move_to_well( + pipette_id="pipette-id-abc123", + labware_id="labware-id-abc123", + well_name="A3", + well_location=well_location, + current_well=None, + force_direct=False, + minimum_z_height=None, + speed=None, + operation_volume=None, + ) + ).then_return(Point(x=1, y=2, z=3)) + + decoy.when(state_view.labware.get_definition("labware-id-abc123")).then_return( + evotips_definition + ) + + decoy.when( + await pipetting.dispense_in_place( + pipette_id="pipette-id-abc123", volume=100.0, flow_rate=456.0, push_out=None + ) + ).then_return(100) + + decoy.when(await gantry_mover.get_position("pipette-id-abc123")).then_return( + Point(1, 2, 3) + ) + + result = await subject.execute(data) + + assert result == SuccessData( + public=EvotipDispenseResult(volume=100), + state_update=update_types.StateUpdate( + pipette_location=update_types.PipetteLocationUpdate( + pipette_id="pipette-id-abc123", + new_location=update_types.Well( + labware_id="labware-id-abc123", + well_name="A3", + ), + new_deck_point=DeckPoint.model_construct(x=1, y=2, z=3), + ), + pipette_aspirated_fluid=update_types.PipetteEjectedFluidUpdate( + pipette_id="pipette-id-abc123", volume=100 + ), + ), + ) diff --git a/api/tests/opentrons/protocol_engine/commands/test_evotip_seal_pipette.py b/api/tests/opentrons/protocol_engine/commands/test_evotip_seal_pipette.py new file mode 100644 index 00000000000..590156e6513 --- /dev/null +++ b/api/tests/opentrons/protocol_engine/commands/test_evotip_seal_pipette.py @@ -0,0 +1,294 @@ +"""Test evotip seal commands.""" + +import pytest +from datetime import datetime + +from decoy import Decoy, matchers +from unittest.mock import sentinel + +from opentrons.protocols.models import LabwareDefinition + +from opentrons_shared_data.errors.exceptions import StallOrCollisionDetectedError + +from opentrons.types import MountType, Point + +from opentrons.protocol_engine import ( + WellLocation, + PickUpTipWellLocation, + WellOffset, + DeckPoint, +) +from opentrons.protocol_engine.errors import PickUpTipTipNotAttachedError +from opentrons.protocol_engine.execution import MovementHandler, GantryMover, TipHandler +from opentrons.protocol_engine.resources import ModelUtils +from opentrons.protocol_engine.state import update_types +from opentrons.protocol_engine.state.state import StateView +from opentrons.protocol_engine.types import TipGeometry, FluidKind, AspiratedFluid + +from opentrons.protocol_engine.commands.movement_common import StallOrCollisionError +from opentrons.protocol_engine.commands.command import DefinedErrorData, SuccessData +from opentrons.protocol_engine.commands.evotip_seal_pipette import ( + EvotipSealPipetteParams, + EvotipSealPipetteResult, + EvotipSealPipetteImplementation, +) +from opentrons.protocol_engine.execution import ( + PipettingHandler, +) +from opentrons.hardware_control import HardwareControlAPI + +from opentrons_shared_data.labware import load_definition + + +@pytest.fixture +def evotips_definition() -> LabwareDefinition: + """A fixturee of the evotips definition.""" + # TODO (chb 2025-01-29): When we migrate all labware to v3 we can clean this up + return LabwareDefinition.model_validate( + load_definition("evotips_opentrons_96_labware", 1) + ) + + +async def test_success( + decoy: Decoy, + state_view: StateView, + movement: MovementHandler, + tip_handler: TipHandler, + model_utils: ModelUtils, + gantry_mover: GantryMover, + evotips_definition: LabwareDefinition, + hardware_api: HardwareControlAPI, + pipetting: PipettingHandler, +) -> None: + """A PickUpTip command should have an execution implementation.""" + subject = EvotipSealPipetteImplementation( + state_view=state_view, + movement=movement, + tip_handler=tip_handler, + model_utils=model_utils, + gantry_mover=gantry_mover, + hardware_api=hardware_api, + pipetting=pipetting, + ) + + decoy.when(state_view.pipettes.get_mount("pipette-id")).then_return(MountType.LEFT) + + decoy.when( + state_view.geometry.convert_pick_up_tip_well_location( + well_location=PickUpTipWellLocation(offset=WellOffset(x=1, y=2, z=3)) + ) + ).then_return(WellLocation(offset=WellOffset(x=1, y=2, z=3))) + + decoy.when(state_view.labware.get_definition("labware-id")).then_return( + evotips_definition + ) + decoy.when( + await movement.move_to_well( + pipette_id="pipette-id", + labware_id="labware-id", + well_name="A3", + well_location=WellLocation(offset=WellOffset(x=1, y=2, z=3)), + current_well=None, + force_direct=False, + minimum_z_height=None, + speed=None, + operation_volume=None, + ) + ).then_return(Point(x=111, y=222, z=333)) + decoy.when( + state_view.geometry.get_nominal_tip_geometry("pipette-id", "labware-id", "A3") + ).then_return(TipGeometry(length=42, diameter=5, volume=300)) + + decoy.when( + await tip_handler.pick_up_tip( + pipette_id="pipette-id", + labware_id="labware-id", + well_name="A3", + ) + ).then_return(TipGeometry(length=42, diameter=5, volume=300)) + + result = await subject.execute( + EvotipSealPipetteParams( + pipetteId="pipette-id", + labwareId="labware-id", + wellName="A3", + wellLocation=PickUpTipWellLocation(offset=WellOffset(x=1, y=2, z=3)), + ) + ) + + assert result == SuccessData( + public=EvotipSealPipetteResult( + tipLength=42, + tipVolume=300, + tipDiameter=5, + position=DeckPoint(x=111, y=222, z=333), + ), + state_update=update_types.StateUpdate( + pipette_tip_state=update_types.PipetteTipStateUpdate( + pipette_id="pipette-id", + tip_geometry=TipGeometry(length=42, diameter=5, volume=300), + ), + pipette_aspirated_fluid=update_types.PipetteAspiratedFluidUpdate( + pipette_id="pipette-id", + fluid=AspiratedFluid(kind=FluidKind.LIQUID, volume=400), + ), + ), + ) + + +async def test_no_tip_physically_missing_error( + decoy: Decoy, + state_view: StateView, + movement: MovementHandler, + tip_handler: TipHandler, + model_utils: ModelUtils, + gantry_mover: GantryMover, + hardware_api: HardwareControlAPI, + pipetting: PipettingHandler, + evotips_definition: LabwareDefinition, +) -> None: + """It should not return a TipPhysicallyMissingError even though evotips do not sit high enough on the pipette to be detected by the tip sensor.""" + subject = EvotipSealPipetteImplementation( + state_view=state_view, + movement=movement, + tip_handler=tip_handler, + model_utils=model_utils, + gantry_mover=gantry_mover, + hardware_api=hardware_api, + pipetting=pipetting, + ) + + pipette_id = "pipette-id" + labware_id = "labware-id" + well_name = "well-name" + error_id = "error-id" + error_created_at = datetime(1234, 5, 6) + + decoy.when( + state_view.geometry.convert_pick_up_tip_well_location( + well_location=PickUpTipWellLocation(offset=WellOffset()) + ) + ).then_return(WellLocation(offset=WellOffset())) + + decoy.when( + await movement.move_to_well( + pipette_id="pipette-id", + labware_id="labware-id", + well_name="well-name", + well_location=WellLocation(offset=WellOffset()), + current_well=None, + force_direct=False, + minimum_z_height=None, + speed=None, + operation_volume=None, + ) + ).then_return(Point(x=111, y=222, z=333)) + decoy.when( + state_view.geometry.get_nominal_tip_geometry(pipette_id, labware_id, well_name) + ).then_return(TipGeometry(length=42, diameter=5, volume=300)) + + decoy.when( + await tip_handler.pick_up_tip( + pipette_id=pipette_id, labware_id=labware_id, well_name=well_name + ) + ).then_raise(PickUpTipTipNotAttachedError(tip_geometry=sentinel.tip_geometry)) + decoy.when(model_utils.generate_id()).then_return(error_id) + decoy.when(model_utils.get_timestamp()).then_return(error_created_at) + decoy.when(state_view.labware.get_definition(labware_id)).then_return( + evotips_definition + ) + + result = await subject.execute( + EvotipSealPipetteParams( + pipetteId=pipette_id, labwareId=labware_id, wellName=well_name + ) + ) + + assert result == SuccessData( + public=EvotipSealPipetteResult( + tipLength=42, + tipVolume=300, + tipDiameter=5, + position=DeckPoint(x=111, y=222, z=333), + ), + state_update=update_types.StateUpdate( + pipette_tip_state=update_types.PipetteTipStateUpdate( + pipette_id="pipette-id", + tip_geometry=TipGeometry(length=42, diameter=5, volume=300), + ), + pipette_aspirated_fluid=update_types.PipetteAspiratedFluidUpdate( + pipette_id="pipette-id", + fluid=AspiratedFluid(kind=FluidKind.LIQUID, volume=400), + ), + ), + ) + + +async def test_stall_error( + decoy: Decoy, + state_view: StateView, + movement: MovementHandler, + tip_handler: TipHandler, + model_utils: ModelUtils, + gantry_mover: GantryMover, + hardware_api: HardwareControlAPI, + pipetting: PipettingHandler, + evotips_definition: LabwareDefinition, +) -> None: + """It should return a TipPhysicallyMissingError if the HW API indicates that.""" + subject = EvotipSealPipetteImplementation( + state_view=state_view, + movement=movement, + tip_handler=tip_handler, + model_utils=model_utils, + gantry_mover=gantry_mover, + hardware_api=hardware_api, + pipetting=pipetting, + ) + + pipette_id = "pipette-id" + labware_id = "labware-id" + well_name = "well-name" + error_id = "error-id" + error_created_at = datetime(1234, 5, 6) + + decoy.when( + state_view.geometry.convert_pick_up_tip_well_location( + well_location=PickUpTipWellLocation(offset=WellOffset()) + ) + ).then_return(WellLocation(offset=WellOffset())) + + decoy.when( + await movement.move_to_well( + pipette_id="pipette-id", + labware_id="labware-id", + well_name="well-name", + well_location=WellLocation(offset=WellOffset()), + current_well=None, + force_direct=False, + minimum_z_height=None, + speed=None, + operation_volume=None, + ) + ).then_raise(StallOrCollisionDetectedError()) + + decoy.when(model_utils.generate_id()).then_return(error_id) + decoy.when(model_utils.get_timestamp()).then_return(error_created_at) + decoy.when(state_view.labware.get_definition(labware_id)).then_return( + evotips_definition + ) + + result = await subject.execute( + EvotipSealPipetteParams( + pipetteId=pipette_id, labwareId=labware_id, wellName=well_name + ) + ) + + assert result == DefinedErrorData( + public=StallOrCollisionError.model_construct( + id=error_id, createdAt=error_created_at, wrappedErrors=[matchers.Anything()] + ), + state_update=update_types.StateUpdate( + pipette_location=update_types.CLEAR, + ), + ) diff --git a/api/tests/opentrons/protocol_engine/commands/test_evotip_unseal_pipette.py b/api/tests/opentrons/protocol_engine/commands/test_evotip_unseal_pipette.py new file mode 100644 index 00000000000..e1b0f24596c --- /dev/null +++ b/api/tests/opentrons/protocol_engine/commands/test_evotip_unseal_pipette.py @@ -0,0 +1,326 @@ +"""Test evotip unseal commands.""" + +from datetime import datetime + +import pytest +from decoy import Decoy, matchers + +from opentrons_shared_data.errors.exceptions import StallOrCollisionDetectedError + +from opentrons.protocol_engine import ( + DropTipWellLocation, + DropTipWellOrigin, + WellLocation, + WellOffset, + DeckPoint, +) +from opentrons.protocol_engine.commands.command import DefinedErrorData, SuccessData +from opentrons.protocol_engine.commands.evotip_unseal_pipette import ( + EvotipUnsealPipetteParams, + EvotipUnsealPipetteResult, + EvotipUnsealPipetteImplementation, +) +from opentrons.protocol_engine.commands.movement_common import StallOrCollisionError +from opentrons.protocol_engine.errors.exceptions import TipAttachedError +from opentrons.protocol_engine.resources.model_utils import ModelUtils +from opentrons.protocol_engine.state import update_types +from opentrons.protocol_engine.state.state import StateView +from opentrons.protocol_engine.execution import MovementHandler, GantryMover, TipHandler +from opentrons.protocols.models import LabwareDefinition + +from opentrons_shared_data.labware import load_definition + +from opentrons.types import Point + + +@pytest.fixture +def mock_state_view(decoy: Decoy) -> StateView: + """Get a mock StateView.""" + return decoy.mock(cls=StateView) + + +@pytest.fixture +def mock_movement_handler(decoy: Decoy) -> MovementHandler: + """Get a mock MovementHandler.""" + return decoy.mock(cls=MovementHandler) + + +@pytest.fixture +def mock_tip_handler(decoy: Decoy) -> TipHandler: + """Get a mock TipHandler.""" + return decoy.mock(cls=TipHandler) + + +@pytest.fixture +def mock_model_utils(decoy: Decoy) -> ModelUtils: + """Get a mock ModelUtils.""" + return decoy.mock(cls=ModelUtils) + + +def test_drop_tip_params_defaults() -> None: + """A drop tip should use a `WellOrigin.DROP_TIP` by default.""" + default_params = EvotipUnsealPipetteParams.model_validate( + {"pipetteId": "abc", "labwareId": "def", "wellName": "ghj"} + ) + + assert default_params.wellLocation == DropTipWellLocation( + origin=DropTipWellOrigin.DEFAULT, offset=WellOffset(x=0, y=0, z=0) + ) + + +def test_drop_tip_params_default_origin() -> None: + """A drop tip should drop a `WellOrigin.DROP_TIP` by default even if an offset is given.""" + default_params = EvotipUnsealPipetteParams.model_validate( + { + "pipetteId": "abc", + "labwareId": "def", + "wellName": "ghj", + "wellLocation": {"offset": {"x": 1, "y": 2, "z": 3}}, + } + ) + + assert default_params.wellLocation == DropTipWellLocation( + origin=DropTipWellOrigin.DEFAULT, offset=WellOffset(x=1, y=2, z=3) + ) + + +@pytest.fixture +def evotips_definition() -> LabwareDefinition: + """A fixturee of the evotips definition.""" + # TODO (chb 2025-01-29): When we migrate all labware to v3 we can clean this up + return LabwareDefinition.model_validate( + load_definition("evotips_opentrons_96_labware", 1) + ) + + +async def test_drop_tip_implementation( + decoy: Decoy, + mock_state_view: StateView, + mock_movement_handler: MovementHandler, + mock_tip_handler: TipHandler, + mock_model_utils: ModelUtils, + gantry_mover: GantryMover, + evotips_definition: LabwareDefinition, +) -> None: + """A DropTip command should have an execution implementation.""" + subject = EvotipUnsealPipetteImplementation( + state_view=mock_state_view, + movement=mock_movement_handler, + tip_handler=mock_tip_handler, + model_utils=mock_model_utils, + gantry_mover=gantry_mover, + ) + + params = EvotipUnsealPipetteParams( + pipetteId="abc", + labwareId="123", + wellName="A3", + wellLocation=DropTipWellLocation(offset=WellOffset(x=1, y=2, z=3)), + ) + decoy.when(mock_state_view.labware.get_definition("123")).then_return( + evotips_definition + ) + + decoy.when( + mock_state_view.pipettes.get_is_partially_configured(pipette_id="abc") + ).then_return(False) + + decoy.when( + mock_state_view.geometry.get_checked_tip_drop_location( + pipette_id="abc", + labware_id="123", + well_location=DropTipWellLocation(offset=WellOffset(x=1, y=2, z=3)), + partially_configured=False, + ) + ).then_return(WellLocation(offset=WellOffset(x=4, y=5, z=6))) + + decoy.when( + await mock_movement_handler.move_to_well( + pipette_id="abc", + labware_id="123", + well_name="A3", + well_location=WellLocation(offset=WellOffset(x=4, y=5, z=6)), + current_well=None, + force_direct=False, + minimum_z_height=None, + speed=None, + operation_volume=None, + ) + ).then_return(Point(x=111, y=222, z=333)) + + result = await subject.execute(params) + + assert result == SuccessData( + public=EvotipUnsealPipetteResult(position=DeckPoint(x=111, y=222, z=333)), + state_update=update_types.StateUpdate( + pipette_location=update_types.PipetteLocationUpdate( + pipette_id="abc", + new_location=update_types.Well( + labware_id="123", + well_name="A3", + ), + new_deck_point=DeckPoint(x=111, y=222, z=333), + ), + pipette_tip_state=update_types.PipetteTipStateUpdate( + pipette_id="abc", tip_geometry=None + ), + pipette_aspirated_fluid=update_types.PipetteUnknownFluidUpdate( + pipette_id="abc" + ), + ), + ) + decoy.verify( + await mock_tip_handler.drop_tip( + pipette_id="abc", + home_after=None, + do_not_ignore_tip_presence=False, + ignore_plunger=True, + ), + times=1, + ) + + +async def test_tip_attached_error( + decoy: Decoy, + mock_state_view: StateView, + mock_movement_handler: MovementHandler, + mock_tip_handler: TipHandler, + mock_model_utils: ModelUtils, + gantry_mover: GantryMover, + evotips_definition: LabwareDefinition, +) -> None: + """A Evotip Unseal command should have an execution implementation.""" + subject = EvotipUnsealPipetteImplementation( + state_view=mock_state_view, + movement=mock_movement_handler, + tip_handler=mock_tip_handler, + model_utils=mock_model_utils, + gantry_mover=gantry_mover, + ) + + params = EvotipUnsealPipetteParams( + pipetteId="abc", + labwareId="123", + wellName="A3", + wellLocation=DropTipWellLocation(offset=WellOffset(x=1, y=2, z=3)), + ) + decoy.when(mock_state_view.labware.get_definition("123")).then_return( + evotips_definition + ) + + decoy.when( + mock_state_view.pipettes.get_is_partially_configured(pipette_id="abc") + ).then_return(False) + + decoy.when( + mock_state_view.geometry.get_checked_tip_drop_location( + pipette_id="abc", + labware_id="123", + well_location=DropTipWellLocation(offset=WellOffset(x=1, y=2, z=3)), + partially_configured=False, + ) + ).then_return(WellLocation(offset=WellOffset(x=4, y=5, z=6))) + + decoy.when( + await mock_movement_handler.move_to_well( + pipette_id="abc", + labware_id="123", + well_name="A3", + well_location=WellLocation(offset=WellOffset(x=4, y=5, z=6)), + current_well=None, + force_direct=False, + minimum_z_height=None, + speed=None, + operation_volume=None, + ) + ).then_return(Point(x=111, y=222, z=333)) + decoy.when( + await mock_tip_handler.drop_tip( + pipette_id="abc", + home_after=None, + do_not_ignore_tip_presence=False, + ignore_plunger=True, + ) + ).then_raise(TipAttachedError("Egads!")) + + decoy.when(mock_model_utils.generate_id()).then_return("error-id") + decoy.when(mock_model_utils.get_timestamp()).then_return( + datetime(year=1, month=2, day=3) + ) + + with pytest.raises(TipAttachedError): + await subject.execute(params) + + +async def test_stall_error( + decoy: Decoy, + mock_state_view: StateView, + mock_movement_handler: MovementHandler, + mock_tip_handler: TipHandler, + mock_model_utils: ModelUtils, + gantry_mover: GantryMover, + evotips_definition: LabwareDefinition, +) -> None: + """A DropTip command should have an execution implementation.""" + subject = EvotipUnsealPipetteImplementation( + state_view=mock_state_view, + movement=mock_movement_handler, + tip_handler=mock_tip_handler, + model_utils=mock_model_utils, + gantry_mover=gantry_mover, + ) + + params = EvotipUnsealPipetteParams( + pipetteId="abc", + labwareId="123", + wellName="A3", + wellLocation=DropTipWellLocation(offset=WellOffset(x=1, y=2, z=3)), + ) + decoy.when(mock_state_view.labware.get_definition("123")).then_return( + evotips_definition + ) + + decoy.when( + mock_state_view.pipettes.get_is_partially_configured(pipette_id="abc") + ).then_return(False) + + decoy.when( + mock_state_view.geometry.get_checked_tip_drop_location( + pipette_id="abc", + labware_id="123", + well_location=DropTipWellLocation(offset=WellOffset(x=1, y=2, z=3)), + partially_configured=False, + ) + ).then_return(WellLocation(offset=WellOffset(x=4, y=5, z=6))) + + decoy.when( + await mock_movement_handler.move_to_well( + pipette_id="abc", + labware_id="123", + well_name="A3", + well_location=WellLocation(offset=WellOffset(x=4, y=5, z=6)), + current_well=None, + force_direct=False, + minimum_z_height=None, + speed=None, + operation_volume=None, + ) + ).then_raise(StallOrCollisionDetectedError()) + + decoy.when(mock_model_utils.generate_id()).then_return("error-id") + decoy.when(mock_model_utils.get_timestamp()).then_return( + datetime(year=1, month=2, day=3) + ) + + result = await subject.execute(params) + + assert result == DefinedErrorData( + public=StallOrCollisionError.model_construct( + id="error-id", + createdAt=datetime(year=1, month=2, day=3), + wrappedErrors=[matchers.Anything()], + ), + state_update=update_types.StateUpdate( + pipette_location=update_types.CLEAR, + ), + ) diff --git a/api/tests/opentrons/protocol_engine/commands/test_get_next_tip.py b/api/tests/opentrons/protocol_engine/commands/test_get_next_tip.py new file mode 100644 index 00000000000..4221cae864d --- /dev/null +++ b/api/tests/opentrons/protocol_engine/commands/test_get_next_tip.py @@ -0,0 +1,142 @@ +"""Test get next tip in place commands.""" +from decoy import Decoy + +from opentrons.types import NozzleConfigurationType +from opentrons.protocol_engine import StateView +from opentrons.protocol_engine.types import NextTipInfo, NoTipAvailable, NoTipReason +from opentrons.protocol_engine.commands.command import SuccessData +from opentrons.protocol_engine.commands.get_next_tip import ( + GetNextTipParams, + GetNextTipResult, + GetNextTipImplementation, +) + +from opentrons.hardware_control.nozzle_manager import NozzleMap + + +async def test_get_next_tip_implementation( + decoy: Decoy, + state_view: StateView, +) -> None: + """A GetNextTip command should have an execution implementation.""" + subject = GetNextTipImplementation(state_view=state_view) + params = GetNextTipParams( + pipetteId="abc", labwareIds=["123"], startingTipWell="xyz" + ) + mock_nozzle_map = decoy.mock(cls=NozzleMap) + + decoy.when(state_view.tips.get_pipette_active_channels("abc")).then_return(42) + decoy.when(state_view.tips.get_pipette_nozzle_map("abc")).then_return( + mock_nozzle_map + ) + decoy.when(mock_nozzle_map.configuration).then_return(NozzleConfigurationType.FULL) + + decoy.when( + state_view.tips.get_next_tip( + labware_id="123", + num_tips=42, + starting_tip_name="xyz", + nozzle_map=mock_nozzle_map, + ) + ).then_return("foo") + + result = await subject.execute(params) + + assert result == SuccessData( + public=GetNextTipResult( + nextTipInfo=NextTipInfo(labwareId="123", tipStartingWell="foo") + ), + ) + + +async def test_get_next_tip_implementation_multiple_tip_racks( + decoy: Decoy, + state_view: StateView, +) -> None: + """A GetNextTip command with multiple tip racks should not apply starting tip to the following ones.""" + subject = GetNextTipImplementation(state_view=state_view) + params = GetNextTipParams( + pipetteId="abc", labwareIds=["123", "456"], startingTipWell="xyz" + ) + mock_nozzle_map = decoy.mock(cls=NozzleMap) + + decoy.when(state_view.tips.get_pipette_active_channels("abc")).then_return(42) + decoy.when(state_view.tips.get_pipette_nozzle_map("abc")).then_return( + mock_nozzle_map + ) + decoy.when(mock_nozzle_map.configuration).then_return(NozzleConfigurationType.FULL) + + decoy.when( + state_view.tips.get_next_tip( + labware_id="456", + num_tips=42, + starting_tip_name=None, + nozzle_map=mock_nozzle_map, + ) + ).then_return("foo") + + result = await subject.execute(params) + + assert result == SuccessData( + public=GetNextTipResult( + nextTipInfo=NextTipInfo(labwareId="456", tipStartingWell="foo") + ), + ) + + +async def test_get_next_tip_implementation_no_tips( + decoy: Decoy, + state_view: StateView, +) -> None: + """A GetNextTip command should return with NoTipAvailable if there are no available tips.""" + subject = GetNextTipImplementation(state_view=state_view) + params = GetNextTipParams( + pipetteId="abc", labwareIds=["123", "456"], startingTipWell="xyz" + ) + mock_nozzle_map = decoy.mock(cls=NozzleMap) + + decoy.when(state_view.tips.get_pipette_active_channels("abc")).then_return(42) + decoy.when(state_view.tips.get_pipette_nozzle_map("abc")).then_return( + mock_nozzle_map + ) + decoy.when(mock_nozzle_map.configuration).then_return(NozzleConfigurationType.FULL) + + result = await subject.execute(params) + + assert result == SuccessData( + public=GetNextTipResult( + nextTipInfo=NoTipAvailable( + noTipReason=NoTipReason.NO_AVAILABLE_TIPS, + message="No available tips for given pipette, nozzle configuration and provided tip racks.", + ) + ), + ) + + +async def test_get_next_tip_implementation_partial_with_starting_tip( + decoy: Decoy, + state_view: StateView, +) -> None: + """A GetNextTip command should return with NoTipAvailable if there's a starting tip and a partial config.""" + subject = GetNextTipImplementation(state_view=state_view) + params = GetNextTipParams( + pipetteId="abc", labwareIds=["123", "456"], startingTipWell="xyz" + ) + mock_nozzle_map = decoy.mock(cls=NozzleMap) + + decoy.when(state_view.tips.get_pipette_active_channels("abc")).then_return(42) + decoy.when(state_view.tips.get_pipette_nozzle_map("abc")).then_return( + mock_nozzle_map + ) + decoy.when(mock_nozzle_map.configuration).then_return(NozzleConfigurationType.ROW) + + result = await subject.execute(params) + + assert result == SuccessData( + public=GetNextTipResult( + nextTipInfo=NoTipAvailable( + noTipReason=NoTipReason.STARTING_TIP_WITH_PARTIAL, + message="Cannot automatically resolve next tip with starting tip and partial tip configuration.", + ) + ), + ) diff --git a/api/tests/opentrons/protocol_engine/commands/test_liquid_probe.py b/api/tests/opentrons/protocol_engine/commands/test_liquid_probe.py index 2cada4f3e24..14a269bf300 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_liquid_probe.py +++ b/api/tests/opentrons/protocol_engine/commands/test_liquid_probe.py @@ -1,7 +1,11 @@ """Test LiquidProbe commands.""" + from datetime import datetime from typing import Type, Union +from decoy import matchers, Decoy +import pytest + from opentrons.protocol_engine.errors.exceptions import ( MustHomeError, PipetteNotReadyToAspirateError, @@ -10,12 +14,22 @@ ) from opentrons_shared_data.errors.exceptions import ( PipetteLiquidNotFoundError, + StallOrCollisionDetectedError, ) -from decoy import matchers, Decoy -import pytest +from opentrons_shared_data.pipette.pipette_definition import ( + AvailableSensorDefinition, + SupportedTipsDefinition, +) + +from opentrons_shared_data.pipette.types import PipetteNameType from opentrons.protocol_engine.commands.pipetting_common import LiquidNotFoundError from opentrons.protocol_engine.state.state import StateView +from opentrons.protocol_engine.state.pipettes import ( + StaticPipetteConfig, + BoundingNozzlesOffsets, + PipetteBoundingBoxOffsets, +) from opentrons.protocol_engine.state import update_types from opentrons.types import MountType, Point from opentrons.protocol_engine import WellLocation, WellOrigin, WellOffset, DeckPoint @@ -29,14 +43,17 @@ TryLiquidProbeImplementation, ) from opentrons.protocol_engine.commands.command import DefinedErrorData, SuccessData +from opentrons.protocol_engine.commands.movement_common import StallOrCollisionError from opentrons.protocol_engine.execution import ( MovementHandler, PipettingHandler, + GantryMover, ) from opentrons.protocol_engine.resources.model_utils import ModelUtils +from ..pipette_fixtures import get_default_nozzle_map EitherImplementationType = Union[ Type[LiquidProbeImplementation], Type[TryLiquidProbeImplementation] @@ -46,6 +63,12 @@ EitherResultType = Union[Type[LiquidProbeResult], Type[TryLiquidProbeResult]] +@pytest.fixture +def available_sensors() -> AvailableSensorDefinition: + """Provide a list of sensors.""" + return AvailableSensorDefinition(sensors=["pressure", "capacitive", "environment"]) + + @pytest.fixture( params=[ (LiquidProbeImplementation, LiquidProbeParams, LiquidProbeResult), @@ -61,7 +84,7 @@ def types( @pytest.fixture def implementation_type( - types: tuple[EitherImplementationType, object, object] + types: tuple[EitherImplementationType, object, object], ) -> EitherImplementationType: """Return an implementation type. Kept in sync with the params and result types.""" return types[0] @@ -84,6 +107,7 @@ def subject( implementation_type: EitherImplementationType, state_view: StateView, movement: MovementHandler, + gantry_mover: GantryMover, pipetting: PipettingHandler, model_utils: ModelUtils, ) -> Union[LiquidProbeImplementation, TryLiquidProbeImplementation]: @@ -92,6 +116,7 @@ def subject( state_view=state_view, pipetting=pipetting, movement=movement, + gantry_mover=gantry_mover, model_utils=model_utils, ) @@ -105,6 +130,8 @@ async def test_liquid_probe_implementation( params_type: EitherParamsType, result_type: EitherResultType, model_utils: ModelUtils, + available_sensors: AvailableSensorDefinition, + supported_tip_fixture: SupportedTipsDefinition, ) -> None: """It should move to the destination and do a liquid probe there.""" location = WellLocation(origin=WellOrigin.BOTTOM, offset=WellOffset(x=0, y=0, z=1)) @@ -126,6 +153,11 @@ async def test_liquid_probe_implementation( labware_id="123", well_name="A3", well_location=location, + current_well=None, + force_direct=False, + minimum_z_height=None, + speed=None, + operation_volume=None, ), ).then_return(Point(x=1, y=2, z=3)) @@ -145,6 +177,44 @@ async def test_liquid_probe_implementation( height=15.0, ), ).then_return(30.0) + decoy.when( + state_view.pipettes.get_nozzle_configuration_supports_lld("abc") + ).then_return(True) + + decoy.when(state_view.pipettes.get_config("abc")).then_return( + StaticPipetteConfig( + min_volume=1, + max_volume=9001, + channels=1, + model="blah", + display_name="bleh", + serial_number="", + tip_configuration_lookup_table={9001: supported_tip_fixture}, + nominal_tip_overlap={}, + home_position=0, + nozzle_offset_z=0, + bounding_nozzle_offsets=BoundingNozzlesOffsets( + back_left_offset=Point(x=10, y=20, z=30), + front_right_offset=Point(x=40, y=50, z=60), + ), + default_nozzle_map=get_default_nozzle_map(PipetteNameType.P1000_96), + pipette_bounding_box_offsets=PipetteBoundingBoxOffsets( + back_left_corner=Point(x=10, y=20, z=30), + front_right_corner=Point(x=40, y=50, z=60), + front_left_corner=Point(x=10, y=50, z=60), + back_right_corner=Point(x=40, y=20, z=60), + ), + lld_settings={}, + plunger_positions={ + "top": 0.0, + "bottom": 5.0, + "blow_out": 19.0, + "drop_tip": 20.0, + }, + shaft_ul_per_mm=5.0, + available_sensors=available_sensors, + ) + ) timestamp = datetime(year=2020, month=1, day=2) decoy.when(model_utils.get_timestamp()).then_return(timestamp) @@ -179,6 +249,8 @@ async def test_liquid_not_found_error( subject: EitherImplementation, params_type: EitherParamsType, model_utils: ModelUtils, + available_sensors: AvailableSensorDefinition, + supported_tip_fixture: SupportedTipsDefinition, ) -> None: """It should return a liquid not found error if the hardware API indicates that.""" pipette_id = "pipette-id" @@ -201,16 +273,53 @@ async def test_liquid_not_found_error( ) decoy.when(state_view.pipettes.get_aspirated_volume(pipette_id)).then_return(0) - + decoy.when(state_view.pipettes.get_config("pipette-id")).then_return( + StaticPipetteConfig( + min_volume=1, + max_volume=9001, + channels=1, + model="blah", + display_name="bleh", + serial_number="", + tip_configuration_lookup_table={9001: supported_tip_fixture}, + nominal_tip_overlap={}, + home_position=0, + nozzle_offset_z=0, + bounding_nozzle_offsets=BoundingNozzlesOffsets( + back_left_offset=Point(x=10, y=20, z=30), + front_right_offset=Point(x=40, y=50, z=60), + ), + default_nozzle_map=get_default_nozzle_map(PipetteNameType.P1000_96), + pipette_bounding_box_offsets=PipetteBoundingBoxOffsets( + back_left_corner=Point(x=10, y=20, z=30), + front_right_corner=Point(x=40, y=50, z=60), + front_left_corner=Point(x=10, y=50, z=60), + back_right_corner=Point(x=40, y=20, z=60), + ), + lld_settings={}, + plunger_positions={ + "top": 0.0, + "bottom": 5.0, + "blow_out": 19.0, + "drop_tip": 20.0, + }, + shaft_ul_per_mm=5.0, + available_sensors=available_sensors, + ) + ) decoy.when( await movement.move_to_well( pipette_id=pipette_id, labware_id=labware_id, well_name=well_name, well_location=well_location, + current_well=None, + force_direct=False, + minimum_z_height=None, + speed=None, + operation_volume=None, ), ).then_return(position) - decoy.when( await pipetting.liquid_probe_in_place( pipette_id=pipette_id, @@ -219,7 +328,9 @@ async def test_liquid_not_found_error( well_location=well_location, ), ).then_raise(PipetteLiquidNotFoundError()) - + decoy.when( + state_view.pipettes.get_nozzle_configuration_supports_lld(pipette_id) + ).then_return(True) decoy.when(model_utils.generate_id()).then_return(error_id) decoy.when(model_utils.get_timestamp()).then_return(error_timestamp) @@ -241,7 +352,7 @@ async def test_liquid_not_found_error( ) if isinstance(subject, LiquidProbeImplementation): assert result == DefinedErrorData( - public=LiquidNotFoundError.construct( + public=LiquidNotFoundError.model_construct( id=error_id, createdAt=error_timestamp, wrappedErrors=[matchers.Anything()], @@ -263,6 +374,8 @@ async def test_liquid_probe_tip_checking( state_view: StateView, subject: EitherImplementation, params_type: EitherParamsType, + available_sensors: AvailableSensorDefinition, + supported_tip_fixture: SupportedTipsDefinition, ) -> None: """It should raise a TipNotAttached error if the state view indicates that.""" pipette_id = "pipette-id" @@ -278,10 +391,46 @@ async def test_liquid_probe_tip_checking( wellName=well_name, wellLocation=well_location, ) - + decoy.when( + state_view.pipettes.get_nozzle_configuration_supports_lld(pipette_id) + ).then_return(True) decoy.when(state_view.pipettes.get_aspirated_volume(pipette_id)).then_raise( TipNotAttachedError() ) + decoy.when(state_view.pipettes.get_config("pipette-id")).then_return( + StaticPipetteConfig( + min_volume=1, + max_volume=9001, + channels=1, + model="blah", + display_name="bleh", + serial_number="", + tip_configuration_lookup_table={9001: supported_tip_fixture}, + nominal_tip_overlap={}, + home_position=0, + nozzle_offset_z=0, + bounding_nozzle_offsets=BoundingNozzlesOffsets( + back_left_offset=Point(x=10, y=20, z=30), + front_right_offset=Point(x=40, y=50, z=60), + ), + default_nozzle_map=get_default_nozzle_map(PipetteNameType.P1000_96), + pipette_bounding_box_offsets=PipetteBoundingBoxOffsets( + back_left_corner=Point(x=10, y=20, z=30), + front_right_corner=Point(x=40, y=50, z=60), + front_left_corner=Point(x=10, y=50, z=60), + back_right_corner=Point(x=40, y=20, z=60), + ), + lld_settings={}, + plunger_positions={ + "top": 0.0, + "bottom": 5.0, + "blow_out": 19.0, + "drop_tip": 20.0, + }, + shaft_ul_per_mm=5.0, + available_sensors=available_sensors, + ) + ) with pytest.raises(TipNotAttachedError): await subject.execute(data) @@ -291,6 +440,8 @@ async def test_liquid_probe_plunger_preparedness_checking( state_view: StateView, subject: EitherImplementation, params_type: EitherParamsType, + available_sensors: AvailableSensorDefinition, + supported_tip_fixture: SupportedTipsDefinition, ) -> None: """It should raise a PipetteNotReadyToAspirate error if the state view indicates that.""" pipette_id = "pipette-id" @@ -306,7 +457,43 @@ async def test_liquid_probe_plunger_preparedness_checking( wellName=well_name, wellLocation=well_location, ) - + decoy.when( + state_view.pipettes.get_nozzle_configuration_supports_lld(pipette_id) + ).then_return(True) + decoy.when(state_view.pipettes.get_config("pipette-id")).then_return( + StaticPipetteConfig( + min_volume=1, + max_volume=9001, + channels=1, + model="blah", + display_name="bleh", + serial_number="", + tip_configuration_lookup_table={9001: supported_tip_fixture}, + nominal_tip_overlap={}, + home_position=0, + nozzle_offset_z=0, + bounding_nozzle_offsets=BoundingNozzlesOffsets( + back_left_offset=Point(x=10, y=20, z=30), + front_right_offset=Point(x=40, y=50, z=60), + ), + default_nozzle_map=get_default_nozzle_map(PipetteNameType.P1000_96), + pipette_bounding_box_offsets=PipetteBoundingBoxOffsets( + back_left_corner=Point(x=10, y=20, z=30), + front_right_corner=Point(x=40, y=50, z=60), + front_left_corner=Point(x=10, y=50, z=60), + back_right_corner=Point(x=40, y=20, z=60), + ), + lld_settings={}, + plunger_positions={ + "top": 0.0, + "bottom": 5.0, + "blow_out": 19.0, + "drop_tip": 20.0, + }, + shaft_ul_per_mm=5.0, + available_sensors=available_sensors, + ) + ) decoy.when(state_view.pipettes.get_aspirated_volume(pipette_id)).then_return(None) with pytest.raises(PipetteNotReadyToAspirateError): await subject.execute(data) @@ -317,6 +504,8 @@ async def test_liquid_probe_volume_checking( state_view: StateView, subject: EitherImplementation, params_type: EitherParamsType, + available_sensors: AvailableSensorDefinition, + supported_tip_fixture: SupportedTipsDefinition, ) -> None: """It should return a TipNotEmptyError if the hardware API indicates that.""" pipette_id = "pipette-id" @@ -336,12 +525,51 @@ async def test_liquid_probe_volume_checking( decoy.when( state_view.pipettes.get_aspirated_volume(pipette_id=pipette_id), ).then_return(123) + decoy.when(state_view.pipettes.get_config("pipette-id")).then_return( + StaticPipetteConfig( + min_volume=1, + max_volume=9001, + channels=1, + model="blah", + display_name="bleh", + serial_number="", + tip_configuration_lookup_table={9001: supported_tip_fixture}, + nominal_tip_overlap={}, + home_position=0, + nozzle_offset_z=0, + bounding_nozzle_offsets=BoundingNozzlesOffsets( + back_left_offset=Point(x=10, y=20, z=30), + front_right_offset=Point(x=40, y=50, z=60), + ), + default_nozzle_map=get_default_nozzle_map(PipetteNameType.P1000_96), + pipette_bounding_box_offsets=PipetteBoundingBoxOffsets( + back_left_corner=Point(x=10, y=20, z=30), + front_right_corner=Point(x=40, y=50, z=60), + front_left_corner=Point(x=10, y=50, z=60), + back_right_corner=Point(x=40, y=20, z=60), + ), + lld_settings={}, + plunger_positions={ + "top": 0.0, + "bottom": 5.0, + "blow_out": 19.0, + "drop_tip": 20.0, + }, + shaft_ul_per_mm=5.0, + available_sensors=available_sensors, + ) + ) + decoy.when( + state_view.pipettes.get_nozzle_configuration_supports_lld(pipette_id) + ).then_return(True) + with pytest.raises(TipNotEmptyError): await subject.execute(data) decoy.when( state_view.pipettes.get_aspirated_volume(pipette_id=pipette_id), ).then_return(None) + with pytest.raises(PipetteNotReadyToAspirateError): await subject.execute(data) @@ -352,6 +580,8 @@ async def test_liquid_probe_location_checking( movement: MovementHandler, subject: EitherImplementation, params_type: EitherParamsType, + available_sensors: AvailableSensorDefinition, + supported_tip_fixture: SupportedTipsDefinition, ) -> None: """It should return a PositionUnkownError if the hardware API indicates that.""" pipette_id = "pipette-id" @@ -368,10 +598,138 @@ async def test_liquid_probe_location_checking( wellLocation=well_location, ) decoy.when(state_view.pipettes.get_aspirated_volume(pipette_id)).then_return(0) + decoy.when(state_view.pipettes.get_config("pipette-id")).then_return( + StaticPipetteConfig( + min_volume=1, + max_volume=9001, + channels=1, + model="blah", + display_name="bleh", + serial_number="", + tip_configuration_lookup_table={9001: supported_tip_fixture}, + nominal_tip_overlap={}, + home_position=0, + nozzle_offset_z=0, + bounding_nozzle_offsets=BoundingNozzlesOffsets( + back_left_offset=Point(x=10, y=20, z=30), + front_right_offset=Point(x=40, y=50, z=60), + ), + default_nozzle_map=get_default_nozzle_map(PipetteNameType.P1000_96), + pipette_bounding_box_offsets=PipetteBoundingBoxOffsets( + back_left_corner=Point(x=10, y=20, z=30), + front_right_corner=Point(x=40, y=50, z=60), + front_left_corner=Point(x=10, y=50, z=60), + back_right_corner=Point(x=40, y=20, z=60), + ), + lld_settings={}, + plunger_positions={ + "top": 0.0, + "bottom": 5.0, + "blow_out": 19.0, + "drop_tip": 20.0, + }, + shaft_ul_per_mm=5.0, + available_sensors=available_sensors, + ) + ) decoy.when( await movement.check_for_valid_position( mount=MountType.LEFT, ), ).then_return(False) + decoy.when( + state_view.pipettes.get_nozzle_configuration_supports_lld(pipette_id) + ).then_return(True) with pytest.raises(MustHomeError): await subject.execute(data) + + +async def test_liquid_probe_stall( + decoy: Decoy, + movement: MovementHandler, + state_view: StateView, + pipetting: PipettingHandler, + subject: EitherImplementation, + params_type: EitherParamsType, + model_utils: ModelUtils, + available_sensors: AvailableSensorDefinition, + supported_tip_fixture: SupportedTipsDefinition, +) -> None: + """It should move to the destination and do a liquid probe there.""" + location = WellLocation(origin=WellOrigin.BOTTOM, offset=WellOffset(x=0, y=0, z=1)) + + data = params_type( + pipetteId="abc", + labwareId="123", + wellName="A3", + wellLocation=location, + ) + + decoy.when(state_view.pipettes.get_aspirated_volume(pipette_id="abc")).then_return( + 0 + ) + decoy.when(state_view.pipettes.get_config("abc")).then_return( + StaticPipetteConfig( + min_volume=1, + max_volume=9001, + channels=1, + model="blah", + display_name="bleh", + serial_number="", + tip_configuration_lookup_table={9001: supported_tip_fixture}, + nominal_tip_overlap={}, + home_position=0, + nozzle_offset_z=0, + bounding_nozzle_offsets=BoundingNozzlesOffsets( + back_left_offset=Point(x=10, y=20, z=30), + front_right_offset=Point(x=40, y=50, z=60), + ), + default_nozzle_map=get_default_nozzle_map(PipetteNameType.P1000_96), + pipette_bounding_box_offsets=PipetteBoundingBoxOffsets( + back_left_corner=Point(x=10, y=20, z=30), + front_right_corner=Point(x=40, y=50, z=60), + front_left_corner=Point(x=10, y=50, z=60), + back_right_corner=Point(x=40, y=20, z=60), + ), + lld_settings={}, + plunger_positions={ + "top": 0.0, + "bottom": 5.0, + "blow_out": 19.0, + "drop_tip": 20.0, + }, + shaft_ul_per_mm=5.0, + available_sensors=available_sensors, + ) + ) + decoy.when( + state_view.pipettes.get_nozzle_configuration_supports_lld("abc") + ).then_return(True) + + decoy.when( + await movement.move_to_well( + pipette_id="abc", + labware_id="123", + well_name="A3", + well_location=location, + current_well=None, + force_direct=False, + minimum_z_height=None, + speed=None, + operation_volume=None, + ), + ).then_raise(StallOrCollisionDetectedError()) + + error_id = "error-id" + timestamp = datetime(year=2020, month=1, day=2) + decoy.when(model_utils.get_timestamp()).then_return(timestamp) + decoy.when(model_utils.generate_id()).then_return(error_id) + + result = await subject.execute(data) + + assert result == DefinedErrorData( + public=StallOrCollisionError.model_construct( + id=error_id, createdAt=timestamp, wrappedErrors=[matchers.Anything()] + ), + state_update=update_types.StateUpdate(pipette_location=update_types.CLEAR), + ) diff --git a/api/tests/opentrons/protocol_engine/commands/test_load_labware.py b/api/tests/opentrons/protocol_engine/commands/test_load_labware.py index 3873f9854b4..8229d7f4265 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_load_labware.py +++ b/api/tests/opentrons/protocol_engine/commands/test_load_labware.py @@ -1,12 +1,9 @@ """Test load labware commands.""" import inspect from typing import Optional -from opentrons.protocol_engine.state.update_types import ( - LoadedLabwareUpdate, - StateUpdate, -) -import pytest +from unittest.mock import sentinel +import pytest from decoy import Decoy from opentrons.types import DeckSlotName @@ -18,12 +15,19 @@ ) from opentrons.protocol_engine.types import ( + AddressableAreaLocation, DeckSlotLocation, + LabwareLocation, OnLabwareLocation, ) from opentrons.protocol_engine.execution import LoadedLabwareData, EquipmentHandler from opentrons.protocol_engine.resources import labware_validation from opentrons.protocol_engine.state.state import StateView +from opentrons.protocol_engine.state.update_types import ( + AddressableAreaUsedUpdate, + LoadedLabwareUpdate, + StateUpdate, +) from opentrons.protocol_engine.commands.command import SuccessData from opentrons.protocol_engine.commands.load_labware import ( @@ -42,33 +46,40 @@ def patch_mock_labware_validation( monkeypatch.setattr(labware_validation, name, decoy.mock(func=func)) -@pytest.mark.parametrize("display_name", [("My custom display name"), (None)]) -async def test_load_labware_implementation( +@pytest.mark.parametrize("display_name", ["My custom display name", None]) +@pytest.mark.parametrize( + ("location", "expected_addressable_area_name"), + [ + (DeckSlotLocation(slotName=DeckSlotName.SLOT_3), "3"), + (AddressableAreaLocation(addressableAreaName="3"), "3"), + ], +) +async def test_load_labware_on_slot_or_addressable_area( decoy: Decoy, well_plate_def: LabwareDefinition, equipment: EquipmentHandler, state_view: StateView, display_name: Optional[str], + location: LabwareLocation, + expected_addressable_area_name: str, ) -> None: """A LoadLabware command should have an execution implementation.""" subject = LoadLabwareImplementation(equipment=equipment, state_view=state_view) data = LoadLabwareParams( - location=DeckSlotLocation(slotName=DeckSlotName.SLOT_3), + location=location, loadName="some-load-name", namespace="opentrons-test", version=1, displayName=display_name, ) - decoy.when( - state_view.geometry.ensure_location_not_occupied( - DeckSlotLocation(slotName=DeckSlotName.SLOT_3) - ) - ).then_return(DeckSlotLocation(slotName=DeckSlotName.SLOT_4)) + decoy.when(state_view.geometry.ensure_location_not_occupied(location)).then_return( + sentinel.validated_empty_location + ) decoy.when( await equipment.load_labware( - location=DeckSlotLocation(slotName=DeckSlotName.SLOT_4), + location=sentinel.validated_empty_location, load_name="some-load-name", namespace="opentrons-test", version=1, @@ -99,9 +110,12 @@ async def test_load_labware_implementation( labware_id="labware-id", definition=well_plate_def, offset_id="labware-offset-id", - new_location=DeckSlotLocation(slotName=DeckSlotName.SLOT_4), + new_location=sentinel.validated_empty_location, display_name=display_name, - ) + ), + addressable_area_used=AddressableAreaUsedUpdate( + addressable_area_name=expected_addressable_area_name + ), ), ) diff --git a/api/tests/opentrons/protocol_engine/commands/test_load_liquid.py b/api/tests/opentrons/protocol_engine/commands/test_load_liquid.py index dbc584ae2a3..6bd61061f3c 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_load_liquid.py +++ b/api/tests/opentrons/protocol_engine/commands/test_load_liquid.py @@ -9,6 +9,7 @@ LoadLiquidImplementation, LoadLiquidParams, ) +from opentrons.protocol_engine.errors import InvalidLiquidError from opentrons.protocol_engine.state.state import StateView from opentrons.protocol_engine.resources.model_utils import ModelUtils from opentrons.protocol_engine.state import update_types @@ -64,3 +65,37 @@ async def test_load_liquid_implementation( "labware-id", {"A1": 30.0, "B2": 100.0} ) ) + + +async def test_load_empty_liquid_requires_zero_volume( + decoy: Decoy, + subject: LoadLiquidImplementation, + mock_state_view: StateView, + model_utils: ModelUtils, +) -> None: + """Test that loadLiquid requires empty liquids to have 0 volume.""" + data = LoadLiquidParams( + labwareId="labware-id", liquidId="EMPTY", volumeByWell={"A1": 1.0} + ) + timestamp = datetime(year=2020, month=1, day=2) + decoy.when(model_utils.get_timestamp()).then_return(timestamp) + + with pytest.raises(InvalidLiquidError): + await subject.execute(data) + + decoy.verify(mock_state_view.liquid.validate_liquid_id("EMPTY")) + + data2 = LoadLiquidParams( + labwareId="labware-id", liquidId="EMPTY", volumeByWell={"A1": 0.0} + ) + result = await subject.execute(data2) + assert result == SuccessData( + public=LoadLiquidResult(), + state_update=update_types.StateUpdate( + liquid_loaded=update_types.LiquidLoadedUpdate( + labware_id="labware-id", + volumes=data2.volumeByWell, + last_loaded=timestamp, + ) + ), + ) diff --git a/api/tests/opentrons/protocol_engine/commands/test_load_liquid_class.py b/api/tests/opentrons/protocol_engine/commands/test_load_liquid_class.py new file mode 100644 index 00000000000..54de10f3bc2 --- /dev/null +++ b/api/tests/opentrons/protocol_engine/commands/test_load_liquid_class.py @@ -0,0 +1,161 @@ +"""Test load-liquid command.""" +from decoy import Decoy +import pytest + +from opentrons_shared_data.liquid_classes.liquid_class_definition import ( + LiquidClassSchemaV1, +) +from opentrons.protocol_engine.commands.command import SuccessData +from opentrons.protocol_engine.commands.load_liquid_class import ( + LoadLiquidClassImplementation, + LoadLiquidClassParams, + LoadLiquidClassResult, +) +from opentrons.protocol_engine.errors import ( + LiquidClassDoesNotExistError, + LiquidClassRedefinitionError, +) +from opentrons.protocol_engine.resources.model_utils import ModelUtils +from opentrons.protocol_engine.state import update_types +from opentrons.protocol_engine.state.state import StateView +from opentrons.protocol_engine.types import LiquidClassRecord + + +@pytest.fixture +def liquid_class_record( + minimal_liquid_class_def2: LiquidClassSchemaV1, +) -> LiquidClassRecord: + """A dummy LiquidClassRecord for testing.""" + pipette_0 = minimal_liquid_class_def2.byPipette[0] + by_tip_type_0 = pipette_0.byTipType[0] + return LiquidClassRecord( + liquidClassName=minimal_liquid_class_def2.liquidClassName, + pipetteModel=pipette_0.pipetteModel, + tiprack=by_tip_type_0.tiprack, + aspirate=by_tip_type_0.aspirate, + singleDispense=by_tip_type_0.singleDispense, + multiDispense=by_tip_type_0.multiDispense, + ) + + +async def test_load_liquid_class_new_liquid_class_no_id( + decoy: Decoy, + state_view: StateView, + model_utils: ModelUtils, + liquid_class_record: LiquidClassRecord, +) -> None: + """Load a new liquid class with no liquidClassId specified. Should assign a new ID.""" + subject = LoadLiquidClassImplementation( + state_view=state_view, model_utils=model_utils + ) + decoy.when(model_utils.generate_id()).then_return("new-generated-id") + + params = LoadLiquidClassParams(liquidClassRecord=liquid_class_record) + result = await subject.execute(params) + assert result == SuccessData( + public=LoadLiquidClassResult(liquidClassId="new-generated-id"), + state_update=update_types.StateUpdate( + liquid_class_loaded=update_types.LiquidClassLoadedUpdate( + liquid_class_id="new-generated-id", + liquid_class_record=liquid_class_record, + ) + ), + ) + + +async def test_load_liquid_class_existing_liquid_class_no_id( + decoy: Decoy, + state_view: StateView, + model_utils: ModelUtils, + liquid_class_record: LiquidClassRecord, +) -> None: + """Load an existing liquid class with no liquidClassId specified. Should find and reuse existing ID.""" + subject = LoadLiquidClassImplementation( + state_view=state_view, model_utils=model_utils + ) + decoy.when( + state_view.liquid_classes.get_id_for_liquid_class_record(liquid_class_record) + ).then_return("existing-id") + + params = LoadLiquidClassParams(liquidClassRecord=liquid_class_record) + result = await subject.execute(params) + assert result == SuccessData( + public=LoadLiquidClassResult(liquidClassId="existing-id"), + state_update=update_types.StateUpdate(), # no state change since liquid class already loaded + ) + + +async def test_load_liquid_class_new_liquid_class_specified_id( + decoy: Decoy, + state_view: StateView, + model_utils: ModelUtils, + liquid_class_record: LiquidClassRecord, +) -> None: + """Load a new liquid class with the liquidClassId specified. Should store the new liquid class.""" + subject = LoadLiquidClassImplementation( + state_view=state_view, model_utils=model_utils + ) + decoy.when(state_view.liquid_classes.get("liquid-class-1")).then_raise( + LiquidClassDoesNotExistError() + ) + + params = LoadLiquidClassParams( + liquidClassId="liquid-class-1", liquidClassRecord=liquid_class_record + ) + result = await subject.execute(params) + assert result == SuccessData( + public=LoadLiquidClassResult(liquidClassId="liquid-class-1"), + state_update=update_types.StateUpdate( + liquid_class_loaded=update_types.LiquidClassLoadedUpdate( + liquid_class_id="liquid-class-1", + liquid_class_record=liquid_class_record, + ) + ), + ) + + +async def test_load_liquid_class_existing_liquid_class_specified_id( + decoy: Decoy, + state_view: StateView, + model_utils: ModelUtils, + liquid_class_record: LiquidClassRecord, +) -> None: + """Load a liquid class with a liquidClassId that was already loaded before. Should be a no-op.""" + subject = LoadLiquidClassImplementation( + state_view=state_view, model_utils=model_utils + ) + decoy.when(state_view.liquid_classes.get("liquid-class-1")).then_return( + liquid_class_record + ) + + params = LoadLiquidClassParams( + liquidClassId="liquid-class-1", liquidClassRecord=liquid_class_record + ) + result = await subject.execute(params) + assert result == SuccessData( + public=LoadLiquidClassResult(liquidClassId="liquid-class-1"), + state_update=update_types.StateUpdate(), # no state change since liquid class already loaded + ) + + +async def test_load_liquid_class_conflicting_definition_for_id( + decoy: Decoy, + state_view: StateView, + model_utils: ModelUtils, + liquid_class_record: LiquidClassRecord, +) -> None: + """Should raise when we try to load a modified liquid class with the same liquidClassId.""" + subject = LoadLiquidClassImplementation( + state_view=state_view, model_utils=model_utils + ) + decoy.when(state_view.liquid_classes.get("liquid-class-1")).then_return( + liquid_class_record + ) + + new_liquid_class_record = liquid_class_record.model_copy(deep=True) + new_liquid_class_record.aspirate.offset.x += 123 # make it different + params = LoadLiquidClassParams( + liquidClassId="liquid-class-1", liquidClassRecord=new_liquid_class_record + ) + with pytest.raises(LiquidClassRedefinitionError): + await subject.execute(params) 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 ce68f5c9f8a..65ee30e7a88 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,12 @@ """Test load module command.""" -import pytest from typing import cast +from unittest.mock import sentinel + +import pytest from decoy import Decoy from opentrons.protocol_engine.errors import LocationIsOccupiedError +from opentrons.protocol_engine.state import update_types from opentrons.protocol_engine.state.state import StateView from opentrons_shared_data.robot.types import RobotType from opentrons.types import DeckSlotName @@ -57,7 +60,7 @@ async def test_load_module_implementation( 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.labware.get_deck_definition()).then_return(deck_def) decoy.when( state_view.addressable_areas.get_cutout_id_by_deck_slot_name( DeckSlotName.SLOT_D1 @@ -70,6 +73,13 @@ async def test_load_module_implementation( ) ).then_return(DeckSlotLocation(slotName=DeckSlotName.SLOT_2)) + decoy.when( + state_view.modules.ensure_and_convert_module_fixture_location( + deck_slot=data.location.slotName, + model=data.model, + ) + ).then_return(sentinel.addressable_area_provided_by_module) + decoy.when( await equipment.load_module( model=ModuleModel.TEMPERATURE_MODULE_V2, @@ -85,6 +95,11 @@ async def test_load_module_implementation( ) result = await subject.execute(data) + decoy.verify( + state_view.addressable_areas.raise_if_area_not_in_deck_configuration( + sentinel.addressable_area_provided_by_module + ) + ) assert result == SuccessData( public=LoadModuleResult( moduleId="module-id", @@ -92,6 +107,11 @@ async def test_load_module_implementation( model=ModuleModel.TEMPERATURE_MODULE_V2, definition=tempdeck_v2_def, ), + state_update=update_types.StateUpdate( + addressable_area_used=update_types.AddressableAreaUsedUpdate( + addressable_area_name=data.location.slotName.id + ) + ), ) @@ -112,7 +132,7 @@ async def test_load_module_implementation_mag_block( 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.labware.get_deck_definition()).then_return(deck_def) decoy.when( state_view.addressable_areas.get_cutout_id_by_deck_slot_name( DeckSlotName.SLOT_D1 @@ -125,6 +145,13 @@ async def test_load_module_implementation_mag_block( ) ).then_return(DeckSlotLocation(slotName=DeckSlotName.SLOT_2)) + decoy.when( + state_view.modules.ensure_and_convert_module_fixture_location( + deck_slot=data.location.slotName, + model=data.model, + ) + ).then_return(sentinel.addressable_area_provided_by_module) + decoy.when( await equipment.load_magnetic_block( model=ModuleModel.MAGNETIC_BLOCK_V1, @@ -140,6 +167,11 @@ async def test_load_module_implementation_mag_block( ) result = await subject.execute(data) + decoy.verify( + state_view.addressable_areas.raise_if_area_not_in_deck_configuration( + sentinel.addressable_area_provided_by_module + ) + ) assert result == SuccessData( public=LoadModuleResult( moduleId="module-id", @@ -147,6 +179,11 @@ async def test_load_module_implementation_mag_block( model=ModuleModel.MAGNETIC_BLOCK_V1, definition=mag_block_v1_def, ), + state_update=update_types.StateUpdate( + addressable_area_used=update_types.AddressableAreaUsedUpdate( + addressable_area_name=data.location.slotName.id + ) + ), ) @@ -167,7 +204,7 @@ async def test_load_module_implementation_abs_reader( 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.labware.get_deck_definition()).then_return(deck_def) decoy.when( state_view.addressable_areas.get_cutout_id_by_deck_slot_name( DeckSlotName.SLOT_D3 @@ -180,6 +217,13 @@ async def test_load_module_implementation_abs_reader( ) ).then_return(DeckSlotLocation(slotName=DeckSlotName.SLOT_D3)) + decoy.when( + state_view.modules.ensure_and_convert_module_fixture_location( + deck_slot=data.location.slotName, + model=data.model, + ) + ).then_return(sentinel.addressable_area_name) + decoy.when( await equipment.load_module( model=ModuleModel.ABSORBANCE_READER_V1, @@ -202,6 +246,11 @@ async def test_load_module_implementation_abs_reader( model=ModuleModel.ABSORBANCE_READER_V1, definition=abs_reader_v1_def, ), + state_update=update_types.StateUpdate( + addressable_area_used=update_types.AddressableAreaUsedUpdate( + addressable_area_name=data.location.slotName.id + ) + ), ) @@ -221,7 +270,7 @@ async def test_load_module_raises_if_location_occupied( 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.labware.get_deck_definition()).then_return(deck_def) decoy.when( state_view.addressable_areas.get_cutout_id_by_deck_slot_name( DeckSlotName.SLOT_D1 @@ -303,9 +352,7 @@ async def test_load_module_raises_wrong_location( 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.labware.get_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) @@ -361,9 +408,7 @@ async def test_load_module_raises_module_fixture_id_does_not_exist( 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.labware.get_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) diff --git a/api/tests/opentrons/protocol_engine/commands/test_load_pipette.py b/api/tests/opentrons/protocol_engine/commands/test_load_pipette.py index 44a9db61863..a251c6aef1f 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_load_pipette.py +++ b/api/tests/opentrons/protocol_engine/commands/test_load_pipette.py @@ -3,12 +3,14 @@ LoadPipetteUpdate, PipetteConfigUpdate, StateUpdate, + PipetteUnknownFluidUpdate, ) import pytest from decoy import Decoy from opentrons_shared_data.pipette.types import PipetteNameType from opentrons_shared_data.robot.types import RobotType +from opentrons_shared_data.pipette.pipette_definition import AvailableSensorDefinition from opentrons.types import MountType, Point from opentrons.protocol_engine.errors import InvalidSpecificationForRobotTypeError @@ -27,6 +29,12 @@ from ..pipette_fixtures import get_default_nozzle_map +@pytest.fixture +def available_sensors() -> AvailableSensorDefinition: + """Provide a list of sensors.""" + return AvailableSensorDefinition(sensors=["pressure", "capacitive", "environment"]) + + @pytest.mark.parametrize( "data", [ @@ -48,6 +56,7 @@ async def test_load_pipette_implementation( equipment: EquipmentHandler, state_view: StateView, data: LoadPipetteParams, + available_sensors: AvailableSensorDefinition, ) -> None: """A LoadPipette command should have an execution implementation.""" subject = LoadPipetteImplementation(equipment=equipment, state_view=state_view) @@ -68,6 +77,14 @@ async def test_load_pipette_implementation( back_left_corner_offset=Point(x=1, y=2, z=3), front_right_corner_offset=Point(x=4, y=5, z=6), pipette_lld_settings={}, + plunger_positions={ + "top": 0.0, + "bottom": 5.0, + "blow_out": 19.0, + "drop_tip": 20.0, + }, + shaft_ul_per_mm=5.0, + available_sensors=available_sensors, ) decoy.when( @@ -101,6 +118,7 @@ async def test_load_pipette_implementation( serial_number="some-serial-number", config=config_data, ), + pipette_aspirated_fluid=PipetteUnknownFluidUpdate(pipette_id="some id"), ), ) @@ -109,6 +127,7 @@ async def test_load_pipette_implementation_96_channel( decoy: Decoy, equipment: EquipmentHandler, state_view: StateView, + available_sensors: AvailableSensorDefinition, ) -> None: """A LoadPipette command should have an execution implementation.""" subject = LoadPipetteImplementation(equipment=equipment, state_view=state_view) @@ -135,6 +154,14 @@ async def test_load_pipette_implementation_96_channel( back_left_corner_offset=Point(x=1, y=2, z=3), front_right_corner_offset=Point(x=4, y=5, z=6), pipette_lld_settings={}, + plunger_positions={ + "top": 0.0, + "bottom": 5.0, + "blow_out": 19.0, + "drop_tip": 20.0, + }, + shaft_ul_per_mm=5.0, + available_sensors=available_sensors, ) decoy.when( @@ -166,6 +193,7 @@ async def test_load_pipette_implementation_96_channel( serial_number="some id", config=config_data, ), + pipette_aspirated_fluid=PipetteUnknownFluidUpdate(pipette_id="pipette-id"), ), ) diff --git a/api/tests/opentrons/protocol_engine/commands/test_move_labware.py b/api/tests/opentrons/protocol_engine/commands/test_move_labware.py index a946eccf05d..2036bda558a 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_move_labware.py +++ b/api/tests/opentrons/protocol_engine/commands/test_move_labware.py @@ -1,16 +1,19 @@ """Test the ``moveLabware`` command.""" + from datetime import datetime import inspect +from unittest.mock import sentinel + import pytest from decoy import Decoy, matchers +from opentrons_shared_data.labware.labware_definition import Parameters, Dimensions from opentrons_shared_data.errors.exceptions import ( EnumeratedError, FailedGripperPickupError, LabwareDroppedError, StallOrCollisionDetectedError, ) -from opentrons_shared_data.labware.labware_definition import Parameters, Dimensions from opentrons_shared_data.gripper.constants import GRIPPER_PADDLE_WIDTH from opentrons.protocol_engine.state import update_types @@ -90,9 +93,10 @@ async def test_manual_move_labware_implementation( times_pause_called: int, ) -> None: """It should execute a pause and return the new offset.""" + new_location = DeckSlotLocation(slotName=DeckSlotName.SLOT_4) data = MoveLabwareParams( labwareId="my-cool-labware-id", - newLocation=DeckSlotLocation(slotName=DeckSlotName.SLOT_4), + newLocation=new_location, strategy=strategy, ) @@ -131,7 +135,10 @@ async def test_manual_move_labware_implementation( labware_id="my-cool-labware-id", offset_id="wowzers-a-new-offset-id", new_location=DeckSlotLocation(slotName=DeckSlotName.SLOT_5), - ) + ), + addressable_area_used=update_types.AddressableAreaUsedUpdate( + addressable_area_name=new_location.slotName.id + ), ), ) @@ -162,7 +169,7 @@ async def test_move_labware_implementation_on_labware( decoy.when( state_view.labware.get_definition(labware_id="my-cool-labware-id") ).then_return( - LabwareDefinition.construct(namespace="spacename") # type: ignore[call-arg] + LabwareDefinition.model_construct(namespace="spacename") # type: ignore[call-arg] ) decoy.when( state_view.geometry.ensure_location_not_occupied( @@ -183,7 +190,7 @@ async def test_move_labware_implementation_on_labware( "my-even-cooler-labware-id" ), state_view.labware.raise_if_labware_cannot_be_stacked( - LabwareDefinition.construct(namespace="spacename"), # type: ignore[call-arg] + LabwareDefinition.model_construct(namespace="spacename"), # type: ignore[call-arg] "my-even-cooler-labware-id", ), ) @@ -211,20 +218,19 @@ async def test_gripper_move_labware_implementation( """It should delegate to the equipment handler and return the new offset.""" from_location = DeckSlotLocation(slotName=DeckSlotName.SLOT_1) new_location = DeckSlotLocation(slotName=DeckSlotName.SLOT_5) + pick_up_offset = LabwareOffsetVector(x=1, y=2, z=3) data = MoveLabwareParams( labwareId="my-cool-labware-id", - newLocation=DeckSlotLocation(slotName=DeckSlotName.SLOT_4), + newLocation=new_location, strategy=LabwareMovementStrategy.USING_GRIPPER, - pickUpOffset=LabwareOffsetVector(x=1, y=2, z=3), + pickUpOffset=pick_up_offset, dropOffset=None, ) decoy.when( state_view.labware.get_definition(labware_id="my-cool-labware-id") - ).then_return( - LabwareDefinition.construct(namespace="my-cool-namespace") # type: ignore[call-arg] - ) + ).then_return(sentinel.labware_definition) decoy.when(state_view.labware.get(labware_id="my-cool-labware-id")).then_return( LoadedLabware( id="my-cool-labware-id", @@ -235,29 +241,25 @@ async def test_gripper_move_labware_implementation( ) ) decoy.when( - state_view.geometry.ensure_location_not_occupied( - location=DeckSlotLocation(slotName=DeckSlotName.SLOT_4), - ) - ).then_return(DeckSlotLocation(slotName=DeckSlotName.SLOT_5)) + state_view.geometry.ensure_location_not_occupied(location=new_location) + ).then_return(sentinel.new_location_validated_unoccupied) decoy.when( equipment.find_applicable_labware_offset_id( labware_definition_uri="opentrons-test/load-name/1", - labware_location=new_location, + labware_location=sentinel.new_location_validated_unoccupied, ) ).then_return("wowzers-a-new-offset-id") - validated_from_location = DeckSlotLocation(slotName=DeckSlotName.SLOT_6) - validated_new_location = DeckSlotLocation(slotName=DeckSlotName.SLOT_7) decoy.when( state_view.geometry.ensure_valid_gripper_location(from_location) - ).then_return(validated_from_location) - decoy.when( - state_view.geometry.ensure_valid_gripper_location(new_location) - ).then_return(validated_new_location) + ).then_return(sentinel.from_location_validated_for_gripper) decoy.when( - labware_validation.validate_gripper_compatible( - LabwareDefinition.construct(namespace="my-cool-namespace") # type: ignore[call-arg] + state_view.geometry.ensure_valid_gripper_location( + sentinel.new_location_validated_unoccupied ) + ).then_return(sentinel.new_location_validated_for_gripper) + decoy.when( + labware_validation.validate_gripper_compatible(sentinel.labware_definition) ).then_return(True) result = await subject.execute(data) @@ -265,10 +267,10 @@ async def test_gripper_move_labware_implementation( state_view.labware.raise_if_labware_has_labware_on_top("my-cool-labware-id"), await labware_movement.move_labware_with_gripper( labware_id="my-cool-labware-id", - current_location=validated_from_location, - new_location=validated_new_location, + current_location=sentinel.from_location_validated_for_gripper, + new_location=sentinel.new_location_validated_for_gripper, user_offset_data=LabwareMovementOffsetData( - pickUpOffset=LabwareOffsetVector(x=1, y=2, z=3), + pickUpOffset=pick_up_offset, dropOffset=LabwareOffsetVector(x=0, y=0, z=0), ), post_drop_slide_offset=None, @@ -282,9 +284,12 @@ async def test_gripper_move_labware_implementation( pipette_location=update_types.CLEAR, labware_location=update_types.LabwareLocationUpdate( labware_id="my-cool-labware-id", - new_location=new_location, + new_location=sentinel.new_location_validated_unoccupied, offset_id="wowzers-a-new-offset-id", ), + addressable_area_used=update_types.AddressableAreaUsedUpdate( + addressable_area_name=new_location.slotName.id + ), ), ) @@ -310,7 +315,7 @@ async def test_gripper_error( labware_namespace = "labware-namespace" labware_load_name = "load-name" labware_definition_uri = "opentrons-test/load-name/1" - labware_def = LabwareDefinition.construct( # type: ignore[call-arg] + labware_def = LabwareDefinition.model_construct( # type: ignore[call-arg] namespace=labware_namespace, ) original_location = DeckSlotLocation(slotName=DeckSlotName.SLOT_A1) @@ -320,7 +325,7 @@ async def test_gripper_error( # Common MoveLabwareImplementation boilerplate: decoy.when(state_view.labware.get_definition(labware_id=labware_id)).then_return( - LabwareDefinition.construct(namespace=labware_namespace) # type: ignore[call-arg] + LabwareDefinition.model_construct(namespace=labware_namespace) # type: ignore[call-arg] ) decoy.when(state_view.labware.get(labware_id=labware_id)).then_return( LoadedLabware( @@ -370,7 +375,7 @@ async def test_gripper_error( result = await subject.execute(params) assert result == DefinedErrorData( - public=GripperMovementError.construct( + public=GripperMovementError.model_construct( id=error_id, createdAt=error_created_at, errorCode=underlying_exception.code.value.code, @@ -380,6 +385,9 @@ async def test_gripper_error( state_update=update_types.StateUpdate( labware_location=update_types.NO_CHANGE, pipette_location=update_types.CLEAR, + addressable_area_used=update_types.AddressableAreaUsedUpdate( + addressable_area_name=new_location.slotName.id + ), ), ) @@ -455,7 +463,7 @@ async def test_gripper_move_to_waste_chute_implementation( pickUpOffset=LabwareOffsetVector(x=1, y=2, z=3), dropOffset=None, ) - labware_def = LabwareDefinition.construct( # type: ignore[call-arg] + labware_def = LabwareDefinition.model_construct( # type: ignore[call-arg] namespace="my-cool-namespace", dimensions=Dimensions( yDimension=labware_width, zDimension=labware_width, xDimension=labware_width @@ -520,6 +528,9 @@ async def test_gripper_move_to_waste_chute_implementation( new_location=new_location, offset_id="wowzers-a-new-offset-id", ), + addressable_area_used=update_types.AddressableAreaUsedUpdate( + addressable_area_name=new_location.addressableAreaName + ), ), ) @@ -660,8 +671,8 @@ async def test_move_labware_raises_when_moving_adapter_with_gripper( strategy=LabwareMovementStrategy.USING_GRIPPER, ) - definition = LabwareDefinition.construct( # type: ignore[call-arg] - parameters=Parameters.construct(loadName="My cool adapter"), # type: ignore[call-arg] + definition = LabwareDefinition.model_construct( # type: ignore[call-arg] + parameters=Parameters.model_construct(loadName="My cool adapter"), # type: ignore[call-arg] ) decoy.when(state_view.labware.get(labware_id="my-cool-labware-id")).then_return( @@ -701,8 +712,8 @@ async def test_move_labware_raises_when_moving_labware_with_gripper_incompatible strategy=LabwareMovementStrategy.USING_GRIPPER, ) - definition = LabwareDefinition.construct( # type: ignore[call-arg] - parameters=Parameters.construct(loadName="My cool labware"), # type: ignore[call-arg] + definition = LabwareDefinition.model_construct( # type: ignore[call-arg] + parameters=Parameters.model_construct(loadName="My cool labware"), # type: ignore[call-arg] ) decoy.when(state_view.labware.get(labware_id="my-cool-labware-id")).then_return( @@ -751,7 +762,7 @@ async def test_move_labware_with_gripper_raises_on_ot2( decoy.when( state_view.labware.get_definition(labware_id="my-cool-labware-id") ).then_return( - LabwareDefinition.construct(namespace="spacename") # type: ignore[call-arg] + LabwareDefinition.model_construct(namespace="spacename") # type: ignore[call-arg] ) decoy.when(state_view.config).then_return( @@ -773,8 +784,10 @@ async def test_move_labware_raises_when_moving_fixed_trash_labware( strategy=LabwareMovementStrategy.USING_GRIPPER, ) - definition = LabwareDefinition.construct( # type: ignore[call-arg] - parameters=Parameters.construct(loadName="My cool labware", quirks=["fixedTrash"]), # type: ignore[call-arg] + definition = LabwareDefinition.model_construct( # type: ignore[call-arg] + parameters=Parameters.model_construct( # type: ignore[call-arg] + loadName="My cool labware", quirks=["fixedTrash"] + ), ) decoy.when(state_view.labware.get(labware_id="my-cool-labware-id")).then_return( diff --git a/api/tests/opentrons/protocol_engine/commands/test_move_relative.py b/api/tests/opentrons/protocol_engine/commands/test_move_relative.py index 01522e4dc45..7a993c16d35 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_move_relative.py +++ b/api/tests/opentrons/protocol_engine/commands/test_move_relative.py @@ -1,25 +1,38 @@ """Test move relative commands.""" -from decoy import Decoy +from datetime import datetime + +from decoy import Decoy, matchers +import pytest + +from opentrons_shared_data.errors.exceptions import StallOrCollisionDetectedError from opentrons.protocol_engine.state import update_types from opentrons.protocol_engine.types import DeckPoint, MovementAxis from opentrons.protocol_engine.execution import MovementHandler from opentrons.types import Point -from opentrons.protocol_engine.commands.command import SuccessData +from opentrons.protocol_engine.commands.command import SuccessData, DefinedErrorData +from opentrons.protocol_engine.commands.movement_common import StallOrCollisionError from opentrons.protocol_engine.commands.move_relative import ( MoveRelativeParams, MoveRelativeResult, MoveRelativeImplementation, ) +from opentrons.protocol_engine.resources.model_utils import ModelUtils + + +@pytest.fixture +def subject( + movement: MovementHandler, model_utils: ModelUtils +) -> MoveRelativeImplementation: + """Build a MoveRelativeImplementation with injected dependencies.""" + return MoveRelativeImplementation(movement=movement, model_utils=model_utils) async def test_move_relative_implementation( - decoy: Decoy, - movement: MovementHandler, + decoy: Decoy, movement: MovementHandler, subject: MoveRelativeImplementation ) -> None: """A MoveRelative command should have an execution implementation.""" - subject = MoveRelativeImplementation(movement=movement) data = MoveRelativeParams( pipetteId="pipette-id", axis=MovementAxis.X, @@ -46,3 +59,34 @@ async def test_move_relative_implementation( ) ), ) + + +async def test_move_relative_stalls( + decoy: Decoy, + movement: MovementHandler, + model_utils: ModelUtils, + subject: MoveRelativeImplementation, +) -> None: + """A MoveRelative command should handle stalls.""" + data = MoveRelativeParams(pipetteId="pipette-id", axis=MovementAxis.Y, distance=40) + + decoy.when( + await movement.move_relative( + pipette_id="pipette-id", axis=MovementAxis.Y, distance=40 + ) + ).then_raise(StallOrCollisionDetectedError()) + + timestamp = datetime.now() + test_id = "test-id" + + decoy.when(model_utils.get_timestamp()).then_return(timestamp) + decoy.when(model_utils.generate_id()).then_return(test_id) + + result = await subject.execute(data) + + assert result == DefinedErrorData( + public=StallOrCollisionError.model_construct( + id=test_id, createdAt=timestamp, wrappedErrors=[matchers.Anything()] + ), + state_update=update_types.StateUpdate(pipette_location=update_types.CLEAR), + ) diff --git a/api/tests/opentrons/protocol_engine/commands/test_move_to_addressable_area.py b/api/tests/opentrons/protocol_engine/commands/test_move_to_addressable_area.py index 6925fd7cce4..0570d91c8bc 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_move_to_addressable_area.py +++ b/api/tests/opentrons/protocol_engine/commands/test_move_to_addressable_area.py @@ -1,7 +1,10 @@ """Test move to addressable area commands.""" -from decoy import Decoy +from datetime import datetime + +from decoy import Decoy, matchers import pytest +from opentrons_shared_data.errors.exceptions import StallOrCollisionDetectedError from opentrons_shared_data.pipette.types import PipetteNameType from opentrons.protocol_engine import DeckPoint, AddressableOffsetVector, LoadedPipette from opentrons.protocol_engine.execution import MovementHandler @@ -9,12 +12,24 @@ from opentrons.protocol_engine.state.state import StateView from opentrons.types import Point, MountType -from opentrons.protocol_engine.commands.command import SuccessData +from opentrons.protocol_engine.commands.command import SuccessData, DefinedErrorData from opentrons.protocol_engine.commands.move_to_addressable_area import ( MoveToAddressableAreaParams, MoveToAddressableAreaResult, MoveToAddressableAreaImplementation, ) +from opentrons.protocol_engine.commands.movement_common import StallOrCollisionError +from opentrons.protocol_engine.resources.model_utils import ModelUtils + + +@pytest.fixture +def subject( + movement: MovementHandler, state_view: StateView, model_utils: ModelUtils +) -> MoveToAddressableAreaImplementation: + """Build an execution implementation with injected dependencies.""" + return MoveToAddressableAreaImplementation( + movement=movement, state_view=state_view, model_utils=model_utils + ) @pytest.mark.parametrize( @@ -39,12 +54,9 @@ async def test_move_to_addressable_area_implementation_non_gen1( state_view: StateView, movement: MovementHandler, pipette_name: PipetteNameType, + subject: MoveToAddressableAreaImplementation, ) -> None: """A MoveToAddressableArea command should have an execution implementation.""" - subject = MoveToAddressableAreaImplementation( - movement=movement, state_view=state_view - ) - data = MoveToAddressableAreaParams( pipetteId="abc", addressableAreaName="123", @@ -67,6 +79,7 @@ async def test_move_to_addressable_area_implementation_non_gen1( minimum_z_height=4.56, speed=7.89, stay_at_highest_possible_z=True, + ignore_tip_configuration=True, highest_possible_z_extra_offset=None, ) ).then_return(Point(x=9, y=8, z=7)) @@ -80,7 +93,10 @@ async def test_move_to_addressable_area_implementation_non_gen1( pipette_id="abc", new_location=update_types.AddressableArea(addressable_area_name="123"), new_deck_point=DeckPoint(x=9, y=8, z=7), - ) + ), + addressable_area_used=update_types.AddressableAreaUsedUpdate( + addressable_area_name="123" + ), ), ) @@ -102,12 +118,9 @@ async def test_move_to_addressable_area_implementation_with_gen1( state_view: StateView, movement: MovementHandler, pipette_name: PipetteNameType, + subject: MoveToAddressableAreaImplementation, ) -> None: """A MoveToAddressableArea command should have an execution implementation.""" - subject = MoveToAddressableAreaImplementation( - movement=movement, state_view=state_view - ) - data = MoveToAddressableAreaParams( pipetteId="abc", addressableAreaName="123", @@ -130,6 +143,7 @@ async def test_move_to_addressable_area_implementation_with_gen1( minimum_z_height=4.56, speed=7.89, stay_at_highest_possible_z=True, + ignore_tip_configuration=True, highest_possible_z_extra_offset=5.0, ) ).then_return(Point(x=9, y=8, z=7)) @@ -143,6 +157,65 @@ async def test_move_to_addressable_area_implementation_with_gen1( pipette_id="abc", new_location=update_types.AddressableArea(addressable_area_name="123"), new_deck_point=DeckPoint(x=9, y=8, z=7), - ) + ), + addressable_area_used=update_types.AddressableAreaUsedUpdate( + addressable_area_name="123" + ), + ), + ) + + +async def test_move_to_addressable_area_implementation_handles_stalls( + decoy: Decoy, + state_view: StateView, + movement: MovementHandler, + model_utils: ModelUtils, + subject: MoveToAddressableAreaImplementation, +) -> None: + """A MoveToAddressableArea command should handle stalls.""" + data = MoveToAddressableAreaParams( + pipetteId="abc", + addressableAreaName="123", + offset=AddressableOffsetVector(x=1, y=2, z=3), + forceDirect=True, + minimumZHeight=4.56, + speed=7.89, + stayAtHighestPossibleZ=True, + ) + test_id = "test-id" + timestamp = datetime.now() + + decoy.when(state_view.pipettes.get("abc")).then_return( + LoadedPipette( + id="abc", pipetteName=PipetteNameType.P1000_SINGLE, mount=MountType.LEFT + ) + ) + decoy.when(model_utils.generate_id()).then_return(test_id) + decoy.when(model_utils.get_timestamp()).then_return(timestamp) + decoy.when( + await movement.move_to_addressable_area( + pipette_id="abc", + addressable_area_name="123", + offset=AddressableOffsetVector(x=1, y=2, z=3), + force_direct=True, + minimum_z_height=4.56, + speed=7.89, + stay_at_highest_possible_z=True, + ignore_tip_configuration=True, + highest_possible_z_extra_offset=5.0, + ) + ).then_raise(StallOrCollisionDetectedError()) + + result = await subject.execute(data) + + assert result == DefinedErrorData( + public=StallOrCollisionError.model_construct( + id=test_id, createdAt=timestamp, wrappedErrors=[matchers.Anything()] + ), + state_update=update_types.StateUpdate( + pipette_location=update_types.CLEAR, + addressable_area_used=update_types.AddressableAreaUsedUpdate( + addressable_area_name="123" + ), ), ) diff --git a/api/tests/opentrons/protocol_engine/commands/test_move_to_addressable_area_for_drop_tip.py b/api/tests/opentrons/protocol_engine/commands/test_move_to_addressable_area_for_drop_tip.py index faca36d8121..e90bb7271f7 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_move_to_addressable_area_for_drop_tip.py +++ b/api/tests/opentrons/protocol_engine/commands/test_move_to_addressable_area_for_drop_tip.py @@ -1,5 +1,10 @@ """Test move to addressable area for drop tip commands.""" -from decoy import Decoy +from datetime import datetime + +from decoy import Decoy, matchers +import pytest + +from opentrons_shared_data.errors.exceptions import StallOrCollisionDetectedError from opentrons.protocol_engine import DeckPoint, AddressableOffsetVector from opentrons.protocol_engine.execution import MovementHandler @@ -7,24 +12,33 @@ from opentrons.protocol_engine.state.state import StateView from opentrons.types import Point -from opentrons.protocol_engine.commands.command import SuccessData +from opentrons.protocol_engine.resources.model_utils import ModelUtils +from opentrons.protocol_engine.commands.command import SuccessData, DefinedErrorData from opentrons.protocol_engine.commands.move_to_addressable_area_for_drop_tip import ( MoveToAddressableAreaForDropTipParams, MoveToAddressableAreaForDropTipResult, MoveToAddressableAreaForDropTipImplementation, ) +from opentrons.protocol_engine.commands.movement_common import StallOrCollisionError + + +@pytest.fixture +def subject( + state_view: StateView, movement: MovementHandler, model_utils: ModelUtils +) -> MoveToAddressableAreaForDropTipImplementation: + """Get a command implementation with injected dependencies.""" + return MoveToAddressableAreaForDropTipImplementation( + state_view=state_view, movement=movement, model_utils=model_utils + ) async def test_move_to_addressable_area_for_drop_tip_implementation( decoy: Decoy, state_view: StateView, movement: MovementHandler, + subject: MoveToAddressableAreaForDropTipImplementation, ) -> None: """A MoveToAddressableAreaForDropTip command should have an execution implementation.""" - subject = MoveToAddressableAreaForDropTipImplementation( - movement=movement, state_view=state_view - ) - data = MoveToAddressableAreaForDropTipParams( pipetteId="abc", addressableAreaName="123", @@ -50,7 +64,9 @@ async def test_move_to_addressable_area_for_drop_tip_implementation( force_direct=True, minimum_z_height=4.56, speed=7.89, + stay_at_highest_possible_z=False, ignore_tip_configuration=False, + highest_possible_z_extra_offset=None, ) ).then_return(Point(x=9, y=8, z=7)) @@ -63,6 +79,67 @@ async def test_move_to_addressable_area_for_drop_tip_implementation( pipette_id="abc", new_location=update_types.AddressableArea(addressable_area_name="123"), new_deck_point=DeckPoint(x=9, y=8, z=7), - ) + ), + addressable_area_used=update_types.AddressableAreaUsedUpdate( + addressable_area_name="123" + ), + ), + ) + + +async def test_move_to_addressable_area_for_drop_tip_handles_stalls( + decoy: Decoy, + state_view: StateView, + movement: MovementHandler, + model_utils: ModelUtils, + subject: MoveToAddressableAreaForDropTipImplementation, +) -> None: + """A MoveToAddressableAreaForDropTip command should have an execution implementation.""" + data = MoveToAddressableAreaForDropTipParams( + pipetteId="abc", + addressableAreaName="123", + offset=AddressableOffsetVector(x=1, y=2, z=3), + forceDirect=True, + minimumZHeight=4.56, + speed=7.89, + alternateDropLocation=True, + ignoreTipConfiguration=False, + ) + + decoy.when( + state_view.geometry.get_next_tip_drop_location_for_addressable_area( + addressable_area_name="123", pipette_id="abc" + ) + ).then_return(AddressableOffsetVector(x=10, y=11, z=12)) + + decoy.when( + await movement.move_to_addressable_area( + pipette_id="abc", + addressable_area_name="123", + offset=AddressableOffsetVector(x=10, y=11, z=12), + force_direct=True, + minimum_z_height=4.56, + speed=7.89, + stay_at_highest_possible_z=False, + ignore_tip_configuration=False, + highest_possible_z_extra_offset=None, + ) + ).then_raise(StallOrCollisionDetectedError()) + timestamp = datetime.now() + test_id = "test-id" + decoy.when(model_utils.generate_id()).then_return(test_id) + decoy.when(model_utils.get_timestamp()).then_return(timestamp) + + result = await subject.execute(data) + + assert result == DefinedErrorData( + public=StallOrCollisionError.model_construct( + id=test_id, createdAt=timestamp, wrappedErrors=[matchers.Anything()] + ), + state_update=update_types.StateUpdate( + pipette_location=update_types.CLEAR, + addressable_area_used=update_types.AddressableAreaUsedUpdate( + addressable_area_name="123" + ), ), ) diff --git a/api/tests/opentrons/protocol_engine/commands/test_move_to_coordinates.py b/api/tests/opentrons/protocol_engine/commands/test_move_to_coordinates.py index 2e3ada1d3d3..3c9fc10bb1c 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_move_to_coordinates.py +++ b/api/tests/opentrons/protocol_engine/commands/test_move_to_coordinates.py @@ -1,14 +1,19 @@ """Test move-to-coordinates commands.""" -from decoy import Decoy +from datetime import datetime + +import pytest +from decoy import Decoy, matchers + +from opentrons_shared_data.errors.exceptions import StallOrCollisionDetectedError -from opentrons.hardware_control import HardwareControlAPI from opentrons.protocol_engine.execution import MovementHandler from opentrons.protocol_engine.state import update_types -from opentrons.protocol_engine.state.state import StateView from opentrons.protocol_engine.types import DeckPoint +from opentrons.protocol_engine.resources.model_utils import ModelUtils from opentrons.types import Point -from opentrons.protocol_engine.commands.command import SuccessData +from opentrons.protocol_engine.commands.movement_common import StallOrCollisionError +from opentrons.protocol_engine.commands.command import SuccessData, DefinedErrorData from opentrons.protocol_engine.commands.move_to_coordinates import ( MoveToCoordinatesParams, MoveToCoordinatesResult, @@ -16,26 +21,18 @@ ) +@pytest.fixture +def subject( + movement: MovementHandler, model_utils: ModelUtils +) -> MoveToCoordinatesImplementation: + """Build a command implementation with injected dependencies.""" + return MoveToCoordinatesImplementation(movement=movement, model_utils=model_utils) + + async def test_move_to_coordinates_implementation( - decoy: Decoy, - state_view: StateView, - hardware_api: HardwareControlAPI, - movement: MovementHandler, + decoy: Decoy, movement: MovementHandler, subject: MoveToCoordinatesImplementation ) -> None: - """Test the `moveToCoordinates` implementation. - - It should: - - 1. Query the hardware controller for the given pipette's current position - and how high it can go with its current tip. - 2. Plan the movement, taking the above into account, plus the input parameters. - 3. Iterate through the waypoints of the movement. - """ - subject = MoveToCoordinatesImplementation( - state_view=state_view, - movement=movement, - ) - + """Test the `moveToCoordinates` implementation.""" params = MoveToCoordinatesParams( pipetteId="pipette-id", coordinates=DeckPoint(x=1.11, y=2.22, z=3.33), @@ -66,3 +63,42 @@ async def test_move_to_coordinates_implementation( ) ), ) + + +async def test_move_to_coordinates_stall( + decoy: Decoy, + movement: MovementHandler, + model_utils: ModelUtils, + subject: MoveToCoordinatesImplementation, +) -> None: + """It should handle stall errors.""" + params = MoveToCoordinatesParams( + pipetteId="pipette-id", + coordinates=DeckPoint(x=1.11, y=2.22, z=3.33), + minimumZHeight=1234, + forceDirect=True, + speed=567.8, + ) + + decoy.when( + await movement.move_to_coordinates( + pipette_id="pipette-id", + deck_coordinates=DeckPoint(x=1.11, y=2.22, z=3.33), + direct=True, + additional_min_travel_z=1234, + speed=567.8, + ) + ).then_raise(StallOrCollisionDetectedError()) + test_id = "test-id" + timestamp = datetime.now() + decoy.when(model_utils.get_timestamp()).then_return(timestamp) + decoy.when(model_utils.generate_id()).then_return(test_id) + + result = await subject.execute(params=params) + + assert result == DefinedErrorData( + public=StallOrCollisionError.model_construct( + id=test_id, createdAt=timestamp, wrappedErrors=[matchers.Anything()] + ), + state_update=update_types.StateUpdate(pipette_location=update_types.CLEAR), + ) diff --git a/api/tests/opentrons/protocol_engine/commands/test_move_to_well.py b/api/tests/opentrons/protocol_engine/commands/test_move_to_well.py index fdfcfb45af7..56a2691bbee 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_move_to_well.py +++ b/api/tests/opentrons/protocol_engine/commands/test_move_to_well.py @@ -1,6 +1,11 @@ """Test move to well commands.""" + +from datetime import datetime + import pytest -from decoy import Decoy +from decoy import Decoy, matchers + +from opentrons_shared_data.errors.exceptions import StallOrCollisionDetectedError from opentrons.protocol_engine import ( WellLocation, @@ -12,13 +17,15 @@ from opentrons.protocol_engine.state import update_types from opentrons.types import Point -from opentrons.protocol_engine.commands.command import SuccessData +from opentrons.protocol_engine.commands.command import SuccessData, DefinedErrorData from opentrons.protocol_engine.commands.move_to_well import ( MoveToWellParams, MoveToWellResult, MoveToWellImplementation, ) +from opentrons.protocol_engine.commands.movement_common import StallOrCollisionError from opentrons.protocol_engine.state.state import StateView +from opentrons.protocol_engine.resources.model_utils import ModelUtils @pytest.fixture @@ -27,13 +34,22 @@ def mock_state_view(decoy: Decoy) -> StateView: return decoy.mock(cls=StateView) +@pytest.fixture +def mock_model_utils(decoy: Decoy) -> ModelUtils: + """Get a mock ModelUtils.""" + return decoy.mock(cls=ModelUtils) + + async def test_move_to_well_implementation( decoy: Decoy, state_view: StateView, movement: MovementHandler, + mock_model_utils: ModelUtils, ) -> None: """A MoveToWell command should have an execution implementation.""" - subject = MoveToWellImplementation(state_view=state_view, movement=movement) + subject = MoveToWellImplementation( + state_view=state_view, movement=movement, model_utils=mock_model_utils + ) data = MoveToWellParams( pipetteId="abc", @@ -54,6 +70,8 @@ async def test_move_to_well_implementation( force_direct=True, minimum_z_height=4.56, speed=7.89, + current_well=None, + operation_volume=None, ) ).then_return(Point(x=9, y=8, z=7)) @@ -75,9 +93,12 @@ async def test_move_to_well_with_tip_rack_and_volume_offset( decoy: Decoy, mock_state_view: StateView, movement: MovementHandler, + mock_model_utils: ModelUtils, ) -> None: """It should disallow movement to a tip rack when volumeOffset is specified.""" - subject = MoveToWellImplementation(state_view=mock_state_view, movement=movement) + subject = MoveToWellImplementation( + state_view=mock_state_view, movement=movement, model_utils=mock_model_utils + ) data = MoveToWellParams( pipetteId="abc", @@ -93,3 +114,52 @@ async def test_move_to_well_with_tip_rack_and_volume_offset( with pytest.raises(errors.LabwareIsTipRackError): await subject.execute(data) + + +async def test_move_to_well_stall_defined_error( + decoy: Decoy, + mock_state_view: StateView, + movement: MovementHandler, + mock_model_utils: ModelUtils, +) -> None: + """It should catch StallOrCollisionError exceptions and make them DefinedErrors.""" + error_id = "error-id" + error_timestamp = datetime(year=2020, month=1, day=2) + decoy.when( + await movement.move_to_well( + pipette_id="abc", + labware_id="123", + well_name="A3", + well_location=WellLocation(offset=WellOffset(x=1, y=2, z=3)), + force_direct=True, + minimum_z_height=4.56, + speed=7.89, + current_well=None, + operation_volume=None, + ) + ).then_raise(StallOrCollisionDetectedError()) + decoy.when(mock_model_utils.generate_id()).then_return(error_id) + decoy.when(mock_model_utils.get_timestamp()).then_return(error_timestamp) + + subject = MoveToWellImplementation( + state_view=mock_state_view, movement=movement, model_utils=mock_model_utils + ) + + data = MoveToWellParams( + pipetteId="abc", + labwareId="123", + wellName="A3", + wellLocation=WellLocation(offset=WellOffset(x=1, y=2, z=3)), + forceDirect=True, + minimumZHeight=4.56, + speed=7.89, + ) + + result = await subject.execute(data) + assert isinstance(result, DefinedErrorData) + assert result == DefinedErrorData( + public=StallOrCollisionError.model_construct( + id=error_id, createdAt=error_timestamp, wrappedErrors=[matchers.Anything()] + ), + state_update=update_types.StateUpdate(pipette_location=update_types.CLEAR), + ) diff --git a/api/tests/opentrons/protocol_engine/commands/test_pick_up_tip.py b/api/tests/opentrons/protocol_engine/commands/test_pick_up_tip.py index 00a3bc1c8a8..d4c53ea5992 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_pick_up_tip.py +++ b/api/tests/opentrons/protocol_engine/commands/test_pick_up_tip.py @@ -1,9 +1,13 @@ """Test pick up tip commands.""" + from datetime import datetime from decoy import Decoy, matchers from unittest.mock import sentinel + +from opentrons_shared_data.errors.exceptions import StallOrCollisionDetectedError + from opentrons.types import MountType, Point from opentrons.protocol_engine import ( @@ -19,6 +23,7 @@ from opentrons.protocol_engine.state.state import StateView from opentrons.protocol_engine.types import TipGeometry +from opentrons.protocol_engine.commands.movement_common import StallOrCollisionError from opentrons.protocol_engine.commands.command import DefinedErrorData, SuccessData from opentrons.protocol_engine.commands.pick_up_tip import ( PickUpTipParams, @@ -57,6 +62,11 @@ async def test_success( labware_id="labware-id", well_name="A3", well_location=WellLocation(offset=WellOffset(x=1, y=2, z=3)), + current_well=None, + force_direct=False, + minimum_z_height=None, + speed=None, + operation_volume=None, ) ).then_return(Point(x=111, y=222, z=333)) @@ -97,6 +107,9 @@ async def test_success( tips_used=update_types.TipsUsedUpdate( pipette_id="pipette-id", labware_id="labware-id", well_name="A3" ), + pipette_aspirated_fluid=update_types.PipetteEmptyFluidUpdate( + pipette_id="pipette-id" + ), ), ) @@ -134,6 +147,11 @@ async def test_tip_physically_missing_error( labware_id="labware-id", well_name="well-name", well_location=WellLocation(offset=WellOffset()), + current_well=None, + force_direct=False, + minimum_z_height=None, + speed=None, + operation_volume=None, ) ).then_return(Point(x=111, y=222, z=333)) decoy.when( @@ -149,7 +167,7 @@ async def test_tip_physically_missing_error( ) assert result == DefinedErrorData( - public=TipPhysicallyMissingError.construct( + public=TipPhysicallyMissingError.model_construct( id=error_id, createdAt=error_created_at, wrappedErrors=[matchers.Anything()] ), state_update=update_types.StateUpdate( @@ -163,10 +181,84 @@ async def test_tip_physically_missing_error( tips_used=update_types.TipsUsedUpdate( pipette_id="pipette-id", labware_id="labware-id", well_name="well-name" ), + pipette_aspirated_fluid=update_types.PipetteUnknownFluidUpdate( + pipette_id="pipette-id" + ), ), state_update_if_false_positive=update_types.StateUpdate( pipette_tip_state=update_types.PipetteTipStateUpdate( pipette_id="pipette-id", tip_geometry=sentinel.tip_geometry - ) + ), + pipette_aspirated_fluid=update_types.PipetteEmptyFluidUpdate( + pipette_id="pipette-id" + ), + tips_used=update_types.TipsUsedUpdate( + pipette_id="pipette-id", labware_id="labware-id", well_name="well-name" + ), + pipette_location=update_types.PipetteLocationUpdate( + pipette_id="pipette-id", + new_location=update_types.Well( + labware_id="labware-id", well_name="well-name" + ), + new_deck_point=DeckPoint(x=111, y=222, z=333), + ), + ), + ) + + +async def test_stall_error( + decoy: Decoy, + state_view: StateView, + movement: MovementHandler, + tip_handler: TipHandler, + model_utils: ModelUtils, +) -> None: + """It should return a TipPhysicallyMissingError if the HW API indicates that.""" + subject = PickUpTipImplementation( + state_view=state_view, + movement=movement, + tip_handler=tip_handler, + model_utils=model_utils, + ) + + pipette_id = "pipette-id" + labware_id = "labware-id" + well_name = "well-name" + error_id = "error-id" + error_created_at = datetime(1234, 5, 6) + + decoy.when( + state_view.geometry.convert_pick_up_tip_well_location( + well_location=PickUpTipWellLocation(offset=WellOffset()) + ) + ).then_return(WellLocation(offset=WellOffset())) + + decoy.when( + await movement.move_to_well( + pipette_id="pipette-id", + labware_id="labware-id", + well_name="well-name", + well_location=WellLocation(offset=WellOffset()), + current_well=None, + force_direct=False, + minimum_z_height=None, + speed=None, + operation_volume=None, + ) + ).then_raise(StallOrCollisionDetectedError()) + + decoy.when(model_utils.generate_id()).then_return(error_id) + decoy.when(model_utils.get_timestamp()).then_return(error_created_at) + + result = await subject.execute( + PickUpTipParams(pipetteId=pipette_id, labwareId=labware_id, wellName=well_name) + ) + + assert result == DefinedErrorData( + public=StallOrCollisionError.model_construct( + id=error_id, createdAt=error_created_at, wrappedErrors=[matchers.Anything()] + ), + state_update=update_types.StateUpdate( + pipette_location=update_types.CLEAR, ), ) diff --git a/api/tests/opentrons/protocol_engine/commands/test_prepare_to_aspirate.py b/api/tests/opentrons/protocol_engine/commands/test_prepare_to_aspirate.py index bb4f8c5f4d9..5e77529f646 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_prepare_to_aspirate.py +++ b/api/tests/opentrons/protocol_engine/commands/test_prepare_to_aspirate.py @@ -17,6 +17,7 @@ from opentrons.protocol_engine.execution.gantry_mover import GantryMover from opentrons.protocol_engine.resources.model_utils import ModelUtils from opentrons.protocol_engine.commands.pipetting_common import OverpressureError +from opentrons.protocol_engine.state import update_types from opentrons_shared_data.errors.exceptions import PipetteOverpressureError @@ -32,18 +33,30 @@ def subject( ) -async def test_prepare_to_aspirate_implmenetation( - decoy: Decoy, subject: PrepareToAspirateImplementation, pipetting: PipettingHandler +async def test_prepare_to_aspirate_implementation( + decoy: Decoy, + gantry_mover: GantryMover, + subject: PrepareToAspirateImplementation, + pipetting: PipettingHandler, ) -> None: """A PrepareToAspirate command should have an executing implementation.""" data = PrepareToAspirateParams(pipetteId="some id") + position = Point(x=1, y=2, z=3) decoy.when(await pipetting.prepare_for_aspirate(pipette_id="some id")).then_return( None ) + decoy.when(await gantry_mover.get_position("some id")).then_return(position) result = await subject.execute(data) - assert result == SuccessData(public=PrepareToAspirateResult()) + assert result == SuccessData( + public=PrepareToAspirateResult(), + state_update=update_types.StateUpdate( + pipette_aspirated_fluid=update_types.PipetteEmptyFluidUpdate( + pipette_id="some id" + ) + ), + ) async def test_overpressure_error( @@ -78,10 +91,15 @@ async def test_overpressure_error( result = await subject.execute(data) assert result == DefinedErrorData( - public=OverpressureError.construct( + public=OverpressureError.model_construct( id=error_id, createdAt=error_timestamp, wrappedErrors=[matchers.Anything()], errorInfo={"retryLocation": (position.x, position.y, position.z)}, ), + state_update=update_types.StateUpdate( + pipette_aspirated_fluid=update_types.PipetteUnknownFluidUpdate( + pipette_id="pipette-id" + ) + ), ) diff --git a/api/tests/opentrons/protocol_engine/commands/test_touch_tip.py b/api/tests/opentrons/protocol_engine/commands/test_touch_tip.py index d00f44fd108..5756810c9ee 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_touch_tip.py +++ b/api/tests/opentrons/protocol_engine/commands/test_touch_tip.py @@ -1,10 +1,12 @@ """Test touch tip commands.""" + import pytest from decoy import Decoy from opentrons.hardware_control.types import CriticalPoint from opentrons.motion_planning import Waypoint from opentrons.protocol_engine import WellLocation, WellOffset, DeckPoint, errors +from opentrons.protocol_engine.resources import ModelUtils from opentrons.protocol_engine.execution import MovementHandler, GantryMover from opentrons.protocol_engine.state import update_types from opentrons.protocol_engine.state.state import StateView @@ -24,6 +26,12 @@ def mock_state_view(decoy: Decoy) -> StateView: return decoy.mock(cls=StateView) +@pytest.fixture +def mock_model_utils(decoy: Decoy) -> ModelUtils: + """Get a mock ModelUtils.""" + return decoy.mock(cls=ModelUtils) + + @pytest.fixture def mock_movement_handler(decoy: Decoy) -> MovementHandler: """Get a mock MovementHandler.""" @@ -41,12 +49,14 @@ def subject( mock_state_view: StateView, mock_movement_handler: MovementHandler, mock_gantry_mover: GantryMover, + mock_model_utils: ModelUtils, ) -> TouchTipImplementation: """Get the test subject.""" return TouchTipImplementation( state_view=mock_state_view, movement=mock_movement_handler, gantry_mover=mock_gantry_mover, + model_utils=mock_model_utils, ) @@ -73,6 +83,11 @@ async def test_touch_tip_implementation( labware_id="123", well_name="A3", well_location=WellLocation(offset=WellOffset(x=1, y=2, z=3)), + current_well=None, + force_direct=False, + minimum_z_height=None, + speed=None, + operation_volume=None, ) ).then_return(Point(x=1, y=2, z=3)) @@ -87,8 +102,99 @@ async def test_touch_tip_implementation( pipette_id="abc", labware_id="123", well_name="A3", - center_point=Point(x=1, y=2, z=3), radius=0.456, + mm_from_edge=0, + center_point=Point(x=1, y=2, z=3), + ) + ).then_return( + [ + Waypoint( + position=Point(x=11, y=22, z=33), + critical_point=CriticalPoint.XY_CENTER, + ), + Waypoint( + position=Point(x=44, y=55, z=66), + critical_point=CriticalPoint.XY_CENTER, + ), + ] + ) + + decoy.when( + await mock_gantry_mover.move_to( + pipette_id="abc", + waypoints=[ + Waypoint( + position=Point(x=11, y=22, z=33), + critical_point=CriticalPoint.XY_CENTER, + ), + Waypoint( + position=Point(x=44, y=55, z=66), + critical_point=CriticalPoint.XY_CENTER, + ), + ], + speed=9001, + ) + ).then_return(Point(x=4, y=5, z=6)) + + result = await subject.execute(params) + + assert result == SuccessData( + public=TouchTipResult(position=DeckPoint(x=4, y=5, z=6)), + state_update=update_types.StateUpdate( + pipette_location=update_types.PipetteLocationUpdate( + pipette_id="abc", + new_location=update_types.Well(labware_id="123", well_name="A3"), + new_deck_point=DeckPoint(x=4, y=5, z=6), + ) + ), + ) + + +async def test_touch_tip_implementation_with_mm_to_edge( + decoy: Decoy, + mock_state_view: StateView, + mock_movement_handler: MovementHandler, + mock_gantry_mover: GantryMover, + subject: TouchTipImplementation, +) -> None: + """A TouchTip command should use mmFromEdge if provided.""" + params = TouchTipParams( + pipetteId="abc", + labwareId="123", + wellName="A3", + wellLocation=WellLocation(offset=WellOffset(x=1, y=2, z=3)), + mmFromEdge=0.789, + speed=42.0, + ) + + decoy.when( + await mock_movement_handler.move_to_well( + pipette_id="abc", + labware_id="123", + well_name="A3", + well_location=WellLocation(offset=WellOffset(x=1, y=2, z=3)), + current_well=None, + force_direct=False, + minimum_z_height=None, + speed=None, + operation_volume=None, + ) + ).then_return(Point(x=1, y=2, z=3)) + + decoy.when( + mock_state_view.pipettes.get_movement_speed( + pipette_id="abc", requested_speed=42.0 + ) + ).then_return(9001) + + decoy.when( + mock_state_view.motion.get_touch_tip_waypoints( + pipette_id="abc", + labware_id="123", + well_name="A3", + radius=1.0, + mm_from_edge=0.789, + center_point=Point(x=1, y=2, z=3), ) ).then_return( [ @@ -168,3 +274,20 @@ async def test_touch_tip_no_tip_racks( with pytest.raises(errors.LabwareIsTipRackError): await subject.execute(params) + + +async def test_touch_tip_incompatible_arguments( + decoy: Decoy, mock_state_view: StateView, subject: TouchTipImplementation +) -> None: + """It should disallow touch tip if radius and mmFromEdge is provided.""" + params = TouchTipParams( + pipetteId="abc", + labwareId="123", + wellName="A3", + wellLocation=WellLocation(), + radius=1.23, + mmFromEdge=4.56, + ) + + with pytest.raises(errors.TouchTipIncompatibleArgumentsError): + await subject.execute(params) diff --git a/api/tests/opentrons/protocol_engine/commands/test_verify_tip_presence.py b/api/tests/opentrons/protocol_engine/commands/test_verify_tip_presence.py index 53eb1f5a59e..ef6d79629be 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_verify_tip_presence.py +++ b/api/tests/opentrons/protocol_engine/commands/test_verify_tip_presence.py @@ -23,13 +23,13 @@ async def test_verify_tip_presence_implementation( expectedState=TipPresenceStatus.PRESENT, ) - decoy.when( + result = await subject.execute(data) + + assert result == SuccessData(public=VerifyTipPresenceResult()) + decoy.verify( await tip_handler.verify_tip_presence( pipette_id="pipette-id", expected=TipPresenceStatus.PRESENT, + follow_singular_sensor=None, ) - ).then_return(None) - - result = await subject.execute(data) - - assert result == SuccessData(public=VerifyTipPresenceResult()) + ) diff --git a/api/tests/opentrons/protocol_engine/commands/unsafe/test_engage_axes.py b/api/tests/opentrons/protocol_engine/commands/unsafe/test_engage_axes.py index 72fb761ad23..1f40523e4e1 100644 --- a/api/tests/opentrons/protocol_engine/commands/unsafe/test_engage_axes.py +++ b/api/tests/opentrons/protocol_engine/commands/unsafe/test_engage_axes.py @@ -22,21 +22,28 @@ async def test_engage_axes_implementation( ) data = UnsafeEngageAxesParams( - axes=[MotorAxis.LEFT_Z, MotorAxis.LEFT_PLUNGER, MotorAxis.X, MotorAxis.Y] - ) - - decoy.when(gantry_mover.motor_axis_to_hardware_axis(MotorAxis.LEFT_Z)).then_return( - Axis.Z_L + axes=[ + MotorAxis.LEFT_Z, + MotorAxis.LEFT_PLUNGER, + MotorAxis.X, + MotorAxis.Y, + MotorAxis.RIGHT_Z, + MotorAxis.RIGHT_PLUNGER, + ] ) decoy.when( - gantry_mover.motor_axis_to_hardware_axis(MotorAxis.LEFT_PLUNGER) - ).then_return(Axis.P_L) - decoy.when(gantry_mover.motor_axis_to_hardware_axis(MotorAxis.X)).then_return( - Axis.X - ) - decoy.when(gantry_mover.motor_axis_to_hardware_axis(MotorAxis.Y)).then_return( - Axis.Y - ) + gantry_mover.motor_axes_to_present_hardware_axes( + [ + MotorAxis.LEFT_Z, + MotorAxis.LEFT_PLUNGER, + MotorAxis.X, + MotorAxis.Y, + MotorAxis.RIGHT_Z, + MotorAxis.RIGHT_PLUNGER, + ] + ) + ).then_return([Axis.Z_L, Axis.P_L, Axis.X, Axis.Y]) + decoy.when( await ot3_hardware_api.update_axis_position_estimations( [Axis.Z_L, Axis.P_L, Axis.X, Axis.Y] diff --git a/api/tests/opentrons/protocol_engine/commands/unsafe/test_unsafe_blow_out_in_place.py b/api/tests/opentrons/protocol_engine/commands/unsafe/test_unsafe_blow_out_in_place.py index aec5df2620d..88ad9a8ecf8 100644 --- a/api/tests/opentrons/protocol_engine/commands/unsafe/test_unsafe_blow_out_in_place.py +++ b/api/tests/opentrons/protocol_engine/commands/unsafe/test_unsafe_blow_out_in_place.py @@ -3,6 +3,7 @@ from opentrons.types import MountType from opentrons.protocol_engine.state.state import StateView +from opentrons.protocol_engine.state import update_types from opentrons.protocol_engine.commands.unsafe.unsafe_blow_out_in_place import ( UnsafeBlowOutInPlaceParams, UnsafeBlowOutInPlaceResult, @@ -41,7 +42,14 @@ async def test_blow_out_in_place_implementation( result = await subject.execute(data) - assert result == SuccessData(public=UnsafeBlowOutInPlaceResult()) + assert result == SuccessData( + public=UnsafeBlowOutInPlaceResult(), + state_update=update_types.StateUpdate( + pipette_aspirated_fluid=update_types.PipetteEmptyFluidUpdate( + pipette_id="pipette-id" + ) + ), + ) decoy.verify( await ot3_hardware_api.update_axis_position_estimations([Axis.P_L]), diff --git a/api/tests/opentrons/protocol_engine/commands/unsafe/test_unsafe_drop_tip_in_place.py b/api/tests/opentrons/protocol_engine/commands/unsafe/test_unsafe_drop_tip_in_place.py index fb23d96d987..e7c684554c8 100644 --- a/api/tests/opentrons/protocol_engine/commands/unsafe/test_unsafe_drop_tip_in_place.py +++ b/api/tests/opentrons/protocol_engine/commands/unsafe/test_unsafe_drop_tip_in_place.py @@ -1,6 +1,7 @@ """Test unsafe drop tip in place commands.""" from opentrons.protocol_engine.state.update_types import ( PipetteTipStateUpdate, + PipetteUnknownFluidUpdate, StateUpdate, ) import pytest @@ -51,7 +52,10 @@ async def test_drop_tip_implementation( assert result == SuccessData( public=UnsafeDropTipInPlaceResult(), state_update=StateUpdate( - pipette_tip_state=PipetteTipStateUpdate(pipette_id="abc", tip_geometry=None) + pipette_tip_state=PipetteTipStateUpdate( + pipette_id="abc", tip_geometry=None + ), + pipette_aspirated_fluid=PipetteUnknownFluidUpdate(pipette_id="abc"), ), ) diff --git a/api/tests/opentrons/protocol_engine/commands/unsafe/test_update_position_estimators.py b/api/tests/opentrons/protocol_engine/commands/unsafe/test_update_position_estimators.py index 79131994299..e281502308c 100644 --- a/api/tests/opentrons/protocol_engine/commands/unsafe/test_update_position_estimators.py +++ b/api/tests/opentrons/protocol_engine/commands/unsafe/test_update_position_estimators.py @@ -22,26 +22,27 @@ async def test_update_position_estimators_implementation( ) data = UpdatePositionEstimatorsParams( - axes=[MotorAxis.LEFT_Z, MotorAxis.LEFT_PLUNGER, MotorAxis.X, MotorAxis.Y] - ) - - decoy.when(gantry_mover.motor_axis_to_hardware_axis(MotorAxis.LEFT_Z)).then_return( - Axis.Z_L + axes=[ + MotorAxis.LEFT_Z, + MotorAxis.LEFT_PLUNGER, + MotorAxis.X, + MotorAxis.Y, + MotorAxis.RIGHT_Z, + MotorAxis.RIGHT_PLUNGER, + ] ) decoy.when( - gantry_mover.motor_axis_to_hardware_axis(MotorAxis.LEFT_PLUNGER) - ).then_return(Axis.P_L) - decoy.when(gantry_mover.motor_axis_to_hardware_axis(MotorAxis.X)).then_return( - Axis.X - ) - decoy.when(gantry_mover.motor_axis_to_hardware_axis(MotorAxis.Y)).then_return( - Axis.Y - ) - decoy.when( - await ot3_hardware_api.update_axis_position_estimations( - [Axis.Z_L, Axis.P_L, Axis.X, Axis.Y] + gantry_mover.motor_axes_to_present_hardware_axes( + [ + MotorAxis.LEFT_Z, + MotorAxis.LEFT_PLUNGER, + MotorAxis.X, + MotorAxis.Y, + MotorAxis.RIGHT_Z, + MotorAxis.RIGHT_PLUNGER, + ] ) - ).then_return(None) + ).then_return([Axis.Z_L, Axis.P_L, Axis.X, Axis.Y]) result = await subject.execute(data) diff --git a/api/tests/opentrons/protocol_engine/conftest.py b/api/tests/opentrons/protocol_engine/conftest.py index 76c5d754f3e..48ce28e7a98 100644 --- a/api/tests/opentrons/protocol_engine/conftest.py +++ b/api/tests/opentrons/protocol_engine/conftest.py @@ -78,7 +78,7 @@ def ot3_standard_deck_def() -> DeckDefinitionV5: @pytest.fixture(scope="session") def ot2_fixed_trash_def() -> LabwareDefinition: """Get the definition of the OT-2 standard fixed trash.""" - return LabwareDefinition.parse_obj( + return LabwareDefinition.model_validate( load_definition("opentrons_1_trash_1100ml_fixed", 1) ) @@ -86,7 +86,7 @@ def ot2_fixed_trash_def() -> LabwareDefinition: @pytest.fixture(scope="session") def ot2_short_fixed_trash_def() -> LabwareDefinition: """Get the definition of the OT-2 short fixed trash.""" - return LabwareDefinition.parse_obj( + return LabwareDefinition.model_validate( load_definition("opentrons_1_trash_850ml_fixed", 1) ) @@ -94,7 +94,7 @@ def ot2_short_fixed_trash_def() -> LabwareDefinition: @pytest.fixture(scope="session") def ot3_fixed_trash_def() -> LabwareDefinition: """Get the definition of the OT-3 fixed trash.""" - return LabwareDefinition.parse_obj( + return LabwareDefinition.model_validate( load_definition("opentrons_1_trash_3200ml_fixed", 1) ) @@ -102,7 +102,7 @@ def ot3_fixed_trash_def() -> LabwareDefinition: @pytest.fixture(scope="session") def ot3_absorbance_reader_lid() -> LabwareDefinition: """Get the definition of the OT-3 plate reader lid.""" - return LabwareDefinition.parse_obj( + return LabwareDefinition.model_validate( load_definition("opentrons_flex_lid_absorbance_plate_reader_module", 1) ) @@ -110,7 +110,7 @@ def ot3_absorbance_reader_lid() -> LabwareDefinition: @pytest.fixture(scope="session") def well_plate_def() -> LabwareDefinition: """Get the definition of a 96 well plate.""" - return LabwareDefinition.parse_obj( + return LabwareDefinition.model_validate( load_definition("corning_96_wellplate_360ul_flat", 2) ) @@ -118,7 +118,7 @@ def well_plate_def() -> LabwareDefinition: @pytest.fixture(scope="session") def flex_50uL_tiprack() -> LabwareDefinition: """Get the definition of a Flex 50uL tiprack.""" - return LabwareDefinition.parse_obj( + return LabwareDefinition.model_validate( load_definition("opentrons_flex_96_filtertiprack_50ul", 1) ) @@ -126,7 +126,7 @@ def flex_50uL_tiprack() -> LabwareDefinition: @pytest.fixture(scope="session") def adapter_plate_def() -> LabwareDefinition: """Get the definition of a h/s adapter plate.""" - return LabwareDefinition.parse_obj( + return LabwareDefinition.model_validate( load_definition("opentrons_universal_flat_adapter", 1) ) @@ -134,25 +134,31 @@ def adapter_plate_def() -> LabwareDefinition: @pytest.fixture(scope="session") def reservoir_def() -> LabwareDefinition: """Get the definition of single-row reservoir.""" - return LabwareDefinition.parse_obj(load_definition("nest_12_reservoir_15ml", 1)) + return LabwareDefinition.model_validate( + load_definition("nest_12_reservoir_15ml", 1) + ) @pytest.fixture(scope="session") def tip_rack_def() -> LabwareDefinition: """Get the definition of Opentrons 300 uL tip rack.""" - return LabwareDefinition.parse_obj(load_definition("opentrons_96_tiprack_300ul", 1)) + return LabwareDefinition.model_validate( + load_definition("opentrons_96_tiprack_300ul", 1) + ) @pytest.fixture(scope="session") def adapter_def() -> LabwareDefinition: """Get the definition of Opentrons 96 PCR adapter.""" - return LabwareDefinition.parse_obj(load_definition("opentrons_96_pcr_adapter", 1)) + return LabwareDefinition.model_validate( + load_definition("opentrons_96_pcr_adapter", 1) + ) @pytest.fixture(scope="session") def falcon_tuberack_def() -> LabwareDefinition: """Get the definition of the 6-well Falcon tuberack.""" - return LabwareDefinition.parse_obj( + return LabwareDefinition.model_validate( load_definition("opentrons_6_tuberack_falcon_50ml_conical", 1) ) @@ -160,7 +166,7 @@ def falcon_tuberack_def() -> LabwareDefinition: @pytest.fixture(scope="session") def magdeck_well_plate_def() -> LabwareDefinition: """Get the definition of a well place compatible with magdeck.""" - return LabwareDefinition.parse_obj( + return LabwareDefinition.model_validate( load_definition("nest_96_wellplate_100ul_pcr_full_skirt", 1) ) @@ -169,63 +175,63 @@ def magdeck_well_plate_def() -> LabwareDefinition: def tempdeck_v1_def() -> ModuleDefinition: """Get the definition of a V1 tempdeck.""" definition = load_shared_data("module/definitions/3/temperatureModuleV1.json") - return ModuleDefinition.parse_raw(definition) + return ModuleDefinition.model_validate_json(definition) @pytest.fixture(scope="session") def tempdeck_v2_def() -> ModuleDefinition: """Get the definition of a V2 tempdeck.""" definition = load_shared_data("module/definitions/3/temperatureModuleV2.json") - return ModuleDefinition.parse_raw(definition) + return ModuleDefinition.model_validate_json(definition) @pytest.fixture(scope="session") def magdeck_v1_def() -> ModuleDefinition: """Get the definition of a V1 magdeck.""" definition = load_shared_data("module/definitions/3/magneticModuleV1.json") - return ModuleDefinition.parse_raw(definition) + return ModuleDefinition.model_validate_json(definition) @pytest.fixture(scope="session") def magdeck_v2_def() -> ModuleDefinition: """Get the definition of a V2 magdeck.""" definition = load_shared_data("module/definitions/3/magneticModuleV2.json") - return ModuleDefinition.parse_raw(definition) + return ModuleDefinition.model_validate_json(definition) @pytest.fixture(scope="session") def thermocycler_v1_def() -> ModuleDefinition: """Get the definition of a V2 thermocycler.""" definition = load_shared_data("module/definitions/3/thermocyclerModuleV1.json") - return ModuleDefinition.parse_raw(definition) + return ModuleDefinition.model_validate_json(definition) @pytest.fixture(scope="session") def thermocycler_v2_def() -> ModuleDefinition: """Get the definition of a V2 thermocycler.""" definition = load_shared_data("module/definitions/3/thermocyclerModuleV2.json") - return ModuleDefinition.parse_raw(definition) + return ModuleDefinition.model_validate_json(definition) @pytest.fixture(scope="session") def heater_shaker_v1_def() -> ModuleDefinition: """Get the definition of a V1 heater-shaker.""" definition = load_shared_data("module/definitions/3/heaterShakerModuleV1.json") - return ModuleDefinition.parse_raw(definition) + return ModuleDefinition.model_validate_json(definition) @pytest.fixture(scope="session") def mag_block_v1_def() -> ModuleDefinition: """Get the definition of a V1 Mag Block.""" definition = load_shared_data("module/definitions/3/magneticBlockV1.json") - return ModuleDefinition.parse_raw(definition) + return ModuleDefinition.model_validate_json(definition) @pytest.fixture(scope="session") def abs_reader_v1_def() -> ModuleDefinition: """Get the definition of a V1 absorbance plate reader.""" definition = load_shared_data("module/definitions/3/absorbanceReaderV1.json") - return ModuleDefinition.parse_raw(definition) + return ModuleDefinition.model_validate_json(definition) @pytest.fixture(scope="session") diff --git a/api/tests/opentrons/protocol_engine/errors/test_error_occurrence.py b/api/tests/opentrons/protocol_engine/errors/test_error_occurrence.py index a2feb8261f7..531c2decc98 100644 --- a/api/tests/opentrons/protocol_engine/errors/test_error_occurrence.py +++ b/api/tests/opentrons/protocol_engine/errors/test_error_occurrence.py @@ -1,4 +1,5 @@ """Test ErrorOccurrence module.""" + import datetime from typing import List @@ -11,7 +12,7 @@ def test_error_occurrence_schema() -> None: This is explicitly tested because we are overriding the schema due to a default value for errorCode. """ - required_items: List[str] = ErrorOccurrence.schema()["definitions"][ + required_items: List[str] = ErrorOccurrence.model_json_schema()["$defs"][ "ErrorOccurrence" ]["required"] assert "errorCode" in required_items @@ -24,7 +25,7 @@ def test_parse_error_occurrence() -> None: """ input = '{"id": "abcdefg","errorType": "a bad one","createdAt": "2023-06-12 15:08:54.730451","detail": "This is a bad error"}' - result = ErrorOccurrence.parse_raw(input) + result = ErrorOccurrence.model_validate_json(input) expected = ErrorOccurrence( id="abcdefg", 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 eb84ceb018b..d838eaded87 100644 --- a/api/tests/opentrons/protocol_engine/execution/test_command_executor.py +++ b/api/tests/opentrons/protocol_engine/execution/test_command_executor.py @@ -1,11 +1,12 @@ """Smoke tests for the CommandExecutor class.""" + import asyncio from datetime import datetime -from typing import Any, Optional, Type, Union, cast +from typing import Optional, Type, cast, Any, Union import pytest from decoy import Decoy, matchers -from pydantic import BaseModel +from pydantic import BaseModel, PrivateAttr from opentrons.hardware_control import HardwareControlAPI, OT2HardwareControlAPI @@ -13,6 +14,7 @@ from opentrons.protocol_engine.error_recovery_policy import ( ErrorRecoveryPolicy, ErrorRecoveryType, + never_recover, ) from opentrons.protocol_engine.errors.error_occurrence import ErrorOccurrence from opentrons.protocol_engine.errors.exceptions import ( @@ -253,6 +255,12 @@ async def test_execute( TestCommandImplCls = decoy.mock(func=_TestCommandImpl) command_impl = decoy.mock(cls=_TestCommandImpl) + # Note: private attrs (which are attrs that start with _) are instantiated via deep + # copy from a provided default in the model, so if + # _TestCommand()._ImplementationCls != _TestCommand._ImplementationCls.default if + # we provide a default. Therefore, provide a default factory, so we can always have + # the same object. + class _TestCommand( BaseCommand[_TestCommandParams, _TestCommandResult, ErrorOccurrence] ): @@ -260,14 +268,16 @@ class _TestCommand( params: _TestCommandParams result: Optional[_TestCommandResult] - _ImplementationCls: Type[_TestCommandImpl] = TestCommandImplCls + _ImplementationCls: Type[_TestCommandImpl] = PrivateAttr( + default_factory=lambda: TestCommandImplCls + ) command_params = _TestCommandParams() command_result = SuccessData(public=_TestCommandResult()) queued_command = cast( Command, - _TestCommand( + _TestCommand.model_construct( # type: ignore[call-arg] id="command-id", key="command-key", createdAt=datetime(year=2021, month=1, day=1), @@ -278,7 +288,7 @@ class _TestCommand( running_command = cast( Command, - _TestCommand( + _TestCommand.model_construct( # type: ignore[call-arg] id="command-id", key="command-key", createdAt=datetime(year=2021, month=1, day=1), @@ -299,7 +309,7 @@ class _TestCommand( expected_completed_command = cast( Command, - _TestCommand( + _TestCommand.model_construct( id="command-id", key="command-key", createdAt=datetime(year=2021, month=1, day=1), @@ -327,7 +337,6 @@ class _TestCommand( state_store.commands.get(command_id="command-id") ).then_return(running_command) ) - decoy.when( queued_command._ImplementationCls( state_view=state_store, @@ -358,6 +367,10 @@ class _TestCommand( datetime(year=2023, month=3, day=3), ) + decoy.when(state_store.commands.get_error_recovery_policy()).then_return( + never_recover + ) + await subject.execute("command-id") decoy.verify( @@ -421,13 +434,15 @@ class _TestCommand( params: _TestCommandParams result: Optional[_TestCommandResult] - _ImplementationCls: Type[_TestCommandImpl] = TestCommandImplCls + _ImplementationCls: Type[_TestCommandImpl] = PrivateAttr( + default_factory=lambda: TestCommandImplCls + ) command_params = _TestCommandParams() queued_command = cast( Command, - _TestCommand( + _TestCommand.model_construct( # type: ignore[call-arg] id="command-id", key="command-key", createdAt=datetime(year=2021, month=1, day=1), @@ -438,7 +453,7 @@ class _TestCommand( running_command = cast( Command, - _TestCommand( + _TestCommand.model_construct( # type: ignore[call-arg] id="command-id", key="command-key", createdAt=datetime(year=2021, month=1, day=1), @@ -556,7 +571,9 @@ class _TestCommand( params: _TestCommandParams result: Optional[_TestCommandResult] - _ImplementationCls: Type[_TestCommandImpl] = TestCommandImplCls + _ImplementationCls: Type[_TestCommandImpl] = PrivateAttr( + default_factory=lambda: TestCommandImplCls + ) command_params = _TestCommandParams() command_id = "command-id" @@ -569,7 +586,7 @@ class _TestCommand( ) queued_command = cast( Command, - _TestCommand( + _TestCommand.model_construct( # type: ignore[call-arg] id=command_id, key="command-key", createdAt=created_at, @@ -579,7 +596,7 @@ class _TestCommand( ) running_command = cast( Command, - _TestCommand( + _TestCommand.model_construct( # type: ignore[call-arg] id=command_id, key="command-key", createdAt=created_at, diff --git a/api/tests/opentrons/protocol_engine/execution/test_equipment_handler.py b/api/tests/opentrons/protocol_engine/execution/test_equipment_handler.py index b7a020c2d35..29117a894b5 100644 --- a/api/tests/opentrons/protocol_engine/execution/test_equipment_handler.py +++ b/api/tests/opentrons/protocol_engine/execution/test_equipment_handler.py @@ -1,4 +1,5 @@ """Test equipment command execution side effects.""" + import pytest from _pytest.fixtures import SubRequest import inspect @@ -69,6 +70,14 @@ def _make_config(use_virtual_modules: bool) -> Config: ) +@pytest.fixture +def available_sensors() -> pipette_definition.AvailableSensorDefinition: + """Provide a list of sensors.""" + return pipette_definition.AvailableSensorDefinition( + sensors=["pressure", "capacitive", "environment"] + ) + + @pytest.fixture(autouse=True) def patch_mock_pipette_data_provider( decoy: Decoy, @@ -133,6 +142,7 @@ def tip_overlap_versions(request: SubRequest) -> str: def loaded_static_pipette_data( supported_tip_fixture: pipette_definition.SupportedTipsDefinition, target_tip_overlap_data: Dict[str, float], + available_sensors: pipette_definition.AvailableSensorDefinition, ) -> LoadedStaticPipetteData: """Get a pipette config data value object.""" return LoadedStaticPipetteData( @@ -154,6 +164,14 @@ def loaded_static_pipette_data( back_left_corner_offset=Point(x=1, y=2, z=3), front_right_corner_offset=Point(x=4, y=5, z=6), pipette_lld_settings={}, + plunger_positions={ + "top": 0.0, + "bottom": 5.0, + "blow_out": 19.0, + "drop_tip": 20.0, + }, + shaft_ul_per_mm=5.0, + available_sensors=available_sensors, ) @@ -631,7 +649,7 @@ async def test_load_pipette( decoy.when(state_store.config.use_virtual_pipettes).then_return(False) decoy.when(model_utils.generate_id()).then_return("unique-id") decoy.when(state_store.pipettes.get_by_mount(MountType.RIGHT)).then_return( - LoadedPipette.construct(pipetteName=PipetteNameType.P300_MULTI) # type: ignore[call-arg] + LoadedPipette.model_construct(pipetteName=PipetteNameType.P300_MULTI) # type: ignore[call-arg] ) decoy.when(hardware_api.get_attached_instrument(mount=HwMount.LEFT)).then_return( pipette_dict diff --git a/api/tests/opentrons/protocol_engine/execution/test_gantry_mover.py b/api/tests/opentrons/protocol_engine/execution/test_gantry_mover.py index b9dbd798ff2..caa8d0cc3b6 100644 --- a/api/tests/opentrons/protocol_engine/execution/test_gantry_mover.py +++ b/api/tests/opentrons/protocol_engine/execution/test_gantry_mover.py @@ -3,7 +3,8 @@ import pytest from decoy import Decoy -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Dict, Optional +from collections import OrderedDict from opentrons.types import Mount, MountType, Point from opentrons.hardware_control import API as HardwareAPI @@ -466,6 +467,118 @@ async def test_home_z( ) +@pytest.mark.parametrize( + argnames=[ + "axis_map", + "critical_point", + "relative_move", + "expected_mount", + "call_to_hw", + "final_position", + ], + argvalues=[ + [ + {MotorAxis.X: 10.0, MotorAxis.Y: 15.0, MotorAxis.RIGHT_Z: 20.0}, + {MotorAxis.X: 2.0, MotorAxis.Y: 1.0, MotorAxis.RIGHT_Z: 1.0}, + False, + Mount.RIGHT, + OrderedDict( + {HardwareAxis.X: -2.0, HardwareAxis.Y: 4.0, HardwareAxis.A: 19.0} + ), + {HardwareAxis.X: -2.0, HardwareAxis.Y: 4.0, HardwareAxis.A: 9.0}, + ], + [ + {MotorAxis.RIGHT_Z: 20.0}, + None, + True, + Mount.RIGHT, + OrderedDict({HardwareAxis.A: 29.0}), + { + HardwareAxis.X: 10.0, + HardwareAxis.Y: 15.0, + HardwareAxis.Z: 10.0, + HardwareAxis.A: 30.0, + }, + ], + [ + {MotorAxis.AXIS_96_CHANNEL_CAM: 10.0}, + None, + False, + Mount.LEFT, + OrderedDict({HardwareAxis.Q: 10.0}), + {HardwareAxis.Q: 10.0}, + ], + ], +) +@pytest.mark.ot3_only +async def test_move_axes( + decoy: Decoy, + ot3_hardware_api: OT3API, + mock_state_view: StateView, + axis_map: Dict[MotorAxis, float], + critical_point: Optional[Dict[MotorAxis, float]], + expected_mount: Mount, + relative_move: bool, + call_to_hw: "OrderedDict[HardwareAxis, float]", + final_position: Dict[HardwareAxis, float], +) -> None: + """Test the move axes function.""" + subject = HardwareGantryMover( + state_view=mock_state_view, hardware_api=ot3_hardware_api + ) + curr_pos = { + HardwareAxis.X: 10.0, + HardwareAxis.Y: 15.0, + HardwareAxis.Z: 10.0, + HardwareAxis.A: 10.0, + } + call_count = 0 + + def _current_position(mount: Mount, refresh: bool) -> Dict[HardwareAxis, float]: + nonlocal call_count + nonlocal curr_pos + nonlocal final_position + if call_count == 0 and relative_move: + call_count += 1 + return curr_pos + else: + return final_position + + decoy.when( + await ot3_hardware_api.current_position(expected_mount, refresh=True) + ).then_do(_current_position) + + decoy.when(ot3_hardware_api.config.left_mount_offset).then_return(Point(1, 1, 1)) + decoy.when(ot3_hardware_api.config.right_mount_offset).then_return( + Point(10, 10, 10) + ) + decoy.when(ot3_hardware_api.config.gripper_mount_offset).then_return( + Point(0.5, 0.5, 0.5) + ) + + decoy.when(ot3_hardware_api.get_deck_from_machine(curr_pos)).then_return(curr_pos) + + decoy.when(ot3_hardware_api.get_deck_from_machine(final_position)).then_return( + final_position + ) + if not critical_point: + decoy.when(ot3_hardware_api.critical_point_for(expected_mount)).then_return( + Point(1, 1, 1) + ) + + pos = await subject.move_axes(axis_map, critical_point, 100, relative_move) + decoy.verify( + await ot3_hardware_api.move_axes( + position=call_to_hw, speed=100, expect_stalls=False + ), + times=1, + ) + assert pos == { + subject._hardware_axis_to_motor_axis(ax): pos + for ax, pos in final_position.items() + } + + async def test_virtual_get_position( decoy: Decoy, mock_state_view: StateView, @@ -562,3 +675,39 @@ async def test_virtual_move_to( ) assert result == Point(4, 5, 6) + + +@pytest.mark.parametrize( + argnames=["axis_map", "critical_point", "relative_move", "expected_position"], + argvalues=[ + [ + {MotorAxis.X: 10, MotorAxis.Y: 15, MotorAxis.RIGHT_Z: 20}, + {MotorAxis.X: 2, MotorAxis.Y: 1, MotorAxis.RIGHT_Z: 1}, + False, + {MotorAxis.X: 8, MotorAxis.Y: 14, MotorAxis.RIGHT_Z: 19}, + ], + [ + {MotorAxis.RIGHT_Z: 20}, + None, + True, + {MotorAxis.X: 0.0, MotorAxis.Y: 0.0, MotorAxis.RIGHT_Z: 20}, + ], + [ + {MotorAxis.AXIS_96_CHANNEL_CAM: 10}, + None, + False, + {MotorAxis.AXIS_96_CHANNEL_CAM: 10}, + ], + ], +) +async def test_virtual_move_axes( + decoy: Decoy, + virtual_subject: VirtualGantryMover, + axis_map: Dict[MotorAxis, float], + critical_point: Optional[Dict[MotorAxis, float]], + relative_move: bool, + expected_position: Dict[MotorAxis, float], +) -> None: + """It should simulate moving a set of axis by a certain distance.""" + pos = await virtual_subject.move_axes(axis_map, critical_point, 100, relative_move) + assert pos == expected_position diff --git a/api/tests/opentrons/protocol_engine/execution/test_hardware_stopper.py b/api/tests/opentrons/protocol_engine/execution/test_hardware_stopper.py index 503d681bced..c3a1e38d490 100644 --- a/api/tests/opentrons/protocol_engine/execution/test_hardware_stopper.py +++ b/api/tests/opentrons/protocol_engine/execution/test_hardware_stopper.py @@ -1,4 +1,5 @@ """Test hardware stopping execution and side effects.""" + from __future__ import annotations import pytest @@ -78,12 +79,12 @@ async def test_hardware_halt( @pytest.mark.parametrize( - argnames=["post_run_hardware_state", "expected_home_after"], - argvalues=[ - (PostRunHardwareState.STAY_ENGAGED_IN_PLACE, False), - (PostRunHardwareState.DISENGAGE_IN_PLACE, False), - (PostRunHardwareState.HOME_AND_STAY_ENGAGED, True), - (PostRunHardwareState.HOME_THEN_DISENGAGE, True), + "post_run_hardware_state", + [ + PostRunHardwareState.STAY_ENGAGED_IN_PLACE, + PostRunHardwareState.DISENGAGE_IN_PLACE, + PostRunHardwareState.HOME_AND_STAY_ENGAGED, + PostRunHardwareState.HOME_THEN_DISENGAGE, ], ) async def test_hardware_stopping_sequence( @@ -94,7 +95,6 @@ async def test_hardware_stopping_sequence( mock_tip_handler: TipHandler, subject: HardwareStopper, post_run_hardware_state: PostRunHardwareState, - expected_home_after: bool, ) -> None: """It should stop the hardware, and home the robot. Flex no longer performs automatic drop tip..""" decoy.when(state_store.pipettes.get_all_attached_tips()).then_return( @@ -113,7 +113,7 @@ async def test_hardware_stopping_sequence( await movement.home( axes=[MotorAxis.X, MotorAxis.Y, MotorAxis.LEFT_Z, MotorAxis.RIGHT_Z] ), - await hardware_api.stop(home_after=expected_home_after), + await hardware_api.stop(home_after=False), ) @@ -122,6 +122,7 @@ async def test_hardware_stopping_sequence_without_pipette_tips( hardware_api: HardwareAPI, state_store: StateStore, subject: HardwareStopper, + movement: MovementHandler, ) -> None: """Don't drop tip when there aren't any tips attached to pipettes.""" decoy.when(state_store.pipettes.get_all_attached_tips()).then_return([]) @@ -132,7 +133,10 @@ async def test_hardware_stopping_sequence_without_pipette_tips( ) decoy.verify( - await hardware_api.stop(home_after=True), + await hardware_api.stop(home_after=False), + await movement.home( + [MotorAxis.X, MotorAxis.Y, MotorAxis.LEFT_Z, MotorAxis.RIGHT_Z] + ), ) @@ -171,6 +175,7 @@ async def test_hardware_stopping_sequence_no_pipette( state_store: StateStore, hardware_api: HardwareAPI, mock_tip_handler: TipHandler, + movement: MovementHandler, subject: HardwareStopper, ) -> None: """It should gracefully no-op if the HW API reports no attached pipette.""" @@ -193,8 +198,14 @@ async def test_hardware_stopping_sequence_no_pipette( ) decoy.verify( - await hardware_api.stop(home_after=True), - times=1, + await hardware_api.stop(home_after=False), + await movement.home( + [MotorAxis.X, MotorAxis.Y, MotorAxis.LEFT_Z, MotorAxis.RIGHT_Z] + ), + await hardware_api.stop(home_after=False), + await movement.home( + [MotorAxis.X, MotorAxis.Y, MotorAxis.LEFT_Z, MotorAxis.RIGHT_Z] + ), ) @@ -232,7 +243,11 @@ async def test_hardware_stopping_sequence_with_gripper( await movement.home( axes=[MotorAxis.X, MotorAxis.Y, MotorAxis.LEFT_Z, MotorAxis.RIGHT_Z] ), - await ot3_hardware_api.stop(home_after=True), + await ot3_hardware_api.stop(home_after=False), + await ot3_hardware_api.home_z(mount=OT3Mount.GRIPPER), + await movement.home( + axes=[MotorAxis.X, MotorAxis.Y, MotorAxis.LEFT_Z, MotorAxis.RIGHT_Z] + ), ) @@ -284,7 +299,11 @@ async def test_hardware_stopping_sequence_with_fixed_trash( pipette_id="pipette-id", home_after=False, ), - await ot3_hardware_api.stop(home_after=True), + await ot3_hardware_api.stop(home_after=False), + await ot3_hardware_api.home_z(mount=OT3Mount.GRIPPER), + await movement.home( + axes=[MotorAxis.X, MotorAxis.Y, MotorAxis.LEFT_Z, MotorAxis.RIGHT_Z] + ), ) @@ -336,5 +355,8 @@ async def test_hardware_stopping_sequence_with_OT2_addressable_area( pipette_id="pipette-id", home_after=False, ), - await hardware_api.stop(home_after=True), + await hardware_api.stop(home_after=False), + await movement.home( + axes=[MotorAxis.X, MotorAxis.Y, MotorAxis.LEFT_Z, MotorAxis.RIGHT_Z] + ), ) diff --git a/api/tests/opentrons/protocol_engine/execution/test_tip_handler.py b/api/tests/opentrons/protocol_engine/execution/test_tip_handler.py index c03a611966c..b0f5d618361 100644 --- a/api/tests/opentrons/protocol_engine/execution/test_tip_handler.py +++ b/api/tests/opentrons/protocol_engine/execution/test_tip_handler.py @@ -1,4 +1,5 @@ """Pipetting execution handler.""" + import pytest from decoy import Decoy, matchers @@ -51,7 +52,7 @@ def mock_labware_data_provider(decoy: Decoy) -> LabwareDataProvider: @pytest.fixture def tip_rack_definition() -> LabwareDefinition: """Get a tip rack defintion value object.""" - return LabwareDefinition.construct(namespace="test", version=42) # type: ignore[call-arg] + return LabwareDefinition.model_construct(namespace="test", version=42) # type: ignore[call-arg] MOCK_MAP = NozzleMap.build( @@ -114,9 +115,9 @@ async def test_flex_pick_up_tip_state( decoy.when(mock_state_view.labware.get_definition("labware-id")).then_return( tip_rack_definition ) - decoy.when(mock_state_view.pipettes.state.nozzle_configuration_by_id).then_return( - {"pipette-id": MOCK_MAP} - ) + decoy.when( + mock_state_view.pipettes.get_nozzle_configuration("pipette-id") + ).then_return(MOCK_MAP) decoy.when( mock_state_view.geometry.get_nominal_tip_geometry( pipette_id="pipette-id", @@ -189,9 +190,9 @@ async def test_pick_up_tip( MountType.LEFT ) - decoy.when(mock_state_view.pipettes.state.nozzle_configuration_by_id).then_return( - {"pipette-id": MOCK_MAP} - ) + decoy.when( + mock_state_view.pipettes.get_nozzle_configuration(pipette_id="pipette-id") + ).then_return(MOCK_MAP) decoy.when( mock_state_view.geometry.get_nominal_tip_geometry( @@ -249,14 +250,16 @@ async def test_drop_tip( decoy.when(mock_state_view.pipettes.get_mount("pipette-id")).then_return( MountType.RIGHT ) - decoy.when(mock_state_view.pipettes.state.nozzle_configuration_by_id).then_return( - {"pipette-id": MOCK_MAP} - ) + decoy.when( + mock_state_view.pipettes.get_nozzle_configuration("pipette-id") + ).then_return(MOCK_MAP) await subject.drop_tip(pipette_id="pipette-id", home_after=True) decoy.verify( - await mock_hardware_api.tip_drop_moves(mount=Mount.RIGHT, home_after=True) + await mock_hardware_api.tip_drop_moves( + mount=Mount.RIGHT, ignore_plunger=False, home_after=True + ) ) decoy.verify(mock_hardware_api.remove_tip(mount=Mount.RIGHT)) decoy.verify( @@ -558,8 +561,8 @@ async def test_verify_tip_presence_on_ot3( ) decoy.when( - mock_state_view.pipettes.state.nozzle_configuration_by_id - ).then_return({"pipette-id": MOCK_MAP}) + mock_state_view.pipettes.get_nozzle_configuration("pipette-id") + ).then_return(MOCK_MAP) await subject.verify_tip_presence("pipette-id", expected, None) diff --git a/api/tests/opentrons/protocol_engine/resources/test_labware_data_provider.py b/api/tests/opentrons/protocol_engine/resources/test_labware_data_provider.py index 92718c70d89..a666e7a697d 100644 --- a/api/tests/opentrons/protocol_engine/resources/test_labware_data_provider.py +++ b/api/tests/opentrons/protocol_engine/resources/test_labware_data_provider.py @@ -22,7 +22,7 @@ async def test_labware_data_gets_standard_definition() -> None: version=1, ) - assert result == LabwareDefinition.parse_obj(expected) + assert result == LabwareDefinition.model_validate(expected) async def test_labware_hash_match() -> None: @@ -38,9 +38,9 @@ async def test_labware_hash_match() -> None: version=1, ) - labware_model = LabwareDefinition.parse_obj(labware_dict) + labware_model = LabwareDefinition.model_validate(labware_dict) labware_model_dict = cast( - LabwareDefDict, labware_model.dict(exclude_none=True, exclude_unset=True) + LabwareDefDict, labware_model.model_dump(exclude_none=True, exclude_unset=True) ) assert hash_labware_def(labware_dict) == hash_labware_def(labware_model_dict) diff --git a/api/tests/opentrons/protocol_engine/resources/test_labware_validation.py b/api/tests/opentrons/protocol_engine/resources/test_labware_validation.py index 663aec7337f..fbe9d6c21a4 100644 --- a/api/tests/opentrons/protocol_engine/resources/test_labware_validation.py +++ b/api/tests/opentrons/protocol_engine/resources/test_labware_validation.py @@ -14,9 +14,18 @@ @pytest.mark.parametrize( ("definition", "expected_result"), [ - (LabwareDefinition.construct(allowedRoles=[LabwareRole.labware]), True), # type: ignore[call-arg] - (LabwareDefinition.construct(allowedRoles=[]), True), # type: ignore[call-arg] - (LabwareDefinition.construct(allowedRoles=[LabwareRole.adapter]), False), # type: ignore[call-arg] + ( + LabwareDefinition.model_construct(allowedRoles=[LabwareRole.labware]), # type: ignore[call-arg] + True, + ), + ( + LabwareDefinition.model_construct(allowedRoles=[]), # type: ignore[call-arg] + True, + ), + ( + LabwareDefinition.model_construct(allowedRoles=[LabwareRole.adapter]), # type: ignore[call-arg] + False, + ), ], ) def test_validate_definition_is_labware( @@ -29,9 +38,18 @@ def test_validate_definition_is_labware( @pytest.mark.parametrize( ("definition", "expected_result"), [ - (LabwareDefinition.construct(allowedRoles=[LabwareRole.adapter]), True), # type: ignore[call-arg] - (LabwareDefinition.construct(allowedRoles=[]), False), # type: ignore[call-arg] - (LabwareDefinition.construct(allowedRoles=[LabwareRole.labware]), False), # type: ignore[call-arg] + ( + LabwareDefinition.model_construct(allowedRoles=[LabwareRole.adapter]), # type: ignore[call-arg] + True, + ), + ( + LabwareDefinition.model_construct(allowedRoles=[]), # type: ignore[call-arg] + False, + ), + ( + LabwareDefinition.model_construct(allowedRoles=[LabwareRole.labware]), # type: ignore[call-arg] + False, + ), ], ) def test_validate_definition_is_adapter( @@ -44,9 +62,22 @@ def test_validate_definition_is_adapter( @pytest.mark.parametrize( ("definition", "expected_result"), [ - (LabwareDefinition.construct(stackingOffsetWithLabware={"labware123": OverlapOffset(x=4, y=5, z=6)}), True), # type: ignore[call-arg] - (LabwareDefinition.construct(stackingOffsetWithLabware={"labwareXYZ": OverlapOffset(x=4, y=5, z=6)}), False), # type: ignore[call-arg] - (LabwareDefinition.construct(stackingOffsetWithLabware={}), False), # type: ignore[call-arg] + ( + LabwareDefinition.model_construct( # type: ignore[call-arg] + stackingOffsetWithLabware={"labware123": OverlapOffset(x=4, y=5, z=6)} + ), + True, + ), + ( + LabwareDefinition.model_construct( # type: ignore[call-arg] + stackingOffsetWithLabware={"labwareXYZ": OverlapOffset(x=4, y=5, z=6)} + ), + False, + ), + ( + LabwareDefinition.model_construct(stackingOffsetWithLabware={}), # type: ignore[call-arg] + False, + ), ], ) def test_validate_labware_can_be_stacked( @@ -62,9 +93,24 @@ def test_validate_labware_can_be_stacked( @pytest.mark.parametrize( ("definition", "expected_result"), [ - (LabwareDefinition.construct(parameters=Parameters.construct(quirks=None)), True), # type: ignore[call-arg] - (LabwareDefinition.construct(parameters=Parameters.construct(quirks=["foo"])), True), # type: ignore[call-arg] - (LabwareDefinition.construct(parameters=Parameters.construct(quirks=["gripperIncompatible"])), False), # type: ignore[call-arg] + ( + LabwareDefinition.model_construct( # type: ignore[call-arg] + parameters=Parameters.model_construct(quirks=None) # type: ignore[call-arg] + ), + True, + ), + ( + LabwareDefinition.model_construct( # type: ignore[call-arg] + parameters=Parameters.model_construct(quirks=["foo"]) # type: ignore[call-arg] + ), + True, + ), + ( + LabwareDefinition.model_construct( # type: ignore[call-arg] + parameters=Parameters.model_construct(quirks=["gripperIncompatible"]) # type: ignore[call-arg] + ), + False, + ), ], ) def test_validate_gripper_compatible( diff --git a/api/tests/opentrons/protocol_engine/resources/test_pipette_data_provider.py b/api/tests/opentrons/protocol_engine/resources/test_pipette_data_provider.py index 086b3ec297b..ae3d78d2230 100644 --- a/api/tests/opentrons/protocol_engine/resources/test_pipette_data_provider.py +++ b/api/tests/opentrons/protocol_engine/resources/test_pipette_data_provider.py @@ -7,6 +7,7 @@ from opentrons_shared_data.pipette.pipette_definition import ( PipetteBoundingBoxOffsetDefinition, TIP_OVERLAP_VERSION_MAXIMUM, + AvailableSensorDefinition, ) from opentrons.hardware_control.dev_types import PipetteDict @@ -24,6 +25,12 @@ from opentrons.types import Point +@pytest.fixture +def available_sensors() -> AvailableSensorDefinition: + """Provide a list of sensors.""" + return AvailableSensorDefinition(sensors=["pressure", "capacitive", "environment"]) + + @pytest.fixture def subject_instance() -> VirtualPipetteDataProvider: """Instance of a VirtualPipetteDataProvider for test.""" @@ -32,6 +39,7 @@ def subject_instance() -> VirtualPipetteDataProvider: def test_get_virtual_pipette_static_config( subject_instance: VirtualPipetteDataProvider, + available_sensors: AvailableSensorDefinition, ) -> None: """It should return config data given a pipette name.""" result = subject_instance.get_virtual_pipette_static_config( @@ -65,11 +73,20 @@ def test_get_virtual_pipette_static_config( back_left_corner_offset=Point(0, 0, 10.45), front_right_corner_offset=Point(0, 0, 10.45), pipette_lld_settings={}, + plunger_positions={ + "top": 19.5, + "bottom": -8.5, + "blow_out": -13.0, + "drop_tip": -27.0, + }, + shaft_ul_per_mm=0.785, + available_sensors=AvailableSensorDefinition(sensors=[]), ) def test_configure_virtual_pipette_for_volume( subject_instance: VirtualPipetteDataProvider, + available_sensors: AvailableSensorDefinition, ) -> None: """It should return an updated config if the liquid class changes.""" result1 = subject_instance.get_virtual_pipette_static_config( @@ -94,6 +111,14 @@ def test_configure_virtual_pipette_for_volume( back_left_corner_offset=Point(-8.0, -22.0, -259.15), front_right_corner_offset=Point(-8.0, -22.0, -259.15), pipette_lld_settings={"t50": {"minHeight": 1.0, "minVolume": 0.0}}, + plunger_positions={ + "top": 0.0, + "bottom": 71.5, + "blow_out": 76.5, + "drop_tip": 90.5, + }, + shaft_ul_per_mm=0.785, + available_sensors=available_sensors, ) subject_instance.configure_virtual_pipette_for_volume( "my-pipette", 1, result1.model @@ -120,11 +145,20 @@ def test_configure_virtual_pipette_for_volume( back_left_corner_offset=Point(-8.0, -22.0, -259.15), front_right_corner_offset=Point(-8.0, -22.0, -259.15), pipette_lld_settings={"t50": {"minHeight": 1.0, "minVolume": 0.0}}, + plunger_positions={ + "top": 0.0, + "bottom": 61.5, + "blow_out": 76.5, + "drop_tip": 90.5, + }, + shaft_ul_per_mm=0.785, + available_sensors=available_sensors, ) def test_load_virtual_pipette_by_model_string( subject_instance: VirtualPipetteDataProvider, + available_sensors: AvailableSensorDefinition, ) -> None: """It should return config data given a pipette model.""" result = subject_instance.get_virtual_pipette_static_config_by_model_string( @@ -149,6 +183,14 @@ def test_load_virtual_pipette_by_model_string( back_left_corner_offset=Point(-16.0, 43.15, 35.52), front_right_corner_offset=Point(16.0, -43.15, 35.52), pipette_lld_settings={}, + plunger_positions={ + "top": 19.5, + "bottom": -14.5, + "blow_out": -19.0, + "drop_tip": -33.4, + }, + shaft_ul_per_mm=9.621, + available_sensors=AvailableSensorDefinition(sensors=[]), ) @@ -193,6 +235,7 @@ def test_load_virtual_pipette_nozzle_layout( @pytest.fixture def pipette_dict( supported_tip_fixture: pipette_definition.SupportedTipsDefinition, + available_sensors: AvailableSensorDefinition, ) -> PipetteDict: """Get a pipette dict.""" return { @@ -246,6 +289,9 @@ def pipette_dict( "t200": {"minHeight": 0.5, "minVolume": 0}, "t1000": {"minHeight": 0.5, "minVolume": 0}, }, + "plunger_positions": {"top": 100, "bottom": 20, "blow_out": 10, "drop_tip": 0}, + "shaft_ul_per_mm": 5.0, + "available_sensors": available_sensors, } @@ -263,6 +309,7 @@ def test_get_pipette_static_config( pipette_dict: PipetteDict, tip_overlap_version: str, overlap_data: Dict[str, float], + available_sensors: AvailableSensorDefinition, ) -> None: """It should return config data given a PipetteDict.""" result = subject.get_pipette_static_config(pipette_dict, tip_overlap_version) @@ -292,6 +339,9 @@ def test_get_pipette_static_config( "t200": {"minHeight": 0.5, "minVolume": 0}, "t1000": {"minHeight": 0.5, "minVolume": 0}, }, + plunger_positions={"top": 100, "bottom": 20, "blow_out": 10, "drop_tip": 0}, + shaft_ul_per_mm=5.0, + available_sensors=available_sensors, ) diff --git a/api/tests/opentrons/protocol_engine/state/command_fixtures.py b/api/tests/opentrons/protocol_engine/state/command_fixtures.py index 5ac522095f2..31f06858b53 100644 --- a/api/tests/opentrons/protocol_engine/state/command_fixtures.py +++ b/api/tests/opentrons/protocol_engine/state/command_fixtures.py @@ -1,4 +1,5 @@ """Command factories to use in tests as data fixtures.""" + from datetime import datetime from pydantic import BaseModel from typing import Optional, cast, Dict @@ -21,6 +22,12 @@ ) +class FixtureModel(BaseModel): + """Fixture Model.""" + + ... + + def create_queued_command( command_id: str = "command-id", command_key: str = "command-key", @@ -29,6 +36,10 @@ def create_queued_command( params: Optional[BaseModel] = None, ) -> cmd.Command: """Given command data, build a pending command model.""" + + class DummyParams(BaseModel): + pass + return cast( cmd.Command, cmd.BaseCommand( @@ -37,7 +48,7 @@ def create_queued_command( commandType=command_type, createdAt=datetime(year=2021, month=1, day=1), status=cmd.CommandStatus.QUEUED, - params=params or BaseModel(), + params=params or DummyParams(), intent=intent, ), ) @@ -59,7 +70,7 @@ def create_running_command( createdAt=created_at, commandType=command_type, status=cmd.CommandStatus.RUNNING, - params=params or BaseModel(), + params=params or FixtureModel(), ), ) @@ -84,7 +95,7 @@ def create_failed_command( completedAt=completed_at, commandType=command_type, status=cmd.CommandStatus.FAILED, - params=params or BaseModel(), + params=params or FixtureModel(), error=error, intent=intent, ), @@ -108,8 +119,8 @@ def create_succeeded_command( createdAt=created_at, commandType=command_type, status=cmd.CommandStatus.SUCCEEDED, - params=params or BaseModel(), - result=result or BaseModel(), + params=params or FixtureModel(), + result=result or FixtureModel(), ), ) @@ -193,7 +204,7 @@ def create_load_module_command( moduleId=module_id, model=model, serialNumber=None, - definition=ModuleDefinition.construct(), # type: ignore[call-arg] + definition=ModuleDefinition.model_construct(), # type: ignore[call-arg] ) return cmd.LoadModule( diff --git a/api/tests/opentrons/protocol_engine/state/inner_geometry_test_params.py b/api/tests/opentrons/protocol_engine/state/inner_geometry_test_params.py new file mode 100644 index 00000000000..0a60bf4206c --- /dev/null +++ b/api/tests/opentrons/protocol_engine/state/inner_geometry_test_params.py @@ -0,0 +1,150 @@ +"""Arguments needed to test inner geometry. + +Each labware has 2 nominal volumes calculated in solidworks. +- One is a nominal bottom volume, calculated some set distance from the bottom of the inside of the well. +- The other is a nominal top volume, calculated some set distance from the top of the inside of the well. +""" +INNER_WELL_GEOMETRY_TEST_PARAMS = [ + [ + "opentrons_10_tuberack_nest_4x50ml_6x15ml_conical", + "conicalWell15mL", + 16.7, + 15546.9, + 3.0, + 5.0, + ], + [ + "opentrons_10_tuberack_nest_4x50ml_6x15ml_conical", + "conicalWell50mL", + 111.2, + 56110.3, + 3.0, + 5.0, + ], + ["opentrons_24_tuberack_nest_2ml_screwcap", "conicalWell", 66.6, 2104.9, 3.0, 3.0], + [ + "opentrons_24_tuberack_nest_1.5ml_screwcap", + "conicalWell", + 19.5, + 1750.8, + 3.0, + 3.0, + ], + ["nest_1_reservoir_290ml", "cuboidalWell", 16570.380, 271690.520, 3.0, 3.0], + ["opentrons_24_tuberack_nest_2ml_snapcap", "conicalWell", 69.62, 2148.5, 3.0, 3.0], + ["nest_96_wellplate_2ml_deep", "cuboidalWell", 118.3, 2060.4, 3.0, 3.0], + ["opentrons_24_tuberack_nest_1.5ml_snapcap", "conicalWell", 27.8, 1682.3, 3.0, 3.0], + ["nest_12_reservoir_15ml", "cuboidalWell", 1219.0, 13236.1, 3.0, 3.0], + ["nest_1_reservoir_195ml", "cuboidalWell", 14034.2, 172301.9, 3.0, 3.0], + [ + "opentrons_24_tuberack_nest_0.5ml_screwcap", + "conicalWell", + 21.95, + 795.4, + 3.0, + 3.0, + ], + [ + "opentrons_96_wellplate_200ul_pcr_full_skirt", + "conicalWell", + 14.3, + 150.2, + 3.0, + 3.0, + ], + ["nest_96_wellplate_100ul_pcr_full_skirt", "conicalWell", 15.5, 150.8, 3.0, 3.0], + ["nest_96_wellplate_200ul_flat", "conicalWell", 96.3, 259.8, 3.0, 3.0], + [ + "opentrons_10_tuberack_falcon_4x50ml_6x15ml_conical", + "50mlconicalWell", + 163.9, + 57720.5, + 3.0, + 3.0, + ], + [ + "opentrons_10_tuberack_falcon_4x50ml_6x15ml_conical", + "15mlconicalWell", + 40.8, + 15956.6, + 3.0, + 3.0, + ], + ["usascientific_12_reservoir_22ml", "cuboidalWell", 529.36, 21111.5, 3.0, 3.0], + ["thermoscientificnunc_96_wellplate_2000ul", "conicalWell", 73.5, 1768.0, 3.0, 3.0], + [ + "usascientific_96_wellplate_2.4ml_deep", + "cuboidalWell", + 72.220, + 2241.360, + 3.0, + 3.0, + ], + ["agilent_1_reservoir_290ml", "cuboidalWell", 15652.9, 268813.8, 3.0, 3.0], + [ + "opentrons_24_tuberack_eppendorf_1.5ml_safelock_snapcap", + "conicalWell", + 25.8, + 1576.1, + 3.0, + 3.0, + ], + ["thermoscientificnunc_96_wellplate_1300ul", "conicalWell", 73.5, 1155.1, 3.0, 3.0], + ["corning_12_wellplate_6.9ml_flat", "conicalWell", 1156.3, 5654.8, 3.0, 3.0], + ["corning_24_wellplate_3.4ml_flat", "conicalWell", 579.0, 2853.4, 3.0, 3.0], + ["corning_6_wellplate_16.8ml_flat", "conicalWell", 2862.1, 13901.9, 3.0, 3.0], + ["corning_48_wellplate_1.6ml_flat", "conicalWell", 268.9, 1327.0, 3.0, 3.0], + ["biorad_96_wellplate_200ul_pcr", "conicalWell", 17.9, 161.2, 3.0, 3.0], + ["axygen_1_reservoir_90ml", "cuboidalWell", 22373.4, 70450.6, 3.0, 3.0], + ["corning_384_wellplate_112ul_flat", "flatWell", 22.4, 77.4, 2.88, 3.0], + ["corning_96_wellplate_360ul_flat", "conicalWell", 97.2, 257.1, 3.0, 3.0], + ["biorad_384_wellplate_50ul", "conicalWell", 7.7, 27.8, 3.0, 3.0], + [ + "appliedbiosystemsmicroamp_384_wellplate_40ul", + "conicalWell", + 7.44, + 26.19, + 3.0, + 3.0, + ], + [ + "opentrons_24_tuberack_eppendorf_2ml_safelock_snapcap", + "conicalWell", + 60.940, + 2163.980, + 3.0, + 3.0, + ], + [ + "opentrons_10_tuberack_nest_4x50ml_6x15ml_conical", + "conicalWell15mL", + 16.690, + 15546.930, + 3.0, + 5.0, + ], + [ + "opentrons_10_tuberack_nest_4x50ml_6x15ml_conical", + "conicalWell50mL", + 111.200, + 56110.279, + 3.0, + 5.0, + ], + [ + "opentrons_10_tuberack_falcon_4x50ml_6x15ml_conical", + "15mlconicalWell", + 40.830, + 15956.600, + 3.0, + 3.0, + ], + [ + "opentrons_10_tuberack_falcon_4x50ml_6x15ml_conical", + "50mlconicalWell", + 163.860, + 57720.510, + 3.0, + 3.0, + ], +] 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 deleted file mode 100644 index 44c72e38e86..00000000000 --- a/api/tests/opentrons/protocol_engine/state/test_addressable_area_store.py +++ /dev/null @@ -1,317 +0,0 @@ -"""Addressable area state store tests. - -DEPRECATED: Testing AddressableAreaStore independently of AddressableAreaView is no -longer helpful. Add new tests to test_addressable_area_state.py, where they can be -tested together. -""" - -import pytest - -from opentrons_shared_data.deck.types import DeckDefinitionV5 -from opentrons_shared_data.labware.labware_definition import Parameters -from opentrons.protocols.models import LabwareDefinition -from opentrons.types import DeckSlotName - -from opentrons.protocol_engine.commands import Command -from opentrons.protocol_engine.actions import ( - SucceedCommandAction, - AddAddressableAreaAction, -) -from opentrons.protocol_engine.state.config import Config -from opentrons.protocol_engine.state.addressable_areas import ( - AddressableAreaStore, - AddressableAreaState, -) -from opentrons.protocol_engine.types import ( - DeckType, - DeckConfigurationType, - ModuleModel, - LabwareMovementStrategy, - DeckSlotLocation, - AddressableAreaLocation, -) - -from .command_fixtures import ( - create_load_labware_command, - create_load_module_command, - create_move_labware_command, - create_move_to_addressable_area_command, -) - - -def _make_deck_config() -> DeckConfigurationType: - return [ - ("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: DeckDefinitionV5, -) -> AddressableAreaStore: - """Get an AddressableAreaStore test subject, under simulated deck conditions.""" - return AddressableAreaStore( - deck_configuration=[], - config=Config( - use_simulated_deck_config=True, - robot_type="OT-3 Standard", - deck_type=DeckType.OT3_STANDARD, - ), - deck_definition=ot3_standard_deck_def, - robot_definition={ - "displayName": "OT-3", - "robotType": "OT-3 Standard", - "models": ["OT-3 Standard"], - "extents": [477.2, 493.8, 0.0], - "paddingOffsets": { - "rear": -177.42, - "front": 51.8, - "leftSide": 31.88, - "rightSide": -80.32, - }, - "mountOffsets": { - "left": [-13.5, -60.5, 255.675], - "right": [40.5, -60.5, 255.675], - "gripper": [84.55, -12.75, 93.85], - }, - }, - ) - - -@pytest.fixture -def subject( - ot3_standard_deck_def: DeckDefinitionV5, -) -> AddressableAreaStore: - """Get an AddressableAreaStore test subject.""" - return AddressableAreaStore( - deck_configuration=_make_deck_config(), - config=Config( - use_simulated_deck_config=False, - robot_type="OT-3 Standard", - deck_type=DeckType.OT3_STANDARD, - ), - deck_definition=ot3_standard_deck_def, - robot_definition={ - "displayName": "OT-3", - "robotType": "OT-3 Standard", - "models": ["OT-3 Standard"], - "extents": [477.2, 493.8, 0.0], - "paddingOffsets": { - "rear": -177.42, - "front": 51.8, - "leftSide": 31.88, - "rightSide": -80.32, - }, - "mountOffsets": { - "left": [-13.5, -60.5, 255.675], - "right": [40.5, -60.5, 255.675], - "gripper": [84.55, -12.75, 93.85], - }, - }, - ) - - -def test_initial_state_simulated( - ot3_standard_deck_def: DeckDefinitionV5, - simulated_subject: AddressableAreaStore, -) -> None: - """It should create the Addressable Area store with no loaded addressable areas.""" - assert simulated_subject.state == AddressableAreaState( - loaded_addressable_areas_by_name={}, - potential_cutout_fixtures_by_cutout_id={}, - deck_definition=ot3_standard_deck_def, - deck_configuration=[], - robot_type="OT-3 Standard", - use_simulated_deck_config=True, - robot_definition={ - "displayName": "OT-3", - "robotType": "OT-3 Standard", - "models": ["OT-3 Standard"], - "extents": [477.2, 493.8, 0.0], - "paddingOffsets": { - "rear": -177.42, - "front": 51.8, - "leftSide": 31.88, - "rightSide": -80.32, - }, - "mountOffsets": { - "left": [-13.5, -60.5, 255.675], - "right": [40.5, -60.5, 255.675], - "gripper": [84.55, -12.75, 93.85], - }, - }, - ) - - -def test_initial_state( - ot3_standard_deck_def: DeckDefinitionV5, - subject: AddressableAreaStore, -) -> None: - """It should create the Addressable Area store with loaded addressable areas.""" - assert subject.state.potential_cutout_fixtures_by_cutout_id == {} - assert not subject.state.use_simulated_deck_config - assert subject.state.deck_definition == ot3_standard_deck_def - assert subject.state.deck_configuration == _make_deck_config() - # Loading 9 regular slots, 1 trash, 2 Staging Area slots and 4 waste chute types - assert len(subject.state.loaded_addressable_areas_by_name) == 16 - - -@pytest.mark.parametrize( - ("command", "expected_area"), - ( - ( - create_load_labware_command( - location=DeckSlotLocation(slotName=DeckSlotName.SLOT_A1), - labware_id="test-labware-id", - definition=LabwareDefinition.construct( # type: ignore[call-arg] - parameters=Parameters.construct(loadName="blah"), # type: ignore[call-arg] - namespace="bleh", - version=123, - ), - offset_id="offset-id", - display_name="display-name", - ), - "A1", - ), - ( - create_load_labware_command( - location=AddressableAreaLocation(addressableAreaName="A4"), - labware_id="test-labware-id", - definition=LabwareDefinition.construct( # type: ignore[call-arg] - parameters=Parameters.construct(loadName="blah"), # type: ignore[call-arg] - namespace="bleh", - version=123, - ), - offset_id="offset-id", - display_name="display-name", - ), - "A4", - ), - ( - create_load_module_command( - location=DeckSlotLocation(slotName=DeckSlotName.SLOT_A1), - module_id="test-module-id", - model=ModuleModel.TEMPERATURE_MODULE_V2, - ), - "A1", - ), - ( - create_move_labware_command( - new_location=DeckSlotLocation(slotName=DeckSlotName.SLOT_A1), - strategy=LabwareMovementStrategy.USING_GRIPPER, - ), - "A1", - ), - ( - create_move_labware_command( - new_location=AddressableAreaLocation(addressableAreaName="A4"), - strategy=LabwareMovementStrategy.USING_GRIPPER, - ), - "A4", - ), - ( - create_move_to_addressable_area_command( - pipette_id="pipette-id", addressable_area_name="gripperWasteChute" - ), - "gripperWasteChute", - ), - ), -) -def test_addressable_area_referencing_commands_load_on_simulated_deck( - command: Command, - expected_area: str, - simulated_subject: AddressableAreaStore, -) -> None: - """It should check and store the addressable area when referenced in a command.""" - simulated_subject.handle_action(SucceedCommandAction(command=command)) - assert expected_area in simulated_subject.state.loaded_addressable_areas_by_name - - -@pytest.mark.parametrize( - ("command", "expected_area"), - ( - ( - create_load_labware_command( - location=DeckSlotLocation(slotName=DeckSlotName.SLOT_A1), - labware_id="test-labware-id", - definition=LabwareDefinition.construct( # type: ignore[call-arg] - parameters=Parameters.construct(loadName="blah"), # type: ignore[call-arg] - namespace="bleh", - version=123, - ), - offset_id="offset-id", - display_name="display-name", - ), - "A1", - ), - ( - create_load_labware_command( - location=AddressableAreaLocation(addressableAreaName="C4"), - labware_id="test-labware-id", - definition=LabwareDefinition.construct( # type: ignore[call-arg] - parameters=Parameters.construct(loadName="blah"), # type: ignore[call-arg] - namespace="bleh", - version=123, - ), - offset_id="offset-id", - display_name="display-name", - ), - "C4", - ), - ( - create_load_module_command( - location=DeckSlotLocation(slotName=DeckSlotName.SLOT_A1), - module_id="test-module-id", - model=ModuleModel.TEMPERATURE_MODULE_V2, - ), - "A1", - ), - ( - create_move_labware_command( - new_location=DeckSlotLocation(slotName=DeckSlotName.SLOT_A1), - strategy=LabwareMovementStrategy.USING_GRIPPER, - ), - "A1", - ), - ( - create_move_labware_command( - new_location=AddressableAreaLocation(addressableAreaName="C4"), - strategy=LabwareMovementStrategy.USING_GRIPPER, - ), - "C4", - ), - ), -) -def test_addressable_area_referencing_commands_load( - command: Command, - expected_area: str, - subject: AddressableAreaStore, -) -> None: - """It should check that the addressable area is in the deck config.""" - subject.handle_action(SucceedCommandAction(command=command)) - assert expected_area in subject.state.loaded_addressable_areas_by_name - - -def test_add_addressable_area_action( - simulated_subject: AddressableAreaStore, -) -> None: - """It should add the addressable area to the store.""" - simulated_subject.handle_action( - AddAddressableAreaAction( - addressable_area=AddressableAreaLocation( - addressableAreaName="movableTrashA1" - ) - ) - ) - assert "movableTrashA1" in simulated_subject.state.loaded_addressable_areas_by_name diff --git a/api/tests/opentrons/protocol_engine/state/test_addressable_area_store_old.py b/api/tests/opentrons/protocol_engine/state/test_addressable_area_store_old.py new file mode 100644 index 00000000000..b04237c702d --- /dev/null +++ b/api/tests/opentrons/protocol_engine/state/test_addressable_area_store_old.py @@ -0,0 +1,195 @@ +"""Addressable area state store tests. + +DEPRECATED: Testing AddressableAreaStore independently of AddressableAreaView is no +longer helpful. Try to add new tests to test_addressable_area_state.py, where they can be +tested together, treating AddressableAreaState as a private implementation detail. +""" + +import pytest + +from opentrons_shared_data.deck.types import DeckDefinitionV5 + +from opentrons.protocol_engine.commands import Command, Comment +from opentrons.protocol_engine.actions import ( + SucceedCommandAction, + AddAddressableAreaAction, +) +from opentrons.protocol_engine.state import update_types +from opentrons.protocol_engine.state.config import Config +from opentrons.protocol_engine.state.addressable_areas import ( + AddressableAreaStore, + AddressableAreaState, +) +from opentrons.protocol_engine.types import ( + DeckType, + DeckConfigurationType, +) + + +def _make_deck_config() -> DeckConfigurationType: + return [ + ("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), + ] + + +def _dummy_command() -> Command: + """Return a placeholder command.""" + return Comment.model_construct() # type: ignore[call-arg] + + +@pytest.fixture +def simulated_subject( + ot3_standard_deck_def: DeckDefinitionV5, +) -> AddressableAreaStore: + """Get an AddressableAreaStore test subject, under simulated deck conditions.""" + return AddressableAreaStore( + deck_configuration=[], + config=Config( + use_simulated_deck_config=True, + robot_type="OT-3 Standard", + deck_type=DeckType.OT3_STANDARD, + ), + deck_definition=ot3_standard_deck_def, + robot_definition={ + "displayName": "OT-3", + "robotType": "OT-3 Standard", + "models": ["OT-3 Standard"], + "extents": [477.2, 493.8, 0.0], + "paddingOffsets": { + "rear": -177.42, + "front": 51.8, + "leftSide": 31.88, + "rightSide": -80.32, + }, + "mountOffsets": { + "left": [-13.5, -60.5, 255.675], + "right": [40.5, -60.5, 255.675], + "gripper": [84.55, -12.75, 93.85], + }, + }, + ) + + +@pytest.fixture +def subject( + ot3_standard_deck_def: DeckDefinitionV5, +) -> AddressableAreaStore: + """Get an AddressableAreaStore test subject.""" + return AddressableAreaStore( + deck_configuration=_make_deck_config(), + config=Config( + use_simulated_deck_config=False, + robot_type="OT-3 Standard", + deck_type=DeckType.OT3_STANDARD, + ), + deck_definition=ot3_standard_deck_def, + robot_definition={ + "displayName": "OT-3", + "robotType": "OT-3 Standard", + "models": ["OT-3 Standard"], + "extents": [477.2, 493.8, 0.0], + "paddingOffsets": { + "rear": -177.42, + "front": 51.8, + "leftSide": 31.88, + "rightSide": -80.32, + }, + "mountOffsets": { + "left": [-13.5, -60.5, 255.675], + "right": [40.5, -60.5, 255.675], + "gripper": [84.55, -12.75, 93.85], + }, + }, + ) + + +def test_initial_state_simulated( + ot3_standard_deck_def: DeckDefinitionV5, + simulated_subject: AddressableAreaStore, +) -> None: + """It should create the Addressable Area store with no loaded addressable areas.""" + assert simulated_subject.state == AddressableAreaState( + loaded_addressable_areas_by_name={}, + potential_cutout_fixtures_by_cutout_id={}, + deck_definition=ot3_standard_deck_def, + deck_configuration=[], + robot_type="OT-3 Standard", + use_simulated_deck_config=True, + robot_definition={ + "displayName": "OT-3", + "robotType": "OT-3 Standard", + "models": ["OT-3 Standard"], + "extents": [477.2, 493.8, 0.0], + "paddingOffsets": { + "rear": -177.42, + "front": 51.8, + "leftSide": 31.88, + "rightSide": -80.32, + }, + "mountOffsets": { + "left": [-13.5, -60.5, 255.675], + "right": [40.5, -60.5, 255.675], + "gripper": [84.55, -12.75, 93.85], + }, + }, + ) + + +def test_initial_state( + ot3_standard_deck_def: DeckDefinitionV5, + subject: AddressableAreaStore, +) -> None: + """It should create the Addressable Area store with loaded addressable areas.""" + assert subject.state.potential_cutout_fixtures_by_cutout_id == {} + assert not subject.state.use_simulated_deck_config + assert subject.state.deck_definition == ot3_standard_deck_def + assert subject.state.deck_configuration == _make_deck_config() + # Loading 9 regular slots, 1 trash, 2 Staging Area slots and 4 waste chute types + assert len(subject.state.loaded_addressable_areas_by_name) == 16 + + +@pytest.mark.parametrize("addressable_area_name", ["A1", "A4", "gripperWasteChute"]) +def test_addressable_area_usage_in_simulation( + simulated_subject: AddressableAreaStore, + addressable_area_name: str, +) -> None: + """Simulating stores should correctly handle `StateUpdate`s with addressable areas.""" + assert ( + addressable_area_name + not in simulated_subject.state.loaded_addressable_areas_by_name + ) + simulated_subject.handle_action( + SucceedCommandAction( + command=_dummy_command(), + state_update=update_types.StateUpdate( + addressable_area_used=update_types.AddressableAreaUsedUpdate( + addressable_area_name + ) + ), + ) + ) + assert ( + addressable_area_name + in simulated_subject.state.loaded_addressable_areas_by_name + ) + + +def test_add_addressable_area_action( + simulated_subject: AddressableAreaStore, +) -> None: + """It should add the addressable area to the store.""" + simulated_subject.handle_action( + AddAddressableAreaAction(addressable_area_name="movableTrashA1") + ) + assert "movableTrashA1" in simulated_subject.state.loaded_addressable_areas_by_name 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 deleted file mode 100644 index 30ca1b9e7c4..00000000000 --- a/api/tests/opentrons/protocol_engine/state/test_addressable_area_view.py +++ /dev/null @@ -1,492 +0,0 @@ -"""Addressable area state view tests. - -DEPRECATED: Testing AddressableAreaView independently of AddressableAreaStore is no -longer helpful. Add new tests to test_addressable_area_state.py, where they can be -tested together. -""" - -import inspect - -import pytest -from decoy import Decoy -from typing import Dict, Set, Optional, cast - -from opentrons_shared_data.robot.types import RobotType -from opentrons_shared_data.deck.types import DeckDefinitionV5 -from opentrons.types import Point, DeckSlotName - -from opentrons.protocol_engine.errors import ( - AreaNotInDeckConfigurationError, - IncompatibleAddressableAreaError, - SlotDoesNotExistError, - AddressableAreaDoesNotExistError, -) -from opentrons.protocol_engine.resources import deck_configuration_provider -from opentrons.protocol_engine.state.addressable_areas import ( - AddressableAreaState, - AddressableAreaView, -) -from opentrons.protocol_engine.types import ( - AddressableArea, - AreaType, - DeckConfigurationType, - PotentialCutoutFixture, - Dimensions, - DeckPoint, - AddressableOffsetVector, -) - - -@pytest.fixture(autouse=True) -def patch_mock_deck_configuration_provider( - decoy: Decoy, monkeypatch: pytest.MonkeyPatch -) -> None: - """Mock out deck_configuration_provider.py functions.""" - for name, func in inspect.getmembers( - deck_configuration_provider, inspect.isfunction - ): - monkeypatch.setattr(deck_configuration_provider, name, decoy.mock(func=func)) - - -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"}), - robot_definition={ - "displayName": "OT-3", - "robotType": "OT-3 Standard", - "models": ["OT-3 Standard"], - "extents": [477.2, 493.8, 0.0], - "paddingOffsets": { - "rear": -177.42, - "front": 51.8, - "leftSide": 31.88, - "rightSide": -80.32, - }, - "mountOffsets": { - "left": [-13.5, -60.5, 255.675], - "right": [40.5, -60.5, 255.675], - "gripper": [84.55, -12.75, 93.85], - }, - }, - deck_configuration=deck_configuration or [], - robot_type=robot_type, - use_simulated_deck_config=use_simulated_deck_config, - ) - - return AddressableAreaView(state=state) - - -def test_get_all_cutout_fixtures_simulated_deck_config() -> None: - """It should return no cutout fixtures when the deck config is simulated.""" - subject = get_addressable_area_view( - deck_configuration=None, - use_simulated_deck_config=True, - ) - assert subject.get_all_cutout_fixtures() is None - - -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", None), - ("cutout-id-2", "cutout-fixture-id-2", None), - ], - use_simulated_deck_config=False, - ) - assert subject.get_all_cutout_fixtures() == [ - "cutout-fixture-id-1", - "cutout-fixture-id-2", - ] - - -def test_get_loaded_addressable_area() -> None: - """It should get the loaded addressable area.""" - addressable_area = AddressableArea( - area_name="area", - area_type=AreaType.SLOT, - base_slot=DeckSlotName.SLOT_D3, - display_name="fancy name", - bounding_box=Dimensions(x=1, y=2, z=3), - position=AddressableOffsetVector(x=7, y=8, z=9), - compatible_module_types=["magneticModuleType"], - ) - subject = get_addressable_area_view( - loaded_addressable_areas_by_name={"abc": addressable_area} - ) - - assert subject.get_addressable_area("abc") is addressable_area - - -def test_get_loaded_addressable_area_raises() -> None: - """It should raise if the addressable area does not exist.""" - subject = get_addressable_area_view() - - with pytest.raises(AreaNotInDeckConfigurationError): - subject.get_addressable_area("abc") - - -def test_get_addressable_area_for_simulation_already_loaded() -> None: - """It should get the addressable area for a simulation that has not been loaded yet.""" - addressable_area = AddressableArea( - area_name="area", - area_type=AreaType.SLOT, - base_slot=DeckSlotName.SLOT_D3, - display_name="fancy name", - bounding_box=Dimensions(x=1, y=2, z=3), - position=AddressableOffsetVector(x=7, y=8, z=9), - compatible_module_types=["magneticModuleType"], - ) - subject = get_addressable_area_view( - loaded_addressable_areas_by_name={"abc": addressable_area}, - use_simulated_deck_config=True, - ) - - assert subject.get_addressable_area("abc") is addressable_area - - -def test_get_addressable_area_for_simulation_not_loaded(decoy: Decoy) -> None: - """It should get the addressable area for a simulation that has not been loaded yet.""" - subject = get_addressable_area_view( - potential_cutout_fixtures_by_cutout_id={ - "cutoutA1": { - PotentialCutoutFixture( - cutout_id="cutoutA1", - cutout_fixture_id="blah", - provided_addressable_areas=frozenset(), - ) - } - }, - use_simulated_deck_config=True, - ) - - addressable_area = AddressableArea( - area_name="area", - area_type=AreaType.SLOT, - base_slot=DeckSlotName.SLOT_D3, - display_name="fancy name", - bounding_box=Dimensions(x=1, y=2, z=3), - position=AddressableOffsetVector(x=7, y=8, z=9), - compatible_module_types=["magneticModuleType"], - ) - - decoy.when( - deck_configuration_provider.get_potential_cutout_fixtures( - "abc", subject.state.deck_definition - ) - ).then_return( - ( - "cutoutA1", - { - PotentialCutoutFixture( - cutout_id="cutoutA1", - cutout_fixture_id="blah", - provided_addressable_areas=frozenset(), - ) - }, - ) - ) - - decoy.when( - deck_configuration_provider.get_cutout_position( - "cutoutA1", subject.state.deck_definition - ) - ).then_return(DeckPoint(x=1, y=2, z=3)) - - decoy.when( - deck_configuration_provider.get_addressable_area_from_name( - "abc", - DeckPoint(x=1, y=2, z=3), - DeckSlotName.SLOT_A1, - subject.state.deck_definition, - ) - ).then_return(addressable_area) - - assert subject.get_addressable_area("abc") is addressable_area - - -def test_get_addressable_area_for_simulation_raises(decoy: Decoy) -> None: - """It should raise if the requested addressable area is incompatible with loaded ones.""" - subject = get_addressable_area_view( - potential_cutout_fixtures_by_cutout_id={ - "123": { - PotentialCutoutFixture( - cutout_id="789", - cutout_fixture_id="bleh", - provided_addressable_areas=frozenset(), - ) - } - }, - use_simulated_deck_config=True, - ) - - decoy.when( - deck_configuration_provider.get_potential_cutout_fixtures( - "abc", subject.state.deck_definition - ) - ).then_return( - ( - "123", - { - PotentialCutoutFixture( - cutout_id="123", - cutout_fixture_id="blah", - provided_addressable_areas=frozenset(), - ) - }, - ) - ) - - decoy.when( - deck_configuration_provider.get_provided_addressable_area_names( - "bleh", "789", subject.state.deck_definition - ) - ).then_return([]) - - with pytest.raises(IncompatibleAddressableAreaError): - subject.get_addressable_area("abc") - - -def test_get_addressable_area_position() -> None: - """It should get the absolute location of the addressable area.""" - subject = get_addressable_area_view( - loaded_addressable_areas_by_name={ - "abc": AddressableArea( - area_name="area", - area_type=AreaType.SLOT, - base_slot=DeckSlotName.SLOT_D3, - display_name="fancy name", - bounding_box=Dimensions(x=10, y=20, z=30), - position=AddressableOffsetVector(x=1, y=2, z=3), - compatible_module_types=[], - ) - } - ) - - result = subject.get_addressable_area_position("abc") - assert result == Point(1, 2, 3) - - -def test_get_addressable_area_move_to_location() -> None: - """It should get the absolute location of an addressable area's move to location.""" - subject = get_addressable_area_view( - loaded_addressable_areas_by_name={ - "abc": AddressableArea( - area_name="area", - area_type=AreaType.SLOT, - base_slot=DeckSlotName.SLOT_D3, - display_name="fancy name", - bounding_box=Dimensions(x=10, y=20, z=30), - position=AddressableOffsetVector(x=1, y=2, z=3), - compatible_module_types=[], - ) - } - ) - - result = subject.get_addressable_area_move_to_location("abc") - assert result == Point(6, 12, 33) - - -def test_get_addressable_area_center() -> None: - """It should get the absolute location of an addressable area's center.""" - subject = get_addressable_area_view( - loaded_addressable_areas_by_name={ - "abc": AddressableArea( - area_name="area", - area_type=AreaType.SLOT, - base_slot=DeckSlotName.SLOT_D3, - display_name="fancy name", - bounding_box=Dimensions(x=10, y=20, z=30), - position=AddressableOffsetVector(x=1, y=2, z=3), - compatible_module_types=[], - ) - } - ) - - result = subject.get_addressable_area_center("abc") - assert result == Point(6, 12, 3) - - -def test_get_fixture_height(decoy: Decoy) -> None: - """It should return the height of the requested fixture.""" - subject = get_addressable_area_view() - decoy.when( - deck_configuration_provider.get_cutout_fixture( - "someShortCutoutFixture", subject.state.deck_definition - ) - ).then_return( - { - "height": 10, - # These values don't matter: - "id": "id", - "expectOpentronsModuleSerialNumber": False, - "fixtureGroup": {}, - "mayMountTo": [], - "displayName": "", - "providesAddressableAreas": {}, - } - ) - - decoy.when( - deck_configuration_provider.get_cutout_fixture( - "someTallCutoutFixture", subject.state.deck_definition - ) - ).then_return( - { - "height": 9000.1, - # These values don't matter: - "id": "id", - "expectOpentronsModuleSerialNumber": False, - "fixtureGroup": {}, - "mayMountTo": [], - "displayName": "", - "providesAddressableAreas": {}, - } - ) - - assert subject.get_fixture_height("someShortCutoutFixture") == 10 - assert subject.get_fixture_height("someTallCutoutFixture") == 9000.1 - - -def test_get_slot_definition() -> None: - """It should return a deck slot's definition.""" - subject = get_addressable_area_view( - loaded_addressable_areas_by_name={ - "6": AddressableArea( - area_name="area", - area_type=AreaType.SLOT, - base_slot=DeckSlotName.SLOT_D3, - display_name="fancy name", - bounding_box=Dimensions(x=1, y=2, z=3), - position=AddressableOffsetVector(x=7, y=8, z=9), - compatible_module_types=["magneticModuleType"], - ) - } - ) - - result = subject.get_slot_definition(DeckSlotName.SLOT_6.id) - - assert result == { - "id": "area", - "position": [7, 8, 9], - "boundingBox": { - "xDimension": 1, - "yDimension": 2, - "zDimension": 3, - }, - "displayName": "fancy name", - "compatibleModuleTypes": ["magneticModuleType"], - } - - -def test_get_slot_definition_raises_with_bad_slot_name(decoy: Decoy) -> None: - """It should raise a SlotDoesNotExistError if a bad slot name is given.""" - subject = get_addressable_area_view() - - decoy.when( - deck_configuration_provider.get_potential_cutout_fixtures( - "foo", subject.state.deck_definition - ) - ).then_raise(AddressableAreaDoesNotExistError()) - - with pytest.raises(SlotDoesNotExistError): - subject.get_slot_definition("foo") - - -def test_raise_if_area_not_in_deck_configuration_on_robot(decoy: Decoy) -> None: - """It should raise if the requested addressable area name is not loaded in state.""" - subject = get_addressable_area_view( - loaded_addressable_areas_by_name={"real": decoy.mock(cls=AddressableArea)} - ) - - subject.raise_if_area_not_in_deck_configuration("real") - - with pytest.raises(AreaNotInDeckConfigurationError): - subject.raise_if_area_not_in_deck_configuration("fake") - - -def test_raise_if_area_not_in_deck_configuration_simulated_config(decoy: Decoy) -> None: - """It should raise if the requested addressable area name is not loaded in state.""" - subject = get_addressable_area_view( - use_simulated_deck_config=True, - potential_cutout_fixtures_by_cutout_id={ - "waluigi": { - PotentialCutoutFixture( - cutout_id="fire flower", - cutout_fixture_id="1up", - provided_addressable_areas=frozenset(), - ) - }, - "wario": { - PotentialCutoutFixture( - cutout_id="mushroom", - cutout_fixture_id="star", - provided_addressable_areas=frozenset(), - ) - }, - }, - ) - - decoy.when( - deck_configuration_provider.get_potential_cutout_fixtures( - "mario", subject.state.deck_definition - ) - ).then_return( - ( - "wario", - { - PotentialCutoutFixture( - cutout_id="mushroom", - cutout_fixture_id="star", - provided_addressable_areas=frozenset(), - ) - }, - ) - ) - - subject.raise_if_area_not_in_deck_configuration("mario") - - decoy.when( - deck_configuration_provider.get_potential_cutout_fixtures( - "luigi", subject.state.deck_definition - ) - ).then_return( - ( - "waluigi", - { - PotentialCutoutFixture( - cutout_id="mushroom", - cutout_fixture_id="star", - provided_addressable_areas=frozenset(), - ) - }, - ) - ) - - decoy.when( - deck_configuration_provider.get_provided_addressable_area_names( - "1up", "fire flower", subject.state.deck_definition - ) - ).then_return([]) - - decoy.when( - deck_configuration_provider.get_addressable_area_display_name( - "luigi", subject.state.deck_definition - ) - ).then_return("super luigi") - - with pytest.raises(IncompatibleAddressableAreaError): - subject.raise_if_area_not_in_deck_configuration("luigi") diff --git a/api/tests/opentrons/protocol_engine/state/test_addressable_area_view_old.py b/api/tests/opentrons/protocol_engine/state/test_addressable_area_view_old.py new file mode 100644 index 00000000000..5aa157c59db --- /dev/null +++ b/api/tests/opentrons/protocol_engine/state/test_addressable_area_view_old.py @@ -0,0 +1,501 @@ +"""Addressable area state view tests. + +DEPRECATED: Testing AddressableAreaView independently of AddressableAreaStore is no +longer helpful. Try to add new tests to test_addressable_area_state.py, where they can be +tested together, treating AddressableAreaState as a private implementation detail. +""" + +import inspect +from unittest.mock import sentinel + +import pytest +from decoy import Decoy +from typing import Dict, Set, Optional, cast + +from opentrons_shared_data.robot.types import RobotType +from opentrons_shared_data.deck.types import DeckDefinitionV5 +from opentrons.types import Point, DeckSlotName + +from opentrons.protocol_engine.errors import ( + AreaNotInDeckConfigurationError, + IncompatibleAddressableAreaError, + SlotDoesNotExistError, + AddressableAreaDoesNotExistError, +) +from opentrons.protocol_engine.resources import deck_configuration_provider +from opentrons.protocol_engine.state.addressable_areas import ( + AddressableAreaState, + AddressableAreaView, +) +from opentrons.protocol_engine.types import ( + AddressableArea, + AreaType, + DeckConfigurationType, + PotentialCutoutFixture, + Dimensions, + DeckPoint, + AddressableOffsetVector, +) + + +@pytest.fixture(autouse=True) +def patch_mock_deck_configuration_provider( + decoy: Decoy, monkeypatch: pytest.MonkeyPatch +) -> None: + """Mock out deck_configuration_provider.py functions.""" + for name, func in inspect.getmembers( + deck_configuration_provider, inspect.isfunction + ): + monkeypatch.setattr(deck_configuration_provider, name, decoy.mock(func=func)) + + +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"}), + robot_definition={ + "displayName": "OT-3", + "robotType": "OT-3 Standard", + "models": ["OT-3 Standard"], + "extents": [477.2, 493.8, 0.0], + "paddingOffsets": { + "rear": -177.42, + "front": 51.8, + "leftSide": 31.88, + "rightSide": -80.32, + }, + "mountOffsets": { + "left": [-13.5, -60.5, 255.675], + "right": [40.5, -60.5, 255.675], + "gripper": [84.55, -12.75, 93.85], + }, + }, + deck_configuration=deck_configuration or [], + robot_type=robot_type, + use_simulated_deck_config=use_simulated_deck_config, + ) + + return AddressableAreaView(state=state) + + +def test_get_all_cutout_fixtures_simulated_deck_config() -> None: + """It should return no cutout fixtures when the deck config is simulated.""" + subject = get_addressable_area_view( + deck_configuration=None, + use_simulated_deck_config=True, + ) + assert subject.get_all_cutout_fixtures() is None + + +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", None), + ("cutout-id-2", "cutout-fixture-id-2", None), + ], + use_simulated_deck_config=False, + ) + assert subject.get_all_cutout_fixtures() == [ + "cutout-fixture-id-1", + "cutout-fixture-id-2", + ] + + +def test_get_loaded_addressable_area() -> None: + """It should get the loaded addressable area.""" + addressable_area = AddressableArea( + area_name="area", + area_type=AreaType.SLOT, + base_slot=DeckSlotName.SLOT_D3, + display_name="fancy name", + bounding_box=Dimensions(x=1, y=2, z=3), + position=AddressableOffsetVector(x=7, y=8, z=9), + compatible_module_types=["magneticModuleType"], + ) + subject = get_addressable_area_view( + loaded_addressable_areas_by_name={"abc": addressable_area} + ) + + assert subject.get_addressable_area("abc") is addressable_area + + +def test_get_loaded_addressable_area_raises() -> None: + """It should raise if the addressable area does not exist.""" + subject = get_addressable_area_view() + + with pytest.raises(AreaNotInDeckConfigurationError): + subject.get_addressable_area("abc") + + +def test_get_addressable_area_for_simulation_already_loaded() -> None: + """It should get the addressable area for a simulation that has not been loaded yet.""" + addressable_area = AddressableArea( + area_name="area", + area_type=AreaType.SLOT, + base_slot=DeckSlotName.SLOT_D3, + display_name="fancy name", + bounding_box=Dimensions(x=1, y=2, z=3), + position=AddressableOffsetVector(x=7, y=8, z=9), + compatible_module_types=["magneticModuleType"], + ) + subject = get_addressable_area_view( + loaded_addressable_areas_by_name={"abc": addressable_area}, + use_simulated_deck_config=True, + ) + + assert subject.get_addressable_area("abc") is addressable_area + + +def test_get_addressable_area_for_simulation_not_loaded(decoy: Decoy) -> None: + """It should get the addressable area for a simulation that has not been loaded yet.""" + subject = get_addressable_area_view( + potential_cutout_fixtures_by_cutout_id={ + "cutoutA1": { + PotentialCutoutFixture( + cutout_id="cutoutA1", + cutout_fixture_id="blah", + provided_addressable_areas=frozenset(), + ) + } + }, + use_simulated_deck_config=True, + deck_definition=sentinel.deck_definition, + ) + + addressable_area = AddressableArea( + area_name="area", + area_type=AreaType.SLOT, + base_slot=DeckSlotName.SLOT_D3, + display_name="fancy name", + bounding_box=Dimensions(x=1, y=2, z=3), + position=AddressableOffsetVector(x=7, y=8, z=9), + compatible_module_types=["magneticModuleType"], + ) + + decoy.when( + deck_configuration_provider.get_potential_cutout_fixtures( + "abc", + sentinel.deck_definition, + ) + ).then_return( + ( + "cutoutA1", + { + PotentialCutoutFixture( + cutout_id="cutoutA1", + cutout_fixture_id="blah", + provided_addressable_areas=frozenset(), + ) + }, + ) + ) + + decoy.when( + deck_configuration_provider.get_cutout_position( + "cutoutA1", + sentinel.deck_definition, + ) + ).then_return(DeckPoint(x=1, y=2, z=3)) + + decoy.when( + deck_configuration_provider.get_addressable_area_from_name( + "abc", + DeckPoint(x=1, y=2, z=3), + DeckSlotName.SLOT_A1, + sentinel.deck_definition, + ) + ).then_return(addressable_area) + + assert subject.get_addressable_area("abc") is addressable_area + + +def test_get_addressable_area_for_simulation_raises(decoy: Decoy) -> None: + """It should raise if the requested addressable area is incompatible with loaded ones.""" + subject = get_addressable_area_view( + potential_cutout_fixtures_by_cutout_id={ + "123": { + PotentialCutoutFixture( + cutout_id="789", + cutout_fixture_id="bleh", + provided_addressable_areas=frozenset(), + ) + } + }, + use_simulated_deck_config=True, + deck_definition=sentinel.deck_definition, + ) + + decoy.when( + deck_configuration_provider.get_potential_cutout_fixtures( + "abc", sentinel.deck_definition + ) + ).then_return( + ( + "123", + { + PotentialCutoutFixture( + cutout_id="123", + cutout_fixture_id="blah", + provided_addressable_areas=frozenset(), + ) + }, + ) + ) + + decoy.when( + deck_configuration_provider.get_provided_addressable_area_names( + "bleh", "789", sentinel.deck_definition + ) + ).then_return([]) + + with pytest.raises(IncompatibleAddressableAreaError): + subject.get_addressable_area("abc") + + +def test_get_addressable_area_position() -> None: + """It should get the absolute location of the addressable area.""" + subject = get_addressable_area_view( + loaded_addressable_areas_by_name={ + "abc": AddressableArea( + area_name="area", + area_type=AreaType.SLOT, + base_slot=DeckSlotName.SLOT_D3, + display_name="fancy name", + bounding_box=Dimensions(x=10, y=20, z=30), + position=AddressableOffsetVector(x=1, y=2, z=3), + compatible_module_types=[], + ) + } + ) + + result = subject.get_addressable_area_position("abc") + assert result == Point(1, 2, 3) + + +def test_get_addressable_area_move_to_location() -> None: + """It should get the absolute location of an addressable area's move to location.""" + subject = get_addressable_area_view( + loaded_addressable_areas_by_name={ + "abc": AddressableArea( + area_name="area", + area_type=AreaType.SLOT, + base_slot=DeckSlotName.SLOT_D3, + display_name="fancy name", + bounding_box=Dimensions(x=10, y=20, z=30), + position=AddressableOffsetVector(x=1, y=2, z=3), + compatible_module_types=[], + ) + } + ) + + result = subject.get_addressable_area_move_to_location("abc") + assert result == Point(6, 12, 33) + + +def test_get_addressable_area_center() -> None: + """It should get the absolute location of an addressable area's center.""" + subject = get_addressable_area_view( + loaded_addressable_areas_by_name={ + "abc": AddressableArea( + area_name="area", + area_type=AreaType.SLOT, + base_slot=DeckSlotName.SLOT_D3, + display_name="fancy name", + bounding_box=Dimensions(x=10, y=20, z=30), + position=AddressableOffsetVector(x=1, y=2, z=3), + compatible_module_types=[], + ) + } + ) + + result = subject.get_addressable_area_center("abc") + assert result == Point(6, 12, 3) + + +def test_get_fixture_height(decoy: Decoy) -> None: + """It should return the height of the requested fixture.""" + subject = get_addressable_area_view(deck_definition=sentinel.deck_definition) + decoy.when( + deck_configuration_provider.get_cutout_fixture( + "someShortCutoutFixture", sentinel.deck_definition + ) + ).then_return( + { + "height": 10, + # These values don't matter: + "id": "id", + "expectOpentronsModuleSerialNumber": False, + "fixtureGroup": {}, + "mayMountTo": [], + "displayName": "", + "providesAddressableAreas": {}, + } + ) + + decoy.when( + deck_configuration_provider.get_cutout_fixture( + "someTallCutoutFixture", sentinel.deck_definition + ) + ).then_return( + { + "height": 9000.1, + # These values don't matter: + "id": "id", + "expectOpentronsModuleSerialNumber": False, + "fixtureGroup": {}, + "mayMountTo": [], + "displayName": "", + "providesAddressableAreas": {}, + } + ) + + assert subject.get_fixture_height("someShortCutoutFixture") == 10 + assert subject.get_fixture_height("someTallCutoutFixture") == 9000.1 + + +def test_get_slot_definition() -> None: + """It should return a deck slot's definition.""" + subject = get_addressable_area_view( + loaded_addressable_areas_by_name={ + "6": AddressableArea( + area_name="area", + area_type=AreaType.SLOT, + base_slot=DeckSlotName.SLOT_D3, + display_name="fancy name", + bounding_box=Dimensions(x=1, y=2, z=3), + position=AddressableOffsetVector(x=7, y=8, z=9), + compatible_module_types=["magneticModuleType"], + ) + } + ) + + result = subject.get_slot_definition(DeckSlotName.SLOT_6.id) + + assert result == { + "id": "area", + "position": [7, 8, 9], + "boundingBox": { + "xDimension": 1, + "yDimension": 2, + "zDimension": 3, + }, + "displayName": "fancy name", + "compatibleModuleTypes": ["magneticModuleType"], + } + + +def test_get_slot_definition_raises_with_bad_slot_name(decoy: Decoy) -> None: + """It should raise a SlotDoesNotExistError if a bad slot name is given.""" + deck_definition = cast(DeckDefinitionV5, {"otId": "fake"}) + subject = get_addressable_area_view( + deck_definition=deck_definition, + ) + + decoy.when( + deck_configuration_provider.get_potential_cutout_fixtures( + "foo", deck_definition + ) + ).then_raise(AddressableAreaDoesNotExistError()) + + with pytest.raises(SlotDoesNotExistError): + subject.get_slot_definition("foo") + + +def test_raise_if_area_not_in_deck_configuration_on_robot(decoy: Decoy) -> None: + """It should raise if the requested addressable area name is not loaded in state.""" + subject = get_addressable_area_view( + loaded_addressable_areas_by_name={"real": decoy.mock(cls=AddressableArea)} + ) + + subject.raise_if_area_not_in_deck_configuration("real") + + with pytest.raises(AreaNotInDeckConfigurationError): + subject.raise_if_area_not_in_deck_configuration("fake") + + +def test_raise_if_area_not_in_deck_configuration_simulated_config(decoy: Decoy) -> None: + """It should raise if the requested addressable area name is not loaded in state.""" + subject = get_addressable_area_view( + use_simulated_deck_config=True, + potential_cutout_fixtures_by_cutout_id={ + "waluigi": { + PotentialCutoutFixture( + cutout_id="fire flower", + cutout_fixture_id="1up", + provided_addressable_areas=frozenset(), + ) + }, + "wario": { + PotentialCutoutFixture( + cutout_id="mushroom", + cutout_fixture_id="star", + provided_addressable_areas=frozenset(), + ) + }, + }, + deck_definition=sentinel.deck_definition, + ) + + decoy.when( + deck_configuration_provider.get_potential_cutout_fixtures( + "mario", sentinel.deck_definition + ) + ).then_return( + ( + "wario", + { + PotentialCutoutFixture( + cutout_id="mushroom", + cutout_fixture_id="star", + provided_addressable_areas=frozenset(), + ) + }, + ) + ) + + subject.raise_if_area_not_in_deck_configuration("mario") + + decoy.when( + deck_configuration_provider.get_potential_cutout_fixtures( + "luigi", sentinel.deck_definition + ) + ).then_return( + ( + "waluigi", + { + PotentialCutoutFixture( + cutout_id="mushroom", + cutout_fixture_id="star", + provided_addressable_areas=frozenset(), + ) + }, + ) + ) + + decoy.when( + deck_configuration_provider.get_provided_addressable_area_names( + "1up", "fire flower", sentinel.deck_definition + ) + ).then_return([]) + + decoy.when( + deck_configuration_provider.get_addressable_area_display_name( + "luigi", sentinel.deck_definition + ) + ).then_return("super luigi") + + with pytest.raises(IncompatibleAddressableAreaError): + subject.raise_if_area_not_in_deck_configuration("luigi") diff --git a/api/tests/opentrons/protocol_engine/state/test_command_history.py b/api/tests/opentrons/protocol_engine/state/test_command_history.py index 14eaa2a42f3..fabf17e26d1 100644 --- a/api/tests/opentrons/protocol_engine/state/test_command_history.py +++ b/api/tests/opentrons/protocol_engine/state/test_command_history.py @@ -202,13 +202,13 @@ def test_set_fixit_running_command_id(command_history: CommandHistory) -> None: """It should set the ID of the currently running fixit command.""" command_entry = create_queued_command() command_history.append_queued_command(command_entry) - running_command = command_entry.copy( + running_command = command_entry.model_copy( update={ "status": CommandStatus.RUNNING, } ) command_history.set_command_running(running_command) - finished_command = command_entry.copy( + finished_command = command_entry.model_copy( update={ "status": CommandStatus.SUCCEEDED, } @@ -218,7 +218,7 @@ def test_set_fixit_running_command_id(command_history: CommandHistory) -> None: command_id="fixit-id", intent=CommandIntent.FIXIT ) command_history.append_queued_command(fixit_command_entry) - fixit_running_command = fixit_command_entry.copy( + fixit_running_command = fixit_command_entry.model_copy( update={ "status": CommandStatus.RUNNING, } diff --git a/api/tests/opentrons/protocol_engine/state/test_command_state.py b/api/tests/opentrons/protocol_engine/state/test_command_state.py index fde0d66e654..c52cd8ca74d 100644 --- a/api/tests/opentrons/protocol_engine/state/test_command_state.py +++ b/api/tests/opentrons/protocol_engine/state/test_command_state.py @@ -30,6 +30,7 @@ from opentrons.protocol_engine.state.commands import ( CommandStore, CommandView, + CommandErrorSlice, ) from opentrons.protocol_engine.state.config import Config from opentrons.protocol_engine.state.update_types import StateUpdate @@ -193,7 +194,7 @@ def test_command_failure(error_recovery_type: ErrorRecoveryType) -> None: ) assert subject_view.get("command-id") == expected_failed_command - assert subject.state.failed_command_errors == [expected_error_occurrence] + assert subject_view.get_all_errors() == [expected_error_occurrence] def test_command_failure_clears_queues() -> None: @@ -255,7 +256,7 @@ def test_command_failure_clears_queues() -> None: 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 - assert subject.state.failed_command_errors == [expected_error_occurance] + assert subject_view.get_all_errors() == [expected_error_occurance] def test_setup_command_failure_only_clears_setup_command_queue() -> None: @@ -555,7 +556,7 @@ def test_door_during_error_recovery() -> None: subject.handle_action(play) assert subject_view.get_status() == EngineStatus.AWAITING_RECOVERY assert subject_view.get_next_to_execute() == "command-id-2" - assert subject.state.failed_command_errors == [expected_error_occurance] + assert subject_view.get_all_errors() == [expected_error_occurance] @pytest.mark.parametrize("close_door_before_queueing", [False, True]) @@ -732,7 +733,7 @@ def test_error_recovery_type_tracking() -> None: id="c2-error", createdAt=datetime(year=2023, month=3, day=3), error=exception ) - assert subject.state.failed_command_errors == [ + assert view.get_all_errors() == [ error_occurrence_1, error_occurrence_2, ] @@ -1100,3 +1101,94 @@ def test_get_state_update_for_false_positive() -> None: subject.handle_action(resume_from_recovery) assert subject_view.get_state_update_for_false_positive() == empty_state_update + + +def test_get_errors_slice_empty() -> None: + """It should return an empty error list.""" + subject = CommandStore( + config=_make_config(), + error_recovery_policy=_placeholder_error_recovery_policy, + is_door_open=False, + ) + subject_view = CommandView(subject.state) + result = subject_view.get_errors_slice(cursor=0, length=2) + + assert result == CommandErrorSlice(commands_errors=[], cursor=0, total_length=0) + + +def test_get_errors_slice() -> None: + """It should return a slice of all command errors.""" + subject = CommandStore( + config=_make_config(), + error_recovery_policy=_placeholder_error_recovery_policy, + 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_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=[], + type=ErrorRecoveryType.CONTINUE_WITH_ERROR, + ) + subject.handle_action(fail_2_setup) + + result = subject_view.get_errors_slice(cursor=1, length=3) + + assert result == CommandErrorSlice( + [ + ErrorOccurrence( + id="error-id", + createdAt=datetime(2023, 3, 3, 0, 0), + isDefined=False, + errorType="ProtocolEngineError", + errorCode="4000", + detail="oh no", + errorInfo={}, + wrappedErrors=[], + ) + ], + cursor=0, + total_length=1, + ) diff --git a/api/tests/opentrons/protocol_engine/state/test_command_store_old.py b/api/tests/opentrons/protocol_engine/state/test_command_store_old.py index d5f171b7ea9..a9022803bcf 100644 --- a/api/tests/opentrons/protocol_engine/state/test_command_store_old.py +++ b/api/tests/opentrons/protocol_engine/state/test_command_store_old.py @@ -1,7 +1,8 @@ """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. +Try to add new tests to test_command_state.py, where they can be tested together, +treating CommandState as a private implementation detail. """ @@ -333,7 +334,6 @@ def test_command_store_handles_pause_action(pause_source: PauseSource) -> None: recovery_target=None, latest_protocol_command_hash=None, stopped_by_estop=False, - failed_command_errors=[], error_recovery_policy=matchers.Anything(), has_entered_error_recovery=False, ) @@ -363,7 +363,6 @@ def test_command_store_handles_play_action(pause_source: PauseSource) -> None: run_started_at=datetime(year=2021, month=1, day=1), latest_protocol_command_hash=None, stopped_by_estop=False, - failed_command_errors=[], error_recovery_policy=matchers.Anything(), has_entered_error_recovery=False, ) @@ -398,7 +397,6 @@ def test_command_store_handles_finish_action() -> None: run_started_at=datetime(year=2021, month=1, day=1), latest_protocol_command_hash=None, stopped_by_estop=False, - failed_command_errors=[], error_recovery_policy=matchers.Anything(), has_entered_error_recovery=False, ) @@ -453,7 +451,6 @@ def test_command_store_handles_stop_action( run_started_at=datetime(year=2021, month=1, day=1), latest_protocol_command_hash=None, stopped_by_estop=from_estop, - failed_command_errors=[], error_recovery_policy=matchers.Anything(), has_entered_error_recovery=False, ) @@ -491,7 +488,6 @@ def test_command_store_handles_stop_action_when_awaiting_recovery() -> None: run_started_at=datetime(year=2021, month=1, day=1), latest_protocol_command_hash=None, stopped_by_estop=False, - failed_command_errors=[], error_recovery_policy=matchers.Anything(), has_entered_error_recovery=False, ) @@ -525,7 +521,6 @@ def test_command_store_cannot_restart_after_should_stop() -> None: run_started_at=None, latest_protocol_command_hash=None, stopped_by_estop=False, - failed_command_errors=[], error_recovery_policy=matchers.Anything(), has_entered_error_recovery=False, ) @@ -672,7 +667,6 @@ def test_command_store_wraps_unknown_errors() -> None: recovery_target=None, latest_protocol_command_hash=None, stopped_by_estop=False, - failed_command_errors=[], error_recovery_policy=matchers.Anything(), has_entered_error_recovery=False, ) @@ -742,7 +736,6 @@ def __init__(self, message: str) -> None: run_started_at=None, latest_protocol_command_hash=None, stopped_by_estop=False, - failed_command_errors=[], error_recovery_policy=matchers.Anything(), has_entered_error_recovery=False, ) @@ -778,7 +771,6 @@ def test_command_store_ignores_stop_after_graceful_finish() -> None: run_started_at=datetime(year=2021, month=1, day=1), latest_protocol_command_hash=None, stopped_by_estop=False, - failed_command_errors=[], error_recovery_policy=matchers.Anything(), has_entered_error_recovery=False, ) @@ -814,7 +806,6 @@ def test_command_store_ignores_finish_after_non_graceful_stop() -> None: run_started_at=datetime(year=2021, month=1, day=1), latest_protocol_command_hash=None, stopped_by_estop=False, - failed_command_errors=[], error_recovery_policy=matchers.Anything(), has_entered_error_recovery=False, ) @@ -850,7 +841,6 @@ def test_handles_hardware_stopped() -> None: run_started_at=None, latest_protocol_command_hash=None, stopped_by_estop=False, - failed_command_errors=[], error_recovery_policy=matchers.Anything(), has_entered_error_recovery=False, ) diff --git a/api/tests/opentrons/protocol_engine/state/test_command_view_old.py b/api/tests/opentrons/protocol_engine/state/test_command_view_old.py index f7b1d6cd31f..de242d83f51 100644 --- a/api/tests/opentrons/protocol_engine/state/test_command_view_old.py +++ b/api/tests/opentrons/protocol_engine/state/test_command_view_old.py @@ -1,7 +1,8 @@ """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. +Try to add new tests to test_command_state.py, where they can be tested together, +treating CommandState as a private implementation detail. """ @@ -28,19 +29,17 @@ CommandState, CommandView, CommandSlice, - CommandErrorSlice, CommandPointer, RunResult, QueueStatus, ) -from opentrons.protocol_engine.state.command_history import CommandEntry +from opentrons.protocol_engine.state.command_history import CommandEntry, CommandHistory from opentrons.protocol_engine.errors import ProtocolCommandFailedError, ErrorOccurrence from opentrons_shared_data.errors.codes import ErrorCodes -from opentrons.protocol_engine.state.command_history import CommandHistory from opentrons.protocol_engine.state.update_types import StateUpdate from .command_fixtures import ( @@ -77,7 +76,6 @@ def get_command_view( # noqa: C901 finish_error: Optional[errors.ErrorOccurrence] = None, commands: Sequence[cmd.Command] = (), latest_command_hash: Optional[str] = None, - failed_command_errors: Optional[List[ErrorOccurrence]] = None, has_entered_error_recovery: bool = False, ) -> CommandView: """Get a command view test subject.""" @@ -121,7 +119,6 @@ def get_command_view( # noqa: C901 run_started_at=run_started_at, latest_protocol_command_hash=latest_command_hash, stopped_by_estop=False, - failed_command_errors=failed_command_errors or [], has_entered_error_recovery=has_entered_error_recovery, error_recovery_policy=_placeholder_error_recovery_policy, ) @@ -896,7 +893,7 @@ def test_get_current() -> None: created_at=datetime(year=2022, month=2, day=2), ) subject = get_command_view(commands=[command_1, command_2]) - subject.state.command_history._set_most_recently_completed_command_id(command_1.id) + subject._state.command_history._set_most_recently_completed_command_id(command_1.id) assert subject.get_current() == CommandPointer( index=1, @@ -916,7 +913,7 @@ def test_get_current() -> None: created_at=datetime(year=2022, month=2, day=2), ) subject = get_command_view(commands=[command_1, command_2]) - subject.state.command_history._set_most_recently_completed_command_id(command_1.id) + subject._state.command_history._set_most_recently_completed_command_id(command_1.id) assert subject.get_current() == CommandPointer( index=1, @@ -1031,42 +1028,6 @@ def test_get_slice_default_cursor_running() -> None: ) -def test_get_errors_slice_empty() -> None: - """It should return a slice from the tail if no current command.""" - subject = get_command_view(failed_command_errors=[]) - result = subject.get_errors_slice(cursor=0, length=2) - - assert result == CommandErrorSlice(commands_errors=[], cursor=0, total_length=0) - - -def test_get_errors_slice() -> None: - """It should return a slice of all command errors.""" - error_1 = ErrorOccurrence.construct(id="error-id-1") # type: ignore[call-arg] - error_2 = ErrorOccurrence.construct(id="error-id-2") # type: ignore[call-arg] - error_3 = ErrorOccurrence.construct(id="error-id-3") # type: ignore[call-arg] - error_4 = ErrorOccurrence.construct(id="error-id-4") # type: ignore[call-arg] - - subject = get_command_view( - failed_command_errors=[error_1, error_2, error_3, error_4] - ) - - result = subject.get_errors_slice(cursor=1, length=3) - - assert result == CommandErrorSlice( - commands_errors=[error_2, error_3, error_4], - cursor=1, - total_length=4, - ) - - result = subject.get_errors_slice(cursor=-3, length=10) - - assert result == CommandErrorSlice( - commands_errors=[error_1, error_2, error_3, error_4], - cursor=0, - total_length=4, - ) - - def test_get_slice_without_fixit() -> None: """It should select a cursor based on the running command, if present.""" command_1 = create_succeeded_command(command_id="command-id-1") diff --git a/api/tests/opentrons/protocol_engine/state/test_fluid_stack.py b/api/tests/opentrons/protocol_engine/state/test_fluid_stack.py new file mode 100644 index 00000000000..e958b92036d --- /dev/null +++ b/api/tests/opentrons/protocol_engine/state/test_fluid_stack.py @@ -0,0 +1,219 @@ +"""Test pipette internal fluid tracking.""" +import pytest + +from opentrons.protocol_engine.state.fluid_stack import FluidStack +from opentrons.protocol_engine.types import AspiratedFluid, FluidKind + + +@pytest.mark.parametrize( + "fluids,resulting_stack", + [ + ( + [ + AspiratedFluid(FluidKind.LIQUID, 10), + AspiratedFluid(FluidKind.LIQUID, 10), + ], + [AspiratedFluid(FluidKind.LIQUID, 20)], + ), + ( + [AspiratedFluid(FluidKind.AIR, 10), AspiratedFluid(FluidKind.LIQUID, 20)], + [AspiratedFluid(FluidKind.AIR, 10), AspiratedFluid(FluidKind.LIQUID, 20)], + ), + ( + [ + AspiratedFluid(FluidKind.AIR, 10), + AspiratedFluid(FluidKind.LIQUID, 20), + AspiratedFluid(FluidKind.LIQUID, 10), + AspiratedFluid(FluidKind.AIR, 20), + ], + [ + AspiratedFluid(FluidKind.AIR, 10), + AspiratedFluid(FluidKind.LIQUID, 30), + AspiratedFluid(FluidKind.AIR, 20), + ], + ), + ], +) +def test_add_fluid( + fluids: list[AspiratedFluid], resulting_stack: list[AspiratedFluid] +) -> None: + """It should add fluids.""" + stack = FluidStack() + for fluid in fluids: + stack.add_fluid(fluid) + assert stack._fluid_stack == resulting_stack + + +@pytest.mark.parametrize( + "starting_fluids,remove_volume,resulting_stack", + [ + ([], 1, []), + ([], 0, []), + ( + [AspiratedFluid(FluidKind.LIQUID, 10)], + 0, + [AspiratedFluid(FluidKind.LIQUID, 10)], + ), + ( + [AspiratedFluid(FluidKind.LIQUID, 10)], + 5, + [AspiratedFluid(FluidKind.LIQUID, 5)], + ), + ([AspiratedFluid(FluidKind.LIQUID, 10)], 11, []), + ( + [AspiratedFluid(FluidKind.LIQUID, 10), AspiratedFluid(FluidKind.AIR, 10)], + 11, + [AspiratedFluid(FluidKind.LIQUID, 9)], + ), + ( + [AspiratedFluid(FluidKind.LIQUID, 10), AspiratedFluid(FluidKind.AIR, 10)], + 20, + [], + ), + ( + [ + AspiratedFluid(FluidKind.LIQUID, 10), + AspiratedFluid(FluidKind.AIR, 10), + AspiratedFluid(FluidKind.LIQUID, 10), + ], + 28, + [AspiratedFluid(FluidKind.LIQUID, 2)], + ), + ], +) +def test_remove_fluid( + starting_fluids: list[AspiratedFluid], + remove_volume: float, + resulting_stack: list[AspiratedFluid], +) -> None: + """It should remove fluids.""" + stack = FluidStack(_fluid_stack=[f for f in starting_fluids]) + stack.remove_fluid(remove_volume) + assert stack._fluid_stack == resulting_stack + + +@pytest.mark.parametrize( + "starting_fluids,filter,result", + [ + ([], None, 0), + ([], FluidKind.LIQUID, 0), + ([], FluidKind.AIR, 0), + ([AspiratedFluid(FluidKind.LIQUID, 10)], None, 10), + ([AspiratedFluid(FluidKind.LIQUID, 10)], FluidKind.LIQUID, 10), + ([AspiratedFluid(FluidKind.LIQUID, 10)], FluidKind.AIR, 0), + ( + [AspiratedFluid(FluidKind.LIQUID, 10), AspiratedFluid(FluidKind.AIR, 10)], + None, + 20, + ), + ( + [AspiratedFluid(FluidKind.LIQUID, 10), AspiratedFluid(FluidKind.AIR, 10)], + FluidKind.LIQUID, + 10, + ), + ( + [AspiratedFluid(FluidKind.LIQUID, 10), AspiratedFluid(FluidKind.AIR, 10)], + FluidKind.AIR, + 10, + ), + ], +) +def test_aspirated_volume( + starting_fluids: list[AspiratedFluid], filter: FluidKind | None, result: float +) -> None: + """It should represent aspirated volume with filtering.""" + stack = FluidStack(_fluid_stack=starting_fluids) + assert stack.aspirated_volume(kind=filter) == result + + +@pytest.mark.parametrize( + "starting_fluids,dispense_volume,result", + [ + ([], 0, 0), + ([], 1, 0), + ([AspiratedFluid(FluidKind.AIR, 10)], 10, 0), + ([AspiratedFluid(FluidKind.AIR, 10)], 0, 0), + ([AspiratedFluid(FluidKind.LIQUID, 10)], 10, 10), + ([AspiratedFluid(FluidKind.LIQUID, 10)], 0, 0), + ( + [ + AspiratedFluid(FluidKind.AIR, 10), + AspiratedFluid(FluidKind.LIQUID, 10), + ], + 10, + 10, + ), + ( + [ + AspiratedFluid(FluidKind.AIR, 10), + AspiratedFluid(FluidKind.LIQUID, 10), + ], + 20, + 10, + ), + ( + [ + AspiratedFluid(FluidKind.AIR, 10), + AspiratedFluid(FluidKind.LIQUID, 10), + ], + 30, + 10, + ), + ( + [ + AspiratedFluid(FluidKind.AIR, 10), + AspiratedFluid(FluidKind.LIQUID, 10), + ], + 5, + 5, + ), + ( + [ + AspiratedFluid(FluidKind.LIQUID, 10), + AspiratedFluid(FluidKind.AIR, 10), + ], + 5, + 0, + ), + ( + [ + AspiratedFluid(FluidKind.LIQUID, 10), + AspiratedFluid(FluidKind.AIR, 10), + ], + 10, + 0, + ), + ( + [ + AspiratedFluid(FluidKind.LIQUID, 10), + AspiratedFluid(FluidKind.AIR, 10), + ], + 11, + 1, + ), + ( + [ + AspiratedFluid(FluidKind.LIQUID, 10), + AspiratedFluid(FluidKind.AIR, 10), + ], + 20, + 10, + ), + ( + [ + AspiratedFluid(FluidKind.LIQUID, 10), + AspiratedFluid(FluidKind.AIR, 10), + ], + 30, + 10, + ), + ], +) +def test_liquid_part_of_dispense_volume( + starting_fluids: list[AspiratedFluid], + dispense_volume: float, + result: float, +) -> None: + """It should predict resulting liquid from a dispense.""" + stack = FluidStack(_fluid_stack=starting_fluids) + assert stack.liquid_part_of_dispense_volume(dispense_volume) == result diff --git a/api/tests/opentrons/protocol_engine/state/test_geometry_view.py b/api/tests/opentrons/protocol_engine/state/test_geometry_view.py index 42ee037c1ce..8f28c6de88e 100644 --- a/api/tests/opentrons/protocol_engine/state/test_geometry_view.py +++ b/api/tests/opentrons/protocol_engine/state/test_geometry_view.py @@ -1,10 +1,12 @@ """Test state getters for retrieving geometry views of state.""" + import inspect import json from datetime import datetime from math import isclose from typing import cast, List, Tuple, Optional, NamedTuple, Dict from unittest.mock import sentinel +from os import listdir, path import pytest from decoy import Decoy @@ -14,6 +16,7 @@ StateUpdate, ) +from opentrons_shared_data import get_shared_data_root, load_shared_data from opentrons_shared_data.deck.types import DeckDefinitionV5 from opentrons_shared_data.deck import load as load_deck from opentrons_shared_data.labware.types import LabwareUri @@ -26,8 +29,8 @@ Dimensions as LabwareDimensions, Parameters as LabwareDefinitionParameters, CornerOffsetFromSlot, + ConicalFrustum, ) -from opentrons_shared_data import load_shared_data from opentrons.protocol_engine import errors from opentrons.protocol_engine.types import ( @@ -95,12 +98,21 @@ _volume_from_height_circular, _volume_from_height_rectangular, ) +from .inner_geometry_test_params import INNER_WELL_GEOMETRY_TEST_PARAMS from ..pipette_fixtures import get_default_nozzle_map from ..mock_circular_frusta import TEST_EXAMPLES as CIRCULAR_TEST_EXAMPLES from ..mock_rectangular_frusta import TEST_EXAMPLES as RECTANGULAR_TEST_EXAMPLES from ...protocol_runner.test_json_translator import _load_labware_definition_data +@pytest.fixture +def available_sensors() -> pipette_definition.AvailableSensorDefinition: + """Provide a list of sensors.""" + return pipette_definition.AvailableSensorDefinition( + sensors=["pressure", "capacitive", "environment"] + ) + + @pytest.fixture def mock_labware_view(decoy: Decoy) -> LabwareView: """Get a mock in the shape of a LabwareView.""" @@ -249,7 +261,7 @@ def addressable_area_view( @pytest.fixture def nice_labware_definition() -> LabwareDefinition: """Load a nice labware def that won't blow up your terminal.""" - return LabwareDefinition.parse_obj( + return LabwareDefinition.model_validate( json.loads( load_shared_data("labware/fixtures/2/fixture_12_trough_v2.json").decode( "utf-8" @@ -261,7 +273,7 @@ def nice_labware_definition() -> LabwareDefinition: @pytest.fixture def nice_adapter_definition() -> LabwareDefinition: """Load a friendly adapter definition.""" - return LabwareDefinition.parse_obj( + return LabwareDefinition.model_validate( json.loads( load_shared_data( "labware/definitions/2/opentrons_aluminum_flat_bottom_plate/1.json" @@ -841,8 +853,8 @@ def test_get_all_obstacle_highest_z_with_modules( subject: GeometryView, ) -> None: """It should get the highest Z including modules.""" - module_1 = LoadedModule.construct(id="module-id-1") # type: ignore[call-arg] - module_2 = LoadedModule.construct(id="module-id-2") # type: ignore[call-arg] + module_1 = LoadedModule.model_construct(id="module-id-1") # type: ignore[call-arg] + module_2 = LoadedModule.model_construct(id="module-id-2") # type: ignore[call-arg] decoy.when(mock_labware_view.get_all()).then_return([]) decoy.when(mock_addressable_area_view.get_all()).then_return([]) @@ -931,7 +943,7 @@ def test_get_highest_z_in_slot_with_single_module( ) -> 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. - module_in_slot = LoadedModule.construct( + module_in_slot = LoadedModule.model_construct( id="only-module", model=ModuleModel.THERMOCYCLER_MODULE_V2, location=DeckSlotLocation(slotName=DeckSlotName.SLOT_4), @@ -1086,7 +1098,7 @@ def test_get_highest_z_in_slot_with_labware_stack_on_module( location=ModuleLocation(moduleId="module-id"), offsetId="offset-id2", ) - module_on_slot = LoadedModule.construct( + module_on_slot = LoadedModule.model_construct( id="module-id", model=ModuleModel.THERMOCYCLER_MODULE_V2, location=DeckSlotLocation(slotName=DeckSlotName.SLOT_4), @@ -1974,7 +1986,7 @@ def test_get_relative_well_location( assert result == WellLocation( origin=WellOrigin.TOP, - offset=WellOffset.construct( + offset=WellOffset.model_construct( x=cast(float, pytest.approx(7)), y=cast(float, pytest.approx(8)), z=cast(float, pytest.approx(9)), @@ -1999,7 +2011,7 @@ def test_get_relative_liquid_handling_well_location( assert result == LiquidHandlingWellLocation( origin=WellOrigin.MENISCUS, - offset=WellOffset.construct( + offset=WellOffset.model_construct( x=0.0, y=0.0, z=cast(float, pytest.approx(-2)), @@ -2449,8 +2461,8 @@ def test_get_slot_item( subject: GeometryView, ) -> None: """It should get items in certain slots.""" - labware = LoadedLabware.construct(id="cool-labware") # type: ignore[call-arg] - module = LoadedModule.construct(id="cool-module") # type: ignore[call-arg] + labware = LoadedLabware.model_construct(id="cool-labware") # type: ignore[call-arg] + module = LoadedModule.model_construct(id="cool-module") # type: ignore[call-arg] decoy.when(mock_labware_view.get_by_slot(DeckSlotName.SLOT_1)).then_return(None) decoy.when(mock_labware_view.get_by_slot(DeckSlotName.SLOT_2)).then_return(labware) @@ -2477,7 +2489,7 @@ def test_get_slot_item_that_is_overflowed_module( subject: GeometryView, ) -> None: """It should return the module that occupies the slot, even if not loaded on it.""" - module = LoadedModule.construct(id="cool-module") # type: ignore[call-arg] + module = LoadedModule.model_construct(id="cool-module") # type: ignore[call-arg] decoy.when(mock_labware_view.get_by_slot(DeckSlotName.SLOT_3)).then_return(None) decoy.when(mock_module_view.get_by_slot(DeckSlotName.SLOT_3)).then_return(None) decoy.when( @@ -2575,6 +2587,7 @@ def test_get_next_drop_tip_location( pipette_mount: MountType, expected_locations: List[DropTipWellLocation], supported_tip_fixture: pipette_definition.SupportedTipsDefinition, + available_sensors: pipette_definition.AvailableSensorDefinition, ) -> None: """It should provide the next location to drop tips into within a labware.""" decoy.when(mock_labware_view.is_fixed_trash(labware_id="abc")).then_return(True) @@ -2611,6 +2624,14 @@ def test_get_next_drop_tip_location( back_right_corner=Point(x=40, y=20, z=60), ), lld_settings={}, + plunger_positions={ + "top": 0.0, + "bottom": 5.0, + "blow_out": 19.0, + "drop_tip": 20.0, + }, + shaft_ul_per_mm=5.0, + available_sensors=available_sensors, ) ) decoy.when(mock_pipette_view.get_mount("pip-123")).then_return(pipette_mount) @@ -2884,19 +2905,19 @@ def test_check_gripper_labware_tip_collision( ) ) - definition = LabwareDefinition.construct( # type: ignore[call-arg] + definition = LabwareDefinition.model_construct( # type: ignore[call-arg] namespace="hello", - dimensions=LabwareDimensions.construct( + dimensions=LabwareDimensions.model_construct( yDimension=1, zDimension=2, xDimension=3 ), version=1, - parameters=LabwareDefinitionParameters.construct( + parameters=LabwareDefinitionParameters.model_construct( format="96Standard", loadName="labware-id", isTiprack=True, isMagneticModuleCompatible=False, ), - cornerOffsetFromSlot=CornerOffsetFromSlot.construct(x=1, y=2, z=3), + cornerOffsetFromSlot=CornerOffsetFromSlot.model_construct(x=1, y=2, z=3), ordering=[], ) @@ -3260,19 +3281,23 @@ def _find_volume_from_height_(index: int) -> None: nonlocal total_frustum_height, bottom_radius top_radius = frustum["radius"][index] target_height = frustum["height"][index] - + segment = ConicalFrustum( + shape="conical", + bottomDiameter=bottom_radius * 2, + topDiameter=top_radius * 2, + topHeight=total_frustum_height, + bottomHeight=0.0, + xCount=1, + yCount=1, + ) found_volume = _volume_from_height_circular( target_height=target_height, - total_frustum_height=total_frustum_height, - top_radius=top_radius, - bottom_radius=bottom_radius, + segment=segment, ) found_height = _height_from_volume_circular( - volume=found_volume, - total_frustum_height=total_frustum_height, - top_radius=top_radius, - bottom_radius=bottom_radius, + target_volume=found_volume, + segment=segment, ) assert isclose(found_height, frustum["height"][index]) @@ -3342,3 +3367,133 @@ def test_validate_dispense_volume_into_well_meniscus( ), volume=1100000.0, ) + + +@pytest.mark.parametrize( + [ + "labware_id", + "well_name", + "input_volume_bottom", + "input_volume_top", + "expected_height_from_bottom_mm", + "expected_height_from_top_mm", + ], + INNER_WELL_GEOMETRY_TEST_PARAMS, +) +def test_get_well_height_at_volume( + decoy: Decoy, + subject: GeometryView, + labware_id: str, + well_name: str, + input_volume_bottom: float, + input_volume_top: float, + expected_height_from_bottom_mm: float, + expected_height_from_top_mm: float, + mock_labware_view: LabwareView, +) -> None: + """Test getting the well height at a given volume.""" + + def _get_labware_def() -> LabwareDefinition: + def_dir = str(get_shared_data_root()) + f"/labware/definitions/3/{labware_id}" + version_str = max([str(version) for version in listdir(def_dir)]) + def_path = path.join(def_dir, version_str) + _labware_def = LabwareDefinition.model_validate( + json.loads(load_shared_data(def_path).decode("utf-8")) + ) + return _labware_def + + labware_def = _get_labware_def() + assert labware_def.innerLabwareGeometry is not None + well_geometry = labware_def.innerLabwareGeometry.get(well_name) + assert well_geometry is not None + well_definition = [ + well + for well in labware_def.wells.values() + if well.geometryDefinitionId == well_name + ][0] + + decoy.when(mock_labware_view.get_well_geometry(labware_id, well_name)).then_return( + well_geometry + ) + decoy.when( + mock_labware_view.get_well_definition(labware_id, well_name) + ).then_return(well_definition) + + found_height_bottom = subject.get_well_height_at_volume( + labware_id=labware_id, well_name=well_name, volume=input_volume_bottom + ) + found_height_top = subject.get_well_height_at_volume( + labware_id=labware_id, well_name=well_name, volume=input_volume_top + ) + assert isclose(found_height_bottom, expected_height_from_bottom_mm, rel_tol=0.01) + vol_2_expected_height_from_bottom = ( + subject.get_well_height(labware_id=labware_id, well_name=well_name) + - expected_height_from_top_mm + ) + assert isclose(found_height_top, vol_2_expected_height_from_bottom, rel_tol=0.01) + + +@pytest.mark.parametrize( + [ + "labware_id", + "well_name", + "expected_volume_bottom", + "expected_volume_top", + "input_height_from_bottom_mm", + "input_height_from_top_mm", + ], + INNER_WELL_GEOMETRY_TEST_PARAMS, +) +def test_get_well_volume_at_height( + decoy: Decoy, + subject: GeometryView, + labware_id: str, + well_name: str, + expected_volume_bottom: float, + expected_volume_top: float, + input_height_from_bottom_mm: float, + input_height_from_top_mm: float, + mock_labware_view: LabwareView, +) -> None: + """Test getting the volume at a given height.""" + + def _get_labware_def() -> LabwareDefinition: + def_dir = str(get_shared_data_root()) + f"/labware/definitions/3/{labware_id}" + version_str = max([str(version) for version in listdir(def_dir)]) + def_path = path.join(def_dir, version_str) + _labware_def = LabwareDefinition.model_validate( + json.loads(load_shared_data(def_path).decode("utf-8")) + ) + return _labware_def + + labware_def = _get_labware_def() + assert labware_def.innerLabwareGeometry is not None + well_geometry = labware_def.innerLabwareGeometry.get(well_name) + assert well_geometry is not None + well_definition = [ + well + for well in labware_def.wells.values() + if well.geometryDefinitionId == well_name + ][0] + + decoy.when(mock_labware_view.get_well_geometry(labware_id, well_name)).then_return( + well_geometry + ) + decoy.when( + mock_labware_view.get_well_definition(labware_id, well_name) + ).then_return(well_definition) + + found_volume_bottom = subject.get_well_volume_at_height( + labware_id=labware_id, well_name=well_name, height=input_height_from_bottom_mm + ) + vol_2_input_height_from_bottom = ( + subject.get_well_height(labware_id=labware_id, well_name=well_name) + - input_height_from_top_mm + ) + found_volume_top = subject.get_well_volume_at_height( + labware_id=labware_id, + well_name=well_name, + height=vol_2_input_height_from_bottom, + ) + assert isclose(found_volume_bottom, expected_volume_bottom, rel_tol=0.01) + assert isclose(found_volume_top, expected_volume_top, rel_tol=0.01) diff --git a/api/tests/opentrons/protocol_engine/state/test_labware_store.py b/api/tests/opentrons/protocol_engine/state/test_labware_store.py deleted file mode 100644 index 47150ec425f..00000000000 --- a/api/tests/opentrons/protocol_engine/state/test_labware_store.py +++ /dev/null @@ -1,338 +0,0 @@ -"""Labware state store tests.""" -from typing import Optional -from opentrons.protocol_engine.state import update_types -import pytest - -from datetime import datetime - -from opentrons.calibration_storage.helpers import uri_from_details -from opentrons_shared_data.deck.types import DeckDefinitionV5 -from opentrons.protocols.models import LabwareDefinition -from opentrons.types import DeckSlotName - -from opentrons.protocol_engine.types import ( - LabwareOffset, - LabwareOffsetCreate, - LabwareOffsetVector, - LabwareOffsetLocation, - DeckSlotLocation, - LoadedLabware, - OFF_DECK_LOCATION, -) -from opentrons.protocol_engine.actions import ( - AddLabwareOffsetAction, - AddLabwareDefinitionAction, - SucceedCommandAction, -) -from opentrons.protocol_engine.state.labware import LabwareStore, LabwareState - -from .command_fixtures import ( - create_comment_command, -) - - -@pytest.fixture -def subject( - ot2_standard_deck_def: DeckDefinitionV5, -) -> LabwareStore: - """Get a LabwareStore test subject.""" - return LabwareStore( - deck_definition=ot2_standard_deck_def, - deck_fixed_labware=[], - ) - - -def test_initial_state( - ot2_standard_deck_def: DeckDefinitionV5, - subject: LabwareStore, -) -> None: - """It should create the labware store with preloaded fixed labware.""" - assert subject.state == LabwareState( - deck_definition=ot2_standard_deck_def, - labware_by_id={}, - labware_offsets_by_id={}, - definitions_by_uri={}, - ) - - -def test_handles_add_labware_offset( - subject: LabwareStore, -) -> None: - """It should add the labware offset to the state and add the ID.""" - request = LabwareOffsetCreate( - definitionUri="offset-definition-uri", - location=LabwareOffsetLocation(slotName=DeckSlotName.SLOT_1), - vector=LabwareOffsetVector(x=1, y=2, z=3), - ) - - resolved_offset = LabwareOffset( - id="offset-id", - createdAt=datetime(year=2021, month=1, day=2), - definitionUri="offset-definition-uri", - location=LabwareOffsetLocation(slotName=DeckSlotName.SLOT_1), - vector=LabwareOffsetVector(x=1, y=2, z=3), - ) - - subject.handle_action( - AddLabwareOffsetAction( - labware_offset_id="offset-id", - created_at=datetime(year=2021, month=1, day=2), - request=request, - ) - ) - - assert subject.state.labware_offsets_by_id == {"offset-id": resolved_offset} - - -@pytest.mark.parametrize( - "display_name, offset_id", [("display-name", "offset-id"), (None, None)] -) -def test_handles_load_labware( - subject: LabwareStore, - well_plate_def: LabwareDefinition, - display_name: Optional[str], - offset_id: Optional[str], -) -> None: - """It should add the labware data to the state.""" - offset_request = LabwareOffsetCreate( - definitionUri="offset-definition-uri", - location=LabwareOffsetLocation(slotName=DeckSlotName.SLOT_1), - vector=LabwareOffsetVector(x=1, y=2, z=3), - ) - - command = create_comment_command() - - expected_definition_uri = uri_from_details( - load_name=well_plate_def.parameters.loadName, - namespace=well_plate_def.namespace, - version=well_plate_def.version, - ) - - expected_labware_data = LoadedLabware( - id="test-labware-id", - loadName=well_plate_def.parameters.loadName, - definitionUri=expected_definition_uri, - location=DeckSlotLocation(slotName=DeckSlotName.SLOT_1), - offsetId=offset_id, - displayName=display_name, - ) - - subject.handle_action( - AddLabwareOffsetAction( - request=offset_request, - labware_offset_id="offset-id", - created_at=datetime(year=2021, month=1, day=2), - ) - ) - subject.handle_action( - SucceedCommandAction( - command=command, - state_update=update_types.StateUpdate( - loaded_labware=update_types.LoadedLabwareUpdate( - labware_id="test-labware-id", - definition=well_plate_def, - offset_id=offset_id, - display_name=display_name, - new_location=DeckSlotLocation(slotName=DeckSlotName.SLOT_1), - ), - ), - ) - ) - - assert subject.state.labware_by_id["test-labware-id"] == expected_labware_data - - assert subject.state.definitions_by_uri[expected_definition_uri] == well_plate_def - - -def test_handles_reload_labware( - subject: LabwareStore, - well_plate_def: LabwareDefinition, -) -> None: - """It should override labware data in the state.""" - command = create_comment_command() - - subject.handle_action( - SucceedCommandAction( - command=command, - state_update=update_types.StateUpdate( - loaded_labware=update_types.LoadedLabwareUpdate( - labware_id="test-labware-id", - definition=well_plate_def, - offset_id=None, - display_name="display-name", - new_location=DeckSlotLocation(slotName=DeckSlotName.SLOT_1), - ), - ), - ) - ) - expected_definition_uri = uri_from_details( - load_name=well_plate_def.parameters.loadName, - namespace=well_plate_def.namespace, - version=well_plate_def.version, - ) - assert ( - subject.state.labware_by_id["test-labware-id"].definitionUri - == expected_definition_uri - ) - - offset_request = LabwareOffsetCreate( - definitionUri="offset-definition-uri", - location=LabwareOffsetLocation(slotName=DeckSlotName.SLOT_1), - vector=LabwareOffsetVector(x=1, y=2, z=3), - ) - subject.handle_action( - AddLabwareOffsetAction( - request=offset_request, - labware_offset_id="offset-id", - created_at=datetime(year=2021, month=1, day=2), - ) - ) - comment_command_2 = create_comment_command( - command_id="comment-id-1", - ) - subject.handle_action( - SucceedCommandAction( - command=comment_command_2, - state_update=update_types.StateUpdate( - labware_location=update_types.LabwareLocationUpdate( - new_location=DeckSlotLocation(slotName=DeckSlotName.SLOT_1), - offset_id="offset-id", - labware_id="test-labware-id", - ) - ), - ) - ) - - expected_labware_data = LoadedLabware( - id="test-labware-id", - loadName=well_plate_def.parameters.loadName, - definitionUri=expected_definition_uri, - location=DeckSlotLocation(slotName=DeckSlotName.SLOT_1), - offsetId="offset-id", - displayName="display-name", - ) - assert subject.state.labware_by_id["test-labware-id"] == expected_labware_data - assert subject.state.definitions_by_uri[expected_definition_uri] == well_plate_def - - -def test_handles_add_labware_definition( - subject: LabwareStore, - well_plate_def: LabwareDefinition, -) -> None: - """It should add the labware definition to the state.""" - expected_uri = uri_from_details( - load_name=well_plate_def.parameters.loadName, - namespace=well_plate_def.namespace, - version=well_plate_def.version, - ) - - subject.handle_action(AddLabwareDefinitionAction(definition=well_plate_def)) - - assert subject.state.definitions_by_uri[expected_uri] == well_plate_def - - -def test_handles_move_labware( - subject: LabwareStore, - well_plate_def: LabwareDefinition, -) -> None: - """It should update labware state with new location & offset.""" - comment_command = create_comment_command() - offset_request = LabwareOffsetCreate( - definitionUri="offset-definition-uri", - location=LabwareOffsetLocation(slotName=DeckSlotName.SLOT_1), - vector=LabwareOffsetVector(x=1, y=2, z=3), - ) - subject.handle_action( - AddLabwareOffsetAction( - request=offset_request, - labware_offset_id="old-offset-id", - created_at=datetime(year=2021, month=1, day=2), - ) - ) - subject.handle_action( - SucceedCommandAction( - command=comment_command, - state_update=update_types.StateUpdate( - loaded_labware=update_types.LoadedLabwareUpdate( - labware_id="my-labware-id", - definition=well_plate_def, - offset_id=None, - display_name="display-name", - new_location=DeckSlotLocation(slotName=DeckSlotName.SLOT_1), - ), - ), - ) - ) - - comment_2 = create_comment_command( - command_id="my-command-id", - ) - subject.handle_action( - SucceedCommandAction( - command=comment_2, - state_update=update_types.StateUpdate( - labware_location=update_types.LabwareLocationUpdate( - labware_id="my-labware-id", - offset_id="my-new-offset", - new_location=DeckSlotLocation(slotName=DeckSlotName.SLOT_1), - ), - ), - ) - ) - - assert subject.state.labware_by_id["my-labware-id"].location == DeckSlotLocation( - slotName=DeckSlotName.SLOT_1 - ) - assert subject.state.labware_by_id["my-labware-id"].offsetId == "my-new-offset" - - -def test_handles_move_labware_off_deck( - subject: LabwareStore, - well_plate_def: LabwareDefinition, -) -> None: - """It should update labware state with new location & offset.""" - comment_command = create_comment_command() - offset_request = LabwareOffsetCreate( - definitionUri="offset-definition-uri", - location=LabwareOffsetLocation(slotName=DeckSlotName.SLOT_1), - vector=LabwareOffsetVector(x=1, y=2, z=3), - ) - subject.handle_action( - AddLabwareOffsetAction( - request=offset_request, - labware_offset_id="old-offset-id", - created_at=datetime(year=2021, month=1, day=2), - ) - ) - subject.handle_action( - SucceedCommandAction( - command=comment_command, - state_update=update_types.StateUpdate( - loaded_labware=update_types.LoadedLabwareUpdate( - labware_id="my-labware-id", - definition=well_plate_def, - offset_id=None, - display_name="display-name", - new_location=DeckSlotLocation(slotName=DeckSlotName.SLOT_1), - ), - ), - ) - ) - - comment_2 = create_comment_command( - command_id="my-command-id", - ) - subject.handle_action( - SucceedCommandAction( - command=comment_2, - state_update=update_types.StateUpdate( - labware_location=update_types.LabwareLocationUpdate( - labware_id="my-labware-id", - new_location=OFF_DECK_LOCATION, - offset_id=None, - ) - ), - ) - ) - assert subject.state.labware_by_id["my-labware-id"].location == OFF_DECK_LOCATION - assert subject.state.labware_by_id["my-labware-id"].offsetId is None diff --git a/api/tests/opentrons/protocol_engine/state/test_labware_store_old.py b/api/tests/opentrons/protocol_engine/state/test_labware_store_old.py new file mode 100644 index 00000000000..3b539df58e3 --- /dev/null +++ b/api/tests/opentrons/protocol_engine/state/test_labware_store_old.py @@ -0,0 +1,343 @@ +"""Labware state store tests. + +DEPRECATED: Testing LabwareStore independently of LabwareView is no +longer helpful. Try to add new tests to test_labware_state.py, where they can be +tested together, treating LabwareState as a private implementation detail. +""" +from typing import Optional +from opentrons.protocol_engine.state import update_types +import pytest + +from datetime import datetime + +from opentrons.calibration_storage.helpers import uri_from_details +from opentrons_shared_data.deck.types import DeckDefinitionV5 +from opentrons.protocols.models import LabwareDefinition +from opentrons.types import DeckSlotName + +from opentrons.protocol_engine.types import ( + LabwareOffset, + LabwareOffsetCreate, + LabwareOffsetVector, + LabwareOffsetLocation, + DeckSlotLocation, + LoadedLabware, + OFF_DECK_LOCATION, +) +from opentrons.protocol_engine.actions import ( + AddLabwareOffsetAction, + AddLabwareDefinitionAction, + SucceedCommandAction, +) +from opentrons.protocol_engine.state.labware import LabwareStore, LabwareState + +from .command_fixtures import ( + create_comment_command, +) + + +@pytest.fixture +def subject( + ot2_standard_deck_def: DeckDefinitionV5, +) -> LabwareStore: + """Get a LabwareStore test subject.""" + return LabwareStore( + deck_definition=ot2_standard_deck_def, + deck_fixed_labware=[], + ) + + +def test_initial_state( + ot2_standard_deck_def: DeckDefinitionV5, + subject: LabwareStore, +) -> None: + """It should create the labware store with preloaded fixed labware.""" + assert subject.state == LabwareState( + deck_definition=ot2_standard_deck_def, + labware_by_id={}, + labware_offsets_by_id={}, + definitions_by_uri={}, + ) + + +def test_handles_add_labware_offset( + subject: LabwareStore, +) -> None: + """It should add the labware offset to the state and add the ID.""" + request = LabwareOffsetCreate( + definitionUri="offset-definition-uri", + location=LabwareOffsetLocation(slotName=DeckSlotName.SLOT_1), + vector=LabwareOffsetVector(x=1, y=2, z=3), + ) + + resolved_offset = LabwareOffset( + id="offset-id", + createdAt=datetime(year=2021, month=1, day=2), + definitionUri="offset-definition-uri", + location=LabwareOffsetLocation(slotName=DeckSlotName.SLOT_1), + vector=LabwareOffsetVector(x=1, y=2, z=3), + ) + + subject.handle_action( + AddLabwareOffsetAction( + labware_offset_id="offset-id", + created_at=datetime(year=2021, month=1, day=2), + request=request, + ) + ) + + assert subject.state.labware_offsets_by_id == {"offset-id": resolved_offset} + + +@pytest.mark.parametrize( + "display_name, offset_id", [("display-name", "offset-id"), (None, None)] +) +def test_handles_load_labware( + subject: LabwareStore, + well_plate_def: LabwareDefinition, + display_name: Optional[str], + offset_id: Optional[str], +) -> None: + """It should add the labware data to the state.""" + offset_request = LabwareOffsetCreate( + definitionUri="offset-definition-uri", + location=LabwareOffsetLocation(slotName=DeckSlotName.SLOT_1), + vector=LabwareOffsetVector(x=1, y=2, z=3), + ) + + command = create_comment_command() + + expected_definition_uri = uri_from_details( + load_name=well_plate_def.parameters.loadName, + namespace=well_plate_def.namespace, + version=well_plate_def.version, + ) + + expected_labware_data = LoadedLabware( + id="test-labware-id", + loadName=well_plate_def.parameters.loadName, + definitionUri=expected_definition_uri, + location=DeckSlotLocation(slotName=DeckSlotName.SLOT_1), + offsetId=offset_id, + displayName=display_name, + ) + + subject.handle_action( + AddLabwareOffsetAction( + request=offset_request, + labware_offset_id="offset-id", + created_at=datetime(year=2021, month=1, day=2), + ) + ) + subject.handle_action( + SucceedCommandAction( + command=command, + state_update=update_types.StateUpdate( + loaded_labware=update_types.LoadedLabwareUpdate( + labware_id="test-labware-id", + definition=well_plate_def, + offset_id=offset_id, + display_name=display_name, + new_location=DeckSlotLocation(slotName=DeckSlotName.SLOT_1), + ), + ), + ) + ) + + assert subject.state.labware_by_id["test-labware-id"] == expected_labware_data + + assert subject.state.definitions_by_uri[expected_definition_uri] == well_plate_def + + +def test_handles_reload_labware( + subject: LabwareStore, + well_plate_def: LabwareDefinition, +) -> None: + """It should override labware data in the state.""" + command = create_comment_command() + + subject.handle_action( + SucceedCommandAction( + command=command, + state_update=update_types.StateUpdate( + loaded_labware=update_types.LoadedLabwareUpdate( + labware_id="test-labware-id", + definition=well_plate_def, + offset_id=None, + display_name="display-name", + new_location=DeckSlotLocation(slotName=DeckSlotName.SLOT_1), + ), + ), + ) + ) + expected_definition_uri = uri_from_details( + load_name=well_plate_def.parameters.loadName, + namespace=well_plate_def.namespace, + version=well_plate_def.version, + ) + assert ( + subject.state.labware_by_id["test-labware-id"].definitionUri + == expected_definition_uri + ) + + offset_request = LabwareOffsetCreate( + definitionUri="offset-definition-uri", + location=LabwareOffsetLocation(slotName=DeckSlotName.SLOT_1), + vector=LabwareOffsetVector(x=1, y=2, z=3), + ) + subject.handle_action( + AddLabwareOffsetAction( + request=offset_request, + labware_offset_id="offset-id", + created_at=datetime(year=2021, month=1, day=2), + ) + ) + comment_command_2 = create_comment_command( + command_id="comment-id-1", + ) + subject.handle_action( + SucceedCommandAction( + command=comment_command_2, + state_update=update_types.StateUpdate( + labware_location=update_types.LabwareLocationUpdate( + new_location=DeckSlotLocation(slotName=DeckSlotName.SLOT_1), + offset_id="offset-id", + labware_id="test-labware-id", + ) + ), + ) + ) + + expected_labware_data = LoadedLabware( + id="test-labware-id", + loadName=well_plate_def.parameters.loadName, + definitionUri=expected_definition_uri, + location=DeckSlotLocation(slotName=DeckSlotName.SLOT_1), + offsetId="offset-id", + displayName="display-name", + ) + assert subject.state.labware_by_id["test-labware-id"] == expected_labware_data + assert subject.state.definitions_by_uri[expected_definition_uri] == well_plate_def + + +def test_handles_add_labware_definition( + subject: LabwareStore, + well_plate_def: LabwareDefinition, +) -> None: + """It should add the labware definition to the state.""" + expected_uri = uri_from_details( + load_name=well_plate_def.parameters.loadName, + namespace=well_plate_def.namespace, + version=well_plate_def.version, + ) + + subject.handle_action(AddLabwareDefinitionAction(definition=well_plate_def)) + + assert subject.state.definitions_by_uri[expected_uri] == well_plate_def + + +def test_handles_move_labware( + subject: LabwareStore, + well_plate_def: LabwareDefinition, +) -> None: + """It should update labware state with new location & offset.""" + comment_command = create_comment_command() + offset_request = LabwareOffsetCreate( + definitionUri="offset-definition-uri", + location=LabwareOffsetLocation(slotName=DeckSlotName.SLOT_1), + vector=LabwareOffsetVector(x=1, y=2, z=3), + ) + subject.handle_action( + AddLabwareOffsetAction( + request=offset_request, + labware_offset_id="old-offset-id", + created_at=datetime(year=2021, month=1, day=2), + ) + ) + subject.handle_action( + SucceedCommandAction( + command=comment_command, + state_update=update_types.StateUpdate( + loaded_labware=update_types.LoadedLabwareUpdate( + labware_id="my-labware-id", + definition=well_plate_def, + offset_id=None, + display_name="display-name", + new_location=DeckSlotLocation(slotName=DeckSlotName.SLOT_1), + ), + ), + ) + ) + + comment_2 = create_comment_command( + command_id="my-command-id", + ) + subject.handle_action( + SucceedCommandAction( + command=comment_2, + state_update=update_types.StateUpdate( + labware_location=update_types.LabwareLocationUpdate( + labware_id="my-labware-id", + offset_id="my-new-offset", + new_location=DeckSlotLocation(slotName=DeckSlotName.SLOT_1), + ), + ), + ) + ) + + assert subject.state.labware_by_id["my-labware-id"].location == DeckSlotLocation( + slotName=DeckSlotName.SLOT_1 + ) + assert subject.state.labware_by_id["my-labware-id"].offsetId == "my-new-offset" + + +def test_handles_move_labware_off_deck( + subject: LabwareStore, + well_plate_def: LabwareDefinition, +) -> None: + """It should update labware state with new location & offset.""" + comment_command = create_comment_command() + offset_request = LabwareOffsetCreate( + definitionUri="offset-definition-uri", + location=LabwareOffsetLocation(slotName=DeckSlotName.SLOT_1), + vector=LabwareOffsetVector(x=1, y=2, z=3), + ) + subject.handle_action( + AddLabwareOffsetAction( + request=offset_request, + labware_offset_id="old-offset-id", + created_at=datetime(year=2021, month=1, day=2), + ) + ) + subject.handle_action( + SucceedCommandAction( + command=comment_command, + state_update=update_types.StateUpdate( + loaded_labware=update_types.LoadedLabwareUpdate( + labware_id="my-labware-id", + definition=well_plate_def, + offset_id=None, + display_name="display-name", + new_location=DeckSlotLocation(slotName=DeckSlotName.SLOT_1), + ), + ), + ) + ) + + comment_2 = create_comment_command( + command_id="my-command-id", + ) + subject.handle_action( + SucceedCommandAction( + command=comment_2, + state_update=update_types.StateUpdate( + labware_location=update_types.LabwareLocationUpdate( + labware_id="my-labware-id", + new_location=OFF_DECK_LOCATION, + offset_id=None, + ) + ), + ) + ) + assert subject.state.labware_by_id["my-labware-id"].location == OFF_DECK_LOCATION + assert subject.state.labware_by_id["my-labware-id"].offsetId is None diff --git a/api/tests/opentrons/protocol_engine/state/test_labware_view.py b/api/tests/opentrons/protocol_engine/state/test_labware_view.py deleted file mode 100644 index 56113aff419..00000000000 --- a/api/tests/opentrons/protocol_engine/state/test_labware_view.py +++ /dev/null @@ -1,1620 +0,0 @@ -"""Labware state store tests.""" -import pytest -from datetime import datetime -from typing import Dict, Optional, cast, ContextManager, Any, Union, NamedTuple, List -from contextlib import nullcontext as does_not_raise - -from opentrons_shared_data.deck import load as load_deck -from opentrons_shared_data.deck.types import DeckDefinitionV5 -from opentrons_shared_data.pipette.types import LabwareUri -from opentrons_shared_data.labware import load_definition -from opentrons_shared_data.labware.labware_definition import ( - Parameters, - LabwareRole, - OverlapOffset as SharedDataOverlapOffset, - GripperOffsets, - OffsetVector, -) - -from opentrons.protocols.api_support.deck_type import ( - STANDARD_OT2_DECK, - STANDARD_OT3_DECK, -) -from opentrons.protocols.models import LabwareDefinition -from opentrons.types import DeckSlotName, MountType - -from opentrons.protocol_engine import errors -from opentrons.protocol_engine.types import ( - DeckSlotLocation, - Dimensions, - LabwareOffset, - LabwareOffsetVector, - LabwareOffsetLocation, - LoadedLabware, - ModuleModel, - ModuleLocation, - OnLabwareLocation, - LabwareLocation, - AddressableAreaLocation, - OFF_DECK_LOCATION, - OverlapOffset, - LabwareMovementOffsetData, -) -from opentrons.protocol_engine.state._move_types import EdgePathType -from opentrons.protocol_engine.state.labware import ( - LabwareState, - LabwareView, - LabwareLoadParams, -) - -plate = LoadedLabware( - id="plate-id", - loadName="plate-load-name", - location=DeckSlotLocation(slotName=DeckSlotName.SLOT_1), - definitionUri="some-plate-uri", - offsetId=None, - displayName="Fancy Plate Name", -) - -flex_tiprack = LoadedLabware( - id="flex-tiprack-id", - loadName="flex-tiprack-load-name", - location=DeckSlotLocation(slotName=DeckSlotName.SLOT_1), - definitionUri="some-flex-tiprack-uri", - offsetId=None, - displayName="Flex Tiprack Name", -) - -reservoir = LoadedLabware( - id="reservoir-id", - loadName="reservoir-load-name", - location=DeckSlotLocation(slotName=DeckSlotName.SLOT_2), - definitionUri="some-reservoir-uri", - offsetId=None, -) - -trash = LoadedLabware( - id="trash-id", - loadName="trash-load-name", - location=DeckSlotLocation(slotName=DeckSlotName.SLOT_2), - definitionUri="some-trash-uri", - offsetId=None, -) - -tube_rack = LoadedLabware( - id="tube-rack-id", - loadName="tube-rack-load-name", - location=DeckSlotLocation(slotName=DeckSlotName.SLOT_1), - definitionUri="some-tube-rack-uri", - offsetId=None, -) - -tip_rack = LoadedLabware( - id="tip-rack-id", - loadName="tip-rack-load-name", - location=DeckSlotLocation(slotName=DeckSlotName.SLOT_1), - definitionUri="some-tip-rack-uri", - offsetId=None, -) - -adapter_plate = LoadedLabware( - id="adapter-plate-id", - loadName="adapter-load-name", - location=DeckSlotLocation(slotName=DeckSlotName.SLOT_1), - definitionUri="some-adapter-uri", - offsetId=None, -) - - -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[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(DeckDefinitionV5, {"fake": True}), - ) - - return LabwareView(state=state) - - -def test_get_labware_data_bad_id() -> None: - """get_labware_data_by_id should raise if labware ID doesn't exist.""" - subject = get_labware_view() - - with pytest.raises(errors.LabwareNotLoadedError): - subject.get("asdfghjkl") - - -def test_get_labware_data_by_id() -> None: - """It should retrieve labware data from the state.""" - subject = get_labware_view(labware_by_id={"plate-id": plate}) - - assert subject.get("plate-id") == plate - - -def test_get_id_by_module() -> None: - """Should return the labware id associated to the module.""" - subject = get_labware_view( - labware_by_id={ - "labware-id": LoadedLabware( - id="labware-id", - loadName="test", - definitionUri="test-uri", - location=ModuleLocation(moduleId="module-id"), - ) - } - ) - assert subject.get_id_by_module(module_id="module-id") == "labware-id" - - -def test_get_id_by_module_raises_error() -> None: - """Should raise error that labware not found.""" - subject = get_labware_view( - labware_by_id={ - "labware-id": LoadedLabware( - id="labware-id", - loadName="test", - definitionUri="test-uri", - location=ModuleLocation(moduleId="module-id"), - ) - } - ) - with pytest.raises(errors.exceptions.LabwareNotLoadedOnModuleError): - subject.get_id_by_module(module_id="no-module-id") - - -def test_get_id_by_labware() -> None: - """Should return the labware id associated to the labware.""" - subject = get_labware_view( - labware_by_id={ - "labware-id": LoadedLabware( - id="labware-id", - loadName="test", - definitionUri="test-uri", - location=OnLabwareLocation(labwareId="other-labware-id"), - ) - } - ) - assert subject.get_id_by_labware(labware_id="other-labware-id") == "labware-id" - - -def test_get_id_by_labware_raises_error() -> None: - """Should raise error that labware not found.""" - subject = get_labware_view( - labware_by_id={ - "labware-id": LoadedLabware( - id="labware-id", - loadName="test", - definitionUri="test-uri", - location=OnLabwareLocation(labwareId="other-labware-id"), - ) - } - ) - with pytest.raises(errors.exceptions.LabwareNotLoadedOnLabwareError): - subject.get_id_by_labware(labware_id="no-labware-id") - - -def test_raise_if_labware_has_labware_on_top() -> None: - """It should raise if labware has another labware on top.""" - subject = get_labware_view( - labware_by_id={ - "labware-id-1": LoadedLabware( - id="labware-id-1", - loadName="test", - definitionUri="test-uri", - location=DeckSlotLocation(slotName=DeckSlotName.SLOT_1), - ), - "labware-id-2": LoadedLabware( - id="labware-id-2", - loadName="test", - definitionUri="test-uri", - location=ModuleLocation(moduleId="module-id"), - ), - "labware-id-3": LoadedLabware( - id="labware-id-3", - loadName="test", - definitionUri="test-uri", - location=OnLabwareLocation(labwareId="labware-id-1"), - ), - } - ) - subject.raise_if_labware_has_labware_on_top("labware-id-2") - subject.raise_if_labware_has_labware_on_top("labware-id-3") - with pytest.raises(errors.exceptions.LabwareIsInStackError): - subject.raise_if_labware_has_labware_on_top("labware-id-1") - - -def test_get_labware_definition(well_plate_def: LabwareDefinition) -> None: - """It should get a labware's definition from the state.""" - subject = get_labware_view( - labware_by_id={"plate-id": plate}, - definitions_by_uri={"some-plate-uri": well_plate_def}, - ) - - assert subject.get_definition("plate-id") == well_plate_def - - -def test_get_labware_definition_bad_id() -> None: - """get_labware_definition should raise if labware definition doesn't exist.""" - subject = get_labware_view() - - with pytest.raises(errors.LabwareDefinitionDoesNotExistError): - subject.get_definition_by_uri(cast(LabwareUri, "not-a-uri")) - - -@pytest.mark.parametrize( - argnames=["namespace", "version"], - argvalues=[("world", 123), (None, 123), ("world", None), (None, None)], -) -def test_find_custom_labware_params( - namespace: Optional[str], version: Optional[int] -) -> None: - """It should find the missing (if any) load labware parameters.""" - labware_def = LabwareDefinition.construct( # type: ignore[call-arg] - parameters=Parameters.construct(loadName="hello"), # type: ignore[call-arg] - namespace="world", - version=123, - ) - standard_def = LabwareDefinition.construct( # type: ignore[call-arg] - parameters=Parameters.construct(loadName="goodbye"), # type: ignore[call-arg] - namespace="opentrons", - version=456, - ) - - subject = get_labware_view( - definitions_by_uri={ - "some-labware-uri": labware_def, - "some-standard-uri": standard_def, - }, - ) - - result = subject.find_custom_labware_load_params() - - assert result == [ - LabwareLoadParams(load_name="hello", namespace="world", version=123) - ] - - -def test_get_all_labware( - well_plate_def: LabwareDefinition, - reservoir_def: LabwareDefinition, -) -> None: - """It should return all labware.""" - subject = get_labware_view( - labware_by_id={ - "plate-id": plate, - "reservoir-id": reservoir, - } - ) - - all_labware = subject.get_all() - - assert all_labware == [plate, reservoir] - - -def test_get_labware_location() -> None: - """It should return labware location.""" - subject = get_labware_view(labware_by_id={"plate-id": plate}) - - result = subject.get_location("plate-id") - - assert result == DeckSlotLocation(slotName=DeckSlotName.SLOT_1) - - -@pytest.mark.parametrize( - argnames="location", - argvalues=[ - DeckSlotLocation(slotName=DeckSlotName.SLOT_D1), - ModuleLocation(moduleId="module-id"), - OFF_DECK_LOCATION, - ], -) -def test_get_parent_location(location: LabwareLocation) -> None: - """It should return the non-OnLabware location of a labware.""" - subject = get_labware_view( - labware_by_id={ - "labware-id": LoadedLabware( - id="plate-id", - loadName="load-name", - location=location, - definitionUri="some-uri", - ) - } - ) - - result = subject.get_parent_location(labware_id="labware-id") - - assert result == location - - -@pytest.mark.parametrize( - argnames="location", - argvalues=[ - DeckSlotLocation(slotName=DeckSlotName.SLOT_D1), - ModuleLocation(moduleId="module-id"), - ], -) -def test_get_parent_location_on_labware(location: LabwareLocation) -> None: - """It should return the non-OnLabware location of a labware.""" - subject = get_labware_view( - labware_by_id={ - "top-id": LoadedLabware( - id="top-id", - loadName="load-name", - location=OnLabwareLocation(labwareId="middle-id"), - definitionUri="some-uri", - ), - "middle-id": LoadedLabware( - id="middle-id", - loadName="load-name", - location=OnLabwareLocation(labwareId="bottom-id"), - definitionUri="some-uri", - ), - "bottom-id": LoadedLabware( - id="bottom-id", - loadName="load-name", - location=location, - definitionUri="some-uri", - ), - } - ) - - result = subject.get_parent_location(labware_id="top-id") - - assert result == location - - -def test_get_has_quirk( - well_plate_def: LabwareDefinition, - reservoir_def: LabwareDefinition, -) -> None: - """It should return whether a labware by ID has a given quirk.""" - subject = get_labware_view( - labware_by_id={ - "plate-id": plate, - "reservoir-id": reservoir, - }, - definitions_by_uri={ - "some-plate-uri": well_plate_def, - "some-reservoir-uri": reservoir_def, - }, - ) - - well_plate_has_center_quirk = subject.get_has_quirk( - labware_id="plate-id", - quirk="centerMultichannelOnWells", - ) - - reservoir_has_center_quirk = subject.get_has_quirk( - labware_id="reservoir-id", - quirk="centerMultichannelOnWells", - ) - - assert well_plate_has_center_quirk is False - assert reservoir_has_center_quirk is True - - -def test_quirks( - well_plate_def: LabwareDefinition, - reservoir_def: LabwareDefinition, -) -> None: - """It should return a labware's quirks.""" - subject = get_labware_view( - labware_by_id={ - "plate-id": plate, - "reservoir-id": reservoir, - }, - definitions_by_uri={ - "some-plate-uri": well_plate_def, - "some-reservoir-uri": reservoir_def, - }, - ) - - well_plate_quirks = subject.get_quirks("plate-id") - reservoir_quirks = subject.get_quirks("reservoir-id") - - assert well_plate_quirks == [] - assert reservoir_quirks == ["centerMultichannelOnWells", "touchTipDisabled"] - - -def test_get_well_definition_bad_name(well_plate_def: LabwareDefinition) -> None: - """get_well_definition should raise if well name doesn't exist.""" - subject = get_labware_view( - labware_by_id={"plate-id": plate}, - definitions_by_uri={"some-plate-uri": well_plate_def}, - ) - - with pytest.raises(errors.WellDoesNotExistError): - subject.get_well_definition(labware_id="plate-id", well_name="foobar") - - -def test_get_well_definition(well_plate_def: LabwareDefinition) -> None: - """It should return a well definition by well name.""" - subject = get_labware_view( - labware_by_id={"plate-id": plate}, - definitions_by_uri={"some-plate-uri": well_plate_def}, - ) - - expected_well_def = well_plate_def.wells["B2"] - result = subject.get_well_definition(labware_id="plate-id", well_name="B2") - - assert result == expected_well_def - - -def test_get_well_definition_get_first(well_plate_def: LabwareDefinition) -> None: - """It should return the first well definition if no given well name.""" - subject = get_labware_view( - labware_by_id={"plate-id": plate}, - definitions_by_uri={"some-plate-uri": well_plate_def}, - ) - - expected_well_def = well_plate_def.wells["A1"] - result = subject.get_well_definition(labware_id="plate-id", well_name=None) - - assert result == expected_well_def - - -def test_get_well_geometry_raises_error(well_plate_def: LabwareDefinition) -> None: - """It should raise an IncompleteLabwareDefinitionError when there's no innerLabwareGeometry.""" - subject = get_labware_view( - labware_by_id={"plate-id": plate}, - definitions_by_uri={"some-plate-uri": well_plate_def}, - ) - - with pytest.raises(errors.IncompleteLabwareDefinitionError): - subject.get_well_geometry(labware_id="plate-id") - - -def test_get_well_size_circular(well_plate_def: LabwareDefinition) -> None: - """It should return the well dimensions of a circular well.""" - subject = get_labware_view( - labware_by_id={"plate-id": plate}, - definitions_by_uri={"some-plate-uri": well_plate_def}, - ) - expected_well_def = well_plate_def.wells["A2"] - expected_size = ( - expected_well_def.diameter, - expected_well_def.diameter, - expected_well_def.depth, - ) - - result = subject.get_well_size(labware_id="plate-id", well_name="A2") - - assert result == expected_size - - -def test_get_well_size_rectangular(reservoir_def: LabwareDefinition) -> None: - """It should return the well dimensions of a rectangular well.""" - subject = get_labware_view( - labware_by_id={"reservoir-id": reservoir}, - definitions_by_uri={"some-reservoir-uri": reservoir_def}, - ) - expected_well_def = reservoir_def.wells["A2"] - expected_size = ( - expected_well_def.xDimension, - expected_well_def.yDimension, - expected_well_def.depth, - ) - - result = subject.get_well_size(labware_id="reservoir-id", well_name="A2") - - assert result == expected_size - - -def test_labware_has_well(falcon_tuberack_def: LabwareDefinition) -> None: - """It should return a list of wells from definition.""" - subject = get_labware_view( - labware_by_id={"tube-rack-id": tube_rack}, - definitions_by_uri={"some-tube-rack-uri": falcon_tuberack_def}, - ) - - result = subject.validate_liquid_allowed_in_labware( - labware_id="tube-rack-id", wells={"A1": 30, "B1": 100} - ) - assert result == ["A1", "B1"] - - with pytest.raises(errors.WellDoesNotExistError): - subject.validate_liquid_allowed_in_labware( - labware_id="tube-rack-id", wells={"AA": 30} - ) - - with pytest.raises(errors.LabwareNotLoadedError): - subject.validate_liquid_allowed_in_labware(labware_id="no-id", wells={"A1": 30}) - - -def test_validate_liquid_allowed_raises_incompatible_labware() -> None: - """It should raise when validating labware that is a tiprack or an adapter.""" - subject = get_labware_view( - labware_by_id={ - "tiprack-id": LoadedLabware( - id="tiprack-id", - loadName="test1", - definitionUri="some-tiprack-uri", - location=DeckSlotLocation(slotName=DeckSlotName.SLOT_1), - ), - "adapter-id": LoadedLabware( - id="adapter-id", - loadName="test2", - definitionUri="some-adapter-uri", - location=DeckSlotLocation(slotName=DeckSlotName.SLOT_2), - ), - }, - definitions_by_uri={ - "some-tiprack-uri": LabwareDefinition.construct( # type: ignore[call-arg] - parameters=Parameters.construct(isTiprack=True), # type: ignore[call-arg] - wells={}, - ), - "some-adapter-uri": LabwareDefinition.construct( # type: ignore[call-arg] - parameters=Parameters.construct(isTiprack=False), # type: ignore[call-arg] - allowedRoles=[LabwareRole.adapter], - wells={}, - ), - }, - ) - - with pytest.raises(errors.LabwareIsTipRackError): - subject.validate_liquid_allowed_in_labware(labware_id="tiprack-id", wells={}) - - with pytest.raises(errors.LabwareIsAdapterError): - subject.validate_liquid_allowed_in_labware(labware_id="adapter-id", wells={}) - - -def test_get_tip_length_raises_with_non_tip_rack( - well_plate_def: LabwareDefinition, -) -> None: - """It should raise if you try to get the tip length of a regular labware.""" - subject = get_labware_view( - labware_by_id={"plate-id": plate}, - definitions_by_uri={"some-plate-uri": well_plate_def}, - ) - - with pytest.raises(errors.LabwareIsNotTipRackError): - subject.get_tip_length("plate-id") - - -def test_get_tip_length_gets_length_from_definition( - tip_rack_def: LabwareDefinition, -) -> None: - """It should return the tip length from the definition.""" - subject = get_labware_view( - labware_by_id={"tip-rack-id": tip_rack}, - definitions_by_uri={"some-tip-rack-uri": tip_rack_def}, - ) - - length = subject.get_tip_length("tip-rack-id", 12.3) - assert length == tip_rack_def.parameters.tipLength - 12.3 # type: ignore[operator] - - -def test_get_tip_drop_z_offset() -> None: - """It should get a tip drop z offset by scaling the tip length.""" - tip_rack_def = LabwareDefinition.construct( # type: ignore[call-arg] - parameters=Parameters.construct( # type: ignore[call-arg] - tipLength=100, - ) - ) - - subject = get_labware_view( - labware_by_id={"tip-rack-id": tip_rack}, - definitions_by_uri={"some-tip-rack-uri": tip_rack_def}, - ) - - result = subject.get_tip_drop_z_offset( - labware_id="tip-rack-id", length_scale=0.5, additional_offset=-0.123 - ) - - assert result == -50.123 - - -def test_get_labware_uri_from_definition(tip_rack_def: LabwareDefinition) -> None: - """It should return the labware's definition URI.""" - tip_rack = LoadedLabware( - id="tip-rack-id", - loadName="tip-rack-load-name", - location=DeckSlotLocation(slotName=DeckSlotName.SLOT_1), - definitionUri="some-tip-rack-uri", - offsetId=None, - ) - - subject = get_labware_view( - labware_by_id={"tip-rack-id": tip_rack}, - definitions_by_uri={"some-tip-rack-uri": tip_rack_def}, - ) - - result = subject.get_definition_uri(labware_id="tip-rack-id") - assert result == "some-tip-rack-uri" - - -def test_get_labware_uri_from_full_definition(tip_rack_def: LabwareDefinition) -> None: - """It should be able to construct a URI given a full definition.""" - subject = get_labware_view() - result = subject.get_uri_from_definition(tip_rack_def) - assert result == "opentrons/opentrons_96_tiprack_300ul/1" - - -def test_is_tiprack( - tip_rack_def: LabwareDefinition, reservoir_def: LabwareDefinition -) -> None: - """It should determine if labware is a tip rack.""" - subject = get_labware_view( - labware_by_id={ - "tip-rack-id": tip_rack, - "reservoir-id": reservoir, - }, - definitions_by_uri={ - "some-tip-rack-uri": tip_rack_def, - "some-reservoir-uri": reservoir_def, - }, - ) - - assert subject.is_tiprack(labware_id="tip-rack-id") is True - assert subject.is_tiprack(labware_id="reservoir-id") is False - - -def test_get_load_name(reservoir_def: LabwareDefinition) -> None: - """It should return the load name.""" - subject = get_labware_view( - labware_by_id={"reservoir-id": reservoir}, - definitions_by_uri={"some-reservoir-uri": reservoir_def}, - ) - - result = subject.get_load_name("reservoir-id") - - assert result == reservoir_def.parameters.loadName - - -def test_get_dimensions(well_plate_def: LabwareDefinition) -> None: - """It should compute the dimensions of a labware.""" - subject = get_labware_view( - labware_by_id={"plate-id": plate}, - definitions_by_uri={"some-plate-uri": well_plate_def}, - ) - - result = subject.get_dimensions(labware_id="plate-id") - - assert result == Dimensions( - x=well_plate_def.dimensions.xDimension, - y=well_plate_def.dimensions.yDimension, - z=well_plate_def.dimensions.zDimension, - ) - - -def test_get_labware_overlap_offsets() -> None: - """It should get the labware overlap offsets.""" - subject = get_labware_view() - result = subject.get_labware_overlap_offsets( - definition=LabwareDefinition.construct( # type: ignore[call-arg] - stackingOffsetWithLabware={ - "bottom-labware-name": SharedDataOverlapOffset(x=1, y=2, z=3) - } - ), - below_labware_name="bottom-labware-name", - ) - - assert result == OverlapOffset(x=1, y=2, z=3) - - -class ModuleOverlapSpec(NamedTuple): - """Spec data to test LabwareView.get_module_overlap_offsets.""" - - spec_deck_definition: DeckDefinitionV5 - module_model: ModuleModel - stacking_offset_with_module: Dict[str, SharedDataOverlapOffset] - expected_offset: OverlapOffset - - -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, 5), - module_model=ModuleModel.TEMPERATURE_MODULE_V2, - stacking_offset_with_module={ - str(ModuleModel.TEMPERATURE_MODULE_V2.value): SharedDataOverlapOffset( - x=1, y=2, z=3 - ), - }, - expected_offset=OverlapOffset(x=1, y=2, z=3), - ), - ModuleOverlapSpec( - # Labware on TC Gen1 on OT2, with stacking overlap for TC Gen1 - 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( - x=11, y=22, z=33 - ), - }, - expected_offset=OverlapOffset(x=11, y=22, z=33), - ), - ModuleOverlapSpec( - # Labware on TC Gen2 on OT2, with no stacking overlap - 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, 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, 5), - module_model=ModuleModel.THERMOCYCLER_MODULE_V2, - stacking_offset_with_module={ - str(ModuleModel.THERMOCYCLER_MODULE_V2.value): SharedDataOverlapOffset( - x=111, y=222, z=333 - ), - }, - expected_offset=OverlapOffset(x=111, y=222, z=333), - ), -] - - -@pytest.mark.parametrize( - argnames=ModuleOverlapSpec._fields, - argvalues=module_overlap_specs, -) -def test_get_module_overlap_offsets( - spec_deck_definition: DeckDefinitionV5, - module_model: ModuleModel, - stacking_offset_with_module: Dict[str, SharedDataOverlapOffset], - expected_offset: OverlapOffset, -) -> None: - """It should get the labware overlap offsets.""" - subject = get_labware_view( - deck_definition=spec_deck_definition, - ) - result = subject.get_module_overlap_offsets( - definition=LabwareDefinition.construct( # type: ignore[call-arg] - stackingOffsetWithModule=stacking_offset_with_module - ), - module_model=module_model, - ) - - assert result == expected_offset - - -def test_get_default_magnet_height( - magdeck_well_plate_def: LabwareDefinition, -) -> None: - """Should get get the default value for magnetic height.""" - well_plate = LoadedLabware( - id="well-plate-id", - loadName="load-name", - location=ModuleLocation(moduleId="module-id"), - definitionUri="well-plate-uri", - offsetId=None, - ) - - subject = get_labware_view( - labware_by_id={"well-plate-id": well_plate}, - definitions_by_uri={"well-plate-uri": magdeck_well_plate_def}, - ) - - assert subject.get_default_magnet_height(module_id="module-id", offset=2) == 12.0 - - -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) - - assert subject.get_deck_definition() == ot2_standard_deck_def - - -def test_get_labware_offset_vector() -> None: - """It should get a labware's offset vector.""" - labware_without_offset = LoadedLabware( - id="without-offset-labware-id", - loadName="labware-load-name", - location=DeckSlotLocation(slotName=DeckSlotName.SLOT_1), - definitionUri="some-labware-uri", - offsetId=None, - ) - - labware_with_offset = LoadedLabware( - id="with-offset-labware-id", - loadName="labware-load-name", - location=DeckSlotLocation(slotName=DeckSlotName.SLOT_1), - definitionUri="some-labware-uri", - offsetId="offset-id", - ) - - offset_vector = LabwareOffsetVector(x=1, y=2, z=3) - offset = LabwareOffset( - id="offset-id", - createdAt=datetime(year=2021, month=1, day=2), - definitionUri="some-labware-uri", - location=LabwareOffsetLocation(slotName=DeckSlotName.SLOT_1), - vector=offset_vector, - ) - - subject = get_labware_view( - labware_by_id={ - labware_without_offset.id: labware_without_offset, - labware_with_offset.id: labware_with_offset, - }, - labware_offsets_by_id={"offset-id": offset}, - ) - - assert subject.get_labware_offset_vector(labware_with_offset.id) == offset.vector - - assert subject.get_labware_offset_vector( - labware_without_offset.id - ) == LabwareOffsetVector(x=0, y=0, z=0) - - with pytest.raises(errors.LabwareNotLoadedError): - subject.get_labware_offset_vector("wrong-labware-id") - - -def test_get_labware_offset() -> None: - """It should return the requested labware offset, if it exists.""" - offset_a = LabwareOffset( - id="id-a", - createdAt=datetime(year=2021, month=1, day=1), - definitionUri="uri-a", - location=LabwareOffsetLocation(slotName=DeckSlotName.SLOT_1), - vector=LabwareOffsetVector(x=1, y=1, z=1), - ) - - offset_b = LabwareOffset( - id="id-b", - createdAt=datetime(year=2022, month=2, day=2), - definitionUri="uri-b", - location=LabwareOffsetLocation(slotName=DeckSlotName.SLOT_2), - vector=LabwareOffsetVector(x=2, y=2, z=2), - ) - - subject = get_labware_view( - labware_offsets_by_id={"id-a": offset_a, "id-b": offset_b} - ) - - assert subject.get_labware_offset("id-a") == offset_a - assert subject.get_labware_offset("id-b") == offset_b - with pytest.raises(errors.LabwareOffsetDoesNotExistError): - subject.get_labware_offset("wrong-labware-offset-id") - - -def test_get_labware_offsets() -> None: - """It should return a list of all labware offsets, in order.""" - offset_a = LabwareOffset( - id="id-a", - createdAt=datetime(year=2021, month=1, day=1), - definitionUri="uri-a", - location=LabwareOffsetLocation(slotName=DeckSlotName.SLOT_1), - vector=LabwareOffsetVector(x=1, y=1, z=1), - ) - - offset_b = LabwareOffset( - id="id-b", - createdAt=datetime(year=2022, month=2, day=2), - definitionUri="uri-b", - location=LabwareOffsetLocation(slotName=DeckSlotName.SLOT_2), - vector=LabwareOffsetVector(x=2, y=2, z=2), - ) - - empty_subject = get_labware_view() - assert empty_subject.get_labware_offsets() == [] - - filled_subject_a_before_b = get_labware_view( - labware_offsets_by_id={"id-a": offset_a, "id-b": offset_b} - ) - assert filled_subject_a_before_b.get_labware_offsets() == [offset_a, offset_b] - - filled_subject_b_before_a = get_labware_view( - labware_offsets_by_id={"id-b": offset_b, "id-a": offset_a} - ) - assert filled_subject_b_before_a.get_labware_offsets() == [offset_b, offset_a] - - -def test_find_applicable_labware_offset() -> None: - """It should return the most recent offset with matching URI and location.""" - offset_1 = LabwareOffset( - id="id-1", - createdAt=datetime(year=2021, month=1, day=1), - definitionUri="definition-uri", - location=LabwareOffsetLocation(slotName=DeckSlotName.SLOT_1), - vector=LabwareOffsetVector(x=1, y=1, z=1), - ) - - # Same definitionUri and location; different id, createdAt, and offset. - offset_2 = LabwareOffset( - id="id-2", - createdAt=datetime(year=2022, month=2, day=2), - definitionUri="definition-uri", - location=LabwareOffsetLocation(slotName=DeckSlotName.SLOT_1), - vector=LabwareOffsetVector(x=2, y=2, z=2), - ) - - offset_3 = LabwareOffset( - id="id-3", - createdAt=datetime(year=2023, month=3, day=3), - definitionUri="on-module-definition-uri", - location=LabwareOffsetLocation( - slotName=DeckSlotName.SLOT_1, - moduleModel=ModuleModel.TEMPERATURE_MODULE_V1, - ), - vector=LabwareOffsetVector(x=3, y=3, z=3), - ) - - subject = get_labware_view( - # Simulate offset_2 having been added after offset_1. - labware_offsets_by_id={"id-1": offset_1, "id-2": offset_2, "id-3": offset_3} - ) - - # Matching both definitionURI and location. Should return 2nd (most recent) offset. - assert ( - subject.find_applicable_labware_offset( - definition_uri="definition-uri", - location=LabwareOffsetLocation(slotName=DeckSlotName.SLOT_1), - ) - == offset_2 - ) - - assert ( - subject.find_applicable_labware_offset( - definition_uri="on-module-definition-uri", - location=LabwareOffsetLocation( - slotName=DeckSlotName.SLOT_1, - moduleModel=ModuleModel.TEMPERATURE_MODULE_V1, - ), - ) - == offset_3 - ) - - # Doesn't match anything, since definitionUri is different. - assert ( - subject.find_applicable_labware_offset( - definition_uri="different-definition-uri", - location=LabwareOffsetLocation(slotName=DeckSlotName.SLOT_1), - ) - is None - ) - - # Doesn't match anything, since location is different. - assert ( - subject.find_applicable_labware_offset( - definition_uri="different-definition-uri", - location=LabwareOffsetLocation(slotName=DeckSlotName.SLOT_2), - ) - is None - ) - - -def test_get_user_specified_display_name() -> None: - """It should get a labware's user-specified display name.""" - subject = get_labware_view( - labware_by_id={ - "plate_with_display_name": plate, - "reservoir_without_display_name": reservoir, - }, - ) - - assert ( - subject.get_user_specified_display_name("plate_with_display_name") - == "Fancy Plate Name" - ) - assert ( - subject.get_user_specified_display_name("reservoir_without_display_name") - is None - ) - - -def test_get_display_name( - well_plate_def: LabwareDefinition, - reservoir_def: LabwareDefinition, -) -> None: - """It should get the labware's display name.""" - subject = get_labware_view( - labware_by_id={ - "plate_with_custom_display_name": plate, - "reservoir_with_default_display_name": reservoir, - }, - definitions_by_uri={ - "some-plate-uri": well_plate_def, - "some-reservoir-uri": reservoir_def, - }, - ) - assert ( - subject.get_display_name("plate_with_custom_display_name") == "Fancy Plate Name" - ) - assert ( - subject.get_display_name("reservoir_with_default_display_name") - == "NEST 12 Well Reservoir 15 mL" - ) - - -def test_get_fixed_trash_id() -> None: - """It should return the ID of the labware loaded into the fixed trash slot.""" - # OT-2 fixed trash slot: - subject = get_labware_view( - labware_by_id={ - "abc123": LoadedLabware( - id="abc123", - loadName="trash-load-name", - location=DeckSlotLocation(slotName=DeckSlotName.FIXED_TRASH), - definitionUri="trash-definition-uri", - offsetId=None, - displayName=None, - ) - }, - ) - assert subject.get_fixed_trash_id() == "abc123" - - # OT-3 fixed trash slot: - subject = get_labware_view( - labware_by_id={ - "abc123": LoadedLabware( - id="abc123", - loadName="trash-load-name", - location=DeckSlotLocation(slotName=DeckSlotName.SLOT_A3), - definitionUri="trash-definition-uri", - offsetId=None, - displayName=None, - ) - }, - ) - assert subject.get_fixed_trash_id() == "abc123" - - # Nothing in the fixed trash slot: - subject = get_labware_view( - labware_by_id={ - "abc123": LoadedLabware( - id="abc123", - loadName="trash-load-name", - location=DeckSlotLocation(slotName=DeckSlotName.SLOT_1), - definitionUri="trash-definition-uri", - offsetId=None, - displayName=None, - ) - }, - ) - assert subject.get_fixed_trash_id() is None - - -@pytest.mark.parametrize( - argnames=["location", "expected_raise"], - argvalues=[ - ( - DeckSlotLocation(slotName=DeckSlotName.SLOT_1), - pytest.raises(errors.LocationIsOccupiedError), - ), - ( - ModuleLocation(moduleId="module-id"), - pytest.raises(errors.LocationIsOccupiedError), - ), - (DeckSlotLocation(slotName=DeckSlotName.SLOT_2), does_not_raise()), - (ModuleLocation(moduleId="non-matching-id"), does_not_raise()), - ], -) -def test_raise_if_labware_in_location( - location: Union[DeckSlotLocation, ModuleLocation], - expected_raise: ContextManager[Any], -) -> None: - """It should raise if there is labware in specified location.""" - subject = get_labware_view( - labware_by_id={ - "abc123": LoadedLabware( - id="abc123", - loadName="labware-1", - location=DeckSlotLocation(slotName=DeckSlotName.SLOT_1), - definitionUri="labware-definition-uri", - offsetId=None, - displayName=None, - ), - "xyz456": LoadedLabware( - id="xyz456", - loadName="labware-2", - location=ModuleLocation(moduleId="module-id"), - definitionUri="labware-definition-uri", - offsetId=None, - displayName=None, - ), - } - ) - with expected_raise: - subject.raise_if_labware_in_location(location=location) - - -def test_get_by_slot() -> None: - """It should get the labware in a given slot.""" - labware_1 = LoadedLabware.construct( # type: ignore[call-arg] - id="1", location=DeckSlotLocation(slotName=DeckSlotName.SLOT_1) - ) - labware_2 = LoadedLabware.construct( # type: ignore[call-arg] - id="2", location=DeckSlotLocation(slotName=DeckSlotName.SLOT_2) - ) - labware_3 = LoadedLabware.construct( # type: ignore[call-arg] - id="3", location=ModuleLocation(moduleId="cool-module") - ) - - subject = get_labware_view( - labware_by_id={"1": labware_1, "2": labware_2, "3": labware_3} - ) - - assert subject.get_by_slot(DeckSlotName.SLOT_1) == labware_1 - assert subject.get_by_slot(DeckSlotName.SLOT_2) == labware_2 - assert subject.get_by_slot(DeckSlotName.SLOT_3) is None - - -@pytest.mark.parametrize( - ["well_name", "mount", "labware_slot", "next_to_module", "expected_result"], - [ - ("abc", MountType.RIGHT, DeckSlotName.SLOT_3, False, EdgePathType.LEFT), - ("abc", MountType.RIGHT, DeckSlotName.SLOT_D3, False, EdgePathType.LEFT), - ("abc", MountType.RIGHT, DeckSlotName.SLOT_1, True, EdgePathType.LEFT), - ("abc", MountType.RIGHT, DeckSlotName.SLOT_D1, True, EdgePathType.LEFT), - ("pqr", MountType.LEFT, DeckSlotName.SLOT_3, True, EdgePathType.RIGHT), - ("pqr", MountType.LEFT, DeckSlotName.SLOT_D3, True, EdgePathType.RIGHT), - ("pqr", MountType.LEFT, DeckSlotName.SLOT_3, False, EdgePathType.DEFAULT), - ("pqr", MountType.LEFT, DeckSlotName.SLOT_D3, False, EdgePathType.DEFAULT), - ("pqr", MountType.RIGHT, DeckSlotName.SLOT_3, True, EdgePathType.DEFAULT), - ("pqr", MountType.RIGHT, DeckSlotName.SLOT_D3, True, EdgePathType.DEFAULT), - ("def", MountType.LEFT, DeckSlotName.SLOT_3, True, EdgePathType.DEFAULT), - ("def", MountType.LEFT, DeckSlotName.SLOT_D3, True, EdgePathType.DEFAULT), - ], -) -def test_get_edge_path_type( - well_name: str, - mount: MountType, - labware_slot: DeckSlotName, - next_to_module: bool, - expected_result: EdgePathType, -) -> None: - """It should get the proper edge path type based on well name, mount, and labware position.""" - labware = LoadedLabware( - id="tip-rack-id", - loadName="load-name", - location=DeckSlotLocation(slotName=DeckSlotName.SLOT_1), - definitionUri="some-labware-uri", - offsetId=None, - ) - - labware_def = LabwareDefinition.construct( # type: ignore[call-arg] - ordering=[["abc", "def"], ["ghi", "jkl"], ["mno", "pqr"]] - ) - - subject = get_labware_view( - labware_by_id={"labware-id": labware}, - definitions_by_uri={ - "some-labware-uri": labware_def, - }, - ) - - result = subject.get_edge_path_type( - "labware-id", well_name, mount, labware_slot, next_to_module - ) - - assert result == expected_result - - -def test_get_all_labware_definition( - tip_rack_def: LabwareDefinition, falcon_tuberack_def: LabwareDefinition -) -> None: - """It should return the loaded labware definition list.""" - subject = get_labware_view( - labware_by_id={ - "labware-id": LoadedLabware( - id="labware-id", - loadName="test", - definitionUri="opentrons_96_tiprack_300ul", - location=ModuleLocation(moduleId="module-id"), - ) - }, - definitions_by_uri={ - "opentrons_96_tiprack_300ul": tip_rack_def, - "falcon-definition": falcon_tuberack_def, - }, - ) - - result = subject.get_loaded_labware_definitions() - - assert result == [tip_rack_def] - - -def test_get_all_labware_definition_empty() -> None: - """It should return an empty list.""" - subject = get_labware_view( - labware_by_id={}, - ) - - result = subject.get_loaded_labware_definitions() - - assert result == [] - - -def test_raise_if_labware_inaccessible_by_pipette_staging_area() -> None: - """It should raise if the labware is on a staging slot.""" - subject = get_labware_view( - labware_by_id={ - "labware-id": LoadedLabware( - id="labware-id", - loadName="test", - definitionUri="def-uri", - location=AddressableAreaLocation(addressableAreaName="B4"), - ) - }, - ) - - with pytest.raises( - errors.LocationNotAccessibleByPipetteError, match="on staging slot" - ): - subject.raise_if_labware_inaccessible_by_pipette("labware-id") - - -def test_raise_if_labware_inaccessible_by_pipette_off_deck() -> None: - """It should raise if the labware is off-deck.""" - subject = get_labware_view( - labware_by_id={ - "labware-id": LoadedLabware( - id="labware-id", - loadName="test", - definitionUri="def-uri", - location=OFF_DECK_LOCATION, - ) - }, - ) - - with pytest.raises(errors.LocationNotAccessibleByPipetteError, match="off-deck"): - subject.raise_if_labware_inaccessible_by_pipette("labware-id") - - -def test_raise_if_labware_inaccessible_by_pipette_stacked_labware_on_staging_area() -> ( - None -): - """It should raise if the labware is stacked on a staging slot.""" - subject = get_labware_view( - labware_by_id={ - "labware-id": LoadedLabware( - id="labware-id", - loadName="test", - definitionUri="def-uri", - location=OnLabwareLocation(labwareId="lower-labware-id"), - ), - "lower-labware-id": LoadedLabware( - id="lower-labware-id", - loadName="test", - definitionUri="def-uri", - location=AddressableAreaLocation(addressableAreaName="B4"), - ), - }, - ) - - with pytest.raises( - errors.LocationNotAccessibleByPipetteError, match="on staging slot" - ): - subject.raise_if_labware_inaccessible_by_pipette("labware-id") - - -def test_raise_if_labware_cannot_be_stacked_is_adapter() -> None: - """It should raise if the labware trying to be stacked is an adapter.""" - subject = get_labware_view() - - with pytest.raises( - errors.LabwareCannotBeStackedError, match="defined as an adapter" - ): - subject.raise_if_labware_cannot_be_stacked( - top_labware_definition=LabwareDefinition.construct( # type: ignore[call-arg] - parameters=Parameters.construct( # type: ignore[call-arg] - loadName="name" - ), - allowedRoles=[LabwareRole.adapter], - ), - bottom_labware_id="labware-id", - ) - - -def test_raise_if_labware_cannot_be_stacked_not_validated() -> None: - """It should raise if the labware name is not in the definition stacking overlap.""" - subject = get_labware_view( - labware_by_id={ - "labware-id": LoadedLabware( - id="labware-id", - loadName="test", - definitionUri="def-uri", - location=DeckSlotLocation(slotName=DeckSlotName.SLOT_1), - ) - }, - ) - - with pytest.raises( - errors.LabwareCannotBeStackedError, match="loaded onto labware test" - ): - subject.raise_if_labware_cannot_be_stacked( - top_labware_definition=LabwareDefinition.construct( # type: ignore[call-arg] - parameters=Parameters.construct( # type: ignore[call-arg] - loadName="name" - ), - stackingOffsetWithLabware={}, - ), - bottom_labware_id="labware-id", - ) - - -def test_raise_if_labware_cannot_be_stacked_on_module_not_adapter() -> None: - """It should raise if the below labware on a module is not an adapter.""" - subject = get_labware_view( - labware_by_id={ - "labware-id": LoadedLabware( - id="labware-id", - loadName="test", - definitionUri="def-uri", - location=ModuleLocation(moduleId="module-id"), - ) - }, - definitions_by_uri={ - "def-uri": LabwareDefinition.construct( # type: ignore[call-arg] - allowedRoles=[LabwareRole.labware] - ) - }, - ) - - with pytest.raises(errors.LabwareCannotBeStackedError, match="module"): - subject.raise_if_labware_cannot_be_stacked( - top_labware_definition=LabwareDefinition.construct( # type: ignore[call-arg] - parameters=Parameters.construct( # type: ignore[call-arg] - loadName="name" - ), - stackingOffsetWithLabware={ - "test": SharedDataOverlapOffset(x=0, y=0, z=0) - }, - ), - bottom_labware_id="labware-id", - ) - - -def test_raise_if_labware_cannot_be_stacked_on_labware_on_adapter() -> None: - """It should raise if the OnLabware location is on an adapter.""" - subject = get_labware_view( - labware_by_id={ - "labware-id": LoadedLabware( - id="labware-id", - loadName="test", - definitionUri="def-uri-1", - location=OnLabwareLocation(labwareId="below-id"), - ), - "below-id": LoadedLabware( - id="below-id", - loadName="adapter-name", - definitionUri="def-uri-2", - location=DeckSlotLocation(slotName=DeckSlotName.SLOT_1), - ), - }, - definitions_by_uri={ - "def-uri-1": LabwareDefinition.construct( # type: ignore[call-arg] - allowedRoles=[LabwareRole.labware] - ), - "def-uri-2": LabwareDefinition.construct( # type: ignore[call-arg] - allowedRoles=[LabwareRole.adapter] - ), - }, - ) - - with pytest.raises( - errors.LabwareCannotBeStackedError, match="cannot be loaded to stack" - ): - subject.raise_if_labware_cannot_be_stacked( - top_labware_definition=LabwareDefinition.construct( # type: ignore[call-arg] - parameters=Parameters.construct( # type: ignore[call-arg] - loadName="name" - ), - stackingOffsetWithLabware={ - "test": SharedDataOverlapOffset(x=0, y=0, z=0) - }, - ), - bottom_labware_id="labware-id", - ) - - -@pytest.mark.parametrize( - argnames=[ - "allowed_roles", - "stacking_quirks", - "exception", - ], - argvalues=[ - [ - [LabwareRole.labware], - [], - pytest.raises(errors.LabwareCannotBeStackedError), - ], - [ - [LabwareRole.lid], - ["stackingMaxFive"], - does_not_raise(), - ], - ], -) -def test_labware_stacking_height_passes_or_raises( - allowed_roles: List[LabwareRole], - stacking_quirks: List[str], - exception: ContextManager[None], -) -> None: - """It should raise if the labware is stacked too high, and pass if the labware definition allowed this.""" - subject = get_labware_view( - labware_by_id={ - "labware-id4": LoadedLabware( - id="labware-id4", - loadName="test", - definitionUri="def-uri-1", - location=OnLabwareLocation(labwareId="labware-id3"), - ), - "labware-id3": LoadedLabware( - id="labware-id3", - loadName="test", - definitionUri="def-uri-1", - location=OnLabwareLocation(labwareId="labware-id2"), - ), - "labware-id2": LoadedLabware( - id="labware-id2", - loadName="test", - definitionUri="def-uri-1", - location=OnLabwareLocation(labwareId="labware-id1"), - ), - "labware-id1": LoadedLabware( - id="labware-id1", - loadName="test", - definitionUri="def-uri-1", - location=DeckSlotLocation(slotName=DeckSlotName.SLOT_1), - ), - }, - definitions_by_uri={ - "def-uri-1": LabwareDefinition.construct( # type: ignore[call-arg] - allowedRoles=allowed_roles, - parameters=Parameters.construct( - format="irregular", - quirks=stacking_quirks, - isTiprack=False, - loadName="name", - isMagneticModuleCompatible=False, - ), - ) - }, - ) - - with exception: - subject.raise_if_labware_cannot_be_stacked( - top_labware_definition=LabwareDefinition.construct( # type: ignore[call-arg] - parameters=Parameters.construct( - format="irregular", - quirks=stacking_quirks, - isTiprack=False, - loadName="name", - isMagneticModuleCompatible=False, - ), - stackingOffsetWithLabware={ - "test": SharedDataOverlapOffset(x=0, y=0, z=0) - }, - ), - bottom_labware_id="labware-id4", - ) - - -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) - - assert subject.get_deck_default_gripper_offsets() == LabwareMovementOffsetData( - pickUpOffset=LabwareOffsetVector(x=0, y=0, z=0), - dropOffset=LabwareOffsetVector(x=0, y=0, z=-0.75), - ) - - -def test_get_labware_gripper_offsets( - well_plate_def: LabwareDefinition, - adapter_plate_def: LabwareDefinition, -) -> None: - """It should get the labware's gripper offsets.""" - subject = get_labware_view( - labware_by_id={"plate-id": plate, "adapter-plate-id": adapter_plate}, - definitions_by_uri={ - "some-plate-uri": well_plate_def, - "some-adapter-uri": adapter_plate_def, - }, - ) - - assert ( - subject.get_child_gripper_offsets(labware_id="plate-id", slot_name=None) is None - ) - assert subject.get_child_gripper_offsets( - labware_id="adapter-plate-id", slot_name=DeckSlotName.SLOT_D1 - ) == LabwareMovementOffsetData( - pickUpOffset=LabwareOffsetVector(x=0, y=0, z=0), - dropOffset=LabwareOffsetVector(x=2, y=0, z=0), - ) - - -def test_get_labware_gripper_offsets_default_no_slots( - well_plate_def: LabwareDefinition, - adapter_plate_def: LabwareDefinition, -) -> None: - """It should get the labware's gripper offsets with only a default gripper offset entry.""" - subject = get_labware_view( - labware_by_id={ - "labware-id": LoadedLabware( - id="labware-id", - loadName="labware-load-name", - location=DeckSlotLocation(slotName=DeckSlotName.SLOT_1), - definitionUri="some-labware-uri", - offsetId=None, - displayName="Fancy Labware Name", - ) - }, - definitions_by_uri={ - "some-labware-uri": LabwareDefinition.construct( # type: ignore[call-arg] - gripperOffsets={ - "default": GripperOffsets( - pickUpOffset=OffsetVector(x=1, y=2, z=3), - dropOffset=OffsetVector(x=4, y=5, z=6), - ) - } - ), - }, - ) - - assert ( - subject.get_child_gripper_offsets( - labware_id="labware-id", slot_name=DeckSlotName.SLOT_D1 - ) - is None - ) - - assert subject.get_child_gripper_offsets( - labware_id="labware-id", slot_name=None - ) == LabwareMovementOffsetData( - pickUpOffset=LabwareOffsetVector(x=1, y=2, z=3), - dropOffset=LabwareOffsetVector(x=4, y=5, z=6), - ) - - -def test_get_grip_force( - flex_50uL_tiprack: LabwareDefinition, - reservoir_def: LabwareDefinition, -) -> None: - """It should get the grip force, if present, from labware definition or return default.""" - subject = get_labware_view() - - assert subject.get_grip_force(flex_50uL_tiprack) == 16 # from definition - assert subject.get_grip_force(reservoir_def) == 15 # default - - -def test_get_grip_height_from_labware_bottom( - well_plate_def: LabwareDefinition, - reservoir_def: LabwareDefinition, -) -> None: - """It should get the grip height, if present, from labware definition or return default.""" - subject = get_labware_view() - assert ( - subject.get_grip_height_from_labware_bottom(well_plate_def) == 12.2 - ) # from definition - assert subject.get_grip_height_from_labware_bottom(reservoir_def) == 15.7 # default - - -@pytest.mark.parametrize( - "labware_to_check,well_bbox", - [ - ("opentrons_universal_flat_adapter", Dimensions(0, 0, 0)), - ( - "corning_96_wellplate_360ul_flat", - Dimensions(116.81 - 10.95, 77.67 - 7.81, 14.22), - ), - ("nest_12_reservoir_15ml", Dimensions(117.48 - 10.28, 78.38 - 7.18, 31.4)), - ], -) -def test_calculates_well_bounding_box( - labware_to_check: str, well_bbox: Dimensions -) -> None: - """It should be able to calculate well bounding boxes.""" - definition = LabwareDefinition.parse_obj(load_definition(labware_to_check, 1)) - subject = get_labware_view() - assert subject.get_well_bbox(definition).x == pytest.approx(well_bbox.x) - assert subject.get_well_bbox(definition).y == pytest.approx(well_bbox.y) - assert subject.get_well_bbox(definition).z == pytest.approx(well_bbox.z) diff --git a/api/tests/opentrons/protocol_engine/state/test_labware_view_old.py b/api/tests/opentrons/protocol_engine/state/test_labware_view_old.py new file mode 100644 index 00000000000..ac92d5e5eaf --- /dev/null +++ b/api/tests/opentrons/protocol_engine/state/test_labware_view_old.py @@ -0,0 +1,1617 @@ +"""Labware state store tests. + +DEPRECATED: Testing LabwareView independently of LabwareStore is no +longer helpful. Try to add new tests to test_labware_state.py, where they can be +tested together, treating LabwareState as a private implementation detail. +""" +import pytest +from datetime import datetime +from typing import Dict, Optional, cast, ContextManager, Any, Union, NamedTuple, List +from contextlib import nullcontext as does_not_raise + +from opentrons_shared_data.deck import load as load_deck +from opentrons_shared_data.deck.types import DeckDefinitionV5 +from opentrons_shared_data.pipette.types import LabwareUri +from opentrons_shared_data.labware import load_definition +from opentrons_shared_data.labware.labware_definition import ( + Parameters, + LabwareRole, + OverlapOffset as SharedDataOverlapOffset, + GripperOffsets, + OffsetVector, +) + +from opentrons.protocols.api_support.deck_type import ( + STANDARD_OT2_DECK, + STANDARD_OT3_DECK, +) +from opentrons.protocols.models import LabwareDefinition +from opentrons.types import DeckSlotName, MountType + +from opentrons.protocol_engine import errors +from opentrons.protocol_engine.types import ( + DeckSlotLocation, + Dimensions, + LabwareOffset, + LabwareOffsetVector, + LabwareOffsetLocation, + LoadedLabware, + ModuleModel, + ModuleLocation, + OnLabwareLocation, + LabwareLocation, + AddressableAreaLocation, + OFF_DECK_LOCATION, + OverlapOffset, + LabwareMovementOffsetData, +) +from opentrons.protocol_engine.state._move_types import EdgePathType +from opentrons.protocol_engine.state.labware import ( + LabwareState, + LabwareView, + LabwareLoadParams, +) + +plate = LoadedLabware( + id="plate-id", + loadName="plate-load-name", + location=DeckSlotLocation(slotName=DeckSlotName.SLOT_1), + definitionUri="some-plate-uri", + offsetId=None, + displayName="Fancy Plate Name", +) + +flex_tiprack = LoadedLabware( + id="flex-tiprack-id", + loadName="flex-tiprack-load-name", + location=DeckSlotLocation(slotName=DeckSlotName.SLOT_1), + definitionUri="some-flex-tiprack-uri", + offsetId=None, + displayName="Flex Tiprack Name", +) + +reservoir = LoadedLabware( + id="reservoir-id", + loadName="reservoir-load-name", + location=DeckSlotLocation(slotName=DeckSlotName.SLOT_2), + definitionUri="some-reservoir-uri", + offsetId=None, +) + +trash = LoadedLabware( + id="trash-id", + loadName="trash-load-name", + location=DeckSlotLocation(slotName=DeckSlotName.SLOT_2), + definitionUri="some-trash-uri", + offsetId=None, +) + +tube_rack = LoadedLabware( + id="tube-rack-id", + loadName="tube-rack-load-name", + location=DeckSlotLocation(slotName=DeckSlotName.SLOT_1), + definitionUri="some-tube-rack-uri", + offsetId=None, +) + +tip_rack = LoadedLabware( + id="tip-rack-id", + loadName="tip-rack-load-name", + location=DeckSlotLocation(slotName=DeckSlotName.SLOT_1), + definitionUri="some-tip-rack-uri", + offsetId=None, +) + +adapter_plate = LoadedLabware( + id="adapter-plate-id", + loadName="adapter-load-name", + location=DeckSlotLocation(slotName=DeckSlotName.SLOT_1), + definitionUri="some-adapter-uri", + offsetId=None, +) + + +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[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(DeckDefinitionV5, {"fake": True}), + ) + + return LabwareView(state=state) + + +def test_get_labware_data_bad_id() -> None: + """get_labware_data_by_id should raise if labware ID doesn't exist.""" + subject = get_labware_view() + + with pytest.raises(errors.LabwareNotLoadedError): + subject.get("asdfghjkl") + + +def test_get_labware_data_by_id() -> None: + """It should retrieve labware data from the state.""" + subject = get_labware_view(labware_by_id={"plate-id": plate}) + + assert subject.get("plate-id") == plate + + +def test_get_id_by_module() -> None: + """Should return the labware id associated to the module.""" + subject = get_labware_view( + labware_by_id={ + "labware-id": LoadedLabware( + id="labware-id", + loadName="test", + definitionUri="test-uri", + location=ModuleLocation(moduleId="module-id"), + ) + } + ) + assert subject.get_id_by_module(module_id="module-id") == "labware-id" + + +def test_get_id_by_module_raises_error() -> None: + """Should raise error that labware not found.""" + subject = get_labware_view( + labware_by_id={ + "labware-id": LoadedLabware( + id="labware-id", + loadName="test", + definitionUri="test-uri", + location=ModuleLocation(moduleId="module-id"), + ) + } + ) + with pytest.raises(errors.exceptions.LabwareNotLoadedOnModuleError): + subject.get_id_by_module(module_id="no-module-id") + + +def test_get_id_by_labware() -> None: + """Should return the labware id associated to the labware.""" + subject = get_labware_view( + labware_by_id={ + "labware-id": LoadedLabware( + id="labware-id", + loadName="test", + definitionUri="test-uri", + location=OnLabwareLocation(labwareId="other-labware-id"), + ) + } + ) + assert subject.get_id_by_labware(labware_id="other-labware-id") == "labware-id" + + +def test_get_id_by_labware_raises_error() -> None: + """Should raise error that labware not found.""" + subject = get_labware_view( + labware_by_id={ + "labware-id": LoadedLabware( + id="labware-id", + loadName="test", + definitionUri="test-uri", + location=OnLabwareLocation(labwareId="other-labware-id"), + ) + } + ) + with pytest.raises(errors.exceptions.LabwareNotLoadedOnLabwareError): + subject.get_id_by_labware(labware_id="no-labware-id") + + +def test_raise_if_labware_has_labware_on_top() -> None: + """It should raise if labware has another labware on top.""" + subject = get_labware_view( + labware_by_id={ + "labware-id-1": LoadedLabware( + id="labware-id-1", + loadName="test", + definitionUri="test-uri", + location=DeckSlotLocation(slotName=DeckSlotName.SLOT_1), + ), + "labware-id-2": LoadedLabware( + id="labware-id-2", + loadName="test", + definitionUri="test-uri", + location=ModuleLocation(moduleId="module-id"), + ), + "labware-id-3": LoadedLabware( + id="labware-id-3", + loadName="test", + definitionUri="test-uri", + location=OnLabwareLocation(labwareId="labware-id-1"), + ), + } + ) + subject.raise_if_labware_has_labware_on_top("labware-id-2") + subject.raise_if_labware_has_labware_on_top("labware-id-3") + with pytest.raises(errors.exceptions.LabwareIsInStackError): + subject.raise_if_labware_has_labware_on_top("labware-id-1") + + +def test_get_labware_definition(well_plate_def: LabwareDefinition) -> None: + """It should get a labware's definition from the state.""" + subject = get_labware_view( + labware_by_id={"plate-id": plate}, + definitions_by_uri={"some-plate-uri": well_plate_def}, + ) + + assert subject.get_definition("plate-id") == well_plate_def + + +def test_get_labware_definition_bad_id() -> None: + """get_labware_definition should raise if labware definition doesn't exist.""" + subject = get_labware_view() + + with pytest.raises(errors.LabwareDefinitionDoesNotExistError): + subject.get_definition_by_uri(cast(LabwareUri, "not-a-uri")) + + +@pytest.mark.parametrize( + argnames=["namespace", "version"], + argvalues=[("world", 123), (None, 123), ("world", None), (None, None)], +) +def test_find_custom_labware_params( + namespace: Optional[str], version: Optional[int] +) -> None: + """It should find the missing (if any) load labware parameters.""" + labware_def = LabwareDefinition.model_construct( # type: ignore[call-arg] + parameters=Parameters.model_construct(loadName="hello"), # type: ignore[call-arg] + namespace="world", + version=123, + ) + standard_def = LabwareDefinition.model_construct( # type: ignore[call-arg] + parameters=Parameters.model_construct(loadName="goodbye"), # type: ignore[call-arg] + namespace="opentrons", + version=456, + ) + + subject = get_labware_view( + definitions_by_uri={ + "some-labware-uri": labware_def, + "some-standard-uri": standard_def, + }, + ) + + result = subject.find_custom_labware_load_params() + + assert result == [ + LabwareLoadParams(load_name="hello", namespace="world", version=123) + ] + + +def test_get_all_labware( + well_plate_def: LabwareDefinition, + reservoir_def: LabwareDefinition, +) -> None: + """It should return all labware.""" + subject = get_labware_view( + labware_by_id={ + "plate-id": plate, + "reservoir-id": reservoir, + } + ) + + all_labware = subject.get_all() + + assert all_labware == [plate, reservoir] + + +def test_get_labware_location() -> None: + """It should return labware location.""" + subject = get_labware_view(labware_by_id={"plate-id": plate}) + + result = subject.get_location("plate-id") + + assert result == DeckSlotLocation(slotName=DeckSlotName.SLOT_1) + + +@pytest.mark.parametrize( + argnames="location", + argvalues=[ + DeckSlotLocation(slotName=DeckSlotName.SLOT_D1), + ModuleLocation(moduleId="module-id"), + OFF_DECK_LOCATION, + ], +) +def test_get_parent_location(location: LabwareLocation) -> None: + """It should return the non-OnLabware location of a labware.""" + subject = get_labware_view( + labware_by_id={ + "labware-id": LoadedLabware( + id="plate-id", + loadName="load-name", + location=location, + definitionUri="some-uri", + ) + } + ) + + result = subject.get_parent_location(labware_id="labware-id") + + assert result == location + + +@pytest.mark.parametrize( + argnames="location", + argvalues=[ + DeckSlotLocation(slotName=DeckSlotName.SLOT_D1), + ModuleLocation(moduleId="module-id"), + ], +) +def test_get_parent_location_on_labware(location: LabwareLocation) -> None: + """It should return the non-OnLabware location of a labware.""" + subject = get_labware_view( + labware_by_id={ + "top-id": LoadedLabware( + id="top-id", + loadName="load-name", + location=OnLabwareLocation(labwareId="middle-id"), + definitionUri="some-uri", + ), + "middle-id": LoadedLabware( + id="middle-id", + loadName="load-name", + location=OnLabwareLocation(labwareId="bottom-id"), + definitionUri="some-uri", + ), + "bottom-id": LoadedLabware( + id="bottom-id", + loadName="load-name", + location=location, + definitionUri="some-uri", + ), + } + ) + + result = subject.get_parent_location(labware_id="top-id") + + assert result == location + + +def test_get_has_quirk( + well_plate_def: LabwareDefinition, + reservoir_def: LabwareDefinition, +) -> None: + """It should return whether a labware by ID has a given quirk.""" + subject = get_labware_view( + labware_by_id={ + "plate-id": plate, + "reservoir-id": reservoir, + }, + definitions_by_uri={ + "some-plate-uri": well_plate_def, + "some-reservoir-uri": reservoir_def, + }, + ) + + well_plate_has_center_quirk = subject.get_has_quirk( + labware_id="plate-id", + quirk="centerMultichannelOnWells", + ) + + reservoir_has_center_quirk = subject.get_has_quirk( + labware_id="reservoir-id", + quirk="centerMultichannelOnWells", + ) + + assert well_plate_has_center_quirk is False + assert reservoir_has_center_quirk is True + + +def test_quirks( + well_plate_def: LabwareDefinition, + reservoir_def: LabwareDefinition, +) -> None: + """It should return a labware's quirks.""" + subject = get_labware_view( + labware_by_id={ + "plate-id": plate, + "reservoir-id": reservoir, + }, + definitions_by_uri={ + "some-plate-uri": well_plate_def, + "some-reservoir-uri": reservoir_def, + }, + ) + + well_plate_quirks = subject.get_quirks("plate-id") + reservoir_quirks = subject.get_quirks("reservoir-id") + + assert well_plate_quirks == [] + assert reservoir_quirks == ["centerMultichannelOnWells", "touchTipDisabled"] + + +def test_get_well_definition_bad_name(well_plate_def: LabwareDefinition) -> None: + """get_well_definition should raise if well name doesn't exist.""" + subject = get_labware_view( + labware_by_id={"plate-id": plate}, + definitions_by_uri={"some-plate-uri": well_plate_def}, + ) + + with pytest.raises(errors.WellDoesNotExistError): + subject.get_well_definition(labware_id="plate-id", well_name="foobar") + + +def test_get_well_definition(well_plate_def: LabwareDefinition) -> None: + """It should return a well definition by well name.""" + subject = get_labware_view( + labware_by_id={"plate-id": plate}, + definitions_by_uri={"some-plate-uri": well_plate_def}, + ) + + expected_well_def = well_plate_def.wells["B2"] + result = subject.get_well_definition(labware_id="plate-id", well_name="B2") + + assert result == expected_well_def + + +def test_get_well_definition_get_first(well_plate_def: LabwareDefinition) -> None: + """It should return the first well definition if no given well name.""" + subject = get_labware_view( + labware_by_id={"plate-id": plate}, + definitions_by_uri={"some-plate-uri": well_plate_def}, + ) + + expected_well_def = well_plate_def.wells["A1"] + result = subject.get_well_definition(labware_id="plate-id", well_name=None) + + assert result == expected_well_def + + +def test_get_well_geometry_raises_error(well_plate_def: LabwareDefinition) -> None: + """It should raise an IncompleteLabwareDefinitionError when there's no innerLabwareGeometry.""" + subject = get_labware_view( + labware_by_id={"plate-id": plate}, + definitions_by_uri={"some-plate-uri": well_plate_def}, + ) + + with pytest.raises(errors.IncompleteLabwareDefinitionError): + subject.get_well_geometry(labware_id="plate-id") + + +def test_get_well_size_circular(well_plate_def: LabwareDefinition) -> None: + """It should return the well dimensions of a circular well.""" + subject = get_labware_view( + labware_by_id={"plate-id": plate}, + definitions_by_uri={"some-plate-uri": well_plate_def}, + ) + expected_well_def = well_plate_def.wells["A2"] + expected_size = ( + expected_well_def.diameter, + expected_well_def.diameter, + expected_well_def.depth, + ) + + result = subject.get_well_size(labware_id="plate-id", well_name="A2") + + assert result == expected_size + + +def test_get_well_size_rectangular(reservoir_def: LabwareDefinition) -> None: + """It should return the well dimensions of a rectangular well.""" + subject = get_labware_view( + labware_by_id={"reservoir-id": reservoir}, + definitions_by_uri={"some-reservoir-uri": reservoir_def}, + ) + expected_well_def = reservoir_def.wells["A2"] + expected_size = ( + expected_well_def.xDimension, + expected_well_def.yDimension, + expected_well_def.depth, + ) + + result = subject.get_well_size(labware_id="reservoir-id", well_name="A2") + + assert result == expected_size + + +def test_labware_has_well(falcon_tuberack_def: LabwareDefinition) -> None: + """It should return a list of wells from definition.""" + subject = get_labware_view( + labware_by_id={"tube-rack-id": tube_rack}, + definitions_by_uri={"some-tube-rack-uri": falcon_tuberack_def}, + ) + + result = subject.validate_liquid_allowed_in_labware( + labware_id="tube-rack-id", wells={"A1": 30, "B1": 100} + ) + assert result == ["A1", "B1"] + + with pytest.raises(errors.WellDoesNotExistError): + subject.validate_liquid_allowed_in_labware( + labware_id="tube-rack-id", wells={"AA": 30} + ) + + with pytest.raises(errors.LabwareNotLoadedError): + subject.validate_liquid_allowed_in_labware(labware_id="no-id", wells={"A1": 30}) + + +def test_validate_liquid_allowed_raises_incompatible_labware() -> None: + """It should raise when validating labware that is a tiprack or an adapter.""" + subject = get_labware_view( + labware_by_id={ + "tiprack-id": LoadedLabware( + id="tiprack-id", + loadName="test1", + definitionUri="some-tiprack-uri", + location=DeckSlotLocation(slotName=DeckSlotName.SLOT_1), + ), + "adapter-id": LoadedLabware( + id="adapter-id", + loadName="test2", + definitionUri="some-adapter-uri", + location=DeckSlotLocation(slotName=DeckSlotName.SLOT_2), + ), + }, + definitions_by_uri={ + "some-tiprack-uri": LabwareDefinition.model_construct( # type: ignore[call-arg] + parameters=Parameters.model_construct(isTiprack=True), # type: ignore[call-arg] + wells={}, + ), + "some-adapter-uri": LabwareDefinition.model_construct( # type: ignore[call-arg] + parameters=Parameters.model_construct(isTiprack=False), # type: ignore[call-arg] + allowedRoles=[LabwareRole.adapter], + wells={}, + ), + }, + ) + + with pytest.raises(errors.LabwareIsTipRackError): + subject.validate_liquid_allowed_in_labware(labware_id="tiprack-id", wells={}) + + with pytest.raises(errors.LabwareIsAdapterError): + subject.validate_liquid_allowed_in_labware(labware_id="adapter-id", wells={}) + + +def test_get_tip_length_raises_with_non_tip_rack( + well_plate_def: LabwareDefinition, +) -> None: + """It should raise if you try to get the tip length of a regular labware.""" + subject = get_labware_view( + labware_by_id={"plate-id": plate}, + definitions_by_uri={"some-plate-uri": well_plate_def}, + ) + + with pytest.raises(errors.LabwareIsNotTipRackError): + subject.get_tip_length("plate-id") + + +def test_get_tip_length_gets_length_from_definition( + tip_rack_def: LabwareDefinition, +) -> None: + """It should return the tip length from the definition.""" + subject = get_labware_view( + labware_by_id={"tip-rack-id": tip_rack}, + definitions_by_uri={"some-tip-rack-uri": tip_rack_def}, + ) + + length = subject.get_tip_length("tip-rack-id", 12.3) + assert length == tip_rack_def.parameters.tipLength - 12.3 # type: ignore[operator] + + +def test_get_tip_drop_z_offset() -> None: + """It should get a tip drop z offset by scaling the tip length.""" + tip_rack_def = LabwareDefinition.model_construct( # type: ignore[call-arg] + parameters=Parameters.model_construct( # type: ignore[call-arg] + tipLength=100, + ) + ) + + subject = get_labware_view( + labware_by_id={"tip-rack-id": tip_rack}, + definitions_by_uri={"some-tip-rack-uri": tip_rack_def}, + ) + + result = subject.get_tip_drop_z_offset( + labware_id="tip-rack-id", length_scale=0.5, additional_offset=-0.123 + ) + + assert result == -50.123 + + +def test_get_labware_uri_from_definition(tip_rack_def: LabwareDefinition) -> None: + """It should return the labware's definition URI.""" + tip_rack = LoadedLabware( + id="tip-rack-id", + loadName="tip-rack-load-name", + location=DeckSlotLocation(slotName=DeckSlotName.SLOT_1), + definitionUri="some-tip-rack-uri", + offsetId=None, + ) + + subject = get_labware_view( + labware_by_id={"tip-rack-id": tip_rack}, + definitions_by_uri={"some-tip-rack-uri": tip_rack_def}, + ) + + result = subject.get_definition_uri(labware_id="tip-rack-id") + assert result == "some-tip-rack-uri" + + +def test_get_labware_uri_from_full_definition(tip_rack_def: LabwareDefinition) -> None: + """It should be able to construct a URI given a full definition.""" + subject = get_labware_view() + result = subject.get_uri_from_definition(tip_rack_def) + assert result == "opentrons/opentrons_96_tiprack_300ul/1" + + +def test_is_tiprack( + tip_rack_def: LabwareDefinition, reservoir_def: LabwareDefinition +) -> None: + """It should determine if labware is a tip rack.""" + subject = get_labware_view( + labware_by_id={ + "tip-rack-id": tip_rack, + "reservoir-id": reservoir, + }, + definitions_by_uri={ + "some-tip-rack-uri": tip_rack_def, + "some-reservoir-uri": reservoir_def, + }, + ) + + assert subject.is_tiprack(labware_id="tip-rack-id") is True + assert subject.is_tiprack(labware_id="reservoir-id") is False + + +def test_get_load_name(reservoir_def: LabwareDefinition) -> None: + """It should return the load name.""" + subject = get_labware_view( + labware_by_id={"reservoir-id": reservoir}, + definitions_by_uri={"some-reservoir-uri": reservoir_def}, + ) + + result = subject.get_load_name("reservoir-id") + + assert result == reservoir_def.parameters.loadName + + +def test_get_dimensions(well_plate_def: LabwareDefinition) -> None: + """It should compute the dimensions of a labware.""" + subject = get_labware_view( + labware_by_id={"plate-id": plate}, + definitions_by_uri={"some-plate-uri": well_plate_def}, + ) + + result = subject.get_dimensions(labware_id="plate-id") + + assert result == Dimensions( + x=well_plate_def.dimensions.xDimension, + y=well_plate_def.dimensions.yDimension, + z=well_plate_def.dimensions.zDimension, + ) + + +def test_get_labware_overlap_offsets() -> None: + """It should get the labware overlap offsets.""" + subject = get_labware_view() + result = subject.get_labware_overlap_offsets( + definition=LabwareDefinition.model_construct( # type: ignore[call-arg] + stackingOffsetWithLabware={ + "bottom-labware-name": SharedDataOverlapOffset(x=1, y=2, z=3) + } + ), + below_labware_name="bottom-labware-name", + ) + + assert result == OverlapOffset(x=1, y=2, z=3) + + +class ModuleOverlapSpec(NamedTuple): + """Spec data to test LabwareView.get_module_overlap_offsets.""" + + spec_deck_definition: DeckDefinitionV5 + module_model: ModuleModel + stacking_offset_with_module: Dict[str, SharedDataOverlapOffset] + expected_offset: OverlapOffset + + +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, 5), + module_model=ModuleModel.TEMPERATURE_MODULE_V2, + stacking_offset_with_module={ + str(ModuleModel.TEMPERATURE_MODULE_V2.value): SharedDataOverlapOffset( + x=1, y=2, z=3 + ), + }, + expected_offset=OverlapOffset(x=1, y=2, z=3), + ), + ModuleOverlapSpec( + # Labware on TC Gen1 on OT2, with stacking overlap for TC Gen1 + 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( + x=11, y=22, z=33 + ), + }, + expected_offset=OverlapOffset(x=11, y=22, z=33), + ), + ModuleOverlapSpec( + # Labware on TC Gen2 on OT2, with no stacking overlap + 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, 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, 5), + module_model=ModuleModel.THERMOCYCLER_MODULE_V2, + stacking_offset_with_module={ + str(ModuleModel.THERMOCYCLER_MODULE_V2.value): SharedDataOverlapOffset( + x=111, y=222, z=333 + ), + }, + expected_offset=OverlapOffset(x=111, y=222, z=333), + ), +] + + +@pytest.mark.parametrize( + argnames=ModuleOverlapSpec._fields, + argvalues=module_overlap_specs, +) +def test_get_module_overlap_offsets( + spec_deck_definition: DeckDefinitionV5, + module_model: ModuleModel, + stacking_offset_with_module: Dict[str, SharedDataOverlapOffset], + expected_offset: OverlapOffset, +) -> None: + """It should get the labware overlap offsets.""" + subject = get_labware_view( + deck_definition=spec_deck_definition, + ) + result = subject.get_module_overlap_offsets( + definition=LabwareDefinition.model_construct( # type: ignore[call-arg] + stackingOffsetWithModule=stacking_offset_with_module + ), + module_model=module_model, + ) + + assert result == expected_offset + + +def test_get_default_magnet_height( + magdeck_well_plate_def: LabwareDefinition, +) -> None: + """Should get get the default value for magnetic height.""" + well_plate = LoadedLabware( + id="well-plate-id", + loadName="load-name", + location=ModuleLocation(moduleId="module-id"), + definitionUri="well-plate-uri", + offsetId=None, + ) + + subject = get_labware_view( + labware_by_id={"well-plate-id": well_plate}, + definitions_by_uri={"well-plate-uri": magdeck_well_plate_def}, + ) + + assert subject.get_default_magnet_height(module_id="module-id", offset=2) == 12.0 + + +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) + + assert subject.get_deck_definition() == ot2_standard_deck_def + + +def test_get_labware_offset_vector() -> None: + """It should get a labware's offset vector.""" + labware_without_offset = LoadedLabware( + id="without-offset-labware-id", + loadName="labware-load-name", + location=DeckSlotLocation(slotName=DeckSlotName.SLOT_1), + definitionUri="some-labware-uri", + offsetId=None, + ) + + labware_with_offset = LoadedLabware( + id="with-offset-labware-id", + loadName="labware-load-name", + location=DeckSlotLocation(slotName=DeckSlotName.SLOT_1), + definitionUri="some-labware-uri", + offsetId="offset-id", + ) + + offset_vector = LabwareOffsetVector(x=1, y=2, z=3) + offset = LabwareOffset( + id="offset-id", + createdAt=datetime(year=2021, month=1, day=2), + definitionUri="some-labware-uri", + location=LabwareOffsetLocation(slotName=DeckSlotName.SLOT_1), + vector=offset_vector, + ) + + subject = get_labware_view( + labware_by_id={ + labware_without_offset.id: labware_without_offset, + labware_with_offset.id: labware_with_offset, + }, + labware_offsets_by_id={"offset-id": offset}, + ) + + assert subject.get_labware_offset_vector(labware_with_offset.id) == offset.vector + + assert subject.get_labware_offset_vector( + labware_without_offset.id + ) == LabwareOffsetVector(x=0, y=0, z=0) + + with pytest.raises(errors.LabwareNotLoadedError): + subject.get_labware_offset_vector("wrong-labware-id") + + +def test_get_labware_offset() -> None: + """It should return the requested labware offset, if it exists.""" + offset_a = LabwareOffset( + id="id-a", + createdAt=datetime(year=2021, month=1, day=1), + definitionUri="uri-a", + location=LabwareOffsetLocation(slotName=DeckSlotName.SLOT_1), + vector=LabwareOffsetVector(x=1, y=1, z=1), + ) + + offset_b = LabwareOffset( + id="id-b", + createdAt=datetime(year=2022, month=2, day=2), + definitionUri="uri-b", + location=LabwareOffsetLocation(slotName=DeckSlotName.SLOT_2), + vector=LabwareOffsetVector(x=2, y=2, z=2), + ) + + subject = get_labware_view( + labware_offsets_by_id={"id-a": offset_a, "id-b": offset_b} + ) + + assert subject.get_labware_offset("id-a") == offset_a + assert subject.get_labware_offset("id-b") == offset_b + with pytest.raises(errors.LabwareOffsetDoesNotExistError): + subject.get_labware_offset("wrong-labware-offset-id") + + +def test_get_labware_offsets() -> None: + """It should return a list of all labware offsets, in order.""" + offset_a = LabwareOffset( + id="id-a", + createdAt=datetime(year=2021, month=1, day=1), + definitionUri="uri-a", + location=LabwareOffsetLocation(slotName=DeckSlotName.SLOT_1), + vector=LabwareOffsetVector(x=1, y=1, z=1), + ) + + offset_b = LabwareOffset( + id="id-b", + createdAt=datetime(year=2022, month=2, day=2), + definitionUri="uri-b", + location=LabwareOffsetLocation(slotName=DeckSlotName.SLOT_2), + vector=LabwareOffsetVector(x=2, y=2, z=2), + ) + + empty_subject = get_labware_view() + assert empty_subject.get_labware_offsets() == [] + + filled_subject_a_before_b = get_labware_view( + labware_offsets_by_id={"id-a": offset_a, "id-b": offset_b} + ) + assert filled_subject_a_before_b.get_labware_offsets() == [offset_a, offset_b] + + filled_subject_b_before_a = get_labware_view( + labware_offsets_by_id={"id-b": offset_b, "id-a": offset_a} + ) + assert filled_subject_b_before_a.get_labware_offsets() == [offset_b, offset_a] + + +def test_find_applicable_labware_offset() -> None: + """It should return the most recent offset with matching URI and location.""" + offset_1 = LabwareOffset( + id="id-1", + createdAt=datetime(year=2021, month=1, day=1), + definitionUri="definition-uri", + location=LabwareOffsetLocation(slotName=DeckSlotName.SLOT_1), + vector=LabwareOffsetVector(x=1, y=1, z=1), + ) + + # Same definitionUri and location; different id, createdAt, and offset. + offset_2 = LabwareOffset( + id="id-2", + createdAt=datetime(year=2022, month=2, day=2), + definitionUri="definition-uri", + location=LabwareOffsetLocation(slotName=DeckSlotName.SLOT_1), + vector=LabwareOffsetVector(x=2, y=2, z=2), + ) + + offset_3 = LabwareOffset( + id="id-3", + createdAt=datetime(year=2023, month=3, day=3), + definitionUri="on-module-definition-uri", + location=LabwareOffsetLocation( + slotName=DeckSlotName.SLOT_1, + moduleModel=ModuleModel.TEMPERATURE_MODULE_V1, + ), + vector=LabwareOffsetVector(x=3, y=3, z=3), + ) + + subject = get_labware_view( + # Simulate offset_2 having been added after offset_1. + labware_offsets_by_id={"id-1": offset_1, "id-2": offset_2, "id-3": offset_3} + ) + + # Matching both definitionURI and location. Should return 2nd (most recent) offset. + assert ( + subject.find_applicable_labware_offset( + definition_uri="definition-uri", + location=LabwareOffsetLocation(slotName=DeckSlotName.SLOT_1), + ) + == offset_2 + ) + + assert ( + subject.find_applicable_labware_offset( + definition_uri="on-module-definition-uri", + location=LabwareOffsetLocation( + slotName=DeckSlotName.SLOT_1, + moduleModel=ModuleModel.TEMPERATURE_MODULE_V1, + ), + ) + == offset_3 + ) + + # Doesn't match anything, since definitionUri is different. + assert ( + subject.find_applicable_labware_offset( + definition_uri="different-definition-uri", + location=LabwareOffsetLocation(slotName=DeckSlotName.SLOT_1), + ) + is None + ) + + # Doesn't match anything, since location is different. + assert ( + subject.find_applicable_labware_offset( + definition_uri="different-definition-uri", + location=LabwareOffsetLocation(slotName=DeckSlotName.SLOT_2), + ) + is None + ) + + +def test_get_user_specified_display_name() -> None: + """It should get a labware's user-specified display name.""" + subject = get_labware_view( + labware_by_id={ + "plate_with_display_name": plate, + "reservoir_without_display_name": reservoir, + }, + ) + + assert ( + subject.get_user_specified_display_name("plate_with_display_name") + == "Fancy Plate Name" + ) + assert ( + subject.get_user_specified_display_name("reservoir_without_display_name") + is None + ) + + +def test_get_display_name( + well_plate_def: LabwareDefinition, + reservoir_def: LabwareDefinition, +) -> None: + """It should get the labware's display name.""" + subject = get_labware_view( + labware_by_id={ + "plate_with_custom_display_name": plate, + "reservoir_with_default_display_name": reservoir, + }, + definitions_by_uri={ + "some-plate-uri": well_plate_def, + "some-reservoir-uri": reservoir_def, + }, + ) + assert ( + subject.get_display_name("plate_with_custom_display_name") == "Fancy Plate Name" + ) + assert ( + subject.get_display_name("reservoir_with_default_display_name") + == "NEST 12 Well Reservoir 15 mL" + ) + + +def test_get_fixed_trash_id() -> None: + """It should return the ID of the labware loaded into the fixed trash slot.""" + # OT-2 fixed trash slot: + subject = get_labware_view( + labware_by_id={ + "abc123": LoadedLabware( + id="abc123", + loadName="trash-load-name", + location=DeckSlotLocation(slotName=DeckSlotName.FIXED_TRASH), + definitionUri="trash-definition-uri", + offsetId=None, + displayName=None, + ) + }, + ) + assert subject.get_fixed_trash_id() == "abc123" + + # OT-3 fixed trash slot: + subject = get_labware_view( + labware_by_id={ + "abc123": LoadedLabware( + id="abc123", + loadName="trash-load-name", + location=DeckSlotLocation(slotName=DeckSlotName.SLOT_A3), + definitionUri="trash-definition-uri", + offsetId=None, + displayName=None, + ) + }, + ) + assert subject.get_fixed_trash_id() == "abc123" + + # Nothing in the fixed trash slot: + subject = get_labware_view( + labware_by_id={ + "abc123": LoadedLabware( + id="abc123", + loadName="trash-load-name", + location=DeckSlotLocation(slotName=DeckSlotName.SLOT_1), + definitionUri="trash-definition-uri", + offsetId=None, + displayName=None, + ) + }, + ) + assert subject.get_fixed_trash_id() is None + + +@pytest.mark.parametrize( + argnames=["location", "expected_raise"], + argvalues=[ + ( + DeckSlotLocation(slotName=DeckSlotName.SLOT_1), + pytest.raises(errors.LocationIsOccupiedError), + ), + ( + ModuleLocation(moduleId="module-id"), + pytest.raises(errors.LocationIsOccupiedError), + ), + (DeckSlotLocation(slotName=DeckSlotName.SLOT_2), does_not_raise()), + (ModuleLocation(moduleId="non-matching-id"), does_not_raise()), + ], +) +def test_raise_if_labware_in_location( + location: Union[DeckSlotLocation, ModuleLocation], + expected_raise: ContextManager[Any], +) -> None: + """It should raise if there is labware in specified location.""" + subject = get_labware_view( + labware_by_id={ + "abc123": LoadedLabware( + id="abc123", + loadName="labware-1", + location=DeckSlotLocation(slotName=DeckSlotName.SLOT_1), + definitionUri="labware-definition-uri", + offsetId=None, + displayName=None, + ), + "xyz456": LoadedLabware( + id="xyz456", + loadName="labware-2", + location=ModuleLocation(moduleId="module-id"), + definitionUri="labware-definition-uri", + offsetId=None, + displayName=None, + ), + } + ) + with expected_raise: + subject.raise_if_labware_in_location(location=location) + + +def test_get_by_slot() -> None: + """It should get the labware in a given slot.""" + labware_1 = LoadedLabware.model_construct( # type: ignore[call-arg] + id="1", location=DeckSlotLocation(slotName=DeckSlotName.SLOT_1) + ) + labware_2 = LoadedLabware.model_construct( # type: ignore[call-arg] + id="2", location=DeckSlotLocation(slotName=DeckSlotName.SLOT_2) + ) + labware_3 = LoadedLabware.model_construct( # type: ignore[call-arg] + id="3", location=ModuleLocation(moduleId="cool-module") + ) + + subject = get_labware_view( + labware_by_id={"1": labware_1, "2": labware_2, "3": labware_3} + ) + + assert subject.get_by_slot(DeckSlotName.SLOT_1) == labware_1 + assert subject.get_by_slot(DeckSlotName.SLOT_2) == labware_2 + assert subject.get_by_slot(DeckSlotName.SLOT_3) is None + + +@pytest.mark.parametrize( + ["well_name", "mount", "labware_slot", "next_to_module", "expected_result"], + [ + ("abc", MountType.RIGHT, DeckSlotName.SLOT_3, False, EdgePathType.LEFT), + ("abc", MountType.RIGHT, DeckSlotName.SLOT_D3, False, EdgePathType.LEFT), + ("abc", MountType.RIGHT, DeckSlotName.SLOT_1, True, EdgePathType.LEFT), + ("abc", MountType.RIGHT, DeckSlotName.SLOT_D1, True, EdgePathType.LEFT), + ("pqr", MountType.LEFT, DeckSlotName.SLOT_3, True, EdgePathType.RIGHT), + ("pqr", MountType.LEFT, DeckSlotName.SLOT_D3, True, EdgePathType.RIGHT), + ("pqr", MountType.LEFT, DeckSlotName.SLOT_3, False, EdgePathType.DEFAULT), + ("pqr", MountType.LEFT, DeckSlotName.SLOT_D3, False, EdgePathType.DEFAULT), + ("pqr", MountType.RIGHT, DeckSlotName.SLOT_3, True, EdgePathType.DEFAULT), + ("pqr", MountType.RIGHT, DeckSlotName.SLOT_D3, True, EdgePathType.DEFAULT), + ("def", MountType.LEFT, DeckSlotName.SLOT_3, True, EdgePathType.DEFAULT), + ("def", MountType.LEFT, DeckSlotName.SLOT_D3, True, EdgePathType.DEFAULT), + ], +) +def test_get_edge_path_type( + well_name: str, + mount: MountType, + labware_slot: DeckSlotName, + next_to_module: bool, + expected_result: EdgePathType, +) -> None: + """It should get the proper edge path type based on well name, mount, and labware position.""" + labware = LoadedLabware( + id="tip-rack-id", + loadName="load-name", + location=DeckSlotLocation(slotName=DeckSlotName.SLOT_1), + definitionUri="some-labware-uri", + offsetId=None, + ) + + labware_def = LabwareDefinition.model_construct( # type: ignore[call-arg] + ordering=[["abc", "def"], ["ghi", "jkl"], ["mno", "pqr"]] + ) + + subject = get_labware_view( + labware_by_id={"labware-id": labware}, + definitions_by_uri={ + "some-labware-uri": labware_def, + }, + ) + + result = subject.get_edge_path_type( + "labware-id", well_name, mount, labware_slot, next_to_module + ) + + assert result == expected_result + + +def test_get_all_labware_definition( + tip_rack_def: LabwareDefinition, falcon_tuberack_def: LabwareDefinition +) -> None: + """It should return the loaded labware definition list.""" + subject = get_labware_view( + labware_by_id={ + "labware-id": LoadedLabware( + id="labware-id", + loadName="test", + definitionUri="opentrons_96_tiprack_300ul", + location=ModuleLocation(moduleId="module-id"), + ) + }, + definitions_by_uri={ + "opentrons_96_tiprack_300ul": tip_rack_def, + "falcon-definition": falcon_tuberack_def, + }, + ) + + result = subject.get_loaded_labware_definitions() + + assert result == [tip_rack_def] + + +def test_get_all_labware_definition_empty() -> None: + """It should return an empty list.""" + subject = get_labware_view( + labware_by_id={}, + ) + + result = subject.get_loaded_labware_definitions() + + assert result == [] + + +def test_raise_if_labware_inaccessible_by_pipette_staging_area() -> None: + """It should raise if the labware is on a staging slot.""" + subject = get_labware_view( + labware_by_id={ + "labware-id": LoadedLabware( + id="labware-id", + loadName="test", + definitionUri="def-uri", + location=AddressableAreaLocation(addressableAreaName="B4"), + ) + }, + ) + + with pytest.raises( + errors.LocationNotAccessibleByPipetteError, match="on staging slot" + ): + subject.raise_if_labware_inaccessible_by_pipette("labware-id") + + +def test_raise_if_labware_inaccessible_by_pipette_off_deck() -> None: + """It should raise if the labware is off-deck.""" + subject = get_labware_view( + labware_by_id={ + "labware-id": LoadedLabware( + id="labware-id", + loadName="test", + definitionUri="def-uri", + location=OFF_DECK_LOCATION, + ) + }, + ) + + with pytest.raises(errors.LocationNotAccessibleByPipetteError, match="off-deck"): + subject.raise_if_labware_inaccessible_by_pipette("labware-id") + + +def test_raise_if_labware_inaccessible_by_pipette_stacked_labware_on_staging_area() -> ( + None +): + """It should raise if the labware is stacked on a staging slot.""" + subject = get_labware_view( + labware_by_id={ + "labware-id": LoadedLabware( + id="labware-id", + loadName="test", + definitionUri="def-uri", + location=OnLabwareLocation(labwareId="lower-labware-id"), + ), + "lower-labware-id": LoadedLabware( + id="lower-labware-id", + loadName="test", + definitionUri="def-uri", + location=AddressableAreaLocation(addressableAreaName="B4"), + ), + }, + ) + + with pytest.raises( + errors.LocationNotAccessibleByPipetteError, match="on staging slot" + ): + subject.raise_if_labware_inaccessible_by_pipette("labware-id") + + +def test_raise_if_labware_cannot_be_stacked_is_adapter() -> None: + """It should raise if the labware trying to be stacked is an adapter.""" + subject = get_labware_view() + + with pytest.raises( + errors.LabwareCannotBeStackedError, match="defined as an adapter" + ): + subject.raise_if_labware_cannot_be_stacked( + top_labware_definition=LabwareDefinition.model_construct( # type: ignore[call-arg] + parameters=Parameters.model_construct(loadName="name"), # type: ignore[call-arg] + allowedRoles=[LabwareRole.adapter], + ), + bottom_labware_id="labware-id", + ) + + +def test_raise_if_labware_cannot_be_stacked_not_validated() -> None: + """It should raise if the labware name is not in the definition stacking overlap.""" + subject = get_labware_view( + labware_by_id={ + "labware-id": LoadedLabware( + id="labware-id", + loadName="test", + definitionUri="def-uri", + location=DeckSlotLocation(slotName=DeckSlotName.SLOT_1), + ) + }, + ) + + with pytest.raises( + errors.LabwareCannotBeStackedError, match="loaded onto labware test" + ): + subject.raise_if_labware_cannot_be_stacked( + top_labware_definition=LabwareDefinition.model_construct( # type: ignore[call-arg] + parameters=Parameters.model_construct(loadName="name"), # type: ignore[call-arg] + stackingOffsetWithLabware={}, + ), + bottom_labware_id="labware-id", + ) + + +def test_raise_if_labware_cannot_be_stacked_on_module_not_adapter() -> None: + """It should raise if the below labware on a module is not an adapter.""" + subject = get_labware_view( + labware_by_id={ + "labware-id": LoadedLabware( + id="labware-id", + loadName="test", + definitionUri="def-uri", + location=ModuleLocation(moduleId="module-id"), + ) + }, + definitions_by_uri={ + "def-uri": LabwareDefinition.model_construct( # type: ignore[call-arg] + allowedRoles=[LabwareRole.labware] + ) + }, + ) + + with pytest.raises(errors.LabwareCannotBeStackedError, match="module"): + subject.raise_if_labware_cannot_be_stacked( + top_labware_definition=LabwareDefinition.model_construct( # type: ignore[call-arg] + parameters=Parameters.model_construct(loadName="name"), # type: ignore[call-arg] + stackingOffsetWithLabware={ + "test": SharedDataOverlapOffset(x=0, y=0, z=0) + }, + ), + bottom_labware_id="labware-id", + ) + + +def test_raise_if_labware_cannot_be_stacked_on_labware_on_adapter() -> None: + """It should raise if the OnLabware location is on an adapter.""" + subject = get_labware_view( + labware_by_id={ + "labware-id": LoadedLabware( + id="labware-id", + loadName="test", + definitionUri="def-uri-1", + location=OnLabwareLocation(labwareId="below-id"), + ), + "below-id": LoadedLabware( + id="below-id", + loadName="adapter-name", + definitionUri="def-uri-2", + location=DeckSlotLocation(slotName=DeckSlotName.SLOT_1), + ), + }, + definitions_by_uri={ + "def-uri-1": LabwareDefinition.model_construct( # type: ignore[call-arg] + allowedRoles=[LabwareRole.labware] + ), + "def-uri-2": LabwareDefinition.model_construct( # type: ignore[call-arg] + allowedRoles=[LabwareRole.adapter] + ), + }, + ) + + with pytest.raises( + errors.LabwareCannotBeStackedError, match="cannot be loaded to stack" + ): + subject.raise_if_labware_cannot_be_stacked( + top_labware_definition=LabwareDefinition.model_construct( # type: ignore[call-arg] + parameters=Parameters.model_construct(loadName="name"), # type: ignore[call-arg] + stackingOffsetWithLabware={ + "test": SharedDataOverlapOffset(x=0, y=0, z=0) + }, + ), + bottom_labware_id="labware-id", + ) + + +@pytest.mark.parametrize( + argnames=[ + "allowed_roles", + "stack_limit", + "exception", + ], + argvalues=[ + [ + [LabwareRole.labware], + 1, + pytest.raises(errors.LabwareCannotBeStackedError), + ], + [ + [LabwareRole.lid], + 5, + does_not_raise(), + ], + ], +) +def test_labware_stacking_height_passes_or_raises( + allowed_roles: List[LabwareRole], + stack_limit: int, + exception: ContextManager[None], +) -> None: + """It should raise if the labware is stacked too high, and pass if the labware definition allowed this.""" + subject = get_labware_view( + labware_by_id={ + "labware-id4": LoadedLabware( + id="labware-id4", + loadName="test", + definitionUri="def-uri-1", + location=OnLabwareLocation(labwareId="labware-id3"), + ), + "labware-id3": LoadedLabware( + id="labware-id3", + loadName="test", + definitionUri="def-uri-1", + location=OnLabwareLocation(labwareId="labware-id2"), + ), + "labware-id2": LoadedLabware( + id="labware-id2", + loadName="test", + definitionUri="def-uri-1", + location=OnLabwareLocation(labwareId="labware-id1"), + ), + "labware-id1": LoadedLabware( + id="labware-id1", + loadName="test", + definitionUri="def-uri-1", + location=DeckSlotLocation(slotName=DeckSlotName.SLOT_1), + ), + }, + definitions_by_uri={ + "def-uri-1": LabwareDefinition.model_construct( # type: ignore[call-arg] + allowedRoles=allowed_roles, + parameters=Parameters.model_construct( + format="irregular", + isTiprack=False, + loadName="name", + isMagneticModuleCompatible=False, + ), + stackLimit=stack_limit, + ) + }, + ) + + with exception: + subject.raise_if_labware_cannot_be_stacked( + top_labware_definition=LabwareDefinition.model_construct( # type: ignore[call-arg] + parameters=Parameters.model_construct( + format="irregular", + isTiprack=False, + loadName="name", + isMagneticModuleCompatible=False, + ), + stackingOffsetWithLabware={ + "test": SharedDataOverlapOffset(x=0, y=0, z=0) + }, + stackLimit=stack_limit, + ), + bottom_labware_id="labware-id4", + ) + + +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) + + assert subject.get_deck_default_gripper_offsets() == LabwareMovementOffsetData( + pickUpOffset=LabwareOffsetVector(x=0, y=0, z=0), + dropOffset=LabwareOffsetVector(x=0, y=0, z=-0.75), + ) + + +def test_get_labware_gripper_offsets( + well_plate_def: LabwareDefinition, + adapter_plate_def: LabwareDefinition, +) -> None: + """It should get the labware's gripper offsets.""" + subject = get_labware_view( + labware_by_id={"plate-id": plate, "adapter-plate-id": adapter_plate}, + definitions_by_uri={ + "some-plate-uri": well_plate_def, + "some-adapter-uri": adapter_plate_def, + }, + ) + + assert ( + subject.get_child_gripper_offsets(labware_id="plate-id", slot_name=None) is None + ) + assert subject.get_child_gripper_offsets( + labware_id="adapter-plate-id", slot_name=DeckSlotName.SLOT_D1 + ) == LabwareMovementOffsetData( + pickUpOffset=LabwareOffsetVector(x=0, y=0, z=0), + dropOffset=LabwareOffsetVector(x=2, y=0, z=0), + ) + + +def test_get_labware_gripper_offsets_default_no_slots( + well_plate_def: LabwareDefinition, + adapter_plate_def: LabwareDefinition, +) -> None: + """It should get the labware's gripper offsets with only a default gripper offset entry.""" + subject = get_labware_view( + labware_by_id={ + "labware-id": LoadedLabware( + id="labware-id", + loadName="labware-load-name", + location=DeckSlotLocation(slotName=DeckSlotName.SLOT_1), + definitionUri="some-labware-uri", + offsetId=None, + displayName="Fancy Labware Name", + ) + }, + definitions_by_uri={ + "some-labware-uri": LabwareDefinition.model_construct( # type: ignore[call-arg] + gripperOffsets={ + "default": GripperOffsets( + pickUpOffset=OffsetVector(x=1, y=2, z=3), + dropOffset=OffsetVector(x=4, y=5, z=6), + ) + } + ), + }, + ) + + assert ( + subject.get_child_gripper_offsets( + labware_id="labware-id", slot_name=DeckSlotName.SLOT_D1 + ) + is None + ) + + assert subject.get_child_gripper_offsets( + labware_id="labware-id", slot_name=None + ) == LabwareMovementOffsetData( + pickUpOffset=LabwareOffsetVector(x=1, y=2, z=3), + dropOffset=LabwareOffsetVector(x=4, y=5, z=6), + ) + + +def test_get_grip_force( + flex_50uL_tiprack: LabwareDefinition, + reservoir_def: LabwareDefinition, +) -> None: + """It should get the grip force, if present, from labware definition or return default.""" + subject = get_labware_view() + + assert subject.get_grip_force(flex_50uL_tiprack) == 16 # from definition + assert subject.get_grip_force(reservoir_def) == 15 # default + + +def test_get_grip_height_from_labware_bottom( + well_plate_def: LabwareDefinition, + reservoir_def: LabwareDefinition, +) -> None: + """It should get the grip height, if present, from labware definition or return default.""" + subject = get_labware_view() + assert ( + subject.get_grip_height_from_labware_bottom(well_plate_def) == 12.2 + ) # from definition + assert subject.get_grip_height_from_labware_bottom(reservoir_def) == 15.7 # default + + +@pytest.mark.parametrize( + "labware_to_check,well_bbox", + [ + ("opentrons_universal_flat_adapter", Dimensions(0, 0, 0)), + ( + "corning_96_wellplate_360ul_flat", + Dimensions(116.81 - 10.95, 77.67 - 7.81, 14.22), + ), + ("nest_12_reservoir_15ml", Dimensions(117.48 - 10.28, 78.38 - 7.18, 31.4)), + ], +) +def test_calculates_well_bounding_box( + labware_to_check: str, well_bbox: Dimensions +) -> None: + """It should be able to calculate well bounding boxes.""" + definition = LabwareDefinition.model_validate(load_definition(labware_to_check, 1)) + subject = get_labware_view() + assert subject.get_well_bbox(definition).x == pytest.approx(well_bbox.x) + assert subject.get_well_bbox(definition).y == pytest.approx(well_bbox.y) + assert subject.get_well_bbox(definition).z == pytest.approx(well_bbox.z) diff --git a/api/tests/opentrons/protocol_engine/state/test_liquid_class_store_old.py b/api/tests/opentrons/protocol_engine/state/test_liquid_class_store_old.py new file mode 100644 index 00000000000..9d910d9495a --- /dev/null +++ b/api/tests/opentrons/protocol_engine/state/test_liquid_class_store_old.py @@ -0,0 +1,64 @@ +"""Liquid state store tests. + +DEPRECATED: Testing LiquidClassStore independently of LiquidClassView is no +longer helpful. Try to add new tests to test_liquid_class_state.py, where they can be +tested together, treating LiquidClassState as a private implementation detail. +""" +import pytest + +from opentrons_shared_data.liquid_classes.liquid_class_definition import ( + LiquidClassSchemaV1, +) +from opentrons.protocol_engine import actions +from opentrons.protocol_engine.commands.load_liquid_class import LoadLiquidClass +from opentrons.protocol_engine.state import update_types +from opentrons.protocol_engine.state.liquid_classes import LiquidClassStore +from opentrons.protocol_engine.types import LiquidClassRecord + + +@pytest.fixture +def subject() -> LiquidClassStore: + """The LiquidClassStore test subject.""" + return LiquidClassStore() + + +def test_handles_add_liquid_class( + subject: LiquidClassStore, minimal_liquid_class_def2: LiquidClassSchemaV1 +) -> None: + """Should add the LiquidClassRecord to the store.""" + pipette_0 = minimal_liquid_class_def2.byPipette[0] + by_tip_type_0 = pipette_0.byTipType[0] + liquid_class_record = LiquidClassRecord( + liquidClassName=minimal_liquid_class_def2.liquidClassName, + pipetteModel=pipette_0.pipetteModel, + tiprack=by_tip_type_0.tiprack, + aspirate=by_tip_type_0.aspirate, + singleDispense=by_tip_type_0.singleDispense, + multiDispense=by_tip_type_0.multiDispense, + ) + + subject.handle_action( + actions.SucceedCommandAction( + command=LoadLiquidClass.model_construct(), # type: ignore[call-arg] + state_update=update_types.StateUpdate( + liquid_class_loaded=update_types.LiquidClassLoadedUpdate( + liquid_class_id="liquid-class-id", + liquid_class_record=liquid_class_record, + ), + ), + ) + ) + + assert len(subject.state.liquid_class_record_by_id) == 1 + assert ( + subject.state.liquid_class_record_by_id["liquid-class-id"] + == liquid_class_record + ) + + assert len(subject.state.liquid_class_record_to_id) == 1 + # Make sure that LiquidClassRecords are hashable, and that we can query for LiquidClassRecords by value: + assert ( + subject.state.liquid_class_record_to_id[liquid_class_record] + == "liquid-class-id" + ) + # If this fails with an error like "TypeError: unhashable type: AspirateProperties", then you broke something. diff --git a/api/tests/opentrons/protocol_engine/state/test_liquid_class_view_old.py b/api/tests/opentrons/protocol_engine/state/test_liquid_class_view_old.py new file mode 100644 index 00000000000..f28438b3756 --- /dev/null +++ b/api/tests/opentrons/protocol_engine/state/test_liquid_class_view_old.py @@ -0,0 +1,67 @@ +"""Liquid view tests. + +DEPRECATED: Testing LiquidClassView independently of LiquidClassStore is no +longer helpful. Try to add new tests to test_liquid_class_state.py, where they can be +tested together, treating LiquidClassState as a private implementation detail. +""" +import pytest + +from opentrons_shared_data.liquid_classes.liquid_class_definition import ( + LiquidClassSchemaV1, +) + +from opentrons.protocol_engine.state.liquid_classes import ( + LiquidClassState, + LiquidClassView, +) +from opentrons.protocol_engine.types import LiquidClassRecord + + +@pytest.fixture +def liquid_class_record( + minimal_liquid_class_def2: LiquidClassSchemaV1, +) -> LiquidClassRecord: + """An example LiquidClassRecord for tests.""" + pipette_0 = minimal_liquid_class_def2.byPipette[0] + by_tip_type_0 = pipette_0.byTipType[0] + return LiquidClassRecord( + liquidClassName=minimal_liquid_class_def2.liquidClassName, + pipetteModel=pipette_0.pipetteModel, + tiprack=by_tip_type_0.tiprack, + aspirate=by_tip_type_0.aspirate, + singleDispense=by_tip_type_0.singleDispense, + multiDispense=by_tip_type_0.multiDispense, + ) + + +@pytest.fixture +def subject(liquid_class_record: LiquidClassRecord) -> LiquidClassView: + """The LiquidClassView test subject.""" + state = LiquidClassState( + liquid_class_record_by_id={"liquid-class-id": liquid_class_record}, + liquid_class_record_to_id={liquid_class_record: "liquid-class-id"}, + ) + return LiquidClassView(state) + + +def test_get_by_id( + subject: LiquidClassView, liquid_class_record: LiquidClassRecord +) -> None: + """Should look up LiquidClassRecord by ID.""" + assert subject.get("liquid-class-id") == liquid_class_record + + +def test_get_by_liquid_class_record( + subject: LiquidClassView, liquid_class_record: LiquidClassRecord +) -> None: + """Should look up existing ID given a LiquidClassRecord.""" + assert ( + subject.get_id_for_liquid_class_record(liquid_class_record) == "liquid-class-id" + ) + + +def test_get_all( + subject: LiquidClassView, liquid_class_record: LiquidClassRecord +) -> None: + """Should get all LiquidClassRecords in the store.""" + assert subject.get_all() == {"liquid-class-id": liquid_class_record} diff --git a/api/tests/opentrons/protocol_engine/state/test_liquid_store.py b/api/tests/opentrons/protocol_engine/state/test_liquid_store.py deleted file mode 100644 index fe21b43193e..00000000000 --- a/api/tests/opentrons/protocol_engine/state/test_liquid_store.py +++ /dev/null @@ -1,27 +0,0 @@ -"""Liquid state store tests.""" -import pytest -from opentrons.protocol_engine.state.liquids import LiquidStore -from opentrons.protocol_engine import Liquid -from opentrons.protocol_engine.actions.actions import AddLiquidAction - - -@pytest.fixture -def subject() -> LiquidStore: - """Liquid store test subject.""" - return LiquidStore() - - -def test_handles_add_liquid(subject: LiquidStore) -> None: - """It should add the liquid to the state.""" - expected_liquid = Liquid( - id="water-id", displayName="water", description="water-desc" - ) - subject.handle_action( - AddLiquidAction( - Liquid(id="water-id", displayName="water", description="water-desc") - ) - ) - - assert len(subject.state.liquids_by_id) == 1 - - assert subject.state.liquids_by_id["water-id"] == expected_liquid diff --git a/api/tests/opentrons/protocol_engine/state/test_liquid_store_old.py b/api/tests/opentrons/protocol_engine/state/test_liquid_store_old.py new file mode 100644 index 00000000000..18417fe7570 --- /dev/null +++ b/api/tests/opentrons/protocol_engine/state/test_liquid_store_old.py @@ -0,0 +1,32 @@ +"""Liquid state store tests. + +DEPRECATED: Testing LiquidStore independently of LiquidView is no longer helpful. +Try to add new tests to test_liquid_state.py, where they can be tested together, +treating LiquidState as a private implementation detail. +""" +import pytest +from opentrons.protocol_engine.state.liquids import LiquidStore +from opentrons.protocol_engine import Liquid +from opentrons.protocol_engine.actions.actions import AddLiquidAction + + +@pytest.fixture +def subject() -> LiquidStore: + """Liquid store test subject.""" + return LiquidStore() + + +def test_handles_add_liquid(subject: LiquidStore) -> None: + """It should add the liquid to the state.""" + expected_liquid = Liquid( + id="water-id", displayName="water", description="water-desc" + ) + subject.handle_action( + AddLiquidAction( + Liquid(id="water-id", displayName="water", description="water-desc") + ) + ) + + assert len(subject.state.liquids_by_id) == 1 + + assert subject.state.liquids_by_id["water-id"] == expected_liquid diff --git a/api/tests/opentrons/protocol_engine/state/test_liquid_view.py b/api/tests/opentrons/protocol_engine/state/test_liquid_view.py deleted file mode 100644 index f3424932b0e..00000000000 --- a/api/tests/opentrons/protocol_engine/state/test_liquid_view.py +++ /dev/null @@ -1,35 +0,0 @@ -"""Liquid view tests.""" -import pytest - -from opentrons.protocol_engine.state.liquids import LiquidState, LiquidView -from opentrons.protocol_engine import Liquid -from opentrons.protocol_engine.errors import LiquidDoesNotExistError - - -@pytest.fixture -def subject() -> LiquidView: - """Get a liquid view test subject.""" - state = LiquidState( - liquids_by_id={ - "water-id": Liquid( - id="water-id", displayName="water", description="water desc" - ) - } - ) - - return LiquidView(state) - - -def test_get_all(subject: LiquidView) -> None: - """Should return a list of liquids.""" - assert subject.get_all() == [ - Liquid(id="water-id", displayName="water", description="water desc") - ] - - -def test_has_liquid(subject: LiquidView) -> None: - """Should return true if an item exists in the liquids list.""" - assert subject.validate_liquid_id("water-id") == "water-id" - - with pytest.raises(LiquidDoesNotExistError): - subject.validate_liquid_id("no-id") diff --git a/api/tests/opentrons/protocol_engine/state/test_liquid_view_old.py b/api/tests/opentrons/protocol_engine/state/test_liquid_view_old.py new file mode 100644 index 00000000000..92a9a2bd667 --- /dev/null +++ b/api/tests/opentrons/protocol_engine/state/test_liquid_view_old.py @@ -0,0 +1,59 @@ +"""Liquid view tests. + +DEPRECATED: Testing LiquidView independently of LiquidStore is no longer helpful. +Try to add new tests to test_liquid_state.py, where they can be tested together, +treating LiquidState as a private implementation detail. +""" +import pytest + +from opentrons.protocol_engine.state.liquids import LiquidState, LiquidView +from opentrons.protocol_engine import Liquid +from opentrons.protocol_engine.errors import LiquidDoesNotExistError, InvalidLiquidError + + +@pytest.fixture +def subject() -> LiquidView: + """Get a liquid view test subject.""" + state = LiquidState( + liquids_by_id={ + "water-id": Liquid( + id="water-id", displayName="water", description="water desc" + ) + } + ) + + return LiquidView(state) + + +def test_get_all(subject: LiquidView) -> None: + """Should return a list of liquids.""" + assert subject.get_all() == [ + Liquid(id="water-id", displayName="water", description="water desc") + ] + + +def test_has_liquid(subject: LiquidView) -> None: + """Should return true if an item exists in the liquids list.""" + assert subject.validate_liquid_id("water-id") == "water-id" + + with pytest.raises(LiquidDoesNotExistError): + subject.validate_liquid_id("no-id") + + +def test_validate_liquid_prevents_empty(subject: LiquidView) -> None: + """It should not allow loading a liquid with the special id EMPTY.""" + with pytest.raises(InvalidLiquidError): + subject.validate_liquid_allowed( + Liquid(id="EMPTY", displayName="empty", description="nothing") + ) + + +def test_validate_liquid_allows_non_empty(subject: LiquidView) -> None: + """It should allow a valid liquid.""" + valid_liquid = Liquid( + id="some-id", + displayName="some-display-name", + description="some-description", + displayColor=None, + ) + assert subject.validate_liquid_allowed(valid_liquid) == valid_liquid diff --git a/api/tests/opentrons/protocol_engine/state/test_module_store.py b/api/tests/opentrons/protocol_engine/state/test_module_store.py deleted file mode 100644 index 832713ed0a4..00000000000 --- a/api/tests/opentrons/protocol_engine/state/test_module_store.py +++ /dev/null @@ -1,720 +0,0 @@ -"""Module state store tests.""" -from typing import List, Set, cast, Dict, Optional - -import pytest -from opentrons_shared_data.robot.types import RobotType -from opentrons_shared_data.deck.types import DeckDefinitionV5 -from pytest_lazyfixture import lazy_fixture # type: ignore[import-untyped] - -from opentrons.types import DeckSlotName -from opentrons.protocol_engine import commands, actions -from opentrons.protocol_engine.commands import ( - heater_shaker as hs_commands, - temperature_module as temp_commands, - thermocycler as tc_commands, -) -from opentrons.protocol_engine.types import ( - DeckSlotLocation, - ModuleDefinition, - ModuleModel, - HeaterShakerLatchStatus, - DeckType, - AddressableArea, - DeckConfigurationType, - PotentialCutoutFixture, -) - -from opentrons.protocol_engine.state.modules import ( - ModuleStore, - ModuleState, - HardwareModule, -) - -from opentrons.protocol_engine.state.module_substates import ( - MagneticModuleId, - MagneticModuleSubState, - HeaterShakerModuleId, - HeaterShakerModuleSubState, - TemperatureModuleId, - TemperatureModuleSubState, - ThermocyclerModuleId, - 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 - - -_OT2_STANDARD_CONFIG = Config( - use_simulated_deck_config=False, - robot_type="OT-2 Standard", - deck_type=DeckType.OT2_STANDARD, -) - - -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_definition={ - "displayName": "OT-3", - "robotType": "OT-3 Standard", - "models": ["OT-3 Standard"], - "extents": [477.2, 493.8, 0.0], - "paddingOffsets": { - "rear": -177.42, - "front": 51.8, - "leftSide": 31.88, - "rightSide": -80.32, - }, - "mountOffsets": { - "left": [-13.5, -60.5, 255.675], - "right": [40.5, -60.5, 255.675], - "gripper": [84.55, -12.75, 93.85], - }, - }, - 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, - deck_fixed_labware=[], - ) - - assert subject.state == ModuleState( - deck_type=DeckType.OT2_STANDARD, - requested_model_by_id={}, - slot_by_module_id={}, - hardware_by_module_id={}, - substate_by_module_id={}, - module_offset_by_serial={}, - additional_slots_occupied_by_module_id={}, - deck_fixed_labware=[], - ) - - -@pytest.mark.parametrize( - argnames=["module_definition", "params_model", "result_model", "expected_substate"], - argvalues=[ - ( - lazy_fixture("magdeck_v2_def"), - ModuleModel.MAGNETIC_MODULE_V2, - ModuleModel.MAGNETIC_MODULE_V2, - MagneticModuleSubState( - module_id=MagneticModuleId("module-id"), - model=ModuleModel.MAGNETIC_MODULE_V2, - ), - ), - ( - lazy_fixture("heater_shaker_v1_def"), - ModuleModel.HEATER_SHAKER_MODULE_V1, - ModuleModel.HEATER_SHAKER_MODULE_V1, - HeaterShakerModuleSubState( - module_id=HeaterShakerModuleId("module-id"), - labware_latch_status=HeaterShakerLatchStatus.UNKNOWN, - is_plate_shaking=False, - plate_target_temperature=None, - ), - ), - ( - lazy_fixture("tempdeck_v1_def"), - ModuleModel.TEMPERATURE_MODULE_V1, - ModuleModel.TEMPERATURE_MODULE_V1, - TemperatureModuleSubState( - module_id=TemperatureModuleId("module-id"), - plate_target_temperature=None, - ), - ), - ( - lazy_fixture("tempdeck_v1_def"), - ModuleModel.TEMPERATURE_MODULE_V2, - ModuleModel.TEMPERATURE_MODULE_V1, - TemperatureModuleSubState( - module_id=TemperatureModuleId("module-id"), - plate_target_temperature=None, - ), - ), - ( - lazy_fixture("tempdeck_v2_def"), - ModuleModel.TEMPERATURE_MODULE_V1, - ModuleModel.TEMPERATURE_MODULE_V2, - TemperatureModuleSubState( - module_id=TemperatureModuleId("module-id"), - plate_target_temperature=None, - ), - ), - ( - lazy_fixture("tempdeck_v2_def"), - ModuleModel.TEMPERATURE_MODULE_V2, - ModuleModel.TEMPERATURE_MODULE_V2, - TemperatureModuleSubState( - module_id=TemperatureModuleId("module-id"), - plate_target_temperature=None, - ), - ), - ( - lazy_fixture("thermocycler_v1_def"), - ModuleModel.THERMOCYCLER_MODULE_V1, - ModuleModel.THERMOCYCLER_MODULE_V1, - ThermocyclerModuleSubState( - module_id=ThermocyclerModuleId("module-id"), - is_lid_open=False, - target_block_temperature=None, - target_lid_temperature=None, - ), - ), - ], -) -def test_load_module( - module_definition: ModuleDefinition, - params_model: ModuleModel, - result_model: ModuleModel, - expected_substate: ModuleSubStateType, -) -> None: - """It should handle a successful LoadModule command.""" - action = actions.SucceedCommandAction( - command=commands.LoadModule.construct( # type: ignore[call-arg] - params=commands.LoadModuleParams( - model=params_model, - location=DeckSlotLocation(slotName=DeckSlotName.SLOT_1), - ), - result=commands.LoadModuleResult( - moduleId="module-id", - model=result_model, - serialNumber="serial-number", - definition=module_definition, - ), - ), - ) - - subject = ModuleStore( - config=_OT2_STANDARD_CONFIG, - deck_fixed_labware=[], - ) - subject.handle_action(action) - - assert subject.state == ModuleState( - deck_type=DeckType.OT2_STANDARD, - slot_by_module_id={"module-id": DeckSlotName.SLOT_1}, - requested_model_by_id={"module-id": params_model}, - hardware_by_module_id={ - "module-id": HardwareModule( - serial_number="serial-number", - definition=module_definition, - ) - }, - substate_by_module_id={"module-id": expected_substate}, - module_offset_by_serial={}, - additional_slots_occupied_by_module_id={}, - deck_fixed_labware=[], - ) - - -@pytest.mark.parametrize( - argnames=["tc_slot", "deck_type", "robot_type", "expected_additional_slots"], - argvalues=[ - ( - DeckSlotName.SLOT_7, - DeckType.OT2_STANDARD, - "OT-2 Standard", - [DeckSlotName.SLOT_8, DeckSlotName.SLOT_10, DeckSlotName.SLOT_11], - ), - ( - DeckSlotName.SLOT_B1, - DeckType.OT3_STANDARD, - "OT-3 Standard", - [DeckSlotName.SLOT_A1], - ), - ], -) -def test_load_thermocycler_in_thermocycler_slot( - tc_slot: DeckSlotName, - deck_type: DeckType, - robot_type: RobotType, - expected_additional_slots: List[DeckSlotName], - thermocycler_v2_def: ModuleDefinition, -) -> None: - """It should update additional slots for thermocycler module.""" - action = actions.SucceedCommandAction( - command=commands.LoadModule.construct( # type: ignore[call-arg] - params=commands.LoadModuleParams( - model=ModuleModel.THERMOCYCLER_MODULE_V2, - location=DeckSlotLocation(slotName=tc_slot), - ), - result=commands.LoadModuleResult( - moduleId="module-id", - model=ModuleModel.THERMOCYCLER_MODULE_V2, - serialNumber="serial-number", - definition=thermocycler_v2_def, - ), - ), - ) - - subject = ModuleStore( - Config( - use_simulated_deck_config=False, - robot_type=robot_type, - deck_type=deck_type, - ), - deck_fixed_labware=[], - ) - subject.handle_action(action) - - assert subject.state.slot_by_module_id == {"module-id": tc_slot} - assert subject.state.additional_slots_occupied_by_module_id == { - "module-id": expected_additional_slots - } - - -@pytest.mark.parametrize( - argnames=["module_definition", "live_data", "expected_substate"], - argvalues=[ - ( - lazy_fixture("magdeck_v2_def"), - {}, - MagneticModuleSubState( - module_id=MagneticModuleId("module-id"), - model=ModuleModel.MAGNETIC_MODULE_V2, - ), - ), - ( - lazy_fixture("heater_shaker_v1_def"), - { - "status": "abc", - "data": { - "labwareLatchStatus": "idle_closed", - "speedStatus": "holding at target", - "targetSpeed": 123, - "targetTemp": 123, - }, - }, - HeaterShakerModuleSubState( - module_id=HeaterShakerModuleId("module-id"), - labware_latch_status=HeaterShakerLatchStatus.CLOSED, - is_plate_shaking=True, - plate_target_temperature=123, - ), - ), - ( - lazy_fixture("tempdeck_v2_def"), - {"status": "abc", "data": {"targetTemp": 123}}, - TemperatureModuleSubState( - module_id=TemperatureModuleId("module-id"), - plate_target_temperature=123, - ), - ), - ( - lazy_fixture("thermocycler_v1_def"), - { - "status": "abc", - "data": { - "targetTemp": 123, - "lidTarget": 321, - "lid": "open", - }, - }, - ThermocyclerModuleSubState( - module_id=ThermocyclerModuleId("module-id"), - is_lid_open=True, - target_block_temperature=123, - target_lid_temperature=321, - ), - ), - ], -) -def test_add_module_action( - module_definition: ModuleDefinition, - live_data: LiveData, - expected_substate: ModuleSubStateType, -) -> None: - """It should be able to add attached modules directly into state.""" - action = actions.AddModuleAction( - module_id="module-id", - serial_number="serial-number", - definition=module_definition, - module_live_data=live_data, - ) - - subject = ModuleStore( - config=_OT2_STANDARD_CONFIG, - deck_fixed_labware=[], - ) - subject.handle_action(action) - - assert subject.state == ModuleState( - deck_type=DeckType.OT2_STANDARD, - slot_by_module_id={"module-id": None}, - requested_model_by_id={"module-id": None}, - hardware_by_module_id={ - "module-id": HardwareModule( - serial_number="serial-number", - definition=module_definition, - ) - }, - substate_by_module_id={"module-id": expected_substate}, - module_offset_by_serial={}, - additional_slots_occupied_by_module_id={}, - deck_fixed_labware=[], - ) - - -def test_handle_hs_temperature_commands(heater_shaker_v1_def: ModuleDefinition) -> None: - """It should update `plate_target_temperature` correctly.""" - load_module_cmd = commands.LoadModule.construct( # type: ignore[call-arg] - params=commands.LoadModuleParams( - model=ModuleModel.HEATER_SHAKER_MODULE_V1, - location=DeckSlotLocation(slotName=DeckSlotName.SLOT_1), - ), - result=commands.LoadModuleResult( - moduleId="module-id", - model=ModuleModel.HEATER_SHAKER_MODULE_V1, - serialNumber="serial-number", - definition=heater_shaker_v1_def, - ), - ) - set_temp_cmd = hs_commands.SetTargetTemperature.construct( # type: ignore[call-arg] - params=hs_commands.SetTargetTemperatureParams(moduleId="module-id", celsius=42), - result=hs_commands.SetTargetTemperatureResult(), - ) - deactivate_cmd = hs_commands.DeactivateHeater.construct( # type: ignore[call-arg] - params=hs_commands.DeactivateHeaterParams(moduleId="module-id"), - result=hs_commands.DeactivateHeaterResult(), - ) - subject = ModuleStore( - config=_OT2_STANDARD_CONFIG, - deck_fixed_labware=[], - ) - - subject.handle_action(actions.SucceedCommandAction(command=load_module_cmd)) - subject.handle_action(actions.SucceedCommandAction(command=set_temp_cmd)) - assert subject.state.substate_by_module_id == { - "module-id": HeaterShakerModuleSubState( - module_id=HeaterShakerModuleId("module-id"), - labware_latch_status=HeaterShakerLatchStatus.UNKNOWN, - is_plate_shaking=False, - plate_target_temperature=42, - ) - } - subject.handle_action(actions.SucceedCommandAction(command=deactivate_cmd)) - assert subject.state.substate_by_module_id == { - "module-id": HeaterShakerModuleSubState( - module_id=HeaterShakerModuleId("module-id"), - labware_latch_status=HeaterShakerLatchStatus.UNKNOWN, - is_plate_shaking=False, - plate_target_temperature=None, - ) - } - - -def test_handle_hs_shake_commands(heater_shaker_v1_def: ModuleDefinition) -> None: - """It should update heater-shaker's `is_plate_shaking` correctly.""" - load_module_cmd = commands.LoadModule.construct( # type: ignore[call-arg] - params=commands.LoadModuleParams( - model=ModuleModel.HEATER_SHAKER_MODULE_V1, - location=DeckSlotLocation(slotName=DeckSlotName.SLOT_1), - ), - result=commands.LoadModuleResult( - moduleId="module-id", - model=ModuleModel.HEATER_SHAKER_MODULE_V1, - serialNumber="serial-number", - definition=heater_shaker_v1_def, - ), - ) - set_shake_cmd = hs_commands.SetAndWaitForShakeSpeed.construct( # type: ignore[call-arg] - params=hs_commands.SetAndWaitForShakeSpeedParams(moduleId="module-id", rpm=111), - result=hs_commands.SetAndWaitForShakeSpeedResult(pipetteRetracted=False), - ) - deactivate_cmd = hs_commands.DeactivateShaker.construct( # type: ignore[call-arg] - params=hs_commands.DeactivateShakerParams(moduleId="module-id"), - result=hs_commands.DeactivateShakerResult(), - ) - subject = ModuleStore( - config=_OT2_STANDARD_CONFIG, - deck_fixed_labware=[], - ) - - subject.handle_action(actions.SucceedCommandAction(command=load_module_cmd)) - subject.handle_action(actions.SucceedCommandAction(command=set_shake_cmd)) - assert subject.state.substate_by_module_id == { - "module-id": HeaterShakerModuleSubState( - module_id=HeaterShakerModuleId("module-id"), - labware_latch_status=HeaterShakerLatchStatus.UNKNOWN, - is_plate_shaking=True, - plate_target_temperature=None, - ) - } - subject.handle_action(actions.SucceedCommandAction(command=deactivate_cmd)) - assert subject.state.substate_by_module_id == { - "module-id": HeaterShakerModuleSubState( - module_id=HeaterShakerModuleId("module-id"), - labware_latch_status=HeaterShakerLatchStatus.UNKNOWN, - is_plate_shaking=False, - plate_target_temperature=None, - ) - } - - -def test_handle_hs_labware_latch_commands( - heater_shaker_v1_def: ModuleDefinition, -) -> None: - """It should update heater-shaker's `is_labware_latch_closed` correctly.""" - load_module_cmd = commands.LoadModule.construct( # type: ignore[call-arg] - params=commands.LoadModuleParams( - model=ModuleModel.HEATER_SHAKER_MODULE_V1, - location=DeckSlotLocation(slotName=DeckSlotName.SLOT_1), - ), - result=commands.LoadModuleResult( - moduleId="module-id", - model=ModuleModel.HEATER_SHAKER_MODULE_V1, - serialNumber="serial-number", - definition=heater_shaker_v1_def, - ), - ) - close_latch_cmd = hs_commands.CloseLabwareLatch.construct( # type: ignore[call-arg] - params=hs_commands.CloseLabwareLatchParams(moduleId="module-id"), - result=hs_commands.CloseLabwareLatchResult(), - ) - open_latch_cmd = hs_commands.OpenLabwareLatch.construct( # type: ignore[call-arg] - params=hs_commands.OpenLabwareLatchParams(moduleId="module-id"), - result=hs_commands.OpenLabwareLatchResult(pipetteRetracted=False), - ) - subject = ModuleStore( - config=_OT2_STANDARD_CONFIG, - deck_fixed_labware=[], - ) - - subject.handle_action(actions.SucceedCommandAction(command=load_module_cmd)) - assert subject.state.substate_by_module_id == { - "module-id": HeaterShakerModuleSubState( - module_id=HeaterShakerModuleId("module-id"), - labware_latch_status=HeaterShakerLatchStatus.UNKNOWN, - is_plate_shaking=False, - plate_target_temperature=None, - ) - } - - subject.handle_action(actions.SucceedCommandAction(command=close_latch_cmd)) - assert subject.state.substate_by_module_id == { - "module-id": HeaterShakerModuleSubState( - module_id=HeaterShakerModuleId("module-id"), - labware_latch_status=HeaterShakerLatchStatus.CLOSED, - is_plate_shaking=False, - plate_target_temperature=None, - ) - } - subject.handle_action(actions.SucceedCommandAction(command=open_latch_cmd)) - assert subject.state.substate_by_module_id == { - "module-id": HeaterShakerModuleSubState( - module_id=HeaterShakerModuleId("module-id"), - labware_latch_status=HeaterShakerLatchStatus.OPEN, - is_plate_shaking=False, - plate_target_temperature=None, - ) - } - - -def test_handle_tempdeck_temperature_commands( - tempdeck_v2_def: ModuleDefinition, -) -> None: - """It should update Tempdeck's `plate_target_temperature` correctly.""" - load_module_cmd = commands.LoadModule.construct( # type: ignore[call-arg] - params=commands.LoadModuleParams( - model=ModuleModel.TEMPERATURE_MODULE_V2, - location=DeckSlotLocation(slotName=DeckSlotName.SLOT_1), - ), - result=commands.LoadModuleResult( - moduleId="module-id", - model=ModuleModel.TEMPERATURE_MODULE_V2, - serialNumber="serial-number", - definition=tempdeck_v2_def, - ), - ) - set_temp_cmd = temp_commands.SetTargetTemperature.construct( # type: ignore[call-arg] - params=temp_commands.SetTargetTemperatureParams( - moduleId="module-id", celsius=42.4 - ), - result=temp_commands.SetTargetTemperatureResult(targetTemperature=42), - ) - deactivate_cmd = temp_commands.DeactivateTemperature.construct( # type: ignore[call-arg] - params=temp_commands.DeactivateTemperatureParams(moduleId="module-id"), - result=temp_commands.DeactivateTemperatureResult(), - ) - subject = ModuleStore( - config=_OT2_STANDARD_CONFIG, - deck_fixed_labware=[], - ) - - subject.handle_action(actions.SucceedCommandAction(command=load_module_cmd)) - subject.handle_action(actions.SucceedCommandAction(command=set_temp_cmd)) - assert subject.state.substate_by_module_id == { - "module-id": TemperatureModuleSubState( - module_id=TemperatureModuleId("module-id"), plate_target_temperature=42 - ) - } - subject.handle_action(actions.SucceedCommandAction(command=deactivate_cmd)) - assert subject.state.substate_by_module_id == { - "module-id": TemperatureModuleSubState( - module_id=TemperatureModuleId("module-id"), plate_target_temperature=None - ) - } - - -def test_handle_thermocycler_temperature_commands( - thermocycler_v1_def: ModuleDefinition, -) -> None: - """It should update thermocycler's temperature statuses correctly.""" - load_module_cmd = commands.LoadModule.construct( # type: ignore[call-arg] - params=commands.LoadModuleParams( - model=ModuleModel.THERMOCYCLER_MODULE_V1, - location=DeckSlotLocation(slotName=DeckSlotName.SLOT_1), - ), - result=commands.LoadModuleResult( - moduleId="module-id", - model=ModuleModel.THERMOCYCLER_MODULE_V1, - serialNumber="serial-number", - definition=thermocycler_v1_def, - ), - ) - set_block_temp_cmd = tc_commands.SetTargetBlockTemperature.construct( # type: ignore[call-arg] - params=tc_commands.SetTargetBlockTemperatureParams( - moduleId="module-id", celsius=42.4 - ), - result=tc_commands.SetTargetBlockTemperatureResult(targetBlockTemperature=42.4), - ) - deactivate_block_cmd = tc_commands.DeactivateBlock.construct( # type: ignore[call-arg] - params=tc_commands.DeactivateBlockParams(moduleId="module-id"), - result=tc_commands.DeactivateBlockResult(), - ) - set_lid_temp_cmd = tc_commands.SetTargetLidTemperature.construct( # type: ignore[call-arg] - params=tc_commands.SetTargetLidTemperatureParams( - moduleId="module-id", celsius=35.3 - ), - result=tc_commands.SetTargetLidTemperatureResult(targetLidTemperature=35.3), - ) - deactivate_lid_cmd = tc_commands.DeactivateLid.construct( # type: ignore[call-arg] - params=tc_commands.DeactivateLidParams(moduleId="module-id"), - result=tc_commands.DeactivateLidResult(), - ) - subject = ModuleStore( - config=_OT2_STANDARD_CONFIG, - deck_fixed_labware=[], - ) - - subject.handle_action(actions.SucceedCommandAction(command=load_module_cmd)) - subject.handle_action(actions.SucceedCommandAction(command=set_block_temp_cmd)) - assert subject.state.substate_by_module_id == { - "module-id": ThermocyclerModuleSubState( - module_id=ThermocyclerModuleId("module-id"), - is_lid_open=False, - target_block_temperature=42.4, - target_lid_temperature=None, - ) - } - subject.handle_action(actions.SucceedCommandAction(command=set_lid_temp_cmd)) - assert subject.state.substate_by_module_id == { - "module-id": ThermocyclerModuleSubState( - module_id=ThermocyclerModuleId("module-id"), - is_lid_open=False, - target_block_temperature=42.4, - target_lid_temperature=35.3, - ) - } - subject.handle_action(actions.SucceedCommandAction(command=deactivate_lid_cmd)) - assert subject.state.substate_by_module_id == { - "module-id": ThermocyclerModuleSubState( - module_id=ThermocyclerModuleId("module-id"), - is_lid_open=False, - target_block_temperature=42.4, - target_lid_temperature=None, - ) - } - subject.handle_action(actions.SucceedCommandAction(command=deactivate_block_cmd)) - assert subject.state.substate_by_module_id == { - "module-id": ThermocyclerModuleSubState( - module_id=ThermocyclerModuleId("module-id"), - is_lid_open=False, - target_block_temperature=None, - target_lid_temperature=None, - ) - } - - -def test_handle_thermocycler_lid_commands( - thermocycler_v1_def: ModuleDefinition, -) -> None: - """It should update thermocycler's lid status after executing lid commands.""" - load_module_cmd = commands.LoadModule.construct( # type: ignore[call-arg] - params=commands.LoadModuleParams( - model=ModuleModel.THERMOCYCLER_MODULE_V1, - location=DeckSlotLocation(slotName=DeckSlotName.SLOT_1), - ), - result=commands.LoadModuleResult( - moduleId="module-id", - model=ModuleModel.THERMOCYCLER_MODULE_V1, - serialNumber="serial-number", - definition=thermocycler_v1_def, - ), - ) - - open_lid_cmd = tc_commands.OpenLid.construct( # type: ignore[call-arg] - params=tc_commands.OpenLidParams(moduleId="module-id"), - result=tc_commands.OpenLidResult(), - ) - close_lid_cmd = tc_commands.CloseLid.construct( # type: ignore[call-arg] - params=tc_commands.CloseLidParams(moduleId="module-id"), - result=tc_commands.CloseLidResult(), - ) - - subject = ModuleStore( - Config( - use_simulated_deck_config=False, - robot_type="OT-3 Standard", - deck_type=DeckType.OT3_STANDARD, - ), - deck_fixed_labware=[], - ) - - subject.handle_action(actions.SucceedCommandAction(command=load_module_cmd)) - subject.handle_action(actions.SucceedCommandAction(command=open_lid_cmd)) - assert subject.state.substate_by_module_id == { - "module-id": ThermocyclerModuleSubState( - module_id=ThermocyclerModuleId("module-id"), - is_lid_open=True, - target_block_temperature=None, - target_lid_temperature=None, - ) - } - - subject.handle_action(actions.SucceedCommandAction(command=close_lid_cmd)) - assert subject.state.substate_by_module_id == { - "module-id": ThermocyclerModuleSubState( - module_id=ThermocyclerModuleId("module-id"), - is_lid_open=False, - target_block_temperature=None, - target_lid_temperature=None, - ) - } diff --git a/api/tests/opentrons/protocol_engine/state/test_module_store_old.py b/api/tests/opentrons/protocol_engine/state/test_module_store_old.py new file mode 100644 index 00000000000..4767ecad16b --- /dev/null +++ b/api/tests/opentrons/protocol_engine/state/test_module_store_old.py @@ -0,0 +1,725 @@ +"""Module state store tests. + +DEPRECATED: Testing ModuleStore independently of ModuleView is no longer helpful. +Try to add new tests to test_module_state.py, where they can be tested together, +treating ModuleState as a private implementation detail. +""" +from typing import List, Set, cast, Dict, Optional + +import pytest +from opentrons_shared_data.robot.types import RobotType +from opentrons_shared_data.deck.types import DeckDefinitionV5 +from pytest_lazyfixture import lazy_fixture # type: ignore[import-untyped] + +from opentrons.types import DeckSlotName +from opentrons.protocol_engine import commands, actions +from opentrons.protocol_engine.commands import ( + heater_shaker as hs_commands, + temperature_module as temp_commands, + thermocycler as tc_commands, +) +from opentrons.protocol_engine.types import ( + DeckSlotLocation, + ModuleDefinition, + ModuleModel, + HeaterShakerLatchStatus, + DeckType, + AddressableArea, + DeckConfigurationType, + PotentialCutoutFixture, +) + +from opentrons.protocol_engine.state.modules import ( + ModuleStore, + ModuleState, + HardwareModule, +) + +from opentrons.protocol_engine.state.module_substates import ( + MagneticModuleId, + MagneticModuleSubState, + HeaterShakerModuleId, + HeaterShakerModuleSubState, + TemperatureModuleId, + TemperatureModuleSubState, + ThermocyclerModuleId, + 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 + + +_OT2_STANDARD_CONFIG = Config( + use_simulated_deck_config=False, + robot_type="OT-2 Standard", + deck_type=DeckType.OT2_STANDARD, +) + + +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_definition={ + "displayName": "OT-3", + "robotType": "OT-3 Standard", + "models": ["OT-3 Standard"], + "extents": [477.2, 493.8, 0.0], + "paddingOffsets": { + "rear": -177.42, + "front": 51.8, + "leftSide": 31.88, + "rightSide": -80.32, + }, + "mountOffsets": { + "left": [-13.5, -60.5, 255.675], + "right": [40.5, -60.5, 255.675], + "gripper": [84.55, -12.75, 93.85], + }, + }, + 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, + deck_fixed_labware=[], + ) + + assert subject.state == ModuleState( + deck_type=DeckType.OT2_STANDARD, + requested_model_by_id={}, + slot_by_module_id={}, + hardware_by_module_id={}, + substate_by_module_id={}, + module_offset_by_serial={}, + additional_slots_occupied_by_module_id={}, + deck_fixed_labware=[], + ) + + +@pytest.mark.parametrize( + argnames=["module_definition", "params_model", "result_model", "expected_substate"], + argvalues=[ + ( + lazy_fixture("magdeck_v2_def"), + ModuleModel.MAGNETIC_MODULE_V2, + ModuleModel.MAGNETIC_MODULE_V2, + MagneticModuleSubState( + module_id=MagneticModuleId("module-id"), + model=ModuleModel.MAGNETIC_MODULE_V2, + ), + ), + ( + lazy_fixture("heater_shaker_v1_def"), + ModuleModel.HEATER_SHAKER_MODULE_V1, + ModuleModel.HEATER_SHAKER_MODULE_V1, + HeaterShakerModuleSubState( + module_id=HeaterShakerModuleId("module-id"), + labware_latch_status=HeaterShakerLatchStatus.UNKNOWN, + is_plate_shaking=False, + plate_target_temperature=None, + ), + ), + ( + lazy_fixture("tempdeck_v1_def"), + ModuleModel.TEMPERATURE_MODULE_V1, + ModuleModel.TEMPERATURE_MODULE_V1, + TemperatureModuleSubState( + module_id=TemperatureModuleId("module-id"), + plate_target_temperature=None, + ), + ), + ( + lazy_fixture("tempdeck_v1_def"), + ModuleModel.TEMPERATURE_MODULE_V2, + ModuleModel.TEMPERATURE_MODULE_V1, + TemperatureModuleSubState( + module_id=TemperatureModuleId("module-id"), + plate_target_temperature=None, + ), + ), + ( + lazy_fixture("tempdeck_v2_def"), + ModuleModel.TEMPERATURE_MODULE_V1, + ModuleModel.TEMPERATURE_MODULE_V2, + TemperatureModuleSubState( + module_id=TemperatureModuleId("module-id"), + plate_target_temperature=None, + ), + ), + ( + lazy_fixture("tempdeck_v2_def"), + ModuleModel.TEMPERATURE_MODULE_V2, + ModuleModel.TEMPERATURE_MODULE_V2, + TemperatureModuleSubState( + module_id=TemperatureModuleId("module-id"), + plate_target_temperature=None, + ), + ), + ( + lazy_fixture("thermocycler_v1_def"), + ModuleModel.THERMOCYCLER_MODULE_V1, + ModuleModel.THERMOCYCLER_MODULE_V1, + ThermocyclerModuleSubState( + module_id=ThermocyclerModuleId("module-id"), + is_lid_open=False, + target_block_temperature=None, + target_lid_temperature=None, + ), + ), + ], +) +def test_load_module( + module_definition: ModuleDefinition, + params_model: ModuleModel, + result_model: ModuleModel, + expected_substate: ModuleSubStateType, +) -> None: + """It should handle a successful LoadModule command.""" + action = actions.SucceedCommandAction( + command=commands.LoadModule.model_construct( # type: ignore[call-arg] + params=commands.LoadModuleParams( + model=params_model, + location=DeckSlotLocation(slotName=DeckSlotName.SLOT_1), + ), + result=commands.LoadModuleResult( + moduleId="module-id", + model=result_model, + serialNumber="serial-number", + definition=module_definition, + ), + ), + ) + + subject = ModuleStore( + config=_OT2_STANDARD_CONFIG, + deck_fixed_labware=[], + ) + subject.handle_action(action) + + assert subject.state == ModuleState( + deck_type=DeckType.OT2_STANDARD, + slot_by_module_id={"module-id": DeckSlotName.SLOT_1}, + requested_model_by_id={"module-id": params_model}, + hardware_by_module_id={ + "module-id": HardwareModule( + serial_number="serial-number", + definition=module_definition, + ) + }, + substate_by_module_id={"module-id": expected_substate}, + module_offset_by_serial={}, + additional_slots_occupied_by_module_id={}, + deck_fixed_labware=[], + ) + + +@pytest.mark.parametrize( + argnames=["tc_slot", "deck_type", "robot_type", "expected_additional_slots"], + argvalues=[ + ( + DeckSlotName.SLOT_7, + DeckType.OT2_STANDARD, + "OT-2 Standard", + [DeckSlotName.SLOT_8, DeckSlotName.SLOT_10, DeckSlotName.SLOT_11], + ), + ( + DeckSlotName.SLOT_B1, + DeckType.OT3_STANDARD, + "OT-3 Standard", + [DeckSlotName.SLOT_A1], + ), + ], +) +def test_load_thermocycler_in_thermocycler_slot( + tc_slot: DeckSlotName, + deck_type: DeckType, + robot_type: RobotType, + expected_additional_slots: List[DeckSlotName], + thermocycler_v2_def: ModuleDefinition, +) -> None: + """It should update additional slots for thermocycler module.""" + action = actions.SucceedCommandAction( + command=commands.LoadModule.model_construct( # type: ignore[call-arg] + params=commands.LoadModuleParams( + model=ModuleModel.THERMOCYCLER_MODULE_V2, + location=DeckSlotLocation(slotName=tc_slot), + ), + result=commands.LoadModuleResult( + moduleId="module-id", + model=ModuleModel.THERMOCYCLER_MODULE_V2, + serialNumber="serial-number", + definition=thermocycler_v2_def, + ), + ), + ) + + subject = ModuleStore( + Config( + use_simulated_deck_config=False, + robot_type=robot_type, + deck_type=deck_type, + ), + deck_fixed_labware=[], + ) + subject.handle_action(action) + + assert subject.state.slot_by_module_id == {"module-id": tc_slot} + assert subject.state.additional_slots_occupied_by_module_id == { + "module-id": expected_additional_slots + } + + +@pytest.mark.parametrize( + argnames=["module_definition", "live_data", "expected_substate"], + argvalues=[ + ( + lazy_fixture("magdeck_v2_def"), + {}, + MagneticModuleSubState( + module_id=MagneticModuleId("module-id"), + model=ModuleModel.MAGNETIC_MODULE_V2, + ), + ), + ( + lazy_fixture("heater_shaker_v1_def"), + { + "status": "abc", + "data": { + "labwareLatchStatus": "idle_closed", + "speedStatus": "holding at target", + "targetSpeed": 123, + "targetTemp": 123, + }, + }, + HeaterShakerModuleSubState( + module_id=HeaterShakerModuleId("module-id"), + labware_latch_status=HeaterShakerLatchStatus.CLOSED, + is_plate_shaking=True, + plate_target_temperature=123, + ), + ), + ( + lazy_fixture("tempdeck_v2_def"), + {"status": "abc", "data": {"targetTemp": 123}}, + TemperatureModuleSubState( + module_id=TemperatureModuleId("module-id"), + plate_target_temperature=123, + ), + ), + ( + lazy_fixture("thermocycler_v1_def"), + { + "status": "abc", + "data": { + "targetTemp": 123, + "lidTarget": 321, + "lid": "open", + }, + }, + ThermocyclerModuleSubState( + module_id=ThermocyclerModuleId("module-id"), + is_lid_open=True, + target_block_temperature=123, + target_lid_temperature=321, + ), + ), + ], +) +def test_add_module_action( + module_definition: ModuleDefinition, + live_data: LiveData, + expected_substate: ModuleSubStateType, +) -> None: + """It should be able to add attached modules directly into state.""" + action = actions.AddModuleAction( + module_id="module-id", + serial_number="serial-number", + definition=module_definition, + module_live_data=live_data, + ) + + subject = ModuleStore( + config=_OT2_STANDARD_CONFIG, + deck_fixed_labware=[], + ) + subject.handle_action(action) + + assert subject.state == ModuleState( + deck_type=DeckType.OT2_STANDARD, + slot_by_module_id={"module-id": None}, + requested_model_by_id={"module-id": None}, + hardware_by_module_id={ + "module-id": HardwareModule( + serial_number="serial-number", + definition=module_definition, + ) + }, + substate_by_module_id={"module-id": expected_substate}, + module_offset_by_serial={}, + additional_slots_occupied_by_module_id={}, + deck_fixed_labware=[], + ) + + +def test_handle_hs_temperature_commands(heater_shaker_v1_def: ModuleDefinition) -> None: + """It should update `plate_target_temperature` correctly.""" + load_module_cmd = commands.LoadModule.model_construct( # type: ignore[call-arg] + params=commands.LoadModuleParams( + model=ModuleModel.HEATER_SHAKER_MODULE_V1, + location=DeckSlotLocation(slotName=DeckSlotName.SLOT_1), + ), + result=commands.LoadModuleResult( + moduleId="module-id", + model=ModuleModel.HEATER_SHAKER_MODULE_V1, + serialNumber="serial-number", + definition=heater_shaker_v1_def, + ), + ) + set_temp_cmd = hs_commands.SetTargetTemperature.model_construct( # type: ignore[call-arg] + params=hs_commands.SetTargetTemperatureParams(moduleId="module-id", celsius=42), + result=hs_commands.SetTargetTemperatureResult(), + ) + deactivate_cmd = hs_commands.DeactivateHeater.model_construct( # type: ignore[call-arg] + params=hs_commands.DeactivateHeaterParams(moduleId="module-id"), + result=hs_commands.DeactivateHeaterResult(), + ) + subject = ModuleStore( + config=_OT2_STANDARD_CONFIG, + deck_fixed_labware=[], + ) + + subject.handle_action(actions.SucceedCommandAction(command=load_module_cmd)) + subject.handle_action(actions.SucceedCommandAction(command=set_temp_cmd)) + assert subject.state.substate_by_module_id == { + "module-id": HeaterShakerModuleSubState( + module_id=HeaterShakerModuleId("module-id"), + labware_latch_status=HeaterShakerLatchStatus.UNKNOWN, + is_plate_shaking=False, + plate_target_temperature=42, + ) + } + subject.handle_action(actions.SucceedCommandAction(command=deactivate_cmd)) + assert subject.state.substate_by_module_id == { + "module-id": HeaterShakerModuleSubState( + module_id=HeaterShakerModuleId("module-id"), + labware_latch_status=HeaterShakerLatchStatus.UNKNOWN, + is_plate_shaking=False, + plate_target_temperature=None, + ) + } + + +def test_handle_hs_shake_commands(heater_shaker_v1_def: ModuleDefinition) -> None: + """It should update heater-shaker's `is_plate_shaking` correctly.""" + load_module_cmd = commands.LoadModule.model_construct( # type: ignore[call-arg] + params=commands.LoadModuleParams( + model=ModuleModel.HEATER_SHAKER_MODULE_V1, + location=DeckSlotLocation(slotName=DeckSlotName.SLOT_1), + ), + result=commands.LoadModuleResult( + moduleId="module-id", + model=ModuleModel.HEATER_SHAKER_MODULE_V1, + serialNumber="serial-number", + definition=heater_shaker_v1_def, + ), + ) + set_shake_cmd = hs_commands.SetAndWaitForShakeSpeed.model_construct( # type: ignore[call-arg] + params=hs_commands.SetAndWaitForShakeSpeedParams(moduleId="module-id", rpm=111), + result=hs_commands.SetAndWaitForShakeSpeedResult(pipetteRetracted=False), + ) + deactivate_cmd = hs_commands.DeactivateShaker.model_construct( # type: ignore[call-arg] + params=hs_commands.DeactivateShakerParams(moduleId="module-id"), + result=hs_commands.DeactivateShakerResult(), + ) + subject = ModuleStore( + config=_OT2_STANDARD_CONFIG, + deck_fixed_labware=[], + ) + + subject.handle_action(actions.SucceedCommandAction(command=load_module_cmd)) + subject.handle_action(actions.SucceedCommandAction(command=set_shake_cmd)) + assert subject.state.substate_by_module_id == { + "module-id": HeaterShakerModuleSubState( + module_id=HeaterShakerModuleId("module-id"), + labware_latch_status=HeaterShakerLatchStatus.UNKNOWN, + is_plate_shaking=True, + plate_target_temperature=None, + ) + } + subject.handle_action(actions.SucceedCommandAction(command=deactivate_cmd)) + assert subject.state.substate_by_module_id == { + "module-id": HeaterShakerModuleSubState( + module_id=HeaterShakerModuleId("module-id"), + labware_latch_status=HeaterShakerLatchStatus.UNKNOWN, + is_plate_shaking=False, + plate_target_temperature=None, + ) + } + + +def test_handle_hs_labware_latch_commands( + heater_shaker_v1_def: ModuleDefinition, +) -> None: + """It should update heater-shaker's `is_labware_latch_closed` correctly.""" + load_module_cmd = commands.LoadModule.model_construct( # type: ignore[call-arg] + params=commands.LoadModuleParams( + model=ModuleModel.HEATER_SHAKER_MODULE_V1, + location=DeckSlotLocation(slotName=DeckSlotName.SLOT_1), + ), + result=commands.LoadModuleResult( + moduleId="module-id", + model=ModuleModel.HEATER_SHAKER_MODULE_V1, + serialNumber="serial-number", + definition=heater_shaker_v1_def, + ), + ) + close_latch_cmd = hs_commands.CloseLabwareLatch.model_construct( # type: ignore[call-arg] + params=hs_commands.CloseLabwareLatchParams(moduleId="module-id"), + result=hs_commands.CloseLabwareLatchResult(), + ) + open_latch_cmd = hs_commands.OpenLabwareLatch.model_construct( # type: ignore[call-arg] + params=hs_commands.OpenLabwareLatchParams(moduleId="module-id"), + result=hs_commands.OpenLabwareLatchResult(pipetteRetracted=False), + ) + subject = ModuleStore( + config=_OT2_STANDARD_CONFIG, + deck_fixed_labware=[], + ) + + subject.handle_action(actions.SucceedCommandAction(command=load_module_cmd)) + assert subject.state.substate_by_module_id == { + "module-id": HeaterShakerModuleSubState( + module_id=HeaterShakerModuleId("module-id"), + labware_latch_status=HeaterShakerLatchStatus.UNKNOWN, + is_plate_shaking=False, + plate_target_temperature=None, + ) + } + + subject.handle_action(actions.SucceedCommandAction(command=close_latch_cmd)) + assert subject.state.substate_by_module_id == { + "module-id": HeaterShakerModuleSubState( + module_id=HeaterShakerModuleId("module-id"), + labware_latch_status=HeaterShakerLatchStatus.CLOSED, + is_plate_shaking=False, + plate_target_temperature=None, + ) + } + subject.handle_action(actions.SucceedCommandAction(command=open_latch_cmd)) + assert subject.state.substate_by_module_id == { + "module-id": HeaterShakerModuleSubState( + module_id=HeaterShakerModuleId("module-id"), + labware_latch_status=HeaterShakerLatchStatus.OPEN, + is_plate_shaking=False, + plate_target_temperature=None, + ) + } + + +def test_handle_tempdeck_temperature_commands( + tempdeck_v2_def: ModuleDefinition, +) -> None: + """It should update Tempdeck's `plate_target_temperature` correctly.""" + load_module_cmd = commands.LoadModule.model_construct( # type: ignore[call-arg] + params=commands.LoadModuleParams( + model=ModuleModel.TEMPERATURE_MODULE_V2, + location=DeckSlotLocation(slotName=DeckSlotName.SLOT_1), + ), + result=commands.LoadModuleResult( + moduleId="module-id", + model=ModuleModel.TEMPERATURE_MODULE_V2, + serialNumber="serial-number", + definition=tempdeck_v2_def, + ), + ) + set_temp_cmd = temp_commands.SetTargetTemperature.model_construct( # type: ignore[call-arg] + params=temp_commands.SetTargetTemperatureParams( + moduleId="module-id", celsius=42.4 + ), + result=temp_commands.SetTargetTemperatureResult(targetTemperature=42), + ) + deactivate_cmd = temp_commands.DeactivateTemperature.model_construct( # type: ignore[call-arg] + params=temp_commands.DeactivateTemperatureParams(moduleId="module-id"), + result=temp_commands.DeactivateTemperatureResult(), + ) + subject = ModuleStore( + config=_OT2_STANDARD_CONFIG, + deck_fixed_labware=[], + ) + + subject.handle_action(actions.SucceedCommandAction(command=load_module_cmd)) + subject.handle_action(actions.SucceedCommandAction(command=set_temp_cmd)) + assert subject.state.substate_by_module_id == { + "module-id": TemperatureModuleSubState( + module_id=TemperatureModuleId("module-id"), plate_target_temperature=42 + ) + } + subject.handle_action(actions.SucceedCommandAction(command=deactivate_cmd)) + assert subject.state.substate_by_module_id == { + "module-id": TemperatureModuleSubState( + module_id=TemperatureModuleId("module-id"), plate_target_temperature=None + ) + } + + +def test_handle_thermocycler_temperature_commands( + thermocycler_v1_def: ModuleDefinition, +) -> None: + """It should update thermocycler's temperature statuses correctly.""" + load_module_cmd = commands.LoadModule.model_construct( # type: ignore[call-arg] + params=commands.LoadModuleParams( + model=ModuleModel.THERMOCYCLER_MODULE_V1, + location=DeckSlotLocation(slotName=DeckSlotName.SLOT_1), + ), + result=commands.LoadModuleResult( + moduleId="module-id", + model=ModuleModel.THERMOCYCLER_MODULE_V1, + serialNumber="serial-number", + definition=thermocycler_v1_def, + ), + ) + set_block_temp_cmd = tc_commands.SetTargetBlockTemperature.model_construct( # type: ignore[call-arg] + params=tc_commands.SetTargetBlockTemperatureParams( + moduleId="module-id", celsius=42.4 + ), + result=tc_commands.SetTargetBlockTemperatureResult(targetBlockTemperature=42.4), + ) + deactivate_block_cmd = tc_commands.DeactivateBlock.model_construct( # type: ignore[call-arg] + params=tc_commands.DeactivateBlockParams(moduleId="module-id"), + result=tc_commands.DeactivateBlockResult(), + ) + set_lid_temp_cmd = tc_commands.SetTargetLidTemperature.model_construct( # type: ignore[call-arg] + params=tc_commands.SetTargetLidTemperatureParams( + moduleId="module-id", celsius=35.3 + ), + result=tc_commands.SetTargetLidTemperatureResult(targetLidTemperature=35.3), + ) + deactivate_lid_cmd = tc_commands.DeactivateLid.model_construct( # type: ignore[call-arg] + params=tc_commands.DeactivateLidParams(moduleId="module-id"), + result=tc_commands.DeactivateLidResult(), + ) + subject = ModuleStore( + config=_OT2_STANDARD_CONFIG, + deck_fixed_labware=[], + ) + + subject.handle_action(actions.SucceedCommandAction(command=load_module_cmd)) + subject.handle_action(actions.SucceedCommandAction(command=set_block_temp_cmd)) + assert subject.state.substate_by_module_id == { + "module-id": ThermocyclerModuleSubState( + module_id=ThermocyclerModuleId("module-id"), + is_lid_open=False, + target_block_temperature=42.4, + target_lid_temperature=None, + ) + } + subject.handle_action(actions.SucceedCommandAction(command=set_lid_temp_cmd)) + assert subject.state.substate_by_module_id == { + "module-id": ThermocyclerModuleSubState( + module_id=ThermocyclerModuleId("module-id"), + is_lid_open=False, + target_block_temperature=42.4, + target_lid_temperature=35.3, + ) + } + subject.handle_action(actions.SucceedCommandAction(command=deactivate_lid_cmd)) + assert subject.state.substate_by_module_id == { + "module-id": ThermocyclerModuleSubState( + module_id=ThermocyclerModuleId("module-id"), + is_lid_open=False, + target_block_temperature=42.4, + target_lid_temperature=None, + ) + } + subject.handle_action(actions.SucceedCommandAction(command=deactivate_block_cmd)) + assert subject.state.substate_by_module_id == { + "module-id": ThermocyclerModuleSubState( + module_id=ThermocyclerModuleId("module-id"), + is_lid_open=False, + target_block_temperature=None, + target_lid_temperature=None, + ) + } + + +def test_handle_thermocycler_lid_commands( + thermocycler_v1_def: ModuleDefinition, +) -> None: + """It should update thermocycler's lid status after executing lid commands.""" + load_module_cmd = commands.LoadModule.model_construct( # type: ignore[call-arg] + params=commands.LoadModuleParams( + model=ModuleModel.THERMOCYCLER_MODULE_V1, + location=DeckSlotLocation(slotName=DeckSlotName.SLOT_1), + ), + result=commands.LoadModuleResult( + moduleId="module-id", + model=ModuleModel.THERMOCYCLER_MODULE_V1, + serialNumber="serial-number", + definition=thermocycler_v1_def, + ), + ) + + open_lid_cmd = tc_commands.OpenLid.model_construct( # type: ignore[call-arg] + params=tc_commands.OpenLidParams(moduleId="module-id"), + result=tc_commands.OpenLidResult(), + ) + close_lid_cmd = tc_commands.CloseLid.model_construct( # type: ignore[call-arg] + params=tc_commands.CloseLidParams(moduleId="module-id"), + result=tc_commands.CloseLidResult(), + ) + + subject = ModuleStore( + Config( + use_simulated_deck_config=False, + robot_type="OT-3 Standard", + deck_type=DeckType.OT3_STANDARD, + ), + deck_fixed_labware=[], + ) + + subject.handle_action(actions.SucceedCommandAction(command=load_module_cmd)) + subject.handle_action(actions.SucceedCommandAction(command=open_lid_cmd)) + assert subject.state.substate_by_module_id == { + "module-id": ThermocyclerModuleSubState( + module_id=ThermocyclerModuleId("module-id"), + is_lid_open=True, + target_block_temperature=None, + target_lid_temperature=None, + ) + } + + subject.handle_action(actions.SucceedCommandAction(command=close_lid_cmd)) + assert subject.state.substate_by_module_id == { + "module-id": ThermocyclerModuleSubState( + module_id=ThermocyclerModuleId("module-id"), + is_lid_open=False, + target_block_temperature=None, + target_lid_temperature=None, + ) + } diff --git a/api/tests/opentrons/protocol_engine/state/test_module_view.py b/api/tests/opentrons/protocol_engine/state/test_module_view.py deleted file mode 100644 index 66152a57240..00000000000 --- a/api/tests/opentrons/protocol_engine/state/test_module_view.py +++ /dev/null @@ -1,1983 +0,0 @@ -"""Tests for module state accessors in the protocol engine state store.""" -import pytest -from math import isclose -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, - Set, - cast, -) - -from opentrons_shared_data.robot.types import RobotType -from opentrons_shared_data.deck.types import DeckDefinitionV5 - -from opentrons_shared_data import load_shared_data -from opentrons.types import DeckSlotName, MountType -from opentrons.protocol_engine import errors -from opentrons.protocol_engine.types import ( - LoadedModule, - DeckSlotLocation, - ModuleDefinition, - ModuleModel, - LabwareOffsetVector, - DeckType, - 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, - HeaterShakerModuleId, - MagneticModuleSubState, - MagneticModuleId, - TemperatureModuleSubState, - TemperatureModuleId, - ThermocyclerModuleSubState, - 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_definition={ - "displayName": "OT-3", - "robotType": "OT-3 Standard", - "models": ["OT-3 Standard"], - "extents": [477.2, 493.8, 0.0], - "paddingOffsets": { - "rear": -177.42, - "front": 51.8, - "leftSide": 31.88, - "rightSide": -80.32, - }, - "mountOffsets": { - "left": [-13.5, -60.5, 255.675], - "right": [40.5, -60.5, 255.675], - "gripper": [84.55, -12.75, 93.85], - }, - }, - robot_type=robot_type, - use_simulated_deck_config=use_simulated_deck_config, - ) - - return AddressableAreaView(state=state) - - -def make_module_view( - deck_type: Optional[DeckType] = None, - slot_by_module_id: Optional[Dict[str, Optional[DeckSlotName]]] = None, - requested_model_by_module_id: Optional[Dict[str, Optional[ModuleModel]]] = None, - hardware_by_module_id: Optional[Dict[str, HardwareModule]] = None, - substate_by_module_id: Optional[Dict[str, ModuleSubStateType]] = None, - module_offset_by_serial: Optional[Dict[str, ModuleOffsetData]] = None, - additional_slots_occupied_by_module_id: Optional[ - Dict[str, List[DeckSlotName]] - ] = None, -) -> ModuleView: - """Get a module view test subject with the specified state.""" - state = ModuleState( - deck_type=deck_type or DeckType.OT2_STANDARD, - slot_by_module_id=slot_by_module_id or {}, - requested_model_by_id=requested_model_by_module_id or {}, - hardware_by_module_id=hardware_by_module_id or {}, - substate_by_module_id=substate_by_module_id or {}, - module_offset_by_serial=module_offset_by_serial or {}, - additional_slots_occupied_by_module_id=additional_slots_occupied_by_module_id - or {}, - deck_fixed_labware=[], - ) - - return ModuleView(state=state) - - -def get_sample_parent_module_view( - matching_module_def: ModuleDefinition, - matching_module_id: str, -) -> ModuleView: - """Get a ModuleView with attached modules including a requested matching module.""" - definition = load_shared_data("module/definitions/2/magneticModuleV1.json") - magdeck_def = ModuleDefinition.parse_raw(definition) - - return make_module_view( - slot_by_module_id={ - "id-non-matching": DeckSlotName.SLOT_1, - matching_module_id: DeckSlotName.SLOT_2, - "id-another-non-matching": DeckSlotName.SLOT_3, - }, - hardware_by_module_id={ - "id-non-matching": HardwareModule( - serial_number="serial-non-matching", - definition=magdeck_def, - ), - matching_module_id: HardwareModule( - serial_number="serial-matching", - definition=matching_module_def, - ), - "id-another-non-matching": HardwareModule( - serial_number="serial-another-non-matching", - definition=magdeck_def, - ), - }, - ) - - -def test_initial_module_data_by_id() -> None: - """It should raise if module ID doesn't exist.""" - subject = make_module_view() - - with pytest.raises(errors.ModuleNotLoadedError): - subject.get("helloWorld") - - -def test_get_missing_hardware() -> None: - """It should raise if no loaded hardware.""" - subject = make_module_view(slot_by_module_id={"module-id": DeckSlotName.SLOT_1}) - - with pytest.raises(errors.ModuleNotLoadedError): - subject.get("module-id") - - -def test_get_module_data(tempdeck_v1_def: ModuleDefinition) -> None: - """It should get module data from state by ID.""" - subject = make_module_view( - slot_by_module_id={"module-id": DeckSlotName.SLOT_1}, - hardware_by_module_id={ - "module-id": HardwareModule( - serial_number="serial-number", - definition=tempdeck_v1_def, - ) - }, - ) - - assert subject.get("module-id") == LoadedModule( - id="module-id", - model=ModuleModel.TEMPERATURE_MODULE_V1, - location=DeckSlotLocation(slotName=DeckSlotName.SLOT_1), - serialNumber="serial-number", - ) - - -def test_get_location(tempdeck_v1_def: ModuleDefinition) -> None: - """It should return the module's location or raise.""" - subject = make_module_view( - slot_by_module_id={ - "module-1": DeckSlotName.SLOT_1, - "module-2": None, - }, - hardware_by_module_id={ - "module-1": HardwareModule( - serial_number="serial-1", - definition=tempdeck_v1_def, - ), - "module-2": HardwareModule( - serial_number="serial-2", - definition=tempdeck_v1_def, - ), - }, - ) - - assert subject.get_location("module-1") == DeckSlotLocation( - slotName=DeckSlotName.SLOT_1 - ) - - with pytest.raises(errors.ModuleNotOnDeckError): - assert subject.get_location("module-2") - - -def test_get_all_modules( - tempdeck_v1_def: ModuleDefinition, - tempdeck_v2_def: ModuleDefinition, -) -> None: - """It should return all modules in state.""" - subject = make_module_view( - slot_by_module_id={ - "module-1": DeckSlotName.SLOT_1, - "module-2": DeckSlotName.SLOT_2, - }, - hardware_by_module_id={ - "module-1": HardwareModule( - serial_number="serial-1", - definition=tempdeck_v1_def, - ), - "module-2": HardwareModule( - serial_number="serial-2", - definition=tempdeck_v2_def, - ), - }, - ) - - assert subject.get_all() == [ - LoadedModule( - id="module-1", - serialNumber="serial-1", - model=ModuleModel.TEMPERATURE_MODULE_V1, - location=DeckSlotLocation(slotName=DeckSlotName.SLOT_1), - ), - LoadedModule( - id="module-2", - serialNumber="serial-2", - model=ModuleModel.TEMPERATURE_MODULE_V2, - location=DeckSlotLocation(slotName=DeckSlotName.SLOT_2), - ), - ] - - -def test_get_properties_by_id( - tempdeck_v2_def: ModuleDefinition, - magdeck_v1_def: ModuleDefinition, - mag_block_v1_def: ModuleDefinition, -) -> None: - """It should return a loaded module's properties by ID.""" - subject = make_module_view( - slot_by_module_id={ - "module-1": DeckSlotName.SLOT_1, - "module-2": DeckSlotName.SLOT_2, - "module-3": DeckSlotName.SLOT_3, - }, - requested_model_by_module_id={ - "module-1": ModuleModel.TEMPERATURE_MODULE_V1, - "module-2": ModuleModel.MAGNETIC_MODULE_V1, - "module-3": ModuleModel.MAGNETIC_BLOCK_V1, - }, - hardware_by_module_id={ - "module-1": HardwareModule( - serial_number="serial-1", - # Intentionally different from requested model. - definition=tempdeck_v2_def, - ), - "module-2": HardwareModule( - serial_number="serial-2", - definition=magdeck_v1_def, - ), - "module-3": HardwareModule(serial_number=None, definition=mag_block_v1_def), - }, - ) - - assert subject.get_definition("module-1") == tempdeck_v2_def - assert subject.get_dimensions("module-1") == tempdeck_v2_def.dimensions - assert subject.get_requested_model("module-1") == ModuleModel.TEMPERATURE_MODULE_V1 - assert subject.get_connected_model("module-1") == ModuleModel.TEMPERATURE_MODULE_V2 - assert subject.get_serial_number("module-1") == "serial-1" - assert subject.get_location("module-1") == DeckSlotLocation( - slotName=DeckSlotName.SLOT_1 - ) - - assert subject.get_definition("module-2") == magdeck_v1_def - assert subject.get_dimensions("module-2") == magdeck_v1_def.dimensions - assert subject.get_requested_model("module-2") == ModuleModel.MAGNETIC_MODULE_V1 - assert subject.get_connected_model("module-2") == ModuleModel.MAGNETIC_MODULE_V1 - assert subject.get_serial_number("module-2") == "serial-2" - assert subject.get_location("module-2") == DeckSlotLocation( - slotName=DeckSlotName.SLOT_2 - ) - - assert subject.get_definition("module-3") == mag_block_v1_def - assert subject.get_dimensions("module-3") == mag_block_v1_def.dimensions - assert subject.get_requested_model("module-3") == ModuleModel.MAGNETIC_BLOCK_V1 - assert subject.get_connected_model("module-3") == ModuleModel.MAGNETIC_BLOCK_V1 - assert subject.get_location("module-3") == DeckSlotLocation( - slotName=DeckSlotName.SLOT_3 - ) - - with pytest.raises(errors.ModuleNotConnectedError): - subject.get_serial_number("module-3") - - with pytest.raises(errors.ModuleNotLoadedError): - subject.get_definition("Not a module ID oh no") - - -@pytest.mark.parametrize( - argnames=["module_def", "slot", "expected_offset"], - argvalues=[ - ( - lazy_fixture("tempdeck_v1_def"), - DeckSlotName.SLOT_1, - LabwareOffsetVector(x=-0.15, y=-0.15, z=80.09), - ), - ( - lazy_fixture("tempdeck_v2_def"), - DeckSlotName.SLOT_1, - LabwareOffsetVector(x=-1.45, y=-0.15, z=80.09), - ), - ( - lazy_fixture("tempdeck_v2_def"), - DeckSlotName.SLOT_3, - LabwareOffsetVector(x=1.15, y=-0.15, z=80.09), - ), - ( - lazy_fixture("magdeck_v1_def"), - DeckSlotName.SLOT_1, - LabwareOffsetVector(x=0.125, y=-0.125, z=82.25), - ), - ( - lazy_fixture("magdeck_v2_def"), - DeckSlotName.SLOT_1, - LabwareOffsetVector(x=-1.175, y=-0.125, z=82.25), - ), - ( - lazy_fixture("magdeck_v2_def"), - DeckSlotName.SLOT_3, - LabwareOffsetVector(x=1.425, y=-0.125, z=82.25), - ), - ( - lazy_fixture("thermocycler_v1_def"), - DeckSlotName.SLOT_7, - LabwareOffsetVector(x=0, y=82.56, z=97.8), - ), - ( - lazy_fixture("thermocycler_v2_def"), - DeckSlotName.SLOT_7, - LabwareOffsetVector(x=0, y=68.8, z=108.96), - ), - ( - lazy_fixture("heater_shaker_v1_def"), - DeckSlotName.SLOT_1, - LabwareOffsetVector(x=-0.125, y=1.125, z=68.275), - ), - ( - lazy_fixture("heater_shaker_v1_def"), - DeckSlotName.SLOT_3, - LabwareOffsetVector(x=0.125, y=-1.125, z=68.275), - ), - ], -) -def test_get_module_offset_for_ot2_standard( - module_def: ModuleDefinition, - slot: DeckSlotName, - expected_offset: LabwareOffsetVector, -) -> None: - """It should return the correct labware offset for module in specified slot.""" - subject = make_module_view( - deck_type=DeckType.OT2_STANDARD, - slot_by_module_id={"module-id": slot}, - hardware_by_module_id={ - "module-id": HardwareModule( - serial_number="module-serial", - definition=module_def, - ) - }, - ) - assert ( - subject.get_nominal_offset_to_child("module-id", get_addressable_area_view()) - == expected_offset - ) - - -@pytest.mark.parametrize( - 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.to_ot3_equivalent(), - LabwareOffsetVector(x=0, y=0, z=38.0), - lazy_fixture("ot3_standard_deck_def"), - ), - ], -) -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( - deck_type=DeckType.OT3_STANDARD, - slot_by_module_id={"module-id": slot}, - hardware_by_module_id={ - "module-id": HardwareModule( - serial_number="module-serial", - definition=module_def, - ) - }, - ) - - result_offset = subject.get_nominal_offset_to_child( - "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) - ) - - -def test_get_magnetic_module_substate( - magdeck_v1_def: ModuleDefinition, - magdeck_v2_def: ModuleDefinition, - heater_shaker_v1_def: ModuleDefinition, -) -> None: - """It should return a substate for the given Magnetic Module, if valid.""" - subject = make_module_view( - slot_by_module_id={ - "magnetic-module-gen1-id": DeckSlotName.SLOT_1, - "magnetic-module-gen2-id": DeckSlotName.SLOT_2, - "heatshake-module-id": DeckSlotName.SLOT_3, - }, - hardware_by_module_id={ - "magnetic-module-gen1-id": HardwareModule( - serial_number="magnetic-module-gen1-serial", - definition=magdeck_v1_def, - ), - "magnetic-module-gen2-id": HardwareModule( - serial_number="magnetic-module-gen2-serial", - definition=magdeck_v2_def, - ), - "heatshake-module-id": HardwareModule( - serial_number="heatshake-module-serial", - definition=heater_shaker_v1_def, - ), - }, - substate_by_module_id={ - "magnetic-module-gen1-id": MagneticModuleSubState( - module_id=MagneticModuleId("magnetic-module-gen1-id"), - model=ModuleModel.MAGNETIC_MODULE_V1, - ), - "magnetic-module-gen2-id": MagneticModuleSubState( - module_id=MagneticModuleId("magnetic-module-gen2-id"), - model=ModuleModel.MAGNETIC_MODULE_V2, - ), - "heatshake-module-id": HeaterShakerModuleSubState( - module_id=HeaterShakerModuleId("heatshake-module-id"), - labware_latch_status=HeaterShakerLatchStatus.UNKNOWN, - is_plate_shaking=False, - plate_target_temperature=None, - ), - }, - ) - - module_1_substate = subject.get_magnetic_module_substate( - module_id="magnetic-module-gen1-id" - ) - assert module_1_substate.module_id == "magnetic-module-gen1-id" - assert module_1_substate.model == ModuleModel.MAGNETIC_MODULE_V1 - - module_2_substate = subject.get_magnetic_module_substate( - module_id="magnetic-module-gen2-id" - ) - assert module_2_substate.module_id == "magnetic-module-gen2-id" - assert module_2_substate.model == ModuleModel.MAGNETIC_MODULE_V2 - - with pytest.raises(errors.WrongModuleTypeError): - subject.get_magnetic_module_substate(module_id="heatshake-module-id") - - with pytest.raises(errors.ModuleNotLoadedError): - subject.get_magnetic_module_substate(module_id="nonexistent-module-id") - - -def test_get_heater_shaker_module_substate( - magdeck_v2_def: ModuleDefinition, - heater_shaker_v1_def: ModuleDefinition, -) -> None: - """It should return a heater-shaker module substate.""" - subject = make_module_view( - slot_by_module_id={ - "magnetic-module-gen2-id": DeckSlotName.SLOT_2, - "heatshake-module-id": DeckSlotName.SLOT_3, - }, - hardware_by_module_id={ - "magnetic-module-gen2-id": HardwareModule( - serial_number="magnetic-module-gen2-serial", - definition=magdeck_v2_def, - ), - "heatshake-module-id": HardwareModule( - serial_number="heatshake-module-serial", - definition=heater_shaker_v1_def, - ), - }, - substate_by_module_id={ - "magnetic-module-gen2-id": MagneticModuleSubState( - module_id=MagneticModuleId("magnetic-module-gen2-id"), - model=ModuleModel.MAGNETIC_MODULE_V2, - ), - "heatshake-module-id": HeaterShakerModuleSubState( - module_id=HeaterShakerModuleId("heatshake-module-id"), - plate_target_temperature=432, - labware_latch_status=HeaterShakerLatchStatus.UNKNOWN, - is_plate_shaking=True, - ), - }, - ) - - hs_substate = subject.get_heater_shaker_module_substate( - module_id="heatshake-module-id" - ) - assert hs_substate.module_id == "heatshake-module-id" - assert hs_substate.plate_target_temperature == 432 - assert hs_substate.is_plate_shaking is True - assert hs_substate.labware_latch_status == HeaterShakerLatchStatus.UNKNOWN - - with pytest.raises(errors.WrongModuleTypeError): - subject.get_heater_shaker_module_substate(module_id="magnetic-module-gen2-id") - - with pytest.raises(errors.ModuleNotLoadedError): - subject.get_heater_shaker_module_substate(module_id="nonexistent-module-id") - - -def test_get_temperature_module_substate( - tempdeck_v1_def: ModuleDefinition, - tempdeck_v2_def: ModuleDefinition, - heater_shaker_v1_def: ModuleDefinition, -) -> None: - """It should return a substate for the given Temperature Module, if valid.""" - subject = make_module_view( - slot_by_module_id={ - "temp-module-gen1-id": DeckSlotName.SLOT_1, - "temp-module-gen2-id": DeckSlotName.SLOT_2, - "heatshake-module-id": DeckSlotName.SLOT_3, - }, - hardware_by_module_id={ - "temp-module-gen1-id": HardwareModule( - serial_number="temp-module-gen1-serial", - definition=tempdeck_v1_def, - ), - "temp-module-gen2-id": HardwareModule( - serial_number="temp-module-gen2-serial", - definition=tempdeck_v2_def, - ), - "heatshake-module-id": HardwareModule( - serial_number="heatshake-module-serial", - definition=heater_shaker_v1_def, - ), - }, - substate_by_module_id={ - "temp-module-gen1-id": TemperatureModuleSubState( - module_id=TemperatureModuleId("temp-module-gen1-id"), - plate_target_temperature=None, - ), - "temp-module-gen2-id": TemperatureModuleSubState( - module_id=TemperatureModuleId("temp-module-gen2-id"), - plate_target_temperature=123, - ), - "heatshake-module-id": HeaterShakerModuleSubState( - module_id=HeaterShakerModuleId("heatshake-module-id"), - labware_latch_status=HeaterShakerLatchStatus.UNKNOWN, - is_plate_shaking=False, - plate_target_temperature=None, - ), - }, - ) - - module_1_substate = subject.get_temperature_module_substate( - module_id="temp-module-gen1-id" - ) - assert module_1_substate.module_id == "temp-module-gen1-id" - assert module_1_substate.plate_target_temperature is None - - module_2_substate = subject.get_temperature_module_substate( - module_id="temp-module-gen2-id" - ) - assert module_2_substate.module_id == "temp-module-gen2-id" - assert module_2_substate.plate_target_temperature == 123 - - with pytest.raises(errors.WrongModuleTypeError): - subject.get_temperature_module_substate(module_id="heatshake-module-id") - - with pytest.raises(errors.ModuleNotLoadedError): - subject.get_temperature_module_substate(module_id="nonexistent-module-id") - - -def test_get_plate_target_temperature(heater_shaker_v1_def: ModuleDefinition) -> None: - """It should return whether target temperature is set.""" - module_view = make_module_view( - slot_by_module_id={"module-id": DeckSlotName.SLOT_1}, - hardware_by_module_id={ - "module-id": HardwareModule( - serial_number="serial-number", - definition=heater_shaker_v1_def, - ) - }, - substate_by_module_id={ - "module-id": HeaterShakerModuleSubState( - module_id=HeaterShakerModuleId("module-id"), - labware_latch_status=HeaterShakerLatchStatus.UNKNOWN, - is_plate_shaking=False, - plate_target_temperature=12.3, - ) - }, - ) - subject = module_view.get_heater_shaker_module_substate("module-id") - assert subject.get_plate_target_temperature() == 12.3 - - -def test_get_plate_target_temperature_no_target( - heater_shaker_v1_def: ModuleDefinition, -) -> None: - """It should raise if no target temperature is set.""" - module_view = make_module_view( - slot_by_module_id={"module-id": DeckSlotName.SLOT_1}, - hardware_by_module_id={ - "module-id": HardwareModule( - serial_number="serial-number", - definition=heater_shaker_v1_def, - ) - }, - substate_by_module_id={ - "module-id": HeaterShakerModuleSubState( - module_id=HeaterShakerModuleId("module-id"), - labware_latch_status=HeaterShakerLatchStatus.UNKNOWN, - is_plate_shaking=False, - plate_target_temperature=None, - ) - }, - ) - subject = module_view.get_heater_shaker_module_substate("module-id") - - with pytest.raises(errors.NoTargetTemperatureSetError): - subject.get_plate_target_temperature() - - -def test_get_magnet_home_to_base_offset() -> None: - """It should return the model-specific offset to bottom.""" - subject = make_module_view() - assert ( - subject.get_magnet_home_to_base_offset( - module_model=ModuleModel.MAGNETIC_MODULE_V1 - ) - == 2.5 - ) - assert ( - subject.get_magnet_home_to_base_offset( - module_model=ModuleModel.MAGNETIC_MODULE_V2 - ) - == 2.5 - ) - - -@pytest.mark.parametrize( - "module_model", [ModuleModel.MAGNETIC_MODULE_V1, ModuleModel.MAGNETIC_MODULE_V2] -) -def test_calculate_magnet_height(module_model: ModuleModel) -> None: - """It should use true millimeters as hardware units.""" - subject = make_module_view() - - assert ( - subject.calculate_magnet_height( - module_model=module_model, - height_from_base=100, - ) - == 100 - ) - - # todo(mm, 2022-02-28): - # It's unclear whether this expected result should actually be the same - # between GEN1 and GEN2. - # The GEN1 homing backoff distance looks accidentally halved, for the same reason - # that its heights are halved. If the limit switch hardware is the same for both - # modules, we'd expect the backoff difference to cause a difference in the - # height_from_home test, even though we're measuring everything in true mm. - # https://github.com/Opentrons/opentrons/issues/9585 - assert ( - subject.calculate_magnet_height( - module_model=module_model, - height_from_home=100, - ) - == 97.5 - ) - - assert ( - subject.calculate_magnet_height( - module_model=module_model, - labware_default_height=100, - offset_from_labware_default=10.0, - ) - == 110 - ) - - -@pytest.mark.parametrize( - argnames=["from_slot", "to_slot", "should_dodge"], - argvalues=[ - (DeckSlotName.SLOT_1, DeckSlotName.FIXED_TRASH, True), - (DeckSlotName.FIXED_TRASH, DeckSlotName.SLOT_1, True), - (DeckSlotName.SLOT_4, DeckSlotName.FIXED_TRASH, True), - (DeckSlotName.FIXED_TRASH, DeckSlotName.SLOT_4, True), - (DeckSlotName.SLOT_4, DeckSlotName.SLOT_9, True), - (DeckSlotName.SLOT_9, DeckSlotName.SLOT_4, True), - (DeckSlotName.SLOT_4, DeckSlotName.SLOT_8, True), - (DeckSlotName.SLOT_8, DeckSlotName.SLOT_4, True), - (DeckSlotName.SLOT_1, DeckSlotName.SLOT_8, True), - (DeckSlotName.SLOT_8, DeckSlotName.SLOT_1, True), - (DeckSlotName.SLOT_4, DeckSlotName.SLOT_11, True), - (DeckSlotName.SLOT_11, DeckSlotName.SLOT_4, True), - (DeckSlotName.SLOT_1, DeckSlotName.SLOT_11, True), - (DeckSlotName.SLOT_11, DeckSlotName.SLOT_1, True), - (DeckSlotName.SLOT_2, DeckSlotName.SLOT_4, False), - ], -) -def test_thermocycler_dodging_by_slots( - thermocycler_v1_def: ModuleDefinition, - from_slot: DeckSlotName, - to_slot: DeckSlotName, - should_dodge: bool, -) -> None: - """It should specify if thermocycler dodging is needed. - - It should return True if thermocycler exists and movement is between bad pairs of - slot locations. - """ - subject = make_module_view( - slot_by_module_id={"module-id": DeckSlotName.SLOT_1}, - hardware_by_module_id={ - "module-id": HardwareModule( - serial_number="serial-number", - definition=thermocycler_v1_def, - ) - }, - ) - - assert ( - subject.should_dodge_thermocycler(from_slot=from_slot, to_slot=to_slot) - is should_dodge - ) - - -@pytest.mark.parametrize( - argnames=["from_slot", "to_slot"], - argvalues=[ - (DeckSlotName.SLOT_8, DeckSlotName.SLOT_1), - (DeckSlotName.SLOT_B2, DeckSlotName.SLOT_D1), - ], -) -@pytest.mark.parametrize( - argnames=["module_definition", "should_dodge"], - argvalues=[ - (lazy_fixture("tempdeck_v1_def"), False), - (lazy_fixture("tempdeck_v2_def"), False), - (lazy_fixture("magdeck_v1_def"), False), - (lazy_fixture("magdeck_v2_def"), False), - (lazy_fixture("thermocycler_v1_def"), True), - (lazy_fixture("thermocycler_v2_def"), True), - (lazy_fixture("heater_shaker_v1_def"), False), - ], -) -def test_thermocycler_dodging_by_modules( - from_slot: DeckSlotName, - to_slot: DeckSlotName, - module_definition: ModuleDefinition, - should_dodge: bool, -) -> None: - """It should specify if thermocycler dodging is needed if there is a thermocycler module.""" - subject = make_module_view( - slot_by_module_id={"module-id": DeckSlotName.SLOT_1}, - hardware_by_module_id={ - "module-id": HardwareModule( - serial_number="serial-number", - definition=module_definition, - ) - }, - ) - assert ( - subject.should_dodge_thermocycler(from_slot=from_slot, to_slot=to_slot) - is should_dodge - ) - - -def test_select_hardware_module_to_load_rejects_missing() -> None: - """It should raise if the correct module isn't attached.""" - subject = make_module_view() - - with pytest.raises(errors.ModuleNotAttachedError): - subject.select_hardware_module_to_load( - model=ModuleModel.TEMPERATURE_MODULE_V1, - location=DeckSlotLocation(slotName=DeckSlotName.SLOT_1), - attached_modules=[], - ) - - -@pytest.mark.parametrize( - argnames=["requested_model", "attached_definition"], - argvalues=[ - (ModuleModel.TEMPERATURE_MODULE_V1, lazy_fixture("tempdeck_v1_def")), - (ModuleModel.TEMPERATURE_MODULE_V2, lazy_fixture("tempdeck_v2_def")), - (ModuleModel.TEMPERATURE_MODULE_V1, lazy_fixture("tempdeck_v2_def")), - (ModuleModel.TEMPERATURE_MODULE_V2, lazy_fixture("tempdeck_v1_def")), - (ModuleModel.MAGNETIC_MODULE_V1, lazy_fixture("magdeck_v1_def")), - (ModuleModel.MAGNETIC_MODULE_V2, lazy_fixture("magdeck_v2_def")), - (ModuleModel.THERMOCYCLER_MODULE_V1, lazy_fixture("thermocycler_v1_def")), - (ModuleModel.THERMOCYCLER_MODULE_V2, lazy_fixture("thermocycler_v2_def")), - ], -) -def test_select_hardware_module_to_load( - requested_model: ModuleModel, - attached_definition: ModuleDefinition, -) -> None: - """It should return the first attached module that matches.""" - subject = make_module_view() - - attached_modules = [ - HardwareModule(serial_number="serial-1", definition=attached_definition), - HardwareModule(serial_number="serial-2", definition=attached_definition), - ] - - result = subject.select_hardware_module_to_load( - model=requested_model, - location=DeckSlotLocation(slotName=DeckSlotName.SLOT_1), - attached_modules=attached_modules, - ) - - assert result == attached_modules[0] - - -def test_select_hardware_module_to_load_skips_non_matching( - magdeck_v1_def: ModuleDefinition, - magdeck_v2_def: ModuleDefinition, -) -> None: - """It should skip over non-matching modules.""" - subject = make_module_view() - - attached_modules = [ - HardwareModule(serial_number="serial-1", definition=magdeck_v1_def), - HardwareModule(serial_number="serial-2", definition=magdeck_v2_def), - ] - - result = subject.select_hardware_module_to_load( - model=ModuleModel.MAGNETIC_MODULE_V2, - location=DeckSlotLocation(slotName=DeckSlotName.SLOT_1), - attached_modules=attached_modules, - ) - - assert result == attached_modules[1] - - -def test_select_hardware_module_to_load_skips_already_loaded( - magdeck_v1_def: ModuleDefinition, -) -> None: - """It should skip over already assigned modules.""" - subject = make_module_view( - hardware_by_module_id={ - "module-1": HardwareModule( - serial_number="serial-1", - definition=magdeck_v1_def, - ) - } - ) - - attached_modules = [ - HardwareModule(serial_number="serial-1", definition=magdeck_v1_def), - HardwareModule(serial_number="serial-2", definition=magdeck_v1_def), - ] - - result = subject.select_hardware_module_to_load( - model=ModuleModel.MAGNETIC_MODULE_V1, - location=DeckSlotLocation(slotName=DeckSlotName.SLOT_3), - attached_modules=attached_modules, - ) - - assert result == attached_modules[1] - - -def test_select_hardware_module_to_load_reuses_already_loaded( - magdeck_v1_def: ModuleDefinition, -) -> None: - """It should reuse over already assigned modules in the same location.""" - subject = make_module_view( - slot_by_module_id={ - "module-1": DeckSlotName.SLOT_1, - }, - hardware_by_module_id={ - "module-1": HardwareModule( - serial_number="serial-1", - definition=magdeck_v1_def, - ) - }, - ) - - attached_modules = [ - HardwareModule(serial_number="serial-1", definition=magdeck_v1_def), - HardwareModule(serial_number="serial-2", definition=magdeck_v1_def), - ] - - result = subject.select_hardware_module_to_load( - model=ModuleModel.MAGNETIC_MODULE_V1, - location=DeckSlotLocation(slotName=DeckSlotName.SLOT_1), - attached_modules=attached_modules, - ) - - assert result == attached_modules[0] - - -def test_select_hardware_module_to_load_rejects_location_reassignment( - magdeck_v1_def: ModuleDefinition, - tempdeck_v1_def: ModuleDefinition, -) -> None: - """It should raise if a non-matching module is already present in the slot.""" - subject = make_module_view( - slot_by_module_id={ - "module-1": DeckSlotName.SLOT_1, - }, - hardware_by_module_id={ - "module-1": HardwareModule( - serial_number="serial-1", - definition=magdeck_v1_def, - ) - }, - ) - - attached_modules = [ - HardwareModule(serial_number="serial-1", definition=magdeck_v1_def), - HardwareModule(serial_number="serial-2", definition=tempdeck_v1_def), - ] - - with pytest.raises(errors.ModuleAlreadyPresentError): - subject.select_hardware_module_to_load( - model=ModuleModel.TEMPERATURE_MODULE_V1, - location=DeckSlotLocation(slotName=DeckSlotName.SLOT_1), - attached_modules=attached_modules, - ) - - -class _CalculateMagnetHardwareHeightTestParams(NamedTuple): - definition: ModuleDefinition - mm_from_base: float - expected_result: Optional[float] - expected_exception_type: Union[Type[Exception], None] - - -@pytest.mark.parametrize( - "definition, mm_from_base, expected_result, expected_exception_type", - [ - # Happy cases: - _CalculateMagnetHardwareHeightTestParams( - definition=lazy_fixture("magdeck_v1_def"), - mm_from_base=10, - # TODO(mm, 2022-03-09): It's unclear if this expected result is correct. - # https://github.com/Opentrons/opentrons/issues/9585 - expected_result=25, - expected_exception_type=None, - ), - _CalculateMagnetHardwareHeightTestParams( - definition=lazy_fixture("magdeck_v2_def"), - mm_from_base=10, - expected_result=12.5, - expected_exception_type=None, - ), - # Boundary conditions: - # - # TODO(mm, 2022-03-09): - # In Python >=3.9, improve precision with math.nextafter(). - # Also consider relying on shared constants instead of hard-coding bounds. - # - # TODO(mm, 2022-03-09): It's unclear if the bounds used for V1 modules - # are physically correct. https://github.com/Opentrons/opentrons/issues/9585 - _CalculateMagnetHardwareHeightTestParams( # V1 barely too low. - definition=lazy_fixture("magdeck_v1_def"), - mm_from_base=-2.51, - expected_result=None, - expected_exception_type=errors.EngageHeightOutOfRangeError, - ), - _CalculateMagnetHardwareHeightTestParams( # V1 lowest allowed. - definition=lazy_fixture("magdeck_v1_def"), - mm_from_base=-2.5, - expected_result=0, - expected_exception_type=None, - ), - _CalculateMagnetHardwareHeightTestParams( # V1 highest allowed. - definition=lazy_fixture("magdeck_v1_def"), - mm_from_base=20, - expected_result=45, - expected_exception_type=None, - ), - _CalculateMagnetHardwareHeightTestParams( # V1 barely too high. - definition=lazy_fixture("magdeck_v1_def"), - mm_from_base=20.01, - expected_result=None, - expected_exception_type=errors.EngageHeightOutOfRangeError, - ), - _CalculateMagnetHardwareHeightTestParams( # V2 barely too low. - definition=lazy_fixture("magdeck_v2_def"), - mm_from_base=-2.51, - expected_result=None, - expected_exception_type=errors.EngageHeightOutOfRangeError, - ), - _CalculateMagnetHardwareHeightTestParams( # V2 lowest allowed. - definition=lazy_fixture("magdeck_v2_def"), - mm_from_base=-2.5, - expected_result=0, - expected_exception_type=None, - ), - _CalculateMagnetHardwareHeightTestParams( # V2 highest allowed. - definition=lazy_fixture("magdeck_v2_def"), - mm_from_base=22.5, - expected_result=25, - expected_exception_type=None, - ), - _CalculateMagnetHardwareHeightTestParams( # V2 barely too high. - definition=lazy_fixture("magdeck_v2_def"), - mm_from_base=22.51, - expected_result=None, - expected_exception_type=errors.EngageHeightOutOfRangeError, - ), - ], -) -def test_magnetic_module_view_calculate_magnet_hardware_height( - definition: ModuleDefinition, - mm_from_base: float, - expected_result: float, - expected_exception_type: Union[Type[Exception], None], -) -> None: - """It should return the expected height or raise the expected exception.""" - module_view = make_module_view( - slot_by_module_id={"module-id": DeckSlotName.SLOT_1}, - hardware_by_module_id={ - "module-id": HardwareModule( - serial_number="serial-number", - definition=definition, - ) - }, - substate_by_module_id={ - "module-id": MagneticModuleSubState( - module_id=MagneticModuleId("module-id"), - model=definition.model, # type: ignore [arg-type] - ) - }, - ) - subject = module_view.get_magnetic_module_substate("module-id") - expected_raise: ContextManager[None] = ( - # Not sure why mypy has trouble with this. - does_not_raise() # type: ignore[assignment] - if expected_exception_type is None - else pytest.raises(expected_exception_type) - ) - with expected_raise: - result = subject.calculate_magnet_hardware_height(mm_from_base=mm_from_base) - assert result == expected_result - - -@pytest.mark.parametrize("target_temp", [36.8, 95.1]) -def test_validate_heater_shaker_target_temperature_raises( - heater_shaker_v1_def: ModuleDefinition, - target_temp: float, -) -> None: - """It should verify if a target temperature is valid for the specified module.""" - module_view = make_module_view( - slot_by_module_id={"module-id": DeckSlotName.SLOT_1}, - hardware_by_module_id={ - "module-id": HardwareModule( - serial_number="serial-number", - definition=heater_shaker_v1_def, - ) - }, - substate_by_module_id={ - "module-id": HeaterShakerModuleSubState( - module_id=HeaterShakerModuleId("module-id"), - labware_latch_status=HeaterShakerLatchStatus.UNKNOWN, - is_plate_shaking=False, - plate_target_temperature=None, - ) - }, - ) - subject = module_view.get_heater_shaker_module_substate("module-id") - with pytest.raises(errors.InvalidTargetTemperatureError): - subject.validate_target_temperature(target_temp) - - -@pytest.mark.parametrize("target_temp", [37, 94.8]) -def test_validate_heater_shaker_target_temperature( - heater_shaker_v1_def: ModuleDefinition, - target_temp: float, -) -> None: - """It should verify if a target temperature is valid for the specified module.""" - module_view = make_module_view( - slot_by_module_id={"module-id": DeckSlotName.SLOT_1}, - hardware_by_module_id={ - "module-id": HardwareModule( - serial_number="serial-number", - definition=heater_shaker_v1_def, - ) - }, - substate_by_module_id={ - "module-id": HeaterShakerModuleSubState( - module_id=HeaterShakerModuleId("module-id"), - labware_latch_status=HeaterShakerLatchStatus.UNKNOWN, - is_plate_shaking=False, - plate_target_temperature=None, - ) - }, - ) - subject = module_view.get_heater_shaker_module_substate("module-id") - assert subject.validate_target_temperature(target_temp) == target_temp - - -@pytest.mark.parametrize("target_temp", [-10, 99.9]) -def test_validate_temp_module_target_temperature_raises( - tempdeck_v1_def: ModuleDefinition, - target_temp: float, -) -> None: - """It should verify if a target temperature is valid for the specified module.""" - module_view = make_module_view( - slot_by_module_id={"module-id": DeckSlotName.SLOT_1}, - hardware_by_module_id={ - "module-id": HardwareModule( - serial_number="serial-number", - definition=tempdeck_v1_def, - ) - }, - substate_by_module_id={ - "module-id": TemperatureModuleSubState( - module_id=TemperatureModuleId("module-id"), - plate_target_temperature=None, - ) - }, - ) - subject = module_view.get_temperature_module_substate("module-id") - with pytest.raises(errors.InvalidTargetTemperatureError): - subject.validate_target_temperature(target_temp) - - -@pytest.mark.parametrize( - ["target_temp", "validated_temp"], [(-9.431, -9), (0, 0), (99.1, 99)] -) -def test_validate_temp_module_target_temperature( - tempdeck_v2_def: ModuleDefinition, target_temp: float, validated_temp: int -) -> None: - """It should verify if a target temperature is valid for the specified module.""" - module_view = make_module_view( - slot_by_module_id={"module-id": DeckSlotName.SLOT_1}, - hardware_by_module_id={ - "module-id": HardwareModule( - serial_number="serial-number", - definition=tempdeck_v2_def, - ) - }, - substate_by_module_id={ - "module-id": TemperatureModuleSubState( - module_id=TemperatureModuleId("module-id"), - plate_target_temperature=None, - ) - }, - ) - subject = module_view.get_temperature_module_substate("module-id") - assert subject.validate_target_temperature(target_temp) == validated_temp - - -@pytest.mark.parametrize( - argnames=["rpm_param", "validated_param"], - argvalues=[(200.1, 200), (250.6, 251), (300.9, 301)], -) -def test_validate_heater_shaker_target_speed_converts_to_int( - rpm_param: float, validated_param: bool, heater_shaker_v1_def: ModuleDefinition -) -> None: - """It should validate heater-shaker target rpm.""" - module_view = make_module_view( - slot_by_module_id={"module-id": DeckSlotName.SLOT_1}, - hardware_by_module_id={ - "module-id": HardwareModule( - serial_number="serial-number", - definition=heater_shaker_v1_def, - ) - }, - substate_by_module_id={ - "module-id": HeaterShakerModuleSubState( - module_id=HeaterShakerModuleId("module-id"), - labware_latch_status=HeaterShakerLatchStatus.UNKNOWN, - is_plate_shaking=False, - plate_target_temperature=None, - ) - }, - ) - subject = module_view.get_heater_shaker_module_substate("module-id") - assert subject.validate_target_speed(rpm_param) == validated_param - - -@pytest.mark.parametrize( - argnames=["rpm_param", "expected_valid"], - argvalues=[(199.4, False), (199.5, True), (3000.7, False), (3000.4, True)], -) -def test_validate_heater_shaker_target_speed_raises_error( - rpm_param: float, expected_valid: bool, heater_shaker_v1_def: ModuleDefinition -) -> None: - """It should validate heater-shaker target rpm.""" - module_view = make_module_view( - slot_by_module_id={"module-id": DeckSlotName.SLOT_1}, - hardware_by_module_id={ - "module-id": HardwareModule( - serial_number="serial-number", - definition=heater_shaker_v1_def, - ) - }, - substate_by_module_id={ - "module-id": HeaterShakerModuleSubState( - module_id=HeaterShakerModuleId("module-id"), - labware_latch_status=HeaterShakerLatchStatus.UNKNOWN, - is_plate_shaking=False, - plate_target_temperature=None, - ) - }, - ) - subject = module_view.get_heater_shaker_module_substate("module-id") - if not expected_valid: - with pytest.raises(errors.InvalidTargetSpeedError): - subject.validate_target_speed(rpm_param) - - -def test_raise_if_labware_latch_not_closed( - heater_shaker_v1_def: ModuleDefinition, -) -> None: - """It should raise an error if labware latch is not closed.""" - module_view = make_module_view( - slot_by_module_id={"module-id": DeckSlotName.SLOT_1}, - hardware_by_module_id={ - "module-id": HardwareModule( - serial_number="serial-number", - definition=heater_shaker_v1_def, - ) - }, - substate_by_module_id={ - "module-id": HeaterShakerModuleSubState( - module_id=HeaterShakerModuleId("module-id"), - labware_latch_status=HeaterShakerLatchStatus.OPEN, - is_plate_shaking=False, - plate_target_temperature=None, - ) - }, - ) - subject = module_view.get_heater_shaker_module_substate("module-id") - with pytest.raises(errors.CannotPerformModuleAction, match="is open"): - subject.raise_if_labware_latch_not_closed() - - -def test_raise_if_labware_latch_unknown( - heater_shaker_v1_def: ModuleDefinition, -) -> None: - """It should raise an error if labware latch is not closed.""" - module_view = make_module_view( - slot_by_module_id={"module-id": DeckSlotName.SLOT_1}, - hardware_by_module_id={ - "module-id": HardwareModule( - serial_number="serial-number", - definition=heater_shaker_v1_def, - ) - }, - substate_by_module_id={ - "module-id": HeaterShakerModuleSubState( - module_id=HeaterShakerModuleId("module-id"), - labware_latch_status=HeaterShakerLatchStatus.UNKNOWN, - is_plate_shaking=False, - plate_target_temperature=None, - ) - }, - ) - subject = module_view.get_heater_shaker_module_substate("module-id") - with pytest.raises(errors.CannotPerformModuleAction, match="set to closed"): - subject.raise_if_labware_latch_not_closed() - - -def test_heater_shaker_raise_if_shaking( - heater_shaker_v1_def: ModuleDefinition, -) -> None: - """It should raise when heater-shaker is shaking.""" - module_view = make_module_view( - slot_by_module_id={"module-id": DeckSlotName.SLOT_1}, - hardware_by_module_id={ - "module-id": HardwareModule( - serial_number="serial-number", - definition=heater_shaker_v1_def, - ) - }, - substate_by_module_id={ - "module-id": HeaterShakerModuleSubState( - module_id=HeaterShakerModuleId("module-id"), - labware_latch_status=HeaterShakerLatchStatus.UNKNOWN, - is_plate_shaking=True, - plate_target_temperature=None, - ) - }, - ) - subject = module_view.get_heater_shaker_module_substate("module-id") - with pytest.raises(errors.CannotPerformModuleAction): - subject.raise_if_shaking() - - -def test_get_heater_shaker_movement_data( - heater_shaker_v1_def: ModuleDefinition, - tempdeck_v2_def: ModuleDefinition, -) -> None: - """It should get heater-shaker movement data.""" - module_view = make_module_view( - slot_by_module_id={ - "module-id": DeckSlotName.SLOT_1, - "other-module-id": DeckSlotName.SLOT_5, - }, - hardware_by_module_id={ - "module-id": HardwareModule( - serial_number="serial-number", - definition=heater_shaker_v1_def, - ), - "other-module-id": HardwareModule( - serial_number="other-serial-number", - definition=tempdeck_v2_def, - ), - }, - substate_by_module_id={ - "module-id": HeaterShakerModuleSubState( - module_id=HeaterShakerModuleId("module-id"), - labware_latch_status=HeaterShakerLatchStatus.CLOSED, - is_plate_shaking=False, - plate_target_temperature=None, - ), - "other-module-id": TemperatureModuleSubState( - module_id=TemperatureModuleId("other-module-id"), - plate_target_temperature=None, - ), - }, - ) - subject = module_view.get_heater_shaker_movement_restrictors() - assert len(subject) == 1 - for hs_movement_data in subject: - assert not hs_movement_data.plate_shaking - assert hs_movement_data.latch_status - assert hs_movement_data.deck_slot == 1 - - -def test_tempdeck_get_plate_target_temperature( - tempdeck_v2_def: ModuleDefinition, -) -> None: - """It should return whether target temperature is set.""" - module_view = make_module_view( - slot_by_module_id={"module-id": DeckSlotName.SLOT_1}, - hardware_by_module_id={ - "module-id": HardwareModule( - serial_number="serial-number", - definition=tempdeck_v2_def, - ) - }, - substate_by_module_id={ - "module-id": TemperatureModuleSubState( - module_id=TemperatureModuleId("module-id"), - plate_target_temperature=12, - ) - }, - ) - subject = module_view.get_temperature_module_substate("module-id") - assert subject.get_plate_target_temperature() == 12 - - -def test_tempdeck_get_plate_target_temperature_no_target( - tempdeck_v2_def: ModuleDefinition, -) -> None: - """It should raise if no target temperature is set.""" - module_view = make_module_view( - slot_by_module_id={"module-id": DeckSlotName.SLOT_1}, - hardware_by_module_id={ - "module-id": HardwareModule( - serial_number="serial-number", - definition=tempdeck_v2_def, - ) - }, - substate_by_module_id={ - "module-id": TemperatureModuleSubState( - module_id=TemperatureModuleId("module-id"), - plate_target_temperature=None, - ) - }, - ) - subject = module_view.get_temperature_module_substate("module-id") - - with pytest.raises(errors.NoTargetTemperatureSetError): - subject.get_plate_target_temperature() - - -def test_thermocycler_get_target_temperatures( - thermocycler_v1_def: ModuleDefinition, -) -> None: - """It should return whether target temperature for thermocycler is set.""" - module_view = make_module_view( - slot_by_module_id={"module-id": DeckSlotName.SLOT_1}, - hardware_by_module_id={ - "module-id": HardwareModule( - serial_number="serial-number", - definition=thermocycler_v1_def, - ) - }, - substate_by_module_id={ - "module-id": ThermocyclerModuleSubState( - module_id=ThermocyclerModuleId("module-id"), - is_lid_open=False, - target_block_temperature=14, - target_lid_temperature=28, - ) - }, - ) - subject = module_view.get_thermocycler_module_substate("module-id") - assert subject.get_target_block_temperature() == 14 - assert subject.get_target_lid_temperature() == 28 - - -def test_thermocycler_get_target_temperatures_no_target( - thermocycler_v1_def: ModuleDefinition, -) -> None: - """It should raise if no target temperature is set.""" - module_view = make_module_view( - slot_by_module_id={"module-id": DeckSlotName.SLOT_1}, - hardware_by_module_id={ - "module-id": HardwareModule( - serial_number="serial-number", - definition=thermocycler_v1_def, - ) - }, - substate_by_module_id={ - "module-id": ThermocyclerModuleSubState( - module_id=ThermocyclerModuleId("module-id"), - is_lid_open=False, - target_block_temperature=None, - target_lid_temperature=None, - ) - }, - ) - subject = module_view.get_thermocycler_module_substate("module-id") - - with pytest.raises(errors.NoTargetTemperatureSetError): - subject.get_target_block_temperature() - subject.get_target_lid_temperature() - - -@pytest.fixture -def module_view_with_thermocycler(thermocycler_v1_def: ModuleDefinition) -> ModuleView: - """Get a module state view with a loaded thermocycler.""" - return make_module_view( - slot_by_module_id={"module-id": DeckSlotName.SLOT_1}, - hardware_by_module_id={ - "module-id": HardwareModule( - serial_number="serial-number", - definition=thermocycler_v1_def, - ) - }, - substate_by_module_id={ - "module-id": ThermocyclerModuleSubState( - module_id=ThermocyclerModuleId("module-id"), - target_block_temperature=None, - target_lid_temperature=None, - is_lid_open=False, - ) - }, - ) - - -@pytest.mark.parametrize("input_temperature", [0, 0.0, 0.001, 98.999, 99, 99.0]) -def test_thermocycler_validate_target_block_temperature( - module_view_with_thermocycler: ModuleView, - input_temperature: float, -) -> None: - """It should return a valid target block temperature.""" - subject = module_view_with_thermocycler.get_thermocycler_module_substate( - "module-id" - ) - result = subject.validate_target_block_temperature(input_temperature) - - assert result == input_temperature - - -@pytest.mark.parametrize( - argnames=["input_time", "validated_time"], - argvalues=[(0.0, 0.0), (0.123, 0.123), (123.456, 123.456), (1234567, 1234567)], -) -def test_thermocycler_validate_hold_time( - module_view_with_thermocycler: ModuleView, - input_time: float, - validated_time: float, -) -> None: - """It should return a valid hold time.""" - subject = module_view_with_thermocycler.get_thermocycler_module_substate( - "module-id" - ) - result = subject.validate_hold_time(input_time) - - assert result == validated_time - - -@pytest.mark.parametrize("input_time", [-0.1, -123]) -def test_thermocycler_validate_hold_time_raises( - module_view_with_thermocycler: ModuleView, - input_time: float, -) -> None: - """It should raise on invalid hold time.""" - subject = module_view_with_thermocycler.get_thermocycler_module_substate( - "module-id" - ) - - with pytest.raises(errors.InvalidHoldTimeError): - subject.validate_hold_time(input_time) - - -@pytest.mark.parametrize("input_temperature", [-0.001, 99.001]) -def test_thermocycler_validate_target_block_temperature_raises( - module_view_with_thermocycler: ModuleView, - input_temperature: float, -) -> None: - """It should raise on invalid target block temperature.""" - subject = module_view_with_thermocycler.get_thermocycler_module_substate( - "module-id" - ) - - with pytest.raises(errors.InvalidTargetTemperatureError): - subject.validate_target_block_temperature(input_temperature) - - -@pytest.mark.parametrize("input_volume", [0, 0.0, 0.001, 50.0, 99.999, 100, 100.0]) -def test_thermocycler_validate_block_max_volume( - module_view_with_thermocycler: ModuleView, - input_volume: float, -) -> None: - """It should return a validated max block volume value.""" - subject = module_view_with_thermocycler.get_thermocycler_module_substate( - "module-id" - ) - result = subject.validate_max_block_volume(input_volume) - - assert result == input_volume - - -@pytest.mark.parametrize("input_volume", [-10, -0.001, 100.001]) -def test_thermocycler_validate_block_max_volume_raises( - module_view_with_thermocycler: ModuleView, - input_volume: float, -) -> None: - """It should raise on invalid block volume temperature.""" - subject = module_view_with_thermocycler.get_thermocycler_module_substate( - "module-id" - ) - - with pytest.raises(errors.InvalidBlockVolumeError): - subject.validate_max_block_volume(input_volume) - - -@pytest.mark.parametrize("input_temperature", [37, 37.0, 37.001, 109.999, 110, 110.0]) -def test_thermocycler_validate_target_lid_temperature( - module_view_with_thermocycler: ModuleView, - input_temperature: float, -) -> None: - """It should return a valid target block temperature.""" - subject = module_view_with_thermocycler.get_thermocycler_module_substate( - "module-id" - ) - result = subject.validate_target_lid_temperature(input_temperature) - - assert result == input_temperature - - -@pytest.mark.parametrize("input_temperature", [36.999, 110.001]) -def test_thermocycler_validate_target_lid_temperature_raises( - module_view_with_thermocycler: ModuleView, - input_temperature: float, -) -> None: - """It should raise on invalid target block temperature.""" - subject = module_view_with_thermocycler.get_thermocycler_module_substate( - "module-id" - ) - - with pytest.raises(errors.InvalidTargetTemperatureError): - subject.validate_target_lid_temperature(input_temperature) - - -@pytest.mark.parametrize( - ("module_definition", "expected_height"), - [ - (lazy_fixture("thermocycler_v1_def"), 98.0), - (lazy_fixture("tempdeck_v1_def"), 84.0), - (lazy_fixture("tempdeck_v2_def"), 84.0), - (lazy_fixture("magdeck_v1_def"), 110.152), - (lazy_fixture("magdeck_v2_def"), 110.152), - (lazy_fixture("heater_shaker_v1_def"), 82.0), - ], -) -def test_get_overall_height( - module_definition: ModuleDefinition, - expected_height: float, -) -> None: - """It should get a module's overall height.""" - subject = make_module_view( - slot_by_module_id={"module-id": DeckSlotName.SLOT_7}, - hardware_by_module_id={ - "module-id": HardwareModule( - serial_number="serial-number", - definition=module_definition, - ) - }, - ) - - result = subject.get_overall_height("module-id") - assert result == expected_height - - -@pytest.mark.parametrize( - argnames=["location", "expected_raise"], - argvalues=[ - ( - DeckSlotLocation(slotName=DeckSlotName.SLOT_1), - pytest.raises(errors.LocationIsOccupiedError), - ), - (DeckSlotLocation(slotName=DeckSlotName.SLOT_2), does_not_raise()), - (DeckSlotLocation(slotName=DeckSlotName.FIXED_TRASH), does_not_raise()), - ], -) -def test_raise_if_labware_in_location( - location: DeckSlotLocation, - expected_raise: ContextManager[Any], - thermocycler_v1_def: ModuleDefinition, -) -> None: - """It should raise if there is module in specified location.""" - subject = make_module_view( - slot_by_module_id={"module-id-1": DeckSlotName.SLOT_1}, - hardware_by_module_id={ - "module-id-1": HardwareModule( - serial_number="serial-number", - definition=thermocycler_v1_def, - ) - }, - substate_by_module_id={ - "module-id-1": ThermocyclerModuleSubState( - module_id=ThermocyclerModuleId("module-id-1"), - is_lid_open=False, - target_block_temperature=None, - target_lid_temperature=None, - ) - }, - ) - with expected_raise: - subject.raise_if_module_in_location(location=location) - - -def test_get_by_slot() -> None: - """It should get the module in a given slot.""" - subject = make_module_view( - slot_by_module_id={ - "1": DeckSlotName.SLOT_1, - "2": DeckSlotName.SLOT_2, - }, - hardware_by_module_id={ - "1": HardwareModule( - serial_number="serial-number-1", - definition=ModuleDefinition.construct( # type: ignore[call-arg] - model=ModuleModel.TEMPERATURE_MODULE_V1 - ), - ), - "2": HardwareModule( - serial_number="serial-number-2", - definition=ModuleDefinition.construct( # type: ignore[call-arg] - model=ModuleModel.TEMPERATURE_MODULE_V2 - ), - ), - }, - ) - - assert subject.get_by_slot(DeckSlotName.SLOT_1) == LoadedModule( - id="1", - location=DeckSlotLocation(slotName=DeckSlotName.SLOT_1), - model=ModuleModel.TEMPERATURE_MODULE_V1, - serialNumber="serial-number-1", - ) - assert subject.get_by_slot(DeckSlotName.SLOT_2) == LoadedModule( - id="2", - location=DeckSlotLocation(slotName=DeckSlotName.SLOT_2), - model=ModuleModel.TEMPERATURE_MODULE_V2, - serialNumber="serial-number-2", - ) - assert subject.get_by_slot(DeckSlotName.SLOT_3) is None - - -def test_get_by_slot_prefers_later() -> None: - """It should get the module in a slot, preferring later items if locations match.""" - subject = make_module_view( - slot_by_module_id={ - "1": DeckSlotName.SLOT_1, - "1-again": DeckSlotName.SLOT_1, - }, - hardware_by_module_id={ - "1": HardwareModule( - serial_number="serial-number-1", - definition=ModuleDefinition.construct( # type: ignore[call-arg] - model=ModuleModel.TEMPERATURE_MODULE_V1 - ), - ), - "1-again": HardwareModule( - serial_number="serial-number-1-again", - definition=ModuleDefinition.construct( # type: ignore[call-arg] - model=ModuleModel.TEMPERATURE_MODULE_V1 - ), - ), - }, - ) - - assert subject.get_by_slot(DeckSlotName.SLOT_1) == LoadedModule( - id="1-again", - location=DeckSlotLocation(slotName=DeckSlotName.SLOT_1), - model=ModuleModel.TEMPERATURE_MODULE_V1, - serialNumber="serial-number-1-again", - ) - - -@pytest.mark.parametrize( - argnames=["mount", "target_slot", "expected_result"], - argvalues=[ - (MountType.RIGHT, DeckSlotName.SLOT_1, False), - (MountType.RIGHT, DeckSlotName.SLOT_2, True), - (MountType.RIGHT, DeckSlotName.SLOT_5, False), - (MountType.LEFT, DeckSlotName.SLOT_3, False), - (MountType.RIGHT, DeckSlotName.SLOT_5, False), - (MountType.LEFT, DeckSlotName.SLOT_8, True), - ], -) -def test_is_edge_move_unsafe( - mount: MountType, target_slot: DeckSlotName, expected_result: bool -) -> None: - """It should determine if an edge move would be unsafe.""" - subject = make_module_view( - slot_by_module_id={"foo": DeckSlotName.SLOT_1, "bar": DeckSlotName.SLOT_9} - ) - - result = subject.is_edge_move_unsafe(mount=mount, target_slot=target_slot) - - assert result is expected_result - - -@pytest.mark.parametrize( - argnames=["module_def", "expected_offset_data"], - argvalues=[ - ( - lazy_fixture("thermocycler_v2_def"), - LabwareMovementOffsetData( - pickUpOffset=LabwareOffsetVector(x=0, y=0, z=4.6), - dropOffset=LabwareOffsetVector(x=0, y=0, z=5.6), - ), - ), - ( - lazy_fixture("heater_shaker_v1_def"), - LabwareMovementOffsetData( - pickUpOffset=LabwareOffsetVector(x=0, y=0, z=0), - dropOffset=LabwareOffsetVector(x=0, y=0, z=1.0), - ), - ), - ( - lazy_fixture("tempdeck_v1_def"), - None, - ), - ], -) -def test_get_default_gripper_offsets( - module_def: ModuleDefinition, - expected_offset_data: Optional[LabwareMovementOffsetData], -) -> None: - """It should return the correct gripper offsets, if present.""" - subject = make_module_view( - slot_by_module_id={ - "module-1": DeckSlotName.SLOT_1, - }, - requested_model_by_module_id={ - "module-1": ModuleModel.TEMPERATURE_MODULE_V1, # Does not matter - }, - hardware_by_module_id={ - "module-1": HardwareModule( - serial_number="serial-1", - definition=module_def, - ), - }, - ) - assert subject.get_default_gripper_offsets("module-1") == expected_offset_data - - -@pytest.mark.parametrize( - argnames=["deck_type", "slot_name", "expected_highest_z", "deck_definition"], - argvalues=[ - ( - 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( - tempdeck_v2_def: ModuleDefinition, - 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( - deck_type=deck_type, - slot_by_module_id={"module-id": slot_name}, - requested_model_by_module_id={ - "module-id": ModuleModel.TEMPERATURE_MODULE_V2, - }, - hardware_by_module_id={ - "module-id": HardwareModule( - serial_number="module-serial", - definition=tempdeck_v2_def, - ) - }, - ) - assert isclose( - 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, - ) - - -def test_get_overflowed_module_in_slot(tempdeck_v1_def: ModuleDefinition) -> None: - """It should return the module occupying but not loaded in the given slot.""" - subject = make_module_view( - slot_by_module_id={"module-id": DeckSlotName.SLOT_1}, - hardware_by_module_id={ - "module-id": HardwareModule( - serial_number="serial-number", - definition=tempdeck_v1_def, - ) - }, - additional_slots_occupied_by_module_id={ - "module-id": [DeckSlotName.SLOT_6, DeckSlotName.SLOT_A1], - }, - ) - assert subject.get_overflowed_module_in_slot(DeckSlotName.SLOT_6) == LoadedModule( - id="module-id", - model=ModuleModel.TEMPERATURE_MODULE_V1, - location=DeckSlotLocation(slotName=DeckSlotName.SLOT_1), - serialNumber="serial-number", - ) - - -@pytest.mark.parametrize( - argnames=["deck_type", "module_def", "module_slot", "expected_result"], - argvalues=[ - ( - DeckType.OT3_STANDARD, - lazy_fixture("thermocycler_v2_def"), - DeckSlotName.SLOT_A1, - True, - ), - ( - DeckType.OT3_STANDARD, - lazy_fixture("tempdeck_v1_def"), - DeckSlotName.SLOT_A1, - False, - ), - ( - DeckType.OT3_STANDARD, - lazy_fixture("thermocycler_v2_def"), - DeckSlotName.SLOT_1, - False, - ), - ( - DeckType.OT2_STANDARD, - lazy_fixture("thermocycler_v2_def"), - DeckSlotName.SLOT_A1, - False, - ), - ], -) -def test_is_flex_deck_with_thermocycler( - deck_type: DeckType, - module_def: ModuleDefinition, - module_slot: DeckSlotName, - expected_result: bool, -) -> None: - """It should return True if there is a thermocycler on Flex.""" - subject = make_module_view( - slot_by_module_id={"module-id": DeckSlotName.SLOT_B1}, - hardware_by_module_id={ - "module-id": HardwareModule( - serial_number="serial-number", - definition=module_def, - ) - }, - additional_slots_occupied_by_module_id={ - "module-id": [module_slot, DeckSlotName.SLOT_C1], - }, - deck_type=deck_type, - ) - assert subject.is_flex_deck_with_thermocycler() == expected_result diff --git a/api/tests/opentrons/protocol_engine/state/test_module_view_old.py b/api/tests/opentrons/protocol_engine/state/test_module_view_old.py new file mode 100644 index 00000000000..65e1a467977 --- /dev/null +++ b/api/tests/opentrons/protocol_engine/state/test_module_view_old.py @@ -0,0 +1,1988 @@ +"""Tests for module state accessors in the protocol engine state store. + +DEPRECATED: Testing ModuleView independently of ModuleView is no longer helpful. +Try to add new tests to test_module_state.py, where they can be tested together, +treating ModuleState as a private implementation detail. +""" +import pytest +from math import isclose +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, + Set, + cast, +) + +from opentrons_shared_data.robot.types import RobotType +from opentrons_shared_data.deck.types import DeckDefinitionV5 + +from opentrons_shared_data import load_shared_data +from opentrons.types import DeckSlotName, MountType +from opentrons.protocol_engine import errors +from opentrons.protocol_engine.types import ( + LoadedModule, + DeckSlotLocation, + ModuleDefinition, + ModuleModel, + LabwareOffsetVector, + DeckType, + 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, + HeaterShakerModuleId, + MagneticModuleSubState, + MagneticModuleId, + TemperatureModuleSubState, + TemperatureModuleId, + ThermocyclerModuleSubState, + 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_definition={ + "displayName": "OT-3", + "robotType": "OT-3 Standard", + "models": ["OT-3 Standard"], + "extents": [477.2, 493.8, 0.0], + "paddingOffsets": { + "rear": -177.42, + "front": 51.8, + "leftSide": 31.88, + "rightSide": -80.32, + }, + "mountOffsets": { + "left": [-13.5, -60.5, 255.675], + "right": [40.5, -60.5, 255.675], + "gripper": [84.55, -12.75, 93.85], + }, + }, + robot_type=robot_type, + use_simulated_deck_config=use_simulated_deck_config, + ) + + return AddressableAreaView(state=state) + + +def make_module_view( + deck_type: Optional[DeckType] = None, + slot_by_module_id: Optional[Dict[str, Optional[DeckSlotName]]] = None, + requested_model_by_module_id: Optional[Dict[str, Optional[ModuleModel]]] = None, + hardware_by_module_id: Optional[Dict[str, HardwareModule]] = None, + substate_by_module_id: Optional[Dict[str, ModuleSubStateType]] = None, + module_offset_by_serial: Optional[Dict[str, ModuleOffsetData]] = None, + additional_slots_occupied_by_module_id: Optional[ + Dict[str, List[DeckSlotName]] + ] = None, +) -> ModuleView: + """Get a module view test subject with the specified state.""" + state = ModuleState( + deck_type=deck_type or DeckType.OT2_STANDARD, + slot_by_module_id=slot_by_module_id or {}, + requested_model_by_id=requested_model_by_module_id or {}, + hardware_by_module_id=hardware_by_module_id or {}, + substate_by_module_id=substate_by_module_id or {}, + module_offset_by_serial=module_offset_by_serial or {}, + additional_slots_occupied_by_module_id=additional_slots_occupied_by_module_id + or {}, + deck_fixed_labware=[], + ) + + return ModuleView(state=state) + + +def get_sample_parent_module_view( + matching_module_def: ModuleDefinition, + matching_module_id: str, +) -> ModuleView: + """Get a ModuleView with attached modules including a requested matching module.""" + definition = load_shared_data("module/definitions/2/magneticModuleV1.json") + magdeck_def = ModuleDefinition.model_validate_json(definition) + + return make_module_view( + slot_by_module_id={ + "id-non-matching": DeckSlotName.SLOT_1, + matching_module_id: DeckSlotName.SLOT_2, + "id-another-non-matching": DeckSlotName.SLOT_3, + }, + hardware_by_module_id={ + "id-non-matching": HardwareModule( + serial_number="serial-non-matching", + definition=magdeck_def, + ), + matching_module_id: HardwareModule( + serial_number="serial-matching", + definition=matching_module_def, + ), + "id-another-non-matching": HardwareModule( + serial_number="serial-another-non-matching", + definition=magdeck_def, + ), + }, + ) + + +def test_initial_module_data_by_id() -> None: + """It should raise if module ID doesn't exist.""" + subject = make_module_view() + + with pytest.raises(errors.ModuleNotLoadedError): + subject.get("helloWorld") + + +def test_get_missing_hardware() -> None: + """It should raise if no loaded hardware.""" + subject = make_module_view(slot_by_module_id={"module-id": DeckSlotName.SLOT_1}) + + with pytest.raises(errors.ModuleNotLoadedError): + subject.get("module-id") + + +def test_get_module_data(tempdeck_v1_def: ModuleDefinition) -> None: + """It should get module data from state by ID.""" + subject = make_module_view( + slot_by_module_id={"module-id": DeckSlotName.SLOT_1}, + hardware_by_module_id={ + "module-id": HardwareModule( + serial_number="serial-number", + definition=tempdeck_v1_def, + ) + }, + ) + + assert subject.get("module-id") == LoadedModule( + id="module-id", + model=ModuleModel.TEMPERATURE_MODULE_V1, + location=DeckSlotLocation(slotName=DeckSlotName.SLOT_1), + serialNumber="serial-number", + ) + + +def test_get_location(tempdeck_v1_def: ModuleDefinition) -> None: + """It should return the module's location or raise.""" + subject = make_module_view( + slot_by_module_id={ + "module-1": DeckSlotName.SLOT_1, + "module-2": None, + }, + hardware_by_module_id={ + "module-1": HardwareModule( + serial_number="serial-1", + definition=tempdeck_v1_def, + ), + "module-2": HardwareModule( + serial_number="serial-2", + definition=tempdeck_v1_def, + ), + }, + ) + + assert subject.get_location("module-1") == DeckSlotLocation( + slotName=DeckSlotName.SLOT_1 + ) + + with pytest.raises(errors.ModuleNotOnDeckError): + assert subject.get_location("module-2") + + +def test_get_all_modules( + tempdeck_v1_def: ModuleDefinition, + tempdeck_v2_def: ModuleDefinition, +) -> None: + """It should return all modules in state.""" + subject = make_module_view( + slot_by_module_id={ + "module-1": DeckSlotName.SLOT_1, + "module-2": DeckSlotName.SLOT_2, + }, + hardware_by_module_id={ + "module-1": HardwareModule( + serial_number="serial-1", + definition=tempdeck_v1_def, + ), + "module-2": HardwareModule( + serial_number="serial-2", + definition=tempdeck_v2_def, + ), + }, + ) + + assert subject.get_all() == [ + LoadedModule( + id="module-1", + serialNumber="serial-1", + model=ModuleModel.TEMPERATURE_MODULE_V1, + location=DeckSlotLocation(slotName=DeckSlotName.SLOT_1), + ), + LoadedModule( + id="module-2", + serialNumber="serial-2", + model=ModuleModel.TEMPERATURE_MODULE_V2, + location=DeckSlotLocation(slotName=DeckSlotName.SLOT_2), + ), + ] + + +def test_get_properties_by_id( + tempdeck_v2_def: ModuleDefinition, + magdeck_v1_def: ModuleDefinition, + mag_block_v1_def: ModuleDefinition, +) -> None: + """It should return a loaded module's properties by ID.""" + subject = make_module_view( + slot_by_module_id={ + "module-1": DeckSlotName.SLOT_1, + "module-2": DeckSlotName.SLOT_2, + "module-3": DeckSlotName.SLOT_3, + }, + requested_model_by_module_id={ + "module-1": ModuleModel.TEMPERATURE_MODULE_V1, + "module-2": ModuleModel.MAGNETIC_MODULE_V1, + "module-3": ModuleModel.MAGNETIC_BLOCK_V1, + }, + hardware_by_module_id={ + "module-1": HardwareModule( + serial_number="serial-1", + # Intentionally different from requested model. + definition=tempdeck_v2_def, + ), + "module-2": HardwareModule( + serial_number="serial-2", + definition=magdeck_v1_def, + ), + "module-3": HardwareModule(serial_number=None, definition=mag_block_v1_def), + }, + ) + + assert subject.get_definition("module-1") == tempdeck_v2_def + assert subject.get_dimensions("module-1") == tempdeck_v2_def.dimensions + assert subject.get_requested_model("module-1") == ModuleModel.TEMPERATURE_MODULE_V1 + assert subject.get_connected_model("module-1") == ModuleModel.TEMPERATURE_MODULE_V2 + assert subject.get_serial_number("module-1") == "serial-1" + assert subject.get_location("module-1") == DeckSlotLocation( + slotName=DeckSlotName.SLOT_1 + ) + + assert subject.get_definition("module-2") == magdeck_v1_def + assert subject.get_dimensions("module-2") == magdeck_v1_def.dimensions + assert subject.get_requested_model("module-2") == ModuleModel.MAGNETIC_MODULE_V1 + assert subject.get_connected_model("module-2") == ModuleModel.MAGNETIC_MODULE_V1 + assert subject.get_serial_number("module-2") == "serial-2" + assert subject.get_location("module-2") == DeckSlotLocation( + slotName=DeckSlotName.SLOT_2 + ) + + assert subject.get_definition("module-3") == mag_block_v1_def + assert subject.get_dimensions("module-3") == mag_block_v1_def.dimensions + assert subject.get_requested_model("module-3") == ModuleModel.MAGNETIC_BLOCK_V1 + assert subject.get_connected_model("module-3") == ModuleModel.MAGNETIC_BLOCK_V1 + assert subject.get_location("module-3") == DeckSlotLocation( + slotName=DeckSlotName.SLOT_3 + ) + + with pytest.raises(errors.ModuleNotConnectedError): + subject.get_serial_number("module-3") + + with pytest.raises(errors.ModuleNotLoadedError): + subject.get_definition("Not a module ID oh no") + + +@pytest.mark.parametrize( + argnames=["module_def", "slot", "expected_offset"], + argvalues=[ + ( + lazy_fixture("tempdeck_v1_def"), + DeckSlotName.SLOT_1, + LabwareOffsetVector(x=-0.15, y=-0.15, z=80.09), + ), + ( + lazy_fixture("tempdeck_v2_def"), + DeckSlotName.SLOT_1, + LabwareOffsetVector(x=-1.45, y=-0.15, z=80.09), + ), + ( + lazy_fixture("tempdeck_v2_def"), + DeckSlotName.SLOT_3, + LabwareOffsetVector(x=1.15, y=-0.15, z=80.09), + ), + ( + lazy_fixture("magdeck_v1_def"), + DeckSlotName.SLOT_1, + LabwareOffsetVector(x=0.125, y=-0.125, z=82.25), + ), + ( + lazy_fixture("magdeck_v2_def"), + DeckSlotName.SLOT_1, + LabwareOffsetVector(x=-1.175, y=-0.125, z=82.25), + ), + ( + lazy_fixture("magdeck_v2_def"), + DeckSlotName.SLOT_3, + LabwareOffsetVector(x=1.425, y=-0.125, z=82.25), + ), + ( + lazy_fixture("thermocycler_v1_def"), + DeckSlotName.SLOT_7, + LabwareOffsetVector(x=0, y=82.56, z=97.8), + ), + ( + lazy_fixture("thermocycler_v2_def"), + DeckSlotName.SLOT_7, + LabwareOffsetVector(x=0, y=68.8, z=108.96), + ), + ( + lazy_fixture("heater_shaker_v1_def"), + DeckSlotName.SLOT_1, + LabwareOffsetVector(x=-0.125, y=1.125, z=68.275), + ), + ( + lazy_fixture("heater_shaker_v1_def"), + DeckSlotName.SLOT_3, + LabwareOffsetVector(x=0.125, y=-1.125, z=68.275), + ), + ], +) +def test_get_module_offset_for_ot2_standard( + module_def: ModuleDefinition, + slot: DeckSlotName, + expected_offset: LabwareOffsetVector, +) -> None: + """It should return the correct labware offset for module in specified slot.""" + subject = make_module_view( + deck_type=DeckType.OT2_STANDARD, + slot_by_module_id={"module-id": slot}, + hardware_by_module_id={ + "module-id": HardwareModule( + serial_number="module-serial", + definition=module_def, + ) + }, + ) + assert ( + subject.get_nominal_offset_to_child("module-id", get_addressable_area_view()) + == expected_offset + ) + + +@pytest.mark.parametrize( + 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.to_ot3_equivalent(), + LabwareOffsetVector(x=0, y=0, z=38.0), + lazy_fixture("ot3_standard_deck_def"), + ), + ], +) +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( + deck_type=DeckType.OT3_STANDARD, + slot_by_module_id={"module-id": slot}, + hardware_by_module_id={ + "module-id": HardwareModule( + serial_number="module-serial", + definition=module_def, + ) + }, + ) + + result_offset = subject.get_nominal_offset_to_child( + "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) + ) + + +def test_get_magnetic_module_substate( + magdeck_v1_def: ModuleDefinition, + magdeck_v2_def: ModuleDefinition, + heater_shaker_v1_def: ModuleDefinition, +) -> None: + """It should return a substate for the given Magnetic Module, if valid.""" + subject = make_module_view( + slot_by_module_id={ + "magnetic-module-gen1-id": DeckSlotName.SLOT_1, + "magnetic-module-gen2-id": DeckSlotName.SLOT_2, + "heatshake-module-id": DeckSlotName.SLOT_3, + }, + hardware_by_module_id={ + "magnetic-module-gen1-id": HardwareModule( + serial_number="magnetic-module-gen1-serial", + definition=magdeck_v1_def, + ), + "magnetic-module-gen2-id": HardwareModule( + serial_number="magnetic-module-gen2-serial", + definition=magdeck_v2_def, + ), + "heatshake-module-id": HardwareModule( + serial_number="heatshake-module-serial", + definition=heater_shaker_v1_def, + ), + }, + substate_by_module_id={ + "magnetic-module-gen1-id": MagneticModuleSubState( + module_id=MagneticModuleId("magnetic-module-gen1-id"), + model=ModuleModel.MAGNETIC_MODULE_V1, + ), + "magnetic-module-gen2-id": MagneticModuleSubState( + module_id=MagneticModuleId("magnetic-module-gen2-id"), + model=ModuleModel.MAGNETIC_MODULE_V2, + ), + "heatshake-module-id": HeaterShakerModuleSubState( + module_id=HeaterShakerModuleId("heatshake-module-id"), + labware_latch_status=HeaterShakerLatchStatus.UNKNOWN, + is_plate_shaking=False, + plate_target_temperature=None, + ), + }, + ) + + module_1_substate = subject.get_magnetic_module_substate( + module_id="magnetic-module-gen1-id" + ) + assert module_1_substate.module_id == "magnetic-module-gen1-id" + assert module_1_substate.model == ModuleModel.MAGNETIC_MODULE_V1 + + module_2_substate = subject.get_magnetic_module_substate( + module_id="magnetic-module-gen2-id" + ) + assert module_2_substate.module_id == "magnetic-module-gen2-id" + assert module_2_substate.model == ModuleModel.MAGNETIC_MODULE_V2 + + with pytest.raises(errors.WrongModuleTypeError): + subject.get_magnetic_module_substate(module_id="heatshake-module-id") + + with pytest.raises(errors.ModuleNotLoadedError): + subject.get_magnetic_module_substate(module_id="nonexistent-module-id") + + +def test_get_heater_shaker_module_substate( + magdeck_v2_def: ModuleDefinition, + heater_shaker_v1_def: ModuleDefinition, +) -> None: + """It should return a heater-shaker module substate.""" + subject = make_module_view( + slot_by_module_id={ + "magnetic-module-gen2-id": DeckSlotName.SLOT_2, + "heatshake-module-id": DeckSlotName.SLOT_3, + }, + hardware_by_module_id={ + "magnetic-module-gen2-id": HardwareModule( + serial_number="magnetic-module-gen2-serial", + definition=magdeck_v2_def, + ), + "heatshake-module-id": HardwareModule( + serial_number="heatshake-module-serial", + definition=heater_shaker_v1_def, + ), + }, + substate_by_module_id={ + "magnetic-module-gen2-id": MagneticModuleSubState( + module_id=MagneticModuleId("magnetic-module-gen2-id"), + model=ModuleModel.MAGNETIC_MODULE_V2, + ), + "heatshake-module-id": HeaterShakerModuleSubState( + module_id=HeaterShakerModuleId("heatshake-module-id"), + plate_target_temperature=432, + labware_latch_status=HeaterShakerLatchStatus.UNKNOWN, + is_plate_shaking=True, + ), + }, + ) + + hs_substate = subject.get_heater_shaker_module_substate( + module_id="heatshake-module-id" + ) + assert hs_substate.module_id == "heatshake-module-id" + assert hs_substate.plate_target_temperature == 432 + assert hs_substate.is_plate_shaking is True + assert hs_substate.labware_latch_status == HeaterShakerLatchStatus.UNKNOWN + + with pytest.raises(errors.WrongModuleTypeError): + subject.get_heater_shaker_module_substate(module_id="magnetic-module-gen2-id") + + with pytest.raises(errors.ModuleNotLoadedError): + subject.get_heater_shaker_module_substate(module_id="nonexistent-module-id") + + +def test_get_temperature_module_substate( + tempdeck_v1_def: ModuleDefinition, + tempdeck_v2_def: ModuleDefinition, + heater_shaker_v1_def: ModuleDefinition, +) -> None: + """It should return a substate for the given Temperature Module, if valid.""" + subject = make_module_view( + slot_by_module_id={ + "temp-module-gen1-id": DeckSlotName.SLOT_1, + "temp-module-gen2-id": DeckSlotName.SLOT_2, + "heatshake-module-id": DeckSlotName.SLOT_3, + }, + hardware_by_module_id={ + "temp-module-gen1-id": HardwareModule( + serial_number="temp-module-gen1-serial", + definition=tempdeck_v1_def, + ), + "temp-module-gen2-id": HardwareModule( + serial_number="temp-module-gen2-serial", + definition=tempdeck_v2_def, + ), + "heatshake-module-id": HardwareModule( + serial_number="heatshake-module-serial", + definition=heater_shaker_v1_def, + ), + }, + substate_by_module_id={ + "temp-module-gen1-id": TemperatureModuleSubState( + module_id=TemperatureModuleId("temp-module-gen1-id"), + plate_target_temperature=None, + ), + "temp-module-gen2-id": TemperatureModuleSubState( + module_id=TemperatureModuleId("temp-module-gen2-id"), + plate_target_temperature=123, + ), + "heatshake-module-id": HeaterShakerModuleSubState( + module_id=HeaterShakerModuleId("heatshake-module-id"), + labware_latch_status=HeaterShakerLatchStatus.UNKNOWN, + is_plate_shaking=False, + plate_target_temperature=None, + ), + }, + ) + + module_1_substate = subject.get_temperature_module_substate( + module_id="temp-module-gen1-id" + ) + assert module_1_substate.module_id == "temp-module-gen1-id" + assert module_1_substate.plate_target_temperature is None + + module_2_substate = subject.get_temperature_module_substate( + module_id="temp-module-gen2-id" + ) + assert module_2_substate.module_id == "temp-module-gen2-id" + assert module_2_substate.plate_target_temperature == 123 + + with pytest.raises(errors.WrongModuleTypeError): + subject.get_temperature_module_substate(module_id="heatshake-module-id") + + with pytest.raises(errors.ModuleNotLoadedError): + subject.get_temperature_module_substate(module_id="nonexistent-module-id") + + +def test_get_plate_target_temperature(heater_shaker_v1_def: ModuleDefinition) -> None: + """It should return whether target temperature is set.""" + module_view = make_module_view( + slot_by_module_id={"module-id": DeckSlotName.SLOT_1}, + hardware_by_module_id={ + "module-id": HardwareModule( + serial_number="serial-number", + definition=heater_shaker_v1_def, + ) + }, + substate_by_module_id={ + "module-id": HeaterShakerModuleSubState( + module_id=HeaterShakerModuleId("module-id"), + labware_latch_status=HeaterShakerLatchStatus.UNKNOWN, + is_plate_shaking=False, + plate_target_temperature=12.3, + ) + }, + ) + subject = module_view.get_heater_shaker_module_substate("module-id") + assert subject.get_plate_target_temperature() == 12.3 + + +def test_get_plate_target_temperature_no_target( + heater_shaker_v1_def: ModuleDefinition, +) -> None: + """It should raise if no target temperature is set.""" + module_view = make_module_view( + slot_by_module_id={"module-id": DeckSlotName.SLOT_1}, + hardware_by_module_id={ + "module-id": HardwareModule( + serial_number="serial-number", + definition=heater_shaker_v1_def, + ) + }, + substate_by_module_id={ + "module-id": HeaterShakerModuleSubState( + module_id=HeaterShakerModuleId("module-id"), + labware_latch_status=HeaterShakerLatchStatus.UNKNOWN, + is_plate_shaking=False, + plate_target_temperature=None, + ) + }, + ) + subject = module_view.get_heater_shaker_module_substate("module-id") + + with pytest.raises(errors.NoTargetTemperatureSetError): + subject.get_plate_target_temperature() + + +def test_get_magnet_home_to_base_offset() -> None: + """It should return the model-specific offset to bottom.""" + subject = make_module_view() + assert ( + subject.get_magnet_home_to_base_offset( + module_model=ModuleModel.MAGNETIC_MODULE_V1 + ) + == 2.5 + ) + assert ( + subject.get_magnet_home_to_base_offset( + module_model=ModuleModel.MAGNETIC_MODULE_V2 + ) + == 2.5 + ) + + +@pytest.mark.parametrize( + "module_model", [ModuleModel.MAGNETIC_MODULE_V1, ModuleModel.MAGNETIC_MODULE_V2] +) +def test_calculate_magnet_height(module_model: ModuleModel) -> None: + """It should use true millimeters as hardware units.""" + subject = make_module_view() + + assert ( + subject.calculate_magnet_height( + module_model=module_model, + height_from_base=100, + ) + == 100 + ) + + # todo(mm, 2022-02-28): + # It's unclear whether this expected result should actually be the same + # between GEN1 and GEN2. + # The GEN1 homing backoff distance looks accidentally halved, for the same reason + # that its heights are halved. If the limit switch hardware is the same for both + # modules, we'd expect the backoff difference to cause a difference in the + # height_from_home test, even though we're measuring everything in true mm. + # https://github.com/Opentrons/opentrons/issues/9585 + assert ( + subject.calculate_magnet_height( + module_model=module_model, + height_from_home=100, + ) + == 97.5 + ) + + assert ( + subject.calculate_magnet_height( + module_model=module_model, + labware_default_height=100, + offset_from_labware_default=10.0, + ) + == 110 + ) + + +@pytest.mark.parametrize( + argnames=["from_slot", "to_slot", "should_dodge"], + argvalues=[ + (DeckSlotName.SLOT_1, DeckSlotName.FIXED_TRASH, True), + (DeckSlotName.FIXED_TRASH, DeckSlotName.SLOT_1, True), + (DeckSlotName.SLOT_4, DeckSlotName.FIXED_TRASH, True), + (DeckSlotName.FIXED_TRASH, DeckSlotName.SLOT_4, True), + (DeckSlotName.SLOT_4, DeckSlotName.SLOT_9, True), + (DeckSlotName.SLOT_9, DeckSlotName.SLOT_4, True), + (DeckSlotName.SLOT_4, DeckSlotName.SLOT_8, True), + (DeckSlotName.SLOT_8, DeckSlotName.SLOT_4, True), + (DeckSlotName.SLOT_1, DeckSlotName.SLOT_8, True), + (DeckSlotName.SLOT_8, DeckSlotName.SLOT_1, True), + (DeckSlotName.SLOT_4, DeckSlotName.SLOT_11, True), + (DeckSlotName.SLOT_11, DeckSlotName.SLOT_4, True), + (DeckSlotName.SLOT_1, DeckSlotName.SLOT_11, True), + (DeckSlotName.SLOT_11, DeckSlotName.SLOT_1, True), + (DeckSlotName.SLOT_2, DeckSlotName.SLOT_4, False), + ], +) +def test_thermocycler_dodging_by_slots( + thermocycler_v1_def: ModuleDefinition, + from_slot: DeckSlotName, + to_slot: DeckSlotName, + should_dodge: bool, +) -> None: + """It should specify if thermocycler dodging is needed. + + It should return True if thermocycler exists and movement is between bad pairs of + slot locations. + """ + subject = make_module_view( + slot_by_module_id={"module-id": DeckSlotName.SLOT_1}, + hardware_by_module_id={ + "module-id": HardwareModule( + serial_number="serial-number", + definition=thermocycler_v1_def, + ) + }, + ) + + assert ( + subject.should_dodge_thermocycler(from_slot=from_slot, to_slot=to_slot) + is should_dodge + ) + + +@pytest.mark.parametrize( + argnames=["from_slot", "to_slot"], + argvalues=[ + (DeckSlotName.SLOT_8, DeckSlotName.SLOT_1), + (DeckSlotName.SLOT_B2, DeckSlotName.SLOT_D1), + ], +) +@pytest.mark.parametrize( + argnames=["module_definition", "should_dodge"], + argvalues=[ + (lazy_fixture("tempdeck_v1_def"), False), + (lazy_fixture("tempdeck_v2_def"), False), + (lazy_fixture("magdeck_v1_def"), False), + (lazy_fixture("magdeck_v2_def"), False), + (lazy_fixture("thermocycler_v1_def"), True), + (lazy_fixture("thermocycler_v2_def"), True), + (lazy_fixture("heater_shaker_v1_def"), False), + ], +) +def test_thermocycler_dodging_by_modules( + from_slot: DeckSlotName, + to_slot: DeckSlotName, + module_definition: ModuleDefinition, + should_dodge: bool, +) -> None: + """It should specify if thermocycler dodging is needed if there is a thermocycler module.""" + subject = make_module_view( + slot_by_module_id={"module-id": DeckSlotName.SLOT_1}, + hardware_by_module_id={ + "module-id": HardwareModule( + serial_number="serial-number", + definition=module_definition, + ) + }, + ) + assert ( + subject.should_dodge_thermocycler(from_slot=from_slot, to_slot=to_slot) + is should_dodge + ) + + +def test_select_hardware_module_to_load_rejects_missing() -> None: + """It should raise if the correct module isn't attached.""" + subject = make_module_view() + + with pytest.raises(errors.ModuleNotAttachedError): + subject.select_hardware_module_to_load( + model=ModuleModel.TEMPERATURE_MODULE_V1, + location=DeckSlotLocation(slotName=DeckSlotName.SLOT_1), + attached_modules=[], + ) + + +@pytest.mark.parametrize( + argnames=["requested_model", "attached_definition"], + argvalues=[ + (ModuleModel.TEMPERATURE_MODULE_V1, lazy_fixture("tempdeck_v1_def")), + (ModuleModel.TEMPERATURE_MODULE_V2, lazy_fixture("tempdeck_v2_def")), + (ModuleModel.TEMPERATURE_MODULE_V1, lazy_fixture("tempdeck_v2_def")), + (ModuleModel.TEMPERATURE_MODULE_V2, lazy_fixture("tempdeck_v1_def")), + (ModuleModel.MAGNETIC_MODULE_V1, lazy_fixture("magdeck_v1_def")), + (ModuleModel.MAGNETIC_MODULE_V2, lazy_fixture("magdeck_v2_def")), + (ModuleModel.THERMOCYCLER_MODULE_V1, lazy_fixture("thermocycler_v1_def")), + (ModuleModel.THERMOCYCLER_MODULE_V2, lazy_fixture("thermocycler_v2_def")), + ], +) +def test_select_hardware_module_to_load( + requested_model: ModuleModel, + attached_definition: ModuleDefinition, +) -> None: + """It should return the first attached module that matches.""" + subject = make_module_view() + + attached_modules = [ + HardwareModule(serial_number="serial-1", definition=attached_definition), + HardwareModule(serial_number="serial-2", definition=attached_definition), + ] + + result = subject.select_hardware_module_to_load( + model=requested_model, + location=DeckSlotLocation(slotName=DeckSlotName.SLOT_1), + attached_modules=attached_modules, + ) + + assert result == attached_modules[0] + + +def test_select_hardware_module_to_load_skips_non_matching( + magdeck_v1_def: ModuleDefinition, + magdeck_v2_def: ModuleDefinition, +) -> None: + """It should skip over non-matching modules.""" + subject = make_module_view() + + attached_modules = [ + HardwareModule(serial_number="serial-1", definition=magdeck_v1_def), + HardwareModule(serial_number="serial-2", definition=magdeck_v2_def), + ] + + result = subject.select_hardware_module_to_load( + model=ModuleModel.MAGNETIC_MODULE_V2, + location=DeckSlotLocation(slotName=DeckSlotName.SLOT_1), + attached_modules=attached_modules, + ) + + assert result == attached_modules[1] + + +def test_select_hardware_module_to_load_skips_already_loaded( + magdeck_v1_def: ModuleDefinition, +) -> None: + """It should skip over already assigned modules.""" + subject = make_module_view( + hardware_by_module_id={ + "module-1": HardwareModule( + serial_number="serial-1", + definition=magdeck_v1_def, + ) + } + ) + + attached_modules = [ + HardwareModule(serial_number="serial-1", definition=magdeck_v1_def), + HardwareModule(serial_number="serial-2", definition=magdeck_v1_def), + ] + + result = subject.select_hardware_module_to_load( + model=ModuleModel.MAGNETIC_MODULE_V1, + location=DeckSlotLocation(slotName=DeckSlotName.SLOT_3), + attached_modules=attached_modules, + ) + + assert result == attached_modules[1] + + +def test_select_hardware_module_to_load_reuses_already_loaded( + magdeck_v1_def: ModuleDefinition, +) -> None: + """It should reuse over already assigned modules in the same location.""" + subject = make_module_view( + slot_by_module_id={ + "module-1": DeckSlotName.SLOT_1, + }, + hardware_by_module_id={ + "module-1": HardwareModule( + serial_number="serial-1", + definition=magdeck_v1_def, + ) + }, + ) + + attached_modules = [ + HardwareModule(serial_number="serial-1", definition=magdeck_v1_def), + HardwareModule(serial_number="serial-2", definition=magdeck_v1_def), + ] + + result = subject.select_hardware_module_to_load( + model=ModuleModel.MAGNETIC_MODULE_V1, + location=DeckSlotLocation(slotName=DeckSlotName.SLOT_1), + attached_modules=attached_modules, + ) + + assert result == attached_modules[0] + + +def test_select_hardware_module_to_load_rejects_location_reassignment( + magdeck_v1_def: ModuleDefinition, + tempdeck_v1_def: ModuleDefinition, +) -> None: + """It should raise if a non-matching module is already present in the slot.""" + subject = make_module_view( + slot_by_module_id={ + "module-1": DeckSlotName.SLOT_1, + }, + hardware_by_module_id={ + "module-1": HardwareModule( + serial_number="serial-1", + definition=magdeck_v1_def, + ) + }, + ) + + attached_modules = [ + HardwareModule(serial_number="serial-1", definition=magdeck_v1_def), + HardwareModule(serial_number="serial-2", definition=tempdeck_v1_def), + ] + + with pytest.raises(errors.ModuleAlreadyPresentError): + subject.select_hardware_module_to_load( + model=ModuleModel.TEMPERATURE_MODULE_V1, + location=DeckSlotLocation(slotName=DeckSlotName.SLOT_1), + attached_modules=attached_modules, + ) + + +class _CalculateMagnetHardwareHeightTestParams(NamedTuple): + definition: ModuleDefinition + mm_from_base: float + expected_result: Optional[float] + expected_exception_type: Union[Type[Exception], None] + + +@pytest.mark.parametrize( + "definition, mm_from_base, expected_result, expected_exception_type", + [ + # Happy cases: + _CalculateMagnetHardwareHeightTestParams( + definition=lazy_fixture("magdeck_v1_def"), + mm_from_base=10, + # TODO(mm, 2022-03-09): It's unclear if this expected result is correct. + # https://github.com/Opentrons/opentrons/issues/9585 + expected_result=25, + expected_exception_type=None, + ), + _CalculateMagnetHardwareHeightTestParams( + definition=lazy_fixture("magdeck_v2_def"), + mm_from_base=10, + expected_result=12.5, + expected_exception_type=None, + ), + # Boundary conditions: + # + # TODO(mm, 2022-03-09): + # In Python >=3.9, improve precision with math.nextafter(). + # Also consider relying on shared constants instead of hard-coding bounds. + # + # TODO(mm, 2022-03-09): It's unclear if the bounds used for V1 modules + # are physically correct. https://github.com/Opentrons/opentrons/issues/9585 + _CalculateMagnetHardwareHeightTestParams( # V1 barely too low. + definition=lazy_fixture("magdeck_v1_def"), + mm_from_base=-2.51, + expected_result=None, + expected_exception_type=errors.EngageHeightOutOfRangeError, + ), + _CalculateMagnetHardwareHeightTestParams( # V1 lowest allowed. + definition=lazy_fixture("magdeck_v1_def"), + mm_from_base=-2.5, + expected_result=0, + expected_exception_type=None, + ), + _CalculateMagnetHardwareHeightTestParams( # V1 highest allowed. + definition=lazy_fixture("magdeck_v1_def"), + mm_from_base=20, + expected_result=45, + expected_exception_type=None, + ), + _CalculateMagnetHardwareHeightTestParams( # V1 barely too high. + definition=lazy_fixture("magdeck_v1_def"), + mm_from_base=20.01, + expected_result=None, + expected_exception_type=errors.EngageHeightOutOfRangeError, + ), + _CalculateMagnetHardwareHeightTestParams( # V2 barely too low. + definition=lazy_fixture("magdeck_v2_def"), + mm_from_base=-2.51, + expected_result=None, + expected_exception_type=errors.EngageHeightOutOfRangeError, + ), + _CalculateMagnetHardwareHeightTestParams( # V2 lowest allowed. + definition=lazy_fixture("magdeck_v2_def"), + mm_from_base=-2.5, + expected_result=0, + expected_exception_type=None, + ), + _CalculateMagnetHardwareHeightTestParams( # V2 highest allowed. + definition=lazy_fixture("magdeck_v2_def"), + mm_from_base=22.5, + expected_result=25, + expected_exception_type=None, + ), + _CalculateMagnetHardwareHeightTestParams( # V2 barely too high. + definition=lazy_fixture("magdeck_v2_def"), + mm_from_base=22.51, + expected_result=None, + expected_exception_type=errors.EngageHeightOutOfRangeError, + ), + ], +) +def test_magnetic_module_view_calculate_magnet_hardware_height( + definition: ModuleDefinition, + mm_from_base: float, + expected_result: float, + expected_exception_type: Union[Type[Exception], None], +) -> None: + """It should return the expected height or raise the expected exception.""" + module_view = make_module_view( + slot_by_module_id={"module-id": DeckSlotName.SLOT_1}, + hardware_by_module_id={ + "module-id": HardwareModule( + serial_number="serial-number", + definition=definition, + ) + }, + substate_by_module_id={ + "module-id": MagneticModuleSubState( + module_id=MagneticModuleId("module-id"), + model=definition.model, # type: ignore [arg-type] + ) + }, + ) + subject = module_view.get_magnetic_module_substate("module-id") + expected_raise: ContextManager[None] = ( + # Not sure why mypy has trouble with this. + does_not_raise() # type: ignore[assignment] + if expected_exception_type is None + else pytest.raises(expected_exception_type) + ) + with expected_raise: + result = subject.calculate_magnet_hardware_height(mm_from_base=mm_from_base) + assert result == expected_result + + +@pytest.mark.parametrize("target_temp", [36.8, 95.1]) +def test_validate_heater_shaker_target_temperature_raises( + heater_shaker_v1_def: ModuleDefinition, + target_temp: float, +) -> None: + """It should verify if a target temperature is valid for the specified module.""" + module_view = make_module_view( + slot_by_module_id={"module-id": DeckSlotName.SLOT_1}, + hardware_by_module_id={ + "module-id": HardwareModule( + serial_number="serial-number", + definition=heater_shaker_v1_def, + ) + }, + substate_by_module_id={ + "module-id": HeaterShakerModuleSubState( + module_id=HeaterShakerModuleId("module-id"), + labware_latch_status=HeaterShakerLatchStatus.UNKNOWN, + is_plate_shaking=False, + plate_target_temperature=None, + ) + }, + ) + subject = module_view.get_heater_shaker_module_substate("module-id") + with pytest.raises(errors.InvalidTargetTemperatureError): + subject.validate_target_temperature(target_temp) + + +@pytest.mark.parametrize("target_temp", [37, 94.8]) +def test_validate_heater_shaker_target_temperature( + heater_shaker_v1_def: ModuleDefinition, + target_temp: float, +) -> None: + """It should verify if a target temperature is valid for the specified module.""" + module_view = make_module_view( + slot_by_module_id={"module-id": DeckSlotName.SLOT_1}, + hardware_by_module_id={ + "module-id": HardwareModule( + serial_number="serial-number", + definition=heater_shaker_v1_def, + ) + }, + substate_by_module_id={ + "module-id": HeaterShakerModuleSubState( + module_id=HeaterShakerModuleId("module-id"), + labware_latch_status=HeaterShakerLatchStatus.UNKNOWN, + is_plate_shaking=False, + plate_target_temperature=None, + ) + }, + ) + subject = module_view.get_heater_shaker_module_substate("module-id") + assert subject.validate_target_temperature(target_temp) == target_temp + + +@pytest.mark.parametrize("target_temp", [-10, 99.9]) +def test_validate_temp_module_target_temperature_raises( + tempdeck_v1_def: ModuleDefinition, + target_temp: float, +) -> None: + """It should verify if a target temperature is valid for the specified module.""" + module_view = make_module_view( + slot_by_module_id={"module-id": DeckSlotName.SLOT_1}, + hardware_by_module_id={ + "module-id": HardwareModule( + serial_number="serial-number", + definition=tempdeck_v1_def, + ) + }, + substate_by_module_id={ + "module-id": TemperatureModuleSubState( + module_id=TemperatureModuleId("module-id"), + plate_target_temperature=None, + ) + }, + ) + subject = module_view.get_temperature_module_substate("module-id") + with pytest.raises(errors.InvalidTargetTemperatureError): + subject.validate_target_temperature(target_temp) + + +@pytest.mark.parametrize( + ["target_temp", "validated_temp"], [(-9.431, -9), (0, 0), (99.1, 99)] +) +def test_validate_temp_module_target_temperature( + tempdeck_v2_def: ModuleDefinition, target_temp: float, validated_temp: int +) -> None: + """It should verify if a target temperature is valid for the specified module.""" + module_view = make_module_view( + slot_by_module_id={"module-id": DeckSlotName.SLOT_1}, + hardware_by_module_id={ + "module-id": HardwareModule( + serial_number="serial-number", + definition=tempdeck_v2_def, + ) + }, + substate_by_module_id={ + "module-id": TemperatureModuleSubState( + module_id=TemperatureModuleId("module-id"), + plate_target_temperature=None, + ) + }, + ) + subject = module_view.get_temperature_module_substate("module-id") + assert subject.validate_target_temperature(target_temp) == validated_temp + + +@pytest.mark.parametrize( + argnames=["rpm_param", "validated_param"], + argvalues=[(200.1, 200), (250.6, 251), (300.9, 301)], +) +def test_validate_heater_shaker_target_speed_converts_to_int( + rpm_param: float, validated_param: bool, heater_shaker_v1_def: ModuleDefinition +) -> None: + """It should validate heater-shaker target rpm.""" + module_view = make_module_view( + slot_by_module_id={"module-id": DeckSlotName.SLOT_1}, + hardware_by_module_id={ + "module-id": HardwareModule( + serial_number="serial-number", + definition=heater_shaker_v1_def, + ) + }, + substate_by_module_id={ + "module-id": HeaterShakerModuleSubState( + module_id=HeaterShakerModuleId("module-id"), + labware_latch_status=HeaterShakerLatchStatus.UNKNOWN, + is_plate_shaking=False, + plate_target_temperature=None, + ) + }, + ) + subject = module_view.get_heater_shaker_module_substate("module-id") + assert subject.validate_target_speed(rpm_param) == validated_param + + +@pytest.mark.parametrize( + argnames=["rpm_param", "expected_valid"], + argvalues=[(199.4, False), (199.5, True), (3000.7, False), (3000.4, True)], +) +def test_validate_heater_shaker_target_speed_raises_error( + rpm_param: float, expected_valid: bool, heater_shaker_v1_def: ModuleDefinition +) -> None: + """It should validate heater-shaker target rpm.""" + module_view = make_module_view( + slot_by_module_id={"module-id": DeckSlotName.SLOT_1}, + hardware_by_module_id={ + "module-id": HardwareModule( + serial_number="serial-number", + definition=heater_shaker_v1_def, + ) + }, + substate_by_module_id={ + "module-id": HeaterShakerModuleSubState( + module_id=HeaterShakerModuleId("module-id"), + labware_latch_status=HeaterShakerLatchStatus.UNKNOWN, + is_plate_shaking=False, + plate_target_temperature=None, + ) + }, + ) + subject = module_view.get_heater_shaker_module_substate("module-id") + if not expected_valid: + with pytest.raises(errors.InvalidTargetSpeedError): + subject.validate_target_speed(rpm_param) + + +def test_raise_if_labware_latch_not_closed( + heater_shaker_v1_def: ModuleDefinition, +) -> None: + """It should raise an error if labware latch is not closed.""" + module_view = make_module_view( + slot_by_module_id={"module-id": DeckSlotName.SLOT_1}, + hardware_by_module_id={ + "module-id": HardwareModule( + serial_number="serial-number", + definition=heater_shaker_v1_def, + ) + }, + substate_by_module_id={ + "module-id": HeaterShakerModuleSubState( + module_id=HeaterShakerModuleId("module-id"), + labware_latch_status=HeaterShakerLatchStatus.OPEN, + is_plate_shaking=False, + plate_target_temperature=None, + ) + }, + ) + subject = module_view.get_heater_shaker_module_substate("module-id") + with pytest.raises(errors.CannotPerformModuleAction, match="is open"): + subject.raise_if_labware_latch_not_closed() + + +def test_raise_if_labware_latch_unknown( + heater_shaker_v1_def: ModuleDefinition, +) -> None: + """It should raise an error if labware latch is not closed.""" + module_view = make_module_view( + slot_by_module_id={"module-id": DeckSlotName.SLOT_1}, + hardware_by_module_id={ + "module-id": HardwareModule( + serial_number="serial-number", + definition=heater_shaker_v1_def, + ) + }, + substate_by_module_id={ + "module-id": HeaterShakerModuleSubState( + module_id=HeaterShakerModuleId("module-id"), + labware_latch_status=HeaterShakerLatchStatus.UNKNOWN, + is_plate_shaking=False, + plate_target_temperature=None, + ) + }, + ) + subject = module_view.get_heater_shaker_module_substate("module-id") + with pytest.raises(errors.CannotPerformModuleAction, match="set to closed"): + subject.raise_if_labware_latch_not_closed() + + +def test_heater_shaker_raise_if_shaking( + heater_shaker_v1_def: ModuleDefinition, +) -> None: + """It should raise when heater-shaker is shaking.""" + module_view = make_module_view( + slot_by_module_id={"module-id": DeckSlotName.SLOT_1}, + hardware_by_module_id={ + "module-id": HardwareModule( + serial_number="serial-number", + definition=heater_shaker_v1_def, + ) + }, + substate_by_module_id={ + "module-id": HeaterShakerModuleSubState( + module_id=HeaterShakerModuleId("module-id"), + labware_latch_status=HeaterShakerLatchStatus.UNKNOWN, + is_plate_shaking=True, + plate_target_temperature=None, + ) + }, + ) + subject = module_view.get_heater_shaker_module_substate("module-id") + with pytest.raises(errors.CannotPerformModuleAction): + subject.raise_if_shaking() + + +def test_get_heater_shaker_movement_data( + heater_shaker_v1_def: ModuleDefinition, + tempdeck_v2_def: ModuleDefinition, +) -> None: + """It should get heater-shaker movement data.""" + module_view = make_module_view( + slot_by_module_id={ + "module-id": DeckSlotName.SLOT_1, + "other-module-id": DeckSlotName.SLOT_5, + }, + hardware_by_module_id={ + "module-id": HardwareModule( + serial_number="serial-number", + definition=heater_shaker_v1_def, + ), + "other-module-id": HardwareModule( + serial_number="other-serial-number", + definition=tempdeck_v2_def, + ), + }, + substate_by_module_id={ + "module-id": HeaterShakerModuleSubState( + module_id=HeaterShakerModuleId("module-id"), + labware_latch_status=HeaterShakerLatchStatus.CLOSED, + is_plate_shaking=False, + plate_target_temperature=None, + ), + "other-module-id": TemperatureModuleSubState( + module_id=TemperatureModuleId("other-module-id"), + plate_target_temperature=None, + ), + }, + ) + subject = module_view.get_heater_shaker_movement_restrictors() + assert len(subject) == 1 + for hs_movement_data in subject: + assert not hs_movement_data.plate_shaking + assert hs_movement_data.latch_status + assert hs_movement_data.deck_slot == 1 + + +def test_tempdeck_get_plate_target_temperature( + tempdeck_v2_def: ModuleDefinition, +) -> None: + """It should return whether target temperature is set.""" + module_view = make_module_view( + slot_by_module_id={"module-id": DeckSlotName.SLOT_1}, + hardware_by_module_id={ + "module-id": HardwareModule( + serial_number="serial-number", + definition=tempdeck_v2_def, + ) + }, + substate_by_module_id={ + "module-id": TemperatureModuleSubState( + module_id=TemperatureModuleId("module-id"), + plate_target_temperature=12, + ) + }, + ) + subject = module_view.get_temperature_module_substate("module-id") + assert subject.get_plate_target_temperature() == 12 + + +def test_tempdeck_get_plate_target_temperature_no_target( + tempdeck_v2_def: ModuleDefinition, +) -> None: + """It should raise if no target temperature is set.""" + module_view = make_module_view( + slot_by_module_id={"module-id": DeckSlotName.SLOT_1}, + hardware_by_module_id={ + "module-id": HardwareModule( + serial_number="serial-number", + definition=tempdeck_v2_def, + ) + }, + substate_by_module_id={ + "module-id": TemperatureModuleSubState( + module_id=TemperatureModuleId("module-id"), + plate_target_temperature=None, + ) + }, + ) + subject = module_view.get_temperature_module_substate("module-id") + + with pytest.raises(errors.NoTargetTemperatureSetError): + subject.get_plate_target_temperature() + + +def test_thermocycler_get_target_temperatures( + thermocycler_v1_def: ModuleDefinition, +) -> None: + """It should return whether target temperature for thermocycler is set.""" + module_view = make_module_view( + slot_by_module_id={"module-id": DeckSlotName.SLOT_1}, + hardware_by_module_id={ + "module-id": HardwareModule( + serial_number="serial-number", + definition=thermocycler_v1_def, + ) + }, + substate_by_module_id={ + "module-id": ThermocyclerModuleSubState( + module_id=ThermocyclerModuleId("module-id"), + is_lid_open=False, + target_block_temperature=14, + target_lid_temperature=28, + ) + }, + ) + subject = module_view.get_thermocycler_module_substate("module-id") + assert subject.get_target_block_temperature() == 14 + assert subject.get_target_lid_temperature() == 28 + + +def test_thermocycler_get_target_temperatures_no_target( + thermocycler_v1_def: ModuleDefinition, +) -> None: + """It should raise if no target temperature is set.""" + module_view = make_module_view( + slot_by_module_id={"module-id": DeckSlotName.SLOT_1}, + hardware_by_module_id={ + "module-id": HardwareModule( + serial_number="serial-number", + definition=thermocycler_v1_def, + ) + }, + substate_by_module_id={ + "module-id": ThermocyclerModuleSubState( + module_id=ThermocyclerModuleId("module-id"), + is_lid_open=False, + target_block_temperature=None, + target_lid_temperature=None, + ) + }, + ) + subject = module_view.get_thermocycler_module_substate("module-id") + + with pytest.raises(errors.NoTargetTemperatureSetError): + subject.get_target_block_temperature() + subject.get_target_lid_temperature() + + +@pytest.fixture +def module_view_with_thermocycler(thermocycler_v1_def: ModuleDefinition) -> ModuleView: + """Get a module state view with a loaded thermocycler.""" + return make_module_view( + slot_by_module_id={"module-id": DeckSlotName.SLOT_1}, + hardware_by_module_id={ + "module-id": HardwareModule( + serial_number="serial-number", + definition=thermocycler_v1_def, + ) + }, + substate_by_module_id={ + "module-id": ThermocyclerModuleSubState( + module_id=ThermocyclerModuleId("module-id"), + target_block_temperature=None, + target_lid_temperature=None, + is_lid_open=False, + ) + }, + ) + + +@pytest.mark.parametrize("input_temperature", [0, 0.0, 0.001, 98.999, 99, 99.0]) +def test_thermocycler_validate_target_block_temperature( + module_view_with_thermocycler: ModuleView, + input_temperature: float, +) -> None: + """It should return a valid target block temperature.""" + subject = module_view_with_thermocycler.get_thermocycler_module_substate( + "module-id" + ) + result = subject.validate_target_block_temperature(input_temperature) + + assert result == input_temperature + + +@pytest.mark.parametrize( + argnames=["input_time", "validated_time"], + argvalues=[(0.0, 0.0), (0.123, 0.123), (123.456, 123.456), (1234567, 1234567)], +) +def test_thermocycler_validate_hold_time( + module_view_with_thermocycler: ModuleView, + input_time: float, + validated_time: float, +) -> None: + """It should return a valid hold time.""" + subject = module_view_with_thermocycler.get_thermocycler_module_substate( + "module-id" + ) + result = subject.validate_hold_time(input_time) + + assert result == validated_time + + +@pytest.mark.parametrize("input_time", [-0.1, -123]) +def test_thermocycler_validate_hold_time_raises( + module_view_with_thermocycler: ModuleView, + input_time: float, +) -> None: + """It should raise on invalid hold time.""" + subject = module_view_with_thermocycler.get_thermocycler_module_substate( + "module-id" + ) + + with pytest.raises(errors.InvalidHoldTimeError): + subject.validate_hold_time(input_time) + + +@pytest.mark.parametrize("input_temperature", [-0.001, 99.001]) +def test_thermocycler_validate_target_block_temperature_raises( + module_view_with_thermocycler: ModuleView, + input_temperature: float, +) -> None: + """It should raise on invalid target block temperature.""" + subject = module_view_with_thermocycler.get_thermocycler_module_substate( + "module-id" + ) + + with pytest.raises(errors.InvalidTargetTemperatureError): + subject.validate_target_block_temperature(input_temperature) + + +@pytest.mark.parametrize("input_volume", [0, 0.0, 0.001, 50.0, 99.999, 100, 100.0]) +def test_thermocycler_validate_block_max_volume( + module_view_with_thermocycler: ModuleView, + input_volume: float, +) -> None: + """It should return a validated max block volume value.""" + subject = module_view_with_thermocycler.get_thermocycler_module_substate( + "module-id" + ) + result = subject.validate_max_block_volume(input_volume) + + assert result == input_volume + + +@pytest.mark.parametrize("input_volume", [-10, -0.001, 100.001]) +def test_thermocycler_validate_block_max_volume_raises( + module_view_with_thermocycler: ModuleView, + input_volume: float, +) -> None: + """It should raise on invalid block volume temperature.""" + subject = module_view_with_thermocycler.get_thermocycler_module_substate( + "module-id" + ) + + with pytest.raises(errors.InvalidBlockVolumeError): + subject.validate_max_block_volume(input_volume) + + +@pytest.mark.parametrize("input_temperature", [37, 37.0, 37.001, 109.999, 110, 110.0]) +def test_thermocycler_validate_target_lid_temperature( + module_view_with_thermocycler: ModuleView, + input_temperature: float, +) -> None: + """It should return a valid target block temperature.""" + subject = module_view_with_thermocycler.get_thermocycler_module_substate( + "module-id" + ) + result = subject.validate_target_lid_temperature(input_temperature) + + assert result == input_temperature + + +@pytest.mark.parametrize("input_temperature", [36.999, 110.001]) +def test_thermocycler_validate_target_lid_temperature_raises( + module_view_with_thermocycler: ModuleView, + input_temperature: float, +) -> None: + """It should raise on invalid target block temperature.""" + subject = module_view_with_thermocycler.get_thermocycler_module_substate( + "module-id" + ) + + with pytest.raises(errors.InvalidTargetTemperatureError): + subject.validate_target_lid_temperature(input_temperature) + + +@pytest.mark.parametrize( + ("module_definition", "expected_height"), + [ + (lazy_fixture("thermocycler_v1_def"), 98.0), + (lazy_fixture("tempdeck_v1_def"), 84.0), + (lazy_fixture("tempdeck_v2_def"), 84.0), + (lazy_fixture("magdeck_v1_def"), 110.152), + (lazy_fixture("magdeck_v2_def"), 110.152), + (lazy_fixture("heater_shaker_v1_def"), 82.0), + ], +) +def test_get_overall_height( + module_definition: ModuleDefinition, + expected_height: float, +) -> None: + """It should get a module's overall height.""" + subject = make_module_view( + slot_by_module_id={"module-id": DeckSlotName.SLOT_7}, + hardware_by_module_id={ + "module-id": HardwareModule( + serial_number="serial-number", + definition=module_definition, + ) + }, + ) + + result = subject.get_overall_height("module-id") + assert result == expected_height + + +@pytest.mark.parametrize( + argnames=["location", "expected_raise"], + argvalues=[ + ( + DeckSlotLocation(slotName=DeckSlotName.SLOT_1), + pytest.raises(errors.LocationIsOccupiedError), + ), + (DeckSlotLocation(slotName=DeckSlotName.SLOT_2), does_not_raise()), + (DeckSlotLocation(slotName=DeckSlotName.FIXED_TRASH), does_not_raise()), + ], +) +def test_raise_if_labware_in_location( + location: DeckSlotLocation, + expected_raise: ContextManager[Any], + thermocycler_v1_def: ModuleDefinition, +) -> None: + """It should raise if there is module in specified location.""" + subject = make_module_view( + slot_by_module_id={"module-id-1": DeckSlotName.SLOT_1}, + hardware_by_module_id={ + "module-id-1": HardwareModule( + serial_number="serial-number", + definition=thermocycler_v1_def, + ) + }, + substate_by_module_id={ + "module-id-1": ThermocyclerModuleSubState( + module_id=ThermocyclerModuleId("module-id-1"), + is_lid_open=False, + target_block_temperature=None, + target_lid_temperature=None, + ) + }, + ) + with expected_raise: + subject.raise_if_module_in_location(location=location) + + +def test_get_by_slot() -> None: + """It should get the module in a given slot.""" + subject = make_module_view( + slot_by_module_id={ + "1": DeckSlotName.SLOT_1, + "2": DeckSlotName.SLOT_2, + }, + hardware_by_module_id={ + "1": HardwareModule( + serial_number="serial-number-1", + definition=ModuleDefinition.model_construct( # type: ignore[call-arg] + model=ModuleModel.TEMPERATURE_MODULE_V1 + ), + ), + "2": HardwareModule( + serial_number="serial-number-2", + definition=ModuleDefinition.model_construct( # type: ignore[call-arg] + model=ModuleModel.TEMPERATURE_MODULE_V2 + ), + ), + }, + ) + + assert subject.get_by_slot(DeckSlotName.SLOT_1) == LoadedModule( + id="1", + location=DeckSlotLocation(slotName=DeckSlotName.SLOT_1), + model=ModuleModel.TEMPERATURE_MODULE_V1, + serialNumber="serial-number-1", + ) + assert subject.get_by_slot(DeckSlotName.SLOT_2) == LoadedModule( + id="2", + location=DeckSlotLocation(slotName=DeckSlotName.SLOT_2), + model=ModuleModel.TEMPERATURE_MODULE_V2, + serialNumber="serial-number-2", + ) + assert subject.get_by_slot(DeckSlotName.SLOT_3) is None + + +def test_get_by_slot_prefers_later() -> None: + """It should get the module in a slot, preferring later items if locations match.""" + subject = make_module_view( + slot_by_module_id={ + "1": DeckSlotName.SLOT_1, + "1-again": DeckSlotName.SLOT_1, + }, + hardware_by_module_id={ + "1": HardwareModule( + serial_number="serial-number-1", + definition=ModuleDefinition.model_construct( # type: ignore[call-arg] + model=ModuleModel.TEMPERATURE_MODULE_V1 + ), + ), + "1-again": HardwareModule( + serial_number="serial-number-1-again", + definition=ModuleDefinition.model_construct( # type: ignore[call-arg] + model=ModuleModel.TEMPERATURE_MODULE_V1 + ), + ), + }, + ) + + assert subject.get_by_slot(DeckSlotName.SLOT_1) == LoadedModule( + id="1-again", + location=DeckSlotLocation(slotName=DeckSlotName.SLOT_1), + model=ModuleModel.TEMPERATURE_MODULE_V1, + serialNumber="serial-number-1-again", + ) + + +@pytest.mark.parametrize( + argnames=["mount", "target_slot", "expected_result"], + argvalues=[ + (MountType.RIGHT, DeckSlotName.SLOT_1, False), + (MountType.RIGHT, DeckSlotName.SLOT_2, True), + (MountType.RIGHT, DeckSlotName.SLOT_5, False), + (MountType.LEFT, DeckSlotName.SLOT_3, False), + (MountType.RIGHT, DeckSlotName.SLOT_5, False), + (MountType.LEFT, DeckSlotName.SLOT_8, True), + ], +) +def test_is_edge_move_unsafe( + mount: MountType, target_slot: DeckSlotName, expected_result: bool +) -> None: + """It should determine if an edge move would be unsafe.""" + subject = make_module_view( + slot_by_module_id={"foo": DeckSlotName.SLOT_1, "bar": DeckSlotName.SLOT_9} + ) + + result = subject.is_edge_move_unsafe(mount=mount, target_slot=target_slot) + + assert result is expected_result + + +@pytest.mark.parametrize( + argnames=["module_def", "expected_offset_data"], + argvalues=[ + ( + lazy_fixture("thermocycler_v2_def"), + LabwareMovementOffsetData( + pickUpOffset=LabwareOffsetVector(x=0, y=0, z=4.6), + dropOffset=LabwareOffsetVector(x=0, y=0, z=5.6), + ), + ), + ( + lazy_fixture("heater_shaker_v1_def"), + LabwareMovementOffsetData( + pickUpOffset=LabwareOffsetVector(x=0, y=0, z=0), + dropOffset=LabwareOffsetVector(x=0, y=0, z=1.0), + ), + ), + ( + lazy_fixture("tempdeck_v1_def"), + None, + ), + ], +) +def test_get_default_gripper_offsets( + module_def: ModuleDefinition, + expected_offset_data: Optional[LabwareMovementOffsetData], +) -> None: + """It should return the correct gripper offsets, if present.""" + subject = make_module_view( + slot_by_module_id={ + "module-1": DeckSlotName.SLOT_1, + }, + requested_model_by_module_id={ + "module-1": ModuleModel.TEMPERATURE_MODULE_V1, # Does not matter + }, + hardware_by_module_id={ + "module-1": HardwareModule( + serial_number="serial-1", + definition=module_def, + ), + }, + ) + assert subject.get_default_gripper_offsets("module-1") == expected_offset_data + + +@pytest.mark.parametrize( + argnames=["deck_type", "slot_name", "expected_highest_z", "deck_definition"], + argvalues=[ + ( + 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( + tempdeck_v2_def: ModuleDefinition, + 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( + deck_type=deck_type, + slot_by_module_id={"module-id": slot_name}, + requested_model_by_module_id={ + "module-id": ModuleModel.TEMPERATURE_MODULE_V2, + }, + hardware_by_module_id={ + "module-id": HardwareModule( + serial_number="module-serial", + definition=tempdeck_v2_def, + ) + }, + ) + assert isclose( + 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, + ) + + +def test_get_overflowed_module_in_slot(tempdeck_v1_def: ModuleDefinition) -> None: + """It should return the module occupying but not loaded in the given slot.""" + subject = make_module_view( + slot_by_module_id={"module-id": DeckSlotName.SLOT_1}, + hardware_by_module_id={ + "module-id": HardwareModule( + serial_number="serial-number", + definition=tempdeck_v1_def, + ) + }, + additional_slots_occupied_by_module_id={ + "module-id": [DeckSlotName.SLOT_6, DeckSlotName.SLOT_A1], + }, + ) + assert subject.get_overflowed_module_in_slot(DeckSlotName.SLOT_6) == LoadedModule( + id="module-id", + model=ModuleModel.TEMPERATURE_MODULE_V1, + location=DeckSlotLocation(slotName=DeckSlotName.SLOT_1), + serialNumber="serial-number", + ) + + +@pytest.mark.parametrize( + argnames=["deck_type", "module_def", "module_slot", "expected_result"], + argvalues=[ + ( + DeckType.OT3_STANDARD, + lazy_fixture("thermocycler_v2_def"), + DeckSlotName.SLOT_A1, + True, + ), + ( + DeckType.OT3_STANDARD, + lazy_fixture("tempdeck_v1_def"), + DeckSlotName.SLOT_A1, + False, + ), + ( + DeckType.OT3_STANDARD, + lazy_fixture("thermocycler_v2_def"), + DeckSlotName.SLOT_1, + False, + ), + ( + DeckType.OT2_STANDARD, + lazy_fixture("thermocycler_v2_def"), + DeckSlotName.SLOT_A1, + False, + ), + ], +) +def test_is_flex_deck_with_thermocycler( + deck_type: DeckType, + module_def: ModuleDefinition, + module_slot: DeckSlotName, + expected_result: bool, +) -> None: + """It should return True if there is a thermocycler on Flex.""" + subject = make_module_view( + slot_by_module_id={"module-id": DeckSlotName.SLOT_B1}, + hardware_by_module_id={ + "module-id": HardwareModule( + serial_number="serial-number", + definition=module_def, + ) + }, + additional_slots_occupied_by_module_id={ + "module-id": [module_slot, DeckSlotName.SLOT_C1], + }, + deck_type=deck_type, + ) + assert subject.is_flex_deck_with_thermocycler() == expected_result diff --git a/api/tests/opentrons/protocol_engine/state/test_motion_view.py b/api/tests/opentrons/protocol_engine/state/test_motion_view.py index 9e7307f29a7..3e9d60da79a 100644 --- a/api/tests/opentrons/protocol_engine/state/test_motion_view.py +++ b/api/tests/opentrons/protocol_engine/state/test_motion_view.py @@ -928,6 +928,7 @@ def test_get_touch_tip_waypoints( x_radius=1.2, y_radius=3.4, edge_path_type=_move_types.EdgePathType.RIGHT, + mm_from_edge=0.456, ) ).then_return([Point(x=11, y=22, z=33), Point(x=44, y=55, z=66)]) @@ -937,6 +938,7 @@ def test_get_touch_tip_waypoints( well_name="B2", center_point=center_point, radius=0.123, + mm_from_edge=0.456, ) assert result == [ diff --git a/api/tests/opentrons/protocol_engine/state/test_move_types.py b/api/tests/opentrons/protocol_engine/state/test_move_types.py index 9d46cb8a1ab..27f43ffbc0d 100644 --- a/api/tests/opentrons/protocol_engine/state/test_move_types.py +++ b/api/tests/opentrons/protocol_engine/state/test_move_types.py @@ -53,43 +53,47 @@ def test_get_move_type_to_well( ( subject.EdgePathType.LEFT, [ - Point(5, 20, 30), + Point(8, 20, 30), Point(10, 20, 30), - Point(10, 30, 30), - Point(10, 10, 30), + Point(10, 27, 30), + Point(10, 13, 30), Point(10, 20, 30), ], ), ( subject.EdgePathType.RIGHT, [ - Point(15, 20, 30), + Point(12, 20, 30), Point(10, 20, 30), - Point(10, 30, 30), - Point(10, 10, 30), + Point(10, 27, 30), + Point(10, 13, 30), Point(10, 20, 30), ], ), ( subject.EdgePathType.DEFAULT, [ - Point(15, 20, 30), - Point(5, 20, 30), + Point(12, 20, 30), + Point(8, 20, 30), Point(10, 20, 30), - Point(10, 30, 30), - Point(10, 10, 30), + Point(10, 27, 30), + Point(10, 13, 30), Point(10, 20, 30), ], ), ], ) -def get_edge_point_list( +def test_get_edge_point_list( edge_path_type: subject.EdgePathType, expected_result: List[Point], ) -> None: """It should get a list of well edge points.""" result = subject.get_edge_point_list( - Point(x=10, y=20, z=30), x_radius=5, y_radius=10, edge_path_type=edge_path_type + Point(x=10, y=20, z=30), + x_radius=5, + y_radius=10, + mm_from_edge=3, + edge_path_type=edge_path_type, ) assert result == expected_result diff --git a/api/tests/opentrons/protocol_engine/state/test_pipette_store.py b/api/tests/opentrons/protocol_engine/state/test_pipette_store.py deleted file mode 100644 index c8eab566abe..00000000000 --- a/api/tests/opentrons/protocol_engine/state/test_pipette_store.py +++ /dev/null @@ -1,701 +0,0 @@ -"""Tests for pipette state changes in the protocol_engine state store.""" -import pytest - -from opentrons_shared_data.pipette.types import PipetteNameType -from opentrons_shared_data.pipette import pipette_definition - -from opentrons.protocol_engine.state import update_types -from opentrons.types import MountType, Point -from opentrons.protocol_engine import commands as cmd -from opentrons.protocol_engine.types import ( - CurrentAddressableArea, - DeckPoint, - LoadedPipette, - FlowRates, - CurrentWell, - TipGeometry, -) -from opentrons.protocol_engine.actions import ( - SetPipetteMovementSpeedAction, - SucceedCommandAction, -) -from opentrons.protocol_engine.state.pipettes import ( - PipetteStore, - PipetteState, - CurrentDeckPoint, - StaticPipetteConfig, - BoundingNozzlesOffsets, - PipetteBoundingBoxOffsets, -) -from opentrons.protocol_engine.resources.pipette_data_provider import ( - LoadedStaticPipetteData, -) - -from .command_fixtures import ( - create_load_pipette_command, - create_aspirate_command, - create_aspirate_in_place_command, - create_dispense_command, - create_dispense_in_place_command, - create_pick_up_tip_command, - create_drop_tip_command, - create_drop_tip_in_place_command, - create_succeeded_command, - create_unsafe_drop_tip_in_place_command, - create_blow_out_command, - create_blow_out_in_place_command, - create_prepare_to_aspirate_command, - create_unsafe_blow_out_in_place_command, -) -from ..pipette_fixtures import get_default_nozzle_map - - -@pytest.fixture -def subject() -> PipetteStore: - """Get a PipetteStore test subject for all subsequent tests.""" - return PipetteStore() - - -def test_sets_initial_state(subject: PipetteStore) -> None: - """It should initialize its state object properly.""" - result = subject.state - - assert result == PipetteState( - pipettes_by_id={}, - aspirated_volume_by_id={}, - current_location=None, - current_deck_point=CurrentDeckPoint(mount=None, deck_point=None), - attached_tip_by_id={}, - movement_speed_by_id={}, - static_config_by_id={}, - flow_rates_by_id={}, - nozzle_configuration_by_id={}, - liquid_presence_detection_by_id={}, - ) - - -def test_location_state_update(subject: PipetteStore) -> None: - """It should update pipette locations.""" - load_command = create_load_pipette_command( - pipette_id="pipette-id", - pipette_name=PipetteNameType.P300_SINGLE, - mount=MountType.RIGHT, - ) - subject.handle_action(SucceedCommandAction(command=load_command)) - - # Update the location to a well: - dummy_command = create_succeeded_command() - subject.handle_action( - SucceedCommandAction( - command=dummy_command, - state_update=update_types.StateUpdate( - pipette_location=update_types.PipetteLocationUpdate( - pipette_id="pipette-id", - new_location=update_types.Well( - labware_id="come on barbie", - well_name="let's go party", - ), - new_deck_point=DeckPoint(x=111, y=222, z=333), - ), - loaded_pipette=update_types.LoadPipetteUpdate( - pipette_id="pipette-id", - liquid_presence_detection=None, - pipette_name=PipetteNameType.P300_SINGLE, - mount=MountType.RIGHT, - ), - ), - ) - ) - assert subject.state.current_location == CurrentWell( - pipette_id="pipette-id", labware_id="come on barbie", well_name="let's go party" - ) - assert subject.state.current_deck_point == CurrentDeckPoint( - mount=MountType.RIGHT, deck_point=DeckPoint(x=111, y=222, z=333) - ) - - # Update the location to an addressable area: - subject.handle_action( - SucceedCommandAction( - command=dummy_command, - state_update=update_types.StateUpdate( - pipette_location=update_types.PipetteLocationUpdate( - pipette_id="pipette-id", - new_location=update_types.AddressableArea( - addressable_area_name="na na na na na" - ), - new_deck_point=DeckPoint(x=333, y=444, z=555), - ) - ), - ) - ) - assert subject.state.current_location == CurrentAddressableArea( - pipette_id="pipette-id", addressable_area_name="na na na na na" - ) - assert subject.state.current_deck_point == CurrentDeckPoint( - mount=MountType.RIGHT, deck_point=DeckPoint(x=333, y=444, z=555) - ) - - # Clear the logical location: - subject.handle_action( - SucceedCommandAction( - command=dummy_command, - state_update=update_types.StateUpdate( - pipette_location=update_types.PipetteLocationUpdate( - pipette_id="pipette-id", - new_location=None, - new_deck_point=update_types.NO_CHANGE, - ) - ), - ) - ) - assert subject.state.current_location is None - assert subject.state.current_deck_point == CurrentDeckPoint( - mount=MountType.RIGHT, deck_point=DeckPoint(x=333, y=444, z=555) - ) - - # Repopulate the locations, then test clearing all pipette locations: - subject.handle_action( - SucceedCommandAction( - command=dummy_command, - state_update=update_types.StateUpdate( - pipette_location=update_types.PipetteLocationUpdate( - pipette_id="pipette-id", - new_location=update_types.AddressableArea( - addressable_area_name="na na na na na" - ), - new_deck_point=DeckPoint(x=333, y=444, z=555), - ) - ), - ) - ) - assert subject.state.current_location is not None - assert subject.state.current_deck_point != CurrentDeckPoint( - mount=None, deck_point=None - ) - subject.handle_action( - SucceedCommandAction( - command=dummy_command, - state_update=update_types.StateUpdate(pipette_location=update_types.CLEAR), - ) - ) - assert subject.state.current_location is None - assert subject.state.current_deck_point == CurrentDeckPoint( - mount=None, deck_point=None - ) - - -def test_handles_load_pipette( - subject: PipetteStore, - supported_tip_fixture: pipette_definition.SupportedTipsDefinition, -) -> None: - """It should add the pipette data to the state.""" - dummy_command = create_succeeded_command() - - load_pipette_update = update_types.LoadPipetteUpdate( - pipette_id="pipette-id", - pipette_name=PipetteNameType.P300_SINGLE, - mount=MountType.LEFT, - liquid_presence_detection=None, - ) - - config = LoadedStaticPipetteData( - model="pipette-model", - display_name="pipette name", - min_volume=1.23, - max_volume=4.56, - channels=7, - flow_rates=FlowRates( - default_aspirate={"a": 1}, - default_dispense={"b": 2}, - default_blow_out={"c": 3}, - ), - tip_configuration_lookup_table={4: supported_tip_fixture}, - nominal_tip_overlap={"default": 5}, - home_position=8.9, - nozzle_offset_z=10.11, - nozzle_map=get_default_nozzle_map(PipetteNameType.P300_SINGLE), - back_left_corner_offset=Point(x=1, y=2, z=3), - front_right_corner_offset=Point(x=4, y=5, z=6), - pipette_lld_settings={}, - ) - config_update = update_types.PipetteConfigUpdate( - pipette_id="pipette-id", - config=config, - serial_number="pipette-serial", - ) - - subject.handle_action( - SucceedCommandAction( - command=dummy_command, - state_update=update_types.StateUpdate( - loaded_pipette=load_pipette_update, pipette_config=config_update - ), - ) - ) - - result = subject.state - - assert result.pipettes_by_id["pipette-id"] == LoadedPipette( - id="pipette-id", - pipetteName=PipetteNameType.P300_SINGLE, - mount=MountType.LEFT, - ) - assert result.aspirated_volume_by_id["pipette-id"] is None - assert result.movement_speed_by_id["pipette-id"] is None - assert result.attached_tip_by_id["pipette-id"] is None - - -def test_handles_pick_up_and_drop_tip(subject: PipetteStore) -> None: - """It should set tip and volume details on pick up and drop tip.""" - load_pipette_command = create_load_pipette_command( - pipette_id="abc", - pipette_name=PipetteNameType.P300_SINGLE, - mount=MountType.LEFT, - ) - - pick_up_tip_command = create_pick_up_tip_command( - pipette_id="abc", tip_volume=42, tip_length=101, tip_diameter=8.0 - ) - - drop_tip_command = create_drop_tip_command( - pipette_id="abc", - ) - - subject.handle_action( - SucceedCommandAction( - command=load_pipette_command, - state_update=update_types.StateUpdate( - loaded_pipette=update_types.LoadPipetteUpdate( - pipette_id="abc", - pipette_name=PipetteNameType.P300_SINGLE, - mount=MountType.LEFT, - liquid_presence_detection=None, - ) - ), - ) - ) - - subject.handle_action( - SucceedCommandAction( - command=pick_up_tip_command, - state_update=update_types.StateUpdate( - pipette_tip_state=update_types.PipetteTipStateUpdate( - pipette_id="abc", - tip_geometry=TipGeometry(volume=42, length=101, diameter=8.0), - ) - ), - ) - ) - assert subject.state.attached_tip_by_id["abc"] == TipGeometry( - volume=42, length=101, diameter=8.0 - ) - assert subject.state.aspirated_volume_by_id["abc"] == 0 - - subject.handle_action( - SucceedCommandAction( - command=drop_tip_command, - state_update=update_types.StateUpdate( - pipette_tip_state=update_types.PipetteTipStateUpdate( - pipette_id="abc", tip_geometry=None - ) - ), - ) - ) - assert subject.state.attached_tip_by_id["abc"] is None - assert subject.state.aspirated_volume_by_id["abc"] is None - - -def test_handles_drop_tip_in_place(subject: PipetteStore) -> None: - """It should clear tip and volume details after a drop tip in place.""" - load_pipette_command = create_load_pipette_command( - pipette_id="xyz", - pipette_name=PipetteNameType.P300_SINGLE, - mount=MountType.LEFT, - ) - - pick_up_tip_command = create_pick_up_tip_command( - pipette_id="xyz", tip_volume=42, tip_length=101, tip_diameter=8.0 - ) - - drop_tip_in_place_command = create_drop_tip_in_place_command( - pipette_id="xyz", - ) - - subject.handle_action( - SucceedCommandAction( - command=load_pipette_command, - state_update=update_types.StateUpdate( - loaded_pipette=update_types.LoadPipetteUpdate( - pipette_id="xyz", - pipette_name=PipetteNameType.P300_SINGLE, - mount=MountType.LEFT, - liquid_presence_detection=None, - ) - ), - ) - ) - subject.handle_action( - SucceedCommandAction( - command=pick_up_tip_command, - state_update=update_types.StateUpdate( - pipette_tip_state=update_types.PipetteTipStateUpdate( - pipette_id="xyz", - tip_geometry=TipGeometry(volume=42, length=101, diameter=8.0), - ) - ), - ) - ) - assert subject.state.attached_tip_by_id["xyz"] == TipGeometry( - volume=42, length=101, diameter=8.0 - ) - assert subject.state.aspirated_volume_by_id["xyz"] == 0 - - subject.handle_action( - SucceedCommandAction( - command=drop_tip_in_place_command, - state_update=update_types.StateUpdate( - pipette_tip_state=update_types.PipetteTipStateUpdate( - pipette_id="xyz", tip_geometry=None - ) - ), - ) - ) - assert subject.state.attached_tip_by_id["xyz"] is None - assert subject.state.aspirated_volume_by_id["xyz"] is None - - -def test_handles_unsafe_drop_tip_in_place(subject: PipetteStore) -> None: - """It should clear tip and volume details after a drop tip in place.""" - load_pipette_command = create_load_pipette_command( - pipette_id="xyz", - pipette_name=PipetteNameType.P300_SINGLE, - mount=MountType.LEFT, - ) - - pick_up_tip_command = create_pick_up_tip_command( - pipette_id="xyz", tip_volume=42, tip_length=101, tip_diameter=8.0 - ) - - unsafe_drop_tip_in_place_command = create_unsafe_drop_tip_in_place_command( - pipette_id="xyz", - ) - - subject.handle_action( - SucceedCommandAction( - command=load_pipette_command, - state_update=update_types.StateUpdate( - loaded_pipette=update_types.LoadPipetteUpdate( - pipette_id="xyz", - pipette_name=PipetteNameType.P300_SINGLE, - mount=MountType.LEFT, - liquid_presence_detection=None, - ) - ), - ) - ) - subject.handle_action( - SucceedCommandAction( - command=pick_up_tip_command, - state_update=update_types.StateUpdate( - pipette_tip_state=update_types.PipetteTipStateUpdate( - pipette_id="xyz", - tip_geometry=TipGeometry(volume=42, length=101, diameter=8.0), - ) - ), - ) - ) - assert subject.state.attached_tip_by_id["xyz"] == TipGeometry( - volume=42, length=101, diameter=8.0 - ) - assert subject.state.aspirated_volume_by_id["xyz"] == 0 - - subject.handle_action( - SucceedCommandAction( - command=unsafe_drop_tip_in_place_command, - state_update=update_types.StateUpdate( - pipette_tip_state=update_types.PipetteTipStateUpdate( - pipette_id="xyz", tip_geometry=None - ) - ), - ) - ) - assert subject.state.attached_tip_by_id["xyz"] is None - assert subject.state.aspirated_volume_by_id["xyz"] is None - - -@pytest.mark.parametrize( - "aspirate_command", - [ - create_aspirate_command(pipette_id="pipette-id", volume=42, flow_rate=1.23), - create_aspirate_in_place_command( - pipette_id="pipette-id", volume=42, flow_rate=1.23 - ), - ], -) -def test_aspirate_adds_volume( - subject: PipetteStore, aspirate_command: cmd.Command -) -> None: - """It should add volume to pipette after an aspirate.""" - load_command = create_load_pipette_command( - pipette_id="pipette-id", - pipette_name=PipetteNameType.P300_SINGLE, - mount=MountType.LEFT, - ) - - subject.handle_action( - SucceedCommandAction( - command=load_command, - state_update=update_types.StateUpdate( - loaded_pipette=update_types.LoadPipetteUpdate( - pipette_id="pipette-id", - pipette_name=PipetteNameType.P300_SINGLE, - mount=MountType.LEFT, - liquid_presence_detection=None, - ) - ), - ) - ) - subject.handle_action(SucceedCommandAction(command=aspirate_command)) - - assert subject.state.aspirated_volume_by_id["pipette-id"] == 42 - - subject.handle_action(SucceedCommandAction(command=aspirate_command)) - - assert subject.state.aspirated_volume_by_id["pipette-id"] == 84 - - -@pytest.mark.parametrize( - "dispense_command", - [ - create_dispense_command(pipette_id="pipette-id", volume=21, flow_rate=1.23), - create_dispense_in_place_command( - pipette_id="pipette-id", - volume=21, - flow_rate=1.23, - ), - ], -) -def test_dispense_subtracts_volume( - subject: PipetteStore, dispense_command: cmd.Command -) -> None: - """It should subtract volume from pipette after a dispense.""" - load_command = create_load_pipette_command( - pipette_id="pipette-id", - pipette_name=PipetteNameType.P300_SINGLE, - mount=MountType.LEFT, - ) - aspirate_command = create_aspirate_command( - pipette_id="pipette-id", - volume=42, - flow_rate=1.23, - ) - - subject.handle_action( - SucceedCommandAction( - command=load_command, - state_update=update_types.StateUpdate( - loaded_pipette=update_types.LoadPipetteUpdate( - pipette_id="pipette-id", - pipette_name=PipetteNameType.P300_SINGLE, - mount=MountType.LEFT, - liquid_presence_detection=None, - ) - ), - ) - ) - subject.handle_action(SucceedCommandAction(command=aspirate_command)) - subject.handle_action(SucceedCommandAction(command=dispense_command)) - - assert subject.state.aspirated_volume_by_id["pipette-id"] == 21 - - subject.handle_action(SucceedCommandAction(command=dispense_command)) - - assert subject.state.aspirated_volume_by_id["pipette-id"] == 0 - - -@pytest.mark.parametrize( - "blow_out_command", - [ - create_blow_out_command("pipette-id", 1.23), - create_blow_out_in_place_command("pipette-id", 1.23), - create_unsafe_blow_out_in_place_command("pipette-id", 1.23), - ], -) -def test_blow_out_clears_volume( - subject: PipetteStore, blow_out_command: cmd.Command -) -> None: - """It should wipe out the aspirated volume after a blowOut.""" - load_command = create_load_pipette_command( - pipette_id="pipette-id", - pipette_name=PipetteNameType.P300_SINGLE, - mount=MountType.LEFT, - ) - aspirate_command = create_aspirate_command( - pipette_id="pipette-id", - volume=42, - flow_rate=1.23, - ) - - subject.handle_action( - SucceedCommandAction( - command=load_command, - state_update=update_types.StateUpdate( - loaded_pipette=update_types.LoadPipetteUpdate( - pipette_id="pipette-id", - pipette_name=PipetteNameType.P300_SINGLE, - mount=MountType.LEFT, - liquid_presence_detection=None, - ) - ), - ) - ) - subject.handle_action(SucceedCommandAction(command=aspirate_command)) - subject.handle_action(SucceedCommandAction(command=blow_out_command)) - - assert subject.state.aspirated_volume_by_id["pipette-id"] is None - - -def test_set_movement_speed(subject: PipetteStore) -> None: - """It should issue an action to set the movement speed.""" - pipette_id = "pipette-id" - load_pipette_command = create_load_pipette_command( - pipette_id=pipette_id, - pipette_name=PipetteNameType.P300_SINGLE, - mount=MountType.LEFT, - ) - subject.handle_action(SucceedCommandAction(command=load_pipette_command)) - subject.handle_action( - SetPipetteMovementSpeedAction(pipette_id=pipette_id, speed=123.456) - ) - assert subject.state.movement_speed_by_id[pipette_id] == 123.456 - - -def test_add_pipette_config( - subject: PipetteStore, - supported_tip_fixture: pipette_definition.SupportedTipsDefinition, -) -> None: - """It should update state from any pipette config private result.""" - command = cmd.LoadPipette.construct( # type: ignore[call-arg] - params=cmd.LoadPipetteParams.construct( - mount=MountType.LEFT, pipetteName="p300_single" # type: ignore[arg-type] - ), - result=cmd.LoadPipetteResult(pipetteId="pipette-id"), - ) - config = LoadedStaticPipetteData( - model="pipette-model", - display_name="pipette name", - min_volume=1.23, - max_volume=4.56, - channels=7, - flow_rates=FlowRates( - default_aspirate={"a": 1}, - default_dispense={"b": 2}, - default_blow_out={"c": 3}, - ), - tip_configuration_lookup_table={4: supported_tip_fixture}, - nominal_tip_overlap={"default": 5}, - home_position=8.9, - nozzle_offset_z=10.11, - nozzle_map=get_default_nozzle_map(PipetteNameType.P300_SINGLE), - back_left_corner_offset=Point(x=1, y=2, z=3), - front_right_corner_offset=Point(x=4, y=5, z=6), - pipette_lld_settings={}, - ) - - subject.handle_action( - SucceedCommandAction( - command=command, - state_update=update_types.StateUpdate( - pipette_config=update_types.PipetteConfigUpdate( - pipette_id="pipette-id", - config=config, - serial_number="pipette-serial", - ) - ), - ) - ) - - assert subject.state.static_config_by_id["pipette-id"] == StaticPipetteConfig( - model="pipette-model", - serial_number="pipette-serial", - display_name="pipette name", - min_volume=1.23, - max_volume=4.56, - channels=7, - tip_configuration_lookup_table={4: supported_tip_fixture}, - nominal_tip_overlap={"default": 5}, - home_position=8.9, - nozzle_offset_z=10.11, - bounding_nozzle_offsets=BoundingNozzlesOffsets( - back_left_offset=Point(x=0, y=0, z=0), - front_right_offset=Point(x=0, y=0, z=0), - ), - default_nozzle_map=get_default_nozzle_map(PipetteNameType.P300_SINGLE), - pipette_bounding_box_offsets=PipetteBoundingBoxOffsets( - back_left_corner=Point(x=1, y=2, z=3), - front_right_corner=Point(x=4, y=5, z=6), - front_left_corner=Point(x=1, y=5, z=3), - back_right_corner=Point(x=4, y=2, z=3), - ), - lld_settings={}, - ) - assert subject.state.flow_rates_by_id["pipette-id"].default_aspirate == {"a": 1.0} - assert subject.state.flow_rates_by_id["pipette-id"].default_dispense == {"b": 2.0} - assert subject.state.flow_rates_by_id["pipette-id"].default_blow_out == {"c": 3.0} - - -@pytest.mark.parametrize( - "previous", - [ - create_blow_out_command(pipette_id="pipette-id", flow_rate=1.0), - create_dispense_command(pipette_id="pipette-id", volume=10, flow_rate=1.0), - ], -) -def test_prepare_to_aspirate_marks_pipette_ready( - subject: PipetteStore, previous: cmd.Command -) -> None: - """It should mark a pipette as ready to aspirate.""" - load_pipette_command = create_load_pipette_command( - pipette_id="pipette-id", - pipette_name=PipetteNameType.P50_MULTI_FLEX, - mount=MountType.LEFT, - ) - pick_up_tip_command = create_pick_up_tip_command( - pipette_id="pipette-id", tip_volume=42, tip_length=101, tip_diameter=8.0 - ) - subject.handle_action( - SucceedCommandAction( - command=load_pipette_command, - state_update=update_types.StateUpdate( - loaded_pipette=update_types.LoadPipetteUpdate( - pipette_id="pipette-id", - pipette_name=PipetteNameType.P50_MULTI_FLEX, - mount=MountType.LEFT, - liquid_presence_detection=None, - ) - ), - ) - ) - subject.handle_action( - SucceedCommandAction( - command=pick_up_tip_command, - state_update=update_types.StateUpdate( - pipette_tip_state=update_types.PipetteTipStateUpdate( - pipette_id="pipette-id", - tip_geometry=TipGeometry(volume=42, length=101, diameter=8.0), - ) - ), - ) - ) - - subject.handle_action( - SucceedCommandAction( - command=previous, - ) - ) - - prepare_to_aspirate_command = create_prepare_to_aspirate_command( - pipette_id="pipette-id" - ) - subject.handle_action(SucceedCommandAction(command=prepare_to_aspirate_command)) - assert subject.state.aspirated_volume_by_id["pipette-id"] == 0.0 diff --git a/api/tests/opentrons/protocol_engine/state/test_pipette_store_old.py b/api/tests/opentrons/protocol_engine/state/test_pipette_store_old.py new file mode 100644 index 00000000000..9e4db725415 --- /dev/null +++ b/api/tests/opentrons/protocol_engine/state/test_pipette_store_old.py @@ -0,0 +1,941 @@ +"""Tests for pipette state changes in the protocol_engine state store. + +DEPRECATED: Testing PipetteStore independently of PipetteView is no longer helpful. +Try to add new tests to test_pipette_state.py, where they can be tested together, +treating PipetteState as a private implementation detail. +""" +import pytest + +from opentrons_shared_data.pipette.types import PipetteNameType +from opentrons_shared_data.pipette import pipette_definition + +from opentrons.protocol_engine.state import update_types +from opentrons.types import MountType, Point +from opentrons.protocol_engine import commands as cmd +from opentrons.protocol_engine.types import ( + CurrentAddressableArea, + DeckPoint, + LoadedPipette, + FlowRates, + CurrentWell, + TipGeometry, + AspiratedFluid, + FluidKind, +) +from opentrons.protocol_engine.actions import ( + SetPipetteMovementSpeedAction, + SucceedCommandAction, +) +from opentrons.protocol_engine.state.pipettes import ( + PipetteStore, + PipetteState, + CurrentDeckPoint, + StaticPipetteConfig, + BoundingNozzlesOffsets, + PipetteBoundingBoxOffsets, +) +from opentrons.protocol_engine.resources.pipette_data_provider import ( + LoadedStaticPipetteData, +) +from opentrons.protocol_engine.state.fluid_stack import FluidStack + +from .command_fixtures import ( + create_load_pipette_command, + create_aspirate_command, + create_aspirate_in_place_command, + create_dispense_command, + create_dispense_in_place_command, + create_pick_up_tip_command, + create_drop_tip_command, + create_drop_tip_in_place_command, + create_succeeded_command, + create_unsafe_drop_tip_in_place_command, + create_blow_out_command, + create_blow_out_in_place_command, + create_prepare_to_aspirate_command, + create_unsafe_blow_out_in_place_command, +) +from ..pipette_fixtures import get_default_nozzle_map + + +@pytest.fixture +def available_sensors() -> pipette_definition.AvailableSensorDefinition: + """Provide a list of sensors.""" + return pipette_definition.AvailableSensorDefinition( + sensors=["pressure", "capacitive", "environment"] + ) + + +@pytest.fixture +def subject() -> PipetteStore: + """Get a PipetteStore test subject for all subsequent tests.""" + return PipetteStore() + + +def test_sets_initial_state(subject: PipetteStore) -> None: + """It should initialize its state object properly.""" + result = subject.state + + assert result == PipetteState( + pipettes_by_id={}, + pipette_contents_by_id={}, + current_location=None, + current_deck_point=CurrentDeckPoint(mount=None, deck_point=None), + attached_tip_by_id={}, + movement_speed_by_id={}, + static_config_by_id={}, + flow_rates_by_id={}, + nozzle_configuration_by_id={}, + liquid_presence_detection_by_id={}, + ) + + +def test_location_state_update(subject: PipetteStore) -> None: + """It should update pipette locations.""" + load_command = create_load_pipette_command( + pipette_id="pipette-id", + pipette_name=PipetteNameType.P300_SINGLE, + mount=MountType.RIGHT, + ) + subject.handle_action(SucceedCommandAction(command=load_command)) + + # Update the location to a well: + dummy_command = create_succeeded_command() + subject.handle_action( + SucceedCommandAction( + command=dummy_command, + state_update=update_types.StateUpdate( + pipette_location=update_types.PipetteLocationUpdate( + pipette_id="pipette-id", + new_location=update_types.Well( + labware_id="come on barbie", + well_name="let's go party", + ), + new_deck_point=DeckPoint(x=111, y=222, z=333), + ), + loaded_pipette=update_types.LoadPipetteUpdate( + pipette_id="pipette-id", + liquid_presence_detection=None, + pipette_name=PipetteNameType.P300_SINGLE, + mount=MountType.RIGHT, + ), + ), + ) + ) + assert subject.state.current_location == CurrentWell( + pipette_id="pipette-id", labware_id="come on barbie", well_name="let's go party" + ) + assert subject.state.current_deck_point == CurrentDeckPoint( + mount=MountType.RIGHT, deck_point=DeckPoint(x=111, y=222, z=333) + ) + + # Update the location to an addressable area: + subject.handle_action( + SucceedCommandAction( + command=dummy_command, + state_update=update_types.StateUpdate( + pipette_location=update_types.PipetteLocationUpdate( + pipette_id="pipette-id", + new_location=update_types.AddressableArea( + addressable_area_name="na na na na na" + ), + new_deck_point=DeckPoint(x=333, y=444, z=555), + ) + ), + ) + ) + assert subject.state.current_location == CurrentAddressableArea( + pipette_id="pipette-id", addressable_area_name="na na na na na" + ) + assert subject.state.current_deck_point == CurrentDeckPoint( + mount=MountType.RIGHT, deck_point=DeckPoint(x=333, y=444, z=555) + ) + + # Clear the logical location: + subject.handle_action( + SucceedCommandAction( + command=dummy_command, + state_update=update_types.StateUpdate( + pipette_location=update_types.PipetteLocationUpdate( + pipette_id="pipette-id", + new_location=None, + new_deck_point=update_types.NO_CHANGE, + ) + ), + ) + ) + assert subject.state.current_location is None + assert subject.state.current_deck_point == CurrentDeckPoint( + mount=MountType.RIGHT, deck_point=DeckPoint(x=333, y=444, z=555) + ) + + # Repopulate the locations, then test clearing all pipette locations: + subject.handle_action( + SucceedCommandAction( + command=dummy_command, + state_update=update_types.StateUpdate( + pipette_location=update_types.PipetteLocationUpdate( + pipette_id="pipette-id", + new_location=update_types.AddressableArea( + addressable_area_name="na na na na na" + ), + new_deck_point=DeckPoint(x=333, y=444, z=555), + ) + ), + ) + ) + assert subject.state.current_location is not None + assert subject.state.current_deck_point != CurrentDeckPoint( + mount=None, deck_point=None + ) + subject.handle_action( + SucceedCommandAction( + command=dummy_command, + state_update=update_types.StateUpdate(pipette_location=update_types.CLEAR), + ) + ) + assert subject.state.current_location is None + assert subject.state.current_deck_point == CurrentDeckPoint( + mount=None, deck_point=None + ) + + +def test_handles_load_pipette( + subject: PipetteStore, + supported_tip_fixture: pipette_definition.SupportedTipsDefinition, + available_sensors: pipette_definition.AvailableSensorDefinition, +) -> None: + """It should add the pipette data to the state.""" + dummy_command = create_succeeded_command() + + load_pipette_update = update_types.LoadPipetteUpdate( + pipette_id="pipette-id", + pipette_name=PipetteNameType.P300_SINGLE, + mount=MountType.LEFT, + liquid_presence_detection=None, + ) + + config = LoadedStaticPipetteData( + model="pipette-model", + display_name="pipette name", + min_volume=1.23, + max_volume=4.56, + channels=7, + flow_rates=FlowRates( + default_aspirate={"a": 1}, + default_dispense={"b": 2}, + default_blow_out={"c": 3}, + ), + tip_configuration_lookup_table={4: supported_tip_fixture}, + nominal_tip_overlap={"default": 5}, + home_position=8.9, + nozzle_offset_z=10.11, + nozzle_map=get_default_nozzle_map(PipetteNameType.P300_SINGLE), + back_left_corner_offset=Point(x=1, y=2, z=3), + front_right_corner_offset=Point(x=4, y=5, z=6), + pipette_lld_settings={}, + plunger_positions={ + "top": 0.0, + "bottom": 5.0, + "blow_out": 19.0, + "drop_tip": 20.0, + }, + shaft_ul_per_mm=5.0, + available_sensors=available_sensors, + ) + config_update = update_types.PipetteConfigUpdate( + pipette_id="pipette-id", + config=config, + serial_number="pipette-serial", + ) + contents_update = update_types.PipetteUnknownFluidUpdate(pipette_id="pipette-id") + + subject.handle_action( + SucceedCommandAction( + command=dummy_command, + state_update=update_types.StateUpdate( + loaded_pipette=load_pipette_update, + pipette_config=config_update, + pipette_aspirated_fluid=contents_update, + ), + ) + ) + + result = subject.state + + assert result.pipettes_by_id["pipette-id"] == LoadedPipette( + id="pipette-id", + pipetteName=PipetteNameType.P300_SINGLE, + mount=MountType.LEFT, + ) + assert result.pipette_contents_by_id["pipette-id"] is None + assert result.movement_speed_by_id["pipette-id"] is None + assert result.attached_tip_by_id["pipette-id"] is None + + +def test_handles_pick_up_and_drop_tip(subject: PipetteStore) -> None: + """It should set tip and volume details on pick up and drop tip.""" + load_pipette_command = create_load_pipette_command( + pipette_id="abc", + pipette_name=PipetteNameType.P300_SINGLE, + mount=MountType.LEFT, + ) + + pick_up_tip_command = create_pick_up_tip_command( + pipette_id="abc", tip_volume=42, tip_length=101, tip_diameter=8.0 + ) + + drop_tip_command = create_drop_tip_command( + pipette_id="abc", + ) + + subject.handle_action( + SucceedCommandAction( + command=load_pipette_command, + state_update=update_types.StateUpdate( + loaded_pipette=update_types.LoadPipetteUpdate( + pipette_id="abc", + pipette_name=PipetteNameType.P300_SINGLE, + mount=MountType.LEFT, + liquid_presence_detection=None, + ), + pipette_aspirated_fluid=update_types.PipetteUnknownFluidUpdate( + pipette_id="abc" + ), + ), + ) + ) + + subject.handle_action( + SucceedCommandAction( + command=pick_up_tip_command, + state_update=update_types.StateUpdate( + pipette_tip_state=update_types.PipetteTipStateUpdate( + pipette_id="abc", + tip_geometry=TipGeometry(volume=42, length=101, diameter=8.0), + ), + pipette_aspirated_fluid=update_types.PipetteEmptyFluidUpdate( + pipette_id="abc" + ), + ), + ) + ) + assert subject.state.attached_tip_by_id["abc"] == TipGeometry( + volume=42, length=101, diameter=8.0 + ) + assert subject.state.pipette_contents_by_id["abc"] == FluidStack() + + subject.handle_action( + SucceedCommandAction( + command=drop_tip_command, + state_update=update_types.StateUpdate( + pipette_tip_state=update_types.PipetteTipStateUpdate( + pipette_id="abc", tip_geometry=None + ), + pipette_aspirated_fluid=update_types.PipetteUnknownFluidUpdate( + pipette_id="abc" + ), + ), + ) + ) + assert subject.state.attached_tip_by_id["abc"] is None + assert subject.state.pipette_contents_by_id["abc"] is None + + +def test_handles_drop_tip_in_place(subject: PipetteStore) -> None: + """It should clear tip and volume details after a drop tip in place.""" + load_pipette_command = create_load_pipette_command( + pipette_id="xyz", + pipette_name=PipetteNameType.P300_SINGLE, + mount=MountType.LEFT, + ) + + pick_up_tip_command = create_pick_up_tip_command( + pipette_id="xyz", tip_volume=42, tip_length=101, tip_diameter=8.0 + ) + + drop_tip_in_place_command = create_drop_tip_in_place_command( + pipette_id="xyz", + ) + + subject.handle_action( + SucceedCommandAction( + command=load_pipette_command, + state_update=update_types.StateUpdate( + loaded_pipette=update_types.LoadPipetteUpdate( + pipette_id="xyz", + pipette_name=PipetteNameType.P300_SINGLE, + mount=MountType.LEFT, + liquid_presence_detection=None, + ), + pipette_aspirated_fluid=update_types.PipetteUnknownFluidUpdate( + pipette_id="xyz" + ), + ), + ) + ) + subject.handle_action( + SucceedCommandAction( + command=pick_up_tip_command, + state_update=update_types.StateUpdate( + pipette_tip_state=update_types.PipetteTipStateUpdate( + pipette_id="xyz", + tip_geometry=TipGeometry(volume=42, length=101, diameter=8.0), + ), + pipette_aspirated_fluid=update_types.PipetteEmptyFluidUpdate( + pipette_id="xyz" + ), + ), + ) + ) + assert subject.state.attached_tip_by_id["xyz"] == TipGeometry( + volume=42, length=101, diameter=8.0 + ) + assert subject.state.pipette_contents_by_id["xyz"] == FluidStack() + + subject.handle_action( + SucceedCommandAction( + command=drop_tip_in_place_command, + state_update=update_types.StateUpdate( + pipette_tip_state=update_types.PipetteTipStateUpdate( + pipette_id="xyz", tip_geometry=None + ), + pipette_aspirated_fluid=update_types.PipetteUnknownFluidUpdate( + pipette_id="xyz" + ), + ), + ) + ) + assert subject.state.attached_tip_by_id["xyz"] is None + assert subject.state.pipette_contents_by_id["xyz"] is None + + +def test_handles_unsafe_drop_tip_in_place(subject: PipetteStore) -> None: + """It should clear tip and volume details after a drop tip in place.""" + load_pipette_command = create_load_pipette_command( + pipette_id="xyz", + pipette_name=PipetteNameType.P300_SINGLE, + mount=MountType.LEFT, + ) + + pick_up_tip_command = create_pick_up_tip_command( + pipette_id="xyz", tip_volume=42, tip_length=101, tip_diameter=8.0 + ) + + unsafe_drop_tip_in_place_command = create_unsafe_drop_tip_in_place_command( + pipette_id="xyz", + ) + + subject.handle_action( + SucceedCommandAction( + command=load_pipette_command, + state_update=update_types.StateUpdate( + loaded_pipette=update_types.LoadPipetteUpdate( + pipette_id="xyz", + pipette_name=PipetteNameType.P300_SINGLE, + mount=MountType.LEFT, + liquid_presence_detection=None, + ), + pipette_aspirated_fluid=update_types.PipetteUnknownFluidUpdate( + pipette_id="xyz" + ), + ), + ) + ) + subject.handle_action( + SucceedCommandAction( + command=pick_up_tip_command, + state_update=update_types.StateUpdate( + pipette_tip_state=update_types.PipetteTipStateUpdate( + pipette_id="xyz", + tip_geometry=TipGeometry(volume=42, length=101, diameter=8.0), + ), + pipette_aspirated_fluid=update_types.PipetteEmptyFluidUpdate( + pipette_id="xyz" + ), + ), + ) + ) + assert subject.state.attached_tip_by_id["xyz"] == TipGeometry( + volume=42, length=101, diameter=8.0 + ) + assert subject.state.pipette_contents_by_id["xyz"] == FluidStack() + + subject.handle_action( + SucceedCommandAction( + command=unsafe_drop_tip_in_place_command, + state_update=update_types.StateUpdate( + pipette_tip_state=update_types.PipetteTipStateUpdate( + pipette_id="xyz", tip_geometry=None + ), + pipette_aspirated_fluid=update_types.PipetteUnknownFluidUpdate( + pipette_id="xyz" + ), + ), + ) + ) + assert subject.state.attached_tip_by_id["xyz"] is None + assert subject.state.pipette_contents_by_id["xyz"] is None + + +@pytest.mark.parametrize( + "aspirate_command,aspirate_update", + [ + ( + create_aspirate_command(pipette_id="pipette-id", volume=42, flow_rate=1.23), + update_types.StateUpdate( + pipette_aspirated_fluid=update_types.PipetteAspiratedFluidUpdate( + pipette_id="pipette-id", + fluid=AspiratedFluid(kind=FluidKind.LIQUID, volume=42), + ) + ), + ), + ( + create_aspirate_in_place_command( + pipette_id="pipette-id", volume=42, flow_rate=1.23 + ), + update_types.StateUpdate( + pipette_aspirated_fluid=update_types.PipetteAspiratedFluidUpdate( + pipette_id="pipette-id", + fluid=AspiratedFluid(kind=FluidKind.LIQUID, volume=42), + ) + ), + ), + ], +) +def test_aspirate_adds_volume( + subject: PipetteStore, + aspirate_command: cmd.Command, + aspirate_update: update_types.StateUpdate, +) -> None: + """It should add volume to pipette after an aspirate.""" + load_command = create_load_pipette_command( + pipette_id="pipette-id", + pipette_name=PipetteNameType.P300_SINGLE, + mount=MountType.LEFT, + ) + pick_up_tip_command = create_pick_up_tip_command( + pipette_id="pipette-id", tip_volume=42, tip_length=101, tip_diameter=8.0 + ) + + subject.handle_action( + SucceedCommandAction( + command=load_command, + state_update=update_types.StateUpdate( + loaded_pipette=update_types.LoadPipetteUpdate( + pipette_id="pipette-id", + pipette_name=PipetteNameType.P300_SINGLE, + mount=MountType.LEFT, + liquid_presence_detection=None, + ), + pipette_aspirated_fluid=update_types.PipetteUnknownFluidUpdate( + pipette_id="pipette-id" + ), + ), + ) + ) + subject.handle_action( + SucceedCommandAction( + command=pick_up_tip_command, + state_update=update_types.StateUpdate( + pipette_tip_state=update_types.PipetteTipStateUpdate( + pipette_id="pipette-id", + tip_geometry=TipGeometry(volume=42, length=101, diameter=8.0), + ), + pipette_aspirated_fluid=update_types.PipetteEmptyFluidUpdate( + pipette_id="pipette-id" + ), + ), + ) + ) + subject.handle_action( + SucceedCommandAction( + command=aspirate_command, + state_update=aspirate_update, + ) + ) + + assert subject.state.pipette_contents_by_id["pipette-id"] == FluidStack( + _fluid_stack=[AspiratedFluid(kind=FluidKind.LIQUID, volume=42)] + ) + + subject.handle_action( + SucceedCommandAction(command=aspirate_command, state_update=aspirate_update) + ) + + assert subject.state.pipette_contents_by_id["pipette-id"] == FluidStack( + _fluid_stack=[AspiratedFluid(kind=FluidKind.LIQUID, volume=84)] + ) + + +@pytest.mark.parametrize( + "dispense_command,dispense_update", + [ + ( + create_dispense_command(pipette_id="pipette-id", volume=21, flow_rate=1.23), + update_types.StateUpdate( + pipette_aspirated_fluid=update_types.PipetteEjectedFluidUpdate( + pipette_id="pipette-id", volume=21 + ) + ), + ), + ( + create_dispense_in_place_command( + pipette_id="pipette-id", + volume=21, + flow_rate=1.23, + ), + update_types.StateUpdate( + pipette_aspirated_fluid=update_types.PipetteEjectedFluidUpdate( + pipette_id="pipette-id", volume=21 + ) + ), + ), + ], +) +def test_dispense_subtracts_volume( + subject: PipetteStore, + dispense_command: cmd.Command, + dispense_update: update_types.StateUpdate, +) -> None: + """It should subtract volume from pipette after a dispense.""" + load_command = create_load_pipette_command( + pipette_id="pipette-id", + pipette_name=PipetteNameType.P300_SINGLE, + mount=MountType.LEFT, + ) + pick_up_tip_command = create_pick_up_tip_command( + pipette_id="pipette-id", tip_volume=47, tip_length=101, tip_diameter=8.0 + ) + + aspirate_command = create_aspirate_command( + pipette_id="pipette-id", + volume=42, + flow_rate=1.23, + ) + + subject.handle_action( + SucceedCommandAction( + command=load_command, + state_update=update_types.StateUpdate( + loaded_pipette=update_types.LoadPipetteUpdate( + pipette_id="pipette-id", + pipette_name=PipetteNameType.P300_SINGLE, + mount=MountType.LEFT, + liquid_presence_detection=None, + ), + pipette_aspirated_fluid=update_types.PipetteUnknownFluidUpdate( + pipette_id="pipette-id" + ), + ), + ) + ) + subject.handle_action( + SucceedCommandAction( + command=pick_up_tip_command, + state_update=update_types.StateUpdate( + pipette_tip_state=update_types.PipetteTipStateUpdate( + pipette_id="pipette-id", + tip_geometry=TipGeometry(volume=47, length=101, diameter=8.0), + ), + pipette_aspirated_fluid=update_types.PipetteEmptyFluidUpdate( + pipette_id="pipette-id" + ), + ), + ) + ) + subject.handle_action( + SucceedCommandAction( + command=aspirate_command, + state_update=update_types.StateUpdate( + pipette_aspirated_fluid=update_types.PipetteAspiratedFluidUpdate( + pipette_id="pipette-id", + fluid=AspiratedFluid(kind=FluidKind.LIQUID, volume=42), + ) + ), + ) + ) + subject.handle_action( + SucceedCommandAction(command=dispense_command, state_update=dispense_update) + ) + + assert subject.state.pipette_contents_by_id["pipette-id"] == FluidStack( + _fluid_stack=[AspiratedFluid(kind=FluidKind.LIQUID, volume=21)] + ) + + subject.handle_action( + SucceedCommandAction(command=dispense_command, state_update=dispense_update) + ) + + assert subject.state.pipette_contents_by_id["pipette-id"] == FluidStack() + + +@pytest.mark.parametrize( + "blow_out_command", + [ + create_blow_out_command("pipette-id", 1.23), + create_blow_out_in_place_command("pipette-id", 1.23), + create_unsafe_blow_out_in_place_command("pipette-id", 1.23), + ], +) +def test_blow_out_clears_volume( + subject: PipetteStore, blow_out_command: cmd.Command +) -> None: + """It should wipe out the aspirated volume after a blowOut.""" + load_command = create_load_pipette_command( + pipette_id="pipette-id", + pipette_name=PipetteNameType.P300_SINGLE, + mount=MountType.LEFT, + ) + pick_up_tip_command = create_pick_up_tip_command( + pipette_id="pipette-id", tip_volume=47, tip_length=101, tip_diameter=8.0 + ) + + aspirate_command = create_aspirate_command( + pipette_id="pipette-id", + volume=42, + flow_rate=1.23, + ) + + subject.handle_action( + SucceedCommandAction( + command=load_command, + state_update=update_types.StateUpdate( + loaded_pipette=update_types.LoadPipetteUpdate( + pipette_id="pipette-id", + pipette_name=PipetteNameType.P300_SINGLE, + mount=MountType.LEFT, + liquid_presence_detection=None, + ) + ), + ) + ) + subject.handle_action( + SucceedCommandAction( + command=pick_up_tip_command, + state_update=update_types.StateUpdate( + pipette_tip_state=update_types.PipetteTipStateUpdate( + pipette_id="pipette-id", + tip_geometry=TipGeometry(volume=47, length=101, diameter=8.0), + ), + pipette_aspirated_fluid=update_types.PipetteEmptyFluidUpdate( + pipette_id="pipette-id" + ), + ), + ) + ) + subject.handle_action( + SucceedCommandAction( + command=aspirate_command, + state_update=update_types.StateUpdate( + pipette_aspirated_fluid=update_types.PipetteAspiratedFluidUpdate( + pipette_id="pipette-id", + fluid=AspiratedFluid(kind=FluidKind.LIQUID, volume=42), + ) + ), + ) + ) + subject.handle_action( + SucceedCommandAction( + command=blow_out_command, + state_update=update_types.StateUpdate( + pipette_aspirated_fluid=update_types.PipetteUnknownFluidUpdate( + pipette_id="pipette-id" + ) + ), + ) + ) + + assert subject.state.pipette_contents_by_id["pipette-id"] is None + + +def test_set_movement_speed(subject: PipetteStore) -> None: + """It should issue an action to set the movement speed.""" + pipette_id = "pipette-id" + load_pipette_command = create_load_pipette_command( + pipette_id=pipette_id, + pipette_name=PipetteNameType.P300_SINGLE, + mount=MountType.LEFT, + ) + subject.handle_action(SucceedCommandAction(command=load_pipette_command)) + subject.handle_action( + SetPipetteMovementSpeedAction(pipette_id=pipette_id, speed=123.456) + ) + assert subject.state.movement_speed_by_id[pipette_id] == 123.456 + + +def test_add_pipette_config( + subject: PipetteStore, + supported_tip_fixture: pipette_definition.SupportedTipsDefinition, + available_sensors: pipette_definition.AvailableSensorDefinition, +) -> None: + """It should update state from any pipette config private result.""" + command = cmd.LoadPipette.model_construct( + params=cmd.LoadPipetteParams.model_construct( # type: ignore[call-arg] + mount=MountType.LEFT, pipetteName="p300_single" # type: ignore[arg-type] + ), + result=cmd.LoadPipetteResult(pipetteId="pipette-id"), + ) + config = LoadedStaticPipetteData( + model="pipette-model", + display_name="pipette name", + min_volume=1.23, + max_volume=4.56, + channels=7, + flow_rates=FlowRates( + default_aspirate={"a": 1}, + default_dispense={"b": 2}, + default_blow_out={"c": 3}, + ), + tip_configuration_lookup_table={4: supported_tip_fixture}, + nominal_tip_overlap={"default": 5}, + home_position=8.9, + nozzle_offset_z=10.11, + nozzle_map=get_default_nozzle_map(PipetteNameType.P300_SINGLE), + back_left_corner_offset=Point(x=1, y=2, z=3), + front_right_corner_offset=Point(x=4, y=5, z=6), + pipette_lld_settings={}, + plunger_positions={ + "top": 0.0, + "bottom": 5.0, + "blow_out": 19.0, + "drop_tip": 20.0, + }, + shaft_ul_per_mm=5.0, + available_sensors=available_sensors, + ) + + subject.handle_action( + SucceedCommandAction( + command=command, + state_update=update_types.StateUpdate( + pipette_config=update_types.PipetteConfigUpdate( + pipette_id="pipette-id", + config=config, + serial_number="pipette-serial", + ) + ), + ) + ) + + assert subject.state.static_config_by_id["pipette-id"] == StaticPipetteConfig( + model="pipette-model", + serial_number="pipette-serial", + display_name="pipette name", + min_volume=1.23, + max_volume=4.56, + channels=7, + tip_configuration_lookup_table={4: supported_tip_fixture}, + nominal_tip_overlap={"default": 5}, + home_position=8.9, + nozzle_offset_z=10.11, + bounding_nozzle_offsets=BoundingNozzlesOffsets( + back_left_offset=Point(x=0, y=0, z=0), + front_right_offset=Point(x=0, y=0, z=0), + ), + default_nozzle_map=get_default_nozzle_map(PipetteNameType.P300_SINGLE), + pipette_bounding_box_offsets=PipetteBoundingBoxOffsets( + back_left_corner=Point(x=1, y=2, z=3), + front_right_corner=Point(x=4, y=5, z=6), + front_left_corner=Point(x=1, y=5, z=3), + back_right_corner=Point(x=4, y=2, z=3), + ), + lld_settings={}, + plunger_positions={ + "top": 0.0, + "bottom": 5.0, + "blow_out": 19.0, + "drop_tip": 20.0, + }, + shaft_ul_per_mm=5.0, + available_sensors=available_sensors, + ) + assert subject.state.flow_rates_by_id["pipette-id"].default_aspirate == {"a": 1.0} + assert subject.state.flow_rates_by_id["pipette-id"].default_dispense == {"b": 2.0} + assert subject.state.flow_rates_by_id["pipette-id"].default_blow_out == {"c": 3.0} + + +@pytest.mark.parametrize( + "previous_cmd,previous_state", + [ + ( + create_blow_out_command(pipette_id="pipette-id", flow_rate=1.0), + update_types.StateUpdate( + pipette_aspirated_fluid=update_types.PipetteUnknownFluidUpdate( + pipette_id="pipette-id" + ) + ), + ), + ( + create_dispense_command(pipette_id="pipette-id", volume=10, flow_rate=1.0), + update_types.StateUpdate( + pipette_aspirated_fluid=update_types.PipetteEjectedFluidUpdate( + pipette_id="pipette-id", volume=10 + ) + ), + ), + ], +) +def test_prepare_to_aspirate_marks_pipette_ready( + subject: PipetteStore, + previous_cmd: cmd.Command, + previous_state: update_types.StateUpdate, +) -> None: + """It should mark a pipette as ready to aspirate.""" + load_pipette_command = create_load_pipette_command( + pipette_id="pipette-id", + pipette_name=PipetteNameType.P50_MULTI_FLEX, + mount=MountType.LEFT, + ) + pick_up_tip_command = create_pick_up_tip_command( + pipette_id="pipette-id", tip_volume=42, tip_length=101, tip_diameter=8.0 + ) + subject.handle_action( + SucceedCommandAction( + command=load_pipette_command, + state_update=update_types.StateUpdate( + loaded_pipette=update_types.LoadPipetteUpdate( + pipette_id="pipette-id", + pipette_name=PipetteNameType.P50_MULTI_FLEX, + mount=MountType.LEFT, + liquid_presence_detection=None, + ), + pipette_aspirated_fluid=update_types.PipetteUnknownFluidUpdate( + pipette_id="pipette-id" + ), + ), + ) + ) + subject.handle_action( + SucceedCommandAction( + command=pick_up_tip_command, + state_update=update_types.StateUpdate( + pipette_tip_state=update_types.PipetteTipStateUpdate( + pipette_id="pipette-id", + tip_geometry=TipGeometry(volume=42, length=101, diameter=8.0), + ), + pipette_aspirated_fluid=update_types.PipetteEmptyFluidUpdate( + pipette_id="xyz" + ), + ), + ) + ) + + subject.handle_action( + SucceedCommandAction(command=previous_cmd, state_update=previous_state) + ) + + prepare_to_aspirate_command = create_prepare_to_aspirate_command( + pipette_id="pipette-id" + ) + subject.handle_action( + SucceedCommandAction( + command=prepare_to_aspirate_command, + state_update=update_types.StateUpdate( + pipette_aspirated_fluid=update_types.PipetteEmptyFluidUpdate( + pipette_id="pipette-id" + ) + ), + ) + ) + assert subject.state.pipette_contents_by_id["pipette-id"] == FluidStack() diff --git a/api/tests/opentrons/protocol_engine/state/test_pipette_view.py b/api/tests/opentrons/protocol_engine/state/test_pipette_view.py deleted file mode 100644 index 3b4d04bd967..00000000000 --- a/api/tests/opentrons/protocol_engine/state/test_pipette_view.py +++ /dev/null @@ -1,970 +0,0 @@ -"""Tests for pipette state accessors in the protocol_engine state store.""" -from collections import OrderedDict - -import pytest -from typing import cast, Dict, List, Optional, Tuple, NamedTuple - -from opentrons_shared_data.pipette.types import PipetteNameType -from opentrons_shared_data.pipette import pipette_definition -from opentrons_shared_data.pipette.pipette_definition import ValidNozzleMaps - -from opentrons.config.defaults_ot2 import Z_RETRACT_DISTANCE -from opentrons.hardware_control import CriticalPoint -from opentrons.types import MountType, Mount as HwMount, Point -from opentrons.hardware_control.dev_types import PipetteDict -from opentrons.protocol_engine import errors -from opentrons.protocol_engine.types import ( - LoadedPipette, - MotorAxis, - FlowRates, - DeckPoint, - CurrentPipetteLocation, - TipGeometry, -) -from opentrons.protocol_engine.state.pipettes import ( - PipetteState, - PipetteView, - CurrentDeckPoint, - HardwarePipette, - StaticPipetteConfig, - BoundingNozzlesOffsets, - PipetteBoundingBoxOffsets, -) -from opentrons.hardware_control.nozzle_manager import NozzleMap, NozzleConfigurationType -from opentrons.protocol_engine.errors import TipNotAttachedError, PipetteNotLoadedError - -from ..pipette_fixtures import ( - NINETY_SIX_ROWS, - NINETY_SIX_COLS, - NINETY_SIX_MAP, - EIGHT_CHANNEL_ROWS, - EIGHT_CHANNEL_COLS, - EIGHT_CHANNEL_MAP, - get_default_nozzle_map, -) - -_SAMPLE_NOZZLE_BOUNDS_OFFSETS = BoundingNozzlesOffsets( - back_left_offset=Point(x=10, y=20, z=30), front_right_offset=Point(x=40, y=50, z=60) -) -_SAMPLE_PIPETTE_BOUNDING_BOX_OFFSETS = PipetteBoundingBoxOffsets( - back_left_corner=Point(x=10, y=20, z=30), - front_right_corner=Point(x=40, y=50, z=60), - front_left_corner=Point(x=10, y=50, z=60), - back_right_corner=Point(x=40, y=20, z=60), -) - - -def get_pipette_view( - pipettes_by_id: Optional[Dict[str, LoadedPipette]] = None, - aspirated_volume_by_id: Optional[Dict[str, Optional[float]]] = None, - current_well: Optional[CurrentPipetteLocation] = None, - current_deck_point: CurrentDeckPoint = CurrentDeckPoint( - mount=None, deck_point=None - ), - attached_tip_by_id: Optional[Dict[str, Optional[TipGeometry]]] = None, - movement_speed_by_id: Optional[Dict[str, Optional[float]]] = None, - static_config_by_id: Optional[Dict[str, StaticPipetteConfig]] = None, - flow_rates_by_id: Optional[Dict[str, FlowRates]] = None, - nozzle_layout_by_id: Optional[Dict[str, NozzleMap]] = None, - liquid_presence_detection_by_id: Optional[Dict[str, bool]] = None, -) -> PipetteView: - """Get a pipette view test subject with the specified state.""" - state = PipetteState( - pipettes_by_id=pipettes_by_id or {}, - aspirated_volume_by_id=aspirated_volume_by_id or {}, - current_location=current_well, - current_deck_point=current_deck_point, - attached_tip_by_id=attached_tip_by_id or {}, - movement_speed_by_id=movement_speed_by_id or {}, - static_config_by_id=static_config_by_id or {}, - flow_rates_by_id=flow_rates_by_id or {}, - nozzle_configuration_by_id=nozzle_layout_by_id or {}, - liquid_presence_detection_by_id=liquid_presence_detection_by_id or {}, - ) - - return PipetteView(state=state) - - -def create_pipette_config( - name: str, - back_compat_names: Optional[List[str]] = None, - ready_to_aspirate: bool = False, -) -> PipetteDict: - """Create a fake but valid (enough) PipetteDict object.""" - return cast( - PipetteDict, - { - "name": name, - "back_compat_names": back_compat_names or [], - "ready_to_aspirate": ready_to_aspirate, - }, - ) - - -def test_initial_pipette_data_by_id() -> None: - """It should should raise if pipette ID doesn't exist.""" - subject = get_pipette_view() - - with pytest.raises(errors.PipetteNotLoadedError): - subject.get("asdfghjkl") - - -def test_initial_pipette_data_by_mount() -> None: - """It should return None if mount isn't present.""" - subject = get_pipette_view() - - assert subject.get_by_mount(MountType.LEFT) is None - assert subject.get_by_mount(MountType.RIGHT) is None - - -def test_get_pipette_data() -> None: - """It should get pipette data by ID and mount from the state.""" - pipette_data = LoadedPipette( - id="pipette-id", - pipetteName=PipetteNameType.P300_SINGLE, - mount=MountType.LEFT, - ) - - subject = get_pipette_view(pipettes_by_id={"pipette-id": pipette_data}) - - result_by_id = subject.get("pipette-id") - result_by_mount = subject.get_by_mount(MountType.LEFT) - - assert result_by_id == pipette_data - assert result_by_mount == pipette_data - assert subject.get_mount("pipette-id") == MountType.LEFT - - -def test_get_hardware_pipette() -> None: - """It maps a pipette ID to a config given the HC's attached pipettes.""" - pipette_config = create_pipette_config("p300_single") - attached_pipettes: Dict[HwMount, PipetteDict] = { - HwMount.LEFT: pipette_config, - HwMount.RIGHT: cast(PipetteDict, {}), - } - - subject = get_pipette_view( - pipettes_by_id={ - "left-id": LoadedPipette( - id="left-id", - mount=MountType.LEFT, - pipetteName=PipetteNameType.P300_SINGLE, - ), - "right-id": LoadedPipette( - id="right-id", - mount=MountType.RIGHT, - pipetteName=PipetteNameType.P300_MULTI, - ), - } - ) - - result = subject.get_hardware_pipette( - pipette_id="left-id", - attached_pipettes=attached_pipettes, - ) - - assert result == HardwarePipette(mount=HwMount.LEFT, config=pipette_config) - - with pytest.raises(errors.PipetteNotAttachedError): - subject.get_hardware_pipette( - pipette_id="right-id", - attached_pipettes=attached_pipettes, - ) - - -def test_get_hardware_pipette_with_back_compat() -> None: - """It maps a pipette ID to a config given the HC's attached pipettes. - - In this test, the hardware config specs "p300_single_gen2", and the - loaded pipette name in state is "p300_single," which is is allowed. - """ - pipette_config = create_pipette_config( - "p300_single_gen2", - back_compat_names=["p300_single"], - ) - attached_pipettes: Dict[HwMount, PipetteDict] = { - HwMount.LEFT: pipette_config, - HwMount.RIGHT: cast(PipetteDict, {}), - } - - subject = get_pipette_view( - pipettes_by_id={ - "pipette-id": LoadedPipette( - id="pipette-id", - mount=MountType.LEFT, - pipetteName=PipetteNameType.P300_SINGLE, - ), - } - ) - - result = subject.get_hardware_pipette( - pipette_id="pipette-id", - attached_pipettes=attached_pipettes, - ) - - assert result == HardwarePipette(mount=HwMount.LEFT, config=pipette_config) - - -def test_get_hardware_pipette_raises_with_name_mismatch() -> None: - """It maps a pipette ID to a config given the HC's attached pipettes. - - In this test, the hardware config specs "p300_single_gen2", but the - loaded pipette name in state is "p10_single," which does not match. - """ - pipette_config = create_pipette_config("p300_single_gen2") - attached_pipettes: Dict[HwMount, Optional[PipetteDict]] = { - HwMount.LEFT: pipette_config, - HwMount.RIGHT: cast(PipetteDict, {}), - } - - subject = get_pipette_view( - pipettes_by_id={ - "pipette-id": LoadedPipette( - id="pipette-id", - mount=MountType.LEFT, - pipetteName=PipetteNameType.P10_SINGLE, - ), - } - ) - - with pytest.raises(errors.PipetteNotAttachedError): - subject.get_hardware_pipette( - pipette_id="pipette-id", - attached_pipettes=attached_pipettes, - ) - - -def test_get_aspirated_volume() -> None: - """It should get the aspirate volume for a pipette.""" - subject = get_pipette_view( - aspirated_volume_by_id={ - "pipette-id": 42, - "pipette-id-none": None, - "pipette-id-no-tip": None, - }, - attached_tip_by_id={ - "pipette-id": TipGeometry(length=1, volume=2, diameter=3), - "pipette-id-none": TipGeometry(length=4, volume=5, diameter=6), - "pipette-id-no-tip": None, - }, - ) - - assert subject.get_aspirated_volume("pipette-id") == 42 - assert subject.get_aspirated_volume("pipette-id-none") is None - - with pytest.raises(errors.PipetteNotLoadedError): - subject.get_aspirated_volume("not-an-id") - - with pytest.raises(errors.TipNotAttachedError): - subject.get_aspirated_volume("pipette-id-no-tip") - - -def test_get_pipette_working_volume( - supported_tip_fixture: pipette_definition.SupportedTipsDefinition, -) -> None: - """It should get the minimum value of tip volume and max volume.""" - subject = get_pipette_view( - attached_tip_by_id={ - "pipette-id": TipGeometry(length=1, volume=1337, diameter=42.0), - }, - static_config_by_id={ - "pipette-id": StaticPipetteConfig( - min_volume=1, - max_volume=9001, - channels=5, - model="blah", - display_name="bleh", - serial_number="", - tip_configuration_lookup_table={9001: supported_tip_fixture}, - nominal_tip_overlap={}, - home_position=0, - nozzle_offset_z=0, - bounding_nozzle_offsets=_SAMPLE_NOZZLE_BOUNDS_OFFSETS, - default_nozzle_map=get_default_nozzle_map(PipetteNameType.P300_SINGLE), - pipette_bounding_box_offsets=_SAMPLE_PIPETTE_BOUNDING_BOX_OFFSETS, - lld_settings={}, - ) - }, - ) - - assert subject.get_working_volume("pipette-id") == 1337 - - -def test_get_pipette_working_volume_raises_if_tip_volume_is_none( - supported_tip_fixture: pipette_definition.SupportedTipsDefinition, -) -> None: - """Should raise an exception that no tip is attached.""" - subject = get_pipette_view( - attached_tip_by_id={ - "pipette-id": None, - }, - static_config_by_id={ - "pipette-id": StaticPipetteConfig( - min_volume=1, - max_volume=9001, - channels=5, - model="blah", - display_name="bleh", - serial_number="", - tip_configuration_lookup_table={9001: supported_tip_fixture}, - nominal_tip_overlap={}, - home_position=0, - nozzle_offset_z=0, - bounding_nozzle_offsets=_SAMPLE_NOZZLE_BOUNDS_OFFSETS, - default_nozzle_map=get_default_nozzle_map(PipetteNameType.P300_SINGLE), - pipette_bounding_box_offsets=_SAMPLE_PIPETTE_BOUNDING_BOX_OFFSETS, - lld_settings={}, - ) - }, - ) - - with pytest.raises(TipNotAttachedError): - subject.get_working_volume("pipette-id") - - with pytest.raises(PipetteNotLoadedError): - subject.get_working_volume("wrong-id") - - -def test_get_pipette_available_volume( - supported_tip_fixture: pipette_definition.SupportedTipsDefinition, -) -> None: - """It should get the available volume for a pipette.""" - subject = get_pipette_view( - attached_tip_by_id={ - "pipette-id": TipGeometry( - length=1, - diameter=2, - volume=100, - ), - }, - aspirated_volume_by_id={"pipette-id": 58}, - static_config_by_id={ - "pipette-id": StaticPipetteConfig( - min_volume=1, - max_volume=123, - channels=3, - model="blah", - display_name="bleh", - serial_number="", - tip_configuration_lookup_table={123: supported_tip_fixture}, - nominal_tip_overlap={}, - home_position=0, - nozzle_offset_z=0, - bounding_nozzle_offsets=_SAMPLE_NOZZLE_BOUNDS_OFFSETS, - default_nozzle_map=get_default_nozzle_map(PipetteNameType.P300_SINGLE), - pipette_bounding_box_offsets=_SAMPLE_PIPETTE_BOUNDING_BOX_OFFSETS, - lld_settings={}, - ), - "pipette-id-none": StaticPipetteConfig( - min_volume=1, - max_volume=123, - channels=5, - model="blah", - display_name="bleh", - serial_number="", - tip_configuration_lookup_table={123: supported_tip_fixture}, - nominal_tip_overlap={}, - home_position=0, - nozzle_offset_z=0, - bounding_nozzle_offsets=_SAMPLE_NOZZLE_BOUNDS_OFFSETS, - default_nozzle_map=get_default_nozzle_map(PipetteNameType.P300_SINGLE), - pipette_bounding_box_offsets=_SAMPLE_PIPETTE_BOUNDING_BOX_OFFSETS, - lld_settings={}, - ), - }, - ) - - assert subject.get_available_volume("pipette-id") == 42 - - -def test_get_attached_tip() -> None: - """It should get the tip-rack ID map of a pipette's attached tip.""" - subject = get_pipette_view( - attached_tip_by_id={ - "foo": TipGeometry(length=1, volume=2, diameter=3), - "bar": None, - } - ) - - assert subject.get_attached_tip("foo") == TipGeometry( - length=1, volume=2, diameter=3 - ) - assert subject.get_attached_tip("bar") is None - assert subject.get_all_attached_tips() == [ - ("foo", TipGeometry(length=1, volume=2, diameter=3)), - ] - - -def test_validate_tip_state() -> None: - """It should validate a pipette's tip attached state.""" - subject = get_pipette_view( - attached_tip_by_id={ - "has-tip": TipGeometry(length=1, volume=2, diameter=3), - "no-tip": None, - } - ) - - subject.validate_tip_state(pipette_id="has-tip", expected_has_tip=True) - subject.validate_tip_state(pipette_id="no-tip", expected_has_tip=False) - - with pytest.raises(errors.TipAttachedError): - subject.validate_tip_state(pipette_id="has-tip", expected_has_tip=False) - - with pytest.raises(errors.TipNotAttachedError): - subject.validate_tip_state(pipette_id="no-tip", expected_has_tip=True) - - -def test_get_movement_speed() -> None: - """It should return the movement speed that was set for the given pipette.""" - subject = get_pipette_view( - movement_speed_by_id={ - "pipette-with-movement-speed": 123.456, - "pipette-without-movement-speed": None, - } - ) - - assert ( - subject.get_movement_speed(pipette_id="pipette-with-movement-speed") == 123.456 - ) - assert ( - subject.get_movement_speed(pipette_id="pipette-without-movement-speed") is None - ) - - -@pytest.mark.parametrize( - ("mount", "deck_point", "expected_deck_point"), - [ - (MountType.LEFT, DeckPoint(x=1, y=2, z=3), DeckPoint(x=1, y=2, z=3)), - (MountType.LEFT, None, None), - (MountType.RIGHT, DeckPoint(x=1, y=2, z=3), None), - (None, DeckPoint(x=1, y=2, z=3), None), - (None, None, None), - ], -) -def test_get_deck_point( - mount: Optional[MountType], - deck_point: Optional[DeckPoint], - expected_deck_point: Optional[DeckPoint], -) -> None: - """It should return the deck point for the given pipette.""" - pipette_data = LoadedPipette( - id="pipette-id", - pipetteName=PipetteNameType.P300_SINGLE, - mount=MountType.LEFT, - ) - - subject = get_pipette_view( - pipettes_by_id={"pipette-id": pipette_data}, - current_deck_point=CurrentDeckPoint( - mount=MountType.LEFT, deck_point=DeckPoint(x=1, y=2, z=3) - ), - ) - - assert subject.get_deck_point(pipette_id="pipette-id") == DeckPoint(x=1, y=2, z=3) - - -def test_get_static_config( - supported_tip_fixture: pipette_definition.SupportedTipsDefinition, -) -> None: - """It should return the static pipette configuration that was set for the given pipette.""" - config = StaticPipetteConfig( - model="pipette-model", - display_name="display name", - serial_number="serial-number", - min_volume=1.23, - max_volume=4.56, - channels=9, - tip_configuration_lookup_table={4.56: supported_tip_fixture}, - nominal_tip_overlap={}, - home_position=10.12, - nozzle_offset_z=12.13, - bounding_nozzle_offsets=_SAMPLE_NOZZLE_BOUNDS_OFFSETS, - default_nozzle_map=get_default_nozzle_map(PipetteNameType.P300_SINGLE), - pipette_bounding_box_offsets=_SAMPLE_PIPETTE_BOUNDING_BOX_OFFSETS, - lld_settings={}, - ) - - subject = get_pipette_view( - pipettes_by_id={ - "pipette-id": LoadedPipette( - id="pipette-id", - mount=MountType.LEFT, - pipetteName=PipetteNameType.P300_SINGLE, - ) - }, - attached_tip_by_id={ - "pipette-id": TipGeometry(length=1, volume=4.56, diameter=3), - }, - static_config_by_id={"pipette-id": config}, - ) - - assert subject.get_config("pipette-id") == config - assert subject.get_model_name("pipette-id") == "pipette-model" - assert subject.get_display_name("pipette-id") == "display name" - assert subject.get_serial_number("pipette-id") == "serial-number" - assert subject.get_minimum_volume("pipette-id") == 1.23 - assert subject.get_maximum_volume("pipette-id") == 4.56 - assert subject.get_return_tip_scale("pipette-id") == 0.5 - assert ( - subject.get_instrument_max_height_ot2("pipette-id") - == 22.25 - Z_RETRACT_DISTANCE - ) - - -def test_get_nominal_tip_overlap( - supported_tip_fixture: pipette_definition.SupportedTipsDefinition, -) -> None: - """It should return the static pipette configuration that was set for the given pipette.""" - config = StaticPipetteConfig( - model="", - display_name="", - serial_number="", - min_volume=0, - max_volume=0, - channels=10, - tip_configuration_lookup_table={0: supported_tip_fixture}, - nominal_tip_overlap={ - "some-uri": 100, - "default": 10, - }, - home_position=0, - nozzle_offset_z=0, - bounding_nozzle_offsets=_SAMPLE_NOZZLE_BOUNDS_OFFSETS, - default_nozzle_map=get_default_nozzle_map(PipetteNameType.P300_SINGLE), - pipette_bounding_box_offsets=_SAMPLE_PIPETTE_BOUNDING_BOX_OFFSETS, - lld_settings={}, - ) - - subject = get_pipette_view(static_config_by_id={"pipette-id": config}) - - assert subject.get_nominal_tip_overlap("pipette-id", "some-uri") == 100 - assert subject.get_nominal_tip_overlap("pipette-id", "missing-uri") == 10 - - -@pytest.mark.parametrize( - ("mount", "expected_z_axis", "expected_plunger_axis"), - [ - (MountType.LEFT, MotorAxis.LEFT_Z, MotorAxis.LEFT_PLUNGER), - (MountType.RIGHT, MotorAxis.RIGHT_Z, MotorAxis.RIGHT_PLUNGER), - ], -) -def test_get_motor_axes( - mount: MountType, expected_z_axis: MotorAxis, expected_plunger_axis: MotorAxis -) -> None: - """It should get a pipette's motor axes.""" - subject = get_pipette_view( - pipettes_by_id={ - "pipette-id": LoadedPipette( - id="pipette-id", - mount=mount, - pipetteName=PipetteNameType.P300_SINGLE, - ), - }, - ) - - assert subject.get_z_axis("pipette-id") == expected_z_axis - assert subject.get_plunger_axis("pipette-id") == expected_plunger_axis - - -def test_nozzle_configuration_getters() -> None: - """Test that pipette view returns correct nozzle configuration data.""" - nozzle_map = NozzleMap.build( - physical_nozzles=OrderedDict({"A1": Point(0, 0, 0)}), - physical_rows=OrderedDict({"A": ["A1"]}), - physical_columns=OrderedDict({"1": ["A1"]}), - starting_nozzle="A1", - back_left_nozzle="A1", - front_right_nozzle="A1", - valid_nozzle_maps=ValidNozzleMaps(maps={"A1": ["A1"]}), - ) - subject = get_pipette_view(nozzle_layout_by_id={"pipette-id": nozzle_map}) - assert subject.get_nozzle_layout_type("pipette-id") == NozzleConfigurationType.FULL - assert subject.get_is_partially_configured("pipette-id") is False - assert subject.get_primary_nozzle("pipette-id") == "A1" - - -class _PipetteSpecs(NamedTuple): - tip_length: float - bounding_box_offsets: PipetteBoundingBoxOffsets - nozzle_map: NozzleMap - critical_point: Optional[CriticalPoint] - destination_position: Point - pipette_bounds_result: Tuple[Point, Point, Point, Point] - - -_pipette_spec_cases = [ - _PipetteSpecs( - # 8-channel P300, full configuration - tip_length=42, - bounding_box_offsets=PipetteBoundingBoxOffsets( - back_left_corner=Point(0.0, 31.5, 35.52), - front_right_corner=Point(0.0, -31.5, 35.52), - front_left_corner=Point(0.0, -31.5, 35.52), - back_right_corner=Point(0.0, 31.5, 35.52), - ), - nozzle_map=NozzleMap.build( - physical_nozzles=EIGHT_CHANNEL_MAP, - physical_rows=EIGHT_CHANNEL_ROWS, - physical_columns=EIGHT_CHANNEL_COLS, - starting_nozzle="A1", - back_left_nozzle="A1", - front_right_nozzle="H1", - valid_nozzle_maps=ValidNozzleMaps(maps={"Full": EIGHT_CHANNEL_COLS["1"]}), - ), - critical_point=None, - destination_position=Point(100, 200, 300), - pipette_bounds_result=( - ( - Point(x=100.0, y=200.0, z=342.0), - Point(x=100.0, y=137.0, z=342.0), - Point(x=100.0, y=200.0, z=342.0), - Point(x=100.0, y=137.0, z=342.0), - ) - ), - ), - _PipetteSpecs( - # 8-channel P300, single configuration - tip_length=42, - bounding_box_offsets=PipetteBoundingBoxOffsets( - back_left_corner=Point(0.0, 31.5, 35.52), - front_right_corner=Point(0.0, -31.5, 35.52), - front_left_corner=Point(0.0, -31.5, 35.52), - back_right_corner=Point(0.0, 31.5, 35.52), - ), - nozzle_map=NozzleMap.build( - physical_nozzles=EIGHT_CHANNEL_MAP, - physical_rows=EIGHT_CHANNEL_ROWS, - physical_columns=EIGHT_CHANNEL_COLS, - starting_nozzle="H1", - back_left_nozzle="H1", - front_right_nozzle="H1", - valid_nozzle_maps=ValidNozzleMaps(maps={"H1": ["H1"]}), - ), - critical_point=None, - destination_position=Point(100, 200, 300), - pipette_bounds_result=( - ( - Point(x=100.0, y=263.0, z=342.0), - Point(x=100.0, y=200.0, z=342.0), - Point(x=100.0, y=263.0, z=342.0), - Point(x=100.0, y=200.0, z=342.0), - ) - ), - ), - _PipetteSpecs( - # 8-channel P300, full configuration. Critical point of XY_CENTER - tip_length=42, - bounding_box_offsets=PipetteBoundingBoxOffsets( - back_left_corner=Point(0.0, 31.5, 35.52), - front_right_corner=Point(0.0, -31.5, 35.52), - front_left_corner=Point(0.0, -31.5, 35.52), - back_right_corner=Point(0.0, 31.5, 35.52), - ), - nozzle_map=NozzleMap.build( - physical_nozzles=EIGHT_CHANNEL_MAP, - physical_rows=EIGHT_CHANNEL_ROWS, - physical_columns=EIGHT_CHANNEL_COLS, - starting_nozzle="A1", - back_left_nozzle="A1", - front_right_nozzle="H1", - valid_nozzle_maps=ValidNozzleMaps(maps={"Full": EIGHT_CHANNEL_COLS["1"]}), - ), - critical_point=CriticalPoint.XY_CENTER, - destination_position=Point(100, 200, 300), - pipette_bounds_result=( - ( - Point(x=100.0, y=231.5, z=342.0), - Point(x=100.0, y=168.5, z=342.0), - Point(x=100.0, y=231.5, z=342.0), - Point(x=100.0, y=168.5, z=342.0), - ) - ), - ), - _PipetteSpecs( - # 8-channel P300, Partial A1-E1 configuration. Critical point of XY_CENTER - tip_length=42, - bounding_box_offsets=PipetteBoundingBoxOffsets( - back_left_corner=Point(0.0, 31.5, 35.52), - front_right_corner=Point(0.0, -31.5, 35.52), - front_left_corner=Point(0.0, -31.5, 35.52), - back_right_corner=Point(0.0, 31.5, 35.52), - ), - nozzle_map=NozzleMap.build( - physical_nozzles=EIGHT_CHANNEL_MAP, - physical_rows=EIGHT_CHANNEL_ROWS, - physical_columns=EIGHT_CHANNEL_COLS, - starting_nozzle="H1", - back_left_nozzle="E1", - front_right_nozzle="H1", - valid_nozzle_maps=ValidNozzleMaps( - maps={ - "H1toE1": ["E1", "F1", "G1", "H1"], - } - ), - ), - critical_point=CriticalPoint.XY_CENTER, - destination_position=Point(100, 200, 300), - pipette_bounds_result=( - ( - Point(x=100.0, y=249.5, z=342.0), - Point(x=100.0, y=186.5, z=342.0), - Point(x=100.0, y=249.5, z=342.0), - Point(x=100.0, y=186.5, z=342.0), - ) - ), - ), - _PipetteSpecs( - # 96-channel P1000, full configuration - tip_length=42, - bounding_box_offsets=PipetteBoundingBoxOffsets( - back_left_corner=Point(-36.0, -25.5, -259.15), - front_right_corner=Point(63.0, -88.5, -259.15), - front_left_corner=Point(-36.0, -88.5, -259.15), - back_right_corner=Point(63.0, -25.5, -259.15), - ), - nozzle_map=NozzleMap.build( - physical_nozzles=NINETY_SIX_MAP, - physical_rows=NINETY_SIX_ROWS, - physical_columns=NINETY_SIX_COLS, - starting_nozzle="A1", - back_left_nozzle="A1", - front_right_nozzle="H12", - valid_nozzle_maps=ValidNozzleMaps( - maps={ - "Full": sum( - [ - NINETY_SIX_ROWS["A"], - NINETY_SIX_ROWS["B"], - NINETY_SIX_ROWS["C"], - NINETY_SIX_ROWS["D"], - NINETY_SIX_ROWS["E"], - NINETY_SIX_ROWS["F"], - NINETY_SIX_ROWS["G"], - NINETY_SIX_ROWS["H"], - ], - [], - ) - } - ), - ), - critical_point=None, - destination_position=Point(100, 200, 300), - pipette_bounds_result=( - ( - Point(x=100.0, y=200.0, z=342.0), - Point(x=199.0, y=137.0, z=342.0), - Point(x=199.0, y=200.0, z=342.0), - Point(x=100.0, y=137.0, z=342.0), - ) - ), - ), - _PipetteSpecs( - # 96-channel P1000, A1 COLUMN configuration - tip_length=42, - bounding_box_offsets=PipetteBoundingBoxOffsets( - back_left_corner=Point(-36.0, -25.5, -259.15), - front_right_corner=Point(63.0, -88.5, -259.15), - front_left_corner=Point(-36.0, -88.5, -259.15), - back_right_corner=Point(63.0, -25.5, -259.15), - ), - nozzle_map=NozzleMap.build( - physical_nozzles=NINETY_SIX_MAP, - physical_rows=NINETY_SIX_ROWS, - physical_columns=NINETY_SIX_COLS, - starting_nozzle="A1", - back_left_nozzle="A1", - front_right_nozzle="H1", - valid_nozzle_maps=ValidNozzleMaps(maps={"Column1": NINETY_SIX_COLS["1"]}), - ), - critical_point=None, - destination_position=Point(100, 200, 300), - pipette_bounds_result=( - Point(100, 200, 342), - Point(199, 137, 342), - Point(199, 200, 342), - Point(100, 137, 342), - ), - ), - _PipetteSpecs( - # 96-channel P1000, A12 COLUMN configuration - tip_length=42, - bounding_box_offsets=PipetteBoundingBoxOffsets( - back_left_corner=Point(-36.0, -25.5, -259.15), - front_right_corner=Point(63.0, -88.5, -259.15), - front_left_corner=Point(-36.0, -88.5, -259.15), - back_right_corner=Point(63.0, -25.5, -259.15), - ), - nozzle_map=NozzleMap.build( - physical_nozzles=NINETY_SIX_MAP, - physical_rows=NINETY_SIX_ROWS, - physical_columns=NINETY_SIX_COLS, - starting_nozzle="A12", - back_left_nozzle="A12", - front_right_nozzle="H12", - valid_nozzle_maps=ValidNozzleMaps(maps={"Column12": NINETY_SIX_COLS["12"]}), - ), - critical_point=None, - destination_position=Point(100, 200, 300), - pipette_bounds_result=( - Point(1, 200, 342), - Point(100, 137, 342), - Point(100, 200, 342), - Point(1, 137, 342), - ), - ), - _PipetteSpecs( - # 96-channel P1000, ROW configuration - tip_length=42, - bounding_box_offsets=PipetteBoundingBoxOffsets( - back_left_corner=Point(-36.0, -25.5, -259.15), - front_right_corner=Point(63.0, -88.5, -259.15), - front_left_corner=Point(-36.0, -88.5, -259.15), - back_right_corner=Point(63.0, -25.5, -259.15), - ), - nozzle_map=NozzleMap.build( - physical_nozzles=NINETY_SIX_MAP, - physical_rows=NINETY_SIX_ROWS, - physical_columns=NINETY_SIX_COLS, - starting_nozzle="A1", - back_left_nozzle="A1", - front_right_nozzle="A12", - valid_nozzle_maps=ValidNozzleMaps(maps={"RowA": NINETY_SIX_ROWS["A"]}), - ), - critical_point=None, - destination_position=Point(100, 200, 300), - pipette_bounds_result=( - Point(100, 200, 342), - Point(199, 137, 342), - Point(199, 200, 342), - Point(100, 137, 342), - ), - ), - _PipetteSpecs( - # 96-channel P1000, ROW configuration. Critical point of XY_CENTER - tip_length=42, - bounding_box_offsets=PipetteBoundingBoxOffsets( - back_left_corner=Point(-36.0, -25.5, -259.15), - front_right_corner=Point(63.0, -88.5, -259.15), - front_left_corner=Point(-36.0, -88.5, -259.15), - back_right_corner=Point(63.0, -25.5, -259.15), - ), - nozzle_map=NozzleMap.build( - physical_nozzles=NINETY_SIX_MAP, - physical_rows=NINETY_SIX_ROWS, - physical_columns=NINETY_SIX_COLS, - starting_nozzle="A1", - back_left_nozzle="A1", - front_right_nozzle="A12", - valid_nozzle_maps=ValidNozzleMaps(maps={"RowA": NINETY_SIX_ROWS["A"]}), - ), - critical_point=CriticalPoint.XY_CENTER, - destination_position=Point(100, 200, 300), - pipette_bounds_result=( - Point(x=50.5, y=200, z=342), - Point(x=149.5, y=137, z=342), - Point(x=149.5, y=200, z=342), - Point(x=50.5, y=137, z=342), - ), - ), - _PipetteSpecs( - # 96-channel P1000, A12 COLUMN configuration. Critical point of Y_CENTER - tip_length=42, - bounding_box_offsets=PipetteBoundingBoxOffsets( - back_left_corner=Point(-36.0, -25.5, -259.15), - front_right_corner=Point(63.0, -88.5, -259.15), - front_left_corner=Point(-36.0, -88.5, -259.15), - back_right_corner=Point(63.0, -25.5, -259.15), - ), - nozzle_map=NozzleMap.build( - physical_nozzles=NINETY_SIX_MAP, - physical_rows=NINETY_SIX_ROWS, - physical_columns=NINETY_SIX_COLS, - starting_nozzle="A12", - back_left_nozzle="A12", - front_right_nozzle="H12", - valid_nozzle_maps=ValidNozzleMaps(maps={"Column12": NINETY_SIX_COLS["12"]}), - ), - critical_point=CriticalPoint.Y_CENTER, - destination_position=Point(100, 200, 300), - pipette_bounds_result=( - Point(1, 231.5, 342), - Point(100, 168.5, 342), - Point(100, 231.5, 342), - Point(1, 168.5, 342), - ), - ), - _PipetteSpecs( - # 96-channel P1000, A1 COLUMN configuration. Critical point of XY_CENTER - tip_length=42, - bounding_box_offsets=PipetteBoundingBoxOffsets( - back_left_corner=Point(-36.0, -25.5, -259.15), - front_right_corner=Point(63.0, -88.5, -259.15), - front_left_corner=Point(-36.0, -88.5, -259.15), - back_right_corner=Point(63.0, -25.5, -259.15), - ), - nozzle_map=NozzleMap.build( - physical_nozzles=NINETY_SIX_MAP, - physical_rows=NINETY_SIX_ROWS, - physical_columns=NINETY_SIX_COLS, - starting_nozzle="A1", - back_left_nozzle="A1", - front_right_nozzle="H1", - valid_nozzle_maps=ValidNozzleMaps(maps={"Column1": NINETY_SIX_COLS["1"]}), - ), - critical_point=CriticalPoint.XY_CENTER, - destination_position=Point(100, 200, 300), - pipette_bounds_result=( - Point(100, 231.5, 342), - Point(199, 168.5, 342), - Point(199, 231.5, 342), - Point(100, 168.5, 342), - ), - ), -] - - -@pytest.mark.parametrize( - argnames=_PipetteSpecs._fields, - argvalues=_pipette_spec_cases, -) -def test_get_pipette_bounds_at_location( - tip_length: float, - bounding_box_offsets: PipetteBoundingBoxOffsets, - nozzle_map: NozzleMap, - destination_position: Point, - critical_point: Optional[CriticalPoint], - pipette_bounds_result: Tuple[Point, Point, Point, Point], -) -> None: - """It should get the pipette's nozzle's bounds at the given location.""" - subject = get_pipette_view( - nozzle_layout_by_id={"pipette-id": nozzle_map}, - attached_tip_by_id={ - "pipette-id": TipGeometry(length=tip_length, diameter=123, volume=123), - }, - static_config_by_id={ - "pipette-id": StaticPipetteConfig( - min_volume=1, - max_volume=9001, - channels=5, - model="blah", - display_name="bleh", - serial_number="", - tip_configuration_lookup_table={}, - nominal_tip_overlap={}, - home_position=0, - nozzle_offset_z=0, - default_nozzle_map=get_default_nozzle_map(PipetteNameType.P300_SINGLE), - bounding_nozzle_offsets=_SAMPLE_NOZZLE_BOUNDS_OFFSETS, - pipette_bounding_box_offsets=bounding_box_offsets, - lld_settings={}, - ) - }, - ) - assert ( - subject.get_pipette_bounds_at_specified_move_to_position( - pipette_id="pipette-id", - destination_position=destination_position, - critical_point=critical_point, - ) - == pipette_bounds_result - ) diff --git a/api/tests/opentrons/protocol_engine/state/test_pipette_view_old.py b/api/tests/opentrons/protocol_engine/state/test_pipette_view_old.py new file mode 100644 index 00000000000..ce07e5fda8e --- /dev/null +++ b/api/tests/opentrons/protocol_engine/state/test_pipette_view_old.py @@ -0,0 +1,1191 @@ +"""Tests for pipette state accessors in the protocol_engine state store. + +DEPRECATED: Testing PipetteView independently of PipetteStore is no longer helpful. +Try to add new tests to test_pipette_state.py, where they can be tested together, +treating PipetteState as a private implementation detail. +""" + +from collections import OrderedDict +from typing import cast, Dict, List, Optional, Tuple, NamedTuple + +import pytest +from decoy import Decoy + + +from opentrons_shared_data.pipette.types import PipetteNameType +from opentrons_shared_data.pipette import pipette_definition +from opentrons_shared_data.pipette.pipette_definition import ( + ValidNozzleMaps, + AvailableSensorDefinition, +) + +from opentrons.config.defaults_ot2 import Z_RETRACT_DISTANCE +from opentrons.hardware_control import CriticalPoint +from opentrons.types import MountType, Mount as HwMount, Point, NozzleConfigurationType +from opentrons.hardware_control.dev_types import PipetteDict +from opentrons.protocol_engine import errors +from opentrons.protocol_engine.types import ( + LoadedPipette, + MotorAxis, + FlowRates, + DeckPoint, + CurrentPipetteLocation, + TipGeometry, +) +from opentrons.protocol_engine.state.pipettes import ( + PipetteState, + PipetteView, + CurrentDeckPoint, + HardwarePipette, + StaticPipetteConfig, + BoundingNozzlesOffsets, + PipetteBoundingBoxOffsets, +) +from opentrons.protocol_engine.state import fluid_stack +from opentrons.hardware_control.nozzle_manager import NozzleMap +from opentrons.protocol_engine.errors import TipNotAttachedError, PipetteNotLoadedError + +from ..pipette_fixtures import ( + NINETY_SIX_ROWS, + NINETY_SIX_COLS, + NINETY_SIX_MAP, + EIGHT_CHANNEL_ROWS, + EIGHT_CHANNEL_COLS, + EIGHT_CHANNEL_MAP, + get_default_nozzle_map, +) + +_SAMPLE_NOZZLE_BOUNDS_OFFSETS = BoundingNozzlesOffsets( + back_left_offset=Point(x=10, y=20, z=30), front_right_offset=Point(x=40, y=50, z=60) +) +_SAMPLE_PIPETTE_BOUNDING_BOX_OFFSETS = PipetteBoundingBoxOffsets( + back_left_corner=Point(x=10, y=20, z=30), + front_right_corner=Point(x=40, y=50, z=60), + front_left_corner=Point(x=10, y=50, z=60), + back_right_corner=Point(x=40, y=20, z=60), +) + + +@pytest.fixture +def available_sensors() -> AvailableSensorDefinition: + """Provide a list of sensors.""" + return AvailableSensorDefinition(sensors=["pressure", "capacitive", "environment"]) + + +def get_pipette_view( + pipettes_by_id: Optional[Dict[str, LoadedPipette]] = None, + current_well: Optional[CurrentPipetteLocation] = None, + current_deck_point: CurrentDeckPoint = CurrentDeckPoint( + mount=None, deck_point=None + ), + attached_tip_by_id: Optional[Dict[str, Optional[TipGeometry]]] = None, + movement_speed_by_id: Optional[Dict[str, Optional[float]]] = None, + static_config_by_id: Optional[Dict[str, StaticPipetteConfig]] = None, + flow_rates_by_id: Optional[Dict[str, FlowRates]] = None, + nozzle_layout_by_id: Optional[Dict[str, NozzleMap]] = None, + liquid_presence_detection_by_id: Optional[Dict[str, bool]] = None, + pipette_contents_by_id: Optional[ + Dict[str, Optional[fluid_stack.FluidStack]] + ] = None, +) -> PipetteView: + """Get a pipette view test subject with the specified state.""" + state = PipetteState( + pipettes_by_id=pipettes_by_id or {}, + pipette_contents_by_id=pipette_contents_by_id or {}, + current_location=current_well, + current_deck_point=current_deck_point, + attached_tip_by_id=attached_tip_by_id or {}, + movement_speed_by_id=movement_speed_by_id or {}, + static_config_by_id=static_config_by_id or {}, + flow_rates_by_id=flow_rates_by_id or {}, + nozzle_configuration_by_id=nozzle_layout_by_id or {}, + liquid_presence_detection_by_id=liquid_presence_detection_by_id or {}, + ) + + return PipetteView(state=state) + + +def create_pipette_config( + name: str, + back_compat_names: Optional[List[str]] = None, + ready_to_aspirate: bool = False, +) -> PipetteDict: + """Create a fake but valid (enough) PipetteDict object.""" + return cast( + PipetteDict, + { + "name": name, + "back_compat_names": back_compat_names or [], + "ready_to_aspirate": ready_to_aspirate, + }, + ) + + +def test_initial_pipette_data_by_id() -> None: + """It should should raise if pipette ID doesn't exist.""" + subject = get_pipette_view() + + with pytest.raises(errors.PipetteNotLoadedError): + subject.get("asdfghjkl") + + +def test_initial_pipette_data_by_mount() -> None: + """It should return None if mount isn't present.""" + subject = get_pipette_view() + + assert subject.get_by_mount(MountType.LEFT) is None + assert subject.get_by_mount(MountType.RIGHT) is None + + +def test_get_pipette_data() -> None: + """It should get pipette data by ID and mount from the state.""" + pipette_data = LoadedPipette( + id="pipette-id", + pipetteName=PipetteNameType.P300_SINGLE, + mount=MountType.LEFT, + ) + + subject = get_pipette_view(pipettes_by_id={"pipette-id": pipette_data}) + + result_by_id = subject.get("pipette-id") + result_by_mount = subject.get_by_mount(MountType.LEFT) + + assert result_by_id == pipette_data + assert result_by_mount == pipette_data + assert subject.get_mount("pipette-id") == MountType.LEFT + + +def test_get_hardware_pipette() -> None: + """It maps a pipette ID to a config given the HC's attached pipettes.""" + pipette_config = create_pipette_config("p300_single") + attached_pipettes: Dict[HwMount, PipetteDict] = { + HwMount.LEFT: pipette_config, + HwMount.RIGHT: cast(PipetteDict, {}), + } + + subject = get_pipette_view( + pipettes_by_id={ + "left-id": LoadedPipette( + id="left-id", + mount=MountType.LEFT, + pipetteName=PipetteNameType.P300_SINGLE, + ), + "right-id": LoadedPipette( + id="right-id", + mount=MountType.RIGHT, + pipetteName=PipetteNameType.P300_MULTI, + ), + } + ) + + result = subject.get_hardware_pipette( + pipette_id="left-id", + attached_pipettes=attached_pipettes, + ) + + assert result == HardwarePipette(mount=HwMount.LEFT, config=pipette_config) + + with pytest.raises(errors.PipetteNotAttachedError): + subject.get_hardware_pipette( + pipette_id="right-id", + attached_pipettes=attached_pipettes, + ) + + +def test_get_hardware_pipette_with_back_compat() -> None: + """It maps a pipette ID to a config given the HC's attached pipettes. + + In this test, the hardware config specs "p300_single_gen2", and the + loaded pipette name in state is "p300_single," which is is allowed. + """ + pipette_config = create_pipette_config( + "p300_single_gen2", + back_compat_names=["p300_single"], + ) + attached_pipettes: Dict[HwMount, PipetteDict] = { + HwMount.LEFT: pipette_config, + HwMount.RIGHT: cast(PipetteDict, {}), + } + + subject = get_pipette_view( + pipettes_by_id={ + "pipette-id": LoadedPipette( + id="pipette-id", + mount=MountType.LEFT, + pipetteName=PipetteNameType.P300_SINGLE, + ), + } + ) + + result = subject.get_hardware_pipette( + pipette_id="pipette-id", + attached_pipettes=attached_pipettes, + ) + + assert result == HardwarePipette(mount=HwMount.LEFT, config=pipette_config) + + +def test_get_hardware_pipette_raises_with_name_mismatch() -> None: + """It maps a pipette ID to a config given the HC's attached pipettes. + + In this test, the hardware config specs "p300_single_gen2", but the + loaded pipette name in state is "p10_single," which does not match. + """ + pipette_config = create_pipette_config("p300_single_gen2") + attached_pipettes: Dict[HwMount, Optional[PipetteDict]] = { + HwMount.LEFT: pipette_config, + HwMount.RIGHT: cast(PipetteDict, {}), + } + + subject = get_pipette_view( + pipettes_by_id={ + "pipette-id": LoadedPipette( + id="pipette-id", + mount=MountType.LEFT, + pipetteName=PipetteNameType.P10_SINGLE, + ), + } + ) + + with pytest.raises(errors.PipetteNotAttachedError): + subject.get_hardware_pipette( + pipette_id="pipette-id", + attached_pipettes=attached_pipettes, + ) + + +def test_get_aspirated_volume(decoy: Decoy) -> None: + """It should get the aspirate volume for a pipette.""" + stack = decoy.mock(cls=fluid_stack.FluidStack) + subject = get_pipette_view( + pipette_contents_by_id={ + "pipette-id": stack, + "pipette-id-none": None, + "pipette-id-no-tip": None, + }, + attached_tip_by_id={ + "pipette-id": TipGeometry(length=1, volume=2, diameter=3), + "pipette-id-none": TipGeometry(length=4, volume=5, diameter=6), + "pipette-id-no-tip": None, + }, + ) + decoy.when(stack.aspirated_volume()).then_return(42) + + assert subject.get_aspirated_volume("pipette-id") == 42 + assert subject.get_aspirated_volume("pipette-id-none") is None + + with pytest.raises(errors.PipetteNotLoadedError): + subject.get_aspirated_volume("not-an-id") + + with pytest.raises(errors.TipNotAttachedError): + subject.get_aspirated_volume("pipette-id-no-tip") + + +def test_get_pipette_working_volume( + supported_tip_fixture: pipette_definition.SupportedTipsDefinition, + available_sensors: AvailableSensorDefinition, +) -> None: + """It should get the minimum value of tip volume and max volume.""" + subject = get_pipette_view( + attached_tip_by_id={ + "pipette-id": TipGeometry(length=1, volume=1337, diameter=42.0), + }, + static_config_by_id={ + "pipette-id": StaticPipetteConfig( + min_volume=1, + max_volume=9001, + channels=5, + model="blah", + display_name="bleh", + serial_number="", + tip_configuration_lookup_table={9001: supported_tip_fixture}, + nominal_tip_overlap={}, + home_position=0, + nozzle_offset_z=0, + bounding_nozzle_offsets=_SAMPLE_NOZZLE_BOUNDS_OFFSETS, + default_nozzle_map=get_default_nozzle_map(PipetteNameType.P300_SINGLE), + pipette_bounding_box_offsets=_SAMPLE_PIPETTE_BOUNDING_BOX_OFFSETS, + lld_settings={}, + plunger_positions={ + "top": 0.0, + "bottom": 5.0, + "blow_out": 19.0, + "drop_tip": 20.0, + }, + shaft_ul_per_mm=5.0, + available_sensors=available_sensors, + ) + }, + ) + + assert subject.get_working_volume("pipette-id") == 1337 + + +def test_get_pipette_working_volume_raises_if_tip_volume_is_none( + supported_tip_fixture: pipette_definition.SupportedTipsDefinition, + available_sensors: AvailableSensorDefinition, +) -> None: + """Should raise an exception that no tip is attached.""" + subject = get_pipette_view( + attached_tip_by_id={ + "pipette-id": None, + }, + static_config_by_id={ + "pipette-id": StaticPipetteConfig( + min_volume=1, + max_volume=9001, + channels=5, + model="blah", + display_name="bleh", + serial_number="", + tip_configuration_lookup_table={9001: supported_tip_fixture}, + nominal_tip_overlap={}, + home_position=0, + nozzle_offset_z=0, + bounding_nozzle_offsets=_SAMPLE_NOZZLE_BOUNDS_OFFSETS, + default_nozzle_map=get_default_nozzle_map(PipetteNameType.P300_SINGLE), + pipette_bounding_box_offsets=_SAMPLE_PIPETTE_BOUNDING_BOX_OFFSETS, + lld_settings={}, + plunger_positions={ + "top": 0.0, + "bottom": 5.0, + "blow_out": 19.0, + "drop_tip": 20.0, + }, + shaft_ul_per_mm=5.0, + available_sensors=available_sensors, + ) + }, + ) + + with pytest.raises(TipNotAttachedError): + subject.get_working_volume("pipette-id") + + with pytest.raises(PipetteNotLoadedError): + subject.get_working_volume("wrong-id") + + +def test_get_pipette_available_volume( + supported_tip_fixture: pipette_definition.SupportedTipsDefinition, + decoy: Decoy, + available_sensors: AvailableSensorDefinition, +) -> None: + """It should get the available volume for a pipette.""" + stack = decoy.mock(cls=fluid_stack.FluidStack) + decoy.when(stack.aspirated_volume()).then_return(58) + subject = get_pipette_view( + attached_tip_by_id={ + "pipette-id": TipGeometry( + length=1, + diameter=2, + volume=100, + ), + }, + pipette_contents_by_id={"pipette-id": stack}, + static_config_by_id={ + "pipette-id": StaticPipetteConfig( + min_volume=1, + max_volume=123, + channels=3, + model="blah", + display_name="bleh", + serial_number="", + tip_configuration_lookup_table={123: supported_tip_fixture}, + nominal_tip_overlap={}, + home_position=0, + nozzle_offset_z=0, + bounding_nozzle_offsets=_SAMPLE_NOZZLE_BOUNDS_OFFSETS, + default_nozzle_map=get_default_nozzle_map(PipetteNameType.P300_SINGLE), + pipette_bounding_box_offsets=_SAMPLE_PIPETTE_BOUNDING_BOX_OFFSETS, + lld_settings={}, + plunger_positions={ + "top": 0.0, + "bottom": 5.0, + "blow_out": 19.0, + "drop_tip": 20.0, + }, + shaft_ul_per_mm=5.0, + available_sensors=available_sensors, + ), + "pipette-id-none": StaticPipetteConfig( + min_volume=1, + max_volume=123, + channels=5, + model="blah", + display_name="bleh", + serial_number="", + tip_configuration_lookup_table={123: supported_tip_fixture}, + nominal_tip_overlap={}, + home_position=0, + nozzle_offset_z=0, + bounding_nozzle_offsets=_SAMPLE_NOZZLE_BOUNDS_OFFSETS, + default_nozzle_map=get_default_nozzle_map(PipetteNameType.P300_SINGLE), + pipette_bounding_box_offsets=_SAMPLE_PIPETTE_BOUNDING_BOX_OFFSETS, + lld_settings={}, + plunger_positions={ + "top": 0.0, + "bottom": 5.0, + "blow_out": 19.0, + "drop_tip": 20.0, + }, + shaft_ul_per_mm=5.0, + available_sensors=available_sensors, + ), + }, + ) + + assert subject.get_available_volume("pipette-id") == 42 + + +def test_get_attached_tip() -> None: + """It should get the tip-rack ID map of a pipette's attached tip.""" + subject = get_pipette_view( + attached_tip_by_id={ + "foo": TipGeometry(length=1, volume=2, diameter=3), + "bar": None, + } + ) + + assert subject.get_attached_tip("foo") == TipGeometry( + length=1, volume=2, diameter=3 + ) + assert subject.get_attached_tip("bar") is None + assert subject.get_all_attached_tips() == [ + ("foo", TipGeometry(length=1, volume=2, diameter=3)), + ] + + +def test_validate_tip_state() -> None: + """It should validate a pipette's tip attached state.""" + subject = get_pipette_view( + attached_tip_by_id={ + "has-tip": TipGeometry(length=1, volume=2, diameter=3), + "no-tip": None, + } + ) + + subject.validate_tip_state(pipette_id="has-tip", expected_has_tip=True) + subject.validate_tip_state(pipette_id="no-tip", expected_has_tip=False) + + with pytest.raises(errors.TipAttachedError): + subject.validate_tip_state(pipette_id="has-tip", expected_has_tip=False) + + with pytest.raises(errors.TipNotAttachedError): + subject.validate_tip_state(pipette_id="no-tip", expected_has_tip=True) + + +def test_get_movement_speed() -> None: + """It should return the movement speed that was set for the given pipette.""" + subject = get_pipette_view( + movement_speed_by_id={ + "pipette-with-movement-speed": 123.456, + "pipette-without-movement-speed": None, + } + ) + + assert ( + subject.get_movement_speed(pipette_id="pipette-with-movement-speed") == 123.456 + ) + assert ( + subject.get_movement_speed(pipette_id="pipette-without-movement-speed") is None + ) + + +@pytest.mark.parametrize( + ("mount", "deck_point", "expected_deck_point"), + [ + (MountType.LEFT, DeckPoint(x=1, y=2, z=3), DeckPoint(x=1, y=2, z=3)), + (MountType.LEFT, None, None), + (MountType.RIGHT, DeckPoint(x=1, y=2, z=3), None), + (None, DeckPoint(x=1, y=2, z=3), None), + (None, None, None), + ], +) +def test_get_deck_point( + mount: Optional[MountType], + deck_point: Optional[DeckPoint], + expected_deck_point: Optional[DeckPoint], +) -> None: + """It should return the deck point for the given pipette.""" + pipette_data = LoadedPipette( + id="pipette-id", + pipetteName=PipetteNameType.P300_SINGLE, + mount=MountType.LEFT, + ) + + subject = get_pipette_view( + pipettes_by_id={"pipette-id": pipette_data}, + current_deck_point=CurrentDeckPoint( + mount=MountType.LEFT, deck_point=DeckPoint(x=1, y=2, z=3) + ), + ) + + assert subject.get_deck_point(pipette_id="pipette-id") == DeckPoint(x=1, y=2, z=3) + + +def test_get_static_config( + supported_tip_fixture: pipette_definition.SupportedTipsDefinition, + available_sensors: AvailableSensorDefinition, +) -> None: + """It should return the static pipette configuration that was set for the given pipette.""" + config = StaticPipetteConfig( + model="pipette-model", + display_name="display name", + serial_number="serial-number", + min_volume=1.23, + max_volume=4.56, + channels=9, + tip_configuration_lookup_table={4.56: supported_tip_fixture}, + nominal_tip_overlap={}, + home_position=10.12, + nozzle_offset_z=12.13, + bounding_nozzle_offsets=_SAMPLE_NOZZLE_BOUNDS_OFFSETS, + default_nozzle_map=get_default_nozzle_map(PipetteNameType.P300_SINGLE), + pipette_bounding_box_offsets=_SAMPLE_PIPETTE_BOUNDING_BOX_OFFSETS, + lld_settings={}, + plunger_positions={ + "top": 0.0, + "bottom": 5.0, + "blow_out": 19.0, + "drop_tip": 20.0, + }, + shaft_ul_per_mm=5.0, + available_sensors=available_sensors, + ) + + subject = get_pipette_view( + pipettes_by_id={ + "pipette-id": LoadedPipette( + id="pipette-id", + mount=MountType.LEFT, + pipetteName=PipetteNameType.P300_SINGLE, + ) + }, + attached_tip_by_id={ + "pipette-id": TipGeometry(length=1, volume=4.56, diameter=3), + }, + static_config_by_id={"pipette-id": config}, + ) + + assert subject.get_config("pipette-id") == config + assert subject.get_model_name("pipette-id") == "pipette-model" + assert subject.get_display_name("pipette-id") == "display name" + assert subject.get_serial_number("pipette-id") == "serial-number" + assert subject.get_minimum_volume("pipette-id") == 1.23 + assert subject.get_maximum_volume("pipette-id") == 4.56 + assert subject.get_return_tip_scale("pipette-id") == 0.5 + assert ( + subject.get_instrument_max_height_ot2("pipette-id") + == 22.25 - Z_RETRACT_DISTANCE + ) + + +def test_get_nominal_tip_overlap( + supported_tip_fixture: pipette_definition.SupportedTipsDefinition, + available_sensors: AvailableSensorDefinition, +) -> None: + """It should return the static pipette configuration that was set for the given pipette.""" + config = StaticPipetteConfig( + model="", + display_name="", + serial_number="", + min_volume=0, + max_volume=0, + channels=10, + tip_configuration_lookup_table={0: supported_tip_fixture}, + nominal_tip_overlap={ + "some-uri": 100, + "default": 10, + }, + home_position=0, + nozzle_offset_z=0, + bounding_nozzle_offsets=_SAMPLE_NOZZLE_BOUNDS_OFFSETS, + default_nozzle_map=get_default_nozzle_map(PipetteNameType.P300_SINGLE), + pipette_bounding_box_offsets=_SAMPLE_PIPETTE_BOUNDING_BOX_OFFSETS, + lld_settings={}, + plunger_positions={ + "top": 0.0, + "bottom": 5.0, + "blow_out": 19.0, + "drop_tip": 20.0, + }, + shaft_ul_per_mm=5.0, + available_sensors=available_sensors, + ) + + subject = get_pipette_view(static_config_by_id={"pipette-id": config}) + + assert subject.get_nominal_tip_overlap("pipette-id", "some-uri") == 100 + assert subject.get_nominal_tip_overlap("pipette-id", "missing-uri") == 10 + + +@pytest.mark.parametrize( + ("mount", "expected_z_axis", "expected_plunger_axis"), + [ + (MountType.LEFT, MotorAxis.LEFT_Z, MotorAxis.LEFT_PLUNGER), + (MountType.RIGHT, MotorAxis.RIGHT_Z, MotorAxis.RIGHT_PLUNGER), + ], +) +def test_get_motor_axes( + mount: MountType, expected_z_axis: MotorAxis, expected_plunger_axis: MotorAxis +) -> None: + """It should get a pipette's motor axes.""" + subject = get_pipette_view( + pipettes_by_id={ + "pipette-id": LoadedPipette( + id="pipette-id", + mount=mount, + pipetteName=PipetteNameType.P300_SINGLE, + ), + }, + ) + + assert subject.get_z_axis("pipette-id") == expected_z_axis + assert subject.get_plunger_axis("pipette-id") == expected_plunger_axis + + +def test_nozzle_configuration_getters() -> None: + """Test that pipette view returns correct nozzle configuration data.""" + nozzle_map = NozzleMap.build( + physical_nozzles=OrderedDict({"A1": Point(0, 0, 0)}), + physical_rows=OrderedDict({"A": ["A1"]}), + physical_columns=OrderedDict({"1": ["A1"]}), + starting_nozzle="A1", + back_left_nozzle="A1", + front_right_nozzle="A1", + valid_nozzle_maps=ValidNozzleMaps(maps={"A1": ["A1"]}), + ) + subject = get_pipette_view(nozzle_layout_by_id={"pipette-id": nozzle_map}) + assert subject.get_nozzle_layout_type("pipette-id") == NozzleConfigurationType.FULL + assert subject.get_is_partially_configured("pipette-id") is False + assert subject.get_primary_nozzle("pipette-id") == "A1" + + +class _PipetteSpecs(NamedTuple): + tip_length: float + bounding_box_offsets: PipetteBoundingBoxOffsets + nozzle_map: NozzleMap + critical_point: Optional[CriticalPoint] + destination_position: Point + pipette_bounds_result: Tuple[Point, Point, Point, Point] + + +_pipette_spec_cases = [ + _PipetteSpecs( + # 8-channel P300, full configuration + tip_length=42, + bounding_box_offsets=PipetteBoundingBoxOffsets( + back_left_corner=Point(0.0, 31.5, 35.52), + front_right_corner=Point(0.0, -31.5, 35.52), + front_left_corner=Point(0.0, -31.5, 35.52), + back_right_corner=Point(0.0, 31.5, 35.52), + ), + nozzle_map=NozzleMap.build( + physical_nozzles=EIGHT_CHANNEL_MAP, + physical_rows=EIGHT_CHANNEL_ROWS, + physical_columns=EIGHT_CHANNEL_COLS, + starting_nozzle="A1", + back_left_nozzle="A1", + front_right_nozzle="H1", + valid_nozzle_maps=ValidNozzleMaps(maps={"Full": EIGHT_CHANNEL_COLS["1"]}), + ), + critical_point=None, + destination_position=Point(100, 200, 300), + pipette_bounds_result=( + ( + Point(x=100.0, y=200.0, z=342.0), + Point(x=100.0, y=137.0, z=342.0), + Point(x=100.0, y=200.0, z=342.0), + Point(x=100.0, y=137.0, z=342.0), + ) + ), + ), + _PipetteSpecs( + # 8-channel P300, single configuration + tip_length=42, + bounding_box_offsets=PipetteBoundingBoxOffsets( + back_left_corner=Point(0.0, 31.5, 35.52), + front_right_corner=Point(0.0, -31.5, 35.52), + front_left_corner=Point(0.0, -31.5, 35.52), + back_right_corner=Point(0.0, 31.5, 35.52), + ), + nozzle_map=NozzleMap.build( + physical_nozzles=EIGHT_CHANNEL_MAP, + physical_rows=EIGHT_CHANNEL_ROWS, + physical_columns=EIGHT_CHANNEL_COLS, + starting_nozzle="H1", + back_left_nozzle="H1", + front_right_nozzle="H1", + valid_nozzle_maps=ValidNozzleMaps(maps={"H1": ["H1"]}), + ), + critical_point=None, + destination_position=Point(100, 200, 300), + pipette_bounds_result=( + ( + Point(x=100.0, y=263.0, z=342.0), + Point(x=100.0, y=200.0, z=342.0), + Point(x=100.0, y=263.0, z=342.0), + Point(x=100.0, y=200.0, z=342.0), + ) + ), + ), + _PipetteSpecs( + # 8-channel P300, full configuration. Critical point of XY_CENTER + tip_length=42, + bounding_box_offsets=PipetteBoundingBoxOffsets( + back_left_corner=Point(0.0, 31.5, 35.52), + front_right_corner=Point(0.0, -31.5, 35.52), + front_left_corner=Point(0.0, -31.5, 35.52), + back_right_corner=Point(0.0, 31.5, 35.52), + ), + nozzle_map=NozzleMap.build( + physical_nozzles=EIGHT_CHANNEL_MAP, + physical_rows=EIGHT_CHANNEL_ROWS, + physical_columns=EIGHT_CHANNEL_COLS, + starting_nozzle="A1", + back_left_nozzle="A1", + front_right_nozzle="H1", + valid_nozzle_maps=ValidNozzleMaps(maps={"Full": EIGHT_CHANNEL_COLS["1"]}), + ), + critical_point=CriticalPoint.XY_CENTER, + destination_position=Point(100, 200, 300), + pipette_bounds_result=( + ( + Point(x=100.0, y=231.5, z=342.0), + Point(x=100.0, y=168.5, z=342.0), + Point(x=100.0, y=231.5, z=342.0), + Point(x=100.0, y=168.5, z=342.0), + ) + ), + ), + _PipetteSpecs( + # 8-channel P300, Partial A1-E1 configuration. Critical point of XY_CENTER + tip_length=42, + bounding_box_offsets=PipetteBoundingBoxOffsets( + back_left_corner=Point(0.0, 31.5, 35.52), + front_right_corner=Point(0.0, -31.5, 35.52), + front_left_corner=Point(0.0, -31.5, 35.52), + back_right_corner=Point(0.0, 31.5, 35.52), + ), + nozzle_map=NozzleMap.build( + physical_nozzles=EIGHT_CHANNEL_MAP, + physical_rows=EIGHT_CHANNEL_ROWS, + physical_columns=EIGHT_CHANNEL_COLS, + starting_nozzle="H1", + back_left_nozzle="E1", + front_right_nozzle="H1", + valid_nozzle_maps=ValidNozzleMaps( + maps={ + "H1toE1": ["E1", "F1", "G1", "H1"], + } + ), + ), + critical_point=CriticalPoint.XY_CENTER, + destination_position=Point(100, 200, 300), + pipette_bounds_result=( + ( + Point(x=100.0, y=249.5, z=342.0), + Point(x=100.0, y=186.5, z=342.0), + Point(x=100.0, y=249.5, z=342.0), + Point(x=100.0, y=186.5, z=342.0), + ) + ), + ), + _PipetteSpecs( + # 96-channel P1000, full configuration + tip_length=42, + bounding_box_offsets=PipetteBoundingBoxOffsets( + back_left_corner=Point(-36.0, -25.5, -259.15), + front_right_corner=Point(63.0, -88.5, -259.15), + front_left_corner=Point(-36.0, -88.5, -259.15), + back_right_corner=Point(63.0, -25.5, -259.15), + ), + nozzle_map=NozzleMap.build( + physical_nozzles=NINETY_SIX_MAP, + physical_rows=NINETY_SIX_ROWS, + physical_columns=NINETY_SIX_COLS, + starting_nozzle="A1", + back_left_nozzle="A1", + front_right_nozzle="H12", + valid_nozzle_maps=ValidNozzleMaps( + maps={ + "Full": sum( + [ + NINETY_SIX_ROWS["A"], + NINETY_SIX_ROWS["B"], + NINETY_SIX_ROWS["C"], + NINETY_SIX_ROWS["D"], + NINETY_SIX_ROWS["E"], + NINETY_SIX_ROWS["F"], + NINETY_SIX_ROWS["G"], + NINETY_SIX_ROWS["H"], + ], + [], + ) + } + ), + ), + critical_point=None, + destination_position=Point(100, 200, 300), + pipette_bounds_result=( + ( + Point(x=100.0, y=200.0, z=342.0), + Point(x=199.0, y=137.0, z=342.0), + Point(x=199.0, y=200.0, z=342.0), + Point(x=100.0, y=137.0, z=342.0), + ) + ), + ), + _PipetteSpecs( + # 96-channel P1000, A1 COLUMN configuration + tip_length=42, + bounding_box_offsets=PipetteBoundingBoxOffsets( + back_left_corner=Point(-36.0, -25.5, -259.15), + front_right_corner=Point(63.0, -88.5, -259.15), + front_left_corner=Point(-36.0, -88.5, -259.15), + back_right_corner=Point(63.0, -25.5, -259.15), + ), + nozzle_map=NozzleMap.build( + physical_nozzles=NINETY_SIX_MAP, + physical_rows=NINETY_SIX_ROWS, + physical_columns=NINETY_SIX_COLS, + starting_nozzle="A1", + back_left_nozzle="A1", + front_right_nozzle="H1", + valid_nozzle_maps=ValidNozzleMaps(maps={"Column1": NINETY_SIX_COLS["1"]}), + ), + critical_point=None, + destination_position=Point(100, 200, 300), + pipette_bounds_result=( + Point(100, 200, 342), + Point(199, 137, 342), + Point(199, 200, 342), + Point(100, 137, 342), + ), + ), + _PipetteSpecs( + # 96-channel P1000, A12 COLUMN configuration + tip_length=42, + bounding_box_offsets=PipetteBoundingBoxOffsets( + back_left_corner=Point(-36.0, -25.5, -259.15), + front_right_corner=Point(63.0, -88.5, -259.15), + front_left_corner=Point(-36.0, -88.5, -259.15), + back_right_corner=Point(63.0, -25.5, -259.15), + ), + nozzle_map=NozzleMap.build( + physical_nozzles=NINETY_SIX_MAP, + physical_rows=NINETY_SIX_ROWS, + physical_columns=NINETY_SIX_COLS, + starting_nozzle="A12", + back_left_nozzle="A12", + front_right_nozzle="H12", + valid_nozzle_maps=ValidNozzleMaps(maps={"Column12": NINETY_SIX_COLS["12"]}), + ), + critical_point=None, + destination_position=Point(100, 200, 300), + pipette_bounds_result=( + Point(1, 200, 342), + Point(100, 137, 342), + Point(100, 200, 342), + Point(1, 137, 342), + ), + ), + _PipetteSpecs( + # 96-channel P1000, ROW configuration + tip_length=42, + bounding_box_offsets=PipetteBoundingBoxOffsets( + back_left_corner=Point(-36.0, -25.5, -259.15), + front_right_corner=Point(63.0, -88.5, -259.15), + front_left_corner=Point(-36.0, -88.5, -259.15), + back_right_corner=Point(63.0, -25.5, -259.15), + ), + nozzle_map=NozzleMap.build( + physical_nozzles=NINETY_SIX_MAP, + physical_rows=NINETY_SIX_ROWS, + physical_columns=NINETY_SIX_COLS, + starting_nozzle="A1", + back_left_nozzle="A1", + front_right_nozzle="A12", + valid_nozzle_maps=ValidNozzleMaps(maps={"RowA": NINETY_SIX_ROWS["A"]}), + ), + critical_point=None, + destination_position=Point(100, 200, 300), + pipette_bounds_result=( + Point(100, 200, 342), + Point(199, 137, 342), + Point(199, 200, 342), + Point(100, 137, 342), + ), + ), + _PipetteSpecs( + # 96-channel P1000, ROW configuration. Critical point of XY_CENTER + tip_length=42, + bounding_box_offsets=PipetteBoundingBoxOffsets( + back_left_corner=Point(-36.0, -25.5, -259.15), + front_right_corner=Point(63.0, -88.5, -259.15), + front_left_corner=Point(-36.0, -88.5, -259.15), + back_right_corner=Point(63.0, -25.5, -259.15), + ), + nozzle_map=NozzleMap.build( + physical_nozzles=NINETY_SIX_MAP, + physical_rows=NINETY_SIX_ROWS, + physical_columns=NINETY_SIX_COLS, + starting_nozzle="A1", + back_left_nozzle="A1", + front_right_nozzle="A12", + valid_nozzle_maps=ValidNozzleMaps(maps={"RowA": NINETY_SIX_ROWS["A"]}), + ), + critical_point=CriticalPoint.XY_CENTER, + destination_position=Point(100, 200, 300), + pipette_bounds_result=( + Point(x=50.5, y=200, z=342), + Point(x=149.5, y=137, z=342), + Point(x=149.5, y=200, z=342), + Point(x=50.5, y=137, z=342), + ), + ), + _PipetteSpecs( + # 96-channel P1000, A12 COLUMN configuration. Critical point of Y_CENTER + tip_length=42, + bounding_box_offsets=PipetteBoundingBoxOffsets( + back_left_corner=Point(-36.0, -25.5, -259.15), + front_right_corner=Point(63.0, -88.5, -259.15), + front_left_corner=Point(-36.0, -88.5, -259.15), + back_right_corner=Point(63.0, -25.5, -259.15), + ), + nozzle_map=NozzleMap.build( + physical_nozzles=NINETY_SIX_MAP, + physical_rows=NINETY_SIX_ROWS, + physical_columns=NINETY_SIX_COLS, + starting_nozzle="A12", + back_left_nozzle="A12", + front_right_nozzle="H12", + valid_nozzle_maps=ValidNozzleMaps(maps={"Column12": NINETY_SIX_COLS["12"]}), + ), + critical_point=CriticalPoint.Y_CENTER, + destination_position=Point(100, 200, 300), + pipette_bounds_result=( + Point(1, 231.5, 342), + Point(100, 168.5, 342), + Point(100, 231.5, 342), + Point(1, 168.5, 342), + ), + ), + _PipetteSpecs( + # 96-channel P1000, A1 COLUMN configuration. Critical point of XY_CENTER + tip_length=42, + bounding_box_offsets=PipetteBoundingBoxOffsets( + back_left_corner=Point(-36.0, -25.5, -259.15), + front_right_corner=Point(63.0, -88.5, -259.15), + front_left_corner=Point(-36.0, -88.5, -259.15), + back_right_corner=Point(63.0, -25.5, -259.15), + ), + nozzle_map=NozzleMap.build( + physical_nozzles=NINETY_SIX_MAP, + physical_rows=NINETY_SIX_ROWS, + physical_columns=NINETY_SIX_COLS, + starting_nozzle="A1", + back_left_nozzle="A1", + front_right_nozzle="H1", + valid_nozzle_maps=ValidNozzleMaps(maps={"Column1": NINETY_SIX_COLS["1"]}), + ), + critical_point=CriticalPoint.XY_CENTER, + destination_position=Point(100, 200, 300), + pipette_bounds_result=( + Point(100, 231.5, 342), + Point(199, 168.5, 342), + Point(199, 231.5, 342), + Point(100, 168.5, 342), + ), + ), +] + + +@pytest.mark.parametrize( + argnames=_PipetteSpecs._fields, + argvalues=_pipette_spec_cases, +) +def test_get_pipette_bounds_at_location( + tip_length: float, + bounding_box_offsets: PipetteBoundingBoxOffsets, + nozzle_map: NozzleMap, + destination_position: Point, + critical_point: Optional[CriticalPoint], + pipette_bounds_result: Tuple[Point, Point, Point, Point], + available_sensors: AvailableSensorDefinition, +) -> None: + """It should get the pipette's nozzle's bounds at the given location.""" + subject = get_pipette_view( + nozzle_layout_by_id={"pipette-id": nozzle_map}, + attached_tip_by_id={ + "pipette-id": TipGeometry(length=tip_length, diameter=123, volume=123), + }, + static_config_by_id={ + "pipette-id": StaticPipetteConfig( + min_volume=1, + max_volume=9001, + channels=5, + model="blah", + display_name="bleh", + serial_number="", + tip_configuration_lookup_table={}, + nominal_tip_overlap={}, + home_position=0, + nozzle_offset_z=0, + default_nozzle_map=get_default_nozzle_map(PipetteNameType.P300_SINGLE), + bounding_nozzle_offsets=_SAMPLE_NOZZLE_BOUNDS_OFFSETS, + pipette_bounding_box_offsets=bounding_box_offsets, + lld_settings={}, + plunger_positions={ + "top": 0.0, + "bottom": 5.0, + "blow_out": 19.0, + "drop_tip": 20.0, + }, + shaft_ul_per_mm=5.0, + available_sensors=available_sensors, + ) + }, + ) + assert ( + subject.get_pipette_bounds_at_specified_move_to_position( + pipette_id="pipette-id", + destination_position=destination_position, + critical_point=critical_point, + ) + == pipette_bounds_result + ) + + +@pytest.mark.parametrize( + "nozzle_map,allowed", + [ + ( + NozzleMap.build( + physical_nozzles=NINETY_SIX_MAP, + physical_rows=NINETY_SIX_ROWS, + physical_columns=NINETY_SIX_COLS, + starting_nozzle="A1", + back_left_nozzle="A1", + front_right_nozzle="H12", + valid_nozzle_maps=ValidNozzleMaps( + maps={ + "Full": sum( + [ + NINETY_SIX_ROWS["A"], + NINETY_SIX_ROWS["B"], + NINETY_SIX_ROWS["C"], + NINETY_SIX_ROWS["D"], + NINETY_SIX_ROWS["E"], + NINETY_SIX_ROWS["F"], + NINETY_SIX_ROWS["G"], + NINETY_SIX_ROWS["H"], + ], + [], + ) + } + ), + ), + True, + ), + ( + NozzleMap.build( + physical_nozzles=NINETY_SIX_MAP, + physical_rows=NINETY_SIX_ROWS, + physical_columns=NINETY_SIX_COLS, + starting_nozzle="A1", + back_left_nozzle="A1", + front_right_nozzle="H1", + valid_nozzle_maps=ValidNozzleMaps( + maps={"Column1": NINETY_SIX_COLS["1"]} + ), + ), + True, + ), + ( + NozzleMap.build( + physical_nozzles=NINETY_SIX_MAP, + physical_rows=NINETY_SIX_ROWS, + physical_columns=NINETY_SIX_COLS, + starting_nozzle="A12", + back_left_nozzle="A12", + front_right_nozzle="H12", + valid_nozzle_maps=ValidNozzleMaps( + maps={"Column12": NINETY_SIX_COLS["12"]} + ), + ), + True, + ), + ( + NozzleMap.build( + physical_nozzles=NINETY_SIX_MAP, + physical_rows=NINETY_SIX_ROWS, + physical_columns=NINETY_SIX_COLS, + starting_nozzle="A1", + back_left_nozzle="A1", + front_right_nozzle="A1", + valid_nozzle_maps=ValidNozzleMaps(maps={"Single": ["A1"]}), + ), + True, + ), + ( + NozzleMap.build( + physical_nozzles=OrderedDict((("A1", Point(0.0, 1.0, 2.0)),)), + physical_rows=OrderedDict((("1", ["A1"]),)), + physical_columns=OrderedDict((("A", ["A1"]),)), + starting_nozzle="A1", + back_left_nozzle="A1", + front_right_nozzle="A1", + valid_nozzle_maps=ValidNozzleMaps(maps={"Single": ["A1"]}), + ), + True, + ), + ( + NozzleMap.build( + physical_nozzles=EIGHT_CHANNEL_MAP, + physical_rows=EIGHT_CHANNEL_ROWS, + physical_columns=EIGHT_CHANNEL_COLS, + starting_nozzle="A1", + back_left_nozzle="A1", + front_right_nozzle="H1", + valid_nozzle_maps=ValidNozzleMaps( + maps={"Full": EIGHT_CHANNEL_COLS["1"]} + ), + ), + True, + ), + ( + NozzleMap.build( + physical_nozzles=EIGHT_CHANNEL_MAP, + physical_rows=EIGHT_CHANNEL_ROWS, + physical_columns=EIGHT_CHANNEL_COLS, + starting_nozzle="A1", + back_left_nozzle="A1", + front_right_nozzle="A1", + valid_nozzle_maps=ValidNozzleMaps(maps={"Full": ["A1"]}), + ), + True, + ), + ( + NozzleMap.build( + physical_nozzles=NINETY_SIX_MAP, + physical_rows=NINETY_SIX_ROWS, + physical_columns=NINETY_SIX_COLS, + starting_nozzle="A5", + back_left_nozzle="A5", + front_right_nozzle="H5", + valid_nozzle_maps=ValidNozzleMaps( + maps={"Column12": NINETY_SIX_COLS["5"]} + ), + ), + False, + ), + ], +) +def test_lld_config_validation(nozzle_map: NozzleMap, allowed: bool) -> None: + """It should validate partial tip configurations for LLD.""" + pipette_id = "pipette-id" + subject = get_pipette_view( + nozzle_layout_by_id={pipette_id: nozzle_map}, + ) + assert subject.get_nozzle_configuration_supports_lld(pipette_id) == allowed diff --git a/api/tests/opentrons/protocol_engine/state/test_tip_state.py b/api/tests/opentrons/protocol_engine/state/test_tip_state.py index abb408d7418..7246a5f4cb2 100644 --- a/api/tests/opentrons/protocol_engine/state/test_tip_state.py +++ b/api/tests/opentrons/protocol_engine/state/test_tip_state.py @@ -22,6 +22,9 @@ ) from opentrons.types import DeckSlotName, Point from opentrons_shared_data.pipette.types import PipetteNameType +from opentrons_shared_data.pipette.pipette_definition import ( + AvailableSensorDefinition, +) from ..pipette_fixtures import ( NINETY_SIX_MAP, NINETY_SIX_COLS, @@ -29,7 +32,13 @@ get_default_nozzle_map, ) -_tip_rack_parameters = LabwareParameters.construct(isTiprack=True) # type: ignore[call-arg] +_tip_rack_parameters = LabwareParameters.model_construct(isTiprack=True) # type: ignore[call-arg] + + +@pytest.fixture +def available_sensors() -> AvailableSensorDefinition: + """Provide a list of sensors.""" + return AvailableSensorDefinition(sensors=["pressure", "capacitive", "environment"]) @pytest.fixture @@ -41,7 +50,7 @@ def subject() -> TipStore: @pytest.fixture def labware_definition() -> LabwareDefinition: """Get a labware definition value object.""" - return LabwareDefinition.construct( # type: ignore[call-arg] + return LabwareDefinition.model_construct( # type: ignore[call-arg] ordering=[ ["A1", "B1", "C1", "D1", "E1", "F1", "G1", "H1"], ["A2", "B2", "C2", "D2", "E2", "F2", "G2", "H2"], @@ -81,19 +90,18 @@ def load_labware_action( def _dummy_command() -> commands.Command: """Return a placeholder command.""" - return commands.Comment.construct() # type: ignore[call-arg] + return commands.Comment.model_construct() # type: ignore[call-arg] @pytest.mark.parametrize( "labware_definition", - [ - LabwareDefinition.construct(ordering=[], parameters=_tip_rack_parameters) # type: ignore[call-arg] - ], + [LabwareDefinition.model_construct(ordering=[], parameters=_tip_rack_parameters)], # type: ignore[call-arg] ) def test_get_next_tip_returns_none( load_labware_action: actions.SucceedCommandAction, subject: TipStore, supported_tip_fixture: pipette_definition.SupportedTipsDefinition, + available_sensors: AvailableSensorDefinition, ) -> None: """It should start at the first tip in the labware.""" subject.handle_action(load_labware_action) @@ -119,6 +127,14 @@ def test_get_next_tip_returns_none( back_left_corner_offset=Point(0, 0, 0), front_right_corner_offset=Point(0, 0, 0), pipette_lld_settings={}, + plunger_positions={ + "top": 0.0, + "bottom": 5.0, + "blow_out": 19.0, + "drop_tip": 20.0, + }, + shaft_ul_per_mm=5.0, + available_sensors=available_sensors, ), ) subject.handle_action( @@ -144,6 +160,7 @@ def test_get_next_tip_returns_first_tip( subject: TipStore, input_tip_amount: int, supported_tip_fixture: pipette_definition.SupportedTipsDefinition, + available_sensors: AvailableSensorDefinition, ) -> None: """It should start at the first tip in the labware.""" subject.handle_action(load_labware_action) @@ -177,6 +194,14 @@ def test_get_next_tip_returns_first_tip( back_left_corner_offset=Point(0, 0, 0), front_right_corner_offset=Point(0, 0, 0), pipette_lld_settings={}, + plunger_positions={ + "top": 0.0, + "bottom": 5.0, + "blow_out": 19.0, + "drop_tip": 20.0, + }, + shaft_ul_per_mm=5.0, + available_sensors=available_sensors, ), ) subject.handle_action( @@ -203,6 +228,7 @@ def test_get_next_tip_used_starting_tip( input_tip_amount: int, result_well_name: str, supported_tip_fixture: pipette_definition.SupportedTipsDefinition, + available_sensors: AvailableSensorDefinition, ) -> None: """It should start searching at the given starting tip.""" subject.handle_action(load_labware_action) @@ -229,6 +255,14 @@ def test_get_next_tip_used_starting_tip( back_left_corner_offset=Point(0, 0, 0), front_right_corner_offset=Point(0, 0, 0), pipette_lld_settings={}, + plunger_positions={ + "top": 0.0, + "bottom": 5.0, + "blow_out": 19.0, + "drop_tip": 20.0, + }, + shaft_ul_per_mm=5.0, + available_sensors=available_sensors, ), ) subject.handle_action( @@ -270,6 +304,7 @@ def test_get_next_tip_skips_picked_up_tip( input_starting_tip: Optional[str], result_well_name: Optional[str], supported_tip_fixture: pipette_definition.SupportedTipsDefinition, + available_sensors: AvailableSensorDefinition, ) -> None: """It should get the next tip in the column if one has been picked up.""" subject.handle_action(load_labware_action) @@ -314,6 +349,14 @@ def test_get_next_tip_skips_picked_up_tip( back_left_corner_offset=Point(0, 0, 0), front_right_corner_offset=Point(0, 0, 0), pipette_lld_settings={}, + plunger_positions={ + "top": 0.0, + "bottom": 5.0, + "blow_out": 19.0, + "drop_tip": 20.0, + }, + shaft_ul_per_mm=5.0, + available_sensors=available_sensors, ), ) subject.handle_action( @@ -351,6 +394,7 @@ def test_get_next_tip_with_starting_tip( subject: TipStore, load_labware_action: actions.SucceedCommandAction, supported_tip_fixture: pipette_definition.SupportedTipsDefinition, + available_sensors: AvailableSensorDefinition, ) -> None: """It should return the starting tip, and then the following tip after that.""" subject.handle_action(load_labware_action) @@ -377,6 +421,14 @@ def test_get_next_tip_with_starting_tip( back_left_corner_offset=Point(x=1, y=2, z=3), front_right_corner_offset=Point(x=4, y=5, z=6), pipette_lld_settings={}, + plunger_positions={ + "top": 0.0, + "bottom": 5.0, + "blow_out": 19.0, + "drop_tip": 20.0, + }, + shaft_ul_per_mm=5.0, + available_sensors=available_sensors, ), ) subject.handle_action( @@ -418,6 +470,7 @@ def test_get_next_tip_with_starting_tip_8_channel( subject: TipStore, load_labware_action: actions.SucceedCommandAction, supported_tip_fixture: pipette_definition.SupportedTipsDefinition, + available_sensors: AvailableSensorDefinition, ) -> None: """It should return the starting tip, and then the following tip after that.""" subject.handle_action(load_labware_action) @@ -444,6 +497,14 @@ def test_get_next_tip_with_starting_tip_8_channel( back_left_corner_offset=Point(0, 0, 0), front_right_corner_offset=Point(0, 0, 0), pipette_lld_settings={}, + plunger_positions={ + "top": 0.0, + "bottom": 5.0, + "blow_out": 19.0, + "drop_tip": 20.0, + }, + shaft_ul_per_mm=5.0, + available_sensors=available_sensors, ), ) subject.handle_action( @@ -488,6 +549,7 @@ def test_get_next_tip_with_1_channel_followed_by_8_channel( subject: TipStore, load_labware_action: actions.SucceedCommandAction, supported_tip_fixture: pipette_definition.SupportedTipsDefinition, + available_sensors: AvailableSensorDefinition, ) -> None: """It should return the first tip of column 2 for the 8 channel after performing a single tip pickup on column 1.""" subject.handle_action(load_labware_action) @@ -514,6 +576,14 @@ def test_get_next_tip_with_1_channel_followed_by_8_channel( back_left_corner_offset=Point(0, 0, 0), front_right_corner_offset=Point(0, 0, 0), pipette_lld_settings={}, + plunger_positions={ + "top": 0.0, + "bottom": 5.0, + "blow_out": 19.0, + "drop_tip": 20.0, + }, + shaft_ul_per_mm=5.0, + available_sensors=available_sensors, ), ) subject.handle_action( @@ -522,7 +592,6 @@ def test_get_next_tip_with_1_channel_followed_by_8_channel( command=_dummy_command(), ) ) - config_update_2 = update_types.PipetteConfigUpdate( pipette_id="pipette-id2", serial_number="pipette-serial2", @@ -545,6 +614,14 @@ def test_get_next_tip_with_1_channel_followed_by_8_channel( back_left_corner_offset=Point(0, 0, 0), front_right_corner_offset=Point(0, 0, 0), pipette_lld_settings={}, + plunger_positions={ + "top": 0.0, + "bottom": 5.0, + "blow_out": 19.0, + "drop_tip": 20.0, + }, + shaft_ul_per_mm=5.0, + available_sensors=available_sensors, ), ) subject.handle_action( @@ -589,6 +666,7 @@ def test_get_next_tip_with_starting_tip_out_of_tips( subject: TipStore, load_labware_action: actions.SucceedCommandAction, supported_tip_fixture: pipette_definition.SupportedTipsDefinition, + available_sensors: AvailableSensorDefinition, ) -> None: """It should return the starting tip of H12 and then None after that.""" subject.handle_action(load_labware_action) @@ -615,6 +693,14 @@ def test_get_next_tip_with_starting_tip_out_of_tips( back_left_corner_offset=Point(0, 0, 0), front_right_corner_offset=Point(0, 0, 0), pipette_lld_settings={}, + plunger_positions={ + "top": 0.0, + "bottom": 5.0, + "blow_out": 19.0, + "drop_tip": 20.0, + }, + shaft_ul_per_mm=5.0, + available_sensors=available_sensors, ), ) subject.handle_action( @@ -659,6 +745,7 @@ def test_get_next_tip_with_column_and_starting_tip( subject: TipStore, load_labware_action: actions.SucceedCommandAction, supported_tip_fixture: pipette_definition.SupportedTipsDefinition, + available_sensors: AvailableSensorDefinition, ) -> None: """It should return the first tip in a column, taking starting tip into account.""" subject.handle_action(load_labware_action) @@ -685,6 +772,14 @@ def test_get_next_tip_with_column_and_starting_tip( back_left_corner_offset=Point(0, 0, 0), front_right_corner_offset=Point(0, 0, 0), pipette_lld_settings={}, + plunger_positions={ + "top": 0.0, + "bottom": 5.0, + "blow_out": 19.0, + "drop_tip": 20.0, + }, + shaft_ul_per_mm=5.0, + available_sensors=available_sensors, ), ) subject.handle_action( @@ -708,6 +803,7 @@ def test_reset_tips( subject: TipStore, load_labware_action: actions.SucceedCommandAction, supported_tip_fixture: pipette_definition.SupportedTipsDefinition, + available_sensors: AvailableSensorDefinition, ) -> None: """It should be able to reset tip tracking state.""" subject.handle_action(load_labware_action) @@ -734,6 +830,14 @@ def test_reset_tips( back_left_corner_offset=Point(x=1, y=2, z=3), front_right_corner_offset=Point(x=4, y=5, z=6), pipette_lld_settings={}, + plunger_positions={ + "top": 0.0, + "bottom": 5.0, + "blow_out": 19.0, + "drop_tip": 20.0, + }, + shaft_ul_per_mm=5.0, + available_sensors=available_sensors, ), ) @@ -771,7 +875,9 @@ def get_result() -> str | None: def test_handle_pipette_config_action( - subject: TipStore, supported_tip_fixture: pipette_definition.SupportedTipsDefinition + subject: TipStore, + supported_tip_fixture: pipette_definition.SupportedTipsDefinition, + available_sensors: AvailableSensorDefinition, ) -> None: """Should add pipette channel to state.""" config_update = update_types.PipetteConfigUpdate( @@ -796,6 +902,14 @@ def test_handle_pipette_config_action( back_left_corner_offset=Point(x=1, y=2, z=3), front_right_corner_offset=Point(x=4, y=5, z=6), pipette_lld_settings={}, + plunger_positions={ + "top": 0.0, + "bottom": 5.0, + "blow_out": 19.0, + "drop_tip": 20.0, + }, + shaft_ul_per_mm=5.0, + available_sensors=available_sensors, ), ) subject.handle_action( @@ -812,9 +926,9 @@ def test_handle_pipette_config_action( @pytest.mark.parametrize( "labware_definition", [ - LabwareDefinition.construct( # type: ignore[call-arg] + LabwareDefinition.model_construct( # type: ignore[call-arg] ordering=[["A1"]], - parameters=LabwareParameters.construct(isTiprack=False), # type: ignore[call-arg] + parameters=LabwareParameters.model_construct(isTiprack=False), # type: ignore[call-arg] ) ], ) @@ -904,6 +1018,7 @@ def test_active_channels( supported_tip_fixture: pipette_definition.SupportedTipsDefinition, nozzle_map: NozzleMap, expected_channels: int, + available_sensors: AvailableSensorDefinition, ) -> None: """Should update active channels after pipette configuration change.""" # Load pipette to update state @@ -929,6 +1044,14 @@ def test_active_channels( back_left_corner_offset=Point(x=1, y=2, z=3), front_right_corner_offset=Point(x=4, y=5, z=6), pipette_lld_settings={}, + plunger_positions={ + "top": 0.0, + "bottom": 5.0, + "blow_out": 19.0, + "drop_tip": 20.0, + }, + shaft_ul_per_mm=5.0, + available_sensors=available_sensors, ), ) subject.handle_action( @@ -961,6 +1084,7 @@ def test_next_tip_uses_active_channels( subject: TipStore, supported_tip_fixture: pipette_definition.SupportedTipsDefinition, load_labware_action: actions.SucceedCommandAction, + available_sensors: AvailableSensorDefinition, ) -> None: """Test that tip tracking logic uses pipette's active channels.""" # Load labware @@ -989,6 +1113,14 @@ def test_next_tip_uses_active_channels( back_left_corner_offset=Point(x=1, y=2, z=3), front_right_corner_offset=Point(x=4, y=5, z=6), pipette_lld_settings={}, + plunger_positions={ + "top": 0.0, + "bottom": 5.0, + "blow_out": 19.0, + "drop_tip": 20.0, + }, + shaft_ul_per_mm=5.0, + available_sensors=available_sensors, ), ) subject.handle_action( @@ -1059,6 +1191,7 @@ def test_next_tip_automatic_tip_tracking_with_partial_configurations( subject: TipStore, supported_tip_fixture: pipette_definition.SupportedTipsDefinition, load_labware_action: actions.SucceedCommandAction, + available_sensors: AvailableSensorDefinition, ) -> None: """Test tip tracking logic using multiple pipette configurations.""" # Load labware @@ -1087,6 +1220,14 @@ def test_next_tip_automatic_tip_tracking_with_partial_configurations( back_left_corner_offset=Point(x=1, y=2, z=3), front_right_corner_offset=Point(x=4, y=5, z=6), pipette_lld_settings={}, + plunger_positions={ + "top": 0.0, + "bottom": 5.0, + "blow_out": 19.0, + "drop_tip": 20.0, + }, + shaft_ul_per_mm=5.0, + available_sensors=available_sensors, ), ) subject.handle_action( @@ -1211,6 +1352,7 @@ def test_next_tip_automatic_tip_tracking_tiprack_limits( subject: TipStore, supported_tip_fixture: pipette_definition.SupportedTipsDefinition, load_labware_action: actions.SucceedCommandAction, + available_sensors: AvailableSensorDefinition, ) -> None: """Test tip tracking logic to ensure once a tiprack is consumed it returns None when consuming tips using multiple pipette configurations.""" # Load labware @@ -1239,6 +1381,14 @@ def test_next_tip_automatic_tip_tracking_tiprack_limits( back_left_corner_offset=Point(x=1, y=2, z=3), front_right_corner_offset=Point(x=4, y=5, z=6), pipette_lld_settings={}, + plunger_positions={ + "top": 0.0, + "bottom": 5.0, + "blow_out": 19.0, + "drop_tip": 20.0, + }, + shaft_ul_per_mm=5.0, + available_sensors=available_sensors, ), ) subject.handle_action( diff --git a/api/tests/opentrons/protocol_engine/state/test_update_types.py b/api/tests/opentrons/protocol_engine/state/test_update_types.py new file mode 100644 index 00000000000..325f2611f37 --- /dev/null +++ b/api/tests/opentrons/protocol_engine/state/test_update_types.py @@ -0,0 +1,93 @@ +"""Unit tests for the utilities in `update_types`.""" +from opentrons.protocol_engine import DeckSlotLocation, ModuleLocation +from opentrons.protocol_engine.state import update_types +from opentrons.types import DeckSlotName + + +def test_append() -> None: + """Test `StateUpdate.append()`.""" + state_update = update_types.StateUpdate( + labware_location=update_types.LabwareLocationUpdate( + labware_id="test-123", + new_location=DeckSlotLocation(slotName=DeckSlotName.SLOT_A1), + offset_id=None, + ) + ) + + # Populating a new field should leave the original ones unchanged. + result = state_update.append( + update_types.StateUpdate(pipette_location=update_types.CLEAR) + ) + assert result is state_update + assert state_update.labware_location == update_types.LabwareLocationUpdate( + labware_id="test-123", + new_location=DeckSlotLocation(slotName=DeckSlotName.SLOT_A1), + offset_id=None, + ) + assert state_update.pipette_location == update_types.CLEAR + + # Populating a field that's already been populated should overwrite it. + result = state_update.append( + update_types.StateUpdate( + labware_location=update_types.LabwareLocationUpdate( + labware_id="test-123", + new_location=ModuleLocation(moduleId="module-123"), + offset_id=None, + ) + ) + ) + assert result is state_update + assert state_update.labware_location == update_types.LabwareLocationUpdate( + labware_id="test-123", + new_location=ModuleLocation(moduleId="module-123"), + offset_id=None, + ) + assert state_update.pipette_location == update_types.CLEAR + + +def test_reduce() -> None: + """Test `StateUpdate.reduce()`.""" + assert update_types.StateUpdate.reduce() == update_types.StateUpdate() + + # It should union all the set fields together. + assert update_types.StateUpdate.reduce( + update_types.StateUpdate( + labware_location=update_types.LabwareLocationUpdate( + labware_id="test-123", + new_location=DeckSlotLocation(slotName=DeckSlotName.SLOT_A1), + offset_id=None, + ) + ), + update_types.StateUpdate(pipette_location=update_types.CLEAR), + ) == update_types.StateUpdate( + labware_location=update_types.LabwareLocationUpdate( + labware_id="test-123", + new_location=DeckSlotLocation(slotName=DeckSlotName.SLOT_A1), + offset_id=None, + ), + pipette_location=update_types.CLEAR, + ) + + # When one field appears multiple times, the last write wins. + assert update_types.StateUpdate.reduce( + update_types.StateUpdate( + labware_location=update_types.LabwareLocationUpdate( + labware_id="test-123", + new_location=DeckSlotLocation(slotName=DeckSlotName.SLOT_A1), + offset_id=None, + ) + ), + update_types.StateUpdate( + labware_location=update_types.LabwareLocationUpdate( + labware_id="test-123", + new_location=ModuleLocation(moduleId="module-123"), + offset_id=None, + ) + ), + ) == update_types.StateUpdate( + labware_location=update_types.LabwareLocationUpdate( + labware_id="test-123", + new_location=ModuleLocation(moduleId="module-123"), + offset_id=None, + ) + ) diff --git a/api/tests/opentrons/protocol_engine/state/test_well_math.py b/api/tests/opentrons/protocol_engine/state/test_well_math.py new file mode 100644 index 00000000000..bb3dd545514 --- /dev/null +++ b/api/tests/opentrons/protocol_engine/state/test_well_math.py @@ -0,0 +1,421 @@ +"""Tests for well math.""" + +import json +import pathlib +from itertools import chain +from typing import Any, cast +from collections import OrderedDict + +import pytest + +from opentrons_shared_data.pipette.pipette_definition import ValidNozzleMaps + +from opentrons.types import Point +from opentrons.hardware_control.nozzle_manager import NozzleMap +from opentrons.protocol_engine.state._well_math import ( + wells_covered_dense, + wells_covered_sparse, + nozzles_per_well, +) + +from .. import pipette_fixtures + +_96_FULL_MAP = NozzleMap.build( + physical_nozzles=pipette_fixtures.NINETY_SIX_MAP, + physical_rows=pipette_fixtures.NINETY_SIX_ROWS, + physical_columns=pipette_fixtures.NINETY_SIX_COLS, + starting_nozzle="A1", + back_left_nozzle="A1", + front_right_nozzle="H12", + valid_nozzle_maps=ValidNozzleMaps( + maps={ + "Full": sum( + [ + pipette_fixtures.NINETY_SIX_ROWS["A"], + pipette_fixtures.NINETY_SIX_ROWS["B"], + pipette_fixtures.NINETY_SIX_ROWS["C"], + pipette_fixtures.NINETY_SIX_ROWS["D"], + pipette_fixtures.NINETY_SIX_ROWS["E"], + pipette_fixtures.NINETY_SIX_ROWS["F"], + pipette_fixtures.NINETY_SIX_ROWS["G"], + pipette_fixtures.NINETY_SIX_ROWS["H"], + ], + [], + ) + } + ), +) +_96_COL1_MAP = NozzleMap.build( + physical_nozzles=pipette_fixtures.NINETY_SIX_MAP, + physical_rows=pipette_fixtures.NINETY_SIX_ROWS, + physical_columns=pipette_fixtures.NINETY_SIX_COLS, + starting_nozzle="A1", + back_left_nozzle="A1", + front_right_nozzle="H1", + valid_nozzle_maps=ValidNozzleMaps( + maps={"Column1": pipette_fixtures.NINETY_SIX_COLS["1"]} + ), +) + +_96_COL12_MAP = NozzleMap.build( + physical_nozzles=pipette_fixtures.NINETY_SIX_MAP, + physical_rows=pipette_fixtures.NINETY_SIX_ROWS, + physical_columns=pipette_fixtures.NINETY_SIX_COLS, + starting_nozzle="A12", + back_left_nozzle="A12", + front_right_nozzle="H12", + valid_nozzle_maps=ValidNozzleMaps( + maps={"Column12": pipette_fixtures.NINETY_SIX_COLS["12"]} + ), +) + +_96_SINGLE_FR_MAP = NozzleMap.build( + physical_nozzles=pipette_fixtures.NINETY_SIX_MAP, + physical_rows=pipette_fixtures.NINETY_SIX_ROWS, + physical_columns=pipette_fixtures.NINETY_SIX_COLS, + starting_nozzle="H12", + back_left_nozzle="H12", + front_right_nozzle="H12", + valid_nozzle_maps=ValidNozzleMaps(maps={"Single": ["H12"]}), +) +_96_SINGLE_BL_MAP = NozzleMap.build( + physical_nozzles=pipette_fixtures.NINETY_SIX_MAP, + physical_rows=pipette_fixtures.NINETY_SIX_ROWS, + physical_columns=pipette_fixtures.NINETY_SIX_COLS, + starting_nozzle="A1", + back_left_nozzle="A1", + front_right_nozzle="A1", + valid_nozzle_maps=ValidNozzleMaps(maps={"Single": ["A1"]}), +) +_96_RECTANGLE_MAP = NozzleMap.build( + physical_nozzles=pipette_fixtures.NINETY_SIX_MAP, + physical_rows=pipette_fixtures.NINETY_SIX_ROWS, + physical_columns=pipette_fixtures.NINETY_SIX_COLS, + starting_nozzle="A1", + back_left_nozzle="A1", + front_right_nozzle="E2", + valid_nozzle_maps=ValidNozzleMaps( + maps={ + "Subrect": [ + "A1", + "A2", + "B1", + "B2", + "C1", + "C2", + "D1", + "D2", + "E1", + "E2", + ] + } + ), +) +_8_FULL_MAP = NozzleMap.build( + physical_nozzles=pipette_fixtures.EIGHT_CHANNEL_MAP, + physical_rows=pipette_fixtures.EIGHT_CHANNEL_ROWS, + physical_columns=pipette_fixtures.EIGHT_CHANNEL_COLS, + starting_nozzle="A1", + back_left_nozzle="A1", + front_right_nozzle="H1", + valid_nozzle_maps=ValidNozzleMaps( + maps={"Full": pipette_fixtures.EIGHT_CHANNEL_COLS["1"]} + ), +) +_8_SINGLE_MAP = NozzleMap.build( + physical_nozzles=pipette_fixtures.EIGHT_CHANNEL_MAP, + physical_rows=pipette_fixtures.EIGHT_CHANNEL_ROWS, + physical_columns=pipette_fixtures.EIGHT_CHANNEL_COLS, + starting_nozzle="A1", + back_left_nozzle="A1", + front_right_nozzle="A1", + valid_nozzle_maps=ValidNozzleMaps(maps={"Full": ["A1"]}), +) +_8_HALF_MAP = NozzleMap.build( + physical_nozzles=pipette_fixtures.EIGHT_CHANNEL_MAP, + physical_rows=pipette_fixtures.EIGHT_CHANNEL_ROWS, + physical_columns=pipette_fixtures.EIGHT_CHANNEL_COLS, + starting_nozzle="A1", + back_left_nozzle="A1", + front_right_nozzle="D1", + valid_nozzle_maps=ValidNozzleMaps(maps={"Half": ["A1", "B1", "C1", "D1"]}), +) +_SINGLE_MAP = NozzleMap.build( + physical_nozzles=OrderedDict((("A1", Point(0.0, 1.0, 2.0)),)), + physical_rows=OrderedDict((("1", ["A1"]),)), + physical_columns=OrderedDict((("A", ["A1"]),)), + starting_nozzle="A1", + back_left_nozzle="A1", + front_right_nozzle="A1", + valid_nozzle_maps=ValidNozzleMaps(maps={"Single": ["A1"]}), +) + + +def _fixture(fixture_name: str) -> Any: + return json.load( + open( + pathlib.Path(__file__).parent + / f"../../../../../shared-data/labware/fixtures/2/{fixture_name}.json" + ) + ) + + +@pytest.fixture +def _96_wells() -> list[list[str]]: + return fixture_map("fixture_96_plate") + + +@pytest.fixture +def _384_wells() -> list[list[str]]: + return fixture_map("fixture_384_plate") + + +@pytest.fixture +def _12_reservoir() -> list[list[str]]: + return fixture_map("fixture_12_trough_v2") + + +@pytest.fixture +def _1_reservoir() -> list[list[str]]: + return [["A1"]] + + +def all_wells(fixture_name: str) -> list[str]: + """All wells in a labware as a flat list.""" + ordering = fixture_map(fixture_name) + return list(chain(*ordering)) + + +def fixture_map(fixture_name: str) -> list[list[str]]: + """The ordering map.""" + return cast(list[list[str]], _fixture(fixture_name)["ordering"]) + + +@pytest.mark.parametrize( + "nozzle_map,target_well,result", + [ + # these configurations have all the nozzles on/in the wellplate + (_SINGLE_MAP, "A1", ["A1"]), + (_SINGLE_MAP, "D7", ["D7"]), + (_8_FULL_MAP, "A1", ["A1", "B1", "C1", "D1", "E1", "F1", "G1", "H1"]), + (_8_FULL_MAP, "A10", ["A10", "B10", "C10", "D10", "E10", "F10", "G10", "H10"]), + (_96_FULL_MAP, "A1", all_wells("fixture_96_plate")), + ( + _96_RECTANGLE_MAP, + "C8", + ["C8", "D8", "E8", "F8", "G8", "C9", "D9", "E9", "F9", "G9"], + ), + # these configurations have some nozzles below or to the right + (_8_FULL_MAP, "D1", ["D1", "E1", "F1", "G1", "H1"]), + ( + _96_FULL_MAP, + "C8", + [ + well + for well in all_wells("fixture_96_plate") + if ord(well[0]) >= ord("C") and int(well[1:]) >= 8 + ], + ), + ], +) +def test_wells_covered_dense_96( + nozzle_map: NozzleMap, + target_well: str, + result: list[str], + _96_wells: list[list[str]], +) -> None: + """It should calculate well coverage for an SBS 96.""" + assert list(wells_covered_dense(nozzle_map, target_well, _96_wells)) == result + + +@pytest.mark.parametrize( + "nozzle_map,target_well,result", + [ + # these configurations have all the nozzles on/in the wellplate + (_SINGLE_MAP, "A1", ["A1"]), + (_SINGLE_MAP, "D7", ["D7"]), + (_8_FULL_MAP, "A1", ["A1", "C1", "E1", "G1", "I1", "K1", "M1", "O1"]), + (_8_FULL_MAP, "B1", ["B1", "D1", "F1", "H1", "J1", "L1", "N1", "P1"]), + (_8_FULL_MAP, "A10", ["A10", "C10", "E10", "G10", "I10", "K10", "M10", "O10"]), + # well offsets inside the four-well clusters that are the size of each 96-well well + ( + _96_FULL_MAP, + "A1", + [ + well + for well in all_wells("fixture_384_plate") + if well[0] in "ACEGIKMO" + and int(well[1:]) in [1, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23] + ], + ), + ( + _96_FULL_MAP, + "A2", + [ + well + for well in all_wells("fixture_384_plate") + if well[0] in "ACEGIKMO" + and int(well[1:]) in [2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24] + ], + ), + ( + _96_FULL_MAP, + "B1", + [ + well + for well in all_wells("fixture_384_plate") + if well[0] in "BDFHJLNP" + and int(well[1:]) in [1, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23] + ], + ), + ( + _96_FULL_MAP, + "B2", + [ + well + for well in all_wells("fixture_384_plate") + if well[0] in "BDFHJLNP" + and int(well[1:]) in [2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24] + ], + ), + ( + _96_RECTANGLE_MAP, + "C8", + ["C8", "E8", "G8", "I8", "K8", "C10", "E10", "G10", "I10", "K10"], + ), + ( + _96_RECTANGLE_MAP, + "C9", + ["C9", "E9", "G9", "I9", "K9", "C11", "E11", "G11", "I11", "K11"], + ), + ( + _96_RECTANGLE_MAP, + "D8", + ["D8", "F8", "H8", "J8", "L8", "D10", "F10", "H10", "J10", "L10"], + ), + ( + _96_RECTANGLE_MAP, + "D9", + ["D9", "F9", "H9", "J9", "L9", "D11", "F11", "H11", "J11", "L11"], + ), + ], +) +def test_wells_covered_dense_384( + nozzle_map: NozzleMap, + target_well: str, + result: list[str], + _384_wells: list[list[str]], +) -> None: + """It should calculate well coverage for an SBS 384.""" + assert list(wells_covered_dense(nozzle_map, target_well, _384_wells)) == result + + +@pytest.mark.parametrize( + "nozzle_map,target_well,result", + [ + (_SINGLE_MAP, "A1", ["A1"]), + (_SINGLE_MAP, "A8", ["A8"]), + (_8_FULL_MAP, "A1", ["A1"]), + (_8_FULL_MAP, "A8", ["A8"]), + ( + _96_FULL_MAP, + "A1", + all_wells("fixture_12_trough_v2"), + ), + ( + _96_FULL_MAP, + "A8", + [well for well in all_wells("fixture_12_trough_v2") if int(well[1:]) >= 8], + ), + (_96_RECTANGLE_MAP, "A1", ["A1", "A2"]), + (_96_RECTANGLE_MAP, "A8", ["A8", "A9"]), + ], +) +def test_wells_covered_sparse_12( + nozzle_map: NozzleMap, + target_well: str, + result: list[str], + _12_reservoir: list[list[str]], +) -> None: + """It should calculate well coverage for a 12 column reservoir.""" + assert list(wells_covered_sparse(nozzle_map, target_well, _12_reservoir)) == result + + +@pytest.mark.parametrize( + "nozzle_map", + [ + _SINGLE_MAP, + _8_FULL_MAP, + _96_FULL_MAP, + _96_RECTANGLE_MAP, + ], +) +def test_wells_covered_sparse_1( + nozzle_map: NozzleMap, _1_reservoir: list[list[str]] +) -> None: + """It should calculate well coverage for a 1 column reservoir.""" + assert list(wells_covered_sparse(nozzle_map, "A1", _1_reservoir)) == ["A1"] + + +@pytest.mark.parametrize( + "nozzle_map", + [ + _SINGLE_MAP, + _8_FULL_MAP, + _96_FULL_MAP, + _96_RECTANGLE_MAP, + _8_SINGLE_MAP, + _96_SINGLE_BL_MAP, + _96_SINGLE_FR_MAP, + ], +) +@pytest.mark.parametrize("fixture_name", ["fixture_384_plate", "fixture_96_plate"]) +def test_nozzles_per_well_dense_force_1( + nozzle_map: NozzleMap, fixture_name: str +) -> None: + """It should calculate nozzles per well for SBS dense labware.""" + all_fixture_wells = all_wells(fixture_name) + # it's a bit unreasonable to test every well of a 384 plate so walk the diagonal + well_name = "A1" + while True: + if well_name not in all_fixture_wells: + break + assert nozzles_per_well(nozzle_map, well_name, fixture_map(fixture_name)) == 1 + well_name = f"{chr(ord(well_name[0])+1)}{str(int(well_name[1:])+1)}" + + +@pytest.mark.parametrize( + "nozzle_map,target_well,result", + [ + (_SINGLE_MAP, "A1", 1), + (_SINGLE_MAP, "A12", 1), + (_8_FULL_MAP, "A1", 8), + (_96_FULL_MAP, "A1", 8), + (_96_FULL_MAP, "A12", 8), + (_96_RECTANGLE_MAP, "A4", 5), + ], +) +def test_nozzles_per_well_12column( + nozzle_map: NozzleMap, target_well: str, result: int +) -> None: + """It should calculate nozzles per well for a 12 column.""" + assert ( + nozzles_per_well(nozzle_map, target_well, fixture_map("fixture_12_trough_v2")) + == result + ) + + +@pytest.mark.parametrize( + "nozzle_map,result", + [ + (_SINGLE_MAP, 1), + (_SINGLE_MAP, 1), + (_8_FULL_MAP, 8), + (_96_FULL_MAP, 96), + (_96_FULL_MAP, 96), + (_96_RECTANGLE_MAP, 10), + ], +) +def test_nozzles_per_well_1column(nozzle_map: NozzleMap, result: int) -> None: + """It should calculate nozzles per well for a 1-well reservoir.""" + assert nozzles_per_well(nozzle_map, "A1", [["A1"]]) == result diff --git a/api/tests/opentrons/protocol_engine/state/test_well_store.py b/api/tests/opentrons/protocol_engine/state/test_well_store.py deleted file mode 100644 index ec59a643db0..00000000000 --- a/api/tests/opentrons/protocol_engine/state/test_well_store.py +++ /dev/null @@ -1,229 +0,0 @@ -"""Well state store tests.""" -import pytest -from datetime import datetime -from opentrons.protocol_engine.state.wells import WellStore -from opentrons.protocol_engine.actions.actions import SucceedCommandAction -from opentrons.protocol_engine.state import update_types - -from .command_fixtures import ( - create_liquid_probe_command, - create_load_liquid_command, - create_aspirate_command, -) - - -@pytest.fixture -def subject() -> WellStore: - """Well store test subject.""" - return WellStore() - - -def test_handles_liquid_probe_success(subject: WellStore) -> None: - """It should add the well to the state after a successful liquid probe.""" - labware_id = "labware-id" - well_name = "well-name" - liquid_probe = create_liquid_probe_command() - timestamp = datetime(year=2020, month=1, day=2) - - subject.handle_action( - SucceedCommandAction( - command=liquid_probe, - state_update=update_types.StateUpdate( - liquid_probed=update_types.LiquidProbedUpdate( - labware_id="labware-id", - well_name="well-name", - height=15.0, - volume=30.0, - last_probed=timestamp, - ) - ), - ) - ) - - assert len(subject.state.probed_heights) == 1 - assert len(subject.state.probed_volumes) == 1 - - assert subject.state.probed_heights[labware_id][well_name].height == 15.0 - assert subject.state.probed_heights[labware_id][well_name].last_probed == timestamp - assert subject.state.probed_volumes[labware_id][well_name].volume == 30.0 - assert subject.state.probed_volumes[labware_id][well_name].last_probed == timestamp - assert ( - subject.state.probed_volumes[labware_id][well_name].operations_since_probe == 0 - ) - - -def test_handles_load_liquid_success(subject: WellStore) -> None: - """It should add the well to the state after a successful load liquid.""" - labware_id = "labware-id" - well_name_1 = "well-name-1" - well_name_2 = "well-name-2" - load_liquid = create_load_liquid_command( - labware_id=labware_id, volume_by_well={well_name_1: 30, well_name_2: 100} - ) - timestamp = datetime(year=2020, month=1, day=2) - - subject.handle_action( - SucceedCommandAction( - command=load_liquid, - state_update=update_types.StateUpdate( - liquid_loaded=update_types.LiquidLoadedUpdate( - labware_id=labware_id, - volumes={well_name_1: 30, well_name_2: 100}, - last_loaded=timestamp, - ) - ), - ) - ) - - assert len(subject.state.loaded_volumes) == 1 - assert len(subject.state.loaded_volumes[labware_id]) == 2 - - assert subject.state.loaded_volumes[labware_id][well_name_1].volume == 30.0 - assert ( - subject.state.loaded_volumes[labware_id][well_name_1].last_loaded == timestamp - ) - assert ( - subject.state.loaded_volumes[labware_id][well_name_1].operations_since_load == 0 - ) - assert subject.state.loaded_volumes[labware_id][well_name_2].volume == 100.0 - assert ( - subject.state.loaded_volumes[labware_id][well_name_2].last_loaded == timestamp - ) - assert ( - subject.state.loaded_volumes[labware_id][well_name_2].operations_since_load == 0 - ) - - -def test_handles_load_liquid_and_aspirate(subject: WellStore) -> None: - """It should populate the well state after load liquid and update the well state after aspirate.""" - pipette_id = "pipette-id" - labware_id = "labware-id" - well_name_1 = "well-name-1" - well_name_2 = "well-name-2" - aspirated_volume = 10.0 - load_liquid = create_load_liquid_command( - labware_id=labware_id, volume_by_well={well_name_1: 30, well_name_2: 100} - ) - aspirate_1 = create_aspirate_command( - pipette_id=pipette_id, - volume=aspirated_volume, - flow_rate=1.0, - labware_id=labware_id, - well_name=well_name_1, - ) - aspirate_2 = create_aspirate_command( - pipette_id=pipette_id, - volume=aspirated_volume, - flow_rate=1.0, - labware_id=labware_id, - well_name=well_name_2, - ) - timestamp = datetime(year=2020, month=1, day=2) - - subject.handle_action( - SucceedCommandAction( - command=load_liquid, - state_update=update_types.StateUpdate( - liquid_loaded=update_types.LiquidLoadedUpdate( - labware_id=labware_id, - volumes={well_name_1: 30, well_name_2: 100}, - last_loaded=timestamp, - ) - ), - ) - ) - subject.handle_action( - SucceedCommandAction( - command=aspirate_1, - state_update=update_types.StateUpdate( - liquid_operated=update_types.LiquidOperatedUpdate( - labware_id=labware_id, - well_name=well_name_1, - volume_added=-aspirated_volume, - ) - ), - ) - ) - subject.handle_action( - SucceedCommandAction( - command=aspirate_2, - state_update=update_types.StateUpdate( - liquid_operated=update_types.LiquidOperatedUpdate( - labware_id=labware_id, - well_name=well_name_2, - volume_added=-aspirated_volume, - ) - ), - ) - ) - - assert len(subject.state.loaded_volumes) == 1 - assert len(subject.state.loaded_volumes[labware_id]) == 2 - - assert subject.state.loaded_volumes[labware_id][well_name_1].volume == 20.0 - assert ( - subject.state.loaded_volumes[labware_id][well_name_1].last_loaded == timestamp - ) - assert ( - subject.state.loaded_volumes[labware_id][well_name_1].operations_since_load == 1 - ) - assert subject.state.loaded_volumes[labware_id][well_name_2].volume == 90.0 - assert ( - subject.state.loaded_volumes[labware_id][well_name_2].last_loaded == timestamp - ) - assert ( - subject.state.loaded_volumes[labware_id][well_name_2].operations_since_load == 1 - ) - - -def test_handles_liquid_probe_and_aspirate(subject: WellStore) -> None: - """It should populate the well state after liquid probe and update the well state after aspirate.""" - pipette_id = "pipette-id" - labware_id = "labware-id" - well_name = "well-name" - aspirated_volume = 10.0 - liquid_probe = create_liquid_probe_command() - aspirate = create_aspirate_command( - pipette_id=pipette_id, - volume=aspirated_volume, - flow_rate=1.0, - labware_id=labware_id, - well_name=well_name, - ) - timestamp = datetime(year=2020, month=1, day=2) - - subject.handle_action( - SucceedCommandAction( - command=liquid_probe, - state_update=update_types.StateUpdate( - liquid_probed=update_types.LiquidProbedUpdate( - labware_id="labware-id", - well_name="well-name", - height=15.0, - volume=30.0, - last_probed=timestamp, - ) - ), - ) - ) - subject.handle_action( - SucceedCommandAction( - command=aspirate, - state_update=update_types.StateUpdate( - liquid_operated=update_types.LiquidOperatedUpdate( - labware_id="labware-id", - well_name="well-name", - volume_added=-aspirated_volume, - ) - ), - ) - ) - - assert len(subject.state.probed_heights[labware_id]) == 0 - assert len(subject.state.probed_volumes) == 1 - - assert subject.state.probed_volumes[labware_id][well_name].volume == 20.0 - assert subject.state.probed_volumes[labware_id][well_name].last_probed == timestamp - assert ( - subject.state.probed_volumes[labware_id][well_name].operations_since_probe == 1 - ) diff --git a/api/tests/opentrons/protocol_engine/state/test_well_store_old.py b/api/tests/opentrons/protocol_engine/state/test_well_store_old.py new file mode 100644 index 00000000000..51142afd9da --- /dev/null +++ b/api/tests/opentrons/protocol_engine/state/test_well_store_old.py @@ -0,0 +1,235 @@ +"""Well state store tests. + +DEPRECATED: Testing WellStore independently of WellView is no longer helpful. +Try to add new tests to well_state.py, where they can be tested together, +treating WellState as a private implementation detail. +""" + +import pytest +from datetime import datetime +from opentrons.protocol_engine.state.wells import WellStore +from opentrons.protocol_engine.actions.actions import SucceedCommandAction +from opentrons.protocol_engine.state import update_types + +from .command_fixtures import ( + create_liquid_probe_command, + create_load_liquid_command, + create_aspirate_command, +) + + +@pytest.fixture +def subject() -> WellStore: + """Well store test subject.""" + return WellStore() + + +def test_handles_liquid_probe_success(subject: WellStore) -> None: + """It should add the well to the state after a successful liquid probe.""" + labware_id = "labware-id" + well_name = "well-name" + liquid_probe = create_liquid_probe_command() + timestamp = datetime(year=2020, month=1, day=2) + + subject.handle_action( + SucceedCommandAction( + command=liquid_probe, + state_update=update_types.StateUpdate( + liquid_probed=update_types.LiquidProbedUpdate( + labware_id="labware-id", + well_name="well-name", + height=15.0, + volume=30.0, + last_probed=timestamp, + ) + ), + ) + ) + + assert len(subject.state.probed_heights) == 1 + assert len(subject.state.probed_volumes) == 1 + + assert subject.state.probed_heights[labware_id][well_name].height == 15.0 + assert subject.state.probed_heights[labware_id][well_name].last_probed == timestamp + assert subject.state.probed_volumes[labware_id][well_name].volume == 30.0 + assert subject.state.probed_volumes[labware_id][well_name].last_probed == timestamp + assert ( + subject.state.probed_volumes[labware_id][well_name].operations_since_probe == 0 + ) + + +def test_handles_load_liquid_success(subject: WellStore) -> None: + """It should add the well to the state after a successful load liquid.""" + labware_id = "labware-id" + well_name_1 = "well-name-1" + well_name_2 = "well-name-2" + load_liquid = create_load_liquid_command( + labware_id=labware_id, volume_by_well={well_name_1: 30, well_name_2: 100} + ) + timestamp = datetime(year=2020, month=1, day=2) + + subject.handle_action( + SucceedCommandAction( + command=load_liquid, + state_update=update_types.StateUpdate( + liquid_loaded=update_types.LiquidLoadedUpdate( + labware_id=labware_id, + volumes={well_name_1: 30, well_name_2: 100}, + last_loaded=timestamp, + ) + ), + ) + ) + + assert len(subject.state.loaded_volumes) == 1 + assert len(subject.state.loaded_volumes[labware_id]) == 2 + + assert subject.state.loaded_volumes[labware_id][well_name_1].volume == 30.0 + assert ( + subject.state.loaded_volumes[labware_id][well_name_1].last_loaded == timestamp + ) + assert ( + subject.state.loaded_volumes[labware_id][well_name_1].operations_since_load == 0 + ) + assert subject.state.loaded_volumes[labware_id][well_name_2].volume == 100.0 + assert ( + subject.state.loaded_volumes[labware_id][well_name_2].last_loaded == timestamp + ) + assert ( + subject.state.loaded_volumes[labware_id][well_name_2].operations_since_load == 0 + ) + + +def test_handles_load_liquid_and_aspirate(subject: WellStore) -> None: + """It should populate the well state after load liquid and update the well state after aspirate.""" + pipette_id = "pipette-id" + labware_id = "labware-id" + well_name_1 = "well-name-1" + well_name_2 = "well-name-2" + aspirated_volume = 10.0 + load_liquid = create_load_liquid_command( + labware_id=labware_id, volume_by_well={well_name_1: 30, well_name_2: 100} + ) + aspirate_1 = create_aspirate_command( + pipette_id=pipette_id, + volume=aspirated_volume, + flow_rate=1.0, + labware_id=labware_id, + well_name=well_name_1, + ) + aspirate_2 = create_aspirate_command( + pipette_id=pipette_id, + volume=aspirated_volume, + flow_rate=1.0, + labware_id=labware_id, + well_name=well_name_2, + ) + timestamp = datetime(year=2020, month=1, day=2) + + subject.handle_action( + SucceedCommandAction( + command=load_liquid, + state_update=update_types.StateUpdate( + liquid_loaded=update_types.LiquidLoadedUpdate( + labware_id=labware_id, + volumes={well_name_1: 30, well_name_2: 100}, + last_loaded=timestamp, + ) + ), + ) + ) + subject.handle_action( + SucceedCommandAction( + command=aspirate_1, + state_update=update_types.StateUpdate( + liquid_operated=update_types.LiquidOperatedUpdate( + labware_id=labware_id, + well_names=[well_name_1, well_name_2], + volume_added=-aspirated_volume, + ) + ), + ) + ) + subject.handle_action( + SucceedCommandAction( + command=aspirate_2, + state_update=update_types.StateUpdate( + liquid_operated=update_types.LiquidOperatedUpdate( + labware_id=labware_id, + well_names=[well_name_2], + volume_added=-aspirated_volume, + ) + ), + ) + ) + + assert len(subject.state.loaded_volumes) == 1 + assert len(subject.state.loaded_volumes[labware_id]) == 2 + + assert subject.state.loaded_volumes[labware_id][well_name_1].volume == 20.0 + assert ( + subject.state.loaded_volumes[labware_id][well_name_1].last_loaded == timestamp + ) + assert ( + subject.state.loaded_volumes[labware_id][well_name_1].operations_since_load == 1 + ) + assert subject.state.loaded_volumes[labware_id][well_name_2].volume == 80.0 + assert ( + subject.state.loaded_volumes[labware_id][well_name_2].last_loaded == timestamp + ) + assert ( + subject.state.loaded_volumes[labware_id][well_name_2].operations_since_load == 2 + ) + + +def test_handles_liquid_probe_and_aspirate(subject: WellStore) -> None: + """It should populate the well state after liquid probe and update the well state after aspirate.""" + pipette_id = "pipette-id" + labware_id = "labware-id" + well_name = "well-name" + aspirated_volume = 10.0 + liquid_probe = create_liquid_probe_command() + aspirate = create_aspirate_command( + pipette_id=pipette_id, + volume=aspirated_volume, + flow_rate=1.0, + labware_id=labware_id, + well_name=well_name, + ) + timestamp = datetime(year=2020, month=1, day=2) + + subject.handle_action( + SucceedCommandAction( + command=liquid_probe, + state_update=update_types.StateUpdate( + liquid_probed=update_types.LiquidProbedUpdate( + labware_id="labware-id", + well_name="well-name", + height=15.0, + volume=30.0, + last_probed=timestamp, + ) + ), + ) + ) + subject.handle_action( + SucceedCommandAction( + command=aspirate, + state_update=update_types.StateUpdate( + liquid_operated=update_types.LiquidOperatedUpdate( + labware_id="labware-id", + well_names=["well-name"], + volume_added=-aspirated_volume, + ) + ), + ) + ) + + assert len(subject.state.probed_heights[labware_id]) == 0 + assert len(subject.state.probed_volumes) == 1 + + assert subject.state.probed_volumes[labware_id][well_name].volume == 20.0 + assert subject.state.probed_volumes[labware_id][well_name].last_probed == timestamp + assert ( + subject.state.probed_volumes[labware_id][well_name].operations_since_probe == 1 + ) diff --git a/api/tests/opentrons/protocol_engine/state/test_well_view.py b/api/tests/opentrons/protocol_engine/state/test_well_view.py deleted file mode 100644 index 5025e4ee93e..00000000000 --- a/api/tests/opentrons/protocol_engine/state/test_well_view.py +++ /dev/null @@ -1,58 +0,0 @@ -"""Well view tests.""" -from datetime import datetime -from opentrons.protocol_engine.types import ( - LoadedVolumeInfo, - ProbedHeightInfo, - ProbedVolumeInfo, -) -import pytest -from opentrons.protocol_engine.state.wells import WellState, WellView - - -@pytest.fixture -def subject() -> WellView: - """Get a well view test subject.""" - loaded_volume_info = LoadedVolumeInfo( - volume=30.0, last_loaded=datetime.now(), operations_since_load=0 - ) - probed_height_info = ProbedHeightInfo(height=5.5, last_probed=datetime.now()) - probed_volume_info = ProbedVolumeInfo( - volume=25.0, last_probed=datetime.now(), operations_since_probe=0 - ) - state = WellState( - loaded_volumes={"labware_id_1": {"well_name": loaded_volume_info}}, - probed_heights={"labware_id_2": {"well_name": probed_height_info}}, - probed_volumes={"labware_id_2": {"well_name": probed_volume_info}}, - ) - - return WellView(state) - - -def test_get_well_liquid_info(subject: WellView) -> None: - """Should return a tuple of well infos.""" - volume_info = subject.get_well_liquid_info( - labware_id="labware_id_1", well_name="well_name" - ) - assert volume_info.loaded_volume is not None - assert volume_info.probed_height is None - assert volume_info.probed_volume is None - assert volume_info.loaded_volume.volume == 30.0 - - volume_info = subject.get_well_liquid_info( - labware_id="labware_id_2", well_name="well_name" - ) - assert volume_info.loaded_volume is None - assert volume_info.probed_height is not None - assert volume_info.probed_volume is not None - assert volume_info.probed_height.height == 5.5 - assert volume_info.probed_volume.volume == 25.0 - - -def test_get_all(subject: WellView) -> None: - """Should return a list of well summaries.""" - summaries = subject.get_all() - - assert len(summaries) == 2, f"{summaries}" - assert summaries[0].loaded_volume == 30.0 - assert summaries[1].probed_height == 5.5 - assert summaries[1].probed_volume == 25.0 diff --git a/api/tests/opentrons/protocol_engine/state/test_well_view_old.py b/api/tests/opentrons/protocol_engine/state/test_well_view_old.py new file mode 100644 index 00000000000..9ced4db9df0 --- /dev/null +++ b/api/tests/opentrons/protocol_engine/state/test_well_view_old.py @@ -0,0 +1,63 @@ +"""Well view tests. + +DEPRECATED: Testing WellView independently of WellStore is no longer helpful. +Try to add new tests to test_well_state.py, where they can be tested together, +treating WellState as a private implementation detail. +""" +from datetime import datetime +from opentrons.protocol_engine.types import ( + LoadedVolumeInfo, + ProbedHeightInfo, + ProbedVolumeInfo, +) +import pytest +from opentrons.protocol_engine.state.wells import WellState, WellView + + +@pytest.fixture +def subject() -> WellView: + """Get a well view test subject.""" + loaded_volume_info = LoadedVolumeInfo( + volume=30.0, last_loaded=datetime.now(), operations_since_load=0 + ) + probed_height_info = ProbedHeightInfo(height=5.5, last_probed=datetime.now()) + probed_volume_info = ProbedVolumeInfo( + volume=25.0, last_probed=datetime.now(), operations_since_probe=0 + ) + state = WellState( + loaded_volumes={"labware_id_1": {"well_name": loaded_volume_info}}, + probed_heights={"labware_id_2": {"well_name": probed_height_info}}, + probed_volumes={"labware_id_2": {"well_name": probed_volume_info}}, + ) + + return WellView(state) + + +def test_get_well_liquid_info(subject: WellView) -> None: + """Should return a tuple of well infos.""" + volume_info = subject.get_well_liquid_info( + labware_id="labware_id_1", well_name="well_name" + ) + assert volume_info.loaded_volume is not None + assert volume_info.probed_height is None + assert volume_info.probed_volume is None + assert volume_info.loaded_volume.volume == 30.0 + + volume_info = subject.get_well_liquid_info( + labware_id="labware_id_2", well_name="well_name" + ) + assert volume_info.loaded_volume is None + assert volume_info.probed_height is not None + assert volume_info.probed_volume is not None + assert volume_info.probed_height.height == 5.5 + assert volume_info.probed_volume.volume == 25.0 + + +def test_get_all(subject: WellView) -> None: + """Should return a list of well summaries.""" + summaries = subject.get_all() + + assert len(summaries) == 2, f"{summaries}" + assert summaries[0].loaded_volume == 30.0 + assert summaries[1].probed_height == 5.5 + assert summaries[1].probed_volume == 25.0 diff --git a/api/tests/opentrons/protocol_engine/test_protocol_engine.py b/api/tests/opentrons/protocol_engine/test_protocol_engine.py index ac83e987153..95289d681b8 100644 --- a/api/tests/opentrons/protocol_engine/test_protocol_engine.py +++ b/api/tests/opentrons/protocol_engine/test_protocol_engine.py @@ -1,4 +1,5 @@ """Tests for the ProtocolEngine class.""" + import inspect from datetime import datetime from typing import Any @@ -32,7 +33,6 @@ ModuleModel, Liquid, PostRunHardwareState, - AddressableAreaLocation, ) from opentrons.protocol_engine.execution import ( QueueWorker, @@ -684,7 +684,7 @@ 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(state_store.commands.state.stopped_by_estop).then_return(False) + decoy.when(state_store.commands.get_is_stopped_by_estop()).then_return(False) decoy.when(model_utils.get_timestamp()).then_return(completed_at) await subject.finish( @@ -719,7 +719,7 @@ async def test_finish_with_defaults( state_store: StateStore, ) -> None: """It should be able to gracefully tell the engine it's done.""" - decoy.when(state_store.commands.state.stopped_by_estop).then_return(False) + decoy.when(state_store.commands.get_is_stopped_by_estop()).then_return(False) await subject.finish() decoy.verify( @@ -761,7 +761,7 @@ async def test_finish_with_error( error=error, ) - decoy.when(state_store.commands.state.stopped_by_estop).then_return( + decoy.when(state_store.commands.get_is_stopped_by_estop()).then_return( stopped_by_estop ) decoy.when(model_utils.generate_id()).then_return("error-id") @@ -804,9 +804,9 @@ async def test_finish_with_estop_error_will_not_drop_tip_and_home( ) -> None: """It should be able to tell the engine it's finished because of an error and will not drop tip and home.""" error = ProtocolCommandFailedError( - original_error=ErrorOccurrence.construct( # type: ignore[call-arg] + original_error=ErrorOccurrence.model_construct( # type: ignore[call-arg] wrappedErrors=[ - ErrorOccurrence.construct(errorCode="3008") # type: ignore[call-arg] + ErrorOccurrence.model_construct(errorCode="3008") # type: ignore[call-arg] ] ) ) @@ -861,7 +861,7 @@ async def test_finish_stops_hardware_if_queue_worker_join_fails( await queue_worker.join(), ).then_raise(exception) - decoy.when(state_store.commands.state.stopped_by_estop).then_return(False) + decoy.when(state_store.commands.get_is_stopped_by_estop()).then_return(False) error_id = "error-id" completed_at = datetime(2021, 1, 1, 0, 0) @@ -997,8 +997,7 @@ async def test_estop_noops_if_invalid( subject.estop() # Should not raise. decoy.verify( - action_dispatcher.dispatch(), # type: ignore - ignore_extra_args=True, + action_dispatcher.dispatch(expected_action), times=0, ) decoy.verify( @@ -1120,11 +1119,7 @@ def test_add_addressable_area( decoy.verify( action_dispatcher.dispatch( - AddAddressableAreaAction( - addressable_area=AddressableAreaLocation( - addressableAreaName="my_funky_area" - ) - ) + AddAddressableAreaAction(addressable_area_name="my_funky_area") ) ) @@ -1133,21 +1128,18 @@ def test_add_liquid( decoy: Decoy, action_dispatcher: ActionDispatcher, subject: ProtocolEngine, + state_store: StateStore, ) -> None: """It should dispatch an AddLiquidAction action.""" + liquid_obj = Liquid(id="water-id", displayName="water", description="water desc") + decoy.when( + state_store.liquid.validate_liquid_allowed(liquid=liquid_obj) + ).then_return(liquid_obj) subject.add_liquid( id="water-id", name="water", description="water desc", color=None ) - decoy.verify( - action_dispatcher.dispatch( - AddLiquidAction( - liquid=Liquid( - id="water-id", displayName="water", description="water desc" - ) - ) - ) - ) + decoy.verify(action_dispatcher.dispatch(AddLiquidAction(liquid=liquid_obj))) async def test_use_attached_temp_and_mag_modules( diff --git a/api/tests/opentrons/protocol_engine/test_types.py b/api/tests/opentrons/protocol_engine/test_types.py index ccf6b91de7f..d48c67ee61e 100644 --- a/api/tests/opentrons/protocol_engine/test_types.py +++ b/api/tests/opentrons/protocol_engine/test_types.py @@ -9,10 +9,14 @@ def test_hex_validation(hex_color: str) -> None: """Should allow creating a HexColor.""" # make sure noting is raised when instantiating this class - assert HexColor(__root__=hex_color) + assert HexColor(hex_color) + assert HexColor.model_validate_json(f'"{hex_color}"') -def test_handles_invalid_hex() -> None: +@pytest.mark.parametrize("invalid_hex_color", ["true", "null", "#123456789"]) +def test_handles_invalid_hex(invalid_hex_color: str) -> None: """Should raise a validation error.""" with pytest.raises(ValidationError): - HexColor(__root__="#123456789") + HexColor(invalid_hex_color) + with pytest.raises(ValidationError): + HexColor.model_validate_json(f'"{invalid_hex_color}"') diff --git a/api/tests/opentrons/protocol_runner/smoke_tests/conftest.py b/api/tests/opentrons/protocol_runner/smoke_tests/conftest.py index 5a758922e59..ade6ed4dae8 100644 --- a/api/tests/opentrons/protocol_runner/smoke_tests/conftest.py +++ b/api/tests/opentrons/protocol_runner/smoke_tests/conftest.py @@ -17,7 +17,7 @@ def tempdeck_v1_def() -> ModuleDefinition: """Get the definition of a V1 tempdeck.""" definition = load_shared_data("module/definitions/3/temperatureModuleV1.json") - return ModuleDefinition.parse_raw(definition) + return ModuleDefinition.model_validate_json(definition) @pytest.fixture() diff --git a/api/tests/opentrons/protocol_runner/smoke_tests/test_legacy_command_mapper.py b/api/tests/opentrons/protocol_runner/smoke_tests/test_legacy_command_mapper.py index a652d76eac3..0cc542c4971 100644 --- a/api/tests/opentrons/protocol_runner/smoke_tests/test_legacy_command_mapper.py +++ b/api/tests/opentrons/protocol_runner/smoke_tests/test_legacy_command_mapper.py @@ -159,7 +159,7 @@ async def test_big_protocol_commands(big_protocol_file: Path) -> None: assert len(commands_result) == 32 - assert commands_result[0] == commands.Home.construct( + assert commands_result[0] == commands.Home.model_construct( id=matchers.IsA(str), key=matchers.IsA(str), status=commands.CommandStatus.SUCCEEDED, @@ -170,7 +170,7 @@ async def test_big_protocol_commands(big_protocol_file: Path) -> None: notes=[], result=commands.HomeResult(), ) - assert commands_result[1] == commands.LoadLabware.construct( + assert commands_result[1] == commands.LoadLabware.model_construct( id=matchers.IsA(str), key=matchers.IsA(str), status=commands.CommandStatus.SUCCEEDED, @@ -186,7 +186,7 @@ async def test_big_protocol_commands(big_protocol_file: Path) -> None: notes=[], result=tiprack_1_result_captor, ) - assert commands_result[2] == commands.LoadLabware.construct( + assert commands_result[2] == commands.LoadLabware.model_construct( id=matchers.IsA(str), key=matchers.IsA(str), status=commands.CommandStatus.SUCCEEDED, @@ -202,7 +202,7 @@ async def test_big_protocol_commands(big_protocol_file: Path) -> None: notes=[], result=tiprack_2_result_captor, ) - assert commands_result[3] == commands.LoadModule.construct( + assert commands_result[3] == commands.LoadModule.model_construct( id=matchers.IsA(str), key=matchers.IsA(str), status=commands.CommandStatus.SUCCEEDED, @@ -217,7 +217,7 @@ async def test_big_protocol_commands(big_protocol_file: Path) -> None: notes=[], result=module_1_result_captor, ) - assert commands_result[4] == commands.LoadLabware.construct( + assert commands_result[4] == commands.LoadLabware.model_construct( id=matchers.IsA(str), key=matchers.IsA(str), status=commands.CommandStatus.SUCCEEDED, @@ -233,7 +233,7 @@ async def test_big_protocol_commands(big_protocol_file: Path) -> None: notes=[], result=well_plate_1_result_captor, ) - assert commands_result[5] == commands.LoadLabware.construct( + assert commands_result[5] == commands.LoadLabware.model_construct( id=matchers.IsA(str), key=matchers.IsA(str), status=commands.CommandStatus.SUCCEEDED, @@ -250,7 +250,7 @@ async def test_big_protocol_commands(big_protocol_file: Path) -> None: result=module_plate_1_result_captor, ) - assert commands_result[6] == commands.LoadPipette.construct( + assert commands_result[6] == commands.LoadPipette.model_construct( id=matchers.IsA(str), key=matchers.IsA(str), status=commands.CommandStatus.SUCCEEDED, @@ -264,7 +264,7 @@ async def test_big_protocol_commands(big_protocol_file: Path) -> None: result=pipette_left_result_captor, ) - assert commands_result[7] == commands.LoadPipette.construct( + assert commands_result[7] == commands.LoadPipette.model_construct( id=matchers.IsA(str), key=matchers.IsA(str), status=commands.CommandStatus.SUCCEEDED, @@ -278,16 +278,14 @@ async def test_big_protocol_commands(big_protocol_file: Path) -> None: result=pipette_right_result_captor, ) - # TODO(mc, 2021-11-11): not sure why I have to dict-access these properties - # might be a bug in Decoy, might be something weird that Pydantic does - tiprack_1_id = tiprack_1_result_captor.value["labwareId"] - tiprack_2_id = tiprack_2_result_captor.value["labwareId"] - well_plate_1_id = well_plate_1_result_captor.value["labwareId"] - module_plate_1_id = module_plate_1_result_captor.value["labwareId"] - pipette_left_id = pipette_left_result_captor.value["pipetteId"] - pipette_right_id = pipette_right_result_captor.value["pipetteId"] + tiprack_1_id = tiprack_1_result_captor.value.labwareId + tiprack_2_id = tiprack_2_result_captor.value.labwareId + well_plate_1_id = well_plate_1_result_captor.value.labwareId + module_plate_1_id = module_plate_1_result_captor.value.labwareId + pipette_left_id = pipette_left_result_captor.value.pipetteId + pipette_right_id = pipette_right_result_captor.value.pipetteId - assert commands_result[8] == commands.PickUpTip.construct( + assert commands_result[8] == commands.PickUpTip.model_construct( id=matchers.IsA(str), key=matchers.IsA(str), status=commands.CommandStatus.SUCCEEDED, @@ -304,7 +302,7 @@ async def test_big_protocol_commands(big_protocol_file: Path) -> None: tipVolume=300.0, tipLength=51.83, position=DeckPoint(x=0, y=0, z=0) ), ) - assert commands_result[9] == commands.PickUpTip.construct( + assert commands_result[9] == commands.PickUpTip.model_construct( id=matchers.IsA(str), key=matchers.IsA(str), status=commands.CommandStatus.SUCCEEDED, @@ -322,7 +320,7 @@ async def test_big_protocol_commands(big_protocol_file: Path) -> None: ), ) - assert commands_result[10] == commands.DropTip.construct( + assert commands_result[10] == commands.DropTip.model_construct( id=matchers.IsA(str), key=matchers.IsA(str), status=commands.CommandStatus.SUCCEEDED, @@ -338,7 +336,7 @@ async def test_big_protocol_commands(big_protocol_file: Path) -> None: result=commands.DropTipResult(position=DeckPoint(x=0, y=0, z=0)), ) - assert commands_result[11] == commands.PickUpTip.construct( + assert commands_result[11] == commands.PickUpTip.model_construct( id=matchers.IsA(str), key=matchers.IsA(str), status=commands.CommandStatus.SUCCEEDED, @@ -355,7 +353,7 @@ async def test_big_protocol_commands(big_protocol_file: Path) -> None: tipVolume=300.0, tipLength=51.83, position=DeckPoint(x=0, y=0, z=0) ), ) - assert commands_result[12] == commands.Aspirate.construct( + assert commands_result[12] == commands.Aspirate.model_construct( id=matchers.IsA(str), key=matchers.IsA(str), status=commands.CommandStatus.SUCCEEDED, @@ -372,7 +370,7 @@ async def test_big_protocol_commands(big_protocol_file: Path) -> None: notes=[], result=commands.AspirateResult(volume=40, position=DeckPoint(x=0, y=0, z=0)), ) - assert commands_result[13] == commands.Dispense.construct( + assert commands_result[13] == commands.Dispense.model_construct( id=matchers.IsA(str), key=matchers.IsA(str), status=commands.CommandStatus.SUCCEEDED, @@ -389,7 +387,7 @@ async def test_big_protocol_commands(big_protocol_file: Path) -> None: notes=[], result=commands.DispenseResult(volume=35, position=DeckPoint(x=0, y=0, z=0)), ) - assert commands_result[14] == commands.Aspirate.construct( + assert commands_result[14] == commands.Aspirate.model_construct( id=matchers.IsA(str), key=matchers.IsA(str), status=commands.CommandStatus.SUCCEEDED, @@ -406,7 +404,7 @@ async def test_big_protocol_commands(big_protocol_file: Path) -> None: notes=[], result=commands.AspirateResult(volume=40, position=DeckPoint(x=0, y=0, z=0)), ) - assert commands_result[15] == commands.Dispense.construct( + assert commands_result[15] == commands.Dispense.model_construct( id=matchers.IsA(str), key=matchers.IsA(str), status=commands.CommandStatus.SUCCEEDED, @@ -423,7 +421,7 @@ async def test_big_protocol_commands(big_protocol_file: Path) -> None: notes=[], result=commands.DispenseResult(volume=35, position=DeckPoint(x=0, y=0, z=0)), ) - assert commands_result[16] == commands.BlowOut.construct( + assert commands_result[16] == commands.BlowOut.model_construct( id=matchers.IsA(str), key=matchers.IsA(str), status=commands.CommandStatus.SUCCEEDED, @@ -439,7 +437,7 @@ async def test_big_protocol_commands(big_protocol_file: Path) -> None: notes=[], result=commands.BlowOutResult(position=DeckPoint(x=0, y=0, z=0)), ) - assert commands_result[17] == commands.Aspirate.construct( + assert commands_result[17] == commands.Aspirate.model_construct( id=matchers.IsA(str), key=matchers.IsA(str), status=commands.CommandStatus.SUCCEEDED, @@ -456,7 +454,7 @@ async def test_big_protocol_commands(big_protocol_file: Path) -> None: notes=[], result=commands.AspirateResult(volume=50, position=DeckPoint(x=0, y=0, z=0)), ) - assert commands_result[18] == commands.Dispense.construct( + assert commands_result[18] == commands.Dispense.model_construct( id=matchers.IsA(str), key=matchers.IsA(str), status=commands.CommandStatus.SUCCEEDED, @@ -473,7 +471,7 @@ async def test_big_protocol_commands(big_protocol_file: Path) -> None: notes=[], result=commands.DispenseResult(volume=50, position=DeckPoint(x=0, y=0, z=0)), ) - assert commands_result[19] == commands.BlowOut.construct( + assert commands_result[19] == commands.BlowOut.model_construct( id=matchers.IsA(str), key=matchers.IsA(str), status=commands.CommandStatus.SUCCEEDED, @@ -489,7 +487,7 @@ async def test_big_protocol_commands(big_protocol_file: Path) -> None: notes=[], result=commands.BlowOutResult(position=DeckPoint(x=0, y=0, z=0)), ) - assert commands_result[20] == commands.Aspirate.construct( + assert commands_result[20] == commands.Aspirate.model_construct( id=matchers.IsA(str), key=matchers.IsA(str), status=commands.CommandStatus.SUCCEEDED, @@ -506,7 +504,7 @@ async def test_big_protocol_commands(big_protocol_file: Path) -> None: notes=[], result=commands.AspirateResult(volume=300, position=DeckPoint(x=0, y=0, z=0)), ) - assert commands_result[21] == commands.Dispense.construct( + assert commands_result[21] == commands.Dispense.model_construct( id=matchers.IsA(str), key=matchers.IsA(str), status=commands.CommandStatus.SUCCEEDED, @@ -523,7 +521,7 @@ async def test_big_protocol_commands(big_protocol_file: Path) -> None: notes=[], result=commands.DispenseResult(volume=300, position=DeckPoint(x=0, y=0, z=0)), ) - assert commands_result[22] == commands.BlowOut.construct( + assert commands_result[22] == commands.BlowOut.model_construct( id=matchers.IsA(str), key=matchers.IsA(str), status=commands.CommandStatus.SUCCEEDED, @@ -540,7 +538,7 @@ async def test_big_protocol_commands(big_protocol_file: Path) -> None: result=commands.BlowOutResult(position=DeckPoint(x=0, y=0, z=0)), ) # TODO:(jr, 15.08.2022): this should map to move_to when move_to is mapped in a followup ticket RSS-62 - assert commands_result[23] == commands.Custom.construct( + assert commands_result[23] == commands.Custom.model_construct( id=matchers.IsA(str), key=matchers.IsA(str), status=commands.CommandStatus.SUCCEEDED, @@ -556,7 +554,7 @@ async def test_big_protocol_commands(big_protocol_file: Path) -> None: ) # TODO:(jr, 15.08.2022): aspirate commands with no labware get filtered # into custom. Refactor this in followup legacy command mapping - assert commands_result[24] == commands.Custom.construct( + assert commands_result[24] == commands.Custom.model_construct( id=matchers.IsA(str), key=matchers.IsA(str), status=commands.CommandStatus.SUCCEEDED, @@ -572,7 +570,7 @@ async def test_big_protocol_commands(big_protocol_file: Path) -> None: ) # TODO:(jr, 15.08.2022): dispense commands with no labware get filtered # into custom. Refactor this in followup legacy command mapping - assert commands_result[25] == commands.Custom.construct( + assert commands_result[25] == commands.Custom.model_construct( id=matchers.IsA(str), key=matchers.IsA(str), status=commands.CommandStatus.SUCCEEDED, @@ -588,7 +586,7 @@ async def test_big_protocol_commands(big_protocol_file: Path) -> None: ) # TODO:(jr, 15.08.2022): blow_out commands with no labware get filtered # into custom. Refactor this in followup legacy command mapping - assert commands_result[26] == commands.Custom.construct( + assert commands_result[26] == commands.Custom.model_construct( id=matchers.IsA(str), key=matchers.IsA(str), status=commands.CommandStatus.SUCCEEDED, @@ -602,7 +600,7 @@ async def test_big_protocol_commands(big_protocol_file: Path) -> None: notes=[], result=commands.CustomResult(), ) - assert commands_result[27] == commands.Aspirate.construct( + assert commands_result[27] == commands.Aspirate.model_construct( id=matchers.IsA(str), key=matchers.IsA(str), status=commands.CommandStatus.SUCCEEDED, @@ -619,7 +617,7 @@ async def test_big_protocol_commands(big_protocol_file: Path) -> None: notes=[], result=commands.AspirateResult(volume=50, position=DeckPoint(x=0, y=0, z=0)), ) - assert commands_result[28] == commands.Dispense.construct( + assert commands_result[28] == commands.Dispense.model_construct( id=matchers.IsA(str), key=matchers.IsA(str), status=commands.CommandStatus.SUCCEEDED, @@ -638,7 +636,7 @@ async def test_big_protocol_commands(big_protocol_file: Path) -> None: ) # TODO:(jr, 15.08.2022): aspirate commands with no labware get filtered # into custom. Refactor this in followup legacy command mapping - assert commands_result[29] == commands.Custom.construct( + assert commands_result[29] == commands.Custom.model_construct( id=matchers.IsA(str), key=matchers.IsA(str), status=commands.CommandStatus.SUCCEEDED, @@ -654,7 +652,7 @@ async def test_big_protocol_commands(big_protocol_file: Path) -> None: ) # TODO:(jr, 15.08.2022): dispense commands with no labware get filtered # into custom. Refactor this in followup legacy command mapping - assert commands_result[30] == commands.Custom.construct( + assert commands_result[30] == commands.Custom.model_construct( id=matchers.IsA(str), key=matchers.IsA(str), status=commands.CommandStatus.SUCCEEDED, @@ -668,7 +666,7 @@ async def test_big_protocol_commands(big_protocol_file: Path) -> None: notes=[], result=commands.CustomResult(), ) - assert commands_result[31] == commands.DropTip.construct( + assert commands_result[31] == commands.DropTip.model_construct( id=matchers.IsA(str), key=matchers.IsA(str), status=commands.CommandStatus.SUCCEEDED, @@ -814,7 +812,7 @@ def run(protocol): ) result_commands = await simulate_and_get_commands(path) [initial_home, comment] = result_commands - assert comment == commands.Comment.construct( + assert comment == commands.Comment.model_construct( status=commands.CommandStatus.SUCCEEDED, params=commands.CommentParams(message="oy."), notes=[], diff --git a/api/tests/opentrons/protocol_runner/smoke_tests/test_legacy_custom_labware.py b/api/tests/opentrons/protocol_runner/smoke_tests/test_legacy_custom_labware.py index 7ed54b17ebe..dcc95593c38 100644 --- a/api/tests/opentrons/protocol_runner/smoke_tests/test_legacy_custom_labware.py +++ b/api/tests/opentrons/protocol_runner/smoke_tests/test_legacy_custom_labware.py @@ -58,7 +58,7 @@ async def test_legacy_custom_labware(custom_labware_protocol_files: List[Path]) ) result = await subject.run(deck_configuration=[], protocol_source=protocol_source) - expected_labware = LoadedLabware.construct( + expected_labware = LoadedLabware.model_construct( id=matchers.Anything(), location=DeckSlotLocation(slotName=DeckSlotName.SLOT_1), loadName="fixture_96_plate", diff --git a/api/tests/opentrons/protocol_runner/smoke_tests/test_legacy_module_commands.py b/api/tests/opentrons/protocol_runner/smoke_tests/test_legacy_module_commands.py index de14413a7ab..5650312b5f6 100644 --- a/api/tests/opentrons/protocol_runner/smoke_tests/test_legacy_module_commands.py +++ b/api/tests/opentrons/protocol_runner/smoke_tests/test_legacy_module_commands.py @@ -75,7 +75,7 @@ async def test_runner_with_modules_in_legacy_python( thermocycler_result_captor = matchers.Captor() heater_shaker_result_captor = matchers.Captor() - assert commands_result[0] == commands.Home.construct( + assert commands_result[0] == commands.Home.model_construct( id=matchers.IsA(str), key=matchers.IsA(str), status=commands.CommandStatus.SUCCEEDED, @@ -86,7 +86,7 @@ async def test_runner_with_modules_in_legacy_python( notes=[], result=commands.HomeResult(), ) - assert commands_result[1] == commands.LoadLabware.construct( + assert commands_result[1] == commands.LoadLabware.model_construct( id=matchers.IsA(str), key=matchers.IsA(str), status=commands.CommandStatus.SUCCEEDED, @@ -98,7 +98,7 @@ async def test_runner_with_modules_in_legacy_python( result=matchers.Anything(), ) - assert commands_result[2] == commands.LoadModule.construct( + assert commands_result[2] == commands.LoadModule.model_construct( id=matchers.IsA(str), key=matchers.IsA(str), status=commands.CommandStatus.SUCCEEDED, @@ -110,7 +110,7 @@ async def test_runner_with_modules_in_legacy_python( result=temp_module_result_captor, ) - assert commands_result[3] == commands.LoadModule.construct( + assert commands_result[3] == commands.LoadModule.model_construct( id=matchers.IsA(str), key=matchers.IsA(str), status=commands.CommandStatus.SUCCEEDED, @@ -122,7 +122,7 @@ async def test_runner_with_modules_in_legacy_python( result=mag_module_result_captor, ) - assert commands_result[4] == commands.LoadModule.construct( + assert commands_result[4] == commands.LoadModule.model_construct( id=matchers.IsA(str), key=matchers.IsA(str), status=commands.CommandStatus.SUCCEEDED, @@ -134,7 +134,7 @@ async def test_runner_with_modules_in_legacy_python( result=thermocycler_result_captor, ) - assert commands_result[5] == commands.LoadModule.construct( + assert commands_result[5] == commands.LoadModule.model_construct( id=matchers.IsA(str), key=matchers.IsA(str), status=commands.CommandStatus.SUCCEEDED, @@ -146,12 +146,9 @@ async def test_runner_with_modules_in_legacy_python( result=heater_shaker_result_captor, ) - assert temp_module_result_captor.value["model"] == ModuleModel.TEMPERATURE_MODULE_V1 - assert mag_module_result_captor.value["model"] == ModuleModel.MAGNETIC_MODULE_V1 + assert temp_module_result_captor.value.model == ModuleModel.TEMPERATURE_MODULE_V1 + assert mag_module_result_captor.value.model == ModuleModel.MAGNETIC_MODULE_V1 + assert thermocycler_result_captor.value.model == ModuleModel.THERMOCYCLER_MODULE_V1 assert ( - thermocycler_result_captor.value["model"] == ModuleModel.THERMOCYCLER_MODULE_V1 - ) - assert ( - heater_shaker_result_captor.value["model"] - == ModuleModel.HEATER_SHAKER_MODULE_V1 + heater_shaker_result_captor.value.model == ModuleModel.HEATER_SHAKER_MODULE_V1 ) diff --git a/api/tests/opentrons/protocol_runner/smoke_tests/test_protocol_runner.py b/api/tests/opentrons/protocol_runner/smoke_tests/test_protocol_runner.py index 1a8da30bd76..5db66e55eb2 100644 --- a/api/tests/opentrons/protocol_runner/smoke_tests/test_protocol_runner.py +++ b/api/tests/opentrons/protocol_runner/smoke_tests/test_protocol_runner.py @@ -58,13 +58,13 @@ async def test_runner_with_python( pipette_id_captor = matchers.Captor() labware_id_captor = matchers.Captor() - expected_pipette = LoadedPipette.construct( + expected_pipette = LoadedPipette.model_construct( id=pipette_id_captor, pipetteName=PipetteNameType.P300_SINGLE, mount=MountType.LEFT, ) - expected_labware = LoadedLabware.construct( + expected_labware = LoadedLabware.model_construct( id=labware_id_captor, location=DeckSlotLocation(slotName=DeckSlotName.SLOT_1), loadName="opentrons_96_tiprack_300ul", @@ -75,7 +75,7 @@ async def test_runner_with_python( offsetId=None, ) - expected_module = LoadedModule.construct( + expected_module = LoadedModule.model_construct( id=matchers.IsA(str), model=ModuleModel.TEMPERATURE_MODULE_V1, location=DeckSlotLocation(slotName=DeckSlotName.SLOT_3), @@ -86,7 +86,7 @@ async def test_runner_with_python( assert expected_labware in labware_result assert expected_module in modules_result - expected_command = commands.PickUpTip.construct( + expected_command = commands.PickUpTip.model_construct( id=matchers.IsA(str), key=matchers.IsA(str), status=commands.CommandStatus.SUCCEEDED, @@ -148,7 +148,7 @@ async def test_runner_with_json(json_protocol_file: Path) -> None: assert expected_pipette in pipettes_result assert expected_labware in labware_result - expected_command = commands.PickUpTip.construct( + expected_command = commands.PickUpTip.model_construct( id=matchers.IsA(str), key=matchers.IsA(str), status=commands.CommandStatus.SUCCEEDED, @@ -196,13 +196,13 @@ async def test_runner_with_legacy_python(legacy_python_protocol_file: Path) -> N pipette_id_captor = matchers.Captor() labware_id_captor = matchers.Captor() - expected_pipette = LoadedPipette.construct( + expected_pipette = LoadedPipette.model_construct( id=pipette_id_captor, pipetteName=PipetteNameType.P300_SINGLE, mount=MountType.LEFT, ) - expected_labware = LoadedLabware.construct( + expected_labware = LoadedLabware.model_construct( id=labware_id_captor, location=DeckSlotLocation(slotName=DeckSlotName.SLOT_1), loadName="opentrons_96_tiprack_300ul", @@ -215,7 +215,7 @@ async def test_runner_with_legacy_python(legacy_python_protocol_file: Path) -> N assert expected_pipette in pipettes_result assert expected_labware in labware_result - expected_command = commands.PickUpTip.construct( + expected_command = commands.PickUpTip.model_construct( id=matchers.IsA(str), key=matchers.IsA(str), status=commands.CommandStatus.SUCCEEDED, @@ -260,13 +260,13 @@ async def test_runner_with_legacy_json(legacy_json_protocol_file: Path) -> None: pipette_id_captor = matchers.Captor() labware_id_captor = matchers.Captor() - expected_pipette = LoadedPipette.construct( + expected_pipette = LoadedPipette.model_construct( id=pipette_id_captor, pipetteName=PipetteNameType.P300_SINGLE, mount=MountType.LEFT, ) - expected_labware = LoadedLabware.construct( + expected_labware = LoadedLabware.model_construct( id=labware_id_captor, location=DeckSlotLocation(slotName=DeckSlotName.SLOT_1), loadName="opentrons_96_tiprack_300ul", @@ -280,7 +280,7 @@ async def test_runner_with_legacy_json(legacy_json_protocol_file: Path) -> None: assert expected_pipette in pipettes_result assert expected_labware in labware_result - expected_command = commands.PickUpTip.construct( + expected_command = commands.PickUpTip.model_construct( id=matchers.IsA(str), key=matchers.IsA(str), status=commands.CommandStatus.SUCCEEDED, @@ -327,13 +327,13 @@ async def test_runner_with_python_and_run_time_parameters( tiprack_id_captor = matchers.Captor() reservoir_id_captor = matchers.Captor() - expected_pipette = LoadedPipette.construct( + expected_pipette = LoadedPipette.model_construct( id=pipette_id_captor, pipetteName=PipetteNameType.P300_SINGLE, mount=MountType.LEFT, ) - expected_tiprack = LoadedLabware.construct( + expected_tiprack = LoadedLabware.model_construct( id=tiprack_id_captor, location=DeckSlotLocation(slotName=DeckSlotName.SLOT_1), loadName="opentrons_96_tiprack_300ul", @@ -344,7 +344,7 @@ async def test_runner_with_python_and_run_time_parameters( offsetId=None, ) - expected_reservoir = LoadedLabware.construct( + expected_reservoir = LoadedLabware.model_construct( id=reservoir_id_captor, location=DeckSlotLocation(slotName=DeckSlotName.SLOT_2), loadName="nest_1_reservoir_195ml", @@ -361,14 +361,14 @@ async def test_runner_with_python_and_run_time_parameters( assert result.state_summary.status == EngineStatus.SUCCEEDED - expected_command = commands.Aspirate.construct( + expected_command = commands.Aspirate.model_construct( id=matchers.IsA(str), key=matchers.IsA(str), status=commands.CommandStatus.SUCCEEDED, createdAt=matchers.IsA(datetime), startedAt=matchers.IsA(datetime), completedAt=matchers.IsA(datetime), - params=commands.AspirateParams.construct( + params=commands.AspirateParams.model_construct( labwareId=reservoir_id_captor.value, wellName=matchers.IsA(str), wellLocation=matchers.Anything(), diff --git a/api/tests/opentrons/protocol_runner/test_json_translator.py b/api/tests/opentrons/protocol_runner/test_json_translator.py index e2735e4cdbc..b48c18f95c9 100644 --- a/api/tests/opentrons/protocol_runner/test_json_translator.py +++ b/api/tests/opentrons/protocol_runner/test_json_translator.py @@ -1,4 +1,5 @@ """Tests for the JSON JsonTranslator interface.""" + import pytest from typing import Dict, List @@ -193,7 +194,7 @@ wellName="A1", ), ), - protocol_schema_v8.Command( + protocol_schema_v8.Command.model_construct( commandType="dropTip", params={ "pipetteId": "pipette-id-1", @@ -230,7 +231,7 @@ wellName="A1", ), ), - protocol_schema_v8.Command( + protocol_schema_v8.Command.model_construct( commandType="pickUpTip", params={ "pipetteId": "pipette-id-1", @@ -272,7 +273,7 @@ ), ), ), - protocol_schema_v8.Command( + protocol_schema_v8.Command.model_construct( commandType="touchTip", params={ "pipetteId": "pipette-id-1", @@ -307,7 +308,7 @@ pipetteId="pipette-id-1", mount="left", pipetteName="p10_single" ), ), - protocol_schema_v8.Command( + protocol_schema_v8.Command.model_construct( commandType="loadPipette", params={ "pipetteId": "pipette-id-1", @@ -339,7 +340,7 @@ location=Location(slotName="3"), ), ), - protocol_schema_v8.Command( + protocol_schema_v8.Command.model_construct( commandType="loadModule", params={ "moduleId": "module-id-1", @@ -374,7 +375,7 @@ displayName="Trash", ), ), - protocol_schema_v8.Command( + protocol_schema_v8.Command.model_construct( commandType="loadLabware", params={ "labwareId": "labware-id-2", @@ -423,7 +424,7 @@ flowRate=1.23, ), ), - protocol_schema_v8.Command( + protocol_schema_v8.Command.model_construct( commandType="blowout", params={ "pipetteId": "pipette-id-1", @@ -458,7 +459,7 @@ commandType="delay", params=protocol_schema_v7.Params(waitForResume=True, message="hello world"), ), - protocol_schema_v8.Command( + protocol_schema_v8.Command.model_construct( commandType="delay", params={"waitForResume": True, "message": "hello world"}, ), @@ -475,7 +476,7 @@ commandType="delay", params=protocol_schema_v7.Params(seconds=12.34, message="hello world"), ), - protocol_schema_v8.Command( + protocol_schema_v8.Command.model_construct( commandType="delay", params={"seconds": 12.34, "message": "hello world"}, ), @@ -495,7 +496,7 @@ commandType="waitForResume", params=protocol_schema_v7.Params(message="hello world"), ), - protocol_schema_v8.Command( + protocol_schema_v8.Command.model_construct( commandType="waitForResume", params={"message": "hello world"}, ), @@ -512,7 +513,7 @@ commandType="waitForDuration", params=protocol_schema_v7.Params(seconds=12.34, message="hello world"), ), - protocol_schema_v8.Command( + protocol_schema_v8.Command.model_construct( commandType="waitForDuration", params={"seconds": 12.34, "message": "hello world"}, ), @@ -542,7 +543,7 @@ forceDirect=True, ), ), - protocol_schema_v8.Command( + protocol_schema_v8.Command.model_construct( commandType="moveToCoordinates", params={ "pipetteId": "pipette-id-1", @@ -595,7 +596,7 @@ ], ), ), - protocol_schema_v8.Command( + protocol_schema_v8.Command.model_construct( commandType="thermocycler/runProfile", params={ "moduleId": "module-id-2", @@ -646,7 +647,7 @@ volumeByWell={"A1": 32, "B2": 50}, ), ), - protocol_schema_v8.Command( + protocol_schema_v8.Command.model_construct( commandType="loadLiquid", key=None, params={ @@ -873,6 +874,6 @@ def test_load_liquid( id="liquid-id-555", displayName="water", description="water description", - displayColor=HexColor(__root__="#F00"), + displayColor=HexColor("#F00"), ) ] 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 42c589ba7d3..a91066c01f8 100644 --- a/api/tests/opentrons/protocol_runner/test_legacy_command_mapper.py +++ b/api/tests/opentrons/protocol_runner/test_legacy_command_mapper.py @@ -117,7 +117,7 @@ def test_map_after_command() -> None: assert result == [ pe_actions.SucceedCommandAction( - command=pe_commands.Comment.construct( + command=pe_commands.Comment.model_construct( id="command.COMMENT-0", key="command.COMMENT-0", status=pe_commands.CommandStatus.SUCCEEDED, @@ -240,7 +240,7 @@ def test_command_stack() -> None: command_id="command.COMMENT-1", started_at=matchers.IsA(datetime) ), pe_actions.SucceedCommandAction( - command=pe_commands.Comment.construct( + command=pe_commands.Comment.model_construct( id="command.COMMENT-0", key="command.COMMENT-0", status=pe_commands.CommandStatus.SUCCEEDED, @@ -302,7 +302,7 @@ def test_map_labware_load(minimal_labware_def: LabwareDefinition) -> None: started_at=matchers.IsA(datetime), ) expected_succeed = pe_actions.SucceedCommandAction( - command=pe_commands.LoadLabware.construct( + command=pe_commands.LoadLabware.model_construct( id=expected_id_and_key, key=expected_id_and_key, params=expected_params, @@ -310,7 +310,7 @@ def test_map_labware_load(minimal_labware_def: LabwareDefinition) -> None: createdAt=matchers.IsA(datetime), startedAt=matchers.IsA(datetime), completedAt=matchers.IsA(datetime), - result=pe_commands.LoadLabwareResult.construct( + result=pe_commands.LoadLabwareResult.model_construct( labwareId=matchers.IsA(str), # Trusting that the exact fields within in the labware definition # get passed through correctly. @@ -352,7 +352,7 @@ def test_map_instrument_load(decoy: Decoy) -> None: ).then_return(pipette_config) expected_id_and_key = "commands.LOAD_PIPETTE-0" - expected_params = pe_commands.LoadPipetteParams.construct( + expected_params = pe_commands.LoadPipetteParams.model_construct( pipetteName=PipetteNameType.P1000_SINGLE_GEN2, mount=MountType.LEFT ) expected_queue = pe_actions.QueueCommandAction( @@ -367,7 +367,7 @@ def test_map_instrument_load(decoy: Decoy) -> None: command_id=expected_id_and_key, started_at=matchers.IsA(datetime) ) expected_succeed = pe_actions.SucceedCommandAction( - command=pe_commands.LoadPipette.construct( + command=pe_commands.LoadPipette.model_construct( id=expected_id_and_key, key=expected_id_and_key, status=pe_commands.CommandStatus.SUCCEEDED, @@ -410,7 +410,7 @@ def test_map_module_load( module_data_provider: ModuleDataProvider, ) -> None: """It should correctly map a module load.""" - test_definition = ModuleDefinition.parse_obj(minimal_module_def) + test_definition = ModuleDefinition.model_validate(minimal_module_def) input = LegacyModuleLoadInfo( requested_model=TemperatureModuleModel.TEMPERATURE_V1, loaded_model=TemperatureModuleModel.TEMPERATURE_V2, @@ -423,7 +423,7 @@ def test_map_module_load( ).then_return(test_definition) expected_id_and_key = "commands.LOAD_MODULE-0" - expected_params = pe_commands.LoadModuleParams.construct( + expected_params = pe_commands.LoadModuleParams.model_construct( model=ModuleModel.TEMPERATURE_MODULE_V1, location=DeckSlotLocation(slotName=DeckSlotName.SLOT_1), moduleId=matchers.IsA(str), @@ -440,7 +440,7 @@ def test_map_module_load( command_id=expected_id_and_key, started_at=matchers.IsA(datetime) ) expected_succeed = pe_actions.SucceedCommandAction( - command=pe_commands.LoadModule.construct( + command=pe_commands.LoadModule.model_construct( id=expected_id_and_key, key=expected_id_and_key, status=pe_commands.CommandStatus.SUCCEEDED, @@ -448,7 +448,7 @@ def test_map_module_load( startedAt=matchers.IsA(datetime), completedAt=matchers.IsA(datetime), params=expected_params, - result=pe_commands.LoadModuleResult.construct( + result=pe_commands.LoadModuleResult.model_construct( moduleId=matchers.IsA(str), serialNumber="module-serial", definition=test_definition, @@ -481,7 +481,7 @@ def test_map_module_labware_load(minimal_labware_def: LabwareDefinition) -> None ) expected_id_and_key = "commands.LOAD_LABWARE-0" - expected_params = pe_commands.LoadLabwareParams.construct( + expected_params = pe_commands.LoadLabwareParams.model_construct( location=ModuleLocation(moduleId="module-123"), namespace="some_namespace", loadName="some_load_name", @@ -503,7 +503,7 @@ def test_map_module_labware_load(minimal_labware_def: LabwareDefinition) -> None started_at=matchers.IsA(datetime), ) expected_succeed = pe_actions.SucceedCommandAction( - command=pe_commands.LoadLabware.construct( + command=pe_commands.LoadLabware.model_construct( id=expected_id_and_key, key=expected_id_and_key, params=expected_params, @@ -511,7 +511,7 @@ def test_map_module_labware_load(minimal_labware_def: LabwareDefinition) -> None createdAt=matchers.IsA(datetime), startedAt=matchers.IsA(datetime), completedAt=matchers.IsA(datetime), - result=pe_commands.LoadLabwareResult.construct( + result=pe_commands.LoadLabwareResult.model_construct( labwareId=matchers.IsA(str), # Trusting that the exact fields within in the labware definition # get passed through correctly. @@ -578,7 +578,7 @@ def test_map_pause() -> None: started_at=matchers.IsA(datetime), ), pe_actions.SucceedCommandAction( - command=pe_commands.WaitForResume.construct( + command=pe_commands.WaitForResume.model_construct( id="command.PAUSE-0", key="command.PAUSE-0", status=pe_commands.CommandStatus.SUCCEEDED, diff --git a/api/tests/opentrons/protocol_runner/test_protocol_runner.py b/api/tests/opentrons/protocol_runner/test_protocol_runner.py index 2f06e27c2c2..2080ec69587 100644 --- a/api/tests/opentrons/protocol_runner/test_protocol_runner.py +++ b/api/tests/opentrons/protocol_runner/test_protocol_runner.py @@ -361,7 +361,7 @@ async def test_run_json_runner_stop_requested_stops_enqueuing( json_translator: JsonTranslator, ) -> None: """It should run a protocol to completion.""" - labware_definition = LabwareDefinition.construct() # type: ignore[call-arg] + labware_definition = LabwareDefinition.model_construct() # type: ignore[call-arg] json_protocol_source = ProtocolSource( directory=Path("/dev/null"), main_file=Path("/dev/null/abc.json"), @@ -388,7 +388,7 @@ async def test_run_json_runner_stop_requested_stops_enqueuing( Liquid(id="water-id", displayName="water", description="water desc") ] - json_protocol = ProtocolSchemaV6.construct() # type: ignore[call-arg] + json_protocol = ProtocolSchemaV6.model_construct() # type: ignore[call-arg] decoy.when( await protocol_reader.extract_labware_definitions(json_protocol_source) @@ -401,7 +401,7 @@ async def test_run_json_runner_stop_requested_stops_enqueuing( pe_commands.HomeCreate(params=pe_commands.HomeParams()), ) ).then_return( - pe_commands.Home.construct(status=pe_commands.CommandStatus.SUCCEEDED) # type: ignore[call-arg] + pe_commands.Home.model_construct(status=pe_commands.CommandStatus.SUCCEEDED) # type: ignore[call-arg] ) decoy.when( await protocol_engine.add_and_execute_command_wait_for_recovery( @@ -410,7 +410,7 @@ async def test_run_json_runner_stop_requested_stops_enqueuing( ), ) ).then_return( - pe_commands.WaitForDuration.construct( # type: ignore[call-arg] + pe_commands.WaitForDuration.model_construct( # type: ignore[call-arg] id="protocol-command-id", error=pe_errors.ErrorOccurrence.from_failed( id="some-id", @@ -448,11 +448,12 @@ async def test_run_json_runner_stop_requested_stops_enqueuing( await run_func() +@pytest.mark.filterwarnings("ignore::decoy.warnings.RedundantVerifyWarning") @pytest.mark.parametrize( "schema_version, json_protocol", [ - (6, ProtocolSchemaV6.construct()), # type: ignore[call-arg] - (7, ProtocolSchemaV7.construct()), # type: ignore[call-arg] + (6, ProtocolSchemaV6.model_construct()), # type: ignore[call-arg] + (7, ProtocolSchemaV7.model_construct()), # type: ignore[call-arg] ], ) async def test_load_json_runner( @@ -466,7 +467,7 @@ async def test_load_json_runner( json_protocol: Union[ProtocolSchemaV6, ProtocolSchemaV7], ) -> None: """It should load a JSON protocol file.""" - labware_definition = LabwareDefinition.construct() # type: ignore[call-arg] + labware_definition = LabwareDefinition.model_construct() # type: ignore[call-arg] json_protocol_source = ProtocolSource( directory=Path("/dev/null"), @@ -527,7 +528,7 @@ async def test_load_json_runner( ), ) ).then_return( - pe_commands.WaitForResume.construct( # type: ignore[call-arg] + pe_commands.WaitForResume.model_construct( # type: ignore[call-arg] id="command-id-1", status=CommandStatus.SUCCEEDED, error=None, @@ -540,7 +541,7 @@ async def test_load_json_runner( ), ) ).then_return( - pe_commands.WaitForResume.construct( # type: ignore[call-arg] + pe_commands.WaitForResume.model_construct( # type: ignore[call-arg] id="command-id-2", status=CommandStatus.SUCCEEDED, error=None, @@ -555,7 +556,7 @@ async def test_load_json_runner( ), ) ).then_return( - pe_commands.WaitForResume.construct( # type: ignore[call-arg] + pe_commands.WaitForResume.model_construct( # type: ignore[call-arg] id="command-id-3", status=CommandStatus.SUCCEEDED, error=None, @@ -600,7 +601,7 @@ async def test_load_legacy_python( python_runner_subject: PythonAndLegacyRunner, ) -> None: """It should load a legacy context-based Python protocol.""" - labware_definition = LabwareDefinition.construct() # type: ignore[call-arg] + labware_definition = LabwareDefinition.model_construct() # type: ignore[call-arg] legacy_protocol_source = ProtocolSource( directory=Path("/dev/null"), @@ -751,7 +752,7 @@ async def test_load_legacy_json( python_runner_subject: PythonAndLegacyRunner, ) -> None: """It should load a legacy context-based JSON protocol.""" - labware_definition = LabwareDefinition.construct() # type: ignore[call-arg] + labware_definition = LabwareDefinition.model_construct() # type: ignore[call-arg] legacy_protocol_source = ProtocolSource( directory=Path("/dev/null"), diff --git a/api/tests/opentrons/protocol_runner/test_run_orchestrator.py b/api/tests/opentrons/protocol_runner/test_run_orchestrator.py index c2cea3e0e7e..b7281953f22 100644 --- a/api/tests/opentrons/protocol_runner/test_run_orchestrator.py +++ b/api/tests/opentrons/protocol_runner/test_run_orchestrator.py @@ -256,11 +256,11 @@ async def test_add_command_and_wait_for_interval( verify_calls: int, ) -> None: """Should add a command a wait for it to complete.""" - load_command = pe_commands.HomeCreate.construct( - params=pe_commands.HomeParams.construct() + load_command = pe_commands.HomeCreate.model_construct( + params=pe_commands.HomeParams.model_construct() ) added_command = pe_commands.Home( - params=pe_commands.HomeParams.construct(), + params=pe_commands.HomeParams.model_construct(), id="test-123", createdAt=datetime(year=2024, month=1, day=1), key="123", diff --git a/api/tests/opentrons/protocols/advanced_control/transfers/__init__.py b/api/tests/opentrons/protocols/advanced_control/transfers/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/api/tests/opentrons/protocols/advanced_control/transfers/test_common_functions.py b/api/tests/opentrons/protocols/advanced_control/transfers/test_common_functions.py new file mode 100644 index 00000000000..644c2b7094f --- /dev/null +++ b/api/tests/opentrons/protocols/advanced_control/transfers/test_common_functions.py @@ -0,0 +1,77 @@ +"""Test the common utility functions used in transfers.""" +import pytest +from contextlib import nullcontext as does_not_raise +from typing import ContextManager, Any, Iterable, List, Tuple + +from opentrons.protocols.advanced_control.transfers.common import ( + Target, + check_valid_volume_parameters, + expand_for_volume_constraints, +) + + +@pytest.mark.parametrize( + argnames=["disposal_volume", "air_gap", "max_volume", "expected_raise"], + argvalues=[ + (9.9, 9.9, 10, pytest.raises(ValueError, match="The sum of")), + (9.9, 10, 10, pytest.raises(ValueError, match="The air gap must be less than")), + ( + 10, + 9.9, + 10, + pytest.raises(ValueError, match="The disposal volume must be less than"), + ), + (9.9, 9.9, 20, does_not_raise()), + ], +) +def test_check_valid_volume_parameters( + disposal_volume: float, + air_gap: float, + max_volume: float, + expected_raise: ContextManager[Any], +) -> None: + """It should raise the expected error for invalid parameters.""" + with expected_raise: + check_valid_volume_parameters( + disposal_volume=disposal_volume, + air_gap=air_gap, + max_volume=max_volume, + ) + + +@pytest.mark.parametrize( + argnames=["volumes", "targets", "max_volume", "expanded_list_result"], + argvalues=[ + ( + [60, 70, 75], + [("a", "b"), ("c", "d"), ("e", "f")], + 20, + [ + (20, ("a", "b")), + (20, ("a", "b")), + (20, ("a", "b")), + (20, ("c", "d")), + (20, ("c", "d")), + (15, ("c", "d")), + (15, ("c", "d")), + (20, ("e", "f")), + (20, ("e", "f")), + (17.5, ("e", "f")), + (17.5, ("e", "f")), + ], + ), + ], +) +def test_expand_for_volume_constraints( + volumes: Iterable[float], + targets: Iterable[Target], + max_volume: float, + expanded_list_result: List[Tuple[float, Target]], +) -> None: + """It should raise the expected error for invalid parameters.""" + result = expand_for_volume_constraints( + volumes=volumes, + targets=targets, + max_volume=max_volume, + ) + assert list(result) == expanded_list_result diff --git a/api/tests/opentrons/protocols/advanced_control/test_transfers.py b/api/tests/opentrons/protocols/advanced_control/transfers/test_transfers.py similarity index 99% rename from api/tests/opentrons/protocols/advanced_control/test_transfers.py rename to api/tests/opentrons/protocols/advanced_control/transfers/test_transfers.py index 7fc02d36aab..d54ad38a62b 100644 --- a/api/tests/opentrons/protocols/advanced_control/test_transfers.py +++ b/api/tests/opentrons/protocols/advanced_control/transfers/test_transfers.py @@ -3,7 +3,8 @@ from typing import TypedDict from opentrons.types import Mount, TransferTipPolicy -from opentrons.protocols.advanced_control import transfers as tx +from opentrons.protocols.advanced_control.transfers import transfer as tx +from opentrons.protocols.advanced_control.common import MixStrategy from opentrons.protocols.api_support.types import APIVersion from opentrons.hardware_control import ThreadManagedHardware from opentrons.protocol_api.protocol_context import ProtocolContext @@ -633,7 +634,7 @@ def test_touchtip_mix(_instr_labware: InstrLabware) -> None: transfer=options.transfer._replace( new_tip=TransferTipPolicy.NEVER, touch_tip_strategy=tx.TouchTipStrategy.ALWAYS, - mix_strategy=tx.MixStrategy.AFTER, + mix_strategy=MixStrategy.AFTER, ) ) @@ -759,7 +760,7 @@ def test_all_options(_instr_labware: InstrLabware) -> None: new_tip=TransferTipPolicy.ONCE, drop_tip_strategy=tx.DropTipStrategy.RETURN, touch_tip_strategy=tx.TouchTipStrategy.ALWAYS, - mix_strategy=tx.MixStrategy.AFTER, + mix_strategy=MixStrategy.AFTER, ), pick_up_tip=options.pick_up_tip._replace(presses=4, increment=2), touch_tip=options.touch_tip._replace(speed=1.6), diff --git a/api/tests/opentrons/protocols/geometry/test_frustum_helpers.py b/api/tests/opentrons/protocols/geometry/test_frustum_helpers.py index 0b8d3429527..d9bd8173834 100644 --- a/api/tests/opentrons/protocols/geometry/test_frustum_helpers.py +++ b/api/tests/opentrons/protocols/geometry/test_frustum_helpers.py @@ -11,7 +11,6 @@ _cross_section_area_rectangular, _cross_section_area_circular, _reject_unacceptable_heights, - _circular_frustum_polynomial_roots, _rectangular_frustum_polynomial_roots, _volume_from_height_rectangular, _volume_from_height_circular, @@ -211,39 +210,25 @@ def test_volume_and_height_circular(well: List[Any]) -> None: """Test both volume and height calculations for circular frusta.""" if well[-1].shape == "spherical": return - total_height = well[0].topHeight for segment in well: if segment.shape == "conical": - top_radius = segment.topDiameter / 2 - bottom_radius = segment.bottomDiameter / 2 - a = pi * ((top_radius - bottom_radius) ** 2) / (3 * total_height**2) - b = pi * bottom_radius * (top_radius - bottom_radius) / total_height - c = pi * bottom_radius**2 - assert _circular_frustum_polynomial_roots( - top_radius=top_radius, - bottom_radius=bottom_radius, - total_frustum_height=total_height, - ) == (a, b, c) + a = segment.topDiameter / 2 + b = segment.bottomDiameter / 2 # test volume within a bunch of arbitrary heights - for target_height in range(round(total_height)): - expected_volume = ( - a * (target_height**3) - + b * (target_height**2) - + c * target_height + segment_height = segment.topHeight - segment.bottomHeight + for target_height in range(round(segment_height)): + r_y = (target_height / segment_height) * (a - b) + b + expected_volume = (pi * target_height / 3) * ( + b**2 + b * r_y + r_y**2 ) found_volume = _volume_from_height_circular( target_height=target_height, - total_frustum_height=total_height, - bottom_radius=bottom_radius, - top_radius=top_radius, + segment=segment, ) - assert found_volume == expected_volume + assert isclose(found_volume, expected_volume) # test going backwards to get height back found_height = _height_from_volume_circular( - volume=found_volume, - total_frustum_height=total_height, - bottom_radius=bottom_radius, - top_radius=top_radius, + target_volume=found_volume, segment=segment ) assert isclose(found_height, target_height) diff --git a/api/tests/opentrons/protocols/models/test_json_protocol.py b/api/tests/opentrons/protocols/models/test_json_protocol.py index 696524ac84a..afb2770f21a 100644 --- a/api/tests/opentrons/protocols/models/test_json_protocol.py +++ b/api/tests/opentrons/protocols/models/test_json_protocol.py @@ -25,7 +25,7 @@ def test_json_protocol_model( ) # Create the model - d = json_protocol.Model.parse_obj(fx) + d = json_protocol.Model.model_validate(fx) # Compare the dict created by pydantic to the loaded json - assert d.dict(exclude_unset=True, by_alias=True) == fx + assert d.model_dump(exclude_unset=True, by_alias=True) == fx diff --git a/api/tests/opentrons/test_execute.py b/api/tests/opentrons/test_execute.py index b9f791f4253..1e72a3757bf 100644 --- a/api/tests/opentrons/test_execute.py +++ b/api/tests/opentrons/test_execute.py @@ -131,6 +131,7 @@ def test_execute_function_apiv2( converted_model_v15.pipette_type, converted_model_v15.pipette_channels, converted_model_v15.pipette_version, + converted_model_v15.oem_type, ), "id": "testid", } @@ -139,6 +140,7 @@ def test_execute_function_apiv2( converted_model_v1.pipette_type, converted_model_v1.pipette_channels, converted_model_v1.pipette_version, + converted_model_v1.oem_type, ), "id": "testid2", } @@ -177,6 +179,7 @@ def emit_runlog(entry: Any) -> None: converted_model_v15.pipette_type, converted_model_v15.pipette_channels, converted_model_v15.pipette_version, + converted_model_v15.oem_type, ), "id": "testid", } @@ -215,6 +218,7 @@ def emit_runlog(entry: Any) -> None: converted_model_v15.pipette_type, converted_model_v15.pipette_channels, converted_model_v15.pipette_version, + converted_model_v15.oem_type, ), "id": "testid", } @@ -253,6 +257,7 @@ def emit_runlog(entry: Any) -> None: converted_model_v15.pipette_type, converted_model_v15.pipette_channels, converted_model_v15.pipette_version, + converted_model_v15.oem_type, ), "id": "testid", } @@ -292,6 +297,7 @@ def emit_runlog(entry: Any) -> None: converted_model_v15.pipette_type, converted_model_v15.pipette_channels, converted_model_v15.pipette_version, + converted_model_v15.oem_type, ), "id": "testid", } diff --git a/app-shell-odd/Makefile b/app-shell-odd/Makefile index 5d2d7ac37bd..f94cab4a611 100644 --- a/app-shell-odd/Makefile +++ b/app-shell-odd/Makefile @@ -72,7 +72,7 @@ dist-ot3: clean lib NO_USB_DETECTION=true OT_APP_DEPLOY_BUCKET=opentrons-app OT_APP_DEPLOY_FOLDER=builds OPENTRONS_PROJECT=$(OPENTRONS_PROJECT) $(builder) --linux --arm64 .PHONY: push-ot3 -push-ot3: dist-ot3 deps +push-ot3: deps dist-ot3 tar -zcvf opentrons-robot-app.tar.gz -C ./dist/linux-arm64-unpacked/ ./ scp $(if $(ssh_key),-i $(ssh_key)) $(ssh_opts) -r ./opentrons-robot-app.tar.gz root@$(host): ssh $(if $(ssh_key),-i $(ssh_key)) $(ssh_opts) root@$(host) "mount -o remount,rw / && systemctl stop opentrons-robot-app && rm -rf /opt/opentrons-app && mkdir -p /opt/opentrons-app" diff --git a/app-shell-odd/electron-builder.config.js b/app-shell-odd/electron-builder.config.js index d5cd4ac7eea..10faa01b3b0 100644 --- a/app-shell-odd/electron-builder.config.js +++ b/app-shell-odd/electron-builder.config.js @@ -2,7 +2,7 @@ module.exports = { appId: 'com.opentrons.odd', - electronVersion: '27.0.0', + electronVersion: '33.2.1', npmRebuild: false, files: [ '**/*', diff --git a/app-shell-odd/package.json b/app-shell-odd/package.json index 253b41ca895..6ced69031f9 100644 --- a/app-shell-odd/package.json +++ b/app-shell-odd/package.json @@ -8,7 +8,7 @@ "types": "lib/main.d.ts", "scripts": { "start": "make dev", - "rebuild": "electron-rebuild" + "rebuild": "electron-rebuild --force-abi=3.71.0" }, "repository": { "type": "git", @@ -42,7 +42,7 @@ "dateformat": "3.0.3", "electron-devtools-installer": "3.2.0", "electron-store": "5.1.1", - "electron-updater": "4.1.2", + "electron-updater": "6.3.9", "execa": "4.0.0", "form-data": "2.5.0", "fs-extra": "10.0.0", diff --git a/app-shell-odd/src/config/__fixtures__/index.ts b/app-shell-odd/src/config/__fixtures__/index.ts index 7f9a48dc02c..1b05dfed7a6 100644 --- a/app-shell-odd/src/config/__fixtures__/index.ts +++ b/app-shell-odd/src/config/__fixtures__/index.ts @@ -13,6 +13,7 @@ import type { ConfigV23, ConfigV24, ConfigV25, + ConfigV26, } from '@opentrons/app/src/redux/config/types' const PKG_VERSION: string = _PKG_VERSION_ @@ -181,3 +182,12 @@ export const MOCK_CONFIG_V25: ConfigV25 = { systemLanguage: null, }, } + +export const MOCK_CONFIG_V26: ConfigV26 = { + ...MOCK_CONFIG_V25, + version: 26, + onDeviceDisplaySettings: { + ...MOCK_CONFIG_V25.onDeviceDisplaySettings, + unfinishedUnboxingFlowRoute: '/choose-language', + }, +} diff --git a/app-shell-odd/src/config/__tests__/migrate.test.ts b/app-shell-odd/src/config/__tests__/migrate.test.ts index 7ea91ee8d53..f1be25b5fe9 100644 --- a/app-shell-odd/src/config/__tests__/migrate.test.ts +++ b/app-shell-odd/src/config/__tests__/migrate.test.ts @@ -17,13 +17,14 @@ import { MOCK_CONFIG_V23, MOCK_CONFIG_V24, MOCK_CONFIG_V25, + MOCK_CONFIG_V26, } from '../__fixtures__' import { migrate } from '../migrate' vi.mock('uuid/v4') -const NEWEST_VERSION = 25 -const NEWEST_MOCK_CONFIG = MOCK_CONFIG_V25 +const NEWEST_VERSION = 26 +const NEWEST_MOCK_CONFIG = MOCK_CONFIG_V26 describe('config migration', () => { beforeEach(() => { @@ -129,10 +130,17 @@ describe('config migration', () => { expect(result.version).toBe(NEWEST_VERSION) expect(result).toEqual(NEWEST_MOCK_CONFIG) }) - it('should keep version 25', () => { + it('should migrate version 25 to latest', () => { const v25Config = MOCK_CONFIG_V25 const result = migrate(v25Config) + expect(result.version).toBe(NEWEST_VERSION) + expect(result).toEqual(NEWEST_MOCK_CONFIG) + }) + it('should keep version 26', () => { + const v26Config = MOCK_CONFIG_V26 + const result = migrate(v26Config) + expect(result.version).toBe(NEWEST_VERSION) expect(result).toEqual(NEWEST_MOCK_CONFIG) }) diff --git a/app-shell-odd/src/config/migrate.ts b/app-shell-odd/src/config/migrate.ts index b6977fbf489..48d30e1297a 100644 --- a/app-shell-odd/src/config/migrate.ts +++ b/app-shell-odd/src/config/migrate.ts @@ -18,13 +18,14 @@ import type { ConfigV23, ConfigV24, ConfigV25, + ConfigV26, } from '@opentrons/app/src/redux/config/types' // format // base config v12 defaults // any default values for later config versions are specified in the migration // functions for those version below -const CONFIG_VERSION_LATEST = 25 // update this after each config version bump +const CONFIG_VERSION_LATEST = 26 // update this after each config version bump const PKG_VERSION: string = _PKG_VERSION_ export const DEFAULTS_V12: ConfigV12 = { @@ -238,6 +239,21 @@ const toVersion25 = (prevConfig: ConfigV24): ConfigV25 => { } return nextConfig } +const toVersion26 = (prevConfig: ConfigV25): ConfigV26 => { + const nextConfig = { + ...prevConfig, + version: 26 as const, + onDeviceDisplaySettings: { + ...prevConfig.onDeviceDisplaySettings, + unfinishedUnboxingFlowRoute: + prevConfig.onDeviceDisplaySettings.unfinishedUnboxingFlowRoute === + '/welcome' + ? '/choose-language' + : prevConfig.onDeviceDisplaySettings.unfinishedUnboxingFlowRoute, + }, + } + return nextConfig +} const MIGRATIONS: [ (prevConfig: ConfigV12) => ConfigV13, @@ -252,7 +268,8 @@ const MIGRATIONS: [ (prevConfig: ConfigV21) => ConfigV22, (prevConfig: ConfigV22) => ConfigV23, (prevConfig: ConfigV23) => ConfigV24, - (prevConfig: ConfigV24) => ConfigV25 + (prevConfig: ConfigV24) => ConfigV25, + (prevConfig: ConfigV25) => ConfigV26 ] = [ toVersion13, toVersion14, @@ -267,6 +284,7 @@ const MIGRATIONS: [ toVersion23, toVersion24, toVersion25, + toVersion26, ] export const DEFAULTS: Config = migrate(DEFAULTS_V12) @@ -287,6 +305,7 @@ export function migrate( | ConfigV23 | ConfigV24 | ConfigV25 + | ConfigV26 ): Config { let result = prevConfig // loop through the migrations, skipping any migrations that are unnecessary diff --git a/app-shell-odd/src/main.ts b/app-shell-odd/src/main.ts index ccb9ff61aa2..9eb17a016cc 100644 --- a/app-shell-odd/src/main.ts +++ b/app-shell-odd/src/main.ts @@ -197,7 +197,10 @@ function installDevtools(): void { log.debug('Installing devtools') - install(extensions, forceReinstall) + install(extensions, { + loadExtensionOptions: { allowFileAccess: true }, + forceDownload: forceReinstall, + }) .then(() => log.debug('Devtools extensions installed')) .catch((error: unknown) => { log.warn('Failed to install devtools extensions', { diff --git a/app-shell/Makefile b/app-shell/Makefile index 74e4e4b1912..ec54213bf69 100644 --- a/app-shell/Makefile +++ b/app-shell/Makefile @@ -59,7 +59,7 @@ no_python_bundle ?= builder := yarn electron-builder \ --config electron-builder.config.js \ - --config.electronVersion=27.0.0 \ + --config.electronVersion=33.2.1 \ --publish never diff --git a/app-shell/build/release-notes-internal.md b/app-shell/build/release-notes-internal.md index b7780dc8c88..565c5e1aa9b 100644 --- a/app-shell/build/release-notes-internal.md +++ b/app-shell/build/release-notes-internal.md @@ -1,6 +1,22 @@ For more details about this release, please see the full [technical changelog][]. [technical change log]: https://github.com/Opentrons/opentrons/releases +## Internal Release 2.3.0-alpha.2 + +This internal release, pulled from the `edge` branch, contains features being developed for 8.3.0. It's for internal testing only. + +## Internal Release 2.3.0-alpha.1 + +This internal release, pulled from the `edge` branch, contains features being developed for 8.3.0. It's for internal testing only. + +## Internal Release 2.3.0-alpha.0 + +This internal release, pulled from the `edge` branch, contains features being developed for evo tip functionality. It's for internal testing only. + +## Internal Release 2.2.0-alpha.1 + +This internal release, pulled from the `edge` branch, contains features being developed for 8.2.0. It's for internal testing only. + ## Internal Release 2.2.0-alpha.0 This internal release, pulled from the `edge` branch, contains features being developed for 8.2.0. It's for internal testing only. diff --git a/app-shell/build/release-notes.md b/app-shell/build/release-notes.md index 9b9231e9709..85567a0481e 100644 --- a/app-shell/build/release-notes.md +++ b/app-shell/build/release-notes.md @@ -8,6 +8,22 @@ By installing and using Opentrons software, you agree to the Opentrons End-User --- +## Opentrons App Changes in 8.3.0 + +Welcome to the v8.3.0 release of the Opentrons App! This release adds support for Mandarin in the app or Flex touchscreen and includes other beta features for our commercial partners. + +Note: The Mac and Linux versions of the Opentrons App now require macOS 10.16 and Ubuntu 20.04 or newer. + +### New Features + +- Change the app or Flex touchscreen language to Mandarin in Settings. This feature is only supported in app v8.3.0 or higher. If you need to downgrade your software version, first change the app language back to English in Settings. + +### Improved Features + +- Improvements to the Flex error recovery feature help protocols recover from detected stalls and collisions, saving you valuable time and resources. + +--- + ## Opentrons App Changes in 8.2.0 Welcome to the v8.2.0 release of the Opentrons App! This release adds support for the Opentrons Absorbance Plate Reader Module, as well as other features. diff --git a/app-shell/electron-builder.config.js b/app-shell/electron-builder.config.js index 1b048915255..26bcf518e28 100644 --- a/app-shell/electron-builder.config.js +++ b/app-shell/electron-builder.config.js @@ -1,11 +1,7 @@ 'use strict' const path = require('path') -const { - OT_APP_DEPLOY_BUCKET, - OT_APP_DEPLOY_FOLDER, - APPLE_TEAM_ID, -} = process.env +const { OT_APP_DEPLOY_BUCKET, OT_APP_DEPLOY_FOLDER } = process.env const DEV_MODE = process.env.NODE_ENV !== 'production' const USE_PYTHON = process.env.NO_PYTHON !== 'true' const WINDOWS_SIGN = process.env.WINDOWS_SIGN === 'true' @@ -25,7 +21,7 @@ const publishConfig = module.exports = async () => ({ appId: project === 'robot-stack' ? 'com.opentrons.app' : 'com.opentrons.appot3', - electronVersion: '27.0.0', + electronVersion: '33.2.1', npmRebuild: false, releaseInfo: { releaseNotesFile: @@ -62,9 +58,7 @@ module.exports = async () => ({ icon: project === 'robot-stack' ? 'build/icon.icns' : 'build/three.icns', forceCodeSigning: !DEV_MODE, gatekeeperAssess: true, - notarize: { - teamId: APPLE_TEAM_ID, - }, + // note: notarize.teamId is passed by implicitly sending through the APPLE_TEAM_ID env var }, dmg: { icon: null, diff --git a/app-shell/package.json b/app-shell/package.json index e94f1f269e2..99dab77203d 100644 --- a/app-shell/package.json +++ b/app-shell/package.json @@ -8,7 +8,7 @@ "types": "lib/main.d.ts", "scripts": { "start": "make dev", - "rebuild": "electron-rebuild" + "rebuild": "electron-rebuild --force-abi=3.71.0" }, "repository": { "type": "git", @@ -50,7 +50,7 @@ "electron-localshortcut": "3.2.1", "electron-devtools-installer": "3.2.0", "electron-store": "5.1.1", - "electron-updater": "4.1.2", + "electron-updater": "6.3.9", "execa": "4.0.0", "form-data": "2.5.0", "fs-extra": "10.0.0", diff --git a/app-shell/src/__fixtures__/config.ts b/app-shell/src/__fixtures__/config.ts index dd344c78532..197f594fcd3 100644 --- a/app-shell/src/__fixtures__/config.ts +++ b/app-shell/src/__fixtures__/config.ts @@ -25,6 +25,7 @@ import type { ConfigV23, ConfigV24, ConfigV25, + ConfigV26, } from '@opentrons/app/src/redux/config/types' export const MOCK_CONFIG_V0: ConfigV0 = { @@ -312,3 +313,8 @@ export const MOCK_CONFIG_V25: ConfigV25 = { systemLanguage: null, }, } + +export const MOCK_CONFIG_V26: ConfigV26 = { + ...MOCK_CONFIG_V25, + version: 26, +} diff --git a/app-shell/src/__tests__/update.test.ts b/app-shell/src/__tests__/update.test.ts index 19d22e65b8f..3c0afe3e514 100644 --- a/app-shell/src/__tests__/update.test.ts +++ b/app-shell/src/__tests__/update.test.ts @@ -38,7 +38,7 @@ describe('update', () => { vi.mocked(ElectronUpdater.autoUpdater).emit('update-available', { version: '1.0.0', - }) + } as any) expect(dispatch).toHaveBeenCalledWith({ type: 'shell:CHECK_UPDATE_RESULT', @@ -50,7 +50,7 @@ describe('update', () => { handleAction({ type: 'shell:CHECK_UPDATE', meta: { shell: true } }) vi.mocked(ElectronUpdater.autoUpdater).emit('update-not-available', { version: '1.0.0', - }) + } as any) expect(dispatch).toHaveBeenCalledWith({ type: 'shell:CHECK_UPDATE_RESULT', @@ -82,7 +82,7 @@ describe('update', () => { vi.mocked(ElectronUpdater.autoUpdater).downloadUpdate ).toHaveBeenCalledTimes(1) - const progress = { + const progress: any = { percent: 20, } @@ -97,7 +97,7 @@ describe('update', () => { vi.mocked(ElectronUpdater.autoUpdater).emit('update-downloaded', { version: '1.0.0', - }) + } as any) expect(dispatch).toHaveBeenCalledWith({ type: 'shell:DOWNLOAD_UPDATE_RESULT', diff --git a/app-shell/src/config/__tests__/migrate.test.ts b/app-shell/src/config/__tests__/migrate.test.ts index ddc151fc2cf..b61210561c5 100644 --- a/app-shell/src/config/__tests__/migrate.test.ts +++ b/app-shell/src/config/__tests__/migrate.test.ts @@ -29,13 +29,14 @@ import { MOCK_CONFIG_V23, MOCK_CONFIG_V24, MOCK_CONFIG_V25, + MOCK_CONFIG_V26, } from '../../__fixtures__' import { migrate } from '../migrate' vi.mock('uuid/v4') -const NEWEST_VERSION = 25 -const NEWEST_MOCK_CONFIG = MOCK_CONFIG_V25 +const NEWEST_VERSION = 26 +const NEWEST_MOCK_CONFIG = MOCK_CONFIG_V26 describe('config migration', () => { beforeEach(() => { @@ -234,10 +235,17 @@ describe('config migration', () => { expect(result.version).toBe(NEWEST_VERSION) expect(result).toEqual(NEWEST_MOCK_CONFIG) }) - it('should keep version 25', () => { + it('should migrate version 25 to latest', () => { const v25Config = MOCK_CONFIG_V25 const result = migrate(v25Config) + expect(result.version).toBe(NEWEST_VERSION) + expect(result).toEqual(NEWEST_MOCK_CONFIG) + }) + it('should keep version 26', () => { + const v26Config = MOCK_CONFIG_V26 + const result = migrate(v26Config) + expect(result.version).toBe(NEWEST_VERSION) expect(result).toEqual(NEWEST_MOCK_CONFIG) }) diff --git a/app-shell/src/config/migrate.ts b/app-shell/src/config/migrate.ts index 69c53ab2e72..1ad6f4900cb 100644 --- a/app-shell/src/config/migrate.ts +++ b/app-shell/src/config/migrate.ts @@ -29,13 +29,14 @@ import type { ConfigV23, ConfigV24, ConfigV25, + ConfigV26, } from '@opentrons/app/src/redux/config/types' // format // base config v0 defaults // any default values for later config versions are specified in the migration // functions for those version below -const CONFIG_VERSION_LATEST = 25 +const CONFIG_VERSION_LATEST = 26 export const DEFAULTS_V0: ConfigV0 = { version: 0, @@ -443,6 +444,14 @@ const toVersion25 = (prevConfig: ConfigV24): ConfigV25 => { return nextConfig } +const toVersion26 = (prevConfig: ConfigV25): ConfigV26 => { + const nextConfig = { + ...prevConfig, + version: 26 as const, + } + return nextConfig +} + const MIGRATIONS: [ (prevConfig: ConfigV0) => ConfigV1, (prevConfig: ConfigV1) => ConfigV2, @@ -468,7 +477,8 @@ const MIGRATIONS: [ (prevConfig: ConfigV21) => ConfigV22, (prevConfig: ConfigV22) => ConfigV23, (prevConfig: ConfigV23) => ConfigV24, - (prevConfig: ConfigV24) => ConfigV25 + (prevConfig: ConfigV24) => ConfigV25, + (prevConfig: ConfigV25) => ConfigV26 ] = [ toVersion1, toVersion2, @@ -495,6 +505,7 @@ const MIGRATIONS: [ toVersion23, toVersion24, toVersion25, + toVersion26, ] export const DEFAULTS: Config = migrate(DEFAULTS_V0) @@ -527,6 +538,7 @@ export function migrate( | ConfigV23 | ConfigV24 | ConfigV25 + | ConfigV26 ): Config { const prevVersion = prevConfig.version let result = prevConfig diff --git a/app-shell/src/main.ts b/app-shell/src/main.ts index 0f4ab41733b..e09b9d0ae4c 100644 --- a/app-shell/src/main.ts +++ b/app-shell/src/main.ts @@ -145,7 +145,10 @@ function installDevtools(): Promise { log.debug('Installing devtools') if (typeof install === 'function') { - return install(extensions, forceReinstall) + return install(extensions, { + loadExtensionOptions: { allowFileAccess: true }, + forceDownload: forceReinstall, + }) .then(() => log.debug('Devtools extensions installed')) .catch((error: unknown) => { log.warn('Failed to install devtools extensions', { diff --git a/app-shell/src/preload.ts b/app-shell/src/preload.ts index cf1f4ef7bef..16ef6b7aa30 100644 --- a/app-shell/src/preload.ts +++ b/app-shell/src/preload.ts @@ -1,7 +1,15 @@ // preload script for renderer process // defines subset of Electron API that renderer process is allowed to access // for security reasons -import { ipcRenderer } from 'electron' +import { ipcRenderer, webUtils } from 'electron' + +// The renderer process is not permitted the file path for any type "file" input +// post Electron v32. The correct way of doing this involves the context bridge, +// see comments in Electron settings. +// See https://www.electronjs.org/docs/latest/breaking-changes#removed-filepath +const getFilePathFrom = (file: File): Promise => { + return Promise.resolve(webUtils.getPathForFile(file)) +} // @ts-expect-error can't get TS to recognize global.d.ts -global.APP_SHELL_REMOTE = { ipcRenderer } +global.APP_SHELL_REMOTE = { ipcRenderer, getFilePathFrom } diff --git a/app-shell/src/protocol-storage/__tests__/file-system.test.ts b/app-shell/src/protocol-storage/__tests__/file-system.test.ts index 4da2cd23abe..cc742e0b89b 100644 --- a/app-shell/src/protocol-storage/__tests__/file-system.test.ts +++ b/app-shell/src/protocol-storage/__tests__/file-system.test.ts @@ -109,66 +109,67 @@ describe('protocol storage directory utilities', () => { }) describe('parseProtocolDirs', () => { - it('reads and parses directories', () => { + it('reads and parses directories', async () => { const protocolsDir = makeEmptyDir() - const firstProtocolDirName = 'protocol_item_1' const secondProtocolDirName = 'protocol_item_2' - const firstDirPath = path.join(protocolsDir, firstProtocolDirName) const secondDirPath = path.join(protocolsDir, secondProtocolDirName) - return Promise.all([ - fs.emptyDir(path.join(protocolsDir, firstProtocolDirName)), - fs.emptyDir(path.join(protocolsDir, firstProtocolDirName, 'src')), - fs.createFile( - path.join(protocolsDir, firstProtocolDirName, 'src', 'main.py') - ), - fs.emptyDir(path.join(protocolsDir, firstProtocolDirName, 'analysis')), - fs.createFile( - path.join( - protocolsDir, - firstProtocolDirName, - 'analysis', - 'fake_timestamp0.json' - ) - ), - fs.emptyDir(path.join(protocolsDir, secondProtocolDirName)), - fs.emptyDir(path.join(protocolsDir, secondProtocolDirName, 'src')), - fs.createFile( - path.join(protocolsDir, secondProtocolDirName, 'src', 'main.json') - ), - fs.emptyDir(path.join(protocolsDir, secondProtocolDirName, 'analysis')), - fs.createFile( - path.join( - protocolsDir, - secondProtocolDirName, - 'analysis', - 'fake_timestamp1.json' - ) - ), - ]).then(() => { - return expect( - parseProtocolDirs([firstDirPath, secondDirPath]) - ).resolves.toEqual([ - { - dirPath: firstDirPath, - modified: expect.any(Number), - srcFilePaths: [path.join(firstDirPath, 'src', 'main.py')], - analysisFilePaths: [ - path.join(firstDirPath, 'analysis', 'fake_timestamp0.json'), - ], - }, - { - dirPath: secondDirPath, - modified: expect.any(Number), - srcFilePaths: [path.join(secondDirPath, 'src', 'main.json')], - analysisFilePaths: [ - path.join(secondDirPath, 'analysis', 'fake_timestamp1.json'), - ], - }, - ]) - }) + await fs.emptyDir(path.join(protocolsDir, firstProtocolDirName)) + await fs.emptyDir(path.join(protocolsDir, firstProtocolDirName, 'src')) + await fs.emptyDir( + path.join(protocolsDir, firstProtocolDirName, 'analysis') + ) + await fs.createFile( + path.join(protocolsDir, firstProtocolDirName, 'src', 'main.py') + ) + await fs.createFile( + path.join( + protocolsDir, + firstProtocolDirName, + 'analysis', + 'fake_timestamp0.json' + ) + ) + + await fs.emptyDir(path.join(protocolsDir, secondProtocolDirName)) + await fs.emptyDir(path.join(protocolsDir, secondProtocolDirName, 'src')) + await fs.emptyDir( + path.join(protocolsDir, secondProtocolDirName, 'analysis') + ) + await fs.createFile( + path.join(protocolsDir, secondProtocolDirName, 'src', 'main.json') + ) + await fs.createFile( + path.join( + protocolsDir, + secondProtocolDirName, + 'analysis', + 'fake_timestamp1.json' + ) + ) + + const result = await parseProtocolDirs([firstDirPath, secondDirPath]) + + expect(result).toEqual([ + { + dirPath: firstDirPath, + modified: expect.any(Number), + srcFilePaths: [path.join(firstDirPath, 'src', 'main.py')], + analysisFilePaths: [ + path.join(firstDirPath, 'analysis', 'fake_timestamp0.json'), + ], + }, + { + dirPath: secondDirPath, + modified: expect.any(Number), + srcFilePaths: [path.join(secondDirPath, 'src', 'main.json')], + analysisFilePaths: [ + path.join(secondDirPath, 'analysis', 'fake_timestamp1.json'), + ], + }, + ]) }) }) diff --git a/app-shell/src/update.ts b/app-shell/src/update.ts index 5742904ae8b..5b926a17c71 100644 --- a/app-shell/src/update.ts +++ b/app-shell/src/update.ts @@ -11,6 +11,7 @@ const autoUpdater = updater.autoUpdater autoUpdater.logger = createLogger('update') autoUpdater.autoDownload = false +autoUpdater.forceDevUpdateConfig = true export const CURRENT_VERSION: string = autoUpdater.currentVersion.version @@ -77,16 +78,9 @@ interface ProgressInfo { percent: number bytesPerSecond: number } -interface DownloadingPayload { - progress: ProgressInfo - bytesPerSecond: number - percent: number - total: number - transferred: number -} function downloadUpdate(dispatch: Dispatch): void { - const onDownloading = (payload: DownloadingPayload): void => { + const onDownloading = (payload: ProgressInfo): void => { dispatch({ type: 'shell:DOWNLOAD_PERCENTAGE', payload }) } const onDownloaded = (): void => { diff --git a/app/Makefile b/app/Makefile index b11fed0b2b3..8f77700e3b7 100644 --- a/app/Makefile +++ b/app/Makefile @@ -47,7 +47,7 @@ clean: dist: export NODE_ENV := production dist: echo "Building app JS bundle (browser layer)" - vite build + NODE_OPTIONS="--max-old-space-size=8192" vite build # development ##################################################################### diff --git a/app/package.json b/app/package.json index 034e5dd7cec..33ae6252d1a 100644 --- a/app/package.json +++ b/app/package.json @@ -63,11 +63,13 @@ "reselect": "4.0.0", "rxjs": "^6.5.1", "semver": "5.7.2", + "simple-keyboard-layouts": "3.4.41", "styled-components": "5.3.6", "typeface-open-sans": "0.0.75", "uuid": "3.2.1" }, "devDependencies": { + "@tanstack/react-query-devtools": "5.59.16", "@types/classnames": "2.2.5", "@types/file-saver": "2.0.1", "@types/jszip": "3.1.7", @@ -75,6 +77,7 @@ "@types/node-fetch": "2.6.11", "@types/styled-components": "^5.1.26", "axios": "^0.21.1", + "electron-updater": "6.3.9", "postcss-apply": "0.12.0", "postcss-color-mod-function": "3.0.3", "postcss-import": "16.0.0", diff --git a/app/src/App/DesktopApp.tsx b/app/src/App/DesktopApp.tsx index 6b09da2e2d2..029ec99ee26 100644 --- a/app/src/App/DesktopApp.tsx +++ b/app/src/App/DesktopApp.tsx @@ -1,7 +1,6 @@ import { useState, Fragment } from 'react' import { Navigate, Route, Routes, useMatch } from 'react-router-dom' import { ErrorBoundary } from 'react-error-boundary' - import { Box, COLORS, @@ -38,6 +37,8 @@ import { useRobot, useIsFlex } from '/app/redux-resources/robots' import { ProtocolTimeline } from '/app/pages/Desktop/Protocols/ProtocolDetails/ProtocolTimeline' import { PortalRoot as ModalPortalRoot } from './portal' import { DesktopAppFallback } from './DesktopAppFallback' +import { ReactQueryDevtools } from './tools' +import { useFeatureFlag } from '../redux/config' import type { RouteProps } from './types' @@ -48,6 +49,22 @@ export const DesktopApp = (): JSX.Element => { setIsEmergencyStopModalDismissed, ] = useState(false) + // note for react-scan + const enableReactScan = useFeatureFlag('reactScan') + // Dynamically import `react-scan` to avoid build errors + if (typeof window !== 'undefined' && enableReactScan) { + import('react-scan') + .then(({ scan }) => { + scan({ + enabled: enableReactScan, + log: true, + }) + }) + .catch(error => { + console.error('Failed to load react-scan:', error) + }) + } + const desktopRoutes: RouteProps[] = [ { Component: ProtocolsLanding, @@ -108,6 +125,7 @@ export const DesktopApp = (): JSX.Element => { + diff --git a/app/src/App/Navbar.tsx b/app/src/App/Navbar.tsx index db8a4acd005..ebef216e9f8 100644 --- a/app/src/App/Navbar.tsx +++ b/app/src/App/Navbar.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useCallback } from 'react' import { useTranslation } from 'react-i18next' import { NavLink, useNavigate } from 'react-router-dom' import styled from 'styled-components' @@ -26,6 +26,7 @@ import logoSvgThree from '/app/assets/images/logo_nav_three.svg' import { NAV_BAR_WIDTH } from './constants' +import type { MouseEvent } from 'react' import type { RouteProps } from './types' const SALESFORCE_HELP_LINK = 'https://support.opentrons.com/s/' @@ -112,13 +113,11 @@ const LogoImg = styled('img')` export function Navbar({ routes }: { routes: RouteProps[] }): JSX.Element { const { t } = useTranslation('top_navigation') - const navigate = useNavigate() const navRoutes = routes.filter( ({ navLinkTo }: RouteProps) => navLinkTo != null ) - - const debouncedNavigate = React.useCallback( + const debouncedNavigate = useCallback( debounce((path: string) => { navigate(path) }, DEBOUNCE_DURATION_MS), @@ -164,7 +163,7 @@ export function Navbar({ routes }: { routes: RouteProps[] }): JSX.Element { ) => { + onClick={(e: MouseEvent) => { e.preventDefault() debouncedNavigate('/app-settings') }} diff --git a/app/src/App/OnDeviceDisplayApp.tsx b/app/src/App/OnDeviceDisplayApp.tsx index 2e2217ce20e..5337965fc52 100644 --- a/app/src/App/OnDeviceDisplayApp.tsx +++ b/app/src/App/OnDeviceDisplayApp.tsx @@ -9,7 +9,6 @@ import { COLORS, OVERFLOW_AUTO, POSITION_RELATIVE, - useIdle, useScrolling, } from '@opentrons/components' import { ApiHostProvider } from '@opentrons/react-api-client' @@ -52,9 +51,10 @@ import { updateConfigValue, } from '/app/redux/config' import { updateBrightness } from '/app/redux/shell' -import { SLEEP_NEVER_MS } from '/app/local-resources/config' +import { useScreenIdle, SLEEP_NEVER_MS } from '/app/local-resources/dom-utils' import { useProtocolReceiptToast, useSoftwareUpdatePoll } from './hooks' import { ODDTopLevelRedirects } from './ODDTopLevelRedirects' +import { ReactQueryDevtools } from '/app/App/tools' import { OnDeviceDisplayAppFallback } from './OnDeviceDisplayAppFallback' @@ -165,7 +165,7 @@ export const OnDeviceDisplayApp = (): JSX.Element => { initialState: false, } const dispatch = useDispatch() - const isIdle = useIdle(sleepTime, options) + const isIdle = useScreenIdle(sleepTime, options) useEffect(() => { if (isIdle) { @@ -183,6 +183,7 @@ export const OnDeviceDisplayApp = (): JSX.Element => { // TODO (sb:6/12/23) Create a notification manager to set up preference and order of takeover modals return ( + diff --git a/app/src/App/__mocks__/portal.tsx b/app/src/App/__mocks__/portal.tsx index f80b1deb44e..498f3220f65 100644 --- a/app/src/App/__mocks__/portal.tsx +++ b/app/src/App/__mocks__/portal.tsx @@ -1,8 +1,8 @@ // mock portal for enzyme tests -import type * as React from 'react' +import type { ReactNode } from 'react' interface Props { - children: React.ReactNode + children: ReactNode } // replace Portal with a pass-through React.Fragment diff --git a/app/src/App/__tests__/DesktopApp.test.tsx b/app/src/App/__tests__/DesktopApp.test.tsx index 085ff9ef7ba..0bd31ff4aaa 100644 --- a/app/src/App/__tests__/DesktopApp.test.tsx +++ b/app/src/App/__tests__/DesktopApp.test.tsx @@ -81,6 +81,7 @@ describe('DesktopApp', () => { ).mockImplementation((props: LocalizationProviderProps) => ( <>{props.children} )) + when(vi.mocked(useFeatureFlag)).calledWith('reactScan').thenReturn(false) }) afterEach(() => { vi.resetAllMocks() diff --git a/app/src/App/__tests__/hooks.test.tsx b/app/src/App/__tests__/hooks.test.tsx index 5b3f315049b..2423414c748 100644 --- a/app/src/App/__tests__/hooks.test.tsx +++ b/app/src/App/__tests__/hooks.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { vi, describe, beforeEach, afterEach, expect, it } from 'vitest' import { renderHook } from '@testing-library/react' import { createStore } from 'redux' @@ -9,11 +8,12 @@ import { i18n } from '/app/i18n' import { checkShellUpdate } from '/app/redux/shell' import { useSoftwareUpdatePoll } from '../hooks' +import type { FunctionComponent, ReactNode } from 'react' import type { Store } from 'redux' import type { State } from '/app/redux/types' describe('useSoftwareUpdatePoll', () => { - let wrapper: React.FunctionComponent<{ children: React.ReactNode }> + let wrapper: FunctionComponent<{ children: ReactNode }> let store: Store beforeEach(() => { vi.useFakeTimers() diff --git a/app/src/App/hooks.ts b/app/src/App/hooks.ts index d01082d8dc1..737393f7a99 100644 --- a/app/src/App/hooks.ts +++ b/app/src/App/hooks.ts @@ -31,7 +31,7 @@ export function useSoftwareUpdatePoll(): void { export function useProtocolReceiptToast(): void { const host = useHost() - const { t } = useTranslation('protocol_info') + const { t, i18n } = useTranslation(['protocol_info', 'shared']) const { makeToast } = useToaster() const queryClient = useQueryClient() const protocolIdsQuery = useAllProtocolIdsQuery( @@ -83,7 +83,7 @@ export function useProtocolReceiptToast(): void { }) as string, 'success', { - closeButton: true, + buttonText: i18n.format(t('shared:close'), 'capitalize'), disableTimeout: true, displayType: 'odd', } diff --git a/app/src/App/index.tsx b/app/src/App/index.tsx index f0ba1de0304..0ffcf0dd751 100644 --- a/app/src/App/index.tsx +++ b/app/src/App/index.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { useSelector } from 'react-redux' import { Flex, POSITION_FIXED, DIRECTION_ROW } from '@opentrons/components' @@ -9,7 +8,9 @@ import { DesktopApp } from './DesktopApp' import { OnDeviceDisplayApp } from './OnDeviceDisplayApp' import { TopPortalRoot } from './portal' -const stopEvent = (event: React.MouseEvent): void => { +import type { MouseEvent } from 'react' + +const stopEvent = (event: MouseEvent): void => { event.preventDefault() } diff --git a/app/src/App/tools/ReactQueryDevtools.tsx b/app/src/App/tools/ReactQueryDevtools.tsx new file mode 100644 index 00000000000..a57f2df3d4d --- /dev/null +++ b/app/src/App/tools/ReactQueryDevtools.tsx @@ -0,0 +1,22 @@ +import { lazy, Suspense } from 'react' + +import { useFeatureFlag } from '/app/redux/config' + +// Lazily load to enable devtools when env.process.DEV is false (ex, when dev code is pushed to a physical ODD) +const ReactQueryTools = lazy(() => + import('react-query/devtools/development').then(d => ({ + default: d.ReactQueryDevtools, + })) +) + +export function ReactQueryDevtools(): JSX.Element { + const enableRQTools = useFeatureFlag('reactQueryDevtools') + + return ( + + {enableRQTools && ( + + )} + + ) +} diff --git a/app/src/App/tools/__tests__/ReactQueryDevtools.test.tsx b/app/src/App/tools/__tests__/ReactQueryDevtools.test.tsx new file mode 100644 index 00000000000..dcca081d19e --- /dev/null +++ b/app/src/App/tools/__tests__/ReactQueryDevtools.test.tsx @@ -0,0 +1,52 @@ +import { screen } from '@testing-library/react' +import { vi, describe, it, expect, beforeEach } from 'vitest' + +import { renderWithProviders } from '/app/__testing-utils__' +import { i18n } from '/app/i18n' +import { ReactQueryDevtools } from '/app/App/tools' +import { useFeatureFlag } from '/app/redux/config' + +vi.mock('react-query/devtools/development', () => ({ + ReactQueryDevtools: vi + .fn() + .mockReturnValue(
MOCK_REACT_QUERY_DEVTOOLS
), +})) +vi.mock('/app/redux/config') + +const render = () => { + return renderWithProviders(, { + i18nInstance: i18n, + }) +} + +describe('ReactQueryDevtools', () => { + const mockUseFF = vi.fn() + + beforeEach(() => { + vi.mocked(useFeatureFlag).mockReturnValue(true) + }) + + it('uses the correct feature flag', () => { + vi.mocked(useFeatureFlag).mockImplementation(mockUseFF) + + render() + + expect(mockUseFF).toHaveBeenCalledWith('reactQueryDevtools') + }) + + it('renders the devtools if the FF is enabled', async () => { + render() + + await screen.findByText('MOCK_REACT_QUERY_DEVTOOLS') + }) + + it('does not the devtools if the FF is disabled', async () => { + vi.mocked(useFeatureFlag).mockReturnValue(false) + + render() + + expect( + screen.queryByText('MOCK_REACT_QUERY_DEVTOOLS') + ).not.toBeInTheDocument() + }) +}) diff --git a/app/src/App/tools/index.ts b/app/src/App/tools/index.ts new file mode 100644 index 00000000000..99f2b6dc3fd --- /dev/null +++ b/app/src/App/tools/index.ts @@ -0,0 +1 @@ +export * from './ReactQueryDevtools' diff --git a/app/src/App/types.ts b/app/src/App/types.ts index 87d8f77d4a1..c6a0822260d 100644 --- a/app/src/App/types.ts +++ b/app/src/App/types.ts @@ -1,11 +1,11 @@ -import type * as React from 'react' +import type { FC } from 'react' export interface RouteProps { /** * the component rendered by a route match * drop developed components into slots held by placeholder div components */ - Component: React.FC + Component: FC /** * a route/page name to render in the nav bar */ diff --git a/app/src/LocalizationProvider.tsx b/app/src/LocalizationProvider.tsx index df2bbc8bc40..3c8fcf9feab 100644 --- a/app/src/LocalizationProvider.tsx +++ b/app/src/LocalizationProvider.tsx @@ -7,10 +7,10 @@ import { i18n, i18nCb, i18nConfig } from '/app/i18n' import { getAppLanguage } from '/app/redux/config' import { useIsOEMMode } from '/app/resources/robot-settings/hooks' -import type * as React from 'react' +import type { ReactNode } from 'react' export interface LocalizationProviderProps { - children?: React.ReactNode + children?: ReactNode } export const BRANDED_RESOURCE = 'branded' diff --git a/app/src/__testing-utils__/renderWithProviders.tsx b/app/src/__testing-utils__/renderWithProviders.tsx index 11e3ba16d9b..4c3115281f5 100644 --- a/app/src/__testing-utils__/renderWithProviders.tsx +++ b/app/src/__testing-utils__/renderWithProviders.tsx @@ -1,6 +1,5 @@ // render using targetted component using @testing-library/react // with wrapping providers for i18next and redux -import type * as React from 'react' import { QueryClient, QueryClientProvider } from 'react-query' import { I18nextProvider } from 'react-i18next' import { Provider } from 'react-redux' @@ -8,16 +7,22 @@ import { vi } from 'vitest' import { render } from '@testing-library/react' import { createStore } from 'redux' +import type { + ComponentProps, + ComponentType, + PropsWithChildren, + ReactElement, +} from 'react' 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'] + i18nInstance: ComponentProps['i18n'] } export function renderWithProviders( - Component: React.ReactElement, + Component: ReactElement, options?: RenderWithProvidersOptions ): [RenderResult, Store] { // eslint-disable-next-line @typescript-eslint/consistent-type-assertions @@ -32,7 +37,7 @@ export function renderWithProviders( const queryClient = new QueryClient() - const ProviderWrapper: React.ComponentType> = ({ + const ProviderWrapper: ComponentType> = ({ children, }) => { const BaseWrapper = ( diff --git a/app/src/assets/localization/en/anonymous.json b/app/src/assets/localization/en/anonymous.json index 280e602088d..bcfb90dc7eb 100644 --- a/app/src/assets/localization/en/anonymous.json +++ b/app/src/assets/localization/en/anonymous.json @@ -71,6 +71,7 @@ "storage_limit_reached_description": "Your robot has reached the limit of quick transfers that it can store. You must delete an existing quick transfer before creating a new one.", "system_language_preferences_update_description": "Your system’s language was recently updated. Would you like to use the updated language as the default for the app?", "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.", + "u2e_driver_description": "The OT-2 uses this adapter for its USB connection to the desktop app.", "unexpected_error": "An unexpected error has occurred. If the issue persists, contact customer support for assistance.", "update_requires_restarting_app": "Updating requires restarting the app.", "update_robot_software_description": "Bypass the auto-update process and update the robot software manually.", diff --git a/app/src/assets/localization/en/app_settings.json b/app/src/assets/localization/en/app_settings.json index 77ee3a9fd3f..540df5ef394 100644 --- a/app/src/assets/localization/en/app_settings.json +++ b/app/src/assets/localization/en/app_settings.json @@ -1,10 +1,12 @@ { "__dev_internal__enableLabwareCreator": "Enable App Labware Creator", - "__dev_internal__enableLocalization": "Enable App Localization", "__dev_internal__enableRunNotes": "Display Notes During a Protocol Run", "__dev_internal__forceHttpPolling": "Poll all network requests instead of using MQTT", + "__dev_internal__lpcRedesign": "LPC Redesign", "__dev_internal__protocolStats": "Protocol Stats", "__dev_internal__protocolTimeline": "Protocol Timeline", + "__dev_internal__reactQueryDevtools": "Enable React Query Devtools", + "__dev_internal__reactScan": "Enable React Scan", "add_folder_button": "Add labware source folder", "add_ip_button": "Add", "add_ip_error": "Enter an IP Address or Hostname", @@ -35,7 +37,9 @@ "connect_ip_link": "Learn more about connecting a robot manually", "discovery_timeout": "Discovery timed out.", "dont_change": "Don’t change", + "dont_remind_me": "Don't remind me again", "download_update": "Downloading update...", + "driver_out_of_date": "Realtek USB-to-Ethernet Driver Update Available", "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_desktop_app_description": "You need to reload the app. Contact support with the following error message:", @@ -44,14 +48,15 @@ "error_recovery_mode_description": "Pause on protocol errors instead of canceling the run.", "feature_flags": "Feature Flags", "general": "General", + "get_update": "get update", "heater_shaker_attach_description": "Display a reminder to attach the Heater-Shaker properly before running a test shake or using it in a protocol.", "heater_shaker_attach_visible": "Confirm Heater-Shaker Module Attachment", "how_to_restore": "How to Restore a Previous Software Version", "installing_update": "Installing update...", "ip_available": "Available", "ip_description_first": "Enter an IP address or hostname to connect to a robot.", - "language_preference": "Language preference", "language": "Language", + "language_preference": "Language preference", "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", @@ -64,6 +69,7 @@ "ot2_advanced_settings": "OT-2 Advanced Settings", "override_path": "override path", "override_path_to_python": "Override Path to Python", + "please_update_driver": "Please update your computer's driver to ensure a reliable connection to your OT-2.", "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.", "privacy": "Privacy", @@ -92,6 +98,7 @@ "trash_bin": "Always use trash bin to calibrate", "try_restarting_the_update": "Try restarting the update.", "turn_off_updates": "Turn off software update notifications in App Settings.", + "u2e_driver_outdated_message": "There is an updated Realtek USB-to-Ethernet adapter driver available for your computer.", "up_to_date": "Up to date", "update_alerts": "Software Update Alerts", "update_app_now": "Update app now", @@ -110,6 +117,7 @@ "usb_to_ethernet_unknown_manufacturer": "Unknown Manufacturer", "usb_to_ethernet_unknown_product": "Unknown Adapter", "use_system_language": "Use system language", + "view_adapter_info": "view adapter info", "view_software_update": "View software update", "view_update": "View Update" } diff --git a/app/src/assets/localization/en/branded.json b/app/src/assets/localization/en/branded.json index 0760c3061b4..42c22baa8ad 100644 --- a/app/src/assets/localization/en/branded.json +++ b/app/src/assets/localization/en/branded.json @@ -71,6 +71,7 @@ "storage_limit_reached_description": "Your Opentrons Flex has reached the limit of quick transfers that it can store. You must delete an existing quick transfer before creating a new one.", "system_language_preferences_update_description": "Your system’s language was recently updated. Would you like to use the updated language as the default for the Opentrons App?", "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.", + "u2e_driver_description": "The OT-2 uses this adapter for its USB connection to the Opentrons App.", "unexpected_error": "An unexpected error has occurred. If the issue persists, contact Opentrons Support for assistance.", "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.", diff --git a/app/src/assets/localization/en/change_pipette.json b/app/src/assets/localization/en/change_pipette.json index b9ca35752d5..f63445ea779 100644 --- a/app/src/assets/localization/en/change_pipette.json +++ b/app/src/assets/localization/en/change_pipette.json @@ -1,8 +1,8 @@ { "are_you_sure_exit": "Are you sure you want to exit before {{direction}} your pipette?", "attach_name_pipette": "Attach a {{pipette}} Pipette", - "attach_pipette_type": "Attach a {{pipetteName}} Pipette", "attach_pipette": "Attach a pipette", + "attach_pipette_type": "Attach a {{pipetteName}} Pipette", "attach_the_pipette": "

Attach the pipette

Push in the white connector tab until you feel it plug into the pipette.", "attached_pipette_does_not_match": "The attached {{name}} does not match the {{pipette}} you had originally selected.", "attaching": "attaching", @@ -16,10 +16,10 @@ "confirming_attachment": "Confirming attachment", "confirming_detachment": "Confirming detachment", "continue": "Continue", - "detach_pipette_from_mount": "Detach Pipette from {{mount}} Mount", + "detach": "Detach pipette", "detach_pipette": "Detach {{pipette}} from {{mount}} Mount", + "detach_pipette_from_mount": "Detach Pipette from {{mount}} Mount", "detach_try_again": "Detach and try again", - "detach": "Detach pipette", "detaching": "detaching", "get_started": "Get started", "go_back": "Go back", diff --git a/app/src/assets/localization/en/device_details.json b/app/src/assets/localization/en/device_details.json index ee0be003723..f394a9a9ac8 100644 --- a/app/src/assets/localization/en/device_details.json +++ b/app/src/assets/localization/en/device_details.json @@ -1,16 +1,16 @@ { "about_gripper": "About gripper", "about_module": "About {{name}}", - "about_pipette_name": "About {{name}} Pipette", "about_pipette": "About pipette", - "abs_reader_status": "Absorbance Plate Reader Status", + "about_pipette_name": "About {{name}} Pipette", "abs_reader_lid_status": "Lid status: {{status}}", + "abs_reader_status": "Absorbance Plate Reader Status", + "add": "Add", "add_fixture_description": "Add this hardware 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": "An error occurred while updating your pipette's settings.", "an_error_occurred_while_updating_module": "An error occurred while updating your {{moduleName}}. Please try again.", "an_error_occurred_while_updating_please_try_again": "An error occurred while updating your pipette's settings. Please try again.", - "an_error_occurred_while_updating": "An error occurred while updating your pipette's settings.", "attach_gripper": "Attach gripper", "attach_pipette": "Attach pipette", "bad_run": "run could not be loaded", @@ -18,13 +18,13 @@ "bundle_firmware_file_not_found": "Bundled fw file not found for module of type: {{module}}", "calibrate_gripper": "Calibrate gripper", "calibrate_now": "Calibrate now", - "calibrate_pipette_offset": "Calibrate pipette offset", "calibrate_pipette": "Calibrate pipette", - "calibration_needed_without_link": "Calibration needed.", + "calibrate_pipette_offset": "Calibrate pipette offset", "calibration_needed": "Calibration needed. Calibrate now", + "calibration_needed_without_link": "Calibration needed.", "canceled": "canceled", - "changes_will_be_lost_description": "Are you sure you want to exit without saving your deck configuration?", "changes_will_be_lost": "Changes will be lost", + "changes_will_be_lost_description": "Are you sure you want to exit without saving your deck configuration?", "choose_protocol_to_run": "Choose protocol to Run on {{name}}", "close_lid": "Close lid", "completed": "completed", @@ -36,9 +36,9 @@ "current_temp": "Current: {{temp}} °C", "current_version": "Current Version", "deck_cal_missing": "Pipette Offset calibration missing. Calibrate deck first.", + "deck_configuration": "deck configuration", "deck_configuration_is_not_available_when_robot_is_busy": "Deck configuration is not available when the robot is busy", "deck_configuration_is_not_available_when_run_is_in_progress": "Deck configuration is not available when run is in progress", - "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_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.", @@ -58,14 +58,14 @@ "estop_pressed": "E-stop pressed. Robot movement is halted.", "failed": "failed", "files": "Files", - "firmware_update_needed": "Instrument firmware update needed. Start the update on the robot's touchscreen.", "firmware_update_available": "Firmware update available.", "firmware_update_failed": "Failed to update module firmware", - "firmware_updated_successfully": "Firmware updated successfully", + "firmware_update_needed": "Instrument firmware update needed. Start the update on the robot's touchscreen.", "firmware_update_occurring": "Firmware update in progress...", + "firmware_updated_successfully": "Firmware updated successfully", "fixture": "Fixture", - "have_not_run_description": "After you run some protocols, they will appear here.", "have_not_run": "No recent runs", + "have_not_run_description": "After you run some protocols, they will appear here.", "heater": "Heater", "height_ranges": "{{gen}} Height Ranges", "hot_to_the_touch": "Module is hot to the touch", @@ -74,12 +74,12 @@ "instruments_and_modules": "Instruments and Modules", "labware_bottom": "Labware Bottom", "last_run_time": "last run {{number}}", - "left_right": "Left+Right Mounts", "left": "left", + "left_right": "Left + Right Mounts", "lights": "Lights", "link_firmware_update": "View Firmware Update", - "location_conflicts": "Location conflicts", "location": "Location", + "location_conflicts": "Location conflicts", "magdeck_gen1_height": "Height: {{height}}", "magdeck_gen2_height": "Height: {{height}} mm", "max_engage_height": "Max Engage Height", @@ -88,13 +88,13 @@ "missing_hardware": "missing hardware", "missing_instrument": "missing {{num}} instrument", "missing_instruments_plural": "missing {{count}} instruments", - "missing_module_plural": "missing {{count}} modules", "missing_module": "missing {{num}} module", + "missing_module_plural": "missing {{count}} modules", "module_actions_unavailable": "Module actions unavailable while protocol is running", + "module_calibration_required": "Module calibration required.", "module_calibration_required_no_pipette_attached": "Module calibration required. Attach a pipette before running module calibration.", "module_calibration_required_no_pipette_calibrated": "Module calibration required. Calibrate pipette before running module calibration. ", "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": "Module error", "module_name_error": "{{moduleName}} error", @@ -105,8 +105,8 @@ "no_deck_fixtures": "No deck fixtures", "no_protocol_runs": "No protocol runs yet!", "no_protocols_found": "No protocols found", - "no_recent_runs_description": "After you run some protocols, they will appear here.", "no_recent_runs": "No recent runs", + "no_recent_runs_description": "After you run some protocols, they will appear here.", "num_units": "{{num}} mm", "offline_deck_configuration": "Robot must be on the network to see deck configuration", "offline_instruments_and_modules": "Robot must be on the network to see connected instruments and modules", @@ -132,32 +132,32 @@ "protocol_analysis_failed": "Protocol failed in-app analysis. ", "protocol_analysis_stale": "Protocol analysis out of date. ", "protocol_details_page_reanalyze": "Go to the protocol details screen to reanalyze.", - "ready_to_run": "ready to run", "ready": "Ready", + "ready_to_run": "ready to run", "recalibrate_gripper": "Recalibrate gripper", "recalibrate_now": "Recalibrate now", - "recalibrate_pipette_offset": "Recalibrate pipette offset", "recalibrate_pipette": "Recalibrate pipette", + "recalibrate_pipette_offset": "Recalibrate pipette offset", "recent_protocol_runs": "Recent Protocol Runs", - "rerun_now": "Rerun protocol now", "rerun_loading": "Protocol re-run is disabled until data connection fully loads", + "rerun_now": "Rerun protocol now", "reset_all": "Reset all", "reset_estop": "Reset E-stop", "resume_operation": "Resume operation", "right": "right", "robot_control_not_available": "Some robot controls are not available when run is in progress", "robot_initializing": "Initializing...", + "run": "Run", "run_a_protocol": "Run a protocol", "run_again": "Run again", "run_duration": "Run duration", - "run": "Run", "select_options": "Select options", "serial_number": "Serial Number", "set_block_temp": "Set temperature", "set_block_temperature": "Set block temperature", + "set_engage_height": "Set Engage Height", "set_engage_height_and_enter_integer": "Set the engage height for this Magnetic Module. Enter an integer between {{lower}} and {{higher}}.", "set_engage_height_for_module": "Set Engage Height for {{name}}", - "set_engage_height": "Set Engage Height", "set_lid_temperature": "Set lid temperature", "set_shake_of_hs": "Set rpm for this module.", "set_shake_speed": "Set shake speed", @@ -174,8 +174,8 @@ "target_temp": "Target: {{temp}} °C", "tc_block": "Block", "tc_lid": "Lid", - "tc_set_temperature_body": "Pre heat or cool your Thermocycler {{part}}. Enter a whole number between {{min}} °C and {{max}} °C.", "tc_set_temperature": "Set {{part}} Temperature for {{name}}", + "tc_set_temperature_body": "Pre heat or cool your Thermocycler {{part}}. Enter a whole number between {{min}} °C and {{max}} °C.", "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", @@ -185,12 +185,12 @@ "trash": "Trash", "update_now": "Update now", "updating_firmware": "Updating firmware...", - "usb_port_not_connected": "usb not connected", "usb_port": "usb-{{port}}", + "usb_port_not_connected": "usb not connected", "version": "Version {{version}}", + "view": "View", "view_pipette_setting": "Pipette Settings", "view_run_record": "View protocol run record", - "view": "View", "waste_chute": "Waste chute", "welcome_modal_description": "A place to run protocols, manage your instruments, and view robot status.", "welcome_to_your_dashboard": "Welcome to your dashboard!", diff --git a/app/src/assets/localization/en/device_settings.json b/app/src/assets/localization/en/device_settings.json index 79416a09f73..e0a199fbee4 100644 --- a/app/src/assets/localization/en/device_settings.json +++ b/app/src/assets/localization/en/device_settings.json @@ -3,6 +3,7 @@ "about_calibration_description": "For the robot to move accurately and precisely, you need to calibrate it. Positional calibration happens in three parts: deck calibration, pipette offset calibration and tip length calibration.", "about_calibration_description_ot3": "For the robot to move accurately and precisely, you need to calibrate it. Pipette and gripper calibration is an automated process that uses a calibration probe or pin.After calibration is complete, you can save the calibration data to your computer as a JSON file.", "about_calibration_title": "About Calibration", + "add_new": "Add new...", "advanced": "Advanced", "alpha_description": "Warning: alpha releases are feature-complete but may contain significant bugs.", "alternative_security_types": "Alternative security types", @@ -10,10 +11,12 @@ "apply_historic_offsets": "Apply Labware Offsets", "are_you_sure_you_want_to_disconnect": "Are you sure you want to disconnect from {{ssid}}?", "attach_a_pipette_before_calibrating": "Attach a pipette in order to perform calibration", + "authentication": "Authentication", "boot_scripts": "Boot scripts", "both": "Both", "browse_file_system": "Browse file system", "bug_fixes": "Bug Fixes", + "but_we_expected": "but we expected", "calibrate_deck": "Calibrate deck", "calibrate_deck_description": "For pre-2019 robots that do not have crosses etched on the deck.", "calibrate_deck_to_dots": "Calibrate deck to dots", @@ -28,8 +31,10 @@ "change_network": "Change network", "characters_max": "17 characters max", "check_for_updates": "Check for updates", + "check_to_verify_update": "Check your robot's settings page to verify whether or not the update was successful", "checking_for_updates": "Checking for updates", "choose": "Choose...", + "choose_a_network": "Choose a network...", "choose_file": "Choose file", "choose_network_type": "Choose network type", "choose_reset_settings": "Choose reset settings", @@ -56,7 +61,9 @@ "confirm_device_reset_heading": "Are you sure you want to reset your device?", "connect": "Connect", "connect_the_estop_to_continue": "Connect the E-stop to continue", + "connect_to_ssid": "Connect to {{ssid}}", "connect_to_wifi_network": "Connect to Wi-Fi network", + "connect_to_wifi_network_failure": "Your robot was unable to connect to Wi-Fi network {{ssid}}", "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.", @@ -65,6 +72,7 @@ "connected_to_ssid": "Connected to {{ssid}}", "connected_via": "Connected via {{networkInterface}}", "connecting_to": "Connecting to {{ssid}}...", + "connecting_to_wifi_network": "Connecting to Wi-Fi network {{ssid}}", "connection_description_ethernet": "Connect to your lab's wired network.", "connection_description_wifi": "Find a network in your lab or enter your own.", "connection_to_robot_lost": "Connection to robot lost", @@ -96,6 +104,7 @@ "display_sleep_settings": "Display Sleep Settings", "do_not_turn_off": "This could take up to {{minutes}} minutes. Don't turn off the robot.", "done": "Done", + "downgrade": "downgrade", "download": "Download", "download_calibration_data": "Download calibration logs", "download_error": "Download error", @@ -109,6 +118,7 @@ "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_name_security_type": "Enter the network name and security type.", "enter_network_name": "Enter network name", "enter_password": "Enter password", "estop": "E-stop", @@ -127,6 +137,8 @@ "factory_resets_cannot_be_undone": "Factory resets cannot be undone.", "failed_to_connect_to_ssid": "Failed to connect to {{ssid}}", "feature_flags": "Feature Flags", + "field_is_required": "{{field}} is required", + "find_and_join_network": "Find and join a Wi-Fi network", "finish_setup": "Finish setup", "firmware_version": "Firmware Version", "fully_calibrate_before_checking_health": "Fully calibrate your robot before checking calibration health", @@ -154,6 +166,7 @@ "last_calibrated_label": "Last Calibrated", "launch_jupyter_notebook": "Launch Jupyter Notebook", "legacy_settings": "Legacy Settings", + "likely_incorrect_password": "Likely incorrect network password.", "mac_address": "MAC Address", "manage_oem_settings": "Manage OEM settings", "minutes": "{{minute}} minutes", @@ -171,7 +184,10 @@ "name_your_robot": "Name your robot", "name_your_robot_description": "Don’t worry, you can always change this in your settings.", "need_another_security_type": "Need another security type?", + "network_is_unsecured": "Wi-Fi network {{ssid}} is unsecured", "network_name": "Network Name", + "network_requires_auth": "Wi-Fi network {{ssid}} requires 802.1X authentication", + "network_requires_wpa_password": "Wi-Fi network {{ssid}} requires a WPA2 password", "network_settings": "Network Settings", "networking": "Networking", "never": "Never", @@ -183,6 +199,7 @@ "no_modules_attached": "No modules attached", "no_network_found": "No network found", "no_pipette_attached": "No pipette attached", + "no_update_files": "Unable to retrieve update for this robot. Ensure your computer is connected to the internet and try again later.", "none_description": "Not recommended", "not_calibrated": "Not calibrated yet", "not_calibrated_short": "Not calibrated", @@ -197,8 +214,10 @@ "on": "On", "one_hour": "1 hour", "other_networks": "Other Networks", + "other_robot_updating": "Unable to update because the app is currently updating a different robot.", "password": "Password", "password_error_message": "Must be at least 8 characters", + "password_not_long_enough": "Password must be at least {{minLength}} characters", "pause_protocol": "Pause protocol when robot door opens", "pause_protocol_description": "When enabled, opening the robot door during a run will pause the robot after it has completed its current motion.", "pipette_calibrations_description": "Pipette calibration uses a metal probe to determine the pipette's exact position relative to precision-cut squares on deck slots.", @@ -208,6 +227,7 @@ "pipette_offset_calibration_recommended": "Pipette Offset calibration recommended", "pipette_offset_calibrations_history": "See all Pipette Offset Calibration history", "pipette_offset_calibrations_title": "Pipette Offset Calibrations", + "please_check_credentials": "Please double-check your network credentials", "privacy": "Privacy", "problem_during_update": "This update is taking longer than usual.", "proceed_without_updating": "Proceed without update", @@ -238,9 +258,12 @@ "returns_your_device_to_new_state": "This returns your device to a new state.", "robot_busy_protocol": "This robot cannot be updated while a protocol is running on it", "robot_calibration_data": "Robot Calibration Data", + "robot_has_bad_capabilities": "Robot has incorrect capabilities shape", "robot_initializing": "Initializing robot...", "robot_name": "Robot Name", "robot_operating_update_available": "Robot Operating System Update Available", + "robot_reconnected_with version": "Robot reconnected with version", + "robot_requires_premigration": "This robot must be updated by the system before a custom update can occur", "robot_serial_number": "Robot Serial Number", "robot_server_version": "Robot Server Version", "robot_settings": "Robot Settings", @@ -259,7 +282,9 @@ "select_a_network": "Select a network", "select_a_security_type": "Select a security type", "select_all_settings": "Select all settings", + "select_auth_method_short": "Select authentication method", "select_authentication_method": "Select authentication method for your selected network.", + "select_file": "Select file", "sending_software": "Sending software...", "serial": "Serial", "setup_mode": "Setup mode", @@ -275,6 +300,9 @@ "subnet_mask": "Subnet Mask", "successfully_connected": "Successfully connected!", "successfully_connected_to_network": "Successfully connected to {{ssid}}!", + "successfully_connected_to_ssid": "Your robot has successfully connected to Wi-Fi network {{ssid}}", + "successfully_connected_to_wifi": "Successfully connected to Wi-Fi", + "successfully_disconnected_from_wifi": "Successfully disconnected from Wi-Fi", "supported_protocol_api_versions": "Supported Protocol API Versions", "text_size": "Text Size", "text_size_description": "Text on all screens will adjust to the size you choose below.", @@ -286,6 +314,14 @@ "troubleshooting": "Troubleshooting", "try_again": "Try again", "try_restarting_the_update": "Try restarting the update.", + "unable_to_cancel_update": "Unable to cancel in-progress update session", + "unable_to_commit_update": "Unable to commit update", + "unable_to_connect": "Unable to connect to Wi-Fi", + "unable_to_disconnect": "Unable to disconnect from Wi-Fi", + "unable_to_find_robot_with_name": "Unable to find online robot with name", + "unable_to_find_system_file": "Unable to find system file for update", + "unable_to_restart": "Unable to restart robot", + "unable_to_start_update_session": "Unable to start update session", "up_to_date": "up to date", "update_available": "Update Available", "update_channel_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.", @@ -294,7 +330,10 @@ "update_requires_restarting_robot": "Updating the robot software requires restarting the robot", "update_robot_now": "Update robot now", "update_robot_software": "Update robot software manually with a local file (.zip)", + "update_server_unavailable": "Unable to update because your robot's update server is not responding.", + "update_unavailable": "Update unavailable", "updating": "Updating", + "upgrade": "upgrade", "upload_custom_logo": "Upload custom logo", "upload_custom_logo_description": "Upload a logo for the robot to display during boot up.", "upload_custom_logo_dimensions": "The logo must fit within dimensions 1024 x 600 and be a PNG file (.png).", diff --git a/app/src/assets/localization/en/error_recovery.json b/app/src/assets/localization/en/error_recovery.json index b46e276c48b..3f22e2e63f9 100644 --- a/app/src/assets/localization/en/error_recovery.json +++ b/app/src/assets/localization/en/error_recovery.json @@ -8,6 +8,7 @@ "blowout_failed": "Blowout failed", "cancel_run": "Cancel run", "canceling_run": "Canceling run", + "carefully_move_labware": "Carefully move any misplaced labware and clean up any spilled liquid.Close the robot door before proceeding.", "change_location": "Change location", "change_tip_pickup_location": "Change tip pick-up location", "choose_a_recovery_action": "Choose a recovery action", @@ -32,6 +33,9 @@ "gripper_errors_occur_when": "Gripper errors occur when the gripper stalls or collides with another object on the deck and are usually caused by improperly placed labware or inaccurate labware offsets", "gripper_releasing_labware": "Gripper releasing labware", "gripper_will_release_in_s": "Gripper will release labware in {{seconds}} seconds", + "home_and_retry": "Home gantry and retry step", + "home_gantry": "Home gantry", + "home_now": "Home now", "homing_pipette_dangerous": "Homing the {{mount}} pipette with liquid in the tips may damage it. You must remove all tips before using the pipette again.", "if_issue_persists_gripper_error": " If the issue persists, cancel the run and rerun gripper calibration", "if_issue_persists_overpressure": " If the issue persists, cancel the run and make the necessary changes to the protocol", @@ -57,7 +61,9 @@ "overpressure_is_usually_caused": "Overpressure is usually caused by a tip contacting labware, a clog, or moving viscous liquid too quickly", "pick_up_tips": "Pick up tips", "pipette_overpressure": "Pipette overpressure", + "prepare_deck_for_homing": "Prepare deck for homing", "proceed_to_cancel": "Proceed to cancel", + "proceed_to_home": "Proceed to home", "proceed_to_tip_selection": "Proceed to tip selection", "recovery_action_failed": "{{action}} failed", "recovery_mode": "Recovery mode", @@ -96,16 +102,20 @@ "skip_to_next_step_same_tips": "Skip to next step with same tips", "skipping_to_step_succeeded": "Skipping to step {{step}} succeeded.", "skipping_to_step_succeeded_na": "Skipping to next step succeeded.", + "stall_or_collision_detected_when": "A stall or collision is detected when the robot's motors are blocked", + "stall_or_collision_error": "Stall or collision", "stand_back": "Stand back, robot is in motion", "stand_back_picking_up_tips": "Stand back, picking up tips", "stand_back_resuming": "Stand back, resuming current step", "stand_back_retrying": "Stand back, retrying failed step", "stand_back_skipping_to_next_step": "Stand back, skipping to next step", "take_any_necessary_precautions": "Take any necessary precautions before positioning yourself to stabilize or catch the labware. Once confirmed, a countdown will begin before the gripper releases.", - "take_necessary_actions": "First, take any necessary actions to prepare the robot to retry the failed step.Then, close the robot door before proceeding.", + "take_necessary_actions": "Take any necessary additional actions to prepare the robot to retry the failed step.Close the robot door before proceeding.", "take_necessary_actions_failed_pickup": "First, take any necessary actions to prepare the robot to retry the failed tip pickup.Then, close the robot door before proceeding.", "take_necessary_actions_failed_tip_drop": "First, take any necessary actions to prepare the robot to retry the failed tip drop.Then, close the robot door before proceeding.", + "take_necessary_actions_home": "Take any necessary actions to prepare the robot to move the gantry to its home position.Close the robot door before proceeding.", "terminate_remote_activity": "Terminate remote activity", + "the_robot_must_return_to_home_position": "The robot must return to its home position before proceeding", "tip_drop_failed": "Tip drop failed", "tip_not_detected": "Tip not detected", "tip_presence_errors_are_caused": "Tip presence errors are usually caused by improperly placed labware or inaccurate labware offsets", diff --git a/app/src/assets/localization/en/gripper_wizard_flows.json b/app/src/assets/localization/en/gripper_wizard_flows.json index ff98d8e07f0..929627bf064 100644 --- a/app/src/assets/localization/en/gripper_wizard_flows.json +++ b/app/src/assets/localization/en/gripper_wizard_flows.json @@ -5,19 +5,17 @@ "before_you_begin": "Before you begin", "begin_calibration": "Begin calibration", "calibrate_gripper": "Calibrate Gripper", - "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", "continue": "Continue", "continue_calibration": "Continue calibration", "detach_gripper": "Detach Gripper", - "firmware_updating": "A firmware update is required, instrument is updating...", "firmware_up_to_date": "Firmware is up to date.", + "firmware_updating": "A firmware update is required, instrument is updating...", "get_started": "Get started", "gripper_calibration": "Gripper Calibration", "gripper_recalibration": "Gripper Recalibration", "gripper_successfully_attached": "Gripper successfully attached", - "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", diff --git a/app/src/assets/localization/en/heater_shaker.json b/app/src/assets/localization/en/heater_shaker.json index 12ac83a4123..0eec55e6a11 100644 --- a/app/src/assets/localization/en/heater_shaker.json +++ b/app/src/assets/localization/en/heater_shaker.json @@ -4,43 +4,43 @@ "cannot_shake": "Cannot shake when labware latch is open", "close_labware_latch": "Close labware latch", "close_latch": "Close latch", - "closed_and_locked": "Closed and Locked", "closed": "Closed", + "closed_and_locked": "Closed and Locked", "closing": "Closing...", "complete": "Complete", "confirm_attachment": "Confirm attachment", "confirm_heater_shaker_modal_attachment": "Confirm Heater-Shaker Module attachment", "continue_shaking_protocol_start_prompt": "Continue shaking while the protocol starts?", + "deactivate": "Deactivate", "deactivate_heater": "Deactivate heater", "deactivate_shaker": "Deactivate shaker", - "deactivate": "Deactivate", "heater_shaker_in_slot": "Attach {{moduleName}} in Slot {{slotName}} before proceeding", "heater_shaker_is_shaking": "Heater-Shaker Module is currently shaking", "keep_shaking_start_run": "Keep shaking and start run", - "labware_latch": "Labware Latch", "labware": "Labware", + "labware_latch": "Labware Latch", "min_max_rpm": "{{min}} - {{max}} rpm", "module_anchors_extended": "Before the run begins, module should have both anchors fully extended for a firm attachment to the deck.", "module_in_slot": "{{moduleName}} in Slot {{slotName}}", "module_should_have_anchors": "Module should have both anchors fully extended for a firm attachment to the deck.", + "open": "Open", "open_labware_latch": "Open labware latch", "open_latch": "Open latch", - "open": "Open", "opening": "Opening...", "proceed_to_run": "Proceed to run", "set_shake_speed": "Set shake speed", "set_temperature": "Set module temperature", "shake_speed": "Shake speed", "show_attachment_instructions": "Show attachment instructions", - "stop_shaking_start_run": "Stop shaking and start run", "stop_shaking": "Stop Shaking", + "stop_shaking_start_run": "Stop shaking and start run", "t10_torx_screwdriver": "{{name}} Screwdriver", "t10_torx_screwdriver_subtitle": "Provided with the Heater-Shaker. Using another size can strip the module's screws.", + "test_shake": "Test shake", "test_shake_banner_information": "If you want to add labware to the module before doing a test shake, you can use the labware latch controls to hold the latches open.", "test_shake_banner_labware_information": "If you want to add the {{labware}} to the module before doing a test shake, you can use the labware latch controls.", "test_shake_slideout_banner_info": "If you want to add labware to the module before doing a test shake, you can use the labware latch controls to hold the latches open.", "test_shake_troubleshooting_slideout_description": "Revisit instructions for attaching the module to the deck as well as attaching the thermal adapter.", - "test_shake": "Test shake", "thermal_adapter_attached_to_module": "The thermal adapter should be attached to the module.", "troubleshoot_step_1": "Return to Step 1 to see instructions for securing the module to the deck.", "troubleshoot_step_3": "Return to Step 3 to see instructions for securing the thermal adapter to the module.", diff --git a/app/src/assets/localization/en/incompatible_modules.json b/app/src/assets/localization/en/incompatible_modules.json index d9b1a231f0c..6829c971d24 100644 --- a/app/src/assets/localization/en/incompatible_modules.json +++ b/app/src/assets/localization/en/incompatible_modules.json @@ -1,7 +1,7 @@ { "incompatible_modules_attached": "incompatible module detected", - "remove_before_running_protocol": "Remove the following hardware before running a protocol:", + "is_not_compatible": "{{module_name}} is not compatible with the {{robot_type}}", "needs_your_assistance": "{{robot_name}} needs your assistance", - "remove_before_using": "You must remove incompatible modules before using this robot.", - "is_not_compatible": "{{module_name}} is not compatible with the {{robot_type}}" + "remove_before_running_protocol": "Remove the following hardware before running a protocol:", + "remove_before_using": "You must remove incompatible modules before using this robot." } diff --git a/app/src/assets/localization/en/labware_details.json b/app/src/assets/localization/en/labware_details.json index 3b25beffefc..ef1b786d666 100644 --- a/app/src/assets/localization/en/labware_details.json +++ b/app/src/assets/localization/en/labware_details.json @@ -8,8 +8,8 @@ "generic": "generic", "height": "height", "length": "length", - "manufacturer_number": "manufacturer / catalog #", "manufacturer": "manufacturer", + "manufacturer_number": "manufacturer / catalog #", "max_volume": "max volume", "measurements": "Measurements (mm)", "na": "n/a", @@ -21,8 +21,8 @@ "u": "U_Bottom", "v": "V_Bottom", "various": "various", - "well_count": "Well Count", "well": "Well", + "well_count": "Well Count", "width": "width", "x_offset": "x-offset", "x_size": "x-size", diff --git a/app/src/assets/localization/en/labware_landing.json b/app/src/assets/localization/en/labware_landing.json index 17eebda979d..3780788b4ee 100644 --- a/app/src/assets/localization/en/labware_landing.json +++ b/app/src/assets/localization/en/labware_landing.json @@ -3,20 +3,20 @@ "cancel": "cancel", "cannot-run-python-missing-labware": "Robots cannot run Python protocols with missing labware definitions.", "category": "Category", - "choose_file_to_upload": "Or choose a file from your computer to upload.", "choose_file": "Choose file", + "choose_file_to_upload": "Or choose a file from your computer to upload.", "copied": "Copied!", "create_new_def": "Create a new labware definition", "custom_def": "Custom Definition", "date_added": "Added", "def_moved_to_trash": "This labware definition will be moved to this computer’s trash and may be unrecoverable.", - "delete_this_labware": "Delete this labware definition?", "delete": "Delete", + "delete_this_labware": "Delete this labware definition?", "duplicate_labware_def": "Duplicate labware definition", "error_importing_file": "Error importing {{filename}}.", "go_to_def": "Go to labware definition", - "import_custom_def": "Import a Custom Labware Definition", "import": "Import", + "import_custom_def": "Import a Custom Labware Definition", "imported": "{{filename}} imported.", "invalid_labware_def": "Invalid labware definition", "labware": "labware", diff --git a/app/src/assets/localization/en/labware_position_check.json b/app/src/assets/localization/en/labware_position_check.json index 4072826650a..3d4e83fe706 100644 --- a/app/src/assets/localization/en/labware_position_check.json +++ b/app/src/assets/localization/en/labware_position_check.json @@ -25,8 +25,8 @@ "confirm_position_and_pick_up_tip": "Confirm position, pick up tip", "confirm_position_and_return_tip": "Confirm position, return tip to Slot {{next_slot}} and home", "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.", + "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.", "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.", @@ -35,9 +35,10 @@ "install_probe": "Take the calibration probe from its storage location. Ensure its collar is fully unlocked. Push the pipette ejector up and press the probe firmly onto the {{location}} pipette nozzle as far as it can go. Twist the collar to lock the probe. Test that the probe is secure by gently pulling it back and forth.", "jog_controls_adjustment": "Need to make an adjustment?", "jupyter_notebook": "Jupyter Notebook", + "labware": "labware", "labware_display_location_text": "Deck Slot {{slot}}", - "labware_offset_data": "labware offset data", "labware_offset": "Labware Offset", + "labware_offset_data": "labware offset data", "labware_offsets_deleted_warning": "Once you begin Labware Position Check, previously created Labware Offsets will be discarded.", "labware_offsets_summary_labware": "Labware", "labware_offsets_summary_location": "Location", @@ -46,22 +47,21 @@ "labware_position_check_description": "Labware Position Check is a guided workflow that checks every labware on the deck for an added degree of precision in your protocol.Labware Position Check first checks tip racks, and then checks all other labware used in your protocol.", "labware_position_check_overview": "Labware Position Check Overview", "labware_position_check_title": "Labware Position Check", - "labware_step_detail_labware_plural": "The tips should be centered above column 1 in {{labware_name}} and level with the top of the labware.", "labware_step_detail_labware": "The tip should be centered above A1 in {{labware_name}} and level with the top of the labware.", + "labware_step_detail_labware_plural": "The tips should be centered above column 1 in {{labware_name}} and level with the top of the labware.", "labware_step_detail_link": "See how to tell if the pipette is centered", "labware_step_detail_modal_heading": "How to tell if the pipette is centered and level", + "labware_step_detail_modal_nozzle": "To ensure that the nozzle is centered, check from a second side of your OT-2.", "labware_step_detail_modal_nozzle_image_1_text": "Viewed from front, it appears centered...", "labware_step_detail_modal_nozzle_image_2_nozzle_text": "Nozzle is not centered", "labware_step_detail_modal_nozzle_image_2_text": "...but viewed from side, it requires adjustment", + "labware_step_detail_modal_nozzle_or_tip": "To ensure the nozzle or tip is level with the top of the labware, position yourself at eye-level and/or slide a sheet of paper between the nozzle and tip.", "labware_step_detail_modal_nozzle_or_tip_image_1_text": "Viewed from standing height, it appears level...", "labware_step_detail_modal_nozzle_or_tip_image_2_nozzle_text": "Nozzle is not level", "labware_step_detail_modal_nozzle_or_tip_image_2_text": "... but viewed from eye-level, it requires adjustment", "labware_step_detail_modal_nozzle_or_tip_image_3_text": "If you’re having trouble, slide 1 sheet of printer paper between the nozzle and the tip. A single piece of paper should barely pass between them.", - "labware_step_detail_modal_nozzle_or_tip": "To ensure the nozzle or tip is level with the top of the labware, position yourself at eye-level and/or slide a sheet of paper between the nozzle and tip.", - "labware_step_detail_modal_nozzle": "To ensure that the nozzle is centered, check from a second side of your OT-2.", - "labware_step_detail_tiprack_plural": "The pipette nozzles should be centered above column 1 in {{tiprack_name}} and level with the top of the tips.", "labware_step_detail_tiprack": "The pipette nozzle should be centered above A1 in {{tiprack_name}} and level with the top of the tip.", - "labware": "labware", + "labware_step_detail_tiprack_plural": "The pipette nozzles should be centered above column 1 in {{tiprack_name}} and level with the top of the tips.", "learn_more": "Learn more", "location": "location", "lpc_complete_summary_screen_heading": "Labware Position Check Complete", @@ -73,9 +73,9 @@ "new_labware_offset_data": "New labware offset data", "ninety_six_probe_location": "A1 (back left corner)", "no_labware_offsets": "No Labware Offset", + "no_offset_data": "No offset data available", "no_offset_data_available": "No labware offset data available", "no_offset_data_on_robot": "This robot has no useable labware offset data for this run.", - "no_offset_data": "No offset data available", "offsets": "offsets", "pick_up_tip_from_rack_in_location": "Pick up tip from tip rack in {{location}}", "picking_up_tip_title": "Picking up tip in slot {{slot}}", @@ -98,13 +98,13 @@ "robot_has_no_offsets_from_previous_runs": "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. You can add new offsets with Labware Position Check in later steps.", "robot_has_offsets_from_previous_runs": "This robot has offsets for labware used in this protocol. If you apply these offsets, you can still adjust them with Labware Position Check.", "robot_in_motion": "Stand back, robot is in motion.", - "run_labware_position_check": "run labware position check", "run": "Run", + "run_labware_position_check": "run labware position check", "secondary_pipette_tipracks_section": "Check tip racks with {{secondary_mount}} Pipette", "see_how_offsets_work": "See how labware offsets work", + "slot": "Slot {{slotName}}", "slot_location": "slot location", "slot_name": "slot {{slotName}}", - "slot": "Slot {{slotName}}", "start_position_check": "begin labware position check, move to Slot {{initial_labware_slot}}", "stored_offset_data": "Apply Stored Labware Offset Data?", "stored_offsets_for_this_protocol": "Stored Labware Offset data that applies to this protocol", diff --git a/app/src/assets/localization/en/module_wizard_flows.json b/app/src/assets/localization/en/module_wizard_flows.json index 34e14017162..2ca733e2725 100644 --- a/app/src/assets/localization/en/module_wizard_flows.json +++ b/app/src/assets/localization/en/module_wizard_flows.json @@ -1,21 +1,21 @@ { "attach_probe": "Attach probe to pipette", "begin_calibration": "Begin calibration", - "calibrate_pipette": "Calibrate pipettes before proceeding to module calibration", "calibrate": "Calibrate", + "calibrate_pipette": "Calibrate pipettes before proceeding to module calibration", + "calibration": "{{module}} Calibration", "calibration_adapter_heatershaker": "Calibration Adapter", "calibration_adapter_temperature": "Calibration Adapter", "calibration_adapter_thermocycler": "Calibration Adapter", - "calibration_probe_touching_thermocycler": "The calibration probe will touch the sides of the calibration square in Thermocycler to determine its exact position", - "calibration_probe_touching": "The calibration probe will touch the sides of the calibration square in {{module}} in slot {{slotNumber}} to determine its exact position", "calibration_probe": "Take the calibration probe from its storage location. Ensure its collar is unlocked. Push the pipette ejector up and press the probe firmly onto the pipette nozzle. Twist the collar to lock the probe. Test that the probe is secure by gently pulling it back and forth.", - "calibration": "{{module}} Calibration", + "calibration_probe_touching": "The calibration probe will touch the sides of the calibration square in {{module}} in slot {{slotNumber}} to determine its exact position", + "calibration_probe_touching_thermocycler": "The calibration probe will touch the sides of the calibration square in Thermocycler to determine its exact position", "checking_firmware": "Checking {{module}} firmware", "complete_calibration": "Complete calibration", "confirm_location": "Confirm location", "confirm_placement": "Confirm placement", - "detach_probe_description": "Unlock the pipette calibration probe, remove it from the nozzle, and return it to its storage location.", "detach_probe": "Remove pipette probe", + "detach_probe_description": "Unlock the pipette calibration probe, remove it from the nozzle, and return it to its storage location.", "error_during_calibration": "Error during calibration", "error_prepping_module": "Error prepping module for calibration", "exit": "Exit", @@ -32,16 +32,16 @@ "move_gantry_to_front": "Move gantry to front", "next": "Next", "pipette_probe": "Pipette probe", + "place_flush": "Place the adapter flush on top of the module.", "place_flush_heater_shaker": "Place the adapter flush on the top of the module. Secure the adapter to the module with a thermal adapter screw and T10 Torx screwdriver.", "place_flush_thermocycler": "Ensure the Thermocycler lid is open and place the adapter flush on top of the module where the labware would normally go. ", - "place_flush": "Place the adapter flush on top of the module.", "prepping_module": "Prepping {{module}} for module calibration", "recalibrate": "Recalibrate", "select_location": "Select module location", "select_the_slot": "Select the slot where you installed the {{module}} on the deck map to the right. The location must be correct for successful calibration.", "slot_unavailable": "Slot unavailable", - "stand_back_robot_in_motion": "Stand back, robot is in motion", "stand_back": "Stand back, calibration in progress", + "stand_back_robot_in_motion": "Stand back, robot is in motion", "start_setup": "Start setup", "successfully_calibrated": "{{module}} successfully calibrated" } diff --git a/app/src/assets/localization/en/pipette_wizard_flows.json b/app/src/assets/localization/en/pipette_wizard_flows.json index 78dc2b852a6..02b186d4026 100644 --- a/app/src/assets/localization/en/pipette_wizard_flows.json +++ b/app/src/assets/localization/en/pipette_wizard_flows.json @@ -2,14 +2,14 @@ "align_the_connector": "Attach the pipette to the robot by aligning the connector and pressing to ensure a secure connection. Hold the pipette in place and use the hex screwdriver to tighten the pipette screws. Then test that the pipette is securely attached by gently pulling it side to side.", "all_pipette_detached": "All pipettes successfully detached", "are_you_sure_exit": "Are you sure you want to exit before completing {{flow}}?", - "attach_96_channel_plus_detach": "Detach {{pipetteName}} and Attach 96-Channel Pipette", + "attach": "Attaching Pipette", "attach_96_channel": "Attach 96-Channel Pipette", - "attach_mounting_plate_instructions": "Attach the mounting plate by aligning the pins on the plate to the slots on the gantry carriage. You may need to adjust the position of the right pipette mount to achieve proper alignment.", + "attach_96_channel_plus_detach": "Detach {{pipetteName}} and Attach 96-Channel Pipette", "attach_mounting_plate": "Attach Mounting Plate", + "attach_mounting_plate_instructions": "Attach the mounting plate by aligning the pins on the plate to the slots on the gantry carriage. You may need to adjust the position of the right pipette mount to achieve proper alignment.", "attach_pip": "attach pipette", "attach_pipette": "attach {{mount}} pipette", "attach_probe": "attach calibration probe", - "attach": "Attaching Pipette", "backmost": "backmost", "before_you_begin": "Before you begin", "begin_calibration": "Begin calibration", @@ -25,6 +25,7 @@ "connect_and_secure_pipette": "connect and secure pipette", "continue": "Continue", "critical_unskippable_step": "this is a critical step that should not be skipped", + "detach": "Detaching Pipette", "detach_96_attach_mount": "Detach 96-Channel Pipette and Attach {{mount}} Pipette", "detach_96_channel": "Detach 96-Channel Pipette", "detach_and_reattach": "Detach and reattach pipette", @@ -32,14 +33,13 @@ "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": "detach {{mount}} pipette", + "detach_pipette_to_attach_96": "Detach {{pipetteName}} and Attach 96-Channel 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.", - "detach": "Detaching Pipette", "exit_cal": "Exit calibration", - "firmware_updating": "A firmware update is required, instrument is updating...", "firmware_up_to_date": "No firmware update found.", + "firmware_updating": "A firmware update is required, instrument is updating...", "gantry_empty_for_96_channel_success": "Now that both mounts are empty, you can begin the 96-Channel Pipette attachment process.", "get_started_detach": "To get started, remove labware from the deck and clean up the working area to make detachment easier. Also gather the needed equipment shown to the right.", "grab_screwdriver": "While continuing to hold in place, grab your 2.5mm driver and tighten screws as shown in the animation. Test the pipette attachment by giving it a wiggle before pressing continue", @@ -67,11 +67,12 @@ "pipette_heavy": "The 96-Channel Pipette is heavy ({{weight}}). Ask a labmate for help, if needed.", "please_install_correct_pip": "Install {{pipetteName}} instead", "progress_will_be_lost": "{{flow}} progress will be lost", + "provided_with_robot": "Provided with the robot. Using another size can strip the instruments’s screws.", "reattach_carriage": "reattach z-axis carriage", "recalibrate_pipette": "recalibrate {{mount}} pipette", "remove_cal_probe": "remove calibration probe", - "remove_labware_to_get_started": "To get started, remove labware from the deck and clean up the working area to make calibration easier. Also gather the needed equipment shown to the right.The calibration probe is included with the robot and should be stored on the front pillar of the robot.", "remove_labware": "To get started, remove labware from the deck and clean up the working area to make attachment and calibration easier. Also gather the needed equipment shown to the right.The calibration probe is included with the robot and should be stored on the front pillar of the robot.", + "remove_labware_to_get_started": "To get started, remove labware from the deck and clean up the working area to make calibration easier. Also gather the needed equipment shown to the right.The calibration probe is included with the robot and should be stored on the front pillar of the robot.", "remove_probe": "unlock the calibration probe, remove it from the nozzle, and return it to its storage location.", "replace_pipette": "replace {{mount}} pipette", "return_probe_error": "Return the calibration probe to its storage location before exiting. {{error}}", diff --git a/app/src/assets/localization/en/protocol_command_text.json b/app/src/assets/localization/en/protocol_command_text.json index e8dee50f26c..51f37c9fa30 100644 --- a/app/src/assets/localization/en/protocol_command_text.json +++ b/app/src/assets/localization/en/protocol_command_text.json @@ -5,11 +5,14 @@ "absorbance_reader_read": "Reading plate in Absorbance Reader", "adapter_in_mod_in_slot": "{{adapter}} on {{module}} in Slot {{slot}}", "adapter_in_slot": "{{adapter}} in Slot {{slot}}", + "air_gap_in_place": "Air gapping {{volume}} µL", + "all_nozzles": "all nozzles", "aspirate": "Aspirating {{volume}} µL from well {{well_name}} of {{labware}} in {{labware_location}} at {{flow_rate}} µL/sec", "aspirate_in_place": "Aspirating {{volume}} µL in place at {{flow_rate}} µL/sec ", "blowout": "Blowing out at well {{well_name}} of {{labware}} in {{labware_location}} at {{flow_rate}} µL/sec", "blowout_in_place": "Blowing out in place at {{flow_rate}} µL/sec", "closing_tc_lid": "Closing Thermocycler lid", + "column_layout": "column layout", "comment": "Comment", "configure_for_volume": "Configure {{pipette}} to aspirate {{volume}} µL", "configure_nozzle_layout": "Configure {{pipette}} to use {{layout}}", @@ -56,20 +59,25 @@ "offdeck": "offdeck", "on_location": "on {{location}}", "opening_tc_lid": "Opening Thermocycler lid", + "partial_layout": "partial layout", "pause": "Pause", "pause_on": "Pause on {{robot_name}}", "pickup_tip": "Picking up tip(s) from {{well_range}} of {{labware}} in {{labware_location}}", "prepare_to_aspirate": "Preparing {{pipette}} to aspirate", + "pressurizing_to_dispense": "Pressurize pipette to dispense {{volume}} µL from resin tip at {{flow_rate}} µL/sec", "reloading_labware": "Reloading {{labware}}", "return_tip": "Returning tip to {{well_name}} of {{labware}} in {{labware_location}}", "right": "Right", + "row_layout": "row layout", "save_position": "Saving position", + "sealing_to_location": "Sealing to {{labware}} in {{location}}", "set_and_await_hs_shake": "Setting Heater-Shaker to shake at {{rpm}} rpm and waiting until reached", "setting_hs_temp": "Setting Target Temperature of Heater-Shaker to {{temp}}", "setting_temperature_module_temp": "Setting Temperature Module to {{temp}} (rounded to nearest integer)", "setting_thermocycler_block_temp": "Setting Thermocycler block temperature to {{temp}} with hold time of {{hold_time_seconds}} seconds after target reached", "setting_thermocycler_lid_temp": "Setting Thermocycler lid temperature to {{temp}}", "single": "single", + "single_nozzle_layout": "single nozzle layout", "slot": "Slot {{slot_name}}", "target_temperature": "target temperature", "tc_awaiting_for_duration": "Waiting for Thermocycler profile to complete", @@ -83,6 +91,7 @@ "turning_rail_lights_off": "Turning rail lights off", "turning_rail_lights_on": "Turning rail lights on", "unlatching_hs_latch": "Unlatching labware on Heater-Shaker", + "unsealing_from_location": "Unsealing from {{labware}} in {{location}}", "wait_for_duration": "Pausing for {{seconds}} seconds. {{message}}", "wait_for_resume": "Pausing protocol", "waiting_for_hs_to_reach": "Waiting for Heater-Shaker to reach target temperature", diff --git a/app/src/assets/localization/en/protocol_details.json b/app/src/assets/localization/en/protocol_details.json index b93e675a1c5..53531f1e00a 100644 --- a/app/src/assets/localization/en/protocol_details.json +++ b/app/src/assets/localization/en/protocol_details.json @@ -10,31 +10,32 @@ "connected": "connected", "connection_status": "connection status", "creation_method": "creation method", - "csv_file_type_required": "CSV file type required", "csv_file": "CSV file", + "csv_file_type_required": "CSV file type required", + "deck": "deck", "deck_view": "Deck View", "default_value": "Default Value", - "delete_protocol_perm": "{{name}} and its run history will be permanently deleted.", "delete_protocol": "Delete Protocol", + "delete_protocol_perm": "{{name}} and its run history will be permanently deleted.", "delete_this_protocol": "Delete this protocol?", "description": "description", "extension_mount": "extension mount", "file_required": "File required", "go_to_labware_definition": "Go to labware definition", "go_to_timeline": "Go to timeline", - "gripper_pick_up_count_description": "individual move labware commands that use the gripper.", "gripper_pick_up_count": "Grip Count", + "gripper_pick_up_count_description": "individual move labware commands that use the gripper.", "hardware": "hardware", - "labware_name": "Labware name", "labware": "labware", + "labware_name": "Labware name", "last_analyzed": "last analyzed", "last_updated": "last updated", "left_and_right_mounts": "left + right mounts", "left_mount": "left mount", "left_right": "Left, Right", "liquid_name": "liquid name", - "liquids_not_in_protocol": "no liquids are specified for this protocol", "liquids": "liquids", + "liquids_not_in_protocol": "no liquids are specified for this protocol", "listed_values_are_view_only": "Listed values are view-only", "location": "location", "modules": "modules", @@ -50,16 +51,16 @@ "num_choices": "{{num}} choices", "num_options": "{{num}} options", "off": "Off", - "on_off": "On, off", "on": "On", + "on_off": "On, off", "org_or_author": "org/author", "parameters": "Parameters", - "pipette_aspirate_count_description": "individual aspirate commands per pipette.", "pipette_aspirate_count": "{{pipette}} aspirate count", - "pipette_dispense_count_description": "individual dispense commands per pipette.", + "pipette_aspirate_count_description": "individual aspirate commands per pipette.", "pipette_dispense_count": "{{pipette}} dispense count", - "pipette_pick_up_count_description": "individual pick up tip commands per pipette.", + "pipette_dispense_count_description": "individual dispense commands per pipette.", "pipette_pick_up_count": "{{pipette}} pick up tip count", + "pipette_pick_up_count_description": "individual pick up tip commands per pipette.", "proceed_to_setup": "Proceed to setup", "protocol_designer_version": "Protocol Designer {{version}}", "protocol_failed_app_analysis": "This protocol failed in-app analysis. It may be unusable on robots without custom software configurations.", @@ -73,24 +74,25 @@ "requires_upload": "Requires upload", "restore_defaults": "Restore default values", "right_mount": "right mount", + "robot": "robot", "robot_configuration": "robot configuration", - "robot_is_busy_with_protocol": "{{robotName}} is busy with {{protocolName}} in {{runStatus}} state. Do you want to clear it and proceed?", "robot_is_busy": "{{robotName}} is busy", - "robot": "robot", + "robot_is_busy_with_protocol": "{{robotName}} is busy with {{protocolName}} in {{runStatus}} state. Do you want to clear it and proceed?", "run_protocol": "Run protocol", "select_parameters_for_robot": "Select parameters for {{robot_name}}", "send": "Send", "sending": "Sending", "show_in_folder": "Show in folder", "slot": "Slot {{slotName}}", - "start_setup_customize_values": "Start setup to customize values", "start_setup": "Start setup", + "start_setup_customize_values": "Start setup to customize values", "successfully_sent": "Successfully sent", + "summary": "Summary", "total_volume": "total volume", - "unavailable_or_busy_robot_not_listed_plural": "{{count}} unavailable or busy robots are not listed.", "unavailable_or_busy_robot_not_listed": "{{count}} unavailable or busy robot is not listed.", - "unavailable_robot_not_listed_plural": "{{count}} unavailable robots are not listed.", + "unavailable_or_busy_robot_not_listed_plural": "{{count}} unavailable or busy robots are not listed.", "unavailable_robot_not_listed": "{{count}} unavailable robot is not listed.", + "unavailable_robot_not_listed_plural": "{{count}} unavailable robots are not listed.", "unsuccessfully_sent": "Unsuccessfully sent", "value_out_of_range": "Value must be between {{min}}-{{max}}", "view_run_details": "View run details", diff --git a/app/src/assets/localization/en/protocol_info.json b/app/src/assets/localization/en/protocol_info.json index 3307c45363f..9278d55361e 100644 --- a/app/src/assets/localization/en/protocol_info.json +++ b/app/src/assets/localization/en/protocol_info.json @@ -10,6 +10,7 @@ "creation_method": "Creation Method", "custom_labware_not_supported": "Robot doesn't support custom labware", "date_added": "Date Added", + "date_added_date": "Date added {{date}}", "delete_protocol": "Delete protocol", "description": "Description", "drag_file_here": "Drag and drop protocol file here", @@ -22,9 +23,9 @@ "exit_modal_heading": "Confirm Close Protocol", "failed_analysis": "failed analysis", "get_labware_offset_data": "Get Labware Offset Data", + "import": "Import", "import_a_file": "Import a protocol to get started", "import_new_protocol": "Import a Protocol", - "import": "Import", "incompatible_file_type": "Incompatible file type", "instrument_cal_data_title": "Calibration data", "instrument_not_attached": "Not attached", @@ -37,10 +38,11 @@ "labware_offset_data_title": "Labware Offset data", "labware_offsets_info": "{{number}} Labware Offsets", "labware_position_check_complete_toast_no_offsets": "Labware Position Check complete. No Labware Offsets created.", - "labware_position_check_complete_toast_with_offsets_plural": "Labware Position Check complete. {{count}} Labware Offsets created.", "labware_position_check_complete_toast_with_offsets": "Labware Position Check complete. {{count}} Labware Offset created.", + "labware_position_check_complete_toast_with_offsets_plural": "Labware Position Check complete. {{count}} Labware Offsets created.", "labware_title": "Required Labware", "last_run": "Last Run", + "last_run_time": "Last run {{time}}", "last_updated": "Last Updated", "launch_protocol_designer": "Open Protocol Designer", "manual_steps_learn_more": "Learn more about manual steps", @@ -81,7 +83,7 @@ "unpin_protocol": "Unpin protocol", "unpinned_protocol": "Unpinned protocol", "update_robot_for_custom_labware": "You have custom labware definitions saved to your app, but this robot needs to be updated before you can use these definitions with Python protocols", - "upload_and_simulate": "Open a protocol to run on {{robot_name}}", "upload": "Upload", + "upload_and_simulate": "Open a protocol to run on {{robot_name}}", "valid_file_types": "Valid file types: Python files (.py) or Protocol Designer files (.json)" } diff --git a/app/src/assets/localization/en/protocol_list.json b/app/src/assets/localization/en/protocol_list.json index d70d8e6e7b7..bfc177829c5 100644 --- a/app/src/assets/localization/en/protocol_list.json +++ b/app/src/assets/localization/en/protocol_list.json @@ -16,8 +16,8 @@ "reanalyze_to_view": "Reanalyze protocol", "right_mount": "right mount", "robot": "robot", - "send_to_robot_overflow": "Send to {{robot_display_name}}", "send_to_robot": "Send protocol to {{robot_display_name}}", + "send_to_robot_overflow": "Send to {{robot_display_name}}", "show_in_folder": "Show in folder", "start_setup": "Start setup", "this_protocol_will_be_trashed": "This protocol will be moved to this computer’s trash and may be unrecoverable.", diff --git a/app/src/assets/localization/en/protocol_setup.json b/app/src/assets/localization/en/protocol_setup.json index 17f60958d55..d740eec25a1 100644 --- a/app/src/assets/localization/en/protocol_setup.json +++ b/app/src/assets/localization/en/protocol_setup.json @@ -263,8 +263,8 @@ "restore_defaults": "Restore default values", "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.", "robot_calibration_step_description": "Review required pipettes and tip length calibrations for this protocol.", + "robot_calibration_step_description_pipettes_only": "Review required instruments and calibrations for this protocol.", "robot_calibration_step_ready": "Calibration ready", "robot_calibration_step_title": "Instruments", "run": "Run", diff --git a/app/src/assets/localization/en/quick_transfer.json b/app/src/assets/localization/en/quick_transfer.json index 2ebdb5699b8..42717e3a1b6 100644 --- a/app/src/assets/localization/en/quick_transfer.json +++ b/app/src/assets/localization/en/quick_transfer.json @@ -37,6 +37,7 @@ "consolidate_volume_error": "The selected destination well is too small to consolidate into. Try consolidating from fewer wells.", "create_new_to_edit": "Create a new quick transfer to edit", "create_new_transfer": "Create new quick transfer", + "create_to_get_started": "Create a new quick transfer to get started.", "create_transfer": "Create transfer", "delay": "Delay", "delay_after_aspirating": "Delay after aspirating", @@ -44,7 +45,6 @@ "delay_duration_s": "Delay duration (seconds)", "delay_position_mm": "Delay position from bottom of well (mm)", "delay_value": "{{delay}}s, {{position}} mm from bottom", - "create_to_get_started": "Create a new quick transfer to get started.", "delete_this_transfer": "Delete this quick transfer?", "delete_transfer": "Delete quick transfer", "deleted_transfer": "Deleted quick transfer", @@ -63,8 +63,8 @@ "enter_characters": "Enter up to 60 characters", "error_analyzing": "An error occurred while attempting to analyze {{transferName}}.", "exit_quick_transfer": "Exit quick transfer?", - "flow_rate_value": "{{flow_rate}} µL/s", "failed_analysis": "failed analysis", + "flow_rate_value": "{{flow_rate}} µL/s", "got_it": "Got it", "grid": "grid", "grids": "grids", @@ -104,8 +104,8 @@ "reservoir": "Reservoirs", "right_mount": "Right Mount", "run_now": "Run now", - "run_transfer": "Run quick transfer", "run_quick_transfer_now": "Do you want to run your quick transfer now?", + "run_transfer": "Run quick transfer", "save": "Save", "save_for_later": "Save for later", "save_to_run_later": "Save your quick transfer to run it in the future.", @@ -129,15 +129,13 @@ "tip_position": "Tip position", "tip_position_value": "{{position}} mm from the bottom", "tip_rack": "Tip rack", + "too_many_pins_body": "Remove a quick transfer in order to add more transfers to your pinned list.", + "too_many_pins_header": "You've hit your max!", "touch_tip": "Touch tip", "touch_tip_after_aspirating": "Touch tip after aspirating", "touch_tip_before_dispensing": "Touch tip before dispensing", "touch_tip_position_mm": "Touch tip position from bottom of well (mm)", "touch_tip_value": "{{position}} mm from bottom", - "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.", - "value_out_of_range": "Value must be between {{min}}-{{max}}", - "too_many_pins_body": "Remove a quick transfer in order to add more transfers to your pinned list.", - "too_many_pins_header": "You've hit your max!", "transfer_analysis_failed": "quick transfer analysis failed", "transfer_name": "Transfer Name", "trashBin": "Trash bin", @@ -145,6 +143,8 @@ "tubeRack": "Tube racks", "unpin_transfer": "Unpin quick transfer", "unpinned_transfer": "Unpinned quick transfer", + "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.", + "value_out_of_range": "Value must be between {{min}}-{{max}}", "volume_per_well": "Volume per well", "volume_per_well_µL": "Volume per well (µL)", "wasteChute": "Waste chute", diff --git a/app/src/assets/localization/en/shared.json b/app/src/assets/localization/en/shared.json index f4e542e761b..6ea0f44ab7a 100644 --- a/app/src/assets/localization/en/shared.json +++ b/app/src/assets/localization/en/shared.json @@ -11,8 +11,8 @@ "change_robot": "Change robot", "clear_data": "clear data", "close": "close", - "closed": "closed", "close_robot_door": "Close the robot door before starting the run.", + "closed": "closed", "confirm": "Confirm", "confirm_placement": "Confirm placement", "confirm_position": "Confirm position", diff --git a/app/src/assets/localization/en/top_navigation.json b/app/src/assets/localization/en/top_navigation.json index 178b02042b9..c3391804a59 100644 --- a/app/src/assets/localization/en/top_navigation.json +++ b/app/src/assets/localization/en/top_navigation.json @@ -1,19 +1,25 @@ { + "app_settings": "App Settings", "attached_pipettes_do_not_match": "Attached pipettes do not match pipettes specified in loaded protocol", "calibrate_deck_to_proceed": "Calibrate your deck to proceed", + "calibration_dashboard": "Calibration Dashboard", "deck_setup": "Deck Setup", + "device": "Device", "devices": "Devices", "instruments": "Instruments", "labware": "Labware", "modules": "modules", - "pipettes_not_calibrated": "Please calibrate all pipettes specified in loaded protocol to proceed", "pipettes": "pipettes", + "pipettes_not_calibrated": "Please calibrate all pipettes specified in loaded protocol to proceed", "please_connect_to_a_robot": "Please connect to a robot to proceed", "please_load_a_protocol": "Please load a protocol to proceed", + "protocol_details": "Protocol Details", "protocol_runs": "Protocol Runs", + "protocol_timeline": "Protocol Timeline", "protocols": "Protocols", "quick_transfer": "Quick Transfer", "robot_settings": "Robot Settings", "run": "run", + "run_details": "Run Details", "settings": "Settings" } diff --git a/app/src/assets/localization/zh/anonymous.json b/app/src/assets/localization/zh/anonymous.json index 045245c84f7..7abd8f08615 100644 --- a/app/src/assets/localization/zh/anonymous.json +++ b/app/src/assets/localization/zh/anonymous.json @@ -1,9 +1,9 @@ { - "a_robot_software_update_is_available": "需要更新工作站软件版本才能使用该版本的桌面应用程序运行协议。转到工作站转到工作站", "about_flex_gripper": "关于转板抓手", "alternative_security_types_description": "工作站支持连接到各种企业接入点。通过USB连接并在桌面应用程序中完成设置。", - "attach_a_pipette_for_quick_transfer": "为创建快速移液,您需要在工作站上安装移液器。", "attach_a_pipette": "将移液器连接到工作站", + "attach_a_pipette_for_quick_transfer": "为创建快速移液,您需要在工作站上安装移液器。", "calibration_block_description": "这个金属块是一个特制的工具,完美适配您的甲板,有助于校准。如果您没有校准块,请发送电子邮件给支持团队,以便我们寄送一个给您。在您提供的信息中,请确保包括您的姓名、公司或机构名称和寄送地址。在等待校准块到达过程中,您可以暂时利用工作站里垃圾桶上的平面进行校准。", "calibration_on_opentrons_tips_is_important": "使用上述吸头和吸头盒进行校准非常重要,因为工作站的准确性是基于这些吸头的已知尺寸来确定的。", "choose_what_data_to_share": "选择要共享的工作站数据。", @@ -23,13 +23,14 @@ "find_your_robot": "在应用程序的“设备”栏找到您的工作站,以安装软件更新。", "firmware_update_download_logs": "请与支持人员联系以获得帮助。", "general_error_message": "如果该消息反复出现,请尝试重新启动您的应用程序和工作站。如果这不能解决问题,请与支持人员联系。", + "gripper": "转板抓手", "gripper_still_attached": "转板抓手仍处于连接状态", "gripper_successfully_attached_and_calibrated": "转板抓手已成功连接并校准", "gripper_successfully_calibrated": "转板抓手已成功校准", "gripper_successfully_detached": "转板抓手已成功卸下", - "gripper": "转板抓手", "help_us_improve_send_error_report": "通过向支持团队发送错误报告,帮助我们改进您的使用体验", "ip_description_second": "请联系网络管理员,为工作站分配静态IP地址。", + "language_preference_description": "除非您在下面选择其他语言,否则应用将与您的系统语言匹配。您可以稍后在应用设置中更改语言。", "learn_uninstalling": "了解更多有关卸载应用程序的信息", "loosen_screws_and_detach": "松开螺丝并卸下转板抓手", "modal_instructions": "有关设置模块的分步说明,请参阅随包装附带的快速指引。", @@ -48,8 +49,8 @@ "opentrons_def": "已验证的数据", "opentrons_flex_quickstart_guide": "快速入门指南", "opentrons_labware_def": "已验证的实验耗材数据", - "opentrons_tip_racks_recommended": "建议使用Opentrons吸头盒。其他吸头盒无法保证精度。", "opentrons_tip_rack_name": "opentrons", + "opentrons_tip_racks_recommended": "建议使用Opentrons吸头盒。其他吸头盒无法保证精度。", "previous_releases": "查看以前的版本", "receive_alert": "当软件更新可用时接收提醒。", "restore_description": "不建议恢复到过往的软件版本,但您可以访问下方的过往版本。为了获得最佳效果,请在安装过往版本之前卸载现有应用程序并删除其配置文件。", @@ -68,7 +69,10 @@ "show_labware_offset_snippets_description": "仅适用于需要在应用程序之外应用耗材校准数据的用户。启用后,在设置协议过程中可访问Jupyter Notebook和SSH的代码片段。", "something_seems_wrong": "您的移液器可能有问题。退出设置并联系支持人员以获取帮助。", "storage_limit_reached_description": "您的工作站已达到可存储的快速移液数量上限。在创建新的快速移液之前,您必须删除一个现有的快速移液。", + "system_language_preferences_update_description": "您系统的语言最近已更新。您想使用更新后的语言作为应用的默认语言吗?", "these_are_advanced_settings": "这些是高级设置。请勿在没有支持团队帮助的情况下尝试调整这些设置。更改这些设置可能会影响您的移液器寿命。这些设置不会覆盖协议中定义的任何移液器设置。", + "u2e_driver_description": "OT-2 通过此适配器,使用 USB 连接 Opentrons APP。", + "unexpected_error": "发生意外错误。", "update_requires_restarting_app": "更新需要重新启动应用程序。", "update_robot_software_description": "绕过自动更新过程并手动更新工作站软件", "update_robot_software_link": "启动软件更新页面", diff --git a/app/src/assets/localization/zh/app_settings.json b/app/src/assets/localization/zh/app_settings.json index 3405d5edbfd..b84f0c5b1e8 100644 --- a/app/src/assets/localization/zh/app_settings.json +++ b/app/src/assets/localization/zh/app_settings.json @@ -1,10 +1,12 @@ { "__dev_internal__enableLabwareCreator": "启用应用实验耗材创建器", - "__dev_internal__enableLocalization": "Enable App Localization", - "__dev_internal__forceHttpPolling": "强制轮询所有网络请求,而不是使用MQTT", "__dev_internal__enableRunNotes": "在协议运行期间显示备注", + "__dev_internal__forceHttpPolling": "强制轮询所有网络请求,而不是使用MQTT", + "__dev_internal__lpcRedesign": "LPC 重新设计", "__dev_internal__protocolStats": "协议统计", "__dev_internal__protocolTimeline": "协议时间线", + "__dev_internal__reactQueryDevtools": "启用开发者工具", + "__dev_internal__reactScan": "启用 React 组件扫描", "add_folder_button": "添加实验耗材源文件夹", "add_ip_button": "添加", "add_ip_error": "输入IP地址或主机名", @@ -15,11 +17,14 @@ "additional_labware_folder_title": "其他定制实验耗材源文件夹", "advanced": "高级", "app_changes": "应用程序更改于", + "app_language_description": "应用的所有功能界面将使用所选语言显示,但协议和用户生成的内容(如文本或文件)将保持原有语言,不会随语言设置而改变。", + "app_language_preferences": "应用程序语言偏好设置", "app_settings": "应用设置", "bug_fixes": "错误修复", "cal_block": "始终使用校准块进行校准", "change_folder_button": "更改实验耗材源文件夹", "channel": "通道", + "choose_your_language": "选择语言", "clear_confirm": "清除不可用的工作站", "clear_robots_button": "清除不可用工作站列表", "clear_robots_description": "清除设备页面上不可用工作站的列表。此操作无法撤消。", @@ -31,19 +36,27 @@ "connect_ip_button": "完成", "connect_ip_link": "了解更多关于手动连接工作站的信息", "discovery_timeout": "发现超时。", + "dont_change": "不改变", + "dont_remind_me": "不需要再次提醒", "download_update": "正在下载更新...", + "driver_out_of_date": "网卡驱动程序更新可用", "enable_dev_tools": "开发者工具", "enable_dev_tools_description": "启用此设置将在应用启动时打开开发者工具,打开额外的日志记录并访问功能标志。", "error_boundary_desktop_app_description": "您需要重新加载应用程序。出现以下错误信息,请联系技术支持:", "error_boundary_title": "发生未知错误", + "error_recovery_mode": "恢复模式", + "error_recovery_mode_description": "出现协议错误时暂停,而不是取消运行。", "feature_flags": "功能标志", "general": "通用", + "get_update": "获取更新", "heater_shaker_attach_description": "在进行测试振荡功能或在协议中使用热震荡模块功能之前,显示正确连接热震荡模块的提醒。", "heater_shaker_attach_visible": "确认热震荡模块连接", "how_to_restore": "如何恢复过往的软件版本", "installing_update": "正在安装更新...", "ip_available": "可用", "ip_description_first": "输入IP地址或主机名以连接到工作站。", + "language": "语言 (Language)", + "language_preference": "语言偏好", "manage_versions": "工作站版本和应用程序软件版本必须一致。通过工作站设置 > 高级查看工作站软件版本。", "new_features": "新功能", "no_folder": "未指定其他源文件夹", @@ -56,6 +69,7 @@ "ot2_advanced_settings": "OT-2高级设置", "override_path": "覆盖路径", "override_path_to_python": "覆盖Python路径", + "please_update_driver": "请更新您的计算机的驱动程序以确保与 OT-2 稳定连接。", "prevent_robot_caching": "阻止工作站进行缓存", "prevent_robot_caching_description": "启用此功能后,应用程序将立即清除不可用的工作站,并且不会记住它们。在网络上有许多工作站的情况下,防止缓存可能会提高网络性能,但代价是在应用程序启动时工作站发现的速度变慢且可靠性降低。", "privacy": "隐私", @@ -69,6 +83,8 @@ "restarting_app": "下载完成,正在重启应用程序...", "restore_previous": "查看如何恢复过往软件版本", "searching": "正在搜索30秒", + "select_a_language": "请选择使用语言。", + "select_language": "选择语言 (Select language)", "setup_connection": "设置连接", "share_display_usage": "分享屏幕使用情况", "share_robot_logs": "分享工作站日志", @@ -77,10 +93,12 @@ "software_update_available": "有可用的软件更新", "software_version": "应用程序软件版本", "successfully_deleted_unavail_robots": "成功删除不可用的工作站", + "system_language_preferences_update": "更新您的系统语言偏好设置", "tip_length_cal_method": "吸头长度校准方法", "trash_bin": "始终使用垃圾桶进行校准", "try_restarting_the_update": "尝试重新启动更新。", "turn_off_updates": "在应用程序设置中关闭软件更新通知。", + "u2e_driver_outdated_message": "您的计算机有可用的网卡驱动程序更新。", "up_to_date": "最新", "update_alerts": "软件更新提醒", "update_app_now": "立即更新应用程序", @@ -98,6 +116,8 @@ "usb_to_ethernet_not_connected": "没有连接USB-to-Ethernet适配器", "usb_to_ethernet_unknown_manufacturer": "未知制造商", "usb_to_ethernet_unknown_product": "未知适配器", + "use_system_language": "使用系统语言", + "view_adapter_info": "查看适配器信息", "view_software_update": "查看软件更新", "view_update": "查看更新" } diff --git a/app/src/assets/localization/zh/branded.json b/app/src/assets/localization/zh/branded.json index c38888398f1..b7cbc41d684 100644 --- a/app/src/assets/localization/zh/branded.json +++ b/app/src/assets/localization/zh/branded.json @@ -2,8 +2,8 @@ "a_robot_software_update_is_available": "需要更新工作站软件才能使用此版本的Opentrons应用程序运行协议。转到工作站", "about_flex_gripper": "关于Flex转板抓手", "alternative_security_types_description": "Opentrons应用程序支持将Flex连接到各种企业接入点。通过USB连接并在应用程序中完成设置。", - "attach_a_pipette_for_quick_transfer": "要创建快速移液,您需要将移液器安装到您的Opentrons Flex上。", "attach_a_pipette": "将移液器连接到Flex", + "attach_a_pipette_for_quick_transfer": "要创建快速移液,您需要将移液器安装到您的Opentrons Flex上。", "calibration_block_description": "这个金属块是一个特制的工具,完美适配您的甲板,有助于校准。如果您没有校准块,请发送电子邮件至support@opentrons.com,以便我们寄送一个给您。在您提供的信息中,请确保包括您的姓名、公司或机构名称和寄送地址。在等待校准块到达过程中,您可以暂时利用工作站里垃圾桶上的平面进行校准。", "calibration_on_opentrons_tips_is_important": "使用上述Opentrons吸头和吸头盒进行校准非常重要,因为工作站的准确性是基于这些吸头的已知尺寸来确定的。", "choose_what_data_to_share": "选择要与Opentrons共享的数据。", @@ -23,13 +23,14 @@ "find_your_robot": "在Opentrons应用程序中找到您的工作站以安装软件更新。", "firmware_update_download_logs": "从Opentrons应用程序下载工作站日志并将其发送到support@opentrons.com寻求帮助。", "general_error_message": "如果您一直收到此消息,请尝试重新启动您的应用程序和工作站。如果这不能解决问题,请与Opentrons支持人员联系。", + "gripper": "Flex转板抓手", "gripper_still_attached": "Flex转板抓手仍处于连接状态", "gripper_successfully_attached_and_calibrated": "Flex转板抓手已成功连接并校准", "gripper_successfully_calibrated": "Flex转板抓手已成功校准", "gripper_successfully_detached": "Flex转板抓手已成功卸下", - "gripper": "Flex转板抓手", "help_us_improve_send_error_report": "通过向{{support_email}}发送错误报告,帮助我们改进您的使用体验", "ip_description_second": "Opentrons建议您联系网络管理员,为工作站分配静态IP地址。", + "language_preference_description": "Opentrons APP默认匹配与您的系统语言,您也可以选择使用下方其他语言。当然,后续您也可以在APP设置中进行语言更改。", "learn_uninstalling": "了解更多有关卸载Opentrons应用程序的信息", "loosen_screws_and_detach": "松开螺丝并卸下Flex转板抓手", "modal_instructions": "有关设置模块的分步说明,请参阅随包装附带的快速指引。您也可以单击下面的链接或扫描二维码访问Opentrons帮助中心的模块部分。", @@ -68,7 +69,10 @@ "show_labware_offset_snippets_description": "仅适用于需要在Opentrons应用程序之外应用耗材校准数据的用户。启用后,在设置协议过程中可访问Jupyter Notebook和SSH的代码片段。", "something_seems_wrong": "您的移液器可能有问题。退出设置并联系Opentrons支持人员以获取帮助。", "storage_limit_reached_description": "您的 Opentrons Flex 已达到可存储的快速移液数量上限。在创建新的快速移液之前,您必须删除一个现有的快速移液。", + "system_language_preferences_update_description": "您的系统语言最近已更新。您想将更新后的语言用作 Opentrons APP的默认语言吗?", "these_are_advanced_settings": "这些是高级设置。请勿在没有Opentrons支持团队帮助的情况下尝试调整这些设置。更改这些设置可能会影响您的移液器寿命。这些设置不会覆盖协议中定义的任何移液器设置。", + "u2e_driver_description": "OT-2 通过此适配器,使用 USB 连接 Opentrons APP。", + "unexpected_error": "发生意外错误。如果问题仍然存在,请联系 Opentrons 支持团队寻求帮助。", "update_requires_restarting_app": "更新需要重新启动Opentrons应用程序。", "update_robot_software_description": "绕过Opentrons应用程序自动更新过程并手动更新工作站软件", "update_robot_software_link": "启动Opentrons软件更新页面", diff --git a/app/src/assets/localization/zh/change_pipette.json b/app/src/assets/localization/zh/change_pipette.json index 9818fd56f85..3a7f16bb724 100644 --- a/app/src/assets/localization/zh/change_pipette.json +++ b/app/src/assets/localization/zh/change_pipette.json @@ -1,8 +1,8 @@ { "are_you_sure_exit": "您确定要在{{direction}}移液器之前退出吗?", "attach_name_pipette": "安装一个{{pipette}}移液器", - "attach_pipette_type": "安装一个{{pipetteName}}移液器", "attach_pipette": "安装一个移液器", + "attach_pipette_type": "安装一个{{pipetteName}}移液器", "attach_the_pipette": "

连接移液器

推入白色连接器,直到感觉它插入移液器。", "attached_pipette_does_not_match": "连接的{{name}}与您最初选择的{{pipette}}不匹配。", "attaching": "正在连接", @@ -16,10 +16,10 @@ "confirming_attachment": "正在确认连接", "confirming_detachment": "正在确认拆卸", "continue": "继续", - "detach_pipette_from_mount": "从{{mount}}安装支架上卸下移液器", + "detach": "卸下移液器", "detach_pipette": "从{{mount}}安装支架上卸下{{pipette}}", + "detach_pipette_from_mount": "从{{mount}}安装支架上卸下移液器", "detach_try_again": "卸下并重试", - "detach": "卸下移液器", "detaching": "正在卸下", "get_started": "开始", "go_back": "返回", diff --git a/app/src/assets/localization/zh/device_details.json b/app/src/assets/localization/zh/device_details.json index a19e61a365b..fdcab146c28 100644 --- a/app/src/assets/localization/zh/device_details.json +++ b/app/src/assets/localization/zh/device_details.json @@ -1,15 +1,16 @@ { "about_gripper": "关于转板抓手", "about_module": "关于{{name}}", - "about_pipette_name": "关于{{name}}移液器", "about_pipette": "关于移液器", + "about_pipette_name": "关于{{name}}移液器", + "abs_reader_lid_status": "上盖状态: {{status}}", "abs_reader_status": "吸光度读板器状态", + "add": "添加", "add_fixture_description": "将此硬件添加至甲板配置。它在协议分析期间将会被引用。", "add_to_slot": "添加到板位{{slotName}}", - "add": "添加", + "an_error_occurred_while_updating": "更新移液器设置时发生错误。", "an_error_occurred_while_updating_module": "更新{{moduleName}}时出现错误,请重试。", "an_error_occurred_while_updating_please_try_again": "更新移液器设置时出错,请重试。", - "an_error_occurred_while_updating": "更新移液器设置时发生错误。", "attach_gripper": "安装转板抓手", "attach_pipette": "安装移液器", "bad_run": "无法加载运行", @@ -17,13 +18,13 @@ "bundle_firmware_file_not_found": "未找到类型为{{module}}的模块固件包文件。", "calibrate_gripper": "校准转板抓手", "calibrate_now": "立即校准", - "calibrate_pipette_offset": "校准移液器数据", "calibrate_pipette": "校准移液器", - "calibration_needed_without_link": "需要校准。", + "calibrate_pipette_offset": "校准移液器数据", "calibration_needed": "需要校准。 立即校准", + "calibration_needed_without_link": "需要校准。", "canceled": "已取消", - "changes_will_be_lost_description": "确定不保存甲板配置直接退出而吗?", "changes_will_be_lost": "更改将丢失", + "changes_will_be_lost_description": "确定不保存甲板配置直接退出而吗?", "choose_protocol_to_run": "选择在{{name}}上运行的协议", "close_lid": "关闭上盖", "completed": "已完成", @@ -35,9 +36,9 @@ "current_temp": "当前:{{temp}}°C", "current_version": "当前版本", "deck_cal_missing": "缺少移液器校准数据,请先校准甲板。", + "deck_configuration": "甲板配置", "deck_configuration_is_not_available_when_robot_is_busy": "工作站忙碌时,甲板配置不可用", "deck_configuration_is_not_available_when_run_is_in_progress": "工作站运行时,甲板配置不可用", - "deck_configuration": "甲板配置", "deck_fixture_setup_instructions": "甲板配置安装说明", "deck_fixture_setup_modal_bottom_description_desktop": "针对不同类型的配置,扫描二维码或访问下方链接获取详细说明。", "deck_fixture_setup_modal_top_description": "首先,拧松并移除计划安装模组的甲板。然后放置模组,并进行固定。", @@ -57,14 +58,14 @@ "estop_pressed": "急停按钮被按下。工作站运动已停止。", "failed": "失败", "files": "文件", - "firmware_update_needed": "需要更新工作站固件。请在工作站的触摸屏上开始更新。", "firmware_update_available": "固件更新可用。", "firmware_update_failed": "未能更新模块固件", - "firmware_updated_successfully": "固件更新成功", + "firmware_update_needed": "需要更新工作站固件。请在工作站的触摸屏上开始更新。", "firmware_update_occurring": "固件更新正在进行中...", + "firmware_updated_successfully": "固件更新成功", "fixture": "配置模组", - "have_not_run_description": "运行一些协议后,它们会在这里显示。", "have_not_run": "无最近运行记录", + "have_not_run_description": "运行一些协议后,它们会在这里显示。", "heater": "加热器", "height_ranges": "{{gen}}高度范围", "hot_to_the_touch": "模块接触时很热", @@ -73,12 +74,12 @@ "instruments_and_modules": "设备与模块", "labware_bottom": "耗材底部", "last_run_time": "最后一次运行{{number}}", - "left_right": "左右支架", "left": "左侧", + "left_right": "左右支架", "lights": "灯光", "link_firmware_update": "查看固件更新", - "location_conflicts": "位置冲突", "location": "位置", + "location_conflicts": "位置冲突", "magdeck_gen1_height": "高度:{{height}}", "magdeck_gen2_height": "高度:{{height}}毫米", "max_engage_height": "最大可用高度", @@ -87,13 +88,13 @@ "missing_hardware": "缺少硬件", "missing_instrument": "缺少{{num}}个设备", "missing_instruments_plural": "缺少{{count}}个设备", - "missing_module_plural": "缺少{{count}}个模块", "missing_module": "缺少{{num}}个模块", + "missing_module_plural": "缺少{{count}}个模块", "module_actions_unavailable": "协议运行时模块操作不可用", + "module_calibration_required": "需要模块校准。", "module_calibration_required_no_pipette_attached": "需要模块校准。在运行模块校准前,请连接移液器。", "module_calibration_required_no_pipette_calibrated": "需要模块校准。在校准模块前,请先校准移液器。", "module_calibration_required_update_pipette_FW": "在进行必要的模块校准前,请先更新移液器固件。", - "module_calibration_required": "需要模块校准。", "module_controls": "模块控制", "module_error": "模块错误", "module_name_error": "{{moduleName}}错误", @@ -104,8 +105,8 @@ "no_deck_fixtures": "无甲板配置", "no_protocol_runs": "暂无协议运行记录!", "no_protocols_found": "未找到协议", - "no_recent_runs_description": "运行一些协议后,它们将显示在此处。", "no_recent_runs": "无最近运行记录", + "no_recent_runs_description": "运行一些协议后,它们将显示在此处。", "num_units": "{{num}}毫米", "offline_deck_configuration": "工作站必须连接网络才能查看甲板配置", "offline_instruments_and_modules": "工作站必须连接网络才能查看已连接的设备和模块", @@ -131,12 +132,12 @@ "protocol_analysis_failed": "协议APP分析失败。", "protocol_analysis_stale": "协议分析已过期。", "protocol_details_page_reanalyze": "前往协议详情页面重新分析。", - "ready_to_run": "运行工作准备完成", "ready": "准备就绪", + "ready_to_run": "运行工作准备完成", "recalibrate_gripper": "重新校准转板抓手", "recalibrate_now": "立即重新校准", - "recalibrate_pipette_offset": "重新校准移液器偏移", "recalibrate_pipette": "重新校准移液器", + "recalibrate_pipette_offset": "重新校准移液器偏移", "recent_protocol_runs": "最近的协议运行", "rerun_loading": "数据完全加载前,禁止协议重新运行", "rerun_now": "立即重新运行协议", @@ -146,17 +147,17 @@ "right": "右侧", "robot_control_not_available": "运行过程中某些工作站控制功能不可用", "robot_initializing": "初始化中...", + "run": "运行", "run_a_protocol": "运行协议", "run_again": "再次运行", "run_duration": "运行时长", - "run": "运行", "select_options": "选择选项", "serial_number": "序列号", "set_block_temp": "设置温度", "set_block_temperature": "设置模块温度", + "set_engage_height": "设置启用高度", "set_engage_height_and_enter_integer": "为此磁力模块设置启用高度。请输入一个介于{{lower}}和{{higher}}之间的整数。", "set_engage_height_for_module": "为{{name}}设置启用高度", - "set_engage_height": "设置启用高度", "set_lid_temperature": "设置上盖温度", "set_shake_of_hs": "为此模块设置转速。", "set_shake_speed": "设置震荡速度", @@ -173,8 +174,8 @@ "target_temp": "目标温度:{{temp}}°C", "tc_block": "模块", "tc_lid": "上盖", - "tc_set_temperature_body": "预热或预冷您的热循环模块的{{part}}。请输入介于{{min}}°C 和{{max}}°C之间的一个整数。", "tc_set_temperature": "为{{name}}设置温度{{part}}", + "tc_set_temperature_body": "预热或预冷您的热循环模块的{{part}}。请输入介于{{min}}°C 和{{max}}°C之间的一个整数。", "tempdeck_slideout_body": "预热或冷却您的{{model}}。输入4°C至96°C之间的一个整数。", "tempdeck_slideout_title": "为{{name}}设置温度", "temperature": "温度", @@ -184,12 +185,12 @@ "trash": "垃圾桶", "update_now": "立即更新", "updating_firmware": "正在更新固件...", - "usb_port_not_connected": "USB未连接", "usb_port": "USB端口-{{port}}", + "usb_port_not_connected": "USB未连接", "version": "版本{{version}}", + "view": "查看", "view_pipette_setting": "移液器设置", "view_run_record": "查看协议运行记录", - "view": "查看", "waste_chute": "外置垃圾槽", "welcome_modal_description": "运行协议、管理设备及查看工作站状态的地方。", "welcome_to_your_dashboard": "欢迎来到您的控制面板!", diff --git a/app/src/assets/localization/zh/device_settings.json b/app/src/assets/localization/zh/device_settings.json index ecd81c941dd..40e59b5c354 100644 --- a/app/src/assets/localization/zh/device_settings.json +++ b/app/src/assets/localization/zh/device_settings.json @@ -3,6 +3,7 @@ "about_calibration_description": "为了让工作站精确移动,您需要对其进行校准。位置校准分为三部分:甲板校准、移液器偏移校准和吸头长度校准。", "about_calibration_description_ot3": "为了让工作站精确移动,您需要对其进行校准。移液器和转板抓手校准是一个自动化过程,使用校准探头或销钉。校准完成后,您可以将校准数据以JSON文件的形式保存到计算机中。", "about_calibration_title": "关于校准", + "add_new": "添加新的...", "advanced": "高级", "alpha_description": "警告:alpha版本功能完整,但可能包含重大错误。", "alternative_security_types": "可选的安全类型", @@ -10,10 +11,12 @@ "apply_historic_offsets": "应用耗材偏移校准数据", "are_you_sure_you_want_to_disconnect": "您确定要断开与{{ssid}}的连接吗?", "attach_a_pipette_before_calibrating": "在执行校准之前,请安装移液器", + "authentication": "验证", "boot_scripts": "启动脚本", "both": "两者", "browse_file_system": "浏览文件系统", "bug_fixes": "错误修复", + "but_we_expected": "但我们预计", "calibrate_deck": "校准甲板", "calibrate_deck_description": "适用于没有在甲板上蚀刻十字的2019年之前的工作站。", "calibrate_deck_to_dots": "根据校准点校准甲板", @@ -24,11 +27,14 @@ "calibration": "校准", "calibration_health_check_description": "检查关键校准点的精度,无需重新校准工作站。", "calibration_health_check_title": "校准运行状况检查", + "cancel_software_update": "取消软件更新", "change_network": "更改网络", "characters_max": "最多17个字符", "check_for_updates": "检查更新", + "check_to_verify_update": "检查设备的设置页面以验证更新是否成功", "checking_for_updates": "正在检查更新", "choose": "选择...", + "choose_a_network": "选择网络...", "choose_file": "选择文件", "choose_network_type": "选择网络类型", "choose_reset_settings": "选择重置设置", @@ -50,13 +56,14 @@ "clear_option_runs_history": "清除协议运行历史", "clear_option_runs_history_subtext": "清除所有协议的过往运行信息。点击并应用", "clear_option_tip_length_calibrations": "清除吸头长度校准", - "cancel_software_update": "取消软件更新", "complete_and_restart_robot": "完成并重新启动工作站", "confirm_device_reset_description": "这将永久删除所有协议、校准和其他数据。您需要重新进行初始设置才能再次使用工作站。", "confirm_device_reset_heading": "您确定要重置您的设备吗?", "connect": "连接", "connect_the_estop_to_continue": "连接紧急停止按钮以继续", + "connect_to_ssid": "连接到 {{ssid}}", "connect_to_wifi_network": "连接到Wi-Fi网络", + "connect_to_wifi_network_failure": "您的设备无法连接到 Wi-Fi 网络 {{ssid}}", "connect_via": "通过{{type}}连接", "connect_via_usb_description_1": "1. 将 USB A-to-B 连接线连接到工作站的 USB-B 端口。", "connect_via_usb_description_2": "2. 将电缆连接到计算机上的一个空闲USB端口。", @@ -65,6 +72,7 @@ "connected_to_ssid": "已连接到{{ssid}}", "connected_via": "通过{{networkInterface}}连接", "connecting_to": "正在连接到{{ssid}}...", + "connecting_to_wifi_network": "连接到 Wi-Fi 网络 {{ssid}}", "connection_description_ethernet": "连接到您实验室的有线网络。", "connection_description_wifi": "在您的实验室中找到一个网络,或者输入您自己的网络。", "connection_to_robot_lost": "与工作站的连接中断", @@ -81,7 +89,6 @@ "device_reset_description": "将耗材校准、启动脚本和/或工作站校准重置为出厂设置。", "device_reset_slideout_description": "选择单独的设置以仅清除特定的数据类型。", "device_resets_cannot_be_undone": "重置无法撤销", - "release_notes": "发行说明", "directly_connected_to_this_computer": "直接连接到这台计算机。", "disconnect": "断开连接", "disconnect_from_ssid": "断开与{{ssid}}的连接", @@ -97,6 +104,7 @@ "display_sleep_settings": "屏幕睡眠设置", "do_not_turn_off": "这可能需要最多{{minutes}}分钟。请不要关闭工作站。", "done": "完成", + "downgrade": "降级", "download": "下载", "download_calibration_data": "下载校准日志", "download_error": "下载错误日志", @@ -110,6 +118,7 @@ "enable_status_light_description": "打开或关闭工作站前部的指示LED灯条。", "engaged": "已连接", "enter_factory_password": "输入工厂密码", + "enter_name_security_type": "输入网络名称和安全类型。", "enter_network_name": "输入网络名称", "enter_password": "输入密码", "estop": "紧急停止按钮", @@ -128,6 +137,8 @@ "factory_resets_cannot_be_undone": "工厂重置无法撤销。", "failed_to_connect_to_ssid": "无法连接到{{ssid}}", "feature_flags": "功能标志", + "field_is_required": "需要{{field}} ", + "find_and_join_network": "查找并加入 Wi-Fi 网络", "finish_setup": "完成设置", "firmware_version": "固件版本", "fully_calibrate_before_checking_health": "在检查校准健康之前,请完全校准您的工作站", @@ -155,6 +166,7 @@ "last_calibrated_label": "最后校准", "launch_jupyter_notebook": "启动Jupyter Notebook", "legacy_settings": "遗留设置", + "likely_incorrect_password": "可能网络密码不正确。", "mac_address": "MAC地址", "manage_oem_settings": "管理OEM设置", "minutes": "{{minute}}分钟", @@ -172,17 +184,22 @@ "name_your_robot": "给您的工作站起个名字", "name_your_robot_description": "别担心,您可以在设置中随时更改这个名称。", "need_another_security_type": "需要另一种安全类型吗?", + "network_is_unsecured": "Wi-Fi 网络 {{ssid}} 不安全", "network_name": "网络名称", + "network_requires_auth": "Wi-Fi 网络 {{ssid}} 需要 802.1X 身份验证", + "network_requires_wpa_password": "Wi-Fi 网络 {{ssid}} 需要 WPA2 密码", "network_settings": "网络设置", "networking": "网络连接", "never": "从不", "new_features": "新功能", "next_step": "下一步", + "no_calibration_required": "无需校准", "no_connection_found": "未找到连接", "no_gripper_attached": "未连接转板抓手", "no_modules_attached": "未连接模块", "no_network_found": "未找到网络", "no_pipette_attached": "未连接移液器", + "no_update_files": "无法检索此设备的更新状态。请确保您的计算机已连接到互联网,然后稍后重试。", "none_description": "不推荐", "not_calibrated": "尚未校准", "not_calibrated_short": "未校准", @@ -194,11 +211,13 @@ "not_now": "不是现在", "oem_mode": "OEM模式", "off": "关闭", - "one_hour": "1小时", "on": "开启", + "one_hour": "1小时", "other_networks": "其他网络", + "other_robot_updating": "无法更新,因为该APP目前正在更新其他设备。", "password": "密码", "password_error_message": "至少需要8个字符", + "password_not_long_enough": "密码必须至少为 {{minLength}} 字符", "pause_protocol": "当工作站前门打开时暂停协议", "pause_protocol_description": "启用后,在运行过程中打开工作站前门,工作站会在完成当前动作后暂停。", "pipette_calibrations_description": "使用校准探头来确定移液器相对于甲板槽上的精密切割方格的确切位置。", @@ -208,6 +227,7 @@ "pipette_offset_calibration_recommended": "建议进行移液器偏移校准", "pipette_offset_calibrations_history": "查看所有移液器偏移校准历史", "pipette_offset_calibrations_title": "移液器偏移校准", + "please_check_credentials": "请仔细检查你的网络凭证", "privacy": "隐私", "problem_during_update": "此次更新耗时比平常要长。", "proceed_without_updating": "跳过更新以继续", @@ -220,6 +240,7 @@ "recalibrate_tip_and_pipette": "重新校准吸头长度和移液器偏移量", "recalibration_recommended": "建议重新校准", "reinstall": "重新安装", + "release_notes": "发行说明", "remind_me_later": "稍后提醒我", "rename_robot": "重命名工作站", "rename_robot_input_error": "哎呀!工作站名称必须遵循字符计数和限制。", @@ -237,9 +258,12 @@ "returns_your_device_to_new_state": "这将使您的设备恢复到新的状态。", "robot_busy_protocol": "当协议正在运行时,此工作站无法更新", "robot_calibration_data": "工作站校准数据", + "robot_has_bad_capabilities": "设备配置不正确", "robot_initializing": "正在初始化工作站...", "robot_name": "工作站名称", "robot_operating_update_available": "工作站操作系统更新可用", + "robot_reconnected_with version": "设备重新获取版本", + "robot_requires_premigration": "系统必须先更新此设备,然后才能进行自定义更新", "robot_serial_number": "工作站序列号", "robot_server_version": "工作站服务器版本", "robot_settings": "工作站设置", @@ -258,7 +282,9 @@ "select_a_network": "选择一个网络", "select_a_security_type": "选择一个安全类型", "select_all_settings": "选择所有设置", + "select_auth_method_short": "选择身份验证方法", "select_authentication_method": "为您所选的网络选择身份验证方法。", + "select_file": "选择文件", "sending_software": "正在发送软件...", "serial": "序列号", "setup_mode": "设置模式", @@ -274,6 +300,9 @@ "subnet_mask": "子网掩码", "successfully_connected": "成功连接!", "successfully_connected_to_network": "已成功连接到{{ssid}}!", + "successfully_connected_to_ssid": "您的设备已成功连接至 Wi-Fi 网络 {{ssid}}", + "successfully_connected_to_wifi": "成功连接 Wi-Fi", + "successfully_disconnected_from_wifi": "已成功断开 Wi-Fi 连接", "supported_protocol_api_versions": "支持的协议API版本", "text_size": "文本大小", "text_size_description": "所有屏幕上的文本都会根据您在下方选择的大小进行调整。", @@ -285,18 +314,29 @@ "troubleshooting": "故障排查", "try_again": "再试一次", "try_restarting_the_update": "尝试重新启动更新。", + "unable_to_cancel_update": "无法取消正在进行的更新", + "unable_to_commit_update": "无法提交更新", + "unable_to_connect": "无法连接到 Wi-Fi", + "unable_to_disconnect": "无法断开 Wi-Fi 连接", + "unable_to_find_robot_with_name": "无法找到在线设备", + "unable_to_find_system_file": "无法找到要更新的系统文件", + "unable_to_restart": "无法重启设备", + "unable_to_start_update_session": "无法启动更新", "up_to_date": "最新的", "update_available": "有可用更新", "update_channel_description": "稳定版接收最新的稳定版发布。Beta 版允许您在新功能在稳定版发布前先行试用,但这些新功能尚未完成测试。", "update_complete": "更新完成!", "update_found": "发现更新!", + "update_requires_restarting_robot": "更新工作站软件需要重启工作站", "update_robot_now": "现在更新工作站", "update_robot_software": "使用本地文件(.zip)手动更新工作站软件", + "update_server_unavailable": "无法更新,因为您设备的更新服务器没有响应。", + "update_unavailable": "无法更新", "updating": "正在更新", - "update_requires_restarting_robot": "更新工作站软件需要重启工作站", + "upgrade": "升级", + "upload_custom_logo": "上传自定义Logo", "upload_custom_logo_description": "上传一个Logo,用于工作站启动时显示。", "upload_custom_logo_dimensions": "Logo必须符合 1024 x 600 的尺寸,且是 PNG 文件(.png)。", - "upload_custom_logo": "上传自定义Logo", "usage_settings": "使用设置", "usb": "USB", "usb_to_ethernet_description": "正在查找 USB-to-Ethernet 适配器信息?", diff --git a/app/src/assets/localization/zh/devices_landing.json b/app/src/assets/localization/zh/devices_landing.json index 8e6af9d5ba9..4c649c41b02 100644 --- a/app/src/assets/localization/zh/devices_landing.json +++ b/app/src/assets/localization/zh/devices_landing.json @@ -23,9 +23,9 @@ "lights_on": "开启灯光", "loading": "加载中", "looking_for_robots": "寻找工作站", - "ninety_six_mount": "左侧+右侧安装支架", "make_sure_robot_is_connected": "确保工作站已连接到此计算机", "modules": "模块", + "ninety_six_mount": "左侧+右侧安装支架", "no_robots_found": "未找到工作站", "not_available": "不可用({{count}})", "ot2_quickstart_guide": "OT-2 快速入门指南", diff --git a/app/src/assets/localization/zh/drop_tip_wizard.json b/app/src/assets/localization/zh/drop_tip_wizard.json index a5ae8faebfc..307c2aa7e72 100644 --- a/app/src/assets/localization/zh/drop_tip_wizard.json +++ b/app/src/assets/localization/zh/drop_tip_wizard.json @@ -16,9 +16,9 @@ "drop_tip_failed": "丢弃吸头操作未能完成,请联系技术支持获取帮助。", "drop_tips": "丢弃吸头", "error_dropping_tips": "丢弃吸头时发生错误", - "exit_and_home_pipette": "退出并归位移液器", - "exit_screen_title": "在完成吸头丢弃前退出?", "exit": "退出", + "exit_and_home_pipette": "退出并归位移液器", + "fixed_trash_in_12": "固定垃圾存放位置为12", "getting_ready": "正在准备…", "go_back": "返回", "jog_too_far": "移动过远?", @@ -30,7 +30,6 @@ "position_and_drop_tip": "确保移液器吸头尖端位于指定位置的正上方并保持水平。如果不是,请使用下面的控制键或键盘微调移液器直到正确位置。", "position_the_pipette": "调整移液器位置", "remove_any_attached_tips": "移除任何已安装的吸头", - "remove_the_tips": "在协议中再次使用前,您可能需要从{{mount}}移液器上移除吸头。", "remove_the_tips_from_pipette": "在协议中再次使用前,您可能需要从移液器上移除吸头。", "remove_the_tips_manually": "手动移除吸头,然后使龙门架回原点。在拾取吸头的状态下归位可能导致移液器吸入液体并损坏。", "remove_tips": "移除吸头", @@ -38,8 +37,8 @@ "select_blowout_slot_odd": "您可以将液体吹入耗材中。
龙门架移动到选定的板位后,使用位置控制按键将移液器移动到吹出液体的确切位置。", "select_drop_tip_slot": "您可以将吸头返回吸头架或丢弃它们。在右侧的甲板图上选择您想丢弃吸头的板位。确认后龙门架将移动到选定的板位。", "select_drop_tip_slot_odd": "您可以将吸头放回吸头架或丢弃它们。
龙门架移动到选定的板位后,使用位置控制按键将移液器移动到丢弃吸头的确切位置。", - "skip_and_home_pipette": "跳过并归位移液器", "skip": "跳过", + "skip_and_home_pipette": "跳过并归位移液器", "stand_back_blowing_out": "请远离,工作站正在吹出液体", "stand_back_dropping_tips": "请远离,工作站正在丢弃吸头", "stand_back_robot_in_motion": "请远离,工作站正在移动", diff --git a/app/src/assets/localization/zh/error_recovery.json b/app/src/assets/localization/zh/error_recovery.json index dddf7923d4b..a740eac779d 100644 --- a/app/src/assets/localization/zh/error_recovery.json +++ b/app/src/assets/localization/zh/error_recovery.json @@ -6,9 +6,9 @@ "before_you_begin": "开始前", "begin_removal": "开始移除", "blowout_failed": "吹出液体失败", - "overpressure_is_usually_caused": "探测器感应到压力过大通常是由吸头碰撞到实验用品、吸头堵塞或吸取/排出粘稠液体速度过快引起。如果问题持续存在,请取消运行并对协议进行必要的修改。", "cancel_run": "取消运行", "canceling_run": "正在取消运行", + "carefully_move_labware": "小心地移开放错位置的实验用品并清理溢出的液体。继续操作之前请关闭设备前门。", "change_location": "更改位置", "change_tip_pickup_location": "更换拾取吸头的位置", "choose_a_recovery_action": "选择恢复操作", @@ -19,10 +19,12 @@ "continue": "继续", "continue_run_now": "现在继续运行", "continue_to_drop_tip": "继续丢弃吸头", + "do_you_need_to_blowout": "首先,请问需要排出枪头内的液体吗?", + "door_open_robot_home": "在手动移动实验用品前,设备需要安全归位。", "ensure_lw_is_accurately_placed": "确保实验耗材已准确放置在甲板槽中,防止进一步出现错误", + "error": "错误", "error_details": "错误详情", "error_on_robot": "{{robot}}上的错误", - "error": "错误", "failed_dispense_step_not_completed": "中断运行的最后一步液体排出失败,恢复程序将不会继续运行这一步骤,请手动完成这一步的移液操作。运行将继续从下一步开始。继续之前,请关闭工作站门。", "failed_step": "失败步骤", "first_is_gripper_holding_labware": "首先,抓扳手是否夹着实验耗材?", @@ -31,6 +33,9 @@ "gripper_errors_occur_when": "当抓扳手停滞或与甲板上另一物体碰撞时,会发生抓扳手错误,这通常是由于实验耗材放置不当或实验耗材偏移不准确所致", "gripper_releasing_labware": "抓扳手正在释放实验耗材", "gripper_will_release_in_s": "抓扳手将在{{seconds}}秒后释放实验耗材", + "home_and_retry": "归位并重试该步骤", + "home_gantry": "归位", + "home_now": "现在归位", "homing_pipette_dangerous": "如果移液器中有液体,将{{mount}}移液器归位可能会损坏它。您必须在使用移液器之前取下所有吸头。", "if_issue_persists_gripper_error": "如果问题持续存在,请取消运行并重新运行抓扳手校准", "if_issue_persists_overpressure": "如果问题持续存在,请取消运行并对协议进行必要的更改", @@ -40,6 +45,7 @@ "ignore_error_and_skip": "忽略错误并跳到下一步", "ignore_only_this_error": "仅忽略此错误", "ignore_similar_errors_later_in_run": "要在后续的运行中忽略类似错误吗?", + "inspect_the_robot": "首先,检查设备以确保它已准备好从下一步继续运行。然后,关闭设备前门,再继续操作。", "labware_released_from_current_height": "将从当前高度释放实验耗材", "launch_recovery_mode": "启动恢复模式", "manually_fill_liquid_in_well": "手动填充孔位{{well}}中的液体", @@ -48,29 +54,36 @@ "manually_move_lw_on_deck": "手动移动实验耗材", "manually_replace_lw_and_retry": "手动更换实验耗材并重试此步骤", "manually_replace_lw_on_deck": "手动更换实验耗材", + "na": "N/A", "next_step": "下一步", "next_try_another_action": "接下来,您可以尝试另一个恢复操作或取消运行。", "no_liquid_detected": "未检测到液体", + "overpressure_is_usually_caused": "探测器感应到压力过大通常是由吸头碰撞到实验用品、吸头堵塞或吸取/排出粘稠液体速度过快引起。如果问题持续存在,请取消运行并对协议进行必要的修改。", "pick_up_tips": "取吸头", "pipette_overpressure": "移液器超压", - "preserve_aspirated_liquid": "首先,您需要保留已吸取的液体吗?", + "prepare_deck_for_homing": "整理甲板,准备归位", "proceed_to_cancel": "继续取消", + "proceed_to_home": "归位中", "proceed_to_tip_selection": "继续选择吸头", "recovery_action_failed": "{{action}}失败", "recovery_mode": "恢复模式", "recovery_mode_explanation": "恢复模式为您提供运行错误后的手动处理引导。
您可以进行调整以确保发生错误时正在进行的步骤可以完成,或者选择取消协议。当做出调整且未检测到后续错误时,该模式操作完成。根据导致错误的条件,系统将提供相应的调整选项。", - "release_labware_from_gripper": "从抓板手中释放实验耗材", "release": "释放", + "release_labware_from_gripper": "从抓板手中释放实验耗材", "remove_any_attached_tips": "移除任何已安装的吸头", + "replace_tips_and_select_loc_partial_tip": "更换吸头并选择最后用于偏转移液吸头拾取的位置。", "replace_tips_and_select_location": "建议更换吸头并选择最后一次取吸头的位置。", "replace_used_tips_in_rack_location": "在吸头板位{{location}}更换已使用的吸头", "replace_with_new_tip_rack": "更换新的吸头盒", "resume": "继续", - "retrying_step_succeeded": "重试步骤{{step}}成功", + "retry_dropping_tip": "重试弹出吸头", "retry_now": "现在重试", + "retry_picking_up_tip": "重试拾取吸头", "retry_step": "重试步骤", "retry_with_new_tips": "使用新吸头重试", "retry_with_same_tips": "使用相同吸头重试", + "retrying_step_succeeded": "重试步骤{{step}}成功", + "retrying_step_succeeded_na": "重试当前步骤成功。", "return_to_menu": "返回菜单", "robot_door_is_open": "工作站前门已打开", "robot_is_canceling_run": "工作站正在取消运行", @@ -83,25 +96,30 @@ "robot_will_retry_with_tips": "工作站将使用新吸头重试失败的步骤。", "run_paused": "运行暂停", "select_tip_pickup_location": "选择取吸头位置", - "skipping_to_step_succeeded": "跳转到步骤{{step}}成功", "skip_and_home_pipette": "跳过并归位移液器", "skip_to_next_step": "跳到下一步", "skip_to_next_step_new_tips": "使用新吸头跳到下一步", "skip_to_next_step_same_tips": "使用相同吸头跳到下一步", + "skipping_to_step_succeeded": "跳转到步骤{{step}}成功", + "skipping_to_step_succeeded_na": "跳至下一步成功。", + "stall_or_collision_detected_when": "当设备电机堵塞时,检测到失速或碰撞", + "stall_or_collision_error": "失速或碰撞", "stand_back": "请远离,工作站正在运行", "stand_back_picking_up_tips": "请远离,正在拾取吸头", "stand_back_resuming": "请远离,正在恢复当前步骤", "stand_back_retrying": "请远离,正在重试失败步骤", "stand_back_skipping_to_next_step": "请远离,正在跳到下一步骤", "take_any_necessary_precautions": "在接住实验耗材之前,请采取必要的预防措施。确认后,夹爪将开始倒计时再释放。", - "take_necessary_actions_failed_pickup": "首先,采取任何必要的行动,让工作站重新尝试移液器拾取。然后,在继续之前关闭工作站门。", "take_necessary_actions": "首先,采取任何必要的行动,让工作站重新尝试失败的步骤。然后,在继续之前关闭工作站门。", + "take_necessary_actions_failed_pickup": "首先,采取任何必要的行动,让工作站重新尝试移液器拾取。然后,在继续之前关闭工作站门。", + "take_necessary_actions_failed_tip_drop": "首先,采取一切必要的措施,让设备准备好重试弹出吸头。然后,关闭设备前门,再继续操作。", + "take_necessary_actions_home": "采取一切必要的措施,准备让设备归位。继续操作之前请关闭设备前门。", "terminate_remote_activity": "终止远程活动", + "the_robot_must_return_to_home_position": "设备必须归位才能继续", "tip_drop_failed": "丢弃吸头失败", "tip_not_detected": "未检测到吸头", "tip_presence_errors_are_caused": "吸头识别错误通常是由实验器皿放置不当或器皿偏移不准确引起的。", "view_error_details": "查看错误详情", "view_recovery_options": "查看恢复选项", - "you_can_still_drop_tips": "在继续选择吸头之前,您仍然可以丢弃移液器上现存的吸头。", - "you_may_want_to_remove": "在协议中再次使用之前,您可能需要从{{mount}}移液器上移除吸头。" + "you_can_still_drop_tips": "在继续选择吸头之前,您仍然可以丢弃移液器上现存的吸头。" } diff --git a/app/src/assets/localization/zh/gripper_wizard_flows.json b/app/src/assets/localization/zh/gripper_wizard_flows.json index 6617e15d83a..00319f11f6d 100644 --- a/app/src/assets/localization/zh/gripper_wizard_flows.json +++ b/app/src/assets/localization/zh/gripper_wizard_flows.json @@ -5,19 +5,17 @@ "before_you_begin": "开始之前", "begin_calibration": "开始校准", "calibrate_gripper": "校准转板抓手", - "calibration_pin": "校准钉", "calibration_pin_touching": "校准钉将触碰甲板{{slot}}中的校准块,以确定其精确位置。", "complete_calibration": "完成校准", "continue": "继续", "continue_calibration": "继续校准", "detach_gripper": "卸下转板抓手", - "firmware_updating": "需要固件更新,设备正在更新中...", "firmware_up_to_date": "固件已为最新版本。", + "firmware_updating": "需要固件更新,设备正在更新中...", "get_started": "开始操作", "gripper_calibration": "转板抓手校准", "gripper_recalibration": "转板抓手重新校准", "gripper_successfully_attached": "转板抓手已成功安装", - "hex_screwdriver": "2.5mm 六角螺丝刀", "hold_gripper_and_loosen_screws": "用手扶住转板抓手,首先松开上方螺丝,再松开下方螺丝。(螺丝固定于抓手上,不会脱落。)之后小心卸下抓手。", "insert_pin_into_front_jaw": "将校准钉插入前夹爪", "insert_pin_into_rear_jaw": "将校准钉插入后夹爪", diff --git a/app/src/assets/localization/zh/heater_shaker.json b/app/src/assets/localization/zh/heater_shaker.json index 249f1cc6138..efc48439868 100644 --- a/app/src/assets/localization/zh/heater_shaker.json +++ b/app/src/assets/localization/zh/heater_shaker.json @@ -4,43 +4,43 @@ "cannot_shake": "模块闩锁打开时无法执行震荡混匀命令", "close_labware_latch": "关闭耗材闩锁", "close_latch": "关闭闩锁", - "closed_and_locked": "关闭并锁定", "closed": "已关闭", + "closed_and_locked": "关闭并锁定", "closing": "正在关闭", "complete": "完成", "confirm_attachment": "确认已连接", "confirm_heater_shaker_modal_attachment": "确认热震荡模块已连接", "continue_shaking_protocol_start_prompt": "协议启动时是否继续执行震荡混匀命令?", + "deactivate": "停用", "deactivate_heater": "停止加热", "deactivate_shaker": "停止震荡混匀", - "deactivate": "停用", "heater_shaker_in_slot": "在继续前,请在板位{{slotName}}中安装{{moduleName}}", "heater_shaker_is_shaking": "热震荡模块当前正在震荡混匀", "keep_shaking_start_run": "继续震荡混匀并开始运行", - "labware_latch": "耗材闩锁", "labware": "耗材", + "labware_latch": "耗材闩锁", "min_max_rpm": "{{min}}-{{max}}rpm", "module_anchors_extended": "运行开始前,应将模块下方的固定位的漏丝拧紧,确保模块稳固跟甲板结合", "module_in_slot": "{{moduleName}}位于板位{{slotName}}槽中", "module_should_have_anchors": "模块应将两个固定锚完全伸出,以确保稳固地连接到甲板上", + "open": "打开", "open_labware_latch": "打开耗材闩锁", "open_latch": "打开闩锁", - "open": "打开", "opening": "正在打开", "proceed_to_run": "继续运行", "set_shake_speed": "设置震荡混匀速度", "set_temperature": "设置模块温度", "shake_speed": "震荡混匀速度", "show_attachment_instructions": "显示固定操作安装说明", - "stop_shaking_start_run": "停止震荡混匀并开始运行", "stop_shaking": "停止震荡混匀", + "stop_shaking_start_run": "停止震荡混匀并开始运行", "t10_torx_screwdriver": "{{name}}型螺丝刀", "t10_torx_screwdriver_subtitle": "热震荡模块附带。使用其他尺寸可能会损坏模块螺丝", + "test_shake": "测试震荡混匀", "test_shake_banner_information": "测试震荡混匀功能前,如果想往热震荡模块转移耗材,需要在控制页面控制模块开启闩锁", "test_shake_banner_labware_information": "测试震荡混匀功能前,如果想往热震荡模块转移{{labware}},需要控制模块闩锁", "test_shake_slideout_banner_info": "测试震荡混匀功能前,如果想往热震荡模块转移耗材,需要在控制页面控制模块开启闩锁", "test_shake_troubleshooting_slideout_description": "请重新查看模块固定安装及其适配器安装相关说明", - "test_shake": "测试震荡混匀", "thermal_adapter_attached_to_module": "适配器应已连接到模块上", "troubleshoot_step_1": "返回步骤1查看模块固定安装相关说明", "troubleshoot_step_3": "返回步骤3查看模块适配器安装相关说明", diff --git a/app/src/assets/localization/zh/incompatible_modules.json b/app/src/assets/localization/zh/incompatible_modules.json index 7e6da2f253e..0ff716c1326 100644 --- a/app/src/assets/localization/zh/incompatible_modules.json +++ b/app/src/assets/localization/zh/incompatible_modules.json @@ -1,7 +1,7 @@ { "incompatible_modules_attached": "检测到不适用模块,", - "remove_before_running_protocol": "运行协议前请移除以下硬件:", + "is_not_compatible": "{{module_name}}不适用于{{robot_type}},", "needs_your_assistance": "{{robot_name}}需要您的协助", - "remove_before_using": "使用此工作站前,请移除不适用模块.", - "is_not_compatible": "{{module_name}}不适用于{{robot_type}}," + "remove_before_running_protocol": "运行协议前请移除以下硬件:", + "remove_before_using": "使用此工作站前,请移除不适用模块." } diff --git a/app/src/assets/localization/zh/labware_details.json b/app/src/assets/localization/zh/labware_details.json index ba1183c2ea4..76ade48d727 100644 --- a/app/src/assets/localization/zh/labware_details.json +++ b/app/src/assets/localization/zh/labware_details.json @@ -8,8 +8,8 @@ "generic": "通用型", "height": "高度", "length": "长度", - "manufacturer_number": "制造商/目录编号", "manufacturer": "制造商", + "manufacturer_number": "制造商/目录编号", "max_volume": "最大容量", "measurements": "尺寸 (mm)", "na": "不适用", @@ -21,8 +21,8 @@ "u": "U型底", "v": "V型底", "various": "多种", - "well_count": "孔数", "well": "孔", + "well_count": "孔数", "width": "宽度", "x_offset": "X-初始位置", "x_size": "X-尺寸", diff --git a/app/src/assets/localization/zh/labware_landing.json b/app/src/assets/localization/zh/labware_landing.json index 386114541f3..f209e48c3e5 100644 --- a/app/src/assets/localization/zh/labware_landing.json +++ b/app/src/assets/localization/zh/labware_landing.json @@ -3,20 +3,20 @@ "cancel": "取消", "cannot-run-python-missing-labware": "工作站缺少耗材定义,无法运行Python协议。", "category": "类别", - "choose_file_to_upload": "或从您的计算机中选择文件上传。", "choose_file": "选择文件", + "choose_file_to_upload": "或从您的计算机中选择文件上传。", "copied": "已复制!", "create_new_def": "创建新的自定义耗材", "custom_def": "自定义耗材", "date_added": "添加日期", "def_moved_to_trash": "该耗材将被转移到这台电脑的回收站,可能无法恢复。", - "delete_this_labware": "删除此耗材?", "delete": "删除", + "delete_this_labware": "删除此耗材?", "duplicate_labware_def": "复制耗材", "error_importing_file": "{{filename}}导入错误。", "go_to_def": "前往耗材页面", - "import_custom_def": "导入自定义耗材", "import": "导入", + "import_custom_def": "导入自定义耗材", "imported": "{{filename}}已导入。", "invalid_labware_def": "无效耗材", "labware": "耗材", diff --git a/app/src/assets/localization/zh/labware_position_check.json b/app/src/assets/localization/zh/labware_position_check.json index cb27f78e66c..57fd1552a65 100644 --- a/app/src/assets/localization/zh/labware_position_check.json +++ b/app/src/assets/localization/zh/labware_position_check.json @@ -25,8 +25,8 @@ "confirm_position_and_pick_up_tip": "确认位置,取吸头", "confirm_position_and_return_tip": "确认位置,将吸头返回至板位{{next_slot}}并复位", "detach_probe": "移除校准探头", - "ensure_nozzle_position_odd": "请检查并确认{{tip_type}}位于{{item_location}}正上方并水平对齐。如果位置不正确,请点击移动移液器,然后微调移液器直至完全对齐。", "ensure_nozzle_position_desktop": "请检查并确认{{tip_type}}位于{{item_location}}正上方并水平对齐。如果位置不正确,请使用下方的控制按键或键盘微调移液器直至完全对齐。", + "ensure_nozzle_position_odd": "请检查并确认{{tip_type}}位于{{item_location}}正上方并水平对齐。如果位置不正确,请点击移动移液器,然后微调移液器直至完全对齐。", "exit_screen_confirm_exit": "不保存耗材校准数据并退出", "exit_screen_go_back": "返回耗材位置校准", "exit_screen_subtitle": "如果您现在退出,所有耗材校准数据都将不保留,且无法恢复。", @@ -35,9 +35,10 @@ "install_probe": "从存储位置取出校准探头,将探头的锁套旋钮按顺时针方向拧松。对准图示位置,将校准探头向上轻推并压到顶部,使探头在{{location}}移液器吸嘴上压紧。随后将锁套旋钮按逆时针方向拧紧,并轻拉确认是否固定稳妥。", "jog_controls_adjustment": "需要进行调整吗?", "jupyter_notebook": "Jupyter Notebook", + "labware": "耗材", "labware_display_location_text": "甲板板位{{slot}}", - "labware_offset_data": "耗材校准数据", "labware_offset": "耗材校准数据", + "labware_offset_data": "耗材校准数据", "labware_offsets_deleted_warning": "一旦开始耗材位置校准,之前创建的耗材校准数据将会丢失。", "labware_offsets_summary_labware": "耗材", "labware_offsets_summary_location": "位置", @@ -46,22 +47,21 @@ "labware_position_check_description": "耗材位置校准是一个引导式工作流程,为了提高实验中移液器的位置精确度,它会检查甲板上的每一个耗材位置。首先检查吸头盒,然后检查协议中使用到的其它所有耗材。", "labware_position_check_overview": "耗材位置校准概览", "labware_position_check_title": "耗材位置校准", - "labware_step_detail_labware_plural": "吸头应位于 {{labware_name}} 第一列正上方,居中对齐,并且与耗材顶部水平对齐。", "labware_step_detail_labware": "吸头应位于 {{labware_name}} 的A1孔正上方,居中对齐,并且与耗材顶部水平对齐。", + "labware_step_detail_labware_plural": "吸头应位于 {{labware_name}} 第一列正上方,居中对齐,并且与耗材顶部水平对齐。", "labware_step_detail_link": "查看如何判断移液器是否居中", "labware_step_detail_modal_heading": "如何判断移液器是否居中且水平对齐", + "labware_step_detail_modal_nozzle": "为确保移液器吸嘴完全居中,请额外从OT-2的另一侧进行检查。", "labware_step_detail_modal_nozzle_image_1_text": "从正前方看,似乎已经居中...", "labware_step_detail_modal_nozzle_image_2_nozzle_text": "移液器吸嘴未居中", "labware_step_detail_modal_nozzle_image_2_text": "...但从侧面看,需要调整", + "labware_step_detail_modal_nozzle_or_tip": "为确保吸嘴或吸头与耗材顶部水平对齐,请水平直视进行判断,或在吸嘴与吸头间插入一张纸片辅助判断。", "labware_step_detail_modal_nozzle_or_tip_image_1_text": "从俯视角度看,似乎是水平的...", "labware_step_detail_modal_nozzle_or_tip_image_2_nozzle_text": "移液器吸嘴不水平", "labware_step_detail_modal_nozzle_or_tip_image_2_text": "...但从水平高度看,需要调整", "labware_step_detail_modal_nozzle_or_tip_image_3_text": "如遇觉得难以判断,请在吸嘴与吸头之间放入一张常规纸张。当这张纸能刚好卡在两者之间时,可确认高度位置。", - "labware_step_detail_modal_nozzle_or_tip": "为确保吸嘴或吸头与耗材顶部水平对齐,请水平直视进行判断,或在吸嘴与吸头间插入一张纸片辅助判断。", - "labware_step_detail_modal_nozzle": "为确保移液器吸嘴完全居中,请额外从OT-2的另一侧进行检查。", - "labware_step_detail_tiprack_plural": "移液器吸嘴应位于{{tiprack_name}}第一列正上方并居中对齐,并且与吸头顶部水平对齐。", "labware_step_detail_tiprack": "移液器吸嘴应居中于{{tiprack_name}}的A1位置上方,并且与吸头顶部水平对齐。", - "labware": "耗材", + "labware_step_detail_tiprack_plural": "移液器吸嘴应位于{{tiprack_name}}第一列正上方并居中对齐,并且与吸头顶部水平对齐。", "learn_more": "了解更多", "location": "位置", "lpc_complete_summary_screen_heading": "耗材位置校准完成", @@ -73,9 +73,9 @@ "new_labware_offset_data": "新的耗材校准数据", "ninety_six_probe_location": "A1(左上角)", "no_labware_offsets": "无耗材校准数据", + "no_offset_data": "没有可用的校准数据", "no_offset_data_available": "没有可用的耗材校准数据", "no_offset_data_on_robot": "这轮运行中此工作站没有可用的耗材校准数据。", - "no_offset_data": "没有可用的校准数据", "offsets": "校准数据", "pick_up_tip_from_rack_in_location": "从位于{{location}}的吸头盒上拾取吸头", "picking_up_tip_title": "在板位{{slot}}拾取吸头", @@ -98,13 +98,13 @@ "robot_has_no_offsets_from_previous_runs": "耗材校准数据引用自之前运行的协议,以节省您的时间。如果本协议中的所有耗材已在之前的运行中检查过,这些数据将应用于本次运行。 您可以在后面的步骤中使用耗材位置校准添加新的偏移量。", "robot_has_offsets_from_previous_runs": "此工作站具有本协议中所用耗材的校准数据。如果您应用了这些校准数据,仍可通过耗材位置校准程序进行调整。", "robot_in_motion": "工作站正在运行,请远离。", - "run_labware_position_check": "运行耗材位置校准程序", "run": "运行", + "run_labware_position_check": "运行耗材位置校准程序", "secondary_pipette_tipracks_section": "使用{{secondary_mount}}移液器检查吸头盒", "see_how_offsets_work": "了解耗材校准的工作原理", + "slot": "板位{{slotName}}", "slot_location": "板位位置", "slot_name": "板位{{slotName}}", - "slot": "板位{{slotName}}", "start_position_check": "开始耗材位置校准程序,移至板位{{initial_labware_slot}}", "stored_offset_data": "应用已存储的耗材校准数据?", "stored_offsets_for_this_protocol": "适用于本协议的已存储耗材校准数据", diff --git a/app/src/assets/localization/zh/module_wizard_flows.json b/app/src/assets/localization/zh/module_wizard_flows.json index b1cd0b086d6..c2d6af09862 100644 --- a/app/src/assets/localization/zh/module_wizard_flows.json +++ b/app/src/assets/localization/zh/module_wizard_flows.json @@ -1,21 +1,21 @@ { "attach_probe": "将校准探头安装到移液器", "begin_calibration": "开始校准", - "calibrate_pipette": "在进行模块校准之前,请先校准移液器", "calibrate": "校准", + "calibrate_pipette": "在进行模块校准之前,请先校准移液器", + "calibration": "{{module}}校准", "calibration_adapter_heatershaker": "校准适配器", "calibration_adapter_temperature": "校准适配器", "calibration_adapter_thermocycler": "校准适配器", - "calibration_probe_touching_thermocycler": "校准探头将接触探测热循环仪的校准方块,以确定其确切位置", - "calibration_probe_touching": "校准探头将接触探测{{slotNumber}}板位中{{module}}的校准方块,以确定其确切位置", "calibration_probe": "从存储位置取出校准探头,将探头的锁套旋钮按顺时针方向拧松。对准图示位置,将校准探头向上轻推并压到顶部。随后将锁套旋钮按逆时针方向拧紧,并轻拉确认是否固定稳妥。", - "calibration": "{{module}}校准", + "calibration_probe_touching": "校准探头将接触探测{{slotNumber}}板位中{{module}}的校准方块,以确定其确切位置", + "calibration_probe_touching_thermocycler": "校准探头将接触探测热循环仪的校准方块,以确定其确切位置", "checking_firmware": "检查{{module}}固件", "complete_calibration": "完成校准", "confirm_location": "确认位置", "confirm_placement": "确认已放置", - "detach_probe_description": "解锁校准探头,将其从吸嘴上卸下并放回原储存位置。", "detach_probe": "卸下移液器校准探头", + "detach_probe_description": "解锁校准探头,将其从吸嘴上卸下并放回原储存位置。", "error_during_calibration": "校准过程中出现错误", "error_prepping_module": "模块校准出错", "exit": "退出", @@ -32,16 +32,16 @@ "move_gantry_to_front": "将龙门架移至前端", "next": "下一步", "pipette_probe": "移液器校准探头", + "place_flush": "将适配器水平放到模块上。", "place_flush_heater_shaker": "将适配器水平放到模块上。使用热震荡模块专用螺丝和 T10 Torx 螺丝刀将适配器固定到模块上。", "place_flush_thermocycler": "确保热循环仪盖子已打开,并将适配器水平放置到模块上,即通常放置pcr板的位置。", - "place_flush": "将适配器水平放到模块上。", "prepping_module": "正在准备{{module}}进行模块校准", "recalibrate": "重新校准", "select_location": "选择模块放置位置", "select_the_slot": "请在右侧的甲板图中选择放置了{{module}}的板位。请确认所选择位置的正确性以确保顺利完成校准。", "slot_unavailable": "板位不可用", - "stand_back_robot_in_motion": "工作站正在运行,请远离", "stand_back": "正在进行校准,请远离", + "stand_back_robot_in_motion": "工作站正在运行,请远离", "start_setup": "开始设置", "successfully_calibrated": "{{module}}已成功校准" } diff --git a/app/src/assets/localization/zh/pipette_wizard_flows.json b/app/src/assets/localization/zh/pipette_wizard_flows.json index 1e75fe6223d..a1bec2143bb 100644 --- a/app/src/assets/localization/zh/pipette_wizard_flows.json +++ b/app/src/assets/localization/zh/pipette_wizard_flows.json @@ -2,14 +2,14 @@ "align_the_connector": "对准对接孔,将移液器安装到工作站上。使用六角螺丝刀拧紧螺丝以固定移液器。然后手动检查其是否已完全固定。", "all_pipette_detached": "所有移液器已成功卸下", "are_you_sure_exit": "您确定要在完成{{flow}}之前退出吗?", - "attach_96_channel_plus_detach": "卸下{{pipetteName}}并安装96通道移液器", + "attach": "正在安装移液器", "attach_96_channel": "安装96通道移液器", - "attach_mounting_plate_instructions": "对准对接孔,将移液器安装到工作站上。为了准确对齐,您可能需要调整右侧移液器支架的位置。", + "attach_96_channel_plus_detach": "卸下{{pipetteName}}并安装96通道移液器", "attach_mounting_plate": "安装固定板", + "attach_mounting_plate_instructions": "对准对接孔,将移液器安装到工作站上。为了准确对齐,您可能需要调整右侧移液器支架的位置。", "attach_pip": "安装移液器", "attach_pipette": "安装{{mount}}移液器", "attach_probe": "安装校准探头", - "attach": "正在安装移液器", "backmost": "最后面的", "before_you_begin": "开始之前", "begin_calibration": "开始校准", @@ -25,6 +25,7 @@ "connect_and_secure_pipette": "连接并固定移液器", "continue": "继续", "critical_unskippable_step": "这是关键步骤,不应跳过", + "detach": "正在卸下移液器", "detach_96_attach_mount": "卸下96通道移液器并安装{{mount}}移液器", "detach_96_channel": "卸下96通道移液器", "detach_and_reattach": "卸下并重新安装移液器", @@ -32,14 +33,13 @@ "detach_mount_attach_96": "卸下{{mount}}移液器并安装96通道移液器", "detach_mounting_plate_instructions": "抓住板子,防止其掉落。拧开移液器固定板的销钉。", "detach_next_pipette": "卸下下一个移液器", - "detach_pipette_to_attach_96": "卸下{{pipetteName}}并安装96通道移液器", "detach_pipette": "卸下{{mount}}移液器", + "detach_pipette_to_attach_96": "卸下{{pipetteName}}并安装96通道移液器", "detach_pipettes_attach_96": "卸下移液器并安装96通道移液器", "detach_z_axis_screw_again": "在安装96通道移液器前,先拧开Z轴螺丝。", - "detach": "正在卸下移液器", "exit_cal": "退出校准", - "firmware_updating": "需要固件更新,仪器正在更新中...", "firmware_up_to_date": "已是最新版本。", + "firmware_updating": "需要固件更新,仪器正在更新中...", "gantry_empty_for_96_channel_success": "现在两个移液器支架都为空,您可以开始进行96通道移液器的安装。", "get_started_detach": "开始前,请移除甲板上的实验耗材,清理工作区以便卸载。同时准备好屏幕所示的所需设备。", "grab_screwdriver": "保持原位不动,用2.5毫米螺丝刀,按照引导动画所示拧紧螺丝。在继续操作前,手动测试移液器安装和固定情况。", @@ -67,11 +67,12 @@ "pipette_heavy": "96通道移液器较重({{weight}})。如有需要,可以请其他人员帮忙。", "please_install_correct_pip": "请改用{{pipetteName}}", "progress_will_be_lost": "{{flow}}的进度将会丢失", + "provided_with_robot": "随工作站提供。使用其他尺寸的工具可能会损坏机器的螺丝。", "reattach_carriage": "重新连接Z轴板", "recalibrate_pipette": "重新校准{{mount}}移液器", "remove_cal_probe": "移除校准探头", - "remove_labware_to_get_started": "开始前,请清除甲板上的实验耗材,清理工作区,以便校准。同时收集屏幕显示的所需设备。校准探头随工作站提供,应存放在工作站的右前方支柱上。", "remove_labware": "开始前,请清除甲板上的实验耗材,清理工作区,以便校准。同时收集屏幕显示的所需设备。校准探头随工作站提供,应存放在工作站的右前方支柱上。", + "remove_labware_to_get_started": "开始前,请清除甲板上的实验耗材,清理工作区,以便校准。同时收集屏幕显示的所需设备。校准探头随工作站提供,应存放在工作站的右前方支柱上。", "remove_probe": "拧松校准探头,将其从喷嘴上拆下,并放回存储位置。", "replace_pipette": "更换{{mount}}移液器", "return_probe_error": "退出前,请将校准探头放回其存放位置。 {{error}}", diff --git a/app/src/assets/localization/zh/protocol_command_text.json b/app/src/assets/localization/zh/protocol_command_text.json index 9d976c2bc88..4a33dac6a39 100644 --- a/app/src/assets/localization/zh/protocol_command_text.json +++ b/app/src/assets/localization/zh/protocol_command_text.json @@ -1,11 +1,18 @@ { + "absorbance_reader_close_lid": "正在关闭吸光度读数器盖子", + "absorbance_reader_initialize": "正在初始化吸光度读数器,以便在{{wavelengths}}波长下执行{{mode}}测量", + "absorbance_reader_open_lid": "正在打开吸光度读数器盖子", + "absorbance_reader_read": "正在吸光度读数器中读取微孔板", "adapter_in_mod_in_slot": "{{adapter}}在{{slot}}的{{module}}上", "adapter_in_slot": "{{adapter}}在{{slot}}上", + "air_gap_in_place": "正在形成 {{volume}} µL 的空气间隙", + "all_nozzles": "所有移液喷嘴", "aspirate": "从{{labware_location}}的{{labware}}的{{well_name}}孔中以{{flow_rate}}µL/秒的速度吸液{{volume}}µL", "aspirate_in_place": "在原位以{{flow_rate}}µL/秒的速度吸液{{volume}}µL", "blowout": "在{{labware_location}}的{{labware}}的{{well_name}}孔中以{{flow_rate}}µL/秒的速度排空", "blowout_in_place": "在原位以{{flow_rate}}µL/秒的速度排空", "closing_tc_lid": "关闭热循环仪热盖", + "column_layout": "列布局", "comment": "解释", "configure_for_volume": "配置{{pipette}}以吸液{{volume}}µL", "configure_nozzle_layout": "配置{{pipette}}以使用{{amount}}个通道", @@ -18,63 +25,79 @@ "degrees_c": "{{temp}}°C", "detect_liquid_presence": "正在检测{{labware}}在{{labware_location}}中的{{well_name}}孔中的液体存在", "disengaging_magnetic_module": "下降磁力架模块", - "dispense_push_out": "以{{flow_rate}}µL/秒的速度将{{volume}}µL 排液至{{labware_location}}的{{labware}}的{{well_name}}孔中,并推出{{push_out_volume}}µL", "dispense": "以{{flow_rate}}µL/秒的速度将{{volume}}µL 排液至{{labware_location}}的{{labware}}的{{well_name}}孔中", "dispense_in_place": "在原位以{{flow_rate}}µL/秒的速度排液{{volume}}µL", + "dispense_push_out": "以{{flow_rate}}µL/秒的速度将{{volume}}µL 排液至{{labware_location}}的{{labware}}的{{well_name}}孔中,并推出{{push_out_volume}}µL", "drop_tip": "将吸头丢入{{labware}}的{{well_name}}孔中", "drop_tip_in_place": "在原位丢弃吸头", + "dropping_tip_in_trash": "将吸头丢入{{trash}}", "engaging_magnetic_module": "抬升磁力架模块", "fixed_trash": "垃圾桶", "home_gantry": "复位所有龙门架、移液器和柱塞轴", + "in_location": "在{{location}}", "latching_hs_latch": "在热震荡模块上锁定实验耗材", "left": "左", + "load_labware_to_display_location": "{{display_location}}加载{{labware}}", "load_liquids_info_protocol_setup": "将{{liquid}}加载到{{labware}}中", "load_module_protocol_setup": "在甲板槽{{slot_name}}中加载模块{{module}}", "load_pipette_protocol_setup": "在{{mount_name}}支架上加载{{pipette_name}}", - "module_in_slot_plural": "{{module}}", "module_in_slot": "{{module}}在{{slot_name}}号板位", + "module_in_slot_plural": "{{module}}", + "move_labware": "移动实验耗材", "move_labware_manually": "手动将{{labware}}从{{old_location}}移动到{{new_location}}", "move_labware_on": "在{{robot_name}}上移动实验耗材", "move_labware_using_gripper": "使用转板抓手将{{labware}}从{{old_location}}移动到{{new_location}}", - "move_labware": "移动实验耗材", "move_relative": "沿{{axis}}轴移动{{distance}}毫米", + "move_to_addressable_area": "移动到{{addressable_area}}", + "move_to_addressable_area_drop_tip": "移动到{{addressable_area}}", "move_to_coordinates": "移动到 (X:{{x}}, Y:{{y}}, Z:{{z}})", "move_to_slot": "移动到{{slot_name}}号板位", "move_to_well": "移动到{{labware_location}}的{{labware}}的{{well_name}}孔", - "move_to_addressable_area": "移动到{{addressable_area}}", - "move_to_addressable_area_drop_tip": "移动到{{addressable_area}}", + "multiple": "多个", "notes": "备注", "off_deck": "甲板外", "offdeck": "甲板外", + "on_location": "在{{location}}", "opening_tc_lid": "打开热循环仪热盖", - "pause_on": "在{{robot_name}}上暂停", + "partial_layout": "部分布局", "pause": "暂停", + "pause_on": "在{{robot_name}}上暂停", "pickup_tip": "从{{labware_location}}的{{labware}}的{{well_range}}孔中拾取吸头", "prepare_to_aspirate": "准备使用{{pipette}}吸液", + "pressurizing_to_dispense": "加压移液器进行分配 {{volume}} µL 距离树脂尖端 {{flow_rate}} 微升/秒", "reloading_labware": "正在重新加载{{labware}}", "return_tip": "将吸头返回到{{labware_location}}的{{labware}}的{{well_name}}孔中", "right": "右", + "row_layout": "行布局", "save_position": "保存位置", + "sealing_to_location": "密封至 {{labware}} 在 {{location}}", "set_and_await_hs_shake": "设置热震荡模块以{{rpm}}rpm 震动并等待达到该转速", "setting_hs_temp": "将热震荡模块的目标温度设置为{{temp}}", "setting_temperature_module_temp": "将温控模块设置为{{temp}}(四舍五入到最接近的整数)", "setting_thermocycler_block_temp": "将热循环仪加热块温度设置为{{temp}},并在达到目标后保持{{hold_time_seconds}}秒", "setting_thermocycler_lid_temp": "将热循环仪热盖温度设置为{{temp}}", + "single": "单个", + "single_nozzle_layout": "单个移液喷嘴布局", "slot": "板位{{slot_name}}", "target_temperature": "目标温度", "tc_awaiting_for_duration": "等待热循环仪程序完成", "tc_run_profile_steps": "温度:{{celsius}}°C,时间:{{seconds}}秒", + "tc_starting_extended_profile": "运行热循环仪程序,共有{{elementCount}}个步骤和循环:", + "tc_starting_extended_profile_cycle": "以下步骤重复{{repetitions}}次:", "tc_starting_profile": "热循环仪开始进行由以下步骤组成的{{repetitions}}次循环:", - "trash_bin_in_slot": "垃圾桶在{{slot_name}}", "touch_tip": "吸头接触内壁", + "trash_bin": "垃圾桶", + "trash_bin_in_slot": "垃圾桶在{{slot_name}}", "turning_rail_lights_off": "正在关闭导轨灯", "turning_rail_lights_on": "正在打开导轨灯", "unlatching_hs_latch": "解锁热震荡模块上的实验耗材", + "unsealing_from_location": "解除封印 {{labware}} 在 {{location}}", "wait_for_duration": "暂停{{seconds}}秒。{{message}}", "wait_for_resume": "暂停协议", "waiting_for_hs_to_reach": "等待热震荡模块达到目标温度", "waiting_for_tc_block_to_reach": "等待热循环仪加热块达到目标温度并保持指定时间", "waiting_for_tc_lid_to_reach": "等待热循环仪热盖达到目标温度", "waiting_to_reach_temp_module": "等待温控模块达到{{temp}}", - "waste_chute": "外置垃圾槽" + "waste_chute": "外置垃圾槽", + "with_reference_of": "以{{wavelength}} nm为基准" } diff --git a/app/src/assets/localization/zh/protocol_details.json b/app/src/assets/localization/zh/protocol_details.json index 22f4c0566fc..e52c38dc224 100644 --- a/app/src/assets/localization/zh/protocol_details.json +++ b/app/src/assets/localization/zh/protocol_details.json @@ -10,32 +10,32 @@ "connected": "已连接", "connection_status": "连接状态", "creation_method": "创建方法", - "csv_file_type_required": "需要CSV文件类型", "csv_file": "CSV文件", - "csv_required": "该协议需要CSV文件才能继续。", + "csv_file_type_required": "需要CSV文件类型", + "deck": "甲板", "deck_view": "甲板视图", "default_value": "默认值", - "delete_protocol_perm": "{{name}}及其运行历史将被永久删除。", "delete_protocol": "删除协议", + "delete_protocol_perm": "{{name}}及其运行历史将被永久删除。", "delete_this_protocol": "删除此协议?", "description": "描述", "extension_mount": "扩展安装支架", "file_required": "需要文件", "go_to_labware_definition": "转到实验耗材定义", "go_to_timeline": "转到时间线", - "gripper_pick_up_count_description": "使用转板抓手移动单个耗材的指令。", "gripper_pick_up_count": "转板次数", + "gripper_pick_up_count_description": "使用转板抓手移动单个耗材的指令。", "hardware": "硬件", - "labware_name": "耗材名称", "labware": "耗材", + "labware_name": "耗材名称", "last_analyzed": "上一次分析", "last_updated": "上一次更新", "left_and_right_mounts": "左+右安装架", "left_mount": "左移液器安装位", "left_right": "左,右", "liquid_name": "液体名称", - "liquids_not_in_protocol": "此协议未指定任何液体", "liquids": "液体", + "liquids_not_in_protocol": "此协议未指定任何液体", "listed_values_are_view_only": "列出的值仅供查看", "location": "位置", "modules": "模块", @@ -51,16 +51,16 @@ "num_choices": "{{num}}个选择", "num_options": "{{num}}个选项", "off": "关闭", - "on_off": "开,关", "on": "开启", + "on_off": "开,关", "org_or_author": "组织/作者", "parameters": "参数", - "pipette_aspirate_count_description": "每个移液器的单个吸液指令。", "pipette_aspirate_count": "{{pipette}}吸液次数", - "pipette_dispense_count_description": "每个移液器的单个排液指令。", + "pipette_aspirate_count_description": "每个移液器的单个吸液指令。", "pipette_dispense_count": "{{pipette}}分液次数", - "pipette_pick_up_count_description": "每个移液器的单个拾取吸头指令。", + "pipette_dispense_count_description": "每个移液器的单个排液指令。", "pipette_pick_up_count": "{{pipette}}拾取吸头次数", + "pipette_pick_up_count_description": "每个移液器的单个拾取吸头指令。", "proceed_to_setup": "继续进行设置", "protocol_designer_version": "在线协议编辑器{{version}}", "protocol_failed_app_analysis": "该协议在应用程序内分析失败。它可能在没有自定义软件配置的工作站上无法使用。", @@ -74,24 +74,25 @@ "requires_upload": "需要上传", "restore_defaults": "恢复默认值", "right_mount": "右移液器安装位", + "robot": "工作站", "robot_configuration": "工作站配置", - "robot_is_busy_with_protocol": "{{robotName}}正在运行{{protocolName}},状态为{{runStatus}}。是否要清除并继续?", "robot_is_busy": "{{robotName}}正在工作", - "robot": "工作站", + "robot_is_busy_with_protocol": "{{robotName}}正在运行{{protocolName}},状态为{{runStatus}}。是否要清除并继续?", "run_protocol": "运行协议", "select_parameters_for_robot": "选择{{robot_name}}的参数", "send": "发送", "sending": "发送中", "show_in_folder": "在文件夹中显示", "slot": "{{slotName}}号板位", - "start_setup_customize_values": "开始设置以自定义值", "start_setup": "开始设置", + "start_setup_customize_values": "开始设置以自定义值", "successfully_sent": "发送成功", + "summary": "概括", "total_volume": "总体积", - "unavailable_or_busy_robot_not_listed_plural": "{{count}}台不可用或工作中的工作站未列出。", "unavailable_or_busy_robot_not_listed": "{{count}}台不可用或工作中的工作站未列出。", - "unavailable_robot_not_listed_plural": "{{count}}台不可用的工作站未列出。", + "unavailable_or_busy_robot_not_listed_plural": "{{count}}台不可用或工作中的工作站未列出。", "unavailable_robot_not_listed": "{{count}}台不可用的工作站未列出。.", + "unavailable_robot_not_listed_plural": "{{count}}台不可用的工作站未列出。", "unsuccessfully_sent": "发送失败", "value_out_of_range": "值必须在{{min}}-{{max}}之间", "view_run_details": "查看运行详情", diff --git a/app/src/assets/localization/zh/protocol_info.json b/app/src/assets/localization/zh/protocol_info.json index 073b4fd6319..0654a1134cd 100644 --- a/app/src/assets/localization/zh/protocol_info.json +++ b/app/src/assets/localization/zh/protocol_info.json @@ -10,6 +10,7 @@ "creation_method": "创建方法", "custom_labware_not_supported": "工作站不支持自定义耗材", "date_added": "添加日期", + "date_added_date": "添加日期:{{date}}", "delete_protocol": "删除协议", "description": "描述", "drag_file_here": "将协议文件拖放到此处", @@ -22,9 +23,9 @@ "exit_modal_heading": "确认关闭协议", "failed_analysis": "分析失败", "get_labware_offset_data": "获取耗材校准数据", + "import": "导入", "import_a_file": "导入协议以开始", "import_new_protocol": "导入协议", - "import": "导入", "incompatible_file_type": "不兼容的文件类型", "instrument_cal_data_title": "校准数据", "instrument_not_attached": "未连接", @@ -37,10 +38,11 @@ "labware_offset_data_title": "耗材校准数据", "labware_offsets_info": "{{number}}组耗材校准数据", "labware_position_check_complete_toast_no_offsets": "耗材位置校准完成。无新建的耗材校准数据。", - "labware_position_check_complete_toast_with_offsets_plural": "耗材位置校准完成。新建了{{count}}组耗材校准数据。", "labware_position_check_complete_toast_with_offsets": "耗材位置校准完成。创建了{{count}}组耗材校准数据。", + "labware_position_check_complete_toast_with_offsets_plural": "耗材位置校准完成。新建了{{count}}组耗材校准数据。", "labware_title": "所需耗材", "last_run": "上次运行", + "last_run_time": "上次运行时间:{{time}}", "last_updated": "最近更新", "launch_protocol_designer": "打开在线协议编辑器", "manual_steps_learn_more": "了解更多关于手动步骤的信息", diff --git a/app/src/assets/localization/zh/protocol_list.json b/app/src/assets/localization/zh/protocol_list.json index 9c2b3be0f59..bb931b9e39d 100644 --- a/app/src/assets/localization/zh/protocol_list.json +++ b/app/src/assets/localization/zh/protocol_list.json @@ -16,8 +16,8 @@ "reanalyze_to_view": " 重新分析 协议", "right_mount": "右移液器安装位", "robot": "工作站", - "send_to_robot_overflow": "发送到{{robot_display_name}}", "send_to_robot": "将协议发送到{{robot_display_name}}", + "send_to_robot_overflow": "发送到{{robot_display_name}}", "show_in_folder": "在文件夹中显示", "start_setup": "开始设置", "this_protocol_will_be_trashed": "该协议将被移至此计算机的回收站,可能无法恢复。", diff --git a/app/src/assets/localization/zh/protocol_setup.json b/app/src/assets/localization/zh/protocol_setup.json index 40a5c8c00f9..990fe911f5f 100644 --- a/app/src/assets/localization/zh/protocol_setup.json +++ b/app/src/assets/localization/zh/protocol_setup.json @@ -1,8 +1,8 @@ { "96_mount": "左+右移液器安装位", "action_needed": "需要操作", - "adapter_slot_location_module": "{{slotName}}号板位,{{adapterName}}在{{moduleName}}上", "adapter_slot_location": "{{slotName}}号板位,{{adapterName}}", + "adapter_slot_location_module": "{{slotName}}号板位,{{adapterName}}在{{moduleName}}上", "add_fixture": "将{{fixtureName}}添加到{{locationName}}", "add_this_deck_hardware": "将此硬件添加到您的甲板配置中。它将在协议分析期间被引用。", "add_to_slot": "添加到{{slotName}}号板位", @@ -12,17 +12,18 @@ "applied_labware_offset_data": "已应用的实验耗材偏移数据", "applied_labware_offsets": "已应用的实验耗材偏移", "are_you_sure_you_want_to_proceed": "您确定要继续运行吗?", - "attach_gripper_failure_reason": "连接所需的转板抓手以继续", + "attach": "连接", "attach_gripper": "连接转板抓手", + "attach_gripper_failure_reason": "连接所需的转板抓手以继续", "attach_module": "校准前连接模块", "attach_pipette_before_module_calibration": "在进行模块校准前连接移液器", "attach_pipette_calibration": "连接移液器以查看校准信息", "attach_pipette_cta": "连接移液器", "attach_pipette_failure_reason": "连接所需的移液器以继续", "attach_pipette_tip_length_calibration": "连接移液器以查看吸头长度校准信息", - "attach": "连接", "back_to_top": "回到顶部", "cal_all_pip": "首先校准移液器", + "calibrate": "校准", "calibrate_deck_failure_reason": "校准甲板以继续", "calibrate_deck_to_proceed_to_pipette_calibration": "校准甲板以继续进行移液器校准", "calibrate_deck_to_proceed_to_tip_length_calibration": "校准甲板以继续进行吸头长度校准", @@ -32,16 +33,15 @@ "calibrate_pipette_before_module_calibration": "在进行模块校准前校准移液器", "calibrate_pipette_failure_reason": "校准所需的移液器以继续", "calibrate_tiprack_failure_reason": "校准所需的吸头长度以继续", - "calibrate": "校准", "calibrated": "已校准", + "calibration": "校准", "calibration_data_not_available": "一旦运行开始,校准数据不可用", "calibration_needed": "需要校准", "calibration_ready": "校准就绪", + "calibration_required": "需要校准", "calibration_required_attach_pipette_first": "需要校准,请先连接移液器", "calibration_required_calibrate_pipette_first": "需要校准,请先校准移液器", - "calibration_required": "需要校准", "calibration_status": "校准状态", - "calibration": "校准", "cancel_and_restart_to_edit": "取消运行并重新启动设置以进行编辑", "choose_csv_file": "选择CSV文件", "choose_enum": "选择{{displayName}}", @@ -62,21 +62,21 @@ "connect_modules_for_controls": "连接模块以查看控制", "connection_info_not_available": "一旦运行开始,连接信息不可用", "connection_status": "连接状态", + "csv_file": "CSV 文件", "csv_files_on_robot": "工作站上的CSV文件", "csv_files_on_usb": "USB上的CSV文件", - "csv_file": "CSV 文件", "currently_configured": "当前已配置", "currently_unavailable": "当前不可用", "custom_values": "自定义值", + "deck_cal_description": "这测量了甲板的 X 和 Y 值相对于门架的值。甲板校准是吸头长度校准和移液器偏移校准的基础。", "deck_cal_description_bullet_1": "在新工作站设置期间执行甲板校准。", "deck_cal_description_bullet_2": "如果您搬迁了工作站,请重新进行甲板校准。", - "deck_cal_description": "这测量了甲板的 X 和 Y 值相对于门架的值。甲板校准是吸头长度校准和移液器偏移校准的基础。", "deck_calibration_title": "甲板校准(Deck Calibration)", - "deck_conflict_info_thermocycler": "通过移除位置 A1 和 B1 中的固定装置来更新甲板配置。从甲板配置中移除对应装置或更新协议。", - "deck_conflict_info": "通过移除位置 {{cutout}} 中的 {{currentFixture}} 来更新甲板配置。从甲板配置中移除对应装置或更新协议。", "deck_conflict": "甲板位置冲突", - "deck_hardware_ready": "甲板硬件准备", + "deck_conflict_info": "通过移除位置 {{cutout}} 中的 {{currentFixture}} 来更新甲板配置。从甲板配置中移除对应装置或更新协议。", + "deck_conflict_info_thermocycler": "通过移除位置 A1 和 B1 中的固定装置来更新甲板配置。从甲板配置中移除对应装置或更新协议。", "deck_hardware": "甲板硬件", + "deck_hardware_ready": "甲板硬件准备", "deck_map": "甲板布局图", "default_values": "默认值", "download_files": "下载文件", @@ -86,62 +86,63 @@ "extra_attention_warning_title": "在继续运行前固定耗材和模块", "extra_module_attached": "附加额外模块", "feedback_form_link": "请告诉我们", - "fixture_name": "装置", "fixture": "装置", - "fixtures_connected_plural": "已连接{{count}}个装置", + "fixture_name": "装置", "fixtures_connected": "已连接{{count}}个装置", + "fixtures_connected_plural": "已连接{{count}}个装置", "get_labware_offset_data": "获取耗材校准数据", "hardware_missing": "缺少硬件", "heater_shaker_extra_attention": "使用闩锁控制,便于放置耗材。", "heater_shaker_labware_list_view": "要添加耗材,请使用切换键来控制闩锁", "how_offset_data_works": "耗材校准数据如何工作", "individiual_well_volume": "单个孔体积", - "initial_liquids_num_plural": "{{count}}种初始液体", "initial_liquids_num": "{{count}}种初始液体", + "initial_liquids_num_plural": "{{count}}种初始液体", "initial_location": "初始位置", + "install_modules": "安装所需的模块。", "install_modules_and_fixtures": "安装并校准所需的模块。安装所需的装置。", "install_modules_plural": "安装所需的模块。", - "install_modules": "安装所需的模块。", - "instrument_calibrations_missing_plural": "缺少{{count}}个校准", "instrument_calibrations_missing": "缺少{{count}}个校准", - "instruments_connected_plural": "已连接{{count}}个硬件", - "instruments_connected": "已连接{{count}}个硬件", + "instrument_calibrations_missing_plural": "缺少{{count}}个校准", "instruments": "硬件", - "labware_latch_instructions": "使用闩锁控制,便于放置耗材。", + "instruments_connected": "已连接{{count}}个硬件", + "instruments_connected_plural": "已连接{{count}}个硬件", + "labware": "耗材", "labware_latch": "耗材闩锁", + "labware_latch_instructions": "使用闩锁控制,便于放置耗材。", "labware_location": "耗材位置", "labware_name": "耗材名称", "labware_placement": "实验耗材放置", + "labware_position_check": "耗材位置校准", + "labware_position_check_not_available": "运行开始后,耗材位置校准不可用", "labware_position_check_not_available_analyzing_on_robot": "在工作站上分析协议时,耗材位置校准不可用", "labware_position_check_not_available_empty_protocol": "耗材位置校准需要协议加载耗材和移液器", - "labware_position_check_not_available": "运行开始后,耗材位置校准不可用", "labware_position_check_step_description": "建议的工作流程可帮助您验证每个耗材在甲板上的位置。", "labware_position_check_step_title": "耗材位置校准", "labware_position_check_text": "耗材位置校准流程可帮助您验证甲板上每个耗材的位置。在此位置校准过程中,您可以创建耗材校准数据,以调整工作站在 X、Y 和 Z 方向上的移动。", - "labware_position_check": "耗材位置校准", + "labware_quantity": "数量:{{quantity}}", "labware_setup_step_description": "准备好以下耗材和完整的吸头盒。若不进行耗材位置校准直接运行协议,请将耗材放置在其初始位置并固定。", "labware_setup_step_title": "耗材", - "labware": "耗材", "last_calibrated": "最后校准:{{date}}", "learn_how_it_works": "了解它的工作原理", + "learn_more": "了解更多", "learn_more_about_offset_data": "了解更多关于耗材校准数据的信息", "learn_more_about_robot_cal_link": "了解更多关于工作站校准的信息", - "learn_more": "了解更多", "liquid_information": "液体信息", "liquid_name": "液体名称", "liquid_setup_step_description": "查看液体的起始位置和体积", "liquid_setup_step_title": "液体", + "liquids": "液体", "liquids_confirmed": "液体已确认", "liquids_not_in_setup": "此协议未使用液体", "liquids_not_in_the_protocol": "此协议未指定液体。", "liquids_ready": "液体准备", - "liquids": "液体", "list_view": "列表视图", "loading_data": "加载数据...", "loading_labware_offsets": "加载耗材校准数据", "loading_protocol_details": "加载详情...", - "location_conflict": "位置冲突", "location": "位置", + "location_conflict": "位置冲突", "lpc_and_offset_data_title": "耗材位置校准和耗材校准数据", "lpc_disabled_calibration_not_complete": "确保工作站校准完成后再运行耗材位置校准", "lpc_disabled_modules_and_calibration_not_complete": "确保工作站校准完成并且所有模块已连接后再运行耗材位置校准", @@ -149,36 +150,37 @@ "lpc_disabled_no_tipracks_loaded": "耗材位置校准需要在协议中加载一个吸头盒", "lpc_disabled_no_tipracks_used": "耗材位置校准要求协议中至少有一个吸头可供使用", "map_view": "布局视图", + "missing": "缺少", "missing_gripper": "缺少转板抓手", "missing_instruments": "缺少{{count}}个", - "missing_pipettes_plural": "缺少{{count}}个移液器", "missing_pipettes": "缺少{{count}}个移液器", - "missing": "缺少", + "missing_pipettes_plural": "缺少{{count}}个移液器", "modal_instructions_title": "{{moduleName}}设置说明", + "module": "模块", "module_connected": "已连接", "module_disconnected": "未连接", "module_instructions_link": "{{moduleName}}设置说明", + "module_instructions_manual": "要了解有关设置模块的分步说明,请查阅包装盒内的快速入门指南。您也可以点击下面的链接或扫描二维码,查看模块使用说明书。", "module_mismatch_body": "检查连接到该工作站的模块型号是否正确", "module_name": "模块", "module_not_connected": "未连接", "module_setup_step_ready": "校准准备", "module_setup_step_title": "甲板硬件", "module_slot_location": "{{slotName}}号板位,{{moduleName}}", - "module": "模块", - "modules_connected_plural": "连接了{{count}}个模块", + "modules": "模块", "modules_connected": "连接了{{count}}个模块", + "modules_connected_plural": "连接了{{count}}个模块", "modules_setup_step_title": "模块设置", - "modules": "模块", - "mount_title": "{{mount}}安装支架:", "mount": "{{mount}}安装支架", + "mount_title": "{{mount}}安装支架:", "multiple_fixtures_missing": "缺少{{count}}个装置", + "multiple_modules": "相同类型的多个模块", "multiple_modules_example": "您的协议包含两个温控模块。连接到左侧第一个端口的温控模块对应协议中的第一个温控模块,连接到下一个端口的温控模块对应协议中的第二个温控模块。如果使用集线器,遵循相同的端口排序逻辑。", "multiple_modules_explanation": "在协议中使用多个相同类型的模块时,首先需要将协议中第一个模块连接到工作站编号最小的USB端口,然后以相同方式连接其他模块。", "multiple_modules_help_link_title": "查看如何设置相同类型的多个模块", "multiple_modules_learn_more": "了解更多关于使用相同类型的多个模块的信息", "multiple_modules_missing_plural": "缺少{{count}}个模块", "multiple_modules_modal": "设置相同类型的多个模块", - "multiple_modules": "相同类型的多个模块", "multiple_of_most_modules": "通过以特定顺序连接和加载模块,可以在单个Python协议中使用多种模块类型。无论模块占用哪个甲板板位,工作站都将首先初始化连接到最小编号端口的匹配模块,。", "must_have_labware_and_pip": "协议中必须加载耗材和移液器", "n_a": "不可用", @@ -191,8 +193,8 @@ "no_modules_or_fixtures": "该协议中未指定任何模块或装置。", "no_modules_specified": "该协议中未指定任何模块。", "no_modules_used_in_this_protocol": "该协议中未使用硬件", - "no_parameters_specified_in_protocol": "协议中未指定任何参数", "no_parameters_specified": "未指定参数", + "no_parameters_specified_in_protocol": "协议中未指定任何参数", "no_tiprack_loaded": "协议中必须加载一个吸头盒", "no_tiprack_used": "协议中必须拾取一个吸头", "no_usb_connection_required": "无需USB连接", @@ -200,33 +202,32 @@ "no_usb_required": "无需USB", "not_calibrated": "尚未校准", "not_configured": "未配置", - "off_deck": "甲板外", "off": "关闭", + "off_deck": "甲板外", "offset_data": "偏移校准数据", - "offsets_applied_plural": "应用了{{count}}个偏移校准数据", "offsets_applied": "应用了{{count}}个偏移校准数据", "offsets_confirmed": "偏移校准数据已确认", "offsets_ready": "偏移校准数据准备", - "on_adapter_in_mod": "在{{moduleName}}中的{{adapterName}}上", + "on": "开启", + "on-deck_labware": "{{count}}个在甲板上的耗材", "on_adapter": "在{{adapterName}}上", + "on_adapter_in_mod": "在{{moduleName}}中的{{adapterName}}上", "on_deck": "在甲板上", - "on-deck_labware": "{{count}}个在甲板上的耗材", - "on": "开启", "opening": "打开中...", "parameters": "参数", "pipette_mismatch": "移液器型号不匹配。", "pipette_missing": "移液器缺失", + "pipette_offset_cal": "移液器偏移校准", + "pipette_offset_cal_description": "这会测量移液器相对于移液器安装支架和甲板的X、Y和Z值。移液器偏移校准依赖于甲板校准和吸头长度校准。 ", "pipette_offset_cal_description_bullet_1": "首次将移液器连接到新安装支架时执行移液器偏移校准。", "pipette_offset_cal_description_bullet_2": "在执行甲板校准后重新进行移液器偏移校准。", "pipette_offset_cal_description_bullet_3": "对用于校准移液器的吸头执行吸头长度校准后,重新进行移液器偏移校准。", - "pipette_offset_cal_description": "这会测量移液器相对于移液器安装支架和甲板的X、Y和Z值。移液器偏移校准依赖于甲板校准和吸头长度校准。 ", - "pipette_offset_cal": "移液器偏移校准", + "placement": "放置", "placements_confirmed": "位置已确认", "placements_ready": "位置准备", - "placement": "放置", "plug_in_module_to_configure": "插入{{module}}以将其添加到板位", - "plug_in_required_module_plural": "插入并启动所需模块以继续", "plug_in_required_module": "插入并启动所需模块以继续", + "plug_in_required_module_plural": "插入并启动所需模块以继续", "prepare_to_run": "准备运行", "proceed_to_labware_position_check": "继续进行耗材位置校准", "proceed_to_labware_setup_step": "继续进行耗材设置", @@ -248,34 +249,34 @@ "recalibrating_not_available": "无法重新进行吸头长度校准和耗材位置校准。", "recalibrating_tip_length_not_available": "运行开始后无法重新校准吸头长度", "recommended": "推荐", - "required_instrument_calibrations": "所需的硬件校准", "required": "必需", + "required_instrument_calibrations": "所需的硬件校准", "required_tip_racks_title": "所需的吸头长度校准", - "reset_parameter_values_body": "这将丢弃您所做的任何更改。所有参数将恢复默认值。", "reset_parameter_values": "重置参数值?", + "reset_parameter_values_body": "这将丢弃您所做的任何更改。所有参数将恢复默认值。", "reset_setup": "重新开始设置以进行编辑", "reset_values": "重置值", "resolve": "解决", - "restart_setup_and_try": "重新开始设置并尝试使用不同的参数值。", "restart_setup": "重新开始设置", + "restart_setup_and_try": "重新开始设置并尝试使用不同的参数值。", "restore_default": "恢复默认值", "restore_defaults": "恢复默认值", "robot_cal_description": "工作站校准用于确定其相对于甲板的位置。良好的工作站校准对于成功运行协议至关重要。工作站校准包括3个部分:甲板校准、吸头长度校准和移液器偏移校准。", "robot_cal_help_title": "工作站校准的工作原理", - "robot_calibration_step_description_pipettes_only": "查看该协议所需的硬件和校准。", "robot_calibration_step_description": "查看该协议所需的移液器和吸头长度校准。", + "robot_calibration_step_description_pipettes_only": "查看该协议所需的硬件和校准。", "robot_calibration_step_ready": "校准准备", "robot_calibration_step_title": "硬件", + "run": "运行", "run_disabled_calibration_not_complete": "确保工作站校准完成后再继续运行", "run_disabled_modules_and_calibration_not_complete": "确保工作站校准完成并且所有模块已连接后再继续运行", "run_disabled_modules_not_connected": "确保所有模块已连接后再继续运行", - "run_labware_position_check_to_get_offsets": "运行实验室位置检查以获取实验室偏移数据。", "run_labware_position_check": "运行耗材位置校准", + "run_labware_position_check_to_get_offsets": "运行实验室位置检查以获取实验室偏移数据。", "run_never_started": "运行未开始", - "run": "运行", + "secure": "固定", "secure_labware_instructions": "固定耗材说明", "secure_labware_modal": "将耗材固定到{{name}}", - "secure": "固定", "setup_for_run": "运行设置", "setup_instructions": "设置说明", "setup_is_view_only": "运行开始后设置仅供查看", @@ -287,22 +288,22 @@ "step": "步骤{{index}}", "there_are_no_unconfigured_modules": "没有连接{{module}}。请连接一个模块并放置在{{slot}}号板位中。", "there_are_other_configured_modules": "已有一个{{module}}配置在不同的板位中。退出运行设置,并更新甲板配置以转到已连接的模块。或连接另一个{{module}}继续设置。", - "tip_length_cal_description_bullet": "对移液器上将会用到的每种类型的吸头执行吸头长度校准。", "tip_length_cal_description": "这将测量吸头底部与移液器喷嘴之间的Z轴距离。如果对用于校准移液器的吸头重新进行吸头长度校准,也需要重新进行移液器偏移校准。", + "tip_length_cal_description_bullet": "对移液器上将会用到的每种类型的吸头执行吸头长度校准。", "tip_length_cal_title": "吸头长度校准", "tip_length_calibration": "吸头长度校准", "total_liquid_volume": "总体积", - "update_deck_config": "更新甲板配置", "update_deck": "更新甲板", + "update_deck_config": "更新甲板配置", "update_offsets": "更新偏移校准数据", "updated": "已更新", "usb_connected_no_port_info": "USB端口已连接", "usb_drive_notification": "在运行开始前,请保持USB处于连接状态", "usb_port_connected": "USB端口{{port}}", "usb_port_number": "USB-{{port}}", - "value_out_of_range_generic": "值必须在范围内", - "value_out_of_range": "值必须在{{min}}-{{max}}之间", "value": "值", + "value_out_of_range": "值必须在{{min}}-{{max}}之间", + "value_out_of_range_generic": "值必须在范围内", "values_are_view_only": "值仅供查看", "variable_well_amount": "可变孔数", "view_current_offsets": "查看当前偏移量", diff --git a/app/src/assets/localization/zh/quick_transfer.json b/app/src/assets/localization/zh/quick_transfer.json index 5ecce11bb9c..159b627daf4 100644 --- a/app/src/assets/localization/zh/quick_transfer.json +++ b/app/src/assets/localization/zh/quick_transfer.json @@ -1,23 +1,25 @@ { "a_way_to_move_liquid": "一种将单一液体从一个实验耗材移动到另一个实验耗材的方法。", - "add_or_remove_columns": "添加或移除列", "add_or_remove": "添加或移除", + "add_or_remove_columns": "添加或移除列", "advanced_setting_disabled": "此移液的高级设置已禁用", "advanced_settings": "高级设置", + "air_gap": "空气间隙", + "air_gap_after_aspirating": "吸液后形成空气间隙", "air_gap_before_dispensing": "在分液前设置空气间隙", "air_gap_capacity_error": "移液器空间已满,无法添加空气间隙。", "air_gap_value": "{{volume}} µL", "air_gap_volume_µL": "空气间隙体积(µL)", - "air_gap": "空气间隙", "all": "所有实验耗材", "always": "每次吸液前", - "aspirate_flow_rate_µL": "吸取流速(µL/s)", "aspirate_flow_rate": "吸取流速", + "aspirate_flow_rate_µL": "吸取流速(µL/s)", "aspirate_settings": "吸取设置", "aspirate_tip_position": "吸取移液器位置", "aspirate_volume": "每孔吸液体积", "aspirate_volume_µL": "每孔吸液体积(µL)", "attach_pipette": "连接移液器", + "blow_out": "吹出", "blow_out_after_dispensing": "分液后吹出", "blow_out_destination_well": "目标孔", "blow_out_into_destination_well": "到目标孔", @@ -27,7 +29,6 @@ "blow_out_source_well": "源孔", "blow_out_trash_bin": "垃圾桶", "blow_out_waste_chute": "外置垃圾槽", - "blow_out": "吹出", "both_mounts": "左侧+右侧支架", "change_tip": "更换吸头", "character_limit_error": "字数超出限制", @@ -38,23 +39,24 @@ "create_new_transfer": "创建新的快速移液命令", "create_to_get_started": "创建新的快速移液以开始操作。", "create_transfer": "创建移液命令", + "delay": "延迟", + "delay_after_aspirating": "吸液后延迟", "delay_before_dispensing": "分液前的延迟", "delay_duration_s": "延迟时长(秒)", "delay_position_mm": "距孔底延迟时的位置(mm)", "delay_value": "{{delay}}秒,距离孔底{{position}}mm", - "delay": "延迟", "delete_this_transfer": "确定删除此这个快速移液?", "delete_transfer": "删除快速移液", "deleted_transfer": "已删除快速移液", "destination": "目标", "destination_labware": "目标实验耗材", "disabled": "已禁用", - "dispense_flow_rate_µL": "分液流速(µL/s)", "dispense_flow_rate": "分液流速", + "dispense_flow_rate_µL": "分液流速(µL/s)", "dispense_settings": "分液设置", "dispense_tip_position": "分液吸头位置", - "dispense_volume_µL": "每孔排液体积(µL)", "dispense_volume": "每孔排液体积", + "dispense_volume_µL": "每孔排液体积(µL)", "disposal_volume_µL": "废液量(µL)", "distance_bottom_of_well_mm": "距离孔底的高度(mm)", "distribute_volume_error": "所选源孔太小,无法从中分液。请尝试向更少的孔中分液。", @@ -70,12 +72,12 @@ "learn_more": "了解更多", "left_mount": "左侧支架", "lose_all_progress": "您将失去所有此快速移液流程进度.", + "mix": "混匀", "mix_before_aspirating": "在吸液前混匀", "mix_before_dispensing": "在分液前混匀", "mix_repetitions": "混匀重复次数", "mix_value": "{{volume}} µL,混匀{{reps}}次", "mix_volume_µL": "混匀体积(µL)", - "mix": "混匀", "name_your_transfer": "为您的快速移液流程命名", "none_to_show": "没有快速移液可显示!", "number_wells_selected_error_learn_more": "具有多个源孔{{selectionUnits}}的快速移液是可以进行一对一或者多对多移液的(为此移液流程同样选择{{wellCount}}个目标孔位{{selectionUnits}})或进行多对一移液,即合并为单个孔位(选择1个目标孔{{selectionUnit}})。", @@ -89,24 +91,24 @@ "pin_transfer": "快速移液", "pinned_transfer": "固定快速移液", "pinned_transfers": "固定快速移液", + "pipette": "移液器", + "pipette_currently_attached": "快速移液移液器选项取决于当前您工作站上安装的移液器.", + "pipette_path": "移液器路径", "pipette_path_multi_aspirate": "多次吸取", - "pipette_path_multi_dispense_volume_blowout": "多次分液,{{volume}} 微升废弃量,在{{blowOutLocation}}吹出", "pipette_path_multi_dispense": "多次分液", + "pipette_path_multi_dispense_volume_blowout": "多次分液,{{volume}} 微升废弃量,在{{blowOutLocation}}吹出", "pipette_path_single": "单次转移", - "pipette_path": "移液器路径", - "pipette_currently_attached": "快速移液移液器选项取决于当前您工作站上安装的移液器.", - "pipette": "移液器", "pre_wet_tip": "润湿吸头", - "quick_transfer_volume": "快速移液{{volume}}µL", "quick_transfer": "快速移液", - "right_mount": "右侧支架", + "quick_transfer_volume": "快速移液{{volume}}µL", "reservoir": "储液槽", + "right_mount": "右侧支架", "run_now": "立即运行", "run_quick_transfer_now": "您想立即运行快速移液流程吗?", "run_transfer": "运行快速移液", "save": "保存", - "save_to_run_later": "保存您的快速移液流程以备后续运行.", "save_for_later": "保存备用", + "save_to_run_later": "保存您的快速移液流程以备后续运行.", "select_attached_pipette": "选择已连接的移液器", "select_by": "按...选择", "select_dest_labware": "选择目标实验耗材", @@ -117,23 +119,23 @@ "set_aspirate_volume": "设置吸液体积", "set_dispense_volume": "设置排液体积", "set_transfer_volume": "设置移液体积", + "source": "源", "source_labware": "源实验耗材", "source_labware_c2": "C2 中的源实验耗材", - "source": "源", "starting_well": "起始孔", "storage_limit_reached": "已达到存储限制", - "use_deck_slots": "快速移液将使用板位B2-D2。这些板位将用于放置吸头盒、源实验耗材和目标实验耗材。请确保使用最新的甲板配置,避免碰撞。", "tip_drop_location": "吸头丢弃位置", "tip_management": "吸头管理", - "tip_position_value": "距底部 {{position}} mm", "tip_position": "移液器位置", + "tip_position_value": "距底部 {{position}} mm", "tip_rack": "吸头盒", "too_many_pins_body": "删除一个快速移液,以便向您的固定列表中添加更多传输。", "too_many_pins_header": "您已达到上限!", + "touch_tip": "碰壁动作", + "touch_tip_after_aspirating": "吸液后触碰吸头", "touch_tip_before_dispensing": "在分液前做碰壁动作", "touch_tip_position_mm": "在孔底部做碰壁动作的高度(mm)", "touch_tip_value": "距底部 {{position}} mm", - "touch_tip": "碰壁动作", "transfer_analysis_failed": "快速移液分析失败", "transfer_name": "移液名称", "trashBin": "垃圾桶", @@ -141,16 +143,17 @@ "tubeRack": "试管架", "unpin_transfer": "取消固定的快速移液", "unpinned_transfer": "已取消固定的快速移液", + "use_deck_slots": "快速移液将使用板位B2-D2。这些板位将用于放置吸头盒、源实验耗材和目标实验耗材。请确保使用最新的甲板配置,避免碰撞。", + "value_out_of_range": "值必须在{{min}}-{{max}}之间", "volume_per_well": "每孔体积", "volume_per_well_µL": "每孔体积(µL)", - "value_out_of_range": "值必须在{{min}}-{{max}}之间", "wasteChute": "外置垃圾槽", "wasteChute_location": "位于{{slotName}}的外置垃圾槽", "welcome_to_quick_transfer": "欢迎使用快速移液!", + "well": "孔", "wellPlate": "孔板", - "well_selection": "孔位选择", "well_ratio": "快速移液可以一对一或者多对多进行移液的(为此移液操作选择同样数量的{{wells}})或可以多对一,也就是合并为单孔(选择1个目标孔位)。", - "well": "孔", + "well_selection": "孔位选择", "wells": "孔", "will_be_deleted": "{{transferName}} 将被永久删除。" } diff --git a/app/src/assets/localization/zh/robot_calibration.json b/app/src/assets/localization/zh/robot_calibration.json index d5959a113c6..191e5f080a1 100644 --- a/app/src/assets/localization/zh/robot_calibration.json +++ b/app/src/assets/localization/zh/robot_calibration.json @@ -7,7 +7,7 @@ "calibrate_tip_length": "校准移液器的吸头长度。", "calibrate_tip_on_block": "在校准块上校准吸头", "calibrate_tip_on_trash": "在垃圾桶上校准吸头", - "calibrate_xy_axes": "在{{slotName}}号板位中校准x轴和y轴", + "calibrate_xy_axes": "在 {{slotName}} 板位中校准x轴和y轴", "calibrate_z_axis_on_block": "在校准块上校准z轴", "calibrate_z_axis_on_slot": "在5号板位中校准z轴", "calibrate_z_axis_on_trash": "在垃圾桶上校准z轴", @@ -23,7 +23,7 @@ "change_tip_rack": "更换吸头架", "check_tip_on_block": "在校准块上检查吸头", "check_tip_on_trash": "在垃圾桶上检查吸头", - "check_xy_axes": "在{{slotName}}号板位中检查x轴和y轴", + "check_xy_axes": "在 {{slotName}} 板位中检查x轴和y轴", "check_z_axis_on_block": "在校准块上检查z轴", "check_z_axis_on_slot": "在5号板位上检查z轴", "check_z_axis_on_trash": "在垃圾桶上检查z轴", @@ -31,7 +31,7 @@ "choose_tip_rack": "选择您想要用来校准吸头长度的吸头盒。想要使用这里没有列出的吸头盒吗?请转到实验耗材>导入以添加实验耗材。", "clear_other_slots": "清除所有其他甲板位", "confirm_exit_before_completion": "您确定要在完成{{sessionType}}之前退出吗?", - "confirm_placement": "确认放置位置", + "confirm_placement": "确认耗材位置", "confirm_tip_rack": "确认吸头盒", "custom": "自定义", "deck_calibration": "甲板校准", @@ -88,7 +88,7 @@ "pipette_name_and_serial": "{{name}},{{serial}}", "pipette_offset_calibration": "移液器偏移校准", "pipette_offset_calibration_intro_body": "校准移液器偏移数据将测量移液器与移液器支架和甲板的相对位置。", - "pipette_offset_calibration_on_mount": "当移液器连接到工作站的{{mount}}支架上时,校准此移液器的偏移数据。", + "pipette_offset_calibration_on_mount": "当移液器连接到工作站的 {{mount}} 侧支架上时,校准此移液器的偏移数据。", "pipette_offset_description": "校准移液器拾取默认吸头后的位置。", "pipette_offset_recalibrate_both_mounts": "两个支架的移液器偏移数据都需要重新校准。", "pipette_offset_requires_tip_length": "您还没有为此移液器保存吸头长度数据。在校准移液器偏移数据之前,您需要校准吸头长度。", diff --git a/app/src/assets/localization/zh/run_details.json b/app/src/assets/localization/zh/run_details.json index 2bfa9c1a5e1..b6fabcfe08c 100644 --- a/app/src/assets/localization/zh/run_details.json +++ b/app/src/assets/localization/zh/run_details.json @@ -1,10 +1,11 @@ { "analysis_failure_on_robot": "尝试在{{robotName}}上分析{{protocolName}}时发生错误。请修复以下错误,然后再次尝试运行此协议。", "analyzing_on_robot": "移液工作站分析中", - "anticipated_step": "预期步骤", "anticipated": "预期步骤", + "anticipated_step": "预期步骤", "apply_stored_data": "应用存储的数据", "apply_stored_labware_offset_data": "应用已储存的耗材校准数据?", + "cancel_run": "取消运行", "cancel_run_alert_info_flex": "该动作将终止本次运行并使移液器归位。", "cancel_run_alert_info_ot2": "该动作将终止本次运行,已拾取的吸头将被丢弃,移液器将归位。", "cancel_run_and_restart": "取消运行,重新进行设置以进行编辑", @@ -12,56 +13,56 @@ "cancel_run_modal_confirm": "是,取消运行", "cancel_run_modal_heading": "确定要取消吗?", "cancel_run_module_info": "此外,协议中使用的模块将保持激活状态,直到被禁用。", - "cancel_run": "取消运行", - "canceling_run_dot": "正在取消运行...", "canceling_run": "正在取消运行", - "clear_protocol_to_make_available": "清除工作站的协议以使其可用", + "canceling_run_dot": "正在取消运行...", "clear_protocol": "清除协议", - "close_door_to_resume_run": "关闭工作站门以继续运行", - "close_door_to_resume": "关闭移液工作站的前门以继续运行", + "clear_protocol_to_make_available": "清除工作站的协议以使其可用", "close_door": "关闭移液工作站前门", + "close_door_to_resume": "关闭移液工作站的前门以继续运行", + "close_door_to_resume_run": "关闭工作站门以继续运行", "closing_protocol": "正在关闭协议", - "comment_step": "注释", "comment": "注释", + "comment_step": "注释", "complete_protocol_to_download": "完成协议以下载运行日志", - "current_step_pause_timer": "计时器", - "current_step_pause": "当前步骤 - 用户暂停", "current_step": "当前步骤", + "current_step_pause": "当前步骤 - 用户暂停", + "current_step_pause_timer": "计时器", "current_temperature": "当前:{{temperature}}°C", "custom_values": "自定义值", "data_out_of_date": "此数据可能已过期", "date": "日期", + "device_details": "设备详细信息", "door_is_open": "工作站前门已打开", "door_open_pause": "当前步骤 - 暂停 - 前门已打开", - "download_files": "下载文件", "download": "下载", + "download_files": "下载文件", "download_run_log": "下载运行日志", "downloading_run_log": "正在下载运行日志", "drop_tip": "在{{labware_location}}内的{{labware}}中的{{well_name}}中丢弃吸头", "duration": "持续时间", + "end": "结束", "end_of_protocol": "协议结束", "end_step_time": "结束", - "end": "结束", "error_details": "错误详情", "error_info": "错误{{errorCode}}:{{errorType}}", "error_type": "错误:{{errorType}}", "failed_step": "步骤失败", + "files_available_robot_details": "与协议运行相关的所有文件均可在工作站详情页面查看。", "final_step": "最后一步", "ignore_stored_data": "忽略已存储的数据", - "labware_offset_data": "耗材校准数据", "labware": "耗材", + "labware_offset_data": "耗材校准数据", "left": "左", "listed_values": "列出的值仅供查看", - "load_liquids_info_protocol_setup": "将{{liquid}}加载到{{labware}}中", + "load_labware_info_protocol_setup_plural": "在{{module_name}}中加载{{labware}}", "load_module_protocol_setup_plural": "加载{{module}}", - "load_module_protocol_setup": "在{{slot_name}}号板位中加载{{module}}", - "load_pipette_protocol_setup": "在{{mount_name}}安装位上加载{{pipette_name}}", - "loading_protocol": "正在加载协议", "loading_data": "正在加载数据...", + "loading_protocol": "正在加载协议", "location": "位置", "module_controls": "模块控制", "module_slot_number": "板位{{slot_number}}", "move_labware": "移动耗材", + "na": "不适用", "name": "名称", "no_files_included": "未包含协议文件", "no_of_error": "{{count}}个错误", @@ -74,9 +75,9 @@ "not_started_yet": "未开始", "off_deck": "甲板外", "parameters": "参数", + "pause": "暂停", "pause_protocol": "暂停协议", "pause_run": "暂停运行", - "pause": "暂停", "paused_for": "暂停原因", "pickup_tip": "从{{labware_location}}内的{{labware}}中的{{well_name}}孔位拾取吸头", "plus_more": "+{{count}}更多", @@ -100,39 +101,39 @@ "right": "右", "robot_has_previous_offsets": "该移液工作站已存储了之前运行协议的耗材校准数据。您想将这些数据应用于此协议的运行吗?您仍然可以通过实验器具位置检查调整校准数据。", "robot_was_recalibrated": "在储存此耗材校准数据后,移液工作站已重新校准", + "run": "运行", "run_again": "再次运行", + "run_canceled": "运行已取消。", "run_canceled_splash": "运行已取消", - "run_canceled_with_errors_splash": "因错误取消运行。", "run_canceled_with_errors": "因错误取消运行。", - "run_canceled": "运行已取消。", + "run_canceled_with_errors_splash": "因错误取消运行。", + "run_complete": "运行已完成", + "run_completed": "运行已完成。", "run_completed_splash": "运行完成", - "run_completed_with_warnings_splash": "运行完成,并伴有警告。", "run_completed_with_warnings": "运行完成,并伴有警告。", - "run_completed": "运行已完成。", - "run_complete_splash": "运行已完成", - "run_complete": "运行已完成", + "run_completed_with_warnings_splash": "运行完成,并伴有警告。", "run_cta_disabled": "在开始运行之前,请完成协议选项卡上的所有必要步骤。", + "run_failed": "运行失败。", "run_failed_modal_body": "在协议执行{{command}}时发生错误。", "run_failed_modal_header": "{{errorName}}:{{errorCode}}协议步骤{{count}}", "run_failed_modal_title": "运行失败", "run_failed_splash": "运行失败", - "run_failed": "运行失败。", "run_has_diverged_from_predicted": "运行已偏离预期状态。无法执行新的预期步骤。", "run_preview": "运行预览", "run_protocol": "运行协议", "run_status": "状态:{{status}}", "run_time": "运行时间", - "run": "运行", - "setup_incomplete": "完成“设置”选项卡中所需的步骤", "setup": "设置", + "setup_incomplete": "完成“设置”选项卡中所需的步骤", "slot": "板位{{slotName}}", + "start": "开始", "start_run": "开始运行", "start_step_time": "开始", "start_time": "开始时间", - "start": "开始", + "status": "状态", + "status_awaiting-recovery": "等待恢复", "status_awaiting-recovery-blocked-by-open-door": "暂停 - 门已打开", "status_awaiting-recovery-paused": "暂停", - "status_awaiting-recovery": "等待恢复", "status_blocked-by-open-door": "暂停 - 前门打开", "status_failed": "失败", "status_finishing": "结束中", @@ -142,9 +143,9 @@ "status_stop-requested": "请求停止", "status_stopped": "已取消", "status_succeeded": "已完成", - "status": "状态", "step": "步骤", "step_failed": "步骤失败", + "step_na": "步骤:不适用", "step_number": "步骤{{step_number}}:", "steps_total": "总计{{count}}步", "stored_labware_offset_data": "已储存适用于此协议的耗材校准数据", @@ -152,13 +153,13 @@ "temperature_not_available": "{{temperature_type}}: n/a", "thermocycler_error_tooltip": "模块遇到异常,请联系技术支持。", "total_elapsed_time": "总耗时", - "total_step_count_plural": "总计{{count}}步", "total_step_count": "总计{{count}}步", + "total_step_count_plural": "总计{{count}}步", "unable_to_determine_steps": "无法确定步骤", "view_analysis_error_details": "查看 错误详情", "view_current_step": "查看当前步骤", - "view_error_details": "查看错误详情", "view_error": "查看错误", + "view_error_details": "查看错误详情", "view_warning_details": "查看警告详情", "warning_details": "警告详情" } diff --git a/app/src/assets/localization/zh/shared.json b/app/src/assets/localization/zh/shared.json index 90b597b2820..694e7b80037 100644 --- a/app/src/assets/localization/zh/shared.json +++ b/app/src/assets/localization/zh/shared.json @@ -10,15 +10,16 @@ "change_protocol": "更改协议", "change_robot": "更换工作站", "clear_data": "清除数据", - "close_robot_door": "开始运行前请关闭工作站前门。", "close": "关闭", + "close_robot_door": "开始运行前请关闭工作站前门。", + "closed": "已关闭", + "confirm": "确认", "confirm_placement": "确认放置", "confirm_position": "确认位置", "confirm_values": "确认这些值", - "confirm": "确认", + "continue": "继续", "continue_activity": "继续活动", "continue_to_param": "继续设置参数", - "continue": "继续", "delete": "删除", "did_pipette_pick_up_tip": "移液器是否成功拾取吸头?", "disabled_cannot_connect": "无法连接到工作站", @@ -29,8 +30,8 @@ "drag_and_drop": "拖放或 浏览 您的文件", "empty": "空闲", "ending": "结束中", - "error_encountered": "遇到错误", "error": "错误", + "error_encountered": "遇到错误", "exit": "退出", "extension_mount": "扩展安装支架", "flow_complete": "{{flowName}}完成!", @@ -40,8 +41,8 @@ "instruments": "硬件", "loading": "加载中...", "next": "下一步", - "no_data": "无数据", "no": "否", + "no_data": "无数据", "none": "无", "not_used": "未使用", "off": "关闭", @@ -51,18 +52,18 @@ "proceed_to_setup": "继续设置", "protocol_run_general_error_msg": "无法在工作站上创建协议运行。", "reanalyze": "重新分析", - "refresh_list": "刷新列表", "refresh": "刷新", + "refresh_list": "刷新列表", "remember_my_selection_and_do_not_ask_again": "记住我的选择,不再询问", - "reset_all": "全部重置", "reset": "重置", + "reset_all": "全部重置", "restart": "重新启动", "resume": "继续", "return": "返回", "reverse": "按字母倒序排序", "robot_is_analyzing": "工作站正在分析", - "robot_is_busy_no_protocol_run_allowed": "此工作站正忙,无法运行此协议。转到工作站", "robot_is_busy": "工作站正忙", + "robot_is_busy_no_protocol_run_allowed": "此工作站正忙,无法运行此协议。转到工作站", "robot_is_reachable_but_not_responding": "此工作站的API服务器未能正确响应IP地址{{hostname}}处的请求", "robot_was_seen_but_is_unreachable": "最近看到此工作站,但当前无法访问IP地址{{hostname}}", "save": "保存", @@ -73,11 +74,11 @@ "starting": "启动中", "step": "步骤{{current}}/{{max}}", "stop": "停止", - "terminate_activity": "终止活动", "terminate": "终止远程活动", + "terminate_activity": "终止活动", "try_again": "重试", - "unknown_error": "发生未知错误", "unknown": "未知", + "unknown_error": "发生未知错误", "update": "更新", "view_latest_release_notes": "查看最新发布说明:", "yes": "是", diff --git a/app/src/assets/localization/zh/top_navigation.json b/app/src/assets/localization/zh/top_navigation.json index cb831731be9..79de9180bb8 100644 --- a/app/src/assets/localization/zh/top_navigation.json +++ b/app/src/assets/localization/zh/top_navigation.json @@ -1,20 +1,25 @@ { - "all_protocols": "全部协议", + "app_settings": "APP设置", "attached_pipettes_do_not_match": "安装的移液器与加载的协议中指定的移液器不匹配", "calibrate_deck_to_proceed": "校准甲板以继续", + "calibration_dashboard": "校准面板", "deck_setup": "甲板设置", + "device": "设备", "devices": "设备", "instruments": "硬件", "labware": "耗材", "modules": "模块", - "pipettes_not_calibrated": "请校准加载的协议中指定的所有移液器以继续", "pipettes": "移液器", + "pipettes_not_calibrated": "请校准加载的协议中指定的所有移液器以继续", "please_connect_to_a_robot": "请连接到工作站以继续", "please_load_a_protocol": "请加载协议以继续", + "protocol_details": "协议详细信息", "protocol_runs": "协议运行", + "protocol_timeline": "协议时间线", "protocols": "协议", "quick_transfer": "快速移液", "robot_settings": "工作站设置", "run": "运行", + "run_details": "运行详细信息", "settings": "设置" } diff --git a/app/src/atoms/InlineNotification/__tests__/InlineNotification.test.tsx b/app/src/atoms/InlineNotification/__tests__/InlineNotification.test.tsx index 0e91433e7b2..bb5ba0669b9 100644 --- a/app/src/atoms/InlineNotification/__tests__/InlineNotification.test.tsx +++ b/app/src/atoms/InlineNotification/__tests__/InlineNotification.test.tsx @@ -1,18 +1,19 @@ -import type * as React from 'react' import { describe, it, vi, beforeEach, expect } from 'vitest' import { screen, fireEvent } from '@testing-library/react' import { renderWithProviders } from '/app/__testing-utils__' import { i18n } from '/app/i18n' import { InlineNotification } from '..' -const render = (props: React.ComponentProps) => { +import type { ComponentProps } from 'react' + +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, })[0] } describe('InlineNotification', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { diff --git a/app/src/atoms/InlineNotification/index.tsx b/app/src/atoms/InlineNotification/index.tsx index 5b5bf21aafa..cf413b652cc 100644 --- a/app/src/atoms/InlineNotification/index.tsx +++ b/app/src/atoms/InlineNotification/index.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { css } from 'styled-components' import { ALIGN_CENTER, @@ -19,6 +18,7 @@ import { Link, } from '@opentrons/components' +import type { MouseEventHandler } from 'react' import type { IconProps, StyleProps } from '@opentrons/components' type InlineNotificationType = 'alert' | 'error' | 'neutral' | 'success' @@ -32,9 +32,9 @@ export interface InlineNotificationProps extends StyleProps { /** Optional dynamic width based on contents */ hug?: boolean /** optional handler to show close button/clear alert */ - onCloseClick?: (() => void) | React.MouseEventHandler + onCloseClick?: (() => void) | MouseEventHandler linkText?: string - onLinkClick?: (() => void) | React.MouseEventHandler + onLinkClick?: (() => void) | MouseEventHandler } const INLINE_NOTIFICATION_PROPS_BY_TYPE: Record< diff --git a/app/src/atoms/InstrumentContainer/__tests__/InstrumentContainer.test.tsx b/app/src/atoms/InstrumentContainer/__tests__/InstrumentContainer.test.tsx index e5fa872ab54..318175558fe 100644 --- a/app/src/atoms/InstrumentContainer/__tests__/InstrumentContainer.test.tsx +++ b/app/src/atoms/InstrumentContainer/__tests__/InstrumentContainer.test.tsx @@ -1,15 +1,16 @@ -import type * as React from 'react' import { describe, it } from 'vitest' import { screen } from '@testing-library/react' import { renderWithProviders } from '/app/__testing-utils__' import { InstrumentContainer } from '..' -const render = (props: React.ComponentProps) => { +import type { ComponentProps } from 'react' + +const render = (props: ComponentProps) => { return renderWithProviders()[0] } describe('InstrumentContainer', () => { - let props: React.ComponentProps + let props: ComponentProps it('renders an instrument display name', () => { props = { diff --git a/app/src/atoms/Link/ExternalLink.tsx b/app/src/atoms/Link/ExternalLink.tsx index 5d24a06fdb4..2490266927e 100644 --- a/app/src/atoms/Link/ExternalLink.tsx +++ b/app/src/atoms/Link/ExternalLink.tsx @@ -1,22 +1,33 @@ -import type * as React from 'react' +import { css } from 'styled-components' +import { + DISPLAY_INLINE_BLOCK, + Icon, + Link, + SPACING, + TYPOGRAPHY, +} from '@opentrons/components' -import { Link, Icon, TYPOGRAPHY, SPACING } from '@opentrons/components' +import type { ReactNode } from 'react' import type { LinkProps } from '@opentrons/components' - export interface ExternalLinkProps extends LinkProps { href: string id?: string - children: React.ReactNode + children: ReactNode } export const ExternalLink = (props: ExternalLinkProps): JSX.Element => ( {props.children} + ) + +const SPAN_STYLE = css` + display: ${DISPLAY_INLINE_BLOCK}; + width: 0.4375rem; +` diff --git a/app/src/atoms/Link/__tests__/ExternalLink.test.tsx b/app/src/atoms/Link/__tests__/ExternalLink.test.tsx index e245541c514..863a10e886e 100644 --- a/app/src/atoms/Link/__tests__/ExternalLink.test.tsx +++ b/app/src/atoms/Link/__tests__/ExternalLink.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { describe, it, expect, beforeEach } from 'vitest' import { screen } from '@testing-library/react' import '@testing-library/jest-dom/vitest' @@ -6,14 +5,16 @@ import { COLORS } from '@opentrons/components' import { renderWithProviders } from '/app/__testing-utils__' import { ExternalLink } from '../ExternalLink' +import type { ComponentProps } from 'react' + const TEST_URL = 'https://opentrons.com' -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders()[0] } describe('ExternalLink', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { @@ -38,6 +39,5 @@ describe('ExternalLink', () => { const icon = screen.getByLabelText('open_in_new_icon') expect(icon).toBeInTheDocument() expect(icon).toHaveStyle('width: 0.5rem; height: 0.5rem') - expect(icon).toHaveStyle('margin-left: 0.4375rem') }) }) diff --git a/app/src/atoms/ProgressBar/__tests__/ProgressBar.test.tsx b/app/src/atoms/ProgressBar/__tests__/ProgressBar.test.tsx index 6d5b3d3fa40..557a234e969 100644 --- a/app/src/atoms/ProgressBar/__tests__/ProgressBar.test.tsx +++ b/app/src/atoms/ProgressBar/__tests__/ProgressBar.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { describe, it, expect, beforeEach } from 'vitest' import '@testing-library/jest-dom/vitest' import { screen } from '@testing-library/react' @@ -7,12 +6,14 @@ import { COLORS } from '@opentrons/components' import { renderWithProviders } from '/app/__testing-utils__' import { ProgressBar } from '..' -const render = (props: React.ComponentProps) => { +import type { ComponentProps } from 'react' + +const render = (props: ComponentProps) => { return renderWithProviders() } describe('ProgressBar', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { diff --git a/app/src/atoms/ProgressBar/index.tsx b/app/src/atoms/ProgressBar/index.tsx index 5852dadf2b5..bd598c0105b 100644 --- a/app/src/atoms/ProgressBar/index.tsx +++ b/app/src/atoms/ProgressBar/index.tsx @@ -1,7 +1,7 @@ -import type * as React from 'react' import { css } from 'styled-components' import { COLORS, Box } from '@opentrons/components' +import type { ReactNode } from 'react' import type { FlattenSimpleInterpolation } from 'styled-components' interface ProgressBarProps { @@ -12,7 +12,7 @@ interface ProgressBarProps { /** extra styles to be filled progress element */ innerStyles?: FlattenSimpleInterpolation /** extra elements to be rendered within container */ - children?: React.ReactNode + children?: ReactNode } export function ProgressBar({ diff --git a/app/src/atoms/SelectField/index.tsx b/app/src/atoms/SelectField/index.tsx index 50deed28266..a51ed7a9ecd 100644 --- a/app/src/atoms/SelectField/index.tsx +++ b/app/src/atoms/SelectField/index.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import find from 'lodash/find' import { Select } from './Select' import { @@ -11,6 +10,7 @@ import { } from '@opentrons/components' import { css } from 'styled-components' +import type { ReactNode } from 'react' import type { SelectProps, SelectOption } from './Select' import type { ActionMeta, MultiValue, SingleValue } from 'react-select' @@ -32,9 +32,9 @@ export interface SelectFieldProps { /** render function for the option label passed to react-select */ formatOptionLabel?: SelectProps['formatOptionLabel'] /** optional title */ - title?: React.ReactNode + title?: ReactNode /** optional caption. hidden when `error` is given */ - caption?: React.ReactNode + caption?: ReactNode /** if included, use error style and display error instead of caption */ error?: string | null /** change handler called with (name, value, actionMeta) */ diff --git a/app/src/atoms/Skeleton/__tests__/Skeleton.test.tsx b/app/src/atoms/Skeleton/__tests__/Skeleton.test.tsx index b03bd72e98d..471506eb864 100644 --- a/app/src/atoms/Skeleton/__tests__/Skeleton.test.tsx +++ b/app/src/atoms/Skeleton/__tests__/Skeleton.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { describe, it, expect } from 'vitest' import '@testing-library/jest-dom/vitest' import { screen } from '@testing-library/react' @@ -6,7 +5,9 @@ import { renderWithProviders } from '/app/__testing-utils__' import { i18n } from '/app/i18n' import { Skeleton } from '..' -const render = (props: React.ComponentProps) => { +import type { ComponentProps } from 'react' + +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, })[0] diff --git a/app/src/atoms/Slideout/__tests__/Slideout.test.tsx b/app/src/atoms/Slideout/__tests__/Slideout.test.tsx index 8e3301ad374..cca0cb02929 100644 --- a/app/src/atoms/Slideout/__tests__/Slideout.test.tsx +++ b/app/src/atoms/Slideout/__tests__/Slideout.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { describe, it, expect, vi, beforeEach } from 'vitest' import '@testing-library/jest-dom/vitest' import { screen, fireEvent } from '@testing-library/react' @@ -6,14 +5,16 @@ import { i18n } from '/app/i18n' import { renderWithProviders } from '/app/__testing-utils__' import { Slideout } from '..' -const render = (props: React.ComponentProps) => { +import type { ComponentProps } from 'react' + +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, })[0] } describe('Slideout', () => { - let props: React.ComponentProps + let props: ComponentProps const mockOnClick = vi.fn() beforeEach(() => { props = { diff --git a/app/src/atoms/Slideout/index.tsx b/app/src/atoms/Slideout/index.tsx index b834bd1c6e5..7260da7c342 100644 --- a/app/src/atoms/Slideout/index.tsx +++ b/app/src/atoms/Slideout/index.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useRef, useState, useEffect } from 'react' import { css } from 'styled-components' import { useTranslation } from 'react-i18next' @@ -22,17 +22,19 @@ import { import { Divider } from '../structure' +import type { ReactElement, ReactNode } from 'react' + export interface MultiSlideoutSpecs { currentStep: number maxSteps: number } export interface SlideoutProps { - title: string | React.ReactElement - children: React.ReactNode + title: string | ReactElement + children: ReactNode onCloseClick: () => void // isExpanded is for collapse and expand animation isExpanded?: boolean - footer?: React.ReactNode + footer?: ReactNode multiSlideoutSpecs?: MultiSlideoutSpecs } @@ -124,9 +126,9 @@ export const Slideout = (props: SlideoutProps): JSX.Element => { multiSlideoutSpecs, } = props const { t } = useTranslation('shared') - const slideOutRef = React.useRef(null) - const [isReachedBottom, setIsReachedBottom] = React.useState(false) - const hasBeenExpanded = React.useRef(isExpanded ?? false) + const slideOutRef = useRef(null) + const [isReachedBottom, setIsReachedBottom] = useState(false) + const hasBeenExpanded = useRef(isExpanded ?? false) const handleScroll = (): void => { if (slideOutRef.current == null) return const { scrollTop, scrollHeight, clientHeight } = slideOutRef.current @@ -137,7 +139,7 @@ export const Slideout = (props: SlideoutProps): JSX.Element => { } } - React.useEffect(() => { + useEffect(() => { handleScroll() }, [slideOutRef]) diff --git a/app/src/atoms/SoftwareKeyboard/AlphanumericKeyboard/__tests__/CustomKeyboard.test.tsx b/app/src/atoms/SoftwareKeyboard/AlphanumericKeyboard/__tests__/CustomKeyboard.test.tsx index b4a9abaae89..2fdf2e30b7e 100644 --- a/app/src/atoms/SoftwareKeyboard/AlphanumericKeyboard/__tests__/CustomKeyboard.test.tsx +++ b/app/src/atoms/SoftwareKeyboard/AlphanumericKeyboard/__tests__/CustomKeyboard.test.tsx @@ -1,17 +1,19 @@ -import * as React from 'react' +import { useRef } from 'react' import { describe, it, expect, vi } from 'vitest' import '@testing-library/jest-dom/vitest' import { fireEvent, renderHook, screen } from '@testing-library/react' import { renderWithProviders } from '/app/__testing-utils__' import { AlphanumericKeyboard } from '..' -const render = (props: React.ComponentProps) => { +import type { ComponentProps } from 'react' + +const render = (props: ComponentProps) => { return renderWithProviders()[0] } describe('AlphanumericKeyboard', () => { it('should render alphanumeric keyboard - lower case', () => { - const { result } = renderHook(() => React.useRef(null)) + const { result } = renderHook(() => useRef(null)) const props = { onChange: vi.fn(), keyboardRef: result.current, @@ -55,7 +57,7 @@ describe('AlphanumericKeyboard', () => { }) }) it('should render alphanumeric keyboard - upper case, when clicking ABC key', () => { - const { result } = renderHook(() => React.useRef(null)) + const { result } = renderHook(() => useRef(null)) const props = { onChange: vi.fn(), keyboardRef: result.current, @@ -103,7 +105,7 @@ describe('AlphanumericKeyboard', () => { }) it('should render alphanumeric keyboard - numbers, when clicking number key', () => { - const { result } = renderHook(() => React.useRef(null)) + const { result } = renderHook(() => useRef(null)) const props = { onChange: vi.fn(), keyboardRef: result.current, @@ -133,7 +135,7 @@ describe('AlphanumericKeyboard', () => { }) it('should render alphanumeric keyboard - lower case when layout is numbers and clicking abc ', () => { - const { result } = renderHook(() => React.useRef(null)) + const { result } = renderHook(() => useRef(null)) const props = { onChange: vi.fn(), keyboardRef: result.current, @@ -182,7 +184,7 @@ describe('AlphanumericKeyboard', () => { }) it('should switch each alphanumeric keyboard properly', () => { - const { result } = renderHook(() => React.useRef(null)) + const { result } = renderHook(() => useRef(null)) const props = { onChange: vi.fn(), keyboardRef: result.current, diff --git a/app/src/atoms/SoftwareKeyboard/AlphanumericKeyboard/index.css b/app/src/atoms/SoftwareKeyboard/AlphanumericKeyboard/index.css index 1fa59e2230a..61e4f80d0ca 100644 --- a/app/src/atoms/SoftwareKeyboard/AlphanumericKeyboard/index.css +++ b/app/src/atoms/SoftwareKeyboard/AlphanumericKeyboard/index.css @@ -68,3 +68,12 @@ the rest is the same */ height: 44.75px; width: 330px !important; } + +.hg-candidate-box { + max-width: 400px; +} + +li.hg-candidate-box-list-item { + height: 60px; + width: 60px; +} diff --git a/app/src/atoms/SoftwareKeyboard/AlphanumericKeyboard/index.tsx b/app/src/atoms/SoftwareKeyboard/AlphanumericKeyboard/index.tsx index dccad085c08..d536b18b30c 100644 --- a/app/src/atoms/SoftwareKeyboard/AlphanumericKeyboard/index.tsx +++ b/app/src/atoms/SoftwareKeyboard/AlphanumericKeyboard/index.tsx @@ -1,6 +1,14 @@ -import * as React from 'react' +import { useState } from 'react' import Keyboard from 'react-simple-keyboard' -import { alphanumericKeyboardLayout, customDisplay } from '../constants' +import { useSelector } from 'react-redux' +import { getAppLanguage } from '/app/redux/config' +import { + alphanumericKeyboardLayout, + layoutCandidates, + customDisplay, +} from '../constants' + +import type { MutableRefObject } from 'react' import type { KeyboardReactInterface } from 'react-simple-keyboard' import '../index.css' @@ -9,7 +17,7 @@ import './index.css' // TODO (kk:04/05/2024) add debug to make debugging easy interface AlphanumericKeyboardProps { onChange: (input: string) => void - keyboardRef: React.MutableRefObject + keyboardRef: MutableRefObject debug?: boolean } @@ -18,7 +26,8 @@ export function AlphanumericKeyboard({ keyboardRef, debug = false, // If true, will input a \n }: AlphanumericKeyboardProps): JSX.Element { - const [layoutName, setLayoutName] = React.useState('default') + const [layoutName, setLayoutName] = useState('default') + const appLanguage = useSelector(getAppLanguage) const onKeyPress = (button: string): void => { if (button === '{ABC}') handleShift() if (button === '{numbers}') handleNumber() @@ -42,11 +51,14 @@ export function AlphanumericKeyboard({ return ( (keyboardRef.current = r)} - theme={'hg-theme-default oddTheme1 alphanumericKeyboard'} + theme="hg-theme-default oddTheme1 alphanumericKeyboard" onChange={onChange} onKeyPress={onKeyPress} layoutName={layoutName} layout={alphanumericKeyboardLayout} + layoutCandidates={ + appLanguage != null ? layoutCandidates[appLanguage] : undefined + } display={customDisplay} mergeDisplay={true} useButtonTag={true} diff --git a/app/src/atoms/SoftwareKeyboard/FullKeyboard/__tests__/FullKeyboard.test.tsx b/app/src/atoms/SoftwareKeyboard/FullKeyboard/__tests__/FullKeyboard.test.tsx index 728ae462083..90786ff6a6f 100644 --- a/app/src/atoms/SoftwareKeyboard/FullKeyboard/__tests__/FullKeyboard.test.tsx +++ b/app/src/atoms/SoftwareKeyboard/FullKeyboard/__tests__/FullKeyboard.test.tsx @@ -1,17 +1,19 @@ -import * as React from 'react' +import { useRef } from 'react' import { describe, it, expect, vi } from 'vitest' import '@testing-library/jest-dom/vitest' import { fireEvent, renderHook, screen } from '@testing-library/react' import { renderWithProviders } from '/app/__testing-utils__' import { FullKeyboard } from '..' -const render = (props: React.ComponentProps) => { +import type { ComponentProps } from 'react' + +const render = (props: ComponentProps) => { return renderWithProviders()[0] } describe('FullKeyboard', () => { it('should render FullKeyboard keyboard', () => { - const { result } = renderHook(() => React.useRef(null)) + const { result } = renderHook(() => useRef(null)) const props = { onChange: vi.fn(), keyboardRef: result.current, @@ -59,7 +61,7 @@ describe('FullKeyboard', () => { }) it('should render full keyboard when hitting ABC key', () => { - const { result } = renderHook(() => React.useRef(null)) + const { result } = renderHook(() => useRef(null)) const props = { onChange: vi.fn(), keyboardRef: result.current, @@ -108,7 +110,7 @@ describe('FullKeyboard', () => { }) it('should render full keyboard when hitting 123 key', () => { - const { result } = renderHook(() => React.useRef(null)) + const { result } = renderHook(() => useRef(null)) const props = { onChange: vi.fn(), keyboardRef: result.current, @@ -158,7 +160,7 @@ describe('FullKeyboard', () => { }) it('should render the software keyboards when hitting #+= key', () => { - const { result } = renderHook(() => React.useRef(null)) + const { result } = renderHook(() => useRef(null)) const props = { onChange: vi.fn(), keyboardRef: result.current, @@ -204,7 +206,7 @@ describe('FullKeyboard', () => { }) it('should call mock function when clicking a key', () => { - const { result } = renderHook(() => React.useRef(null)) + const { result } = renderHook(() => useRef(null)) const props = { onChange: vi.fn(), keyboardRef: result.current, diff --git a/app/src/atoms/SoftwareKeyboard/FullKeyboard/index.css b/app/src/atoms/SoftwareKeyboard/FullKeyboard/index.css index b3ff8968da4..4fb38eb50db 100644 --- a/app/src/atoms/SoftwareKeyboard/FullKeyboard/index.css +++ b/app/src/atoms/SoftwareKeyboard/FullKeyboard/index.css @@ -103,3 +103,12 @@ color: #16212d; background-color: #e3e3e3; /* grey30 */ } + +.hg-candidate-box { + max-width: 400px; +} + +li.hg-candidate-box-list-item { + height: 60px; + width: 60px; +} diff --git a/app/src/atoms/SoftwareKeyboard/FullKeyboard/index.tsx b/app/src/atoms/SoftwareKeyboard/FullKeyboard/index.tsx index 663efdd9c24..f1f9a962da6 100644 --- a/app/src/atoms/SoftwareKeyboard/FullKeyboard/index.tsx +++ b/app/src/atoms/SoftwareKeyboard/FullKeyboard/index.tsx @@ -1,6 +1,14 @@ -import * as React from 'react' +import { useState } from 'react' import { KeyboardReact as Keyboard } from 'react-simple-keyboard' -import { customDisplay, fullKeyboardLayout } from '../constants' +import { useSelector } from 'react-redux' +import { getAppLanguage } from '/app/redux/config' +import { + customDisplay, + layoutCandidates, + fullKeyboardLayout, +} from '../constants' + +import type { MutableRefObject } from 'react' import type { KeyboardReactInterface } from 'react-simple-keyboard' import '../index.css' @@ -9,7 +17,7 @@ import './index.css' // TODO (kk:04/05/2024) add debug to make debugging easy interface FullKeyboardProps { onChange: (input: string) => void - keyboardRef: React.MutableRefObject + keyboardRef: MutableRefObject debug?: boolean } @@ -18,7 +26,8 @@ export function FullKeyboard({ keyboardRef, debug = false, }: FullKeyboardProps): JSX.Element { - const [layoutName, setLayoutName] = React.useState('default') + const [layoutName, setLayoutName] = useState('default') + const appLanguage = useSelector(getAppLanguage) const handleShift = (button: string): void => { switch (button) { case '{shift}': @@ -51,11 +60,14 @@ export function FullKeyboard({ return ( (keyboardRef.current = r)} - theme={'hg-theme-default oddTheme1'} + theme="hg-theme-default oddTheme1" onChange={onChange} onKeyPress={onKeyPress} layoutName={layoutName} layout={fullKeyboardLayout} + layoutCandidates={ + appLanguage != null ? layoutCandidates[appLanguage] : undefined + } display={customDisplay} mergeDisplay={true} useButtonTag={true} diff --git a/app/src/atoms/SoftwareKeyboard/IndividualKey/__tests__/IndividualKey.test.tsx b/app/src/atoms/SoftwareKeyboard/IndividualKey/__tests__/IndividualKey.test.tsx index b29404ba226..ecdbdf9aa78 100644 --- a/app/src/atoms/SoftwareKeyboard/IndividualKey/__tests__/IndividualKey.test.tsx +++ b/app/src/atoms/SoftwareKeyboard/IndividualKey/__tests__/IndividualKey.test.tsx @@ -1,17 +1,19 @@ -import * as React from 'react' +import { useRef } from 'react' import { describe, it, vi, expect } from 'vitest' import { fireEvent, renderHook, screen } from '@testing-library/react' import { renderWithProviders } from '/app/__testing-utils__' import { IndividualKey } from '..' -const render = (props: React.ComponentProps) => { +import type { ComponentProps } from 'react' + +const render = (props: ComponentProps) => { return renderWithProviders()[0] } describe('IndividualKey', () => { it('should render the text key', () => { - const { result } = renderHook(() => React.useRef(null)) + const { result } = renderHook(() => useRef(null)) const props = { onChange: vi.fn(), keyboardRef: result.current, @@ -22,7 +24,7 @@ describe('IndividualKey', () => { }) it('should call mock function when clicking text key', () => { - const { result } = renderHook(() => React.useRef(null)) + const { result } = renderHook(() => useRef(null)) const props = { onChange: vi.fn(), keyboardRef: result.current, diff --git a/app/src/atoms/SoftwareKeyboard/IndividualKey/index.tsx b/app/src/atoms/SoftwareKeyboard/IndividualKey/index.tsx index 1ba1ec5e150..23ed71f9851 100644 --- a/app/src/atoms/SoftwareKeyboard/IndividualKey/index.tsx +++ b/app/src/atoms/SoftwareKeyboard/IndividualKey/index.tsx @@ -1,6 +1,8 @@ -import type * as React from 'react' import { KeyboardReact as Keyboard } from 'react-simple-keyboard' + +import type { MutableRefObject } from 'react' import type { KeyboardReactInterface } from 'react-simple-keyboard' + import '../index.css' import './index.css' @@ -11,7 +13,7 @@ const customDisplay = { // TODO (kk:04/05/2024) add debug to make debugging easy interface IndividualKeyProps { onChange: (input: string) => void - keyboardRef: React.MutableRefObject + keyboardRef: MutableRefObject keyText: string debug?: boolean } @@ -34,7 +36,7 @@ export function IndividualKey({ */ (keyboardRef.current = r)} - theme={'hg-theme-default oddTheme1 individual-key'} + theme="hg-theme-default oddTheme1 individual-key" onChange={onChange} layoutName="default" display={customDisplay} diff --git a/app/src/atoms/SoftwareKeyboard/NumericalKeyboard/__tests__/NumericalKeyboard.test.tsx b/app/src/atoms/SoftwareKeyboard/NumericalKeyboard/__tests__/NumericalKeyboard.test.tsx index 1bda1caaa71..722f50dfcf1 100644 --- a/app/src/atoms/SoftwareKeyboard/NumericalKeyboard/__tests__/NumericalKeyboard.test.tsx +++ b/app/src/atoms/SoftwareKeyboard/NumericalKeyboard/__tests__/NumericalKeyboard.test.tsx @@ -1,17 +1,19 @@ -import * as React from 'react' +import { useRef } from 'react' import { describe, it, expect, vi } from 'vitest' import '@testing-library/jest-dom/vitest' import { fireEvent, renderHook, screen } from '@testing-library/react' import { renderWithProviders } from '/app/__testing-utils__' import { NumericalKeyboard } from '..' -const render = (props: React.ComponentProps) => { +import type { ComponentProps } from 'react' + +const render = (props: ComponentProps) => { return renderWithProviders()[0] } describe('NumericalKeyboard', () => { it('should render numerical keyboard isDecimal: false and hasHyphen: false', () => { - const { result } = renderHook(() => React.useRef(null)) + const { result } = renderHook(() => useRef(null)) const props = { onChange: vi.fn(), keyboardRef: result.current, @@ -41,7 +43,7 @@ describe('NumericalKeyboard', () => { }) it('should render numerical keyboard isDecimal: false and hasHyphen: true', () => { - const { result } = renderHook(() => React.useRef(null)) + const { result } = renderHook(() => useRef(null)) const props = { onChange: vi.fn(), keyboardRef: result.current, @@ -72,7 +74,7 @@ describe('NumericalKeyboard', () => { }) it('should render numerical keyboard isDecimal: true and hasHyphen: false', () => { - const { result } = renderHook(() => React.useRef(null)) + const { result } = renderHook(() => useRef(null)) const props = { onChange: vi.fn(), keyboardRef: result.current, @@ -103,7 +105,7 @@ describe('NumericalKeyboard', () => { }) it('should render numerical keyboard isDecimal: true and hasHyphen: true', () => { - const { result } = renderHook(() => React.useRef(null)) + const { result } = renderHook(() => useRef(null)) const props = { onChange: vi.fn(), keyboardRef: result.current, @@ -135,7 +137,7 @@ describe('NumericalKeyboard', () => { }) it('should call mock function when clicking num key', () => { - const { result } = renderHook(() => React.useRef(null)) + const { result } = renderHook(() => useRef(null)) const props = { onChange: vi.fn(), keyboardRef: result.current, @@ -149,7 +151,7 @@ describe('NumericalKeyboard', () => { }) it('should call mock function when clicking decimal point key', () => { - const { result } = renderHook(() => React.useRef(null)) + const { result } = renderHook(() => useRef(null)) const props = { onChange: vi.fn(), keyboardRef: result.current, @@ -163,7 +165,7 @@ describe('NumericalKeyboard', () => { }) it('should call mock function when clicking hyphen key', () => { - const { result } = renderHook(() => React.useRef(null)) + const { result } = renderHook(() => useRef(null)) const props = { onChange: vi.fn(), keyboardRef: result.current, diff --git a/app/src/atoms/SoftwareKeyboard/NumericalKeyboard/index.tsx b/app/src/atoms/SoftwareKeyboard/NumericalKeyboard/index.tsx index f0025b6c972..16180ecbbdb 100644 --- a/app/src/atoms/SoftwareKeyboard/NumericalKeyboard/index.tsx +++ b/app/src/atoms/SoftwareKeyboard/NumericalKeyboard/index.tsx @@ -1,15 +1,16 @@ -import type * as React from 'react' import { KeyboardReact as Keyboard } from 'react-simple-keyboard' import { numericalKeyboardLayout, numericalCustom } from '../constants' +import type { MutableRefObject } from 'react' 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: MutableRefObject isDecimal?: boolean hasHyphen?: boolean debug?: boolean @@ -36,7 +37,7 @@ export function NumericalKeyboard({ */ (keyboardRef.current = r)} - theme={'hg-theme-default oddTheme1 numerical-keyboard'} + theme="hg-theme-default oddTheme1 numerical-keyboard" onInit={keyboard => { keyboard.setInput(initialValue) }} diff --git a/app/src/atoms/SoftwareKeyboard/constants.ts b/app/src/atoms/SoftwareKeyboard/constants.ts index 1808f4bd2f3..6fccfd21b81 100644 --- a/app/src/atoms/SoftwareKeyboard/constants.ts +++ b/app/src/atoms/SoftwareKeyboard/constants.ts @@ -1,3 +1,11 @@ +import chineseLayout from 'simple-keyboard-layouts/build/layouts/chinese' + +type LayoutCandidates = + | { + [key: string]: string + } + | undefined + export const customDisplay = { '{numbers}': '123', '{shift}': 'ABC', @@ -69,3 +77,12 @@ export const numericalKeyboardLayout = { export const numericalCustom = { '{backspace}': 'del', } + +export const layoutCandidates: { + [key: string]: LayoutCandidates +} = { + // @ts-expect-error layout candidates exists but is not on the type + // in the simple-keyboard-layouts package + 'zh-CN': chineseLayout.layoutCandidates, + 'en-US': undefined, +} diff --git a/app/src/atoms/StatusLabel/__tests__/StatusLabel.test.tsx b/app/src/atoms/StatusLabel/__tests__/StatusLabel.test.tsx index 568326f0065..48a7753cab5 100644 --- a/app/src/atoms/StatusLabel/__tests__/StatusLabel.test.tsx +++ b/app/src/atoms/StatusLabel/__tests__/StatusLabel.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { describe, it, expect } from 'vitest' import '@testing-library/jest-dom/vitest' import { screen } from '@testing-library/react' @@ -6,12 +5,14 @@ import { C_SKY_BLUE, COLORS } from '@opentrons/components' import { StatusLabel } from '..' import { renderWithProviders } from '/app/__testing-utils__' -const render = (props: React.ComponentProps) => { +import type { ComponentProps } from 'react' + +const render = (props: ComponentProps) => { return renderWithProviders()[0] } describe('StatusLabel', () => { - let props: React.ComponentProps + let props: ComponentProps it('renders an engaged status label with a blue background and text', () => { props = { diff --git a/app/src/atoms/StepMeter/__tests__/StepMeter.test.tsx b/app/src/atoms/StepMeter/__tests__/StepMeter.test.tsx index 90f027b0f7a..dcd98b06159 100644 --- a/app/src/atoms/StepMeter/__tests__/StepMeter.test.tsx +++ b/app/src/atoms/StepMeter/__tests__/StepMeter.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { describe, it, expect, beforeEach } from 'vitest' import '@testing-library/jest-dom/vitest' import { screen } from '@testing-library/react' @@ -6,14 +5,16 @@ import { i18n } from '/app/i18n' import { renderWithProviders } from '/app/__testing-utils__' import { StepMeter } from '..' -const render = (props: React.ComponentProps) => { +import type { ComponentProps } from 'react' + +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, })[0] } describe('StepMeter', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { diff --git a/app/src/atoms/buttons/BackButton.tsx b/app/src/atoms/buttons/BackButton.tsx index 29657e1f1b2..2819c0eb48e 100644 --- a/app/src/atoms/buttons/BackButton.tsx +++ b/app/src/atoms/buttons/BackButton.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { useTranslation } from 'react-i18next' import { useNavigate } from 'react-router-dom' @@ -11,11 +10,13 @@ import { TYPOGRAPHY, } from '@opentrons/components' +import type { HTMLProps } from 'react' + // TODO(bh, 2022-12-7): finish styling when designs finalized export function BackButton({ onClick, children, -}: React.HTMLProps): JSX.Element { +}: HTMLProps): JSX.Element { const navigate = useNavigate() const { t } = useTranslation('shared') diff --git a/app/src/atoms/buttons/FloatingActionButton.tsx b/app/src/atoms/buttons/FloatingActionButton.tsx index b7e870eab16..6088a5675b4 100644 --- a/app/src/atoms/buttons/FloatingActionButton.tsx +++ b/app/src/atoms/buttons/FloatingActionButton.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { css } from 'styled-components' import { @@ -15,9 +14,10 @@ import { StyledText, } from '@opentrons/components' +import type { ComponentProps } from 'react' import type { IconName } from '@opentrons/components' -interface FloatingActionButtonProps extends React.ComponentProps { +interface FloatingActionButtonProps extends ComponentProps { buttonText: string disabled?: boolean iconName?: IconName diff --git a/app/src/atoms/buttons/IconButton.tsx b/app/src/atoms/buttons/IconButton.tsx index ee754472ff1..43c935d462f 100644 --- a/app/src/atoms/buttons/IconButton.tsx +++ b/app/src/atoms/buttons/IconButton.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { css } from 'styled-components' import { BORDERS, @@ -10,8 +9,10 @@ import { } from '@opentrons/components' import { ODD_FOCUS_VISIBLE } from './constants' -interface IconButtonProps extends React.ComponentProps { - iconName: React.ComponentProps['name'] +import type { ComponentProps } from 'react' + +interface IconButtonProps extends ComponentProps { + iconName: ComponentProps['name'] hasBackground?: boolean } diff --git a/app/src/atoms/buttons/MediumButton.tsx b/app/src/atoms/buttons/MediumButton.tsx index d784029696a..7439cb39a46 100644 --- a/app/src/atoms/buttons/MediumButton.tsx +++ b/app/src/atoms/buttons/MediumButton.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { css } from 'styled-components' import { ALIGN_CENTER, @@ -16,6 +15,7 @@ import { } from '@opentrons/components' import { ODD_FOCUS_VISIBLE } from './constants' +import type { MouseEventHandler, ReactNode } from 'react' import type { IconName, StyleProps } from '@opentrons/components' import type { ButtonCategory } from './SmallButton' @@ -28,12 +28,12 @@ type MediumButtonTypes = | 'tertiaryLowLight' interface MediumButtonProps extends StyleProps { - buttonText: React.ReactNode + buttonText: ReactNode buttonType?: MediumButtonTypes disabled?: boolean iconName?: IconName buttonCategory?: ButtonCategory - onClick: React.MouseEventHandler + onClick: MouseEventHandler } export function MediumButton(props: MediumButtonProps): JSX.Element { diff --git a/app/src/atoms/buttons/SmallButton.tsx b/app/src/atoms/buttons/SmallButton.tsx index e659d52fd58..7ad8c3d1a99 100644 --- a/app/src/atoms/buttons/SmallButton.tsx +++ b/app/src/atoms/buttons/SmallButton.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { css } from 'styled-components' import { ALIGN_CENTER, @@ -15,6 +14,8 @@ import { TYPOGRAPHY, } from '@opentrons/components' import { ODD_FOCUS_VISIBLE } from './constants' + +import type { MouseEventHandler, ReactNode } from 'react' import type { IconName, StyleProps } from '@opentrons/components' export type SmallButtonTypes = @@ -28,9 +29,9 @@ export type ButtonCategory = 'default' | 'rounded' export type IconPlacement = 'startIcon' | 'endIcon' interface SmallButtonProps extends StyleProps { - onClick: React.MouseEventHandler + onClick: MouseEventHandler buttonType?: SmallButtonTypes - buttonText: React.ReactNode + buttonText: ReactNode iconPlacement?: IconPlacement | null iconName?: IconName | null buttonCategory?: ButtonCategory // if not specified, it will be 'default' diff --git a/app/src/atoms/buttons/SubmitPrimaryButton.tsx b/app/src/atoms/buttons/SubmitPrimaryButton.tsx index cdbf3442a65..cc53717bab0 100644 --- a/app/src/atoms/buttons/SubmitPrimaryButton.tsx +++ b/app/src/atoms/buttons/SubmitPrimaryButton.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { css } from 'styled-components' import { SPACING, @@ -8,10 +7,12 @@ import { styleProps, } from '@opentrons/components' +import type { MouseEvent } from 'react' + interface SubmitPrimaryButtonProps { form: string value: string - onClick?: (event: React.MouseEvent) => unknown + onClick?: (event: MouseEvent) => unknown disabled?: boolean } export const SubmitPrimaryButton = ( diff --git a/app/src/atoms/buttons/TextOnlyButton.tsx b/app/src/atoms/buttons/TextOnlyButton.tsx index de3bbc969ab..0acdaf058ed 100644 --- a/app/src/atoms/buttons/TextOnlyButton.tsx +++ b/app/src/atoms/buttons/TextOnlyButton.tsx @@ -1,7 +1,8 @@ -import type * as React from 'react' +import { css } from 'styled-components' import { Btn, StyledText, COLORS, RESPONSIVENESS } from '@opentrons/components' + +import type { ReactNode } from 'react' import type { StyleProps } from '@opentrons/components' -import { css } from 'styled-components' const GO_BACK_BUTTON_STYLE = css` color: ${COLORS.grey50}; @@ -26,7 +27,7 @@ const GO_BACK_BUTTON_DISABLED_STYLE = css` interface TextOnlyButtonProps extends StyleProps { onClick: () => void - buttonText: React.ReactNode + buttonText: ReactNode disabled?: boolean } diff --git a/app/src/atoms/buttons/ToggleButton.tsx b/app/src/atoms/buttons/ToggleButton.tsx index b814f45da1d..42efbde32fb 100644 --- a/app/src/atoms/buttons/ToggleButton.tsx +++ b/app/src/atoms/buttons/ToggleButton.tsx @@ -1,8 +1,8 @@ -import type * as React from 'react' import { css } from 'styled-components' -import { Btn, Icon, COLORS, SIZE_1, SIZE_2 } from '@opentrons/components' +import { Btn, COLORS, Icon } from '@opentrons/components' +import type { MouseEvent } from 'react' import type { StyleProps } from '@opentrons/components' const TOGGLE_DISABLED_STYLES = css` @@ -42,7 +42,7 @@ interface ToggleButtonProps extends StyleProps { toggledOn: boolean disabled?: boolean | null id?: string - onClick?: (e: React.MouseEvent) => unknown + onClick?: (e: MouseEvent) => unknown } export const ToggleButton = (props: ToggleButtonProps): JSX.Element => { @@ -55,12 +55,12 @@ export const ToggleButton = (props: ToggleButtonProps): JSX.Element => { role="switch" aria-label={label} aria-checked={toggledOn} - size={size ?? SIZE_2} + size={size ?? '2rem'} css={props.toggledOn ? TOGGLE_ENABLED_STYLES : TOGGLE_DISABLED_STYLES} {...buttonProps} > {/* TODO(bh, 2022-10-05): implement small and large sizes from design system */} - + ) } diff --git a/app/src/atoms/buttons/__tests__/BackButton.test.tsx b/app/src/atoms/buttons/__tests__/BackButton.test.tsx index 510abd0ee7d..67a65d107da 100644 --- a/app/src/atoms/buttons/__tests__/BackButton.test.tsx +++ b/app/src/atoms/buttons/__tests__/BackButton.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { fireEvent, screen } from '@testing-library/react' import { describe, it, expect, vi } from 'vitest' import '@testing-library/jest-dom/vitest' @@ -9,7 +8,9 @@ import { renderWithProviders } from '/app/__testing-utils__' import { i18n } from '/app/i18n' import { BackButton } from '..' -const render = (props?: React.HTMLProps) => { +import type { HTMLProps } from 'react' + +const render = (props?: HTMLProps) => { return renderWithProviders( ) => { +import type { ComponentProps } from 'react' + +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, })[0] } describe('FloatingActionButton', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { diff --git a/app/src/atoms/buttons/__tests__/MediumButton.test.tsx b/app/src/atoms/buttons/__tests__/MediumButton.test.tsx index 988248413d9..1ce59db9217 100644 --- a/app/src/atoms/buttons/__tests__/MediumButton.test.tsx +++ b/app/src/atoms/buttons/__tests__/MediumButton.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import '@testing-library/jest-dom/vitest' import { describe, it, expect, vi, beforeEach } from 'vitest' import { fireEvent, screen } from '@testing-library/react' @@ -7,12 +6,14 @@ import { renderWithProviders } from '/app/__testing-utils__' import { MediumButton } from '../MediumButton' -const render = (props: React.ComponentProps) => { +import type { ComponentProps } from 'react' + +const render = (props: ComponentProps) => { return renderWithProviders()[0] } describe('MediumButton', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { onClick: vi.fn(), diff --git a/app/src/atoms/buttons/__tests__/QuaternaryButton.test.tsx b/app/src/atoms/buttons/__tests__/QuaternaryButton.test.tsx index 4e10831c73c..7fd27eb874c 100644 --- a/app/src/atoms/buttons/__tests__/QuaternaryButton.test.tsx +++ b/app/src/atoms/buttons/__tests__/QuaternaryButton.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import '@testing-library/jest-dom/vitest' import { describe, it, expect, beforeEach, vi } from 'vitest' import { screen } from '@testing-library/react' @@ -7,6 +6,8 @@ import { renderWithProviders } from '/app/__testing-utils__' import { QuaternaryButton } from '..' +import type { ComponentProps } from 'react' + vi.mock('styled-components', async () => { const actual = await vi.importActual( 'styled-components/dist/styled-components.browser.esm.js' @@ -14,12 +15,12 @@ vi.mock('styled-components', async () => { return actual }) -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders()[0] } describe('QuaternaryButton', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { diff --git a/app/src/atoms/buttons/__tests__/SmallButton.test.tsx b/app/src/atoms/buttons/__tests__/SmallButton.test.tsx index 283f21daf4e..84341d3b39b 100644 --- a/app/src/atoms/buttons/__tests__/SmallButton.test.tsx +++ b/app/src/atoms/buttons/__tests__/SmallButton.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { describe, it, expect, vi, beforeEach } from 'vitest' import '@testing-library/jest-dom/vitest' import { fireEvent, screen } from '@testing-library/react' @@ -7,12 +6,14 @@ import { COLORS, BORDERS } from '@opentrons/components' import { SmallButton } from '../SmallButton' import { renderWithProviders } from '/app/__testing-utils__' -const render = (props: React.ComponentProps) => { +import type { ComponentProps } from 'react' + +const render = (props: ComponentProps) => { return renderWithProviders()[0] } describe('SmallButton', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { diff --git a/app/src/atoms/buttons/__tests__/SubmitPrimaryButton.test.tsx b/app/src/atoms/buttons/__tests__/SubmitPrimaryButton.test.tsx index a62ba9c4a95..6af258d9d3f 100644 --- a/app/src/atoms/buttons/__tests__/SubmitPrimaryButton.test.tsx +++ b/app/src/atoms/buttons/__tests__/SubmitPrimaryButton.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { describe, it, expect, vi, beforeEach } from 'vitest' import '@testing-library/jest-dom/vitest' import { fireEvent, screen } from '@testing-library/react' @@ -6,15 +5,16 @@ import { COLORS, SPACING, TYPOGRAPHY, BORDERS } from '@opentrons/components' import { renderWithProviders } from '/app/__testing-utils__' import { SubmitPrimaryButton } from '..' +import type { ComponentProps } from 'react' const mockOnClick = vi.fn() -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders()[0] } describe('SubmitPrimaryButton', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { diff --git a/app/src/atoms/buttons/__tests__/TertiaryButton.test.tsx b/app/src/atoms/buttons/__tests__/TertiaryButton.test.tsx index 4a3f95369c8..30b15c5bea2 100644 --- a/app/src/atoms/buttons/__tests__/TertiaryButton.test.tsx +++ b/app/src/atoms/buttons/__tests__/TertiaryButton.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { describe, it, expect, beforeEach } from 'vitest' import { screen } from '@testing-library/react' import { renderWithProviders } from '/app/__testing-utils__' @@ -7,12 +6,14 @@ import { COLORS, SPACING, TYPOGRAPHY, BORDERS } from '@opentrons/components' import { TertiaryButton } from '..' -const render = (props: React.ComponentProps) => { +import type { ComponentProps } from 'react' + +const render = (props: ComponentProps) => { return renderWithProviders()[0] } describe('TertiaryButton', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { diff --git a/app/src/atoms/buttons/__tests__/ToggleButton.test.tsx b/app/src/atoms/buttons/__tests__/ToggleButton.test.tsx index 3f61d43c5d0..29991d4ae0f 100644 --- a/app/src/atoms/buttons/__tests__/ToggleButton.test.tsx +++ b/app/src/atoms/buttons/__tests__/ToggleButton.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { describe, it, expect, vi, beforeEach } from 'vitest' import { screen, fireEvent } from '@testing-library/react' import { COLORS, SIZE_2 } from '@opentrons/components' @@ -6,14 +5,16 @@ import { renderWithProviders } from '/app/__testing-utils__' import { ToggleButton } from '..' +import type { ComponentProps } from 'react' + const mockOnClick = vi.fn() -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders()[0] } describe('ToggleButton', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { diff --git a/app/src/atoms/structure/Divider.tsx b/app/src/atoms/structure/Divider.tsx index db55ad84f44..a253f27dce4 100644 --- a/app/src/atoms/structure/Divider.tsx +++ b/app/src/atoms/structure/Divider.tsx @@ -1,7 +1,7 @@ -import type * as React from 'react' import { Box, COLORS, SPACING } from '@opentrons/components' +import type { ComponentProps } from 'react' -type Props = React.ComponentProps +type Props = ComponentProps export function Divider(props: Props): JSX.Element { const { marginY } = props diff --git a/app/src/atoms/structure/Line.tsx b/app/src/atoms/structure/Line.tsx index ecbbecc24cd..8eb456233f6 100644 --- a/app/src/atoms/structure/Line.tsx +++ b/app/src/atoms/structure/Line.tsx @@ -1,7 +1,7 @@ -import type * as React from 'react' import { Box, BORDERS } from '@opentrons/components' +import type { ComponentProps } from 'react' -type Props = React.ComponentProps +type Props = ComponentProps export function Line(props: Props): JSX.Element { return diff --git a/app/src/atoms/structure/__tests__/Divider.test.tsx b/app/src/atoms/structure/__tests__/Divider.test.tsx index 27460be938d..0aa40edb9d4 100644 --- a/app/src/atoms/structure/__tests__/Divider.test.tsx +++ b/app/src/atoms/structure/__tests__/Divider.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { describe, it, expect, beforeEach } from 'vitest' import '@testing-library/jest-dom/vitest' import { screen } from '@testing-library/react' @@ -6,12 +5,14 @@ import { SPACING, COLORS } from '@opentrons/components' import { renderWithProviders } from '/app/__testing-utils__' import { Divider } from '../index' -const render = (props: React.ComponentProps) => { +import type { ComponentProps } from 'react' + +const render = (props: ComponentProps) => { return renderWithProviders()[0] } describe('Divider', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { diff --git a/app/src/atoms/structure/__tests__/Line.test.tsx b/app/src/atoms/structure/__tests__/Line.test.tsx index d9a9caefba2..c4e3e267565 100644 --- a/app/src/atoms/structure/__tests__/Line.test.tsx +++ b/app/src/atoms/structure/__tests__/Line.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { describe, it, expect, beforeEach } from 'vitest' import '@testing-library/jest-dom/vitest' import { screen } from '@testing-library/react' @@ -6,12 +5,14 @@ import { SPACING, COLORS } from '@opentrons/components' import { Line } from '../index' import { renderWithProviders } from '/app/__testing-utils__' -const render = (props: React.ComponentProps) => { +import type { ComponentProps } from 'react' + +const render = (props: ComponentProps) => { return renderWithProviders()[0] } describe('Line', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { diff --git a/app/src/index.tsx b/app/src/index.tsx index a668270ac17..cdb9a3c067d 100644 --- a/app/src/index.tsx +++ b/app/src/index.tsx @@ -6,6 +6,7 @@ import { HashRouter } from 'react-router-dom' import { ApiClientProvider } from '@opentrons/react-api-client' +import { App } from './App' import { createLogger } from './logger' import { uiInitialized } from './redux/shell' @@ -16,8 +17,10 @@ import '../src/atoms/SoftwareKeyboard/FullKeyboard/index.css' import '../src/atoms/SoftwareKeyboard/IndividualKey/index.css' import '../src/atoms/SoftwareKeyboard/NumericalKeyboard/index.css' +// export public types so they can be accessed by external deps +export * from './redux/types' + // component tree -import { App } from './App' const log = createLogger(new URL('', import.meta.url).pathname) diff --git a/app/src/local-resources/commands/hooks/useCommandTextString/index.ts b/app/src/local-resources/commands/hooks/useCommandTextString/index.ts index 0eb04ee588e..86e8314e20c 100644 --- a/app/src/local-resources/commands/hooks/useCommandTextString/index.ts +++ b/app/src/local-resources/commands/hooks/useCommandTextString/index.ts @@ -90,6 +90,10 @@ export function useCommandTextString( case 'dropTip': case 'dropTipInPlace': case 'pickUpTip': + case 'airGapInPlace': + case 'evotipSealPipette': + case 'evotipUnsealPipette': + case 'evotipDispense': return { kind: 'generic', commandText: utils.getPipettingCommandText(fullParams), diff --git a/app/src/local-resources/commands/hooks/useCommandTextString/utils/commandText/getConfigureNozzleLayoutCommandText.ts b/app/src/local-resources/commands/hooks/useCommandTextString/utils/commandText/getConfigureNozzleLayoutCommandText.ts index 8c9e12f3d5b..f6440d69e1d 100644 --- a/app/src/local-resources/commands/hooks/useCommandTextString/utils/commandText/getConfigureNozzleLayoutCommandText.ts +++ b/app/src/local-resources/commands/hooks/useCommandTextString/utils/commandText/getConfigureNozzleLayoutCommandText.ts @@ -13,13 +13,12 @@ export function getConfigureNozzleLayoutCommandText({ pip => pip.id === pipetteId )?.pipetteName - // TODO(cb, 2024-09-10): confirm these strings for copy consistency and add them to i18n const ConfigAmount = { - SINGLE: 'single nozzle layout', - COLUMN: 'column layout', - ROW: 'row layout', - QUADRANT: 'partial layout', - ALL: 'all nozzles', + SINGLE: t('single_nozzle_layout'), + COLUMN: t('column_layout'), + ROW: t('row_layout'), + QUADRANT: t('partial_layout'), + ALL: t('all_nozzles'), } return t('configure_nozzle_layout', { diff --git a/app/src/local-resources/commands/hooks/useCommandTextString/utils/commandText/getPipettingCommandText.ts b/app/src/local-resources/commands/hooks/useCommandTextString/utils/commandText/getPipettingCommandText.ts index 6ef1369691e..38730082699 100644 --- a/app/src/local-resources/commands/hooks/useCommandTextString/utils/commandText/getPipettingCommandText.ts +++ b/app/src/local-resources/commands/hooks/useCommandTextString/utils/commandText/getPipettingCommandText.ts @@ -52,19 +52,21 @@ export const getPipettingCommandText = ({ t, }) + const labwareName = + commandTextData != null + ? getLabwareName({ + loadedLabwares: commandTextData.labware ?? [], + labwareId, + allRunDefs, + }) + : null + switch (command?.commandType) { case 'aspirate': { const { volume, flowRate } = command.params return t('aspirate', { well_name: wellName, - labware: - commandTextData != null - ? getLabwareName({ - loadedLabwares: commandTextData.labware ?? [], - labwareId, - allRunDefs, - }) - : null, + labware: labwareName, labware_location: displayLocation, volume, flow_rate: flowRate, @@ -75,14 +77,7 @@ export const getPipettingCommandText = ({ return pushOut != null ? t('dispense_push_out', { well_name: wellName, - labware: - commandTextData != null - ? getLabwareName({ - loadedLabwares: commandTextData.labware ?? [], - labwareId, - allRunDefs, - }) - : null, + labware: labwareName, labware_location: displayLocation, volume, flow_rate: flowRate, @@ -90,14 +85,7 @@ export const getPipettingCommandText = ({ }) : t('dispense', { well_name: wellName, - labware: - commandTextData != null - ? getLabwareName({ - loadedLabwares: commandTextData.labware ?? [], - labwareId, - allRunDefs, - }) - : null, + labware: labwareName, labware_location: displayLocation, volume, flow_rate: flowRate, @@ -107,14 +95,7 @@ export const getPipettingCommandText = ({ const { flowRate } = command.params return t('blowout', { well_name: wellName, - labware: - commandTextData != null - ? getLabwareName({ - loadedLabwares: commandTextData.labware ?? [], - labwareId, - allRunDefs, - }) - : null, + labware: labwareName, labware_location: displayLocation, flow_rate: flowRate, }) @@ -136,26 +117,12 @@ export const getPipettingCommandText = ({ return Boolean(labwareDef?.parameters.isTiprack) ? t('return_tip', { well_name: wellName, - labware: - commandTextData != null - ? getLabwareName({ - loadedLabwares: commandTextData.labware ?? [], - labwareId, - allRunDefs, - }) - : null, + labware: labwareName, labware_location: displayLocation, }) : t('drop_tip', { well_name: wellName, - labware: - commandTextData != null - ? getLabwareName({ - loadedLabwares: commandTextData.labware ?? [], - labwareId, - allRunDefs, - }) - : null, + labware: labwareName, }) } case 'pickUpTip': { @@ -176,14 +143,7 @@ export const getPipettingCommandText = ({ pipetteName ) : null, - labware: - commandTextData != null - ? getLabwareName({ - loadedLabwares: commandTextData.labware ?? [], - labwareId, - allRunDefs, - }) - : null, + labware: labwareName, labware_location: displayLocation, }) } @@ -209,6 +169,26 @@ export const getPipettingCommandText = ({ const { flowRate, volume } = command.params return t('aspirate_in_place', { volume, flow_rate: flowRate }) } + case 'airGapInPlace': { + const { volume } = command.params + return t('air_gap_in_place', { volume }) + } + case 'evotipSealPipette': { + return t('sealing_to_location', { + labware: labwareName, + location: displayLocation, + }) + } + case 'evotipUnsealPipette': { + return t('unsealing_from_location', { + labware: labwareName, + location: displayLocation, + }) + } + case 'evotipDispense': { + const { flowRate, volume } = command.params + return t('pressurizing_to_dispense', { volume, flow_rate: flowRate }) + } default: { console.warn( 'PipettingCommandText encountered a command with an unrecognized commandType: ', diff --git a/app/src/local-resources/commands/utils/lastRunCommandPromptedErrorRecovery.ts b/app/src/local-resources/commands/utils/lastRunCommandPromptedErrorRecovery.ts index dd07756ef43..673ed431220 100644 --- a/app/src/local-resources/commands/utils/lastRunCommandPromptedErrorRecovery.ts +++ b/app/src/local-resources/commands/utils/lastRunCommandPromptedErrorRecovery.ts @@ -1,13 +1,12 @@ import type { RunCommandSummary } from '@opentrons/api-client' - -// Whether the last run protocol command prompted Error Recovery. +// Whether the last run protocol command prompted Error Recovery, if Error Recovery is enabled. export function lastRunCommandPromptedErrorRecovery( - summary: RunCommandSummary[] + summary: RunCommandSummary[] | null, + isEREnabled: boolean ): boolean { - const lastProtocolCommand = summary.findLast( + const lastProtocolCommand = summary?.findLast( command => command.intent !== 'fixit' && command.error != null ) - // All recoverable protocol commands have defined errors. - return lastProtocolCommand?.error?.isDefined ?? false + return isEREnabled ? lastProtocolCommand?.error?.isDefined ?? false : false } diff --git a/app/src/local-resources/config/constants.ts b/app/src/local-resources/config/constants.ts deleted file mode 100644 index 1c7b0e2727d..00000000000 --- a/app/src/local-resources/config/constants.ts +++ /dev/null @@ -1 +0,0 @@ -export const SLEEP_NEVER_MS = 604800000 diff --git a/app/src/local-resources/config/index.ts b/app/src/local-resources/config/index.ts deleted file mode 100644 index f87cf0102a1..00000000000 --- a/app/src/local-resources/config/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './constants' diff --git a/app/src/local-resources/dom-utils/constants.ts b/app/src/local-resources/dom-utils/constants.ts new file mode 100644 index 00000000000..f48e8566837 --- /dev/null +++ b/app/src/local-resources/dom-utils/constants.ts @@ -0,0 +1,2 @@ +// See RQA-3813 and associated PR. We are stuck using this hardcoded number to mean "never" or migrate the on-filesystem value. +export const SLEEP_NEVER_MS = 604800000 diff --git a/app/src/local-resources/dom-utils/hooks/__tests__/useScreenIdle.test.ts b/app/src/local-resources/dom-utils/hooks/__tests__/useScreenIdle.test.ts new file mode 100644 index 00000000000..e449b0cd82e --- /dev/null +++ b/app/src/local-resources/dom-utils/hooks/__tests__/useScreenIdle.test.ts @@ -0,0 +1,78 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { renderHook } from '@testing-library/react' +import { useScreenIdle } from '../useScreenIdle' +import { SLEEP_NEVER_MS } from '/app/local-resources/dom-utils' + +const MOCK_EVENTS: Array = [ + 'mousedown', + 'click', + 'scroll', +] + +const MOCK_OPTIONS = { + events: MOCK_EVENTS, + initialState: false, +} + +describe('useIdle', () => { + beforeEach(() => { + vi.useFakeTimers({ shouldAdvanceTime: true }) + }) + + it('should return the default initialState', () => { + const mockTime = 1000 + const { result } = renderHook(() => useScreenIdle(mockTime)) + expect(result.current).toBe(true) + }) + + it('should return the given initialState', () => { + const mockTime = 1000 + const { result } = renderHook(() => useScreenIdle(mockTime, MOCK_OPTIONS)) + expect(result.current).toBe(false) + }) + + it('should return true after 1000ms', () => { + const mockTime = 1000 + const { result } = renderHook(() => useScreenIdle(mockTime, MOCK_OPTIONS)) + expect(result.current).toBe(false) + setTimeout(() => { + expect(result.current).toBe(true) + }, 1001) + }) + + it('should return true after 180,000ms - 3min', () => { + const mockTime = 60 * 1000 * 3 + const { result } = renderHook(() => useScreenIdle(mockTime, MOCK_OPTIONS)) + expect(result.current).toBe(false) + setTimeout(() => { + expect(result.current).toBe(true) + }, 180001) + }) + + it('should return true after 180,0000ms - 30min', () => { + const mockTime = 60 * 1000 * 30 + const { result } = renderHook(() => useScreenIdle(mockTime, MOCK_OPTIONS)) + expect(result.current).toBe(false) + setTimeout(() => { + expect(result.current).toBe(true) + }, 1800001) + }) + + it('should return true after 3,600,000ms - 1 hour', () => { + const mockTime = 60 * 1000 * 60 + const { result } = renderHook(() => useScreenIdle(mockTime, MOCK_OPTIONS)) + expect(result.current).toBe(false) + setTimeout(() => { + expect(result.current).toBe(true) + }, 3600001) + }) + + it(`should always return false if the idle time is exactly ${SLEEP_NEVER_MS}`, () => { + const mockTime = SLEEP_NEVER_MS + const { result } = renderHook(() => useScreenIdle(mockTime, MOCK_OPTIONS)) + expect(result.current).toBe(false) + setTimeout(() => { + expect(result.current).toBe(false) + }, 604800001) + }) +}) diff --git a/app/src/local-resources/dom-utils/hooks/index.ts b/app/src/local-resources/dom-utils/hooks/index.ts index 2098c90e0c3..97edf1a5b1d 100644 --- a/app/src/local-resources/dom-utils/hooks/index.ts +++ b/app/src/local-resources/dom-utils/hooks/index.ts @@ -1 +1,2 @@ export * from './useScrollPosition' +export * from './useScreenIdle' diff --git a/app/src/local-resources/dom-utils/hooks/useScreenIdle.ts b/app/src/local-resources/dom-utils/hooks/useScreenIdle.ts new file mode 100644 index 00000000000..d36c6cfb326 --- /dev/null +++ b/app/src/local-resources/dom-utils/hooks/useScreenIdle.ts @@ -0,0 +1,72 @@ +import { useState, useEffect, useRef } from 'react' +import { SLEEP_NEVER_MS } from '/app/local-resources/dom-utils' + +const USER_EVENTS: Array = [ + 'click', + 'dblclick', + 'keypress', + 'mousemove', + 'pointerover', + 'pointerenter', + 'pointerdown', + 'pointermove', + 'pointerout', + 'pointerleave', + 'scroll', + 'touchmove', + 'touchstart', + 'mousedown', +] + +const DEFAULT_OPTIONS = { + events: USER_EVENTS, + initialState: true, +} + +/** + * React hook to check user events + * + * @param {number} idleTime (idle time) + * @param {object} options (events that the app need to check, initialState: initial state true => idle) + * @returns {boolean} + */ +export function useScreenIdle( + idleTime: number, + options?: Partial<{ + events: Array + initialState: boolean + }> +): boolean { + const { events, initialState } = { ...DEFAULT_OPTIONS, ...options } + const [idle, setIdle] = useState(initialState) + const idleTimer = useRef() + + useEffect(() => { + const handleEvents = (): void => { + setIdle(false) + + if (idleTimer.current != null) { + window.clearTimeout(idleTimer.current) + } + + // See RQA-3813 and associated PR. + if (idleTime !== SLEEP_NEVER_MS) { + idleTimer.current = window.setTimeout(() => { + setIdle(true) + }, idleTime) + } + } + + events.forEach(event => { + document.addEventListener(event, handleEvents) + }) + + return () => { + events.forEach(event => { + document.removeEventListener(event, handleEvents) + }) + } + }, [events, idleTime]) + + return idle +} diff --git a/app/src/local-resources/dom-utils/index.ts b/app/src/local-resources/dom-utils/index.ts index fc78d35129c..907d7e254c1 100644 --- a/app/src/local-resources/dom-utils/index.ts +++ b/app/src/local-resources/dom-utils/index.ts @@ -1 +1,2 @@ export * from './hooks' +export * from './constants' diff --git a/app/src/local-resources/instruments/__tests__/hooks.test.ts b/app/src/local-resources/instruments/__tests__/hooks.test.ts index 94b6043a125..468c2da5e0d 100644 --- a/app/src/local-resources/instruments/__tests__/hooks.test.ts +++ b/app/src/local-resources/instruments/__tests__/hooks.test.ts @@ -37,9 +37,11 @@ const mockP1000V2Specs = { 'opentrons/opentrons_flex_96_tiprack_1000ul/1', 'opentrons/opentrons_flex_96_tiprack_200ul/1', 'opentrons/opentrons_flex_96_tiprack_50ul/1', + 'opentrons/opentrons_flex_96_tiprack_20ul/1', 'opentrons/opentrons_flex_96_filtertiprack_1000ul/1', 'opentrons/opentrons_flex_96_filtertiprack_200ul/1', 'opentrons/opentrons_flex_96_filtertiprack_50ul/1', + 'opentrons/opentrons_flex_96_filtertiprack_20ul/1', ], minVolume: 5, maxVolume: 1000, diff --git a/app/src/local-resources/labware/hooks/useAllLabware.ts b/app/src/local-resources/labware/hooks/useAllLabware.ts index 28d4325b2eb..b118113ad55 100644 --- a/app/src/local-resources/labware/hooks/useAllLabware.ts +++ b/app/src/local-resources/labware/hooks/useAllLabware.ts @@ -3,12 +3,21 @@ import { getValidCustomLabware } from '/app/redux/custom-labware' import { getAllDefinitions } from '../utils' import type { LabwareSort, LabwareFilter, LabwareDefAndDate } from '../types' +// labware to filter out from the labware tab of the desktop app +// TODO (sb:1/14/25) remove evotips from blocklist before public launch +const LABWARE_LOADNAME_BLOCKLIST = [ + 'evotips_flex_96_tiprack_adapter', + 'evotips_opentrons_96_labware', +] + export function useAllLabware( sortBy: LabwareSort, filterBy: LabwareFilter ): LabwareDefAndDate[] { const fullLabwareList: LabwareDefAndDate[] = [] - const labwareDefinitions = getAllDefinitions() + const labwareDefinitions = getAllDefinitions().filter( + def => !LABWARE_LOADNAME_BLOCKLIST.includes(def.parameters.loadName) + ) labwareDefinitions.forEach(def => fullLabwareList.push({ definition: def })) const customLabwareList = useSelector(getValidCustomLabware) customLabwareList.forEach(customLabware => diff --git a/app/src/molecules/BackgroundOverlay/__tests__/BackgroundOverlay.test.tsx b/app/src/molecules/BackgroundOverlay/__tests__/BackgroundOverlay.test.tsx index e09b3c11765..882c4a644b4 100644 --- a/app/src/molecules/BackgroundOverlay/__tests__/BackgroundOverlay.test.tsx +++ b/app/src/molecules/BackgroundOverlay/__tests__/BackgroundOverlay.test.tsx @@ -1,15 +1,16 @@ -import type * as React from 'react' import { describe, it, expect, vi } from 'vitest' import { fireEvent, screen } from '@testing-library/react' import { renderWithProviders } from '/app/__testing-utils__' import { BackgroundOverlay } from '..' -const render = (props: React.ComponentProps) => { +import type { ComponentProps } from 'react' + +const render = (props: ComponentProps) => { return renderWithProviders()[0] } describe('BackgroundOverlay', () => { - let props: React.ComponentProps + let props: ComponentProps it('renders background overlay', () => { props = { onClick: vi.fn() } render(props) diff --git a/app/src/molecules/BackgroundOverlay/index.tsx b/app/src/molecules/BackgroundOverlay/index.tsx index 3d6c6d976c6..c0a711ffa91 100644 --- a/app/src/molecules/BackgroundOverlay/index.tsx +++ b/app/src/molecules/BackgroundOverlay/index.tsx @@ -1,8 +1,9 @@ -import type * as React from 'react' import { css } from 'styled-components' import { COLORS, Flex, POSITION_FIXED } from '@opentrons/components' +import type { ComponentProps, MouseEventHandler } from 'react' + const BACKGROUND_OVERLAY_STYLE = css` position: ${POSITION_FIXED}; inset: 0; @@ -10,10 +11,9 @@ const BACKGROUND_OVERLAY_STYLE = css` background-color: ${COLORS.black90}${COLORS.opacity60HexCode}; ` -export interface BackgroundOverlayProps - extends React.ComponentProps { +export interface BackgroundOverlayProps extends ComponentProps { // onClick handler so when you click anywhere in the overlay, the modal/menu closes - onClick: React.MouseEventHandler + onClick: MouseEventHandler } export function BackgroundOverlay(props: BackgroundOverlayProps): JSX.Element { diff --git a/app/src/molecules/CardButton/__tests__/CardButton.test.tsx b/app/src/molecules/CardButton/__tests__/CardButton.test.tsx index 820dfbdc4e9..177d61dfd2d 100644 --- a/app/src/molecules/CardButton/__tests__/CardButton.test.tsx +++ b/app/src/molecules/CardButton/__tests__/CardButton.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { fireEvent, screen } from '@testing-library/react' import '@testing-library/jest-dom/vitest' import { describe, it, expect, vi, beforeEach } from 'vitest' @@ -7,6 +6,8 @@ import { COLORS } from '@opentrons/components' import { renderWithProviders } from '/app/__testing-utils__' import { i18n } from '/app/i18n' import { CardButton } from '..' + +import type { ComponentProps } from 'react' import type { NavigateFunction } from 'react-router-dom' const mockNavigate = vi.fn() @@ -19,7 +20,7 @@ vi.mock('react-router-dom', async importOriginal => { } }) -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders( @@ -31,7 +32,7 @@ const render = (props: React.ComponentProps) => { } describe('CardButton', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { diff --git a/app/src/molecules/CollapsibleSection/index.tsx b/app/src/molecules/CollapsibleSection/index.tsx index 3b9d6c4b8d0..3a359edeb4f 100644 --- a/app/src/molecules/CollapsibleSection/index.tsx +++ b/app/src/molecules/CollapsibleSection/index.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useState } from 'react' import { css } from 'styled-components' import { @@ -12,6 +12,8 @@ import { LegacyStyledText, TYPOGRAPHY, } from '@opentrons/components' + +import type { ReactNode } from 'react' import type { StyleProps } from '@opentrons/components' const ACCORDION_STYLE = css` @@ -26,7 +28,7 @@ const ACCORDION_STYLE = css` interface CollapsibleSectionProps extends StyleProps { title: string - children: React.ReactNode + children: ReactNode isExpandedInitially?: boolean } @@ -34,7 +36,7 @@ export function CollapsibleSection( props: CollapsibleSectionProps ): JSX.Element { const { title, children, isExpandedInitially = true, ...styleProps } = props - const [isExpanded, setIsExpanded] = React.useState(isExpandedInitially) + const [isExpanded, setIsExpanded] = useState(isExpandedInitially) return ( ['as'] + as?: ComponentProps['as'] modernStyledTextDefaults?: false } interface ModernSTProps { - desktopStyle?: React.ComponentProps['desktopStyle'] - oddStyle?: React.ComponentProps['oddStyle'] + desktopStyle?: ComponentProps['desktopStyle'] + oddStyle?: ComponentProps['oddStyle'] modernStyledTextDefaults: true } @@ -174,21 +175,10 @@ function ThermocyclerRunProfile( >
    {shouldPropagateTextLimit(propagateTextLimit, isOnDevice) ? ( -
  • - {stepTexts[0]} -
  • +
  • {stepTexts[0]}
  • ) : ( stepTexts.map((step: string, index: number) => ( -
  • +
  • {' '} {step}
  • @@ -252,11 +242,7 @@ function ThermocyclerRunExtendedProfile( >
      {shouldPropagateTextLimit(propagateTextLimit, isOnDevice) ? ( -
    • +
    • {profileElementTexts[0].kind === 'step' ? profileElementTexts[0].stepText : profileElementTexts[0].cycleText} @@ -264,30 +250,18 @@ function ThermocyclerRunExtendedProfile( ) : ( profileElementTexts.map((element, index: number) => element.kind === 'step' ? ( -
    • +
    • {' '} {element.stepText}
    • ) : ( -
    • +
    • {element.cycleText}
        {element.stepTexts.map( ({ stepText }, stepIndex: number) => (
      • {' '} @@ -305,3 +279,7 @@ function ThermocyclerRunExtendedProfile( ) } + +const LIST_STYLE = css` + margin-left: ${SPACING.spacing4}; +` diff --git a/app/src/molecules/FileUpload/__tests__/FileUpload.test.tsx b/app/src/molecules/FileUpload/__tests__/FileUpload.test.tsx index cd5b5adfd82..9f3c22db269 100644 --- a/app/src/molecules/FileUpload/__tests__/FileUpload.test.tsx +++ b/app/src/molecules/FileUpload/__tests__/FileUpload.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { beforeEach, describe, expect, it, vi } from 'vitest' import { screen } from '@testing-library/react' @@ -7,7 +6,9 @@ import { i18n } from '/app/i18n' import { FileUpload } from '..' import testFile from './test-file.png' -const render = (props: React.ComponentProps) => { +import type { ComponentProps } from 'react' + +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, })[0] @@ -16,7 +17,7 @@ const render = (props: React.ComponentProps) => { const handleClick = vi.fn() describe('FileUpload', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { const file = new File([testFile], 'a-file-to-test.png') diff --git a/app/src/molecules/GenericWizardTile/__tests__/GenericWizardTile.test.tsx b/app/src/molecules/GenericWizardTile/__tests__/GenericWizardTile.test.tsx index 1f53800ff6d..ee1d680d4d2 100644 --- a/app/src/molecules/GenericWizardTile/__tests__/GenericWizardTile.test.tsx +++ b/app/src/molecules/GenericWizardTile/__tests__/GenericWizardTile.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import '@testing-library/jest-dom/vitest' import { describe, it, expect, vi, beforeEach } from 'vitest' import { fireEvent, screen } from '@testing-library/react' @@ -7,16 +6,18 @@ import { renderWithProviders } from '/app/__testing-utils__' import { getIsOnDevice } from '/app/redux/config' import { GenericWizardTile } from '..' +import type { ComponentProps } from 'react' + vi.mock('/app/redux/config') -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, })[0] } describe('GenericWizardTile', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { diff --git a/app/src/molecules/GenericWizardTile/index.tsx b/app/src/molecules/GenericWizardTile/index.tsx index 24883a6ffea..3948daa292a 100644 --- a/app/src/molecules/GenericWizardTile/index.tsx +++ b/app/src/molecules/GenericWizardTile/index.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { useSelector } from 'react-redux' import styled, { css } from 'styled-components' import { useTranslation } from 'react-i18next' @@ -26,6 +25,8 @@ import { getIsOnDevice } from '/app/redux/config' import { NeedHelpLink } from '/app/molecules/OT2CalibrationNeedHelpLink' import { SmallButton, TextOnlyButton } from '/app/atoms/buttons' +import type { ReactNode } from 'react' + const ALIGN_BUTTONS = css` align-items: ${ALIGN_FLEX_END}; @@ -59,13 +60,13 @@ const TILE_CONTAINER_STYLE = css` } ` export interface GenericWizardTileProps { - rightHandBody: React.ReactNode - bodyText: React.ReactNode - header: string | React.ReactNode + rightHandBody: ReactNode + bodyText: ReactNode + header: string | ReactNode getHelp?: string back?: () => void proceed?: () => void - proceedButtonText?: React.ReactNode + proceedButtonText?: ReactNode proceedIsDisabled?: boolean proceedButton?: JSX.Element backIsDisabled?: boolean diff --git a/app/src/molecules/InProgressModal/InProgressModal.tsx b/app/src/molecules/InProgressModal/InProgressModal.tsx index c6fefe761a2..82693b34429 100644 --- a/app/src/molecules/InProgressModal/InProgressModal.tsx +++ b/app/src/molecules/InProgressModal/InProgressModal.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { css } from 'styled-components' import { ALIGN_CENTER, @@ -13,9 +12,11 @@ import { TYPOGRAPHY, } from '@opentrons/components' +import type { ReactNode } from 'react' + interface Props { // optional override of the spinner - alternativeSpinner?: React.ReactNode + alternativeSpinner?: ReactNode description?: string body?: string children?: JSX.Element diff --git a/app/src/molecules/InProgressModal/__tests__/InProgressModal.test.tsx b/app/src/molecules/InProgressModal/__tests__/InProgressModal.test.tsx index f670fa221c3..db7ac2294cf 100644 --- a/app/src/molecules/InProgressModal/__tests__/InProgressModal.test.tsx +++ b/app/src/molecules/InProgressModal/__tests__/InProgressModal.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { screen } from '@testing-library/react' import { describe, it, beforeEach, vi } from 'vitest' import { i18n } from '/app/i18n' @@ -6,15 +5,17 @@ import { getIsOnDevice } from '/app/redux/config' import { renderWithProviders } from '/app/__testing-utils__' import { InProgressModal } from '../InProgressModal' +import type { ComponentProps } from 'react' + vi.mock('/app/redux/config') -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, })[0] } describe('InProgressModal', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { vi.mocked(getIsOnDevice).mockReturnValue(false) }) diff --git a/app/src/molecules/InfoMessage/__tests__/InfoMessage.test.tsx b/app/src/molecules/InfoMessage/__tests__/InfoMessage.test.tsx index 5ff6948976f..378e1eff503 100644 --- a/app/src/molecules/InfoMessage/__tests__/InfoMessage.test.tsx +++ b/app/src/molecules/InfoMessage/__tests__/InfoMessage.test.tsx @@ -1,18 +1,19 @@ -import type * as React from 'react' import { describe, it, beforeEach } from 'vitest' import { renderWithProviders } from '/app/__testing-utils__' import { screen } from '@testing-library/react' import { i18n } from '/app/i18n' import { InfoMessage } from '..' -const render = (props: React.ComponentProps) => { +import type { ComponentProps } from 'react' + +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, })[0] } describe('InfoMessage', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { diff --git a/app/src/molecules/InstrumentCard/MenuOverlay.tsx b/app/src/molecules/InstrumentCard/MenuOverlay.tsx index 674f5659164..18db4780d18 100644 --- a/app/src/molecules/InstrumentCard/MenuOverlay.tsx +++ b/app/src/molecules/InstrumentCard/MenuOverlay.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { Fragment } from 'react' import { BORDERS, @@ -12,11 +12,12 @@ import { import { Divider } from '/app/atoms/structure' +import type { MouseEventHandler, MouseEvent, ReactNode } from 'react' import type { StyleProps } from '@opentrons/components' export interface MenuOverlayItemProps { - label: React.ReactNode - onClick: React.MouseEventHandler + label: ReactNode + onClick: MouseEventHandler disabled?: boolean } @@ -41,14 +42,14 @@ export function MenuOverlay(props: MenuOverlayProps): JSX.Element { right="0" whiteSpace={NO_WRAP} zIndex={10} - onClick={(e: React.MouseEvent) => { + onClick={(e: MouseEvent) => { e.preventDefault() e.stopPropagation() setShowMenuOverlay(false) }} > {menuOverlayItems.map((menuOverlayItem, i) => ( - + {/* insert a divider before the last item if desired */} {hasDivider && i === menuOverlayItems.length - 1 ? ( @@ -59,7 +60,7 @@ export function MenuOverlay(props: MenuOverlayProps): JSX.Element { > {menuOverlayItem.label} - + ))} ) diff --git a/app/src/molecules/InstrumentCard/index.tsx b/app/src/molecules/InstrumentCard/index.tsx index bcb2ccb47ac..2cd838a08bd 100644 --- a/app/src/molecules/InstrumentCard/index.tsx +++ b/app/src/molecules/InstrumentCard/index.tsx @@ -1,5 +1,3 @@ -import type * as React from 'react' - import { ALIGN_CENTER, ALIGN_FLEX_START, @@ -22,6 +20,7 @@ import flexGripper from '/app/assets/images/flex_gripper.png' import { MenuOverlay } from './MenuOverlay' +import type { ReactNode } from 'react' import type { InstrumentDiagramProps, StyleProps } from '@opentrons/components' import type { MenuOverlayItemProps } from './MenuOverlay' @@ -33,7 +32,7 @@ interface InstrumentCardProps extends StyleProps { instrumentDiagramProps?: InstrumentDiagramProps // special casing the gripper at least for now isGripperAttached?: boolean - banner?: React.ReactNode + banner?: ReactNode isEstopNotDisengaged: boolean } @@ -91,7 +90,7 @@ export function InstrumentCard(props: InstrumentCardProps): JSX.Element { pipetteSpecs={instrumentDiagramProps.pipetteSpecs} mount={instrumentDiagramProps.mount} transform="scale(0.3)" - transformOrigin={'-5% 52%'} + transformOrigin="-5% 52%" /> ) : null} diff --git a/app/src/molecules/InterventionModal/DeckMapContent.tsx b/app/src/molecules/InterventionModal/DeckMapContent.tsx index a45bc920e0a..6bafed02bd1 100644 --- a/app/src/molecules/InterventionModal/DeckMapContent.tsx +++ b/app/src/molecules/InterventionModal/DeckMapContent.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useEffect } from 'react' import { css } from 'styled-components' import { Box, @@ -11,6 +11,7 @@ import { useDeckLocationSelect, } from '@opentrons/components' +import type { ComponentProps } from 'react' import type { LabwareDefinition2, RobotType, @@ -22,7 +23,7 @@ export type MapKind = 'intervention' | 'deck-config' export interface InterventionStyleDeckMapContentProps extends Pick< - React.ComponentProps, + ComponentProps, 'deckConfig' | 'robotType' | 'labwareOnDeck' | 'modulesOnDeck' > { kind: 'intervention' @@ -107,7 +108,7 @@ function DeckConfigStyleDeckMapContent({ robotType, 'default' ) - React.useEffect(() => { + useEffect(() => { setSelectedLocation != null && setSelectedLocation(selectedLocation) }, [selectedLocation, setSelectedLocation]) return <>{DeckLocationSelect} diff --git a/app/src/molecules/InterventionModal/InterventionContent/index.tsx b/app/src/molecules/InterventionModal/InterventionContent/index.tsx index 8d2bbba9c8c..e9b156cf0e6 100644 --- a/app/src/molecules/InterventionModal/InterventionContent/index.tsx +++ b/app/src/molecules/InterventionModal/InterventionContent/index.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { Flex, StyledText, @@ -7,15 +6,17 @@ import { RESPONSIVENESS, } from '@opentrons/components' import { InlineNotification } from '/app/atoms/InlineNotification' - import { InterventionInfo } from './InterventionInfo' + +import type { ComponentProps } from 'react' + export type { InterventionInfoProps } from './InterventionInfo' export { InterventionInfo } export interface InterventionContentProps { headline: string - infoProps: React.ComponentProps - notificationProps?: React.ComponentProps + infoProps: ComponentProps + notificationProps?: ComponentProps } export function InterventionContent({ diff --git a/app/src/molecules/InterventionModal/ModalContentMixed.tsx b/app/src/molecules/InterventionModal/ModalContentMixed.tsx index 4c41003c3d8..c7f4a43ea6e 100644 --- a/app/src/molecules/InterventionModal/ModalContentMixed.tsx +++ b/app/src/molecules/InterventionModal/ModalContentMixed.tsx @@ -152,7 +152,7 @@ function ModalContentMixedSpinner( return ( + onChange?: ChangeEventHandler } export interface ModalContentOneColSimpleButtonsProps { @@ -20,14 +22,14 @@ export interface ModalContentOneColSimpleButtonsProps { firstButton: ButtonProps secondButton: ButtonProps furtherButtons?: ButtonProps[] - onSelect?: React.ChangeEventHandler + onSelect?: ChangeEventHandler initialSelected?: string } export function ModalContentOneColSimpleButtons( props: ModalContentOneColSimpleButtonsProps ): JSX.Element { - const [selected, setSelected] = React.useState( + const [selected, setSelected] = useState( props.initialSelected ?? null ) const furtherButtons = props.furtherButtons ?? [] diff --git a/app/src/molecules/InterventionModal/OneColumn.tsx b/app/src/molecules/InterventionModal/OneColumn.tsx index 35a37dd10f9..8fca9f26ff3 100644 --- a/app/src/molecules/InterventionModal/OneColumn.tsx +++ b/app/src/molecules/InterventionModal/OneColumn.tsx @@ -1,14 +1,14 @@ -import type * as React from 'react' - import { Flex, DIRECTION_COLUMN, JUSTIFY_SPACE_BETWEEN, } from '@opentrons/components' + +import type { ReactNode } from 'react' import type { StyleProps } from '@opentrons/components' export interface OneColumnProps extends StyleProps { - children: React.ReactNode + children: ReactNode } export function OneColumn({ diff --git a/app/src/molecules/InterventionModal/OneColumnOrTwoColumn.tsx b/app/src/molecules/InterventionModal/OneColumnOrTwoColumn.tsx index db25d343be5..0a02f3397ac 100644 --- a/app/src/molecules/InterventionModal/OneColumnOrTwoColumn.tsx +++ b/app/src/molecules/InterventionModal/OneColumnOrTwoColumn.tsx @@ -1,5 +1,3 @@ -import type * as React from 'react' - import { css } from 'styled-components' import { Flex, @@ -9,11 +7,13 @@ import { WRAP, RESPONSIVENESS, } from '@opentrons/components' -import type { StyleProps } from '@opentrons/components' import { TWO_COLUMN_ELEMENT_MIN_WIDTH } from './constants' +import type { ReactNode } from 'react' +import type { StyleProps } from '@opentrons/components' + export interface OneColumnOrTwoColumnProps extends StyleProps { - children: [React.ReactNode, React.ReactNode] + children: [ReactNode, ReactNode] } export function OneColumnOrTwoColumn({ diff --git a/app/src/molecules/InterventionModal/TwoColumn.stories.tsx b/app/src/molecules/InterventionModal/TwoColumn.stories.tsx index a1d83ca750d..c233b623bd0 100644 --- a/app/src/molecules/InterventionModal/TwoColumn.stories.tsx +++ b/app/src/molecules/InterventionModal/TwoColumn.stories.tsx @@ -63,7 +63,7 @@ function Image({ imageUrl }: ImageProps): JSX.Element | null { const hasComponent = imageUrl != null && imageUrl.length > 0 && imageUrl[0].length > 0 return hasComponent ? ( - + ) : null } diff --git a/app/src/molecules/InterventionModal/TwoColumn.tsx b/app/src/molecules/InterventionModal/TwoColumn.tsx index fc9072232be..600386ec60f 100644 --- a/app/src/molecules/InterventionModal/TwoColumn.tsx +++ b/app/src/molecules/InterventionModal/TwoColumn.tsx @@ -1,11 +1,11 @@ -import type * as React from 'react' - import { Flex, Box, DIRECTION_ROW, SPACING, WRAP } from '@opentrons/components' import type { StyleProps } from '@opentrons/components' import { TWO_COLUMN_ELEMENT_MIN_WIDTH } from './constants' +import type { ReactNode } from 'react' + export interface TwoColumnProps extends StyleProps { - children: [React.ReactNode, React.ReactNode] + children: [ReactNode, ReactNode] } export function TwoColumn({ diff --git a/app/src/molecules/InterventionModal/__tests__/InterventionModal.test.tsx b/app/src/molecules/InterventionModal/__tests__/InterventionModal.test.tsx index a063bee13bc..bdbe4c95e70 100644 --- a/app/src/molecules/InterventionModal/__tests__/InterventionModal.test.tsx +++ b/app/src/molecules/InterventionModal/__tests__/InterventionModal.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { vi, describe, it, expect, beforeEach } from 'vitest' import { when } from 'vitest-when' import '@testing-library/jest-dom/vitest' @@ -12,6 +11,7 @@ import { getIsOnDevice } from '/app/redux/config' import { InterventionModal } from '../' +import type { ComponentProps } from 'react' import type { ModalType } from '../' import type { State } from '/app/redux/types' @@ -23,7 +23,7 @@ const MOCK_STATE: State = { }, } as any -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, initialState: MOCK_STATE, @@ -31,7 +31,7 @@ const render = (props: React.ComponentProps) => { } describe('InterventionModal', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { diff --git a/app/src/molecules/InterventionModal/__tests__/ModalContentOneColSimpleButtons.test.tsx b/app/src/molecules/InterventionModal/__tests__/ModalContentOneColSimpleButtons.test.tsx index 27a40d82ad4..bb25d211619 100644 --- a/app/src/molecules/InterventionModal/__tests__/ModalContentOneColSimpleButtons.test.tsx +++ b/app/src/molecules/InterventionModal/__tests__/ModalContentOneColSimpleButtons.test.tsx @@ -1,9 +1,10 @@ -import type * as React from 'react' import { vi, describe, it, expect } from 'vitest' import { render, screen, fireEvent } from '@testing-library/react' import { ModalContentOneColSimpleButtons } from '../ModalContentOneColSimpleButtons' +import type { ChangeEventHandler } from 'react' + /* eslint-disable testing-library/no-node-access */ const inputElForButtonFromButtonText = (text: string): HTMLInputElement => ((screen.getByText(text)?.parentElement?.parentElement @@ -17,7 +18,7 @@ describe('InterventionModal', () => { it('renders headline', () => { render( @@ -27,7 +28,7 @@ describe('InterventionModal', () => { it('renders buttons', () => { render( { it('enforces single-item selection', () => { render( { it('can start with a button selected', () => { render( ) expect(inputElForButtonFromButtonText('first button').checked).toBeFalsy() @@ -84,11 +85,11 @@ describe('InterventionModal', () => { const onChange = vi.fn() render( , + onChange: onChange as ChangeEventHandler, }} secondButton={{ label: 'second button', value: 'second' }} furtherButtons={[{ label: 'third button', value: 'third' }]} @@ -110,7 +111,7 @@ describe('InterventionModal', () => { const onSelect = vi.fn() render( void /** overall style hint */ @@ -128,7 +128,7 @@ export interface InterventionModalProps { /* Optional icon size override. */ iconSize?: string /** modal contents */ - children: React.ReactNode + children: ReactNode } export function InterventionModal({ @@ -160,7 +160,7 @@ export function InterventionModal({ {...modalStyle} flexDirection={DIRECTION_COLUMN} border={border} - onClick={(e: React.MouseEvent) => { + onClick={(e: MouseEvent) => { e.stopPropagation() }} > diff --git a/app/src/molecules/InterventionModal/story-utils/StandIn.tsx b/app/src/molecules/InterventionModal/story-utils/StandIn.tsx index 33eb868816d..56153551df3 100644 --- a/app/src/molecules/InterventionModal/story-utils/StandIn.tsx +++ b/app/src/molecules/InterventionModal/story-utils/StandIn.tsx @@ -1,14 +1,14 @@ -import type * as React from 'react' import { Box, BORDERS } from '@opentrons/components' +import type { ReactNode } from 'react' export function StandInContent({ children, }: { - children?: React.ReactNode + children?: ReactNode }): JSX.Element { return ( ( + const [currentPlane, setCurrentPlane] = useState( initialPlane ?? planes[0] ) const { t } = useTranslation(['robot_calibration']) - const handlePlane = (event: React.MouseEvent): void => { + const handlePlane = (event: MouseEvent): void => { setCurrentPlane(event.currentTarget.value as Plane) event.currentTarget.blur() } @@ -449,7 +450,7 @@ export function TouchDirectionControl( props: DirectionControlProps ): JSX.Element { const { planes, jog, stepSize, initialPlane } = props - const [currentPlane, setCurrentPlane] = React.useState( + const [currentPlane, setCurrentPlane] = useState( initialPlane ?? planes[0] ) const { i18n, t } = useTranslation(['robot_calibration']) diff --git a/app/src/molecules/JogControls/StepSizeControl.tsx b/app/src/molecules/JogControls/StepSizeControl.tsx index 77d1e65b870..e8fd2d98860 100644 --- a/app/src/molecules/JogControls/StepSizeControl.tsx +++ b/app/src/molecules/JogControls/StepSizeControl.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { useTranslation } from 'react-i18next' import styled, { css } from 'styled-components' import { @@ -22,6 +21,7 @@ import { import { ControlContainer } from './ControlContainer' import { TouchControlButton } from './TouchControlButton' +import type { MouseEvent } from 'react' import type { StepSize } from './types' const JUMP_SIZE_SUBTITLE = '- / +' @@ -107,9 +107,7 @@ export function StepSizeControl(props: StepSizeControlProps): JSX.Element { if (i > 0) setCurrentStepSize(stepSizes[i - 1]) } - const handleStepSelect = ( - event: React.MouseEvent - ): void => { + const handleStepSelect = (event: MouseEvent): void => { setCurrentStepSize(Number(event.currentTarget.value) as StepSize) event.currentTarget.blur() } diff --git a/app/src/molecules/JogControls/index.tsx b/app/src/molecules/JogControls/index.tsx index c9d0d7b49f0..0208739d025 100644 --- a/app/src/molecules/JogControls/index.tsx +++ b/app/src/molecules/JogControls/index.tsx @@ -1,5 +1,5 @@ // jog controls component -import * as React from 'react' +import { useState } from 'react' import { css } from 'styled-components' import { Flex, @@ -20,15 +20,16 @@ import { DEFAULT_STEP_SIZES, } from './constants' -import type { Jog, Plane, StepSize } from './types' +import type { ReactNode } from 'react' import type { StyleProps } from '@opentrons/components' +import type { Jog, Plane, StepSize } from './types' export type { Jog } export interface JogControlsProps extends StyleProps { jog: Jog planes?: Plane[] stepSizes?: StepSize[] - auxiliaryControl?: React.ReactNode | null + auxiliaryControl?: ReactNode | null directionControlButtonColor?: string initialPlane?: Plane isOnDevice?: boolean @@ -53,9 +54,7 @@ export function JogControls(props: JogControlsProps): JSX.Element { isOnDevice = false, ...styleProps } = props - const [currentStepSize, setCurrentStepSize] = React.useState( - stepSizes[0] - ) + const [currentStepSize, setCurrentStepSize] = useState(stepSizes[0]) const controls = isOnDevice ? ( <> diff --git a/app/src/molecules/MiniCard/__tests__/MiniCard.test.tsx b/app/src/molecules/MiniCard/__tests__/MiniCard.test.tsx index fcc76f38505..0e26b805f16 100644 --- a/app/src/molecules/MiniCard/__tests__/MiniCard.test.tsx +++ b/app/src/molecules/MiniCard/__tests__/MiniCard.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { describe, it, expect, vi, beforeEach } from 'vitest' import '@testing-library/jest-dom/vitest' import { fireEvent, screen } from '@testing-library/react' @@ -6,12 +5,14 @@ import { COLORS, SPACING, BORDERS, CURSOR_POINTER } from '@opentrons/components' import { renderWithProviders } from '/app/__testing-utils__' import { MiniCard } from '../' -const render = (props: React.ComponentProps) => { +import type { ComponentProps } from 'react' + +const render = (props: ComponentProps) => { return renderWithProviders()[0] } describe('MiniCard', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { diff --git a/app/src/molecules/MiniCard/index.tsx b/app/src/molecules/MiniCard/index.tsx index b65ccbbb59d..d95c42e149f 100644 --- a/app/src/molecules/MiniCard/index.tsx +++ b/app/src/molecules/MiniCard/index.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { css } from 'styled-components' import { BORDERS, @@ -8,12 +7,13 @@ import { SPACING, } from '@opentrons/components' +import type { ReactNode } from 'react' import type { StyleProps } from '@opentrons/components' interface MiniCardProps extends StyleProps { onClick: () => void isSelected: boolean - children: React.ReactNode + children: ReactNode isError?: boolean isWarning?: boolean } diff --git a/app/src/molecules/ModuleIcon/__tests__/ModuleIcon.test.tsx b/app/src/molecules/ModuleIcon/__tests__/ModuleIcon.test.tsx index 73c44639e51..6e20f8997fa 100644 --- a/app/src/molecules/ModuleIcon/__tests__/ModuleIcon.test.tsx +++ b/app/src/molecules/ModuleIcon/__tests__/ModuleIcon.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { COLORS, SPACING } from '@opentrons/components' import { describe, it, expect, vi, beforeEach } from 'vitest' import { screen } from '@testing-library/react' @@ -6,6 +5,7 @@ import '@testing-library/jest-dom/vitest' import { renderWithProviders } from '/app/__testing-utils__' import { ModuleIcon } from '../' +import type { ComponentProps } from 'react' import type { AttachedModule } from '/app/redux/modules/types' import type * as OpentronsComponents from '@opentrons/components' @@ -17,7 +17,7 @@ vi.mock('@opentrons/components', async importOriginal => { } }) -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders()[0] } @@ -46,7 +46,7 @@ const mockHeaterShakerModule = { } as AttachedModule describe('ModuleIcon', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { diff --git a/app/src/molecules/ModuleInfo/__tests__/ModuleInfo.test.tsx b/app/src/molecules/ModuleInfo/__tests__/ModuleInfo.test.tsx index 1d732faeb18..de5eef75577 100644 --- a/app/src/molecules/ModuleInfo/__tests__/ModuleInfo.test.tsx +++ b/app/src/molecules/ModuleInfo/__tests__/ModuleInfo.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { screen } from '@testing-library/react' import { describe, it, vi, beforeEach, expect } from 'vitest' import '@testing-library/jest-dom/vitest' @@ -7,11 +6,13 @@ import { when } from 'vitest-when' import { i18n } from '/app/i18n' import { ModuleInfo } from '../ModuleInfo' import { useRunHasStarted } from '/app/resources/runs' + +import type { ComponentProps } from 'react' import type { ModuleModel, ModuleType } from '@opentrons/shared-data' vi.mock('/app/resources/runs') -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, })[0] @@ -26,7 +27,7 @@ const mockTCModule = { const MOCK_RUN_ID = '1' describe('ModuleInfo', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { moduleModel: mockTCModule.model, diff --git a/app/src/molecules/NavTab/__tests__/NavTab.test.tsx b/app/src/molecules/NavTab/__tests__/NavTab.test.tsx index 176c76b60cc..e6a2c9a504f 100644 --- a/app/src/molecules/NavTab/__tests__/NavTab.test.tsx +++ b/app/src/molecules/NavTab/__tests__/NavTab.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { fireEvent, screen } from '@testing-library/react' import '@testing-library/jest-dom/vitest' import { describe, it, expect, beforeEach } from 'vitest' @@ -7,7 +6,9 @@ import { SPACING, COLORS, TYPOGRAPHY, BORDERS } from '@opentrons/components' import { renderWithProviders } from '/app/__testing-utils__' import { NavTab } from '..' -const render = (props: React.ComponentProps) => { +import type { ComponentProps } from 'react' + +const render = (props: ComponentProps) => { return renderWithProviders( @@ -16,7 +17,7 @@ const render = (props: React.ComponentProps) => { } describe('NavTab', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { diff --git a/app/src/molecules/NavTab/index.tsx b/app/src/molecules/NavTab/index.tsx index e170da56e43..6cdf6cb0e6b 100644 --- a/app/src/molecules/NavTab/index.tsx +++ b/app/src/molecules/NavTab/index.tsx @@ -3,6 +3,8 @@ import { NavLink } from 'react-router-dom' import { BORDERS, COLORS, SPACING, TYPOGRAPHY } from '@opentrons/components' +import type { ComponentProps } from 'react' + export const TAB_BORDER_STYLE = css` border-bottom-style: ${BORDERS.styleSolid}; border-bottom-width: 2px; @@ -15,7 +17,7 @@ interface NavTabProps { disabled?: boolean } -const StyledNavLink = styled(NavLink)>` +const StyledNavLink = styled(NavLink)>` padding: 0 ${SPACING.spacing4} ${SPACING.spacing8}; ${TYPOGRAPHY.labelSemiBold} color: ${COLORS.grey50}; diff --git a/app/src/molecules/ODDBackButton/__tests__/ODDBackButton.test.tsx b/app/src/molecules/ODDBackButton/__tests__/ODDBackButton.test.tsx index 2b520279cf8..5b487b2e7dc 100644 --- a/app/src/molecules/ODDBackButton/__tests__/ODDBackButton.test.tsx +++ b/app/src/molecules/ODDBackButton/__tests__/ODDBackButton.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import '@testing-library/jest-dom/vitest' import { describe, it, expect, vi, beforeEach } from 'vitest' import { fireEvent, screen } from '@testing-library/react' @@ -6,12 +5,14 @@ import { COLORS } from '@opentrons/components' import { ODDBackButton } from '..' import { renderWithProviders } from '/app/__testing-utils__' -const render = (props: React.ComponentProps) => { +import type { ComponentProps } from 'react' + +const render = (props: ComponentProps) => { return renderWithProviders()[0] } describe('ODDBackButton', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { diff --git a/app/src/molecules/ODDBackButton/index.tsx b/app/src/molecules/ODDBackButton/index.tsx index 396feaa5e19..0390dc21284 100644 --- a/app/src/molecules/ODDBackButton/index.tsx +++ b/app/src/molecules/ODDBackButton/index.tsx @@ -1,5 +1,3 @@ -import type * as React from 'react' - import { ALIGN_CENTER, Btn, @@ -11,8 +9,10 @@ import { TYPOGRAPHY, } from '@opentrons/components' +import type { HTMLProps } from 'react' + export function ODDBackButton( - props: React.HTMLProps + props: HTMLProps ): JSX.Element { const { onClick, label } = props diff --git a/app/src/molecules/OT2CalibrationNeedHelpLink/NeedHelpLink.tsx b/app/src/molecules/OT2CalibrationNeedHelpLink/NeedHelpLink.tsx index a87a7808827..6dd9a5f8d86 100644 --- a/app/src/molecules/OT2CalibrationNeedHelpLink/NeedHelpLink.tsx +++ b/app/src/molecules/OT2CalibrationNeedHelpLink/NeedHelpLink.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { useTranslation } from 'react-i18next' import { Flex, @@ -11,9 +10,11 @@ import { SPACING, } from '@opentrons/components' +import type { ComponentProps } from 'react' + const SUPPORT_PAGE_URL = 'https://support.opentrons.com/s/ot2-calibration' -interface NeedHelpLinkProps extends React.ComponentProps { +interface NeedHelpLinkProps extends ComponentProps { href?: string } diff --git a/app/src/molecules/OddModal/OddModal.tsx b/app/src/molecules/OddModal/OddModal.tsx index 9b32974000c..e95aaa1d9a4 100644 --- a/app/src/molecules/OddModal/OddModal.tsx +++ b/app/src/molecules/OddModal/OddModal.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { ALIGN_CENTER, BORDERS, @@ -11,14 +10,15 @@ import { import { BackgroundOverlay } from '../BackgroundOverlay' import { OddModalHeader } from './OddModalHeader' +import type { MouseEvent, MouseEventHandler, ReactNode } from 'react' import type { StyleProps } from '@opentrons/components' import type { OddModalHeaderBaseProps, ModalSize } from './types' interface OddModalProps extends StyleProps { /** clicking anywhere outside of the modal closes it */ - onOutsideClick?: React.MouseEventHandler + onOutsideClick?: MouseEventHandler /** modal content */ - children: React.ReactNode + children: ReactNode /** for small, medium, or large modal sizes, medium by default */ modalSize?: ModalSize /** see OddModalHeader component for more details */ @@ -66,7 +66,7 @@ export function OddModal(props: OddModalProps): JSX.Element { margin={SPACING.spacing32} flexDirection={DIRECTION_COLUMN} aria-label={`modal_${modalSize}`} - onClick={(e: React.MouseEvent) => { + onClick={(e: MouseEvent) => { e.stopPropagation() }} > diff --git a/app/src/molecules/OddModal/__tests__/OddModal.test.tsx b/app/src/molecules/OddModal/__tests__/OddModal.test.tsx index d8c129a8b01..9d9097b4729 100644 --- a/app/src/molecules/OddModal/__tests__/OddModal.test.tsx +++ b/app/src/molecules/OddModal/__tests__/OddModal.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { fireEvent, screen } from '@testing-library/react' import '@testing-library/jest-dom/vitest' import { describe, it, expect, vi, beforeEach } from 'vitest' @@ -7,14 +6,16 @@ import { renderWithProviders } from '/app/__testing-utils__' import { OddModalHeader } from '../OddModalHeader' import { OddModal } from '../OddModal' +import type { ComponentProps } from 'react' + vi.mock('../OddModalHeader') -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders()[0] } describe('OddModal', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { onOutsideClick: vi.fn(), diff --git a/app/src/molecules/OddModal/__tests__/OddModalHeader.test.tsx b/app/src/molecules/OddModal/__tests__/OddModalHeader.test.tsx index e824e49648c..94499d35abf 100644 --- a/app/src/molecules/OddModal/__tests__/OddModalHeader.test.tsx +++ b/app/src/molecules/OddModal/__tests__/OddModalHeader.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import '@testing-library/jest-dom/vitest' import { describe, it, expect, vi, beforeEach } from 'vitest' import { fireEvent, screen } from '@testing-library/react' @@ -6,12 +5,14 @@ import { COLORS } from '@opentrons/components' import { renderWithProviders } from '/app/__testing-utils__' import { OddModalHeader } from '../OddModalHeader' -const render = (props: React.ComponentProps) => { +import type { ComponentProps } from 'react' + +const render = (props: ComponentProps) => { return renderWithProviders()[0] } describe('OddModalHeader', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { title: 'title', diff --git a/app/src/molecules/OddModal/types.ts b/app/src/molecules/OddModal/types.ts index b0fa6d103ae..e4f07cc52ad 100644 --- a/app/src/molecules/OddModal/types.ts +++ b/app/src/molecules/OddModal/types.ts @@ -1,10 +1,11 @@ +import type { MouseEventHandler } from 'react' import type { IconName, StyleProps } from '@opentrons/components' export type ModalSize = 'small' | 'medium' | 'large' export interface OddModalHeaderBaseProps extends StyleProps { title: string | JSX.Element - onClick?: React.MouseEventHandler + onClick?: MouseEventHandler hasExitIcon?: boolean iconName?: IconName iconColor?: string diff --git a/app/src/molecules/OffsetVector/__tests__/OffsetVector.test.tsx b/app/src/molecules/OffsetVector/__tests__/OffsetVector.test.tsx index bb247c0175a..e3b4c87da6c 100644 --- a/app/src/molecules/OffsetVector/__tests__/OffsetVector.test.tsx +++ b/app/src/molecules/OffsetVector/__tests__/OffsetVector.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { screen } from '@testing-library/react' import '@testing-library/jest-dom/vitest' import { describe, it, expect, beforeEach } from 'vitest' @@ -7,12 +6,14 @@ import { renderWithProviders } from '/app/__testing-utils__' import { OffsetVector } from '../' -const render = (props: React.ComponentProps) => { +import type { ComponentProps } from 'react' + +const render = (props: ComponentProps) => { return renderWithProviders()[0] } describe('OffsetVector', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { diff --git a/app/src/molecules/OffsetVector/index.tsx b/app/src/molecules/OffsetVector/index.tsx index 155019e8074..af4d8c5dc68 100644 --- a/app/src/molecules/OffsetVector/index.tsx +++ b/app/src/molecules/OffsetVector/index.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { Flex, SPACING, @@ -6,13 +5,14 @@ import { LegacyStyledText, } from '@opentrons/components' +import type { ComponentProps } from 'react' import type { StyleProps } from '@opentrons/components' interface OffsetVectorProps extends StyleProps { x: number y: number z: number - as?: React.ComponentProps['as'] + as?: ComponentProps['as'] } export function OffsetVector(props: OffsetVectorProps): JSX.Element { diff --git a/app/src/molecules/SimpleWizardBody/SimpleWizardBodyContent.tsx b/app/src/molecules/SimpleWizardBody/SimpleWizardBodyContent.tsx index 16113e28c9a..1a179f1b987 100644 --- a/app/src/molecules/SimpleWizardBody/SimpleWizardBodyContent.tsx +++ b/app/src/molecules/SimpleWizardBody/SimpleWizardBodyContent.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { useSelector } from 'react-redux' import { css } from 'styled-components' import { @@ -21,13 +20,15 @@ import SuccessIcon from '/app/assets/images/icon_success.png' import { getIsOnDevice } from '/app/redux/config' import { Skeleton } from '/app/atoms/Skeleton' + +import type { ReactNode } from 'react' import type { RobotType } from '@opentrons/shared-data' interface Props { iconColor: string header: string isSuccess: boolean - children?: React.ReactNode + children?: ReactNode subHeader?: string | JSX.Element isPending?: boolean robotType?: RobotType diff --git a/app/src/molecules/SimpleWizardBody/SimpleWizardInProgressBody.tsx b/app/src/molecules/SimpleWizardBody/SimpleWizardInProgressBody.tsx index 10882025dfc..92f65aa2cf2 100644 --- a/app/src/molecules/SimpleWizardBody/SimpleWizardInProgressBody.tsx +++ b/app/src/molecules/SimpleWizardBody/SimpleWizardInProgressBody.tsx @@ -1,9 +1,10 @@ -import type * as React from 'react' -import type { StyleProps } from '@opentrons/components' import { InProgressModal } from '../InProgressModal/InProgressModal' import { SimpleWizardBodyContainer } from './SimpleWizardBodyContainer' -export type SimpleWizardInProgressBodyProps = React.ComponentProps< +import type { ComponentProps } from 'react' +import type { StyleProps } from '@opentrons/components' + +export type SimpleWizardInProgressBodyProps = ComponentProps< typeof InProgressModal > & StyleProps diff --git a/app/src/molecules/SimpleWizardBody/__tests__/SimpleWizardBody.test.tsx b/app/src/molecules/SimpleWizardBody/__tests__/SimpleWizardBody.test.tsx index 9849e8fa03c..f21e14f0580 100644 --- a/app/src/molecules/SimpleWizardBody/__tests__/SimpleWizardBody.test.tsx +++ b/app/src/molecules/SimpleWizardBody/__tests__/SimpleWizardBody.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { screen } from '@testing-library/react' import { describe, it, expect, vi, beforeEach } from 'vitest' import { COLORS } from '@opentrons/components' @@ -7,14 +6,16 @@ import { Skeleton } from '/app/atoms/Skeleton' import { getIsOnDevice } from '/app/redux/config' import { SimpleWizardBody } from '..' +import type { ComponentProps } from 'react' + vi.mock('/app/atoms/Skeleton') vi.mock('/app/redux/config') -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders()[0] } describe('SimpleWizardBody', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { iconColor: COLORS.red60, diff --git a/app/src/molecules/SimpleWizardBody/index.tsx b/app/src/molecules/SimpleWizardBody/index.tsx index b554b71b90e..c49c49be2a8 100644 --- a/app/src/molecules/SimpleWizardBody/index.tsx +++ b/app/src/molecules/SimpleWizardBody/index.tsx @@ -1,8 +1,9 @@ -import type * as React from 'react' - import { SimpleWizardBodyContainer } from './SimpleWizardBodyContainer' import { SimpleWizardBodyContent } from './SimpleWizardBodyContent' import { SimpleWizardInProgressBody } from './SimpleWizardInProgressBody' + +import type { ComponentProps } from 'react' + export { SimpleWizardBodyContainer, SimpleWizardBodyContent, @@ -10,8 +11,8 @@ export { } export function SimpleWizardBody( - props: React.ComponentProps & - React.ComponentProps + props: ComponentProps & + ComponentProps ): JSX.Element { const { children, ...rest } = props return ( diff --git a/app/src/molecules/ToggleGroup/__tests__/useToggleGroup.test.tsx b/app/src/molecules/ToggleGroup/__tests__/useToggleGroup.test.tsx index c4829b79615..b90da347489 100644 --- a/app/src/molecules/ToggleGroup/__tests__/useToggleGroup.test.tsx +++ b/app/src/molecules/ToggleGroup/__tests__/useToggleGroup.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { Provider } from 'react-redux' import { createStore } from 'redux' import '@testing-library/jest-dom/vitest' @@ -7,6 +6,7 @@ import { renderHook, render, fireEvent, screen } from '@testing-library/react' import { useTrackEvent } from '/app/redux/analytics' import { useToggleGroup } from '../useToggleGroup' +import type { FunctionComponent, ReactNode } from 'react' import type { Store } from 'redux' import type { State } from '/app/redux/types' @@ -23,7 +23,7 @@ describe('useToggleGroup', () => { }) it('should return default selectedValue and toggle buttons', () => { - const wrapper: React.FunctionComponent<{ children: React.ReactNode }> = ({ + const wrapper: FunctionComponent<{ children: ReactNode }> = ({ children, }) => {children} @@ -35,7 +35,7 @@ describe('useToggleGroup', () => { expect(result.current[0]).toBe('List View') }) it('should record an analytics event for list view', async () => { - const wrapper: React.FunctionComponent<{ children: React.ReactNode }> = ({ + const wrapper: FunctionComponent<{ children: ReactNode }> = ({ children, }) => {children} @@ -53,7 +53,7 @@ describe('useToggleGroup', () => { }) }) it('should record an analytics event for map view', () => { - const wrapper: React.FunctionComponent<{ children: React.ReactNode }> = ({ + const wrapper: FunctionComponent<{ children: ReactNode }> = ({ children, }) => {children} diff --git a/app/src/molecules/ToggleGroup/useToggleGroup.tsx b/app/src/molecules/ToggleGroup/useToggleGroup.tsx index ebe3efd14c8..5b356ba74cd 100644 --- a/app/src/molecules/ToggleGroup/useToggleGroup.tsx +++ b/app/src/molecules/ToggleGroup/useToggleGroup.tsx @@ -1,13 +1,15 @@ -import * as React from 'react' +import { useState } from 'react' import { ToggleGroup } from '@opentrons/components' import { useTrackEvent } from '/app/redux/analytics' +import type { ReactNode } from 'react' + export const useToggleGroup = ( left: string, right: string, trackEventName?: string -): [string, React.ReactNode] => { - const [selectedValue, setSelectedValue] = React.useState(left) +): [string, ReactNode] => { + const [selectedValue, setSelectedValue] = useState(left) const trackEvent = useTrackEvent() const handleLeftClick = (): void => { setSelectedValue(left) diff --git a/app/src/molecules/UnorderedList/index.tsx b/app/src/molecules/UnorderedList/index.tsx index cf9937266a8..f48c3e685ba 100644 --- a/app/src/molecules/UnorderedList/index.tsx +++ b/app/src/molecules/UnorderedList/index.tsx @@ -1,9 +1,10 @@ -import type * as React from 'react' import { css } from 'styled-components' import { SPACING, LegacyStyledText } from '@opentrons/components' +import type { ReactNode } from 'react' + interface UnorderedListProps { - items: React.ReactNode[] + items: ReactNode[] } export function UnorderedList(props: UnorderedListProps): JSX.Element { const { items } = props diff --git a/app/src/molecules/UpdateBanner/__tests__/UpdateBanner.test.tsx b/app/src/molecules/UpdateBanner/__tests__/UpdateBanner.test.tsx index 52b074b37d8..1f8470efcd0 100644 --- a/app/src/molecules/UpdateBanner/__tests__/UpdateBanner.test.tsx +++ b/app/src/molecules/UpdateBanner/__tests__/UpdateBanner.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { describe, it, expect, vi, beforeEach } from 'vitest' import '@testing-library/jest-dom/vitest' import { when } from 'vitest-when' @@ -9,10 +8,12 @@ import { useIsEstopNotDisengaged } from '/app/resources/devices/hooks/useIsEstop import { UpdateBanner } from '..' import { renderWithProviders } from '/app/__testing-utils__' +import type { ComponentProps } from 'react' + vi.mock('/app/redux-resources/robots') vi.mock('/app/resources/devices/hooks/useIsEstopNotDisengaged') -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, initialState: { robotsByName: 'test' }, @@ -20,7 +21,7 @@ const render = (props: React.ComponentProps) => { } describe('Module Update Banner', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { diff --git a/app/src/molecules/UploadInput/index.tsx b/app/src/molecules/UploadInput/index.tsx index 77dc5a2616d..89877f38c33 100644 --- a/app/src/molecules/UploadInput/index.tsx +++ b/app/src/molecules/UploadInput/index.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useRef, useState } from 'react' import styled, { css } from 'styled-components' import { useTranslation } from 'react-i18next' import { @@ -18,6 +18,12 @@ import { TYPOGRAPHY, } from '@opentrons/components' +import type { + ChangeEventHandler, + DragEventHandler, + MouseEventHandler, +} from 'react' + const StyledLabel = styled.label` display: ${DISPLAY_FLEX}; cursor: ${CURSOR_POINTER}; @@ -66,39 +72,37 @@ export function UploadInput(props: UploadInputProps): JSX.Element | null { } = props const { t } = useTranslation('protocol_info') - const fileInput = React.useRef(null) - const [isFileOverDropZone, setIsFileOverDropZone] = React.useState( - false - ) - const [isHover, setIsHover] = React.useState(false) - const handleDrop: React.DragEventHandler = e => { + const fileInput = useRef(null) + const [isFileOverDropZone, setIsFileOverDropZone] = useState(false) + const [isHover, setIsHover] = useState(false) + const handleDrop: DragEventHandler = e => { e.preventDefault() e.stopPropagation() Array.from(e.dataTransfer.files).forEach(f => onUpload(f)) setIsFileOverDropZone(false) } - const handleDragEnter: React.DragEventHandler = e => { + const handleDragEnter: DragEventHandler = e => { e.preventDefault() e.stopPropagation() } - const handleDragLeave: React.DragEventHandler = e => { + const handleDragLeave: DragEventHandler = e => { e.preventDefault() e.stopPropagation() setIsFileOverDropZone(false) setIsHover(false) } - const handleDragOver: React.DragEventHandler = e => { + const handleDragOver: DragEventHandler = e => { e.preventDefault() e.stopPropagation() setIsFileOverDropZone(true) setIsHover(true) } - const handleClick: React.MouseEventHandler = _event => { + const handleClick: MouseEventHandler = _event => { onClick != null ? onClick() : fileInput.current?.click() } - const onChange: React.ChangeEventHandler = event => { + const onChange: ChangeEventHandler = event => { ;[...(event.target.files ?? [])].forEach(f => onUpload(f)) if ('value' in event.currentTarget) event.currentTarget.value = '' } diff --git a/app/src/molecules/WizardHeader/__tests__/WizardHeader.test.tsx b/app/src/molecules/WizardHeader/__tests__/WizardHeader.test.tsx index 0e11447cac2..436e594dd59 100644 --- a/app/src/molecules/WizardHeader/__tests__/WizardHeader.test.tsx +++ b/app/src/molecules/WizardHeader/__tests__/WizardHeader.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { describe, it, expect, vi, beforeEach } from 'vitest' import '@testing-library/jest-dom/vitest' import { fireEvent, screen } from '@testing-library/react' @@ -8,17 +7,19 @@ import { StepMeter } from '/app/atoms/StepMeter' import { WizardHeader } from '..' import { renderWithProviders } from '/app/__testing-utils__' +import type { ComponentProps } from 'react' + vi.mock('/app/atoms/StepMeter') vi.mock('/app/redux/config') -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, })[0] } describe('WizardHeader', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { diff --git a/app/src/molecules/WizardRequiredEquipmentList/index.tsx b/app/src/molecules/WizardRequiredEquipmentList/index.tsx index f6f7457a71a..3a39d904639 100644 --- a/app/src/molecules/WizardRequiredEquipmentList/index.tsx +++ b/app/src/molecules/WizardRequiredEquipmentList/index.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { useSelector } from 'react-redux' import { useTranslation } from 'react-i18next' import { css } from 'styled-components' @@ -23,9 +22,10 @@ import { Divider } from '/app/atoms/structure' import { labwareImages } from '/app/local-resources/labware' import { equipmentImages } from './equipmentImages' +import type { ComponentProps } from 'react' import type { StyleProps } from '@opentrons/components' interface WizardRequiredEquipmentListProps extends StyleProps { - equipmentList: Array> + equipmentList: Array> footer?: string } export function WizardRequiredEquipmentList( diff --git a/app/src/molecules/modals/BottomButtonBar.tsx b/app/src/molecules/modals/BottomButtonBar.tsx index d5c202e943c..f7f78f067e4 100644 --- a/app/src/molecules/modals/BottomButtonBar.tsx +++ b/app/src/molecules/modals/BottomButtonBar.tsx @@ -1,18 +1,18 @@ // bottom button bar for modals // TODO(mc, 2018-08-18): maybe make this the default AlertModal behavior -import type * as React from 'react' import cx from 'classnames' import { OutlineButton } from '@opentrons/components' import styles from './styles.module.css' +import type { ReactNode } from 'react' import type { ButtonProps } from '@opentrons/components' type MaybeButtonProps = ButtonProps | null | undefined interface Props { buttons: MaybeButtonProps[] className?: string | null - description?: React.ReactNode | null + description?: ReactNode | null } export function BottomButtonBar(props: Props): JSX.Element { diff --git a/app/src/molecules/modals/ScrollableAlertModal.tsx b/app/src/molecules/modals/ScrollableAlertModal.tsx index c98846899b8..d8ebdc18543 100644 --- a/app/src/molecules/modals/ScrollableAlertModal.tsx +++ b/app/src/molecules/modals/ScrollableAlertModal.tsx @@ -1,12 +1,13 @@ // AlertModal with vertical scrolling -import type * as React from 'react' import omit from 'lodash/omit' import { AlertModal } from '@opentrons/components' import { BottomButtonBar } from './BottomButtonBar' import styles from './styles.module.css' -type Props = React.ComponentProps +import type { ComponentProps } from 'react' + +type Props = ComponentProps export function ScrollableAlertModal(props: Props): JSX.Element { return ( diff --git a/app/src/organisms/ApplyHistoricOffsets/index.tsx b/app/src/organisms/ApplyHistoricOffsets/index.tsx deleted file mode 100644 index 6925145c012..00000000000 --- a/app/src/organisms/ApplyHistoricOffsets/index.tsx +++ /dev/null @@ -1,190 +0,0 @@ -import * as React from 'react' -import { createPortal } from 'react-dom' -import { useSelector } from 'react-redux' -import pick from 'lodash/pick' -import { Trans, useTranslation } from 'react-i18next' -import { - ALIGN_CENTER, - CheckboxField, - DIRECTION_COLUMN, - Flex, - Icon, - JUSTIFY_SPACE_BETWEEN, - Link, - SIZE_1, - SPACING, - LegacyStyledText, - TYPOGRAPHY, - ModalHeader, - ModalShell, -} from '@opentrons/components' -import { getTopPortalEl } from '/app/App/portal' -import { ExternalLink } from '/app/atoms/Link/ExternalLink' -import { PythonLabwareOffsetSnippet } from '/app/molecules/PythonLabwareOffsetSnippet' -import { LabwareOffsetTabs } from '/app/organisms/LabwareOffsetTabs' -import { getLabwareDefinitionsFromCommands } from '/app/local-resources/labware' -import { LabwareOffsetTable } from './LabwareOffsetTable' -import { getIsLabwareOffsetCodeSnippetsOn } from '/app/redux/config' -import type { LabwareOffset } from '@opentrons/api-client' -import type { - LoadedLabware, - LoadedModule, - RunTimeCommand, -} from '@opentrons/shared-data' - -const HOW_OFFSETS_WORK_SUPPORT_URL = - 'https://support.opentrons.com/s/article/How-Labware-Offsets-work-on-the-OT-2' -export interface OffsetCandidate extends LabwareOffset { - runCreatedAt: string - labwareDisplayName: string -} - -interface ApplyHistoricOffsetsProps { - offsetCandidates: OffsetCandidate[] - shouldApplyOffsets: boolean - setShouldApplyOffsets: (shouldApplyOffsets: boolean) => void - commands: RunTimeCommand[] - labware: LoadedLabware[] - modules: LoadedModule[] -} -export function ApplyHistoricOffsets( - props: ApplyHistoricOffsetsProps -): JSX.Element { - const { - offsetCandidates, - shouldApplyOffsets, - setShouldApplyOffsets, - labware, - modules, - commands, - } = props - const [showOffsetDataModal, setShowOffsetDataModal] = React.useState(false) - const { t } = useTranslation('labware_position_check') - const isLabwareOffsetCodeSnippetsOn = useSelector( - getIsLabwareOffsetCodeSnippetsOn - ) - const JupyterSnippet = ( - - pick(o, ['definitionUri', 'vector', 'location']) - )} - {...{ labware, modules, commands }} - /> - ) - const CommandLineSnippet = ( - - pick(o, ['definitionUri', 'vector', 'location']) - )} - {...{ labware, modules, commands }} - /> - ) - const noOffsetData = offsetCandidates.length < 1 - return ( - - ) => { - setShouldApplyOffsets(e.currentTarget.checked) - }} - value={shouldApplyOffsets} - disabled={noOffsetData} - isIndeterminate={noOffsetData} - label={ - - - - {t(noOffsetData ? 'no_offset_data' : 'apply_offset_data')} - - - } - /> - { - setShowOffsetDataModal(true) - }} - css={TYPOGRAPHY.linkPSemiBold} - > - {t(noOffsetData ? 'learn_more' : 'view_data')} - - {showOffsetDataModal - ? createPortal( - { - setShowOffsetDataModal(false) - }} - /> - } - > - - {noOffsetData ? ( - - ), - }} - /> - ) : ( - - {t('robot_has_offsets_from_previous_runs')} - - )} - - {t('see_how_offsets_work')} - - {!noOffsetData ? ( - isLabwareOffsetCodeSnippetsOn ? ( - - } - JupyterComponent={JupyterSnippet} - CommandLineComponent={CommandLineSnippet} - /> - ) : ( - - ) - ) : null} - - , - getTopPortalEl() - ) - : null} - - ) -} diff --git a/app/src/organisms/Desktop/AdvancedSettings/AdditionalCustomLabwareSourceFolder.tsx b/app/src/organisms/Desktop/AdvancedSettings/AdditionalCustomLabwareSourceFolder.tsx index 57708f2854d..75c0d411cff 100644 --- a/app/src/organisms/Desktop/AdvancedSettings/AdditionalCustomLabwareSourceFolder.tsx +++ b/app/src/organisms/Desktop/AdvancedSettings/AdditionalCustomLabwareSourceFolder.tsx @@ -8,10 +8,10 @@ import { Flex, Icon, JUSTIFY_SPACE_BETWEEN, + LegacyStyledText, Link, SPACING_AUTO, SPACING, - LegacyStyledText, TYPOGRAPHY, } from '@opentrons/components' diff --git a/app/src/organisms/Desktop/AdvancedSettings/OT2AdvancedSettings.tsx b/app/src/organisms/Desktop/AdvancedSettings/OT2AdvancedSettings.tsx index 7ab3ebb0bd0..3ff3313a840 100644 --- a/app/src/organisms/Desktop/AdvancedSettings/OT2AdvancedSettings.tsx +++ b/app/src/organisms/Desktop/AdvancedSettings/OT2AdvancedSettings.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { useSelector, useDispatch } from 'react-redux' import { useTranslation } from 'react-i18next' import { css } from 'styled-components' @@ -17,6 +16,7 @@ import { } from '/app/redux/calibration' import { getUseTrashSurfaceForTipCal } from '/app/redux/config' +import type { ChangeEvent } from 'react' import type { Dispatch, State } from '/app/redux/types' const ALWAYS_BLOCK: 'always-block' = 'always-block' @@ -74,7 +74,7 @@ export function OT2AdvancedSettings(): JSX.Element { ? ALWAYS_BLOCK : ALWAYS_PROMPT } - onChange={(event: React.ChangeEvent) => { + onChange={(event: ChangeEvent) => { // you know this is a limited-selection field whose values are only // the elements of BlockSelection; i know this is a limited-selection // field whose values are only the elements of BlockSelection; but sadly, diff --git a/app/src/organisms/Desktop/AdvancedSettings/OverridePathToPython.tsx b/app/src/organisms/Desktop/AdvancedSettings/OverridePathToPython.tsx index d0e5b7d6d93..a87d739172f 100644 --- a/app/src/organisms/Desktop/AdvancedSettings/OverridePathToPython.tsx +++ b/app/src/organisms/Desktop/AdvancedSettings/OverridePathToPython.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { useTranslation } from 'react-i18next' import { useSelector, useDispatch } from 'react-redux' @@ -27,6 +26,7 @@ import { ANALYTICS_CHANGE_PATH_TO_PYTHON_DIRECTORY, } from '/app/redux/analytics' +import type { MouseEventHandler } from 'react' import type { Dispatch } from '/app/redux/types' export function OverridePathToPython(): JSX.Element { @@ -35,7 +35,7 @@ export function OverridePathToPython(): JSX.Element { const dispatch = useDispatch() const trackEvent = useTrackEvent() - const handleClickPythonDirectoryChange: React.MouseEventHandler = _event => { + const handleClickPythonDirectoryChange: MouseEventHandler = _event => { dispatch(changePythonPathOverrideConfig()) trackEvent({ name: ANALYTICS_CHANGE_PATH_TO_PYTHON_DIRECTORY, diff --git a/app/src/organisms/Desktop/AdvancedSettings/UpdatedChannel.tsx b/app/src/organisms/Desktop/AdvancedSettings/UpdatedChannel.tsx index aca0348cb7b..ec261c75083 100644 --- a/app/src/organisms/Desktop/AdvancedSettings/UpdatedChannel.tsx +++ b/app/src/organisms/Desktop/AdvancedSettings/UpdatedChannel.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { useTranslation } from 'react-i18next' import { useDispatch, useSelector } from 'react-redux' @@ -19,6 +18,7 @@ import { updateConfigValue, } from '/app/redux/config' +import type { ComponentProps } from 'react' import type { SelectOption } from '/app/atoms/SelectField/Select' import type { Dispatch } from '/app/redux/types' @@ -31,7 +31,7 @@ export function UpdatedChannel(): JSX.Element { dispatch(updateConfigValue('update.channel', value)) } - const formatOptionLabel: React.ComponentProps< + const formatOptionLabel: ComponentProps< typeof SelectField >['formatOptionLabel'] = (option, index): JSX.Element => { const { label, value } = option diff --git a/app/src/organisms/Desktop/Alerts/AlertsProvider.tsx b/app/src/organisms/Desktop/Alerts/AlertsProvider.tsx index 21aeccbf855..79b6088b445 100644 --- a/app/src/organisms/Desktop/Alerts/AlertsProvider.tsx +++ b/app/src/organisms/Desktop/Alerts/AlertsProvider.tsx @@ -1,21 +1,23 @@ -import * as React from 'react' +import { createContext, useRef } from 'react' import { AlertsModal } from '.' import { useToaster } from '/app/organisms/ToasterOven' +import type { ReactNode } from 'react' + export interface AlertsContextProps { removeActiveAppUpdateToast: () => void } -export const AlertsContext = React.createContext({ +export const AlertsContext = createContext({ removeActiveAppUpdateToast: () => null, }) interface AlertsProps { - children: React.ReactNode + children: ReactNode } export function Alerts({ children }: AlertsProps): JSX.Element { - const toastRef = React.useRef(null) + const toastRef = useRef(null) const { eatToast } = useToaster() const removeActiveAppUpdateToast = (): void => { diff --git a/app/src/organisms/Desktop/Alerts/U2EDriverOutdatedAlert.tsx b/app/src/organisms/Desktop/Alerts/U2EDriverOutdatedAlert.tsx index fbb79a6b935..a327fce6a31 100644 --- a/app/src/organisms/Desktop/Alerts/U2EDriverOutdatedAlert.tsx +++ b/app/src/organisms/Desktop/Alerts/U2EDriverOutdatedAlert.tsx @@ -1,5 +1,6 @@ import { Link as InternalLink } from 'react-router-dom' import styled from 'styled-components' +import { useTranslation } from 'react-i18next' import { AlertModal, @@ -12,20 +13,9 @@ import { ANALYTICS_U2E_DRIVE_ALERT_DISMISSED, ANALYTICS_U2E_DRIVE_LINK_CLICKED, } from '/app/redux/analytics' -import { - U2E_DRIVER_UPDATE_URL, - U2E_DRIVER_OUTDATED_MESSAGE, - U2E_DRIVER_DESCRIPTION, - U2E_DRIVER_OUTDATED_CTA, -} from '/app/redux/system-info' +import { U2E_DRIVER_UPDATE_URL } from '/app/redux/system-info' import type { AlertProps } from './types' -// TODO(mc, 2020-05-07): i18n -const DRIVER_OUT_OF_DATE = 'Realtek USB-to-Ethernet Driver Update Available' -const VIEW_ADAPTER_INFO = 'view adapter info' -const GET_UPDATE = 'get update' -const DONT_REMIND_ME_AGAIN = "Don't remind me again" - const ADAPTER_INFO_URL = '/more/network-and-system' const LinkButton = styled(Link)` @@ -42,19 +32,20 @@ const IgnoreCheckbox = styled(DeprecatedCheckboxField)` export function U2EDriverOutdatedAlert(props: AlertProps): JSX.Element { const trackEvent = useTrackEvent() + const { t } = useTranslation(['app_settings', 'branded']) const [rememberDismiss, toggleRememberDismiss] = useToggle() const { dismissAlert } = props return ( { dismissAlert(rememberDismiss) trackEvent({ @@ -67,7 +58,7 @@ export function U2EDriverOutdatedAlert(props: AlertProps): JSX.Element { Component: LinkButton, href: U2E_DRIVER_UPDATE_URL, external: true, - children: GET_UPDATE, + children: t('get_update'), onClick: () => { dismissAlert(rememberDismiss) trackEvent({ @@ -79,11 +70,11 @@ export function U2EDriverOutdatedAlert(props: AlertProps): JSX.Element { ]} >

        - {U2E_DRIVER_OUTDATED_MESSAGE} {U2E_DRIVER_DESCRIPTION} + {t('u2e_driver_outdated_message')} {t('branded:u2e_driver_description')}

        -

        {U2E_DRIVER_OUTDATED_CTA}

        +

        {t('please_update_driver')}

        diff --git a/app/src/organisms/Desktop/AppSettings/__tests__/ConnectRobotSlideout.test.tsx b/app/src/organisms/Desktop/AppSettings/__tests__/ConnectRobotSlideout.test.tsx index c92e1f495a0..4cddd3e1763 100644 --- a/app/src/organisms/Desktop/AppSettings/__tests__/ConnectRobotSlideout.test.tsx +++ b/app/src/organisms/Desktop/AppSettings/__tests__/ConnectRobotSlideout.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { fireEvent, screen } from '@testing-library/react' import '@testing-library/jest-dom/vitest' import { describe, it, expect, vi, beforeEach } from 'vitest' @@ -8,17 +7,19 @@ import { getConfig } from '/app/redux/config' import { renderWithProviders } from '/app/__testing-utils__' import { ConnectRobotSlideout } from '../ConnectRobotSlideout' +import type { ComponentProps } from 'react' + vi.mock('/app/redux/discovery') vi.mock('/app/redux/config') -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, })[0] } describe('ConnectRobotSlideout', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { vi.mocked(getScanning).mockReturnValue(true) @@ -54,7 +55,7 @@ describe('ConnectRobotSlideout', () => { checkIpAndHostname: vi.fn(), isExpanded: true, onCloseClick: vi.fn(), - } as React.ComponentProps + } as ComponentProps }) it('renders correct title, body, and footer for ConnectRobotSlideout', () => { diff --git a/app/src/organisms/Desktop/AppSettings/__tests__/PreviousVersionModal.test.tsx b/app/src/organisms/Desktop/AppSettings/__tests__/PreviousVersionModal.test.tsx index b4449623c05..59eff47aa2c 100644 --- a/app/src/organisms/Desktop/AppSettings/__tests__/PreviousVersionModal.test.tsx +++ b/app/src/organisms/Desktop/AppSettings/__tests__/PreviousVersionModal.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { fireEvent, screen } from '@testing-library/react' import { describe, it, expect, vi } from 'vitest' import '@testing-library/jest-dom/vitest' @@ -10,12 +9,14 @@ import { PREVIOUS_RELEASES_URL, } from '../PreviousVersionModal' -const render = (props: React.ComponentProps) => { +import type { ComponentProps } from 'react' + +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, }) } -const props: React.ComponentProps = { +const props: ComponentProps = { closeModal: vi.fn(), } diff --git a/app/src/organisms/Desktop/CalibrateDeck/__tests__/CalibrateDeck.test.tsx b/app/src/organisms/Desktop/CalibrateDeck/__tests__/CalibrateDeck.test.tsx index 3f90064c03c..13415c8e47c 100644 --- a/app/src/organisms/Desktop/CalibrateDeck/__tests__/CalibrateDeck.test.tsx +++ b/app/src/organisms/Desktop/CalibrateDeck/__tests__/CalibrateDeck.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { vi, describe, beforeEach, expect, it } from 'vitest' import { fireEvent, screen } from '@testing-library/react' @@ -14,6 +13,7 @@ import { CalibrationError, } from '/app/organisms/Desktop/CalibrationError' +import type { ComponentProps, ComponentType } from 'react' import type { DeckCalibrationStep } from '/app/redux/sessions/types' import type { DispatchRequestsType } from '/app/redux/robot-api' @@ -41,14 +41,14 @@ describe('CalibrateDeck', () => { ...mockDeckCalibrationSessionAttributes, } const render = ( - props: Partial> = {} + props: Partial> = {} ) => { const { showSpinner = false, isJogging = false, session = mockDeckCalSession, } = props - return renderWithProviders>( + return renderWithProviders>( > + Record> > = { [Sessions.DECK_STEP_SESSION_STARTED]: Introduction, [Sessions.DECK_STEP_LABWARE_LOADED]: DeckSetup, @@ -89,7 +90,7 @@ export function CalibrateDeck({ const errorInfo = useCalibrationError(requestIds, session?.id) - const isMulti = React.useMemo(() => { + const isMulti = useMemo(() => { const spec = instrument && getPipetteModelSpecs(instrument.model) return spec ? spec.channels > 1 : false }, [instrument]) diff --git a/app/src/organisms/Desktop/CalibratePipetteOffset/__tests__/CalibratePipetteOffset.test.tsx b/app/src/organisms/Desktop/CalibratePipetteOffset/__tests__/CalibratePipetteOffset.test.tsx index 15117cac92f..ac34e42e5ee 100644 --- a/app/src/organisms/Desktop/CalibratePipetteOffset/__tests__/CalibratePipetteOffset.test.tsx +++ b/app/src/organisms/Desktop/CalibratePipetteOffset/__tests__/CalibratePipetteOffset.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { vi, it, describe, expect, beforeEach } from 'vitest' import { when } from 'vitest-when' import { fireEvent, screen } from '@testing-library/react' @@ -7,13 +6,15 @@ import { renderWithProviders } from '/app/__testing-utils__' import { i18n } from '/app/i18n' import * as Sessions from '/app/redux/sessions' import { mockPipetteOffsetCalibrationSessionAttributes } from '/app/redux/sessions/__fixtures__' -import { CalibratePipetteOffset } from '../index' -import type { PipetteOffsetCalibrationStep } from '/app/redux/sessions/types' -import type { DispatchRequestsType } from '/app/redux/robot-api' import { useCalibrationError, CalibrationError, } from '/app/organisms/Desktop/CalibrationError' +import { CalibratePipetteOffset } from '../index' + +import type { ComponentProps, ComponentType } from 'react' +import type { PipetteOffsetCalibrationStep } from '/app/redux/sessions/types' +import type { DispatchRequestsType } from '/app/redux/robot-api' vi.mock('@opentrons/shared-data', async importOriginal => { const actual = await importOriginal() @@ -35,16 +36,14 @@ interface CalibratePipetteOffsetSpec { describe('CalibratePipetteOffset', () => { let dispatchRequests: DispatchRequestsType const render = ( - props: Partial> = {} + props: Partial> = {} ) => { const { showSpinner = false, isJogging = false, session = mockPipOffsetCalSession, } = props - return renderWithProviders< - React.ComponentType - >( + return renderWithProviders>( > + Record> > = { [Sessions.PIP_OFFSET_STEP_SESSION_STARTED]: Introduction, [Sessions.PIP_OFFSET_STEP_LABWARE_LOADED]: DeckSetup, @@ -88,7 +89,7 @@ export function CalibratePipetteOffset({ const calBlock: CalibrationLabware | null = labware != null ? labware.find(l => !l.isTiprack) ?? null : null - const isMulti = React.useMemo(() => { + const isMulti = useMemo(() => { const spec = instrument != null ? getPipetteModelSpecs(instrument.model) : null return spec != null ? spec.channels > 1 : false diff --git a/app/src/organisms/Desktop/CalibrateTipLength/AskForCalibrationBlockModal.tsx b/app/src/organisms/Desktop/CalibrateTipLength/AskForCalibrationBlockModal.tsx index 62a8ca00ef0..85eb76ae126 100644 --- a/app/src/organisms/Desktop/CalibrateTipLength/AskForCalibrationBlockModal.tsx +++ b/app/src/organisms/Desktop/CalibrateTipLength/AskForCalibrationBlockModal.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useState } from 'react' import { createPortal } from 'react-dom' import { Trans, useTranslation } from 'react-i18next' import { @@ -24,6 +24,7 @@ import { WizardHeader } from '/app/molecules/WizardHeader' import { getTopPortalEl } from '/app/App/portal' import { setUseTrashSurfaceForTipCal } from '/app/redux/calibration' +import type { ChangeEvent } from 'react' import type { Dispatch } from '/app/redux/types' const BLOCK_REQUEST_EMAIL_BODY = @@ -41,9 +42,7 @@ interface Props { export function AskForCalibrationBlockModal(props: Props): JSX.Element { const { t } = useTranslation(['robot_calibration', 'shared', 'branded']) - const [rememberPreference, setRememberPreference] = React.useState( - true - ) + const [rememberPreference, setRememberPreference] = useState(true) const dispatch = useDispatch() const makeSetHasBlock = (hasBlock: boolean) => (): void => { @@ -108,7 +107,7 @@ export function AskForCalibrationBlockModal(props: Props): JSX.Element { > ) => { + onChange={(e: ChangeEvent) => { setRememberPreference(e.currentTarget.checked) }} value={rememberPreference} diff --git a/app/src/organisms/Desktop/CalibrateTipLength/TipLengthCalibrationInfoBox.tsx b/app/src/organisms/Desktop/CalibrateTipLength/TipLengthCalibrationInfoBox.tsx index 7bcf3bd81e3..c13d9f5bab9 100644 --- a/app/src/organisms/Desktop/CalibrateTipLength/TipLengthCalibrationInfoBox.tsx +++ b/app/src/organisms/Desktop/CalibrateTipLength/TipLengthCalibrationInfoBox.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { Box, Text, @@ -7,9 +6,11 @@ import { SPACING, } from '@opentrons/components' +import type { ReactNode } from 'react' + export interface TipLengthCalibrationInfoBoxProps { title: string - children: React.ReactNode + children: ReactNode } export function TipLengthCalibrationInfoBox( diff --git a/app/src/organisms/Desktop/CalibrateTipLength/__tests__/AskForCalibrationBlockModal.test.tsx b/app/src/organisms/Desktop/CalibrateTipLength/__tests__/AskForCalibrationBlockModal.test.tsx index 199ec279988..7ed7cad97e9 100644 --- a/app/src/organisms/Desktop/CalibrateTipLength/__tests__/AskForCalibrationBlockModal.test.tsx +++ b/app/src/organisms/Desktop/CalibrateTipLength/__tests__/AskForCalibrationBlockModal.test.tsx @@ -1,17 +1,18 @@ import { vi, it, describe, expect } from 'vitest' +import { fireEvent, screen } from '@testing-library/react' import { renderWithProviders } from '/app/__testing-utils__' - import { i18n } from '/app/i18n' import { setUseTrashSurfaceForTipCal } from '/app/redux/calibration' import { AskForCalibrationBlockModal } from '../AskForCalibrationBlockModal' -import { fireEvent, screen } from '@testing-library/react' + +import type { ComponentProps } from 'react' describe('AskForCalibrationBlockModal', () => { const onResponse = vi.fn() const render = () => { return renderWithProviders< - React.ComponentProps + ComponentProps >( { @@ -41,14 +41,14 @@ describe('CalibrateTipLength', () => { ...mockTipLengthCalibrationSessionAttributes, } const render = ( - props: Partial> = {} + props: Partial> = {} ) => { const { showSpinner = false, isJogging = false, session = mockTipLengthSession, } = props - return renderWithProviders>( + return renderWithProviders>( > + Record> > = { sessionStarted: Introduction, labwareLoaded: DeckSetup, @@ -80,7 +81,7 @@ export function CalibrateTipLength({ const queryClient = useQueryClient() const host = useHost() - const isMulti = React.useMemo(() => { + const isMulti = useMemo(() => { const spec = instrument != null ? getPipetteModelSpecs(instrument.model) : null return spec != null ? spec.channels > 1 : false diff --git a/app/src/organisms/Desktop/CalibrationError/__tests__/CalibrationError.test.tsx b/app/src/organisms/Desktop/CalibrationError/__tests__/CalibrationError.test.tsx index d8213c793ca..3676c0e95f0 100644 --- a/app/src/organisms/Desktop/CalibrationError/__tests__/CalibrationError.test.tsx +++ b/app/src/organisms/Desktop/CalibrationError/__tests__/CalibrationError.test.tsx @@ -8,7 +8,7 @@ import { CalibrationError } from '..' import type { ComponentProps } from 'react' describe('CalibrationError', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { diff --git a/app/src/organisms/Desktop/CalibrationPanels/CompleteConfirmation.tsx b/app/src/organisms/Desktop/CalibrationPanels/CompleteConfirmation.tsx index cbd0e214cd1..3f6de1fed33 100644 --- a/app/src/organisms/Desktop/CalibrationPanels/CompleteConfirmation.tsx +++ b/app/src/organisms/Desktop/CalibrationPanels/CompleteConfirmation.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { useTranslation } from 'react-i18next' import { ALIGN_CENTER, @@ -16,11 +15,13 @@ import { TYPOGRAPHY, } from '@opentrons/components' +import type { MouseEventHandler, ReactNode } from 'react' + interface CompleteConfirmationProps { - proceed: React.MouseEventHandler + proceed: MouseEventHandler flowName?: string body?: string - visualAid?: React.ReactNode + visualAid?: ReactNode } export function CompleteConfirmation( diff --git a/app/src/organisms/Desktop/CalibrationPanels/Introduction/__tests__/Body.test.tsx b/app/src/organisms/Desktop/CalibrationPanels/Introduction/__tests__/Body.test.tsx index c1300fb3218..26682dff0cc 100644 --- a/app/src/organisms/Desktop/CalibrationPanels/Introduction/__tests__/Body.test.tsx +++ b/app/src/organisms/Desktop/CalibrationPanels/Introduction/__tests__/Body.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { it, describe } from 'vitest' import { screen } from '@testing-library/react' @@ -8,7 +7,9 @@ import * as Sessions from '/app/redux/sessions' import { i18n } from '/app/i18n' import { Body } from '../Body' -const render = (props: React.ComponentProps) => { +import type { ComponentProps } from 'react' + +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, })[0] diff --git a/app/src/organisms/Desktop/CalibrationPanels/Introduction/__tests__/Introduction.test.tsx b/app/src/organisms/Desktop/CalibrationPanels/Introduction/__tests__/Introduction.test.tsx index a34460571ed..4005f6c996c 100644 --- a/app/src/organisms/Desktop/CalibrationPanels/Introduction/__tests__/Introduction.test.tsx +++ b/app/src/organisms/Desktop/CalibrationPanels/Introduction/__tests__/Introduction.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { vi, it, expect, describe, beforeEach, afterEach } from 'vitest' import { fireEvent, screen } from '@testing-library/react' @@ -9,6 +8,8 @@ import { i18n } from '/app/i18n' import { Introduction } from '../' import { ChooseTipRack } from '../../ChooseTipRack' +import type { ComponentProps } from 'react' + vi.mock('../../ChooseTipRack') const mockCalInvalidationHandler = vi.fn() @@ -17,9 +18,7 @@ describe('Introduction', () => { const mockSendCommands = vi.fn() const mockCleanUpAndExit = vi.fn() - const render = ( - props: Partial> = {} - ) => { + const render = (props: Partial> = {}) => { return renderWithProviders( mount && assetMap[mount][isMulti ? 'multi' : 'single'], [mount, isMulti] ) @@ -62,7 +63,7 @@ export function SaveZPoint(props: CalibrationPanelProps): JSX.Element { const isHealthCheck = sessionType === Sessions.SESSION_TYPE_CALIBRATION_HEALTH_CHECK - const proceed: React.MouseEventHandler = _event => { + const proceed: MouseEventHandler = _event => { isHealthCheck ? sendCommands( { command: Sessions.checkCommands.COMPARE_POINT }, diff --git a/app/src/organisms/Desktop/CalibrationPanels/__tests__/ChooseTipRack.test.tsx b/app/src/organisms/Desktop/CalibrationPanels/__tests__/ChooseTipRack.test.tsx index 4bb1cecd0ef..1a8de5f9a00 100644 --- a/app/src/organisms/Desktop/CalibrationPanels/__tests__/ChooseTipRack.test.tsx +++ b/app/src/organisms/Desktop/CalibrationPanels/__tests__/ChooseTipRack.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { fireEvent, screen } from '@testing-library/react' import { vi, it, describe, expect, beforeEach } from 'vitest' @@ -19,6 +18,7 @@ import { import { getCustomTipRackDefinitions } from '/app/redux/custom-labware' import { ChooseTipRack } from '../ChooseTipRack' +import type { ComponentProps } from 'react' import type { AttachedPipettesByMount } from '/app/redux/pipettes/types' vi.mock('@opentrons/react-api-client') @@ -32,14 +32,14 @@ const mockAttachedPipettes: AttachedPipettesByMount = { right: null, } as any -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, })[0] } describe('ChooseTipRack', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { vi.mocked(Select).mockReturnValue(
        mock select
        ) diff --git a/app/src/organisms/Desktop/CalibrationPanels/__tests__/ChosenTipRackRender.test.tsx b/app/src/organisms/Desktop/CalibrationPanels/__tests__/ChosenTipRackRender.test.tsx index 5eb7fa1db8b..27bc9a7d233 100644 --- a/app/src/organisms/Desktop/CalibrationPanels/__tests__/ChosenTipRackRender.test.tsx +++ b/app/src/organisms/Desktop/CalibrationPanels/__tests__/ChosenTipRackRender.test.tsx @@ -1,13 +1,14 @@ -import type * as React from 'react' import { it, describe, beforeEach } from 'vitest' import { screen } from '@testing-library/react' import { i18n } from '/app/i18n' import { renderWithProviders } from '/app/__testing-utils__' import { ChosenTipRackRender } from '../ChosenTipRackRender' + +import type { ComponentProps } from 'react' import type { SelectOption } from '/app/atoms/SelectField/Select' -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, })[0] @@ -19,7 +20,7 @@ const mockSelectValue = { } as SelectOption describe('ChosenTipRackRender', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { selectedValue: mockSelectValue, diff --git a/app/src/organisms/Desktop/CalibrationPanels/__tests__/CompleteConfirmation.test.tsx b/app/src/organisms/Desktop/CalibrationPanels/__tests__/CompleteConfirmation.test.tsx index 99dc73310d1..8dd9575a2c3 100644 --- a/app/src/organisms/Desktop/CalibrationPanels/__tests__/CompleteConfirmation.test.tsx +++ b/app/src/organisms/Desktop/CalibrationPanels/__tests__/CompleteConfirmation.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { fireEvent, screen } from '@testing-library/react' import { vi, it, describe, expect } from 'vitest' @@ -7,10 +6,12 @@ import { i18n } from '/app/i18n' import { CompleteConfirmation } from '../CompleteConfirmation' +import type { ComponentProps } from 'react' + describe('CompleteConfirmation', () => { const mockCleanUpAndExit = vi.fn() const render = ( - props: Partial> = {} + props: Partial> = {} ) => { const { proceed = mockCleanUpAndExit, flowName, body, visualAid } = props return renderWithProviders( diff --git a/app/src/organisms/Desktop/CalibrationPanels/__tests__/ConfirmCrashRecovery.test.tsx b/app/src/organisms/Desktop/CalibrationPanels/__tests__/ConfirmCrashRecovery.test.tsx index 53fb8559e55..11581f72c55 100644 --- a/app/src/organisms/Desktop/CalibrationPanels/__tests__/ConfirmCrashRecovery.test.tsx +++ b/app/src/organisms/Desktop/CalibrationPanels/__tests__/ConfirmCrashRecovery.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { fireEvent, screen } from '@testing-library/react' import { vi, it, describe, expect } from 'vitest' @@ -6,11 +5,13 @@ import { renderWithProviders } from '/app/__testing-utils__' import { i18n } from '/app/i18n' import { ConfirmCrashRecovery } from '../ConfirmCrashRecovery' +import type { ComponentProps } from 'react' + describe('ConfirmCrashRecovery', () => { const mockBack = vi.fn() const mockConfirm = vi.fn() const render = ( - props: Partial> = {} + props: Partial> = {} ) => { const { back = mockBack, confirm = mockConfirm } = props return renderWithProviders( diff --git a/app/src/organisms/Desktop/CalibrationPanels/__tests__/ConfirmExit.test.tsx b/app/src/organisms/Desktop/CalibrationPanels/__tests__/ConfirmExit.test.tsx index 086c122e8bf..c09bf0dcd84 100644 --- a/app/src/organisms/Desktop/CalibrationPanels/__tests__/ConfirmExit.test.tsx +++ b/app/src/organisms/Desktop/CalibrationPanels/__tests__/ConfirmExit.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { fireEvent, screen } from '@testing-library/react' import { vi, it, describe, expect } from 'vitest' @@ -7,12 +6,12 @@ import { i18n } from '/app/i18n' import { ConfirmExit } from '../ConfirmExit' +import type { ComponentProps } from 'react' + describe('ConfirmExit', () => { const mockBack = vi.fn() const mockExit = vi.fn() - const render = ( - props: Partial> = {} - ) => { + const render = (props: Partial> = {}) => { const { heading, body } = props return renderWithProviders( { const mockSendCommands = vi.fn() const mockDeleteSession = vi.fn() - const render = ( - props: Partial> = {} - ) => { + const render = (props: Partial> = {}) => { const { mount = 'left', isMulti = false, diff --git a/app/src/organisms/Desktop/CalibrationPanels/__tests__/MeasureNozzle.test.tsx b/app/src/organisms/Desktop/CalibrationPanels/__tests__/MeasureNozzle.test.tsx index d0de251b0ad..e95c612ec01 100644 --- a/app/src/organisms/Desktop/CalibrationPanels/__tests__/MeasureNozzle.test.tsx +++ b/app/src/organisms/Desktop/CalibrationPanels/__tests__/MeasureNozzle.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { fireEvent, screen } from '@testing-library/react' import { vi, it, describe, expect } from 'vitest' @@ -11,11 +10,13 @@ import { import * as Sessions from '/app/redux/sessions' import { MeasureNozzle } from '../MeasureNozzle' +import type { ComponentProps } from 'react' + describe('MeasureNozzle', () => { const mockSendCommands = vi.fn() const mockDeleteSession = vi.fn() const render = ( - props: Partial> = {} + props: Partial> = {} ) => { const { mount = 'left', diff --git a/app/src/organisms/Desktop/CalibrationPanels/__tests__/MeasureTip.test.tsx b/app/src/organisms/Desktop/CalibrationPanels/__tests__/MeasureTip.test.tsx index 70d8ab54a6b..6f37d74fca5 100644 --- a/app/src/organisms/Desktop/CalibrationPanels/__tests__/MeasureTip.test.tsx +++ b/app/src/organisms/Desktop/CalibrationPanels/__tests__/MeasureTip.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { fireEvent, screen } from '@testing-library/react' import { vi, it, describe, expect } from 'vitest' @@ -12,12 +11,12 @@ import * as Sessions from '/app/redux/sessions' import { MeasureTip } from '../MeasureTip' +import type { ComponentProps } from 'react' + describe('MeasureTip', () => { const mockSendCommands = vi.fn() const mockDeleteSession = vi.fn() - const render = ( - props: Partial> = {} - ) => { + const render = (props: Partial> = {}) => { const { mount = 'left', isMulti = false, diff --git a/app/src/organisms/Desktop/CalibrationPanels/__tests__/SaveXYPoint.test.tsx b/app/src/organisms/Desktop/CalibrationPanels/__tests__/SaveXYPoint.test.tsx index 3d7dbda32a9..1965c0b1a14 100644 --- a/app/src/organisms/Desktop/CalibrationPanels/__tests__/SaveXYPoint.test.tsx +++ b/app/src/organisms/Desktop/CalibrationPanels/__tests__/SaveXYPoint.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { fireEvent, screen } from '@testing-library/react' import { vi, it, describe, expect } from 'vitest' @@ -9,12 +8,12 @@ import { mockDeckCalTipRack } from '/app/redux/sessions/__fixtures__' import * as Sessions from '/app/redux/sessions' import { SaveXYPoint } from '../SaveXYPoint' +import type { ComponentProps } from 'react' + describe('SaveXYPoint', () => { const mockSendCommands = vi.fn() const mockDeleteSession = vi.fn() - const render = ( - props: Partial> = {} - ) => { + const render = (props: Partial> = {}) => { const { mount = 'left', isMulti = false, diff --git a/app/src/organisms/Desktop/CalibrationPanels/__tests__/SaveZPoint.test.tsx b/app/src/organisms/Desktop/CalibrationPanels/__tests__/SaveZPoint.test.tsx index 36f04c1ebb2..dab0a5ee2b4 100644 --- a/app/src/organisms/Desktop/CalibrationPanels/__tests__/SaveZPoint.test.tsx +++ b/app/src/organisms/Desktop/CalibrationPanels/__tests__/SaveZPoint.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { fireEvent, screen } from '@testing-library/react' import { vi, it, describe, expect } from 'vitest' @@ -12,13 +11,13 @@ import { import * as Sessions from '/app/redux/sessions' import { SaveZPoint } from '../SaveZPoint' +import type { ComponentProps } from 'react' + describe('SaveZPoint', () => { const mockSendCommands = vi.fn() const mockDeleteSession = vi.fn() - const render = ( - props: Partial> = {} - ) => { + const render = (props: Partial> = {}) => { const { mount = 'left', isMulti = false, diff --git a/app/src/organisms/Desktop/CalibrationPanels/__tests__/TipConfirmation.test.tsx b/app/src/organisms/Desktop/CalibrationPanels/__tests__/TipConfirmation.test.tsx index f9eee22d78b..979db72ecb0 100644 --- a/app/src/organisms/Desktop/CalibrationPanels/__tests__/TipConfirmation.test.tsx +++ b/app/src/organisms/Desktop/CalibrationPanels/__tests__/TipConfirmation.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { fireEvent, screen } from '@testing-library/react' import { vi, it, describe, expect } from 'vitest' @@ -8,11 +7,13 @@ import { mockDeckCalTipRack } from '/app/redux/sessions/__fixtures__' import * as Sessions from '/app/redux/sessions' import { TipConfirmation } from '../TipConfirmation' +import type { ComponentProps } from 'react' + describe('TipConfirmation', () => { const mockSendCommands = vi.fn() const mockDeleteSession = vi.fn() const render = ( - props: Partial> = {} + props: Partial> = {} ) => { const { mount = 'left', diff --git a/app/src/organisms/Desktop/CalibrationPanels/__tests__/TipPickUp.test.tsx b/app/src/organisms/Desktop/CalibrationPanels/__tests__/TipPickUp.test.tsx index b6836cf96fb..6b8aac3fe88 100644 --- a/app/src/organisms/Desktop/CalibrationPanels/__tests__/TipPickUp.test.tsx +++ b/app/src/organisms/Desktop/CalibrationPanels/__tests__/TipPickUp.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { fireEvent, screen } from '@testing-library/react' import { vi, it, describe, expect } from 'vitest' @@ -8,12 +7,12 @@ import { mockDeckCalTipRack } from '/app/redux/sessions/__fixtures__' import * as Sessions from '/app/redux/sessions' import { TipPickUp } from '../TipPickUp' +import type { ComponentProps } from 'react' + describe('TipPickUp', () => { const mockSendCommands = vi.fn() const mockDeleteSession = vi.fn() - const render = ( - props: Partial> = {} - ) => { + const render = (props: Partial> = {}) => { const { mount = 'left', isMulti = false, diff --git a/app/src/organisms/Desktop/CalibrationStatusCard/__tests__/CalibrationStatusCard.test.tsx b/app/src/organisms/Desktop/CalibrationStatusCard/__tests__/CalibrationStatusCard.test.tsx index 400e73b3078..3a3e09c4005 100644 --- a/app/src/organisms/Desktop/CalibrationStatusCard/__tests__/CalibrationStatusCard.test.tsx +++ b/app/src/organisms/Desktop/CalibrationStatusCard/__tests__/CalibrationStatusCard.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { MemoryRouter } from 'react-router-dom' import { vi, it, describe, expect, beforeEach } from 'vitest' import { fireEvent, screen } from '@testing-library/react' @@ -18,9 +17,11 @@ import { expectedTaskList, } from '../../Devices/hooks/__fixtures__/taskListFixtures' +import type { ComponentProps } from 'react' + vi.mock('../../Devices/hooks') -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders( @@ -38,7 +39,7 @@ describe('CalibrationStatusCard', () => { vi.mocked(useCalibrationTaskList).mockReturnValue(expectedTaskList) }) - const props: React.ComponentProps = { + const props: ComponentProps = { robotName: 'otie', setShowHowCalibrationWorksModal: mockSetShowHowCalibrationWorksModal, } diff --git a/app/src/organisms/Desktop/CalibrationTaskList/__tests__/CalibrationTaskList.test.tsx b/app/src/organisms/Desktop/CalibrationTaskList/__tests__/CalibrationTaskList.test.tsx index ebada58f1f3..884bf35020c 100644 --- a/app/src/organisms/Desktop/CalibrationTaskList/__tests__/CalibrationTaskList.test.tsx +++ b/app/src/organisms/Desktop/CalibrationTaskList/__tests__/CalibrationTaskList.test.tsx @@ -82,7 +82,7 @@ describe('CalibrationTaskList', () => { rerender( { rerender( { rerender( { rerender( { rerender( { rerender( { rerender( { rerender( { rerender( + props: ComponentProps ) => { return renderWithProviders(, { i18nInstance: i18n, @@ -17,7 +18,7 @@ const render = ( } describe('CalibrationHealthCheckResults', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { isCalibrationRecommended: false, diff --git a/app/src/organisms/Desktop/CheckCalibration/ResultsSummary/__tests__/CalibrationResult.test.tsx b/app/src/organisms/Desktop/CheckCalibration/ResultsSummary/__tests__/CalibrationResult.test.tsx index 70432556944..c4d6bd4e67d 100644 --- a/app/src/organisms/Desktop/CheckCalibration/ResultsSummary/__tests__/CalibrationResult.test.tsx +++ b/app/src/organisms/Desktop/CheckCalibration/ResultsSummary/__tests__/CalibrationResult.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { vi, it, describe, beforeEach } from 'vitest' import { screen } from '@testing-library/react' @@ -7,16 +6,18 @@ import { i18n } from '/app/i18n' import { RenderResult } from '../RenderResult' import { CalibrationResult } from '../CalibrationResult' +import type { ComponentProps } from 'react' + vi.mock('../RenderResult') -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, })[0] } describe('PipetteCalibrationResult', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { diff --git a/app/src/organisms/Desktop/CheckCalibration/ResultsSummary/__tests__/RenderMountInformation.test.tsx b/app/src/organisms/Desktop/CheckCalibration/ResultsSummary/__tests__/RenderMountInformation.test.tsx index bae133b6522..3a5fc13d0c1 100644 --- a/app/src/organisms/Desktop/CheckCalibration/ResultsSummary/__tests__/RenderMountInformation.test.tsx +++ b/app/src/organisms/Desktop/CheckCalibration/ResultsSummary/__tests__/RenderMountInformation.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { vi, it, describe, beforeEach } from 'vitest' import { screen } from '@testing-library/react' @@ -10,6 +9,8 @@ import { LEFT, RIGHT } from '/app/redux/pipettes' import * as Fixtures from '/app/redux/sessions/__fixtures__' import { RenderMountInformation } from '../RenderMountInformation' +import type { ComponentProps } from 'react' + vi.mock('@opentrons/shared-data', async importOriginal => { const actual = await importOriginal() return { @@ -20,14 +21,14 @@ vi.mock('@opentrons/shared-data', async importOriginal => { const mockSessionDetails = Fixtures.mockRobotCalibrationCheckSessionDetails -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, })[0] } describe('RenderMountInformation', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { diff --git a/app/src/organisms/Desktop/CheckCalibration/ResultsSummary/__tests__/RenderResult.test.tsx b/app/src/organisms/Desktop/CheckCalibration/ResultsSummary/__tests__/RenderResult.test.tsx index 90ed47fc126..7b60f864218 100644 --- a/app/src/organisms/Desktop/CheckCalibration/ResultsSummary/__tests__/RenderResult.test.tsx +++ b/app/src/organisms/Desktop/CheckCalibration/ResultsSummary/__tests__/RenderResult.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { it, describe, expect, beforeEach } from 'vitest' import { screen } from '@testing-library/react' @@ -9,14 +8,16 @@ import { i18n } from '/app/i18n' import { RenderResult } from '../RenderResult' -const render = (props: React.ComponentProps) => { +import type { ComponentProps } from 'react' + +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, })[0] } describe('RenderResult', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { diff --git a/app/src/organisms/Desktop/CheckCalibration/__tests__/CheckCalibration.test.tsx b/app/src/organisms/Desktop/CheckCalibration/__tests__/CheckCalibration.test.tsx index d5f5a9814d7..853716d3d46 100644 --- a/app/src/organisms/Desktop/CheckCalibration/__tests__/CheckCalibration.test.tsx +++ b/app/src/organisms/Desktop/CheckCalibration/__tests__/CheckCalibration.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { fireEvent, screen } from '@testing-library/react' import { when } from 'vitest-when' import { vi, it, describe, expect, beforeEach, afterEach } from 'vitest' @@ -11,6 +10,8 @@ import * as Sessions from '/app/redux/sessions' import { mockCalibrationCheckSessionAttributes } from '/app/redux/sessions/__fixtures__' import { CheckCalibration } from '../index' + +import type { ComponentProps, ComponentType } from 'react' import type { RobotCalibrationCheckStep } from '/app/redux/sessions/types' vi.mock('/app/redux/calibration/selectors') @@ -36,14 +37,14 @@ describe('CheckCalibration', () => { } const render = ( - props: Partial> = {} + props: Partial> = {} ) => { const { showSpinner = false, isJogging = false, session = mockCalibrationCheckSession, } = props - return renderWithProviders>( + return renderWithProviders>( + [step in RobotCalibrationCheckStep]?: ComponentType } = { [Sessions.CHECK_STEP_SESSION_STARTED]: Introduction, [Sessions.CHECK_STEP_LABWARE_LOADED]: DeckSetup, @@ -124,7 +124,7 @@ export function CheckCalibration( cleanUpAndExit() }, true) - const isMulti = React.useMemo(() => { + const isMulti = useMemo(() => { const spec = activePipette && getPipetteModelSpecs(activePipette.model) return spec ? spec.channels > 1 : false }, [activePipette]) diff --git a/app/src/organisms/Desktop/ChooseProtocolSlideout/__tests__/ChooseProtocolSlideout.test.tsx b/app/src/organisms/Desktop/ChooseProtocolSlideout/__tests__/ChooseProtocolSlideout.test.tsx index fc6a0f281a9..86440066772 100644 --- a/app/src/organisms/Desktop/ChooseProtocolSlideout/__tests__/ChooseProtocolSlideout.test.tsx +++ b/app/src/organisms/Desktop/ChooseProtocolSlideout/__tests__/ChooseProtocolSlideout.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { vi, it, describe, expect, beforeEach } from 'vitest' import { MemoryRouter } from 'react-router-dom' import { fireEvent, screen, waitFor } from '@testing-library/react' @@ -19,6 +18,8 @@ import { useTrackCreateProtocolRunEvent } from '/app/organisms/Desktop/Devices/h import { useCreateRunFromProtocol } from '/app/organisms/Desktop/ChooseRobotToRunProtocolSlideout/useCreateRunFromProtocol' import { ChooseProtocolSlideout } from '../' import { useNotifyDataReady } from '/app/resources/useNotifyDataReady' + +import type { ComponentProps } from 'react' import type { ProtocolAnalysisOutput } from '@opentrons/shared-data' vi.mock( @@ -29,7 +30,7 @@ vi.mock('/app/organisms/Desktop/Devices/hooks') vi.mock('/app/redux/config') vi.mock('/app/resources/useNotifyDataReady') -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders( diff --git a/app/src/organisms/Desktop/ChooseProtocolSlideout/index.tsx b/app/src/organisms/Desktop/ChooseProtocolSlideout/index.tsx index a4824c76c5c..9368fb1b697 100644 --- a/app/src/organisms/Desktop/ChooseProtocolSlideout/index.tsx +++ b/app/src/organisms/Desktop/ChooseProtocolSlideout/index.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useEffect, useState, Fragment } from 'react' import first from 'lodash/first' import { Trans, useTranslation } from 'react-i18next' import { Link, NavLink, useNavigate } from 'react-router-dom' @@ -53,8 +53,8 @@ import { MiniCard } from '/app/molecules/MiniCard' import { UploadInput } from '/app/molecules/UploadInput' import { useTrackCreateProtocolRunEvent } from '/app/organisms/Desktop/Devices/hooks' import { useCreateRunFromProtocol } from '/app/organisms/Desktop/ChooseRobotToRunProtocolSlideout/useCreateRunFromProtocol' -import { ApplyHistoricOffsets } from '/app/organisms/ApplyHistoricOffsets' -import { useOffsetCandidatesForAnalysis } from '/app/organisms/ApplyHistoricOffsets/hooks/useOffsetCandidatesForAnalysis' +import { LegacyApplyHistoricOffsets } from '/app/organisms/LegacyApplyHistoricOffsets' +import { useOffsetCandidatesForAnalysis } from '/app/organisms/LegacyApplyHistoricOffsets/hooks/useOffsetCandidatesForAnalysis' import { FileCard } from '../ChooseRobotSlideout/FileCard' import { getRunTimeParameterFilesForRun, @@ -62,11 +62,13 @@ import { } from '/app/transformations/runs' import { getAnalysisStatus } from '/app/organisms/Desktop/ProtocolsLanding/utils' +import type { MouseEventHandler } from 'react' import type { DropdownOption } from '@opentrons/components' import type { RunTimeParameter } from '@opentrons/shared-data' import type { Robot } from '/app/redux/discovery/types' import type { StoredProtocolData } from '/app/redux/protocol-storage' import type { State } from '/app/redux/types' +import { useFeatureFlag } from '/app/redux/config' export const CARD_OUTLINE_BORDER_STYLE = css` border-style: ${BORDERS.styleSolid}; @@ -100,34 +102,35 @@ export function ChooseProtocolSlideoutComponent( const [ showRestoreValuesTooltip, setShowRestoreValuesTooltip, - ] = React.useState(false) + ] = useState(false) const { robot, showSlideout, onCloseClick } = props const { name } = robot + const isNewLpc = useFeatureFlag('lpcRedesign') + const [ selectedProtocol, setSelectedProtocol, - ] = React.useState(null) - const [ - runTimeParametersOverrides, - setRunTimeParametersOverrides, - ] = React.useState([]) - const [currentPage, setCurrentPage] = React.useState(1) - const [hasParamError, setHasParamError] = React.useState(false) - const [hasMissingFileParam, setHasMissingFileParam] = React.useState( + ] = useState(null) + const [runTimeParametersOverrides, setRunTimeParametersOverrides] = useState< + RunTimeParameter[] + >([]) + const [currentPage, setCurrentPage] = useState(1) + const [hasParamError, setHasParamError] = useState(false) + const [hasMissingFileParam, setHasMissingFileParam] = useState( runTimeParametersOverrides?.some( parameter => parameter.type === 'csv_file' ) ?? false ) - const [isInputFocused, setIsInputFocused] = React.useState(false) + const [isInputFocused, setIsInputFocused] = useState(false) - React.useEffect(() => { + useEffect(() => { setRunTimeParametersOverrides( selectedProtocol?.mostRecentAnalysis?.runTimeParameters ?? [] ) }, [selectedProtocol]) - React.useEffect(() => { + useEffect(() => { setHasParamError(errors.length > 0) setHasMissingFileParam( runTimeParametersOverrides.some( @@ -149,7 +152,7 @@ export function ChooseProtocolSlideoutComponent( const missingAnalysisData = analysisStatus === 'error' || analysisStatus === 'stale' - const [shouldApplyOffsets, setShouldApplyOffsets] = React.useState(true) + const [shouldApplyOffsets, setShouldApplyOffsets] = useState(true) const offsetCandidates = useOffsetCandidatesForAnalysis( (!missingAnalysisData ? selectedProtocol?.mostRecentAnalysis : null) ?? null, @@ -211,7 +214,7 @@ export function ChooseProtocolSlideoutComponent( })) : [] ) - const handleProceed: React.MouseEventHandler = () => { + const handleProceed: MouseEventHandler = () => { if (selectedProtocol != null) { trackCreateProtocolRunEvent({ name: 'createProtocolRecordRequest' }) const dataFilesForProtocolMap = runTimeParametersOverrides.reduce< @@ -489,7 +492,7 @@ export function ChooseProtocolSlideoutComponent( ), }} @@ -651,28 +654,30 @@ export function ChooseProtocolSlideoutComponent( robot?.ip === OPENTRONS_USB ? appShellRequestor : undefined } > - {currentPage === 1 ? ( - - ) : null} + {currentPage === 1 + ? !isNewLpc && ( + + ) + : null} {hasRunTimeParameters ? multiPageFooter : singlePageFooter} } @@ -725,7 +730,7 @@ function StoredProtocolList(props: StoredProtocolListProps): JSX.Element { ).filter( protocol => protocol.mostRecentAnalysis?.robotType === robot.robotModel ) - React.useEffect(() => { + useEffect(() => { handleSelectProtocol(first(storedProtocols) ?? null) }, []) @@ -744,7 +749,7 @@ function StoredProtocolList(props: StoredProtocolListProps): JSX.Element { const requiresCsvRunTimeParameter = analysisStatus === 'parameterRequired' return ( - + ) : null} - + ) })}
        diff --git a/app/src/organisms/Desktop/ChooseRobotSlideout/AvailableRobotOption.tsx b/app/src/organisms/Desktop/ChooseRobotSlideout/AvailableRobotOption.tsx index f06464c2f2a..992e25706dc 100644 --- a/app/src/organisms/Desktop/ChooseRobotSlideout/AvailableRobotOption.tsx +++ b/app/src/organisms/Desktop/ChooseRobotSlideout/AvailableRobotOption.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useEffect, useState } from 'react' import { useDispatch, useSelector } from 'react-redux' import { css } from 'styled-components' import { Trans, useTranslation } from 'react-i18next' @@ -24,6 +24,7 @@ import OT2_PNG from '/app/assets/images/OT2-R_HERO.png' import FLEX_PNG from '/app/assets/images/FLEX.png' import { useCurrentRunId, useNotifyRunQuery } from '/app/resources/runs' +import type { Dispatch as ReactDispatch } from 'react' import type { IconName } from '@opentrons/components' import type { Runs } from '@opentrons/api-client' import type { Robot } from '/app/redux/discovery/types' @@ -35,7 +36,7 @@ interface AvailableRobotOptionProps { onClick: () => void isSelected: boolean isSelectedRobotOnDifferentSoftwareVersion: boolean - registerRobotBusyStatus: React.Dispatch + registerRobotBusyStatus: ReactDispatch isError?: boolean showIdleOnly?: boolean } @@ -59,7 +60,7 @@ export function AvailableRobotOption( getRobotModelByName(state, robotName) ) - const [isBusy, setIsBusy] = React.useState(true) + const [isBusy, setIsBusy] = useState(true) const currentRunId = useCurrentRunId( { @@ -112,7 +113,7 @@ export function AvailableRobotOption( iconName = 'usb' } - React.useEffect(() => { + useEffect(() => { dispatch(fetchStatus(robotName)) // eslint-disable-next-line react-hooks/exhaustive-deps }, []) diff --git a/app/src/organisms/Desktop/ChooseRobotSlideout/__tests__/ChooseRobotSlideout.test.tsx b/app/src/organisms/Desktop/ChooseRobotSlideout/__tests__/ChooseRobotSlideout.test.tsx index 998c82462bc..33f601165d2 100644 --- a/app/src/organisms/Desktop/ChooseRobotSlideout/__tests__/ChooseRobotSlideout.test.tsx +++ b/app/src/organisms/Desktop/ChooseRobotSlideout/__tests__/ChooseRobotSlideout.test.tsx @@ -1,6 +1,4 @@ -import type * as React from 'react' import { vi, it, describe, expect, beforeEach } from 'vitest' - import { MemoryRouter } from 'react-router-dom' import { fireEvent, screen } from '@testing-library/react' import { OT2_ROBOT_TYPE } from '@opentrons/shared-data' @@ -22,13 +20,15 @@ import { import { getNetworkInterfaces } from '/app/redux/networking' import { ChooseRobotSlideout } from '..' import { useNotifyDataReady } from '/app/resources/useNotifyDataReady' + +import type { ComponentProps } from 'react' import type { RunTimeParameter } from '@opentrons/shared-data' vi.mock('/app/redux/discovery') vi.mock('/app/redux/robot-update') vi.mock('/app/redux/networking') vi.mock('/app/resources/useNotifyDataReady') -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders( diff --git a/app/src/organisms/Desktop/ChooseRobotSlideout/__tests__/FileCard.test.tsx b/app/src/organisms/Desktop/ChooseRobotSlideout/__tests__/FileCard.test.tsx index 9a151cd1704..9a150141526 100644 --- a/app/src/organisms/Desktop/ChooseRobotSlideout/__tests__/FileCard.test.tsx +++ b/app/src/organisms/Desktop/ChooseRobotSlideout/__tests__/FileCard.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { vi, it, describe, expect } from 'vitest' import { MemoryRouter } from 'react-router-dom' import { fireEvent, screen } from '@testing-library/react' @@ -7,6 +6,7 @@ import { renderWithProviders } from '/app/__testing-utils__' import { i18n } from '/app/i18n' import { FileCard } from '../FileCard' +import type { ComponentProps } from 'react' import type { CsvFileParameter } from '@opentrons/shared-data' vi.mock('/app/redux/discovery') @@ -15,7 +15,7 @@ vi.mock('/app/redux/networking') vi.mock('/app/resources/useNotifyDataReady') vi.mock('/app/redux/config') -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders( diff --git a/app/src/organisms/Desktop/ChooseRobotToRunProtocolSlideout/__tests__/ChooseRobotToRunProtocolSlideout.test.tsx b/app/src/organisms/Desktop/ChooseRobotToRunProtocolSlideout/__tests__/ChooseRobotToRunProtocolSlideout.test.tsx index 5c7e858fb38..19e3d39c370 100644 --- a/app/src/organisms/Desktop/ChooseRobotToRunProtocolSlideout/__tests__/ChooseRobotToRunProtocolSlideout.test.tsx +++ b/app/src/organisms/Desktop/ChooseRobotToRunProtocolSlideout/__tests__/ChooseRobotToRunProtocolSlideout.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { vi, it, describe, expect, beforeEach, afterEach } from 'vitest' import { MemoryRouter } from 'react-router-dom' import { fireEvent, screen, waitFor } from '@testing-library/react' @@ -27,11 +26,12 @@ import { storedProtocolDataWithCsvRunTimeParameter, } from '/app/redux/protocol-storage/__fixtures__' import { useCreateRunFromProtocol } from '../useCreateRunFromProtocol' -import { useOffsetCandidatesForAnalysis } from '/app/organisms/ApplyHistoricOffsets/hooks/useOffsetCandidatesForAnalysis' +import { useOffsetCandidatesForAnalysis } from '/app/organisms/LegacyApplyHistoricOffsets/hooks/useOffsetCandidatesForAnalysis' import { ChooseRobotToRunProtocolSlideout } from '../' import { useNotifyDataReady } from '/app/resources/useNotifyDataReady' import { useCurrentRunId, useCloseCurrentRun } from '/app/resources/runs' +import type { ComponentProps } from 'react' import type { State } from '/app/redux/types' vi.mock('/app/organisms/Desktop/Devices/hooks') @@ -43,13 +43,13 @@ vi.mock('/app/redux/robot-update') vi.mock('/app/redux/networking') vi.mock('../useCreateRunFromProtocol') vi.mock( - '/app/organisms/ApplyHistoricOffsets/hooks/useOffsetCandidatesForAnalysis' + '/app/organisms/LegacyApplyHistoricOffsets/hooks/useOffsetCandidatesForAnalysis' ) vi.mock('/app/resources/useNotifyDataReady') vi.mock('/app/resources/runs') const render = ( - props: React.ComponentProps + props: ComponentProps ) => { return renderWithProviders( @@ -436,9 +436,7 @@ describe('ChooseRobotToRunProtocolSlideout', () => { fireEvent.click(proceedButton) const confirm = screen.getByRole('button', { name: 'Confirm values' }) fireEvent.pointerEnter(confirm) - await waitFor(() => - screen.findByText('Add the required CSV file to continue.') - ) + await screen.findByText('Add the required CSV file to continue.') }) }) diff --git a/app/src/organisms/Desktop/ChooseRobotToRunProtocolSlideout/index.tsx b/app/src/organisms/Desktop/ChooseRobotToRunProtocolSlideout/index.tsx index f4850a649cf..a2608c16c8f 100644 --- a/app/src/organisms/Desktop/ChooseRobotToRunProtocolSlideout/index.tsx +++ b/app/src/organisms/Desktop/ChooseRobotToRunProtocolSlideout/index.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useState } from 'react' import first from 'lodash/first' import { useTranslation } from 'react-i18next' import { useNavigate } from 'react-router-dom' @@ -29,10 +29,13 @@ import { getRunTimeParameterFilesForRun, getRunTimeParameterValuesForRun, } from '/app/transformations/runs' -import { ApplyHistoricOffsets } from '/app/organisms/ApplyHistoricOffsets' -import { useOffsetCandidatesForAnalysis } from '/app/organisms/ApplyHistoricOffsets/hooks/useOffsetCandidatesForAnalysis' +import { LegacyApplyHistoricOffsets } from '/app/organisms/LegacyApplyHistoricOffsets' +import { useOffsetCandidatesForAnalysis } from '/app/organisms/LegacyApplyHistoricOffsets/hooks/useOffsetCandidatesForAnalysis' import { ChooseRobotSlideout } from '../ChooseRobotSlideout' import { useCreateRunFromProtocol } from './useCreateRunFromProtocol' +import { useFeatureFlag } from '/app/redux/config' + +import type { MouseEventHandler } from 'react' import type { StyleProps } from '@opentrons/components' import type { RunTimeParameter } from '@opentrons/shared-data' import type { Robot } from '/app/redux/discovery/types' @@ -65,16 +68,15 @@ export function ChooseRobotToRunProtocolSlideoutComponent( setSelectedRobot, } = props const navigate = useNavigate() - const [shouldApplyOffsets, setShouldApplyOffsets] = React.useState( - true - ) + const isNewLpc = useFeatureFlag('lpcRedesign') + const [shouldApplyOffsets, setShouldApplyOffsets] = useState(true) const { protocolKey, srcFileNames, srcFiles, mostRecentAnalysis, } = storedProtocolData - const [currentPage, setCurrentPage] = React.useState(1) + const [currentPage, setCurrentPage] = useState(1) const { trackCreateProtocolRunEvent } = useTrackCreateProtocolRunEvent( storedProtocolData, selectedRobot?.name ?? '' @@ -82,12 +84,11 @@ export function ChooseRobotToRunProtocolSlideoutComponent( const runTimeParameters = storedProtocolData.mostRecentAnalysis?.runTimeParameters ?? [] - const [ - runTimeParametersOverrides, - setRunTimeParametersOverrides, - ] = React.useState(runTimeParameters) - const [hasParamError, setHasParamError] = React.useState(false) - const [hasMissingFileParam, setHasMissingFileParam] = React.useState( + const [runTimeParametersOverrides, setRunTimeParametersOverrides] = useState< + RunTimeParameter[] + >(runTimeParameters) + const [hasParamError, setHasParamError] = useState(false) + const [hasMissingFileParam, setHasMissingFileParam] = useState( runTimeParameters?.some(parameter => parameter.type === 'csv_file') ?? false ) @@ -133,7 +134,7 @@ export function ChooseRobotToRunProtocolSlideoutComponent( })) : [] ) - const handleProceed: React.MouseEventHandler = () => { + const handleProceed: MouseEventHandler = () => { trackCreateProtocolRunEvent({ name: 'createProtocolRecordRequest' }) const dataFilesForProtocolMap = runTimeParametersOverrides.reduce< Record @@ -220,8 +221,8 @@ export function ChooseRobotToRunProtocolSlideoutComponent( ) - const offsetsComponent = ( - (null) + const [selectedRobot, setSelectedRobot] = useState(null) return ( void } @@ -26,7 +28,7 @@ export function CheckPipettesButton( ): JSX.Element | null { const { onDone, children, direction } = props const { t } = useTranslation('change_pipette') - const [isPending, setIsPending] = React.useState(false) + const [isPending, setIsPending] = useState(false) const { refetch: refetchPipettes } = usePipettesQuery( { refresh: true }, { diff --git a/app/src/organisms/Desktop/Devices/ChangePipette/ConfirmPipette.tsx b/app/src/organisms/Desktop/Devices/ChangePipette/ConfirmPipette.tsx index f756059d003..6f6585cefe4 100644 --- a/app/src/organisms/Desktop/Devices/ChangePipette/ConfirmPipette.tsx +++ b/app/src/organisms/Desktop/Devices/ChangePipette/ConfirmPipette.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { useTranslation } from 'react-i18next' import { OT2_ROBOT_TYPE } from '@opentrons/shared-data' import { @@ -13,6 +12,7 @@ import { CheckPipettesButton } from './CheckPipettesButton' import { SimpleWizardBody } from '/app/molecules/SimpleWizardBody' import { LevelPipette } from './LevelPipette' +import type { Dispatch, SetStateAction } from 'react' import type { PipetteNameSpecs, PipetteModelSpecs, @@ -36,11 +36,9 @@ export interface ConfirmPipetteProps { // wrongWantedPipette is referring to if the user attaches a pipette that is different // from wantedPipette and they want to use it anyway wrongWantedPipette: PipetteNameSpecs | null - setWrongWantedPipette: React.Dispatch< - React.SetStateAction - > + setWrongWantedPipette: Dispatch> confirmPipetteLevel: boolean - setConfirmPipetteLevel: React.Dispatch> + setConfirmPipetteLevel: Dispatch> tryAgain: () => void exit: () => void nextStep: () => void diff --git a/app/src/organisms/Desktop/Devices/ChangePipette/InstructionStep.tsx b/app/src/organisms/Desktop/Devices/ChangePipette/InstructionStep.tsx index 5b6338be6a5..94d617f2ea4 100644 --- a/app/src/organisms/Desktop/Devices/ChangePipette/InstructionStep.tsx +++ b/app/src/organisms/Desktop/Devices/ChangePipette/InstructionStep.tsx @@ -1,14 +1,15 @@ -import type * as React from 'react' import { Box, Flex, JUSTIFY_SPACE_EVENLY, SPACING } from '@opentrons/components' import type { PipetteChannels, PipetteDisplayCategory, } from '@opentrons/shared-data' + +import type { ReactNode } from 'react' import type { Mount } from '@opentrons/components' import type { Diagram, Direction } from './types' interface Props { - children: React.ReactNode + children: ReactNode direction: Direction mount: Mount channels: PipetteChannels diff --git a/app/src/organisms/Desktop/Devices/ChangePipette/PipetteSelection.tsx b/app/src/organisms/Desktop/Devices/ChangePipette/PipetteSelection.tsx index c306f132154..f998887afbd 100644 --- a/app/src/organisms/Desktop/Devices/ChangePipette/PipetteSelection.tsx +++ b/app/src/organisms/Desktop/Devices/ChangePipette/PipetteSelection.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { useTranslation } from 'react-i18next' import { DIRECTION_COLUMN, @@ -10,7 +9,9 @@ import { import { OT3_PIPETTES } from '@opentrons/shared-data' import { PipetteSelect } from '/app/molecules/PipetteSelect' -export type PipetteSelectionProps = React.ComponentProps +import type { ComponentProps } from 'react' + +export type PipetteSelectionProps = ComponentProps export function PipetteSelection(props: PipetteSelectionProps): JSX.Element { const { t } = useTranslation('change_pipette') diff --git a/app/src/organisms/Desktop/Devices/ChangePipette/__tests__/ChangePipette.test.tsx b/app/src/organisms/Desktop/Devices/ChangePipette/__tests__/ChangePipette.test.tsx index 89112a484d4..6cedab5a47b 100644 --- a/app/src/organisms/Desktop/Devices/ChangePipette/__tests__/ChangePipette.test.tsx +++ b/app/src/organisms/Desktop/Devices/ChangePipette/__tests__/ChangePipette.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { vi, it, describe, expect, beforeEach } from 'vitest' import { fireEvent, screen } from '@testing-library/react' @@ -21,6 +20,7 @@ import { ExitModal } from '../ExitModal' import { ConfirmPipette } from '../ConfirmPipette' import { ChangePipette } from '..' +import type { ComponentProps } from 'react' import type { NavigateFunction } from 'react-router-dom' import type { PipetteNameSpecs } from '@opentrons/shared-data' import type { AttachedPipette } from '/app/redux/pipettes/types' @@ -54,7 +54,7 @@ vi.mock('../ConfirmPipette') vi.mock('/app/resources/instruments') vi.mock('/app/assets/images') -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, })[0] @@ -78,7 +78,7 @@ const mockAttachedPipettes = { } describe('ChangePipette', () => { - let props: React.ComponentProps + let props: ComponentProps let dispatchApiRequest: DispatchApiRequestType beforeEach(() => { diff --git a/app/src/organisms/Desktop/Devices/ChangePipette/__tests__/CheckPipettesButton.test.tsx b/app/src/organisms/Desktop/Devices/ChangePipette/__tests__/CheckPipettesButton.test.tsx index a5fa3a50bd6..ce6f7f2db25 100644 --- a/app/src/organisms/Desktop/Devices/ChangePipette/__tests__/CheckPipettesButton.test.tsx +++ b/app/src/organisms/Desktop/Devices/ChangePipette/__tests__/CheckPipettesButton.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { fireEvent, screen } from '@testing-library/react' import { vi, it, describe, expect, beforeEach } from 'vitest' @@ -8,16 +7,18 @@ import { renderWithProviders } from '/app/__testing-utils__' import { i18n } from '/app/i18n' import { CheckPipettesButton } from '../CheckPipettesButton' +import type { ComponentProps } from 'react' + vi.mock('@opentrons/react-api-client') -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, })[0] } describe('CheckPipettesButton', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { robotName: 'otie', diff --git a/app/src/organisms/Desktop/Devices/ChangePipette/__tests__/ClearDeckModal.test.tsx b/app/src/organisms/Desktop/Devices/ChangePipette/__tests__/ClearDeckModal.test.tsx index f4af4566141..85ae7a52495 100644 --- a/app/src/organisms/Desktop/Devices/ChangePipette/__tests__/ClearDeckModal.test.tsx +++ b/app/src/organisms/Desktop/Devices/ChangePipette/__tests__/ClearDeckModal.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { fireEvent, screen } from '@testing-library/react' import { vi, it, describe, expect, beforeEach } from 'vitest' @@ -6,13 +5,15 @@ import { renderWithProviders } from '/app/__testing-utils__' import { i18n } from '/app/i18n' import { ClearDeckModal } from '../ClearDeckModal' -const render = (props: React.ComponentProps) => { +import type { ComponentProps } from 'react' + +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, })[0] } describe('ClearDeckModal', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { onContinueClick: vi.fn(), diff --git a/app/src/organisms/Desktop/Devices/ChangePipette/__tests__/ConfirmPipette.test.tsx b/app/src/organisms/Desktop/Devices/ChangePipette/__tests__/ConfirmPipette.test.tsx index a328df38383..ebb06f3dc91 100644 --- a/app/src/organisms/Desktop/Devices/ChangePipette/__tests__/ConfirmPipette.test.tsx +++ b/app/src/organisms/Desktop/Devices/ChangePipette/__tests__/ConfirmPipette.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { fireEvent, screen } from '@testing-library/react' import { vi, it, describe, expect } from 'vitest' @@ -9,6 +8,7 @@ import { mockPipetteInfo } from '/app/redux/pipettes/__fixtures__' import { CheckPipettesButton } from '../CheckPipettesButton' import { ConfirmPipette } from '../ConfirmPipette' +import type { ComponentProps } from 'react' import type { PipetteModelSpecs, PipetteNameSpecs, @@ -25,7 +25,7 @@ vi.mock('../LevelPipette', async importOriginal => { } }) -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, })[0] @@ -87,7 +87,7 @@ const MOCK_WANTED_PIPETTE = { } as PipetteNameSpecs describe('ConfirmPipette', () => { - let props: React.ComponentProps + let props: ComponentProps it('Should detach a pipette successfully', () => { props = { diff --git a/app/src/organisms/Desktop/Devices/ChangePipette/__tests__/ExitModal.test.tsx b/app/src/organisms/Desktop/Devices/ChangePipette/__tests__/ExitModal.test.tsx index 29bf417071c..0b4155ebd09 100644 --- a/app/src/organisms/Desktop/Devices/ChangePipette/__tests__/ExitModal.test.tsx +++ b/app/src/organisms/Desktop/Devices/ChangePipette/__tests__/ExitModal.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { fireEvent, screen } from '@testing-library/react' import { vi, it, describe, expect, beforeEach } from 'vitest' @@ -6,13 +5,15 @@ import { renderWithProviders } from '/app/__testing-utils__' import { i18n } from '/app/i18n' import { ExitModal } from '../ExitModal' -const render = (props: React.ComponentProps) => { +import type { ComponentProps } from 'react' + +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, })[0] } describe('ExitModal', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { back: vi.fn(), diff --git a/app/src/organisms/Desktop/Devices/ChangePipette/__tests__/InstructionStep.test.tsx b/app/src/organisms/Desktop/Devices/ChangePipette/__tests__/InstructionStep.test.tsx index 80cdde63972..39e53564b68 100644 --- a/app/src/organisms/Desktop/Devices/ChangePipette/__tests__/InstructionStep.test.tsx +++ b/app/src/organisms/Desktop/Devices/ChangePipette/__tests__/InstructionStep.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { it, describe, beforeEach } from 'vitest' import { screen } from '@testing-library/react' @@ -8,13 +7,15 @@ import { renderWithProviders } from '/app/__testing-utils__' import { i18n } from '/app/i18n' import { InstructionStep } from '../InstructionStep' -const render = (props: React.ComponentProps) => { +import type { ComponentProps } from 'react' + +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, })[0] } describe('InstructionStep', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { children:
        children
        , diff --git a/app/src/organisms/Desktop/Devices/ChangePipette/__tests__/Instructions.test.tsx b/app/src/organisms/Desktop/Devices/ChangePipette/__tests__/Instructions.test.tsx index 024be58e798..4f01b748286 100644 --- a/app/src/organisms/Desktop/Devices/ChangePipette/__tests__/Instructions.test.tsx +++ b/app/src/organisms/Desktop/Devices/ChangePipette/__tests__/Instructions.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { vi, it, describe, expect, beforeEach } from 'vitest' import { fireEvent, screen } from '@testing-library/react' @@ -12,9 +11,11 @@ import { mockPipetteInfo } from '/app/redux/pipettes/__fixtures__' import { Instructions } from '../Instructions' import { CheckPipettesButton } from '../CheckPipettesButton' +import type { ComponentProps } from 'react' + vi.mock('../CheckPipettesButton') -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, })[0] @@ -29,7 +30,7 @@ const MOCK_ACTUAL_PIPETTE = { } as PipetteModelSpecs describe('Instructions', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { diff --git a/app/src/organisms/Desktop/Devices/ChangePipette/__tests__/LevelPipette.test.tsx b/app/src/organisms/Desktop/Devices/ChangePipette/__tests__/LevelPipette.test.tsx index 78c2e9f8d6d..3b74cf10ea1 100644 --- a/app/src/organisms/Desktop/Devices/ChangePipette/__tests__/LevelPipette.test.tsx +++ b/app/src/organisms/Desktop/Devices/ChangePipette/__tests__/LevelPipette.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { vi, it, describe, expect, beforeEach } from 'vitest' import { fireEvent, screen } from '@testing-library/react' @@ -6,9 +5,11 @@ import { LEFT } from '@opentrons/shared-data' import { nestedTextMatcher, renderWithProviders } from '/app/__testing-utils__' import { i18n } from '/app/i18n' import { LevelPipette } from '../LevelPipette' + +import type { ComponentProps } from 'react' import type { PipetteNameSpecs } from '@opentrons/shared-data' -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, })[0] @@ -57,7 +58,7 @@ const MOCK_WANTED_PIPETTE = { } as PipetteNameSpecs describe('LevelPipette', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { diff --git a/app/src/organisms/Desktop/Devices/ChangePipette/__tests__/PipetteSelection.test.tsx b/app/src/organisms/Desktop/Devices/ChangePipette/__tests__/PipetteSelection.test.tsx index f3619e9930d..9f507dc8a36 100644 --- a/app/src/organisms/Desktop/Devices/ChangePipette/__tests__/PipetteSelection.test.tsx +++ b/app/src/organisms/Desktop/Devices/ChangePipette/__tests__/PipetteSelection.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { vi, it, describe, beforeEach } from 'vitest' import { screen } from '@testing-library/react' @@ -7,15 +6,17 @@ import { i18n } from '/app/i18n' import { PipetteSelect } from '/app/molecules/PipetteSelect' import { PipetteSelection } from '../PipetteSelection' +import type { ComponentProps } from 'react' + vi.mock('/app/molecules/PipetteSelect') -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, })[0] } describe('PipetteSelection', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { pipetteName: null, diff --git a/app/src/organisms/Desktop/Devices/ConfigurePipette/ConfigFormGroup.tsx b/app/src/organisms/Desktop/Devices/ConfigurePipette/ConfigFormGroup.tsx index 9c8a2c25acf..e006efe1c5c 100644 --- a/app/src/organisms/Desktop/Devices/ConfigurePipette/ConfigFormGroup.tsx +++ b/app/src/organisms/Desktop/Devices/ConfigurePipette/ConfigFormGroup.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { Controller } from 'react-hook-form' import { CheckboxField, @@ -12,11 +11,12 @@ import { } from '@opentrons/components' import styles from './styles.module.css' +import type { ReactNode } from 'react' import type { Control } from 'react-hook-form' import type { DisplayFieldProps, DisplayQuirkFieldProps } from './ConfigForm' export interface FormColumnProps { - children: React.ReactNode + children: ReactNode } export function FormColumn(props: FormColumnProps): JSX.Element { @@ -65,7 +65,7 @@ export function ConfigFormGroup(props: ConfigFormGroupProps): JSX.Element { export interface ConfigFormRowProps { label: string labelFor: string - children: React.ReactNode + children: ReactNode } const FIELD_ID_PREFIX = '__PipetteConfig__' diff --git a/app/src/organisms/Desktop/Devices/ConfigurePipette/ConfigMessage.tsx b/app/src/organisms/Desktop/Devices/ConfigurePipette/ConfigMessage.tsx deleted file mode 100644 index d6a32fa6d4b..00000000000 --- a/app/src/organisms/Desktop/Devices/ConfigurePipette/ConfigMessage.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import styles from './styles.module.css' - -// TODO (ka 2019-2-12): Add intercom onClick to assistance text -export function ConfigMessage(): JSX.Element { - return ( -
        -

        Warning:

        -

        - These are advanced settings. Please do not attempt to adjust without - assistance from an Opentrons support team member, as doing - so may affect the lifespan of your pipette. -

        -

        - Note that these settings will not override any pipette settings - pre-defined in protocols. -

        -
        - ) -} diff --git a/app/src/organisms/Desktop/Devices/ConfigurePipette/__tests__/ConfigFormResetButton.test.tsx b/app/src/organisms/Desktop/Devices/ConfigurePipette/__tests__/ConfigFormResetButton.test.tsx index 4c2ec08c7d4..7e26987f573 100644 --- a/app/src/organisms/Desktop/Devices/ConfigurePipette/__tests__/ConfigFormResetButton.test.tsx +++ b/app/src/organisms/Desktop/Devices/ConfigurePipette/__tests__/ConfigFormResetButton.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { fireEvent, screen } from '@testing-library/react' import { vi, it, expect, describe, beforeEach } from 'vitest' @@ -6,14 +5,16 @@ import { renderWithProviders } from '/app/__testing-utils__' import { i18n } from '/app/i18n' import { ConfigFormResetButton } from '../ConfigFormResetButton' -const render = (props: React.ComponentProps) => { +import type { ComponentProps } from 'react' + +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, })[0] } describe('ConfigFormResetButton', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { onClick: vi.fn(), diff --git a/app/src/organisms/Desktop/Devices/ConfigurePipette/__tests__/ConfigFormSubmitButton.test.tsx b/app/src/organisms/Desktop/Devices/ConfigurePipette/__tests__/ConfigFormSubmitButton.test.tsx index 3dbab681883..73db2ec1596 100644 --- a/app/src/organisms/Desktop/Devices/ConfigurePipette/__tests__/ConfigFormSubmitButton.test.tsx +++ b/app/src/organisms/Desktop/Devices/ConfigurePipette/__tests__/ConfigFormSubmitButton.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { it, expect, describe, beforeEach } from 'vitest' import { screen } from '@testing-library/react' @@ -6,14 +5,16 @@ import { renderWithProviders } from '/app/__testing-utils__' import { i18n } from '/app/i18n' import { ConfigFormSubmitButton } from '../ConfigFormSubmitButton' -const render = (props: React.ComponentProps) => { +import type { ComponentProps } from 'react' + +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, })[0] } describe('ConfigFormSubmitButton', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { disabled: false, diff --git a/app/src/organisms/Desktop/Devices/ConfigurePipette/__tests__/ConfigurePipette.test.tsx b/app/src/organisms/Desktop/Devices/ConfigurePipette/__tests__/ConfigurePipette.test.tsx index 2d7790bdd24..5f810b3b97c 100644 --- a/app/src/organisms/Desktop/Devices/ConfigurePipette/__tests__/ConfigurePipette.test.tsx +++ b/app/src/organisms/Desktop/Devices/ConfigurePipette/__tests__/ConfigurePipette.test.tsx @@ -1,6 +1,6 @@ -import type * as React from 'react' import { when } from 'vitest-when' import { vi, it, expect, describe, beforeEach } from 'vitest' +import { screen } from '@testing-library/react' import { renderWithProviders } from '/app/__testing-utils__' import { i18n } from '/app/i18n' @@ -9,14 +9,14 @@ import { ConfigurePipette } from '../../ConfigurePipette' import { mockPipetteSettingsFieldsMap } from '/app/redux/pipettes/__fixtures__' import { getConfig } from '/app/redux/config' +import type { ComponentProps } from 'react' import type { DispatchApiRequestType } from '/app/redux/robot-api' import type { State } from '/app/redux/types' -import { screen } from '@testing-library/react' vi.mock('/app/redux/robot-api') vi.mock('/app/redux/config') -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, })[0] @@ -26,7 +26,7 @@ const mockRobotName = 'mockRobotName' describe('ConfigurePipette', () => { let dispatchApiRequest: DispatchApiRequestType - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { diff --git a/app/src/organisms/Desktop/Devices/ErrorRecoveryBanner/__tests__/ErrorRecoveryBanner.test.tsx b/app/src/organisms/Desktop/Devices/ErrorRecoveryBanner/__tests__/ErrorRecoveryBanner.test.tsx index fc234a52629..58a79593fe6 100644 --- a/app/src/organisms/Desktop/Devices/ErrorRecoveryBanner/__tests__/ErrorRecoveryBanner.test.tsx +++ b/app/src/organisms/Desktop/Devices/ErrorRecoveryBanner/__tests__/ErrorRecoveryBanner.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { describe, it, vi, beforeEach } from 'vitest' import { screen } from '@testing-library/react' @@ -6,6 +5,8 @@ import { renderWithProviders } from '/app/__testing-utils__' import { i18n } from '/app/i18n' import { useErrorRecoveryBanner, ErrorRecoveryBanner } from '..' +import type { ComponentProps } from 'react' + vi.mock('..', async importOriginal => { const actualReact = await importOriginal() return { @@ -14,7 +15,7 @@ vi.mock('..', async importOriginal => { } }) -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, })[0] diff --git a/app/src/organisms/Desktop/Devices/GripperCard/__tests__/AboutGripperSlideout.test.tsx b/app/src/organisms/Desktop/Devices/GripperCard/__tests__/AboutGripperSlideout.test.tsx index 9ad4381a40c..373ba5256af 100644 --- a/app/src/organisms/Desktop/Devices/GripperCard/__tests__/AboutGripperSlideout.test.tsx +++ b/app/src/organisms/Desktop/Devices/GripperCard/__tests__/AboutGripperSlideout.test.tsx @@ -1,18 +1,19 @@ -import type * as React from 'react' import { screen, fireEvent } from '@testing-library/react' import { describe, it, vi, beforeEach, expect } from 'vitest' import { renderWithProviders } from '/app/__testing-utils__' import { i18n } from '/app/i18n' import { AboutGripperSlideout } from '../AboutGripperSlideout' -const render = (props: React.ComponentProps) => { +import type { ComponentProps } from 'react' + +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, })[0] } describe('AboutGripperSlideout', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { serialNumber: '123', diff --git a/app/src/organisms/Desktop/Devices/GripperCard/__tests__/GripperCard.test.tsx b/app/src/organisms/Desktop/Devices/GripperCard/__tests__/GripperCard.test.tsx index ccdbc80c2b3..32538ba2cb5 100644 --- a/app/src/organisms/Desktop/Devices/GripperCard/__tests__/GripperCard.test.tsx +++ b/app/src/organisms/Desktop/Devices/GripperCard/__tests__/GripperCard.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { fireEvent, screen } from '@testing-library/react' import { describe, it, vi, beforeEach, expect } from 'vitest' import { renderWithProviders } from '/app/__testing-utils__' @@ -7,20 +6,22 @@ import { i18n } from '/app/i18n' import { GripperWizardFlows } from '/app/organisms/GripperWizardFlows' import { AboutGripperSlideout } from '../AboutGripperSlideout' import { GripperCard } from '../' + +import type { ComponentProps } from 'react' import type { GripperData } from '@opentrons/api-client' vi.mock('/app/organisms/GripperWizardFlows') vi.mock('../AboutGripperSlideout') vi.mock('@opentrons/react-api-client') -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, })[0] } describe('GripperCard', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { attachedGripper: { diff --git a/app/src/organisms/Desktop/Devices/GripperCard/index.tsx b/app/src/organisms/Desktop/Devices/GripperCard/index.tsx index c8a5280049e..0d425640c11 100644 --- a/app/src/organisms/Desktop/Devices/GripperCard/index.tsx +++ b/app/src/organisms/Desktop/Devices/GripperCard/index.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useState, useEffect } from 'react' import { Trans, useTranslation } from 'react-i18next' import { css } from 'styled-components' import { @@ -15,6 +15,7 @@ import { GripperWizardFlows } from '/app/organisms/GripperWizardFlows' import { AboutGripperSlideout } from './AboutGripperSlideout' import { GRIPPER_FLOW_TYPES } from '/app/organisms/GripperWizardFlows/constants' +import type { MouseEventHandler } from 'react' import type { BadGripper, GripperData } from '@opentrons/api-client' import type { GripperModel } from '@opentrons/shared-data' import type { GripperWizardFlowType } from '/app/organisms/GripperWizardFlows/types' @@ -53,26 +54,24 @@ export function GripperCard({ const [ openWizardFlowType, setOpenWizardFlowType, - ] = React.useState(null) + ] = useState(null) const [ showAboutGripperSlideout, setShowAboutGripperSlideout, - ] = React.useState(false) + ] = useState(false) - const handleAttach: React.MouseEventHandler = () => { + const handleAttach: MouseEventHandler = () => { setOpenWizardFlowType(GRIPPER_FLOW_TYPES.ATTACH) } - const handleDetach: React.MouseEventHandler = () => { + const handleDetach: MouseEventHandler = () => { setOpenWizardFlowType(GRIPPER_FLOW_TYPES.DETACH) } - const handleCalibrate: React.MouseEventHandler = () => { + const handleCalibrate: MouseEventHandler = () => { setOpenWizardFlowType(GRIPPER_FLOW_TYPES.RECALIBRATE) } - const [pollForSubsystemUpdate, setPollForSubsystemUpdate] = React.useState( - false - ) + const [pollForSubsystemUpdate, setPollForSubsystemUpdate] = useState(false) const { data: subsystemUpdateData } = useCurrentSubsystemUpdateQuery( 'gripper', { @@ -84,7 +83,7 @@ export function GripperCard({ // detected until the update has been done for 5 seconds // this gives the instruments endpoint time to start reporting // a good instrument - React.useEffect(() => { + useEffect(() => { if (attachedGripper?.ok === false) { setPollForSubsystemUpdate(true) } else if ( diff --git a/app/src/organisms/Desktop/Devices/HistoricalProtocolRunOverflowMenu.tsx b/app/src/organisms/Desktop/Devices/HistoricalProtocolRunOverflowMenu.tsx index 18dc44cb11b..b696d8285cb 100644 --- a/app/src/organisms/Desktop/Devices/HistoricalProtocolRunOverflowMenu.tsx +++ b/app/src/organisms/Desktop/Devices/HistoricalProtocolRunOverflowMenu.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { useTranslation } from 'react-i18next' import { NavLink, useNavigate } from 'react-router-dom' @@ -38,6 +37,7 @@ import { useIsEstopNotDisengaged } from '/app/resources/devices' import { useTrackProtocolRunEvent } from '/app/redux-resources/analytics' import { useRobot } from '/app/redux-resources/robots' +import type { MouseEventHandler } from 'react' import type { Run } from '@opentrons/api-client' export interface HistoricalProtocolRunOverflowMenuProps { @@ -99,7 +99,7 @@ export function HistoricalProtocolRunOverflowMenu( } interface MenuDropdownProps extends HistoricalProtocolRunOverflowMenuProps { - closeOverflowMenu: React.MouseEventHandler + closeOverflowMenu: MouseEventHandler downloadRunLog: () => void isRunLogLoading: boolean } @@ -126,7 +126,7 @@ function MenuDropdown(props: MenuDropdownProps): JSX.Element { `/devices/${robotName}/protocol-runs/${createRunResponse.data.id}/run-preview` ) } - const onDownloadClick: React.MouseEventHandler = e => { + const onDownloadClick: MouseEventHandler = e => { e.preventDefault() e.stopPropagation() downloadRunLog() @@ -143,9 +143,7 @@ function MenuDropdown(props: MenuDropdownProps): JSX.Element { const robotSerialNumber = robot?.health?.robot_serial ?? robot?.serverHealth?.serialNumber ?? null - const handleResetClick: React.MouseEventHandler = ( - e - ): void => { + const handleResetClick: MouseEventHandler = (e): void => { e.preventDefault() e.stopPropagation() @@ -160,7 +158,7 @@ function MenuDropdown(props: MenuDropdownProps): JSX.Element { trackProtocolRunEvent({ name: ANALYTICS_PROTOCOL_RUN_ACTION.AGAIN }) } - const handleDeleteClick: React.MouseEventHandler = e => { + const handleDeleteClick: MouseEventHandler = e => { e.preventDefault() e.stopPropagation() deleteRun(runId) diff --git a/app/src/organisms/Desktop/Devices/PipetteCard/FlexPipetteCard.tsx b/app/src/organisms/Desktop/Devices/PipetteCard/FlexPipetteCard.tsx index 3a1354f7680..50848a92d0c 100644 --- a/app/src/organisms/Desktop/Devices/PipetteCard/FlexPipetteCard.tsx +++ b/app/src/organisms/Desktop/Devices/PipetteCard/FlexPipetteCard.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useState, useEffect } from 'react' import { Trans, useTranslation } from 'react-i18next' import { css } from 'styled-components' import { @@ -29,6 +29,7 @@ import { import { AboutPipetteSlideout } from './AboutPipetteSlideout' +import type { MouseEventHandler } from 'react' import type { BadPipette, HostConfig, @@ -79,12 +80,11 @@ export function FlexPipetteCard({ const [ showAboutPipetteSlideout, setShowAboutPipetteSlideout, - ] = React.useState(false) - const [showChoosePipette, setShowChoosePipette] = React.useState(false) - const [ - selectedPipette, - setSelectedPipette, - ] = React.useState(SINGLE_MOUNT_PIPETTES) + ] = useState(false) + const [showChoosePipette, setShowChoosePipette] = useState(false) + const [selectedPipette, setSelectedPipette] = useState( + SINGLE_MOUNT_PIPETTES + ) const attachedPipetteIs96Channel = attachedPipette?.ok && attachedPipette.instrumentName === 'p1000_96' const selectedPipetteForWizard = attachedPipetteIs96Channel @@ -107,7 +107,7 @@ export function FlexPipetteCard({ host, }) } - const handleChoosePipette: React.MouseEventHandler = () => { + const handleChoosePipette: MouseEventHandler = () => { setShowChoosePipette(true) } const handleAttach = (): void => { @@ -115,17 +115,15 @@ export function FlexPipetteCard({ handleLaunchPipetteWizardFlows(FLOWS.ATTACH) } - const handleDetach: React.MouseEventHandler = () => { + const handleDetach: MouseEventHandler = () => { handleLaunchPipetteWizardFlows(FLOWS.DETACH) } - const handleCalibrate: React.MouseEventHandler = () => { + const handleCalibrate: MouseEventHandler = () => { handleLaunchPipetteWizardFlows(FLOWS.CALIBRATE) } - const [pollForSubsystemUpdate, setPollForSubsystemUpdate] = React.useState( - false - ) + const [pollForSubsystemUpdate, setPollForSubsystemUpdate] = useState(false) const subsystem = attachedPipette?.subsystem ?? null const { data: subsystemUpdateData } = useCurrentSubsystemUpdateQuery( subsystem, @@ -139,7 +137,7 @@ export function FlexPipetteCard({ // detected until the update has been done for 5 seconds // this gives the instruments endpoint time to start reporting // a good instrument - React.useEffect(() => { + useEffect(() => { if (attachedPipette?.ok === false) { setPollForSubsystemUpdate(true) } else if ( diff --git a/app/src/organisms/Desktop/Devices/PipetteCard/__tests__/AboutPipetteSlideout.test.tsx b/app/src/organisms/Desktop/Devices/PipetteCard/__tests__/AboutPipetteSlideout.test.tsx index dd2274a3ab3..c39a0e286fc 100644 --- a/app/src/organisms/Desktop/Devices/PipetteCard/__tests__/AboutPipetteSlideout.test.tsx +++ b/app/src/organisms/Desktop/Devices/PipetteCard/__tests__/AboutPipetteSlideout.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { describe, it, vi, beforeEach, expect } from 'vitest' import '@testing-library/jest-dom/vitest' import { renderWithProviders } from '/app/__testing-utils__' @@ -7,16 +6,18 @@ import { i18n } from '/app/i18n' import { AboutPipetteSlideout } from '../AboutPipetteSlideout' import { mockLeftSpecs } from '/app/redux/pipettes/__fixtures__' +import type { ComponentProps } from 'react' + vi.mock('@opentrons/react-api-client') -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, })[0] } describe('AboutPipetteSlideout', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { pipetteId: '123', diff --git a/app/src/organisms/Desktop/Devices/PipetteCard/__tests__/FlexPipetteCard.test.tsx b/app/src/organisms/Desktop/Devices/PipetteCard/__tests__/FlexPipetteCard.test.tsx index bd753e9f9d3..6bbb7eacbd9 100644 --- a/app/src/organisms/Desktop/Devices/PipetteCard/__tests__/FlexPipetteCard.test.tsx +++ b/app/src/organisms/Desktop/Devices/PipetteCard/__tests__/FlexPipetteCard.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { fireEvent, screen } from '@testing-library/react' import { describe, it, vi, beforeEach, afterEach, expect } from 'vitest' import { renderWithProviders } from '/app/__testing-utils__' @@ -11,8 +10,9 @@ import { FlexPipetteCard } from '../FlexPipetteCard' import { ChoosePipette } from '/app/organisms/PipetteWizardFlows/ChoosePipette' import { useDropTipWizardFlows } from '/app/organisms/DropTipWizardFlows' -import type { PipetteData } from '@opentrons/api-client' +import type { ComponentProps } from 'react' import type { Mock } from 'vitest' +import type { PipetteData } from '@opentrons/api-client' vi.mock('/app/organisms/PipetteWizardFlows') vi.mock('/app/organisms/PipetteWizardFlows/ChoosePipette') @@ -20,7 +20,7 @@ vi.mock('../AboutPipetteSlideout') vi.mock('/app/organisms/DropTipWizardFlows') vi.mock('@opentrons/react-api-client') -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, })[0] @@ -29,7 +29,7 @@ const render = (props: React.ComponentProps) => { let mockDTWizToggle: Mock describe('FlexPipetteCard', () => { - let props: React.ComponentProps + let props: ComponentProps mockDTWizToggle = vi.fn() beforeEach(() => { props = { diff --git a/app/src/organisms/Desktop/Devices/PipetteCard/__tests__/PipetteCard.test.tsx b/app/src/organisms/Desktop/Devices/PipetteCard/__tests__/PipetteCard.test.tsx index e04796bd491..c71ee4ec4b4 100644 --- a/app/src/organisms/Desktop/Devices/PipetteCard/__tests__/PipetteCard.test.tsx +++ b/app/src/organisms/Desktop/Devices/PipetteCard/__tests__/PipetteCard.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { when } from 'vitest-when' import { fireEvent, screen } from '@testing-library/react' import { describe, it, vi, beforeEach, expect } from 'vitest' @@ -15,6 +14,7 @@ import { useDropTipWizardFlows } from '/app/organisms/DropTipWizardFlows' import { mockLeftSpecs, mockRightSpecs } from '/app/redux/pipettes/__fixtures__' +import type { ComponentProps } from 'react' import type { DispatchApiRequestType } from '/app/redux/robot-api' vi.mock('../PipetteOverflowMenu') @@ -24,7 +24,7 @@ vi.mock('@opentrons/react-api-client') vi.mock('/app/redux/pipettes') vi.mock('/app/organisms/DropTipWizardFlows') -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, })[0] @@ -33,7 +33,7 @@ const render = (props: React.ComponentProps) => { const mockRobotName = 'mockRobotName' describe('PipetteCard', () => { let dispatchApiRequest: DispatchApiRequestType - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { dispatchApiRequest = vi.fn() diff --git a/app/src/organisms/Desktop/Devices/PipetteCard/__tests__/PipetteOverflowMenu.test.tsx b/app/src/organisms/Desktop/Devices/PipetteCard/__tests__/PipetteOverflowMenu.test.tsx index 155d955b6ea..273d6bdf7b6 100644 --- a/app/src/organisms/Desktop/Devices/PipetteCard/__tests__/PipetteOverflowMenu.test.tsx +++ b/app/src/organisms/Desktop/Devices/PipetteCard/__tests__/PipetteOverflowMenu.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { fireEvent, screen } from '@testing-library/react' import { describe, it, vi, beforeEach, expect } from 'vitest' import '@testing-library/jest-dom/vitest' @@ -11,8 +10,9 @@ import { } from '/app/redux/pipettes/__fixtures__' import { isFlexPipette } from '@opentrons/shared-data' -import type { Mount } from '/app/redux/pipettes/types' +import type { ComponentProps } from 'react' import type * as SharedData from '@opentrons/shared-data' +import type { Mount } from '/app/redux/pipettes/types' vi.mock('/app/redux/config') vi.mock('@opentrons/shared-data', async importOriginal => { @@ -23,7 +23,7 @@ vi.mock('@opentrons/shared-data', async importOriginal => { } }) -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, })[0] @@ -31,7 +31,7 @@ const render = (props: React.ComponentProps) => { const LEFT = 'left' as Mount describe('PipetteOverflowMenu', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { diff --git a/app/src/organisms/Desktop/Devices/PipetteCard/__tests__/PipetteSettingsSlideout.test.tsx b/app/src/organisms/Desktop/Devices/PipetteCard/__tests__/PipetteSettingsSlideout.test.tsx index 37b6f66b863..c75945cd549 100644 --- a/app/src/organisms/Desktop/Devices/PipetteCard/__tests__/PipetteSettingsSlideout.test.tsx +++ b/app/src/organisms/Desktop/Devices/PipetteCard/__tests__/PipetteSettingsSlideout.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { when } from 'vitest-when' import { fireEvent, waitFor, screen } from '@testing-library/react' import { describe, it, vi, beforeEach, expect } from 'vitest' @@ -16,13 +15,12 @@ import { mockPipetteSettingsFieldsMap, } from '/app/redux/pipettes/__fixtures__' +import type { ComponentProps } from 'react' import type { Mock } from 'vitest' vi.mock('@opentrons/react-api-client') -const render = ( - props: React.ComponentProps -) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, })[0] @@ -31,7 +29,7 @@ const render = ( const mockRobotName = 'mockRobotName' describe('PipetteSettingsSlideout', () => { - let props: React.ComponentProps + let props: ComponentProps let mockUpdatePipetteSettings: Mock beforeEach(() => { diff --git a/app/src/organisms/Desktop/Devices/ProtocolRun/BackToTopButton.tsx b/app/src/organisms/Desktop/Devices/ProtocolRun/BackToTopButton.tsx index a8524988bf2..909b49f1548 100644 --- a/app/src/organisms/Desktop/Devices/ProtocolRun/BackToTopButton.tsx +++ b/app/src/organisms/Desktop/Devices/ProtocolRun/BackToTopButton.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { useTranslation } from 'react-i18next' import { Link } from 'react-router-dom' import { useRobot } from '/app/redux-resources/robots' @@ -10,8 +9,10 @@ import { ANALYTICS_PROTOCOL_PROCEED_TO_RUN, } from '/app/redux/analytics' +import type { RefObject } from 'react' + interface BackToTopButtonProps { - protocolRunHeaderRef: React.RefObject | null + protocolRunHeaderRef: RefObject | null robotName: string runId: string sourceLocation: string diff --git a/app/src/organisms/Desktop/Devices/ProtocolRun/EmptySetupStep.tsx b/app/src/organisms/Desktop/Devices/ProtocolRun/EmptySetupStep.tsx index 24c2b449083..5d9eb70774e 100644 --- a/app/src/organisms/Desktop/Devices/ProtocolRun/EmptySetupStep.tsx +++ b/app/src/organisms/Desktop/Devices/ProtocolRun/EmptySetupStep.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { COLORS, DIRECTION_COLUMN, @@ -10,10 +9,12 @@ import { JUSTIFY_SPACE_BETWEEN, } from '@opentrons/components' +import type { ReactNode } from 'react' + interface EmptySetupStepProps { - title: React.ReactNode + title: ReactNode description: string - rightElement?: React.ReactNode + rightElement?: ReactNode } export function EmptySetupStep(props: EmptySetupStepProps): JSX.Element { diff --git a/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderBannerContainer/ProtocolAnalysisErrorBanner.tsx b/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderBannerContainer/ProtocolAnalysisErrorBanner.tsx index 18a8f0e682a..753ed296d86 100644 --- a/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderBannerContainer/ProtocolAnalysisErrorBanner.tsx +++ b/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderBannerContainer/ProtocolAnalysisErrorBanner.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useState } from 'react' import { createPortal } from 'react-dom' import { Trans, useTranslation } from 'react-i18next' @@ -18,6 +18,7 @@ import { import { getTopPortalEl } from '/app/App/portal' +import type { MouseEventHandler } from 'react' import type { AnalysisError } from '@opentrons/shared-data' interface ProtocolAnalysisErrorBannerProps { @@ -29,9 +30,9 @@ export function ProtocolAnalysisErrorBanner( ): JSX.Element { const { errors } = props const { t } = useTranslation(['run_details']) - const [showErrorDetails, setShowErrorDetails] = React.useState(false) + const [showErrorDetails, setShowErrorDetails] = useState(false) - const handleToggleDetails: React.MouseEventHandler = e => { + const handleToggleDetails: MouseEventHandler = e => { e.preventDefault() e.stopPropagation() setShowErrorDetails(!showErrorDetails) diff --git a/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderBannerContainer/TerminalRunBannerContainer.tsx b/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderBannerContainer/TerminalRunBannerContainer.tsx index c6428c2f385..4148a791611 100644 --- a/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderBannerContainer/TerminalRunBannerContainer.tsx +++ b/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderBannerContainer/TerminalRunBannerContainer.tsx @@ -40,12 +40,15 @@ export function useTerminalRunBannerContainer({ const completedWithErrors = (commandErrorList != null && commandErrorList.length > 0) || highestPriorityError != null + // TODO(jh, 01-10-25): Adding /commandErrors to notifications accomplishes the below with reduced latency. + const completedWithNoErrors = + commandErrorList != null && commandErrorList.length === 0 const showSuccessBanner = runStatus === RUN_STATUS_SUCCEEDED && isRunCurrent && !isResetRunLoading && - !completedWithErrors + completedWithNoErrors // TODO(jh, 08-14-24): Ideally, the backend never returns the "user cancelled a run" error and // cancelledWithoutRecovery becomes unnecessary. @@ -118,14 +121,10 @@ function ProtocolRunErrorBanner({ const { closeCurrentRun } = useCloseCurrentRun() - const { highestPriorityError, commandErrorList } = runErrors + const { highestPriorityError } = runErrors const handleFailedRunClick = (): void => { - // TODO(jh, 08-15-24): Revisit the control flow here here after - // commandErrorList may be fetched for a non-current run. - if (commandErrorList == null) { - closeCurrentRun() - } + closeCurrentRun() runHeaderModalContainerUtils.runFailedModalUtils.toggleModal() } diff --git a/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderBannerContainer/__tests__/ProtocolAnalysisErrorBanner.test.tsx b/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderBannerContainer/__tests__/ProtocolAnalysisErrorBanner.test.tsx index 5b60de044d7..fceea1d15c0 100644 --- a/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderBannerContainer/__tests__/ProtocolAnalysisErrorBanner.test.tsx +++ b/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderBannerContainer/__tests__/ProtocolAnalysisErrorBanner.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { fireEvent, screen } from '@testing-library/react' import { describe, it, beforeEach } from 'vitest' @@ -6,16 +5,16 @@ import { renderWithProviders } from '/app/__testing-utils__' import { i18n } from '/app/i18n' import { ProtocolAnalysisErrorBanner } from '../ProtocolAnalysisErrorBanner' -const render = ( - props: React.ComponentProps -) => { +import type { ComponentProps } from 'react' + +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, })[0] } describe('ProtocolAnalysisErrorBanner', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { diff --git a/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderBannerContainer/getShowGenericRunHeaderBanners.ts b/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderBannerContainer/getShowGenericRunHeaderBanners.ts index dde83ddb02d..35b63a2020b 100644 --- a/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderBannerContainer/getShowGenericRunHeaderBanners.ts +++ b/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderBannerContainer/getShowGenericRunHeaderBanners.ts @@ -31,10 +31,10 @@ export function getShowGenericRunHeaderBanners({ const showDoorOpenBeforeRunBanner = isDoorOpen && + isCancellableStatus(runStatus) && runStatus !== RUN_STATUS_BLOCKED_BY_OPEN_DOOR && runStatus !== RUN_STATUS_AWAITING_RECOVERY_BLOCKED_BY_OPEN_DOOR && runStatus !== RUN_STATUS_AWAITING_RECOVERY_PAUSED - isCancellableStatus(runStatus) const showDoorOpenDuringRunBanner = runStatus === RUN_STATUS_BLOCKED_BY_OPEN_DOOR diff --git a/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderContent/ActionButton/index.tsx b/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderContent/ActionButton/index.tsx index 4b8c0f68076..31495064a14 100644 --- a/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderContent/ActionButton/index.tsx +++ b/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderContent/ActionButton/index.tsx @@ -1,5 +1,3 @@ -import type * as React from 'react' - import { RUN_STATUS_STOP_REQUESTED } from '@opentrons/api-client' import { ALIGN_CENTER, @@ -28,12 +26,13 @@ import { useActionBtnDisabledUtils, useActionButtonProperties } from './hooks' import { getFallbackRobotSerialNumber, isRunAgainStatus } from '../../utils' import { useIsRobotOnWrongVersionOfSoftware } from '/app/redux/robot-update' +import type { MutableRefObject } from 'react' import type { RunHeaderContentProps } from '..' export type BaseActionButtonProps = RunHeaderContentProps interface ActionButtonProps extends BaseActionButtonProps { - isResetRunLoadingRef: React.MutableRefObject + isResetRunLoadingRef: MutableRefObject } export function ActionButton(props: ActionButtonProps): JSX.Element { diff --git a/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderContent/LabeledValue.tsx b/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderContent/LabeledValue.tsx index 135dd72bbae..183f6194a3f 100644 --- a/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderContent/LabeledValue.tsx +++ b/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderContent/LabeledValue.tsx @@ -1,5 +1,3 @@ -import type * as React from 'react' - import { DIRECTION_COLUMN, COLORS, @@ -8,9 +6,11 @@ import { StyledText, } from '@opentrons/components' +import type { ReactNode } from 'react' + interface LabeledValueProps { label: string - value: React.ReactNode + value: ReactNode } export function LabeledValue(props: LabeledValueProps): JSX.Element { diff --git a/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderContent/index.tsx b/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderContent/index.tsx index 51908e4435d..7d653c6f439 100644 --- a/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderContent/index.tsx +++ b/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderContent/index.tsx @@ -1,16 +1,15 @@ -import type * as React from 'react' - import { RunHeaderSectionUpper } from './RunHeaderSectionUpper' import { RunHeaderSectionLower } from './RunHeaderSectionLower' -import type { ProtocolRunHeaderProps } from '..' +import type { MutableRefObject } from 'react' import type { AttachedModule, RunStatus } from '@opentrons/api-client' +import type { ProtocolRunHeaderProps } from '..' import type { RunControls } from '/app/organisms/RunTimeControl' import type { UseRunHeaderModalContainerResult } from '../RunHeaderModalContainer' export type RunHeaderContentProps = ProtocolRunHeaderProps & { runStatus: RunStatus | null - isResetRunLoadingRef: React.MutableRefObject + isResetRunLoadingRef: MutableRefObject attachedModules: AttachedModule[] protocolRunControls: RunControls runHeaderModalContainerUtils: UseRunHeaderModalContainerResult diff --git a/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/RunHeaderModalContainer.tsx b/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/RunHeaderModalContainer.tsx index 0c306339f69..8486296122b 100644 --- a/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/RunHeaderModalContainer.tsx +++ b/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/RunHeaderModalContainer.tsx @@ -52,6 +52,7 @@ export function RunHeaderModalContainer( runStatus={runStatus} runId={runId} unvalidatedFailedCommand={recoveryModalUtils.failedCommand} + runLwDefsByUri={recoveryModalUtils.runLwDefsByUri} protocolAnalysis={robotProtocolAnalysis} /> ) : null} diff --git a/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/hooks/useRunHeaderDropTip.ts b/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/hooks/useRunHeaderDropTip.ts index 2e1da26cd88..3363a225ec7 100644 --- a/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/hooks/useRunHeaderDropTip.ts +++ b/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/hooks/useRunHeaderDropTip.ts @@ -1,13 +1,10 @@ import { useEffect } from 'react' -import { useHost } from '@opentrons/react-api-client' import { RUN_STATUS_IDLE, RUN_STATUS_STOPPED } from '@opentrons/api-client' import { FLEX_ROBOT_TYPE, OT2_ROBOT_TYPE } from '@opentrons/shared-data' +import { useErrorRecoverySettings } from '@opentrons/react-api-client' -import { - useDropTipWizardFlows, - useTipAttachmentStatus, -} from '/app/organisms/DropTipWizardFlows' +import { useDropTipWizardFlows } from '/app/organisms/DropTipWizardFlows' import { useProtocolDropTipModal } from '../modals' import { useCloseCurrentRun, @@ -15,15 +12,16 @@ import { useIsRunCurrent, } from '/app/resources/runs' import { isTerminalRunStatus } from '../../utils' +import { useTipAttachmentStatus } from '/app/resources/instruments' import { lastRunCommandPromptedErrorRecovery } from '/app/local-resources/commands' import type { RobotType } from '@opentrons/shared-data' import type { Run, RunStatus } from '@opentrons/api-client' import type { - DropTipWizardFlowsProps, PipetteWithTip, TipAttachmentStatusResult, -} from '/app/organisms/DropTipWizardFlows' +} from '/app/resources/instruments' +import type { DropTipWizardFlowsProps } from '/app/organisms/DropTipWizardFlows' import type { UseProtocolDropTipModalResult } from '../modals' import type { PipetteDetails } from '/app/resources/maintenance_runs' @@ -51,7 +49,6 @@ export function useRunHeaderDropTip({ robotType, runStatus, }: UseRunHeaderDropTipParams): UseRunHeaderDropTipResult { - const host = useHost() const isRunCurrent = useIsRunCurrent(runId) const enteredER = runRecord?.data.hasEverEnteredErrorRecovery ?? false @@ -68,7 +65,6 @@ export function useRunHeaderDropTip({ } = useTipAttachmentStatus({ runId, runRecord: runRecord ?? null, - host, }) const dropTipModalUtils = useProtocolDropTipModal({ @@ -109,6 +105,8 @@ export function useRunHeaderDropTip({ : { showDTWiz: false, dtWizProps: null } } + const { data } = useErrorRecoverySettings() + const isEREnabled = data?.data.enabled ?? true const runSummaryNoFixit = useCurrentRunCommands( { includeFixitCommands: false, @@ -116,6 +114,7 @@ export function useRunHeaderDropTip({ }, { enabled: isTerminalRunStatus(runStatus) } ) + // Manage tip checking useEffect(() => { // If a user begins a new run without navigating away from the run page, reset tip status. @@ -123,21 +122,16 @@ export function useRunHeaderDropTip({ if (runStatus === RUN_STATUS_IDLE) { resetTipStatus() } - // Only determine tip status when necessary as this can be an expensive operation. Error Recovery handles tips, so don't - // have to do it here if done during Error Recovery. + // Only run tip checking if it wasn't *just* handled during Error Recovery. else if ( - runSummaryNoFixit != null && - runSummaryNoFixit.length > 0 && - !lastRunCommandPromptedErrorRecovery(runSummaryNoFixit) && + !lastRunCommandPromptedErrorRecovery(runSummaryNoFixit, isEREnabled) && + isRunCurrent && isTerminalRunStatus(runStatus) ) { void determineTipStatus() } } - }, [runStatus, robotType, runSummaryNoFixit]) - - // TODO(jh, 08-15-24): The enteredER condition is a hack, because errorCommands are only returned when a run is current. - // Ideally the run should not need to be current to view errorCommands. + }, [runStatus, robotType, isRunCurrent, runSummaryNoFixit, isEREnabled]) // If the run terminates with a "stopped" status, close the run if no tips are attached after running tip check at least once. // This marks the robot as "not busy" if drop tip CTAs are unnecessary. @@ -145,8 +139,7 @@ export function useRunHeaderDropTip({ if ( runStatus === RUN_STATUS_STOPPED && isRunCurrent && - (initialPipettesWithTipsCount === 0 || robotType === OT2_ROBOT_TYPE) && - !enteredER + (initialPipettesWithTipsCount === 0 || robotType === OT2_ROBOT_TYPE) ) { closeCurrentRun() } diff --git a/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/modals/ConfirmCancelModal.tsx b/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/modals/ConfirmCancelModal.tsx index 083b7f752fc..770217dad82 100644 --- a/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/modals/ConfirmCancelModal.tsx +++ b/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/modals/ConfirmCancelModal.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useState, useEffect } from 'react' import { createPortal } from 'react-dom' import { useTranslation } from 'react-i18next' @@ -27,6 +27,7 @@ import { useTrackProtocolRunEvent } from '/app/redux-resources/analytics' import { useIsFlex } from '/app/redux-resources/robots' import { ANALYTICS_PROTOCOL_RUN_ACTION } from '/app/redux/analytics' +import type { MouseEventHandler } from 'react' import type { RunStatus } from '@opentrons/api-client' export interface UseConfirmCancelModalResult { @@ -35,7 +36,7 @@ export interface UseConfirmCancelModalResult { } export function useConfirmCancelModal(): UseConfirmCancelModalResult { - const [showModal, setShowModal] = React.useState(false) + const [showModal, setShowModal] = useState(false) const toggleModal = (): void => { setShowModal(!showModal) @@ -58,14 +59,14 @@ export function ConfirmCancelModal( const { stopRun } = useStopRunMutation() const isFlex = useIsFlex(robotName) const { trackProtocolRunEvent } = useTrackProtocolRunEvent(runId, robotName) - const [isCanceling, setIsCanceling] = React.useState(false) + const [isCanceling, setIsCanceling] = useState(false) 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 => { + const cancelRun: MouseEventHandler = (e): void => { e.preventDefault() e.stopPropagation() setIsCanceling(true) @@ -78,7 +79,8 @@ export function ConfirmCancelModal( }, }) } - React.useEffect(() => { + + useEffect(() => { if ( runStatus === RUN_STATUS_STOP_REQUESTED || runStatus === RUN_STATUS_STOPPED diff --git a/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/modals/HeaterShakerIsRunningModal/__tests__/HeaterShakerIsRunningModal.test.tsx b/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/modals/HeaterShakerIsRunningModal/__tests__/HeaterShakerIsRunningModal.test.tsx index 03b59af1b57..5960c2a39fa 100644 --- a/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/modals/HeaterShakerIsRunningModal/__tests__/HeaterShakerIsRunningModal.test.tsx +++ b/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/modals/HeaterShakerIsRunningModal/__tests__/HeaterShakerIsRunningModal.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { fireEvent, screen } from '@testing-library/react' import { describe, it, vi, beforeEach, expect } from 'vitest' @@ -12,6 +11,7 @@ import { HeaterShakerModuleCard } from '../HeaterShakerModuleCard' import { useAttachedModules } from '/app/resources/modules' import { useMostRecentCompletedAnalysis } from '/app/resources/runs' +import type { ComponentProps } from 'react' import type * as ReactApiClient from '@opentrons/react-api-client' vi.mock('@opentrons/react-api-client', async importOriginal => { @@ -69,16 +69,14 @@ const mockMovingHeaterShakerTwo = { usbPort: { path: '/dev/ot_module_heatershaker0', port: 1 }, } as any -const render = ( - props: React.ComponentProps -) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, })[0] } describe('HeaterShakerIsRunningModal', () => { - let props: React.ComponentProps + let props: ComponentProps let mockCreateLiveCommand = vi.fn() beforeEach(() => { props = { diff --git a/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/modals/HeaterShakerIsRunningModal/__tests__/HeaterShakerModuleCard.test.tsx b/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/modals/HeaterShakerIsRunningModal/__tests__/HeaterShakerModuleCard.test.tsx index 4cd20822890..8e98d4778fa 100644 --- a/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/modals/HeaterShakerIsRunningModal/__tests__/HeaterShakerModuleCard.test.tsx +++ b/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/modals/HeaterShakerIsRunningModal/__tests__/HeaterShakerModuleCard.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { screen } from '@testing-library/react' import { describe, it, vi, beforeEach } from 'vitest' @@ -8,16 +7,18 @@ import { i18n } from '/app/i18n' import { HeaterShakerModuleCard } from '../HeaterShakerModuleCard' import { HeaterShakerModuleData } from '/app/organisms/ModuleCard/HeaterShakerModuleData' +import type { ComponentProps } from 'react' + vi.mock('/app/organisms/ModuleCard/HeaterShakerModuleData') -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, })[0] } describe('HeaterShakerModuleCard', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { module: mockHeaterShaker, diff --git a/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/modals/HeaterShakerIsRunningModal/__tests__/hooks.test.tsx b/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/modals/HeaterShakerIsRunningModal/__tests__/hooks.test.tsx index c5028c6a821..8c9c83fcaf9 100644 --- a/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/modals/HeaterShakerIsRunningModal/__tests__/hooks.test.tsx +++ b/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/modals/HeaterShakerIsRunningModal/__tests__/hooks.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { Provider } from 'react-redux' import { describe, it, vi, beforeEach, expect } from 'vitest' import { createStore } from 'redux' @@ -10,6 +9,7 @@ import { RUN_ID_1 } from '/app/resources/runs/__fixtures__' import { useMostRecentCompletedAnalysis } from '/app/resources/runs' import { useHeaterShakerModuleIdsFromRun } from '../hooks' +import type { FunctionComponent, ReactNode } from 'react' import type { Store } from 'redux' import type { State } from '/app/redux/types' @@ -57,7 +57,7 @@ describe('useHeaterShakerModuleIdsFromRun', () => { }, ], } as any) - const wrapper: React.FunctionComponent<{ children: React.ReactNode }> = ({ + const wrapper: FunctionComponent<{ children: ReactNode }> = ({ children, }) => {children} const { result } = renderHook( @@ -127,7 +127,7 @@ describe('useHeaterShakerModuleIdsFromRun', () => { ], } as any) - const wrapper: React.FunctionComponent<{ children: React.ReactNode }> = ({ + const wrapper: FunctionComponent<{ children: ReactNode }> = ({ children, }) => {children} const { result } = renderHook( diff --git a/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/modals/ProtocolDropTipModal.tsx b/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/modals/ProtocolDropTipModal.tsx index e1f1be57d22..b9f30de446f 100644 --- a/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/modals/ProtocolDropTipModal.tsx +++ b/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/modals/ProtocolDropTipModal.tsx @@ -21,7 +21,7 @@ import { useHomePipettes } from '/app/local-resources/instruments' import type { PipetteData } from '@opentrons/api-client' import type { IconProps } from '@opentrons/components' import type { UseHomePipettesProps } from '/app/local-resources/instruments' -import type { TipAttachmentStatusResult } from '/app/organisms/DropTipWizardFlows' +import type { TipAttachmentStatusResult } from '/app/resources/instruments' type UseProtocolDropTipModalProps = Pick< UseHomePipettesProps, diff --git a/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/modals/RunFailedModal.tsx b/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/modals/RunFailedModal.tsx index 77041e48c18..33d7949a449 100644 --- a/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/modals/RunFailedModal.tsx +++ b/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/modals/RunFailedModal.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useState } from 'react' import { useTranslation } from 'react-i18next' import { css } from 'styled-components' @@ -25,6 +25,7 @@ import { import { useDownloadRunLog } from '../../../../hooks' import { RUN_STATUS_SUCCEEDED } from '@opentrons/api-client' +import type { MouseEventHandler } from 'react' import type { RunStatus } from '@opentrons/api-client' import type { ModalProps } from '@opentrons/components' import type { RunCommandError } from '@opentrons/shared-data' @@ -41,7 +42,7 @@ export interface UseRunFailedModalResult { export function useRunFailedModal( runErrors: UseRunErrorsResult ): UseRunFailedModalResult { - const [showRunFailedModal, setShowRunFailedModal] = React.useState(false) + const [showRunFailedModal, setShowRunFailedModal] = useState(false) const toggleModal = (): void => { setShowRunFailedModal(!showRunFailedModal) @@ -95,7 +96,7 @@ export function RunFailedModal({ toggleModal() } - const handleDownloadClick: React.MouseEventHandler = e => { + const handleDownloadClick: MouseEventHandler = e => { e.preventDefault() e.stopPropagation() downloadRunLog() diff --git a/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/modals/__tests__/ConfirmCancelModal.test.tsx b/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/modals/__tests__/ConfirmCancelModal.test.tsx index c6421040e17..08559f181be 100644 --- a/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/modals/__tests__/ConfirmCancelModal.test.tsx +++ b/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/modals/__tests__/ConfirmCancelModal.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { when } from 'vitest-when' import { fireEvent, screen } from '@testing-library/react' import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' @@ -17,6 +16,7 @@ import { useIsFlex } from '/app/redux-resources/robots' import { useTrackEvent } from '/app/redux/analytics' import { ConfirmCancelModal } from '../ConfirmCancelModal' +import type { ComponentProps } from 'react' import type * as ApiClient from '@opentrons/react-api-client' vi.mock('@opentrons/react-api-client', async importOriginal => { @@ -30,7 +30,7 @@ vi.mock('/app/redux/analytics') vi.mock('/app/redux-resources/analytics') vi.mock('/app/redux-resources/robots') -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, })[0] @@ -43,7 +43,7 @@ let mockTrackProtocolRunEvent: any const ROBOT_NAME = 'otie' describe('ConfirmCancelModal', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { mockTrackEvent = vi.fn() mockStopRun = vi.fn((_runId, opts) => opts.onSuccess()) diff --git a/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/modals/__tests__/ProtocolAnalysisErrorModal.test.tsx b/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/modals/__tests__/ProtocolAnalysisErrorModal.test.tsx index 44fcb0278ad..027f6b76b03 100644 --- a/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/modals/__tests__/ProtocolAnalysisErrorModal.test.tsx +++ b/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/modals/__tests__/ProtocolAnalysisErrorModal.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { fireEvent, screen } from '@testing-library/react' import { describe, it, beforeEach, expect, vi } from 'vitest' @@ -6,16 +5,16 @@ import { renderWithProviders } from '/app/__testing-utils__' import { i18n } from '/app/i18n' import { ProtocolAnalysisErrorModal } from '../ProtocolAnalysisErrorModal' -const render = ( - props: React.ComponentProps -) => { +import type { ComponentProps } from 'react' + +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, })[0] } describe('ProtocolAnalysisErrorModal', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { diff --git a/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/modals/__tests__/ProtocolDropTipModal.test.tsx b/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/modals/__tests__/ProtocolDropTipModal.test.tsx index 0d95071a969..4e583be0648 100644 --- a/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/modals/__tests__/ProtocolDropTipModal.test.tsx +++ b/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/modals/__tests__/ProtocolDropTipModal.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { describe, it, vi, expect, beforeEach } from 'vitest' import { renderHook, act, screen, fireEvent } from '@testing-library/react' @@ -10,6 +9,7 @@ import { ProtocolDropTipModal, } from '../ProtocolDropTipModal' +import type { ComponentProps } from 'react' import type { Mock } from 'vitest' vi.mock('/app/local-resources/instruments') @@ -104,14 +104,14 @@ describe('useProtocolDropTipModal', () => { }) }) -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, })[0] } describe('ProtocolDropTipModal', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { diff --git a/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/modals/__tests__/RunFailedModal.test.tsx b/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/modals/__tests__/RunFailedModal.test.tsx index d49875a0859..75ea8c0c720 100644 --- a/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/modals/__tests__/RunFailedModal.test.tsx +++ b/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/modals/__tests__/RunFailedModal.test.tsx @@ -1,5 +1,5 @@ -import type * as React from 'react' import { describe, it, beforeEach, vi, expect, afterEach } from 'vitest' +import { fireEvent, screen } from '@testing-library/react' import { renderWithProviders } from '/app/__testing-utils__' import { i18n } from '/app/i18n' @@ -7,8 +7,9 @@ import { useDownloadRunLog } from '../../../../../hooks' import { RunFailedModal } from '../RunFailedModal' import { RUN_STATUS_FAILED } from '@opentrons/api-client' + +import type { ComponentProps } from 'react' import type { RunError } from '@opentrons/api-client' -import { fireEvent, screen } from '@testing-library/react' vi.mock('../../../../../hooks') @@ -25,14 +26,14 @@ const mockError: RunError = { wrappedErrors: [], } -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, }) } describe('RunFailedModal - DesktopApp', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { diff --git a/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/useRunHeaderModalContainer.ts b/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/useRunHeaderModalContainer.ts index 48eda0ebfa5..1e0d1e5c073 100644 --- a/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/useRunHeaderModalContainer.ts +++ b/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/useRunHeaderModalContainer.ts @@ -16,9 +16,15 @@ import { useProtocolDetailsForRun } from '/app/resources/runs' import { getFallbackRobotSerialNumber } from '../utils' import { ANALYTICS_PROTOCOL_PROCEED_TO_RUN, + ANALYTICS_PROTOCOL_RUN_ACTION, useTrackEvent, } from '/app/redux/analytics' +import { + useRobotAnalyticsData, + useTrackProtocolRunEvent, +} from '/app/redux-resources/analytics' import { useRobot, useRobotType } from '/app/redux-resources/robots' + import type { AttachedModule, RunStatus, Run } from '@opentrons/api-client' import type { UseErrorRecoveryResult } from '/app/organisms/ErrorRecoveryFlows' import type { @@ -71,7 +77,9 @@ export function useRunHeaderModalContainer({ const robot = useRobot(robotName) const robotSerialNumber = getFallbackRobotSerialNumber(robot) const trackEvent = useTrackEvent() + const { trackProtocolRunEvent } = useTrackProtocolRunEvent(runId, robotName) const robotType = useRobotType(robotName) + const robotAnalyticsData = useRobotAnalyticsData(robotName) function handleProceedToRunClick(): void { navigate(`/devices/${robotName}/protocol-runs/${runId}/run-preview`) @@ -79,6 +87,10 @@ export function useRunHeaderModalContainer({ name: ANALYTICS_PROTOCOL_PROCEED_TO_RUN, properties: { robotSerialNumber }, }) + trackProtocolRunEvent({ + name: ANALYTICS_PROTOCOL_RUN_ACTION.START, + properties: robotAnalyticsData ?? {}, + }) protocolRunControls.play() } diff --git a/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/__tests__/ProtocolRunHeader.test.tsx b/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/__tests__/ProtocolRunHeader.test.tsx index e82d58cb75e..f6c88119707 100644 --- a/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/__tests__/ProtocolRunHeader.test.tsx +++ b/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/__tests__/ProtocolRunHeader.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { describe, it, vi, expect, beforeEach, afterEach } from 'vitest' import { screen } from '@testing-library/react' import { useNavigate } from 'react-router-dom' @@ -26,6 +25,8 @@ import { useRunHeaderRunControls, } from '../hooks' +import type { ComponentProps } from 'react' + vi.mock('react-router-dom') vi.mock('@opentrons/react-api-client') vi.mock('/app/redux-resources/robots') @@ -43,7 +44,7 @@ const MOCK_RUN_ID = 'MOCK_RUN_ID' const MOCK_ROBOT = 'MOCK_ROBOT' describe('ProtocolRunHeader', () => { - let props: React.ComponentProps + let props: ComponentProps const mockNavigate = vi.fn() beforeEach(() => { @@ -92,7 +93,7 @@ describe('ProtocolRunHeader', () => { vi.resetAllMocks() }) - const render = (props: React.ComponentProps) => { + const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, })[0] diff --git a/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/hooks/useRunAnalytics.ts b/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/hooks/useRunAnalytics.ts index 31399cbc541..95658999f4a 100644 --- a/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/hooks/useRunAnalytics.ts +++ b/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/hooks/useRunAnalytics.ts @@ -28,15 +28,12 @@ export function useRunAnalytics({ useEffect(() => { const areReportConditionsValid = - isRunCurrent && - runId != null && - robotAnalyticsData != null && - isTerminalRunStatus(runStatus) + isRunCurrent && runId != null && isTerminalRunStatus(runStatus) if (areReportConditionsValid) { trackProtocolRunEvent({ name: ANALYTICS_PROTOCOL_RUN_ACTION.FINISH, - properties: robotAnalyticsData, + properties: robotAnalyticsData ?? undefined, }) } }, [runStatus, isRunCurrent, runId, robotAnalyticsData]) diff --git a/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/hooks/useRunErrors.ts b/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/hooks/useRunErrors.ts index 4d66b367a0e..593c435029b 100644 --- a/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/hooks/useRunErrors.ts +++ b/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/hooks/useRunErrors.ts @@ -1,7 +1,6 @@ import { useRunCommandErrors } from '@opentrons/react-api-client' import { isTerminalRunStatus } from '../utils' -import { useMostRecentRunId } from '/app/resources/runs' import { getHighestPriorityError } from '/app/transformations/runs' import type { RunStatus, Run } from '@opentrons/api-client' @@ -27,14 +26,11 @@ export function useRunErrors({ runRecord, runStatus, }: UseRunErrorsProps): UseRunErrorsResult { - const mostRecentRunId = useMostRecentRunId() - const isMostRecentRun = mostRecentRunId === runId - const { data: commandErrorList } = useRunCommandErrors( runId, { cursor: 0, pageLength: ALL_COMMANDS_PAGE_LENGTH }, { - enabled: isTerminalRunStatus(runStatus) && isMostRecentRun, + enabled: isTerminalRunStatus(runStatus), } ) diff --git a/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/index.tsx b/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/index.tsx index 40375135db9..fa13d31487b 100644 --- a/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/index.tsx +++ b/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/index.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useEffect, useRef } from 'react' import { useNavigate } from 'react-router-dom' import { css } from 'styled-components' @@ -30,8 +30,10 @@ import { RunHeaderContent } from './RunHeaderContent' import { EQUIPMENT_POLL_MS } from './constants' import { isCancellableStatus } from './utils' +import type { RefObject } from 'react' + export interface ProtocolRunHeaderProps { - protocolRunHeaderRef: React.RefObject | null + protocolRunHeaderRef: RefObject | null robotName: string runId: string makeHandleJumpToStep: (index: number) => () => void @@ -70,7 +72,7 @@ export function ProtocolRunHeader( runErrors, }) - React.useEffect(() => { + useEffect(() => { if (protocolData != null && !isRobotViewable) { navigate('/devices') } @@ -78,7 +80,7 @@ export function ProtocolRunHeader( // To persist "run again" loading conditions into a new run, we need a scalar that persists longer than // the runControl isResetRunLoading, which completes before we want to change user-facing copy/CTAs. - const isResetRunLoadingRef = React.useRef(false) + const isResetRunLoadingRef = useRef(false) if (runStatus === RUN_STATUS_IDLE || runStatus === RUN_STATUS_RUNNING) { isResetRunLoadingRef.current = false } diff --git a/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunSetup.tsx b/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunSetup.tsx index 8e10948795a..4f0119c8a99 100644 --- a/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunSetup.tsx +++ b/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunSetup.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useState } from 'react' import { useTranslation } from 'react-i18next' import { useDispatch, useSelector } from 'react-redux' @@ -59,12 +59,14 @@ import { SetupStep } from './SetupStep' import { SetupLiquids } from './SetupLiquids' import { EmptySetupStep } from './EmptySetupStep' import { HowLPCWorksModal } from './SetupLabwarePositionCheck/HowLPCWorksModal' +import { useFeatureFlag } from '/app/redux/config' +import type { RefObject } from 'react' import type { Dispatch, State } from '/app/redux/types' import type { StepKey } from '/app/redux/protocol-runs' interface ProtocolRunSetupProps { - protocolRunHeaderRef: React.RefObject | null + protocolRunHeaderRef: RefObject | null robotName: string runId: string } @@ -84,7 +86,7 @@ export function ProtocolRunSetup({ orderedApplicableSteps, } = useRequiredSetupStepsInOrder({ runId, protocolAnalysis }) const modules = parseAllRequiredModuleModels(protocolAnalysis?.commands ?? []) - + const isNewLpc = useFeatureFlag('lpcRedesign') const robot = useRobot(robotName) const calibrationStatusRobot = useRunCalibrationStatus(robotName, runId) const calibrationStatusModules = useModuleCalibrationStatus(robotName, runId) @@ -92,9 +94,7 @@ export function ProtocolRunSetup({ const isFlex = useIsFlex(robotName) const runHasStarted = useRunHasStarted(runId) const { analysisErrors } = useProtocolAnalysisErrors(runId) - const [expandedStepKey, setExpandedStepKey] = React.useState( - null - ) + const [expandedStepKey, setExpandedStepKey] = useState(null) const robotType = isFlex ? FLEX_ROBOT_TYPE : OT2_ROBOT_TYPE const deckConfigCompatibility = useDeckConfigurationCompatibility( robotType, @@ -225,6 +225,7 @@ export function ProtocolRunSetup({ } }} offsetsConfirmed={!missingSteps.includes(LPC_STEP_KEY)} + isNewLpc={isNewLpc} /> ), description: t('labware_position_check_step_description'), @@ -481,14 +482,14 @@ function StepRightElement(props: StepRightElementProps): JSX.Element | null { function LearnAboutLPC(): JSX.Element { const { t } = useTranslation('protocol_setup') - const [showLPCHelpModal, setShowLPCHelpModal] = React.useState(false) + const [showLPCHelpModal, setShowLPCHelpModal] = useState(false) return ( <> { + onClick={(e: MouseEvent) => { // clicking link shouldn't toggle step expanded state e.preventDefault() e.stopPropagation() diff --git a/app/src/organisms/Desktop/Devices/ProtocolRun/SetupLabware/__tests__/LabwareListItem.test.tsx b/app/src/organisms/Desktop/Devices/ProtocolRun/SetupLabware/__tests__/LabwareListItem.test.tsx index 50afda3d92f..89622cd7e5e 100644 --- a/app/src/organisms/Desktop/Devices/ProtocolRun/SetupLabware/__tests__/LabwareListItem.test.tsx +++ b/app/src/organisms/Desktop/Devices/ProtocolRun/SetupLabware/__tests__/LabwareListItem.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { fireEvent, screen } from '@testing-library/react' import { describe, it, beforeEach, vi, expect } from 'vitest' import { MemoryRouter } from 'react-router-dom' @@ -18,9 +17,11 @@ import { mockThermocycler, } from '/app/redux/modules/__fixtures__' import { getLocationInfoNames } from '/app/transformations/commands' -import { mockLabwareDef } from '/app/organisms/LabwarePositionCheck/__fixtures__/mockLabwareDef' +import { mockLabwareDef } from '/app/organisms/LegacyLabwarePositionCheck/__fixtures__/mockLabwareDef' import { SecureLabwareModal } from '../SecureLabwareModal' import { LabwareListItem } from '../LabwareListItem' + +import type { ComponentProps } from 'react' import type { LoadLabwareRunTimeCommand, ModuleModel, @@ -78,7 +79,7 @@ const mockThermocyclerModuleDefinition = { const mockModuleId = 'moduleId' const mockNickName = 'nickName' -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders( diff --git a/app/src/organisms/Desktop/Devices/ProtocolRun/SetupLabware/__tests__/OffDeckLabwareList.test.tsx b/app/src/organisms/Desktop/Devices/ProtocolRun/SetupLabware/__tests__/OffDeckLabwareList.test.tsx index 7c6c839dbaf..b543c3f27c8 100644 --- a/app/src/organisms/Desktop/Devices/ProtocolRun/SetupLabware/__tests__/OffDeckLabwareList.test.tsx +++ b/app/src/organisms/Desktop/Devices/ProtocolRun/SetupLabware/__tests__/OffDeckLabwareList.test.tsx @@ -1,17 +1,18 @@ -import type * as React from 'react' import { MemoryRouter } from 'react-router-dom' import { screen } from '@testing-library/react' import { describe, it, beforeEach, vi, expect } from 'vitest' import { renderWithProviders } from '/app/__testing-utils__' import { i18n } from '/app/i18n' -import { mockLabwareDef } from '/app/organisms/LabwarePositionCheck/__fixtures__/mockLabwareDef' +import { mockLabwareDef } from '/app/organisms/LegacyLabwarePositionCheck/__fixtures__/mockLabwareDef' import { LabwareListItem } from '../LabwareListItem' import { OffDeckLabwareList } from '../OffDeckLabwareList' +import type { ComponentProps } from 'react' + vi.mock('../LabwareListItem') -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders( diff --git a/app/src/organisms/Desktop/Devices/ProtocolRun/SetupLabware/__tests__/SecureLabwareModal.test.tsx b/app/src/organisms/Desktop/Devices/ProtocolRun/SetupLabware/__tests__/SecureLabwareModal.test.tsx index 80147006dc1..f367e007375 100644 --- a/app/src/organisms/Desktop/Devices/ProtocolRun/SetupLabware/__tests__/SecureLabwareModal.test.tsx +++ b/app/src/organisms/Desktop/Devices/ProtocolRun/SetupLabware/__tests__/SecureLabwareModal.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { fireEvent, screen } from '@testing-library/react' import { describe, it, beforeEach, vi, expect } from 'vitest' @@ -6,7 +5,9 @@ import { renderWithProviders } from '/app/__testing-utils__' import { i18n } from '/app/i18n' import { SecureLabwareModal } from '../SecureLabwareModal' -const render = (props: React.ComponentProps) => { +import type { ComponentProps } from 'react' + +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, })[0] @@ -15,7 +16,7 @@ const mockTypeMagDeck = 'magneticModuleType' const mockTypeTC = 'thermocyclerModuleType' describe('SecureLabwareModal', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { type: mockTypeMagDeck, onCloseClick: vi.fn() } }) diff --git a/app/src/organisms/Desktop/Devices/ProtocolRun/SetupLabware/__tests__/SetupLabware.test.tsx b/app/src/organisms/Desktop/Devices/ProtocolRun/SetupLabware/__tests__/SetupLabware.test.tsx index 6acaf42445b..055da888623 100644 --- a/app/src/organisms/Desktop/Devices/ProtocolRun/SetupLabware/__tests__/SetupLabware.test.tsx +++ b/app/src/organisms/Desktop/Devices/ProtocolRun/SetupLabware/__tests__/SetupLabware.test.tsx @@ -8,7 +8,7 @@ import { useHoverTooltip } from '@opentrons/components' import { renderWithProviders } from '/app/__testing-utils__' import { i18n } from '/app/i18n' import { useLPCSuccessToast } from '../../../hooks/useLPCSuccessToast' -import { LabwarePositionCheck } from '/app/organisms/LabwarePositionCheck' +import { LegacyLabwarePositionCheck } from '/app/organisms/LegacyLabwarePositionCheck' import { getModuleTypesThatRequireExtraAttention } from '../../utils/getModuleTypesThatRequireExtraAttention' import { getIsLabwareOffsetCodeSnippetsOn } from '/app/redux/config' import { SetupLabwareList } from '../SetupLabwareList' @@ -31,7 +31,7 @@ vi.mock('@opentrons/components', async () => { }) vi.mock('../SetupLabwareList') vi.mock('../SetupLabwareMap') -vi.mock('/app/organisms/LabwarePositionCheck') +vi.mock('/app/organisms/LegacyLabwarePositionCheck') vi.mock('../../utils/getModuleTypesThatRequireExtraAttention') vi.mock('/app/organisms/RunTimeControl/hooks') vi.mock('/app/redux/config') @@ -68,7 +68,7 @@ describe('SetupLabware', () => { .calledWith(expect.anything()) .thenReturn([]) - vi.mocked(LabwarePositionCheck).mockReturnValue( + vi.mocked(LegacyLabwarePositionCheck).mockReturnValue(
        mock Labware Position Check
        ) when(vi.mocked(useUnmatchedModulesForProtocol)) diff --git a/app/src/organisms/Desktop/Devices/ProtocolRun/SetupLabware/__tests__/SetupLabwareList.test.tsx b/app/src/organisms/Desktop/Devices/ProtocolRun/SetupLabware/__tests__/SetupLabwareList.test.tsx index 85068d39a1b..9f9860de1d4 100644 --- a/app/src/organisms/Desktop/Devices/ProtocolRun/SetupLabware/__tests__/SetupLabwareList.test.tsx +++ b/app/src/organisms/Desktop/Devices/ProtocolRun/SetupLabware/__tests__/SetupLabwareList.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { MemoryRouter } from 'react-router-dom' import { describe, it, beforeEach, vi } from 'vitest' import { screen } from '@testing-library/react' @@ -9,13 +8,15 @@ import { renderWithProviders } from '/app/__testing-utils__' import { i18n } from '/app/i18n' import { SetupLabwareList } from '../SetupLabwareList' import { LabwareListItem } from '../LabwareListItem' + +import type { ComponentProps } from 'react' import type { CompletedProtocolAnalysis } from '@opentrons/shared-data' vi.mock('../LabwareListItem') const protocolWithTC = (multiple_tipacks_with_tc as unknown) as CompletedProtocolAnalysis -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders( diff --git a/app/src/organisms/Desktop/Devices/ProtocolRun/SetupLabware/__tests__/SetupLabwareMap.test.tsx b/app/src/organisms/Desktop/Devices/ProtocolRun/SetupLabware/__tests__/SetupLabwareMap.test.tsx index 40f63cbc170..57a84b1f737 100644 --- a/app/src/organisms/Desktop/Devices/ProtocolRun/SetupLabware/__tests__/SetupLabwareMap.test.tsx +++ b/app/src/organisms/Desktop/Devices/ProtocolRun/SetupLabware/__tests__/SetupLabwareMap.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { when } from 'vitest-when' import { MemoryRouter } from 'react-router-dom' import { describe, it, beforeEach, vi, afterEach, expect } from 'vitest' @@ -20,6 +19,7 @@ import { } from '/app/transformations/analysis' import { SetupLabwareMap } from '../SetupLabwareMap' +import type { ComponentProps } from 'react' import type { CompletedProtocolAnalysis, LabwareDefinition2, @@ -86,7 +86,7 @@ const mockTCModule = { type: 'thermocyclerModuleType' as ModuleType, } -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders( diff --git a/app/src/organisms/Desktop/Devices/ProtocolRun/SetupLabwarePositionCheck/CurrentOffsetsTable.tsx b/app/src/organisms/Desktop/Devices/ProtocolRun/SetupLabwarePositionCheck/CurrentOffsetsTable.tsx index e86166b0c9b..a61373da596 100644 --- a/app/src/organisms/Desktop/Devices/ProtocolRun/SetupLabwarePositionCheck/CurrentOffsetsTable.tsx +++ b/app/src/organisms/Desktop/Devices/ProtocolRun/SetupLabwarePositionCheck/CurrentOffsetsTable.tsx @@ -22,7 +22,7 @@ import { getIsLabwareOffsetCodeSnippetsOn } from '/app/redux/config' import { LabwareOffsetTabs } from '/app/organisms/LabwareOffsetTabs' import { OffsetVector } from '/app/molecules/OffsetVector' import { PythonLabwareOffsetSnippet } from '/app/molecules/PythonLabwareOffsetSnippet' -import { getDisplayLocation } from '/app/organisms/LabwarePositionCheck/utils/getDisplayLocation' +import { getDisplayLocation } from '/app/organisms/LegacyLabwarePositionCheck/utils/getDisplayLocation' import { getLabwareDefinitionsFromCommands } from '/app/local-resources/labware' import type { LabwareOffset } from '@opentrons/api-client' import type { diff --git a/app/src/organisms/Desktop/Devices/ProtocolRun/SetupLabwarePositionCheck/__tests__/CurrentOffsetsTable.test.tsx b/app/src/organisms/Desktop/Devices/ProtocolRun/SetupLabwarePositionCheck/__tests__/CurrentOffsetsTable.test.tsx index 622d649bb04..db2e5a8b5a1 100644 --- a/app/src/organisms/Desktop/Devices/ProtocolRun/SetupLabwarePositionCheck/__tests__/CurrentOffsetsTable.test.tsx +++ b/app/src/organisms/Desktop/Devices/ProtocolRun/SetupLabwarePositionCheck/__tests__/CurrentOffsetsTable.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { describe, it, beforeEach, vi, expect, afterEach } from 'vitest' import { screen } from '@testing-library/react' @@ -10,16 +9,17 @@ import { import { renderWithProviders } from '/app/__testing-utils__' import { i18n } from '/app/i18n' import { getIsLabwareOffsetCodeSnippetsOn } from '/app/redux/config' -import { LabwarePositionCheck } from '/app/organisms/LabwarePositionCheck' +import { LegacyLabwarePositionCheck } from '/app/organisms/LegacyLabwarePositionCheck' import { useLPCDisabledReason } from '/app/resources/runs' import { getLatestCurrentOffsets } from '/app/transformations/runs' import { CurrentOffsetsTable } from '../CurrentOffsetsTable' +import type { ComponentProps } from 'react' import type { CompletedProtocolAnalysis } from '@opentrons/shared-data' import type { LabwareOffset } from '@opentrons/api-client' vi.mock('/app/resources/runs') -vi.mock('/app/organisms/LabwarePositionCheck') +vi.mock('/app/organisms/LegacyLabwarePositionCheck') vi.mock('/app/redux/config') vi.mock('/app/transformations/runs') @@ -31,7 +31,7 @@ vi.mock('@opentrons/shared-data', async importOriginal => { } }) -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, })[0] @@ -64,7 +64,7 @@ const mockCurrentOffsets: LabwareOffset[] = [ ] describe('CurrentOffsetsTable', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { currentOffsets: mockCurrentOffsets, @@ -111,7 +111,7 @@ describe('CurrentOffsetsTable', () => { definitionId: 'opentrons/nest_96_wellplate_100ul_pcr_full_skirt/1', }, } as any) - vi.mocked(LabwarePositionCheck).mockReturnValue( + vi.mocked(LegacyLabwarePositionCheck).mockReturnValue(
        mock labware position check
        ) vi.mocked(getIsLabwareOffsetCodeSnippetsOn).mockReturnValue(false) diff --git a/app/src/organisms/Desktop/Devices/ProtocolRun/SetupLabwarePositionCheck/__tests__/HowLPCWorksModal.test.tsx b/app/src/organisms/Desktop/Devices/ProtocolRun/SetupLabwarePositionCheck/__tests__/HowLPCWorksModal.test.tsx index 5dd3d15db69..a12b6948d75 100644 --- a/app/src/organisms/Desktop/Devices/ProtocolRun/SetupLabwarePositionCheck/__tests__/HowLPCWorksModal.test.tsx +++ b/app/src/organisms/Desktop/Devices/ProtocolRun/SetupLabwarePositionCheck/__tests__/HowLPCWorksModal.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { fireEvent, screen } from '@testing-library/react' import { describe, it, vi, beforeEach, expect } from 'vitest' @@ -6,14 +5,16 @@ import { renderWithProviders } from '/app/__testing-utils__' import { i18n } from '/app/i18n' import { HowLPCWorksModal } from '../HowLPCWorksModal' -const render = (props: React.ComponentProps) => { +import type { ComponentProps } from 'react' + +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, })[0] } describe('HowLPCWorksModal', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { onCloseClick: vi.fn() } }) diff --git a/app/src/organisms/Desktop/Devices/ProtocolRun/SetupLabwarePositionCheck/__tests__/SetupLabwarePositionCheck.test.tsx b/app/src/organisms/Desktop/Devices/ProtocolRun/SetupLabwarePositionCheck/__tests__/SetupLabwarePositionCheck.test.tsx index 03ec9d990fb..67d33d6c982 100644 --- a/app/src/organisms/Desktop/Devices/ProtocolRun/SetupLabwarePositionCheck/__tests__/SetupLabwarePositionCheck.test.tsx +++ b/app/src/organisms/Desktop/Devices/ProtocolRun/SetupLabwarePositionCheck/__tests__/SetupLabwarePositionCheck.test.tsx @@ -13,7 +13,7 @@ import { renderWithProviders } from '/app/__testing-utils__' import { i18n } from '/app/i18n' import { useLPCSuccessToast } from '../../../hooks/useLPCSuccessToast' import { getModuleTypesThatRequireExtraAttention } from '../../utils/getModuleTypesThatRequireExtraAttention' -import { useLaunchLPC } from '/app/organisms/LabwarePositionCheck/useLaunchLPC' +import { useLaunchLegacyLPC } from '/app/organisms/LegacyLabwarePositionCheck/useLaunchLegacyLPC' import { getIsLabwareOffsetCodeSnippetsOn } from '/app/redux/config' import { SetupLabwarePositionCheck } from '..' import { @@ -27,7 +27,7 @@ import { useRobotType } from '/app/redux-resources/robots' import type { Mock } from 'vitest' -vi.mock('/app/organisms/LabwarePositionCheck/useLaunchLPC') +vi.mock('/app/organisms/LegacyLabwarePositionCheck/useLaunchLegacyLPC') vi.mock('../../utils/getModuleTypesThatRequireExtraAttention') vi.mock('/app/redux-resources/robots') vi.mock('/app/redux/config') @@ -51,6 +51,7 @@ const render = () => { setOffsetsConfirmed={confirmOffsets} robotName={ROBOT_NAME} runId={RUN_ID} + isNewLpc={false} />
        , { @@ -90,12 +91,10 @@ describe('SetupLabwarePositionCheck', () => { when(vi.mocked(useRobotType)) .calledWith(ROBOT_NAME) .thenReturn(FLEX_ROBOT_TYPE) - when(vi.mocked(useLaunchLPC)) - .calledWith(RUN_ID, FLEX_ROBOT_TYPE, 'test protocol') - .thenReturn({ - launchLPC: mockLaunchLPC, - LPCWizard:
        mock LPC Wizard
        , - }) + vi.mocked(useLaunchLegacyLPC).mockReturnValue({ + launchLegacyLPC: mockLaunchLPC, + LegacyLPCWizard:
        mock LPC Wizard
        , + }) vi.mocked(useNotifyRunQuery).mockReturnValue({ data: { data: { protocolId: 'fakeProtocolId' }, diff --git a/app/src/organisms/Desktop/Devices/ProtocolRun/SetupLabwarePositionCheck/index.tsx b/app/src/organisms/Desktop/Devices/ProtocolRun/SetupLabwarePositionCheck/index.tsx index 637a4814936..a0322a4110d 100644 --- a/app/src/organisms/Desktop/Devices/ProtocolRun/SetupLabwarePositionCheck/index.tsx +++ b/app/src/organisms/Desktop/Devices/ProtocolRun/SetupLabwarePositionCheck/index.tsx @@ -21,7 +21,7 @@ import { useProtocolQuery } from '@opentrons/react-api-client' import { useLPCSuccessToast } from '../../hooks/useLPCSuccessToast' import { useStoredProtocolAnalysis } from '/app/resources/analysis' import { CurrentOffsetsTable } from './CurrentOffsetsTable' -import { useLaunchLPC } from '/app/organisms/LabwarePositionCheck/useLaunchLPC' +import { useLaunchLegacyLPC } from '/app/organisms/LegacyLabwarePositionCheck/useLaunchLegacyLPC' import { getLatestCurrentOffsets } from '/app/transformations/runs' import { useNotifyRunQuery, @@ -36,12 +36,19 @@ interface SetupLabwarePositionCheckProps { setOffsetsConfirmed: (confirmed: boolean) => void robotName: string runId: string + isNewLpc: boolean } export function SetupLabwarePositionCheck( props: SetupLabwarePositionCheckProps ): JSX.Element { - const { robotName, runId, setOffsetsConfirmed, offsetsConfirmed } = props + const { + robotName, + runId, + setOffsetsConfirmed, + offsetsConfirmed, + isNewLpc, + } = props const { t, i18n } = useTranslation('protocol_setup') const robotType = useRobotType(robotName) @@ -88,7 +95,11 @@ export function SetupLabwarePositionCheck( const { setIsShowingLPCSuccessToast } = useLPCSuccessToast() - const { launchLPC, LPCWizard } = useLaunchLPC(runId, robotType, protocolName) + const { launchLegacyLPC, LegacyLPCWizard } = useLaunchLegacyLPC( + runId, + robotType, + protocolName + ) const nonIdentityOffsets = getLatestCurrentOffsets(sortedOffsets) @@ -144,7 +155,7 @@ export function SetupLabwarePositionCheck( { - launchLPC() + isNewLpc ? (() => null)() : launchLegacyLPC() setIsShowingLPCSuccessToast(false) }} id="LabwareSetup_checkLabwarePositionsButton" @@ -159,7 +170,7 @@ export function SetupLabwarePositionCheck( ) : null} - {LPCWizard} + {isNewLpc ? null : LegacyLPCWizard} ) } diff --git a/app/src/organisms/Desktop/Devices/ProtocolRun/SetupLiquids/__tests__/SetupLiquids.test.tsx b/app/src/organisms/Desktop/Devices/ProtocolRun/SetupLiquids/__tests__/SetupLiquids.test.tsx index 736d5f5bc85..8436072e2f1 100644 --- a/app/src/organisms/Desktop/Devices/ProtocolRun/SetupLiquids/__tests__/SetupLiquids.test.tsx +++ b/app/src/organisms/Desktop/Devices/ProtocolRun/SetupLiquids/__tests__/SetupLiquids.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { describe, it, beforeEach, vi, expect } from 'vitest' import { screen, fireEvent } from '@testing-library/react' @@ -11,6 +10,8 @@ import { SetupLiquidsList } from '../SetupLiquidsList' import { SetupLiquidsMap } from '../SetupLiquidsMap' import { useRunHasStarted } from '/app/resources/runs' +import type { ComponentProps } from 'react' + vi.mock('@opentrons/components', async () => { const actual = await vi.importActual('@opentrons/components') return { @@ -24,7 +25,7 @@ vi.mock('/app/resources/runs') describe('SetupLiquids', () => { const render = ( - props: React.ComponentProps & { + props: ComponentProps & { startConfirmed?: boolean } ) => { @@ -47,7 +48,7 @@ describe('SetupLiquids', () => { ) } - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { vi.mocked(SetupLiquidsList).mockReturnValue(
        Mock setup liquids list
        diff --git a/app/src/organisms/Desktop/Devices/ProtocolRun/SetupLiquids/__tests__/SetupLiquidsList.test.tsx b/app/src/organisms/Desktop/Devices/ProtocolRun/SetupLiquids/__tests__/SetupLiquidsList.test.tsx index 60cb6a759a0..2d96d408044 100644 --- a/app/src/organisms/Desktop/Devices/ProtocolRun/SetupLiquids/__tests__/SetupLiquidsList.test.tsx +++ b/app/src/organisms/Desktop/Devices/ProtocolRun/SetupLiquids/__tests__/SetupLiquidsList.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { fireEvent, screen } from '@testing-library/react' import { when } from 'vitest-when' import { describe, it, beforeEach, vi, expect } from 'vitest' @@ -25,6 +24,7 @@ import { import { LiquidsLabwareDetailsModal } from '/app/organisms/LiquidsLabwareDetailsModal' import { useNotifyRunQuery } from '/app/resources/runs' +import type { ComponentProps } from 'react' import type { Mock } from 'vitest' import type * as SharedData from '@opentrons/shared-data' @@ -70,7 +70,7 @@ vi.mock('@opentrons/shared-data', async importOriginal => { vi.mock('/app/redux/analytics') vi.mock('/app/resources/runs') -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, }) @@ -78,7 +78,7 @@ const render = (props: React.ComponentProps) => { let mockTrackEvent: Mock describe('SetupLiquidsList', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { runId: '123', robotName: 'test_flex' } vi.mocked(getTotalVolumePerLiquidId).mockReturnValue(400) diff --git a/app/src/organisms/Desktop/Devices/ProtocolRun/SetupLiquids/__tests__/SetupLiquidsMap.test.tsx b/app/src/organisms/Desktop/Devices/ProtocolRun/SetupLiquids/__tests__/SetupLiquidsMap.test.tsx index 023e73af09c..97e96232b7e 100644 --- a/app/src/organisms/Desktop/Devices/ProtocolRun/SetupLiquids/__tests__/SetupLiquidsMap.test.tsx +++ b/app/src/organisms/Desktop/Devices/ProtocolRun/SetupLiquids/__tests__/SetupLiquidsMap.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { when } from 'vitest-when' import { screen } from '@testing-library/react' import { describe, it, beforeEach, vi, afterEach, expect } from 'vitest' @@ -35,6 +34,7 @@ import { mockFetchModulesSuccessActionPayloadModules } from '/app/redux/modules/ import { SetupLiquidsMap } from '../SetupLiquidsMap' +import type { ComponentProps } from 'react' import type { ModuleModel, ModuleType, @@ -102,7 +102,7 @@ const mockMagneticModule = { quirks: [], } -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, }) @@ -113,7 +113,7 @@ const mockProtocolAnalysis = { } as any describe('SetupLiquidsMap', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { runId: RUN_ID, diff --git a/app/src/organisms/Desktop/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/NotConfiguredModal.test.tsx b/app/src/organisms/Desktop/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/NotConfiguredModal.test.tsx index 9f371faaa64..fc2b584cf22 100644 --- a/app/src/organisms/Desktop/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/NotConfiguredModal.test.tsx +++ b/app/src/organisms/Desktop/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/NotConfiguredModal.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { fireEvent, screen } from '@testing-library/react' import { describe, it, beforeEach, vi, expect } from 'vitest' import { renderWithProviders } from '/app/__testing-utils__' @@ -9,20 +8,21 @@ import { i18n } from '/app/i18n' import { NotConfiguredModal } from '../NotConfiguredModal' import { useNotifyDeckConfigurationQuery } from '/app/resources/deck_configuration' +import type { ComponentProps } from 'react' import type { UseQueryResult } from 'react-query' import type { DeckConfiguration } from '@opentrons/shared-data' vi.mock('@opentrons/react-api-client') vi.mock('/app/resources/deck_configuration') -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, })[0] } describe('NotConfiguredModal', () => { - let props: React.ComponentProps + let props: ComponentProps const mockUpdate = vi.fn() beforeEach(() => { props = { diff --git a/app/src/organisms/Desktop/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/SetupFixtureList.test.tsx b/app/src/organisms/Desktop/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/SetupFixtureList.test.tsx index dd0236ac96d..61708168788 100644 --- a/app/src/organisms/Desktop/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/SetupFixtureList.test.tsx +++ b/app/src/organisms/Desktop/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/SetupFixtureList.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { fireEvent, screen } from '@testing-library/react' import { describe, it, beforeEach, vi } from 'vitest' import { renderWithProviders } from '/app/__testing-utils__' @@ -16,6 +15,7 @@ import { NotConfiguredModal } from '../NotConfiguredModal' import { LocationConflictModal } from '/app/organisms/LocationConflictModal' import { DeckFixtureSetupInstructionsModal } from '/app/organisms/DeviceDetailsDeckConfiguration/DeckFixtureSetupInstructionsModal' +import type { ComponentProps } from 'react' import type { CutoutConfigAndCompatibility } from '/app/resources/deck_configuration/hooks' vi.mock('/app/resources/deck_configuration/hooks') @@ -61,14 +61,14 @@ const mockConflictDeckConfigCompatibility: CutoutConfigAndCompatibility[] = [ }, ] -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, }) } describe('SetupFixtureList', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { deckConfigCompatibility: mockDeckConfigCompatibility, diff --git a/app/src/organisms/Desktop/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/SetupModulesAndDeck.test.tsx b/app/src/organisms/Desktop/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/SetupModulesAndDeck.test.tsx index 21926d3c823..615bb5a487d 100644 --- a/app/src/organisms/Desktop/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/SetupModulesAndDeck.test.tsx +++ b/app/src/organisms/Desktop/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/SetupModulesAndDeck.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { when } from 'vitest-when' import { describe, it, beforeEach, expect, vi } from 'vitest' import { fireEvent, screen } from '@testing-library/react' @@ -20,6 +19,8 @@ import { SetupModulesList } from '../SetupModulesList' import { SetupModulesMap } from '../SetupModulesMap' import { SetupFixtureList } from '../SetupFixtureList' +import type { ComponentProps } from 'react' + vi.mock('/app/redux-resources/robots') vi.mock('../SetupModulesList') vi.mock('../SetupModulesMap') @@ -31,14 +32,14 @@ vi.mock('/app/resources/runs') const MOCK_ROBOT_NAME = 'otie' const MOCK_RUN_ID = '1' -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, })[0] } describe('SetupModuleAndDeck', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { robotName: MOCK_ROBOT_NAME, diff --git a/app/src/organisms/Desktop/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/SetupModulesList.test.tsx b/app/src/organisms/Desktop/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/SetupModulesList.test.tsx index e62d600cea6..95c95fced5a 100644 --- a/app/src/organisms/Desktop/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/SetupModulesList.test.tsx +++ b/app/src/organisms/Desktop/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/SetupModulesList.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { when } from 'vitest-when' import { fireEvent, screen, waitFor } from '@testing-library/react' import { describe, it, beforeEach, expect, vi } from 'vitest' @@ -27,6 +26,7 @@ import { UnMatchedModuleWarning } from '../UnMatchedModuleWarning' import { SetupModulesList } from '../SetupModulesList' import { LocationConflictModal } from '/app/organisms/LocationConflictModal' +import type { ComponentProps } from 'react' import type { ModuleModel, ModuleType } from '@opentrons/shared-data' import type { DiscoveredRobot } from '/app/redux/discovery/types' @@ -77,14 +77,14 @@ const mockCalibratedData = { last_modified: '2023-06-01T14:42:20.131798+00:00', } -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, })[0] } describe('SetupModulesList', () => { - let props: React.ComponentProps + let props: ComponentProps let mockChainLiveCommands = vi.fn() beforeEach(() => { props = { diff --git a/app/src/organisms/Desktop/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/SetupModulesMap.test.tsx b/app/src/organisms/Desktop/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/SetupModulesMap.test.tsx index 9d9449283dc..c7619b44f9b 100644 --- a/app/src/organisms/Desktop/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/SetupModulesMap.test.tsx +++ b/app/src/organisms/Desktop/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/SetupModulesMap.test.tsx @@ -1,6 +1,4 @@ /* eslint-disable @typescript-eslint/no-unsafe-argument */ - -import type * as React from 'react' import { when } from 'vitest-when' import { MemoryRouter } from 'react-router-dom' import { describe, it, beforeEach, vi, afterEach, expect } from 'vitest' @@ -18,6 +16,8 @@ import { useMostRecentCompletedAnalysis } from '/app/resources/runs' import { ModuleInfo } from '/app/molecules/ModuleInfo' import { SetupModulesMap } from '../SetupModulesMap' import { getAttachedProtocolModuleMatches } from '/app/transformations/analysis' + +import type { ComponentProps } from 'react' import type { CompletedProtocolAnalysis, ModuleModel, @@ -47,7 +47,7 @@ vi.mock('/app/transformations/analysis') vi.mock('/app/molecules/ModuleInfo') vi.mock('/app/resources/modules') -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders( @@ -98,7 +98,7 @@ const mockTCModule = { } describe('SetupModulesMap', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { runId: MOCK_RUN_ID, diff --git a/app/src/organisms/Desktop/Devices/ProtocolRun/SetupStep.tsx b/app/src/organisms/Desktop/Devices/ProtocolRun/SetupStep.tsx index 25f2baf1d64..2d5fedf7700 100644 --- a/app/src/organisms/Desktop/Devices/ProtocolRun/SetupStep.tsx +++ b/app/src/organisms/Desktop/Devices/ProtocolRun/SetupStep.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { css } from 'styled-components' import { @@ -18,19 +17,21 @@ import { TYPOGRAPHY, } from '@opentrons/components' +import type { ReactNode } from 'react' + interface SetupStepProps { /** whether or not to show the full contents of the step */ expanded: boolean /** always shown text name of the step */ - title: React.ReactNode + title: ReactNode /** always shown text that provides a one sentence explanation of the contents */ description: string /** callback that should toggle the expanded state (managed by parent) */ toggleExpanded: () => void /** contents to be shown only when expanded */ - children: React.ReactNode + children: ReactNode /** element to be shown (right aligned) regardless of expanded state */ - rightElement: React.ReactNode + rightElement: ReactNode } const EXPANDED_STYLE = css` diff --git a/app/src/organisms/Desktop/Devices/ProtocolRun/__tests__/EmptySetupStep.test.tsx b/app/src/organisms/Desktop/Devices/ProtocolRun/__tests__/EmptySetupStep.test.tsx index 673d8b4806c..b716de57f06 100644 --- a/app/src/organisms/Desktop/Devices/ProtocolRun/__tests__/EmptySetupStep.test.tsx +++ b/app/src/organisms/Desktop/Devices/ProtocolRun/__tests__/EmptySetupStep.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { describe, it, beforeEach } from 'vitest' import { screen } from '@testing-library/react' @@ -6,14 +5,16 @@ import { renderWithProviders } from '/app/__testing-utils__' import { i18n } from '/app/i18n' import { EmptySetupStep } from '../EmptySetupStep' -const render = (props: React.ComponentProps) => { +import type { ComponentProps } from 'react' + +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, })[0] } describe('EmptySetupStep', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { title: 'mockTitle', diff --git a/app/src/organisms/Desktop/Devices/ProtocolRun/__tests__/LabwareInfoOverlay.test.tsx b/app/src/organisms/Desktop/Devices/ProtocolRun/__tests__/LabwareInfoOverlay.test.tsx index 0198aa9e448..a47f37e1136 100644 --- a/app/src/organisms/Desktop/Devices/ProtocolRun/__tests__/LabwareInfoOverlay.test.tsx +++ b/app/src/organisms/Desktop/Devices/ProtocolRun/__tests__/LabwareInfoOverlay.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { when } from 'vitest-when' import { screen } from '@testing-library/react' import { describe, it, beforeEach, vi, afterEach, expect } from 'vitest' @@ -13,6 +12,8 @@ import { getLabwareLocation } from '/app/transformations/commands' import { LabwareInfoOverlay } from '../LabwareInfoOverlay' import { getLabwareDefinitionUri } from '/app/transformations/protocols' import { useLabwareOffsetForLabware } from '../useLabwareOffsetForLabware' + +import type { ComponentProps } from 'react' import type { LabwareDefinition2, ProtocolFile, @@ -33,7 +34,7 @@ vi.mock('@opentrons/shared-data', async importOriginal => { } }) -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders( @@ -51,7 +52,7 @@ const MOCK_LABWARE_VECTOR = { x: 1, y: 2, z: 3 } const MOCK_RUN_ID = 'fake_run_id' describe('LabwareInfoOverlay', () => { - let props: React.ComponentProps + let props: ComponentProps let labware: LoadedLabware[] let labwareDefinitions: ProtocolFile<{}>['labwareDefinitions'] beforeEach(() => { diff --git a/app/src/organisms/Desktop/Devices/ProtocolRun/__tests__/ProtocolRunModuleControls.test.tsx b/app/src/organisms/Desktop/Devices/ProtocolRun/__tests__/ProtocolRunModuleControls.test.tsx index 5dd1507f893..1895c2e4eca 100644 --- a/app/src/organisms/Desktop/Devices/ProtocolRun/__tests__/ProtocolRunModuleControls.test.tsx +++ b/app/src/organisms/Desktop/Devices/ProtocolRun/__tests__/ProtocolRunModuleControls.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { when } from 'vitest-when' import { describe, it, beforeEach, vi, afterEach } from 'vitest' import { screen } from '@testing-library/react' @@ -15,6 +14,8 @@ import { mockThermocycler, mockHeaterShaker, } from '/app/redux/modules/__fixtures__' + +import type { ComponentProps } from 'react' import type { ModuleModel, ModuleType } from '@opentrons/shared-data' vi.mock('@opentrons/react-api-client') @@ -49,9 +50,7 @@ const mockTCModule = { } const MOCK_TC_COORDS = [20, 30, 0] -const render = ( - props: React.ComponentProps -) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, })[0] diff --git a/app/src/organisms/Desktop/Devices/ProtocolRun/__tests__/ProtocolRunRuntimeParameters.test.tsx b/app/src/organisms/Desktop/Devices/ProtocolRun/__tests__/ProtocolRunRuntimeParameters.test.tsx index 59425f6ff6b..99922644961 100644 --- a/app/src/organisms/Desktop/Devices/ProtocolRun/__tests__/ProtocolRunRuntimeParameters.test.tsx +++ b/app/src/organisms/Desktop/Devices/ProtocolRun/__tests__/ProtocolRunRuntimeParameters.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { describe, it, vi, beforeEach, afterEach, expect } from 'vitest' import { screen } from '@testing-library/react' import { when } from 'vitest-when' @@ -16,6 +15,7 @@ import { } from '/app/resources/runs/__fixtures__' import { ProtocolRunRuntimeParameters } from '../ProtocolRunRunTimeParameters' +import type { ComponentProps } from 'react' import type { UseQueryResult } from 'react-query' import type { Run } from '@opentrons/api-client' import type { @@ -100,16 +100,14 @@ const mockCsvRtp = { }, } -const render = ( - props: React.ComponentProps -) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, }) } describe('ProtocolRunRuntimeParameters', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { runId: RUN_ID, diff --git a/app/src/organisms/Desktop/Devices/ProtocolRun/__tests__/SetupCalibrationItem.test.tsx b/app/src/organisms/Desktop/Devices/ProtocolRun/__tests__/SetupCalibrationItem.test.tsx index e39e5d7c83c..9002eb0da0d 100644 --- a/app/src/organisms/Desktop/Devices/ProtocolRun/__tests__/SetupCalibrationItem.test.tsx +++ b/app/src/organisms/Desktop/Devices/ProtocolRun/__tests__/SetupCalibrationItem.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { screen } from '@testing-library/react' import { when } from 'vitest-when' import { describe, it, beforeEach, vi, afterEach } from 'vitest' @@ -9,6 +8,8 @@ import { useRunHasStarted } from '/app/resources/runs' import { formatTimestamp } from '/app/transformations/runs' import { SetupCalibrationItem } from '../SetupCalibrationItem' +import type { ComponentProps } from 'react' + vi.mock('/app/resources/runs') vi.mock('/app/transformations/runs') @@ -21,7 +22,7 @@ describe('SetupCalibrationItem', () => { title = 'stub title', button = , runId = RUN_ID, - }: Partial> = {}) => { + }: Partial> = {}) => { return renderWithProviders( { const render = ({ mount = 'left', runId = RUN_ID, - }: Partial< - React.ComponentProps - > = {}) => { + }: Partial> = {}) => { return renderWithProviders( { mount = 'left', robotName = ROBOT_NAME, runId = RUN_ID, - }: Partial< - React.ComponentProps - > = {}) => { + }: Partial> = {}) => { return renderWithProviders( { nextStep = 'module_setup_step', calibrationStatus = { complete: true }, expandStep = mockExpandStep, - }: Partial> = {}) => { + }: Partial> = {}) => { return renderWithProviders( { @@ -16,7 +16,7 @@ describe('SetupStep', () => { toggleExpanded = toggleExpandedMock, children = , rightElement =
        right element
        , - }: Partial> = {}) => { + }: Partial> = {}) => { return renderWithProviders( { hasCalibrated = false, tipRackDefinition = fixtureTiprack300ul as LabwareDefinition2, isExtendedPipOffset = false, - }: Partial< - React.ComponentProps - > = {}) => { + }: Partial> = {}) => { return renderWithProviders( (false) + ] = useState(false) const [ showConnectionTroubleshootingModal, setShowConnectionTroubleshootingModal, - ] = React.useState(false) + ] = useState(false) const isRobotOnWrongVersionOfSoftware = useIsRobotOnWrongVersionOfSoftware( robot.name @@ -66,20 +67,20 @@ export function RobotOverflowMenu(props: RobotOverflowMenuProps): JSX.Element { const isRobotBusy = useIsRobotBusy({ poll: true }) - const handleClickRun: React.MouseEventHandler = e => { + const handleClickRun: MouseEventHandler = e => { e.preventDefault() e.stopPropagation() setShowChooseProtocolSlideout(true) setShowOverflowMenu(false) } - const handleClickConnectionTroubleshooting: React.MouseEventHandler = e => { + const handleClickConnectionTroubleshooting: MouseEventHandler = e => { e.preventDefault() e.stopPropagation() setShowConnectionTroubleshootingModal(true) setShowOverflowMenu(false) } - let menuItems: React.ReactNode + let menuItems: ReactNode if (robot.status === CONNECTABLE && runId == null) { menuItems = ( <> @@ -161,7 +162,7 @@ export function RobotOverflowMenu(props: RobotOverflowMenuProps): JSX.Element { data-testid={`RobotCard_${String(robot.name)}_overflowMenu`} flexDirection={DIRECTION_COLUMN} position={POSITION_RELATIVE} - onClick={(e: React.MouseEvent) => { + onClick={(e: MouseEvent) => { e.stopPropagation() }} {...styleProps} diff --git a/app/src/organisms/Desktop/Devices/RobotOverviewOverflowMenu.tsx b/app/src/organisms/Desktop/Devices/RobotOverviewOverflowMenu.tsx index 3b6dda678ca..5ff5ac357b6 100644 --- a/app/src/organisms/Desktop/Devices/RobotOverviewOverflowMenu.tsx +++ b/app/src/organisms/Desktop/Devices/RobotOverviewOverflowMenu.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useState } from 'react' import { css } from 'styled-components' import { createPortal } from 'react-dom' import { useTranslation } from 'react-i18next' @@ -35,6 +35,8 @@ import { useIsRobotBusy } from '/app/redux-resources/robots' import { useCanDisconnect } from '/app/resources/networking/hooks' import { useIsEstopNotDisengaged } from '/app/resources/devices/hooks/useIsEstopNotDisengaged' import { useCurrentRunId } from '/app/resources/runs' + +import type { MouseEventHandler, MouseEvent } from 'react' import type { DiscoveredRobot } from '/app/redux/discovery/types' import type { Dispatch } from '/app/redux/types' @@ -61,25 +63,23 @@ export const RobotOverviewOverflowMenu = ( const dispatch = useDispatch() - const handleClickRestart: React.MouseEventHandler = () => { + const handleClickRestart: MouseEventHandler = () => { dispatch(restartRobot(robot.name)) } - const handleClickHomeGantry: React.MouseEventHandler = () => { + const handleClickHomeGantry: MouseEventHandler = () => { dispatch(home(robot.name, ROBOT)) } const [ showChooseProtocolSlideout, setShowChooseProtocolSlideout, - ] = React.useState(false) - const [showDisconnectModal, setShowDisconnectModal] = React.useState( - false - ) + ] = useState(false) + const [showDisconnectModal, setShowDisconnectModal] = useState(false) const canDisconnect = useCanDisconnect(robot.name) - const handleClickDisconnect: React.MouseEventHandler = () => { + const handleClickDisconnect: MouseEventHandler = () => { setShowDisconnectModal(true) } @@ -87,7 +87,7 @@ export const RobotOverviewOverflowMenu = ( dispatch(checkShellUpdate()) }) - const handleClickRun: React.MouseEventHandler = () => { + const handleClickRun: MouseEventHandler = () => { setShowChooseProtocolSlideout(true) } @@ -125,7 +125,7 @@ export const RobotOverviewOverflowMenu = ( top="2.25rem" right={0} flexDirection={DIRECTION_COLUMN} - onClick={(e: React.MouseEvent) => { + onClick={(e: MouseEvent) => { e.preventDefault() e.stopPropagation() setShowOverflowMenu(false) diff --git a/app/src/organisms/Desktop/Devices/RobotSettings/AdvancedTab/AdvancedTabSlideouts/DeviceResetSlideout.tsx b/app/src/organisms/Desktop/Devices/RobotSettings/AdvancedTab/AdvancedTabSlideouts/DeviceResetSlideout.tsx index d4ddba6e764..bf963b22f29 100644 --- a/app/src/organisms/Desktop/Devices/RobotSettings/AdvancedTab/AdvancedTabSlideouts/DeviceResetSlideout.tsx +++ b/app/src/organisms/Desktop/Devices/RobotSettings/AdvancedTab/AdvancedTabSlideouts/DeviceResetSlideout.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useState, useEffect } from 'react' import { useSelector, useDispatch } from 'react-redux' import { useTranslation } from 'react-i18next' import snakeCase from 'lodash/snakeCase' @@ -20,6 +20,7 @@ import { LegacyStyledText, TYPOGRAPHY, } from '@opentrons/components' +import { FLEX_ROBOT_TYPE, OT2_ROBOT_TYPE } from '@opentrons/shared-data' import { Slideout } from '/app/atoms/Slideout' import { Divider } from '/app/atoms/structure' @@ -40,6 +41,7 @@ import { import { useRobot, useIsFlex } from '/app/redux-resources/robots' import { useNotifyAllRunsQuery } from '/app/resources/runs' +import type { MouseEventHandler } from 'react' import type { State, Dispatch } from '/app/redux/types' import type { ResetConfigRequest } from '/app/redux/robot-admin/types' @@ -61,7 +63,7 @@ export function DeviceResetSlideout({ const doTrackEvent = useTrackEvent() const robot = useRobot(robotName) const dispatch = useDispatch() - const [resetOptions, setResetOptions] = React.useState({}) + const [resetOptions, setResetOptions] = useState({}) const runsQueryResponse = useNotifyAllRunsQuery() const isFlex = useIsFlex(robotName) @@ -98,15 +100,17 @@ export function DeviceResetSlideout({ ? options.filter(opt => opt.id.includes('authorizedKeys')) : [] - React.useEffect(() => { + useEffect(() => { dispatch(fetchResetConfigOptions(robotName)) }, [dispatch, robotName]) - const downloadCalibrationLogs: React.MouseEventHandler = e => { + const downloadCalibrationLogs: MouseEventHandler = e => { e.preventDefault() doTrackEvent({ name: ANALYTICS_CALIBRATION_DATA_DOWNLOADED, - properties: {}, + properties: { + robotType: isFlex ? FLEX_ROBOT_TYPE : OT2_ROBOT_TYPE, + }, }) saveAs( new Blob([ @@ -120,7 +124,7 @@ export function DeviceResetSlideout({ ) } - const downloadRunHistoryLogs: React.MouseEventHandler = e => { + const downloadRunHistoryLogs: MouseEventHandler = e => { e.preventDefault() const runsHistory = runsQueryResponse != null ? runsQueryResponse.data?.data : [] diff --git a/app/src/organisms/Desktop/Devices/RobotSettings/AdvancedTab/AdvancedTabSlideouts/FactoryModeSlideout.tsx b/app/src/organisms/Desktop/Devices/RobotSettings/AdvancedTab/AdvancedTabSlideouts/FactoryModeSlideout.tsx index 1472204ce8f..ff083c9b3e5 100644 --- a/app/src/organisms/Desktop/Devices/RobotSettings/AdvancedTab/AdvancedTabSlideouts/FactoryModeSlideout.tsx +++ b/app/src/organisms/Desktop/Devices/RobotSettings/AdvancedTab/AdvancedTabSlideouts/FactoryModeSlideout.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useState, useEffect } from 'react' import { useDispatch } from 'react-redux' import { useForm, Controller } from 'react-hook-form' import { Trans, useTranslation } from 'react-i18next' @@ -28,6 +28,7 @@ import { FileUpload } from '/app/molecules/FileUpload' import { UploadInput } from '/app/molecules/UploadInput' import { restartRobot } from '/app/redux/robot-admin' +import type { ChangeEvent, MouseEventHandler } from 'react' import type { FieldError, Resolver } from 'react-hook-form' import type { RobotSettingsField } from '@opentrons/api-client' import type { Dispatch } from '/app/redux/types' @@ -63,11 +64,11 @@ export function FactoryModeSlideout({ const last = sn?.substring(sn.length - 4) - const [currentStep, setCurrentStep] = React.useState(1) - const [toggleValue, setToggleValue] = React.useState(false) - const [file, setFile] = React.useState(null) - const [fileError, setFileError] = React.useState(null) - const [isUploading, setIsUploading] = React.useState(false) + const [currentStep, setCurrentStep] = useState(1) + const [toggleValue, setToggleValue] = useState(false) + const [file, setFile] = useState(null) + const [fileError, setFileError] = useState(null) + const [isUploading, setIsUploading] = useState(false) const onFinishCompleteClick = (): void => { dispatch(restartRobot(robotName)) @@ -142,11 +143,11 @@ export function FactoryModeSlideout({ void handleSubmit(onSubmit)() } - const handleToggleClick: React.MouseEventHandler = () => { + const handleToggleClick: MouseEventHandler = () => { setToggleValue(toggleValue => !toggleValue) } - const handleCompleteClick: React.MouseEventHandler = () => { + const handleCompleteClick: MouseEventHandler = () => { setIsUploading(true) updateRobotSetting({ id: 'enableOEMMode', value: toggleValue }) } @@ -173,7 +174,7 @@ export function FactoryModeSlideout({ } } - React.useEffect(() => { + useEffect(() => { // initialize local state to OEM mode value if (isOEMMode != null) { setToggleValue(isOEMMode) @@ -229,7 +230,7 @@ export function FactoryModeSlideout({ id="factoryModeInput" name="factoryModeInput" type="text" - onChange={(e: React.ChangeEvent) => { + onChange={(e: ChangeEvent) => { field.onChange(e) clearErrors() }} diff --git a/app/src/organisms/Desktop/Devices/RobotSettings/AdvancedTab/AdvancedTabSlideouts/RenameRobotSlideout.tsx b/app/src/organisms/Desktop/Devices/RobotSettings/AdvancedTab/AdvancedTabSlideouts/RenameRobotSlideout.tsx index 5c02dc8eb95..af75fca2189 100644 --- a/app/src/organisms/Desktop/Devices/RobotSettings/AdvancedTab/AdvancedTabSlideouts/RenameRobotSlideout.tsx +++ b/app/src/organisms/Desktop/Devices/RobotSettings/AdvancedTab/AdvancedTabSlideouts/RenameRobotSlideout.tsx @@ -1,8 +1,9 @@ -import * as React from 'react' +import { useState } from 'react' import { useSelector, useDispatch } from 'react-redux' import { useNavigate } from 'react-router-dom' import { useForm, Controller } from 'react-hook-form' import { useTranslation } from 'react-i18next' + import { COLORS, Banner, @@ -14,6 +15,8 @@ import { SPACING, } from '@opentrons/components' import { useUpdateRobotNameMutation } from '@opentrons/react-api-client' +import { FLEX_ROBOT_TYPE, OT2_ROBOT_TYPE } from '@opentrons/shared-data' + import { removeRobot, getConnectableRobots, @@ -24,9 +27,11 @@ import { useTrackEvent, ANALYTICS_RENAME_ROBOT } from '/app/redux/analytics' import { Slideout } from '/app/atoms/Slideout' import { useIsFlex } from '/app/redux-resources/robots' +import type { ChangeEvent } from 'react' import type { Resolver, FieldError } from 'react-hook-form' import type { UpdatedRobotName } from '@opentrons/api-client' import type { State, Dispatch } from '/app/redux/types' + interface RenameRobotSlideoutProps { isExpanded: boolean onCloseClick: () => void @@ -49,9 +54,7 @@ export function RenameRobotSlideout({ robotName, }: RenameRobotSlideoutProps): JSX.Element { const { t } = useTranslation('device_settings') - const [previousRobotName, setPreviousRobotName] = React.useState( - robotName - ) + const [previousRobotName, setPreviousRobotName] = useState(robotName) const isFlex = useIsFlex(robotName) const trackEvent = useTrackEvent() const navigate = useNavigate() @@ -152,6 +155,7 @@ export function RenameRobotSlideout({ properties: { previousRobotName, newRobotName: newRobotName, + robotType: isFlex ? FLEX_ROBOT_TYPE : OT2_ROBOT_TYPE, }, }) handleSubmit(onSubmit)() @@ -190,7 +194,7 @@ export function RenameRobotSlideout({ id="newRobotName" name="newRobotName" type="text" - onChange={(e: React.ChangeEvent) => { + onChange={(e: ChangeEvent) => { field.onChange(e) trigger('newRobotName') }} diff --git a/app/src/organisms/Desktop/Devices/RobotSettings/AdvancedTab/AdvancedTabSlideouts/__tests__/DeviceResetModal.test.tsx b/app/src/organisms/Desktop/Devices/RobotSettings/AdvancedTab/AdvancedTabSlideouts/__tests__/DeviceResetModal.test.tsx index a5a3a91d219..78e5427bfd5 100644 --- a/app/src/organisms/Desktop/Devices/RobotSettings/AdvancedTab/AdvancedTabSlideouts/__tests__/DeviceResetModal.test.tsx +++ b/app/src/organisms/Desktop/Devices/RobotSettings/AdvancedTab/AdvancedTabSlideouts/__tests__/DeviceResetModal.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { MemoryRouter } from 'react-router-dom' import { fireEvent, screen } from '@testing-library/react' import { describe, it, vi, expect, beforeEach } from 'vitest' @@ -9,6 +8,7 @@ import { resetConfig } from '/app/redux/robot-admin' import { useDispatchApiRequest } from '/app/redux/robot-api' import { DeviceResetModal } from '../DeviceResetModal' +import type { ComponentProps } from 'react' import type { DispatchApiRequestType } from '/app/redux/robot-api' vi.mock('/app/redux-resources/robots') @@ -18,7 +18,7 @@ vi.mock('/app/redux/robot-api') const mockResetOptions = {} const mockCloseModal = vi.fn() const ROBOT_NAME = 'otie' -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders( diff --git a/app/src/organisms/Desktop/Devices/RobotSettings/AdvancedTab/AdvancedTabSlideouts/__tests__/RenameRobotSlideout.test.tsx b/app/src/organisms/Desktop/Devices/RobotSettings/AdvancedTab/AdvancedTabSlideouts/__tests__/RenameRobotSlideout.test.tsx index 6f7b1b4ab28..13612c63f19 100644 --- a/app/src/organisms/Desktop/Devices/RobotSettings/AdvancedTab/AdvancedTabSlideouts/__tests__/RenameRobotSlideout.test.tsx +++ b/app/src/organisms/Desktop/Devices/RobotSettings/AdvancedTab/AdvancedTabSlideouts/__tests__/RenameRobotSlideout.test.tsx @@ -2,6 +2,9 @@ import { MemoryRouter } from 'react-router-dom' import { fireEvent, screen, waitFor } from '@testing-library/react' import { describe, it, vi, expect, beforeEach } from 'vitest' import '@testing-library/jest-dom/vitest' + +import { OT2_ROBOT_TYPE } from '@opentrons/shared-data' + import { renderWithProviders } from '/app/__testing-utils__' import { i18n } from '/app/i18n' import { useTrackEvent, ANALYTICS_RENAME_ROBOT } from '/app/redux/analytics' @@ -14,7 +17,6 @@ import { mockConnectableRobot, mockReachableRobot, } from '/app/redux/discovery/__fixtures__' - import { RenameRobotSlideout } from '../RenameRobotSlideout' import { useIsFlex } from '/app/redux-resources/robots' @@ -111,7 +113,11 @@ describe('RobotSettings RenameRobotSlideout', () => { await waitFor(() => { expect(mockTrackEvent).toHaveBeenCalledWith({ name: ANALYTICS_RENAME_ROBOT, - properties: { newRobotName: 'mockInput', previousRobotName: 'otie' }, + properties: { + newRobotName: 'mockInput', + previousRobotName: 'otie', + robotType: OT2_ROBOT_TYPE, + }, }) }) }) diff --git a/app/src/organisms/Desktop/Devices/RobotSettings/AdvancedTab/DeviceReset.tsx b/app/src/organisms/Desktop/Devices/RobotSettings/AdvancedTab/DeviceReset.tsx index 2249e453fe6..b9cd537894a 100644 --- a/app/src/organisms/Desktop/Devices/RobotSettings/AdvancedTab/DeviceReset.tsx +++ b/app/src/organisms/Desktop/Devices/RobotSettings/AdvancedTab/DeviceReset.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { useTranslation } from 'react-i18next' import { @@ -14,6 +13,8 @@ import { import { TertiaryButton } from '/app/atoms/buttons' +import type { MouseEventHandler } from 'react' + interface DeviceResetProps { updateIsExpanded: ( isExpanded: boolean, @@ -28,7 +29,7 @@ export function DeviceReset({ }: DeviceResetProps): JSX.Element { const { t } = useTranslation('device_settings') - const handleClick: React.MouseEventHandler = () => { + const handleClick: MouseEventHandler = () => { if (!isRobotBusy) { updateIsExpanded(true, 'deviceReset') } diff --git a/app/src/organisms/Desktop/Devices/RobotSettings/AdvancedTab/DisplayRobotName.tsx b/app/src/organisms/Desktop/Devices/RobotSettings/AdvancedTab/DisplayRobotName.tsx index c6a380cd6e5..7f1d51022b4 100644 --- a/app/src/organisms/Desktop/Devices/RobotSettings/AdvancedTab/DisplayRobotName.tsx +++ b/app/src/organisms/Desktop/Devices/RobotSettings/AdvancedTab/DisplayRobotName.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { useTranslation } from 'react-i18next' import { @@ -14,6 +13,8 @@ import { } from '@opentrons/components' import { TertiaryButton } from '/app/atoms/buttons' + +import type { MouseEventHandler } from 'react' interface DisplayRobotNameProps { robotName: string updateIsExpanded: ( @@ -30,7 +31,7 @@ export function DisplayRobotName({ }: DisplayRobotNameProps): JSX.Element { const { t } = useTranslation('device_settings') - const handleClick: React.MouseEventHandler = () => { + const handleClick: MouseEventHandler = () => { if (!isRobotBusy) { updateIsExpanded(true, 'renameRobot') } diff --git a/app/src/organisms/Desktop/Devices/RobotSettings/AdvancedTab/FactoryMode.tsx b/app/src/organisms/Desktop/Devices/RobotSettings/AdvancedTab/FactoryMode.tsx index 14cb3766040..8fea7e0f82d 100644 --- a/app/src/organisms/Desktop/Devices/RobotSettings/AdvancedTab/FactoryMode.tsx +++ b/app/src/organisms/Desktop/Devices/RobotSettings/AdvancedTab/FactoryMode.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { useTranslation } from 'react-i18next' import { @@ -14,9 +13,11 @@ import { import { TertiaryButton } from '/app/atoms/buttons' +import type { Dispatch, SetStateAction } from 'react' + interface FactoryModeProps { isRobotBusy: boolean - setShowFactoryModeSlideout: React.Dispatch> + setShowFactoryModeSlideout: Dispatch> sn: string | null } diff --git a/app/src/organisms/Desktop/Devices/RobotSettings/AdvancedTab/GantryHoming.tsx b/app/src/organisms/Desktop/Devices/RobotSettings/AdvancedTab/GantryHoming.tsx index 96ea82a59cf..6537d1fdf1a 100644 --- a/app/src/organisms/Desktop/Devices/RobotSettings/AdvancedTab/GantryHoming.tsx +++ b/app/src/organisms/Desktop/Devices/RobotSettings/AdvancedTab/GantryHoming.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { useDispatch } from 'react-redux' import { useTranslation } from 'react-i18next' @@ -15,6 +14,7 @@ import { import { ToggleButton } from '/app/atoms/buttons' import { updateSetting } from '/app/redux/robot-settings' +import type { MouseEventHandler } from 'react' import type { Dispatch } from '/app/redux/types' import type { RobotSettingsField } from '/app/redux/robot-settings/types' @@ -34,7 +34,7 @@ export function GantryHoming({ const value = settings?.value ? settings.value : false const id = settings?.id ? settings.id : 'disableHomeOnBoot' - const handleClick: React.MouseEventHandler = () => { + const handleClick: MouseEventHandler = () => { if (!isRobotBusy) { dispatch(updateSetting(robotName, id, !value)) } diff --git a/app/src/organisms/Desktop/Devices/RobotSettings/AdvancedTab/LegacySettings.tsx b/app/src/organisms/Desktop/Devices/RobotSettings/AdvancedTab/LegacySettings.tsx index 5ea0ae6e5ec..7947243d13f 100644 --- a/app/src/organisms/Desktop/Devices/RobotSettings/AdvancedTab/LegacySettings.tsx +++ b/app/src/organisms/Desktop/Devices/RobotSettings/AdvancedTab/LegacySettings.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { useDispatch } from 'react-redux' import { useTranslation } from 'react-i18next' @@ -15,6 +14,7 @@ import { import { ToggleButton } from '/app/atoms/buttons' import { updateSetting } from '/app/redux/robot-settings' +import type { MouseEventHandler } from 'react' import type { Dispatch } from '/app/redux/types' import type { RobotSettingsField } from '/app/redux/robot-settings/types' @@ -34,7 +34,7 @@ export function LegacySettings({ const value = settings?.value ? settings.value : false const id = settings?.id ? settings.id : 'deckCalibrationDots' - const handleClick: React.MouseEventHandler = () => { + const handleClick: MouseEventHandler = () => { if (!isRobotBusy) { dispatch(updateSetting(robotName, id, !value)) } diff --git a/app/src/organisms/Desktop/Devices/RobotSettings/AdvancedTab/ShortTrashBin.tsx b/app/src/organisms/Desktop/Devices/RobotSettings/AdvancedTab/ShortTrashBin.tsx index 5bc00476406..6152163c223 100644 --- a/app/src/organisms/Desktop/Devices/RobotSettings/AdvancedTab/ShortTrashBin.tsx +++ b/app/src/organisms/Desktop/Devices/RobotSettings/AdvancedTab/ShortTrashBin.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { useDispatch } from 'react-redux' import { useTranslation } from 'react-i18next' @@ -15,6 +14,7 @@ import { import { ToggleButton } from '/app/atoms/buttons' import { updateSetting } from '/app/redux/robot-settings' +import type { MouseEventHandler } from 'react' import type { Dispatch } from '/app/redux/types' import type { RobotSettingsField } from '/app/redux/robot-settings/types' @@ -34,7 +34,7 @@ export function ShortTrashBin({ const value = settings?.value ? settings.value : false const id = settings?.id ? settings.id : 'shortTrashBin' - const handleClick: React.MouseEventHandler = () => { + const handleClick: MouseEventHandler = () => { if (!isRobotBusy) { dispatch(updateSetting(robotName, id, !value)) } diff --git a/app/src/organisms/Desktop/Devices/RobotSettings/AdvancedTab/Troubleshooting.tsx b/app/src/organisms/Desktop/Devices/RobotSettings/AdvancedTab/Troubleshooting.tsx index 45b2b96f462..a216c71cbd4 100644 --- a/app/src/organisms/Desktop/Devices/RobotSettings/AdvancedTab/Troubleshooting.tsx +++ b/app/src/organisms/Desktop/Devices/RobotSettings/AdvancedTab/Troubleshooting.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useState, useRef } from 'react' import { useTranslation } from 'react-i18next' import { saveAs } from 'file-saver' import JSZip from 'jszip' @@ -25,6 +25,7 @@ import { useToaster } from '/app/organisms/ToasterOven' import { CONNECTABLE } from '/app/redux/discovery' import { useRobot } from '/app/redux-resources/robots' +import type { MouseEventHandler } from 'react' import type { IconProps } from '@opentrons/components' interface TroubleshootingProps { @@ -38,16 +39,15 @@ export function Troubleshooting({ const robot = useRobot(robotName) const controlDisabled = robot?.status !== CONNECTABLE const logsAvailable = robot?.health?.logs != null - const [ - isDownloadingRobotLogs, - setIsDownloadingRobotLogs, - ] = React.useState(false) + const [isDownloadingRobotLogs, setIsDownloadingRobotLogs] = useState( + false + ) const { makeToast, eatToast } = useToaster() const toastIcon: IconProps = { name: 'ot-spinner', spin: true } const host = useHost() - const handleClick: React.MouseEventHandler = () => { + const handleClick: MouseEventHandler = () => { setIsDownloadingRobotLogs(true) const toastId = makeToast(t('downloading_logs') as string, INFO_TOAST, { disableTimeout: true, @@ -99,8 +99,8 @@ export function Troubleshooting({ } } - // set ref on component to check if component is mounted https://react.dev/reference/react/useRef#manipulating-the-dom-with-a-ref - const mounted = React.useRef(null) + // set ref on component to check if component is mounted https://dev/reference/react/useRef#manipulating-the-dom-with-a-ref + const mounted = useRef(null) return ( (null) + const inputRef = useRef(null) const dispatchStartRobotUpdate = useDispatchStartRobotUpdate() - const handleChange: React.ChangeEventHandler = event => { + const handleChange: ChangeEventHandler = event => { const { files } = event.target - if (files?.length === 1 && !updateDisabled) { - dispatchStartRobotUpdate(robotName, files[0].path) - onUpdateStart() - } - // this is to reset the state of the file picker so users can reselect the same - // system image if the upload fails - if (inputRef.current?.value != null) { - inputRef.current.value = '' + + if (files != null) { + void remote.getFilePathFrom(files[0]).then(filePath => { + if (files.length === 1 && !updateDisabled) { + dispatchStartRobotUpdate(robotName, filePath) + onUpdateStart() + } + // this is to reset the state of the file picker so users can reselect the same + // system image if the upload fails + if (inputRef.current?.value != null) { + inputRef.current.value = '' + } + }) } } - const handleClick: React.MouseEventHandler = () => { + const handleClick: MouseEventHandler = () => { inputRef.current?.click() } @@ -105,7 +112,7 @@ export function UpdateRobotSoftware({ {updateFromFileDisabledReason != null && ( - {updateFromFileDisabledReason} + {t(updateFromFileDisabledReason)} )} diff --git a/app/src/organisms/Desktop/Devices/RobotSettings/AdvancedTab/UsageSettings.tsx b/app/src/organisms/Desktop/Devices/RobotSettings/AdvancedTab/UsageSettings.tsx index e8843af6019..c44f573f7a2 100644 --- a/app/src/organisms/Desktop/Devices/RobotSettings/AdvancedTab/UsageSettings.tsx +++ b/app/src/organisms/Desktop/Devices/RobotSettings/AdvancedTab/UsageSettings.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { useDispatch } from 'react-redux' import { useTranslation } from 'react-i18next' @@ -15,6 +14,7 @@ import { import { ToggleButton } from '/app/atoms/buttons' import { updateSetting } from '/app/redux/robot-settings' +import type { MouseEventHandler } from 'react' import type { Dispatch } from '/app/redux/types' import type { RobotSettingsField } from '/app/redux/robot-settings/types' @@ -34,7 +34,7 @@ export function UsageSettings({ const value = settings?.value ? settings.value : false const id = settings?.id ? settings.id : 'enableDoorSafetySwitch' - const handleClick: React.MouseEventHandler = () => { + const handleClick: MouseEventHandler = () => { if (!isRobotBusy) { dispatch(updateSetting(robotName, id, !value)) } diff --git a/app/src/organisms/Desktop/Devices/RobotSettings/AdvancedTab/UseOlderAspirateBehavior.tsx b/app/src/organisms/Desktop/Devices/RobotSettings/AdvancedTab/UseOlderAspirateBehavior.tsx index c3496621f18..c12941a12a0 100644 --- a/app/src/organisms/Desktop/Devices/RobotSettings/AdvancedTab/UseOlderAspirateBehavior.tsx +++ b/app/src/organisms/Desktop/Devices/RobotSettings/AdvancedTab/UseOlderAspirateBehavior.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { useDispatch } from 'react-redux' import { useTranslation } from 'react-i18next' @@ -15,6 +14,7 @@ import { import { ToggleButton } from '/app/atoms/buttons' import { updateSetting } from '/app/redux/robot-settings' +import type { MouseEventHandler } from 'react' import type { Dispatch } from '/app/redux/types' import type { RobotSettingsField } from '/app/redux/robot-settings/types' @@ -34,7 +34,7 @@ export function UseOlderAspirateBehavior({ const value = settings?.value ? settings.value : false const id = settings?.id ? settings.id : 'useOldAspirationFunctions' - const handleClick: React.MouseEventHandler = () => { + const handleClick: MouseEventHandler = () => { if (!isRobotBusy) { dispatch(updateSetting(robotName, id, !value)) } diff --git a/app/src/organisms/Desktop/Devices/RobotSettings/AdvancedTab/__tests__/EnableErrorRecoveryMode.test.tsx b/app/src/organisms/Desktop/Devices/RobotSettings/AdvancedTab/__tests__/EnableErrorRecoveryMode.test.tsx index 9406e38f768..b3ff80c9341 100644 --- a/app/src/organisms/Desktop/Devices/RobotSettings/AdvancedTab/__tests__/EnableErrorRecoveryMode.test.tsx +++ b/app/src/organisms/Desktop/Devices/RobotSettings/AdvancedTab/__tests__/EnableErrorRecoveryMode.test.tsx @@ -5,21 +5,19 @@ import { renderWithProviders } from '/app/__testing-utils__' import { i18n } from '/app/i18n' import { useErrorRecoverySettingsToggle } from '/app/resources/errorRecovery' import { EnableErrorRecoveryMode } from '../EnableErrorRecoveryMode' -import type * as React from 'react' +import type { ComponentProps } from 'react' vi.mock('/app/resources/errorRecovery') const mockToggleERSettings = vi.fn() -const render = ( - props: React.ComponentProps -) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, }) } describe('EnableErrorRecoveryMode', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { isRobotBusy: false } diff --git a/app/src/organisms/Desktop/Devices/RobotSettings/AdvancedTab/__tests__/EnableStatusLight.test.tsx b/app/src/organisms/Desktop/Devices/RobotSettings/AdvancedTab/__tests__/EnableStatusLight.test.tsx index 2e2cc956bde..c36d79a04db 100644 --- a/app/src/organisms/Desktop/Devices/RobotSettings/AdvancedTab/__tests__/EnableStatusLight.test.tsx +++ b/app/src/organisms/Desktop/Devices/RobotSettings/AdvancedTab/__tests__/EnableStatusLight.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { fireEvent, screen } from '@testing-library/react' import { describe, it, vi, expect, beforeEach } from 'vitest' import '@testing-library/jest-dom/vitest' @@ -8,18 +7,20 @@ import { i18n } from '/app/i18n' import { useLEDLights } from '/app/resources/robot-settings' import { EnableStatusLight } from '../EnableStatusLight' +import type { ComponentProps } from 'react' + vi.mock('/app/resources/robot-settings') const ROBOT_NAME = 'otie' const mockToggleLights = vi.fn() -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, }) } describe('EnableStatusLight', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { diff --git a/app/src/organisms/Desktop/Devices/RobotSettings/AdvancedTab/__tests__/OpenJupyterControl.test.tsx b/app/src/organisms/Desktop/Devices/RobotSettings/AdvancedTab/__tests__/OpenJupyterControl.test.tsx index 57a01e25680..b6c5b2d9be4 100644 --- a/app/src/organisms/Desktop/Devices/RobotSettings/AdvancedTab/__tests__/OpenJupyterControl.test.tsx +++ b/app/src/organisms/Desktop/Devices/RobotSettings/AdvancedTab/__tests__/OpenJupyterControl.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { MemoryRouter } from 'react-router-dom' import { fireEvent, screen } from '@testing-library/react' import { describe, it, vi, beforeEach, expect } from 'vitest' @@ -8,6 +7,8 @@ import { i18n } from '/app/i18n' import { useTrackEvent, ANALYTICS_JUPYTER_OPEN } from '/app/redux/analytics' import { OpenJupyterControl } from '../OpenJupyterControl' +import type { ComponentProps } from 'react' + vi.mock('/app/redux/analytics') const mockIpAddress = '1.1.1.1' @@ -18,7 +19,7 @@ global.window = Object.create(window) Object.defineProperty(window, 'open', { writable: true, configurable: true }) window.open = vi.fn() -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders( @@ -28,7 +29,7 @@ const render = (props: React.ComponentProps) => { } describe('RobotSettings OpenJupyterControl', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { robotIp: mockIpAddress, diff --git a/app/src/organisms/Desktop/Devices/RobotSettings/AdvancedTab/__tests__/Troubleshooting.test.tsx b/app/src/organisms/Desktop/Devices/RobotSettings/AdvancedTab/__tests__/Troubleshooting.test.tsx index a5f28ee7da2..ef9cc7ff185 100644 --- a/app/src/organisms/Desktop/Devices/RobotSettings/AdvancedTab/__tests__/Troubleshooting.test.tsx +++ b/app/src/organisms/Desktop/Devices/RobotSettings/AdvancedTab/__tests__/Troubleshooting.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { MemoryRouter } from 'react-router-dom' import { act, waitFor, screen } from '@testing-library/react' import { when } from 'vitest-when' @@ -16,6 +15,7 @@ import { import { useRobot } from '/app/redux-resources/robots' import { Troubleshooting } from '../Troubleshooting' +import type { ComponentProps } from 'react' import type { HostConfig } from '@opentrons/api-client' import type { ToasterContextType } from '/app/organisms/ToasterOven/ToasterContext' @@ -29,7 +29,7 @@ const HOST_CONFIG: HostConfig = { hostname: 'localhost' } const MOCK_MAKE_TOAST = vi.fn() const MOCK_EAT_TOAST = vi.fn() -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders( @@ -39,7 +39,7 @@ const render = (props: React.ComponentProps) => { } describe('RobotSettings Troubleshooting', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { robotName: ROBOT_NAME, diff --git a/app/src/organisms/Desktop/Devices/RobotSettings/ConnectNetwork/ConnectModal/FormModal.tsx b/app/src/organisms/Desktop/Devices/RobotSettings/ConnectNetwork/ConnectModal/FormModal.tsx index 60ce3d2a88e..73f6004eb73 100644 --- a/app/src/organisms/Desktop/Devices/RobotSettings/ConnectNetwork/ConnectModal/FormModal.tsx +++ b/app/src/organisms/Desktop/Devices/RobotSettings/ConnectNetwork/ConnectModal/FormModal.tsx @@ -1,17 +1,17 @@ import { Controller } from 'react-hook-form' import styled, { css } from 'styled-components' - +import { useTranslation } from 'react-i18next' import { FONT_SIZE_BODY_1, BUTTON_TYPE_SUBMIT, Flex, } from '@opentrons/components' +import { SECURITY_WPA_PSK, SECURITY_WPA_EAP } from '/app/redux/networking' import { ScrollableAlertModal } from '/app/molecules/modals' import { TextField } from './TextField' import { KeyFileField } from './KeyFileField' import { SecurityField } from './SecurityField' import { FIELD_TYPE_KEY_FILE, FIELD_TYPE_SECURITY } from '../constants' -import * as Copy from '../i18n' import type { Control } from 'react-hook-form' import type { ConnectFormField, ConnectFormValues, WifiNetwork } from '../types' @@ -53,16 +53,23 @@ export interface FormModalProps { export const FormModal = (props: FormModalProps): JSX.Element => { const { id, network, fields, isValid, onCancel, control } = props + const { t } = useTranslation(['device_settings', 'shared']) const heading = network !== null - ? Copy.CONNECT_TO_SSID(network.ssid) - : Copy.FIND_AND_JOIN_A_NETWORK + ? t('connect_to_ssid', { ssid: network.ssid }) + : t('find_and_join_network') - const body = - network !== null - ? Copy.NETWORK_REQUIRES_SECURITY(network) - : Copy.ENTER_NAME_AND_SECURITY_TYPE + let bodyText = t('enter_name_security_type') + if (network != null) { + if (network.securityType === SECURITY_WPA_PSK) { + bodyText = t('network_requires_wpa_password', { ssid: network.ssid }) + } else if (network.securityType === SECURITY_WPA_EAP) { + bodyText = t('network_requires_auth', { ssid: network.ssid }) + } else { + bodyText = t('network_is_unsecured', { ssid: network.ssid }) + } + } return ( { iconName="wifi" onCloseClick={onCancel} buttons={[ - { children: Copy.CANCEL, onClick: props.onCancel }, + { children: t('shared:cancel'), onClick: props.onCancel }, { - children: Copy.CONNECT, + children: t('connect'), type: BUTTON_TYPE_SUBMIT, form: id, disabled: !isValid, }, ]} > - {body} + {bodyText} {fields.map(fieldProps => { const { name } = fieldProps diff --git a/app/src/organisms/Desktop/Devices/RobotSettings/ConnectNetwork/ConnectModal/FormRow.tsx b/app/src/organisms/Desktop/Devices/RobotSettings/ConnectNetwork/ConnectModal/FormRow.tsx index 1481d3f40f9..fddda5bc96a 100644 --- a/app/src/organisms/Desktop/Devices/RobotSettings/ConnectNetwork/ConnectModal/FormRow.tsx +++ b/app/src/organisms/Desktop/Devices/RobotSettings/ConnectNetwork/ConnectModal/FormRow.tsx @@ -1,12 +1,13 @@ // presentational components for the wifi connect form -import type * as React from 'react' import styled from 'styled-components' import { FONT_WEIGHT_SEMIBOLD, SPACING } from '@opentrons/components' +import type { ReactNode } from 'react' + export interface FormRowProps { label: string labelFor: string - children: React.ReactNode + children: ReactNode } const StyledRow = styled.div` diff --git a/app/src/organisms/Desktop/Devices/RobotSettings/ConnectNetwork/ConnectModal/KeyFileField.tsx b/app/src/organisms/Desktop/Devices/RobotSettings/ConnectNetwork/ConnectModal/KeyFileField.tsx index 376048ba420..b7ea68d5e36 100644 --- a/app/src/organisms/Desktop/Devices/RobotSettings/ConnectNetwork/ConnectModal/KeyFileField.tsx +++ b/app/src/organisms/Desktop/Devices/RobotSettings/ConnectNetwork/ConnectModal/KeyFileField.tsx @@ -1,9 +1,9 @@ import { useRef } from 'react' +import { useTranslation } from 'react-i18next' import { SelectField } from '@opentrons/components' import { FormRow } from './FormRow' import { UploadKeyInput } from './UploadKeyInput' -import { LABEL_ADD_NEW_KEY } from '../i18n' import { useConnectFormField } from './form-state' import type { WifiKey } from '../types' @@ -27,10 +27,6 @@ export interface KeyFileFieldProps { const ADD_NEW_KEY_VALUE = '__addNewKey__' -const ADD_NEW_KEY_OPTION_GROUP = { - options: [{ value: ADD_NEW_KEY_VALUE, label: LABEL_ADD_NEW_KEY }], -} - const makeKeyOptions = ( keys: WifiKey[] ): { options: Array<{ value: string; label: string }> } => ({ @@ -48,6 +44,11 @@ export const KeyFileField = (props: KeyFileFieldProps): JSX.Element => { field, fieldState, } = props + const { t } = useTranslation('device_settings') + const ADD_NEW_KEY_OPTION_GROUP = { + options: [{ value: ADD_NEW_KEY_VALUE, label: t('add_new') }], + } + const { value, error, setValue, setTouched } = useConnectFormField( field, fieldState @@ -81,7 +82,7 @@ export const KeyFileField = (props: KeyFileFieldProps): JSX.Element => { diff --git a/app/src/organisms/Desktop/Devices/RobotSettings/ConnectNetwork/ConnectModal/SecurityField.tsx b/app/src/organisms/Desktop/Devices/RobotSettings/ConnectNetwork/ConnectModal/SecurityField.tsx index c9fa4e0c069..cb7c2cbf615 100644 --- a/app/src/organisms/Desktop/Devices/RobotSettings/ConnectNetwork/ConnectModal/SecurityField.tsx +++ b/app/src/organisms/Desktop/Devices/RobotSettings/ConnectNetwork/ConnectModal/SecurityField.tsx @@ -1,7 +1,7 @@ +import { useTranslation } from 'react-i18next' import { SelectField } from '@opentrons/components' import { SECURITY_NONE, SECURITY_WPA_PSK } from '../constants' -import { LABEL_SECURITY_NONE, LABEL_SECURITY_PSK } from '../i18n' import { useConnectFormField } from './form-state' import { FormRow } from './FormRow' @@ -25,8 +25,8 @@ export interface SecurityFieldProps { } const ALL_SECURITY_OPTIONS = [ - { options: [{ value: SECURITY_NONE, label: LABEL_SECURITY_NONE }] }, - { options: [{ value: SECURITY_WPA_PSK, label: LABEL_SECURITY_PSK }] }, + { options: [{ value: SECURITY_NONE, label: 'shared:none' }] }, + { options: [{ value: SECURITY_WPA_PSK, label: 'wpa2_personal' }] }, ] const makeEapOptionsGroup = ( @@ -39,6 +39,7 @@ const makeEapOptionsGroup = ( }) export const SecurityField = (props: SecurityFieldProps): JSX.Element => { + const { t } = useTranslation(['device_settings', 'shared']) const { id, name, @@ -62,7 +63,7 @@ export const SecurityField = (props: SecurityFieldProps): JSX.Element => { ] return ( - + { + const { t } = useTranslation('device_settings') const { id, name, label, isPassword, className, field, fieldState } = props const { value, error, onChange, onBlur } = useConnectFormField( field, @@ -42,7 +43,7 @@ export const TextField = (props: TextFieldProps): JSX.Element => { /> {isPassword && ( diff --git a/app/src/organisms/Desktop/Devices/RobotSettings/ConnectNetwork/ConnectModal/UploadKeyInput.tsx b/app/src/organisms/Desktop/Devices/RobotSettings/ConnectNetwork/ConnectModal/UploadKeyInput.tsx index 6d450c336f2..4e79bba2cdc 100644 --- a/app/src/organisms/Desktop/Devices/RobotSettings/ConnectNetwork/ConnectModal/UploadKeyInput.tsx +++ b/app/src/organisms/Desktop/Devices/RobotSettings/ConnectNetwork/ConnectModal/UploadKeyInput.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { forwardRef, useEffect, useRef } from 'react' import styled from 'styled-components' import { useSelector } from 'react-redux' import last from 'lodash/last' @@ -6,6 +6,7 @@ import last from 'lodash/last' import { useDispatchApiRequest } from '/app/redux/robot-api' import { postWifiKeys, getWifiKeyByRequestId } from '/app/redux/networking' +import type { ChangeEventHandler, ForwardedRef } from 'react' import type { State } from '/app/redux/types' export interface UploadKeyInputProps { @@ -28,17 +29,17 @@ const HiddenInput = styled.input` const UploadKeyInputComponent = ( props: UploadKeyInputProps, - ref: React.ForwardedRef + ref: ForwardedRef ): JSX.Element => { const { robotName, label, onUpload } = props const [dispatchApi, requestIds] = useDispatchApiRequest() - const handleUpload = React.useRef<(key: string) => void>() + const handleUpload = useRef<(key: string) => void>() const createdKeyId = useSelector((state: State) => { return getWifiKeyByRequestId(state, robotName, last(requestIds) ?? null) })?.id - const handleFileInput: React.ChangeEventHandler = event => { + const handleFileInput: ChangeEventHandler = event => { if (event.target.files && event.target.files.length > 0) { const file = event.target.files[0] event.target.value = '' @@ -47,11 +48,11 @@ const UploadKeyInputComponent = ( } } - React.useEffect(() => { + useEffect(() => { handleUpload.current = onUpload }, [onUpload]) - React.useEffect(() => { + useEffect(() => { if (createdKeyId != null && handleUpload.current) { handleUpload.current(createdKeyId) } @@ -67,7 +68,6 @@ const UploadKeyInputComponent = ( ) } -export const UploadKeyInput = React.forwardRef< - HTMLInputElement, - UploadKeyInputProps ->(UploadKeyInputComponent) +export const UploadKeyInput = forwardRef( + UploadKeyInputComponent +) diff --git a/app/src/organisms/Desktop/Devices/RobotSettings/ConnectNetwork/ConnectModal/__tests__/form-fields.test.ts b/app/src/organisms/Desktop/Devices/RobotSettings/ConnectNetwork/ConnectModal/__tests__/form-fields.test.ts deleted file mode 100644 index 80336fb0139..00000000000 --- a/app/src/organisms/Desktop/Devices/RobotSettings/ConnectNetwork/ConnectModal/__tests__/form-fields.test.ts +++ /dev/null @@ -1,379 +0,0 @@ -import * as Fixtures from '/app/redux/networking/__fixtures__' -import { describe, it, expect } from 'vitest' - -import { - CONFIGURE_FIELD_SSID, - CONFIGURE_FIELD_PSK, - CONFIGURE_FIELD_SECURITY_TYPE, - SECURITY_WPA_EAP, - SECURITY_WPA_PSK, - SECURITY_NONE, -} from '/app/redux/networking' - -import { - FIELD_TYPE_TEXT, - FIELD_TYPE_KEY_FILE, - FIELD_TYPE_SECURITY, -} from '../../constants' - -import { - LABEL_SECURITY, - LABEL_SSID, - LABEL_PSK, - SELECT_AUTHENTICATION_METHOD, - SELECT_FILE, -} from '../../i18n' - -import { - getConnectFormFields, - validateConnectFormFields, - connectFormToConfigureRequest, -} from '../form-fields' - -describe('getConnectFormFields', () => { - it('should add a string field for SSID if network is unknown', () => { - const fields = getConnectFormFields(null, 'robot-name', [], [], {}) - - expect(fields).toContainEqual({ - type: FIELD_TYPE_TEXT, - name: CONFIGURE_FIELD_SSID, - label: `* ${LABEL_SSID}`, - isPassword: false, - }) - }) - - it('should add a security dropdown field if network is unknown', () => { - const eapOptions = [Fixtures.mockEapOption] - const fields = getConnectFormFields(null, 'robot-name', eapOptions, [], {}) - - expect(fields).toContainEqual({ - type: FIELD_TYPE_SECURITY, - name: CONFIGURE_FIELD_SECURITY_TYPE, - label: `* ${LABEL_SECURITY}`, - eapOptions, - showAllOptions: true, - placeholder: SELECT_AUTHENTICATION_METHOD, - }) - }) - - it('should add a security dropdown field if known network has EAP security', () => { - const eapOptions = [Fixtures.mockEapOption] - const network = { - ...Fixtures.mockWifiNetwork, - securityType: SECURITY_WPA_EAP, - } - const fields = getConnectFormFields( - network, - 'robot-name', - eapOptions, - [], - {} - ) - - expect(fields).toContainEqual({ - type: FIELD_TYPE_SECURITY, - name: CONFIGURE_FIELD_SECURITY_TYPE, - label: `* ${LABEL_SECURITY}`, - eapOptions, - showAllOptions: false, - placeholder: SELECT_AUTHENTICATION_METHOD, - }) - }) - - it('should add a password field for PSK if known network as PSK security', () => { - const network = { - ...Fixtures.mockWifiNetwork, - securityType: SECURITY_WPA_PSK, - } - const fields = getConnectFormFields(network, 'robot-name', [], [], {}) - - expect(fields).toContainEqual({ - type: FIELD_TYPE_TEXT, - name: CONFIGURE_FIELD_PSK, - label: `* ${LABEL_PSK}`, - isPassword: true, - }) - }) - - it('should add a password field for PSK if unknown network and user selects PSK', () => { - const fields = getConnectFormFields(null, 'robot-name', [], [], { - securityType: SECURITY_WPA_PSK, - }) - - expect(fields).toContainEqual({ - type: FIELD_TYPE_TEXT, - name: CONFIGURE_FIELD_PSK, - label: `* ${LABEL_PSK}`, - isPassword: true, - }) - }) - - it('should add EAP options based on the selected eapType if network is unknown', () => { - const eapOptions = [ - { ...Fixtures.mockEapOption, name: 'someEapType', options: [] }, - { ...Fixtures.mockEapOption, name: 'someOtherEapType' }, - ] - const wifiKeys = [Fixtures.mockWifiKey] - const fields = getConnectFormFields( - null, - 'robot-name', - eapOptions, - wifiKeys, - { - securityType: 'someOtherEapType', - } - ) - - expect(fields).toEqual( - expect.arrayContaining([ - { - type: FIELD_TYPE_TEXT, - name: 'eapConfig.stringField', - label: '* String Field', - isPassword: false, - }, - { - type: FIELD_TYPE_TEXT, - name: 'eapConfig.passwordField', - label: 'Password Field', - isPassword: true, - }, - { - type: FIELD_TYPE_KEY_FILE, - name: 'eapConfig.fileField', - label: '* File Field', - robotName: 'robot-name', - wifiKeys, - placeholder: SELECT_FILE, - }, - ]) - ) - }) - - it('should add EAP options based on the selected eapType if network is EAP', () => { - const network = { - ...Fixtures.mockWifiNetwork, - securityType: SECURITY_WPA_EAP, - } - const eapOptions = [ - { ...Fixtures.mockEapOption, name: 'someEapType' }, - { ...Fixtures.mockEapOption, name: 'someOtherEapType', options: [] }, - ] - const wifiKeys = [Fixtures.mockWifiKey] - const fields = getConnectFormFields( - network, - 'robot-name', - eapOptions, - wifiKeys, - { securityType: 'someEapType' } - ) - - expect(fields).toEqual( - expect.arrayContaining([ - { - type: FIELD_TYPE_TEXT, - name: 'eapConfig.stringField', - label: '* String Field', - isPassword: false, - }, - { - type: FIELD_TYPE_TEXT, - name: 'eapConfig.passwordField', - label: 'Password Field', - isPassword: true, - }, - { - type: FIELD_TYPE_KEY_FILE, - name: 'eapConfig.fileField', - label: '* File Field', - robotName: 'robot-name', - wifiKeys, - placeholder: SELECT_FILE, - }, - ]) - ) - }) -}) - -describe('validateConnectFormFields', () => { - it('should error if network is hidden and ssid is blank', () => { - const errors = validateConnectFormFields( - null, - [], - { - securityType: SECURITY_WPA_PSK, - psk: '12345678', - }, - {} - ) - - expect(errors).toEqual({ - ssid: { message: `${LABEL_SSID} is required`, type: 'ssidError' }, - }) - }) - - it('should error if network is hidden and securityType is blank', () => { - const errors = validateConnectFormFields(null, [], { ssid: 'foobar' }, {}) - - expect(errors).toEqual({ - securityType: { - message: `${LABEL_SECURITY} is required`, - type: 'securityTypeError', - }, - }) - }) - - it('should error if network is PSK and psk is blank', () => { - const network = { - ...Fixtures.mockWifiNetwork, - securityType: SECURITY_WPA_PSK, - } - const errors = validateConnectFormFields(network, [], { psk: '' }, {}) - - expect(errors).toEqual({ - psk: { - message: `${LABEL_PSK} must be at least 8 characters`, - type: 'pskError', - }, - }) - }) - - it('should error if selected security is PSK and psk is blank', () => { - const values = { ssid: 'foobar', securityType: SECURITY_WPA_PSK } - const errors = validateConnectFormFields(null, [], values, {}) - - expect(errors).toEqual({ - psk: { - message: `${LABEL_PSK} must be at least 8 characters`, - type: 'pskError', - }, - }) - }) - - it('should error if network is EAP and securityType is blank', () => { - const network = { - ...Fixtures.mockWifiNetwork, - securityType: SECURITY_WPA_EAP, - } - const errors = validateConnectFormFields(network, [], {}, {}) - - expect(errors).toEqual({ - securityType: { - message: `${LABEL_SECURITY} is required`, - type: 'securityTypeError', - }, - }) - }) - - it('should error if any required EAP fields are missing', () => { - const network = { - ...Fixtures.mockWifiNetwork, - securityType: SECURITY_WPA_EAP, - } - const eapOptions = [ - { ...Fixtures.mockEapOption, name: 'someEapType', options: [] }, - { ...Fixtures.mockEapOption, name: 'someOtherEapType' }, - ] - const values = { - securityType: 'someOtherEapType', - eapConfig: { fileField: '123' }, - } - const errors = validateConnectFormFields(network, eapOptions, values, {}) - - expect(errors).toEqual({ - 'eapConfig.stringField': { - message: `String Field is required`, - type: 'eapError', - }, - }) - }) -}) - -describe('connectFormToConfigureRequest', () => { - it('should return null if unknown network and no ssid', () => { - const values = { securityType: SECURITY_NONE } - const result = connectFormToConfigureRequest(null, values) - - expect(result).toEqual(null) - }) - - it('should set ssid and securityType from values if unknown network', () => { - const values = { ssid: 'foobar', securityType: SECURITY_NONE } - const result = connectFormToConfigureRequest(null, values) - - expect(result).toEqual({ - ssid: 'foobar', - securityType: SECURITY_NONE, - hidden: true, - }) - }) - - it('should set ssid from network if known', () => { - const network = { - ...Fixtures.mockWifiNetwork, - ssid: 'foobar', - securityType: SECURITY_NONE, - } - const values = {} - const result = connectFormToConfigureRequest(network, values) - - expect(result).toEqual({ - ssid: 'foobar', - securityType: SECURITY_NONE, - hidden: false, - }) - }) - - it('should set psk from values', () => { - const network = { - ...Fixtures.mockWifiNetwork, - ssid: 'foobar', - securityType: SECURITY_WPA_PSK, - } - const values = { psk: '12345678' } - const result = connectFormToConfigureRequest(network, values) - - expect(result).toEqual({ - ssid: 'foobar', - securityType: SECURITY_WPA_PSK, - hidden: false, - psk: '12345678', - }) - }) - - it('should set eapConfig from values with known network', () => { - const network = { - ...Fixtures.mockWifiNetwork, - ssid: 'foobar', - securityType: SECURITY_WPA_EAP, - } - const values = { - securityType: 'someEapType', - eapConfig: { option1: 'fizzbuzz' }, - } - const result = connectFormToConfigureRequest(network, values) - - expect(result).toEqual({ - ssid: 'foobar', - securityType: SECURITY_WPA_EAP, - hidden: false, - eapConfig: { eapType: 'someEapType', option1: 'fizzbuzz' }, - }) - }) - - it('should set eapConfig from values with unknown network', () => { - const values = { - ssid: 'foobar', - securityType: 'someEapType', - eapConfig: { option1: 'fizzbuzz' }, - } - const result = connectFormToConfigureRequest(null, values) - - expect(result).toEqual({ - ssid: 'foobar', - securityType: SECURITY_WPA_EAP, - hidden: true, - eapConfig: { eapType: 'someEapType', option1: 'fizzbuzz' }, - }) - }) -}) diff --git a/app/src/organisms/Desktop/Devices/RobotSettings/ConnectNetwork/ConnectModal/__tests__/form-fields.test.tsx b/app/src/organisms/Desktop/Devices/RobotSettings/ConnectNetwork/ConnectModal/__tests__/form-fields.test.tsx new file mode 100644 index 00000000000..638d9f1b76c --- /dev/null +++ b/app/src/organisms/Desktop/Devices/RobotSettings/ConnectNetwork/ConnectModal/__tests__/form-fields.test.tsx @@ -0,0 +1,422 @@ +import * as Fixtures from '/app/redux/networking/__fixtures__' +import { describe, it, expect } from 'vitest' +import { screen } from '@testing-library/react' +import { useTranslation } from 'react-i18next' + +import { + SECURITY_WPA_EAP, + SECURITY_WPA_PSK, + SECURITY_NONE, +} from '/app/redux/networking' +import { renderWithProviders } from '/app/__testing-utils__' +import { i18n } from '/app/i18n' + +import { + getConnectFormFields, + validateConnectFormFields, + connectFormToConfigureRequest, +} from '../form-fields' + +import type { FieldError } from 'react-hook-form' +import type { + WifiNetwork, + WifiKey, + EapOption, + ConnectFormValues, +} from '../../types' +import type { ComponentProps } from 'react' + +const TestWrapperConnectFormFields = ({ + network, + robotName, + eapOptions, + wifiKeys, + values, +}: { + network: WifiNetwork | null + robotName: string + eapOptions: EapOption[] + wifiKeys: WifiKey[] + values: ConnectFormValues +}) => { + const { t } = useTranslation('device_settings') + const fields = getConnectFormFields( + network, + robotName, + eapOptions, + wifiKeys, + values, + t + ) + return
        {JSON.stringify(fields)}
        +} + +const renderConnectFormFields = ( + props: ComponentProps +) => { + return renderWithProviders(, { + i18nInstance: i18n, + })[0] +} + +describe('getConnectFormFields', () => { + it('should add a string field for SSID if network is unknown', () => { + const props = { + network: null, + robotName: 'robot-name', + eapOptions: [], + wifiKeys: [], + values: {}, + } + renderConnectFormFields(props) + screen.getByText(/text/) + screen.getByText(/ssid/) + screen.getByText(/ * Network Name/) + }) + + it('should add a security dropdown field if network is unknown', () => { + const props = { + network: null, + robotName: 'robot-name', + eapOptions: [Fixtures.mockEapOption], + wifiKeys: [], + values: {}, + } + renderConnectFormFields(props) + screen.getByText(/security/) + screen.getByText(/ * Authentication/) + screen.getByText(/Select authentication method/) + }) + + it('should add a security dropdown field if known network has EAP security', () => { + const network = { + ...Fixtures.mockWifiNetwork, + securityType: SECURITY_WPA_EAP, + } + const props = { + network: network, + robotName: 'robot-name', + eapOptions: [Fixtures.mockEapOption], + wifiKeys: [], + values: {}, + } + renderConnectFormFields(props) + + screen.getByText(/security/) + screen.getByText(/ * Authentication/) + screen.getByText(/Select authentication method/) + screen.getByText(/EAP Option/) + screen.getByText(/String Field/) + screen.getByText(/Password Field/) + screen.getByText(/File Field/) + }) + + it('should add a password field for PSK if known network as PSK security', () => { + const network = { + ...Fixtures.mockWifiNetwork, + securityType: SECURITY_WPA_PSK, + } + const props = { + network: network, + robotName: 'robot-name', + eapOptions: [], + wifiKeys: [], + values: {}, + } + renderConnectFormFields(props) + + screen.getByText(/psk/) + screen.getByText(/ * Password/) + }) + + it('should add a password field for PSK if unknown network and user selects PSK', () => { + const props = { + network: null, + robotName: 'robot-name', + eapOptions: [], + wifiKeys: [], + values: { securityType: SECURITY_WPA_PSK }, + } + + renderConnectFormFields(props) + screen.getByText(/psk/) + screen.getByText(/ * Password/) + }) + + it('should add EAP options based on the selected eapType if network is unknown', () => { + const eapOptions = [ + { ...Fixtures.mockEapOption, name: 'someEapType', options: [] }, + { ...Fixtures.mockEapOption, name: 'someOtherEapType' }, + ] + const wifiKeys = [Fixtures.mockWifiKey] + const props = { + network: null, + robotName: 'robot-name', + eapOptions: eapOptions, + wifiKeys: wifiKeys, + values: {}, + } + renderConnectFormFields(props) + + screen.getByText(/someEapType/) + screen.getByText(/someOtherEapType/) + screen.getByText(/stringField/) + screen.getByText(/String Field/) + screen.getByText(/passwordField/) + screen.getByText(/Password Field/) + screen.getByText(/fileField/) + screen.getByText(/File Field/) + }) + + it('should add EAP options based on the selected eapType if network is EAP', () => { + const network = { + ...Fixtures.mockWifiNetwork, + securityType: SECURITY_WPA_EAP, + } + const eapOptions = [ + { ...Fixtures.mockEapOption, name: 'someEapType' }, + { ...Fixtures.mockEapOption, name: 'someOtherEapType', options: [] }, + ] + const wifiKeys = [Fixtures.mockWifiKey] + const props = { + network: network, + robotName: 'robot-name', + eapOptions: eapOptions, + wifiKeys: wifiKeys, + values: { securityType: 'someEapType' }, + } + renderConnectFormFields(props) + screen.getByText(/ * String Field/) + screen.getByText(/Password Field/) + screen.getByText(/ * File Field/) + }) +}) + +const TestWrapperValidateFormFields = ({ + network, + eapOptions, + values, + errors, +}: { + network: WifiNetwork | null + eapOptions: EapOption[] + values: ConnectFormValues + errors: Record +}) => { + const { t } = useTranslation('device_settings') + const validationErrors = validateConnectFormFields( + network, + eapOptions, + values, + errors, + t + ) + return
        {JSON.stringify(validationErrors)}
        +} + +const renderValidateFormFields = ( + props: ComponentProps +) => { + return renderWithProviders(, { + i18nInstance: i18n, + })[0] +} + +describe('validateConnectFormFields', () => { + it('should error if network is hidden and ssid is blank', () => { + const props = { + network: null, + eapOptions: [], + values: { + securityType: SECURITY_WPA_PSK, + psk: '12345678', + }, + errors: {}, + } + renderValidateFormFields(props) + screen.getByText(/ssid/) + screen.getByText(/ssidError/) + screen.getByText(/Network Name is required/) + }) + + it('should error if network is hidden and securityType is blank', () => { + const props = { + network: null, + eapOptions: [], + values: { + ssid: 'foobar', + }, + errors: {}, + } + renderValidateFormFields(props) + screen.getByText(/securityType/) + screen.getByText(/securityTypeError/) + screen.getByText(/Authentication is required/) + }) + + it('should error if network is PSK and psk is blank', () => { + const network = { + ...Fixtures.mockWifiNetwork, + securityType: SECURITY_WPA_PSK, + } + const props = { + network: network, + eapOptions: [], + values: { + psk: '', + }, + errors: {}, + } + renderValidateFormFields(props) + screen.getByText(/psk/) + screen.getByText(/pskError/) + screen.getByText(/Password must be at least 8 characters/) + }) + + it('should error if selected security is PSK and psk is blank', () => { + const values = { ssid: 'foobar', securityType: SECURITY_WPA_PSK } + const props = { + network: null, + eapOptions: [], + values: values, + errors: {}, + } + renderValidateFormFields(props) + screen.getByText(/psk/) + screen.getByText(/pskError/) + screen.getByText(/Password must be at least 8 characters/) + }) + + it('should error if network is EAP and securityType is blank', () => { + const network = { + ...Fixtures.mockWifiNetwork, + securityType: SECURITY_WPA_EAP, + } + const props = { + network: network, + eapOptions: [], + values: {}, + errors: {}, + } + + renderValidateFormFields(props) + screen.getByText(/securityType/) + screen.getByText(/securityTypeError/) + screen.getByText(/Authentication is required/) + }) + + it('should error if any required EAP fields are missing', () => { + const network = { + ...Fixtures.mockWifiNetwork, + securityType: SECURITY_WPA_EAP, + } + const eapOptions = [ + { ...Fixtures.mockEapOption, name: 'someEapType', options: [] }, + { ...Fixtures.mockEapOption, name: 'someOtherEapType' }, + ] + const values = { + securityType: 'someOtherEapType', + eapConfig: { fileField: '123' }, + } + const props = { + network: network, + eapOptions: eapOptions, + values: values, + errors: {}, + } + + renderValidateFormFields(props) + screen.getByText(/eapConfig.stringField/) + screen.getByText(/eapError/) + screen.getByText(/String Field is required/) + }) +}) + +describe('connectFormToConfigureRequest', () => { + it('should return null if unknown network and no ssid', () => { + const values = { securityType: SECURITY_NONE } + const result = connectFormToConfigureRequest(null, values) + + expect(result).toEqual(null) + }) + + it('should set ssid and securityType from values if unknown network', () => { + const values = { ssid: 'foobar', securityType: SECURITY_NONE } + const result = connectFormToConfigureRequest(null, values) + + expect(result).toEqual({ + ssid: 'foobar', + securityType: SECURITY_NONE, + hidden: true, + }) + }) + + it('should set ssid from network if known', () => { + const network = { + ...Fixtures.mockWifiNetwork, + ssid: 'foobar', + securityType: SECURITY_NONE, + } + const values = {} + const result = connectFormToConfigureRequest(network, values) + + expect(result).toEqual({ + ssid: 'foobar', + securityType: SECURITY_NONE, + hidden: false, + }) + }) + + it('should set psk from values', () => { + const network = { + ...Fixtures.mockWifiNetwork, + ssid: 'foobar', + securityType: SECURITY_WPA_PSK, + } + const values = { psk: '12345678' } + const result = connectFormToConfigureRequest(network, values) + + expect(result).toEqual({ + ssid: 'foobar', + securityType: SECURITY_WPA_PSK, + hidden: false, + psk: '12345678', + }) + }) + + it('should set eapConfig from values with known network', () => { + const network = { + ...Fixtures.mockWifiNetwork, + ssid: 'foobar', + securityType: SECURITY_WPA_EAP, + } + const values = { + securityType: 'someEapType', + eapConfig: { option1: 'fizzbuzz' }, + } + const result = connectFormToConfigureRequest(network, values) + + expect(result).toEqual({ + ssid: 'foobar', + securityType: SECURITY_WPA_EAP, + hidden: false, + eapConfig: { eapType: 'someEapType', option1: 'fizzbuzz' }, + }) + }) + + it('should set eapConfig from values with unknown network', () => { + const values = { + ssid: 'foobar', + securityType: 'someEapType', + eapConfig: { option1: 'fizzbuzz' }, + } + const result = connectFormToConfigureRequest(null, values) + + expect(result).toEqual({ + ssid: 'foobar', + securityType: SECURITY_WPA_EAP, + hidden: true, + eapConfig: { eapType: 'someEapType', option1: 'fizzbuzz' }, + }) + }) +}) diff --git a/app/src/organisms/Desktop/Devices/RobotSettings/ConnectNetwork/ConnectModal/form-fields.ts b/app/src/organisms/Desktop/Devices/RobotSettings/ConnectNetwork/ConnectModal/form-fields.ts index 1a91d2ac994..b8caeb3824c 100644 --- a/app/src/organisms/Desktop/Devices/RobotSettings/ConnectNetwork/ConnectModal/form-fields.ts +++ b/app/src/organisms/Desktop/Devices/RobotSettings/ConnectNetwork/ConnectModal/form-fields.ts @@ -1,9 +1,9 @@ import get from 'lodash/get' import * as Constants from '../constants' -import * as Copy from '../i18n' import type { FieldError } from 'react-hook-form' +import type { TFunction } from 'i18next' import type { WifiNetwork, WifiKey, @@ -24,28 +24,29 @@ type Errors = Record export const renderLabel = (label: string, required: boolean): string => `${required ? '* ' : ''}${label}` -const FIELD_SSID: ConnectFormTextField = { +const makeFieldSsid = (t: TFunction): ConnectFormTextField => ({ type: Constants.FIELD_TYPE_TEXT, name: Constants.CONFIGURE_FIELD_SSID, - label: renderLabel(Copy.LABEL_SSID, true), + label: renderLabel(t('network_name'), true), isPassword: false, -} +}) -const FIELD_PSK: ConnectFormTextField = { +const makeFieldPsk = (t: TFunction): ConnectFormTextField => ({ type: Constants.FIELD_TYPE_TEXT, name: Constants.CONFIGURE_FIELD_PSK, - label: renderLabel(Copy.LABEL_PSK, true), + label: renderLabel(t('password'), true), isPassword: true, -} +}) const makeSecurityField = ( eapOptions: EapOption[], - showAllOptions: boolean + showAllOptions: boolean, + t: TFunction ): ConnectFormSecurityField => ({ type: Constants.FIELD_TYPE_SECURITY, name: Constants.CONFIGURE_FIELD_SECURITY_TYPE, - label: renderLabel(Copy.LABEL_SECURITY, true), - placeholder: Copy.SELECT_AUTHENTICATION_METHOD, + label: renderLabel(t('authentication'), true), + placeholder: t('select_auth_method_short'), eapOptions, showAllOptions, }) @@ -77,21 +78,22 @@ export function getConnectFormFields( robotName: string, eapOptions: EapOption[], wifiKeys: WifiKey[], - values: ConnectFormValues + values: ConnectFormValues, + t: TFunction ): ConnectFormField[] { const { securityType: formSecurityType } = values const fields = [] // if the network is unknown, display a field to enter the SSID if (network === null) { - fields.push(FIELD_SSID) + fields.push(makeFieldSsid(t)) } // if the network is unknown or the known network is EAP, display a // security dropdown; security dropdown will handle which options to // display based on known or unknown network if (!network || network.securityType === Constants.SECURITY_WPA_EAP) { - fields.push(makeSecurityField(eapOptions, !network)) + fields.push(makeSecurityField(eapOptions, !network, t)) } // if known network is PSK or network is unknown and user has selected PSK @@ -100,7 +102,7 @@ export function getConnectFormFields( network?.securityType === Constants.SECURITY_WPA_PSK || formSecurityType === Constants.SECURITY_WPA_PSK ) { - fields.push(FIELD_PSK) + fields.push(makeFieldPsk(t)) } // if known network is EAP or user selected EAP, map eap options to fields @@ -121,7 +123,7 @@ export function getConnectFormFields( label, robotName, wifiKeys, - placeholder: Copy.SELECT_FILE, + placeholder: t('select_file'), } } @@ -142,7 +144,8 @@ export function validateConnectFormFields( network: WifiNetwork | null, eapOptions: EapOption[], values: ConnectFormValues, - errors: Errors + errors: Errors, + t: TFunction ): Errors { const { ssid: formSsid, @@ -152,7 +155,7 @@ export function validateConnectFormFields( let errorMessage: string | undefined if (network === null && (formSsid == null || formSsid.length === 0)) { - errorMessage = Copy.FIELD_IS_REQUIRED(Copy.LABEL_SSID) + errorMessage = t('field_is_required', { field: t('network_name') }) return errorMessage != null ? { ...errors, @@ -168,7 +171,7 @@ export function validateConnectFormFields( (network === null || network.securityType === Constants.SECURITY_WPA_EAP) && !formSecurityType ) { - errorMessage = Copy.FIELD_IS_REQUIRED(Copy.LABEL_SECURITY) + errorMessage = t('field_is_required', { field: t('authentication') }) return errorMessage != null ? { ...errors, @@ -185,10 +188,9 @@ export function validateConnectFormFields( formSecurityType === Constants.SECURITY_WPA_PSK) && (!formPsk || formPsk.length < Constants.CONFIGURE_PSK_MIN_LENGTH) ) { - errorMessage = Copy.FIELD_NOT_LONG_ENOUGH( - Copy.LABEL_PSK, - Constants.CONFIGURE_PSK_MIN_LENGTH - ) + errorMessage = t('password_not_long_enough', { + minLength: Constants.CONFIGURE_PSK_MIN_LENGTH, + }) return errorMessage != null ? { ...errors, @@ -215,7 +217,9 @@ export function validateConnectFormFields( ) => { const fieldName = getEapFieldName(name) const errorMessage = - displayName != null ? Copy.FIELD_IS_REQUIRED(displayName) : '' + displayName != null + ? t('field_is_required', { field: displayName }) + : '' if (errorMessage != null) { acc[fieldName] = { diff --git a/app/src/organisms/Desktop/Devices/RobotSettings/ConnectNetwork/ConnectModal/index.tsx b/app/src/organisms/Desktop/Devices/RobotSettings/ConnectNetwork/ConnectModal/index.tsx index 3e1c731d33e..2b5d228c12b 100644 --- a/app/src/organisms/Desktop/Devices/RobotSettings/ConnectNetwork/ConnectModal/index.tsx +++ b/app/src/organisms/Desktop/Devices/RobotSettings/ConnectNetwork/ConnectModal/index.tsx @@ -1,4 +1,5 @@ import { useForm } from 'react-hook-form' +import { useTranslation } from 'react-i18next' import { useResetFormOnSecurityChange } from './form-state' import { @@ -10,6 +11,7 @@ import { import { FormModal } from './FormModal' import type { Control, Resolver } from 'react-hook-form' +import type { TFunction } from 'i18next' import type { ConnectFormValues, WifiConfigureRequest, @@ -35,6 +37,7 @@ interface ConnectModalComponentProps extends ConnectModalProps { } export const ConnectModal = (props: ConnectModalProps): JSX.Element => { + const { t } = useTranslation(['device_settings', 'shared']) const { network, eapOptions, onConnect } = props const onSubmit = (values: ConnectFormValues): void => { @@ -45,7 +48,13 @@ export const ConnectModal = (props: ConnectModalProps): JSX.Element => { const handleValidate: Resolver = values => { let errors = {} - errors = validateConnectFormFields(network, eapOptions, values, errors) + errors = validateConnectFormFields( + network, + eapOptions, + values, + errors, + t as TFunction + ) return { values, errors } } @@ -78,6 +87,7 @@ export const ConnectModal = (props: ConnectModalProps): JSX.Element => { export const ConnectModalComponent = ( props: ConnectModalComponentProps ): JSX.Element => { + const { t } = useTranslation(['device_settings', 'shared']) const { robotName, network, @@ -95,7 +105,8 @@ export const ConnectModalComponent = ( robotName, eapOptions, wifiKeys, - values + values, + t as TFunction ) useResetFormOnSecurityChange() diff --git a/app/src/organisms/Desktop/Devices/RobotSettings/ConnectNetwork/ResultModal.tsx b/app/src/organisms/Desktop/Devices/RobotSettings/ConnectNetwork/ResultModal.tsx index 6628c35dfc5..3a372c6df66 100644 --- a/app/src/organisms/Desktop/Devices/RobotSettings/ConnectNetwork/ResultModal.tsx +++ b/app/src/organisms/Desktop/Devices/RobotSettings/ConnectNetwork/ResultModal.tsx @@ -1,6 +1,6 @@ +import { useTranslation } from 'react-i18next' import { AlertModal, SpinnerModal } from '@opentrons/components' -import * as Copy from './i18n' import { ErrorModal } from '/app/molecules/modals' import { DISCONNECT } from './constants' import { PENDING, FAILURE } from '/app/redux/robot-api' @@ -18,29 +18,32 @@ export interface ResultModalProps { export const ResultModal = (props: ResultModalProps): JSX.Element => { const { type, ssid, requestStatus, error, onClose } = props + const { t } = useTranslation(['device_settings', 'shared']) const isDisconnect = type === DISCONNECT if (requestStatus === PENDING) { const message = isDisconnect - ? Copy.DISCONNECTING_FROM_NETWORK(ssid) - : Copy.CONNECTING_TO_NETWORK(ssid) + ? t('disconnecting_from_wifi_network', { ssid: ssid }) + : t('connecting_to_wifi_network', { ssid: ssid }) return } if (error || requestStatus === FAILURE) { const heading = isDisconnect - ? Copy.UNABLE_TO_DISCONNECT - : Copy.UNABLE_TO_CONNECT + ? t('unable_to_disconnect') + : t('unable_to_connect') const message = isDisconnect - ? Copy.YOUR_ROBOT_WAS_UNABLE_TO_DISCONNECT(ssid) - : Copy.YOUR_ROBOT_WAS_UNABLE_TO_CONNECT(ssid) + ? t('disconnect_from_wifi_network_failure', { ssid: ssid }) + : t('connect_to_wifi_network_failure', { ssid: ssid }) - const retryMessage = !isDisconnect ? ` ${Copy.CHECK_YOUR_CREDENTIALS}.` : '' + const retryMessage = !isDisconnect ? t('please_check_credentials') : '' const placeholderError = { - message: `Likely incorrect network password. ${Copy.CHECK_YOUR_CREDENTIALS}.`, + message: `${t('likely_incorrect_password')} ${t( + 'please_check_credentials' + )}.`, } return ( @@ -54,12 +57,12 @@ export const ResultModal = (props: ResultModalProps): JSX.Element => { } const heading = isDisconnect - ? Copy.SUCCESSFULLY_DISCONNECTED - : Copy.SUCCESSFULLY_CONNECTED + ? t('successfully_disconnected') + : t('successfully_connected_to_wifi') const message = isDisconnect - ? Copy.YOUR_ROBOT_HAS_DISCONNECTED(ssid) - : Copy.YOUR_ROBOT_HAS_CONNECTED(ssid) + ? t('disconnect_from_wifi_network_success') + : t('successfully_connected_to_ssid', { ssid: ssid }) return ( { iconName="wifi" heading={heading} onCloseClick={props.onClose} - buttons={[{ children: Copy.CLOSE, onClick: onClose }]} + buttons={[{ children: t('shared:close'), onClick: onClose }]} > {message} diff --git a/app/src/organisms/Desktop/Devices/RobotSettings/ConnectNetwork/SelectSsid/index.tsx b/app/src/organisms/Desktop/Devices/RobotSettings/ConnectNetwork/SelectSsid/index.tsx index b85cc72d563..02de4cfabaa 100644 --- a/app/src/organisms/Desktop/Devices/RobotSettings/ConnectNetwork/SelectSsid/index.tsx +++ b/app/src/organisms/Desktop/Devices/RobotSettings/ConnectNetwork/SelectSsid/index.tsx @@ -1,11 +1,11 @@ -import type * as React from 'react' +import { useTranslation } from 'react-i18next' import { CONTEXT_MENU } from '@opentrons/components' import { SelectField } from '/app/atoms/SelectField' -import * as Copy from '../i18n' import { NetworkOptionLabel, NetworkActionLabel } from './NetworkOptionLabel' +import type { ComponentProps } from 'react' +import type { TFunction } from 'i18next' import type { SelectOptionOrGroup } from '@opentrons/components' - import type { WifiNetwork } from '../types' export interface SelectSsidProps { @@ -20,11 +20,16 @@ const FIELD_NAME = '__SelectSsid__' const JOIN_OTHER_VALUE = '__join-other-network__' -const SELECT_JOIN_OTHER_GROUP = { - options: [{ value: JOIN_OTHER_VALUE, label: Copy.LABEL_JOIN_OTHER_NETWORK }], -} +const formatOptions = ( + list: WifiNetwork[], + t: TFunction +): SelectOptionOrGroup[] => { + const SELECT_JOIN_OTHER_GROUP = { + options: [ + { value: JOIN_OTHER_VALUE, label: `${t('join_other_network')}...` }, + ], + } -const formatOptions = (list: WifiNetwork[]): SelectOptionOrGroup[] => { const ssidOptionsList = { options: list?.map(({ ssid }) => ({ value: ssid })), } @@ -34,6 +39,7 @@ const formatOptions = (list: WifiNetwork[]): SelectOptionOrGroup[] => { } export function SelectSsid(props: SelectSsidProps): JSX.Element { + const { t } = useTranslation('device_settings') const { list, value, onConnect, onJoinOther, isRobotBusy } = props const handleValueChange = (_: string, value: string): void => { @@ -44,7 +50,7 @@ export function SelectSsid(props: SelectSsidProps): JSX.Element { } } - const formatOptionLabel: React.ComponentProps< + const formatOptionLabel: ComponentProps< typeof SelectField >['formatOptionLabel'] = (option, { context }): JSX.Element | null => { const { value, label } = option @@ -69,8 +75,8 @@ export function SelectSsid(props: SelectSsidProps): JSX.Element { disabled={isRobotBusy} name={FIELD_NAME} value={value} - options={formatOptions(list)} - placeholder={Copy.SELECT_NETWORK} + options={formatOptions(list, t as TFunction)} + placeholder={t('choose_a_network')} onValueChange={handleValueChange} formatOptionLabel={formatOptionLabel} width="16rem" diff --git a/app/src/organisms/Desktop/Devices/RobotSettings/ConnectNetwork/i18n.ts b/app/src/organisms/Desktop/Devices/RobotSettings/ConnectNetwork/i18n.ts deleted file mode 100644 index cfee1e77d89..00000000000 --- a/app/src/organisms/Desktop/Devices/RobotSettings/ConnectNetwork/i18n.ts +++ /dev/null @@ -1,103 +0,0 @@ -// TODO(mc, 2020-03-11): i18n -import { - SECURITY_WPA_PSK, - SECURITY_WPA_EAP, - SECURITY_NONE, -} from '/app/redux/networking' - -import type { WifiNetwork } from './types' - -const SECURITY_DESC = { - [SECURITY_WPA_PSK]: 'requires a WPA2 password', - [SECURITY_WPA_EAP]: 'requires 802.1X authentication', - [SECURITY_NONE]: 'is unsecured', -} - -export const FIND_AND_JOIN_A_NETWORK = 'Find and join a Wi-Fi network' - -export const ENTER_NAME_AND_SECURITY_TYPE = - 'Enter the network name and security type.' - -const WIFI_NETWORK = 'Wi-Fi network' - -export const CANCEL = 'cancel' - -export const CONNECT = 'connect' - -export const CLOSE = 'close' - -export const DISCONNECT = 'disconnect' - -export const LABEL_SSID = 'Network Name (SSID)' - -export const LABEL_PSK = 'Password' - -export const LABEL_SECURITY = 'Authentication' - -export const LABEL_SECURITY_NONE = 'None' - -export const LABEL_SECURITY_PSK = 'WPA2 Personal' - -export const LABEL_ADD_NEW_KEY = 'Add new...' - -export const LABEL_SHOW_PASSWORD = 'Show password' - -export const LABEL_JOIN_OTHER_NETWORK = 'Join other network...' - -export const SELECT_AUTHENTICATION_METHOD = 'Select authentication method' - -export const SELECT_FILE = 'Select file' - -export const SELECT_NETWORK = 'Choose a network...' - -export const SUCCESSFULLY_DISCONNECTED = 'Successfully disconnected from Wi-Fi' - -export const SUCCESSFULLY_CONNECTED = 'Successfully connected to Wi-Fi' - -export const UNABLE_TO_DISCONNECT = 'Unable to disconnect from Wi-Fi' - -export const UNABLE_TO_CONNECT = 'Unable to connect to Wi-Fi' - -export const CHECK_YOUR_CREDENTIALS = - 'Please double-check your network credentials' - -export const CONNECT_TO_SSID = (ssid: string): string => `Connect to ${ssid}` - -export const DISCONNECT_FROM_SSID = (ssid: string): string => - `Disconnect from ${ssid}` - -export const ARE_YOU_SURE_YOU_WANT_TO_DISCONNECT = (ssid: string): string => - `Are you sure you want to disconnect from ${ssid}?` - -export const NETWORK_REQUIRES_SECURITY = (network: WifiNetwork): string => - `${WIFI_NETWORK} ${network.ssid} ${SECURITY_DESC[network.securityType]}` - -export const FIELD_IS_REQUIRED = (name: string): string => `${name} is required` - -export const FIELD_NOT_LONG_ENOUGH = ( - name: string, - minLength: number -): string => `${name} must be at least ${minLength} characters` - -const renderMaybeSsid = (ssid: string | null): string => - ssid !== null ? ` network ${ssid}` : '' - -export const CONNECTING_TO_NETWORK = (ssid: string | null): string => - `Connecting to Wi-Fi${renderMaybeSsid(ssid)}` - -export const DISCONNECTING_FROM_NETWORK = (ssid: string | null): string => - `Disconnecting from Wi-Fi${renderMaybeSsid(ssid)}` - -export const YOUR_ROBOT_WAS_UNABLE_TO_CONNECT = (ssid: string | null): string => - `Your robot was unable to connect to Wi-Fi${renderMaybeSsid(ssid)}` - -export const YOUR_ROBOT_WAS_UNABLE_TO_DISCONNECT = ( - ssid: string | null -): string => - `Your robot was unable to disconnect from Wi-Fi${renderMaybeSsid(ssid)}` - -export const YOUR_ROBOT_HAS_DISCONNECTED = (ssid: string | null): string => - `Your robot has successfully disconnected from Wi-Fi${renderMaybeSsid(ssid)}` - -export const YOUR_ROBOT_HAS_CONNECTED = (ssid: string | null): string => - `Your robot has successfully connected to Wi-Fi${renderMaybeSsid(ssid)}` diff --git a/app/src/organisms/Desktop/Devices/RobotSettings/ConnectNetwork/types.ts b/app/src/organisms/Desktop/Devices/RobotSettings/ConnectNetwork/types.ts index 050d26c08c3..ccfa37a7a44 100644 --- a/app/src/organisms/Desktop/Devices/RobotSettings/ConnectNetwork/types.ts +++ b/app/src/organisms/Desktop/Devices/RobotSettings/ConnectNetwork/types.ts @@ -1,3 +1,4 @@ +import type { ChangeEventHandler, FocusEventHandler } from 'react' import type { FieldError } from 'react-hook-form' import type { WifiNetwork, @@ -80,8 +81,8 @@ export type ConnectFormField = export type ConnectFormFieldProps = Readonly<{ value: string | null error: string | null - onChange: React.ChangeEventHandler - onBlur: React.FocusEventHandler + onChange: ChangeEventHandler + onBlur: FocusEventHandler setValue: (value: string) => unknown setTouched: (touched: boolean) => unknown }> diff --git a/app/src/organisms/Desktop/Devices/RobotSettings/RobotSettingsAdvanced.tsx b/app/src/organisms/Desktop/Devices/RobotSettings/RobotSettingsAdvanced.tsx index feb67b08d9f..adf2c92a9a5 100644 --- a/app/src/organisms/Desktop/Devices/RobotSettings/RobotSettingsAdvanced.tsx +++ b/app/src/organisms/Desktop/Devices/RobotSettings/RobotSettingsAdvanced.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useState, useEffect } from 'react' import { createPortal } from 'react-dom' import { useSelector, useDispatch } from 'react-redux' @@ -50,6 +50,7 @@ import { getRobotSerialNumber, UNREACHABLE } from '/app/redux/discovery' import { getTopPortalEl } from '/app/App/portal' import { useIsEstopNotDisengaged } from '/app/resources/devices/hooks/useIsEstopNotDisengaged' +import type { MouseEventHandler } from 'react' import type { State, Dispatch } from '/app/redux/types' import type { RobotSettings, @@ -69,19 +70,18 @@ export function RobotSettingsAdvanced({ const [ showRenameRobotSlideout, setShowRenameRobotSlideout, - ] = React.useState(false) + ] = useState(false) const [ showDeviceResetSlideout, setShowDeviceResetSlideout, - ] = React.useState(false) - const [ - showDeviceResetModal, - setShowDeviceResetModal, - ] = React.useState(false) + ] = useState(false) + const [showDeviceResetModal, setShowDeviceResetModal] = useState( + false + ) const [ showFactoryModeSlideout, setShowFactoryModeSlideout, - ] = React.useState(false) + ] = useState(false) const isRobotBusy = useIsRobotBusy({ poll: true }) const isEstopNotDisengaged = useIsEstopNotDisengaged(robotName) @@ -95,10 +95,8 @@ export function RobotSettingsAdvanced({ const reachable = robot?.status !== UNREACHABLE const sn = robot?.status != null ? getRobotSerialNumber(robot) : null - const [isRobotReachable, setIsRobotReachable] = React.useState( - reachable - ) - const [resetOptions, setResetOptions] = React.useState({}) + const [isRobotReachable, setIsRobotReachable] = useState(reachable) + const [resetOptions, setResetOptions] = useState({}) const findSettings = (id: string): RobotSettingsField | undefined => settings?.find(s => s.id === id) @@ -124,11 +122,11 @@ export function RobotSettingsAdvanced({ const dispatch = useDispatch() - React.useEffect(() => { + useEffect(() => { dispatch(fetchSettings(robotName)) }, [dispatch, robotName]) - React.useEffect(() => { + useEffect(() => { updateRobotStatus(isRobotBusy) }, [isRobotBusy, updateRobotStatus]) @@ -291,7 +289,7 @@ export function FeatureFlagToggle({ if (id == null) return null - const handleClick: React.MouseEventHandler = () => { + const handleClick: MouseEventHandler = () => { if (!isRobotBusy) { dispatch(updateSetting(robotName, id, !value)) } diff --git a/app/src/organisms/Desktop/Devices/RobotSettings/RobotSettingsFeatureFlags.tsx b/app/src/organisms/Desktop/Devices/RobotSettings/RobotSettingsFeatureFlags.tsx index 363589867fc..aa1210cdecc 100644 --- a/app/src/organisms/Desktop/Devices/RobotSettings/RobotSettingsFeatureFlags.tsx +++ b/app/src/organisms/Desktop/Devices/RobotSettings/RobotSettingsFeatureFlags.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useEffect } from 'react' import { useSelector, useDispatch } from 'react-redux' import { @@ -17,6 +17,8 @@ import { getRobotSettings, fetchSettings, } from '/app/redux/robot-settings' + +import type { MouseEventHandler } from 'react' import type { State, Dispatch } from '/app/redux/types' import type { RobotSettings, @@ -50,7 +52,7 @@ export function RobotSettingsFeatureFlags({ const dispatch = useDispatch() - React.useEffect(() => { + useEffect(() => { dispatch(fetchSettings(robotName)) }, [dispatch, robotName]) @@ -81,7 +83,7 @@ export function FeatureFlagToggle({ if (id == null) return null - const handleClick: React.MouseEventHandler = () => { + const handleClick: MouseEventHandler = () => { dispatch(updateSetting(robotName, id, !value)) } diff --git a/app/src/organisms/Desktop/Devices/RobotSettings/SettingToggle.tsx b/app/src/organisms/Desktop/Devices/RobotSettings/SettingToggle.tsx index 4407779d888..bb5b3c0d824 100644 --- a/app/src/organisms/Desktop/Devices/RobotSettings/SettingToggle.tsx +++ b/app/src/organisms/Desktop/Devices/RobotSettings/SettingToggle.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { useDispatch } from 'react-redux' import { @@ -13,6 +12,8 @@ import { import { ToggleButton } from '/app/atoms/buttons' import { updateSetting } from '/app/redux/robot-settings' + +import type { MouseEventHandler } from 'react' import type { Dispatch } from '/app/redux/types' import type { RobotSettingsField } from '/app/redux/robot-settings/types' @@ -38,7 +39,7 @@ export function SettingToggle({ if (id == null) return null - const handleClick: React.MouseEventHandler = () => { + const handleClick: MouseEventHandler = () => { dispatch(updateSetting(robotName, id, !value)) } diff --git a/app/src/organisms/Desktop/Devices/RobotSettings/UpdateBuildroot/RobotUpdateProgressModal.tsx b/app/src/organisms/Desktop/Devices/RobotSettings/UpdateBuildroot/RobotUpdateProgressModal.tsx index 955ead2de49..8a4fc045a65 100644 --- a/app/src/organisms/Desktop/Devices/RobotSettings/UpdateBuildroot/RobotUpdateProgressModal.tsx +++ b/app/src/organisms/Desktop/Devices/RobotSettings/UpdateBuildroot/RobotUpdateProgressModal.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useState, useEffect, useRef } from 'react' import { useTranslation } from 'react-i18next' import { useDispatch, useSelector } from 'react-redux' import { css } from 'styled-components' @@ -32,6 +32,7 @@ import { INIT_STATUS, } from '/app/resources/health/hooks' +import type { ChangeEventHandler } from 'react' import type { State } from '/app/redux/types' import type { SetStatusBarCreateCommand } from '@opentrons/shared-data/protocol' import type { RobotUpdateSession } from '/app/redux/robot-update/types' @@ -67,8 +68,8 @@ export function RobotUpdateProgressModal({ }: RobotUpdateProgressModalProps): JSX.Element { const dispatch = useDispatch() const { t } = useTranslation('device_settings') - const [showFileSelect, setShowFileSelect] = React.useState(false) - const installFromFileRef = React.useRef(null) + const [showFileSelect, setShowFileSelect] = useState(false) + const installFromFileRef = useRef(null) const completeRobotUpdateHandler = (): void => { if (closeUpdateBuildroot != null) closeUpdateBuildroot() @@ -85,14 +86,14 @@ export function RobotUpdateProgressModal({ useStatusBarAnimation(error != null) useCleanupRobotUpdateSessionOnDismount() - const handleFileSelect: React.ChangeEventHandler = event => { + const handleFileSelect: ChangeEventHandler = event => { const { files } = event.target if (files?.length === 1) { dispatch(startRobotUpdate(robotName, files[0].path)) } setShowFileSelect(false) } - React.useEffect(() => { + useEffect(() => { if (showFileSelect && installFromFileRef.current) installFromFileRef.current.click() }, [showFileSelect]) @@ -233,13 +234,11 @@ function useAllowExitIfUpdateStalled( progressPercent: number, robotInitStatus: RobotInitializationStatus ): boolean { - const [letUserExitUpdate, setLetUserExitUpdate] = React.useState( - false - ) - const prevSeenUpdateProgress = React.useRef(null) - const exitTimeoutRef = React.useRef(null) + const [letUserExitUpdate, setLetUserExitUpdate] = useState(false) + const prevSeenUpdateProgress = useRef(null) + const exitTimeoutRef = useRef(null) - React.useEffect(() => { + useEffect(() => { if (updateStep === 'initial' && prevSeenUpdateProgress.current !== null) { prevSeenUpdateProgress.current = null } else if (progressPercent !== prevSeenUpdateProgress.current) { @@ -258,7 +257,7 @@ function useAllowExitIfUpdateStalled( } }, [progressPercent, updateStep, robotInitStatus]) - React.useEffect(() => { + useEffect(() => { return () => { if (exitTimeoutRef.current) clearTimeout(exitTimeoutRef.current) } @@ -298,13 +297,13 @@ function useStatusBarAnimation(isError: boolean): void { } } - React.useEffect(startUpdatingAnimation, []) - React.useEffect(startIdleAnimationIfFailed, [isError]) + useEffect(startUpdatingAnimation, []) + useEffect(startIdleAnimationIfFailed, [isError]) } function useCleanupRobotUpdateSessionOnDismount(): void { const dispatch = useDispatch() - React.useEffect(() => { + useEffect(() => { return () => { dispatch(clearRobotUpdateSession()) } diff --git a/app/src/organisms/Desktop/Devices/RobotSettings/UpdateBuildroot/UpdateRobotModal.tsx b/app/src/organisms/Desktop/Devices/RobotSettings/UpdateBuildroot/UpdateRobotModal.tsx index 4b2225fe868..da77dee89c9 100644 --- a/app/src/organisms/Desktop/Devices/RobotSettings/UpdateBuildroot/UpdateRobotModal.tsx +++ b/app/src/organisms/Desktop/Devices/RobotSettings/UpdateBuildroot/UpdateRobotModal.tsx @@ -88,7 +88,7 @@ export function UpdateRobotModal({ let disabledReason: string = '' if (updateFromFileDisabledReason) - disabledReason = updateFromFileDisabledReason + disabledReason = t(updateFromFileDisabledReason) else if (isRobotBusy) disabledReason = t('robot_busy_protocol') useEffect(() => { diff --git a/app/src/organisms/Desktop/Devices/RobotSettings/UpdateBuildroot/__tests__/RobotUpdateProgressModal.test.tsx b/app/src/organisms/Desktop/Devices/RobotSettings/UpdateBuildroot/__tests__/RobotUpdateProgressModal.test.tsx index a2139f636bd..927c1acfdc8 100644 --- a/app/src/organisms/Desktop/Devices/RobotSettings/UpdateBuildroot/__tests__/RobotUpdateProgressModal.test.tsx +++ b/app/src/organisms/Desktop/Devices/RobotSettings/UpdateBuildroot/__tests__/RobotUpdateProgressModal.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { i18n } from '/app/i18n' import { act, fireEvent, screen } from '@testing-library/react' import { describe, it, vi, beforeEach, expect } from 'vitest' @@ -21,6 +20,7 @@ import { INIT_STATUS, } from '/app/resources/health/hooks' +import type { ComponentProps } from 'react' import type { SetStatusBarCreateCommand } from '@opentrons/shared-data' import type { RobotUpdateSession } from '/app/redux/robot-update/types' @@ -30,9 +30,7 @@ vi.mock('/app/redux/robot-update') vi.mock('/app/redux/robot-update/hooks') vi.mock('/app/resources/health/hooks') -const render = ( - props: React.ComponentProps -) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, }) @@ -50,8 +48,9 @@ describe('DownloadUpdateModal', () => { error: null, } - let props: React.ComponentProps + let props: ComponentProps const mockCreateLiveCommand = vi.fn() + const mockDispatchStartRobotUpdate = vi.fn() beforeEach(() => { mockCreateLiveCommand.mockResolvedValue(null) @@ -68,7 +67,9 @@ describe('DownloadUpdateModal', () => { progressPercent: 50, }) vi.mocked(getRobotSessionIsManualFile).mockReturnValue(false) - vi.mocked(useDispatchStartRobotUpdate).mockReturnValue(vi.fn) + vi.mocked(useDispatchStartRobotUpdate).mockReturnValue( + mockDispatchStartRobotUpdate + ) vi.mocked(getRobotUpdateDownloadError).mockReturnValue(null) }) diff --git a/app/src/organisms/Desktop/Devices/RobotSettings/UpdateBuildroot/__tests__/UpdateRobotModal.test.tsx b/app/src/organisms/Desktop/Devices/RobotSettings/UpdateBuildroot/__tests__/UpdateRobotModal.test.tsx index e77a0df9533..e4c6b7bfb12 100644 --- a/app/src/organisms/Desktop/Devices/RobotSettings/UpdateBuildroot/__tests__/UpdateRobotModal.test.tsx +++ b/app/src/organisms/Desktop/Devices/RobotSettings/UpdateBuildroot/__tests__/UpdateRobotModal.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { createStore } from 'redux' import { fireEvent, screen } from '@testing-library/react' import { describe, it, vi, beforeEach, expect } from 'vitest' @@ -14,6 +13,7 @@ import { getDiscoverableRobotByName } from '/app/redux/discovery' import { UpdateRobotModal, RELEASE_NOTES_URL_BASE } from '../UpdateRobotModal' import { useIsRobotBusy } from '/app/redux-resources/robots' +import type { ComponentProps } from 'react' import type { Store } from 'redux' import type { State } from '/app/redux/types' @@ -21,14 +21,14 @@ vi.mock('/app/redux/robot-update') vi.mock('/app/redux/discovery') vi.mock('/app/redux-resources/robots') -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, }) } describe('UpdateRobotModal', () => { - let props: React.ComponentProps + let props: ComponentProps let store: Store beforeEach(() => { store = createStore(vi.fn(), {}) diff --git a/app/src/organisms/Desktop/Devices/RobotSettings/UpdateBuildroot/__tests__/useRobotUpdateInfo.test.tsx b/app/src/organisms/Desktop/Devices/RobotSettings/UpdateBuildroot/__tests__/useRobotUpdateInfo.test.tsx index 2897110aacc..e81d61f1d5c 100644 --- a/app/src/organisms/Desktop/Devices/RobotSettings/UpdateBuildroot/__tests__/useRobotUpdateInfo.test.tsx +++ b/app/src/organisms/Desktop/Devices/RobotSettings/UpdateBuildroot/__tests__/useRobotUpdateInfo.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { renderHook } from '@testing-library/react' import { createStore } from 'redux' import { I18nextProvider } from 'react-i18next' @@ -9,6 +8,7 @@ import { i18n } from '/app/i18n' import { useRobotUpdateInfo } from '../useRobotUpdateInfo' import { getRobotUpdateDownloadProgress } from '/app/redux/robot-update' +import type { FunctionComponent, ReactNode } from 'react' import type { Store } from 'redux' import type { State } from '/app/redux/types' import type { @@ -21,7 +21,7 @@ vi.mock('/app/redux/robot-update') describe('useRobotUpdateInfo', () => { let store: Store - let wrapper: React.FunctionComponent<{ children: React.ReactNode }> + let wrapper: FunctionComponent<{ children: ReactNode }> const MOCK_ROBOT_NAME = 'testRobot' const mockRobotUpdateSession: RobotUpdateSession | null = { diff --git a/app/src/organisms/Desktop/Devices/RobotStatusHeader.tsx b/app/src/organisms/Desktop/Devices/RobotStatusHeader.tsx index 77cad0933ea..abeeceddc0b 100644 --- a/app/src/organisms/Desktop/Devices/RobotStatusHeader.tsx +++ b/app/src/organisms/Desktop/Devices/RobotStatusHeader.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { useTranslation } from 'react-i18next' import { useSelector, useDispatch } from 'react-redux' import { Link, useNavigate } from 'react-router-dom' @@ -35,6 +34,7 @@ import { import { getNetworkInterfaces, fetchStatus } from '/app/redux/networking' import { useNotifyRunQuery, useCurrentRunId } from '/app/resources/runs' +import type { MouseEvent } from 'react' import type { IconName, StyleProps } from '@opentrons/components' import type { DiscoveredRobot } from '/app/redux/discovery/types' import type { Dispatch, State } from '/app/redux/types' @@ -79,7 +79,7 @@ export function RobotStatusHeader(props: RobotStatusHeaderProps): JSX.Element { currentRunId != null && currentRunStatus != null && displayName != null ? ( { + onClick={(e: MouseEvent) => { e.stopPropagation() }} > diff --git a/app/src/organisms/Desktop/Devices/RunPreview/index.tsx b/app/src/organisms/Desktop/Devices/RunPreview/index.tsx index c2a48aba59a..c217833593e 100644 --- a/app/src/organisms/Desktop/Devices/RunPreview/index.tsx +++ b/app/src/organisms/Desktop/Devices/RunPreview/index.tsx @@ -33,9 +33,11 @@ import { Divider } from '/app/atoms/structure' import { NAV_BAR_WIDTH } from '/app/App/constants' import { getLabwareDefinitionsFromCommands } from '/app/local-resources/labware' +import type { ForwardedRef } from 'react' +import type { ViewportListRef } from 'react-viewport-list' import type { RunStatus } from '@opentrons/api-client' import type { RobotType } from '@opentrons/shared-data' -import type { ViewportListRef } from 'react-viewport-list' + const COLOR_FADE_MS = 500 const LIVE_RUN_COMMANDS_POLL_MS = 3000 // arbitrary large number of commands @@ -49,7 +51,7 @@ interface RunPreviewProps { } export const RunPreviewComponent = ( { runId, jumpedIndex, makeHandleScrollToStep, robotType }: RunPreviewProps, - ref: React.ForwardedRef + ref: ForwardedRef ): JSX.Element | null => { const { t } = useTranslation(['run_details', 'protocol_setup']) const robotSideAnalysis = useMostRecentCompletedAnalysis(runId) diff --git a/app/src/organisms/Desktop/Devices/__tests__/CalibrationStatusBanner.test.tsx b/app/src/organisms/Desktop/Devices/__tests__/CalibrationStatusBanner.test.tsx index fd50c5185ae..316315515b5 100644 --- a/app/src/organisms/Desktop/Devices/__tests__/CalibrationStatusBanner.test.tsx +++ b/app/src/organisms/Desktop/Devices/__tests__/CalibrationStatusBanner.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { screen } from '@testing-library/react' import { describe, it, expect, vi, beforeEach } from 'vitest' import '@testing-library/jest-dom/vitest' @@ -8,11 +7,11 @@ import { i18n } from '/app/i18n' import { CalibrationStatusBanner } from '../CalibrationStatusBanner' import { useCalibrationTaskList } from '../hooks' +import type { ComponentProps } from 'react' + vi.mock('../hooks') -const render = ( - props: React.ComponentProps -) => { +const render = (props: ComponentProps) => { return renderWithProviders( @@ -24,7 +23,7 @@ const render = ( } describe('CalibrationStatusBanner', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { robotName: 'otie' } }) diff --git a/app/src/organisms/Desktop/Devices/__tests__/ConnectionTroubleshootingModal.test.tsx b/app/src/organisms/Desktop/Devices/__tests__/ConnectionTroubleshootingModal.test.tsx index d725929a081..a810d459f12 100644 --- a/app/src/organisms/Desktop/Devices/__tests__/ConnectionTroubleshootingModal.test.tsx +++ b/app/src/organisms/Desktop/Devices/__tests__/ConnectionTroubleshootingModal.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { describe, it, expect, vi, beforeEach } from 'vitest' import '@testing-library/jest-dom/vitest' import { renderWithProviders } from '/app/__testing-utils__' @@ -6,8 +5,10 @@ import { fireEvent, screen } from '@testing-library/react' import { i18n } from '/app/i18n' import { ConnectionTroubleshootingModal } from '../ConnectionTroubleshootingModal' +import type { ComponentProps } from 'react' + const render = ( - props: React.ComponentProps + props: ComponentProps ) => { return renderWithProviders(, { i18nInstance: i18n, @@ -15,7 +16,7 @@ const render = ( } describe('ConnectionTroubleshootingModal', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { onClose: vi.fn(), diff --git a/app/src/organisms/Desktop/Devices/__tests__/EstopBanner.test.tsx b/app/src/organisms/Desktop/Devices/__tests__/EstopBanner.test.tsx index 7d052568378..4e9f74b6310 100644 --- a/app/src/organisms/Desktop/Devices/__tests__/EstopBanner.test.tsx +++ b/app/src/organisms/Desktop/Devices/__tests__/EstopBanner.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { screen } from '@testing-library/react' import { describe, it, beforeEach } from 'vitest' import '@testing-library/jest-dom/vitest' @@ -6,11 +5,13 @@ import { renderWithProviders } from '/app/__testing-utils__' import { i18n } from '/app/i18n' import { EstopBanner } from '../EstopBanner' -const render = (props: React.ComponentProps) => +import type { ComponentProps } from 'react' + +const render = (props: ComponentProps) => renderWithProviders(, { i18nInstance: i18n }) describe('EstopBanner', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { status: 'physicallyEngaged', diff --git a/app/src/organisms/Desktop/Devices/__tests__/HistoricalProtocolRun.test.tsx b/app/src/organisms/Desktop/Devices/__tests__/HistoricalProtocolRun.test.tsx index 070f1c614de..1b9ec0cde4e 100644 --- a/app/src/organisms/Desktop/Devices/__tests__/HistoricalProtocolRun.test.tsx +++ b/app/src/organisms/Desktop/Devices/__tests__/HistoricalProtocolRun.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { screen } from '@testing-library/react' import { describe, it, vi, beforeEach } from 'vitest' import '@testing-library/jest-dom/vitest' @@ -10,6 +9,7 @@ import { useRunStatus, useRunTimestamps } from '/app/resources/runs' import { HistoricalProtocolRun } from '../HistoricalProtocolRun' import { HistoricalProtocolRunOverflowMenu } from '../HistoricalProtocolRunOverflowMenu' +import type { ComponentProps } from 'react' import type { RunStatus, RunData } from '@opentrons/api-client' import type { RunTimeParameter } from '@opentrons/shared-data' @@ -25,14 +25,14 @@ const run = { runTimeParameters: [] as RunTimeParameter[], } as RunData -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, })[0] } describe('RecentProtocolRuns', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { diff --git a/app/src/organisms/Desktop/Devices/__tests__/HistoricalProtocolRunOverflowMenu.test.tsx b/app/src/organisms/Desktop/Devices/__tests__/HistoricalProtocolRunOverflowMenu.test.tsx index 2b2706c9645..72148f614bf 100644 --- a/app/src/organisms/Desktop/Devices/__tests__/HistoricalProtocolRunOverflowMenu.test.tsx +++ b/app/src/organisms/Desktop/Devices/__tests__/HistoricalProtocolRunOverflowMenu.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { fireEvent, screen } from '@testing-library/react' import { describe, it, vi, beforeEach, expect } from 'vitest' import { when } from 'vitest-when' @@ -23,6 +22,7 @@ import { useIsEstopNotDisengaged } from '/app/resources/devices' import { HistoricalProtocolRunOverflowMenu } from '../HistoricalProtocolRunOverflowMenu' import { useNotifyAllCommandsQuery } from '/app/resources/runs' +import type { ComponentProps } from 'react' import type { UseQueryResult } from 'react-query' import type { CommandsData } from '@opentrons/api-client' @@ -40,7 +40,7 @@ vi.mock('/app/redux-resources/analytics') vi.mock('@opentrons/react-api-client') const render = ( - props: React.ComponentProps + props: ComponentProps ) => { return renderWithProviders( @@ -59,7 +59,7 @@ let mockTrackProtocolRunEvent: any const mockDownloadRunLog = vi.fn() describe('HistoricalProtocolRunOverflowMenu', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { mockTrackEvent = vi.fn() vi.mocked(useTrackEvent).mockReturnValue(mockTrackEvent) diff --git a/app/src/organisms/Desktop/Devices/__tests__/RobotCard.test.tsx b/app/src/organisms/Desktop/Devices/__tests__/RobotCard.test.tsx index 89d59c47cf8..f2814630b2b 100644 --- a/app/src/organisms/Desktop/Devices/__tests__/RobotCard.test.tsx +++ b/app/src/organisms/Desktop/Devices/__tests__/RobotCard.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { MemoryRouter } from 'react-router-dom' import { when } from 'vitest-when' import { screen } from '@testing-library/react' @@ -37,6 +36,7 @@ import { useErrorRecoveryBanner, } from '../ErrorRecoveryBanner' +import type { ComponentProps } from 'react' import type { State } from '/app/redux/types' vi.mock('/app/redux/robot-update/selectors') @@ -94,7 +94,7 @@ const MOCK_STATE: State = { }, } as any -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders( @@ -107,7 +107,7 @@ const render = (props: React.ComponentProps) => { } describe('RobotCard', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { robot: mockConnectableRobot } diff --git a/app/src/organisms/Desktop/Devices/__tests__/RobotOverflowMenu.test.tsx b/app/src/organisms/Desktop/Devices/__tests__/RobotOverflowMenu.test.tsx index b3735e72a77..7f8ce943211 100644 --- a/app/src/organisms/Desktop/Devices/__tests__/RobotOverflowMenu.test.tsx +++ b/app/src/organisms/Desktop/Devices/__tests__/RobotOverflowMenu.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { MemoryRouter } from 'react-router-dom' import { fireEvent, screen } from '@testing-library/react' import { describe, it, vi, beforeEach, expect } from 'vitest' @@ -16,6 +15,8 @@ import { mockConnectedRobot, } from '/app/redux/discovery/__fixtures__' +import type { ComponentProps } from 'react' + vi.mock('/app/redux/robot-update/hooks') vi.mock('/app/resources/runs') vi.mock('/app/organisms/Desktop/ChooseProtocolSlideout') @@ -23,7 +24,7 @@ vi.mock('../hooks') vi.mock('/app/redux-resources/robots') vi.mock('/app/resources/devices/hooks/useIsEstopNotDisengaged') -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders( @@ -35,7 +36,7 @@ const render = (props: React.ComponentProps) => { } describe('RobotOverflowMenu', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { diff --git a/app/src/organisms/Desktop/Devices/__tests__/RobotOverview.test.tsx b/app/src/organisms/Desktop/Devices/__tests__/RobotOverview.test.tsx index 5d2513bec23..6f262e0a094 100644 --- a/app/src/organisms/Desktop/Devices/__tests__/RobotOverview.test.tsx +++ b/app/src/organisms/Desktop/Devices/__tests__/RobotOverview.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { MemoryRouter } from 'react-router-dom' import { when } from 'vitest-when' import { screen, fireEvent } from '@testing-library/react' @@ -48,10 +47,11 @@ import { useErrorRecoveryBanner, } from '../ErrorRecoveryBanner' +import type { ComponentProps } from 'react' +import type * as ReactApiClient from '@opentrons/react-api-client' import type { Config } from '/app/redux/config/types' import type { DiscoveryClientRobotAddress } from '/app/redux/discovery/types' import type { State } from '/app/redux/types' -import type * as ReactApiClient from '@opentrons/react-api-client' vi.mock('@opentrons/react-api-client', async importOriginal => { const actual = await importOriginal() @@ -104,7 +104,7 @@ const MOCK_STATE: State = { const mockToggleLights = vi.fn() -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders( @@ -117,7 +117,7 @@ const render = (props: React.ComponentProps) => { } describe('RobotOverview', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { robotName: mockConnectableRobot.name } diff --git a/app/src/organisms/Desktop/Devices/__tests__/RobotOverviewOverflowMenu.test.tsx b/app/src/organisms/Desktop/Devices/__tests__/RobotOverviewOverflowMenu.test.tsx index c4d384d0805..21d8e13b666 100644 --- a/app/src/organisms/Desktop/Devices/__tests__/RobotOverviewOverflowMenu.test.tsx +++ b/app/src/organisms/Desktop/Devices/__tests__/RobotOverviewOverflowMenu.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { MemoryRouter } from 'react-router-dom' import { fireEvent, screen } from '@testing-library/react' import { when } from 'vitest-when' @@ -24,6 +23,8 @@ import { handleUpdateBuildroot } from '../RobotSettings/UpdateBuildroot' import { useIsEstopNotDisengaged } from '/app/resources/devices/hooks/useIsEstopNotDisengaged' import { RobotOverviewOverflowMenu } from '../RobotOverviewOverflowMenu' +import type { ComponentProps } from 'react' + vi.mock('/app/redux/robot-controls') vi.mock('/app/redux/robot-admin') vi.mock('../hooks') @@ -36,9 +37,7 @@ vi.mock('../RobotSettings/UpdateBuildroot') vi.mock('/app/resources/devices/hooks/useIsEstopNotDisengaged') vi.mock('/app/redux-resources/robots') -const render = ( - props: React.ComponentProps -) => { +const render = (props: ComponentProps) => { return renderWithProviders( @@ -50,7 +49,7 @@ const render = ( } describe('RobotOverviewOverflowMenu', () => { - let props: React.ComponentProps + let props: ComponentProps vi.useFakeTimers() beforeEach(() => { diff --git a/app/src/organisms/Desktop/Devices/__tests__/RobotStatusHeader.test.tsx b/app/src/organisms/Desktop/Devices/__tests__/RobotStatusHeader.test.tsx index 465c46cc566..af05d9ce131 100644 --- a/app/src/organisms/Desktop/Devices/__tests__/RobotStatusHeader.test.tsx +++ b/app/src/organisms/Desktop/Devices/__tests__/RobotStatusHeader.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { MemoryRouter } from 'react-router-dom' import { RUN_STATUS_RUNNING } from '@opentrons/api-client' import { when } from 'vitest-when' @@ -20,6 +19,7 @@ import { useIsFlex } from '/app/redux-resources/robots' import { RobotStatusHeader } from '../RobotStatusHeader' import { useNotifyRunQuery, useCurrentRunId } from '/app/resources/runs' +import type { ComponentProps } from 'react' import type { DiscoveryClientRobotAddress } from '/app/redux/discovery/types' import type { SimpleInterfaceStatus } from '/app/redux/networking/types' import type { State } from '/app/redux/types' @@ -45,7 +45,7 @@ const MOCK_BUZZ = { const WIFI_IP = 'wifi-ip' const ETHERNET_IP = 'ethernet-ip' -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders( @@ -56,7 +56,7 @@ const render = (props: React.ComponentProps) => { ) } describe('RobotStatusHeader', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = MOCK_OTIE diff --git a/app/src/organisms/Desktop/Devices/hooks/__tests__/useCalibrationTaskList.test.tsx b/app/src/organisms/Desktop/Devices/hooks/__tests__/useCalibrationTaskList.test.tsx index 9d675c77f7e..1ca9c1741c2 100644 --- a/app/src/organisms/Desktop/Devices/hooks/__tests__/useCalibrationTaskList.test.tsx +++ b/app/src/organisms/Desktop/Devices/hooks/__tests__/useCalibrationTaskList.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { createStore } from 'redux' import { I18nextProvider } from 'react-i18next' import { Provider } from 'react-redux' @@ -30,6 +29,7 @@ import { } from '../__fixtures__/taskListFixtures' import { i18n } from '/app/i18n' +import type { FunctionComponent, ReactNode } from 'react' import type { Store } from 'redux' import type { State } from '/app/redux/types' @@ -41,7 +41,7 @@ const mockTipLengthCalLauncher = vi.fn() const mockDeckCalLauncher = vi.fn() describe('useCalibrationTaskList hook', () => { - let wrapper: React.FunctionComponent<{ children: React.ReactNode }> + let wrapper: FunctionComponent<{ children: ReactNode }> let store: Store const mockDeleteCalibration = vi.fn() diff --git a/app/src/organisms/Desktop/Devices/hooks/__tests__/useDeckCalibrationData.test.tsx b/app/src/organisms/Desktop/Devices/hooks/__tests__/useDeckCalibrationData.test.tsx index c08f0e3e1e5..1aef843c8ef 100644 --- a/app/src/organisms/Desktop/Devices/hooks/__tests__/useDeckCalibrationData.test.tsx +++ b/app/src/organisms/Desktop/Devices/hooks/__tests__/useDeckCalibrationData.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { when } from 'vitest-when' import { vi, it, expect, describe, beforeEach, afterEach } from 'vitest' import { Provider } from 'react-redux' @@ -15,13 +14,13 @@ import { import { getDiscoverableRobotByName } from '/app/redux/discovery' import { mockDeckCalData } from '/app/redux/calibration/__fixtures__' import { useDispatchApiRequest } from '/app/redux/robot-api' +import { useDeckCalibrationData } from '..' +import { mockConnectableRobot } from '/app/redux/discovery/__fixtures__' +import type { FunctionComponent, ReactNode } from 'react' import type { Store } from 'redux' import type { DispatchApiRequestType } from '/app/redux/robot-api' -import { useDeckCalibrationData } from '..' -import { mockConnectableRobot } from '/app/redux/discovery/__fixtures__' - vi.mock('@opentrons/react-api-client') vi.mock('/app/redux/calibration') vi.mock('/app/redux/robot-api') @@ -31,7 +30,7 @@ const store: Store = createStore(vi.fn(), {}) describe('useDeckCalibrationData hook', () => { let dispatchApiRequest: DispatchApiRequestType - let wrapper: React.FunctionComponent<{ children: React.ReactNode }> + let wrapper: FunctionComponent<{ children: ReactNode }> beforeEach(() => { dispatchApiRequest = vi.fn() const queryClient = new QueryClient() diff --git a/app/src/organisms/Desktop/Devices/hooks/__tests__/useLPCSuccessToast.test.ts b/app/src/organisms/Desktop/Devices/hooks/__tests__/useLPCSuccessToast.test.ts index a64b65252a1..6cb6897d8ca 100644 --- a/app/src/organisms/Desktop/Devices/hooks/__tests__/useLPCSuccessToast.test.ts +++ b/app/src/organisms/Desktop/Devices/hooks/__tests__/useLPCSuccessToast.test.ts @@ -1,11 +1,10 @@ -import * as React from 'react' +import { useContext } from 'react' import { vi, it, expect, describe } from 'vitest' import { renderHook } from '@testing-library/react' import { useLPCSuccessToast } from '..' -import type * as ReactType from 'react' vi.mock('react', async importOriginal => { - const actualReact = await importOriginal() + const actualReact = await importOriginal() return { ...actualReact, useContext: vi.fn(), @@ -14,7 +13,7 @@ vi.mock('react', async importOriginal => { describe('useLPCSuccessToast', () => { it('return true when useContext returns true', () => { - vi.mocked(React.useContext).mockReturnValue({ + vi.mocked(useContext).mockReturnValue({ setIsShowingLPCSuccessToast: true, }) const { result } = renderHook(() => useLPCSuccessToast()) @@ -23,7 +22,7 @@ describe('useLPCSuccessToast', () => { }) }) it('return false when useContext returns false', () => { - vi.mocked(React.useContext).mockReturnValue({ + vi.mocked(useContext).mockReturnValue({ setIsShowingLPCSuccessToast: false, }) const { result } = renderHook(() => useLPCSuccessToast()) diff --git a/app/src/organisms/Desktop/Devices/hooks/__tests__/usePipetteOffsetCalibration.test.tsx b/app/src/organisms/Desktop/Devices/hooks/__tests__/usePipetteOffsetCalibration.test.tsx index 2c23dcb0f61..50db68fe850 100644 --- a/app/src/organisms/Desktop/Devices/hooks/__tests__/usePipetteOffsetCalibration.test.tsx +++ b/app/src/organisms/Desktop/Devices/hooks/__tests__/usePipetteOffsetCalibration.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { vi, it, expect, describe, beforeEach, afterEach } from 'vitest' import { when } from 'vitest-when' import { Provider } from 'react-redux' @@ -15,6 +14,7 @@ import { useDispatchApiRequest } from '/app/redux/robot-api' import { useRobot } from '/app/redux-resources/robots' import { usePipetteOffsetCalibration } from '..' +import type { FunctionComponent, ReactNode } from 'react' import type { Store } from 'redux' import type { DiscoveredRobot } from '/app/redux/discovery/types' import type { DispatchApiRequestType } from '/app/redux/robot-api' @@ -32,7 +32,7 @@ const MOUNT = 'left' as Mount describe('usePipetteOffsetCalibration hook', () => { let dispatchApiRequest: DispatchApiRequestType - let wrapper: React.FunctionComponent<{ children: React.ReactNode }> + let wrapper: FunctionComponent<{ children: ReactNode }> beforeEach(() => { dispatchApiRequest = vi.fn() const queryClient = new QueryClient() diff --git a/app/src/organisms/Desktop/Devices/hooks/__tests__/usePipetteOffsetCalibrations.test.tsx b/app/src/organisms/Desktop/Devices/hooks/__tests__/usePipetteOffsetCalibrations.test.tsx index 9305b34af38..de390322d11 100644 --- a/app/src/organisms/Desktop/Devices/hooks/__tests__/usePipetteOffsetCalibrations.test.tsx +++ b/app/src/organisms/Desktop/Devices/hooks/__tests__/usePipetteOffsetCalibrations.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { vi, it, expect, describe, beforeEach, afterEach } from 'vitest' import { when } from 'vitest-when' import { renderHook } from '@testing-library/react' @@ -11,12 +10,14 @@ import { } from '/app/redux/calibration/pipette-offset/__fixtures__' import { usePipetteOffsetCalibrations } from '..' +import type { FunctionComponent, ReactNode } from 'react' + vi.mock('@opentrons/react-api-client') const CALIBRATION_DATA_POLL_MS = 5000 describe('usePipetteOffsetCalibrations hook', () => { - let wrapper: React.FunctionComponent<{ children: React.ReactNode }> + let wrapper: FunctionComponent<{ children: ReactNode }> beforeEach(() => { const queryClient = new QueryClient() wrapper = ({ children }) => ( diff --git a/app/src/organisms/Desktop/Devices/hooks/__tests__/useSyncRobotClock.test.tsx b/app/src/organisms/Desktop/Devices/hooks/__tests__/useSyncRobotClock.test.tsx index 02b593fbaab..1ab5c1f55cb 100644 --- a/app/src/organisms/Desktop/Devices/hooks/__tests__/useSyncRobotClock.test.tsx +++ b/app/src/organisms/Desktop/Devices/hooks/__tests__/useSyncRobotClock.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { vi, it, expect, describe, beforeEach, afterEach } from 'vitest' import { Provider } from 'react-redux' import { createStore } from 'redux' @@ -7,6 +6,8 @@ import { QueryClient, QueryClientProvider } from 'react-query' import { syncSystemTime } from '/app/redux/robot-admin' import { useSyncRobotClock } from '..' + +import type { FunctionComponent, ReactNode } from 'react' import type { Store } from 'redux' vi.mock('/app/redux/discovery') @@ -14,7 +15,7 @@ vi.mock('/app/redux/discovery') const store: Store = createStore(vi.fn(), {}) describe('useSyncRobotClock hook', () => { - let wrapper: React.FunctionComponent<{ children: React.ReactNode }> + let wrapper: FunctionComponent<{ children: ReactNode }> beforeEach(() => { store.dispatch = vi.fn() const queryClient = new QueryClient() diff --git a/app/src/organisms/Desktop/Devices/hooks/__tests__/useTipLengthCalibrations.test.tsx b/app/src/organisms/Desktop/Devices/hooks/__tests__/useTipLengthCalibrations.test.tsx index 7281b009b5c..d2cec816973 100644 --- a/app/src/organisms/Desktop/Devices/hooks/__tests__/useTipLengthCalibrations.test.tsx +++ b/app/src/organisms/Desktop/Devices/hooks/__tests__/useTipLengthCalibrations.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { vi, it, expect, describe, beforeEach, afterEach } from 'vitest' import { when } from 'vitest-when' import { QueryClient, QueryClientProvider } from 'react-query' @@ -11,12 +10,14 @@ import { } from '/app/redux/calibration/tip-length/__fixtures__' import { useTipLengthCalibrations } from '..' +import type { FunctionComponent, ReactNode } from 'react' + vi.mock('@opentrons/react-api-client') const CALIBRATIONS_FETCH_MS = 5000 describe('useTipLengthCalibrations hook', () => { - let wrapper: React.FunctionComponent<{ children: React.ReactNode }> + let wrapper: FunctionComponent<{ children: ReactNode }> beforeEach(() => { const queryClient = new QueryClient() wrapper = ({ children }) => ( diff --git a/app/src/organisms/Desktop/Devices/hooks/__tests__/useTrackCreateProtocolRunEvent.test.tsx b/app/src/organisms/Desktop/Devices/hooks/__tests__/useTrackCreateProtocolRunEvent.test.tsx index b8cf7fd9896..1f7eb5041fa 100644 --- a/app/src/organisms/Desktop/Devices/hooks/__tests__/useTrackCreateProtocolRunEvent.test.tsx +++ b/app/src/organisms/Desktop/Devices/hooks/__tests__/useTrackCreateProtocolRunEvent.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { createStore } from 'redux' import { Provider } from 'react-redux' import { QueryClient, QueryClientProvider } from 'react-query' @@ -12,6 +11,7 @@ import { parseProtocolAnalysisOutput } from '/app/transformations/analysis' import { useTrackEvent } from '/app/redux/analytics' import { storedProtocolData } from '/app/redux/protocol-storage/__fixtures__' +import type { FunctionComponent, ReactNode } from 'react' import type { Mock } from 'vitest' import type { Store } from 'redux' import type { ProtocolAnalyticsData } from '/app/redux/analytics/types' @@ -28,7 +28,7 @@ const PROTOCOL_PROPERTIES = { protocolType: 'python' } as ProtocolAnalyticsData let mockTrackEvent: Mock let mockGetProtocolRunAnalyticsData: Mock -let wrapper: React.FunctionComponent<{ children: React.ReactNode }> +let wrapper: FunctionComponent<{ children: ReactNode }> let store: Store = createStore(vi.fn(), {}) describe('useTrackCreateProtocolRunEvent hook', () => { diff --git a/app/src/organisms/Desktop/HowCalibrationWorksModal/__tests__/HowCalibrationWorksModal.test.tsx b/app/src/organisms/Desktop/HowCalibrationWorksModal/__tests__/HowCalibrationWorksModal.test.tsx index 8eee1543c5a..dfa8ae09955 100644 --- a/app/src/organisms/Desktop/HowCalibrationWorksModal/__tests__/HowCalibrationWorksModal.test.tsx +++ b/app/src/organisms/Desktop/HowCalibrationWorksModal/__tests__/HowCalibrationWorksModal.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { fireEvent, screen } from '@testing-library/react' import '@testing-library/jest-dom/vitest' import { describe, it, vi, beforeEach, expect } from 'vitest' @@ -6,16 +5,16 @@ import { renderWithProviders } from '/app/__testing-utils__' import { i18n } from '/app/i18n' import { HowCalibrationWorksModal } from '..' -const render = ( - props: React.ComponentProps -) => { +import type { ComponentProps } from 'react' + +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, })[0] } describe('HowCalibrationWorksModal', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { onCloseClick: vi.fn() } }) diff --git a/app/src/organisms/Desktop/Labware/AddCustomLabwareSlideout/__tests__/AddCustomLabwareSlideout.test.tsx b/app/src/organisms/Desktop/Labware/AddCustomLabwareSlideout/__tests__/AddCustomLabwareSlideout.test.tsx index 977f44f2cce..9d67949e289 100644 --- a/app/src/organisms/Desktop/Labware/AddCustomLabwareSlideout/__tests__/AddCustomLabwareSlideout.test.tsx +++ b/app/src/organisms/Desktop/Labware/AddCustomLabwareSlideout/__tests__/AddCustomLabwareSlideout.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { MemoryRouter } from 'react-router-dom' import { describe, it, expect, vi, beforeEach } from 'vitest' import { fireEvent, screen } from '@testing-library/react' @@ -10,15 +9,20 @@ import { import { renderWithProviders } from '/app/__testing-utils__' import { AddCustomLabwareSlideout } from '..' +import type { ComponentProps } from 'react' + vi.mock('/app/redux/custom-labware') vi.mock('/app/local-resources/labware') vi.mock('/app/redux/analytics') +vi.mock('/app/redux/shell/remote', () => ({ + remote: { + getFilePathFrom: vi.fn(), + }, +})) let mockTrackEvent: any -const render = ( - props: React.ComponentProps -) => { +const render = (props: ComponentProps) => { return renderWithProviders( @@ -30,7 +34,7 @@ const render = ( } describe('AddCustomLabwareSlideout', () => { - const props: React.ComponentProps = { + const props: ComponentProps = { isExpanded: true, onCloseClick: vi.fn(() => null), } diff --git a/app/src/organisms/Desktop/Labware/AddCustomLabwareSlideout/index.tsx b/app/src/organisms/Desktop/Labware/AddCustomLabwareSlideout/index.tsx index 1ded9827380..448a97c4b79 100644 --- a/app/src/organisms/Desktop/Labware/AddCustomLabwareSlideout/index.tsx +++ b/app/src/organisms/Desktop/Labware/AddCustomLabwareSlideout/index.tsx @@ -19,6 +19,8 @@ import { ANALYTICS_ADD_CUSTOM_LABWARE, } from '/app/redux/analytics' import { UploadInput } from '/app/molecules/UploadInput' +import { remote } from '/app/redux/shell/remote' + import type { Dispatch } from '/app/redux/types' export interface AddCustomLabwareSlideoutProps { @@ -46,7 +48,9 @@ export function AddCustomLabwareSlideout( > { - dispatch(addCustomLabwareFile(file.path)) + void remote.getFilePathFrom(file).then(filePath => { + dispatch(addCustomLabwareFile(filePath)) + }) }} onClick={() => { dispatch(addCustomLabware()) diff --git a/app/src/organisms/Desktop/Labware/LabwareCard/CustomLabwareOverflowMenu.tsx b/app/src/organisms/Desktop/Labware/LabwareCard/CustomLabwareOverflowMenu.tsx index 6183fdf00de..08826189798 100644 --- a/app/src/organisms/Desktop/Labware/LabwareCard/CustomLabwareOverflowMenu.tsx +++ b/app/src/organisms/Desktop/Labware/LabwareCard/CustomLabwareOverflowMenu.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useState } from 'react' import { createPortal } from 'react-dom' import { useDispatch } from 'react-redux' import { useTranslation } from 'react-i18next' @@ -36,6 +36,7 @@ import { openCustomLabwareDirectory, } from '/app/redux/custom-labware' +import type { MouseEventHandler } from 'react' import type { Dispatch } from '/app/redux/types' const LABWARE_CREATOR_HREF = 'https://labware.opentrons.com/create/' @@ -51,7 +52,7 @@ export function CustomLabwareOverflowMenu( const { filename, onDelete } = props const { t } = useTranslation(['labware_landing', 'shared']) const dispatch = useDispatch() - const [showOverflowMenu, setShowOverflowMenu] = React.useState(false) + const [showOverflowMenu, setShowOverflowMenu] = useState(false) const overflowMenuRef = useOnClickOutside({ onClickOutside: () => { setShowOverflowMenu(false) @@ -67,24 +68,24 @@ export function CustomLabwareOverflowMenu( dispatch(deleteCustomLabwareFile(filename)) onDelete?.() }, true) - const handleOpenInFolder: React.MouseEventHandler = e => { + const handleOpenInFolder: MouseEventHandler = e => { e.preventDefault() e.stopPropagation() setShowOverflowMenu(false) dispatch(openCustomLabwareDirectory()) } - const handleClickDelete: React.MouseEventHandler = e => { + const handleClickDelete: MouseEventHandler = e => { e.preventDefault() e.stopPropagation() setShowOverflowMenu(false) confirmDeleteLabware() } - const handleOverflowClick: React.MouseEventHandler = e => { + const handleOverflowClick: MouseEventHandler = e => { e.preventDefault() e.stopPropagation() setShowOverflowMenu(currentShowOverflowMenu => !currentShowOverflowMenu) } - const handleClickLabwareCreator: React.MouseEventHandler = e => { + const handleClickLabwareCreator: MouseEventHandler = e => { e.preventDefault() e.stopPropagation() trackEvent({ @@ -95,7 +96,7 @@ export function CustomLabwareOverflowMenu( window.open(LABWARE_CREATOR_HREF, '_blank') } - const handleCancelModal: React.MouseEventHandler = e => { + const handleCancelModal: MouseEventHandler = e => { e.preventDefault() e.stopPropagation() cancelDeleteLabware() diff --git a/app/src/organisms/Desktop/Labware/LabwareCard/__tests__/CustomLabwareOverflowMenu.test.tsx b/app/src/organisms/Desktop/Labware/LabwareCard/__tests__/CustomLabwareOverflowMenu.test.tsx index 588b47ecea3..5e47b3432f4 100644 --- a/app/src/organisms/Desktop/Labware/LabwareCard/__tests__/CustomLabwareOverflowMenu.test.tsx +++ b/app/src/organisms/Desktop/Labware/LabwareCard/__tests__/CustomLabwareOverflowMenu.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { fireEvent, screen } from '@testing-library/react' import { describe, it, vi, expect, beforeEach, afterEach } from 'vitest' @@ -9,6 +8,7 @@ import { i18n } from '/app/i18n' import { useTrackEvent } from '/app/redux/analytics' import { CustomLabwareOverflowMenu } from '../CustomLabwareOverflowMenu' +import type { ComponentProps } from 'react' import type { Mock } from 'vitest' import type * as OpentronsComponents from '@opentrons/components' @@ -31,7 +31,7 @@ vi.mock('@opentrons/components', async importOriginal => { }) const render = ( - props: React.ComponentProps + props: ComponentProps ): ReturnType => { return renderWithProviders(, { i18nInstance: i18n, @@ -39,7 +39,7 @@ const render = ( } describe('CustomLabwareOverflowMenu', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { diff --git a/app/src/organisms/Desktop/Labware/LabwareCard/__tests__/LabwareCard.test.tsx b/app/src/organisms/Desktop/Labware/LabwareCard/__tests__/LabwareCard.test.tsx index 76abb51c72f..2431472e274 100644 --- a/app/src/organisms/Desktop/Labware/LabwareCard/__tests__/LabwareCard.test.tsx +++ b/app/src/organisms/Desktop/Labware/LabwareCard/__tests__/LabwareCard.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { screen } from '@testing-library/react' import { describe, it, vi, beforeEach } from 'vitest' import { renderWithProviders, nestedTextMatcher } from '/app/__testing-utils__' @@ -8,6 +7,7 @@ import { mockDefinition } from '/app/redux/custom-labware/__fixtures__' import { CustomLabwareOverflowMenu } from '../CustomLabwareOverflowMenu' import { LabwareCard } from '..' +import type { ComponentProps } from 'react' import type * as OpentronsComponents from '@opentrons/components' vi.mock('/app/local-resources/labware') @@ -21,14 +21,14 @@ vi.mock('@opentrons/components', async importOriginal => { } }) -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, }) } describe('LabwareCard', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { vi.mocked(CustomLabwareOverflowMenu).mockReturnValue(
        Mock CustomLabwareOverflowMenu
        diff --git a/app/src/organisms/Desktop/Labware/LabwareCard/hooks.tsx b/app/src/organisms/Desktop/Labware/LabwareCard/hooks.tsx index 62e0589ac51..3a5890d7f60 100644 --- a/app/src/organisms/Desktop/Labware/LabwareCard/hooks.tsx +++ b/app/src/organisms/Desktop/Labware/LabwareCard/hooks.tsx @@ -1,7 +1,8 @@ -import * as React from 'react' +import { useEffect } from 'react' +import type { RefObject } from 'react' export function useCloseOnOutsideClick( - ref: React.RefObject, + ref: RefObject, onClose: () => void ): void { const handleClick = (e: MouseEvent): void => { @@ -11,7 +12,7 @@ export function useCloseOnOutsideClick( } } - React.useEffect(() => { + useEffect(() => { document.addEventListener('click', handleClick) return () => { document.removeEventListener('click', handleClick) diff --git a/app/src/organisms/Desktop/Labware/LabwareDetails/StyledComponents/ExpandingTitle.tsx b/app/src/organisms/Desktop/Labware/LabwareDetails/StyledComponents/ExpandingTitle.tsx index c92a63434cb..a21a769ca43 100644 --- a/app/src/organisms/Desktop/Labware/LabwareDetails/StyledComponents/ExpandingTitle.tsx +++ b/app/src/organisms/Desktop/Labware/LabwareDetails/StyledComponents/ExpandingTitle.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useState } from 'react' import { ALIGN_CENTER, Box, @@ -6,20 +6,21 @@ import { Flex, Icon, JUSTIFY_SPACE_BETWEEN, - Link, - SIZE_1, LegacyStyledText, + Link, TYPOGRAPHY, } from '@opentrons/components' import { Divider } from '/app/atoms/structure' +import type { ReactNode } from 'react' + interface ExpandingTitleProps { - label: React.ReactNode - diagram?: React.ReactNode + label: ReactNode + diagram?: ReactNode } export function ExpandingTitle(props: ExpandingTitleProps): JSX.Element { - const [diagramVisible, setDiagramVisible] = React.useState(false) + const [diagramVisible, setDiagramVisible] = useState(false) const toggleDiagramVisible = (): void => { setDiagramVisible(currentDiagramVisible => !currentDiagramVisible) } @@ -39,7 +40,7 @@ export function ExpandingTitle(props: ExpandingTitleProps): JSX.Element { )} diff --git a/app/src/organisms/Desktop/Labware/LabwareDetails/StyledComponents/__tests__/ExpandingTitle.test.tsx b/app/src/organisms/Desktop/Labware/LabwareDetails/StyledComponents/__tests__/ExpandingTitle.test.tsx index 6ee44619bc8..b7a872e9a1b 100644 --- a/app/src/organisms/Desktop/Labware/LabwareDetails/StyledComponents/__tests__/ExpandingTitle.test.tsx +++ b/app/src/organisms/Desktop/Labware/LabwareDetails/StyledComponents/__tests__/ExpandingTitle.test.tsx @@ -1,11 +1,12 @@ -import type * as React from 'react' import { fireEvent, screen } from '@testing-library/react' import { describe, it, expect, beforeEach } from 'vitest' import { getFootprintDiagram } from '@opentrons/components' import { renderWithProviders } from '/app/__testing-utils__' import { ExpandingTitle } from '../ExpandingTitle' -const render = (props: React.ComponentProps) => { +import type { ComponentProps } from 'react' + +const render = (props: ComponentProps) => { return renderWithProviders() } @@ -13,7 +14,7 @@ const diagram = getFootprintDiagram({}) const DIAGRAM_TEST_ID = 'expanding_title_diagram' describe('ExpandingTitle', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { label: 'Title', diff --git a/app/src/organisms/Desktop/Labware/LabwareDetails/StyledComponents/__tests__/LabeledValue.test.tsx b/app/src/organisms/Desktop/Labware/LabwareDetails/StyledComponents/__tests__/LabeledValue.test.tsx index c3a73771f52..196d85cb707 100644 --- a/app/src/organisms/Desktop/Labware/LabwareDetails/StyledComponents/__tests__/LabeledValue.test.tsx +++ b/app/src/organisms/Desktop/Labware/LabwareDetails/StyledComponents/__tests__/LabeledValue.test.tsx @@ -1,15 +1,16 @@ -import type * as React from 'react' import { screen } from '@testing-library/react' import { describe, it, beforeEach } from 'vitest' import { renderWithProviders } from '/app/__testing-utils__' import { LabeledValue } from '../LabeledValue' -const render = (props: React.ComponentProps) => { +import type { ComponentProps } from 'react' + +const render = (props: ComponentProps) => { return renderWithProviders() } describe('LabeledValue', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { label: 'height', diff --git a/app/src/organisms/Desktop/Labware/LabwareDetails/__tests__/Dimensions.test.tsx b/app/src/organisms/Desktop/Labware/LabwareDetails/__tests__/Dimensions.test.tsx index 32de832214c..2afc92ad6cf 100644 --- a/app/src/organisms/Desktop/Labware/LabwareDetails/__tests__/Dimensions.test.tsx +++ b/app/src/organisms/Desktop/Labware/LabwareDetails/__tests__/Dimensions.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { screen } from '@testing-library/react' import { describe, it, beforeEach } from 'vitest' import { renderWithProviders } from '/app/__testing-utils__' @@ -6,14 +5,16 @@ import { i18n } from '/app/i18n' import { mockDefinition } from '/app/redux/custom-labware/__fixtures__' import { Dimensions } from '../Dimensions' -const render = (props: React.ComponentProps) => { +import type { ComponentProps } from 'react' + +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, }) } describe('Dimensions', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { definition: mockDefinition, diff --git a/app/src/organisms/Desktop/Labware/LabwareDetails/__tests__/Gallery.test.tsx b/app/src/organisms/Desktop/Labware/LabwareDetails/__tests__/Gallery.test.tsx index 344981336b0..5a61aa4684c 100644 --- a/app/src/organisms/Desktop/Labware/LabwareDetails/__tests__/Gallery.test.tsx +++ b/app/src/organisms/Desktop/Labware/LabwareDetails/__tests__/Gallery.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { fireEvent, screen } from '@testing-library/react' import { describe, it, expect, beforeEach } from 'vitest' import { renderWithProviders } from '/app/__testing-utils__' @@ -6,12 +5,14 @@ import { mockDefinition } from '/app/redux/custom-labware/__fixtures__' import { labwareImages } from '../labware-images' import { Gallery } from '../Gallery' -const render = (props: React.ComponentProps) => { +import type { ComponentProps } from 'react' + +const render = (props: ComponentProps) => { return renderWithProviders() } describe('Gallery', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { labwareImages.mock_definition = ['image1'] props = { diff --git a/app/src/organisms/Desktop/Labware/LabwareDetails/__tests__/LabwareDetails.test.tsx b/app/src/organisms/Desktop/Labware/LabwareDetails/__tests__/LabwareDetails.test.tsx index 78c98c50a46..10602db9f9f 100644 --- a/app/src/organisms/Desktop/Labware/LabwareDetails/__tests__/LabwareDetails.test.tsx +++ b/app/src/organisms/Desktop/Labware/LabwareDetails/__tests__/LabwareDetails.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { fireEvent, screen } from '@testing-library/react' import { describe, it, beforeEach, afterEach, vi, expect } from 'vitest' @@ -17,6 +16,8 @@ import { WellSpacing } from '../WellSpacing' import { LabwareDetails } from '..' +import type { ComponentProps } from 'react' + vi.mock('/app/local-resources/labware') vi.mock('../../LabwareCard/CustomLabwareOverflowMenu') vi.mock('../Dimensions') @@ -28,7 +29,7 @@ vi.mock('../WellDimensions') vi.mock('../WellSpacing') const render = ( - props: React.ComponentProps + props: ComponentProps ): ReturnType => { return renderWithProviders(, { i18nInstance: i18n, @@ -36,7 +37,7 @@ const render = ( } describe('LabwareDetails', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { vi.mocked(CustomLabwareOverflowMenu).mockReturnValue(
        Mock CustomLabwareOverflowMenu
        diff --git a/app/src/organisms/Desktop/Labware/LabwareDetails/__tests__/ManufacturerDetails.test.tsx b/app/src/organisms/Desktop/Labware/LabwareDetails/__tests__/ManufacturerDetails.test.tsx index 21dc8c8f1a2..9d50aa61500 100644 --- a/app/src/organisms/Desktop/Labware/LabwareDetails/__tests__/ManufacturerDetails.test.tsx +++ b/app/src/organisms/Desktop/Labware/LabwareDetails/__tests__/ManufacturerDetails.test.tsx @@ -1,18 +1,19 @@ -import type * as React from 'react' import { screen } from '@testing-library/react' import { describe, it, expect, beforeEach } from 'vitest' import { renderWithProviders } from '/app/__testing-utils__' import { i18n } from '/app/i18n' import { ManufacturerDetails } from '../ManufacturerDetails' -const render = (props: React.ComponentProps) => { +import type { ComponentProps } from 'react' + +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, }) } describe('ManufacturerDetails', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { brand: { brand: 'Opentrons' }, diff --git a/app/src/organisms/Desktop/Labware/LabwareDetails/__tests__/WellCount.test.tsx b/app/src/organisms/Desktop/Labware/LabwareDetails/__tests__/WellCount.test.tsx index f833c0c6e3f..6d94737a9c4 100644 --- a/app/src/organisms/Desktop/Labware/LabwareDetails/__tests__/WellCount.test.tsx +++ b/app/src/organisms/Desktop/Labware/LabwareDetails/__tests__/WellCount.test.tsx @@ -1,18 +1,19 @@ -import type * as React from 'react' import { screen } from '@testing-library/react' import { describe, it, beforeEach } from 'vitest' import { renderWithProviders } from '/app/__testing-utils__' import { i18n } from '/app/i18n' import { WellCount } from '../WellCount' -const render = (props: React.ComponentProps) => { +import type { ComponentProps } from 'react' + +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, }) } describe('WellCount', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { count: 1, diff --git a/app/src/organisms/Desktop/Labware/LabwareDetails/__tests__/WellDimensions.test.tsx b/app/src/organisms/Desktop/Labware/LabwareDetails/__tests__/WellDimensions.test.tsx index 3269c3ec241..78af2ac24b2 100644 --- a/app/src/organisms/Desktop/Labware/LabwareDetails/__tests__/WellDimensions.test.tsx +++ b/app/src/organisms/Desktop/Labware/LabwareDetails/__tests__/WellDimensions.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { screen } from '@testing-library/react' import { describe, it, beforeEach, expect } from 'vitest' import { renderWithProviders } from '/app/__testing-utils__' @@ -10,14 +9,16 @@ import { } from '/app/redux/custom-labware/__fixtures__' import { WellDimensions } from '../WellDimensions' -const render = (props: React.ComponentProps) => { +import type { ComponentProps } from 'react' + +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, }) } describe('WellDimensions', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { labwareParams: mockDefinition.parameters, diff --git a/app/src/organisms/Desktop/Labware/LabwareDetails/__tests__/WellProperties.test.tsx b/app/src/organisms/Desktop/Labware/LabwareDetails/__tests__/WellProperties.test.tsx index 3d21e01f4df..40b17b3b31f 100644 --- a/app/src/organisms/Desktop/Labware/LabwareDetails/__tests__/WellProperties.test.tsx +++ b/app/src/organisms/Desktop/Labware/LabwareDetails/__tests__/WellProperties.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { screen } from '@testing-library/react' import { describe, it, expect, beforeEach } from 'vitest' import { renderWithProviders } from '/app/__testing-utils__' @@ -6,14 +5,16 @@ import { i18n } from '/app/i18n' import { mockCircularLabwareWellGroupProperties } from '/app/redux/custom-labware/__fixtures__' import { WellProperties } from '../WellProperties' -const render = (props: React.ComponentProps) => { +import type { ComponentProps } from 'react' + +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, }) } describe('WellProperties', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { wellProperties: mockCircularLabwareWellGroupProperties, diff --git a/app/src/organisms/Desktop/Labware/LabwareDetails/__tests__/WellSpacing.test.tsx b/app/src/organisms/Desktop/Labware/LabwareDetails/__tests__/WellSpacing.test.tsx index 5496bbe33f1..59115886e02 100644 --- a/app/src/organisms/Desktop/Labware/LabwareDetails/__tests__/WellSpacing.test.tsx +++ b/app/src/organisms/Desktop/Labware/LabwareDetails/__tests__/WellSpacing.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { screen } from '@testing-library/react' import { describe, it, beforeEach, expect } from 'vitest' import { renderWithProviders } from '/app/__testing-utils__' @@ -6,14 +5,16 @@ import { i18n } from '/app/i18n' import { mockCircularLabwareWellGroupProperties } from '/app/redux/custom-labware/__fixtures__' import { WellSpacing } from '../WellSpacing' -const render = (props: React.ComponentProps) => { +import type { ComponentProps } from 'react' + +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, }) } describe('WellSpacing', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { wellProperties: mockCircularLabwareWellGroupProperties, diff --git a/app/src/organisms/Desktop/ProtocolAnalysisFailure/ProtocolAnalysisStale.tsx b/app/src/organisms/Desktop/ProtocolAnalysisFailure/ProtocolAnalysisStale.tsx index 9b3a7c00d14..4daa1b24c80 100644 --- a/app/src/organisms/Desktop/ProtocolAnalysisFailure/ProtocolAnalysisStale.tsx +++ b/app/src/organisms/Desktop/ProtocolAnalysisFailure/ProtocolAnalysisStale.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { useDispatch } from 'react-redux' import { useTranslation, Trans } from 'react-i18next' @@ -13,9 +12,11 @@ import { TYPOGRAPHY, WRAP_REVERSE, } from '@opentrons/components' +import { analyzeProtocol } from '/app/redux/protocol-storage' +import type { MouseEventHandler } from 'react' import type { Dispatch } from '/app/redux/types' -import { analyzeProtocol } from '/app/redux/protocol-storage' + interface ProtocolAnalysisStaleProps { protocolKey: string } @@ -27,7 +28,7 @@ export function ProtocolAnalysisStale( const { t } = useTranslation(['protocol_list', 'shared']) const dispatch = useDispatch() - const handleClickReanalyze: React.MouseEventHandler = e => { + const handleClickReanalyze: MouseEventHandler = e => { e.preventDefault() e.stopPropagation() dispatch(analyzeProtocol(protocolKey)) diff --git a/app/src/organisms/Desktop/ProtocolAnalysisFailure/__tests__/ProtocolAnalysisFailure.test.tsx b/app/src/organisms/Desktop/ProtocolAnalysisFailure/__tests__/ProtocolAnalysisFailure.test.tsx index fbdec0a45ec..becdabef509 100644 --- a/app/src/organisms/Desktop/ProtocolAnalysisFailure/__tests__/ProtocolAnalysisFailure.test.tsx +++ b/app/src/organisms/Desktop/ProtocolAnalysisFailure/__tests__/ProtocolAnalysisFailure.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { MemoryRouter } from 'react-router-dom' import { fireEvent, screen } from '@testing-library/react' import { describe, it, expect } from 'vitest' @@ -9,8 +8,10 @@ import { i18n } from '/app/i18n' import { ProtocolAnalysisFailure } from '..' import { analyzeProtocol } from '/app/redux/protocol-storage' +import type { ComponentProps } from 'react' + const render = ( - props: Partial> = {} + props: Partial> = {} ) => { return renderWithProviders( diff --git a/app/src/organisms/Desktop/ProtocolAnalysisFailure/index.tsx b/app/src/organisms/Desktop/ProtocolAnalysisFailure/index.tsx index b2b904dd19d..3fe86f0385f 100644 --- a/app/src/organisms/Desktop/ProtocolAnalysisFailure/index.tsx +++ b/app/src/organisms/Desktop/ProtocolAnalysisFailure/index.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useState } from 'react' import { createPortal } from 'react-dom' import { useDispatch } from 'react-redux' import { useTranslation, Trans } from 'react-i18next' @@ -6,15 +6,15 @@ import { css } from 'styled-components' import { ALIGN_CENTER, - Btn, Banner, + Btn, Flex, JUSTIFY_FLEX_END, - Modal, JUSTIFY_SPACE_BETWEEN, + LegacyStyledText, + Modal, PrimaryButton, SPACING, - LegacyStyledText, TYPOGRAPHY, WRAP_REVERSE, } from '@opentrons/components' @@ -22,6 +22,7 @@ import { import { analyzeProtocol } from '/app/redux/protocol-storage' import { getTopPortalEl } from '/app/App/portal' +import type { MouseEventHandler } from 'react' import type { Dispatch } from '/app/redux/types' interface ProtocolAnalysisFailureProps { errors: string[] @@ -34,19 +35,19 @@ export function ProtocolAnalysisFailure( const { errors, protocolKey } = props const { t } = useTranslation(['protocol_list', 'shared']) const dispatch = useDispatch() - const [showErrorDetails, setShowErrorDetails] = React.useState(false) + const [showErrorDetails, setShowErrorDetails] = useState(false) - const handleClickShowDetails: React.MouseEventHandler = e => { + const handleClickShowDetails: MouseEventHandler = e => { e.preventDefault() e.stopPropagation() setShowErrorDetails(true) } - const handleClickHideDetails: React.MouseEventHandler = e => { + const handleClickHideDetails: MouseEventHandler = e => { e.preventDefault() e.stopPropagation() setShowErrorDetails(false) } - const handleClickReanalyze: React.MouseEventHandler = e => { + const handleClickReanalyze: MouseEventHandler = e => { e.preventDefault() e.stopPropagation() dispatch(analyzeProtocol(protocolKey)) diff --git a/app/src/organisms/Desktop/ProtocolDetails/AnnotatedSteps.tsx b/app/src/organisms/Desktop/ProtocolDetails/AnnotatedSteps.tsx index 52837338fca..ba871e50fef 100644 --- a/app/src/organisms/Desktop/ProtocolDetails/AnnotatedSteps.tsx +++ b/app/src/organisms/Desktop/ProtocolDetails/AnnotatedSteps.tsx @@ -1,4 +1,4 @@ -import { useMemo } from 'react' +import { useMemo, useState } from 'react' import { css } from 'styled-components' import { FLEX_ROBOT_TYPE } from '@opentrons/shared-data' @@ -12,31 +12,34 @@ import { LegacyStyledText, TYPOGRAPHY, OVERFLOW_AUTO, + Icon, + ALIGN_FLEX_START, + CURSOR_POINTER, } from '@opentrons/components' import { CommandIcon, CommandText } from '/app/molecules/Command' import { getLabwareDefinitionsFromCommands } from '/app/local-resources/labware' - import type { CompletedProtocolAnalysis, ProtocolAnalysisOutput, RunTimeCommand, LabwareDefinition2, } from '@opentrons/shared-data' +import type { GroupedCommands, LeafNode } from '/app/redux/protocol-storage' interface AnnotatedStepsProps { analysis: CompletedProtocolAnalysis | ProtocolAnalysisOutput + groupedCommands: GroupedCommands | null currentCommandIndex?: number } export function AnnotatedSteps(props: AnnotatedStepsProps): JSX.Element { - const { analysis, currentCommandIndex } = props + const { analysis, currentCommandIndex, groupedCommands } = props const HIDE_SCROLLBAR = css` ::-webkit-scrollbar { display: none; } ` - const isValidRobotSideAnalysis = analysis != null const allRunDefs = useMemo( () => @@ -45,6 +48,30 @@ export function AnnotatedSteps(props: AnnotatedStepsProps): JSX.Element { : [], [isValidRobotSideAnalysis] ) + const annotations = analysis?.commandAnnotations ?? [] + + // NOTE: isHighlighted is meant to show when running on the protocol in the run log + // but isn't in use during protocol details. Therefore, this info is not in use and is + // merely a proof-of-concept for when we do add this to the run log. + const groupedCommandsHighlightedInfo = groupedCommands?.map(node => { + if ('annotationIndex' in node) { + return { + ...node, + isHighlighted: node.subCommands.some(subNode => subNode.isHighlighted), + subCommands: node.subCommands.map(subNode => ({ + ...subNode, + isHighlighted: + currentCommandIndex === analysis.commands.indexOf(subNode.command), + })), + } + } else { + return { + ...node, + isHighlighted: + currentCommandIndex === analysis.commands.indexOf(node.command), + } + } + }) return ( - {analysis.commands.map((c, i) => ( - - ))} + {groupedCommandsHighlightedInfo != null && + groupedCommandsHighlightedInfo.length > 0 + ? groupedCommandsHighlightedInfo.map((c, i) => + 'annotationIndex' in c ? ( + + ) : ( + + ) + ) + : analysis.commands.map((c, i) => ( + + ))}
        ) } +interface AnnotatedGroupProps { + annotationType: string + subCommands: LeafNode[] + analysis: ProtocolAnalysisOutput | CompletedProtocolAnalysis + stepNumber: string + isHighlighted: boolean + allRunDefs: LabwareDefinition2[] +} +function AnnotatedGroup(props: AnnotatedGroupProps): JSX.Element { + const { + subCommands, + annotationType, + analysis, + stepNumber, + allRunDefs, + isHighlighted, + } = props + const [isExpanded, setIsExpanded] = useState(false) + const backgroundColor = isHighlighted ? COLORS.blue30 : COLORS.grey20 + return ( + { + setIsExpanded(!isExpanded) + }} + cursor={CURSOR_POINTER} + > + {isExpanded ? ( + + + + {stepNumber} + + + + {annotationType} + + + + + + {subCommands.map((c, i) => ( + + ))} + + + ) : ( + + {stepNumber} + + + {annotationType} + + + + + )} + + ) +} + interface IndividualCommandProps { command: RunTimeCommand analysis: ProtocolAnalysisOutput | CompletedProtocolAnalysis diff --git a/app/src/organisms/Desktop/ProtocolDetails/ProtocolLabwareDetails.tsx b/app/src/organisms/Desktop/ProtocolDetails/ProtocolLabwareDetails.tsx index 4fe95406355..cb74884c89e 100644 --- a/app/src/organisms/Desktop/ProtocolDetails/ProtocolLabwareDetails.tsx +++ b/app/src/organisms/Desktop/ProtocolDetails/ProtocolLabwareDetails.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useState } from 'react' import { createPortal } from 'react-dom' import { useTranslation } from 'react-i18next' import { @@ -23,6 +23,7 @@ import { Divider } from '/app/atoms/structure' import { getTopPortalEl } from '/app/App/portal' import { LabwareDetails } from '/app/organisms/Desktop/Labware/LabwareDetails' +import type { MouseEventHandler } from 'react' import type { LoadLabwareRunTimeCommand } from '@opentrons/shared-data' import type { LabwareDefAndDate } from '/app/local-resources/labware' @@ -164,9 +165,9 @@ export const LabwareDetailOverflowMenu = ( const [ showLabwareDetailSlideout, setShowLabwareDetailSlideout, - ] = React.useState(false) + ] = useState(false) - const handleClickMenuItem: React.MouseEventHandler = e => { + const handleClickMenuItem: MouseEventHandler = e => { e.preventDefault() setShowOverflowMenu(false) setShowLabwareDetailSlideout(true) diff --git a/app/src/organisms/Desktop/ProtocolDetails/ProtocolParameters/__tests__/ProtocolParameters.test.tsx b/app/src/organisms/Desktop/ProtocolDetails/ProtocolParameters/__tests__/ProtocolParameters.test.tsx index e52803cc054..78f8184f7a1 100644 --- a/app/src/organisms/Desktop/ProtocolDetails/ProtocolParameters/__tests__/ProtocolParameters.test.tsx +++ b/app/src/organisms/Desktop/ProtocolDetails/ProtocolParameters/__tests__/ProtocolParameters.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { describe, it, vi, beforeEach, afterEach } from 'vitest' import { screen } from '@testing-library/react' @@ -6,6 +5,7 @@ import { renderWithProviders } from '/app/__testing-utils__' import { i18n } from '/app/i18n' import { ProtocolParameters } from '..' +import type { ComponentProps } from 'react' import type { RunTimeParameter } from '@opentrons/shared-data' import type * as Components from '@opentrons/components' @@ -80,14 +80,14 @@ const mockRunTimeParameter: RunTimeParameter[] = [ }, ] -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, }) } describe('ProtocolParameters', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { diff --git a/app/src/organisms/Desktop/ProtocolDetails/RobotConfigurationDetails.tsx b/app/src/organisms/Desktop/ProtocolDetails/RobotConfigurationDetails.tsx index 5789a8af6e6..d53a6eba889 100644 --- a/app/src/organisms/Desktop/ProtocolDetails/RobotConfigurationDetails.tsx +++ b/app/src/organisms/Desktop/ProtocolDetails/RobotConfigurationDetails.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { Fragment } from 'react' import { useTranslation } from 'react-i18next' import { @@ -31,6 +31,7 @@ import { Divider } from '/app/atoms/structure' import { getRobotTypeDisplayName } from '../ProtocolsLanding/utils' import { getSlotsForThermocycler } from './utils' +import type { ReactNode } from 'react' import type { CutoutConfigProtocolSpec, LoadModuleRunTimeCommand, @@ -153,7 +154,7 @@ export const RobotConfigurationDetails = ( ) : null} {requiredModuleDetails.map((module, index) => { return ( - + } /> - + ) })} {nonStandardRequiredFixtureDetails.map((fixture, index) => { return ( - + } /> - + ) })} @@ -217,7 +218,7 @@ export const RobotConfigurationDetails = ( interface RobotConfigurationDetailsItemProps { label: string - item: React.ReactNode + item: ReactNode } export const RobotConfigurationDetailsItem = ( diff --git a/app/src/organisms/Desktop/ProtocolDetails/__tests__/ProtocolDetails.test.tsx b/app/src/organisms/Desktop/ProtocolDetails/__tests__/ProtocolDetails.test.tsx index 8ef02d4d67a..e9a75149199 100644 --- a/app/src/organisms/Desktop/ProtocolDetails/__tests__/ProtocolDetails.test.tsx +++ b/app/src/organisms/Desktop/ProtocolDetails/__tests__/ProtocolDetails.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { act, screen, waitFor } from '@testing-library/react' import { MemoryRouter } from 'react-router-dom' import { describe, it, beforeEach, vi, expect, afterEach } from 'vitest' @@ -26,6 +25,7 @@ import { import { storedProtocolData } from '/app/redux/protocol-storage/__fixtures__' import { ProtocolDetails } from '..' +import type { ComponentProps } from 'react' import type { Mock } from 'vitest' import type { ProtocolAnalysisOutput } from '@opentrons/shared-data' @@ -37,11 +37,13 @@ vi.mock('/app/organisms/Desktop/ChooseRobotToRunProtocolSlideout') vi.mock('/app/organisms/Desktop/SendProtocolToFlexSlideout') const render = ( - props: Partial> = {} + props: Partial> = {} ) => { return renderWithProviders( - + , { i18nInstance: i18n, diff --git a/app/src/organisms/Desktop/ProtocolDetails/__tests__/ProtocolLabwareDetails.test.tsx b/app/src/organisms/Desktop/ProtocolDetails/__tests__/ProtocolLabwareDetails.test.tsx index 93c215643cf..bc25bcb5569 100644 --- a/app/src/organisms/Desktop/ProtocolDetails/__tests__/ProtocolLabwareDetails.test.tsx +++ b/app/src/organisms/Desktop/ProtocolDetails/__tests__/ProtocolLabwareDetails.test.tsx @@ -1,10 +1,10 @@ -import type * as React from 'react' import { screen } from '@testing-library/react' import { describe, it, beforeEach, vi } from 'vitest' import { renderWithProviders } from '/app/__testing-utils__' import { i18n } from '/app/i18n' import { ProtocolLabwareDetails } from '../ProtocolLabwareDetails' +import type { ComponentProps } from 'react' import type { LoadLabwareRunTimeCommand } from '@opentrons/shared-data' import type { InfoScreen } from '@opentrons/components' @@ -67,14 +67,14 @@ const mockRequiredLabwareDetails = [ } as LoadLabwareRunTimeCommand, ] -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, })[0] } describe('ProtocolLabwareDetails', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { requiredLabwareDetails: mockRequiredLabwareDetails, diff --git a/app/src/organisms/Desktop/ProtocolDetails/__tests__/ProtocolLiquidsDetails.test.tsx b/app/src/organisms/Desktop/ProtocolDetails/__tests__/ProtocolLiquidsDetails.test.tsx index 69e55cc1097..9a6f233b312 100644 --- a/app/src/organisms/Desktop/ProtocolDetails/__tests__/ProtocolLiquidsDetails.test.tsx +++ b/app/src/organisms/Desktop/ProtocolDetails/__tests__/ProtocolLiquidsDetails.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { screen } from '@testing-library/react' import { describe, it, beforeEach, vi } from 'vitest' import { parseLiquidsInLoadOrder } from '@opentrons/shared-data' @@ -7,6 +6,7 @@ import { renderWithProviders } from '/app/__testing-utils__' import { i18n } from '/app/i18n' import { ProtocolLiquidsDetails } from '../ProtocolLiquidsDetails' +import type { ComponentProps } from 'react' import type * as SharedData from '@opentrons/shared-data' vi.mock('../../Desktop/Devices/ProtocolRun/SetupLiquids/SetupLiquidsList') @@ -18,14 +18,14 @@ vi.mock('@opentrons/shared-data', async importOriginal => { } }) -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, }) } describe('ProtocolLiquidsDetails', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { commands: [], diff --git a/app/src/organisms/Desktop/ProtocolDetails/__tests__/RobotConfigurationDetails.test.tsx b/app/src/organisms/Desktop/ProtocolDetails/__tests__/RobotConfigurationDetails.test.tsx index b823732ce95..65ce7d6d24d 100644 --- a/app/src/organisms/Desktop/ProtocolDetails/__tests__/RobotConfigurationDetails.test.tsx +++ b/app/src/organisms/Desktop/ProtocolDetails/__tests__/RobotConfigurationDetails.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { describe, it, expect, afterEach, vi } from 'vitest' import { screen } from '@testing-library/react' @@ -7,6 +6,8 @@ import { OT2_STANDARD_MODEL, FLEX_STANDARD_MODEL } from '@opentrons/shared-data' import { renderWithProviders } from '/app/__testing-utils__' import { i18n } from '/app/i18n' import { RobotConfigurationDetails } from '../RobotConfigurationDetails' + +import type { ComponentProps } from 'react' import type { LoadModuleRunTimeCommand } from '@opentrons/shared-data' const mockRequiredModuleDetails = [ @@ -57,16 +58,14 @@ const mockRequiredModuleDetails = [ } as LoadModuleRunTimeCommand, ] -const render = ( - props: React.ComponentProps -) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, })[0] } describe('RobotConfigurationDetails', () => { - let props: React.ComponentProps + let props: ComponentProps afterEach(() => { vi.clearAllMocks() diff --git a/app/src/organisms/Desktop/ProtocolDetails/index.tsx b/app/src/organisms/Desktop/ProtocolDetails/index.tsx index 3bdf4be672e..860c4170a69 100644 --- a/app/src/organisms/Desktop/ProtocolDetails/index.tsx +++ b/app/src/organisms/Desktop/ProtocolDetails/index.tsx @@ -42,9 +42,6 @@ import { getGripperDisplayName, getModuleType, getSimplestDeckConfigForProtocol, - parseInitialLoadedLabwareByAdapter, - parseInitialLoadedLabwareByModuleId, - parseInitialLoadedLabwareBySlot, parseInitialLoadedModulesBySlot, parseInitialPipetteNamesByMount, } from '@opentrons/shared-data' @@ -75,8 +72,15 @@ import { RobotConfigurationDetails } from './RobotConfigurationDetails' import { ProtocolParameters } from './ProtocolParameters' import { AnnotatedSteps } from './AnnotatedSteps' -import type { JsonConfig, PythonConfig } from '@opentrons/shared-data' -import type { StoredProtocolData } from '/app/redux/protocol-storage' +import type { + JsonConfig, + PythonConfig, + LoadLabwareRunTimeCommand, +} from '@opentrons/shared-data' +import type { + GroupedCommands, + StoredProtocolData, +} from '/app/redux/protocol-storage' import type { State, Dispatch } from '/app/redux/types' const GRID_STYLE = css` @@ -202,14 +206,22 @@ const ReadMoreContent = (props: ReadMoreContentProps): JSX.Element => { ) } -interface ProtocolDetailsProps extends StoredProtocolData {} +interface ProtocolDetailsProps extends StoredProtocolData { + groupedCommands: GroupedCommands | null +} export function ProtocolDetails( props: ProtocolDetailsProps ): JSX.Element | null { const trackEvent = useTrackEvent() const dispatch = useDispatch() - const { protocolKey, srcFileNames, mostRecentAnalysis, modified } = props + const { + protocolKey, + srcFileNames, + mostRecentAnalysis, + modified, + groupedCommands, + } = props const { t, i18n } = useTranslation(['protocol_details', 'shared']) const enableProtocolStats = useFeatureFlag('protocolStats') const enableProtocolTimeline = useFeatureFlag('protocolTimeline') @@ -265,28 +277,12 @@ export function ProtocolDetails( : null ) - const requiredLabwareDetails = - mostRecentAnalysis != null - ? map({ - ...parseInitialLoadedLabwareByModuleId( - mostRecentAnalysis.commands != null - ? mostRecentAnalysis.commands - : [] - ), - ...parseInitialLoadedLabwareBySlot( - mostRecentAnalysis.commands != null - ? mostRecentAnalysis.commands - : [] - ), - ...parseInitialLoadedLabwareByAdapter( - mostRecentAnalysis.commands != null - ? mostRecentAnalysis.commands - : [] - ), - }).filter( - labware => labware.result?.definition?.parameters?.format !== 'trash' - ) - : [] + const loadLabwareCommands = + mostRecentAnalysis?.commands.filter( + (command): command is LoadLabwareRunTimeCommand => + command.commandType === 'loadLabware' && + command.result?.definition.parameters.format !== 'trash' + ) ?? [] const protocolDisplayName = getProtocolDisplayName( protocolKey, @@ -323,7 +319,7 @@ export function ProtocolDetails( const contentsByTabName = { labware: ( - + ), robot_config: ( + ) : null, parameters: , } diff --git a/app/src/organisms/Desktop/ProtocolsLanding/ConfirmDeleteProtocolModal.tsx b/app/src/organisms/Desktop/ProtocolsLanding/ConfirmDeleteProtocolModal.tsx index 14d75238bcc..439d48a9f0d 100644 --- a/app/src/organisms/Desktop/ProtocolsLanding/ConfirmDeleteProtocolModal.tsx +++ b/app/src/organisms/Desktop/ProtocolsLanding/ConfirmDeleteProtocolModal.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { useTranslation } from 'react-i18next' import { AlertPrimaryButton, @@ -14,9 +13,11 @@ import { TYPOGRAPHY, } from '@opentrons/components' +import type { MouseEventHandler } from 'react' + interface ConfirmDeleteProtocolModalProps { - cancelDeleteProtocol: React.MouseEventHandler | undefined - handleClickDelete: React.MouseEventHandler + cancelDeleteProtocol: MouseEventHandler | undefined + handleClickDelete: MouseEventHandler } export function ConfirmDeleteProtocolModal( diff --git a/app/src/organisms/Desktop/ProtocolsLanding/ProtocolList.tsx b/app/src/organisms/Desktop/ProtocolsLanding/ProtocolList.tsx index 112999787d0..9cd58925e34 100644 --- a/app/src/organisms/Desktop/ProtocolsLanding/ProtocolList.tsx +++ b/app/src/organisms/Desktop/ProtocolsLanding/ProtocolList.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useState } from 'react' import { useDispatch, useSelector } from 'react-redux' import { useTranslation } from 'react-i18next' import { css } from 'styled-components' @@ -35,6 +35,7 @@ import { ProtocolUploadInput } from './ProtocolUploadInput' import { ProtocolCard } from './ProtocolCard' import { EmptyStateLinks } from './EmptyStateLinks' +import type { MouseEventHandler } from 'react' import type { StoredProtocolData, ProtocolSort, @@ -61,17 +62,17 @@ export function ProtocolList(props: ProtocolListProps): JSX.Element | null { const [ showImportProtocolSlideout, setShowImportProtocolSlideout, - ] = React.useState(false) + ] = useState(false) const [ showChooseRobotToRunProtocolSlideout, setShowChooseRobotToRunProtocolSlideout, - ] = React.useState(false) + ] = useState(false) const [ showSendProtocolToFlexSlideout, setShowSendProtocolToFlexSlideout, - ] = React.useState(false) + ] = useState(false) const sortBy = useSelector(getProtocolsDesktopSortKey) ?? 'alphabetical' - const [showSortByMenu, setShowSortByMenu] = React.useState(false) + const [showSortByMenu, setShowSortByMenu] = useState(false) const toggleSetShowSortByMenu = (): void => { setShowSortByMenu(!showSortByMenu) } @@ -80,13 +81,13 @@ export function ProtocolList(props: ProtocolListProps): JSX.Element | null { const [ selectedProtocol, setSelectedProtocol, - ] = React.useState(null) + ] = useState(null) const sortedStoredProtocols = useSortedProtocols(sortBy, storedProtocols) const dispatch = useDispatch() - const handleClickOutside: React.MouseEventHandler = e => { + const handleClickOutside: MouseEventHandler = e => { e.preventDefault() setShowSortByMenu(false) } diff --git a/app/src/organisms/Desktop/ProtocolsLanding/ProtocolOverflowMenu.tsx b/app/src/organisms/Desktop/ProtocolsLanding/ProtocolOverflowMenu.tsx index 0bcc71f8cc0..eacddbdaca3 100644 --- a/app/src/organisms/Desktop/ProtocolsLanding/ProtocolOverflowMenu.tsx +++ b/app/src/organisms/Desktop/ProtocolsLanding/ProtocolOverflowMenu.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { css } from 'styled-components' import { createPortal } from 'react-dom' import { useTranslation } from 'react-i18next' @@ -35,6 +34,7 @@ import { } from '/app/redux/protocol-storage' import { ConfirmDeleteProtocolModal } from './ConfirmDeleteProtocolModal' +import type { MouseEvent, MouseEventHandler } from 'react' import type { StyleProps } from '@opentrons/components' import type { StoredProtocolData } from '/app/redux/protocol-storage' import type { Dispatch } from '/app/redux/types' @@ -77,13 +77,13 @@ export function ProtocolOverflowMenu( const robotType = mostRecentAnalysis != null ? mostRecentAnalysis?.robotType ?? null : null - const handleClickShowInFolder: React.MouseEventHandler = e => { + const handleClickShowInFolder: MouseEventHandler = e => { e.preventDefault() e.stopPropagation() dispatch(viewProtocolSourceFolder(protocolKey)) setShowOverflowMenu(currentShowOverflowMenu => !currentShowOverflowMenu) } - const handleClickRun: React.MouseEventHandler = e => { + const handleClickRun: MouseEventHandler = e => { e.preventDefault() e.stopPropagation() trackEvent({ @@ -93,25 +93,25 @@ export function ProtocolOverflowMenu( handleRunProtocol(storedProtocolData) setShowOverflowMenu(currentShowOverflowMenu => !currentShowOverflowMenu) } - const handleClickSendToOT3: React.MouseEventHandler = e => { + const handleClickSendToOT3: MouseEventHandler = e => { e.preventDefault() e.stopPropagation() handleSendProtocolToFlex(storedProtocolData) setShowOverflowMenu(currentShowOverflowMenu => !currentShowOverflowMenu) } - const handleClickDelete: React.MouseEventHandler = e => { + const handleClickDelete: MouseEventHandler = e => { e.preventDefault() e.stopPropagation() confirmDeleteProtocol() setShowOverflowMenu(currentShowOverflowMenu => !currentShowOverflowMenu) } - const handleClickReanalyze: React.MouseEventHandler = e => { + const handleClickReanalyze: MouseEventHandler = e => { e.preventDefault() e.stopPropagation() dispatch(analyzeProtocol(protocolKey)) setShowOverflowMenu(currentShowOverflowMenu => !currentShowOverflowMenu) } - const handleClickTimeline: React.MouseEventHandler = e => { + const handleClickTimeline: MouseEventHandler = e => { e.preventDefault() navigate(`/protocols/${protocolKey}/timeline`) setShowOverflowMenu(prevShowOverflowMenu => !prevShowOverflowMenu) @@ -121,7 +121,7 @@ export function ProtocolOverflowMenu( { + onClick={(e: MouseEvent) => { e.stopPropagation() }} > @@ -195,7 +195,7 @@ export function ProtocolOverflowMenu( {showDeleteConfirmation ? createPortal( { + cancelDeleteProtocol={(e: MouseEvent) => { e.preventDefault() e.stopPropagation() cancelDeleteProtocol() diff --git a/app/src/organisms/Desktop/ProtocolsLanding/ProtocolUploadInput.tsx b/app/src/organisms/Desktop/ProtocolsLanding/ProtocolUploadInput.tsx index 35491c61d2d..17313bd567f 100644 --- a/app/src/organisms/Desktop/ProtocolsLanding/ProtocolUploadInput.tsx +++ b/app/src/organisms/Desktop/ProtocolsLanding/ProtocolUploadInput.tsx @@ -18,6 +18,7 @@ import { } from '/app/redux/analytics' import { useLogger } from '/app/logger' import { useToaster } from '/app/organisms/ToasterOven' +import { remote } from '/app/redux/shell/remote' import type { Dispatch } from '/app/redux/types' @@ -38,21 +39,23 @@ export function ProtocolUploadInput( const trackEvent = useTrackEvent() const { makeToast } = useToaster() - const handleUpload = (file: File): void => { - if (file.path === null) { - logger.warn('Failed to upload file, path not found') - } - if (isValidProtocolFileName(file.name)) { - dispatch(addProtocol(file.path)) - } else { - makeToast(t('incompatible_file_type') as string, ERROR_TOAST, { - closeButton: true, + const handleUpload = (file: File): Promise => { + return remote.getFilePathFrom(file).then(filePath => { + if (filePath == null) { + logger.warn('Failed to upload file, path not found') + } + if (isValidProtocolFileName(file.name)) { + dispatch(addProtocol(filePath)) + } else { + makeToast(t('incompatible_file_type') as string, ERROR_TOAST, { + closeButton: true, + }) + } + props.onUpload?.() + trackEvent({ + name: ANALYTICS_IMPORT_PROTOCOL_TO_APP, + properties: { protocolFileName: file.name }, }) - } - props.onUpload?.() - trackEvent({ - name: ANALYTICS_IMPORT_PROTOCOL_TO_APP, - properties: { protocolFileName: file.name }, }) } @@ -64,7 +67,7 @@ export function ProtocolUploadInput( > { - handleUpload(file) + void handleUpload(file) }} uploadText={t('valid_file_types')} dragAndDropText={ diff --git a/app/src/organisms/Desktop/ProtocolsLanding/__tests__/ConfirmDeleteProtocolModal.test.tsx b/app/src/organisms/Desktop/ProtocolsLanding/__tests__/ConfirmDeleteProtocolModal.test.tsx index dd19a80d133..8d040851a56 100644 --- a/app/src/organisms/Desktop/ProtocolsLanding/__tests__/ConfirmDeleteProtocolModal.test.tsx +++ b/app/src/organisms/Desktop/ProtocolsLanding/__tests__/ConfirmDeleteProtocolModal.test.tsx @@ -1,20 +1,19 @@ -import type * as React from 'react' import { fireEvent, screen } from '@testing-library/react' import { describe, it, vi, expect, beforeEach, afterEach } from 'vitest' import { renderWithProviders } from '/app/__testing-utils__' import { i18n } from '/app/i18n' import { ConfirmDeleteProtocolModal } from '../ConfirmDeleteProtocolModal' -const render = ( - props: React.ComponentProps -) => { +import type { ComponentProps } from 'react' + +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, })[0] } describe('ConfirmDeleteProtocolModal', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { diff --git a/app/src/organisms/Desktop/ProtocolsLanding/__tests__/ProtocolList.test.tsx b/app/src/organisms/Desktop/ProtocolsLanding/__tests__/ProtocolList.test.tsx index 59271bc279c..6f3190712c4 100644 --- a/app/src/organisms/Desktop/ProtocolsLanding/__tests__/ProtocolList.test.tsx +++ b/app/src/organisms/Desktop/ProtocolsLanding/__tests__/ProtocolList.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { BrowserRouter } from 'react-router-dom' import { when } from 'vitest-when' import { fireEvent, screen } from '@testing-library/react' @@ -16,13 +15,15 @@ import { useSortedProtocols } from '../hooks' import { EmptyStateLinks } from '../EmptyStateLinks' import { ProtocolCard } from '../ProtocolCard' +import type { ComponentProps } from 'react' + vi.mock('../hooks') vi.mock('/app/redux/protocol-storage') vi.mock('/app/redux/config') vi.mock('../EmptyStateLinks') vi.mock('../ProtocolCard') -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders( @@ -34,7 +35,7 @@ const render = (props: React.ComponentProps) => { } describe('ProtocolList', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { diff --git a/app/src/organisms/Desktop/ProtocolsLanding/__tests__/UploadInput.test.tsx b/app/src/organisms/Desktop/ProtocolsLanding/__tests__/UploadInput.test.tsx index e3b8a8f4cdb..f03aaf861e7 100644 --- a/app/src/organisms/Desktop/ProtocolsLanding/__tests__/UploadInput.test.tsx +++ b/app/src/organisms/Desktop/ProtocolsLanding/__tests__/UploadInput.test.tsx @@ -8,10 +8,16 @@ import { ANALYTICS_IMPORT_PROTOCOL_TO_APP, } from '/app/redux/analytics' import { ProtocolUploadInput } from '../ProtocolUploadInput' +import { remote } from '/app/redux/shell/remote' import type { Mock } from 'vitest' vi.mock('/app/redux/analytics') +vi.mock('/app/redux/shell/remote', () => ({ + remote: { + getFilePathFrom: vi.fn(), + }, +})) describe('ProtocolUploadInput', () => { let onUpload: Mock @@ -31,6 +37,7 @@ describe('ProtocolUploadInput', () => { onUpload = vi.fn() trackEvent = vi.fn() vi.mocked(useTrackEvent).mockReturnValue(trackEvent) + vi.mocked(remote.getFilePathFrom).mockResolvedValue('mockFileName') }) afterEach(() => { vi.resetAllMocks() @@ -56,16 +63,24 @@ describe('ProtocolUploadInput', () => { fireEvent.click(button) expect(input.click).toHaveBeenCalled() }) - it('calls onUpload callback on choose file and trigger analytics event', () => { + it('calls onUpload callback on choose file and trigger analytics event', async () => { render() const input = screen.getByTestId('file_input') + + const mockFile = new File(['mockContent'], 'mockFileName', { + type: 'text/plain', + }) + fireEvent.change(input, { - target: { files: [{ path: 'dummyFile', name: 'dummyName' }] }, + target: { files: [mockFile] }, }) - expect(onUpload).toHaveBeenCalled() - expect(trackEvent).toHaveBeenCalledWith({ - name: ANALYTICS_IMPORT_PROTOCOL_TO_APP, - properties: { protocolFileName: 'dummyName' }, + + await vi.waitFor(() => { + expect(onUpload).toHaveBeenCalled() + expect(trackEvent).toHaveBeenCalledWith({ + name: ANALYTICS_IMPORT_PROTOCOL_TO_APP, + properties: { protocolFileName: 'mockFileName' }, + }) }) }) }) diff --git a/app/src/organisms/Desktop/ProtocolsLanding/__tests__/hooks.test.tsx b/app/src/organisms/Desktop/ProtocolsLanding/__tests__/hooks.test.tsx index 5d1485fe795..0c10c8fb1ef 100644 --- a/app/src/organisms/Desktop/ProtocolsLanding/__tests__/hooks.test.tsx +++ b/app/src/organisms/Desktop/ProtocolsLanding/__tests__/hooks.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { Provider } from 'react-redux' import { createStore } from 'redux' import { renderHook } from '@testing-library/react' @@ -8,6 +7,7 @@ import { FLEX_ROBOT_TYPE, OT2_ROBOT_TYPE } from '@opentrons/shared-data' import { useSortedProtocols } from '../hooks' +import type { FunctionComponent, ReactNode } from 'react' import type { Store } from 'redux' import type { ProtocolAnalysisOutput } from '@opentrons/shared-data' import type { StoredProtocolData } from '/app/redux/protocol-storage' @@ -294,7 +294,7 @@ describe('useSortedProtocols', () => { }) it('should return an object with protocols sorted alphabetically', () => { - const wrapper: React.FunctionComponent<{ children: React.ReactNode }> = ({ + const wrapper: FunctionComponent<{ children: ReactNode }> = ({ children, }) => {children} @@ -318,7 +318,7 @@ describe('useSortedProtocols', () => { }) it('should return an object with protocols sorted reverse alphabetically', () => { - const wrapper: React.FunctionComponent<{ children: React.ReactNode }> = ({ + const wrapper: FunctionComponent<{ children: ReactNode }> = ({ children, }) => {children} @@ -342,7 +342,7 @@ describe('useSortedProtocols', () => { }) it('should return an object with protocols sorted by most recent modified data', () => { - const wrapper: React.FunctionComponent<{ children: React.ReactNode }> = ({ + const wrapper: FunctionComponent<{ children: ReactNode }> = ({ children, }) => {children} @@ -366,7 +366,7 @@ describe('useSortedProtocols', () => { }) it('should return an object with protocols sorted by oldest modified data', () => { - const wrapper: React.FunctionComponent<{ children: React.ReactNode }> = ({ + const wrapper: FunctionComponent<{ children: ReactNode }> = ({ children, }) => {children} @@ -390,7 +390,7 @@ describe('useSortedProtocols', () => { }) it('should return an object with protocols sorted by flex then ot-2', () => { - const wrapper: React.FunctionComponent<{ children: React.ReactNode }> = ({ + const wrapper: FunctionComponent<{ children: ReactNode }> = ({ children, }) => {children} @@ -413,7 +413,7 @@ describe('useSortedProtocols', () => { ) }) it('should return an object with protocols sorted by ot-2 then flex', () => { - const wrapper: React.FunctionComponent<{ children: React.ReactNode }> = ({ + const wrapper: FunctionComponent<{ children: ReactNode }> = ({ children, }) => {children} diff --git a/app/src/organisms/Desktop/RobotSettingsCalibration/CalibrationDataDownload.tsx b/app/src/organisms/Desktop/RobotSettingsCalibration/CalibrationDataDownload.tsx index 4731df09ae4..47058491684 100644 --- a/app/src/organisms/Desktop/RobotSettingsCalibration/CalibrationDataDownload.tsx +++ b/app/src/organisms/Desktop/RobotSettingsCalibration/CalibrationDataDownload.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { saveAs } from 'file-saver' import { useTranslation, Trans } from 'react-i18next' @@ -17,6 +16,8 @@ import { useInstrumentsQuery, useModulesQuery, } from '@opentrons/react-api-client' +import { FLEX_ROBOT_TYPE, OT2_ROBOT_TYPE } from '@opentrons/shared-data' + import { TertiaryButton } from '/app/atoms/buttons' import { useDeckCalibrationData, @@ -30,6 +31,8 @@ import { import { useRobot, useIsFlex } from '/app/redux-resources/robots' import { useIsEstopNotDisengaged } from '/app/resources/devices/hooks/useIsEstopNotDisengaged' +import type { MouseEventHandler } from 'react' + // TODO(bc, 2022-02-08): replace with support article when available const FLEX_CALIBRATION_SUPPORT_URL = 'https://support.opentrons.com' @@ -68,11 +71,13 @@ export function CalibrationDataDownload({ tipLengthCalibrations != null && tipLengthCalibrations.length > 0 - const onClickSaveAs: React.MouseEventHandler = e => { + const onClickSaveAs: MouseEventHandler = e => { e.preventDefault() doTrackEvent({ name: ANALYTICS_CALIBRATION_DATA_DOWNLOADED, - properties: {}, + properties: { + robotType: isFlex ? FLEX_ROBOT_TYPE : OT2_ROBOT_TYPE, + }, }) saveAs( new Blob([ diff --git a/app/src/organisms/Desktop/RobotSettingsCalibration/CalibrationDetails/OverflowMenu.tsx b/app/src/organisms/Desktop/RobotSettingsCalibration/CalibrationDetails/OverflowMenu.tsx index ee95d1abf73..1db51029549 100644 --- a/app/src/organisms/Desktop/RobotSettingsCalibration/CalibrationDetails/OverflowMenu.tsx +++ b/app/src/organisms/Desktop/RobotSettingsCalibration/CalibrationDetails/OverflowMenu.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useState, useEffect } from 'react' import { useTranslation } from 'react-i18next' import { saveAs } from 'file-saver' import { css } from 'styled-components' @@ -35,6 +35,7 @@ import { FLOWS } from '/app/organisms/PipetteWizardFlows/constants' import { useIsEstopNotDisengaged } from '/app/resources/devices' import { useAttachedPipettesFromInstrumentsQuery } from '/app/resources/instruments' +import type { MouseEvent } from 'react' import type { Mount } from '@opentrons/components' import type { PipetteName } from '@opentrons/shared-data' import type { DeleteCalRequestParams } from '@opentrons/api-client' @@ -82,10 +83,9 @@ export function OverflowMenu({ const tipLengthCalibrations = useAllTipLengthCalibrationsQuery().data?.data const { isRunRunning: isRunning } = useRunStatuses() - const [ - showPipetteWizardFlows, - setShowPipetteWizardFlows, - ] = React.useState(false) + const [showPipetteWizardFlows, setShowPipetteWizardFlows] = useState( + false + ) const isEstopNotDisengaged = useIsEstopNotDisengaged(robotName) const isPipetteForFlex = isFlexPipette(pipetteName as PipetteName) const ot3PipCal = @@ -103,7 +103,7 @@ export function OverflowMenu({ calType === 'pipetteOffset' ? applicablePipetteOffsetCal != null : applicableTipLengthCal != null - const handleRecalibrate = (e: React.MouseEvent): void => { + const handleRecalibrate = (e: MouseEvent): void => { e.preventDefault() if ( !isRunning && @@ -116,7 +116,7 @@ export function OverflowMenu({ setShowOverflowMenu(currentShowOverflowMenu => !currentShowOverflowMenu) } - const handleDownload = (e: React.MouseEvent): void => { + const handleDownload = (e: MouseEvent): void => { e.preventDefault() doTrackEvent({ name: ANALYTICS_CALIBRATION_DATA_DOWNLOADED, @@ -137,19 +137,18 @@ export function OverflowMenu({ setShowOverflowMenu(currentShowOverflowMenu => !currentShowOverflowMenu) } - React.useEffect(() => { + useEffect(() => { if (isRunning) { updateRobotStatus(true) } }, [isRunning, updateRobotStatus]) const { deleteCalibration } = useDeleteCalibrationMutation() - const [ - selectedPipette, - setSelectedPipette, - ] = React.useState(SINGLE_MOUNT_PIPETTES) + const [selectedPipette, setSelectedPipette] = useState( + SINGLE_MOUNT_PIPETTES + ) - const handleDeleteCalibration = (e: React.MouseEvent): void => { + const handleDeleteCalibration = (e: MouseEvent): void => { e.preventDefault() let params: DeleteCalRequestParams if (calType === 'pipetteOffset') { diff --git a/app/src/organisms/Desktop/RobotSettingsCalibration/CalibrationDetails/__tests__/ModuleCalibrationItems.test.tsx b/app/src/organisms/Desktop/RobotSettingsCalibration/CalibrationDetails/__tests__/ModuleCalibrationItems.test.tsx index 8cb0dd62dc6..ee0529a1c4e 100644 --- a/app/src/organisms/Desktop/RobotSettingsCalibration/CalibrationDetails/__tests__/ModuleCalibrationItems.test.tsx +++ b/app/src/organisms/Desktop/RobotSettingsCalibration/CalibrationDetails/__tests__/ModuleCalibrationItems.test.tsx @@ -1,6 +1,6 @@ -import type * as React from 'react' import { screen } from '@testing-library/react' import { describe, it, expect, vi, beforeEach } from 'vitest' +import { ABSORBANCE_READER_TYPE } from '@opentrons/shared-data' import { i18n } from '/app/i18n' import { renderWithProviders } from '/app/__testing-utils__' import { mockFetchModulesSuccessActionPayloadModules } from '/app/redux/modules/__fixtures__' @@ -8,8 +8,8 @@ import { ModuleCalibrationOverflowMenu } from '../ModuleCalibrationOverflowMenu' import { formatLastCalibrated } from '../utils' import { ModuleCalibrationItems } from '../ModuleCalibrationItems' +import type { ComponentProps } from 'react' import type { AttachedModule } from '@opentrons/api-client' -import { ABSORBANCE_READER_TYPE } from '@opentrons/shared-data' vi.mock('../ModuleCalibrationOverflowMenu') @@ -55,7 +55,7 @@ const mockCalibratedModule = { const ROBOT_NAME = 'mockRobot' const render = ( - props: React.ComponentProps + props: ComponentProps ): ReturnType => { return renderWithProviders(, { i18nInstance: i18n, @@ -63,7 +63,7 @@ const render = ( } describe('ModuleCalibrationItems', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { diff --git a/app/src/organisms/Desktop/RobotSettingsCalibration/CalibrationDetails/__tests__/ModuleCalibrationOverflowMenu.test.tsx b/app/src/organisms/Desktop/RobotSettingsCalibration/CalibrationDetails/__tests__/ModuleCalibrationOverflowMenu.test.tsx index 47b5dbec249..2807269794b 100644 --- a/app/src/organisms/Desktop/RobotSettingsCalibration/CalibrationDetails/__tests__/ModuleCalibrationOverflowMenu.test.tsx +++ b/app/src/organisms/Desktop/RobotSettingsCalibration/CalibrationDetails/__tests__/ModuleCalibrationOverflowMenu.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { fireEvent, screen, waitFor } from '@testing-library/react' import { when } from 'vitest-when' import { describe, it, expect, vi, beforeEach } from 'vitest' @@ -11,6 +10,7 @@ import { useIsEstopNotDisengaged } from '/app/resources/devices/hooks/useIsEstop import { ModuleCalibrationOverflowMenu } from '../ModuleCalibrationOverflowMenu' +import type { ComponentProps } from 'react' import type { Mount } from '@opentrons/components' vi.mock('@opentrons/react-api-client') @@ -87,7 +87,7 @@ const mockTCHeating = { } as any const render = ( - props: React.ComponentProps + props: ComponentProps ) => { return renderWithProviders(, { i18nInstance: i18n, @@ -97,7 +97,7 @@ const render = ( const ROBOT_NAME = 'mockRobot' describe('ModuleCalibrationOverflowMenu', () => { - let props: React.ComponentProps + let props: ComponentProps let mockChainLiveCommands = vi.fn() beforeEach(() => { diff --git a/app/src/organisms/Desktop/RobotSettingsCalibration/CalibrationDetails/__tests__/OverflowMenu.test.tsx b/app/src/organisms/Desktop/RobotSettingsCalibration/CalibrationDetails/__tests__/OverflowMenu.test.tsx index a0d2ff20096..cf106ccc6b3 100644 --- a/app/src/organisms/Desktop/RobotSettingsCalibration/CalibrationDetails/__tests__/OverflowMenu.test.tsx +++ b/app/src/organisms/Desktop/RobotSettingsCalibration/CalibrationDetails/__tests__/OverflowMenu.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { fireEvent, screen } from '@testing-library/react' import { when } from 'vitest-when' import '@testing-library/jest-dom/vitest' @@ -26,10 +25,11 @@ import { renderWithProviders } from '/app/__testing-utils__' import { useIsEstopNotDisengaged } from '/app/resources/devices' import { OverflowMenu } from '../OverflowMenu' +import type { ComponentProps } from 'react' import type { Mount } from '@opentrons/components' const render = ( - props: React.ComponentProps + props: ComponentProps ): ReturnType => { return renderWithProviders(, { i18nInstance: i18n, @@ -81,7 +81,7 @@ const RUN_STATUSES = { const mockUpdateRobotStatus = vi.fn() describe('OverflowMenu', () => { - let props: React.ComponentProps + let props: ComponentProps const mockDeleteCalibration = vi.fn() beforeEach(() => { diff --git a/app/src/organisms/Desktop/RobotSettingsCalibration/CalibrationDetails/__tests__/PipetteOffsetCalibrationItems.test.tsx b/app/src/organisms/Desktop/RobotSettingsCalibration/CalibrationDetails/__tests__/PipetteOffsetCalibrationItems.test.tsx index 2a6cf82726f..bca259d41c5 100644 --- a/app/src/organisms/Desktop/RobotSettingsCalibration/CalibrationDetails/__tests__/PipetteOffsetCalibrationItems.test.tsx +++ b/app/src/organisms/Desktop/RobotSettingsCalibration/CalibrationDetails/__tests__/PipetteOffsetCalibrationItems.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { when } from 'vitest-when' import { screen } from '@testing-library/react' import { describe, it, expect, vi, beforeEach } from 'vitest' @@ -19,11 +18,12 @@ import { PipetteOffsetCalibrationItems } from '../PipetteOffsetCalibrationItems' import { OverflowMenu } from '../OverflowMenu' import { formatLastCalibrated } from '../utils' +import type { ComponentProps } from 'react' import type { Mount } from '@opentrons/components' import type { AttachedPipettesByMount } from '/app/redux/pipettes/types' const render = ( - props: React.ComponentProps + props: ComponentProps ): ReturnType => { return renderWithProviders(, { i18nInstance: i18n, @@ -73,7 +73,7 @@ const mockAttachedPipettes: AttachedPipettesByMount = { const mockUpdateRobotStatus = vi.fn() describe('PipetteOffsetCalibrationItems', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { vi.mocked(useAttachedPipettesFromInstrumentsQuery).mockReturnValue({ diff --git a/app/src/organisms/Desktop/RobotSettingsCalibration/CalibrationDetails/__tests__/TipLengthCalibrationItems.test.tsx b/app/src/organisms/Desktop/RobotSettingsCalibration/CalibrationDetails/__tests__/TipLengthCalibrationItems.test.tsx index 014ec28ee2c..19270615a06 100644 --- a/app/src/organisms/Desktop/RobotSettingsCalibration/CalibrationDetails/__tests__/TipLengthCalibrationItems.test.tsx +++ b/app/src/organisms/Desktop/RobotSettingsCalibration/CalibrationDetails/__tests__/TipLengthCalibrationItems.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { screen } from '@testing-library/react' import { describe, it, expect, vi, beforeEach } from 'vitest' import '@testing-library/jest-dom/vitest' @@ -6,6 +5,8 @@ import { i18n } from '/app/i18n' import { renderWithProviders } from '/app/__testing-utils__' import { TipLengthCalibrationItems } from '../TipLengthCalibrationItems' import { OverflowMenu } from '../OverflowMenu' + +import type { ComponentProps } from 'react' import type { Mount } from '@opentrons/components' vi.mock('/app/redux/custom-labware/selectors') @@ -54,14 +55,14 @@ const mockTipLengthCalibrations = [ const mockUpdateRobotStatus = vi.fn() const render = ( - props: React.ComponentProps + props: ComponentProps ): ReturnType => { return renderWithProviders(, { i18nInstance: i18n, }) } describe('TipLengthCalibrationItems', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { vi.mocked(OverflowMenu).mockReturnValue(
        mock overflow menu
        ) diff --git a/app/src/organisms/Desktop/RobotSettingsCalibration/__tests__/CalibrationDataDownload.test.tsx b/app/src/organisms/Desktop/RobotSettingsCalibration/__tests__/CalibrationDataDownload.test.tsx index b84b25e2e16..b50a892ca73 100644 --- a/app/src/organisms/Desktop/RobotSettingsCalibration/__tests__/CalibrationDataDownload.test.tsx +++ b/app/src/organisms/Desktop/RobotSettingsCalibration/__tests__/CalibrationDataDownload.test.tsx @@ -16,6 +16,7 @@ import { useModulesQuery, } from '@opentrons/react-api-client' import { instrumentsResponseFixture } from '@opentrons/api-client' +import { OT2_ROBOT_TYPE } from '@opentrons/shared-data' import { i18n } from '/app/i18n' import { @@ -145,7 +146,7 @@ describe('CalibrationDataDownload', () => { fireEvent.click(downloadButton) expect(mockTrackEvent).toHaveBeenCalledWith({ name: ANALYTICS_CALIBRATION_DATA_DOWNLOADED, - properties: {}, + properties: { robotType: OT2_ROBOT_TYPE }, }) }) diff --git a/app/src/organisms/Desktop/RobotSettingsCalibration/__tests__/CalibrationHealthCheck.test.tsx b/app/src/organisms/Desktop/RobotSettingsCalibration/__tests__/CalibrationHealthCheck.test.tsx index ee965c9d042..c434f6c1392 100644 --- a/app/src/organisms/Desktop/RobotSettingsCalibration/__tests__/CalibrationHealthCheck.test.tsx +++ b/app/src/organisms/Desktop/RobotSettingsCalibration/__tests__/CalibrationHealthCheck.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import userEvent from '@testing-library/user-event' import { fireEvent, screen, waitFor } from '@testing-library/react' import { describe, it, expect, vi, beforeEach } from 'vitest' @@ -20,14 +19,13 @@ import { } from '/app/redux/calibration/tip-length/__fixtures__' import { mockAttachedPipette } from '/app/redux/pipettes/__fixtures__' import { useRunStatuses } from '/app/resources/runs' - import { useAttachedPipettes, useAttachedPipetteCalibrations, } from '/app/resources/instruments' - import { CalibrationHealthCheck } from '../CalibrationHealthCheck' +import type { ComponentProps } from 'react' import type { AttachedPipettesByMount, PipetteCalibrationsByMount, @@ -66,7 +64,7 @@ let mockTrackEvent: any const mockDispatchRequests = vi.fn() const render = ( - props?: Partial> + props?: Partial> ) => { return renderWithProviders( > + props?: Partial> ) => { return renderWithProviders( , diff --git a/app/src/organisms/Desktop/RobotSettingsCalibration/__tests__/RobotSettingsGripperCalibration.test.tsx b/app/src/organisms/Desktop/RobotSettingsCalibration/__tests__/RobotSettingsGripperCalibration.test.tsx index e4a4d48eea1..ff0570443d1 100644 --- a/app/src/organisms/Desktop/RobotSettingsCalibration/__tests__/RobotSettingsGripperCalibration.test.tsx +++ b/app/src/organisms/Desktop/RobotSettingsCalibration/__tests__/RobotSettingsGripperCalibration.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { when } from 'vitest-when' import { fireEvent, screen } from '@testing-library/react' import { describe, it, expect, vi, beforeEach } from 'vitest' @@ -10,6 +9,7 @@ import { formatLastCalibrated } from '../CalibrationDetails/utils' import { useIsEstopNotDisengaged } from '/app/resources/devices/hooks/useIsEstopNotDisengaged' import { RobotSettingsGripperCalibration } from '../RobotSettingsGripperCalibration' +import type { ComponentProps } from 'react' import type { GripperData } from '@opentrons/api-client' vi.mock('/app/organisms/GripperWizardFlows') @@ -35,7 +35,7 @@ const mockNotCalibratedGripper = { const ROBOT_NAME = 'mockRobot' const render = ( - props: React.ComponentProps + props: ComponentProps ) => { return renderWithProviders(, { i18nInstance: i18n, @@ -43,7 +43,7 @@ const render = ( } describe('RobotSettingsGripperCalibration', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { vi.mocked(formatLastCalibrated).mockReturnValue('last calibrated 1/2/3') vi.mocked(GripperWizardFlows).mockReturnValue(<>Mock Wizard Flow) diff --git a/app/src/organisms/Desktop/RobotSettingsCalibration/__tests__/RobotSettingsModuleCalibration.test.tsx b/app/src/organisms/Desktop/RobotSettingsCalibration/__tests__/RobotSettingsModuleCalibration.test.tsx index 95b71a450af..63d5014da79 100644 --- a/app/src/organisms/Desktop/RobotSettingsCalibration/__tests__/RobotSettingsModuleCalibration.test.tsx +++ b/app/src/organisms/Desktop/RobotSettingsCalibration/__tests__/RobotSettingsModuleCalibration.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { screen } from '@testing-library/react' import { describe, it, vi, beforeEach } from 'vitest' import { i18n } from '/app/i18n' @@ -7,10 +6,12 @@ import { mockFetchModulesSuccessActionPayloadModules } from '/app/redux/modules/ import { RobotSettingsModuleCalibration } from '../RobotSettingsModuleCalibration' import { ModuleCalibrationItems } from '../CalibrationDetails/ModuleCalibrationItems' +import type { ComponentProps } from 'react' + vi.mock('../CalibrationDetails/ModuleCalibrationItems') const render = ( - props: React.ComponentProps + props: ComponentProps ) => { return renderWithProviders(, { i18nInstance: i18n, @@ -20,7 +21,7 @@ const render = ( const ROBOT_NAME = 'mockRobot' describe('RobotSettingsModuleCalibration', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { diff --git a/app/src/organisms/Desktop/RobotSettingsCalibration/__tests__/RobotSettingsPipetteOffsetCalibration.test.tsx b/app/src/organisms/Desktop/RobotSettingsCalibration/__tests__/RobotSettingsPipetteOffsetCalibration.test.tsx index 0f628dddfef..6b883499831 100644 --- a/app/src/organisms/Desktop/RobotSettingsCalibration/__tests__/RobotSettingsPipetteOffsetCalibration.test.tsx +++ b/app/src/organisms/Desktop/RobotSettingsCalibration/__tests__/RobotSettingsPipetteOffsetCalibration.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { when } from 'vitest-when' import { describe, it, vi, beforeEach } from 'vitest' import { screen } from '@testing-library/react' @@ -18,6 +17,7 @@ import { useIsFlex } from '/app/redux-resources/robots' import { RobotSettingsPipetteOffsetCalibration } from '../RobotSettingsPipetteOffsetCalibration' import { PipetteOffsetCalibrationItems } from '../CalibrationDetails/PipetteOffsetCalibrationItems' +import type { ComponentProps } from 'react' import type { FormattedPipetteOffsetCalibration } from '..' vi.mock('/app/organisms/Desktop/Devices/hooks') @@ -29,9 +29,7 @@ const mockFormattedPipetteOffsetCalibrations: FormattedPipetteOffsetCalibration[ const mockUpdateRobotStatus = vi.fn() const render = ( - props?: Partial< - React.ComponentProps - > + props?: Partial> ) => { return renderWithProviders( ) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, })[0] } describe('InterventionTicks', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { vi.mocked(Tick).mockImplementation(({ index }) => (
        MOCK TICK at index: {index}
        diff --git a/app/src/organisms/Desktop/RunProgressMeter/__tests__/RunProgressMeter.test.tsx b/app/src/organisms/Desktop/RunProgressMeter/__tests__/RunProgressMeter.test.tsx index 928bd2572a9..469c056c087 100644 --- a/app/src/organisms/Desktop/RunProgressMeter/__tests__/RunProgressMeter.test.tsx +++ b/app/src/organisms/Desktop/RunProgressMeter/__tests__/RunProgressMeter.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { when } from 'vitest-when' import { screen } from '@testing-library/react' import { describe, it, expect, vi, beforeEach } from 'vitest' @@ -37,6 +36,7 @@ import { RunProgressMeter } from '..' import { renderWithProviders } from '/app/__testing-utils__' import { useRunningStepCounts } from '/app/resources/protocols/hooks' +import type { ComponentProps } from 'react' import type { RunCommandSummary } from '@opentrons/api-client' import type * as ApiClient from '@opentrons/react-api-client' @@ -55,7 +55,7 @@ vi.mock('../../Devices/hooks') vi.mock('/app/resources/protocols/hooks') vi.mock('/app/redux-resources/robots') -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, })[0] @@ -65,7 +65,7 @@ const NON_DETERMINISTIC_RUN_ID = 'nonDeterministicID' const ROBOT_NAME = 'otie' describe('RunProgressMeter', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { vi.mocked(ProgressBar).mockReturnValue(
        MOCK PROGRESS BAR
        ) vi.mocked(InterventionModal).mockReturnValue( diff --git a/app/src/organisms/Desktop/RunProgressMeter/hooks/useRunProgressCopy.tsx b/app/src/organisms/Desktop/RunProgressMeter/hooks/useRunProgressCopy.tsx index 65e2f27d6b3..aaa90997167 100644 --- a/app/src/organisms/Desktop/RunProgressMeter/hooks/useRunProgressCopy.tsx +++ b/app/src/organisms/Desktop/RunProgressMeter/hooks/useRunProgressCopy.tsx @@ -4,7 +4,6 @@ import { RUN_STATUS_BLOCKED_BY_OPEN_DOOR, RUN_STATUS_IDLE, } from '@opentrons/api-client' -import type * as React from 'react' import { useTranslation } from 'react-i18next' import { getCommandTextData } from '/app/local-resources/commands' @@ -13,6 +12,7 @@ import { LegacyStyledText } from '@opentrons/components' import { CommandText } from '/app/molecules/Command' import { TERMINAL_RUN_STATUSES } from '../constants' +import type { ReactNode } from 'react' import type { CommandDetail, RunStatus } from '@opentrons/api-client' import type { CompletedProtocolAnalysis, @@ -21,7 +21,7 @@ import type { } from '@opentrons/shared-data' interface UseRunProgressResult { - currentStepContents: React.ReactNode + currentStepContents: ReactNode stepCountStr: string | null progressPercentage: number } diff --git a/app/src/organisms/Desktop/RunProgressMeter/index.tsx b/app/src/organisms/Desktop/RunProgressMeter/index.tsx index fb158f5d686..ecf38cd31f6 100644 --- a/app/src/organisms/Desktop/RunProgressMeter/index.tsx +++ b/app/src/organisms/Desktop/RunProgressMeter/index.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { createPortal } from 'react-dom' import { useTranslation } from 'react-i18next' import { css } from 'styled-components' @@ -49,6 +48,8 @@ import { useRobotType } from '/app/redux-resources/robots' import { useRunningStepCounts } from '/app/resources/protocols/hooks' import { useRunProgressCopy } from './hooks' +import type { MouseEventHandler } from 'react' + interface RunProgressMeterProps { runId: string robotName: string @@ -92,7 +93,7 @@ export function RunProgressMeter(props: RunProgressMeterProps): JSX.Element { const { downloadRunLog } = useDownloadRunLog(robotName, runId) - const onDownloadClick: React.MouseEventHandler = e => { + const onDownloadClick: MouseEventHandler = e => { if (downloadIsDisabled) return false e.preventDefault() e.stopPropagation() diff --git a/app/src/organisms/Desktop/SendProtocolToFlexSlideout/__tests__/SendProtocolToFlexSlideout.test.tsx b/app/src/organisms/Desktop/SendProtocolToFlexSlideout/__tests__/SendProtocolToFlexSlideout.test.tsx index 5ed8f96fb1a..08e60f84729 100644 --- a/app/src/organisms/Desktop/SendProtocolToFlexSlideout/__tests__/SendProtocolToFlexSlideout.test.tsx +++ b/app/src/organisms/Desktop/SendProtocolToFlexSlideout/__tests__/SendProtocolToFlexSlideout.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { fireEvent, screen } from '@testing-library/react' import { describe, it, expect, vi, beforeEach } from 'vitest' import '@testing-library/jest-dom/vitest' @@ -36,6 +35,7 @@ import { storedProtocolData as storedProtocolDataFixture } from '/app/redux/prot import { SendProtocolToFlexSlideout } from '..' import { useNotifyAllRunsQuery } from '/app/resources/runs' +import type { ComponentProps } from 'react' import type * as ApiClient from '@opentrons/react-api-client' vi.mock('@opentrons/react-api-client', async importOriginal => { @@ -53,9 +53,7 @@ vi.mock('/app/redux/custom-labware') vi.mock('/app/redux/protocol-storage/selectors') vi.mock('/app/resources/runs') -const render = ( - props: React.ComponentProps -) => { +const render = (props: ComponentProps) => { return renderWithProviders( diff --git a/app/src/organisms/Desktop/SystemLanguagePreferenceModal/__tests__/SystemLanguagePreferenceModal.test.tsx b/app/src/organisms/Desktop/SystemLanguagePreferenceModal/__tests__/SystemLanguagePreferenceModal.test.tsx index a91d7389072..af4c4feb5d0 100644 --- a/app/src/organisms/Desktop/SystemLanguagePreferenceModal/__tests__/SystemLanguagePreferenceModal.test.tsx +++ b/app/src/organisms/Desktop/SystemLanguagePreferenceModal/__tests__/SystemLanguagePreferenceModal.test.tsx @@ -2,15 +2,17 @@ import { useNavigate } from 'react-router-dom' import { fireEvent, screen } from '@testing-library/react' import '@testing-library/jest-dom/vitest' import { describe, it, vi, afterEach, beforeEach, expect } from 'vitest' -import { when } from 'vitest-when' import { renderWithProviders } from '/app/__testing-utils__' import { i18n } from '/app/i18n' +import { + ANALYTICS_LANGUAGE_UPDATED_DESKTOP_APP_MODAL, + useTrackEvent, +} from '/app/redux/analytics' import { getAppLanguage, getStoredSystemLanguage, updateConfigValue, - useFeatureFlag, } from '/app/redux/config' import { getSystemLanguage } from '/app/redux/shell' import { SystemLanguagePreferenceModal } from '..' @@ -18,6 +20,7 @@ import { SystemLanguagePreferenceModal } from '..' vi.mock('react-router-dom') vi.mock('/app/redux/config') vi.mock('/app/redux/shell') +vi.mock('/app/redux/analytics') const render = () => { return renderWithProviders(, { @@ -26,6 +29,7 @@ const render = () => { } const mockNavigate = vi.fn() +const mockTrackEvent = vi.fn() const MOCK_DEFAULT_LANGUAGE = 'en-US' @@ -34,10 +38,8 @@ describe('SystemLanguagePreferenceModal', () => { vi.mocked(getAppLanguage).mockReturnValue(MOCK_DEFAULT_LANGUAGE) vi.mocked(getSystemLanguage).mockReturnValue(MOCK_DEFAULT_LANGUAGE) vi.mocked(getStoredSystemLanguage).mockReturnValue(MOCK_DEFAULT_LANGUAGE) - when(vi.mocked(useFeatureFlag)) - .calledWith('enableLocalization') - .thenReturn(true) vi.mocked(useNavigate).mockReturnValue(mockNavigate) + vi.mocked(useTrackEvent).mockReturnValue(mockTrackEvent) }) afterEach(() => { vi.resetAllMocks() @@ -73,6 +75,14 @@ describe('SystemLanguagePreferenceModal', () => { 'language.systemLanguage', MOCK_DEFAULT_LANGUAGE ) + expect(mockTrackEvent).toBeCalledWith({ + name: ANALYTICS_LANGUAGE_UPDATED_DESKTOP_APP_MODAL, + properties: { + language: MOCK_DEFAULT_LANGUAGE, + systemLanguage: MOCK_DEFAULT_LANGUAGE, + modalType: 'appBootModal', + }, + }) }) it('should default to English (US) if system language is unsupported', () => { @@ -95,6 +105,14 @@ describe('SystemLanguagePreferenceModal', () => { MOCK_DEFAULT_LANGUAGE ) expect(updateConfigValue).toBeCalledWith('language.systemLanguage', 'es-MX') + expect(mockTrackEvent).toBeCalledWith({ + name: ANALYTICS_LANGUAGE_UPDATED_DESKTOP_APP_MODAL, + properties: { + language: MOCK_DEFAULT_LANGUAGE, + systemLanguage: 'es-MX', + modalType: 'appBootModal', + }, + }) }) it('should set a supported app language when system language is an unsupported locale of the same language', () => { @@ -117,6 +135,14 @@ describe('SystemLanguagePreferenceModal', () => { MOCK_DEFAULT_LANGUAGE ) expect(updateConfigValue).toBeCalledWith('language.systemLanguage', 'en-GB') + expect(mockTrackEvent).toBeCalledWith({ + name: ANALYTICS_LANGUAGE_UPDATED_DESKTOP_APP_MODAL, + properties: { + language: MOCK_DEFAULT_LANGUAGE, + systemLanguage: 'en-GB', + modalType: 'appBootModal', + }, + }) }) it('should render the correct header, description, and buttons when system language changes', () => { @@ -144,6 +170,14 @@ describe('SystemLanguagePreferenceModal', () => { 'language.systemLanguage', 'zh-CN' ) + expect(mockTrackEvent).toBeCalledWith({ + name: ANALYTICS_LANGUAGE_UPDATED_DESKTOP_APP_MODAL, + properties: { + language: 'zh-CN', + systemLanguage: 'zh-CN', + modalType: 'systemLanguageUpdateModal', + }, + }) fireEvent.click(secondaryButton) expect(updateConfigValue).toHaveBeenNthCalledWith( 3, @@ -173,6 +207,14 @@ describe('SystemLanguagePreferenceModal', () => { 'language.systemLanguage', 'zh-Hant' ) + expect(mockTrackEvent).toBeCalledWith({ + name: ANALYTICS_LANGUAGE_UPDATED_DESKTOP_APP_MODAL, + properties: { + language: 'zh-CN', + systemLanguage: 'zh-Hant', + modalType: 'systemLanguageUpdateModal', + }, + }) fireEvent.click(secondaryButton) expect(updateConfigValue).toHaveBeenNthCalledWith( 3, @@ -180,4 +222,16 @@ describe('SystemLanguagePreferenceModal', () => { 'zh-Hant' ) }) + + it('should not open update modal when system language changes to an unsuppported language', () => { + vi.mocked(getSystemLanguage).mockReturnValue('es-MX') + render() + + expect(screen.queryByRole('button', { name: 'Don’t change' })).toBeNull() + expect( + screen.queryByRole('button', { + name: 'Use system language', + }) + ).toBeNull() + }) }) diff --git a/app/src/organisms/Desktop/SystemLanguagePreferenceModal/index.tsx b/app/src/organisms/Desktop/SystemLanguagePreferenceModal/index.tsx index 1a3a0d7d9ba..f135c0fe10a 100644 --- a/app/src/organisms/Desktop/SystemLanguagePreferenceModal/index.tsx +++ b/app/src/organisms/Desktop/SystemLanguagePreferenceModal/index.tsx @@ -16,11 +16,14 @@ import { } from '@opentrons/components' import { LANGUAGES } from '/app/i18n' +import { + ANALYTICS_LANGUAGE_UPDATED_DESKTOP_APP_MODAL, + useTrackEvent, +} from '/app/redux/analytics' import { getAppLanguage, getStoredSystemLanguage, updateConfigValue, - useFeatureFlag, } from '/app/redux/config' import { getSystemLanguage } from '/app/redux/shell' @@ -33,8 +36,7 @@ type ArrayElement< export function SystemLanguagePreferenceModal(): JSX.Element | null { const { i18n, t } = useTranslation(['app_settings', 'shared', 'branded']) - const enableLocalization = useFeatureFlag('enableLocalization') - + const trackEvent = useTrackEvent() const [currentOption, setCurrentOption] = useState( LANGUAGES[0] ) @@ -46,11 +48,7 @@ export function SystemLanguagePreferenceModal(): JSX.Element | null { const storedSystemLanguage = useSelector(getStoredSystemLanguage) const showBootModal = appLanguage == null && systemLanguage != null - const showUpdateModal = - appLanguage != null && - systemLanguage != null && - storedSystemLanguage != null && - systemLanguage !== storedSystemLanguage + const [showUpdateModal, setShowUpdateModal] = useState(false) const title = showUpdateModal ? t('system_language_preferences_update') @@ -72,6 +70,16 @@ export function SystemLanguagePreferenceModal(): JSX.Element | null { const handlePrimaryClick = (): void => { dispatch(updateConfigValue('language.appLanguage', currentOption.value)) dispatch(updateConfigValue('language.systemLanguage', systemLanguage)) + trackEvent({ + name: ANALYTICS_LANGUAGE_UPDATED_DESKTOP_APP_MODAL, + properties: { + language: currentOption.value, + systemLanguage, + modalType: showUpdateModal + ? 'systemLanguageUpdateModal' + : 'appBootModal', + }, + }) } const handleDropdownClick = (value: string): void => { @@ -120,10 +128,17 @@ export function SystemLanguagePreferenceModal(): JSX.Element | null { void i18n.changeLanguage(systemLanguage) } } + // only show update modal if we support the language their system has updated to + setShowUpdateModal( + appLanguage != null && + matchedSystemLanguageOption != null && + storedSystemLanguage != null && + systemLanguage !== storedSystemLanguage + ) } }, [i18n, systemLanguage, showBootModal]) - return enableLocalization && (showBootModal || showUpdateModal) ? ( + return showBootModal || showUpdateModal ? ( diff --git a/app/src/organisms/Desktop/UpdateAppModal/__tests__/UpdateAppModal.test.tsx b/app/src/organisms/Desktop/UpdateAppModal/__tests__/UpdateAppModal.test.tsx index 3131bee25a3..4469abc159a 100644 --- a/app/src/organisms/Desktop/UpdateAppModal/__tests__/UpdateAppModal.test.tsx +++ b/app/src/organisms/Desktop/UpdateAppModal/__tests__/UpdateAppModal.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { screen, fireEvent } from '@testing-library/react' import { describe, it, expect, vi, beforeEach } from 'vitest' import '@testing-library/jest-dom/vitest' @@ -9,10 +8,11 @@ import { renderWithProviders } from '/app/__testing-utils__' import { useRemoveActiveAppUpdateToast } from '../../Alerts' import { UpdateAppModal, RELEASE_NOTES_URL_BASE } from '..' +import type { ComponentProps } from 'react' +import type * as Dom from 'react-router-dom' import type { State } from '/app/redux/types' import type { ShellUpdateState } from '/app/redux/shell/types' import type * as ShellState from '/app/redux/shell' -import type * as Dom from 'react-router-dom' import type { UpdateAppModalProps } from '..' vi.mock('/app/redux/shell/update', async importOriginal => { @@ -35,7 +35,7 @@ vi.mock('../../Alerts') const getShellUpdateState = Shell.getShellUpdateState -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, initialState: { @@ -45,7 +45,7 @@ const render = (props: React.ComponentProps) => { } describe('UpdateAppModal', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { diff --git a/app/src/organisms/Desktop/UpdateAppModal/index.tsx b/app/src/organisms/Desktop/UpdateAppModal/index.tsx index 450824c27f5..99a54ba0dc7 100644 --- a/app/src/organisms/Desktop/UpdateAppModal/index.tsx +++ b/app/src/organisms/Desktop/UpdateAppModal/index.tsx @@ -93,7 +93,14 @@ export function UpdateAppModal(props: UpdateAppModalProps): JSX.Element { error, info: updateInfo, } = updateState - const releaseNotes = updateInfo?.releaseNotes + let releaseNotesText = updateInfo?.releaseNotes + if (Array.isArray(releaseNotesText)) { + // it is unclear to me why/how electron-updater would ever expose + // release notes this way, but this should never happen... + // this string representation should always be returned + releaseNotesText = releaseNotesText[0].note + } + const { t } = useTranslation(['app_settings', 'branded']) const navigate = useNavigate() const { removeActiveAppUpdateToast } = useRemoveActiveAppUpdateToast() @@ -192,7 +199,7 @@ export function UpdateAppModal(props: UpdateAppModalProps): JSX.Element { {t('branded:update_requires_restarting_app')} - + ) : null} diff --git a/app/src/organisms/Desktop/UpdateRobotBanner/__tests__/UpdateRobotBanner.test.tsx b/app/src/organisms/Desktop/UpdateRobotBanner/__tests__/UpdateRobotBanner.test.tsx index e7d90f70ea3..e1b67d7c9dc 100644 --- a/app/src/organisms/Desktop/UpdateRobotBanner/__tests__/UpdateRobotBanner.test.tsx +++ b/app/src/organisms/Desktop/UpdateRobotBanner/__tests__/UpdateRobotBanner.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { describe, it, expect, vi, beforeEach } from 'vitest' import '@testing-library/jest-dom/vitest' import { fireEvent, screen } from '@testing-library/react' @@ -12,19 +11,21 @@ import { import { handleUpdateBuildroot } from '../../Devices/RobotSettings/UpdateBuildroot' import { UpdateRobotBanner } from '..' +import type { ComponentProps } from 'react' + vi.mock('/app/redux/robot-update') vi.mock('../../Devices/RobotSettings/UpdateBuildroot') const getUpdateDisplayInfo = Buildroot.getRobotUpdateDisplayInfo -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, })[0] } describe('UpdateRobotBanner', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { diff --git a/app/src/organisms/Desktop/UpdateRobotBanner/index.tsx b/app/src/organisms/Desktop/UpdateRobotBanner/index.tsx index e5d7d2d0e85..390f4bc3ac1 100644 --- a/app/src/organisms/Desktop/UpdateRobotBanner/index.tsx +++ b/app/src/organisms/Desktop/UpdateRobotBanner/index.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { useSelector } from 'react-redux' import { useTranslation } from 'react-i18next' import { @@ -13,6 +12,7 @@ import { import { getRobotUpdateDisplayInfo } from '/app/redux/robot-update' import { handleUpdateBuildroot } from '../Devices/RobotSettings/UpdateBuildroot' +import type { MouseEvent } from 'react' import type { StyleProps } from '@opentrons/components' import type { State } from '/app/redux/types' import type { DiscoveredRobot } from '/app/redux/discovery/types' @@ -35,7 +35,7 @@ export function UpdateRobotBanner( robot !== null && robot.healthStatus === 'ok' ? ( { + onClick={(e: MouseEvent) => { e.stopPropagation() }} flexDirection={DIRECTION_COLUMN} diff --git a/app/src/organisms/DeviceDetailsDeckConfiguration/AddFixtureModal.tsx b/app/src/organisms/DeviceDetailsDeckConfiguration/AddFixtureModal.tsx index 16062be3034..b4b8f462d7f 100644 --- a/app/src/organisms/DeviceDetailsDeckConfiguration/AddFixtureModal.tsx +++ b/app/src/organisms/DeviceDetailsDeckConfiguration/AddFixtureModal.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useState } from 'react' import { useTranslation } from 'react-i18next' import { css } from 'styled-components' @@ -7,15 +7,15 @@ import { BORDERS, Btn, COLORS, + CURSOR_DEFAULT, DIRECTION_COLUMN, DIRECTION_ROW, Flex, JUSTIFY_SPACE_BETWEEN, LegacyStyledText, - SPACING, Modal, + SPACING, TYPOGRAPHY, - CURSOR_DEFAULT, } from '@opentrons/components' import { useModulesQuery, @@ -25,11 +25,11 @@ import { getCutoutDisplayName, getFixtureDisplayName, ABSORBANCE_READER_CUTOUTS, - ABSORBANCE_READER_V1, ABSORBANCE_READER_V1_FIXTURE, + ABSORBANCE_READER_V1, HEATER_SHAKER_CUTOUTS, - HEATERSHAKER_MODULE_V1, HEATERSHAKER_MODULE_V1_FIXTURE, + HEATERSHAKER_MODULE_V1, MAGNETIC_BLOCK_V1_FIXTURE, SINGLE_CENTER_CUTOUTS, SINGLE_LEFT_CUTOUTS, @@ -38,8 +38,8 @@ import { STAGING_AREA_RIGHT_SLOT_FIXTURE, STAGING_AREA_SLOT_WITH_MAGNETIC_BLOCK_V1_FIXTURE, TEMPERATURE_MODULE_CUTOUTS, - TEMPERATURE_MODULE_V2, TEMPERATURE_MODULE_V2_FIXTURE, + TEMPERATURE_MODULE_V2, THERMOCYCLER_MODULE_CUTOUTS, THERMOCYCLER_MODULE_V2, THERMOCYCLER_V2_FRONT_FIXTURE, @@ -54,6 +54,7 @@ import { TertiaryButton } from '/app/atoms/buttons' import { OddModal } from '/app/molecules/OddModal' import { useNotifyDeckConfigurationQuery } from '/app/resources/deck_configuration/' +import type { MouseEventHandler } from 'react' import type { CutoutConfig, CutoutId, @@ -101,9 +102,7 @@ export function AddFixtureModal({ // only show provided options if given as props initialStage = 'providedOptions' } - const [optionStage, setOptionStage] = React.useState( - initialStage - ) + const [optionStage, setOptionStage] = useState(initialStage) const modalHeader: OddModalHeaderBaseProps = { title: t('add_to_slot', { @@ -370,8 +369,8 @@ export function AddFixtureModal({ }} aria-label="back" paddingX={SPACING.spacing16} - marginTop={'1.44rem'} - marginBottom={'0.56rem'} + marginTop="1.44rem" + marginBottom="0.56rem" > {t('shared:go_back')} @@ -425,7 +424,7 @@ const GO_BACK_BUTTON_STYLE = css` ` interface FixtureOptionProps { - onClickHandler: React.MouseEventHandler + onClickHandler: MouseEventHandler optionName: string buttonText: string isOnDevice: boolean diff --git a/app/src/organisms/DeviceDetailsDeckConfiguration/__tests__/AddFixtureModal.test.tsx b/app/src/organisms/DeviceDetailsDeckConfiguration/__tests__/AddFixtureModal.test.tsx index 450a64cc0e6..955c9e0f86b 100644 --- a/app/src/organisms/DeviceDetailsDeckConfiguration/__tests__/AddFixtureModal.test.tsx +++ b/app/src/organisms/DeviceDetailsDeckConfiguration/__tests__/AddFixtureModal.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { fireEvent, screen } from '@testing-library/react' import { describe, it, beforeEach, vi, expect, afterEach } from 'vitest' @@ -16,6 +15,7 @@ import { i18n } from '/app/i18n' import { AddFixtureModal } from '../AddFixtureModal' import { useNotifyDeckConfigurationQuery } from '/app/resources/deck_configuration' +import type { ComponentProps } from 'react' import type { UseQueryResult } from 'react-query' import type { DeckConfiguration } from '@opentrons/shared-data' import type { Modules } from '@opentrons/api-client' @@ -26,14 +26,14 @@ vi.mock('/app/resources/deck_configuration') const mockCloseModal = vi.fn() const mockUpdateDeckConfiguration = vi.fn() -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, }) } describe('Touchscreen AddFixtureModal', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { @@ -88,7 +88,7 @@ describe('Touchscreen AddFixtureModal', () => { }) describe('Desktop AddFixtureModal', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { diff --git a/app/src/organisms/DeviceDetailsDeckConfiguration/__tests__/DeckConfigurationDiscardChangesModal.test.tsx b/app/src/organisms/DeviceDetailsDeckConfiguration/__tests__/DeckConfigurationDiscardChangesModal.test.tsx index fd0e56ffa4d..b0b213170dc 100644 --- a/app/src/organisms/DeviceDetailsDeckConfiguration/__tests__/DeckConfigurationDiscardChangesModal.test.tsx +++ b/app/src/organisms/DeviceDetailsDeckConfiguration/__tests__/DeckConfigurationDiscardChangesModal.test.tsx @@ -1,10 +1,11 @@ -import type * as React from 'react' import { fireEvent, screen } from '@testing-library/react' import { describe, it, beforeEach, vi, expect } from 'vitest' import { renderWithProviders } from '/app/__testing-utils__' import { i18n } from '/app/i18n' import { DeckConfigurationDiscardChangesModal } from '../DeckConfigurationDiscardChangesModal' + +import type { ComponentProps } from 'react' import type { NavigateFunction } from 'react-router-dom' const mockFunc = vi.fn() @@ -19,7 +20,7 @@ vi.mock('react-router-dom', async importOriginal => { }) const render = ( - props: React.ComponentProps + props: ComponentProps ) => { return renderWithProviders( , @@ -30,7 +31,7 @@ const render = ( } describe('DeckConfigurationDiscardChangesModal', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { diff --git a/app/src/organisms/DeviceDetailsDeckConfiguration/__tests__/DeckFixtureSetupInstructionsModal.test.tsx b/app/src/organisms/DeviceDetailsDeckConfiguration/__tests__/DeckFixtureSetupInstructionsModal.test.tsx index ddc9ff33194..bfd218f5e97 100644 --- a/app/src/organisms/DeviceDetailsDeckConfiguration/__tests__/DeckFixtureSetupInstructionsModal.test.tsx +++ b/app/src/organisms/DeviceDetailsDeckConfiguration/__tests__/DeckFixtureSetupInstructionsModal.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { fireEvent, screen } from '@testing-library/react' import { describe, it, beforeEach, vi, expect } from 'vitest' @@ -6,12 +5,14 @@ import { renderWithProviders } from '/app/__testing-utils__' import { i18n } from '/app/i18n' import { DeckFixtureSetupInstructionsModal } from '../DeckFixtureSetupInstructionsModal' +import type { ComponentProps } from 'react' + const mockFunc = vi.fn() const PNG_FILE_NAME = '/app/src/assets/images/on-device-display/deck_fixture_setup_qrcode.png' const render = ( - props: React.ComponentProps + props: ComponentProps ) => { return renderWithProviders(, { i18nInstance: i18n, @@ -19,7 +20,7 @@ const render = ( } describe('Touchscreen DeckFixtureSetupInstructionsModal', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { @@ -49,7 +50,7 @@ describe('Touchscreen DeckFixtureSetupInstructionsModal', () => { }) describe('Desktop DeckFixtureSetupInstructionsModal', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { diff --git a/app/src/organisms/DeviceDetailsDeckConfiguration/__tests__/DeviceDetailsDeckConfiguration.test.tsx b/app/src/organisms/DeviceDetailsDeckConfiguration/__tests__/DeviceDetailsDeckConfiguration.test.tsx index 16ef3db90a7..409f0d3f452 100644 --- a/app/src/organisms/DeviceDetailsDeckConfiguration/__tests__/DeviceDetailsDeckConfiguration.test.tsx +++ b/app/src/organisms/DeviceDetailsDeckConfiguration/__tests__/DeviceDetailsDeckConfiguration.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { fireEvent, screen } from '@testing-library/react' import { when } from 'vitest-when' import { describe, it, beforeEach, vi, afterEach } from 'vitest' @@ -23,6 +22,7 @@ import { useNotifyDeckConfigurationQuery, } from '/app/resources/deck_configuration' +import type { ComponentProps } from 'react' import type { UseQueryResult } from 'react-query' import type { MaintenanceRun } from '@opentrons/api-client' import type { DeckConfiguration } from '@opentrons/shared-data' @@ -62,7 +62,7 @@ const mockCurrnetMaintenanceRun = { } as MaintenanceRun const render = ( - props: React.ComponentProps + props: ComponentProps ) => { return renderWithProviders(, { i18nInstance: i18n, @@ -70,7 +70,7 @@ const render = ( } describe('DeviceDetailsDeckConfiguration', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { diff --git a/app/src/organisms/DropTipWizardFlows/TipsAttachedModal.tsx b/app/src/organisms/DropTipWizardFlows/TipsAttachedModal.tsx index 7198d8bb5ea..86778afe97b 100644 --- a/app/src/organisms/DropTipWizardFlows/TipsAttachedModal.tsx +++ b/app/src/organisms/DropTipWizardFlows/TipsAttachedModal.tsx @@ -19,8 +19,8 @@ import { useHomePipettes } from '/app/local-resources/instruments' import type { HostConfig } from '@opentrons/api-client' import type { OddModalHeaderBaseProps } from '/app/molecules/OddModal/types' import type { UseHomePipettesProps } from '/app/local-resources/instruments' -import type { PipetteWithTip } from './hooks' import type { PipetteDetails } from '/app/resources/maintenance_runs' +import type { PipetteWithTip } from '/app/resources/instruments' type TipsAttachedModalProps = Pick & { aPipetteWithTip: PipetteWithTip diff --git a/app/src/organisms/DropTipWizardFlows/__tests__/DropTipWizard.test.tsx b/app/src/organisms/DropTipWizardFlows/__tests__/DropTipWizard.test.tsx index d1108fc3c18..9ea6a6f2363 100644 --- a/app/src/organisms/DropTipWizardFlows/__tests__/DropTipWizard.test.tsx +++ b/app/src/organisms/DropTipWizardFlows/__tests__/DropTipWizard.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { vi, describe, it, expect, beforeEach } from 'vitest' import { screen } from '@testing-library/react' @@ -31,6 +30,8 @@ import { CONFIRM_POSITION, } from '../constants' +import type { ComponentProps } from 'react' + vi.mock('/app/molecules/InProgressModal') vi.mock('../ExitConfirmation') vi.mock('../steps') @@ -38,7 +39,7 @@ vi.mock('../ErrorInfo') vi.mock('../DropTipWizardHeader') const renderDropTipWizardContainer = ( - props: React.ComponentProps + props: ComponentProps ) => { return renderWithProviders(, { i18nInstance: i18n, @@ -46,7 +47,7 @@ const renderDropTipWizardContainer = ( } describe('DropTipWizardContainer', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = mockDropTipWizardContainerProps @@ -75,7 +76,7 @@ describe('DropTipWizardContainer', () => { }) const renderDropTipWizardContent = ( - props: React.ComponentProps + props: ComponentProps ) => { return renderWithProviders(, { i18nInstance: i18n, @@ -83,7 +84,7 @@ const renderDropTipWizardContent = ( } describe('DropTipWizardContent', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = mockDropTipWizardContainerProps diff --git a/app/src/organisms/DropTipWizardFlows/__tests__/DropTipWizardHeader.test.tsx b/app/src/organisms/DropTipWizardFlows/__tests__/DropTipWizardHeader.test.tsx index c5720adf4ab..74bb70f6a8e 100644 --- a/app/src/organisms/DropTipWizardFlows/__tests__/DropTipWizardHeader.test.tsx +++ b/app/src/organisms/DropTipWizardFlows/__tests__/DropTipWizardHeader.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { beforeEach, describe, expect, it, vi } from 'vitest' import { screen } from '@testing-library/react' @@ -10,17 +9,18 @@ import { DropTipWizardHeader, } from '../DropTipWizardHeader' +import type { ComponentProps } from 'react' import type { Mock } from 'vitest' import type { UseWizardExitHeaderProps } from '../DropTipWizardHeader' -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, })[0] } describe('DropTipWizardHeader', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = mockDropTipWizardContainerProps diff --git a/app/src/organisms/DropTipWizardFlows/__tests__/TipsAttachedModal.test.tsx b/app/src/organisms/DropTipWizardFlows/__tests__/TipsAttachedModal.test.tsx index 917c770c10e..2a71920c4fc 100644 --- a/app/src/organisms/DropTipWizardFlows/__tests__/TipsAttachedModal.test.tsx +++ b/app/src/organisms/DropTipWizardFlows/__tests__/TipsAttachedModal.test.tsx @@ -14,7 +14,7 @@ import { useDropTipWizardFlows } from '..' import type { Mock } from 'vitest' import type { PipetteModelSpecs } from '@opentrons/shared-data' import type { HostConfig } from '@opentrons/api-client' -import type { PipetteWithTip } from '../hooks' +import type { PipetteWithTip } from '/app/resources/instruments' vi.mock('/app/resources/runs/useCloseCurrentRun') vi.mock('..') diff --git a/app/src/organisms/DropTipWizardFlows/hooks/__tests__/useTipAttachmentStatus.test.tsx b/app/src/organisms/DropTipWizardFlows/hooks/__tests__/useTipAttachmentStatus.test.tsx deleted file mode 100644 index 6d9d25719d2..00000000000 --- a/app/src/organisms/DropTipWizardFlows/hooks/__tests__/useTipAttachmentStatus.test.tsx +++ /dev/null @@ -1,119 +0,0 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import { act, renderHook } from '@testing-library/react' - -import { getPipetteModelSpecs } from '@opentrons/shared-data' -import { useInstrumentsQuery } from '@opentrons/react-api-client' - -import { mockPipetteInfo } from '/app/redux/pipettes/__fixtures__' -import { getPipettesWithTipAttached } from '../useTipAttachmentStatus/getPipettesWithTipAttached' -import { DropTipWizard } from '../../DropTipWizard' -import { useTipAttachmentStatus } from '../useTipAttachmentStatus' - -import type { Mock } from 'vitest' -import type { PipetteModelSpecs } from '@opentrons/shared-data' -import type { PipetteWithTip } from '../useTipAttachmentStatus' - -vi.mock('@opentrons/shared-data', async importOriginal => { - const actual = await importOriginal() - return { - ...actual, - getPipetteModelSpecs: vi.fn(), - } -}) -vi.mock('@opentrons/react-api-client') -vi.mock('../useTipAttachmentStatus/getPipettesWithTipAttached') -vi.mock('../../DropTipWizard') - -const MOCK_ACTUAL_PIPETTE = { - ...mockPipetteInfo.pipetteSpecs, - model: 'model', - tipLength: { - value: 20, - }, -} as PipetteModelSpecs - -const mockPipetteWithTip: PipetteWithTip = { - mount: 'left', - specs: MOCK_ACTUAL_PIPETTE, -} - -const mockSecondPipetteWithTip: PipetteWithTip = { - mount: 'right', - specs: MOCK_ACTUAL_PIPETTE, -} - -const mockPipettesWithTip: PipetteWithTip[] = [ - mockPipetteWithTip, - mockSecondPipetteWithTip, -] - -describe('useTipAttachmentStatus', () => { - let mockGetPipettesWithTipAttached: Mock - - beforeEach(() => { - mockGetPipettesWithTipAttached = vi.mocked(getPipettesWithTipAttached) - vi.mocked(getPipetteModelSpecs).mockReturnValue(MOCK_ACTUAL_PIPETTE) - vi.mocked(DropTipWizard).mockReturnValue(
        MOCK DROP TIP WIZ
        ) - mockGetPipettesWithTipAttached.mockResolvedValue(mockPipettesWithTip) - vi.mocked(useInstrumentsQuery).mockReturnValue({ data: {} } as any) - }) - - afterEach(() => { - vi.clearAllMocks() - }) - - it('should return the correct initial state', () => { - const { result } = renderHook(() => useTipAttachmentStatus({} as any)) - - expect(result.current.areTipsAttached).toBe(false) - expect(result.current.aPipetteWithTip).toEqual(null) - }) - - it('should determine tip status and update state accordingly', async () => { - const { result } = renderHook(() => useTipAttachmentStatus({} as any)) - - await act(async () => { - await result.current.determineTipStatus() - }) - - expect(result.current.areTipsAttached).toBe(true) - expect(result.current.aPipetteWithTip).toEqual(mockPipetteWithTip) - }) - - it('should reset tip status', async () => { - const { result } = renderHook(() => useTipAttachmentStatus({} as any)) - - await act(async () => { - await result.current.determineTipStatus() - result.current.resetTipStatus() - }) - - expect(result.current.areTipsAttached).toBe(false) - expect(result.current.aPipetteWithTip).toEqual(null) - }) - - it('should set tip status resolved and update state', async () => { - const { result } = renderHook(() => useTipAttachmentStatus({} as any)) - - await act(async () => { - await result.current.determineTipStatus() - result.current.setTipStatusResolved() - }) - - expect(result.current.aPipetteWithTip).toEqual(mockSecondPipetteWithTip) - }) - - it('should call onEmptyCache callback when cache becomes empty', async () => { - mockGetPipettesWithTipAttached.mockResolvedValueOnce([mockPipetteWithTip]) - - const onEmptyCacheMock = vi.fn() - const { result } = renderHook(() => useTipAttachmentStatus({} as any)) - - await act(async () => { - await result.current.determineTipStatus() - result.current.setTipStatusResolved(onEmptyCacheMock) - }) - - expect(onEmptyCacheMock).toHaveBeenCalled() - }) -}) diff --git a/app/src/organisms/DropTipWizardFlows/hooks/index.ts b/app/src/organisms/DropTipWizardFlows/hooks/index.ts index 3f3f531a9d8..f3145d7d083 100644 --- a/app/src/organisms/DropTipWizardFlows/hooks/index.ts +++ b/app/src/organisms/DropTipWizardFlows/hooks/index.ts @@ -1,6 +1,5 @@ export * from './errors' export * from './useDropTipWithType' -export * from './useTipAttachmentStatus' export * from './useDropTipLocations' export { useDropTipRouting } from './useDropTipRouting' export { useDropTipWithType } from './useDropTipWithType' diff --git a/app/src/organisms/DropTipWizardFlows/hooks/useTipAttachmentStatus/__tests__/getPipettesWithTipAttached.test.ts b/app/src/organisms/DropTipWizardFlows/hooks/useTipAttachmentStatus/__tests__/getPipettesWithTipAttached.test.ts deleted file mode 100644 index bdaeff2dee0..00000000000 --- a/app/src/organisms/DropTipWizardFlows/hooks/useTipAttachmentStatus/__tests__/getPipettesWithTipAttached.test.ts +++ /dev/null @@ -1,239 +0,0 @@ -import { describe, it, beforeEach, expect, vi } from 'vitest' -import { getCommands } from '@opentrons/api-client' - -import { getPipettesWithTipAttached } from '../getPipettesWithTipAttached' -import { LEFT, RIGHT } from '@opentrons/shared-data' - -import type { GetPipettesWithTipAttached } from '../getPipettesWithTipAttached' - -vi.mock('@opentrons/api-client') - -const HOST_NAME = 'localhost' -const RUN_ID = 'testRunId' -const LEFT_PIPETTE_ID = 'testId1' -const RIGHT_PIPETTE_ID = 'testId2' -const LEFT_PIPETTE_NAME = 'testLeftName' -const RIGHT_PIPETTE_NAME = 'testRightName' -const PICK_UP_TIP = 'pickUpTip' -const DROP_TIP = 'dropTip' -const DROP_TIP_IN_PLACE = 'dropTipInPlace' -const LOAD_PIPETTE = 'loadPipette' -const FIXIT_INTENT = 'fixit' - -const LEFT_PIPETTE = { - mount: LEFT, - state: { tipDetected: true }, - instrumentType: 'pipette', - ok: true, -} -const RIGHT_PIPETTE = { - mount: RIGHT, - state: { tipDetected: true }, - instrumentType: 'pipette', - ok: true, -} - -const mockAttachedInstruments = { - data: [LEFT_PIPETTE, RIGHT_PIPETTE], - meta: { cursor: 0, totalLength: 2 }, -} - -const createMockCommand = ( - type: string, - id: string, - pipetteId: string, - status = 'succeeded' -) => ({ - id, - key: `${id}-key`, - commandType: type, - status, - params: { pipetteId }, -}) - -const mockCommands = { - data: [ - createMockCommand(LOAD_PIPETTE, 'load-left', LEFT_PIPETTE_ID), - createMockCommand(LOAD_PIPETTE, 'load-right', RIGHT_PIPETTE_ID), - createMockCommand(PICK_UP_TIP, 'pickup-left', LEFT_PIPETTE_ID), - createMockCommand(DROP_TIP, 'drop-left', LEFT_PIPETTE_ID, 'succeeded'), - ], - meta: { cursor: 0, totalLength: 4 }, -} - -const mockRunRecord = { - data: { - pipettes: [ - { id: LEFT_PIPETTE_ID, pipetteName: LEFT_PIPETTE_NAME, mount: LEFT }, - { id: RIGHT_PIPETTE_ID, pipetteName: RIGHT_PIPETTE_NAME, mount: RIGHT }, - ], - }, -} - -describe('getPipettesWithTipAttached', () => { - let DEFAULT_PARAMS: GetPipettesWithTipAttached - - beforeEach(() => { - DEFAULT_PARAMS = { - host: { hostname: HOST_NAME }, - runId: RUN_ID, - attachedInstruments: mockAttachedInstruments as any, - runRecord: mockRunRecord as any, - } - - vi.mocked(getCommands).mockResolvedValue({ - data: mockCommands, - } as any) - }) - - it('returns an empty array if attachedInstruments is null', async () => { - const params = { ...DEFAULT_PARAMS, attachedInstruments: null } - const result = await getPipettesWithTipAttached(params) - expect(result).toEqual([]) - }) - - it('returns an empty array if runRecord is null', async () => { - const params = { ...DEFAULT_PARAMS, runRecord: null } - const result = await getPipettesWithTipAttached(params) - expect(result).toEqual([]) - }) - - it('returns an empty array when no tips are attached according to protocol', async () => { - const mockCommandsWithoutAttachedTips = { - ...mockCommands, - data: [ - createMockCommand(LOAD_PIPETTE, 'load-left', LEFT_PIPETTE_ID), - createMockCommand(LOAD_PIPETTE, 'load-right', RIGHT_PIPETTE_ID), - createMockCommand(PICK_UP_TIP, 'pickup-left', LEFT_PIPETTE_ID), - createMockCommand(DROP_TIP, 'drop-left', LEFT_PIPETTE_ID, 'succeeded'), - ], - } - - vi.mocked(getCommands).mockResolvedValue({ - data: mockCommandsWithoutAttachedTips, - } as any) - - const result = await getPipettesWithTipAttached(DEFAULT_PARAMS) - expect(result).toEqual([]) - }) - - it('returns pipettes with protocol detected tip attachment', async () => { - const mockCommandsWithPickUpTip = { - ...mockCommands, - data: [ - ...mockCommands.data, - createMockCommand(PICK_UP_TIP, 'pickup-left-2', LEFT_PIPETTE_ID), - createMockCommand(PICK_UP_TIP, 'pickup-right', RIGHT_PIPETTE_ID), - ], - } - - vi.mocked(getCommands).mockResolvedValue({ - data: mockCommandsWithPickUpTip, - } as any) - - const result = await getPipettesWithTipAttached(DEFAULT_PARAMS) - expect(result).toEqual(mockAttachedInstruments.data) - }) - - it('always returns the left mount before the right mount if both pipettes have tips attached', async () => { - const mockCommandsWithPickUpTip = { - ...mockCommands, - data: [ - ...mockCommands.data, - createMockCommand(PICK_UP_TIP, 'pickup-right', RIGHT_PIPETTE_ID), - createMockCommand(PICK_UP_TIP, 'pickup-left-2', LEFT_PIPETTE_ID), - ], - } - - vi.mocked(getCommands).mockResolvedValue({ - data: mockCommandsWithPickUpTip, - } as any) - - const result = await getPipettesWithTipAttached(DEFAULT_PARAMS) - expect(result.length).toBe(2) - expect(result[0].mount).toEqual(LEFT) - expect(result[1].mount).toEqual(RIGHT) - }) - - it('does not return otherwise legitimate failed tip exchange commands if fixit intent tip commands are present and successful', async () => { - const mockCommandsWithFixit = { - ...mockCommands, - data: [ - ...mockCommands.data, - { - ...createMockCommand( - DROP_TIP_IN_PLACE, - 'fixit-drop', - LEFT_PIPETTE_ID - ), - intent: FIXIT_INTENT, - }, - ], - } - - vi.mocked(getCommands).mockResolvedValue({ - data: mockCommandsWithFixit, - } as any) - - const result = await getPipettesWithTipAttached(DEFAULT_PARAMS) - expect(result).toEqual([]) - }) - - it('considers a tip attached only if the last tip exchange command was pickUpTip', async () => { - const mockCommandsWithPickUpTip = { - ...mockCommands, - data: [ - ...mockCommands.data, - createMockCommand(PICK_UP_TIP, 'pickup-left-2', LEFT_PIPETTE_ID), - ], - } - - vi.mocked(getCommands).mockResolvedValue({ - data: mockCommandsWithPickUpTip, - } as any) - - const result = await getPipettesWithTipAttached(DEFAULT_PARAMS) - expect(result).toEqual([mockAttachedInstruments.data[0]]) - }) - - it('returns all valid attached pipettes when an error occurs', async () => { - vi.mocked(getCommands).mockRejectedValueOnce( - new Error('Example network error') - ) - - const result = await getPipettesWithTipAttached(DEFAULT_PARAMS) - - expect(result).toEqual([LEFT_PIPETTE, RIGHT_PIPETTE]) - }) - - it('filters out not ok pipettes', async () => { - vi.mocked(getCommands).mockRejectedValueOnce(new Error('Network error')) - - const mockInvalidPipettes = { - data: [ - LEFT_PIPETTE, - { - ...RIGHT_PIPETTE, - ok: false, - }, - ], - meta: { cursor: 0, totalLength: 2 }, - } - - const params = { - ...DEFAULT_PARAMS, - attachedInstruments: mockInvalidPipettes as any, - } - - const result = await getPipettesWithTipAttached(params) - - expect(result).toEqual([ - { - mount: LEFT, - state: { tipDetected: true }, - instrumentType: 'pipette', - ok: true, - }, - ]) - }) -}) diff --git a/app/src/organisms/DropTipWizardFlows/hooks/useTipAttachmentStatus/getPipettesWithTipAttached.ts b/app/src/organisms/DropTipWizardFlows/hooks/useTipAttachmentStatus/getPipettesWithTipAttached.ts deleted file mode 100644 index 39d4c7dd94f..00000000000 --- a/app/src/organisms/DropTipWizardFlows/hooks/useTipAttachmentStatus/getPipettesWithTipAttached.ts +++ /dev/null @@ -1,155 +0,0 @@ -import { getCommands } from '@opentrons/api-client' -import { LEFT } from '@opentrons/shared-data' - -import type { - HostConfig, - PipetteData, - Run, - CommandsData, - RunCommandSummary, - Instruments, -} from '@opentrons/api-client' -import type { - LoadedPipette, - PipettingRunTimeCommand, -} from '@opentrons/shared-data' - -export interface GetPipettesWithTipAttached { - host: HostConfig | null - runId: string - attachedInstruments: Instruments | null - runRecord: Run | null -} - -export function getPipettesWithTipAttached({ - host, - runId, - attachedInstruments, - runRecord, -}: GetPipettesWithTipAttached): Promise { - if (attachedInstruments == null || runRecord == null) { - return Promise.resolve([]) - } - - return ( - getCommandsExecutedDuringRun(host as HostConfig, runId) - .then(executedCmdData => - checkPipettesForAttachedTips( - executedCmdData.data, - runRecord.data.pipettes, - attachedInstruments.data as PipetteData[] - ) - ) - // If any network error occurs, return all attached pipettes as having tips attached for safety reasons. - .catch(() => Promise.resolve(getPipettesDataFrom(attachedInstruments))) - ) -} - -function getCommandsExecutedDuringRun( - host: HostConfig, - runId: string -): Promise { - return getCommands(host, runId, { - pageLength: 0, - includeFixitCommands: true, - }).then(response => { - const { totalLength } = response.data.meta - return getCommands(host, runId, { - cursor: 0, - pageLength: totalLength, - }).then(response => response.data) - }) -} - -const TIP_EXCHANGE_COMMAND_TYPES = [ - 'dropTip', - 'dropTipInPlace', - 'pickUpTip', - 'moveToAddressableAreaForDropTip', -] - -function checkPipettesForAttachedTips( - commands: RunCommandSummary[], - pipettesUsedInRun: LoadedPipette[], - attachedPipettes: PipetteData[] -): PipetteData[] { - let pipettesWithUnknownTipStatus = pipettesUsedInRun - const mountsWithTipAttached: Array = [] - - // Iterate backwards through commands, finding first tip exchange command for each pipette. - // If there's a chance the tip is still attached, flag the pipette. - for (let i = commands.length - 1; i >= 0; i--) { - if (pipettesWithUnknownTipStatus.length === 0) { - break - } - - const commandType = commands[i].commandType - const pipetteUsedInCommand = (commands[i] as PipettingRunTimeCommand).params - .pipetteId - const isTipExchangeCommand = TIP_EXCHANGE_COMMAND_TYPES.includes( - commandType - ) - const pipetteUsedInCommandWithUnknownTipStatus = - pipettesWithUnknownTipStatus.find( - pipette => pipette.id === pipetteUsedInCommand - ) ?? null - - // If the currently iterated command is a fixit command, we can safely assume the user - // had the option to fix pipettes with tips in this command and all commands - // earlier in the run, during Error Recovery flows. - if ( - commands[i].intent === 'fixit' && - isTipExchangeCommand && - commands[i].status === 'succeeded' - ) { - break - } - - if ( - isTipExchangeCommand && - pipetteUsedInCommandWithUnknownTipStatus != null - ) { - const tipPossiblyAttached = - commands[i].status !== 'succeeded' || commandType === 'pickUpTip' - - if (tipPossiblyAttached) { - mountsWithTipAttached.push( - pipetteUsedInCommandWithUnknownTipStatus.mount - ) - } - pipettesWithUnknownTipStatus = pipettesWithUnknownTipStatus.filter( - pipette => pipette.id !== pipetteUsedInCommand - ) - } - } - - // Convert the array of mounts with attached tips to PipetteData with attached tips. - const pipettesWithTipAttached = attachedPipettes.filter( - attachedPipette => - mountsWithTipAttached.includes(attachedPipette.mount) && - attachedPipette.ok - ) - - // Preferentially assign the left mount as the first element. - if ( - pipettesWithTipAttached.length === 2 && - pipettesWithTipAttached[1].mount === LEFT - ) { - ;[pipettesWithTipAttached[0], pipettesWithTipAttached[1]] = [ - pipettesWithTipAttached[1], - pipettesWithTipAttached[0], - ] - } - - return pipettesWithTipAttached -} - -function getPipettesDataFrom( - attachedInstruments: Instruments | null -): PipetteData[] { - return attachedInstruments != null - ? (attachedInstruments.data.filter( - instrument => instrument.instrumentType === 'pipette' && instrument.ok - ) as PipetteData[]) - : [] -} diff --git a/app/src/organisms/DropTipWizardFlows/hooks/useTipAttachmentStatus/index.ts b/app/src/organisms/DropTipWizardFlows/hooks/useTipAttachmentStatus/index.ts deleted file mode 100644 index 99d4ea9abd8..00000000000 --- a/app/src/organisms/DropTipWizardFlows/hooks/useTipAttachmentStatus/index.ts +++ /dev/null @@ -1,118 +0,0 @@ -import { useState, useCallback } from 'react' -import head from 'lodash/head' - -import { useInstrumentsQuery } from '@opentrons/react-api-client' -import { getPipetteModelSpecs } from '@opentrons/shared-data' - -import { getPipettesWithTipAttached } from './getPipettesWithTipAttached' - -import type { Mount } from '@opentrons/api-client' -import type { PipetteModelSpecs } from '@opentrons/shared-data' -import type { GetPipettesWithTipAttached } from './getPipettesWithTipAttached' - -const INSTRUMENTS_POLL_MS = 5000 - -export interface PipetteWithTip { - mount: Mount - specs: PipetteModelSpecs -} - -export interface TipAttachmentStatusResult { - /** Updates the pipettes with tip cache. Determine whether tips are likely attached on one or more pipettes. - * - * NOTE: Use responsibly! This function can potentially (but not likely) iterate over the entire length of a protocol run. - * */ - determineTipStatus: () => Promise - /* Whether tips are likely attached on *any* pipette. Typically called after determineTipStatus() */ - areTipsAttached: boolean - /* Resets the cached pipettes with tip statuses to null. */ - resetTipStatus: () => void - /** Removes the first element from the tip attached cache if present. - * @param {Function} onEmptyCache After removing the pipette from the cache, if the attached tip cache is empty, invoke this callback. - * @param {Function} onTipsDetected After removing the pipette from the cache, if the attached tip cache is not empty, invoke this callback. - * */ - setTipStatusResolved: ( - onEmptyCache?: () => void, - onTipsDetected?: () => void - ) => Promise - /* Relevant pipette information for a pipette with a tip attached. If both pipettes have tips attached, return the left pipette. */ - aPipetteWithTip: PipetteWithTip | null - /* The initial number of pipettes with tips. Null if there has been no tip check yet. */ - initialPipettesWithTipsCount: number | null -} - -// Returns various utilities for interacting with the cache of pipettes with tips attached. -export function useTipAttachmentStatus( - params: Omit -): TipAttachmentStatusResult { - const [pipettesWithTip, setPipettesWithTip] = useState([]) - const [initialPipettesCount, setInitialPipettesCount] = useState< - number | null - >(null) - const { data: attachedInstruments } = useInstrumentsQuery({ - refetchInterval: INSTRUMENTS_POLL_MS, - }) - - const aPipetteWithTip = head(pipettesWithTip) ?? null - const areTipsAttached = - pipettesWithTip.length > 0 && head(pipettesWithTip)?.specs != null - - const determineTipStatus = useCallback((): Promise => { - return getPipettesWithTipAttached({ - ...params, - attachedInstruments: attachedInstruments ?? null, - }).then(pipettesWithTip => { - const pipettesWithTipsData = pipettesWithTip.map(pipette => { - const specs = getPipetteModelSpecs(pipette.instrumentModel) - return { - specs, - mount: pipette.mount, - } - }) - const pipettesWithTipAndSpecs = pipettesWithTipsData.filter( - pipette => pipette.specs != null - ) as PipetteWithTip[] - - setPipettesWithTip(pipettesWithTipAndSpecs) - // Set only once. - if (initialPipettesCount === null) { - setInitialPipettesCount(pipettesWithTipAndSpecs.length) - } - - return Promise.resolve(pipettesWithTipAndSpecs) - }) - }, [params]) - - const resetTipStatus = (): void => { - setPipettesWithTip([]) - setInitialPipettesCount(null) - } - - const setTipStatusResolved = ( - onEmptyCache?: () => void, - onTipsDetected?: () => void - ): Promise => { - return new Promise(resolve => { - setPipettesWithTip(prevPipettesWithTip => { - const newState = [...prevPipettesWithTip.slice(1)] - if (newState.length === 0) { - onEmptyCache?.() - } else { - onTipsDetected?.() - } - - resolve(newState[0]) - return newState - }) - }) - } - - return { - areTipsAttached, - determineTipStatus, - resetTipStatus, - aPipetteWithTip, - setTipStatusResolved, - initialPipettesWithTipsCount: initialPipettesCount, - } -} diff --git a/app/src/organisms/DropTipWizardFlows/index.ts b/app/src/organisms/DropTipWizardFlows/index.ts index 1b53f36e5c8..05a16f92e49 100644 --- a/app/src/organisms/DropTipWizardFlows/index.ts +++ b/app/src/organisms/DropTipWizardFlows/index.ts @@ -1,6 +1,4 @@ export * from './DropTipWizardFlows' -export { useTipAttachmentStatus } from './hooks' export * from './TipsAttachedModal' -export type { TipAttachmentStatusResult, PipetteWithTip } from './hooks' export type { FixitCommandTypeUtils } from './types' diff --git a/app/src/organisms/EmergencyStop/EstopPressedModal.tsx b/app/src/organisms/EmergencyStop/EstopPressedModal.tsx index 7c78de6b8e2..ccebc8e124a 100644 --- a/app/src/organisms/EmergencyStop/EstopPressedModal.tsx +++ b/app/src/organisms/EmergencyStop/EstopPressedModal.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useState } from 'react' import { createPortal } from 'react-dom' import { useSelector } from 'react-redux' import { useTranslation } from 'react-i18next' @@ -30,11 +30,12 @@ import { SmallButton } from '/app/atoms/buttons' import { OddModal } from '/app/molecules/OddModal' import { getIsOnDevice } from '/app/redux/config' +import type { MouseEventHandler } from 'react' +import type { ModalProps } from '@opentrons/components' import type { OddModalHeaderBaseProps, ModalSize, } from '/app/molecules/OddModal/types' -import type { ModalProps } from '@opentrons/components' // Note (07/13/2023) After the launch, we will unify the modal components into one component. // Then TouchScreenModal and DesktopModal will be TouchScreenContent and DesktopContent that only render each content. @@ -81,7 +82,7 @@ function TouchscreenModal({ setIsWaitingForResumeOperation, }: EstopPressedModalProps): JSX.Element { const { t } = useTranslation(['device_settings', 'branded']) - const [isResuming, setIsResuming] = React.useState(false) + const [isResuming, setIsResuming] = useState(false) const { acknowledgeEstopDisengage } = useAcknowledgeEstopDisengageMutation() const { @@ -156,7 +157,7 @@ function DesktopModal({ setIsWaitingForResumeOperation, }: EstopPressedModalProps): JSX.Element { const { t } = useTranslation('device_settings') - const [isResuming, setIsResuming] = React.useState(false) + const [isResuming, setIsResuming] = useState(false) const { acknowledgeEstopDisengage } = useAcknowledgeEstopDisengageMutation() const { handlePlaceReaderLid, @@ -174,7 +175,7 @@ function DesktopModal({ width: '47rem', } - const handleClick: React.MouseEventHandler = (e): void => { + const handleClick: MouseEventHandler = (e): void => { e.preventDefault() setIsResuming(true) setIsWaitingForResumeOperation() diff --git a/app/src/organisms/EmergencyStop/__tests__/EstopMissingModal.test.tsx b/app/src/organisms/EmergencyStop/__tests__/EstopMissingModal.test.tsx index c2ce7cea0e1..c71c0a146ee 100644 --- a/app/src/organisms/EmergencyStop/__tests__/EstopMissingModal.test.tsx +++ b/app/src/organisms/EmergencyStop/__tests__/EstopMissingModal.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { fireEvent, screen } from '@testing-library/react' import '@testing-library/jest-dom/vitest' import { describe, it, vi, beforeEach, expect } from 'vitest' @@ -8,16 +7,18 @@ import { i18n } from '/app/i18n' import { getIsOnDevice } from '/app/redux/config' import { EstopMissingModal } from '../EstopMissingModal' +import type { ComponentProps } from 'react' + vi.mock('/app/redux/config') -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, }) } describe('EstopMissingModal - Touchscreen', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { @@ -40,7 +41,7 @@ describe('EstopMissingModal - Touchscreen', () => { }) describe('EstopMissingModal - Desktop', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { diff --git a/app/src/organisms/EmergencyStop/__tests__/EstopPressedModal.test.tsx b/app/src/organisms/EmergencyStop/__tests__/EstopPressedModal.test.tsx index 067211c4c06..6d259bd6acb 100644 --- a/app/src/organisms/EmergencyStop/__tests__/EstopPressedModal.test.tsx +++ b/app/src/organisms/EmergencyStop/__tests__/EstopPressedModal.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { fireEvent, screen } from '@testing-library/react' import '@testing-library/jest-dom/vitest' import { describe, it, vi, beforeEach, expect } from 'vitest' @@ -10,18 +9,20 @@ import { getIsOnDevice } from '/app/redux/config' import { EstopPressedModal } from '../EstopPressedModal' import { usePlacePlateReaderLid } from '/app/resources/modules' +import type { ComponentProps } from 'react' + vi.mock('@opentrons/react-api-client') vi.mock('/app/redux/config') vi.mock('/app/resources/modules') -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, }) } describe('EstopPressedModal - Touchscreen', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { @@ -85,7 +86,7 @@ describe('EstopPressedModal - Touchscreen', () => { }) describe('EstopPressedModal - Desktop', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { diff --git a/app/src/organisms/EmergencyStop/__tests__/EstopTakeover.test.tsx b/app/src/organisms/EmergencyStop/__tests__/EstopTakeover.test.tsx index 3ff0503dc69..4ffd822fb80 100644 --- a/app/src/organisms/EmergencyStop/__tests__/EstopTakeover.test.tsx +++ b/app/src/organisms/EmergencyStop/__tests__/EstopTakeover.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { describe, it, beforeEach, expect, vi } from 'vitest' import { screen } from '@testing-library/react' import '@testing-library/jest-dom/vitest' @@ -19,6 +18,8 @@ import { getLocalRobot } from '/app/redux/discovery' import { mockConnectedRobot } from '/app/redux/discovery/__fixtures__' import { EstopTakeover } from '../EstopTakeover' +import type { ComponentProps } from 'react' + vi.mock('@opentrons/react-api-client') vi.mock('../EstopMissingModal') vi.mock('../EstopPressedModal') @@ -33,14 +34,14 @@ const mockPressed = { }, } -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, }) } describe('EstopTakeover', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { diff --git a/app/src/organisms/ErrorRecoveryFlows/ErrorRecoveryWizard.tsx b/app/src/organisms/ErrorRecoveryFlows/ErrorRecoveryWizard.tsx index b28fbe0998e..bd52195faf8 100644 --- a/app/src/organisms/ErrorRecoveryFlows/ErrorRecoveryWizard.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/ErrorRecoveryWizard.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect, useLayoutEffect } from 'react' +import { useState, useEffect } from 'react' import { useTranslation } from 'react-i18next' import { css } from 'styled-components' @@ -19,6 +19,7 @@ import { IgnoreErrorSkipStep, ManualMoveLwAndSkip, ManualReplaceLwAndRetry, + HomeAndRetry, } from './RecoveryOptions' import { useErrorDetailsModal, @@ -29,7 +30,6 @@ import { import { RecoveryInProgress } from './RecoveryInProgress' import { getErrorKind } from './utils' import { RECOVERY_MAP } from './constants' -import { useHomeGripper } from './hooks' import type { LabwareDefinition2, RobotType } from '@opentrons/shared-data' import type { RecoveryRoute, RouteStep, RecoveryContentProps } from './types' @@ -76,23 +76,12 @@ export type ErrorRecoveryWizardProps = ErrorRecoveryFlowsProps & export function ErrorRecoveryWizard( props: ErrorRecoveryWizardProps ): JSX.Element { - const { - hasLaunchedRecovery, - failedCommand, - recoveryCommands, - routeUpdateActions, - } = props - const errorKind = getErrorKind(failedCommand) - - useInitialPipetteHome({ - hasLaunchedRecovery, - recoveryCommands, - routeUpdateActions, - }) - - useHomeGripper(props) - - return + return ( + + ) } export function ErrorRecoveryComponent( @@ -237,6 +226,10 @@ export function ErrorRecoveryContent(props: RecoveryContentProps): JSX.Element { return } + const buildHomeAndRetry = (): JSX.Element => { + return + } + switch (props.recoveryMap.route) { case RECOVERY_MAP.OPTION_SELECTION.ROUTE: return buildSelectRecoveryOption() @@ -276,30 +269,9 @@ export function ErrorRecoveryContent(props: RecoveryContentProps): JSX.Element { return buildRecoveryInProgress() case RECOVERY_MAP.ROBOT_DOOR_OPEN.ROUTE: return buildManuallyRouteToDoorOpen() + case RECOVERY_MAP.HOME_AND_RETRY.ROUTE: + return buildHomeAndRetry() default: return buildSelectRecoveryOption() } } -interface UseInitialPipetteHomeParams { - hasLaunchedRecovery: ErrorRecoveryWizardProps['hasLaunchedRecovery'] - recoveryCommands: ErrorRecoveryWizardProps['recoveryCommands'] - routeUpdateActions: ErrorRecoveryWizardProps['routeUpdateActions'] -} -// Home the Z-axis of all attached pipettes on Error Recovery launch. -export function useInitialPipetteHome({ - hasLaunchedRecovery, - recoveryCommands, - routeUpdateActions, -}: UseInitialPipetteHomeParams): void { - const { homePipetteZAxes } = recoveryCommands - const { handleMotionRouting } = routeUpdateActions - - // Synchronously set the recovery route to "robot in motion" before initial render to prevent screen flicker on ER launch. - useLayoutEffect(() => { - if (hasLaunchedRecovery) { - void handleMotionRouting(true) - .then(() => homePipetteZAxes()) - .finally(() => handleMotionRouting(false)) - } - }, [hasLaunchedRecovery]) -} diff --git a/app/src/organisms/ErrorRecoveryFlows/RecoveryInProgress.tsx b/app/src/organisms/ErrorRecoveryFlows/RecoveryInProgress.tsx index 3a176942a74..3353c9d4b05 100644 --- a/app/src/organisms/ErrorRecoveryFlows/RecoveryInProgress.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/RecoveryInProgress.tsx @@ -101,7 +101,7 @@ export function useGripperRelease({ doorStatusUtils, recoveryMap, }: UseGripperReleaseProps): number { - const { releaseGripperJaws } = recoveryCommands + const { releaseGripperJaws, homeExceptPlungers } = recoveryCommands const { selectedRecoveryOption } = currentRecoveryOptionUtils const { proceedToRouteAndStep, @@ -112,49 +112,47 @@ export function useGripperRelease({ const { MANUAL_MOVE_AND_SKIP, MANUAL_REPLACE_AND_RETRY } = RECOVERY_MAP const [countdown, setCountdown] = useState(GRIPPER_RELEASE_COUNTDOWN_S) - const proceedToValidNextStep = (): void => { - if (isDoorOpen) { - switch (selectedRecoveryOption) { - case MANUAL_MOVE_AND_SKIP.ROUTE: - void proceedToRouteAndStep( - MANUAL_MOVE_AND_SKIP.ROUTE, - MANUAL_MOVE_AND_SKIP.STEPS.CLOSE_DOOR_GRIPPER_Z_HOME - ) - break - case MANUAL_REPLACE_AND_RETRY.ROUTE: - void proceedToRouteAndStep( - MANUAL_REPLACE_AND_RETRY.ROUTE, - MANUAL_REPLACE_AND_RETRY.STEPS.CLOSE_DOOR_GRIPPER_Z_HOME - ) - break - default: { - console.error( - 'Unhandled post grip-release routing when door is open.' - ) - void proceedToRouteAndStep(RECOVERY_MAP.OPTION_SELECTION.ROUTE) - } - } - } else { - switch (selectedRecoveryOption) { - case MANUAL_MOVE_AND_SKIP.ROUTE: - void proceedToRouteAndStep( - MANUAL_MOVE_AND_SKIP.ROUTE, - MANUAL_MOVE_AND_SKIP.STEPS.MANUAL_MOVE - ) - break - case MANUAL_REPLACE_AND_RETRY.ROUTE: - void proceedToRouteAndStep( - MANUAL_REPLACE_AND_RETRY.ROUTE, - MANUAL_REPLACE_AND_RETRY.STEPS.MANUAL_REPLACE - ) - break - default: - console.error('Unhandled post grip-release routing.') - void proceedNextStep() + const proceedToDoorStep = (): void => { + switch (selectedRecoveryOption) { + case MANUAL_MOVE_AND_SKIP.ROUTE: + void proceedToRouteAndStep( + MANUAL_MOVE_AND_SKIP.ROUTE, + MANUAL_MOVE_AND_SKIP.STEPS.CLOSE_DOOR_GRIPPER_Z_HOME + ) + break + case MANUAL_REPLACE_AND_RETRY.ROUTE: + void proceedToRouteAndStep( + MANUAL_REPLACE_AND_RETRY.ROUTE, + MANUAL_REPLACE_AND_RETRY.STEPS.CLOSE_DOOR_GRIPPER_Z_HOME + ) + break + default: { + console.error('Unhandled post grip-release routing when door is open.') + void proceedToRouteAndStep(RECOVERY_MAP.OPTION_SELECTION.ROUTE) } } } + const proceedToValidNextStep = (): void => { + switch (selectedRecoveryOption) { + case MANUAL_MOVE_AND_SKIP.ROUTE: + void proceedToRouteAndStep( + MANUAL_MOVE_AND_SKIP.ROUTE, + MANUAL_MOVE_AND_SKIP.STEPS.MANUAL_MOVE + ) + break + case MANUAL_REPLACE_AND_RETRY.ROUTE: + void proceedToRouteAndStep( + MANUAL_REPLACE_AND_RETRY.ROUTE, + MANUAL_REPLACE_AND_RETRY.STEPS.MANUAL_REPLACE + ) + break + default: + console.error('Unhandled post grip-release routing.') + void proceedNextStep() + } + } + useEffect(() => { let intervalId: NodeJS.Timeout | null = null @@ -167,11 +165,21 @@ export function useGripperRelease({ if (intervalId != null) { clearInterval(intervalId) } - void releaseGripperJaws() - .finally(() => handleMotionRouting(false)) - .then(() => { - proceedToValidNextStep() - }) + + void releaseGripperJaws().then(() => { + if (isDoorOpen) { + return handleMotionRouting(false).then(() => { + proceedToDoorStep() + }) + } + + return handleMotionRouting(true) + .then(() => homeExceptPlungers()) + .then(() => handleMotionRouting(false)) + .then(() => { + proceedToValidNextStep() + }) + }) } return updatedCountdown diff --git a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/CancelRun.tsx b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/CancelRun.tsx index b3cdd5fe257..fa66d614011 100644 --- a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/CancelRun.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/CancelRun.tsx @@ -35,7 +35,9 @@ export function CancelRun(props: RecoveryContentProps): JSX.Element { case CANCEL_RUN.STEPS.CONFIRM_CANCEL: return default: - console.warn(`${step} in ${route} not explicitly handled. Rerouting.`) + console.warn( + `CancelRun: ${step} in ${route} not explicitly handled. Rerouting.` + ) return } } diff --git a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/FillWellAndSkip.tsx b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/FillWellAndSkip.tsx index dc74ed7e529..d01ea7dfe4e 100644 --- a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/FillWellAndSkip.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/FillWellAndSkip.tsx @@ -34,7 +34,9 @@ export function FillWellAndSkip(props: RecoveryContentProps): JSX.Element { case CANCEL_RUN.STEPS.CONFIRM_CANCEL: return default: - console.warn(`${step} in ${route} not explicitly handled. Rerouting.`) + console.warn( + `FillWellAndSkip: ${step} in ${route} not explicitly handled. Rerouting.` + ) return } } diff --git a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/HomeAndRetry.tsx b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/HomeAndRetry.tsx new file mode 100644 index 00000000000..00ebdfb35ee --- /dev/null +++ b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/HomeAndRetry.tsx @@ -0,0 +1,147 @@ +import { Trans, useTranslation } from 'react-i18next' +import { LegacyStyledText } from '@opentrons/components' +import { RECOVERY_MAP } from '../constants' +import { + TwoColTextAndFailedStepNextStep, + TwoColLwInfoAndDeck, + SelectTips, + RecoveryDoorOpenSpecial, + RetryStepInfo, +} from '../shared' +import { ManageTips } from './ManageTips' +import { SelectRecoveryOption } from './SelectRecoveryOption' + +import type { RecoveryContentProps } from '../types' + +const { HOME_AND_RETRY } = RECOVERY_MAP +export function HomeAndRetry(props: RecoveryContentProps): JSX.Element { + const { recoveryMap } = props + const { route, step } = recoveryMap + switch (step) { + case HOME_AND_RETRY.STEPS.PREPARE_DECK_FOR_HOME: { + return + } + case HOME_AND_RETRY.STEPS.REMOVE_TIPS_FROM_PIPETTE: { + // TODO: Make this work the same way as e.g. RetryNewTips by changing one of them. Or both of them. + return + } + case HOME_AND_RETRY.STEPS.REPLACE_TIPS: { + return + } + case HOME_AND_RETRY.STEPS.SELECT_TIPS: { + return + } + case HOME_AND_RETRY.STEPS.HOME_BEFORE_RETRY: { + return + } + case HOME_AND_RETRY.STEPS.CLOSE_DOOR_AND_HOME: { + return + } + case HOME_AND_RETRY.STEPS.CONFIRM_RETRY: { + return + } + default: + console.warn( + `HomeAndRetry: ${step} in ${route} not explicitly handled. Rerouting.}` + ) + return + } +} + +export function RetryAfterHome(props: RecoveryContentProps): JSX.Element { + const { recoveryMap, routeUpdateActions } = props + const { step, route } = recoveryMap + const { HOME_AND_RETRY } = RECOVERY_MAP + const { proceedToRouteAndStep } = routeUpdateActions + + const buildContent = (): JSX.Element => { + switch (step) { + case HOME_AND_RETRY.STEPS.CONFIRM_RETRY: + return ( + + proceedToRouteAndStep( + HOME_AND_RETRY.ROUTE, + HOME_AND_RETRY.STEPS.HOME_BEFORE_RETRY + ) + } + /> + ) + default: + console.warn( + `RetryStep: ${step} in ${route} not explicitly handled. Rerouting.` + ) + return + } + } + return buildContent() +} + +export function PrepareDeckForHome(props: RecoveryContentProps): JSX.Element { + const { t } = useTranslation('error_recovery') + const { routeUpdateActions, tipStatusUtils } = props + const { proceedToRouteAndStep } = routeUpdateActions + const primaryBtnOnClick = (): Promise => + proceedToRouteAndStep( + RECOVERY_MAP.HOME_AND_RETRY.ROUTE, + tipStatusUtils.areTipsAttached + ? RECOVERY_MAP.HOME_AND_RETRY.STEPS.REMOVE_TIPS_FROM_PIPETTE + : RECOVERY_MAP.HOME_AND_RETRY.STEPS.HOME_BEFORE_RETRY + ) + const buildBodyText = (): JSX.Element => ( + }} + /> + ) + return ( + + ) +} + +export function HomeGantryBeforeRetry( + props: RecoveryContentProps +): JSX.Element { + const { t } = useTranslation('error_recovery') + const { routeUpdateActions, tipStatusUtils } = props + const { proceedToRouteAndStep } = routeUpdateActions + const { HOME_AND_RETRY } = RECOVERY_MAP + const buildBodyText = (): JSX.Element => ( + }} + /> + ) + const secondaryBtnOnClick = (): Promise => + proceedToRouteAndStep( + RECOVERY_MAP.HOME_AND_RETRY.ROUTE, + tipStatusUtils.areTipsAttached + ? RECOVERY_MAP.HOME_AND_RETRY.STEPS.REMOVE_TIPS_FROM_PIPETTE + : RECOVERY_MAP.HOME_AND_RETRY.STEPS.PREPARE_DECK_FOR_HOME + ) + + const primaryBtnOnClick = (): Promise => + proceedToRouteAndStep( + HOME_AND_RETRY.ROUTE, + HOME_AND_RETRY.STEPS.CLOSE_DOOR_AND_HOME + ) + return ( + + ) +} diff --git a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/IgnoreErrorSkipStep.tsx b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/IgnoreErrorSkipStep.tsx index c17e947853b..2f8f7016773 100644 --- a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/IgnoreErrorSkipStep.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/IgnoreErrorSkipStep.tsx @@ -27,6 +27,7 @@ import { SkipStepInfo, } from '../shared' +import type { ChangeEvent } from 'react' import type { RecoveryContentProps } from '../types' export function IgnoreErrorSkipStep(props: RecoveryContentProps): JSX.Element { @@ -41,7 +42,9 @@ export function IgnoreErrorSkipStep(props: RecoveryContentProps): JSX.Element { case IGNORE_AND_SKIP.STEPS.SKIP_STEP: return default: - console.warn(`${step} in ${route} not explicitly handled. Rerouting.`) + console.warn( + `IgnoreErrorAndSkipStep: ${step} in ${route} not explicitly handled. Rerouting.` + ) return } } @@ -141,7 +144,7 @@ export function IgnoreErrorStepHome({ > ) => { + onChange={(e: ChangeEvent) => { setSelectedOption(e.currentTarget.value as IgnoreOption) }} options={IGNORE_OPTIONS_IN_ORDER.map(option => { diff --git a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/ManageTips.tsx b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/ManageTips.tsx index 57eef74d2d6..9061ba9b638 100644 --- a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/ManageTips.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/ManageTips.tsx @@ -24,10 +24,8 @@ import { DT_ROUTES } from '/app/organisms/DropTipWizardFlows/constants' import { SelectRecoveryOption } from './SelectRecoveryOption' import type { RecoveryContentProps, RecoveryRoute, RouteStep } from '../types' -import type { - FixitCommandTypeUtils, - PipetteWithTip, -} from '/app/organisms/DropTipWizardFlows' +import type { FixitCommandTypeUtils } from '/app/organisms/DropTipWizardFlows' +import type { PipetteWithTip } from '/app/resources/instruments' // The Drop Tip flow entry point. Includes entry from SelectRecoveryOption and CancelRun. export function ManageTips(props: RecoveryContentProps): JSX.Element { @@ -36,7 +34,7 @@ export function ManageTips(props: RecoveryContentProps): JSX.Element { routeAlternativelyIfNoPipette(props) const buildContent = (): JSX.Element => { - const { DROP_TIP_FLOWS } = RECOVERY_MAP + const { DROP_TIP_FLOWS, HOME_AND_RETRY } = RECOVERY_MAP const { step, route } = recoveryMap switch (step) { @@ -46,8 +44,12 @@ export function ManageTips(props: RecoveryContentProps): JSX.Element { case DROP_TIP_FLOWS.STEPS.CHOOSE_BLOWOUT: case DROP_TIP_FLOWS.STEPS.CHOOSE_TIP_DROP: return + case HOME_AND_RETRY.STEPS.REMOVE_TIPS_FROM_PIPETTE: + return default: - console.warn(`${step} in ${route} not explicitly handled. Rerouting.`) + console.warn( + `ManageTips: ${step} in ${route} not explicitly handled. Rerouting.` + ) return } } @@ -70,11 +72,23 @@ export function BeginRemoval({ } = routeUpdateActions const { cancelRun } = recoveryCommands const { selectedRecoveryOption } = currentRecoveryOptionUtils - const { ROBOT_CANCELING, RETRY_NEW_TIPS } = RECOVERY_MAP + const { + ROBOT_CANCELING, + RETRY_NEW_TIPS, + HOME_AND_RETRY, + DROP_TIP_FLOWS, + } = RECOVERY_MAP const mount = aPipetteWithTip?.mount const primaryOnClick = (): void => { - void proceedNextStep() + if (selectedRecoveryOption === HOME_AND_RETRY.ROUTE) { + void proceedToRouteAndStep( + DROP_TIP_FLOWS.ROUTE, + DROP_TIP_FLOWS.STEPS.BEFORE_BEGINNING + ) + } else { + void proceedNextStep() + } } const secondaryOnClick = (): void => { @@ -83,6 +97,11 @@ export function BeginRemoval({ RETRY_NEW_TIPS.ROUTE, RETRY_NEW_TIPS.STEPS.REPLACE_TIPS ) + } else if (selectedRecoveryOption === HOME_AND_RETRY.ROUTE) { + void proceedToRouteAndStep( + HOME_AND_RETRY.ROUTE, + HOME_AND_RETRY.STEPS.HOME_BEFORE_RETRY + ) } else { void handleMotionRouting(true, ROBOT_CANCELING.ROUTE).then(() => { cancelRun() @@ -151,7 +170,12 @@ function DropTipFlowsContainer( recoveryCommands, currentRecoveryOptionUtils, } = props - const { DROP_TIP_FLOWS, ROBOT_CANCELING, RETRY_NEW_TIPS } = RECOVERY_MAP + const { + DROP_TIP_FLOWS, + ROBOT_CANCELING, + RETRY_NEW_TIPS, + HOME_AND_RETRY, + } = RECOVERY_MAP const { proceedToRouteAndStep, handleMotionRouting } = routeUpdateActions const { selectedRecoveryOption } = currentRecoveryOptionUtils const { setTipStatusResolved } = tipStatusUtils @@ -165,6 +189,11 @@ function DropTipFlowsContainer( RETRY_NEW_TIPS.ROUTE, RETRY_NEW_TIPS.STEPS.REPLACE_TIPS ) + } else if (selectedRecoveryOption === HOME_AND_RETRY.ROUTE) { + void proceedToRouteAndStep( + HOME_AND_RETRY.ROUTE, + HOME_AND_RETRY.STEPS.HOME_BEFORE_RETRY + ) } else { void setTipStatusResolved(onEmptyCache, onTipsDetected) } @@ -210,6 +239,7 @@ export function useDropTipFlowUtils({ SKIP_STEP_WITH_NEW_TIPS, ERROR_WHILE_RECOVERING, DROP_TIP_FLOWS, + HOME_AND_RETRY, } = RECOVERY_MAP const { runId, gripperErrorFirstPipetteWithTip } = tipStatusUtils const { step } = recoveryMap @@ -222,6 +252,7 @@ export function useDropTipFlowUtils({ switch (selectedRecoveryOption) { case RETRY_NEW_TIPS.ROUTE: case SKIP_STEP_WITH_NEW_TIPS.ROUTE: + case HOME_AND_RETRY.ROUTE: return t('proceed_to_tip_selection') default: return t('proceed_to_cancel') @@ -245,6 +276,10 @@ export function useDropTipFlowUtils({ SKIP_STEP_WITH_NEW_TIPS.STEPS.REPLACE_TIPS ) } + case HOME_AND_RETRY.ROUTE: + return () => { + routeTo(selectedRecoveryOption, HOME_AND_RETRY.STEPS.REPLACE_TIPS) + } default: return null } @@ -338,6 +373,7 @@ function routeAlternativelyIfNoPipette(props: RecoveryContentProps): void { RETRY_NEW_TIPS, SKIP_STEP_WITH_NEW_TIPS, OPTION_SELECTION, + HOME_AND_RETRY, } = RECOVERY_MAP if (tipStatusUtils.aPipetteWithTip == null) @@ -356,6 +392,13 @@ function routeAlternativelyIfNoPipette(props: RecoveryContentProps): void { ) break } + case HOME_AND_RETRY.ROUTE: { + proceedToRouteAndStep( + selectedRecoveryOption, + HOME_AND_RETRY.STEPS.HOME_BEFORE_RETRY + ) + break + } default: { proceedToRouteAndStep(OPTION_SELECTION.ROUTE) } diff --git a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/ManualMoveLwAndSkip.tsx b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/ManualMoveLwAndSkip.tsx index 123493480f7..5cf8ef81a65 100644 --- a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/ManualMoveLwAndSkip.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/ManualMoveLwAndSkip.tsx @@ -28,7 +28,9 @@ export function ManualMoveLwAndSkip(props: RecoveryContentProps): JSX.Element { case MANUAL_MOVE_AND_SKIP.STEPS.SKIP: return default: - console.warn(`${step} in ${route} not explicitly handled. Rerouting.`) + console.warn( + `ManualMoveLwAndSkipStep: ${step} in ${route} not explicitly handled. Rerouting.` + ) return } } diff --git a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/ManualReplaceLwAndRetry.tsx b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/ManualReplaceLwAndRetry.tsx index 11ffe783d42..313d3d1f086 100644 --- a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/ManualReplaceLwAndRetry.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/ManualReplaceLwAndRetry.tsx @@ -30,7 +30,9 @@ export function ManualReplaceLwAndRetry( case MANUAL_REPLACE_AND_RETRY.STEPS.RETRY: return default: - console.warn(`${step} in ${route} not explicitly handled. Rerouting.`) + console.warn( + `ManualReplaceLwAndRetry: ${step} in ${route} not explicitly handled. Rerouting.` + ) return } } diff --git a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/RetryNewTips.tsx b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/RetryNewTips.tsx index f6e86cd6923..003e776824d 100644 --- a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/RetryNewTips.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/RetryNewTips.tsx @@ -35,7 +35,9 @@ export function RetryNewTips(props: RecoveryContentProps): JSX.Element { case RETRY_NEW_TIPS.STEPS.RETRY: return default: - console.warn(`${step} in ${route} not explicitly handled. Rerouting.`) + console.warn( + `RetryNewTips: ${step} in ${route} not explicitly handled. Rerouting.` + ) return } } diff --git a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/RetrySameTips.tsx b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/RetrySameTips.tsx index 93a0d84689d..0c28eb2a2da 100644 --- a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/RetrySameTips.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/RetrySameTips.tsx @@ -18,7 +18,9 @@ export function RetrySameTips(props: RecoveryContentProps): JSX.Element { case RETRY_SAME_TIPS.STEPS.RETRY: return default: - console.warn(`${step} in ${route} not explicitly handled. Rerouting.`) + console.warn( + `RetrySameTips: ${step} in ${route} not explicitly handled. Rerouting.` + ) return } } diff --git a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/RetryStep.tsx b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/RetryStep.tsx index 9b1f4d2c85a..a30b68d4358 100644 --- a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/RetryStep.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/RetryStep.tsx @@ -14,7 +14,9 @@ export function RetryStep(props: RecoveryContentProps): JSX.Element { case RETRY_STEP.STEPS.CONFIRM_RETRY: return default: - console.warn(`${step} in ${route} not explicitly handled. Rerouting.`) + console.warn( + `RetryStep: ${step} in ${route} not explicitly handled. Rerouting.` + ) return } } diff --git a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/SelectRecoveryOption.tsx b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/SelectRecoveryOption.tsx index c44252e2da9..e271cc3be23 100644 --- a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/SelectRecoveryOption.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/SelectRecoveryOption.tsx @@ -20,7 +20,7 @@ import { import { RecoverySingleColumnContentWrapper } from '../shared' import type { ErrorKind, RecoveryContentProps, RecoveryRoute } from '../types' -import type { PipetteWithTip } from '/app/organisms/DropTipWizardFlows' +import type { PipetteWithTip } from '/app/resources/instruments' // The "home" route within Error Recovery. When a user completes a non-terminal flow or presses "Go back" enough // to escape the boundaries of any route, they will be redirected here. @@ -168,9 +168,16 @@ export function getRecoveryOptions(errorKind: ErrorKind): RecoveryRoute[] { return GRIPPER_ERROR_OPTIONS case ERROR_KINDS.GENERAL_ERROR: return GENERAL_ERROR_OPTIONS + case ERROR_KINDS.STALL_OR_COLLISION: + return STALL_OR_COLLISION_OPTIONS } } +export const STALL_OR_COLLISION_OPTIONS: RecoveryRoute[] = [ + RECOVERY_MAP.HOME_AND_RETRY.ROUTE, + RECOVERY_MAP.CANCEL_RUN.ROUTE, +] + export const NO_LIQUID_DETECTED_OPTIONS: RecoveryRoute[] = [ RECOVERY_MAP.MANUAL_FILL_AND_SKIP.ROUTE, RECOVERY_MAP.IGNORE_AND_SKIP.ROUTE, diff --git a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/SkipStepNewTips.tsx b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/SkipStepNewTips.tsx index 647bded71e1..b237afd82f0 100644 --- a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/SkipStepNewTips.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/SkipStepNewTips.tsx @@ -29,7 +29,9 @@ export function SkipStepNewTips( case SKIP_STEP_WITH_NEW_TIPS.STEPS.SKIP: return default: - console.warn(`${step} in ${route} not explicitly handled. Rerouting.`) + console.warn( + `SkipStepNewTips: ${step} in ${route} not explicitly handled. Rerouting.` + ) return } } diff --git a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/SkipStepSameTips.tsx b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/SkipStepSameTips.tsx index 2b56012d5ab..9990d94171a 100644 --- a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/SkipStepSameTips.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/SkipStepSameTips.tsx @@ -14,7 +14,9 @@ export function SkipStepSameTips(props: RecoveryContentProps): JSX.Element { case SKIP_STEP_WITH_SAME_TIPS.STEPS.SKIP: return default: - console.warn(`${step} in ${route} not explicitly handled. Rerouting.`) + console.warn( + `SkipStepSameTips: ${step} in ${route} not explicitly handled. Rerouting.` + ) return } } diff --git a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/CancelRun.test.tsx b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/CancelRun.test.tsx index cbf8126e353..06923d65492 100644 --- a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/CancelRun.test.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/CancelRun.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { vi, describe, it, expect, beforeEach } from 'vitest' import { screen, waitFor } from '@testing-library/react' @@ -9,11 +8,13 @@ import { CancelRun } from '../CancelRun' import { RECOVERY_MAP } from '../../constants' import { SelectRecoveryOption } from '../SelectRecoveryOption' import { clickButtonLabeled } from '../../__tests__/util' + +import type { ComponentProps } from 'react' import type { Mock } from 'vitest' vi.mock('../SelectRecoveryOption') -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, })[0] @@ -21,7 +22,7 @@ const render = (props: React.ComponentProps) => { describe('RecoveryFooterButtons', () => { const { CANCEL_RUN, ROBOT_CANCELING, DROP_TIP_FLOWS } = RECOVERY_MAP - let props: React.ComponentProps + let props: ComponentProps let mockGoBackPrevStep: Mock let mockhandleMotionRouting: Mock let mockProceedToRouteAndStep: Mock diff --git a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/FillWellAndSkip.test.tsx b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/FillWellAndSkip.test.tsx index 3c7675ec21c..6f0145a9ef3 100644 --- a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/FillWellAndSkip.test.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/FillWellAndSkip.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { describe, it, vi, expect, beforeEach } from 'vitest' import { screen, waitFor } from '@testing-library/react' @@ -11,6 +10,7 @@ import { CancelRun } from '../CancelRun' import { SelectRecoveryOption } from '../SelectRecoveryOption' import { clickButtonLabeled } from '../../__tests__/util' +import type { ComponentProps } from 'react' import type { Mock } from 'vitest' vi.mock('../../shared', async () => { @@ -32,28 +32,26 @@ vi.mock('../CancelRun') vi.mock('../SelectRecoveryOption') vi.mock('/app/molecules/Command') -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, })[0] } -const renderFillWell = (props: React.ComponentProps) => { +const renderFillWell = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, })[0] } -const renderSkipToNextStep = ( - props: React.ComponentProps -) => { +const renderSkipToNextStep = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, })[0] } describe('FillWellAndSkip', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { @@ -118,7 +116,7 @@ describe('FillWellAndSkip', () => { }) describe('FillWell', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { @@ -136,7 +134,7 @@ describe('FillWell', () => { }) describe('SkipToNextStep', () => { - let props: React.ComponentProps + let props: ComponentProps let mockhandleMotionRouting: Mock let mockGoBackPrevStep: Mock let mockProceedToRouteAndStep: Mock diff --git a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/HomeAndRetry.test.tsx b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/HomeAndRetry.test.tsx new file mode 100644 index 00000000000..aae543bc559 --- /dev/null +++ b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/HomeAndRetry.test.tsx @@ -0,0 +1,155 @@ +import { describe, it, vi, beforeEach, afterEach } from 'vitest' +import { screen } from '@testing-library/react' + +import { mockRecoveryContentProps } from '../../__fixtures__' +import { renderWithProviders } from '/app/__testing-utils__' +import { i18n } from '/app/i18n' +import { RECOVERY_MAP } from '../../constants' +import { SelectRecoveryOption } from '../SelectRecoveryOption' +import { HomeAndRetry } from '../HomeAndRetry' +import { TipSelection } from '../../shared/TipSelection' + +import type { ComponentProps } from 'react' + +vi.mock('../SelectRecoveryOption') +vi.mock('../../shared/TipSelection') + +const render = (props: ComponentProps) => { + return renderWithProviders(, { + i18nInstance: i18n, + })[0] +} + +describe('HomeAndRetry', () => { + let props: ComponentProps + beforeEach(() => { + props = { + ...mockRecoveryContentProps, + currentRecoveryOptionUtils: { + ...mockRecoveryContentProps.currentRecoveryOptionUtils, + selectedRecoveryOption: RECOVERY_MAP.HOME_AND_RETRY.ROUTE, + }, + } + vi.mocked(SelectRecoveryOption).mockReturnValue( +
        MOCK_SELECT_RECOVERY_OPTION
        + ) + vi.mocked(TipSelection).mockReturnValue(
        WELL_SELECTION
        ) + }) + afterEach(() => { + vi.resetAllMocks() + }) + it(`renders PrepareDeckForHome when step is ${RECOVERY_MAP.HOME_AND_RETRY.STEPS.PREPARE_DECK_FOR_HOME}`, () => { + props = { + ...props, + recoveryMap: { + ...props.recoveryMap, + step: RECOVERY_MAP.HOME_AND_RETRY.STEPS.PREPARE_DECK_FOR_HOME, + }, + } + render(props) + screen.getByText('Prepare deck for homing') + }) + it(`renders ManageTips when step is ${RECOVERY_MAP.HOME_AND_RETRY.STEPS.REMOVE_TIPS_FROM_PIPETTE}`, () => { + props = { + ...props, + recoveryMap: { + ...props.recoveryMap, + step: RECOVERY_MAP.HOME_AND_RETRY.STEPS.REMOVE_TIPS_FROM_PIPETTE, + }, + tipStatusUtils: { + ...props.tipStatusUtils, + aPipetteWithTip: { + mount: 'left', + } as any, + }, + } + render(props) + screen.getByText('Remove any attached tips') + }) + it(`renders labware info when step is ${RECOVERY_MAP.HOME_AND_RETRY.STEPS.REPLACE_TIPS}`, () => { + props = { + ...props, + recoveryMap: { + ...props.recoveryMap, + step: RECOVERY_MAP.HOME_AND_RETRY.STEPS.REPLACE_TIPS, + }, + failedLabwareUtils: { + ...props.failedLabwareUtils, + relevantWellName: 'A2', + failedLabwareLocations: { + ...props.failedLabwareUtils.failedLabwareLocations, + displayNameCurrentLoc: 'B2', + }, + }, + } + + render(props) + screen.getByText('Replace used tips in rack location A2 in B2') + }) + it(`renders SelectTips when step is ${RECOVERY_MAP.HOME_AND_RETRY.STEPS.SELECT_TIPS}`, () => { + props = { + ...props, + recoveryMap: { + ...props.recoveryMap, + step: RECOVERY_MAP.HOME_AND_RETRY.STEPS.SELECT_TIPS, + }, + failedLabwareUtils: { + ...props.failedLabwareUtils, + failedLabwareLocations: { + ...props.failedLabwareUtils.failedLabwareLocations, + displayNameCurrentLoc: 'B2', + }, + }, + } + render(props) + screen.getByText('Select tip pick-up location') + }) + it(`renders HomeGantryBeforeRetry when step is ${RECOVERY_MAP.HOME_AND_RETRY.STEPS.HOME_BEFORE_RETRY}`, () => { + props = { + ...props, + recoveryMap: { + ...props.recoveryMap, + step: RECOVERY_MAP.HOME_AND_RETRY.STEPS.HOME_BEFORE_RETRY, + }, + } + render(props) + screen.getByText('Home gantry') + }) + it(`renders the special door open handler when step is ${RECOVERY_MAP.HOME_AND_RETRY.STEPS.CLOSE_DOOR_AND_HOME}`, () => { + props = { + ...props, + recoveryMap: { + ...props.recoveryMap, + step: RECOVERY_MAP.HOME_AND_RETRY.STEPS.CLOSE_DOOR_AND_HOME, + }, + doorStatusUtils: { + ...props.doorStatusUtils, + isDoorOpen: true, + }, + } + render(props) + screen.getByText('Close the robot door') + }) + it(`renders RetryAfterHome awhen step is ${RECOVERY_MAP.HOME_AND_RETRY.STEPS.CONFIRM_RETRY}`, () => { + props = { + ...props, + recoveryMap: { + ...props.recoveryMap, + step: RECOVERY_MAP.HOME_AND_RETRY.STEPS.CONFIRM_RETRY, + }, + } + render(props) + screen.getByText('Retry step') + }) + it(`renders SelectRecoveryOption as a fallback`, () => { + props = { + ...props, + recoveryMap: { + ...props.recoveryMap, + step: 'UNKNOWN_STEP' as any, + }, + } + render(props) + screen.getByText('MOCK_SELECT_RECOVERY_OPTION') + }) +}) diff --git a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/IgnoreErrorSkipStep.test.tsx b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/IgnoreErrorSkipStep.test.tsx index 547334f77c4..a1655ae57b2 100644 --- a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/IgnoreErrorSkipStep.test.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/IgnoreErrorSkipStep.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { describe, it, vi, expect, beforeEach } from 'vitest' import { screen, fireEvent, waitFor } from '@testing-library/react' @@ -15,6 +14,7 @@ import { SelectRecoveryOption } from '../SelectRecoveryOption' import { clickButtonLabeled } from '../../__tests__/util' import { SkipStepInfo } from '/app/organisms/ErrorRecoveryFlows/shared' +import type { ComponentProps } from 'react' import type { Mock } from 'vitest' vi.mock('/app/organisms/ErrorRecoveryFlows/shared', async () => { @@ -31,14 +31,14 @@ vi.mock('/app/organisms/ErrorRecoveryFlows/shared', async () => { }) vi.mock('../SelectRecoveryOption') -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, })[0] } const renderIgnoreErrorStepHome = ( - props: React.ComponentProps + props: ComponentProps ) => { return renderWithProviders(, { i18nInstance: i18n, @@ -46,7 +46,7 @@ const renderIgnoreErrorStepHome = ( } describe('IgnoreErrorSkipStep', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { @@ -98,7 +98,7 @@ describe('IgnoreErrorSkipStep', () => { }) describe('IgnoreErrorStepHome', () => { - let props: React.ComponentProps + let props: ComponentProps let mockIgnoreErrorKindThisRun: Mock let mockProceedToRouteAndStep: Mock let mockGoBackPrevStep: Mock @@ -184,7 +184,7 @@ describe('IgnoreErrorStepHome', () => { }) describe('IgnoreOptions', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { diff --git a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/ManageTips.test.tsx b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/ManageTips.test.tsx index 941b19081c7..b047f7a463b 100644 --- a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/ManageTips.test.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/ManageTips.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { describe, it, vi, expect, beforeEach } from 'vitest' import { screen, @@ -18,6 +17,7 @@ import { DT_ROUTES } from '/app/organisms/DropTipWizardFlows/constants' import { SelectRecoveryOption } from '../SelectRecoveryOption' import { clickButtonLabeled } from '../../__tests__/util' +import type { ComponentProps } from 'react' import type { Mock } from 'vitest' import type { PipetteModelSpecs } from '@opentrons/shared-data' @@ -34,14 +34,14 @@ const MOCK_ACTUAL_PIPETTE = { }, } as PipetteModelSpecs -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, })[0] } describe('ManageTips', () => { - let props: React.ComponentProps + let props: ComponentProps let mockProceedNextStep: Mock let mockProceedToRouteAndStep: Mock let mockhandleMotionRouting: Mock diff --git a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/ManualMoveLwAndSkip.test.tsx b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/ManualMoveLwAndSkip.test.tsx index 48f8615cf81..8cc5285e31a 100644 --- a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/ManualMoveLwAndSkip.test.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/ManualMoveLwAndSkip.test.tsx @@ -6,7 +6,7 @@ import { i18n } from '/app/i18n' import { ManualMoveLwAndSkip } from '../ManualMoveLwAndSkip' import { RECOVERY_MAP } from '../../constants' -import type * as React from 'react' +import type { ComponentProps } from 'react' vi.mock('../../shared', () => ({ GripperIsHoldingLabware: vi.fn(() => ( @@ -23,7 +23,7 @@ vi.mock('../SelectRecoveryOption', () => ({ })) describe('ManualMoveLwAndSkip', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { @@ -34,7 +34,7 @@ describe('ManualMoveLwAndSkip', () => { } as any }) - const render = (props: React.ComponentProps) => { + const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, })[0] diff --git a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/ManualReplaceLwAndRetry.test.tsx b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/ManualReplaceLwAndRetry.test.tsx index fb47ccb5f2f..0d25fcda029 100644 --- a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/ManualReplaceLwAndRetry.test.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/ManualReplaceLwAndRetry.test.tsx @@ -6,7 +6,7 @@ import { i18n } from '/app/i18n' import { ManualReplaceLwAndRetry } from '../ManualReplaceLwAndRetry' import { RECOVERY_MAP } from '../../constants' -import type * as React from 'react' +import type { ComponentProps } from 'react' vi.mock('../../shared', () => ({ GripperIsHoldingLabware: vi.fn(() => ( @@ -23,7 +23,7 @@ vi.mock('../SelectRecoveryOption', () => ({ })) describe('ManualReplaceLwAndRetry', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { @@ -35,9 +35,7 @@ describe('ManualReplaceLwAndRetry', () => { } as any }) - const render = ( - props: React.ComponentProps - ) => { + const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, })[0] diff --git a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/RetryNewTips.test.tsx b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/RetryNewTips.test.tsx index dcee4c10c56..cfd55c45d77 100644 --- a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/RetryNewTips.test.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/RetryNewTips.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { describe, it, vi, expect, beforeEach, afterEach } from 'vitest' import { screen, waitFor } from '@testing-library/react' @@ -10,6 +9,7 @@ import { RECOVERY_MAP } from '../../constants' import { SelectRecoveryOption } from '../SelectRecoveryOption' import { clickButtonLabeled } from '../../__tests__/util' +import type { ComponentProps } from 'react' import type { Mock } from 'vitest' vi.mock('/app/molecules/Command') @@ -23,14 +23,14 @@ vi.mock('../../shared', async () => { } }) -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, })[0] } const renderRetryWithNewTips = ( - props: React.ComponentProps + props: ComponentProps ) => { return renderWithProviders(, { i18nInstance: i18n, @@ -38,7 +38,7 @@ const renderRetryWithNewTips = ( } describe('RetryNewTips', () => { - let props: React.ComponentProps + let props: ComponentProps let mockProceedToRouteAndStep: Mock beforeEach(() => { @@ -121,7 +121,7 @@ describe('RetryNewTips', () => { }) describe('RetryWithNewTips', () => { - let props: React.ComponentProps + let props: ComponentProps let mockhandleMotionRouting: Mock let mockRetryFailedCommand: Mock let mockResumeRun: Mock diff --git a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/RetrySameTips.test.tsx b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/RetrySameTips.test.tsx index 4514c9cc350..7457b50f824 100644 --- a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/RetrySameTips.test.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/RetrySameTips.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { describe, it, vi, expect, beforeEach, afterEach } from 'vitest' import { screen, waitFor } from '@testing-library/react' @@ -10,19 +9,20 @@ import { RECOVERY_MAP } from '../../constants' import { SelectRecoveryOption } from '../SelectRecoveryOption' import { clickButtonLabeled } from '../../__tests__/util' +import type { ComponentProps } from 'react' import type { Mock } from 'vitest' vi.mock('/app/molecules/Command') vi.mock('../SelectRecoveryOption') -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, })[0] } const renderRetrySameTipsInfo = ( - props: React.ComponentProps + props: ComponentProps ) => { return renderWithProviders(, { i18nInstance: i18n, @@ -30,7 +30,7 @@ const renderRetrySameTipsInfo = ( } describe('RetrySameTips', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { @@ -72,7 +72,7 @@ describe('RetrySameTips', () => { }) describe('RetrySameTipsInfo', () => { - let props: React.ComponentProps + let props: ComponentProps let mockhandleMotionRouting: Mock let mockRetryFailedCommand: Mock let mockResumeRun: Mock diff --git a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/RetryStep.test.tsx b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/RetryStep.test.tsx index ec39f5b7c18..f972275c224 100644 --- a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/RetryStep.test.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/RetryStep.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { describe, it, vi, beforeEach, afterEach } from 'vitest' import { screen } from '@testing-library/react' @@ -9,17 +8,19 @@ import { RetryStep } from '../RetryStep' import { RECOVERY_MAP } from '../../constants' import { SelectRecoveryOption } from '../SelectRecoveryOption' +import type { ComponentProps } from 'react' + vi.mock('/app/molecules/Command') vi.mock('../SelectRecoveryOption') -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, })[0] } describe('RetryStep', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { diff --git a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/SelectRecoveryOptions.test.tsx b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/SelectRecoveryOptions.test.tsx index a0dd0c778ca..caa67196866 100644 --- a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/SelectRecoveryOptions.test.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/SelectRecoveryOptions.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { vi, describe, it, expect, beforeEach } from 'vitest' import { screen, fireEvent } from '@testing-library/react' import { when } from 'vitest-when' @@ -18,14 +17,16 @@ import { TIP_NOT_DETECTED_OPTIONS, TIP_DROP_FAILED_OPTIONS, GRIPPER_ERROR_OPTIONS, + STALL_OR_COLLISION_OPTIONS, } from '../SelectRecoveryOption' import { RECOVERY_MAP, ERROR_KINDS } from '../../constants' import { clickButtonLabeled } from '../../__tests__/util' +import type { ComponentProps } from 'react' import type { Mock } from 'vitest' const renderSelectRecoveryOption = ( - props: React.ComponentProps + props: ComponentProps ) => { return renderWithProviders( , @@ -36,7 +37,7 @@ const renderSelectRecoveryOption = ( } const renderRecoveryOptions = ( - props: React.ComponentProps + props: ComponentProps ) => { return renderWithProviders(, { i18nInstance: i18n, @@ -44,7 +45,7 @@ const renderRecoveryOptions = ( } describe('SelectRecoveryOption', () => { const { RETRY_STEP, RETRY_NEW_TIPS } = RECOVERY_MAP - let props: React.ComponentProps + let props: ComponentProps let mockProceedToRouteAndStep: Mock let mockSetSelectedRecoveryOption: Mock let mockGetRecoveryOptionCopy: Mock @@ -95,6 +96,9 @@ describe('SelectRecoveryOption', () => { expect.any(String) ) .thenReturn('Skip to next step with same tips') + when(mockGetRecoveryOptionCopy) + .calledWith(RECOVERY_MAP.HOME_AND_RETRY.ROUTE, expect.any(String)) + .thenReturn('Home gantry and retry') }) it('sets the selected recovery option when clicking continue', () => { @@ -231,9 +235,25 @@ describe('SelectRecoveryOption', () => { RECOVERY_MAP.RETRY_STEP.ROUTE ) }) + it('renders appropriate "Stall or collision" copy and click behavior', () => { + props = { + ...props, + errorKind: ERROR_KINDS.STALL_OR_COLLISION, + } + renderSelectRecoveryOption(props) + screen.getByText('Choose a recovery action') + const homeGantryAndRetry = screen.getAllByRole('label', { + name: 'Home gantry and retry', + }) + fireEvent.click(homeGantryAndRetry[0]) + clickButtonLabeled('Continue') + expect(mockProceedToRouteAndStep).toHaveBeenCalledWith( + RECOVERY_MAP.HOME_AND_RETRY.ROUTE + ) + }) }) describe('RecoveryOptions', () => { - let props: React.ComponentProps + let props: ComponentProps let mockSetSelectedRoute: Mock let mockGetRecoveryOptionCopy: Mock @@ -292,6 +312,9 @@ describe('RecoveryOptions', () => { expect.any(String) ) .thenReturn('Manually replace labware on deck and retry step') + when(mockGetRecoveryOptionCopy) + .calledWith(RECOVERY_MAP.HOME_AND_RETRY.ROUTE, expect.any(String)) + .thenReturn('Home gantry and retry') }) it('renders valid recovery options for a general error errorKind', () => { @@ -415,6 +438,17 @@ describe('RecoveryOptions', () => { }) screen.getByRole('label', { name: 'Cancel run' }) }) + it(`renders valid recovery options for a ${ERROR_KINDS.STALL_OR_COLLISION} errorKind`, () => { + props = { + ...props, + validRecoveryOptions: STALL_OR_COLLISION_OPTIONS, + } + renderRecoveryOptions(props) + screen.getByRole('label', { + name: 'Home gantry and retry', + }) + screen.getByRole('label', { name: 'Cancel run' }) + }) }) describe('getRecoveryOptions', () => { @@ -475,4 +509,11 @@ describe('getRecoveryOptions', () => { ) expect(overpressureWhileDispensingOptions).toBe(GRIPPER_ERROR_OPTIONS) }) + + it(`returns valid options when the errorKind is ${ERROR_KINDS.STALL_OR_COLLISION}`, () => { + const stallOrCollisionOptions = getRecoveryOptions( + ERROR_KINDS.STALL_OR_COLLISION + ) + expect(stallOrCollisionOptions).toBe(STALL_OR_COLLISION_OPTIONS) + }) }) diff --git a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/SkipStepNewTips.test.tsx b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/SkipStepNewTips.test.tsx index 09afa086dca..a492184cbc7 100644 --- a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/SkipStepNewTips.test.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/SkipStepNewTips.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { describe, it, vi, expect, beforeEach } from 'vitest' import { screen } from '@testing-library/react' @@ -9,6 +8,7 @@ import { SkipStepNewTips } from '../SkipStepNewTips' import { RECOVERY_MAP } from '../../constants' import { SelectRecoveryOption } from '../SelectRecoveryOption' +import type { ComponentProps } from 'react' import type { Mock } from 'vitest' vi.mock('/app/molecules/Command') @@ -23,14 +23,14 @@ vi.mock('../../shared', async () => { } }) -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, })[0] } describe('SkipStepNewTips', () => { - let props: React.ComponentProps + let props: ComponentProps let mockProceedToRouteAndStep: Mock beforeEach(() => { diff --git a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/SkipStepSameTips.test.tsx b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/SkipStepSameTips.test.tsx index 157825b3322..eb716694d68 100644 --- a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/SkipStepSameTips.test.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/SkipStepSameTips.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { describe, it, vi, beforeEach } from 'vitest' import { screen } from '@testing-library/react' @@ -10,18 +9,20 @@ import { RECOVERY_MAP } from '../../constants' import { SelectRecoveryOption } from '../SelectRecoveryOption' import { SkipStepInfo } from '/app/organisms/ErrorRecoveryFlows/shared' +import type { ComponentProps } from 'react' + vi.mock('/app/molecules/Command') vi.mock('/app/organisms/ErrorRecoveryFlows/shared') vi.mock('../SelectRecoveryOption') -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, })[0] } describe('SkipStepSameTips', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { diff --git a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/index.ts b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/index.ts index 0e50d054523..0ad8f530709 100644 --- a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/index.ts +++ b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/index.ts @@ -10,3 +10,4 @@ export { SkipStepNewTips } from './SkipStepNewTips' export { IgnoreErrorSkipStep } from './IgnoreErrorSkipStep' export { ManualMoveLwAndSkip } from './ManualMoveLwAndSkip' export { ManualReplaceLwAndRetry } from './ManualReplaceLwAndRetry' +export { HomeAndRetry } from './HomeAndRetry' diff --git a/app/src/organisms/ErrorRecoveryFlows/RecoverySplash.tsx b/app/src/organisms/ErrorRecoveryFlows/RecoverySplash.tsx index 7a17b443508..153d8c12931 100644 --- a/app/src/organisms/ErrorRecoveryFlows/RecoverySplash.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/RecoverySplash.tsx @@ -6,6 +6,7 @@ import { ALIGN_CENTER, COLORS, DIRECTION_COLUMN, + RESPONSIVENESS, DISPLAY_FLEX, Flex, Icon, @@ -83,13 +84,14 @@ export function RecoverySplash(props: RecoverySplashProps): JSX.Element | null { runStatus, recoveryActionMutationUtils, resumePausedRecovery, + recoveryCommands, } = props const { t } = useTranslation('error_recovery') const errorKind = getErrorKind(failedCommand) const title = useErrorName(errorKind) const { makeToast } = useToaster() - const { proceedToRouteAndStep } = routeUpdateActions + const { proceedToRouteAndStep, handleMotionRouting } = routeUpdateActions const { reportErrorEvent } = analytics const buildTitleHeadingDesktop = (): JSX.Element => { @@ -138,9 +140,16 @@ export function RecoverySplash(props: RecoverySplashProps): JSX.Element | null { const onLaunchERClick = (): void => { const onClick = (): void => { - void toggleERWizAsActiveUser(true, true).then(() => { - reportErrorEvent(failedCommand?.byRunRecord ?? null, 'launch-recovery') - }) + void toggleERWizAsActiveUser(true, true) + .then(() => { + reportErrorEvent( + failedCommand?.byRunRecord ?? null, + 'launch-recovery' + ) + }) + .then(() => handleMotionRouting(true)) + .then(() => recoveryCommands.homePipetteZAxes()) + .finally(() => handleMotionRouting(false)) } handleConditionalClick(onClick) } @@ -192,6 +201,7 @@ export function RecoverySplash(props: RecoverySplashProps): JSX.Element | null { overflowWrap={OVERFLOW_WRAP_BREAK_WORD} color={COLORS.white} textAlign={TEXT_ALIGN_CENTER} + css={TEXT_TRUNCATION_STYLE} />
        @@ -245,6 +255,7 @@ export function RecoverySplash(props: RecoverySplashProps): JSX.Element | null { overflow="hidden" overflowWrap={OVERFLOW_WRAP_BREAK_WORD} textAlign={TEXT_ALIGN_CENTER} + css={TEXT_TRUNCATION_STYLE} />
        @@ -293,6 +304,18 @@ const SplashFrame = styled(Flex)` padding-bottom: 0px; ` +const TEXT_TRUNCATION_STYLE = css` + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; + text-overflow: ellipsis; + + @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { + font-size: ${TYPOGRAPHY.fontSize22}; + } +` + const SHARED_BUTTON_STYLE_ODD = css` width: 29rem; height: 13.5rem; diff --git a/app/src/organisms/ErrorRecoveryFlows/__fixtures__/index.ts b/app/src/organisms/ErrorRecoveryFlows/__fixtures__/index.ts index 1a815b99c1e..aaf0b89e062 100644 --- a/app/src/organisms/ErrorRecoveryFlows/__fixtures__/index.ts +++ b/app/src/organisms/ErrorRecoveryFlows/__fixtures__/index.ts @@ -58,6 +58,7 @@ export const mockRecoveryContentProps: RecoveryContentProps = { byRunRecord: mockFailedCommand, byAnalysis: mockFailedCommand, }, + runLwDefsByUri: {} as any, errorKind: 'GENERAL_ERROR', robotType: FLEX_ROBOT_TYPE, runId: 'MOCK_RUN_ID', diff --git a/app/src/organisms/ErrorRecoveryFlows/__tests__/ErrorRecoveryFlows.test.tsx b/app/src/organisms/ErrorRecoveryFlows/__tests__/ErrorRecoveryFlows.test.tsx index 04719afca56..47f1668af87 100644 --- a/app/src/organisms/ErrorRecoveryFlows/__tests__/ErrorRecoveryFlows.test.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/__tests__/ErrorRecoveryFlows.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { vi, describe, expect, it, beforeEach } from 'vitest' import { screen, renderHook } from '@testing-library/react' @@ -8,7 +7,6 @@ import { RUN_STATUS_RUNNING, RUN_STATUS_STOP_REQUESTED, } from '@opentrons/api-client' -import { getLoadedLabwareDefinitionsByUri } from '@opentrons/shared-data' import { renderWithProviders } from '/app/__testing-utils__' import { i18n } from '/app/i18n' @@ -23,7 +21,9 @@ import { useRecoveryAnalytics } from '/app/redux-resources/analytics' import { getIsOnDevice } from '/app/redux/config' import { useERWizard, ErrorRecoveryWizard } from '../ErrorRecoveryWizard' import { useRecoverySplash, RecoverySplash } from '../RecoverySplash' +import { useRunLoadedLabwareDefinitionsByUri } from '/app/resources/runs' +import type { ComponentProps } from 'react' import type { RunStatus } from '@opentrons/api-client' vi.mock('../ErrorRecoveryWizard') @@ -33,13 +33,7 @@ vi.mock('/app/redux/config') vi.mock('../RecoverySplash') vi.mock('/app/redux-resources/analytics') vi.mock('@opentrons/react-api-client') -vi.mock('@opentrons/shared-data', async () => { - const actual = await vi.importActual('@opentrons/shared-data') - return { - ...actual, - getLoadedLabwareDefinitionsByUri: vi.fn(), - } -}) +vi.mock('/app/resources/runs') vi.mock('react-redux', async () => { const actual = await vi.importActual('react-redux') return { @@ -51,6 +45,7 @@ vi.mock('react-redux', async () => { describe('useErrorRecoveryFlows', () => { beforeEach(() => { vi.mocked(useCurrentlyRecoveringFrom).mockReturnValue('mockCommand' as any) + vi.mocked(useRunLoadedLabwareDefinitionsByUri).mockReturnValue({}) }) it('should have initial state of isERActive as false', () => { @@ -95,12 +90,32 @@ describe('useErrorRecoveryFlows', () => { expect(result.current.failedCommand).toEqual('mockCommand') }) + it("should return the run's labware definitions", () => { + const { result } = renderHook(() => + useErrorRecoveryFlows('MOCK_ID', RUN_STATUS_RUNNING) + ) + + expect(result.current.failedCommand).toEqual('mockCommand') + }) + it(`should return isERActive false if the run status is ${RUN_STATUS_STOP_REQUESTED} before seeing ${RUN_STATUS_AWAITING_RECOVERY}`, () => { const { result } = renderHook(() => useErrorRecoveryFlows('MOCK_ID', RUN_STATUS_STOP_REQUESTED) ) - expect(result.current.isERActive).toEqual(false) + expect(result.current.runLwDefsByUri).toEqual({}) + }) + + it('should not return isERActive if the run labware defintions is null', () => { + vi.mocked(useRunLoadedLabwareDefinitionsByUri).mockReturnValue(null) + + const { result } = renderHook( + runStatus => useErrorRecoveryFlows('MOCK_ID', runStatus), + { + initialProps: RUN_STATUS_AWAITING_RECOVERY, + } + ) + expect(result.current.isERActive).toBe(false) }) it('should set hasSeenAwaitingRecovery to true when runStatus is RUN_STATUS_AWAITING_RECOVERY', () => { @@ -134,14 +149,14 @@ describe('useErrorRecoveryFlows', () => { }) }) -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, })[0] } describe('ErrorRecoveryFlows', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { @@ -149,6 +164,7 @@ describe('ErrorRecoveryFlows', () => { unvalidatedFailedCommand: mockFailedCommand, runId: 'MOCK_RUN_ID', protocolAnalysis: null, + runLwDefsByUri: {}, } vi.mocked(ErrorRecoveryWizard).mockReturnValue(
        MOCK WIZARD
        ) vi.mocked(RecoverySplash).mockReturnValue(
        MOCK RUN PAUSED SPLASH
        ) @@ -173,7 +189,6 @@ describe('ErrorRecoveryFlows', () => { intent: 'recovering', showTakeover: false, }) - vi.mocked(getLoadedLabwareDefinitionsByUri).mockReturnValue({}) }) it('renders the wizard when showERWizard is true', () => { diff --git a/app/src/organisms/ErrorRecoveryFlows/__tests__/ErrorRecoveryWizard.test.tsx b/app/src/organisms/ErrorRecoveryFlows/__tests__/ErrorRecoveryWizard.test.tsx index 62fb2849753..1f358fc574c 100644 --- a/app/src/organisms/ErrorRecoveryFlows/__tests__/ErrorRecoveryWizard.test.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/__tests__/ErrorRecoveryWizard.test.tsx @@ -1,13 +1,11 @@ -import type * as React from 'react' import { vi, describe, it, expect, beforeEach } from 'vitest' -import { renderHook, act, screen, waitFor } from '@testing-library/react' +import { renderHook, act, screen } from '@testing-library/react' import { renderWithProviders } from '/app/__testing-utils__' import { i18n } from '/app/i18n' import { mockRecoveryContentProps } from '../__fixtures__' import { ErrorRecoveryContent, - useInitialPipetteHome, useERWizard, ErrorRecoveryComponent, } from '../ErrorRecoveryWizard' @@ -25,6 +23,7 @@ import { IgnoreErrorSkipStep, ManualReplaceLwAndRetry, ManualMoveLwAndSkip, + HomeAndRetry, } from '../RecoveryOptions' import { RecoveryInProgress } from '../RecoveryInProgress' import { RecoveryError } from '../RecoveryError' @@ -35,7 +34,7 @@ import { RecoveryDoorOpenSpecial, } from '../shared' -import type { Mock } from 'vitest' +import type { ComponentProps } from 'react' vi.mock('../RecoveryOptions') vi.mock('../RecoveryInProgress') @@ -98,7 +97,7 @@ describe('useERWizard', () => { }) const renderRecoveryComponent = ( - props: React.ComponentProps + props: ComponentProps ) => { return renderWithProviders(, { i18nInstance: i18n, @@ -106,7 +105,7 @@ const renderRecoveryComponent = ( } describe('ErrorRecoveryComponent', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = mockRecoveryContentProps @@ -160,7 +159,7 @@ describe('ErrorRecoveryComponent', () => { }) const renderRecoveryContent = ( - props: React.ComponentProps + props: ComponentProps ) => { return renderWithProviders(, { i18nInstance: i18n, @@ -191,9 +190,10 @@ describe('ErrorRecoveryContent', () => { ROBOT_RELEASING_LABWARE, MANUAL_REPLACE_AND_RETRY, MANUAL_MOVE_AND_SKIP, + HOME_AND_RETRY, } = RECOVERY_MAP - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = mockRecoveryContentProps @@ -228,6 +228,7 @@ describe('ErrorRecoveryContent', () => { vi.mocked(RecoveryDoorOpenSpecial).mockReturnValue(
        MOCK_DOOR_OPEN_SPECIAL
        ) + vi.mocked(HomeAndRetry).mockReturnValue(
        MOCK_HOME_AND_RETRY
        ) }) it(`returns SelectRecoveryOption when the route is ${OPTION_SELECTION.ROUTE}`, () => { @@ -508,74 +509,17 @@ describe('ErrorRecoveryContent', () => { screen.getByText('MOCK_DOOR_OPEN_SPECIAL') }) -}) - -describe('useInitialPipetteHome', () => { - let mockZHomePipetteZAxes: Mock - let mockhandleMotionRouting: Mock - let mockRecoveryCommands: any - let mockRouteUpdateActions: any - - beforeEach(() => { - mockZHomePipetteZAxes = vi.fn() - mockhandleMotionRouting = vi.fn() - - mockhandleMotionRouting.mockResolvedValue(() => mockZHomePipetteZAxes()) - mockZHomePipetteZAxes.mockResolvedValue(() => mockhandleMotionRouting()) - - mockRecoveryCommands = { - homePipetteZAxes: mockZHomePipetteZAxes, - } as any - mockRouteUpdateActions = { - handleMotionRouting: mockhandleMotionRouting, - } as any - }) - - it('does not z-home the pipettes if error recovery was not launched', () => { - renderHook(() => - useInitialPipetteHome({ - hasLaunchedRecovery: false, - recoveryCommands: mockRecoveryCommands, - routeUpdateActions: mockRouteUpdateActions, - }) - ) - expect(mockhandleMotionRouting).not.toHaveBeenCalled() - }) - - it('sets the motion screen properly and z-homes all pipettes only on the initial render of Error Recovery', async () => { - const { rerender } = renderHook(() => - useInitialPipetteHome({ - hasLaunchedRecovery: true, - recoveryCommands: mockRecoveryCommands, - routeUpdateActions: mockRouteUpdateActions, - }) - ) - - await waitFor(() => { - expect(mockhandleMotionRouting).toHaveBeenCalledWith(true) - }) - await waitFor(() => { - expect(mockZHomePipetteZAxes).toHaveBeenCalledTimes(1) - }) - await waitFor(() => { - expect(mockhandleMotionRouting).toHaveBeenCalledWith(false) - }) - - expect(mockhandleMotionRouting.mock.invocationCallOrder[0]).toBeLessThan( - mockZHomePipetteZAxes.mock.invocationCallOrder[0] - ) - expect(mockZHomePipetteZAxes.mock.invocationCallOrder[0]).toBeLessThan( - mockhandleMotionRouting.mock.invocationCallOrder[1] - ) - - rerender() + it(`returns HomeAndRetry when the route is ${HOME_AND_RETRY.ROUTE}`, () => { + props = { + ...props, + recoveryMap: { + ...props.recoveryMap, + route: HOME_AND_RETRY.ROUTE, + }, + } + renderRecoveryContent(props) - await waitFor(() => { - expect(mockhandleMotionRouting).toHaveBeenCalledTimes(2) - }) - await waitFor(() => { - expect(mockZHomePipetteZAxes).toHaveBeenCalledTimes(1) - }) + screen.getByText('MOCK_HOME_AND_RETRY') }) }) diff --git a/app/src/organisms/ErrorRecoveryFlows/__tests__/RecoveryDoorOpen.test.tsx b/app/src/organisms/ErrorRecoveryFlows/__tests__/RecoveryDoorOpen.test.tsx index a9fc92e1b84..57b0eff70a9 100644 --- a/app/src/organisms/ErrorRecoveryFlows/__tests__/RecoveryDoorOpen.test.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/__tests__/RecoveryDoorOpen.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { describe, it, beforeEach, vi, expect } from 'vitest' import { screen } from '@testing-library/react' @@ -10,16 +9,17 @@ import { i18n } from '/app/i18n' import { RecoveryDoorOpen } from '../RecoveryDoorOpen' import { clickButtonLabeled } from './util' +import type { ComponentProps } from 'react' import type { Mock } from 'vitest' -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, })[0] } describe('RecoveryDoorOpen', () => { - let props: React.ComponentProps + let props: ComponentProps let mockResumeRecovery: Mock let mockProceedToRouteAndStep: Mock diff --git a/app/src/organisms/ErrorRecoveryFlows/__tests__/RecoveryError.test.tsx b/app/src/organisms/ErrorRecoveryFlows/__tests__/RecoveryError.test.tsx index f46f3f949ba..9d19b32d788 100644 --- a/app/src/organisms/ErrorRecoveryFlows/__tests__/RecoveryError.test.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/__tests__/RecoveryError.test.tsx @@ -1,5 +1,4 @@ /* eslint-disable testing-library/prefer-presence-queries */ -import type * as React from 'react' import { describe, it, vi, expect, beforeEach } from 'vitest' import { screen, fireEvent, waitFor } from '@testing-library/react' @@ -9,9 +8,10 @@ import { i18n } from '/app/i18n' import { RecoveryError } from '../RecoveryError' import { RECOVERY_MAP } from '../constants' +import type { ComponentProps } from 'react' import type { Mock } from 'vitest' -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, })[0] @@ -20,7 +20,7 @@ const render = (props: React.ComponentProps) => { const { ERROR_WHILE_RECOVERING } = RECOVERY_MAP describe('RecoveryError', () => { - let props: React.ComponentProps + let props: ComponentProps let proceedToRouteAndStepMock: Mock let getRecoverOptionCopyMock: Mock let handleMotionRoutingMock: Mock diff --git a/app/src/organisms/ErrorRecoveryFlows/__tests__/RecoveryInProgress.test.tsx b/app/src/organisms/ErrorRecoveryFlows/__tests__/RecoveryInProgress.test.tsx index c3005c10cda..b9c6149a696 100644 --- a/app/src/organisms/ErrorRecoveryFlows/__tests__/RecoveryInProgress.test.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/__tests__/RecoveryInProgress.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { beforeEach, describe, it, vi, afterEach, expect } from 'vitest' import { act, renderHook, screen } from '@testing-library/react' @@ -12,7 +11,9 @@ import { } from '../RecoveryInProgress' import { RECOVERY_MAP } from '../constants' -const render = (props: React.ComponentProps) => { +import type { ComponentProps } from 'react' + +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, })[0] @@ -28,7 +29,7 @@ describe('RecoveryInProgress', () => { ROBOT_SKIPPING_STEP, ROBOT_RELEASING_LABWARE, } = RECOVERY_MAP - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { @@ -39,10 +40,12 @@ describe('RecoveryInProgress', () => { }, recoveryCommands: { releaseGripperJaws: vi.fn(() => Promise.resolve()), + homeExceptPlungers: vi.fn(() => Promise.resolve()), } as any, routeUpdateActions: { handleMotionRouting: vi.fn(() => Promise.resolve()), proceedNextStep: vi.fn(() => Promise.resolve()), + proceedToRouteAndStep: vi.fn(() => Promise.resolve()), } as any, } }) @@ -166,14 +169,12 @@ describe('useGripperRelease', () => { }, recoveryCommands: { releaseGripperJaws: vi.fn().mockResolvedValue(undefined), + homeExceptPlungers: vi.fn().mockResolvedValue(undefined), }, routeUpdateActions: { - proceedToRouteAndStep: vi.fn(), - proceedNextStep: vi.fn(), - handleMotionRouting: vi.fn(), - stashedMap: { - route: RECOVERY_MAP.MANUAL_MOVE_AND_SKIP.ROUTE, - }, + proceedToRouteAndStep: vi.fn().mockResolvedValue(undefined), + proceedNextStep: vi.fn().mockResolvedValue(undefined), + handleMotionRouting: vi.fn().mockResolvedValue(undefined), }, currentRecoveryOptionUtils: { selectedRecoveryOption: RECOVERY_MAP.MANUAL_MOVE_AND_SKIP.ROUTE, @@ -183,6 +184,7 @@ describe('useGripperRelease', () => { beforeEach(() => { vi.useFakeTimers() + vi.clearAllMocks() }) afterEach(() => { @@ -207,118 +209,143 @@ describe('useGripperRelease', () => { expect(result.current).toBe(0) }) - const IS_DOOR_OPEN = [false, true] - - IS_DOOR_OPEN.forEach(doorStatus => { - it(`releases gripper jaws and proceeds to next step after countdown for ${RECOVERY_MAP.MANUAL_REPLACE_AND_RETRY.ROUTE} when the isDoorOpen is ${doorStatus}`, async () => { - renderHook(() => - useGripperRelease({ + describe('when door is closed', () => { + it.each([ + { + recoveryOption: RECOVERY_MAP.MANUAL_MOVE_AND_SKIP.ROUTE, + nextStep: RECOVERY_MAP.MANUAL_MOVE_AND_SKIP.STEPS.MANUAL_MOVE, + }, + { + recoveryOption: RECOVERY_MAP.MANUAL_REPLACE_AND_RETRY.ROUTE, + nextStep: RECOVERY_MAP.MANUAL_REPLACE_AND_RETRY.STEPS.MANUAL_REPLACE, + }, + ])( + 'executes the full sequence of commands for $recoveryOption', + async ({ recoveryOption, nextStep }) => { + const props = { ...mockProps, - doorStatusUtils: { isDoorOpen: doorStatus }, - }) - ) - - act(() => { - vi.advanceTimersByTime(GRIPPER_RELEASE_COUNTDOWN_S * 1000) - }) + currentRecoveryOptionUtils: { + selectedRecoveryOption: recoveryOption, + }, + doorStatusUtils: { isDoorOpen: false }, + } - await vi.runAllTimersAsync() + renderHook(() => useGripperRelease(props)) - expect(mockProps.recoveryCommands.releaseGripperJaws).toHaveBeenCalled() - expect( - mockProps.routeUpdateActions.handleMotionRouting - ).toHaveBeenCalledWith(false) - if (!doorStatus) { - expect( - mockProps.routeUpdateActions.proceedToRouteAndStep - ).toHaveBeenCalledWith( - RECOVERY_MAP.MANUAL_MOVE_AND_SKIP.ROUTE, - RECOVERY_MAP.MANUAL_MOVE_AND_SKIP.STEPS.MANUAL_MOVE - ) - } else { - expect( - mockProps.routeUpdateActions.proceedToRouteAndStep - ).toHaveBeenCalledWith( - RECOVERY_MAP.MANUAL_MOVE_AND_SKIP.ROUTE, - RECOVERY_MAP.MANUAL_MOVE_AND_SKIP.STEPS.CLOSE_DOOR_GRIPPER_Z_HOME + act(() => { + vi.advanceTimersByTime(GRIPPER_RELEASE_COUNTDOWN_S * 1000) + }) + await vi.runAllTimersAsync() + + const { + releaseGripperJaws, + homeExceptPlungers, + } = props.recoveryCommands + const { + handleMotionRouting, + proceedToRouteAndStep, + } = props.routeUpdateActions + + expect(releaseGripperJaws).toHaveBeenCalledTimes(1) + expect(handleMotionRouting).toHaveBeenNthCalledWith(1, true) + expect(homeExceptPlungers).toHaveBeenCalledTimes(1) + expect(handleMotionRouting).toHaveBeenNthCalledWith(2, false) + expect(proceedToRouteAndStep).toHaveBeenCalledWith( + recoveryOption, + nextStep ) } + ) + + describe('when door is open', () => { + it.each([ + { + recoveryOption: RECOVERY_MAP.MANUAL_MOVE_AND_SKIP.ROUTE, + doorStep: + RECOVERY_MAP.MANUAL_MOVE_AND_SKIP.STEPS.CLOSE_DOOR_GRIPPER_Z_HOME, + }, + { + recoveryOption: RECOVERY_MAP.MANUAL_REPLACE_AND_RETRY.ROUTE, + doorStep: + RECOVERY_MAP.MANUAL_REPLACE_AND_RETRY.STEPS + .CLOSE_DOOR_GRIPPER_Z_HOME, + }, + ])( + 'executes proceed to door step for $recoveryOption', + async ({ recoveryOption, doorStep }) => { + const props = { + ...mockProps, + currentRecoveryOptionUtils: { + selectedRecoveryOption: recoveryOption, + }, + doorStatusUtils: { isDoorOpen: true }, + } + + const { + releaseGripperJaws, + homeExceptPlungers, + } = props.recoveryCommands + const { + handleMotionRouting, + proceedToRouteAndStep, + } = props.routeUpdateActions + + renderHook(() => useGripperRelease(props)) + + act(() => { + vi.advanceTimersByTime(GRIPPER_RELEASE_COUNTDOWN_S * 1000) + }) + await vi.runAllTimersAsync() + + expect(releaseGripperJaws).toHaveBeenCalledTimes(1) + expect(handleMotionRouting).toHaveBeenNthCalledWith(1, false) + expect(homeExceptPlungers).not.toHaveBeenCalled() + expect(proceedToRouteAndStep).toHaveBeenCalledWith( + recoveryOption, + doorStep + ) + } + ) }) - }) - IS_DOOR_OPEN.forEach(doorStatus => { - it(`releases gripper jaws and proceeds to next step after countdown for ${RECOVERY_MAP.MANUAL_MOVE_AND_SKIP.ROUTE} when the isDoorOpen is ${doorStatus}`, async () => { - const modifiedProps = { + it('falls back to option selection for unhandled routes when door is open', async () => { + const props = { ...mockProps, - routeUpdateActions: { - ...mockProps.routeUpdateActions, - stashedMap: { - route: RECOVERY_MAP.MANUAL_MOVE_AND_SKIP.ROUTE, - }, - }, currentRecoveryOptionUtils: { - selectedRecoveryOption: RECOVERY_MAP.MANUAL_MOVE_AND_SKIP.ROUTE, + selectedRecoveryOption: 'UNHANDLED_ROUTE', }, + doorStatusUtils: { isDoorOpen: true }, } - renderHook(() => - useGripperRelease({ - ...modifiedProps, - doorStatusUtils: { isDoorOpen: doorStatus }, - }) - ) + renderHook(() => useGripperRelease(props)) act(() => { vi.advanceTimersByTime(GRIPPER_RELEASE_COUNTDOWN_S * 1000) }) - await vi.runAllTimersAsync() - expect(mockProps.recoveryCommands.releaseGripperJaws).toHaveBeenCalled() expect( - mockProps.routeUpdateActions.handleMotionRouting - ).toHaveBeenCalledWith(false) - if (!doorStatus) { - expect( - mockProps.routeUpdateActions.proceedToRouteAndStep - ).toHaveBeenCalledWith( - RECOVERY_MAP.MANUAL_MOVE_AND_SKIP.ROUTE, - RECOVERY_MAP.MANUAL_MOVE_AND_SKIP.STEPS.MANUAL_MOVE - ) - } else { - expect( - mockProps.routeUpdateActions.proceedToRouteAndStep - ).toHaveBeenCalledWith( - RECOVERY_MAP.MANUAL_MOVE_AND_SKIP.ROUTE, - RECOVERY_MAP.MANUAL_MOVE_AND_SKIP.STEPS.CLOSE_DOOR_GRIPPER_Z_HOME - ) - } + props.routeUpdateActions.proceedToRouteAndStep + ).toHaveBeenCalledWith(RECOVERY_MAP.OPTION_SELECTION.ROUTE) }) - }) - it('calls proceedNextStep for unhandled routes', async () => { - const modifiedProps = { - ...mockProps, - routeUpdateActions: { - ...mockProps.routeUpdateActions, - stashedMap: { - route: 'UNHANDLED_ROUTE', + it('falls back to proceedNextStep for unhandled routes when door is closed', async () => { + const props = { + ...mockProps, + currentRecoveryOptionUtils: { + selectedRecoveryOption: 'UNHANDLED_ROUTE', }, - }, - currentRecoveryOptionUtils: { - selectedRecoveryOption: RECOVERY_MAP.MANUAL_FILL_AND_SKIP.ROUTE, - }, - doorStatusUtils: { isDoorOpen: false }, - } - - renderHook(() => useGripperRelease(modifiedProps)) + doorStatusUtils: { isDoorOpen: false }, + } - act(() => { - vi.advanceTimersByTime(GRIPPER_RELEASE_COUNTDOWN_S * 1000) - }) + renderHook(() => useGripperRelease(props)) - await vi.runAllTimersAsync() + act(() => { + vi.advanceTimersByTime(GRIPPER_RELEASE_COUNTDOWN_S * 1000) + }) + await vi.runAllTimersAsync() - expect(modifiedProps.routeUpdateActions.proceedNextStep).toHaveBeenCalled() + expect(props.routeUpdateActions.proceedNextStep).toHaveBeenCalled() + }) }) }) diff --git a/app/src/organisms/ErrorRecoveryFlows/__tests__/RecoverySplash.test.tsx b/app/src/organisms/ErrorRecoveryFlows/__tests__/RecoverySplash.test.tsx index 6bb18c4c5ec..17b7f9fd24d 100644 --- a/app/src/organisms/ErrorRecoveryFlows/__tests__/RecoverySplash.test.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/__tests__/RecoverySplash.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { MemoryRouter } from 'react-router-dom' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { fireEvent, screen, waitFor, renderHook } from '@testing-library/react' @@ -21,6 +20,7 @@ import { StepInfo } from '../shared' import { useToaster } from '../../ToasterOven' import { clickButtonLabeled } from './util' +import type { ComponentProps, FunctionComponent, ReactNode } from 'react' import type { Store } from 'redux' vi.mock('/app/redux/config') @@ -30,7 +30,7 @@ vi.mock('../../ToasterOven') const store: Store = createStore(vi.fn(), {}) describe('useRunPausedSplash', () => { - let wrapper: React.FunctionComponent<{ children: React.ReactNode }> + let wrapper: FunctionComponent<{ children: ReactNode }> beforeEach(() => { vi.mocked(getIsOnDevice).mockReturnValue(true) const queryClient = new QueryClient() @@ -65,7 +65,7 @@ describe('useRunPausedSplash', () => { }) }) -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders( @@ -77,14 +77,17 @@ const render = (props: React.ComponentProps) => { } describe('RecoverySplash', () => { - let props: React.ComponentProps + let props: ComponentProps const mockToggleERWiz = vi.fn(() => Promise.resolve()) const mockProceedToRouteAndStep = vi.fn() + const mockHandleMotionRouting = vi.fn(() => Promise.resolve()) const mockRouteUpdateActions = { proceedToRouteAndStep: mockProceedToRouteAndStep, + handleMotionRouting: mockHandleMotionRouting, } as any const mockMakeToast = vi.fn() const mockResumeRecovery = vi.fn() + const mockHomePipetteZAxes = vi.fn(() => Promise.resolve()) beforeEach(() => { props = { @@ -96,6 +99,7 @@ describe('RecoverySplash', () => { resumeRecovery: mockResumeRecovery, } as any, resumePausedRecovery: true, + recoveryCommands: { homePipetteZAxes: mockHomePipetteZAxes } as any, } vi.mocked(StepInfo).mockReturnValue(
        MOCK STEP INFO
        ) @@ -162,6 +166,13 @@ describe('RecoverySplash', () => { await waitFor(() => { expect(mockToggleERWiz).toHaveBeenCalledWith(true, true) }) + + expect(mockHandleMotionRouting).toHaveBeenNthCalledWith(1, true) + expect(mockHandleMotionRouting).toHaveBeenNthCalledWith(2, false) + + await waitFor(() => { + expect(props.recoveryCommands.homePipetteZAxes).toHaveBeenCalled() + }) }) it('should render a door open toast if the door is open', () => { diff --git a/app/src/organisms/ErrorRecoveryFlows/__tests__/RecoveryTakeover.test.tsx b/app/src/organisms/ErrorRecoveryFlows/__tests__/RecoveryTakeover.test.tsx index 1eec7782713..6a71c84ba3c 100644 --- a/app/src/organisms/ErrorRecoveryFlows/__tests__/RecoveryTakeover.test.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/__tests__/RecoveryTakeover.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { describe, it, vi, expect, beforeEach } from 'vitest' import { screen } from '@testing-library/react' @@ -14,18 +13,19 @@ import { RecoveryTakeover, RecoveryTakeoverDesktop } from '../RecoveryTakeover' import { useUpdateClientDataRecovery } from '/app/resources/client_data' import { clickButtonLabeled } from './util' +import type { ComponentProps } from 'react' import type { Mock } from 'vitest' vi.mock('/app/resources/client_data') -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, })[0] } describe('RecoveryTakeover', () => { - let props: React.ComponentProps + let props: ComponentProps let mockClearClientData: Mock beforeEach(() => { @@ -91,7 +91,7 @@ describe('RecoveryTakeover', () => { }) describe('RecoveryTakeoverDesktop', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { diff --git a/app/src/organisms/ErrorRecoveryFlows/constants.ts b/app/src/organisms/ErrorRecoveryFlows/constants.ts index 75835fd29f3..8be1b6adbe1 100644 --- a/app/src/organisms/ErrorRecoveryFlows/constants.ts +++ b/app/src/organisms/ErrorRecoveryFlows/constants.ts @@ -20,6 +20,7 @@ export const DEFINED_ERROR_TYPES = { TIP_PHYSICALLY_MISSING: 'tipPhysicallyMissing', TIP_PHYSICALLY_ATTACHED: 'tipPhysicallyAttached', GRIPPER_MOVEMENT: 'gripperMovement', + STALL_OR_COLLISION: 'stallOrCollision', } // Client-defined error-handling flows. @@ -32,6 +33,7 @@ export const ERROR_KINDS = { TIP_NOT_DETECTED: 'TIP_NOT_DETECTED', TIP_DROP_FAILED: 'TIP_DROP_FAILED', GRIPPER_ERROR: 'GRIPPER_ERROR', + STALL_OR_COLLISION: 'STALL_OR_COLLISION', } as const // TODO(jh, 06-14-24): Consolidate motion routes to a single route with several steps. @@ -55,6 +57,18 @@ export const RECOVERY_MAP = { DROP_TIP_GENERAL_ERROR: 'drop-tip-general-error', }, }, + HOME_AND_RETRY: { + ROUTE: 'home-and-retry', + STEPS: { + PREPARE_DECK_FOR_HOME: 'prepare-deck-for-home', + REMOVE_TIPS_FROM_PIPETTE: 'remove-tips-from-pipette', + REPLACE_TIPS: 'replace-tips', + SELECT_TIPS: 'select-tips', + HOME_BEFORE_RETRY: 'home-before-retry', + CLOSE_DOOR_AND_HOME: 'close-door-and-home', + CONFIRM_RETRY: 'confirm-retry', + }, + }, ROBOT_CANCELING: { ROUTE: 'robot-cancel-run', STEPS: { @@ -210,6 +224,7 @@ const { MANUAL_REPLACE_AND_RETRY, SKIP_STEP_WITH_NEW_TIPS, SKIP_STEP_WITH_SAME_TIPS, + HOME_AND_RETRY, } = RECOVERY_MAP // The deterministic ordering of steps for a given route. @@ -277,6 +292,15 @@ export const STEP_ORDER: StepOrder = { ERROR_WHILE_RECOVERING.STEPS.DROP_TIP_TIP_DROP_FAILED, ERROR_WHILE_RECOVERING.STEPS.DROP_TIP_BLOWOUT_FAILED, ], + [HOME_AND_RETRY.ROUTE]: [ + HOME_AND_RETRY.STEPS.PREPARE_DECK_FOR_HOME, + HOME_AND_RETRY.STEPS.REMOVE_TIPS_FROM_PIPETTE, + HOME_AND_RETRY.STEPS.REPLACE_TIPS, + HOME_AND_RETRY.STEPS.SELECT_TIPS, + HOME_AND_RETRY.STEPS.HOME_BEFORE_RETRY, + HOME_AND_RETRY.STEPS.CLOSE_DOOR_AND_HOME, + HOME_AND_RETRY.STEPS.CONFIRM_RETRY, + ], } // Contains metadata specific to all routes and/or steps. @@ -333,6 +357,15 @@ export const RECOVERY_MAP_METADATA: RecoveryRouteStepMetadata = { [ROBOT_DOOR_OPEN.ROUTE]: { [ROBOT_DOOR_OPEN.STEPS.DOOR_OPEN]: { allowDoorOpen: false }, }, + [HOME_AND_RETRY.ROUTE]: { + [HOME_AND_RETRY.STEPS.PREPARE_DECK_FOR_HOME]: { allowDoorOpen: true }, + [HOME_AND_RETRY.STEPS.REMOVE_TIPS_FROM_PIPETTE]: { allowDoorOpen: true }, + [HOME_AND_RETRY.STEPS.REPLACE_TIPS]: { allowDoorOpen: true }, + [HOME_AND_RETRY.STEPS.SELECT_TIPS]: { allowDoorOpen: true }, + [HOME_AND_RETRY.STEPS.HOME_BEFORE_RETRY]: { allowDoorOpen: true }, + [HOME_AND_RETRY.STEPS.CLOSE_DOOR_AND_HOME]: { allowDoorOpen: true }, + [HOME_AND_RETRY.STEPS.CONFIRM_RETRY]: { allowDoorOpen: true }, + }, [ROBOT_DOOR_OPEN_SPECIAL.ROUTE]: { [ROBOT_DOOR_OPEN_SPECIAL.STEPS.DOOR_OPEN]: { allowDoorOpen: true }, }, diff --git a/app/src/organisms/ErrorRecoveryFlows/hooks/__tests__/useDeckMapUtils.test.ts b/app/src/organisms/ErrorRecoveryFlows/hooks/__tests__/useDeckMapUtils.test.ts index 1a6d07ba634..5b6855a9e7d 100644 --- a/app/src/organisms/ErrorRecoveryFlows/hooks/__tests__/useDeckMapUtils.test.ts +++ b/app/src/organisms/ErrorRecoveryFlows/hooks/__tests__/useDeckMapUtils.test.ts @@ -198,17 +198,7 @@ describe('getRunCurrentModulesInfo', () => { const result = getRunCurrentModulesInfo({ runRecord: null as any, deckDef: mockDeckDef, - labwareDefinitionsByUri: {}, - }) - - expect(result).toEqual([]) - }) - - it('should return an empty array if protocolAnalysis is null', () => { - const result = getRunCurrentModulesInfo({ - runRecord: mockRunRecord, - deckDef: mockDeckDef, - labwareDefinitionsByUri: null, + runLwDefsByUri: {}, }) expect(result).toEqual([]) @@ -219,7 +209,7 @@ describe('getRunCurrentModulesInfo', () => { const result = getRunCurrentModulesInfo({ runRecord: mockRunRecord, deckDef: mockDeckDef, - labwareDefinitionsByUri: { + runLwDefsByUri: { 'opentrons/opentrons_96_pcr_adapter/1': 'MOCK_LW_DEF', } as any, }) @@ -242,7 +232,7 @@ describe('getRunCurrentModulesInfo', () => { data: { modules: [mockModule], labware: [] }, }, deckDef: mockDeckDef, - labwareDefinitionsByUri: {}, + runLwDefsByUri: {}, }) expect(result).toEqual([ { @@ -261,7 +251,7 @@ describe('getRunCurrentModulesInfo', () => { const result = getRunCurrentModulesInfo({ runRecord: mockRunRecord, deckDef: mockDeckDef, - labwareDefinitionsByUri: null, + runLwDefsByUri: {}, }) expect(result).toEqual([]) }) @@ -286,7 +276,7 @@ describe('getRunCurrentLabwareInfo', () => { it('should return an empty array if runRecord is null', () => { const result = getRunCurrentLabwareInfo({ runRecord: undefined, - labwareDefinitionsByUri: {} as any, + runLwDefsByUri: {} as any, }) expect(result).toEqual([]) @@ -295,7 +285,7 @@ describe('getRunCurrentLabwareInfo', () => { it('should return an empty array if protocolAnalysis is null', () => { const result = getRunCurrentLabwareInfo({ runRecord: { data: { labware: [] } } as any, - labwareDefinitionsByUri: null, + runLwDefsByUri: {}, }) expect(result).toEqual([]) @@ -309,7 +299,7 @@ describe('getRunCurrentLabwareInfo', () => { const result = getRunCurrentLabwareInfo({ runRecord: { data: { labware: [mockPickUpTipLwSlotName] } } as any, - labwareDefinitionsByUri: { + runLwDefsByUri: { [mockPickUpTipLabware.definitionUri]: mockLabwareDef, }, }) diff --git a/app/src/organisms/ErrorRecoveryFlows/hooks/__tests__/useHomeGripper.test.ts b/app/src/organisms/ErrorRecoveryFlows/hooks/__tests__/useHomeGripper.test.ts deleted file mode 100644 index 32de0f0096d..00000000000 --- a/app/src/organisms/ErrorRecoveryFlows/hooks/__tests__/useHomeGripper.test.ts +++ /dev/null @@ -1,124 +0,0 @@ -import { renderHook, act } from '@testing-library/react' -import { describe, it, expect, vi, beforeEach } from 'vitest' - -import { useHomeGripper } from '../useHomeGripper' -import { RECOVERY_MAP } from '/app/organisms/ErrorRecoveryFlows/constants' - -describe('useHomeGripper', () => { - const mockRecoveryCommands = { - homeExceptPlungers: vi.fn().mockResolvedValue(undefined), - } - - const mockRouteUpdateActions = { - handleMotionRouting: vi.fn().mockResolvedValue(undefined), - goBackPrevStep: vi.fn(), - } - - const mockRecoveryMap = { - step: RECOVERY_MAP.MANUAL_REPLACE_AND_RETRY.STEPS.MANUAL_REPLACE, - } - - const mockDoorStatusUtils = { - isDoorOpen: false, - } - - beforeEach(() => { - vi.clearAllMocks() - }) - - it('should home gripper Z axis when in manual gripper step and door is closed', async () => { - renderHook(() => { - useHomeGripper({ - recoveryCommands: mockRecoveryCommands, - routeUpdateActions: mockRouteUpdateActions, - recoveryMap: mockRecoveryMap, - doorStatusUtils: mockDoorStatusUtils, - } as any) - }) - - await act(async () => { - await new Promise(resolve => setTimeout(resolve, 0)) - }) - - expect(mockRouteUpdateActions.handleMotionRouting).toHaveBeenCalledWith( - true - ) - expect(mockRecoveryCommands.homeExceptPlungers).toHaveBeenCalled() - expect(mockRouteUpdateActions.handleMotionRouting).toHaveBeenCalledWith( - false - ) - }) - - it('should go back to previous step when door is open', () => { - renderHook(() => { - useHomeGripper({ - recoveryCommands: mockRecoveryCommands, - routeUpdateActions: mockRouteUpdateActions, - recoveryMap: mockRecoveryMap, - doorStatusUtils: { ...mockDoorStatusUtils, isDoorOpen: true }, - } as any) - }) - - expect(mockRouteUpdateActions.goBackPrevStep).toHaveBeenCalled() - expect(mockRecoveryCommands.homeExceptPlungers).not.toHaveBeenCalled() - }) - - it('should not home again if already homed once', async () => { - const { rerender } = renderHook(() => { - useHomeGripper({ - recoveryCommands: mockRecoveryCommands, - routeUpdateActions: mockRouteUpdateActions, - recoveryMap: mockRecoveryMap, - doorStatusUtils: mockDoorStatusUtils, - } as any) - }) - - await act(async () => { - await new Promise(resolve => setTimeout(resolve, 0)) - }) - - expect(mockRecoveryCommands.homeExceptPlungers).toHaveBeenCalledTimes(1) - - rerender() - - expect(mockRecoveryCommands.homeExceptPlungers).toHaveBeenCalledTimes(1) - }) - - it('should only reset hasHomedOnce when step changes to non-manual gripper step', async () => { - const { rerender } = renderHook( - ({ recoveryMap }) => { - useHomeGripper({ - recoveryCommands: mockRecoveryCommands, - routeUpdateActions: mockRouteUpdateActions, - recoveryMap, - doorStatusUtils: mockDoorStatusUtils, - } as any) - }, - { - initialProps: { recoveryMap: mockRecoveryMap }, - } - ) - - await act(async () => { - await new Promise(resolve => setTimeout(resolve, 0)) - }) - - expect(mockRecoveryCommands.homeExceptPlungers).toHaveBeenCalledTimes(1) - - rerender({ recoveryMap: { step: 'SOME_OTHER_STEP' } as any }) - - await act(async () => { - await new Promise(resolve => setTimeout(resolve, 0)) - }) - - expect(mockRecoveryCommands.homeExceptPlungers).toHaveBeenCalledTimes(1) - - rerender({ recoveryMap: mockRecoveryMap }) - - await act(async () => { - await new Promise(resolve => setTimeout(resolve, 0)) - }) - - expect(mockRecoveryCommands.homeExceptPlungers).toHaveBeenCalledTimes(1) - }) -}) diff --git a/app/src/organisms/ErrorRecoveryFlows/hooks/__tests__/useRecoveryOptionCopy.test.tsx b/app/src/organisms/ErrorRecoveryFlows/hooks/__tests__/useRecoveryOptionCopy.test.tsx index 62a810cd96e..0ec262c3f41 100644 --- a/app/src/organisms/ErrorRecoveryFlows/hooks/__tests__/useRecoveryOptionCopy.test.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/hooks/__tests__/useRecoveryOptionCopy.test.tsx @@ -1,5 +1,3 @@ -import type * as React from 'react' - import { describe, it } from 'vitest' import { screen } from '@testing-library/react' @@ -10,6 +8,8 @@ import type { ErrorKind, RecoveryRoute } from '../../types' import { renderWithProviders } from '/app/__testing-utils__' import { i18n } from '/app/i18n' +import type { ComponentProps } from 'react' + function MockRenderCmpt({ route, errorKind, @@ -26,7 +26,7 @@ function MockRenderCmpt({ ) } -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, })[0] @@ -111,6 +111,11 @@ describe('useRecoveryOptionCopy', () => { screen.getByText('Manually replace labware on deck and retry step') }) + it(`renders the correct copy for ${RECOVERY_MAP.HOME_AND_RETRY.ROUTE}`, () => { + render({ route: RECOVERY_MAP.HOME_AND_RETRY.ROUTE }) + screen.getByText('Home gantry and retry step') + }) + it('renders "Unknown action" for an unknown recovery option', () => { render({ route: 'unknown_route' as RecoveryRoute }) diff --git a/app/src/organisms/ErrorRecoveryFlows/hooks/__tests__/useRecoveryToasts.test.tsx b/app/src/organisms/ErrorRecoveryFlows/hooks/__tests__/useRecoveryToasts.test.tsx index 085551f42e7..1ce852e86aa 100644 --- a/app/src/organisms/ErrorRecoveryFlows/hooks/__tests__/useRecoveryToasts.test.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/hooks/__tests__/useRecoveryToasts.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { vi, describe, it, expect, beforeEach } from 'vitest' import { I18nextProvider } from 'react-i18next' import { i18n } from '/app/i18n' @@ -16,6 +15,7 @@ import { RECOVERY_MAP } from '../../constants' import { useToaster } from '../../../ToasterOven' import { useCommandTextString } from '/app/local-resources/commands' +import type { ReactElement } from 'react' import type { Mock } from 'vitest' import type { BuildToast } from '../useRecoveryToasts' @@ -42,7 +42,7 @@ const DEFAULT_PROPS: BuildToast = { } // Utility function for rendering with I18nextProvider -const renderWithI18n = (component: React.ReactElement) => { +const renderWithI18n = (component: ReactElement) => { return render({component}) } diff --git a/app/src/organisms/ErrorRecoveryFlows/hooks/index.ts b/app/src/organisms/ErrorRecoveryFlows/hooks/index.ts index 75904a24966..497fd3223da 100644 --- a/app/src/organisms/ErrorRecoveryFlows/hooks/index.ts +++ b/app/src/organisms/ErrorRecoveryFlows/hooks/index.ts @@ -5,7 +5,6 @@ export { useRouteUpdateActions } from './useRouteUpdateActions' export { useERUtils } from './useERUtils' export { useRecoveryTakeover } from './useRecoveryTakeover' export { useRetainedFailedCommandBySource } from './useRetainedFailedCommandBySource' -export { useHomeGripper } from './useHomeGripper' export type { ERUtilsProps } from './useERUtils' export type { UseRouteUpdateActionsResult } from './useRouteUpdateActions' diff --git a/app/src/organisms/ErrorRecoveryFlows/hooks/useDeckMapUtils.ts b/app/src/organisms/ErrorRecoveryFlows/hooks/useDeckMapUtils.ts index 458747f5b07..22dd645835a 100644 --- a/app/src/organisms/ErrorRecoveryFlows/hooks/useDeckMapUtils.ts +++ b/app/src/organisms/ErrorRecoveryFlows/hooks/useDeckMapUtils.ts @@ -42,7 +42,7 @@ interface UseDeckMapUtilsProps { runId: ErrorRecoveryFlowsProps['runId'] protocolAnalysis: ErrorRecoveryFlowsProps['protocolAnalysis'] failedLabwareUtils: UseFailedLabwareUtilsResult - labwareDefinitionsByUri: ERUtilsProps['labwareDefinitionsByUri'] + runLwDefsByUri: ERUtilsProps['runLwDefsByUri'] runRecord: Run | undefined } @@ -65,7 +65,7 @@ export function useDeckMapUtils({ runRecord, runId, failedLabwareUtils, - labwareDefinitionsByUri, + runLwDefsByUri, }: UseDeckMapUtilsProps): UseDeckMapUtilsResult { const robotType = protocolAnalysis?.robotType ?? OT2_ROBOT_TYPE const deckConfig = getSimplestDeckConfigForProtocol(protocolAnalysis) @@ -78,9 +78,9 @@ export function useDeckMapUtils({ getRunCurrentModulesInfo({ runRecord, deckDef, - labwareDefinitionsByUri, + runLwDefsByUri, }), - [runRecord, deckDef, labwareDefinitionsByUri] + [runRecord, deckDef, runLwDefsByUri] ) const runCurrentModules = useMemo( @@ -94,8 +94,8 @@ export function useDeckMapUtils({ ) const currentLabwareInfo = useMemo( - () => getRunCurrentLabwareInfo({ runRecord, labwareDefinitionsByUri }), - [runRecord, labwareDefinitionsByUri] + () => getRunCurrentLabwareInfo({ runRecord, runLwDefsByUri }), + [runRecord, runLwDefsByUri] ) const { updatedModules, remainingLabware } = useMemo( @@ -114,32 +114,24 @@ export function useDeckMapUtils({ ) const movedLabwareDef = - labwareDefinitionsByUri != null && failedLabwareUtils.failedLabware != null - ? labwareDefinitionsByUri[failedLabwareUtils.failedLabware.definitionUri] + runLwDefsByUri != null && failedLabwareUtils.failedLabware != null + ? runLwDefsByUri[failedLabwareUtils.failedLabware.definitionUri] : null const moduleRenderInfo = useMemo( () => - runRecord != null && labwareDefinitionsByUri != null - ? getRunModuleRenderInfo( - runRecord.data, - deckDef, - labwareDefinitionsByUri - ) + runRecord != null && runLwDefsByUri != null + ? getRunModuleRenderInfo(runRecord.data, deckDef, runLwDefsByUri) : [], - [deckDef, labwareDefinitionsByUri, runRecord] + [deckDef, runLwDefsByUri, runRecord] ) const labwareRenderInfo = useMemo( () => - runRecord != null && labwareDefinitionsByUri != null - ? getRunLabwareRenderInfo( - runRecord.data, - labwareDefinitionsByUri, - deckDef - ) + runRecord != null && runLwDefsByUri != null + ? getRunLabwareRenderInfo(runRecord.data, runLwDefsByUri, deckDef) : [], - [deckDef, labwareDefinitionsByUri, runRecord] + [deckDef, runLwDefsByUri, runRecord] ) return { @@ -258,13 +250,13 @@ interface RunCurrentModuleInfo { export const getRunCurrentModulesInfo = ({ runRecord, deckDef, - labwareDefinitionsByUri, + runLwDefsByUri, }: { runRecord: UseDeckMapUtilsProps['runRecord'] deckDef: DeckDefinition - labwareDefinitionsByUri?: LabwareDefinitionsByUri | null + runLwDefsByUri: UseDeckMapUtilsProps['runLwDefsByUri'] }): RunCurrentModuleInfo[] => { - if (runRecord == null || labwareDefinitionsByUri == null) { + if (runRecord == null) { return [] } else { return runRecord.data.modules.reduce( @@ -281,7 +273,7 @@ export const getRunCurrentModulesInfo = ({ const nestedLabwareDef = nestedLabware != null - ? labwareDefinitionsByUri[nestedLabware.definitionUri] + ? runLwDefsByUri[nestedLabware.definitionUri] : null const slotPosition = getPositionFromSlotId( @@ -325,12 +317,12 @@ interface RunCurrentLabwareInfo { // Derive the labware info necessary to render labware on the deck. export function getRunCurrentLabwareInfo({ runRecord, - labwareDefinitionsByUri, + runLwDefsByUri, }: { runRecord: UseDeckMapUtilsProps['runRecord'] - labwareDefinitionsByUri?: LabwareDefinitionsByUri | null + runLwDefsByUri: UseDeckMapUtilsProps['runLwDefsByUri'] }): RunCurrentLabwareInfo[] { - if (runRecord == null || labwareDefinitionsByUri == null) { + if (runRecord == null) { return [] } else { const allLabware = runRecord.data.labware.reduce( @@ -341,7 +333,7 @@ export function getRunCurrentLabwareInfo({ runRecord, true ) // Exclude modules since handled separately. - const labwareDef = getLabwareDefinition(lw, labwareDefinitionsByUri) + const labwareDef = getLabwareDefinition(lw, runLwDefsByUri) if (slotName == null || labwareLocation == null) { return acc diff --git a/app/src/organisms/ErrorRecoveryFlows/hooks/useERUtils.ts b/app/src/organisms/ErrorRecoveryFlows/hooks/useERUtils.ts index 72e9bb481bc..4bd4a93643f 100644 --- a/app/src/organisms/ErrorRecoveryFlows/hooks/useERUtils.ts +++ b/app/src/organisms/ErrorRecoveryFlows/hooks/useERUtils.ts @@ -22,11 +22,7 @@ import { useCleanupRecoveryState } from './useCleanupRecoveryState' import { useFailedPipetteUtils } from './useFailedPipetteUtils' import { getRunningStepCountsFrom } from '/app/resources/protocols' -import type { - LabwareDefinition2, - LabwareDefinitionsByUri, - RobotType, -} from '@opentrons/shared-data' +import type { LabwareDefinition2, RobotType } from '@opentrons/shared-data' import type { IRecoveryMap, RouteStep, RecoveryRoute } from '../types' import type { ErrorRecoveryFlowsProps } from '..' import type { UseRouteUpdateActionsResult } from './useRouteUpdateActions' @@ -54,7 +50,6 @@ export type ERUtilsProps = Omit & { failedCommand: ReturnType isActiveUser: UseRecoveryTakeoverResult['isActiveUser'] allRunDefs: LabwareDefinition2[] - labwareDefinitionsByUri: LabwareDefinitionsByUri | null } export interface ERUtilsResults { @@ -90,7 +85,7 @@ export function useERUtils({ isActiveUser, allRunDefs, unvalidatedFailedCommand, - labwareDefinitionsByUri, + runLwDefsByUri, }: ERUtilsProps): ERUtilsResults { const { data: attachedInstruments } = useInstrumentsQuery() const { data: runRecord } = useNotifyRunQuery(runId) @@ -185,7 +180,7 @@ export function useERUtils({ runRecord, protocolAnalysis, failedLabwareUtils, - labwareDefinitionsByUri, + runLwDefsByUri, }) const recoveryActionMutationUtils = useRecoveryActionMutation( diff --git a/app/src/organisms/ErrorRecoveryFlows/hooks/useErrorName.ts b/app/src/organisms/ErrorRecoveryFlows/hooks/useErrorName.ts index 6acd0df2f45..0279b8b675a 100644 --- a/app/src/organisms/ErrorRecoveryFlows/hooks/useErrorName.ts +++ b/app/src/organisms/ErrorRecoveryFlows/hooks/useErrorName.ts @@ -23,6 +23,8 @@ export function useErrorName(errorKind: ErrorKind): string { return t('tip_drop_failed') case ERROR_KINDS.GRIPPER_ERROR: return t('gripper_error') + case ERROR_KINDS.STALL_OR_COLLISION: + return t('stall_or_collision_error') default: return t('error') } diff --git a/app/src/organisms/ErrorRecoveryFlows/hooks/useFailedLabwareUtils.ts b/app/src/organisms/ErrorRecoveryFlows/hooks/useFailedLabwareUtils.ts index bc077d4c624..f1a57aa965f 100644 --- a/app/src/organisms/ErrorRecoveryFlows/hooks/useFailedLabwareUtils.ts +++ b/app/src/organisms/ErrorRecoveryFlows/hooks/useFailedLabwareUtils.ts @@ -148,6 +148,7 @@ export function getRelevantFailedLabwareCmdFrom({ case ERROR_KINDS.OVERPRESSURE_PREPARE_TO_ASPIRATE: case ERROR_KINDS.OVERPRESSURE_WHILE_ASPIRATING: case ERROR_KINDS.OVERPRESSURE_WHILE_DISPENSING: + case ERROR_KINDS.STALL_OR_COLLISION: return getRelevantPickUpTipCommand(failedCommandByRunRecord, runCommands) case ERROR_KINDS.GRIPPER_ERROR: return failedCommandByRunRecord as MoveLabwareRunTimeCommand @@ -155,7 +156,7 @@ export function getRelevantFailedLabwareCmdFrom({ return null default: console.error( - 'No labware associated with failed command. Handle case explicitly.' + `useFailedLabwareUtils: No labware associated with error kind ${errorKind}. Handle case explicitly.` ) return null } diff --git a/app/src/organisms/ErrorRecoveryFlows/hooks/useHomeGripper.ts b/app/src/organisms/ErrorRecoveryFlows/hooks/useHomeGripper.ts deleted file mode 100644 index 55fe64fdcc4..00000000000 --- a/app/src/organisms/ErrorRecoveryFlows/hooks/useHomeGripper.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { useLayoutEffect, useState } from 'react' -import { RECOVERY_MAP } from '/app/organisms/ErrorRecoveryFlows/constants' - -import type { ErrorRecoveryWizardProps } from '/app/organisms/ErrorRecoveryFlows/ErrorRecoveryWizard' - -// Home the gripper implicitly. Because the home is not tied to a CTA, it must be handled here. -export function useHomeGripper({ - recoveryCommands, - routeUpdateActions, - recoveryMap, - doorStatusUtils, -}: ErrorRecoveryWizardProps): void { - const { step } = recoveryMap - const { isDoorOpen } = doorStatusUtils - const [hasHomedOnce, setHasHomedOnce] = useState(false) - - const isManualGripperStep = - step === RECOVERY_MAP.MANUAL_REPLACE_AND_RETRY.STEPS.MANUAL_REPLACE || - step === RECOVERY_MAP.MANUAL_MOVE_AND_SKIP.STEPS.MANUAL_MOVE - - useLayoutEffect(() => { - const { handleMotionRouting, goBackPrevStep } = routeUpdateActions - const { homeExceptPlungers } = recoveryCommands - - if (!hasHomedOnce) { - if (isManualGripperStep) { - if (isDoorOpen) { - void goBackPrevStep() - } else { - void handleMotionRouting(true) - .then(() => homeExceptPlungers()) - .then(() => handleMotionRouting(false)) - .then(() => { - setHasHomedOnce(true) - }) - } - } - } - }, [step, hasHomedOnce, isDoorOpen, isManualGripperStep]) -} diff --git a/app/src/organisms/ErrorRecoveryFlows/hooks/useRecoveryCommands.ts b/app/src/organisms/ErrorRecoveryFlows/hooks/useRecoveryCommands.ts index 3e4b20225c5..01f5c4a7c94 100644 --- a/app/src/organisms/ErrorRecoveryFlows/hooks/useRecoveryCommands.ts +++ b/app/src/organisms/ErrorRecoveryFlows/hooks/useRecoveryCommands.ts @@ -70,6 +70,8 @@ export interface UseRecoveryCommandsResult { homeExceptPlungers: () => Promise /* A non-terminal recovery command */ moveLabwareWithoutPause: () => Promise + /* A non-terminal recovery-command */ + homeAll: () => Promise } // TODO(jh, 07-24-24): Create tighter abstractions for terminal vs. non-terminal commands. @@ -307,6 +309,10 @@ export function useRecoveryCommands({ return chainRunRecoveryCommands([HOME_EXCEPT_PLUNGERS]) }, [chainRunRecoveryCommands]) + const homeAll = useCallback((): Promise => { + return chainRunRecoveryCommands([HOME_ALL]) + }, [chainRunRecoveryCommands]) + const moveLabwareWithoutPause = useCallback((): Promise => { const moveLabwareCmd = buildMoveLabwareWithoutPause( unvalidatedFailedCommand @@ -329,6 +335,7 @@ export function useRecoveryCommands({ moveLabwareWithoutPause, skipFailedCommand, ignoreErrorKindThisRun, + homeAll, } } @@ -371,6 +378,11 @@ export const HOME_EXCEPT_PLUNGERS: CreateCommand = { }, } +export const HOME_ALL: CreateCommand = { + commandType: 'home', + params: {}, +} + const buildMoveLabwareWithoutPause = ( failedCommand: FailedCommand | null ): CreateCommand | null => { diff --git a/app/src/organisms/ErrorRecoveryFlows/hooks/useRecoveryOptionCopy.tsx b/app/src/organisms/ErrorRecoveryFlows/hooks/useRecoveryOptionCopy.tsx index b364af7f9d5..6c7f2f8fc94 100644 --- a/app/src/organisms/ErrorRecoveryFlows/hooks/useRecoveryOptionCopy.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/hooks/useRecoveryOptionCopy.tsx @@ -26,6 +26,8 @@ export function useRecoveryOptionCopy(): ( } case RECOVERY_MAP.CANCEL_RUN.ROUTE: return t('cancel_run') + case RECOVERY_MAP.HOME_AND_RETRY.ROUTE: + return t('home_and_retry') case RECOVERY_MAP.RETRY_NEW_TIPS.ROUTE: return t('retry_with_new_tips') case RECOVERY_MAP.RETRY_SAME_TIPS.ROUTE: diff --git a/app/src/organisms/ErrorRecoveryFlows/hooks/useRecoveryTipStatus.ts b/app/src/organisms/ErrorRecoveryFlows/hooks/useRecoveryTipStatus.ts index 0a12b59d089..8db4af030ea 100644 --- a/app/src/organisms/ErrorRecoveryFlows/hooks/useRecoveryTipStatus.ts +++ b/app/src/organisms/ErrorRecoveryFlows/hooks/useRecoveryTipStatus.ts @@ -1,9 +1,9 @@ import { useState } from 'react' import head from 'lodash/head' -import { useHost, useRunCurrentState } from '@opentrons/react-api-client' +import { useRunCurrentState } from '@opentrons/react-api-client' import { getPipetteModelSpecs } from '@opentrons/shared-data' -import { useTipAttachmentStatus } from '/app/organisms/DropTipWizardFlows' +import { useTipAttachmentStatus } from '/app/resources/instruments' import { ERROR_KINDS } from '/app/organisms/ErrorRecoveryFlows/constants' import { getErrorKind } from '/app/organisms/ErrorRecoveryFlows/utils' @@ -11,7 +11,7 @@ import type { Run, Instruments, PipetteData } from '@opentrons/api-client' import type { PipetteWithTip, TipAttachmentStatusResult, -} from '/app/organisms/DropTipWizardFlows' +} from '/app/resources/instruments' import type { ERUtilsProps } from '/app/organisms/ErrorRecoveryFlows/hooks/useERUtils' interface UseRecoveryTipStatusProps { @@ -38,11 +38,9 @@ export function useRecoveryTipStatus( failedCommandPipette, setFailedCommandPipette, ] = useState(null) - const host = useHost() const tipAttachmentStatusUtils = useTipAttachmentStatus({ ...props, - host, runRecord: props.runRecord ?? null, }) diff --git a/app/src/organisms/ErrorRecoveryFlows/hooks/useRecoveryToasts.ts b/app/src/organisms/ErrorRecoveryFlows/hooks/useRecoveryToasts.ts index 9fef84caca9..a722e6d566d 100644 --- a/app/src/organisms/ErrorRecoveryFlows/hooks/useRecoveryToasts.ts +++ b/app/src/organisms/ErrorRecoveryFlows/hooks/useRecoveryToasts.ts @@ -27,6 +27,7 @@ export function useRecoveryToasts({ ...rest }: BuildToast): RecoveryToasts { const { currentStepNumber, hasRunDiverged } = stepCounts + const { i18n, t } = useTranslation('shared') const { makeToast } = useToaster() const displayType = isOnDevice ? 'odd' : 'desktop' @@ -53,6 +54,10 @@ export function useRecoveryToasts({ const makeSuccessToast = (): void => { if (selectedRecoveryOption !== RECOVERY_MAP.CANCEL_RUN.ROUTE) { makeToast(bodyText, 'success', { + buttonText: + displayType === 'odd' + ? i18n.format(t('shared:close'), 'capitalize') + : undefined, closeButton: true, disableTimeout: true, displayType, @@ -171,6 +176,7 @@ function handleRecoveryOptionAction( case RECOVERY_MAP.RETRY_NEW_TIPS.ROUTE: case RECOVERY_MAP.RETRY_STEP.ROUTE: case RECOVERY_MAP.MANUAL_REPLACE_AND_RETRY.ROUTE: + case RECOVERY_MAP.HOME_AND_RETRY.ROUTE: return currentStepReturnVal default: { return null diff --git a/app/src/organisms/ErrorRecoveryFlows/index.tsx b/app/src/organisms/ErrorRecoveryFlows/index.tsx index 68184d71b46..24834c86305 100644 --- a/app/src/organisms/ErrorRecoveryFlows/index.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/index.tsx @@ -1,4 +1,4 @@ -import { useMemo, useLayoutEffect, useState } from 'react' +import { useLayoutEffect, useState } from 'react' import { useSelector } from 'react-redux' import { @@ -15,10 +15,7 @@ import { RUN_STATUS_STOPPED, RUN_STATUS_SUCCEEDED, } from '@opentrons/api-client' -import { - getLoadedLabwareDefinitionsByUri, - OT2_ROBOT_TYPE, -} from '@opentrons/shared-data' +import { OT2_ROBOT_TYPE } from '@opentrons/shared-data' import { useHost } from '@opentrons/react-api-client' import { getIsOnDevice } from '/app/redux/config' @@ -31,10 +28,12 @@ import { useRecoveryTakeover, useRetainedFailedCommandBySource, } from './hooks' +import { useRunLoadedLabwareDefinitionsByUri } from '/app/resources/runs' import type { RunStatus } from '@opentrons/api-client' import type { CompletedProtocolAnalysis } from '@opentrons/shared-data' import type { FailedCommand } from './types' +import type { RunLoadedLabwareDefinitionsByUri } from '/app/resources/runs' const VALID_ER_RUN_STATUSES: RunStatus[] = [ RUN_STATUS_AWAITING_RECOVERY, @@ -55,10 +54,24 @@ const INVALID_ER_RUN_STATUSES: RunStatus[] = [ RUN_STATUS_IDLE, ] -export interface UseErrorRecoveryResult { +interface UseErrorRecoveryResultBase { isERActive: boolean failedCommand: FailedCommand | null + runLwDefsByUri: ReturnType +} +export interface UseErrorRecoveryActiveResult + extends UseErrorRecoveryResultBase { + isERActive: true + failedCommand: FailedCommand + runLwDefsByUri: RunLoadedLabwareDefinitionsByUri +} +export interface UseErrorRecoveryInactiveResult + extends UseErrorRecoveryResultBase { + isERActive: false } +export type UseErrorRecoveryResult = + | UseErrorRecoveryInactiveResult + | UseErrorRecoveryActiveResult export function useErrorRecoveryFlows( runId: string, @@ -66,6 +79,7 @@ export function useErrorRecoveryFlows( ): UseErrorRecoveryResult { const [isERActive, setIsERActive] = useState(false) const failedCommand = useCurrentlyRecoveringFrom(runId, runStatus) + const runLwDefsByUri = useRunLoadedLabwareDefinitionsByUri(runId) // The complexity of this logic exists to persist Error Recovery screens past the server's definition of Error Recovery. // Ex, show a "cancelling run" modal in Error Recovery flows despite the robot no longer being in a recoverable state. @@ -87,8 +101,7 @@ export function useErrorRecoveryFlows( if (runStatus != null) { const isAwaitingRecovery = VALID_ER_RUN_STATUSES.includes(runStatus) && - runStatus !== RUN_STATUS_STOP_REQUESTED && - failedCommand != null // Prevents one render cycle of an unknown failed command. + runStatus !== RUN_STATUS_STOP_REQUESTED if (isAwaitingRecovery) { setIsERActive(isValidERStatus(runStatus, true)) @@ -98,10 +111,14 @@ export function useErrorRecoveryFlows( } }, [runStatus, failedCommand]) - return { - isERActive, - failedCommand, - } + // Gate ER rendering on data derived from key network requests. + return isERActive && failedCommand != null && runLwDefsByUri != null + ? { + isERActive: true, + failedCommand, + runLwDefsByUri, + } + : { isERActive: false, failedCommand, runLwDefsByUri } } export interface ErrorRecoveryFlowsProps { @@ -111,14 +128,20 @@ export interface ErrorRecoveryFlowsProps { * information derived from the failed command from the run record even if there is no matching command in protocol analysis. * Using a failed command that is not matched to a protocol analysis command is unsafe in most circumstances (ie, in * non-generic recovery flows. Prefer using failedCommandBySource in most circumstances. */ - unvalidatedFailedCommand: FailedCommand | null + unvalidatedFailedCommand: UseErrorRecoveryActiveResult['failedCommand'] + runLwDefsByUri: UseErrorRecoveryActiveResult['runLwDefsByUri'] protocolAnalysis: CompletedProtocolAnalysis | null } export function ErrorRecoveryFlows( props: ErrorRecoveryFlowsProps ): JSX.Element | null { - const { protocolAnalysis, runStatus, unvalidatedFailedCommand } = props + const { + protocolAnalysis, + runStatus, + unvalidatedFailedCommand, + runLwDefsByUri, + } = props const failedCommandBySource = useRetainedFailedCommandBySource( unvalidatedFailedCommand, @@ -130,20 +153,7 @@ export function ErrorRecoveryFlows( const robotType = protocolAnalysis?.robotType ?? OT2_ROBOT_TYPE const robotName = useHost()?.robotName ?? 'robot' - const isValidRobotSideAnalysis = protocolAnalysis != null - - // TODO(jh, 10-22-24): EXEC-769. - const labwareDefinitionsByUri = useMemo( - () => - protocolAnalysis != null - ? getLoadedLabwareDefinitionsByUri(protocolAnalysis?.commands) - : null, - [isValidRobotSideAnalysis] - ) - const allRunDefs = - labwareDefinitionsByUri != null - ? Object.values(labwareDefinitionsByUri) - : [] + const allRunDefs = runLwDefsByUri != null ? Object.values(runLwDefsByUri) : [] const { showTakeover, @@ -161,7 +171,7 @@ export function ErrorRecoveryFlows( isActiveUser, failedCommand: failedCommandBySource, allRunDefs, - labwareDefinitionsByUri, + runLwDefsByUri, }) const renderWizard = diff --git a/app/src/organisms/ErrorRecoveryFlows/shared/ErrorDetailsModal.tsx b/app/src/organisms/ErrorRecoveryFlows/shared/ErrorDetailsModal.tsx index 7eb207a9fe7..070e974bed3 100644 --- a/app/src/organisms/ErrorRecoveryFlows/shared/ErrorDetailsModal.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/shared/ErrorDetailsModal.tsx @@ -1,17 +1,17 @@ -import * as React from 'react' +import { useState } from 'react' import { useTranslation } from 'react-i18next' import { createPortal } from 'react-dom' import { css } from 'styled-components' import { - Flex, - StyledText, - SPACING, - COLORS, - ModalShell, - ModalHeader, BORDERS, + COLORS, DIRECTION_COLUMN, + Flex, + ModalHeader, + ModalShell, + SPACING, + StyledText, } from '@opentrons/components' import { useErrorName } from '../hooks' @@ -22,6 +22,7 @@ import { InlineNotification } from '/app/atoms/InlineNotification' import { StepInfo } from './StepInfo' import { getErrorKind } from '../utils' +import type { ReactNode } from 'react' import type { LabwareDefinition2, RobotType } from '@opentrons/shared-data' import type { IconProps } from '@opentrons/components' import type { OddModalHeaderBaseProps } from '/app/molecules/OddModal/types' @@ -33,7 +34,7 @@ export function useErrorDetailsModal(): { showModal: boolean toggleModal: () => void } { - const [showModal, setShowModal] = React.useState(false) + const [showModal, setShowModal] = useState(false) const toggleModal = (): void => { setShowModal(!showModal) @@ -67,6 +68,7 @@ export function ErrorDetailsModal(props: ErrorDetailsModalProps): JSX.Element { case ERROR_KINDS.OVERPRESSURE_WHILE_DISPENSING: case ERROR_KINDS.TIP_NOT_DETECTED: case ERROR_KINDS.GRIPPER_ERROR: + case ERROR_KINDS.STALL_OR_COLLISION: return true default: return false @@ -112,7 +114,7 @@ export function ErrorDetailsModal(props: ErrorDetailsModalProps): JSX.Element { } type ErrorDetailsModalType = ErrorDetailsModalProps & { - children: React.ReactNode + children: ReactNode modalHeader: OddModalHeaderBaseProps toggleModal: () => void desktopType: DesktopSizeType @@ -213,6 +215,8 @@ export function NotificationBanner({ return case ERROR_KINDS.GRIPPER_ERROR: return + case ERROR_KINDS.STALL_OR_COLLISION: + return default: console.error('Handle error kind notification banners explicitly.') return
        @@ -258,6 +262,18 @@ export function GripperErrorBanner(): JSX.Element { ) } +export function StallErrorBanner(): JSX.Element { + const { t } = useTranslation('error_recovery') + + return ( + + ) +} + // TODO(jh, 07-24-24): Using shared height/width constants for intervention modal sizing and the ErrorDetailsModal sizing // would be ideal. const DESKTOP_STEP_INFO_STYLE = css` diff --git a/app/src/organisms/ErrorRecoveryFlows/shared/GripperIsHoldingLabware.tsx b/app/src/organisms/ErrorRecoveryFlows/shared/GripperIsHoldingLabware.tsx index 036f1aff3d0..d198dcc8dac 100644 --- a/app/src/organisms/ErrorRecoveryFlows/shared/GripperIsHoldingLabware.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/shared/GripperIsHoldingLabware.tsx @@ -29,12 +29,15 @@ export const HOLDING_LABWARE_OPTIONS: HoldingLabwareOption[] = [ export function GripperIsHoldingLabware({ routeUpdateActions, currentRecoveryOptionUtils, + recoveryCommands, }: RecoveryContentProps): JSX.Element { const { proceedNextStep, proceedToRouteAndStep, goBackPrevStep, + handleMotionRouting, } = routeUpdateActions + const { homeExceptPlungers } = recoveryCommands const { selectedRecoveryOption } = currentRecoveryOptionUtils const { MANUAL_MOVE_AND_SKIP, @@ -48,24 +51,29 @@ export function GripperIsHoldingLabware({ const { t } = useTranslation(['error_recovery', 'shared']) const handleNoOption = (): void => { - switch (selectedRecoveryOption) { - case MANUAL_MOVE_AND_SKIP.ROUTE: - void proceedToRouteAndStep( - MANUAL_MOVE_AND_SKIP.ROUTE, - MANUAL_MOVE_AND_SKIP.STEPS.MANUAL_MOVE - ) - break - case MANUAL_REPLACE_AND_RETRY.ROUTE: - void proceedToRouteAndStep( - MANUAL_REPLACE_AND_RETRY.ROUTE, - MANUAL_REPLACE_AND_RETRY.STEPS.MANUAL_REPLACE - ) - break - default: { - console.error('Unexpected recovery option for gripper routing.') - void proceedToRouteAndStep(OPTION_SELECTION.ROUTE) - } - } + // The "yes" option also contains a home, but it occurs later in the control flow, + // after the user has extricated the labware from the gripper jaws. + void handleMotionRouting(true) + .then(() => homeExceptPlungers()) + .finally(() => handleMotionRouting(false)) + .then(() => { + switch (selectedRecoveryOption) { + case MANUAL_MOVE_AND_SKIP.ROUTE: + return proceedToRouteAndStep( + MANUAL_MOVE_AND_SKIP.ROUTE, + MANUAL_MOVE_AND_SKIP.STEPS.MANUAL_MOVE + ) + case MANUAL_REPLACE_AND_RETRY.ROUTE: + return proceedToRouteAndStep( + MANUAL_REPLACE_AND_RETRY.ROUTE, + MANUAL_REPLACE_AND_RETRY.STEPS.MANUAL_REPLACE + ) + default: { + console.error('Unexpected recovery option for gripper routing.') + return proceedToRouteAndStep(OPTION_SELECTION.ROUTE) + } + } + }) } const primaryOnClick = (): void => { diff --git a/app/src/organisms/ErrorRecoveryFlows/shared/LeftColumnLabwareInfo.tsx b/app/src/organisms/ErrorRecoveryFlows/shared/LeftColumnLabwareInfo.tsx index 87cdac57255..c6fa3623cf4 100644 --- a/app/src/organisms/ErrorRecoveryFlows/shared/LeftColumnLabwareInfo.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/shared/LeftColumnLabwareInfo.tsx @@ -1,11 +1,11 @@ import { InterventionContent } from '/app/molecules/InterventionModal/InterventionContent' -import type * as React from 'react' +import type { ComponentProps } from 'react' import type { RecoveryContentProps } from '../types' type LeftColumnLabwareInfoProps = RecoveryContentProps & { title: string - type: React.ComponentProps['infoProps']['type'] + type: ComponentProps['infoProps']['type'] /* Renders a warning InlineNotification if provided. */ bannerText?: string } @@ -24,7 +24,7 @@ export function LeftColumnLabwareInfo({ } = failedLabwareUtils const { displayNameNewLoc, displayNameCurrentLoc } = failedLabwareLocations - const buildNewLocation = (): React.ComponentProps< + const buildNewLocation = (): ComponentProps< typeof InterventionContent >['infoProps']['newLocationProps'] => displayNameNewLoc != null diff --git a/app/src/organisms/ErrorRecoveryFlows/shared/RecoveryContentWrapper.tsx b/app/src/organisms/ErrorRecoveryFlows/shared/RecoveryContentWrapper.tsx index 9274079897f..d7c2abd900f 100644 --- a/app/src/organisms/ErrorRecoveryFlows/shared/RecoveryContentWrapper.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/shared/RecoveryContentWrapper.tsx @@ -1,7 +1,6 @@ // TODO: replace this by making these props true of interventionmodal content wrappers // once error recovery uses interventionmodal consistently -import type * as React from 'react' import { css } from 'styled-components' import { DIRECTION_COLUMN, @@ -18,19 +17,21 @@ import { } from '/app/molecules/InterventionModal' import { RecoveryFooterButtons } from './RecoveryFooterButtons' +import type { ComponentProps, ReactNode } from 'react' + interface SingleColumnContentWrapperProps { - children: React.ReactNode - footerDetails?: React.ComponentProps + children: ReactNode + footerDetails?: ComponentProps } interface TwoColumnContentWrapperProps { - children: [React.ReactNode, React.ReactNode] - footerDetails?: React.ComponentProps + children: [ReactNode, ReactNode] + footerDetails?: ComponentProps } interface OneOrTwoColumnContentWrapperProps { - children: [React.ReactNode, React.ReactNode] - footerDetails?: React.ComponentProps + children: [ReactNode, ReactNode] + footerDetails?: ComponentProps } // For flex-direction: column recovery content with one column only. export function RecoverySingleColumnContentWrapper({ diff --git a/app/src/organisms/ErrorRecoveryFlows/shared/RecoveryDoorOpenSpecial.tsx b/app/src/organisms/ErrorRecoveryFlows/shared/RecoveryDoorOpenSpecial.tsx index 4331a976d5e..00b64839b90 100644 --- a/app/src/organisms/ErrorRecoveryFlows/shared/RecoveryDoorOpenSpecial.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/shared/RecoveryDoorOpenSpecial.tsx @@ -1,4 +1,4 @@ -import { useState } from 'react' +import { useEffect, useState } from 'react' import { css } from 'styled-components' import { useTranslation } from 'react-i18next' @@ -20,7 +20,11 @@ import { RecoverySingleColumnContentWrapper } from './RecoveryContentWrapper' import { RecoveryFooterButtons } from './RecoveryFooterButtons' import { RECOVERY_MAP } from '../constants' -import type { RecoveryContentProps } from '../../ErrorRecoveryFlows/types' +import type { + RecoveryContentProps, + RecoveryRoute, + RouteStep, +} from '../../ErrorRecoveryFlows/types' // Whenever a step uses a custom "close the robot door" view, use this component. // Note that the allowDoorOpen metadata for the route must be set to true for this view to render. @@ -30,9 +34,11 @@ export function RecoveryDoorOpenSpecial({ recoveryActionMutationUtils, routeUpdateActions, doorStatusUtils, + recoveryCommands, }: RecoveryContentProps): JSX.Element { const { selectedRecoveryOption } = currentRecoveryOptionUtils const { resumeRecovery } = recoveryActionMutationUtils + const { proceedToRouteAndStep, handleMotionRouting } = routeUpdateActions const { t } = useTranslation('error_recovery') const [isLoading, setIsLoading] = useState(false) @@ -46,6 +52,7 @@ export function RecoveryDoorOpenSpecial({ switch (selectedRecoveryOption) { case RECOVERY_MAP.MANUAL_REPLACE_AND_RETRY.ROUTE: case RECOVERY_MAP.MANUAL_MOVE_AND_SKIP.ROUTE: + case RECOVERY_MAP.HOME_AND_RETRY.ROUTE: return t('door_open_robot_home') default: { console.error( @@ -56,29 +63,56 @@ export function RecoveryDoorOpenSpecial({ } } - if (!doorStatusUtils.isDoorOpen) { - const { proceedToRouteAndStep } = routeUpdateActions - switch (selectedRecoveryOption) { - case RECOVERY_MAP.MANUAL_REPLACE_AND_RETRY.ROUTE: - void proceedToRouteAndStep( - RECOVERY_MAP.MANUAL_REPLACE_AND_RETRY.ROUTE, - RECOVERY_MAP.MANUAL_REPLACE_AND_RETRY.STEPS.MANUAL_REPLACE - ) - break - case RECOVERY_MAP.MANUAL_MOVE_AND_SKIP.ROUTE: - void proceedToRouteAndStep( - RECOVERY_MAP.MANUAL_MOVE_AND_SKIP.ROUTE, - RECOVERY_MAP.MANUAL_MOVE_AND_SKIP.STEPS.MANUAL_MOVE - ) - break - default: { - console.error( - `Unhandled special-cased door open on route ${selectedRecoveryOption}.` - ) - void proceedToRouteAndStep(RECOVERY_MAP.OPTION_SELECTION.ROUTE) + const handleHomeAllAndRoute = ( + route: RecoveryRoute, + step?: RouteStep + ): void => { + void handleMotionRouting(true, RECOVERY_MAP.ROBOT_IN_MOTION.ROUTE) + .then(() => recoveryCommands.homeAll()) + .finally(() => handleMotionRouting(false)) + .then(() => proceedToRouteAndStep(route, step)) + } + + const handleHomeExceptPlungersAndRoute = ( + route: RecoveryRoute, + step?: RouteStep + ): void => { + void handleMotionRouting(true, RECOVERY_MAP.ROBOT_IN_MOTION.ROUTE) + .then(() => recoveryCommands.homeExceptPlungers()) + .finally(() => handleMotionRouting(false)) + .then(() => proceedToRouteAndStep(route, step)) + } + + useEffect(() => { + if (!doorStatusUtils.isDoorOpen) { + switch (selectedRecoveryOption) { + case RECOVERY_MAP.MANUAL_REPLACE_AND_RETRY.ROUTE: + handleHomeExceptPlungersAndRoute( + RECOVERY_MAP.MANUAL_REPLACE_AND_RETRY.ROUTE, + RECOVERY_MAP.MANUAL_REPLACE_AND_RETRY.STEPS.MANUAL_REPLACE + ) + break + case RECOVERY_MAP.MANUAL_MOVE_AND_SKIP.ROUTE: + handleHomeExceptPlungersAndRoute( + RECOVERY_MAP.MANUAL_MOVE_AND_SKIP.ROUTE, + RECOVERY_MAP.MANUAL_MOVE_AND_SKIP.STEPS.MANUAL_MOVE + ) + break + case RECOVERY_MAP.HOME_AND_RETRY.ROUTE: + handleHomeAllAndRoute( + RECOVERY_MAP.HOME_AND_RETRY.ROUTE, + RECOVERY_MAP.HOME_AND_RETRY.STEPS.CONFIRM_RETRY + ) + break + default: { + console.error( + `Unhandled special-cased door open on route ${selectedRecoveryOption}.` + ) + void proceedToRouteAndStep(RECOVERY_MAP.OPTION_SELECTION.ROUTE) + } } } - } + }, [doorStatusUtils.isDoorOpen]) return ( diff --git a/app/src/organisms/ErrorRecoveryFlows/shared/RecoveryInterventionModal.tsx b/app/src/organisms/ErrorRecoveryFlows/shared/RecoveryInterventionModal.tsx index 82974023805..cba8719e376 100644 --- a/app/src/organisms/ErrorRecoveryFlows/shared/RecoveryInterventionModal.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/shared/RecoveryInterventionModal.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { createPortal } from 'react-dom' import { css } from 'styled-components' @@ -13,11 +12,12 @@ import { import { InterventionModal } from '/app/molecules/InterventionModal' import { getModalPortalEl, getTopPortalEl } from '/app/App/portal' +import type { ComponentProps } from 'react' import type { ModalType } from '/app/molecules/InterventionModal' import type { DesktopSizeType } from '../types' export type RecoveryInterventionModalProps = Omit< - React.ComponentProps, + ComponentProps, 'type' > & { /* If on desktop, specifies the hard-coded dimensions height of the modal. */ @@ -38,7 +38,7 @@ export function RecoveryInterventionModal({ } return createPortal( - + void } +): JSX.Element { const { routeUpdateActions, recoveryCommands, errorKind } = props const { ROBOT_RETRYING_STEP } = RECOVERY_MAP const { t } = useTranslation('error_recovery') diff --git a/app/src/organisms/ErrorRecoveryFlows/shared/StepInfo.tsx b/app/src/organisms/ErrorRecoveryFlows/shared/StepInfo.tsx index bbc12ce0429..ba68ee88507 100644 --- a/app/src/organisms/ErrorRecoveryFlows/shared/StepInfo.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/shared/StepInfo.tsx @@ -1,11 +1,10 @@ -import type * as React from 'react' - import { useTranslation } from 'react-i18next' import { Flex, DISPLAY_INLINE, StyledText } from '@opentrons/components' import { CommandText } from '/app/molecules/Command' +import type { ComponentProps } from 'react' import type { StyleProps } from '@opentrons/components' import type { RecoveryContentProps } from '../types' @@ -15,8 +14,8 @@ interface StepInfoProps extends StyleProps { robotType: RecoveryContentProps['robotType'] protocolAnalysis: RecoveryContentProps['protocolAnalysis'] allRunDefs: RecoveryContentProps['allRunDefs'] - desktopStyle?: React.ComponentProps['desktopStyle'] - oddStyle?: React.ComponentProps['oddStyle'] + desktopStyle?: ComponentProps['desktopStyle'] + oddStyle?: ComponentProps['oddStyle'] } export function StepInfo({ diff --git a/app/src/organisms/ErrorRecoveryFlows/shared/TipSelection.tsx b/app/src/organisms/ErrorRecoveryFlows/shared/TipSelection.tsx index f22c7fb268b..ba2b7ec35a5 100644 --- a/app/src/organisms/ErrorRecoveryFlows/shared/TipSelection.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/shared/TipSelection.tsx @@ -37,6 +37,7 @@ export function TipSelection(props: TipSelectionProps): JSX.Element { relevantActiveNozzleLayout )} allowSelect={allowTipSelection} + allowMultiDrag={false} /> ) } diff --git a/app/src/organisms/ErrorRecoveryFlows/shared/TwoColLwInfoAndDeck.tsx b/app/src/organisms/ErrorRecoveryFlows/shared/TwoColLwInfoAndDeck.tsx index 00fa95072c1..ad7ac489d7c 100644 --- a/app/src/organisms/ErrorRecoveryFlows/shared/TwoColLwInfoAndDeck.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/shared/TwoColLwInfoAndDeck.tsx @@ -1,9 +1,9 @@ import { - Flex, - MoveLabwareOnDeck, COLORS, - Module, + Flex, LabwareRender, + Module, + MoveLabwareOnDeck, } from '@opentrons/components' import { inferModuleOrientationFromXCoordinate } from '@opentrons/shared-data' @@ -14,7 +14,7 @@ import { RecoveryFooterButtons } from './RecoveryFooterButtons' import { LeftColumnLabwareInfo } from './LeftColumnLabwareInfo' import { RECOVERY_MAP } from '../constants' -import type * as React from 'react' +import type { ComponentProps } from 'react' import type { RecoveryContentProps } from '../types' import type { InterventionContent } from '/app/molecules/InterventionModal/InterventionContent' @@ -34,6 +34,7 @@ export function TwoColLwInfoAndDeck( SKIP_STEP_WITH_NEW_TIPS, MANUAL_MOVE_AND_SKIP, MANUAL_REPLACE_AND_RETRY, + HOME_AND_RETRY, } = RECOVERY_MAP const { selectedRecoveryOption } = currentRecoveryOptionUtils const { relevantWellName, failedLabware } = failedLabwareUtils @@ -55,6 +56,7 @@ export function TwoColLwInfoAndDeck( return t('manually_move_lw_on_deck') case MANUAL_REPLACE_AND_RETRY.ROUTE: return t('manually_replace_lw_on_deck') + case HOME_AND_RETRY.ROUTE: case RETRY_NEW_TIPS.ROUTE: case SKIP_STEP_WITH_NEW_TIPS.ROUTE: { // Only special case the "full" 96-channel nozzle config. @@ -72,7 +74,7 @@ export function TwoColLwInfoAndDeck( } default: console.error( - 'Unexpected recovery option. Handle retry step copy explicitly.' + `TwoColLwInfoAndDeck: Unexpected recovery option: ${selectedRecoveryOption}. Handle retry step copy explicitly.` ) return 'UNEXPECTED RECOVERY OPTION' } @@ -84,20 +86,21 @@ export function TwoColLwInfoAndDeck( case MANUAL_REPLACE_AND_RETRY.ROUTE: return t('ensure_lw_is_accurately_placed') case RETRY_NEW_TIPS.ROUTE: - case SKIP_STEP_WITH_NEW_TIPS.ROUTE: { + case SKIP_STEP_WITH_NEW_TIPS.ROUTE: + case HOME_AND_RETRY.ROUTE: { return isPartialTipConfigValid ? t('replace_tips_and_select_loc_partial_tip') : t('replace_tips_and_select_location') } default: console.error( - 'Unexpected recovery option. Handle retry step copy explicitly.' + `TwoColLwInfoAndDeck:buildBannerText: Unexpected recovery option ${selectedRecoveryOption}. Handle retry step copy explicitly.` ) return 'UNEXPECTED RECOVERY OPTION' } } - const buildType = (): React.ComponentProps< + const buildType = (): ComponentProps< typeof InterventionContent >['infoProps']['type'] => { switch (selectedRecoveryOption) { diff --git a/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/ErrorDetailsModal.test.tsx b/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/ErrorDetailsModal.test.tsx index d759aaf3d78..da76c3e09d9 100644 --- a/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/ErrorDetailsModal.test.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/ErrorDetailsModal.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { describe, it, beforeEach, expect, vi } from 'vitest' import { screen, act, renderHook } from '@testing-library/react' @@ -14,8 +13,11 @@ import { OverpressureBanner, TipNotDetectedBanner, GripperErrorBanner, + StallErrorBanner, } from '../ErrorDetailsModal' +import type { ComponentProps } from 'react' + vi.mock('react-dom', () => ({ ...vi.importActual('react-dom'), createPortal: vi.fn((element, container) => element), @@ -46,14 +48,14 @@ describe('useErrorDetailsModal', () => { }) }) -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, })[0] } describe('ErrorDetailsModal', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { @@ -201,3 +203,25 @@ describe('GripperErrorBanner', () => { ) }) }) + +describe('StallErrorBanner', () => { + beforeEach(() => { + vi.mocked(InlineNotification).mockReturnValue( +
        MOCK_INLINE_NOTIFICATION
        + ) + }) + it('renders the InlineNotification', () => { + renderWithProviders(, { + i18nInstance: i18n, + }) + expect(vi.mocked(InlineNotification)).toHaveBeenCalledWith( + expect.objectContaining({ + type: 'alert', + heading: + "A stall or collision is detected when the robot's motors are blocked", + message: 'The robot must return to its home position before proceeding', + }), + {} + ) + }) +}) diff --git a/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/GripperIsHoldingLabware.test.tsx b/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/GripperIsHoldingLabware.test.tsx index 95af112fa58..875c79fe09c 100644 --- a/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/GripperIsHoldingLabware.test.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/GripperIsHoldingLabware.test.tsx @@ -10,13 +10,12 @@ import { GripperIsHoldingLabware, HOLDING_LABWARE_OPTIONS, } from '../GripperIsHoldingLabware' +import { RECOVERY_MAP } from '/app/organisms/ErrorRecoveryFlows/constants' +import type { ComponentProps } from 'react' import type { Mock } from 'vitest' -import { RECOVERY_MAP } from '/app/organisms/ErrorRecoveryFlows/constants' -const render = ( - props: React.ComponentProps -) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, })[0] @@ -24,19 +23,25 @@ const render = ( let mockProceedToRouteAndStep: Mock let mockProceedNextStep: Mock +let mockHandleMotionRouting: Mock +let mockHomeExceptPlungers: Mock describe('GripperIsHoldingLabware', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { mockProceedToRouteAndStep = vi.fn(() => Promise.resolve()) mockProceedNextStep = vi.fn(() => Promise.resolve()) + mockHandleMotionRouting = vi.fn(() => Promise.resolve()) + mockHomeExceptPlungers = vi.fn(() => Promise.resolve()) props = { ...mockRecoveryContentProps, routeUpdateActions: { proceedToRouteAndStep: mockProceedToRouteAndStep, proceedNextStep: mockProceedNextStep, + handleMotionRouting: mockHandleMotionRouting, } as any, + recoveryCommands: { homeExceptPlungers: mockHomeExceptPlungers } as any, } }) @@ -83,6 +88,18 @@ describe('GripperIsHoldingLabware', () => { fireEvent.click(screen.getAllByLabelText('No')[0]) clickButtonLabeled('Continue') + await waitFor(() => { + expect(mockHandleMotionRouting).toHaveBeenCalledWith(true) + }) + + await waitFor(() => { + expect(mockHomeExceptPlungers).toHaveBeenCalled() + }) + + await waitFor(() => { + expect(mockHandleMotionRouting).toHaveBeenCalledWith(false) + }) + await waitFor(() => { expect(mockProceedToRouteAndStep).toHaveBeenCalledWith( RECOVERY_MAP.MANUAL_MOVE_AND_SKIP.ROUTE, diff --git a/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/GripperReleaseLabware.test.tsx b/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/GripperReleaseLabware.test.tsx index 3bdd9f97819..91fdc3d6a9e 100644 --- a/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/GripperReleaseLabware.test.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/GripperReleaseLabware.test.tsx @@ -7,20 +7,21 @@ import { i18n } from '/app/i18n' import { mockRecoveryContentProps } from '/app/organisms/ErrorRecoveryFlows/__fixtures__' import { clickButtonLabeled } from '/app/organisms/ErrorRecoveryFlows/__tests__/util' +import type { ComponentProps } from 'react' import type { Mock } from 'vitest' vi.mock('/app/assets/videos/error-recovery/Gripper_Release.webm', () => ({ default: 'mocked-animation-path.webm', })) -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, })[0] } describe('GripperReleaseLabware', () => { - let props: React.ComponentProps + let props: ComponentProps let mockHandleMotionRouting: Mock beforeEach(() => { diff --git a/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/LeftColumnLabwareInfo.test.tsx b/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/LeftColumnLabwareInfo.test.tsx index f38e1e06922..c8ab8bc4ad6 100644 --- a/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/LeftColumnLabwareInfo.test.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/LeftColumnLabwareInfo.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { describe, it, beforeEach, expect, vi } from 'vitest' import { screen } from '@testing-library/react' @@ -8,16 +7,18 @@ import { i18n } from '/app/i18n' import { LeftColumnLabwareInfo } from '../LeftColumnLabwareInfo' import { InterventionContent } from '/app/molecules/InterventionModal/InterventionContent' +import type { ComponentProps } from 'react' + vi.mock('/app/molecules/InterventionModal/InterventionContent') -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, })[0] } describe('LeftColumnLabwareInfo', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { diff --git a/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/RecoveryDoorOpenSpecial.test.tsx b/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/RecoveryDoorOpenSpecial.test.tsx index 76e42a04c6d..b54ccc649d7 100644 --- a/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/RecoveryDoorOpenSpecial.test.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/RecoveryDoorOpenSpecial.test.tsx @@ -1,5 +1,5 @@ import { describe, it, vi, expect, beforeEach } from 'vitest' -import { screen } from '@testing-library/react' +import { screen, waitFor } from '@testing-library/react' import { RUN_STATUS_AWAITING_RECOVERY_BLOCKED_BY_OPEN_DOOR, @@ -11,11 +11,11 @@ import { i18n } from '/app/i18n' import { RecoveryDoorOpenSpecial } from '../RecoveryDoorOpenSpecial' import { RECOVERY_MAP } from '../../constants' -import type * as React from 'react' +import type { ComponentProps } from 'react' import { clickButtonLabeled } from '/app/organisms/ErrorRecoveryFlows/__tests__/util' describe('RecoveryDoorOpenSpecial', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { @@ -28,16 +28,18 @@ describe('RecoveryDoorOpenSpecial', () => { }, routeUpdateActions: { proceedToRouteAndStep: vi.fn(), + handleMotionRouting: vi.fn().mockImplementation(_ => Promise.resolve()), }, doorStatusUtils: { isDoorOpen: true, }, + recoveryCommands: { + homeExceptPlungers: vi.fn().mockResolvedValue(undefined), + }, } as any }) - const render = ( - props: React.ComponentProps - ) => { + const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, })[0] @@ -70,7 +72,50 @@ describe('RecoveryDoorOpenSpecial', () => { ) }) - it('renders default subtext for unhandled recovery option', () => { + it.each([ + { + recoveryOption: RECOVERY_MAP.MANUAL_REPLACE_AND_RETRY.ROUTE, + expectedRoute: RECOVERY_MAP.MANUAL_REPLACE_AND_RETRY.ROUTE, + expectedStep: RECOVERY_MAP.MANUAL_REPLACE_AND_RETRY.STEPS.MANUAL_REPLACE, + }, + { + recoveryOption: RECOVERY_MAP.MANUAL_MOVE_AND_SKIP.ROUTE, + expectedRoute: RECOVERY_MAP.MANUAL_MOVE_AND_SKIP.ROUTE, + expectedStep: RECOVERY_MAP.MANUAL_MOVE_AND_SKIP.STEPS.MANUAL_MOVE, + }, + ])( + 'executes correct chain of actions when door is closed for $recoveryOption', + async ({ recoveryOption, expectedRoute, expectedStep }) => { + props.currentRecoveryOptionUtils.selectedRecoveryOption = recoveryOption + props.doorStatusUtils.isDoorOpen = false + + render(props) + + await waitFor(() => { + expect( + props.routeUpdateActions.handleMotionRouting + ).toHaveBeenCalledWith(true, RECOVERY_MAP.ROBOT_IN_MOTION.ROUTE) + }) + + await waitFor(() => { + expect(props.recoveryCommands.homeExceptPlungers).toHaveBeenCalled() + }) + + await waitFor(() => { + expect( + props.routeUpdateActions.handleMotionRouting + ).toHaveBeenCalledWith(false) + }) + + await waitFor(() => { + expect( + props.routeUpdateActions.proceedToRouteAndStep + ).toHaveBeenCalledWith(expectedRoute, expectedStep) + }) + } + ) + + it('renders default subtext for an unhandled recovery option', () => { props.currentRecoveryOptionUtils.selectedRecoveryOption = 'UNHANDLED_OPTION' as any render(props) screen.getByText('Close the robot door') @@ -79,26 +124,6 @@ describe('RecoveryDoorOpenSpecial', () => { ) }) - it(`calls proceedToRouteAndStep when door is closed for ${RECOVERY_MAP.MANUAL_REPLACE_AND_RETRY.ROUTE}`, () => { - props.doorStatusUtils.isDoorOpen = false - render(props) - expect(props.routeUpdateActions.proceedToRouteAndStep).toHaveBeenCalledWith( - RECOVERY_MAP.MANUAL_REPLACE_AND_RETRY.ROUTE, - RECOVERY_MAP.MANUAL_REPLACE_AND_RETRY.STEPS.MANUAL_REPLACE - ) - }) - - it(`calls proceedToRouteAndStep when door is closed for ${RECOVERY_MAP.MANUAL_MOVE_AND_SKIP.ROUTE}`, () => { - props.currentRecoveryOptionUtils.selectedRecoveryOption = - RECOVERY_MAP.MANUAL_MOVE_AND_SKIP.ROUTE - props.doorStatusUtils.isDoorOpen = false - render(props) - expect(props.routeUpdateActions.proceedToRouteAndStep).toHaveBeenCalledWith( - RECOVERY_MAP.MANUAL_MOVE_AND_SKIP.ROUTE, - RECOVERY_MAP.MANUAL_MOVE_AND_SKIP.STEPS.MANUAL_MOVE - ) - }) - it('calls proceedToRouteAndStep with OPTION_SELECTION for unhandled recovery option when door is closed', () => { props.currentRecoveryOptionUtils.selectedRecoveryOption = 'UNHANDLED_OPTION' as any props.doorStatusUtils.isDoorOpen = false diff --git a/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/RecoveryFooterButtons.test.tsx b/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/RecoveryFooterButtons.test.tsx index 6381d2b579a..6643dcf8021 100644 --- a/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/RecoveryFooterButtons.test.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/RecoveryFooterButtons.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { vi, describe, it, expect, beforeEach } from 'vitest' import { screen, fireEvent } from '@testing-library/react' @@ -8,16 +7,17 @@ import { renderWithProviders } from '/app/__testing-utils__' import { i18n } from '/app/i18n' import { RecoveryFooterButtons } from '../RecoveryFooterButtons' +import type { ComponentProps } from 'react' import type { Mock } from 'vitest' -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, })[0] } describe('RecoveryFooterButtons', () => { - let props: React.ComponentProps + let props: ComponentProps let mockPrimaryBtnOnClick: Mock let mockSecondaryBtnOnClick: Mock let mockTertiaryBtnOnClick: Mock diff --git a/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/RetryStepInfo.test.tsx b/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/RetryStepInfo.test.tsx index 94f77910657..90ae937dd24 100644 --- a/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/RetryStepInfo.test.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/RetryStepInfo.test.tsx @@ -7,10 +7,11 @@ import { RetryStepInfo } from '../RetryStepInfo' import { ERROR_KINDS, RECOVERY_MAP } from '../../constants' import { clickButtonLabeled } from '/app/organisms/ErrorRecoveryFlows/__tests__/util' +import type { ComponentProps } from 'react' import type { Mock } from 'vitest' describe('RetryStepInfo', () => { - let props: React.ComponentProps + let props: ComponentProps let mockHandleMotionRouting: Mock let mockRetryFailedCommand: Mock let mockResumeRun: Mock @@ -33,7 +34,7 @@ describe('RetryStepInfo', () => { } as any }) - const render = (props: React.ComponentProps) => { + const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, })[0] @@ -107,8 +108,8 @@ describe('RetryStepInfo', () => { render(props) screen.getByText( - 'First, take any necessary actions to prepare the robot to retry the failed step.' + 'Take any necessary additional actions to prepare the robot to retry the failed step.' ) - screen.getByText('Then, close the robot door before proceeding.') + screen.getByText('Close the robot door before proceeding.') }) }) diff --git a/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/SelectTips.test.tsx b/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/SelectTips.test.tsx index 08db6269c4d..425bf68b264 100644 --- a/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/SelectTips.test.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/SelectTips.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { describe, it, vi, expect, beforeEach } from 'vitest' import { screen, fireEvent, waitFor } from '@testing-library/react' @@ -9,19 +8,20 @@ import { SelectTips } from '../SelectTips' import { RECOVERY_MAP } from '../../constants' import { TipSelectionModal } from '../TipSelectionModal' +import type { ComponentProps } from 'react' import type { Mock } from 'vitest' vi.mock('../TipSelectionModal') vi.mock('../TipSelection') -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, })[0] } describe('SelectTips', () => { - let props: React.ComponentProps + let props: ComponentProps let mockGoBackPrevStep: Mock let mockhandleMotionRouting: Mock let mockProceedNextStep: Mock diff --git a/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/SkipStepInfo.test.tsx b/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/SkipStepInfo.test.tsx index 28ef4177648..0bd7b1a5a72 100644 --- a/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/SkipStepInfo.test.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/SkipStepInfo.test.tsx @@ -7,10 +7,11 @@ import { SkipStepInfo } from '../SkipStepInfo' import { RECOVERY_MAP } from '../../constants' import { clickButtonLabeled } from '/app/organisms/ErrorRecoveryFlows/__tests__/util' +import type { ComponentProps } from 'react' import type { Mock } from 'vitest' describe('SkipStepInfo', () => { - let props: React.ComponentProps + let props: ComponentProps let mockHandleMotionRouting: Mock let mockSkipFailedCommand: Mock @@ -32,7 +33,7 @@ describe('SkipStepInfo', () => { } as any }) - const render = (props: React.ComponentProps) => { + const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, })[0] diff --git a/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/StepInfo.test.tsx b/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/StepInfo.test.tsx index d6fbb50c345..aefd143aee1 100644 --- a/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/StepInfo.test.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/StepInfo.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { describe, it, expect, beforeEach, vi } from 'vitest' import { screen } from '@testing-library/react' @@ -8,16 +7,18 @@ import { i18n } from '/app/i18n' import { StepInfo } from '../StepInfo' import { CommandText } from '/app/molecules/Command' +import type { ComponentProps } from 'react' + vi.mock('/app/molecules/Command') -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, })[0] } describe('StepInfo', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { diff --git a/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/TipSelection.test.tsx b/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/TipSelection.test.tsx index 9df7f8e02ec..36f81c3ca35 100644 --- a/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/TipSelection.test.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/TipSelection.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { describe, it, vi, expect, beforeEach } from 'vitest' import { screen } from '@testing-library/react' @@ -8,16 +7,18 @@ import { i18n } from '/app/i18n' import { TipSelection } from '../TipSelection' import { WellSelection } from '../../../WellSelection' +import type { ComponentProps } from 'react' + vi.mock('../../../WellSelection') -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, })[0] } describe('TipSelection', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { ...mockRecoveryContentProps, @@ -41,6 +42,7 @@ describe('TipSelection', () => { channels: props.failedPipetteUtils.failedPipetteInfo?.data.channels ?? 1, allowSelect: props.allowTipSelection, + allowMultiDrag: false, pipetteNozzleDetails: undefined, }), {} diff --git a/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/TipSelectionModal.test.tsx b/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/TipSelectionModal.test.tsx index fed5a44d4ce..32711ec9762 100644 --- a/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/TipSelectionModal.test.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/TipSelectionModal.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { describe, it, vi, beforeEach, expect } from 'vitest' import { screen } from '@testing-library/react' @@ -8,16 +7,18 @@ import { i18n } from '/app/i18n' import { TipSelectionModal } from '../TipSelectionModal' import { TipSelection } from '../TipSelection' +import type { ComponentProps } from 'react' + vi.mock('../TipSelection') -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, })[0] } describe('TipSelectionModal', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { diff --git a/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/TwoColLwInfoAndDeck.test.tsx b/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/TwoColLwInfoAndDeck.test.tsx index 0629038f800..e151cb11d7a 100644 --- a/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/TwoColLwInfoAndDeck.test.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/TwoColLwInfoAndDeck.test.tsx @@ -11,7 +11,7 @@ import { RECOVERY_MAP } from '../../constants' import { LeftColumnLabwareInfo } from '../LeftColumnLabwareInfo' import { getSlotNameAndLwLocFrom } from '../../hooks/useDeckMapUtils' -import type * as React from 'react' +import type { ComponentProps } from 'react' import type { Mock } from 'vitest' vi.mock('@opentrons/components', async () => { @@ -26,14 +26,14 @@ vi.mock('../../hooks/useDeckMapUtils') let mockProceedNextStep: Mock -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, })[0] } describe('TwoColLwInfoAndDeck', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { mockProceedNextStep = vi.fn() diff --git a/app/src/organisms/ErrorRecoveryFlows/utils/__tests__/getErrorKind.test.ts b/app/src/organisms/ErrorRecoveryFlows/utils/__tests__/getErrorKind.test.ts index e9b5722ffa8..fb3637c0eb5 100644 --- a/app/src/organisms/ErrorRecoveryFlows/utils/__tests__/getErrorKind.test.ts +++ b/app/src/organisms/ErrorRecoveryFlows/utils/__tests__/getErrorKind.test.ts @@ -68,6 +68,21 @@ describe('getErrorKind', () => { errorType: 'someHithertoUnknownDefinedErrorType', expectedError: ERROR_KINDS.GENERAL_ERROR, }, + ...([ + 'aspirate', + 'dispense', + 'blowOut', + 'moveToWell', + 'moveToAddressableArea', + 'dropTip', + 'pickUpTip', + 'prepareToAspirate', + ] as const).map(cmd => ({ + commandType: cmd, + errorType: DEFINED_ERROR_TYPES.STALL_OR_COLLISION, + expectedError: ERROR_KINDS.STALL_OR_COLLISION, + isDefined: true, + })), ])( 'returns $expectedError for $commandType with errorType $errorType', ({ commandType, errorType, expectedError, isDefined = true }) => { diff --git a/app/src/organisms/ErrorRecoveryFlows/utils/getErrorKind.ts b/app/src/organisms/ErrorRecoveryFlows/utils/getErrorKind.ts index 1dc5e023a6c..73fe862eb3b 100644 --- a/app/src/organisms/ErrorRecoveryFlows/utils/getErrorKind.ts +++ b/app/src/organisms/ErrorRecoveryFlows/utils/getErrorKind.ts @@ -54,6 +54,8 @@ export function getErrorKind( errorType === DEFINED_ERROR_TYPES.GRIPPER_MOVEMENT ) { return ERROR_KINDS.GRIPPER_ERROR + } else if (errorType === DEFINED_ERROR_TYPES.STALL_OR_COLLISION) { + return ERROR_KINDS.STALL_OR_COLLISION } } diff --git a/app/src/organisms/FirmwareUpdateModal/__tests__/FirmwareUpdateModal.test.tsx b/app/src/organisms/FirmwareUpdateModal/__tests__/FirmwareUpdateModal.test.tsx index bb205f3f852..16aa35ec87d 100644 --- a/app/src/organisms/FirmwareUpdateModal/__tests__/FirmwareUpdateModal.test.tsx +++ b/app/src/organisms/FirmwareUpdateModal/__tests__/FirmwareUpdateModal.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { act, screen, waitFor } from '@testing-library/react' import { describe, it, vi, beforeEach, expect } from 'vitest' import '@testing-library/jest-dom/vitest' @@ -10,6 +9,8 @@ import { } from '@opentrons/react-api-client' import { i18n } from '/app/i18n' import { FirmwareUpdateModal } from '..' + +import type { ComponentProps } from 'react' import type { BadPipette, PipetteData, @@ -18,14 +19,14 @@ import type { vi.mock('@opentrons/react-api-client') -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, })[0] } describe('FirmwareUpdateModal', () => { - let props: React.ComponentProps + let props: ComponentProps const refetch = vi.fn(() => Promise.resolve()) const updateSubsystem = vi.fn(() => Promise.resolve()) beforeEach(() => { diff --git a/app/src/organisms/FirmwareUpdateModal/__tests__/UpdateInProgressModal.test.tsx b/app/src/organisms/FirmwareUpdateModal/__tests__/UpdateInProgressModal.test.tsx index f2ce6047481..2e237468b3d 100644 --- a/app/src/organisms/FirmwareUpdateModal/__tests__/UpdateInProgressModal.test.tsx +++ b/app/src/organisms/FirmwareUpdateModal/__tests__/UpdateInProgressModal.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { describe, it, beforeEach } from 'vitest' import '@testing-library/jest-dom/vitest' import { screen } from '@testing-library/react' @@ -6,14 +5,16 @@ import { renderWithProviders } from '/app/__testing-utils__' import { i18n } from '/app/i18n' import { UpdateInProgressModal } from '../UpdateInProgressModal' -const render = (props: React.ComponentProps) => { +import type { ComponentProps } from 'react' + +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, })[0] } describe('UpdateInProgressModal', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { subsystem: 'pipette_right', diff --git a/app/src/organisms/FirmwareUpdateModal/__tests__/UpdateNeededModal.test.tsx b/app/src/organisms/FirmwareUpdateModal/__tests__/UpdateNeededModal.test.tsx index 53c91223b47..eb8e1cdcc5b 100644 --- a/app/src/organisms/FirmwareUpdateModal/__tests__/UpdateNeededModal.test.tsx +++ b/app/src/organisms/FirmwareUpdateModal/__tests__/UpdateNeededModal.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { describe, it, vi, beforeEach } from 'vitest' import '@testing-library/jest-dom/vitest' import { renderWithProviders } from '/app/__testing-utils__' @@ -12,6 +11,7 @@ import { UpdateNeededModal } from '../UpdateNeededModal' import { UpdateInProgressModal } from '../UpdateInProgressModal' import { UpdateResultsModal } from '../UpdateResultsModal' +import type { ComponentProps } from 'react' import type { BadPipette, SubsystemUpdateProgressData, @@ -21,14 +21,14 @@ vi.mock('@opentrons/react-api-client') vi.mock('../UpdateInProgressModal') vi.mock('../UpdateResultsModal') -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, })[0] } describe('UpdateNeededModal', () => { - let props: React.ComponentProps + let props: ComponentProps const refetch = vi.fn(() => Promise.resolve()) const updateSubsystem = vi.fn(() => Promise.resolve({ diff --git a/app/src/organisms/FirmwareUpdateModal/__tests__/UpdateResultsModal.test.tsx b/app/src/organisms/FirmwareUpdateModal/__tests__/UpdateResultsModal.test.tsx index 29c5233db45..5c4f3f02473 100644 --- a/app/src/organisms/FirmwareUpdateModal/__tests__/UpdateResultsModal.test.tsx +++ b/app/src/organisms/FirmwareUpdateModal/__tests__/UpdateResultsModal.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { fireEvent, screen } from '@testing-library/react' import { describe, it, vi, beforeEach, expect } from 'vitest' import '@testing-library/jest-dom/vitest' @@ -6,14 +5,16 @@ import { renderWithProviders } from '/app/__testing-utils__' import { i18n } from '/app/i18n' import { UpdateResultsModal } from '../UpdateResultsModal' -const render = (props: React.ComponentProps) => { +import type { ComponentProps } from 'react' + +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, })[0] } describe('UpdateResultsModal', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { isSuccess: true, diff --git a/app/src/organisms/GripperWizardFlows/BeforeBeginning.tsx b/app/src/organisms/GripperWizardFlows/BeforeBeginning.tsx index 89395aeee10..316a3a5526a 100644 --- a/app/src/organisms/GripperWizardFlows/BeforeBeginning.tsx +++ b/app/src/organisms/GripperWizardFlows/BeforeBeginning.tsx @@ -13,6 +13,8 @@ import { SCREWDRIVER_LOADNAME, GRIPPER_LOADNAME, CAL_PIN_LOADNAME, + CALIBRATION_PIN_DISPLAY_NAME, + HEX_SCREWDRIVER_DISPLAY_NAME, } from './constants' import type { UseMutateFunction } from 'react-query' @@ -105,9 +107,9 @@ export const BeforeBeginning = ( const equipmentInfoByLoadName: { [loadName: string]: { displayName: string; subtitle?: string } } = { - calibration_pin: { displayName: t('calibration_pin') }, + calibration_pin: { displayName: CALIBRATION_PIN_DISPLAY_NAME }, hex_screwdriver: { - displayName: t('hex_screwdriver'), + displayName: HEX_SCREWDRIVER_DISPLAY_NAME, subtitle: t('provided_with_robot_use_right_size'), }, [GRIPPER_LOADNAME]: { displayName: t('branded:gripper') }, diff --git a/app/src/organisms/GripperWizardFlows/MovePin.tsx b/app/src/organisms/GripperWizardFlows/MovePin.tsx index 1cf5153eaa5..b4cb6ebcf25 100644 --- a/app/src/organisms/GripperWizardFlows/MovePin.tsx +++ b/app/src/organisms/GripperWizardFlows/MovePin.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { useTranslation, Trans } from 'react-i18next' import { EXTENSION } from '@opentrons/shared-data' import { @@ -25,6 +24,7 @@ import movePinRearToStorage from '/app/assets/videos/gripper-wizards/PIN_FROM_RE import calibratingFrontJaw from '/app/assets/videos/gripper-wizards/CALIBRATING_FRONT_JAW.webm' import calibratingRearJaw from '/app/assets/videos/gripper-wizards/CALIBRATING_REAR_JAW.webm' +import type { ReactNode } from 'react' import type { Coordinates } from '@opentrons/shared-data' import type { CreateMaintenanceCommand } from '/app/resources/runs' import type { GripperWizardStepProps, MovePinStep } from './types' @@ -137,10 +137,10 @@ export const MovePin = (props: MovePinProps): JSX.Element | null => { [m in typeof movement]: { inProgressText: string header: string - body: React.ReactNode + body: ReactNode buttonText: string - prepImage: React.ReactNode - inProgressImage?: React.ReactNode + prepImage: ReactNode + inProgressImage?: ReactNode } } = { [MOVE_PIN_TO_FRONT_JAW]: { diff --git a/app/src/organisms/GripperWizardFlows/__tests__/BeforeBeginning.test.tsx b/app/src/organisms/GripperWizardFlows/__tests__/BeforeBeginning.test.tsx index 888e6ba30b2..da3b45eec62 100644 --- a/app/src/organisms/GripperWizardFlows/__tests__/BeforeBeginning.test.tsx +++ b/app/src/organisms/GripperWizardFlows/__tests__/BeforeBeginning.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { fireEvent, screen, waitFor } from '@testing-library/react' import { describe, it, vi, beforeEach, expect } from 'vitest' import { renderWithProviders } from '/app/__testing-utils__' @@ -8,15 +7,17 @@ import { RUN_ID_1 } from '/app/resources/runs/__fixtures__' import { BeforeBeginning } from '../BeforeBeginning' import { GRIPPER_FLOW_TYPES } from '../constants' +import type { ComponentProps } from 'react' + vi.mock('/app/molecules/InProgressModal/InProgressModal') -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, })[0] } describe('BeforeBeginning', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { goBack: vi.fn(), diff --git a/app/src/organisms/GripperWizardFlows/__tests__/ExitConfirmation.test.tsx b/app/src/organisms/GripperWizardFlows/__tests__/ExitConfirmation.test.tsx index 4b20f234bfb..871a58d1ba9 100644 --- a/app/src/organisms/GripperWizardFlows/__tests__/ExitConfirmation.test.tsx +++ b/app/src/organisms/GripperWizardFlows/__tests__/ExitConfirmation.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { fireEvent, screen } from '@testing-library/react' import { describe, it, vi, expect } from 'vitest' import { renderWithProviders } from '/app/__testing-utils__' @@ -7,12 +6,14 @@ import { i18n } from '/app/i18n' import { ExitConfirmation } from '../ExitConfirmation' import { GRIPPER_FLOW_TYPES } from '../constants' +import type { ComponentProps } from 'react' + describe('ExitConfirmation', () => { const mockBack = vi.fn() const mockExit = vi.fn() const render = ( - props: Partial> = {} + props: Partial> = {} ) => { return renderWithProviders( { let mockChainRunCommands: any let mockSetErrorMessage: any - const render = ( - props: Partial> = {} - ) => { + const render = (props: Partial> = {}) => { return renderWithProviders( { @@ -25,9 +26,7 @@ describe('MovePin', () => { const mockSetFrontJawOffset = vi.fn() const mockRunId = 'fakeRunId' - const render = ( - props: Partial> = {} - ) => { + const render = (props: Partial> = {}) => { return renderWithProviders( { const mockProceed = vi.fn() - const render = ( - props: Partial> = {} - ) => { + const render = (props: Partial> = {}) => { return renderWithProviders( { let mockChainRunCommands: any let mockSetErrorMessage: any const render = ( - props: Partial> = {} + props: Partial> = {} ) => { return renderWithProviders( { + setIsExiting(true) + if (maintenanceRunData?.data.id == null) { handleClose() } else { diff --git a/app/src/organisms/IncompatibleModule/__tests__/IncompatibleModuleDesktopModalBody.test.tsx b/app/src/organisms/IncompatibleModule/__tests__/IncompatibleModuleDesktopModalBody.test.tsx index 41edc4439c6..a4bb0f1e1f8 100644 --- a/app/src/organisms/IncompatibleModule/__tests__/IncompatibleModuleDesktopModalBody.test.tsx +++ b/app/src/organisms/IncompatibleModule/__tests__/IncompatibleModuleDesktopModalBody.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { screen } from '@testing-library/react' import { describe, it, beforeEach, vi } from 'vitest' import { when } from 'vitest-when' @@ -9,13 +8,13 @@ import { IncompatibleModuleDesktopModalBody } from '../IncompatibleModuleDesktop import { useIsFlex } from '/app/redux-resources/robots' import * as Fixtures from '../__fixtures__' +import type { ComponentProps } from 'react' + vi.mock('/app/redux-resources/robots') const getRenderer = (isFlex: boolean) => { when(useIsFlex).calledWith('otie').thenReturn(isFlex) - return ( - props: React.ComponentProps - ) => { + return (props: ComponentProps) => { return renderWithProviders( , { @@ -26,7 +25,7 @@ const getRenderer = (isFlex: boolean) => { } describe('IncompatibleModuleDesktopModalBody', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { modules: [], diff --git a/app/src/organisms/IncompatibleModule/__tests__/IncompatibleModuleODDModalBody.test.tsx b/app/src/organisms/IncompatibleModule/__tests__/IncompatibleModuleODDModalBody.test.tsx index e2d4e1a2af0..9c59e9a821a 100644 --- a/app/src/organisms/IncompatibleModule/__tests__/IncompatibleModuleODDModalBody.test.tsx +++ b/app/src/organisms/IncompatibleModule/__tests__/IncompatibleModuleODDModalBody.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { screen } from '@testing-library/react' import { describe, it, beforeEach, expect } from 'vitest' import '@testing-library/jest-dom/vitest' @@ -7,8 +6,10 @@ import { i18n } from '/app/i18n' import { IncompatibleModuleODDModalBody } from '../IncompatibleModuleODDModalBody' import * as Fixtures from '../__fixtures__' +import type { ComponentProps } from 'react' + const render = ( - props: React.ComponentProps + props: ComponentProps ) => { return renderWithProviders(, { i18nInstance: i18n, @@ -16,7 +17,7 @@ const render = ( } describe('IncompatibleModuleODDModalBody', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { modules: [], diff --git a/app/src/organisms/IncompatibleModule/__tests__/IncompatibleModuleTakeover.test.tsx b/app/src/organisms/IncompatibleModule/__tests__/IncompatibleModuleTakeover.test.tsx index d3da5d17958..24e35265621 100644 --- a/app/src/organisms/IncompatibleModule/__tests__/IncompatibleModuleTakeover.test.tsx +++ b/app/src/organisms/IncompatibleModule/__tests__/IncompatibleModuleTakeover.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { screen } from '@testing-library/react' import { describe, it, beforeEach, afterEach, expect, vi } from 'vitest' import { when } from 'vitest-when' @@ -18,6 +17,8 @@ import { } from '/app/App/portal' import * as Fixtures from '../__fixtures__' +import type { ComponentProps } from 'react' + vi.mock('../hooks') vi.mock('../IncompatibleModuleODDModalBody') vi.mock('../IncompatibleModuleDesktopModalBody') @@ -32,7 +33,7 @@ const getRenderer = (incompatibleModules: AttachedModule[]) => { vi.mocked(IncompatibleModuleDesktopModalBody).mockReturnValue(
        TEST ELEMENT DESKTOP
        ) - return (props: React.ComponentProps) => { + return (props: ComponentProps) => { const [rendered] = renderWithProviders( <> @@ -55,7 +56,7 @@ const getRenderer = (incompatibleModules: AttachedModule[]) => { } describe('IncompatibleModuleTakeover', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { isOnDevice: true } }) diff --git a/app/src/organisms/IncompatibleModule/hooks/__tests__/useIncompatibleModulesAttached.test.tsx b/app/src/organisms/IncompatibleModule/hooks/__tests__/useIncompatibleModulesAttached.test.tsx index 7e1a7342db1..c58d75c7b6d 100644 --- a/app/src/organisms/IncompatibleModule/hooks/__tests__/useIncompatibleModulesAttached.test.tsx +++ b/app/src/organisms/IncompatibleModule/hooks/__tests__/useIncompatibleModulesAttached.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { QueryClient, QueryClientProvider } from 'react-query' import { vi, it, expect, describe, beforeEach } from 'vitest' @@ -8,16 +7,17 @@ import { useIncompatibleModulesAttached } from '..' import * as Fixtures from '../__fixtures__' +import type { FunctionComponent, ReactNode } from 'react' import type { Modules } from '@opentrons/api-client' import type { UseQueryResult } from 'react-query' vi.mock('@opentrons/react-api-client') describe('useIncompatibleModulesAttached', () => { - let wrapper: React.FunctionComponent<{ children: React.ReactNode }> + let wrapper: FunctionComponent<{ children: ReactNode }> beforeEach(() => { const queryClient = new QueryClient() - const clientProvider: React.FunctionComponent<{ - children: React.ReactNode + const clientProvider: FunctionComponent<{ + children: ReactNode }> = ({ children }) => ( {children} ) diff --git a/app/src/organisms/InterventionModal/MoveLabwareInterventionContent.tsx b/app/src/organisms/InterventionModal/MoveLabwareInterventionContent.tsx index af561b6c15d..556c051d32d 100644 --- a/app/src/organisms/InterventionModal/MoveLabwareInterventionContent.tsx +++ b/app/src/organisms/InterventionModal/MoveLabwareInterventionContent.tsx @@ -191,7 +191,6 @@ export function MoveLabwareInterventionContent({ -) => { +import type { ComponentProps } from 'react' + +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, })[0] } describe('InterventionCommandMessage', () => { - let props: React.ComponentProps + let props: ComponentProps it('truncates command text greater than 220 characters long', () => { props = { commandMessage: longCommandMessage } diff --git a/app/src/organisms/InterventionModal/__tests__/InterventionCommandMessage.test.tsx b/app/src/organisms/InterventionModal/__tests__/InterventionCommandMessage.test.tsx index 6f3a688b808..a09994c929d 100644 --- a/app/src/organisms/InterventionModal/__tests__/InterventionCommandMessage.test.tsx +++ b/app/src/organisms/InterventionModal/__tests__/InterventionCommandMessage.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { screen } from '@testing-library/react' import { describe, it, expect } from 'vitest' import { renderWithProviders } from '/app/__testing-utils__' @@ -10,16 +9,16 @@ import { truncatedCommandMessage, } from '../__fixtures__' -const render = ( - props: React.ComponentProps -) => { +import type { ComponentProps } from 'react' + +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, })[0] } describe('InterventionCommandMessage', () => { - let props: React.ComponentProps + let props: ComponentProps it('truncates command text greater than 220 characters long', () => { props = { commandMessage: longCommandMessage } diff --git a/app/src/organisms/InterventionModal/__tests__/InterventionModal.test.tsx b/app/src/organisms/InterventionModal/__tests__/InterventionModal.test.tsx index 59b8c659a1a..bb4799e1754 100644 --- a/app/src/organisms/InterventionModal/__tests__/InterventionModal.test.tsx +++ b/app/src/organisms/InterventionModal/__tests__/InterventionModal.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { fireEvent, renderHook, screen } from '@testing-library/react' import { describe, it, expect, vi, beforeEach } from 'vitest' @@ -18,6 +17,7 @@ import { import { InterventionModal, useInterventionModal } from '..' import { useIsFlex } from '/app/redux-resources/robots' +import type { ComponentProps } from 'react' import type { CompletedProtocolAnalysis } from '@opentrons/shared-data' import type { RunData } from '@opentrons/api-client' @@ -90,14 +90,14 @@ describe('useInterventionModal', () => { }) }) -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, })[0] } describe('InterventionModal', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { robotName: ROBOT_NAME, diff --git a/app/src/organisms/LabwareOffsetTabs/index.tsx b/app/src/organisms/LabwareOffsetTabs/index.tsx index 3ad81c51d01..0e32c8e5bd4 100644 --- a/app/src/organisms/LabwareOffsetTabs/index.tsx +++ b/app/src/organisms/LabwareOffsetTabs/index.tsx @@ -73,7 +73,6 @@ export function LabwareOffsetTabs({ void - protocolData: CompletedProtocolAnalysis - registerPosition: React.Dispatch - chainRunCommands: ReturnType['chainRunCommands'] - handleJog: Jog - setFatalError: (errorMessage: string) => void - isRobotMoving: boolean - existingOffsets: LabwareOffset[] - protocolName: string - shouldUseMetalProbe: boolean -}): JSX.Element | null => { - const { - proceed, - protocolData, - chainRunCommands, - isRobotMoving, - setFatalError, - existingOffsets, - protocolName, - shouldUseMetalProbe, - } = props - const isOnDevice = useSelector(getIsOnDevice) - const { t, i18n } = useTranslation(['labware_position_check', 'shared']) - const handleClickStartLPC = (): void => { - const prepCommands = getPrepCommands(protocolData) - chainRunCommands(prepCommands, false) - .then(() => { - proceed() - }) - .catch((e: Error) => { - setFatalError( - `IntroScreen failed to issue prep commands with message: ${e.message}` - ) - }) - } - const requiredEquipmentList = [ - { - loadName: t('all_modules_and_labware_from_protocol', { - protocol_name: protocolName, - }), - displayName: t('all_modules_and_labware_from_protocol', { - protocol_name: protocolName, - }), - }, - ] - if (shouldUseMetalProbe) { - requiredEquipmentList.push(CALIBRATION_PROBE) - } - - if (isRobotMoving) { - return ( - - ) - } - return ( - }} - /> - } - rightElement={ - - } - footer={ - - {isOnDevice ? ( - - ) : ( - - )} - {isOnDevice ? ( - - ) : ( - - {i18n.format(t('shared:get_started'), 'capitalize')} - - )} - - } - /> - ) -} - -const VIEW_OFFSETS_BUTTON_STYLE = css` - ${TYPOGRAPHY.pSemiBold}; - color: ${COLORS.black90}; - font-size: ${TYPOGRAPHY.fontSize22}; - &:hover { - opacity: 100%; - } - &:active { - opacity: 70%; - } -` -interface ViewOffsetsProps { - existingOffsets: LabwareOffset[] - labwareDefinitions: LabwareDefinition2[] -} -function ViewOffsets(props: ViewOffsetsProps): JSX.Element { - const { existingOffsets, labwareDefinitions } = props - const { t, i18n } = useTranslation('labware_position_check') - const [showOffsetsTable, setShowOffsetsModal] = React.useState(false) - const latestCurrentOffsets = getLatestCurrentOffsets(existingOffsets) - return existingOffsets.length > 0 ? ( - <> - { - setShowOffsetsModal(true) - }} - css={VIEW_OFFSETS_BUTTON_STYLE} - aria-label="show labware offsets" - > - - - {i18n.format(t('view_current_offsets'), 'capitalize')} - - - {showOffsetsTable - ? createPortal( - - {i18n.format(t('labware_offset_data'), 'capitalize')} - - } - footer={ - { - setShowOffsetsModal(false) - }} - /> - } - > - - - - , - getTopPortalEl() - ) - : null} - - ) : ( - - ) -} diff --git a/app/src/organisms/LabwarePositionCheck/index.tsx b/app/src/organisms/LabwarePositionCheck/index.tsx deleted file mode 100644 index b1453f9267c..00000000000 --- a/app/src/organisms/LabwarePositionCheck/index.tsx +++ /dev/null @@ -1,102 +0,0 @@ -import * as React from 'react' -import { useLogger } from '../../logger' -import { LabwarePositionCheckComponent } from './LabwarePositionCheckComponent' -import { FatalErrorModal } from './FatalErrorModal' -import { getIsOnDevice } from '/app/redux/config' -import { useSelector } from 'react-redux' - -import { FLEX_ROBOT_TYPE } from '@opentrons/shared-data' -import type { - CompletedProtocolAnalysis, - RobotType, -} from '@opentrons/shared-data' -import type { LabwareOffset } from '@opentrons/api-client' - -interface LabwarePositionCheckModalProps { - onCloseClick: () => void - runId: string - maintenanceRunId: string - robotType: RobotType - existingOffsets: LabwareOffset[] - mostRecentAnalysis: CompletedProtocolAnalysis | null - protocolName: string - caughtError?: Error - setMaintenanceRunId: (id: string | null) => void - isDeletingMaintenanceRun: boolean -} - -// We explicitly wrap LabwarePositionCheckComponent in an ErrorBoundary because an error might occur while pulling in -// the component's dependencies (like useLabwarePositionCheck). If we wrapped the contents of LabwarePositionCheckComponent -// in an ErrorBoundary as part of its return value (render), an error could occur before this point, meaning the error boundary -// would never get invoked -export const LabwarePositionCheck = ( - props: LabwarePositionCheckModalProps -): JSX.Element => { - const logger = useLogger(new URL('', import.meta.url).pathname) - const isOnDevice = useSelector(getIsOnDevice) - return ( - - - - ) -} - -interface ErrorBoundaryProps { - children: React.ReactNode - onClose: () => void - shouldUseMetalProbe: boolean - logger: ReturnType - ErrorComponent: (props: { - errorMessage: string - shouldUseMetalProbe: boolean - onClose: () => void - isOnDevice: boolean - }) => JSX.Element - isOnDevice: boolean -} -class ErrorBoundary extends React.Component< - ErrorBoundaryProps, - { error: Error | null } -> { - constructor(props: ErrorBoundaryProps) { - super(props) - this.state = { error: null } - } - - componentDidCatch(error: Error, errorInfo: React.ErrorInfo): void { - this.props.logger.error(`LPC error message: ${error.message}`) - this.props.logger.error( - `LPC error component stack: ${errorInfo.componentStack}` - ) - this.setState({ - error, - }) - } - - render(): ErrorBoundaryProps['children'] | JSX.Element { - const { - ErrorComponent, - children, - shouldUseMetalProbe, - isOnDevice, - } = this.props - const { error } = this.state - if (error != null) - return ( - - ) - // Normally, just render children - return children - } -} diff --git a/app/src/organisms/LabwarePositionCheck/useLaunchLPC.tsx b/app/src/organisms/LabwarePositionCheck/useLaunchLPC.tsx deleted file mode 100644 index 18c906d2998..00000000000 --- a/app/src/organisms/LabwarePositionCheck/useLaunchLPC.tsx +++ /dev/null @@ -1,89 +0,0 @@ -import { useState } from 'react' - -import { - useCreateMaintenanceRunLabwareDefinitionMutation, - useDeleteMaintenanceRunMutation, -} from '@opentrons/react-api-client' - -import { - useCreateTargetedMaintenanceRunMutation, - useNotifyRunQuery, - useMostRecentCompletedAnalysis, -} from '/app/resources/runs' -import { LabwarePositionCheck } from '.' -import { getLabwareDefinitionsFromCommands } from '/app/local-resources/labware' - -import type { RobotType } from '@opentrons/shared-data' - -export function useLaunchLPC( - runId: string, - robotType: RobotType, - protocolName?: string -): { launchLPC: () => void; LPCWizard: JSX.Element | null } { - const { data: runRecord } = useNotifyRunQuery(runId, { staleTime: Infinity }) - const { - createTargetedMaintenanceRun, - } = useCreateTargetedMaintenanceRunMutation() - const { - deleteMaintenanceRun, - isLoading: isDeletingMaintenanceRun, - } = useDeleteMaintenanceRunMutation() - const mostRecentAnalysis = useMostRecentCompletedAnalysis(runId) - const [maintenanceRunId, setMaintenanceRunId] = useState(null) - const currentOffsets = runRecord?.data?.labwareOffsets ?? [] - const { - createLabwareDefinition, - } = useCreateMaintenanceRunLabwareDefinitionMutation() - - const handleCloseLPC = (): void => { - if (maintenanceRunId != null) { - deleteMaintenanceRun(maintenanceRunId, { - onSettled: () => { - setMaintenanceRunId(null) - }, - }) - } - } - return { - launchLPC: () => - createTargetedMaintenanceRun({ - labwareOffsets: currentOffsets.map( - ({ vector, location, definitionUri }) => ({ - vector, - location, - definitionUri, - }) - ), - }).then(maintenanceRun => - // TODO(BC, 2023-05-15): replace this with a call to the protocol run's GET labware_definitions - // endpoint once it's made we should be adding the definitions to the maintenance run by - // reading from the current protocol run, and not from the analysis - Promise.all( - getLabwareDefinitionsFromCommands( - mostRecentAnalysis?.commands ?? [] - ).map(def => { - createLabwareDefinition({ - maintenanceRunId: maintenanceRun?.data?.id, - labwareDef: def, - }) - }) - ).then(() => { - setMaintenanceRunId(maintenanceRun.data.id) - }) - ), - LPCWizard: - maintenanceRunId != null ? ( - - ) : null, - } -} diff --git a/app/src/organisms/ApplyHistoricOffsets/LabwareOffsetTable.tsx b/app/src/organisms/LegacyApplyHistoricOffsets/LabwareOffsetTable.tsx similarity index 95% rename from app/src/organisms/ApplyHistoricOffsets/LabwareOffsetTable.tsx rename to app/src/organisms/LegacyApplyHistoricOffsets/LabwareOffsetTable.tsx index 3a7276a2ed8..bffcbd94d80 100644 --- a/app/src/organisms/ApplyHistoricOffsets/LabwareOffsetTable.tsx +++ b/app/src/organisms/LegacyApplyHistoricOffsets/LabwareOffsetTable.tsx @@ -1,9 +1,12 @@ import styled from 'styled-components' import { useTranslation } from 'react-i18next' + import { SPACING, TYPOGRAPHY, COLORS } from '@opentrons/components' + import { OffsetVector } from '/app/molecules/OffsetVector' import { formatTimestamp } from '/app/transformations/runs' -import { getDisplayLocation } from '/app/organisms/LabwarePositionCheck/utils/getDisplayLocation' +import { getDisplayLocation } from '/app/organisms/LegacyLabwarePositionCheck/utils/getDisplayLocation' + import type { LabwareDefinition2 } from '@opentrons/shared-data' import type { OffsetCandidate } from './hooks/useOffsetCandidatesForAnalysis' import type { TFunction } from 'i18next' diff --git a/app/src/organisms/ApplyHistoricOffsets/__tests__/ApplyHistoricOffsets.test.tsx b/app/src/organisms/LegacyApplyHistoricOffsets/__tests__/ApplyHistoricOffsets.test.tsx similarity index 95% rename from app/src/organisms/ApplyHistoricOffsets/__tests__/ApplyHistoricOffsets.test.tsx rename to app/src/organisms/LegacyApplyHistoricOffsets/__tests__/ApplyHistoricOffsets.test.tsx index 74f77291834..fb39565b161 100644 --- a/app/src/organisms/ApplyHistoricOffsets/__tests__/ApplyHistoricOffsets.test.tsx +++ b/app/src/organisms/LegacyApplyHistoricOffsets/__tests__/ApplyHistoricOffsets.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { fireEvent, screen } from '@testing-library/react' import { describe, it, expect, vi } from 'vitest' @@ -8,13 +7,14 @@ import { i18n } from '/app/i18n' import { renderWithProviders } from '/app/__testing-utils__' import { getIsLabwareOffsetCodeSnippetsOn } from '/app/redux/config' import { getLabwareDefinitionsFromCommands } from '/app/local-resources/labware' -import { ApplyHistoricOffsets } from '..' +import { LegacyApplyHistoricOffsets } from '..' +import type { ComponentProps } from 'react' import type { LabwareDefinition2 } from '@opentrons/shared-data' import type { OffsetCandidate } from '../hooks/useOffsetCandidatesForAnalysis' vi.mock('/app/redux/config') -vi.mock('/app/organisms/LabwarePositionCheck/utils/labware') +vi.mock('/app/organisms/LegacyLabwarePositionCheck/utils/labware') vi.mock('/app/local-resources/labware') const mockLabwareDef = fixture96Plate as LabwareDefinition2 @@ -64,10 +64,10 @@ const mockFourthCandidate: OffsetCandidate = { describe('ApplyHistoricOffsets', () => { const mockSetShouldApplyOffsets = vi.fn() const render = ( - props?: Partial> + props?: Partial> ) => - renderWithProviders>( - >( + - renderWithProviders>( + renderWithProviders>( { ) it('returns historical run details with newest first', async () => { - const wrapper: React.FunctionComponent<{ children: React.ReactNode }> = ({ + const wrapper: FunctionComponent<{ children: ReactNode }> = ({ children, }) =>
        {children}
        const { result } = renderHook(useHistoricRunDetails, { wrapper }) @@ -57,7 +57,7 @@ describe('useHistoricRunDetails', () => { links: {}, }) ) - const wrapper: React.FunctionComponent<{ children: React.ReactNode }> = ({ + const wrapper: FunctionComponent<{ children: ReactNode }> = ({ children, }) =>
        {children}
        const { result } = renderHook( diff --git a/app/src/organisms/ApplyHistoricOffsets/hooks/__tests__/useOffsetCandidatesForAnalysis.test.tsx b/app/src/organisms/LegacyApplyHistoricOffsets/hooks/__tests__/useOffsetCandidatesForAnalysis.test.tsx similarity index 94% rename from app/src/organisms/ApplyHistoricOffsets/hooks/__tests__/useOffsetCandidatesForAnalysis.test.tsx rename to app/src/organisms/LegacyApplyHistoricOffsets/hooks/__tests__/useOffsetCandidatesForAnalysis.test.tsx index 832417cb9af..b4581abe15e 100644 --- a/app/src/organisms/ApplyHistoricOffsets/hooks/__tests__/useOffsetCandidatesForAnalysis.test.tsx +++ b/app/src/organisms/LegacyApplyHistoricOffsets/hooks/__tests__/useOffsetCandidatesForAnalysis.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { describe, it, expect, vi, beforeEach } from 'vitest' import { when } from 'vitest-when' import { renderHook, waitFor } from '@testing-library/react' @@ -13,6 +12,7 @@ import { getLabwareLocationCombos } from '../getLabwareLocationCombos' import { useOffsetCandidatesForAnalysis } from '../useOffsetCandidatesForAnalysis' import { storedProtocolData as storedProtocolDataFixture } from '/app/redux/protocol-storage/__fixtures__' +import type { FunctionComponent, ReactNode } from 'react' import type { LabwareDefinition2 } from '@opentrons/shared-data' import type { OffsetCandidate } from '../useOffsetCandidatesForAnalysis' @@ -102,7 +102,7 @@ describe('useOffsetCandidatesForAnalysis', () => { }) it('returns an empty array if robot ip but no analysis output', async () => { - const wrapper: React.FunctionComponent<{ children: React.ReactNode }> = ({ + const wrapper: FunctionComponent<{ children: ReactNode }> = ({ children, }) =>
        {children}
        const { result } = renderHook( @@ -115,7 +115,7 @@ describe('useOffsetCandidatesForAnalysis', () => { }) it('returns an empty array if no robot ip', async () => { - const wrapper: React.FunctionComponent<{ children: React.ReactNode }> = ({ + const wrapper: FunctionComponent<{ children: ReactNode }> = ({ children, }) =>
        {children}
        const { result } = renderHook( @@ -131,7 +131,7 @@ describe('useOffsetCandidatesForAnalysis', () => { }) }) it('returns candidates for each first match with newest first', async () => { - const wrapper: React.FunctionComponent<{ children: React.ReactNode }> = ({ + const wrapper: FunctionComponent<{ children: ReactNode }> = ({ children, }) =>
        {children}
        const { result } = renderHook( diff --git a/app/src/organisms/ApplyHistoricOffsets/hooks/getLabwareLocationCombos.ts b/app/src/organisms/LegacyApplyHistoricOffsets/hooks/getLabwareLocationCombos.ts similarity index 100% rename from app/src/organisms/ApplyHistoricOffsets/hooks/getLabwareLocationCombos.ts rename to app/src/organisms/LegacyApplyHistoricOffsets/hooks/getLabwareLocationCombos.ts diff --git a/app/src/organisms/ApplyHistoricOffsets/hooks/useAllHistoricOffsets.ts b/app/src/organisms/LegacyApplyHistoricOffsets/hooks/useAllHistoricOffsets.ts similarity index 100% rename from app/src/organisms/ApplyHistoricOffsets/hooks/useAllHistoricOffsets.ts rename to app/src/organisms/LegacyApplyHistoricOffsets/hooks/useAllHistoricOffsets.ts diff --git a/app/src/organisms/ApplyHistoricOffsets/hooks/useHistoricRunDetails.ts b/app/src/organisms/LegacyApplyHistoricOffsets/hooks/useHistoricRunDetails.ts similarity index 100% rename from app/src/organisms/ApplyHistoricOffsets/hooks/useHistoricRunDetails.ts rename to app/src/organisms/LegacyApplyHistoricOffsets/hooks/useHistoricRunDetails.ts diff --git a/app/src/organisms/ApplyHistoricOffsets/hooks/useOffsetCandidatesForAnalysis.ts b/app/src/organisms/LegacyApplyHistoricOffsets/hooks/useOffsetCandidatesForAnalysis.ts similarity index 100% rename from app/src/organisms/ApplyHistoricOffsets/hooks/useOffsetCandidatesForAnalysis.ts rename to app/src/organisms/LegacyApplyHistoricOffsets/hooks/useOffsetCandidatesForAnalysis.ts diff --git a/app/src/organisms/LegacyApplyHistoricOffsets/index.tsx b/app/src/organisms/LegacyApplyHistoricOffsets/index.tsx new file mode 100644 index 00000000000..e5fe789544b --- /dev/null +++ b/app/src/organisms/LegacyApplyHistoricOffsets/index.tsx @@ -0,0 +1,192 @@ +import { useState } from 'react' +import { createPortal } from 'react-dom' +import { useSelector } from 'react-redux' +import pick from 'lodash/pick' +import { Trans, useTranslation } from 'react-i18next' +import { + ALIGN_CENTER, + CheckboxField, + DIRECTION_COLUMN, + Flex, + Icon, + JUSTIFY_SPACE_BETWEEN, + Link, + SIZE_1, + SPACING, + LegacyStyledText, + TYPOGRAPHY, + ModalHeader, + ModalShell, +} from '@opentrons/components' +import { getTopPortalEl } from '/app/App/portal' +import { ExternalLink } from '/app/atoms/Link/ExternalLink' +import { PythonLabwareOffsetSnippet } from '/app/molecules/PythonLabwareOffsetSnippet' +import { LabwareOffsetTabs } from '/app/organisms/LabwareOffsetTabs' +import { getLabwareDefinitionsFromCommands } from '/app/local-resources/labware' +import { LabwareOffsetTable } from './LabwareOffsetTable' +import { getIsLabwareOffsetCodeSnippetsOn } from '/app/redux/config' + +import type { ChangeEvent } from 'react' +import type { LabwareOffset } from '@opentrons/api-client' +import type { + LoadedLabware, + LoadedModule, + RunTimeCommand, +} from '@opentrons/shared-data' + +const HOW_OFFSETS_WORK_SUPPORT_URL = + 'https://support.opentrons.com/s/article/How-Labware-Offsets-work-on-the-OT-2' +export interface OffsetCandidate extends LabwareOffset { + runCreatedAt: string + labwareDisplayName: string +} + +interface LegacyApplyHistoricOffsetsProps { + offsetCandidates: OffsetCandidate[] + shouldApplyOffsets: boolean + setShouldApplyOffsets: (shouldApplyOffsets: boolean) => void + commands: RunTimeCommand[] + labware: LoadedLabware[] + modules: LoadedModule[] +} +export function LegacyApplyHistoricOffsets( + props: LegacyApplyHistoricOffsetsProps +): JSX.Element { + const { + offsetCandidates, + shouldApplyOffsets, + setShouldApplyOffsets, + labware, + modules, + commands, + } = props + const [showOffsetDataModal, setShowOffsetDataModal] = useState(false) + const { t } = useTranslation('labware_position_check') + const isLabwareOffsetCodeSnippetsOn = useSelector( + getIsLabwareOffsetCodeSnippetsOn + ) + const JupyterSnippet = ( + + pick(o, ['definitionUri', 'vector', 'location']) + )} + {...{ labware, modules, commands }} + /> + ) + const CommandLineSnippet = ( + + pick(o, ['definitionUri', 'vector', 'location']) + )} + {...{ labware, modules, commands }} + /> + ) + const noOffsetData = offsetCandidates.length < 1 + return ( + + ) => { + setShouldApplyOffsets(e.currentTarget.checked) + }} + value={shouldApplyOffsets} + disabled={noOffsetData} + isIndeterminate={noOffsetData} + label={ + + + + {t(noOffsetData ? 'no_offset_data' : 'apply_offset_data')} + + + } + /> + { + setShowOffsetDataModal(true) + }} + css={TYPOGRAPHY.linkPSemiBold} + > + {t(noOffsetData ? 'learn_more' : 'view_data')} + + {showOffsetDataModal + ? createPortal( + { + setShowOffsetDataModal(false) + }} + /> + } + > + + {noOffsetData ? ( + + ), + }} + /> + ) : ( + + {t('robot_has_offsets_from_previous_runs')} + + )} + + {t('see_how_offsets_work')} + + {!noOffsetData ? ( + isLabwareOffsetCodeSnippetsOn ? ( + + } + JupyterComponent={JupyterSnippet} + CommandLineComponent={CommandLineSnippet} + /> + ) : ( + + ) + ) : null} + + , + getTopPortalEl() + ) + : null} + + ) +} diff --git a/app/src/organisms/LabwarePositionCheck/AttachProbe.tsx b/app/src/organisms/LegacyLabwarePositionCheck/AttachProbe.tsx similarity index 95% rename from app/src/organisms/LabwarePositionCheck/AttachProbe.tsx rename to app/src/organisms/LegacyLabwarePositionCheck/AttachProbe.tsx index b421b4be81f..afd9efba19f 100644 --- a/app/src/organisms/LabwarePositionCheck/AttachProbe.tsx +++ b/app/src/organisms/LegacyLabwarePositionCheck/AttachProbe.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useEffect, useState } from 'react' import { Trans, useTranslation } from 'react-i18next' import { RESPONSIVENESS, @@ -15,6 +15,7 @@ import attachProbe8 from '/app/assets/videos/pipette-wizard-flows/Pipette_Attach import attachProbe96 from '/app/assets/videos/pipette-wizard-flows/Pipette_Attach_Probe_96.webm' import { GenericWizardTile } from '/app/molecules/GenericWizardTile' +import type { Dispatch } from 'react' import type { CompletedProtocolAnalysis, CreateCommand, @@ -31,7 +32,7 @@ import type { interface AttachProbeProps extends AttachProbeStep { protocolData: CompletedProtocolAnalysis proceed: () => void - registerPosition: React.Dispatch + registerPosition: Dispatch chainRunCommands: ReturnType['chainRunCommands'] setFatalError: (errorMessage: string) => void workingOffsets: WorkingOffset[] @@ -52,9 +53,7 @@ export const AttachProbe = (props: AttachProbeProps): JSX.Element | null => { setFatalError, isOnDevice, } = props - const [showUnableToDetect, setShowUnableToDetect] = React.useState( - false - ) + const [showUnableToDetect, setShowUnableToDetect] = useState(false) const pipette = protocolData.pipettes.find(p => p.id === pipetteId) const pipetteName = pipette?.pipetteName @@ -72,7 +71,7 @@ export const AttachProbe = (props: AttachProbeProps): JSX.Element | null => { const pipetteMount = pipette?.mount - React.useEffect(() => { + useEffect(() => { // move into correct position for probe attach on mount chainRunCommands( [ diff --git a/app/src/organisms/LabwarePositionCheck/CheckItem.tsx b/app/src/organisms/LegacyLabwarePositionCheck/CheckItem.tsx similarity index 98% rename from app/src/organisms/LabwarePositionCheck/CheckItem.tsx rename to app/src/organisms/LegacyLabwarePositionCheck/CheckItem.tsx index 5d5008554a6..734ee6468b1 100644 --- a/app/src/organisms/LabwarePositionCheck/CheckItem.tsx +++ b/app/src/organisms/LegacyLabwarePositionCheck/CheckItem.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useEffect } from 'react' import omit from 'lodash/omit' import isEqual from 'lodash/isEqual' import { Trans, useTranslation } from 'react-i18next' @@ -29,6 +29,7 @@ import { getCurrentOffsetForLabwareInLocation } from '/app/transformations/analy import { getIsOnDevice } from '/app/redux/config' import { getDisplayLocation } from './utils/getDisplayLocation' +import type { Dispatch } from 'react' import type { LabwareOffset } from '@opentrons/api-client' import type { CompletedProtocolAnalysis, @@ -54,7 +55,7 @@ interface CheckItemProps extends Omit { proceed: () => void chainRunCommands: ReturnType['chainRunCommands'] setFatalError: (errorMessage: string) => void - registerPosition: React.Dispatch + registerPosition: Dispatch workingOffsets: WorkingOffset[] existingOffsets: LabwareOffset[] handleJog: Jog @@ -131,7 +132,7 @@ export const CheckItem = (props: CheckItemProps): JSX.Element | null => { o.initialPosition != null )?.initialPosition - React.useEffect(() => { + useEffect(() => { if (initialPosition == null && modulePrepCommands.length > 0) { chainRunCommands(modulePrepCommands, false) .then(() => {}) diff --git a/app/src/organisms/LabwarePositionCheck/DetachProbe.tsx b/app/src/organisms/LegacyLabwarePositionCheck/DetachProbe.tsx similarity index 96% rename from app/src/organisms/LabwarePositionCheck/DetachProbe.tsx rename to app/src/organisms/LegacyLabwarePositionCheck/DetachProbe.tsx index da0952ca407..dd040654a23 100644 --- a/app/src/organisms/LabwarePositionCheck/DetachProbe.tsx +++ b/app/src/organisms/LegacyLabwarePositionCheck/DetachProbe.tsx @@ -1,10 +1,10 @@ -import * as React from 'react' +import { useEffect } from 'react' import { useTranslation } from 'react-i18next' import { css } from 'styled-components' import { + LegacyStyledText, RESPONSIVENESS, SPACING, - LegacyStyledText, TYPOGRAPHY, } from '@opentrons/components' import { RobotMotionLoader } from './RobotMotionLoader' @@ -14,6 +14,7 @@ import detachProbe8 from '/app/assets/videos/pipette-wizard-flows/Pipette_Detach import detachProbe96 from '/app/assets/videos/pipette-wizard-flows/Pipette_Detach_Probe_96.webm' import { GenericWizardTile } from '/app/molecules/GenericWizardTile' +import type { Dispatch } from 'react' import type { CompletedProtocolAnalysis } from '@opentrons/shared-data' import type { Jog } from '/app/molecules/JogControls/types' import type { useChainRunCommands } from '/app/resources/runs' @@ -27,7 +28,7 @@ import type { LabwareOffset } from '@opentrons/api-client' interface DetachProbeProps extends DetachProbeStep { protocolData: CompletedProtocolAnalysis proceed: () => void - registerPosition: React.Dispatch + registerPosition: Dispatch chainRunCommands: ReturnType['chainRunCommands'] setFatalError: (errorMessage: string) => void workingOffsets: WorkingOffset[] @@ -58,7 +59,7 @@ export const DetachProbe = (props: DetachProbeProps): JSX.Element | null => { } const pipetteMount = pipette?.mount - React.useEffect(() => { + useEffect(() => { // move into correct position for probe detach on mount chainRunCommands( [ diff --git a/app/src/organisms/LabwarePositionCheck/ExitConfirmation.tsx b/app/src/organisms/LegacyLabwarePositionCheck/ExitConfirmation.tsx similarity index 100% rename from app/src/organisms/LabwarePositionCheck/ExitConfirmation.tsx rename to app/src/organisms/LegacyLabwarePositionCheck/ExitConfirmation.tsx diff --git a/app/src/organisms/LabwarePositionCheck/FatalErrorModal.tsx b/app/src/organisms/LegacyLabwarePositionCheck/FatalErrorModal.tsx similarity index 100% rename from app/src/organisms/LabwarePositionCheck/FatalErrorModal.tsx rename to app/src/organisms/LegacyLabwarePositionCheck/FatalErrorModal.tsx diff --git a/app/src/organisms/LabwarePositionCheck/IntroScreen/getPrepCommands.ts b/app/src/organisms/LegacyLabwarePositionCheck/IntroScreen/getPrepCommands.ts similarity index 100% rename from app/src/organisms/LabwarePositionCheck/IntroScreen/getPrepCommands.ts rename to app/src/organisms/LegacyLabwarePositionCheck/IntroScreen/getPrepCommands.ts diff --git a/app/src/organisms/LegacyLabwarePositionCheck/IntroScreen/index.tsx b/app/src/organisms/LegacyLabwarePositionCheck/IntroScreen/index.tsx new file mode 100644 index 00000000000..44e5eb67ded --- /dev/null +++ b/app/src/organisms/LegacyLabwarePositionCheck/IntroScreen/index.tsx @@ -0,0 +1,225 @@ +import { useState } from 'react' +import { createPortal } from 'react-dom' +import { Trans, useTranslation } from 'react-i18next' +import { css } from 'styled-components' + +import { + ALIGN_CENTER, + Box, + Btn, + COLORS, + DIRECTION_COLUMN, + Flex, + Icon, + JUSTIFY_SPACE_BETWEEN, + PrimaryButton, + ModalShell, + SPACING, + LegacyStyledText, + TYPOGRAPHY, +} from '@opentrons/components' +import { RobotMotionLoader } from '../RobotMotionLoader' +import { getPrepCommands } from './getPrepCommands' +import { WizardRequiredEquipmentList } from '/app/molecules/WizardRequiredEquipmentList' +import { getLatestCurrentOffsets } from '/app/transformations/runs' +import { getIsOnDevice } from '/app/redux/config' +import { NeedHelpLink } from '/app/molecules/OT2CalibrationNeedHelpLink' +import { useSelector } from 'react-redux' +import { TwoUpTileLayout } from '../TwoUpTileLayout' +import { getTopPortalEl } from '/app/App/portal' +import { SmallButton } from '/app/atoms/buttons' +import { CALIBRATION_PROBE } from '/app/organisms/PipetteWizardFlows/constants' +import { TerseOffsetTable } from '../ResultsSummary' +import { getLabwareDefinitionsFromCommands } from '/app/local-resources/labware' + +import type { Dispatch } from 'react' +import type { LabwareOffset } from '@opentrons/api-client' +import type { + CompletedProtocolAnalysis, + LabwareDefinition2, +} from '@opentrons/shared-data' +import type { useChainRunCommands } from '/app/resources/runs' +import type { RegisterPositionAction } from '../types' +import type { Jog } from '/app/molecules/JogControls' + +export const INTERVAL_MS = 3000 + +// TODO(BC, 09/01/23): replace updated support article link for LPC on OT-2/Flex +const SUPPORT_PAGE_URL = 'https://support.opentrons.com/s/ot2-calibration' + +export const IntroScreen = (props: { + proceed: () => void + protocolData: CompletedProtocolAnalysis + registerPosition: Dispatch + chainRunCommands: ReturnType['chainRunCommands'] + handleJog: Jog + setFatalError: (errorMessage: string) => void + isRobotMoving: boolean + existingOffsets: LabwareOffset[] + protocolName: string + shouldUseMetalProbe: boolean +}): JSX.Element | null => { + const { + proceed, + protocolData, + chainRunCommands, + isRobotMoving, + setFatalError, + existingOffsets, + protocolName, + shouldUseMetalProbe, + } = props + const isOnDevice = useSelector(getIsOnDevice) + const { t, i18n } = useTranslation(['labware_position_check', 'shared']) + const handleClickStartLPC = (): void => { + const prepCommands = getPrepCommands(protocolData) + chainRunCommands(prepCommands, false) + .then(() => { + proceed() + }) + .catch((e: Error) => { + setFatalError( + `IntroScreen failed to issue prep commands with message: ${e.message}` + ) + }) + } + const requiredEquipmentList = [ + { + loadName: t('all_modules_and_labware_from_protocol', { + protocol_name: protocolName, + }), + displayName: t('all_modules_and_labware_from_protocol', { + protocol_name: protocolName, + }), + }, + ] + if (shouldUseMetalProbe) { + requiredEquipmentList.push(CALIBRATION_PROBE) + } + + if (isRobotMoving) { + return ( + + ) + } + return ( + }} + /> + } + rightElement={ + + } + footer={ + + {isOnDevice ? ( + + ) : ( + + )} + {isOnDevice ? ( + + ) : ( + + {i18n.format(t('shared:get_started'), 'capitalize')} + + )} + + } + /> + ) +} + +const VIEW_OFFSETS_BUTTON_STYLE = css` + ${TYPOGRAPHY.pSemiBold}; + color: ${COLORS.black90}; + font-size: ${TYPOGRAPHY.fontSize22}; + &:hover { + opacity: 100%; + } + &:active { + opacity: 70%; + } +` +interface ViewOffsetsProps { + existingOffsets: LabwareOffset[] + labwareDefinitions: LabwareDefinition2[] +} +function ViewOffsets(props: ViewOffsetsProps): JSX.Element { + const { existingOffsets, labwareDefinitions } = props + const { t, i18n } = useTranslation('labware_position_check') + const [showOffsetsTable, setShowOffsetsModal] = useState(false) + const latestCurrentOffsets = getLatestCurrentOffsets(existingOffsets) + return existingOffsets.length > 0 ? ( + <> + { + setShowOffsetsModal(true) + }} + css={VIEW_OFFSETS_BUTTON_STYLE} + aria-label="show labware offsets" + > + + + {i18n.format(t('view_current_offsets'), 'capitalize')} + + + {showOffsetsTable + ? createPortal( + + {i18n.format(t('labware_offset_data'), 'capitalize')} + + } + footer={ + { + setShowOffsetsModal(false) + }} + /> + } + > + + + + , + getTopPortalEl() + ) + : null} + + ) : ( + + ) +} diff --git a/app/src/organisms/LabwarePositionCheck/JogToWell.tsx b/app/src/organisms/LegacyLabwarePositionCheck/JogToWell.tsx similarity index 96% rename from app/src/organisms/LabwarePositionCheck/JogToWell.tsx rename to app/src/organisms/LegacyLabwarePositionCheck/JogToWell.tsx index e212af695a5..bce4808a514 100644 --- a/app/src/organisms/LabwarePositionCheck/JogToWell.tsx +++ b/app/src/organisms/LegacyLabwarePositionCheck/JogToWell.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useEffect, useState } from 'react' import { createPortal } from 'react-dom' import { useTranslation } from 'react-i18next' import { useSelector } from 'react-redux' @@ -11,14 +11,14 @@ import { Flex, JUSTIFY_SPACE_BETWEEN, LabwareRender, + LegacyStyledText, + ModalShell, PipetteRender, PrimaryButton, RESPONSIVENESS, RobotWorkSpace, SecondaryButton, SPACING, - LegacyStyledText, - ModalShell, TYPOGRAPHY, WELL_LABEL_OPTIONS, } from '@opentrons/components' @@ -40,6 +40,7 @@ import { NeedHelpLink } from '/app/molecules/OT2CalibrationNeedHelpLink' import { JogControls } from '/app/molecules/JogControls' import { LiveOffsetValue } from './LiveOffsetValue' +import type { ReactNode } from 'react' import type { PipetteName, LabwareDefinition2 } from '@opentrons/shared-data' import type { WellStroke } from '@opentrons/components' import type { VectorOffset } from '@opentrons/api-client' @@ -55,8 +56,8 @@ interface JogToWellProps { handleJog: Jog pipetteName: PipetteName labwareDef: LabwareDefinition2 - header: React.ReactNode - body: React.ReactNode + header: ReactNode + body: ReactNode initialPosition: VectorOffset existingOffset: VectorOffset shouldUseMetalProbe: boolean @@ -76,12 +77,12 @@ export const JogToWell = (props: JogToWellProps): JSX.Element | null => { shouldUseMetalProbe, } = props - const [joggedPosition, setJoggedPosition] = React.useState( + const [joggedPosition, setJoggedPosition] = useState( initialPosition ) const isOnDevice = useSelector(getIsOnDevice) - const [showFullJogControls, setShowFullJogControls] = React.useState(false) - React.useEffect(() => { + const [showFullJogControls, setShowFullJogControls] = useState(false) + useEffect(() => { // NOTE: this will perform a "null" jog when the jog controls mount so // if a user reaches the "confirm exit" modal (unmounting this component) // and clicks "go back" we are able so initialize the live offset to whatever diff --git a/app/src/organisms/LabwarePositionCheck/LabwarePositionCheckComponent.tsx b/app/src/organisms/LegacyLabwarePositionCheck/LabwarePositionCheckComponent.tsx similarity index 100% rename from app/src/organisms/LabwarePositionCheck/LabwarePositionCheckComponent.tsx rename to app/src/organisms/LegacyLabwarePositionCheck/LabwarePositionCheckComponent.tsx diff --git a/app/src/organisms/LabwarePositionCheck/LiveOffsetValue.tsx b/app/src/organisms/LegacyLabwarePositionCheck/LiveOffsetValue.tsx similarity index 100% rename from app/src/organisms/LabwarePositionCheck/LiveOffsetValue.tsx rename to app/src/organisms/LegacyLabwarePositionCheck/LiveOffsetValue.tsx diff --git a/app/src/organisms/LabwarePositionCheck/PickUpTip.tsx b/app/src/organisms/LegacyLabwarePositionCheck/PickUpTip.tsx similarity index 98% rename from app/src/organisms/LabwarePositionCheck/PickUpTip.tsx rename to app/src/organisms/LegacyLabwarePositionCheck/PickUpTip.tsx index c7505a13d92..de76e855097 100644 --- a/app/src/organisms/LabwarePositionCheck/PickUpTip.tsx +++ b/app/src/organisms/LegacyLabwarePositionCheck/PickUpTip.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useState } from 'react' import { Trans, useTranslation } from 'react-i18next' import isEqual from 'lodash/isEqual' import { @@ -27,6 +27,7 @@ import { getDisplayLocation } from './utils/getDisplayLocation' import { useSelector } from 'react-redux' import { getIsOnDevice } from '/app/redux/config' +import type { Dispatch } from 'react' import type { CompletedProtocolAnalysis, CreateCommand, @@ -46,7 +47,7 @@ import type { TFunction } from 'i18next' interface PickUpTipProps extends PickUpTipStep { protocolData: CompletedProtocolAnalysis proceed: () => void - registerPosition: React.Dispatch + registerPosition: Dispatch chainRunCommands: ReturnType['chainRunCommands'] setFatalError: (errorMessage: string) => void workingOffsets: WorkingOffset[] @@ -77,7 +78,7 @@ export const PickUpTip = (props: PickUpTipProps): JSX.Element | null => { protocolHasModules, currentStepIndex, } = props - const [showTipConfirmation, setShowTipConfirmation] = React.useState(false) + const [showTipConfirmation, setShowTipConfirmation] = useState(false) const isOnDevice = useSelector(getIsOnDevice) const labwareDef = getLabwareDef(labwareId, protocolData) const pipette = protocolData.pipettes.find(p => p.id === pipetteId) diff --git a/app/src/organisms/LabwarePositionCheck/PrepareSpace.tsx b/app/src/organisms/LegacyLabwarePositionCheck/PrepareSpace.tsx similarity index 97% rename from app/src/organisms/LabwarePositionCheck/PrepareSpace.tsx rename to app/src/organisms/LegacyLabwarePositionCheck/PrepareSpace.tsx index 8820acfef33..f77973cbc1f 100644 --- a/app/src/organisms/LabwarePositionCheck/PrepareSpace.tsx +++ b/app/src/organisms/LegacyLabwarePositionCheck/PrepareSpace.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import styled, { css } from 'styled-components' import { useTranslation } from 'react-i18next' import { useSelector } from 'react-redux' @@ -23,6 +22,7 @@ import { SmallButton } from '/app/atoms/buttons' import { NeedHelpLink } from '/app/molecules/OT2CalibrationNeedHelpLink' import { useNotifyDeckConfigurationQuery } from '/app/resources/deck_configuration' +import type { ReactNode } from 'react' import type { CompletedProtocolAnalysis, LabwareDefinition2, @@ -61,8 +61,8 @@ interface PrepareSpaceProps extends Omit { labwareDef: LabwareDefinition2 protocolData: CompletedProtocolAnalysis confirmPlacement: () => void - header: React.ReactNode - body: React.ReactNode + header: ReactNode + body: ReactNode robotType: RobotType } export const PrepareSpace = (props: PrepareSpaceProps): JSX.Element | null => { diff --git a/app/src/organisms/LabwarePositionCheck/ResultsSummary.tsx b/app/src/organisms/LegacyLabwarePositionCheck/ResultsSummary.tsx similarity index 100% rename from app/src/organisms/LabwarePositionCheck/ResultsSummary.tsx rename to app/src/organisms/LegacyLabwarePositionCheck/ResultsSummary.tsx diff --git a/app/src/organisms/LabwarePositionCheck/ReturnTip.tsx b/app/src/organisms/LegacyLabwarePositionCheck/ReturnTip.tsx similarity index 100% rename from app/src/organisms/LabwarePositionCheck/ReturnTip.tsx rename to app/src/organisms/LegacyLabwarePositionCheck/ReturnTip.tsx diff --git a/app/src/organisms/LabwarePositionCheck/RobotMotionLoader.tsx b/app/src/organisms/LegacyLabwarePositionCheck/RobotMotionLoader.tsx similarity index 100% rename from app/src/organisms/LabwarePositionCheck/RobotMotionLoader.tsx rename to app/src/organisms/LegacyLabwarePositionCheck/RobotMotionLoader.tsx diff --git a/app/src/organisms/LabwarePositionCheck/TerseOffsetTable.stories.tsx b/app/src/organisms/LegacyLabwarePositionCheck/TerseOffsetTable.stories.tsx similarity index 100% rename from app/src/organisms/LabwarePositionCheck/TerseOffsetTable.stories.tsx rename to app/src/organisms/LegacyLabwarePositionCheck/TerseOffsetTable.stories.tsx diff --git a/app/src/organisms/LabwarePositionCheck/TipConfirmation.tsx b/app/src/organisms/LegacyLabwarePositionCheck/TipConfirmation.tsx similarity index 100% rename from app/src/organisms/LabwarePositionCheck/TipConfirmation.tsx rename to app/src/organisms/LegacyLabwarePositionCheck/TipConfirmation.tsx diff --git a/app/src/organisms/LabwarePositionCheck/TwoUpTileLayout.tsx b/app/src/organisms/LegacyLabwarePositionCheck/TwoUpTileLayout.tsx similarity index 93% rename from app/src/organisms/LabwarePositionCheck/TwoUpTileLayout.tsx rename to app/src/organisms/LegacyLabwarePositionCheck/TwoUpTileLayout.tsx index 7c6cd309bb4..396d49d5150 100644 --- a/app/src/organisms/LabwarePositionCheck/TwoUpTileLayout.tsx +++ b/app/src/organisms/LegacyLabwarePositionCheck/TwoUpTileLayout.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import styled, { css } from 'styled-components' import { DIRECTION_COLUMN, @@ -12,6 +11,8 @@ import { DISPLAY_INLINE_BLOCK, } from '@opentrons/components' +import type { ReactNode } from 'react' + const Title = styled.h1` ${TYPOGRAPHY.h1Default}; margin-bottom: ${SPACING.spacing8}; @@ -36,11 +37,11 @@ export interface TwoUpTileLayoutProps { /** main header text on left half */ title: string /** paragraph text below title on left half */ - body: React.ReactNode + body: ReactNode /** entire contents of the right half */ - rightElement: React.ReactNode + rightElement: ReactNode /** footer underneath both halves of content */ - footer: React.ReactNode + footer: ReactNode } export function TwoUpTileLayout(props: TwoUpTileLayoutProps): JSX.Element { diff --git a/app/src/organisms/LabwarePositionCheck/__fixtures__/index.ts b/app/src/organisms/LegacyLabwarePositionCheck/__fixtures__/index.ts similarity index 100% rename from app/src/organisms/LabwarePositionCheck/__fixtures__/index.ts rename to app/src/organisms/LegacyLabwarePositionCheck/__fixtures__/index.ts diff --git a/app/src/organisms/LabwarePositionCheck/__fixtures__/mockCompletedAnalysis.ts b/app/src/organisms/LegacyLabwarePositionCheck/__fixtures__/mockCompletedAnalysis.ts similarity index 100% rename from app/src/organisms/LabwarePositionCheck/__fixtures__/mockCompletedAnalysis.ts rename to app/src/organisms/LegacyLabwarePositionCheck/__fixtures__/mockCompletedAnalysis.ts diff --git a/app/src/organisms/LabwarePositionCheck/__fixtures__/mockExistingOffsets.ts b/app/src/organisms/LegacyLabwarePositionCheck/__fixtures__/mockExistingOffsets.ts similarity index 100% rename from app/src/organisms/LabwarePositionCheck/__fixtures__/mockExistingOffsets.ts rename to app/src/organisms/LegacyLabwarePositionCheck/__fixtures__/mockExistingOffsets.ts diff --git a/app/src/organisms/LabwarePositionCheck/__fixtures__/mockLabwareDef.ts b/app/src/organisms/LegacyLabwarePositionCheck/__fixtures__/mockLabwareDef.ts similarity index 100% rename from app/src/organisms/LabwarePositionCheck/__fixtures__/mockLabwareDef.ts rename to app/src/organisms/LegacyLabwarePositionCheck/__fixtures__/mockLabwareDef.ts diff --git a/app/src/organisms/LabwarePositionCheck/__fixtures__/mockTipRackDef.ts b/app/src/organisms/LegacyLabwarePositionCheck/__fixtures__/mockTipRackDef.ts similarity index 100% rename from app/src/organisms/LabwarePositionCheck/__fixtures__/mockTipRackDef.ts rename to app/src/organisms/LegacyLabwarePositionCheck/__fixtures__/mockTipRackDef.ts diff --git a/app/src/organisms/LabwarePositionCheck/__fixtures__/mockWorkingOffsets.ts b/app/src/organisms/LegacyLabwarePositionCheck/__fixtures__/mockWorkingOffsets.ts similarity index 100% rename from app/src/organisms/LabwarePositionCheck/__fixtures__/mockWorkingOffsets.ts rename to app/src/organisms/LegacyLabwarePositionCheck/__fixtures__/mockWorkingOffsets.ts diff --git a/app/src/organisms/LabwarePositionCheck/__tests__/CheckItem.test.tsx b/app/src/organisms/LegacyLabwarePositionCheck/__tests__/CheckItem.test.tsx similarity index 99% rename from app/src/organisms/LabwarePositionCheck/__tests__/CheckItem.test.tsx rename to app/src/organisms/LegacyLabwarePositionCheck/__tests__/CheckItem.test.tsx index 17442dfc42b..5c2907c5276 100644 --- a/app/src/organisms/LabwarePositionCheck/__tests__/CheckItem.test.tsx +++ b/app/src/organisms/LegacyLabwarePositionCheck/__tests__/CheckItem.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { fireEvent, screen } from '@testing-library/react' import { describe, it, vi, beforeEach, afterEach, expect } from 'vitest' @@ -15,6 +14,7 @@ import { CheckItem } from '../CheckItem' import { SECTIONS } from '../constants' import { mockCompletedAnalysis, mockExistingOffsets } from '../__fixtures__' +import type { ComponentProps } from 'react' import type { Mock } from 'vitest' vi.mock('/app/redux/config') @@ -23,14 +23,14 @@ vi.mock('../../Desktop/Devices/hooks') const mockStartPosition = { x: 10, y: 20, z: 30 } const mockEndPosition = { x: 9, y: 19, z: 29 } -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, })[0] } describe('CheckItem', () => { - let props: React.ComponentProps + let props: ComponentProps let mockChainRunCommands: Mock beforeEach(() => { diff --git a/app/src/organisms/LabwarePositionCheck/__tests__/ExitConfirmation.test.tsx b/app/src/organisms/LegacyLabwarePositionCheck/__tests__/ExitConfirmation.test.tsx similarity index 91% rename from app/src/organisms/LabwarePositionCheck/__tests__/ExitConfirmation.test.tsx rename to app/src/organisms/LegacyLabwarePositionCheck/__tests__/ExitConfirmation.test.tsx index 6a93da71dc5..e710c991ccb 100644 --- a/app/src/organisms/LabwarePositionCheck/__tests__/ExitConfirmation.test.tsx +++ b/app/src/organisms/LegacyLabwarePositionCheck/__tests__/ExitConfirmation.test.tsx @@ -1,18 +1,19 @@ -import type * as React from 'react' import { fireEvent, screen } from '@testing-library/react' import { describe, it, beforeEach, afterEach, expect, vi } from 'vitest' import { ExitConfirmation } from '../ExitConfirmation' import { renderWithProviders } from '/app/__testing-utils__' import { i18n } from '/app/i18n' -const render = (props: React.ComponentProps) => { +import type { ComponentProps } from 'react' + +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, })[0] } describe('ExitConfirmation', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { diff --git a/app/src/organisms/LabwarePositionCheck/__tests__/PickUpTip.test.tsx b/app/src/organisms/LegacyLabwarePositionCheck/__tests__/PickUpTip.test.tsx similarity index 98% rename from app/src/organisms/LabwarePositionCheck/__tests__/PickUpTip.test.tsx rename to app/src/organisms/LegacyLabwarePositionCheck/__tests__/PickUpTip.test.tsx index c23e1c1af2c..286e6f5f095 100644 --- a/app/src/organisms/LabwarePositionCheck/__tests__/PickUpTip.test.tsx +++ b/app/src/organisms/LegacyLabwarePositionCheck/__tests__/PickUpTip.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { fireEvent, screen, waitFor } from '@testing-library/react' import { it, describe, beforeEach, vi, afterEach, expect } from 'vitest' import { FLEX_ROBOT_TYPE, HEATERSHAKER_MODULE_V1 } from '@opentrons/shared-data' @@ -8,23 +7,25 @@ import { getIsOnDevice } from '/app/redux/config' import { PickUpTip } from '../PickUpTip' import { SECTIONS } from '../constants' import { mockCompletedAnalysis, mockExistingOffsets } from '../__fixtures__' -import type { CommandData } from '@opentrons/api-client' import { nestedTextMatcher, renderWithProviders } from '/app/__testing-utils__' + +import type { ComponentProps } from 'react' import type { Mock } from 'vitest' +import type { CommandData } from '@opentrons/api-client' vi.mock('/app/resources/protocols') vi.mock('/app/redux/config') const mockStartPosition = { x: 10, y: 20, z: 30 } -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, })[0] } describe('PickUpTip', () => { - let props: React.ComponentProps + let props: ComponentProps let mockChainRunCommands: Mock beforeEach(() => { diff --git a/app/src/organisms/LabwarePositionCheck/__tests__/ResultsSummary.test.tsx b/app/src/organisms/LegacyLabwarePositionCheck/__tests__/ResultsSummary.test.tsx similarity index 95% rename from app/src/organisms/LabwarePositionCheck/__tests__/ResultsSummary.test.tsx rename to app/src/organisms/LegacyLabwarePositionCheck/__tests__/ResultsSummary.test.tsx index 24101904de4..30a29496aaf 100644 --- a/app/src/organisms/LabwarePositionCheck/__tests__/ResultsSummary.test.tsx +++ b/app/src/organisms/LegacyLabwarePositionCheck/__tests__/ResultsSummary.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { describe, it, beforeEach, afterEach, expect, vi } from 'vitest' import { fireEvent, screen } from '@testing-library/react' import { i18n } from '/app/i18n' @@ -13,16 +12,18 @@ import { mockWorkingOffsets, } from '../__fixtures__' +import type { ComponentProps } from 'react' + vi.mock('/app/redux/config') -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, })[0] } describe('ResultsSummary', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { diff --git a/app/src/organisms/LabwarePositionCheck/__tests__/ReturnTip.test.tsx b/app/src/organisms/LegacyLabwarePositionCheck/__tests__/ReturnTip.test.tsx similarity index 98% rename from app/src/organisms/LabwarePositionCheck/__tests__/ReturnTip.test.tsx rename to app/src/organisms/LegacyLabwarePositionCheck/__tests__/ReturnTip.test.tsx index 0af86097f9c..112be630a31 100644 --- a/app/src/organisms/LabwarePositionCheck/__tests__/ReturnTip.test.tsx +++ b/app/src/organisms/LegacyLabwarePositionCheck/__tests__/ReturnTip.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { fireEvent, screen } from '@testing-library/react' import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' @@ -12,17 +11,19 @@ import { useProtocolMetadata } from '/app/resources/protocols' import { getIsOnDevice } from '/app/redux/config' import { ReturnTip } from '../ReturnTip' +import type { ComponentProps } from 'react' + vi.mock('/app/redux/config') vi.mock('/app/resources/protocols') -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, })[0] } describe('ReturnTip', () => { - let props: React.ComponentProps + let props: ComponentProps let mockChainRunCommands beforeEach(() => { diff --git a/app/src/organisms/LabwarePositionCheck/__tests__/RobotMotionLoader.test.tsx b/app/src/organisms/LegacyLabwarePositionCheck/__tests__/RobotMotionLoader.test.tsx similarity index 100% rename from app/src/organisms/LabwarePositionCheck/__tests__/RobotMotionLoader.test.tsx rename to app/src/organisms/LegacyLabwarePositionCheck/__tests__/RobotMotionLoader.test.tsx diff --git a/app/src/organisms/LabwarePositionCheck/__tests__/TipConfirmation.test.tsx b/app/src/organisms/LegacyLabwarePositionCheck/__tests__/TipConfirmation.test.tsx similarity index 87% rename from app/src/organisms/LabwarePositionCheck/__tests__/TipConfirmation.test.tsx rename to app/src/organisms/LegacyLabwarePositionCheck/__tests__/TipConfirmation.test.tsx index 8f8878a7122..a262641fd84 100644 --- a/app/src/organisms/LabwarePositionCheck/__tests__/TipConfirmation.test.tsx +++ b/app/src/organisms/LegacyLabwarePositionCheck/__tests__/TipConfirmation.test.tsx @@ -1,18 +1,19 @@ -import type * as React from 'react' import { describe, it, beforeEach, afterEach, expect, vi } from 'vitest' import { fireEvent, screen } from '@testing-library/react' import { TipConfirmation } from '../TipConfirmation' import { i18n } from '/app/i18n' import { renderWithProviders } from '/app/__testing-utils__' -const render = (props: React.ComponentProps) => { +import type { ComponentProps } from 'react' + +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, })[0] } describe('TipConfirmation', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { diff --git a/app/src/organisms/LabwarePositionCheck/__tests__/useLaunchLPC.test.tsx b/app/src/organisms/LegacyLabwarePositionCheck/__tests__/useLaunchLPC.test.tsx similarity index 86% rename from app/src/organisms/LabwarePositionCheck/__tests__/useLaunchLPC.test.tsx rename to app/src/organisms/LegacyLabwarePositionCheck/__tests__/useLaunchLPC.test.tsx index fb983097d01..83613b0d778 100644 --- a/app/src/organisms/LabwarePositionCheck/__tests__/useLaunchLPC.test.tsx +++ b/app/src/organisms/LegacyLabwarePositionCheck/__tests__/useLaunchLPC.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { Provider } from 'react-redux' import configureStore from 'redux-mock-store' import { when } from 'vitest-when' @@ -24,9 +23,10 @@ import { useNotifyRunQuery, useMostRecentCompletedAnalysis, } from '/app/resources/runs' -import { useLaunchLPC } from '../useLaunchLPC' -import { LabwarePositionCheck } from '..' +import { useLaunchLegacyLPC } from '../useLaunchLegacyLPC' +import { LegacyLabwarePositionCheck } from '..' +import type { FunctionComponent, ReactNode } from 'react' import type { Mock } from 'vitest' import type { LabwareOffset } from '@opentrons/api-client' import type { LabwareDefinition2 } from '@opentrons/shared-data' @@ -57,7 +57,7 @@ const mockCurrentOffsets: LabwareOffset[] = [ const mockLabwareDef = fixtureTiprack300ul as LabwareDefinition2 describe('useLaunchLPC hook', () => { - let wrapper: React.FunctionComponent<{ children: React.ReactNode }> + let wrapper: FunctionComponent<{ children: ReactNode }> let mockCreateMaintenanceRun: Mock let mockCreateLabwareDefinition: Mock let mockDeleteMaintenanceRun: Mock @@ -84,15 +84,17 @@ describe('useLaunchLPC hook', () => { ) - vi.mocked(LabwarePositionCheck).mockImplementation(({ onCloseClick }) => ( -
        { - onCloseClick() - }} - > - exit -
        - )) + vi.mocked(LegacyLabwarePositionCheck).mockImplementation( + ({ onCloseClick }) => ( +
        { + onCloseClick() + }} + > + exit +
        + ) + ) when(vi.mocked(useNotifyRunQuery)) .calledWith(MOCK_RUN_ID, { staleTime: Infinity }) .thenReturn({ @@ -150,19 +152,19 @@ describe('useLaunchLPC hook', () => { it('returns and no wizard by default', () => { const { result } = renderHook( - () => useLaunchLPC(MOCK_RUN_ID, FLEX_ROBOT_TYPE), + () => useLaunchLegacyLPC(MOCK_RUN_ID, FLEX_ROBOT_TYPE), { wrapper } ) - expect(result.current.LPCWizard).toEqual(null) + expect(result.current.LegacyLPCWizard).toEqual(null) }) it('returns creates maintenance run with current offsets and definitions when create callback is called, closes and deletes when exit is clicked', async () => { const { result } = renderHook( - () => useLaunchLPC(MOCK_RUN_ID, FLEX_ROBOT_TYPE), + () => useLaunchLegacyLPC(MOCK_RUN_ID, FLEX_ROBOT_TYPE), { wrapper } ) act(() => { - result.current.launchLPC() + result.current.launchLegacyLPC() }) await waitFor(() => { expect(mockCreateLabwareDefinition).toHaveBeenCalledWith({ @@ -184,9 +186,9 @@ describe('useLaunchLPC hook', () => { }) await waitFor(() => { - expect(result.current.LPCWizard).not.toBeNull() + expect(result.current.LegacyLPCWizard).not.toBeNull() }) - renderWithProviders(result.current.LPCWizard ?? <>) + renderWithProviders(result.current.LegacyLPCWizard ?? <>) fireEvent.click(screen.getByText('exit')) expect(mockDeleteMaintenanceRun).toHaveBeenCalledWith( MOCK_MAINTENANCE_RUN_ID, @@ -194,6 +196,6 @@ describe('useLaunchLPC hook', () => { onSettled: expect.any(Function), } ) - expect(result.current.LPCWizard).toBeNull() + expect(result.current.LegacyLPCWizard).toBeNull() }) }) diff --git a/app/src/organisms/LabwarePositionCheck/constants.ts b/app/src/organisms/LegacyLabwarePositionCheck/constants.ts similarity index 100% rename from app/src/organisms/LabwarePositionCheck/constants.ts rename to app/src/organisms/LegacyLabwarePositionCheck/constants.ts diff --git a/app/src/organisms/LabwarePositionCheck/getLabwarePositionCheckSteps.ts b/app/src/organisms/LegacyLabwarePositionCheck/getLabwarePositionCheckSteps.ts similarity index 100% rename from app/src/organisms/LabwarePositionCheck/getLabwarePositionCheckSteps.ts rename to app/src/organisms/LegacyLabwarePositionCheck/getLabwarePositionCheckSteps.ts diff --git a/app/src/organisms/LegacyLabwarePositionCheck/index.tsx b/app/src/organisms/LegacyLabwarePositionCheck/index.tsx new file mode 100644 index 00000000000..f09721a9ac2 --- /dev/null +++ b/app/src/organisms/LegacyLabwarePositionCheck/index.tsx @@ -0,0 +1,104 @@ +import { Component } from 'react' +import { useLogger } from '../../logger' +import { LabwarePositionCheckComponent } from './LabwarePositionCheckComponent' +import { FatalErrorModal } from './FatalErrorModal' +import { getIsOnDevice } from '/app/redux/config' +import { useSelector } from 'react-redux' + +import { FLEX_ROBOT_TYPE } from '@opentrons/shared-data' + +import type { ErrorInfo, ReactNode } from 'react' +import type { + CompletedProtocolAnalysis, + RobotType, +} from '@opentrons/shared-data' +import type { LabwareOffset } from '@opentrons/api-client' + +interface LabwarePositionCheckModalProps { + onCloseClick: () => void + runId: string + maintenanceRunId: string + robotType: RobotType + existingOffsets: LabwareOffset[] + mostRecentAnalysis: CompletedProtocolAnalysis | null + protocolName: string + caughtError?: Error + setMaintenanceRunId: (id: string | null) => void + isDeletingMaintenanceRun: boolean +} + +// We explicitly wrap LabwarePositionCheckComponent in an ErrorBoundary because an error might occur while pulling in +// the component's dependencies (like useLabwarePositionCheck). If we wrapped the contents of LabwarePositionCheckComponent +// in an ErrorBoundary as part of its return value (render), an error could occur before this point, meaning the error boundary +// would never get invoked +export const LegacyLabwarePositionCheck = ( + props: LabwarePositionCheckModalProps +): JSX.Element => { + const logger = useLogger(new URL('', import.meta.url).pathname) + const isOnDevice = useSelector(getIsOnDevice) + return ( + + + + ) +} + +interface ErrorBoundaryProps { + children: ReactNode + onClose: () => void + shouldUseMetalProbe: boolean + logger: ReturnType + ErrorComponent: (props: { + errorMessage: string + shouldUseMetalProbe: boolean + onClose: () => void + isOnDevice: boolean + }) => JSX.Element + isOnDevice: boolean +} +class ErrorBoundary extends Component< + ErrorBoundaryProps, + { error: Error | null } +> { + constructor(props: ErrorBoundaryProps) { + super(props) + this.state = { error: null } + } + + componentDidCatch(error: Error, errorInfo: ErrorInfo): void { + this.props.logger.error(`LPC error message: ${error.message}`) + this.props.logger.error( + `LPC error component stack: ${errorInfo.componentStack}` + ) + this.setState({ + error, + }) + } + + render(): ErrorBoundaryProps['children'] | JSX.Element { + const { + ErrorComponent, + children, + shouldUseMetalProbe, + isOnDevice, + } = this.props + const { error } = this.state + if (error != null) + return ( + + ) + // Normally, just render children + return children + } +} diff --git a/app/src/organisms/LabwarePositionCheck/types.ts b/app/src/organisms/LegacyLabwarePositionCheck/types.ts similarity index 100% rename from app/src/organisms/LabwarePositionCheck/types.ts rename to app/src/organisms/LegacyLabwarePositionCheck/types.ts diff --git a/app/src/organisms/LegacyLabwarePositionCheck/useLaunchLegacyLPC.tsx b/app/src/organisms/LegacyLabwarePositionCheck/useLaunchLegacyLPC.tsx new file mode 100644 index 00000000000..e029ebb114e --- /dev/null +++ b/app/src/organisms/LegacyLabwarePositionCheck/useLaunchLegacyLPC.tsx @@ -0,0 +1,89 @@ +import { useState } from 'react' + +import { + useCreateMaintenanceRunLabwareDefinitionMutation, + useDeleteMaintenanceRunMutation, +} from '@opentrons/react-api-client' + +import { + useCreateTargetedMaintenanceRunMutation, + useNotifyRunQuery, + useMostRecentCompletedAnalysis, +} from '/app/resources/runs' +import { LegacyLabwarePositionCheck } from '.' +import { getLabwareDefinitionsFromCommands } from '/app/local-resources/labware' + +import type { RobotType } from '@opentrons/shared-data' + +export function useLaunchLegacyLPC( + runId: string, + robotType: RobotType, + protocolName?: string +): { launchLegacyLPC: () => void; LegacyLPCWizard: JSX.Element | null } { + const { data: runRecord } = useNotifyRunQuery(runId, { staleTime: Infinity }) + const { + createTargetedMaintenanceRun, + } = useCreateTargetedMaintenanceRunMutation() + const { + deleteMaintenanceRun, + isLoading: isDeletingMaintenanceRun, + } = useDeleteMaintenanceRunMutation() + const mostRecentAnalysis = useMostRecentCompletedAnalysis(runId) + const [maintenanceRunId, setMaintenanceRunId] = useState(null) + const currentOffsets = runRecord?.data?.labwareOffsets ?? [] + const { + createLabwareDefinition, + } = useCreateMaintenanceRunLabwareDefinitionMutation() + + const handleCloseLPC = (): void => { + if (maintenanceRunId != null) { + deleteMaintenanceRun(maintenanceRunId, { + onSettled: () => { + setMaintenanceRunId(null) + }, + }) + } + } + return { + launchLegacyLPC: () => + createTargetedMaintenanceRun({ + labwareOffsets: currentOffsets.map( + ({ vector, location, definitionUri }) => ({ + vector, + location, + definitionUri, + }) + ), + }).then(maintenanceRun => + // TODO(BC, 2023-05-15): replace this with a call to the protocol run's GET labware_definitions + // endpoint once it's made we should be adding the definitions to the maintenance run by + // reading from the current protocol run, and not from the analysis + Promise.all( + getLabwareDefinitionsFromCommands( + mostRecentAnalysis?.commands ?? [] + ).map(def => { + createLabwareDefinition({ + maintenanceRunId: maintenanceRun?.data?.id, + labwareDef: def, + }) + }) + ).then(() => { + setMaintenanceRunId(maintenanceRun.data.id) + }) + ), + LegacyLPCWizard: + maintenanceRunId != null ? ( + + ) : null, + } +} diff --git a/app/src/organisms/LabwarePositionCheck/utils/__tests__/doesPipetteVisitAllTipracks.test.ts b/app/src/organisms/LegacyLabwarePositionCheck/utils/__tests__/doesPipetteVisitAllTipracks.test.ts similarity index 100% rename from app/src/organisms/LabwarePositionCheck/utils/__tests__/doesPipetteVisitAllTipracks.test.ts rename to app/src/organisms/LegacyLabwarePositionCheck/utils/__tests__/doesPipetteVisitAllTipracks.test.ts diff --git a/app/src/organisms/LabwarePositionCheck/utils/__tests__/getPrimaryPipetteId.test.ts b/app/src/organisms/LegacyLabwarePositionCheck/utils/__tests__/getPrimaryPipetteId.test.ts similarity index 100% rename from app/src/organisms/LabwarePositionCheck/utils/__tests__/getPrimaryPipetteId.test.ts rename to app/src/organisms/LegacyLabwarePositionCheck/utils/__tests__/getPrimaryPipetteId.test.ts diff --git a/app/src/organisms/LabwarePositionCheck/utils/doesPipetteVisitAllTipracks.ts b/app/src/organisms/LegacyLabwarePositionCheck/utils/doesPipetteVisitAllTipracks.ts similarity index 100% rename from app/src/organisms/LabwarePositionCheck/utils/doesPipetteVisitAllTipracks.ts rename to app/src/organisms/LegacyLabwarePositionCheck/utils/doesPipetteVisitAllTipracks.ts diff --git a/app/src/organisms/LabwarePositionCheck/utils/getDisplayLocation.ts b/app/src/organisms/LegacyLabwarePositionCheck/utils/getDisplayLocation.ts similarity index 100% rename from app/src/organisms/LabwarePositionCheck/utils/getDisplayLocation.ts rename to app/src/organisms/LegacyLabwarePositionCheck/utils/getDisplayLocation.ts diff --git a/app/src/organisms/LabwarePositionCheck/utils/getPrimaryPipetteId.ts b/app/src/organisms/LegacyLabwarePositionCheck/utils/getPrimaryPipetteId.ts similarity index 100% rename from app/src/organisms/LabwarePositionCheck/utils/getPrimaryPipetteId.ts rename to app/src/organisms/LegacyLabwarePositionCheck/utils/getPrimaryPipetteId.ts diff --git a/app/src/organisms/LabwarePositionCheck/utils/getProbeBasedLPCSteps.ts b/app/src/organisms/LegacyLabwarePositionCheck/utils/getProbeBasedLPCSteps.ts similarity index 92% rename from app/src/organisms/LabwarePositionCheck/utils/getProbeBasedLPCSteps.ts rename to app/src/organisms/LegacyLabwarePositionCheck/utils/getProbeBasedLPCSteps.ts index f5e4ed86f0b..d584399457d 100644 --- a/app/src/organisms/LabwarePositionCheck/utils/getProbeBasedLPCSteps.ts +++ b/app/src/organisms/LegacyLabwarePositionCheck/utils/getProbeBasedLPCSteps.ts @@ -1,7 +1,7 @@ import { isEqual } from 'lodash' import { SECTIONS } from '../constants' import { getLabwareDefURI, getPipetteNameSpecs } from '@opentrons/shared-data' -import { getLabwareLocationCombos } from '../../ApplyHistoricOffsets/hooks/getLabwareLocationCombos' +import { getLabwareLocationCombos } from '/app/organisms/LegacyApplyHistoricOffsets/hooks/getLabwareLocationCombos' import { getLabwareDefinitionsFromCommands } from '/app/local-resources/labware' import type { @@ -9,7 +9,7 @@ import type { LoadedPipette, } from '@opentrons/shared-data' import type { LabwarePositionCheckStep, CheckPositionsStep } from '../types' -import type { LabwareLocationCombo } from '../../ApplyHistoricOffsets/hooks/getLabwareLocationCombos' +import type { LabwareLocationCombo } from '/app/organisms/LegacyApplyHistoricOffsets/hooks/getLabwareLocationCombos' function getPrimaryPipetteId(pipettes: LoadedPipette[]): string { if (pipettes.length < 1) { diff --git a/app/src/organisms/LabwarePositionCheck/utils/getTipBasedLPCSteps.ts b/app/src/organisms/LegacyLabwarePositionCheck/utils/getTipBasedLPCSteps.ts similarity index 96% rename from app/src/organisms/LabwarePositionCheck/utils/getTipBasedLPCSteps.ts rename to app/src/organisms/LegacyLabwarePositionCheck/utils/getTipBasedLPCSteps.ts index 47c30424e95..2aba09b84f8 100644 --- a/app/src/organisms/LabwarePositionCheck/utils/getTipBasedLPCSteps.ts +++ b/app/src/organisms/LegacyLabwarePositionCheck/utils/getTipBasedLPCSteps.ts @@ -6,7 +6,7 @@ import { getIsTiprack, FIXED_TRASH_ID, } from '@opentrons/shared-data' -import { getLabwareLocationCombos } from '../../ApplyHistoricOffsets/hooks/getLabwareLocationCombos' +import { getLabwareLocationCombos } from '/app/organisms/LegacyApplyHistoricOffsets/hooks/getLabwareLocationCombos' import type { LabwarePositionCheckStep, @@ -20,7 +20,7 @@ import type { ProtocolAnalysisOutput, PickUpTipRunTimeCommand, } from '@opentrons/shared-data' -import type { LabwareLocationCombo } from '../../ApplyHistoricOffsets/hooks/getLabwareLocationCombos' +import type { LabwareLocationCombo } from '/app/organisms/LegacyApplyHistoricOffsets/hooks/getLabwareLocationCombos' interface LPCArgs { primaryPipetteId: string diff --git a/app/src/organisms/LabwarePositionCheck/utils/labware.ts b/app/src/organisms/LegacyLabwarePositionCheck/utils/labware.ts similarity index 100% rename from app/src/organisms/LabwarePositionCheck/utils/labware.ts rename to app/src/organisms/LegacyLabwarePositionCheck/utils/labware.ts diff --git a/app/src/organisms/LiquidsLabwareDetailsModal/LiquidDetailCard.tsx b/app/src/organisms/LiquidsLabwareDetailsModal/LiquidDetailCard.tsx index cb1591dc72f..7dc33069500 100644 --- a/app/src/organisms/LiquidsLabwareDetailsModal/LiquidDetailCard.tsx +++ b/app/src/organisms/LiquidsLabwareDetailsModal/LiquidDetailCard.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { useSelector } from 'react-redux' import { css } from 'styled-components' import { @@ -27,6 +26,8 @@ import { import { getIsOnDevice } from '/app/redux/config' import { getWellRangeForLiquidLabwarePair } from '/app/transformations/analysis' +import type { Dispatch, SetStateAction } from 'react' + export const CARD_OUTLINE_BORDER_STYLE = css` border-style: ${BORDERS.styleSolid}; border-width: 1px; @@ -56,7 +57,7 @@ interface LiquidDetailCardProps { description: string | null displayColor: string volumeByWell: { [well: string]: number } - setSelectedValue: React.Dispatch> + setSelectedValue: Dispatch> selectedValue: string | undefined labwareWellOrdering: string[][] } diff --git a/app/src/organisms/LiquidsLabwareDetailsModal/__tests__/LiquidDetailCard.test.tsx b/app/src/organisms/LiquidsLabwareDetailsModal/__tests__/LiquidDetailCard.test.tsx index a96c8128897..522fae501b1 100644 --- a/app/src/organisms/LiquidsLabwareDetailsModal/__tests__/LiquidDetailCard.test.tsx +++ b/app/src/organisms/LiquidsLabwareDetailsModal/__tests__/LiquidDetailCard.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { fireEvent, screen } from '@testing-library/react' import { describe, it, beforeEach, vi, expect } from 'vitest' @@ -12,12 +11,14 @@ import { } from '/app/redux/analytics' import { getIsOnDevice } from '/app/redux/config' import { LiquidDetailCard } from '../LiquidDetailCard' + +import type { ComponentProps } from 'react' import type { Mock } from 'vitest' vi.mock('/app/redux/analytics') vi.mock('/app/redux/config') -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, })[0] @@ -25,7 +26,7 @@ const render = (props: React.ComponentProps) => { let mockTrackEvent: Mock describe('LiquidDetailCard', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { mockTrackEvent = vi.fn() diff --git a/app/src/organisms/LiquidsLabwareDetailsModal/__tests__/LiquidsLabwareDetailsModal.test.tsx b/app/src/organisms/LiquidsLabwareDetailsModal/__tests__/LiquidsLabwareDetailsModal.test.tsx index 967a840ee75..e70756ed3f3 100644 --- a/app/src/organisms/LiquidsLabwareDetailsModal/__tests__/LiquidsLabwareDetailsModal.test.tsx +++ b/app/src/organisms/LiquidsLabwareDetailsModal/__tests__/LiquidsLabwareDetailsModal.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { describe, it, beforeEach, vi, afterEach, expect } from 'vitest' import { screen } from '@testing-library/react' @@ -21,6 +20,7 @@ import { import { LiquidsLabwareDetailsModal } from '../LiquidsLabwareDetailsModal' import { LiquidDetailCard } from '../LiquidDetailCard' +import type { ComponentProps } from 'react' import type * as Components from '@opentrons/components' import type * as SharedData from '@opentrons/shared-data' @@ -44,16 +44,14 @@ vi.mock('/app/transformations/commands') vi.mock('/app/transformations/analysis') vi.mock('../LiquidDetailCard') -const render = ( - props: React.ComponentProps -) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, }) } describe('LiquidsLabwareDetailsModal', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { window.HTMLElement.prototype.scrollIntoView = function () {} props = { diff --git a/app/src/organisms/LocationConflictModal/__tests__/LocationConflictModal.test.tsx b/app/src/organisms/LocationConflictModal/__tests__/LocationConflictModal.test.tsx index 207caa02a1b..1d72ae1b858 100644 --- a/app/src/organisms/LocationConflictModal/__tests__/LocationConflictModal.test.tsx +++ b/app/src/organisms/LocationConflictModal/__tests__/LocationConflictModal.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { MemoryRouter } from 'react-router-dom' import { screen, fireEvent } from '@testing-library/react' import '@testing-library/jest-dom/vitest' @@ -21,6 +20,7 @@ import { useCloseCurrentRun } from '/app/resources/runs' import { LocationConflictModal } from '../LocationConflictModal' import { useNotifyDeckConfigurationQuery } from '/app/resources/deck_configuration' +import type { ComponentProps } from 'react' import type { UseQueryResult } from 'react-query' import type { DeckConfiguration } from '@opentrons/shared-data' @@ -33,7 +33,7 @@ const mockFixture = { cutoutFixtureId: STAGING_AREA_RIGHT_SLOT_FIXTURE, } -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders( @@ -45,7 +45,7 @@ const render = (props: React.ComponentProps) => { } describe('LocationConflictModal', () => { - let props: React.ComponentProps + let props: ComponentProps const mockUpdate = vi.fn() beforeEach(() => { props = { diff --git a/app/src/organisms/ModuleCard/Collapsible.tsx b/app/src/organisms/ModuleCard/Collapsible.tsx index cc15a88d4a0..00f032b52ef 100644 --- a/app/src/organisms/ModuleCard/Collapsible.tsx +++ b/app/src/organisms/ModuleCard/Collapsible.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { css } from 'styled-components' import { ALIGN_CENTER, @@ -13,13 +12,15 @@ import { } from '@opentrons/components' import type { IconName } from '@opentrons/components' +import type { ReactNode } from 'react' + interface CollapsibleProps { expanded: boolean - title: React.ReactNode + title: ReactNode expandedIcon?: IconName collapsedIcon?: IconName toggleExpanded: () => void - children: React.ReactNode + children: ReactNode } const EXPANDED_STYLE = css` diff --git a/app/src/organisms/ModuleCard/ConfirmAttachmentModal.tsx b/app/src/organisms/ModuleCard/ConfirmAttachmentModal.tsx index 7031f176425..4a9cb9f3e4a 100644 --- a/app/src/organisms/ModuleCard/ConfirmAttachmentModal.tsx +++ b/app/src/organisms/ModuleCard/ConfirmAttachmentModal.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useState } from 'react' import { useTranslation } from 'react-i18next' import { useDispatch } from 'react-redux' import { @@ -17,6 +17,8 @@ import { TYPOGRAPHY, } from '@opentrons/components' import { updateConfigValue } from '/app/redux/config' + +import type { ChangeEvent } from 'react' import type { Dispatch } from '/app/redux/types' import type { UpdateConfigValueAction } from '/app/redux/config/types' @@ -38,7 +40,7 @@ export const ConfirmAttachmentModal = ( ): JSX.Element | null => { const { isProceedToRunModal, onCloseClick, onConfirmClick } = props const { t } = useTranslation(['heater_shaker', 'shared']) - const [isDismissed, setIsDismissed] = React.useState(false) + const [isDismissed, setIsDismissed] = useState(false) const dispatch = useDispatch() const confirmAttached = (): void => { @@ -81,7 +83,7 @@ export const ConfirmAttachmentModal = ( }`} > ) => { + onChange={(e: ChangeEvent) => { setIsDismissed(e.currentTarget.checked) }} value={isDismissed} diff --git a/app/src/organisms/ModuleCard/HeaterShakerSlideout.tsx b/app/src/organisms/ModuleCard/HeaterShakerSlideout.tsx index 1b189f3174d..bccf5fb3a30 100644 --- a/app/src/organisms/ModuleCard/HeaterShakerSlideout.tsx +++ b/app/src/organisms/ModuleCard/HeaterShakerSlideout.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useState } from 'react' import { useTranslation } from 'react-i18next' import { useCreateLiveCommandMutation } from '@opentrons/react-api-client' @@ -21,6 +21,7 @@ import { import { Slideout } from '/app/atoms/Slideout' import { SubmitPrimaryButton } from '/app/atoms/buttons' +import type { MouseEventHandler } from 'react' import type { HeaterShakerModule } from '/app/redux/modules/types' import type { HeaterShakerSetTargetTemperatureCreateCommand } from '@opentrons/shared-data' @@ -35,12 +36,12 @@ export const HeaterShakerSlideout = ( ): JSX.Element | null => { const { module, onCloseClick, isExpanded } = props const { t } = useTranslation('device_details') - const [hsValue, setHsValue] = React.useState(null) + const [hsValue, setHsValue] = useState(null) const { createLiveCommand } = useCreateLiveCommandMutation() const moduleName = getModuleDisplayName(module.moduleModel) const modulePart = t('temperature') - const sendSetTemperatureCommand: React.MouseEventHandler = e => { + const sendSetTemperatureCommand: MouseEventHandler = e => { e.preventDefault() e.stopPropagation() diff --git a/app/src/organisms/ModuleCard/__tests__/AboutModuleSlideout.test.tsx b/app/src/organisms/ModuleCard/__tests__/AboutModuleSlideout.test.tsx index 35eb81ab169..e397eae0b50 100644 --- a/app/src/organisms/ModuleCard/__tests__/AboutModuleSlideout.test.tsx +++ b/app/src/organisms/ModuleCard/__tests__/AboutModuleSlideout.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { fireEvent, screen } from '@testing-library/react' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' @@ -20,16 +19,18 @@ import { import { useCurrentRunStatus } from '/app/organisms/RunTimeControl' import { AboutModuleSlideout } from '../AboutModuleSlideout' +import type { ComponentProps } from 'react' + vi.mock('/app/organisms/RunTimeControl') -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, })[0] } describe('AboutModuleSlideout', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { module: mockMagneticModule, diff --git a/app/src/organisms/ModuleCard/__tests__/Collapsible.test.tsx b/app/src/organisms/ModuleCard/__tests__/Collapsible.test.tsx index 3db479e3228..9ec5f2d0e1b 100644 --- a/app/src/organisms/ModuleCard/__tests__/Collapsible.test.tsx +++ b/app/src/organisms/ModuleCard/__tests__/Collapsible.test.tsx @@ -1,16 +1,17 @@ -import type * as React from 'react' import { fireEvent, screen } from '@testing-library/react' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { renderWithProviders } from '/app/__testing-utils__' import { Collapsible } from '../Collapsible' -const render = (props: React.ComponentProps) => { +import type { ComponentProps } from 'react' + +const render = (props: ComponentProps) => { return renderWithProviders()[0] } describe('Collapsible', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { expanded: false, diff --git a/app/src/organisms/ModuleCard/__tests__/ConfirmAttachmentModal.test.tsx b/app/src/organisms/ModuleCard/__tests__/ConfirmAttachmentModal.test.tsx index ccc81bcb167..6c4d41973da 100644 --- a/app/src/organisms/ModuleCard/__tests__/ConfirmAttachmentModal.test.tsx +++ b/app/src/organisms/ModuleCard/__tests__/ConfirmAttachmentModal.test.tsx @@ -1,18 +1,19 @@ -import type * as React from 'react' import { fireEvent, screen } from '@testing-library/react' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { renderWithProviders } from '/app/__testing-utils__' import { i18n } from '/app/i18n' import { ConfirmAttachmentModal } from '../ConfirmAttachmentModal' -const render = (props: React.ComponentProps) => { +import type { ComponentProps } from 'react' + +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, })[0] } describe('ConfirmAttachmentBanner', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { diff --git a/app/src/organisms/ModuleCard/__tests__/ErrorInfo.test.tsx b/app/src/organisms/ModuleCard/__tests__/ErrorInfo.test.tsx index bde80a0d7d2..2a93208d89e 100644 --- a/app/src/organisms/ModuleCard/__tests__/ErrorInfo.test.tsx +++ b/app/src/organisms/ModuleCard/__tests__/ErrorInfo.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { fireEvent, screen } from '@testing-library/react' import { beforeEach, describe, expect, it } from 'vitest' import { renderWithProviders } from '/app/__testing-utils__' @@ -9,6 +8,8 @@ import { mockTemperatureModule, mockThermocycler, } from '/app/redux/modules/__fixtures__' + +import type { ComponentProps } from 'react' import type { HeaterShakerModule, ThermocyclerModule, @@ -71,14 +72,14 @@ const mockErrorHeaterShaker = { }, } as HeaterShakerModule -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, })[0] } describe('ErrorInfo', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { attachedModule: mockTemperatureModule, diff --git a/app/src/organisms/ModuleCard/__tests__/FirmwareUpdateFailedModal.test.tsx b/app/src/organisms/ModuleCard/__tests__/FirmwareUpdateFailedModal.test.tsx index 13395bcff69..f3eaa7dc0ed 100644 --- a/app/src/organisms/ModuleCard/__tests__/FirmwareUpdateFailedModal.test.tsx +++ b/app/src/organisms/ModuleCard/__tests__/FirmwareUpdateFailedModal.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { fireEvent, screen } from '@testing-library/react' import { beforeEach, describe, expect, it, vi } from 'vitest' import { renderWithProviders } from '/app/__testing-utils__' @@ -6,16 +5,16 @@ import { i18n } from '/app/i18n' import { mockTemperatureModule } from '/app/redux/modules/__fixtures__' import { FirmwareUpdateFailedModal } from '../FirmwareUpdateFailedModal' -const render = ( - props: React.ComponentProps -) => { +import type { ComponentProps } from 'react' + +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, })[0] } describe('FirmwareUpdateFailedModal', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { onCloseClick: vi.fn(), diff --git a/app/src/organisms/ModuleCard/__tests__/HeaterShakerModuleData.test.tsx b/app/src/organisms/ModuleCard/__tests__/HeaterShakerModuleData.test.tsx index 348fdb614d4..dab6f1fc7ad 100644 --- a/app/src/organisms/ModuleCard/__tests__/HeaterShakerModuleData.test.tsx +++ b/app/src/organisms/ModuleCard/__tests__/HeaterShakerModuleData.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { screen } from '@testing-library/react' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { renderWithProviders } from '/app/__testing-utils__' @@ -6,16 +5,18 @@ import { i18n } from '/app/i18n' import { StatusLabel } from '/app/atoms/StatusLabel' import { HeaterShakerModuleData } from '../HeaterShakerModuleData' +import type { ComponentProps } from 'react' + vi.mock('/app/atoms/StatusLabel') -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, })[0] } describe('HeaterShakerModuleData', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { moduleData: { diff --git a/app/src/organisms/ModuleCard/__tests__/HeaterShakerSlideout.test.tsx b/app/src/organisms/ModuleCard/__tests__/HeaterShakerSlideout.test.tsx index 883d5b6bb7c..b02e5205f42 100644 --- a/app/src/organisms/ModuleCard/__tests__/HeaterShakerSlideout.test.tsx +++ b/app/src/organisms/ModuleCard/__tests__/HeaterShakerSlideout.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { fireEvent, screen } from '@testing-library/react' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' @@ -9,16 +8,18 @@ import { i18n } from '/app/i18n' import { mockHeaterShaker } from '/app/redux/modules/__fixtures__' import { HeaterShakerSlideout } from '../HeaterShakerSlideout' +import type { ComponentProps } from 'react' + vi.mock('@opentrons/react-api-client') -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, })[0] } describe('HeaterShakerSlideout', () => { - let props: React.ComponentProps + let props: ComponentProps let mockCreateLiveCommand = vi.fn() beforeEach(() => { diff --git a/app/src/organisms/ModuleCard/__tests__/MagneticModuleData.test.tsx b/app/src/organisms/ModuleCard/__tests__/MagneticModuleData.test.tsx index b6534d233e3..0440b16b251 100644 --- a/app/src/organisms/ModuleCard/__tests__/MagneticModuleData.test.tsx +++ b/app/src/organisms/ModuleCard/__tests__/MagneticModuleData.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { screen } from '@testing-library/react' import { afterEach, beforeEach, describe, it, vi } from 'vitest' @@ -8,16 +7,18 @@ import { StatusLabel } from '/app/atoms/StatusLabel' import { MagneticModuleData } from '../MagneticModuleData' import { mockMagneticModule } from '/app/redux/modules/__fixtures__' +import type { ComponentProps } from 'react' + vi.mock('/app/atoms/StatusLabel') -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, })[0] } describe('MagneticModuleData', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { moduleHeight: mockMagneticModule.data.height, diff --git a/app/src/organisms/ModuleCard/__tests__/MagneticModuleSlideout.test.tsx b/app/src/organisms/ModuleCard/__tests__/MagneticModuleSlideout.test.tsx index fa10546af90..c52817fb517 100644 --- a/app/src/organisms/ModuleCard/__tests__/MagneticModuleSlideout.test.tsx +++ b/app/src/organisms/ModuleCard/__tests__/MagneticModuleSlideout.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { fireEvent, screen } from '@testing-library/react' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { COLORS } from '@opentrons/components' @@ -13,15 +12,17 @@ import { mockMagneticModuleGen2, } from '/app/redux/modules/__fixtures__' +import type { ComponentProps } from 'react' + vi.mock('@opentrons/react-api-client') -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, })[0] } describe('MagneticModuleSlideout', () => { - let props: React.ComponentProps + let props: ComponentProps let mockCreateLiveCommand = vi.fn() beforeEach(() => { mockCreateLiveCommand = vi.fn() diff --git a/app/src/organisms/ModuleCard/__tests__/ModuleCard.test.tsx b/app/src/organisms/ModuleCard/__tests__/ModuleCard.test.tsx index d30a885b759..078dfe12ada 100644 --- a/app/src/organisms/ModuleCard/__tests__/ModuleCard.test.tsx +++ b/app/src/organisms/ModuleCard/__tests__/ModuleCard.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { when } from 'vitest-when' import { fireEvent, screen } from '@testing-library/react' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' @@ -29,12 +28,13 @@ import { FirmwareUpdateFailedModal } from '../FirmwareUpdateFailedModal' import { ErrorInfo } from '../ErrorInfo' import { ModuleCard } from '..' +import type { ComponentProps } from 'react' +import type { Mock } from 'vitest' import type { HeaterShakerModule, MagneticModule, ThermocyclerModule, } from '/app/redux/modules/types' -import type { Mock } from 'vitest' vi.mock('../ErrorInfo') vi.mock('../MagneticModuleData') @@ -175,14 +175,14 @@ const mockEatToast = vi.fn() const MOCK_LATEST_REQUEST_ID = '1234' -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, })[0] } describe('ModuleCard', () => { - let props: React.ComponentProps + let props: ComponentProps let mockHandleModuleApiRequests: Mock beforeEach(() => { diff --git a/app/src/organisms/ModuleCard/__tests__/ModuleOverflowMenu.test.tsx b/app/src/organisms/ModuleCard/__tests__/ModuleOverflowMenu.test.tsx index 75701934e36..6e54dee83f9 100644 --- a/app/src/organisms/ModuleCard/__tests__/ModuleOverflowMenu.test.tsx +++ b/app/src/organisms/ModuleCard/__tests__/ModuleOverflowMenu.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { fireEvent, screen } from '@testing-library/react' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' @@ -16,13 +15,14 @@ import { useIsFlex } from '/app/redux-resources/robots' import { useCurrentRunId, useRunStatuses } from '/app/resources/runs' import { ModuleOverflowMenu } from '../ModuleOverflowMenu' +import type { ComponentProps } from 'react' import type { TemperatureStatus } from '@opentrons/api-client' vi.mock('/app/resources/legacy_sessions') vi.mock('/app/redux-resources/robots') vi.mock('/app/resources/runs') -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, })[0] @@ -161,7 +161,7 @@ const mockThermocyclerGen2LidClosed = { } as any describe('ModuleOverflowMenu', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { vi.mocked(useIsLegacySessionInProgress).mockReturnValue(false) vi.mocked(useRunStatuses).mockReturnValue({ diff --git a/app/src/organisms/ModuleCard/__tests__/ModuleSetupModal.test.tsx b/app/src/organisms/ModuleCard/__tests__/ModuleSetupModal.test.tsx index 87f340b2845..1b3abfab5ce 100644 --- a/app/src/organisms/ModuleCard/__tests__/ModuleSetupModal.test.tsx +++ b/app/src/organisms/ModuleCard/__tests__/ModuleSetupModal.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { fireEvent, screen } from '@testing-library/react' import { beforeEach, describe, expect, it, vi } from 'vitest' @@ -6,14 +5,16 @@ import { renderWithProviders } from '/app/__testing-utils__' import { i18n } from '/app/i18n' import { ModuleSetupModal } from '../ModuleSetupModal' -const render = (props: React.ComponentProps) => { +import type { ComponentProps } from 'react' + +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, })[0] } describe('ModuleSetupModal', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { close: vi.fn(), moduleDisplayName: 'mockModuleDisplayName' } }) diff --git a/app/src/organisms/ModuleCard/__tests__/TemperatureModuleData.test.tsx b/app/src/organisms/ModuleCard/__tests__/TemperatureModuleData.test.tsx index 5b851ce5796..a9e2d88fb26 100644 --- a/app/src/organisms/ModuleCard/__tests__/TemperatureModuleData.test.tsx +++ b/app/src/organisms/ModuleCard/__tests__/TemperatureModuleData.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { screen } from '@testing-library/react' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' @@ -8,16 +7,18 @@ import { StatusLabel } from '/app/atoms/StatusLabel' import { TemperatureModuleData } from '../TemperatureModuleData' import { mockTemperatureModuleGen2 } from '/app/redux/modules/__fixtures__' +import type { ComponentProps } from 'react' + vi.mock('/app/atoms/StatusLabel') -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, })[0] } describe('TemperatureModuleData', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { moduleStatus: mockTemperatureModuleGen2.data.status, diff --git a/app/src/organisms/ModuleCard/__tests__/TemperatureModuleSlideout.test.tsx b/app/src/organisms/ModuleCard/__tests__/TemperatureModuleSlideout.test.tsx index ce65306741d..12ecc39f2f1 100644 --- a/app/src/organisms/ModuleCard/__tests__/TemperatureModuleSlideout.test.tsx +++ b/app/src/organisms/ModuleCard/__tests__/TemperatureModuleSlideout.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { fireEvent, screen } from '@testing-library/react' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' @@ -12,18 +11,18 @@ import { } from '/app/redux/modules/__fixtures__' import { TemperatureModuleSlideout } from '../TemperatureModuleSlideout' +import type { ComponentProps } from 'react' + vi.mock('@opentrons/react-api-client') -const render = ( - props: React.ComponentProps -) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, })[0] } describe('TemperatureModuleSlideout', () => { - let props: React.ComponentProps + let props: ComponentProps let mockCreateLiveCommand = vi.fn() beforeEach(() => { diff --git a/app/src/organisms/ModuleCard/__tests__/TestShakeSlideout.test.tsx b/app/src/organisms/ModuleCard/__tests__/TestShakeSlideout.test.tsx index f11816df8b6..865c656b1ed 100644 --- a/app/src/organisms/ModuleCard/__tests__/TestShakeSlideout.test.tsx +++ b/app/src/organisms/ModuleCard/__tests__/TestShakeSlideout.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { fireEvent, screen, waitFor } from '@testing-library/react' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' @@ -12,12 +11,14 @@ import { useLatchControls } from '../hooks' import { TestShakeSlideout } from '../TestShakeSlideout' import { ModuleSetupModal } from '../ModuleSetupModal' +import type { ComponentProps } from 'react' + vi.mock('/app/redux/config') vi.mock('@opentrons/react-api-client') vi.mock('../hooks') vi.mock('../ModuleSetupModal') -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, })[0] @@ -90,7 +91,7 @@ const mockMovingHeaterShaker = { } as any describe('TestShakeSlideout', () => { - let props: React.ComponentProps + let props: ComponentProps let mockCreateLiveCommand = vi.fn() const mockToggleLatch = vi.fn() beforeEach(() => { diff --git a/app/src/organisms/ModuleCard/__tests__/ThermocyclerModuleData.test.tsx b/app/src/organisms/ModuleCard/__tests__/ThermocyclerModuleData.test.tsx index 0885c74bb5d..fc2346cf9ba 100644 --- a/app/src/organisms/ModuleCard/__tests__/ThermocyclerModuleData.test.tsx +++ b/app/src/organisms/ModuleCard/__tests__/ThermocyclerModuleData.test.tsx @@ -1,6 +1,4 @@ -import type * as React from 'react' import { screen } from '@testing-library/react' - import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { renderWithProviders } from '/app/__testing-utils__' @@ -11,9 +9,10 @@ import { } from '/app/redux/modules/__fixtures__' import { ThermocyclerModuleData } from '../ThermocyclerModuleData' +import type { ComponentProps } from 'react' import type { ThermocyclerData } from '/app/redux/modules/api-types' -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, })[0] @@ -49,7 +48,7 @@ const mockDataHeating = { } as ThermocyclerData describe('ThermocyclerModuleData', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { data: mockThermocycler.data, diff --git a/app/src/organisms/ModuleCard/__tests__/ThermocyclerModuleSlideout.test.tsx b/app/src/organisms/ModuleCard/__tests__/ThermocyclerModuleSlideout.test.tsx index d93a1b1f607..1557b821dd9 100644 --- a/app/src/organisms/ModuleCard/__tests__/ThermocyclerModuleSlideout.test.tsx +++ b/app/src/organisms/ModuleCard/__tests__/ThermocyclerModuleSlideout.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { fireEvent, screen } from '@testing-library/react' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' @@ -9,18 +8,18 @@ import { i18n } from '/app/i18n' import { mockThermocycler } from '/app/redux/modules/__fixtures__' import { ThermocyclerModuleSlideout } from '../ThermocyclerModuleSlideout' +import type { ComponentProps } from 'react' + vi.mock('@opentrons/react-api-client') -const render = ( - props: React.ComponentProps -) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, })[0] } describe('ThermocyclerModuleSlideout', () => { - let props: React.ComponentProps + let props: ComponentProps let mockCreateLiveCommand = vi.fn() beforeEach(() => { mockCreateLiveCommand = vi.fn() diff --git a/app/src/organisms/ModuleCard/__tests__/hooks.test.tsx b/app/src/organisms/ModuleCard/__tests__/hooks.test.tsx index ce0f0450179..2fe0c5502ef 100644 --- a/app/src/organisms/ModuleCard/__tests__/hooks.test.tsx +++ b/app/src/organisms/ModuleCard/__tests__/hooks.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { Provider } from 'react-redux' import { when } from 'vitest-when' import { createStore } from 'redux' @@ -30,6 +29,7 @@ import { useIsHeaterShakerInProtocol, } from '../hooks' +import type { FunctionComponent, ReactNode } from 'react' import type { Store } from 'redux' import type { State } from '/app/redux/types' @@ -188,7 +188,7 @@ describe('useLatchControls', () => { }) it('should return latch is open and handle latch function and command to close latch', () => { - const wrapper: React.FunctionComponent<{ children: React.ReactNode }> = ({ + const wrapper: FunctionComponent<{ children: ReactNode }> = ({ children, }) => ( @@ -212,7 +212,7 @@ describe('useLatchControls', () => { }) }) it('should return if latch is closed and handle latch function opens latch', () => { - const wrapper: React.FunctionComponent<{ children: React.ReactNode }> = ({ + const wrapper: FunctionComponent<{ children: ReactNode }> = ({ children, }) => ( @@ -263,7 +263,7 @@ describe('useModuleOverflowMenu', () => { vi.restoreAllMocks() }) it('should return everything for menuItemsByModuleType and create deactive heater command', () => { - const wrapper: React.FunctionComponent<{ children: React.ReactNode }> = ({ + const wrapper: FunctionComponent<{ children: ReactNode }> = ({ children, }) => ( @@ -304,7 +304,7 @@ describe('useModuleOverflowMenu', () => { const mockAboutClick = vi.fn() const mockTestShakeClick = vi.fn() const mockHandleWizard = vi.fn() - const wrapper: React.FunctionComponent<{ children: React.ReactNode }> = ({ + const wrapper: FunctionComponent<{ children: ReactNode }> = ({ children, }) => ( @@ -336,7 +336,7 @@ describe('useModuleOverflowMenu', () => { it('should return only 1 menu button when module is a magnetic module and calls handleClick when module is disengaged', () => { const mockHandleClick = vi.fn() - const wrapper: React.FunctionComponent<{ children: React.ReactNode }> = ({ + const wrapper: FunctionComponent<{ children: ReactNode }> = ({ children, }) => ( @@ -366,7 +366,7 @@ describe('useModuleOverflowMenu', () => { }) it('should render magnetic module and create disengage command', () => { - const wrapper: React.FunctionComponent<{ children: React.ReactNode }> = ({ + const wrapper: FunctionComponent<{ children: ReactNode }> = ({ children, }) => ( @@ -404,7 +404,7 @@ describe('useModuleOverflowMenu', () => { it('should render temperature module and call handleClick when module is idle', () => { const mockHandleClick = vi.fn() - const wrapper: React.FunctionComponent<{ children: React.ReactNode }> = ({ + const wrapper: FunctionComponent<{ children: ReactNode }> = ({ children, }) => ( @@ -433,7 +433,7 @@ describe('useModuleOverflowMenu', () => { }) it('should render temp module and create deactivate temp command', () => { - const wrapper: React.FunctionComponent<{ children: React.ReactNode }> = ({ + const wrapper: FunctionComponent<{ children: ReactNode }> = ({ children, }) => ( @@ -470,7 +470,7 @@ describe('useModuleOverflowMenu', () => { it('should render TC module and call handleClick when module is idle', () => { const mockHandleClick = vi.fn() - const wrapper: React.FunctionComponent<{ children: React.ReactNode }> = ({ + const wrapper: FunctionComponent<{ children: ReactNode }> = ({ children, }) => ( @@ -499,7 +499,7 @@ describe('useModuleOverflowMenu', () => { }) it('should render TC module and create open lid command', () => { - const wrapper: React.FunctionComponent<{ children: React.ReactNode }> = ({ + const wrapper: FunctionComponent<{ children: ReactNode }> = ({ children, }) => ( @@ -537,7 +537,7 @@ describe('useModuleOverflowMenu', () => { }) it('should render TC module and create deactivate lid command', () => { - const wrapper: React.FunctionComponent<{ children: React.ReactNode }> = ({ + const wrapper: FunctionComponent<{ children: ReactNode }> = ({ children, }) => ( @@ -575,7 +575,7 @@ describe('useModuleOverflowMenu', () => { }) it('should render TC module gen 2 and create a close lid command', () => { - const wrapper: React.FunctionComponent<{ children: React.ReactNode }> = ({ + const wrapper: FunctionComponent<{ children: ReactNode }> = ({ children, }) => ( @@ -650,7 +650,7 @@ describe('useIsHeaterShakerInProtocol', () => { }) it('should return true when a heater shaker is in the protocol', () => { - const wrapper: React.FunctionComponent<{ children: React.ReactNode }> = ({ + const wrapper: FunctionComponent<{ children: ReactNode }> = ({ children, }) => {children} const { result } = renderHook(useIsHeaterShakerInProtocol, { wrapper }) @@ -674,7 +674,7 @@ describe('useIsHeaterShakerInProtocol', () => { id, })), } as any) - const wrapper: React.FunctionComponent<{ children: React.ReactNode }> = ({ + const wrapper: FunctionComponent<{ children: ReactNode }> = ({ children, }) => {children} const { result } = renderHook(useIsHeaterShakerInProtocol, { wrapper }) diff --git a/app/src/organisms/ModuleWizardFlows/index.tsx b/app/src/organisms/ModuleWizardFlows/index.tsx index 27a3555d70c..b802cd6ca85 100644 --- a/app/src/organisms/ModuleWizardFlows/index.tsx +++ b/app/src/organisms/ModuleWizardFlows/index.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useState, useEffect } from 'react' import { createPortal } from 'react-dom' import { useSelector } from 'react-redux' import { Trans, useTranslation } from 'react-i18next' @@ -37,6 +37,7 @@ import { useNotifyCurrentMaintenanceRun, } from '/app/resources/maintenance_runs' +import type { SetStateAction } from 'react' import type { AttachedModule, CommandData } from '@opentrons/api-client' import { RUN_STATUS_FAILED } from '@opentrons/api-client' import type { @@ -107,7 +108,7 @@ export const ModuleWizardFlows = ( ) ) ?? [] - const [currentStepIndex, setCurrentStepIndex] = React.useState(0) + const [currentStepIndex, setCurrentStepIndex] = useState(0) const totalStepCount = moduleCalibrationSteps.length - 1 const currentStep = moduleCalibrationSteps?.[currentStepIndex] @@ -116,18 +117,16 @@ export const ModuleWizardFlows = ( currentStepIndex === 0 ? currentStepIndex : currentStepIndex - 1 ) } - const [createdMaintenanceRunId, setCreatedMaintenanceRunId] = React.useState< + const [createdMaintenanceRunId, setCreatedMaintenanceRunId] = useState< string | null >(null) - const [createdAdapterId, setCreatedAdapterId] = React.useState( - null - ) + const [createdAdapterId, setCreatedAdapterId] = useState(null) // we should start checking for run deletion only after the maintenance run is created // and the useCurrentRun poll has returned that created id const [ monitorMaintenanceRunForDeletion, setMonitorMaintenanceRunForDeletion, - ] = React.useState(false) + ] = useState(false) const { data: maintenanceRunData } = useNotifyCurrentMaintenanceRun({ refetchInterval: RUN_REFETCH_INTERVAL, @@ -142,16 +141,14 @@ export const ModuleWizardFlows = ( createTargetedMaintenanceRun, isLoading: isCreateLoading, } = useCreateTargetedMaintenanceRunMutation({ - onSuccess: (response: { - data: { id: React.SetStateAction } - }) => { + onSuccess: (response: { data: { id: SetStateAction } }) => { setCreatedMaintenanceRunId(response.data.id) }, }) // this will close the modal in case the run was deleted by the terminate // activity modal on the ODD - React.useEffect(() => { + useEffect(() => { if ( createdMaintenanceRunId !== null && maintenanceRunData?.data.id === createdMaintenanceRunId @@ -171,8 +168,8 @@ export const ModuleWizardFlows = ( closeFlow, ]) - const [errorMessage, setErrorMessage] = React.useState(null) - const [isExiting, setIsExiting] = React.useState(false) + const [errorMessage, setErrorMessage] = useState(null) + const [isExiting, setIsExiting] = useState(false) const proceed = (): void => { if (!isCommandMutationLoading) { setCurrentStepIndex( @@ -216,9 +213,9 @@ export const ModuleWizardFlows = ( } } - const [isRobotMoving, setIsRobotMoving] = React.useState(false) + const [isRobotMoving, setIsRobotMoving] = useState(false) - React.useEffect(() => { + useEffect(() => { if (isCommandMutationLoading || isExiting) { setIsRobotMoving(true) } else { diff --git a/app/src/organisms/ODD/ChildNavigation/__tests__/ChildNavigation.test.tsx b/app/src/organisms/ODD/ChildNavigation/__tests__/ChildNavigation.test.tsx index 82a0dfb0b3c..44ff1414dd6 100644 --- a/app/src/organisms/ODD/ChildNavigation/__tests__/ChildNavigation.test.tsx +++ b/app/src/organisms/ODD/ChildNavigation/__tests__/ChildNavigation.test.tsx @@ -1,19 +1,20 @@ -import type * as React from 'react' import { fireEvent, screen } from '@testing-library/react' import { vi, it, describe, expect, beforeEach } from 'vitest' import { renderWithProviders } from '/app/__testing-utils__' import { ChildNavigation } from '..' + +import type { ComponentProps } from 'react' import type { SmallButton } from '/app/atoms/buttons' -const render = (props: React.ComponentProps) => +const render = (props: ComponentProps) => renderWithProviders() const mockOnClickBack = vi.fn() const mockOnClickButton = vi.fn() const mockOnClickSecondaryButton = vi.fn() -const mockSecondaryButtonProps: React.ComponentProps = { +const mockSecondaryButtonProps: ComponentProps = { onClick: mockOnClickSecondaryButton, buttonText: 'Setup Instructions', buttonType: 'tertiaryLowLight', @@ -22,7 +23,7 @@ const mockSecondaryButtonProps: React.ComponentProps = { } describe('ChildNavigation', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { diff --git a/app/src/organisms/ODD/ChildNavigation/index.tsx b/app/src/organisms/ODD/ChildNavigation/index.tsx index ea6c72f293b..ff1ebed1c95 100644 --- a/app/src/organisms/ODD/ChildNavigation/index.tsx +++ b/app/src/organisms/ODD/ChildNavigation/index.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import styled from 'styled-components' import { @@ -21,6 +20,7 @@ import { ODD_FOCUS_VISIBLE } from '/app/atoms/buttons/constants' import { SmallButton } from '/app/atoms/buttons' import { InlineNotification } from '/app/atoms/InlineNotification' +import type { ComponentProps, MouseEventHandler, ReactNode } from 'react' import type { IconName, StyleProps } from '@opentrons/components' import type { InlineNotificationProps } from '/app/atoms/InlineNotification' import type { @@ -30,15 +30,15 @@ import type { interface ChildNavigationProps extends StyleProps { header: string - onClickBack?: React.MouseEventHandler - buttonText?: React.ReactNode + onClickBack?: MouseEventHandler + buttonText?: ReactNode inlineNotification?: InlineNotificationProps - onClickButton?: React.MouseEventHandler + onClickButton?: MouseEventHandler buttonType?: SmallButtonTypes buttonIsDisabled?: boolean iconName?: IconName iconPlacement?: IconPlacement - secondaryButtonProps?: React.ComponentProps + secondaryButtonProps?: ComponentProps ariaDisabled?: boolean } diff --git a/app/src/organisms/ODD/InstrumentInfo/__tests__/InstrumentInfo.test.tsx b/app/src/organisms/ODD/InstrumentInfo/__tests__/InstrumentInfo.test.tsx index a478716483d..ec8b55bb17a 100644 --- a/app/src/organisms/ODD/InstrumentInfo/__tests__/InstrumentInfo.test.tsx +++ b/app/src/organisms/ODD/InstrumentInfo/__tests__/InstrumentInfo.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { fireEvent, screen } from '@testing-library/react' import { describe, it, vi, beforeEach, expect } from 'vitest' import { renderWithProviders } from '/app/__testing-utils__' @@ -8,22 +7,23 @@ import { PipetteWizardFlows } from '/app/organisms/PipetteWizardFlows' import { GripperWizardFlows } from '/app/organisms/GripperWizardFlows' import { InstrumentInfo } from '..' +import type { ComponentProps } from 'react' +import type * as ReactRouterDom from 'react-router-dom' import type { GripperData } from '@opentrons/api-client' -import type * as Dom from 'react-router-dom' const mockNavigate = vi.fn() vi.mock('/app/organisms/PipetteWizardFlows') vi.mock('/app/organisms/GripperWizardFlows') vi.mock('react-router-dom', async importOriginal => { - const reactRouterDom = await importOriginal() + const reactRouterDom = await importOriginal() return { ...reactRouterDom, useNavigate: () => mockNavigate, } }) -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, })[0] @@ -65,7 +65,7 @@ const mockGripperDataWithCalData: GripperData = { } describe('InstrumentInfo', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { vi.mocked(PipetteWizardFlows).mockReturnValue(
        mock PipetteWizardFlows
        diff --git a/app/src/organisms/ODD/InstrumentInfo/index.tsx b/app/src/organisms/ODD/InstrumentInfo/index.tsx index 68c3ebd5388..ecfb9456b05 100644 --- a/app/src/organisms/ODD/InstrumentInfo/index.tsx +++ b/app/src/organisms/ODD/InstrumentInfo/index.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useState } from 'react' import { useTranslation } from 'react-i18next' import { useNavigate } from 'react-router-dom' import { @@ -8,8 +8,8 @@ import { Flex, JUSTIFY_CENTER, JUSTIFY_SPACE_BETWEEN, - SPACING, LegacyStyledText, + SPACING, TYPOGRAPHY, } from '@opentrons/components' import { @@ -23,6 +23,7 @@ import { FLOWS } from '/app/organisms/PipetteWizardFlows/constants' import { GRIPPER_FLOW_TYPES } from '/app/organisms/GripperWizardFlows/constants' import { formatTimeWithUtcLabel } from '/app/resources/runs' +import type { ComponentProps, MouseEventHandler } from 'react' import type { InstrumentData } from '@opentrons/api-client' import type { PipetteMount } from '@opentrons/shared-data' import type { StyleProps } from '@opentrons/components' @@ -36,14 +37,14 @@ export const InstrumentInfo = (props: InstrumentInfoProps): JSX.Element => { const { t, i18n } = useTranslation('instruments_dashboard') const { instrument } = props const navigate = useNavigate() - const [wizardProps, setWizardProps] = React.useState< - | React.ComponentProps - | React.ComponentProps + const [wizardProps, setWizardProps] = useState< + | ComponentProps + | ComponentProps | null >(null) const sharedGripperWizardProps: Pick< - React.ComponentProps, + ComponentProps, 'attachedGripper' | 'closeFlow' > = { attachedGripper: instrument, @@ -58,7 +59,7 @@ export const InstrumentInfo = (props: InstrumentInfoProps): JSX.Element => { instrument.mount !== 'extension' && instrument.data?.channels === 96 - const handleDetach: React.MouseEventHandler = () => { + const handleDetach: MouseEventHandler = () => { if (instrument != null && instrument.ok) { setWizardProps( instrument.mount === 'extension' @@ -85,7 +86,7 @@ export const InstrumentInfo = (props: InstrumentInfoProps): JSX.Element => { ) } } - const handleRecalibrate: React.MouseEventHandler = () => { + const handleRecalibrate: MouseEventHandler = () => { if (instrument != null && instrument.ok) { setWizardProps( instrument.mount === 'extension' diff --git a/app/src/organisms/ODD/InstrumentMountItem/AttachedInstrumentMountItem.tsx b/app/src/organisms/ODD/InstrumentMountItem/AttachedInstrumentMountItem.tsx index d2de81b8fdc..7af7dd4029a 100644 --- a/app/src/organisms/ODD/InstrumentMountItem/AttachedInstrumentMountItem.tsx +++ b/app/src/organisms/ODD/InstrumentMountItem/AttachedInstrumentMountItem.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useState } from 'react' import { useNavigate } from 'react-router-dom' import { SINGLE_MOUNT_PIPETTES } from '@opentrons/shared-data' @@ -12,6 +12,7 @@ import { FLOWS } from '/app/organisms/PipetteWizardFlows/constants' import { GRIPPER_FLOW_TYPES } from '/app/organisms/GripperWizardFlows/constants' import { LabeledMount } from './LabeledMount' +import type { ComponentProps, MouseEventHandler } from 'react' import type { InstrumentData } from '@opentrons/api-client' import type { GripperModel, PipetteModel } from '@opentrons/shared-data' import type { Mount } from '/app/redux/pipettes/types' @@ -24,8 +25,8 @@ interface AttachedInstrumentMountItemProps { attachedInstrument: InstrumentData | null setWizardProps: ( props: - | React.ComponentProps - | React.ComponentProps + | ComponentProps + | ComponentProps | null ) => void } @@ -36,15 +37,12 @@ export function AttachedInstrumentMountItem( const navigate = useNavigate() const { mount, attachedInstrument, setWizardProps } = props - const [showChoosePipetteModal, setShowChoosePipetteModal] = React.useState( - false + const [showChoosePipetteModal, setShowChoosePipetteModal] = useState(false) + const [selectedPipette, setSelectedPipette] = useState( + SINGLE_MOUNT_PIPETTES ) - const [ - selectedPipette, - setSelectedPipette, - ] = React.useState(SINGLE_MOUNT_PIPETTES) - const handleClick: React.MouseEventHandler = () => { + const handleClick: MouseEventHandler = () => { if (attachedInstrument == null && mount !== 'extension') { setShowChoosePipetteModal(true) } else if (attachedInstrument == null && mount === 'extension') { diff --git a/app/src/organisms/ODD/InstrumentMountItem/LabeledMount.tsx b/app/src/organisms/ODD/InstrumentMountItem/LabeledMount.tsx index 20fe3941604..c909e4768f0 100644 --- a/app/src/organisms/ODD/InstrumentMountItem/LabeledMount.tsx +++ b/app/src/organisms/ODD/InstrumentMountItem/LabeledMount.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { useTranslation } from 'react-i18next' import styled from 'styled-components' import { @@ -15,6 +14,8 @@ import { TEXT_TRANSFORM_CAPITALIZE, TYPOGRAPHY, } from '@opentrons/components' + +import type { MouseEventHandler } from 'react' import type { Mount } from '/app/redux/pipettes/types' const MountButton = styled.button<{ isAttached: boolean }>` @@ -34,7 +35,7 @@ const MountButton = styled.button<{ isAttached: boolean }>` interface LabeledMountProps { mount: Mount | 'extension' instrumentName: string | null - handleClick: React.MouseEventHandler + handleClick: MouseEventHandler } export function LabeledMount(props: LabeledMountProps): JSX.Element { diff --git a/app/src/organisms/ODD/InstrumentMountItem/ProtocolInstrumentMountItem.tsx b/app/src/organisms/ODD/InstrumentMountItem/ProtocolInstrumentMountItem.tsx index be034e8fb7a..86ae69e4107 100644 --- a/app/src/organisms/ODD/InstrumentMountItem/ProtocolInstrumentMountItem.tsx +++ b/app/src/organisms/ODD/InstrumentMountItem/ProtocolInstrumentMountItem.tsx @@ -1,17 +1,17 @@ -import * as React from 'react' +import { useState, useMemo } from 'react' import styled from 'styled-components' import { useTranslation } from 'react-i18next' import { ALIGN_CENTER, - Flex, - COLORS, - SPACING, - TYPOGRAPHY, - Icon, - DIRECTION_COLUMN, ALIGN_FLEX_START, BORDERS, + COLORS, + DIRECTION_COLUMN, + Flex, + Icon, JUSTIFY_FLEX_START, + SPACING, + TYPOGRAPHY, } from '@opentrons/components' import { NINETY_SIX_CHANNEL, @@ -27,6 +27,7 @@ import { FLOWS } from '/app/organisms/PipetteWizardFlows/constants' import { PipetteWizardFlows } from '/app/organisms/PipetteWizardFlows' import { GripperWizardFlows } from '/app/organisms/GripperWizardFlows' +import type { MouseEventHandler } from 'react' import type { InstrumentData } from '@opentrons/api-client' import type { GripperModel, @@ -61,26 +62,24 @@ export function ProtocolInstrumentMountItem( ): JSX.Element { const { i18n, t } = useTranslation('protocol_setup') const { mount, attachedInstrument, speccedName } = props - const [ - showPipetteWizardFlow, - setShowPipetteWizardFlow, - ] = React.useState(false) - const [ - showGripperWizardFlow, - setShowGripperWizardFlow, - ] = React.useState(false) - const memoizedAttachedGripper = React.useMemo( + const [showPipetteWizardFlow, setShowPipetteWizardFlow] = useState( + false + ) + const [showGripperWizardFlow, setShowGripperWizardFlow] = useState( + false + ) + const memoizedAttachedGripper = useMemo( () => attachedInstrument?.instrumentType === 'gripper' && attachedInstrument.ok ? attachedInstrument : null, [] ) - const [flowType, setFlowType] = React.useState(FLOWS.ATTACH) + const [flowType, setFlowType] = useState(FLOWS.ATTACH) const selectedPipette = speccedName === 'p1000_96' ? NINETY_SIX_CHANNEL : SINGLE_MOUNT_PIPETTES - const handleCalibrate: React.MouseEventHandler = () => { + const handleCalibrate: MouseEventHandler = () => { setFlowType(FLOWS.CALIBRATE) if (mount === 'extension') { setShowGripperWizardFlow(true) @@ -88,7 +87,7 @@ export function ProtocolInstrumentMountItem( setShowPipetteWizardFlow(true) } } - const handleAttach: React.MouseEventHandler = () => { + const handleAttach: MouseEventHandler = () => { setFlowType(FLOWS.ATTACH) if (mount === 'extension') { setShowGripperWizardFlow(true) diff --git a/app/src/organisms/ODD/InstrumentMountItem/__tests__/ProtocolInstrumentMountItem.test.tsx b/app/src/organisms/ODD/InstrumentMountItem/__tests__/ProtocolInstrumentMountItem.test.tsx index 6c4c308b8d2..52c62382241 100644 --- a/app/src/organisms/ODD/InstrumentMountItem/__tests__/ProtocolInstrumentMountItem.test.tsx +++ b/app/src/organisms/ODD/InstrumentMountItem/__tests__/ProtocolInstrumentMountItem.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { describe, it, vi, beforeEach } from 'vitest' import { renderWithProviders } from '/app/__testing-utils__' import { LEFT } from '@opentrons/shared-data' @@ -8,6 +7,8 @@ import { PipetteWizardFlows } from '/app/organisms/PipetteWizardFlows' import { GripperWizardFlows } from '/app/organisms/GripperWizardFlows' import { ProtocolInstrumentMountItem } from '..' +import type { ComponentProps } from 'react' + vi.mock('/app/organisms/PipetteWizardFlows') vi.mock('/app/organisms/GripperWizardFlows') vi.mock('../../TakeoverModal') @@ -51,16 +52,14 @@ const mockLeftPipetteData = { ok: true, } -const render = ( - props: React.ComponentProps -) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, })[0] } describe('ProtocolInstrumentMountItem', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { mount: LEFT, diff --git a/app/src/organisms/ODD/NameRobot/__tests__/ConfirmRobotName.test.tsx b/app/src/organisms/ODD/NameRobot/__tests__/ConfirmRobotName.test.tsx index d9e8521260e..21d34d3a59d 100644 --- a/app/src/organisms/ODD/NameRobot/__tests__/ConfirmRobotName.test.tsx +++ b/app/src/organisms/ODD/NameRobot/__tests__/ConfirmRobotName.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { MemoryRouter } from 'react-router-dom' import { fireEvent, screen } from '@testing-library/react' import { beforeEach, describe, expect, it, vi } from 'vitest' @@ -7,6 +6,7 @@ import { renderWithProviders } from '/app/__testing-utils__' import { i18n } from '/app/i18n' import { ConfirmRobotName } from '../ConfirmRobotName' +import type { ComponentProps } from 'react' import type { NavigateFunction } from 'react-router-dom' const mockNavigate = vi.fn() @@ -19,7 +19,7 @@ vi.mock('react-router-dom', async importOriginal => { } }) -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders( @@ -31,7 +31,7 @@ const render = (props: React.ComponentProps) => { } describe('ConfirmRobotName', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { robotName: 'otie', diff --git a/app/src/organisms/ODD/Navigation/NavigationMenu.tsx b/app/src/organisms/ODD/Navigation/NavigationMenu.tsx index d2347bf52b0..0b1cbb40e92 100644 --- a/app/src/organisms/ODD/Navigation/NavigationMenu.tsx +++ b/app/src/organisms/ODD/Navigation/NavigationMenu.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useState } from 'react' import { createPortal } from 'react-dom' import { useDispatch } from 'react-redux' import { useTranslation } from 'react-i18next' @@ -21,10 +21,11 @@ import { useLights } from '/app/resources/devices' import { getTopPortalEl } from '/app/App/portal' import { RestartRobotConfirmationModal } from './RestartRobotConfirmationModal' +import type { MouseEventHandler } from 'react' import type { Dispatch } from '/app/redux/types' interface NavigationMenuProps { - onClick: React.MouseEventHandler + onClick: MouseEventHandler robotName: string setShowNavMenu: (showNavMenu: boolean) => void } @@ -37,7 +38,7 @@ export function NavigationMenu(props: NavigationMenuProps): JSX.Element { const [ showRestartRobotConfirmationModal, setShowRestartRobotConfirmationModal, - ] = React.useState(false) + ] = useState(false) const navigate = useNavigate() diff --git a/app/src/organisms/ODD/Navigation/__tests__/Navigation.test.tsx b/app/src/organisms/ODD/Navigation/__tests__/Navigation.test.tsx index c86ba363d5c..1a212f83e46 100644 --- a/app/src/organisms/ODD/Navigation/__tests__/Navigation.test.tsx +++ b/app/src/organisms/ODD/Navigation/__tests__/Navigation.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { MemoryRouter } from 'react-router-dom' import { fireEvent, screen } from '@testing-library/react' import { beforeEach, describe, expect, it, vi } from 'vitest' @@ -12,6 +11,8 @@ import { NavigationMenu } from '../NavigationMenu' import { Navigation } from '..' import { useScrollPosition } from '/app/local-resources/dom-utils' +import type { ComponentProps } from 'react' + vi.mock('/app/resources/networking/hooks/useNetworkConnection') vi.mock('/app/redux/discovery') vi.mock('../NavigationMenu') @@ -19,7 +20,7 @@ vi.mock('/app/local-resources/dom-utils') mockConnectedRobot.name = '12345678901234567' -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders( @@ -29,7 +30,7 @@ const render = (props: React.ComponentProps) => { } describe('Navigation', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = {} vi.mocked(getLocalRobot).mockReturnValue(mockConnectedRobot) diff --git a/app/src/organisms/ODD/Navigation/__tests__/NavigationMenu.test.tsx b/app/src/organisms/ODD/Navigation/__tests__/NavigationMenu.test.tsx index b40122cdd6b..b4b70528cda 100644 --- a/app/src/organisms/ODD/Navigation/__tests__/NavigationMenu.test.tsx +++ b/app/src/organisms/ODD/Navigation/__tests__/NavigationMenu.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { fireEvent, screen } from '@testing-library/react' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' @@ -9,6 +8,7 @@ import { useLights } from '/app/resources/devices' import { RestartRobotConfirmationModal } from '../RestartRobotConfirmationModal' import { NavigationMenu } from '../NavigationMenu' +import type { ComponentProps } from 'react' import type { NavigateFunction } from 'react-router-dom' vi.mock('/app/redux/robot-admin') @@ -27,14 +27,14 @@ vi.mock('react-router-dom', async importOriginal => { const mockToggleLights = vi.fn() -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, })[0] } describe('NavigationMenu', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { onClick: vi.fn(), diff --git a/app/src/organisms/ODD/Navigation/__tests__/RestartRobotConfirmationModal.test.tsx b/app/src/organisms/ODD/Navigation/__tests__/RestartRobotConfirmationModal.test.tsx index 8922a4225c2..5b3bc007567 100644 --- a/app/src/organisms/ODD/Navigation/__tests__/RestartRobotConfirmationModal.test.tsx +++ b/app/src/organisms/ODD/Navigation/__tests__/RestartRobotConfirmationModal.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { fireEvent, screen } from '@testing-library/react' import { beforeEach, describe, expect, it, vi } from 'vitest' @@ -7,12 +6,14 @@ import { i18n } from '/app/i18n' import { restartRobot } from '/app/redux/robot-admin' import { RestartRobotConfirmationModal } from '../RestartRobotConfirmationModal' +import type { ComponentProps } from 'react' + vi.mock('/app/redux/robot-admin') const mockFunc = vi.fn() const render = ( - props: React.ComponentProps + props: ComponentProps ) => { return renderWithProviders(, { i18nInstance: i18n, @@ -20,7 +21,7 @@ const render = ( } describe('RestartRobotConfirmationModal', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { diff --git a/app/src/organisms/ODD/Navigation/index.tsx b/app/src/organisms/ODD/Navigation/index.tsx index 8b60946f929..e562793e36b 100644 --- a/app/src/organisms/ODD/Navigation/index.tsx +++ b/app/src/organisms/ODD/Navigation/index.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useState, useEffect, useRef } from 'react' import { useSelector } from 'react-redux' import { useTranslation } from 'react-i18next' import { useLocation, NavLink } from 'react-router-dom' @@ -32,6 +32,8 @@ import { useScrollPosition } from '/app/local-resources/dom-utils' import { useNetworkConnection } from '/app/resources/networking/hooks/useNetworkConnection' import { getLocalRobot } from '/app/redux/discovery' import { NavigationMenu } from './NavigationMenu' + +import type { Dispatch, SetStateAction } from 'react' import type { ON_DEVICE_DISPLAY_PATHS } from '/app/App/OnDeviceDisplayApp' const NAV_LINKS: Array = [ @@ -65,7 +67,7 @@ const CHAR_LIMIT_NO_ICON = 15 interface NavigationProps { // optionalProps for setting the zIndex and position between multiple sticky elements // used for ProtocolDashboard - setNavMenuIsOpened?: React.Dispatch> + setNavMenuIsOpened?: Dispatch> longPressModalIsOpened?: boolean } export function Navigation(props: NavigationProps): JSX.Element { @@ -73,7 +75,7 @@ export function Navigation(props: NavigationProps): JSX.Element { const { t } = useTranslation('top_navigation') const location = useLocation() const localRobot = useSelector(getLocalRobot) - const [showNavMenu, setShowNavMenu] = React.useState(false) + const [showNavMenu, setShowNavMenu] = useState(false) const robotName = localRobot?.name != null ? localRobot.name : 'no name' // We need to display an icon for what type of network connection (if any) @@ -95,8 +97,8 @@ export function Navigation(props: NavigationProps): JSX.Element { const { scrollRef, isScrolled } = useScrollPosition() - const navBarScrollRef = React.useRef(null) - React.useEffect(() => { + const navBarScrollRef = useRef(null) + useEffect(() => { navBarScrollRef?.current?.scrollIntoView({ behavior: 'auto', inline: 'center', diff --git a/app/src/organisms/ODD/NetworkSettings/FailedToConnect.tsx b/app/src/organisms/ODD/NetworkSettings/FailedToConnect.tsx index e3859e4ed29..22358b922bc 100644 --- a/app/src/organisms/ODD/NetworkSettings/FailedToConnect.tsx +++ b/app/src/organisms/ODD/NetworkSettings/FailedToConnect.tsx @@ -51,7 +51,7 @@ export function FailedToConnect({ name="ot-alert" size="3rem" color={COLORS.red50} - aria-label={'failed_to_connect_invalidPassword'} + aria-label="failed_to_connect_invalidPassword" /> diff --git a/app/src/organisms/ODD/NetworkSettings/SelectAuthenticationType.tsx b/app/src/organisms/ODD/NetworkSettings/SelectAuthenticationType.tsx index b918d48df5e..9f163929220 100644 --- a/app/src/organisms/ODD/NetworkSettings/SelectAuthenticationType.tsx +++ b/app/src/organisms/ODD/NetworkSettings/SelectAuthenticationType.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useState, useEffect } from 'react' import { useDispatch, useSelector } from 'react-redux' import { useTranslation } from 'react-i18next' @@ -21,6 +21,7 @@ import { getNetworkInterfaces, fetchStatus } from '/app/redux/networking' import { useIsUnboxingFlowOngoing } from '/app/redux-resources/config' import { AlternativeSecurityTypeModal } from './AlternativeSecurityTypeModal' +import type { ChangeEvent } from 'react' import type { WifiSecurityType } from '@opentrons/api-client' import type { Dispatch, State } from '/app/redux/types' @@ -44,7 +45,7 @@ export function SelectAuthenticationType({ const [ showAlternativeSecurityTypeModal, setShowAlternativeSecurityTypeModal, - ] = React.useState(false) + ] = useState(false) const securityButtons = [ { @@ -59,11 +60,11 @@ export function SelectAuthenticationType({ }, ] - const handleChange = (event: React.ChangeEvent): void => { + const handleChange = (event: ChangeEvent): void => { setSelectedAuthType(event.target.value as WifiSecurityType) } - React.useEffect(() => { + useEffect(() => { dispatch(fetchStatus(robotName)) }, [robotName, dispatch]) diff --git a/app/src/organisms/ODD/NetworkSettings/SetWifiSsid.tsx b/app/src/organisms/ODD/NetworkSettings/SetWifiSsid.tsx index d2ea891a254..114d906215b 100644 --- a/app/src/organisms/ODD/NetworkSettings/SetWifiSsid.tsx +++ b/app/src/organisms/ODD/NetworkSettings/SetWifiSsid.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useRef } from 'react' import { useTranslation } from 'react-i18next' import { @@ -15,10 +15,12 @@ import { import { FullKeyboard } from '/app/atoms/SoftwareKeyboard' import { useIsUnboxingFlowOngoing } from '/app/redux-resources/config' +import type { Dispatch, SetStateAction } from 'react' + interface SetWifiSsidProps { errorMessage?: string | null inputSsid: string - setInputSsid: React.Dispatch> + setInputSsid: Dispatch> } export function SetWifiSsid({ @@ -27,7 +29,7 @@ export function SetWifiSsid({ setInputSsid, }: SetWifiSsidProps): JSX.Element { const { t } = useTranslation(['device_settings', 'shared']) - const keyboardRef = React.useRef(null) + const keyboardRef = useRef(null) const isUnboxingFlowOngoing = useIsUnboxingFlowOngoing() return ( diff --git a/app/src/organisms/ODD/NetworkSettings/WifiConnectionDetails.tsx b/app/src/organisms/ODD/NetworkSettings/WifiConnectionDetails.tsx index 04e0fdb66a9..420e5fec5fe 100644 --- a/app/src/organisms/ODD/NetworkSettings/WifiConnectionDetails.tsx +++ b/app/src/organisms/ODD/NetworkSettings/WifiConnectionDetails.tsx @@ -93,7 +93,7 @@ export function WifiConnectionDetails({ /> { navigate('/robot-settings/update-robot-during-onboarding') }} diff --git a/app/src/organisms/ODD/NetworkSettings/__tests__/AlternativeSecurityTypeModal.test.tsx b/app/src/organisms/ODD/NetworkSettings/__tests__/AlternativeSecurityTypeModal.test.tsx index a01af9bba66..2707b07a8f5 100644 --- a/app/src/organisms/ODD/NetworkSettings/__tests__/AlternativeSecurityTypeModal.test.tsx +++ b/app/src/organisms/ODD/NetworkSettings/__tests__/AlternativeSecurityTypeModal.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { fireEvent, screen } from '@testing-library/react' import { beforeEach, describe, expect, it, vi } from 'vitest' @@ -6,6 +5,7 @@ import { renderWithProviders } from '/app/__testing-utils__' import { i18n } from '/app/i18n' import { AlternativeSecurityTypeModal } from '../AlternativeSecurityTypeModal' +import type { ComponentProps } from 'react' import type { NavigateFunction } from 'react-router-dom' const mockFunc = vi.fn() @@ -18,16 +18,14 @@ vi.mock('react-router-dom', async importOriginal => { } }) -const render = ( - props: React.ComponentProps -) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, }) } describe('AlternativeSecurityTypeModal', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { diff --git a/app/src/organisms/ODD/NetworkSettings/__tests__/ConnectingNetwork.test.tsx b/app/src/organisms/ODD/NetworkSettings/__tests__/ConnectingNetwork.test.tsx index e040bee4572..29444afea6d 100644 --- a/app/src/organisms/ODD/NetworkSettings/__tests__/ConnectingNetwork.test.tsx +++ b/app/src/organisms/ODD/NetworkSettings/__tests__/ConnectingNetwork.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { MemoryRouter } from 'react-router-dom' import { screen } from '@testing-library/react' import { beforeEach, describe, expect, it } from 'vitest' @@ -7,7 +6,9 @@ import { renderWithProviders } from '/app/__testing-utils__' import { i18n } from '/app/i18n' import { ConnectingNetwork } from '../ConnectingNetwork' -const render = (props: React.ComponentProps) => { +import type { ComponentProps } from 'react' + +const render = (props: ComponentProps) => { return renderWithProviders( @@ -19,7 +20,7 @@ const render = (props: React.ComponentProps) => { } describe('ConnectingNetwork', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { diff --git a/app/src/organisms/ODD/NetworkSettings/__tests__/DisplayWifiList.test.tsx b/app/src/organisms/ODD/NetworkSettings/__tests__/DisplayWifiList.test.tsx index e1449be3b9d..11e0ef16a9b 100644 --- a/app/src/organisms/ODD/NetworkSettings/__tests__/DisplayWifiList.test.tsx +++ b/app/src/organisms/ODD/NetworkSettings/__tests__/DisplayWifiList.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { fireEvent, screen } from '@testing-library/react' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' @@ -8,6 +7,7 @@ import * as Fixtures from '/app/redux/networking/__fixtures__' import { DisplaySearchNetwork } from '../DisplaySearchNetwork' import { DisplayWifiList } from '../DisplayWifiList' +import type { ComponentProps } from 'react' import type { NavigateFunction } from 'react-router-dom' const mockNavigate = vi.fn() @@ -31,14 +31,14 @@ vi.mock('react-router-dom', async importOriginal => { } }) -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, }) } describe('DisplayWifiList', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { list: mockWifiList, diff --git a/app/src/organisms/ODD/NetworkSettings/__tests__/FailedToConnect.test.tsx b/app/src/organisms/ODD/NetworkSettings/__tests__/FailedToConnect.test.tsx index 3dbf7bf1f46..74e11378af8 100644 --- a/app/src/organisms/ODD/NetworkSettings/__tests__/FailedToConnect.test.tsx +++ b/app/src/organisms/ODD/NetworkSettings/__tests__/FailedToConnect.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { MemoryRouter } from 'react-router-dom' import { fireEvent, screen } from '@testing-library/react' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' @@ -7,9 +6,10 @@ import { renderWithProviders } from '/app/__testing-utils__' import { i18n } from '/app/i18n' import { FailedToConnect } from '../FailedToConnect' +import type { ComponentProps } from 'react' import type { RequestState } from '/app/redux/robot-api/types' -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders( @@ -33,7 +33,7 @@ const failureState = { } as RequestState describe('ConnectedResult', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { diff --git a/app/src/organisms/ODD/NetworkSettings/__tests__/SelectAuthenticationType.test.tsx b/app/src/organisms/ODD/NetworkSettings/__tests__/SelectAuthenticationType.test.tsx index bfce05cc22d..2520e944ad7 100644 --- a/app/src/organisms/ODD/NetworkSettings/__tests__/SelectAuthenticationType.test.tsx +++ b/app/src/organisms/ODD/NetworkSettings/__tests__/SelectAuthenticationType.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { fireEvent, screen } from '@testing-library/react' import { MemoryRouter } from 'react-router-dom' import { afterEach, beforeEach, describe, it, vi } from 'vitest' @@ -11,6 +10,7 @@ import { AlternativeSecurityTypeModal } from '../AlternativeSecurityTypeModal' import { SelectAuthenticationType } from '../SelectAuthenticationType' import { SetWifiCred } from '../SetWifiCred' +import type { ComponentProps } from 'react' import type { NavigateFunction } from 'react-router-dom' const mockNavigate = vi.fn() @@ -36,9 +36,7 @@ const initialMockWifi = { type: INTERFACE_WIFI, } -const render = ( - props: React.ComponentProps -) => { +const render = (props: ComponentProps) => { return renderWithProviders( @@ -50,7 +48,7 @@ const render = ( } describe('SelectAuthenticationType', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { selectedAuthType: 'wpa-psk', diff --git a/app/src/organisms/ODD/NetworkSettings/__tests__/SetWifiCred.test.tsx b/app/src/organisms/ODD/NetworkSettings/__tests__/SetWifiCred.test.tsx index d1a25f069c9..85cc94d895e 100644 --- a/app/src/organisms/ODD/NetworkSettings/__tests__/SetWifiCred.test.tsx +++ b/app/src/organisms/ODD/NetworkSettings/__tests__/SetWifiCred.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { MemoryRouter } from 'react-router-dom' import { fireEvent, screen } from '@testing-library/react' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' @@ -7,11 +6,13 @@ import { renderWithProviders } from '/app/__testing-utils__' import { i18n } from '/app/i18n' import { SetWifiCred } from '../SetWifiCred' +import type { ComponentProps } from 'react' + const mockSetPassword = vi.fn() vi.mock('/app/redux/discovery') vi.mock('/app/redux/robot-api') -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders( @@ -23,7 +24,7 @@ const render = (props: React.ComponentProps) => { } describe('SetWifiCred', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { password: 'mock-password', diff --git a/app/src/organisms/ODD/NetworkSettings/__tests__/SetWifiSsid.test.tsx b/app/src/organisms/ODD/NetworkSettings/__tests__/SetWifiSsid.test.tsx index 11eab279c37..c5b0ff26ee5 100644 --- a/app/src/organisms/ODD/NetworkSettings/__tests__/SetWifiSsid.test.tsx +++ b/app/src/organisms/ODD/NetworkSettings/__tests__/SetWifiSsid.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { MemoryRouter } from 'react-router-dom' import { fireEvent, screen } from '@testing-library/react' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' @@ -7,8 +6,10 @@ import { renderWithProviders } from '/app/__testing-utils__' import { i18n } from '/app/i18n' import { SetWifiSsid } from '../SetWifiSsid' +import type { ComponentProps } from 'react' + const mockSetSelectedSsid = vi.fn() -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders( @@ -20,7 +21,7 @@ const render = (props: React.ComponentProps) => { } describe('SetWifiSsid', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { setInputSsid: mockSetSelectedSsid, diff --git a/app/src/organisms/ODD/NetworkSettings/__tests__/WifiConnectionDetails.test.tsx b/app/src/organisms/ODD/NetworkSettings/__tests__/WifiConnectionDetails.test.tsx index efcee37e0c6..76e63934328 100644 --- a/app/src/organisms/ODD/NetworkSettings/__tests__/WifiConnectionDetails.test.tsx +++ b/app/src/organisms/ODD/NetworkSettings/__tests__/WifiConnectionDetails.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { MemoryRouter } from 'react-router-dom' import { fireEvent, screen } from '@testing-library/react' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' @@ -11,6 +10,7 @@ import * as Fixtures from '/app/redux/networking/__fixtures__' import { NetworkDetailsModal } from '../../RobotSettingsDashboard/NetworkSettings/NetworkDetailsModal' import { WifiConnectionDetails } from '../WifiConnectionDetails' +import type { ComponentProps } from 'react' import type { NavigateFunction } from 'react-router-dom' vi.mock('/app/resources/networking/hooks') @@ -27,7 +27,7 @@ vi.mock('react-router-dom', async importOriginal => { } }) -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders( @@ -51,7 +51,7 @@ const mockWifiList = [ ] describe('WifiConnectionDetails', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { ssid: 'mockWifi', diff --git a/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupDeckConfiguration/__tests__/ProtocolSetupDeckConfiguration.test.tsx b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupDeckConfiguration/__tests__/ProtocolSetupDeckConfiguration.test.tsx index 84a7fd2eb87..21b26365d81 100644 --- a/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupDeckConfiguration/__tests__/ProtocolSetupDeckConfiguration.test.tsx +++ b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupDeckConfiguration/__tests__/ProtocolSetupDeckConfiguration.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { when } from 'vitest-when' import { fireEvent, screen } from '@testing-library/react' import { describe, it, vi, beforeEach, afterEach } from 'vitest' @@ -15,6 +14,7 @@ import { useMostRecentCompletedAnalysis } from '/app/resources/runs' import { useNotifyDeckConfigurationQuery } from '/app/resources/deck_configuration' import { ProtocolSetupDeckConfiguration } from '..' +import type { ComponentProps } from 'react' import type { UseQueryResult } from 'react-query' import type { CompletedProtocolAnalysis, @@ -47,7 +47,7 @@ vi.mock('@opentrons/components', async importOriginal => { }) const render = ( - props: React.ComponentProps + props: ComponentProps ) => { return renderWithProviders(, { i18nInstance: i18n, @@ -55,7 +55,7 @@ const render = ( } describe('ProtocolSetupDeckConfiguration', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { diff --git a/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupDeckConfiguration/index.tsx b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupDeckConfiguration/index.tsx index a7148788639..4f67deb6da6 100644 --- a/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupDeckConfiguration/index.tsx +++ b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupDeckConfiguration/index.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useState } from 'react' import { createPortal } from 'react-dom' import { useTranslation } from 'react-i18next' @@ -26,6 +26,7 @@ import { useMostRecentCompletedAnalysis } from '/app/resources/runs' import { getTopPortalEl } from '/app/App/portal' import { useNotifyDeckConfigurationQuery } from '/app/resources/deck_configuration' +import type { Dispatch, SetStateAction } from 'react' import type { CutoutFixtureId, CutoutId, @@ -37,7 +38,7 @@ import type { SetupScreens } from '../types' interface ProtocolSetupDeckConfigurationProps { cutoutId: CutoutId | null runId: string - setSetupScreen: React.Dispatch> + setSetupScreen: Dispatch> providedFixtureOptions: CutoutFixtureId[] } @@ -53,14 +54,12 @@ export function ProtocolSetupDeckConfiguration({ 'shared', ]) - const [ - showConfigurationModal, - setShowConfigurationModal, - ] = React.useState(true) - const [ - showDiscardChangeModal, - setShowDiscardChangeModal, - ] = React.useState(false) + const [showConfigurationModal, setShowConfigurationModal] = useState( + true + ) + const [showDiscardChangeModal, setShowDiscardChangeModal] = useState( + false + ) const mostRecentAnalysis = useMostRecentCompletedAnalysis(runId) const deckConfig = useNotifyDeckConfigurationQuery()?.data ?? [] diff --git a/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupInstruments/ProtocolSetupInstruments.tsx b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupInstruments/ProtocolSetupInstruments.tsx index 1af859bc431..1826ea10339 100644 --- a/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupInstruments/ProtocolSetupInstruments.tsx +++ b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupInstruments/ProtocolSetupInstruments.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import styled from 'styled-components' import { useTranslation } from 'react-i18next' import { @@ -16,15 +15,16 @@ import { PipetteRecalibrationODDWarning } from '/app/organisms/ODD/PipetteRecali import { getShowPipetteCalibrationWarning } from '/app/transformations/instruments' import { useMostRecentCompletedAnalysis } from '/app/resources/runs' import { ProtocolInstrumentMountItem } from '/app/organisms/ODD/InstrumentMountItem' +import { isGripperInCommands } from '/app/resources/protocols/utils' +import type { Dispatch, SetStateAction } from 'react' import type { GripperData, PipetteData } from '@opentrons/api-client' import type { GripperModel } from '@opentrons/shared-data' import type { SetupScreens } from '../types' -import { isGripperInCommands } from '/app/resources/protocols/utils' export interface ProtocolSetupInstrumentsProps { runId: string - setSetupScreen: React.Dispatch> + setSetupScreen: Dispatch> } export function ProtocolSetupInstruments({ diff --git a/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupLabware/__tests__/LabwareMapView.test.tsx b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupLabware/__tests__/LabwareMapView.test.tsx index 860d927578e..3f387db51f6 100644 --- a/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupLabware/__tests__/LabwareMapView.test.tsx +++ b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupLabware/__tests__/LabwareMapView.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { MemoryRouter } from 'react-router-dom' import { when } from 'vitest-when' import { describe, it, vi, beforeEach, afterEach, expect } from 'vitest' @@ -17,6 +16,7 @@ import { getStandardDeckViewLayerBlockList } from '/app/local-resources/deck_con import { mockProtocolModuleInfo } from '../__fixtures__' import { LabwareMapView } from '../LabwareMapView' +import type { ComponentProps } from 'react' import type { getSimplestDeckConfigForProtocol, CompletedProtocolAnalysis, @@ -50,7 +50,7 @@ vi.mock('@opentrons/components', async importOriginal => { } }) -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders( diff --git a/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupLabware/index.tsx b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupLabware/index.tsx index 2d440fc9516..19875b2336e 100644 --- a/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupLabware/index.tsx +++ b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupLabware/index.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useState } from 'react' import { useTranslation } from 'react-i18next' import { css } from 'styled-components' @@ -7,6 +7,7 @@ import { ALIGN_FLEX_START, BORDERS, Box, + Chip, COLORS, DeckInfoLabel, DIRECTION_COLUMN, @@ -15,18 +16,16 @@ import { Icon, JUSTIFY_SPACE_BETWEEN, JUSTIFY_SPACE_EVENLY, + LegacyStyledText, MODULE_ICON_NAME_BY_TYPE, SPACING, - LegacyStyledText, TYPOGRAPHY, - Chip, } from '@opentrons/components' import { FLEX_ROBOT_TYPE, getDeckDefFromRobotType, - getLabwareDefURI, - getTopLabwareInfo, getModuleDisplayName, + getTopLabwareInfo, HEATERSHAKER_MODULE_TYPE, TC_MODULE_LOCATION_OT3, THERMOCYCLER_MODULE_TYPE, @@ -52,6 +51,7 @@ import { useMostRecentCompletedAnalysis } from '/app/resources/runs' import { LabwareMapView } from './LabwareMapView' import { SingleLabwareModal } from './SingleLabwareModal' +import type { Dispatch, SetStateAction } from 'react' import type { UseQueryResult } from 'react-query' import type { HeaterShakerCloseLatchCreateCommand, @@ -71,7 +71,7 @@ const DECK_CONFIG_POLL_MS = 5000 export interface ProtocolSetupLabwareProps { runId: string - setSetupScreen: React.Dispatch> + setSetupScreen: Dispatch> isConfirmed: boolean setIsConfirmed: (confirmed: boolean) => void } @@ -83,12 +83,12 @@ export function ProtocolSetupLabware({ setIsConfirmed, }: ProtocolSetupLabwareProps): JSX.Element { const { t } = useTranslation('protocol_setup') - const [showMapView, setShowMapView] = React.useState(false) + const [showMapView, setShowMapView] = useState(false) const [ showLabwareDetailsModal, setShowLabwareDetailsModal, - ] = React.useState(false) - const [selectedLabware, setSelectedLabware] = React.useState< + ] = useState(false) + const [selectedLabware, setSelectedLabware] = useState< | (LabwareDefinition2 & { location: LabwareLocation nickName: string | null @@ -129,15 +129,16 @@ export function ProtocolSetupLabware({ ) if (foundLabware != null) { const nickName = onDeckItems.find( - item => getLabwareDefURI(item.definition) === foundLabware.definitionUri + item => item.labwareId === foundLabware.id )?.nickName + const location = onDeckItems.find( item => item.labwareId === foundLabware.id )?.initialLocation if (location != null) { setSelectedLabware({ ...labwareDef, - location: location, + location, nickName: nickName ?? null, id: labwareId, }) @@ -289,7 +290,7 @@ function LabwareLatch({ createLiveCommand, isLoading: isLiveCommandLoading, } = useCreateLiveCommandMutation() - const [isRefetchingModules, setIsRefetchingModules] = React.useState(false) + const [isRefetchingModules, setIsRefetchingModules] = useState(false) const isLatchLoading = isLiveCommandLoading || isRefetchingModules || diff --git a/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupLiquids/__tests__/LiquidDetails.test.tsx b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupLiquids/__tests__/LiquidDetails.test.tsx index 720b6db7545..9d05000cf8f 100644 --- a/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupLiquids/__tests__/LiquidDetails.test.tsx +++ b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupLiquids/__tests__/LiquidDetails.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { screen, fireEvent } from '@testing-library/react' import { describe, it, beforeEach, vi } from 'vitest' @@ -13,20 +12,22 @@ import { MOCK_LABWARE_INFO_BY_LIQUID_ID, MOCK_PROTOCOL_ANALYSIS, } from '../fixtures' + +import type { ComponentProps } from 'react' import type { CompletedProtocolAnalysis } from '@opentrons/shared-data' vi.mock('/app/transformations/analysis') vi.mock('/app/transformations/commands') vi.mock('/app/organisms/LiquidsLabwareDetailsModal') -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, }) } describe('LiquidDetails', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { commands: (MOCK_PROTOCOL_ANALYSIS as CompletedProtocolAnalysis).commands, diff --git a/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupLiquids/__tests__/ProtocolSetupLiquids.test.tsx b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupLiquids/__tests__/ProtocolSetupLiquids.test.tsx index 487fbbd0bce..eed74fae79d 100644 --- a/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupLiquids/__tests__/ProtocolSetupLiquids.test.tsx +++ b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupLiquids/__tests__/ProtocolSetupLiquids.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { describe, it, beforeEach, vi } from 'vitest' import { screen, fireEvent } from '@testing-library/react' @@ -20,6 +19,7 @@ import { } from '../fixtures' import { ProtocolSetupLiquids } from '..' +import type { ComponentProps } from 'react' import type * as SharedData from '@opentrons/shared-data' vi.mock('/app/transformations/analysis') @@ -41,13 +41,13 @@ describe('ProtocolSetupLiquids', () => { isConfirmed = confirmed }) - const render = (props: React.ComponentProps) => { + const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, }) } - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { runId: RUN_ID_1, diff --git a/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupLiquids/index.tsx b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupLiquids/index.tsx index 153315d294b..d8aa63d4b26 100644 --- a/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupLiquids/index.tsx +++ b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupLiquids/index.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { Fragment, useState } from 'react' import { useTranslation } from 'react-i18next' import { ALIGN_CENTER, @@ -25,12 +25,14 @@ import { SmallButton } from '/app/atoms/buttons' import { useMostRecentCompletedAnalysis } from '/app/resources/runs' import { getTotalVolumePerLiquidId } from '/app/transformations/analysis' import { LiquidDetails } from './LiquidDetails' + +import type { Dispatch, SetStateAction } from 'react' import type { ParsedLiquid, RunTimeCommand } from '@opentrons/shared-data' import type { SetupScreens } from '../types' export interface ProtocolSetupLiquidsProps { runId: string - setSetupScreen: React.Dispatch> + setSetupScreen: Dispatch> isConfirmed: boolean setIsConfirmed: (confirmed: boolean) => void } @@ -95,13 +97,13 @@ export function ProtocolSetupLiquids({ {liquidsInLoadOrder?.map(liquid => ( - + - + ))}
        @@ -116,7 +118,7 @@ interface LiquidsListProps { export function LiquidsList(props: LiquidsListProps): JSX.Element { const { liquid, runId, commands } = props - const [openItem, setOpenItem] = React.useState(false) + const [openItem, setOpenItem] = useState(false) const labwareByLiquidId = parseLabwareInfoByLiquidId(commands ?? []) return ( diff --git a/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupModulesAndDeck/FixtureTable.tsx b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupModulesAndDeck/FixtureTable.tsx index 3491d7530c7..7d990e112a0 100644 --- a/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupModulesAndDeck/FixtureTable.tsx +++ b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupModulesAndDeck/FixtureTable.tsx @@ -1,4 +1,5 @@ -import * as React from 'react' +import { useState, Fragment } from 'react' +import { useSelector } from 'react-redux' import { useTranslation } from 'react-i18next' import { ALIGN_CENTER, @@ -29,7 +30,9 @@ import { SmallButton } from '/app/atoms/buttons' import { useDeckConfigurationCompatibility } from '/app/resources/deck_configuration/hooks' import { getRequiredDeckConfig } from '/app/resources/deck_configuration/utils' import { LocationConflictModal } from '/app/organisms/LocationConflictModal' +import { getLocalRobot } from '/app/redux/discovery' +import type { Dispatch, SetStateAction } from 'react' import type { CompletedProtocolAnalysis, CutoutFixtureId, @@ -39,13 +42,11 @@ import type { } from '@opentrons/shared-data' import type { SetupScreens } from '../types' import type { CutoutConfigAndCompatibility } from '/app/resources/deck_configuration/hooks' -import { useSelector } from 'react-redux' -import { getLocalRobot } from '/app/redux/discovery' interface FixtureTableProps { robotType: RobotType mostRecentAnalysis: CompletedProtocolAnalysis | null - setSetupScreen: React.Dispatch> + setSetupScreen: Dispatch> setCutoutId: (cutoutId: CutoutId) => void setProvidedFixtureOptions: (providedFixtureOptions: CutoutFixtureId[]) => void } @@ -134,7 +135,7 @@ export function FixtureTable({ interface FixtureTableItemProps extends CutoutConfigAndCompatibility { lastItem: boolean - setSetupScreen: React.Dispatch> + setSetupScreen: Dispatch> setCutoutId: (cutoutId: CutoutId) => void setProvidedFixtureOptions: (providedFixtureOptions: CutoutFixtureId[]) => void deckDef: DeckDefinition @@ -158,7 +159,7 @@ function FixtureTableItem({ const [ showLocationConflictModal, setShowLocationConflictModal, - ] = React.useState(false) + ] = useState(false) const isCurrentFixtureCompatible = cutoutFixtureId != null && @@ -215,7 +216,7 @@ function FixtureTableItem({ ) } return ( - + {showLocationConflictModal ? ( { @@ -265,6 +266,6 @@ function FixtureTableItem({ {chipLabel}
        - + ) } diff --git a/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupModulesAndDeck/ModuleTable.tsx b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupModulesAndDeck/ModuleTable.tsx index 1a0b93c6f57..d4b0a32ad2d 100644 --- a/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupModulesAndDeck/ModuleTable.tsx +++ b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupModulesAndDeck/ModuleTable.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useState } from 'react' import { useTranslation } from 'react-i18next' import { useSelector } from 'react-redux' @@ -39,6 +39,7 @@ import { } from '/app/resources/runs' import { useNotifyDeckConfigurationQuery } from '/app/resources/deck_configuration' +import type { Dispatch, SetStateAction } from 'react' import type { CommandData } from '@opentrons/api-client' import type { CutoutConfig, DeckDefinition } from '@opentrons/shared-data' import type { ModulePrepCommandsType } from '/app/local-resources/modules' @@ -59,7 +60,7 @@ export function ModuleTable(props: ModuleTableProps): JSX.Element { const [ prepCommandErrorMessage, setPrepCommandErrorMessage, - ] = React.useState('') + ] = useState('') const { data: deckConfig } = useNotifyDeckConfigurationQuery({ refetchInterval: DECK_CONFIG_REFETCH_INTERVAL, @@ -119,7 +120,7 @@ interface ModuleTableItemProps { isLoading: boolean module: AttachedProtocolModuleMatch prepCommandErrorMessage: string - setPrepCommandErrorMessage: React.Dispatch> + setPrepCommandErrorMessage: Dispatch> deckDef: DeckDefinition robotName: string } @@ -162,11 +163,11 @@ function ModuleTableItem({ ) const isModuleReady = module.attachedModuleMatch != null - const [showModuleWizard, setShowModuleWizard] = React.useState(false) + const [showModuleWizard, setShowModuleWizard] = useState(false) const [ showLocationConflictModal, setShowLocationConflictModal, - ] = React.useState(false) + ] = useState(false) let moduleStatus: JSX.Element = ( <> diff --git a/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupModulesAndDeck/ProtocolSetupModulesAndDeck.tsx b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupModulesAndDeck/ProtocolSetupModulesAndDeck.tsx index bcbc2b57bba..542ba3d3624 100644 --- a/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupModulesAndDeck/ProtocolSetupModulesAndDeck.tsx +++ b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupModulesAndDeck/ProtocolSetupModulesAndDeck.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useState, useEffect } from 'react' import { createPortal } from 'react-dom' import { useTranslation } from 'react-i18next' import { useNavigate } from 'react-router-dom' @@ -37,6 +37,7 @@ import { ModuleTable } from './ModuleTable' import { ModulesAndDeckMapView } from './ModulesAndDeckMapView' import { useNotifyDeckConfigurationQuery } from '/app/resources/deck_configuration' +import type { Dispatch, SetStateAction } from 'react' import type { CutoutId, CutoutFixtureId } from '@opentrons/shared-data' import type { SetupScreens } from '../types' @@ -45,7 +46,7 @@ const DECK_CONFIG_POLL_MS = 5000 interface ProtocolSetupModulesAndDeckProps { runId: string - setSetupScreen: React.Dispatch> + setSetupScreen: Dispatch> setCutoutId: (cutoutId: CutoutId) => void setProvidedFixtureOptions: (providedFixtureOptions: CutoutFixtureId[]) => void } @@ -62,7 +63,7 @@ export function ProtocolSetupModulesAndDeck({ const { i18n, t } = useTranslation('protocol_setup') const navigate = useNavigate() const runStatus = useRunStatus(runId) - React.useEffect(() => { + useEffect(() => { if (runStatus === RUN_STATUS_STOPPED) { navigate('/protocols') } @@ -70,12 +71,12 @@ export function ProtocolSetupModulesAndDeck({ const [ showSetupInstructionsModal, setShowSetupInstructionsModal, - ] = React.useState(false) - const [showMapView, setShowMapView] = React.useState(false) + ] = useState(false) + const [showMapView, setShowMapView] = useState(false) const [ clearModuleMismatchBanner, setClearModuleMismatchBanner, - ] = React.useState(false) + ] = useState(false) const mostRecentAnalysis = useMostRecentCompletedAnalysis(runId) const deckDef = getDeckDefFromRobotType(FLEX_ROBOT_TYPE) diff --git a/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupModulesAndDeck/__tests__/FixtureTable.test.tsx b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupModulesAndDeck/__tests__/FixtureTable.test.tsx index e6ca8735d77..b5336d9c535 100644 --- a/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupModulesAndDeck/__tests__/FixtureTable.test.tsx +++ b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupModulesAndDeck/__tests__/FixtureTable.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { fireEvent, screen } from '@testing-library/react' import { describe, it, beforeEach, afterEach, vi, expect } from 'vitest' @@ -18,6 +17,8 @@ import { FixtureTable } from '../FixtureTable' import { getLocalRobot } from '/app/redux/discovery' import { mockConnectedRobot } from '/app/redux/discovery/__fixtures__' +import type { ComponentProps } from 'react' + vi.mock('/app/redux/discovery') vi.mock('/app/resources/deck_configuration/hooks') vi.mock('/app/organisms/LocationConflictModal') @@ -26,14 +27,14 @@ const mockSetSetupScreen = vi.fn() const mockSetCutoutId = vi.fn() const mockSetProvidedFixtureOptions = vi.fn() -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, }) } describe('FixtureTable', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { mostRecentAnalysis: { commands: [], labware: [] } as any, diff --git a/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupModulesAndDeck/__tests__/ModulesAndDeckMapView.test.tsx b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupModulesAndDeck/__tests__/ModulesAndDeckMapView.test.tsx index d31a0312d02..a06ad0118db 100644 --- a/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupModulesAndDeck/__tests__/ModulesAndDeckMapView.test.tsx +++ b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupModulesAndDeck/__tests__/ModulesAndDeckMapView.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { describe, it, vi, beforeEach, afterEach } from 'vitest' import { screen } from '@testing-library/react' @@ -12,6 +11,8 @@ import { renderWithProviders } from '/app/__testing-utils__' import { i18n } from '/app/i18n' import { ModulesAndDeckMapView } from '../ModulesAndDeckMapView' +import type { ComponentProps } from 'react' + vi.mock('@opentrons/components/src/hardware-sim/BaseDeck') vi.mock('@opentrons/api-client') vi.mock('@opentrons/shared-data/js/helpers/getSimplestFlexDeckConfig') @@ -99,14 +100,14 @@ vi.mock('@opentrons/components', async importOriginal => { } }) -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, })[0] } describe('ModulesAndDeckMapView', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { diff --git a/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupModulesAndDeck/__tests__/SetupInstructionsModal.test.tsx b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupModulesAndDeck/__tests__/SetupInstructionsModal.test.tsx index 8f6f4c01739..f4ac8d4fec1 100644 --- a/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupModulesAndDeck/__tests__/SetupInstructionsModal.test.tsx +++ b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupModulesAndDeck/__tests__/SetupInstructionsModal.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { fireEvent, screen } from '@testing-library/react' import { describe, it, expect, beforeEach, vi } from 'vitest' @@ -7,18 +6,20 @@ import { i18n } from '/app/i18n' import { SetupInstructionsModal } from '../SetupInstructionsModal' +import type { ComponentProps } from 'react' + const mockSetShowSetupInstructionsModal = vi.fn() const QR_CODE_IMAGE_FILE = '/app/src/assets/images/on-device-display/setup_instructions_qr_code.png' -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, }) } describe('SetupInstructionsModal', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { diff --git a/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupOffsets/index.tsx b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupOffsets/index.tsx index 3082df45a2a..85e9352d5fe 100644 --- a/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupOffsets/index.tsx +++ b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupOffsets/index.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { useTranslation } from 'react-i18next' import { Chip, @@ -16,7 +15,7 @@ import { useToaster } from '/app/organisms/ToasterOven' import { ODDBackButton } from '/app/molecules/ODDBackButton' import { FloatingActionButton, SmallButton } from '/app/atoms/buttons' import type { SetupScreens } from '../types' -import { TerseOffsetTable } from '/app/organisms/LabwarePositionCheck/ResultsSummary' +import { TerseOffsetTable } from '/app/organisms/LegacyLabwarePositionCheck/ResultsSummary' import { getLabwareDefinitionsFromCommands } from '/app/local-resources/labware' import { useNotifyRunQuery, @@ -24,14 +23,17 @@ import { } from '/app/resources/runs' import { getLatestCurrentOffsets } from '/app/transformations/runs' +import type { Dispatch, SetStateAction } from 'react' + export interface ProtocolSetupOffsetsProps { runId: string - setSetupScreen: React.Dispatch> + setSetupScreen: Dispatch> lpcDisabledReason: string | null launchLPC: () => void LPCWizard: JSX.Element | null isConfirmed: boolean setIsConfirmed: (confirmed: boolean) => void + isNewLpc: boolean } export function ProtocolSetupOffsets({ @@ -42,6 +44,7 @@ export function ProtocolSetupOffsets({ launchLPC, lpcDisabledReason, LPCWizard, + isNewLpc, }: ProtocolSetupOffsetsProps): JSX.Element { const { t } = useTranslation('protocol_setup') const { makeSnackbar } = useToaster() @@ -75,7 +78,7 @@ export function ProtocolSetupOffsets({ const nonIdentityOffsets = getLatestCurrentOffsets(sortedOffsets) return ( <> - {LPCWizard} + {isNewLpc ? null : LPCWizard} {LPCWizard == null && ( <> null)() : launchLPC() } }} /> diff --git a/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupParameters/ViewOnlyParameters.tsx b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupParameters/ViewOnlyParameters.tsx index 1946d122848..7f5b73db3b0 100644 --- a/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupParameters/ViewOnlyParameters.tsx +++ b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupParameters/ViewOnlyParameters.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { useTranslation } from 'react-i18next' import { css } from 'styled-components' @@ -22,11 +21,12 @@ import { useMostRecentCompletedAnalysis } from '/app/resources/runs' import { ChildNavigation } from '/app/organisms/ODD/ChildNavigation' import { useToaster } from '/app/organisms/ToasterOven' +import type { Dispatch, SetStateAction } from 'react' import type { SetupScreens } from '../types' export interface ViewOnlyParametersProps { runId: string - setSetupScreen: React.Dispatch> + setSetupScreen: Dispatch> } export function ViewOnlyParameters({ diff --git a/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupParameters/__tests__/AnalysisFailedModal.test.tsx b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupParameters/__tests__/AnalysisFailedModal.test.tsx index ac43f26d621..dec3fc608e2 100644 --- a/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupParameters/__tests__/AnalysisFailedModal.test.tsx +++ b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupParameters/__tests__/AnalysisFailedModal.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { describe, it, vi, beforeEach, expect } from 'vitest' import { when } from 'vitest-when' import { fireEvent, screen } from '@testing-library/react' @@ -7,6 +6,8 @@ import { useDismissCurrentRunMutation } from '@opentrons/react-api-client' import { renderWithProviders } from '/app/__testing-utils__' import { i18n } from '/app/i18n' import { AnalysisFailedModal } from '../AnalysisFailedModal' + +import type { ComponentProps } from 'react' import type { NavigateFunction } from 'react-router-dom' const PROTOCOL_ID = 'mockProtocolId' @@ -26,14 +27,14 @@ vi.mock('react-router-dom', async importOriginal => { } }) -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, }) } describe('AnalysisFailedModal', () => { - let props: React.ComponentProps + let props: ComponentProps when(vi.mocked(useDismissCurrentRunMutation)) .calledWith() diff --git a/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupParameters/__tests__/ChooseCsvFile.test.tsx b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupParameters/__tests__/ChooseCsvFile.test.tsx index 2f365fa5fbc..560f9f9490a 100644 --- a/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupParameters/__tests__/ChooseCsvFile.test.tsx +++ b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupParameters/__tests__/ChooseCsvFile.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { describe, it, beforeEach, vi, expect } from 'vitest' import { screen, fireEvent } from '@testing-library/react' import { when } from 'vitest-when' @@ -13,6 +12,7 @@ import { getShellUpdateDataFiles } from '/app/redux/shell' import { EmptyFile } from '../EmptyFile' import { ChooseCsvFile } from '../ChooseCsvFile' +import type { ComponentProps } from 'react' import type { CsvFileParameter } from '@opentrons/shared-data' vi.mock('@opentrons/react-api-client') @@ -47,14 +47,14 @@ const mockDataOnRobot = { }, } -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, }) } describe('ChooseCsvFile', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { protocolId: PROTOCOL_ID, diff --git a/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupParameters/__tests__/ChooseEnum.test.tsx b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupParameters/__tests__/ChooseEnum.test.tsx index a65c760d544..613e17c9523 100644 --- a/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupParameters/__tests__/ChooseEnum.test.tsx +++ b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupParameters/__tests__/ChooseEnum.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { it, describe, beforeEach, vi, expect } from 'vitest' import { fireEvent, screen } from '@testing-library/react' import '@testing-library/jest-dom/vitest' @@ -7,14 +6,16 @@ import { renderWithProviders } from '/app/__testing-utils__' import { i18n } from '/app/i18n' import { ChooseEnum } from '../ChooseEnum' +import type { ComponentProps } from 'react' + vi.mocked('../../../../ToasterOven') -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, }) } describe('ChooseEnum', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { diff --git a/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupParameters/__tests__/ChooseNumber.test.tsx b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupParameters/__tests__/ChooseNumber.test.tsx index 611c0e124fc..89af3d22e4c 100644 --- a/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupParameters/__tests__/ChooseNumber.test.tsx +++ b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupParameters/__tests__/ChooseNumber.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { it, describe, beforeEach, vi, expect } from 'vitest' import { fireEvent, screen } from '@testing-library/react' import '@testing-library/jest-dom/vitest' @@ -8,6 +7,7 @@ import { useToaster } from '/app/organisms/ToasterOven' import { mockRunTimeParameterData } from '../../__fixtures__' import { ChooseNumber } from '../ChooseNumber' +import type { ComponentProps } from 'react' import type { NumberParameter } from '@opentrons/shared-data' vi.mock('/app/organisms/ToasterOven') @@ -18,14 +18,14 @@ const mockFloatNumberParameterData = mockRunTimeParameterData[6] as NumberParame const mockSetParameter = vi.fn() const mockMakeSnackbar = vi.fn() -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, }) } describe('ChooseNumber', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { diff --git a/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupParameters/__tests__/ProtocolSetupParameters.test.tsx b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupParameters/__tests__/ProtocolSetupParameters.test.tsx index 18a01391711..f32442f7fda 100644 --- a/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupParameters/__tests__/ProtocolSetupParameters.test.tsx +++ b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupParameters/__tests__/ProtocolSetupParameters.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { when } from 'vitest-when' import { it, describe, beforeEach, vi, expect } from 'vitest' import { fireEvent, screen } from '@testing-library/react' @@ -19,6 +18,7 @@ import { mockRunTimeParameterData } from '../../__fixtures__' import { useToaster } from '/app/organisms/ToasterOven' import { ProtocolSetupParameters } from '..' +import type { ComponentProps } from 'react' import type { NavigateFunction } from 'react-router-dom' import type { HostConfig } from '@opentrons/api-client' import type { CompletedProtocolAnalysis } from '@opentrons/shared-data' @@ -51,16 +51,14 @@ const mockMostRecentAnalysis = ({ } as unknown) as CompletedProtocolAnalysis const mockMakeSnackbar = vi.fn() -const render = ( - props: React.ComponentProps -) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, }) } describe('ProtocolSetupParameters', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { diff --git a/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupParameters/__tests__/ResetValuesModal.test.tsx b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupParameters/__tests__/ResetValuesModal.test.tsx index 4e263f9984b..d777a44da04 100644 --- a/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupParameters/__tests__/ResetValuesModal.test.tsx +++ b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupParameters/__tests__/ResetValuesModal.test.tsx @@ -1,23 +1,24 @@ -import type * as React from 'react' import { describe, it, vi, beforeEach, expect } from 'vitest' import { fireEvent, screen } from '@testing-library/react' import { renderWithProviders } from '/app/__testing-utils__' import { i18n } from '/app/i18n' import { ResetValuesModal } from '../ResetValuesModal' + +import type { ComponentProps } from 'react' import type { RunTimeParameter } from '@opentrons/shared-data' const mockGoBack = vi.fn() const mockSetRunTimeParametersOverrides = vi.fn() -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, }) } describe('ResetValuesModal', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { diff --git a/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupParameters/__tests__/ViewOnlyParameters.test.tsx b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupParameters/__tests__/ViewOnlyParameters.test.tsx index aed74fea585..2c0827de156 100644 --- a/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupParameters/__tests__/ViewOnlyParameters.test.tsx +++ b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupParameters/__tests__/ViewOnlyParameters.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { when } from 'vitest-when' import { it, describe, beforeEach, vi, expect } from 'vitest' import { fireEvent, screen } from '@testing-library/react' @@ -9,17 +8,19 @@ import { useToaster } from '/app/organisms/ToasterOven' import { mockRunTimeParameterData } from '../../__fixtures__' import { ViewOnlyParameters } from '../ViewOnlyParameters' +import type { ComponentProps } from 'react' + vi.mock('/app/resources/runs') vi.mock('/app/organisms/ToasterOven') const RUN_ID = 'mockId' -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, }) } const mockMakeSnackBar = vi.fn() describe('ViewOnlyParameters', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { diff --git a/app/src/organisms/ODD/QuickTransferFlow/CreateNewTransfer.tsx b/app/src/organisms/ODD/QuickTransferFlow/CreateNewTransfer.tsx index 10b036b9064..b74ecc7065e 100644 --- a/app/src/organisms/ODD/QuickTransferFlow/CreateNewTransfer.tsx +++ b/app/src/organisms/ODD/QuickTransferFlow/CreateNewTransfer.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { useTranslation, Trans } from 'react-i18next' import { @@ -12,11 +11,13 @@ import { import { ChildNavigation } from '/app/organisms/ODD/ChildNavigation' import { useNotifyDeckConfigurationQuery } from '/app/resources/deck_configuration' + +import type { ComponentProps } from 'react' import type { SmallButton } from '/app/atoms/buttons' interface CreateNewTransferProps { onNext: () => void - exitButtonProps: React.ComponentProps + exitButtonProps: ComponentProps } export function CreateNewTransfer(props: CreateNewTransferProps): JSX.Element { diff --git a/app/src/organisms/ODD/QuickTransferFlow/NameQuickTransfer.tsx b/app/src/organisms/ODD/QuickTransferFlow/NameQuickTransfer.tsx index 8bff060ac38..2ef4592f568 100644 --- a/app/src/organisms/ODD/QuickTransferFlow/NameQuickTransfer.tsx +++ b/app/src/organisms/ODD/QuickTransferFlow/NameQuickTransfer.tsx @@ -33,7 +33,6 @@ export function NameQuickTransfer(props: NameQuickTransferProps): JSX.Element { if (name.length > 60) { error = t('character_limit_error') } - // TODO add error handling for quick transfer name replication return createPortal( diff --git a/app/src/organisms/ODD/QuickTransferFlow/QuickTransferAdvancedSettings/AirGap.tsx b/app/src/organisms/ODD/QuickTransferFlow/QuickTransferAdvancedSettings/AirGap.tsx index d1cf5ae2c0a..e5c8efbcc69 100644 --- a/app/src/organisms/ODD/QuickTransferFlow/QuickTransferAdvancedSettings/AirGap.tsx +++ b/app/src/organisms/ODD/QuickTransferFlow/QuickTransferAdvancedSettings/AirGap.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useState, useRef } from 'react' import { useTranslation } from 'react-i18next' import { createPortal } from 'react-dom' @@ -19,6 +19,7 @@ import { ChildNavigation } from '/app/organisms/ODD/ChildNavigation' import { useTrackEventWithRobotSerial } from '/app/redux-resources/analytics' import { ACTIONS } from '../constants' +import type { Dispatch } from 'react' import type { QuickTransferSummaryState, QuickTransferSummaryAction, @@ -30,7 +31,7 @@ import { NumericalKeyboard } from '/app/atoms/SoftwareKeyboard' interface AirGapProps { onBack: () => void state: QuickTransferSummaryState - dispatch: React.Dispatch + dispatch: Dispatch kind: FlowRateKind } @@ -38,15 +39,15 @@ export function AirGap(props: AirGapProps): JSX.Element { const { kind, onBack, state, dispatch } = props const { t } = useTranslation('quick_transfer') const { trackEventWithRobotSerial } = useTrackEventWithRobotSerial() - const keyboardRef = React.useRef(null) + const keyboardRef = useRef(null) - const [airGapEnabled, setAirGapEnabled] = React.useState( + const [airGapEnabled, setAirGapEnabled] = useState( kind === 'aspirate' ? state.airGapAspirate != null : state.airGapDispense != null ) - const [currentStep, setCurrentStep] = React.useState(1) - const [volume, setVolume] = React.useState( + const [currentStep, setCurrentStep] = useState(1) + const [volume, setVolume] = useState( kind === 'aspirate' ? state.airGapAspirate ?? null : state.airGapDispense ?? null diff --git a/app/src/organisms/ODD/QuickTransferFlow/QuickTransferAdvancedSettings/BaseSettings.tsx b/app/src/organisms/ODD/QuickTransferFlow/QuickTransferAdvancedSettings/BaseSettings.tsx index 9f300b335ad..297054b1d03 100644 --- a/app/src/organisms/ODD/QuickTransferFlow/QuickTransferAdvancedSettings/BaseSettings.tsx +++ b/app/src/organisms/ODD/QuickTransferFlow/QuickTransferAdvancedSettings/BaseSettings.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useState } from 'react' import { useTranslation } from 'react-i18next' import { ALIGN_CENTER, @@ -17,6 +17,7 @@ import { import { FlowRateEntry } from './FlowRate' import { PipettePath } from './PipettePath' +import type { Dispatch } from 'react' import type { QuickTransferSummaryAction, QuickTransferSummaryState, @@ -24,15 +25,13 @@ import type { interface BaseSettingsProps { state: QuickTransferSummaryState - dispatch: React.Dispatch + dispatch: Dispatch } export function BaseSettings(props: BaseSettingsProps): JSX.Element | null { const { state, dispatch } = props const { t } = useTranslation(['quick_transfer', 'shared']) - const [selectedSetting, setSelectedSetting] = React.useState( - null - ) + const [selectedSetting, setSelectedSetting] = useState(null) let pipettePath: string = '' if (state.path === 'single') { diff --git a/app/src/organisms/ODD/QuickTransferFlow/QuickTransferAdvancedSettings/BlowOut.tsx b/app/src/organisms/ODD/QuickTransferFlow/QuickTransferAdvancedSettings/BlowOut.tsx index 55984b27d8b..e961b480960 100644 --- a/app/src/organisms/ODD/QuickTransferFlow/QuickTransferAdvancedSettings/BlowOut.tsx +++ b/app/src/organisms/ODD/QuickTransferFlow/QuickTransferAdvancedSettings/BlowOut.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useState } from 'react' import isEqual from 'lodash/isEqual' import { useTranslation } from 'react-i18next' import { createPortal } from 'react-dom' @@ -22,6 +22,7 @@ import { useTrackEventWithRobotSerial } from '/app/redux-resources/analytics' import { ChildNavigation } from '/app/organisms/ODD/ChildNavigation' import { ACTIONS } from '../constants' +import type { Dispatch } from 'react' import type { DeckConfiguration } from '@opentrons/shared-data' import type { QuickTransferSummaryState, @@ -35,7 +36,7 @@ import { i18n } from '/app/i18n' interface BlowOutProps { onBack: () => void state: QuickTransferSummaryState - dispatch: React.Dispatch + dispatch: Dispatch kind: FlowRateKind } @@ -96,11 +97,11 @@ export function BlowOut(props: BlowOutProps): JSX.Element { const { trackEventWithRobotSerial } = useTrackEventWithRobotSerial() const deckConfig = useNotifyDeckConfigurationQuery().data ?? [] - const [isBlowOutEnabled, setisBlowOutEnabled] = React.useState( + const [isBlowOutEnabled, setisBlowOutEnabled] = useState( state.blowOut != null ) - const [currentStep, setCurrentStep] = React.useState(1) - const [blowOutLocation, setBlowOutLocation] = React.useState< + const [currentStep, setCurrentStep] = useState(1) + const [blowOutLocation, setBlowOutLocation] = useState< BlowOutLocation | undefined >(state.blowOut) diff --git a/app/src/organisms/ODD/QuickTransferFlow/QuickTransferAdvancedSettings/Delay.tsx b/app/src/organisms/ODD/QuickTransferFlow/QuickTransferAdvancedSettings/Delay.tsx index 0692cc904ac..556ddc181c8 100644 --- a/app/src/organisms/ODD/QuickTransferFlow/QuickTransferAdvancedSettings/Delay.tsx +++ b/app/src/organisms/ODD/QuickTransferFlow/QuickTransferAdvancedSettings/Delay.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useState, useRef } from 'react' import { useTranslation } from 'react-i18next' import { createPortal } from 'react-dom' @@ -8,8 +8,8 @@ import { DIRECTION_COLUMN, Flex, InputField, - RadioButton, POSITION_FIXED, + RadioButton, SPACING, } from '@opentrons/components' import { ANALYTICS_QUICK_TRANSFER_SETTING_SAVED } from '/app/redux/analytics' @@ -18,6 +18,7 @@ import { ChildNavigation } from '/app/organisms/ODD/ChildNavigation' import { useTrackEventWithRobotSerial } from '/app/redux-resources/analytics' import { ACTIONS } from '../constants' +import type { Dispatch } from 'react' import type { QuickTransferSummaryState, QuickTransferSummaryAction, @@ -29,7 +30,7 @@ import { NumericalKeyboard } from '/app/atoms/SoftwareKeyboard' interface DelayProps { onBack: () => void state: QuickTransferSummaryState - dispatch: React.Dispatch + dispatch: Dispatch kind: FlowRateKind } @@ -37,20 +38,20 @@ export function Delay(props: DelayProps): JSX.Element { const { kind, onBack, state, dispatch } = props const { t } = useTranslation('quick_transfer') const { trackEventWithRobotSerial } = useTrackEventWithRobotSerial() - const keyboardRef = React.useRef(null) + const keyboardRef = useRef(null) - const [currentStep, setCurrentStep] = React.useState(1) - const [delayIsEnabled, setDelayIsEnabled] = React.useState( + const [currentStep, setCurrentStep] = useState(1) + const [delayIsEnabled, setDelayIsEnabled] = useState( kind === 'aspirate' ? state.delayAspirate != null : state.delayDispense != null ) - const [delayDuration, setDelayDuration] = React.useState( + const [delayDuration, setDelayDuration] = useState( kind === 'aspirate' ? state.delayAspirate?.delayDuration ?? null : state.delayDispense?.delayDuration ?? null ) - const [position, setPosition] = React.useState( + const [position, setPosition] = useState( kind === 'aspirate' ? state.delayAspirate?.positionFromBottom ?? null : state.delayDispense?.positionFromBottom ?? null diff --git a/app/src/organisms/ODD/QuickTransferFlow/QuickTransferAdvancedSettings/FlowRate.tsx b/app/src/organisms/ODD/QuickTransferFlow/QuickTransferAdvancedSettings/FlowRate.tsx index 88279b1c76a..1b2bed604fc 100644 --- a/app/src/organisms/ODD/QuickTransferFlow/QuickTransferAdvancedSettings/FlowRate.tsx +++ b/app/src/organisms/ODD/QuickTransferFlow/QuickTransferAdvancedSettings/FlowRate.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useState, useRef } from 'react' import { createPortal } from 'react-dom' import { useTranslation } from 'react-i18next' @@ -24,6 +24,7 @@ import { useTrackEventWithRobotSerial } from '/app/redux-resources/analytics' import { ACTIONS } from '../constants' +import type { Dispatch } from 'react' import type { SupportedTip } from '@opentrons/shared-data' import type { QuickTransferSummaryState, @@ -34,7 +35,7 @@ import type { interface FlowRateEntryProps { onBack: () => void state: QuickTransferSummaryState - dispatch: React.Dispatch + dispatch: Dispatch kind: FlowRateKind } @@ -42,9 +43,9 @@ export function FlowRateEntry(props: FlowRateEntryProps): JSX.Element { const { onBack, state, dispatch, kind } = props const { i18n, t } = useTranslation(['quick_transfer', 'shared']) const { trackEventWithRobotSerial } = useTrackEventWithRobotSerial() - const keyboardRef = React.useRef(null) + const keyboardRef = useRef(null) - const [flowRate, setFlowRate] = React.useState( + const [flowRate, setFlowRate] = useState( kind === 'aspirate' ? state.aspirateFlowRate : state.dispenseFlowRate ) diff --git a/app/src/organisms/ODD/QuickTransferFlow/QuickTransferAdvancedSettings/Mix.tsx b/app/src/organisms/ODD/QuickTransferFlow/QuickTransferAdvancedSettings/Mix.tsx index 3774662bc38..b6c7862310c 100644 --- a/app/src/organisms/ODD/QuickTransferFlow/QuickTransferAdvancedSettings/Mix.tsx +++ b/app/src/organisms/ODD/QuickTransferFlow/QuickTransferAdvancedSettings/Mix.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useState, useRef } from 'react' import { useTranslation } from 'react-i18next' import { createPortal } from 'react-dom' @@ -8,8 +8,8 @@ import { DIRECTION_COLUMN, Flex, InputField, - RadioButton, POSITION_FIXED, + RadioButton, SPACING, } from '@opentrons/components' @@ -18,19 +18,20 @@ import { getTopPortalEl } from '/app/App/portal' import { ChildNavigation } from '/app/organisms/ODD/ChildNavigation' import { useTrackEventWithRobotSerial } from '/app/redux-resources/analytics' import { ACTIONS } from '../constants' +import { i18n } from '/app/i18n' +import { NumericalKeyboard } from '/app/atoms/SoftwareKeyboard' +import type { Dispatch } from 'react' import type { QuickTransferSummaryState, QuickTransferSummaryAction, FlowRateKind, } from '../types' -import { i18n } from '/app/i18n' -import { NumericalKeyboard } from '/app/atoms/SoftwareKeyboard' interface MixProps { onBack: () => void state: QuickTransferSummaryState - dispatch: React.Dispatch + dispatch: Dispatch kind: FlowRateKind } @@ -38,20 +39,20 @@ export function Mix(props: MixProps): JSX.Element { const { kind, onBack, state, dispatch } = props const { t } = useTranslation('quick_transfer') const { trackEventWithRobotSerial } = useTrackEventWithRobotSerial() - const keyboardRef = React.useRef(null) + const keyboardRef = useRef(null) - const [mixIsEnabled, setMixIsEnabled] = React.useState( + const [mixIsEnabled, setMixIsEnabled] = useState( kind === 'aspirate' ? state.mixOnAspirate != null : state.mixOnDispense != null ) - const [currentStep, setCurrentStep] = React.useState(1) - const [mixVolume, setMixVolume] = React.useState( + const [currentStep, setCurrentStep] = useState(1) + const [mixVolume, setMixVolume] = useState( kind === 'aspirate' ? state.mixOnAspirate?.mixVolume ?? null : state.mixOnDispense?.mixVolume ?? null ) - const [mixReps, setMixReps] = React.useState( + const [mixReps, setMixReps] = useState( kind === 'aspirate' ? state.mixOnAspirate?.repititions ?? null : state.mixOnDispense?.repititions ?? null diff --git a/app/src/organisms/ODD/QuickTransferFlow/QuickTransferAdvancedSettings/PipettePath.tsx b/app/src/organisms/ODD/QuickTransferFlow/QuickTransferAdvancedSettings/PipettePath.tsx index 9db8923bd58..eabbe9fc678 100644 --- a/app/src/organisms/ODD/QuickTransferFlow/QuickTransferAdvancedSettings/PipettePath.tsx +++ b/app/src/organisms/ODD/QuickTransferFlow/QuickTransferAdvancedSettings/PipettePath.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useState, useRef } from 'react' import isEqual from 'lodash/isEqual' import { useTranslation } from 'react-i18next' import { createPortal } from 'react-dom' @@ -9,8 +9,8 @@ import { DIRECTION_COLUMN, Flex, InputField, - RadioButton, POSITION_FIXED, + RadioButton, SPACING, } from '@opentrons/components' @@ -25,6 +25,7 @@ import { ACTIONS } from '../constants' import { i18n } from '/app/i18n' import { NumericalKeyboard } from '/app/atoms/SoftwareKeyboard' +import type { Dispatch } from 'react' import type { PathOption, QuickTransferSummaryState, @@ -35,25 +36,25 @@ import type { interface PipettePathProps { onBack: () => void state: QuickTransferSummaryState - dispatch: React.Dispatch + dispatch: Dispatch } export function PipettePath(props: PipettePathProps): JSX.Element { const { onBack, state, dispatch } = props const { t } = useTranslation('quick_transfer') const { trackEventWithRobotSerial } = useTrackEventWithRobotSerial() - const keyboardRef = React.useRef(null) + const keyboardRef = useRef(null) const deckConfig = useNotifyDeckConfigurationQuery().data ?? [] - const [selectedPath, setSelectedPath] = React.useState(state.path) - const [currentStep, setCurrentStep] = React.useState(1) - const [blowOutLocation, setBlowOutLocation] = React.useState< + const [selectedPath, setSelectedPath] = useState(state.path) + const [currentStep, setCurrentStep] = useState(1) + const [blowOutLocation, setBlowOutLocation] = useState< BlowOutLocation | undefined >(state.blowOut) - const [disposalVolume, setDisposalVolume] = React.useState< - number | undefined - >(state?.disposalVolume) + const [disposalVolume, setDisposalVolume] = useState( + state?.disposalVolume + ) const maxPipetteVolume = Object.values(state.pipette.liquids)[0].maxVolume const tipVolume = Object.values(state.tipRack.wells)[0].totalLiquidVolume diff --git a/app/src/organisms/ODD/QuickTransferFlow/QuickTransferAdvancedSettings/TipPosition.tsx b/app/src/organisms/ODD/QuickTransferFlow/QuickTransferAdvancedSettings/TipPosition.tsx index 92082cf9c7d..8a5abf1a169 100644 --- a/app/src/organisms/ODD/QuickTransferFlow/QuickTransferAdvancedSettings/TipPosition.tsx +++ b/app/src/organisms/ODD/QuickTransferFlow/QuickTransferAdvancedSettings/TipPosition.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useState, useRef } from 'react' import { useTranslation } from 'react-i18next' import { @@ -19,6 +19,7 @@ import { ACTIONS } from '../constants' import { createPortal } from 'react-dom' import { useTrackEventWithRobotSerial } from '/app/redux-resources/analytics' +import type { Dispatch } from 'react' import type { QuickTransferSummaryState, QuickTransferSummaryAction, @@ -28,7 +29,7 @@ import type { interface TipPositionEntryProps { onBack: () => void state: QuickTransferSummaryState - dispatch: React.Dispatch + dispatch: Dispatch kind: FlowRateKind // TODO: rename flowRateKind to be generic } @@ -36,9 +37,9 @@ export function TipPositionEntry(props: TipPositionEntryProps): JSX.Element { const { onBack, state, dispatch, kind } = props const { i18n, t } = useTranslation(['quick_transfer', 'shared']) const { trackEventWithRobotSerial } = useTrackEventWithRobotSerial() - const keyboardRef = React.useRef(null) + const keyboardRef = useRef(null) - const [tipPosition, setTipPosition] = React.useState( + const [tipPosition, setTipPosition] = useState( kind === 'aspirate' ? state.tipPositionAspirate : state.tipPositionDispense ) diff --git a/app/src/organisms/ODD/QuickTransferFlow/QuickTransferAdvancedSettings/TouchTip.tsx b/app/src/organisms/ODD/QuickTransferFlow/QuickTransferAdvancedSettings/TouchTip.tsx index a30b204d4a4..41780d65181 100644 --- a/app/src/organisms/ODD/QuickTransferFlow/QuickTransferAdvancedSettings/TouchTip.tsx +++ b/app/src/organisms/ODD/QuickTransferFlow/QuickTransferAdvancedSettings/TouchTip.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useState, useRef } from 'react' import { useTranslation } from 'react-i18next' import { createPortal } from 'react-dom' @@ -8,8 +8,8 @@ import { DIRECTION_COLUMN, Flex, InputField, - RadioButton, POSITION_FIXED, + RadioButton, SPACING, } from '@opentrons/components' @@ -21,6 +21,7 @@ import { i18n } from '/app/i18n' import { useTrackEventWithRobotSerial } from '/app/redux-resources/analytics' import { NumericalKeyboard } from '/app/atoms/SoftwareKeyboard' +import type { Dispatch } from 'react' import type { QuickTransferSummaryState, QuickTransferSummaryAction, @@ -30,7 +31,7 @@ import type { interface TouchTipProps { onBack: () => void state: QuickTransferSummaryState - dispatch: React.Dispatch + dispatch: Dispatch kind: FlowRateKind } @@ -38,15 +39,15 @@ export function TouchTip(props: TouchTipProps): JSX.Element { const { kind, onBack, state, dispatch } = props const { t } = useTranslation('quick_transfer') const { trackEventWithRobotSerial } = useTrackEventWithRobotSerial() - const keyboardRef = React.useRef(null) + const keyboardRef = useRef(null) - const [touchTipIsEnabled, setTouchTipIsEnabled] = React.useState( + const [touchTipIsEnabled, setTouchTipIsEnabled] = useState( kind === 'aspirate' ? state.touchTipAspirate != null : state.touchTipDispense != null ) - const [currentStep, setCurrentStep] = React.useState(1) - const [position, setPosition] = React.useState( + const [currentStep, setCurrentStep] = useState(1) + const [position, setPosition] = useState( kind === 'aspirate' ? state.touchTipAspirate ?? null : state.touchTipDispense ?? null diff --git a/app/src/organisms/ODD/QuickTransferFlow/QuickTransferAdvancedSettings/index.tsx b/app/src/organisms/ODD/QuickTransferFlow/QuickTransferAdvancedSettings/index.tsx index 2911b7975d1..33a603b6f75 100644 --- a/app/src/organisms/ODD/QuickTransferFlow/QuickTransferAdvancedSettings/index.tsx +++ b/app/src/organisms/ODD/QuickTransferFlow/QuickTransferAdvancedSettings/index.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useState, useEffect } from 'react' import { useTranslation } from 'react-i18next' import { @@ -34,13 +34,14 @@ import { TouchTip } from './TouchTip' import { AirGap } from './AirGap' import { BlowOut } from './BlowOut' +import type { Dispatch } from 'react' import type { QuickTransferSummaryAction, QuickTransferSummaryState, } from '../types' interface QuickTransferAdvancedSettingsProps { state: QuickTransferSummaryState - dispatch: React.Dispatch + dispatch: Dispatch } export function QuickTransferAdvancedSettings( @@ -48,13 +49,11 @@ export function QuickTransferAdvancedSettings( ): JSX.Element | null { const { state, dispatch } = props const { t, i18n } = useTranslation(['quick_transfer', 'shared']) - const [selectedSetting, setSelectedSetting] = React.useState( - null - ) + const [selectedSetting, setSelectedSetting] = useState(null) const { trackEventWithRobotSerial } = useTrackEventWithRobotSerial() const { makeSnackbar } = useToaster() - React.useEffect(() => { + useEffect(() => { trackEventWithRobotSerial({ name: ANALYTICS_QUICK_TRANSFER_ADVANCED_SETTINGS_TAB, properties: {}, diff --git a/app/src/organisms/ODD/QuickTransferFlow/SelectDestLabware.tsx b/app/src/organisms/ODD/QuickTransferFlow/SelectDestLabware.tsx index ba90e54de4d..62921e8e08d 100644 --- a/app/src/organisms/ODD/QuickTransferFlow/SelectDestLabware.tsx +++ b/app/src/organisms/ODD/QuickTransferFlow/SelectDestLabware.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useState } from 'react' import { useTranslation } from 'react-i18next' import { Flex, @@ -15,6 +15,7 @@ import { import { ChildNavigation } from '/app/organisms/ODD/ChildNavigation' import { getCompatibleLabwareByCategory } from './utils' +import type { ComponentProps, Dispatch } from 'react' import type { LabwareDefinition2 } from '@opentrons/shared-data' import type { SmallButton } from '/app/atoms/buttons' import type { LabwareFilter } from '/app/local-resources/labware' @@ -26,9 +27,9 @@ import type { interface SelectDestLabwareProps { onNext: () => void onBack: () => void - exitButtonProps: React.ComponentProps + exitButtonProps: ComponentProps state: QuickTransferWizardState - dispatch: React.Dispatch + dispatch: Dispatch } export function SelectDestLabware( @@ -44,10 +45,8 @@ export function SelectDestLabware( if (state.pipette?.channels === 1) { labwareDisplayCategoryFilters.push('tubeRack') } - const [selectedCategory, setSelectedCategory] = React.useState( - 'all' - ) - const [selectedLabware, setSelectedLabware] = React.useState< + const [selectedCategory, setSelectedCategory] = useState('all') + const [selectedLabware, setSelectedLabware] = useState< LabwareDefinition2 | 'source' | undefined >(state.destination) diff --git a/app/src/organisms/ODD/QuickTransferFlow/SelectDestWells.tsx b/app/src/organisms/ODD/QuickTransferFlow/SelectDestWells.tsx index 0cb402f6ee8..16102451ebc 100644 --- a/app/src/organisms/ODD/QuickTransferFlow/SelectDestWells.tsx +++ b/app/src/organisms/ODD/QuickTransferFlow/SelectDestWells.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useState } from 'react' import { createPortal } from 'react-dom' import { useTranslation } from 'react-i18next' import without from 'lodash/without' @@ -24,6 +24,12 @@ import { RECTANGULAR_WELL_96_PLATE_DEFINITION_URI, } from './SelectSourceWells' +import type { + ComponentProps, + Dispatch, + SetStateAction, + MouseEvent, +} from 'react' import type { SmallButton } from '/app/atoms/buttons' import type { OddModalHeaderBaseProps } from '/app/molecules/OddModal/types' import type { @@ -35,7 +41,7 @@ interface SelectDestWellsProps { onNext: () => void onBack: () => void state: QuickTransferWizardState - dispatch: React.Dispatch + dispatch: Dispatch } export function SelectDestWells(props: SelectDestWellsProps): JSX.Element { @@ -53,12 +59,11 @@ export function SelectDestWells(props: SelectDestWellsProps): JSX.Element { const [ showNumberWellsSelectedErrorModal, setShowNumberWellsSelectedErrorModal, - ] = React.useState(false) - const [selectedWells, setSelectedWells] = React.useState(destinationWellGroup) - const [ - isNumberWellsSelectedError, - setIsNumberWellsSelectedError, - ] = React.useState(false) + ] = useState(false) + const [selectedWells, setSelectedWells] = useState(destinationWellGroup) + const [isNumberWellsSelectedError, setIsNumberWellsSelectedError] = useState( + false + ) const selectedWellCount = Object.keys(selectedWells).length const sourceWellCount = state.sourceWells?.length ?? 0 @@ -88,7 +93,7 @@ export function SelectDestWells(props: SelectDestWellsProps): JSX.Element { } const is384WellPlate = labwareDefinition?.parameters.format === '384Standard' - const [analyticsStartTime] = React.useState(new Date()) + const [analyticsStartTime] = useState(new Date()) const handleClickNext = (): void => { if ( @@ -118,7 +123,7 @@ export function SelectDestWells(props: SelectDestWellsProps): JSX.Element { }) as string, 'error', { - closeButton: true, + buttonText: i18n.format(t('shared:close'), 'capitalize'), disableTimeout: true, displayType: 'odd', linkText: t('learn_more'), @@ -130,10 +135,10 @@ export function SelectDestWells(props: SelectDestWellsProps): JSX.Element { } } - const resetButtonProps: React.ComponentProps = { + const resetButtonProps: ComponentProps = { buttonType: 'tertiaryLowLight', buttonText: t('shared:reset'), - onClick: (e: React.MouseEvent) => { + onClick: (e: MouseEvent) => { setIsNumberWellsSelectedError(false) setSelectedWells({}) e.currentTarget.blur?.() @@ -214,9 +219,7 @@ function NumberWellsSelectedErrorModal({ selectionUnit, selectionUnits, }: { - setShowNumberWellsSelectedErrorModal: React.Dispatch< - React.SetStateAction - > + setShowNumberWellsSelectedErrorModal: Dispatch> wellCount: number selectionUnit: string selectionUnits: string diff --git a/app/src/organisms/ODD/QuickTransferFlow/SelectPipette.tsx b/app/src/organisms/ODD/QuickTransferFlow/SelectPipette.tsx index 3331800e1a9..0b01a93977d 100644 --- a/app/src/organisms/ODD/QuickTransferFlow/SelectPipette.tsx +++ b/app/src/organisms/ODD/QuickTransferFlow/SelectPipette.tsx @@ -1,18 +1,19 @@ -import * as React from 'react' +import { useState } from 'react' import { useTranslation } from 'react-i18next' import { + DIRECTION_COLUMN, Flex, - SPACING, LegacyStyledText, - TYPOGRAPHY, - DIRECTION_COLUMN, RadioButton, + SPACING, + TYPOGRAPHY, } from '@opentrons/components' import { useInstrumentsQuery } from '@opentrons/react-api-client' import { RIGHT, LEFT } from '@opentrons/shared-data' import { usePipetteSpecsV2 } from '/app/local-resources/instruments' import { ChildNavigation } from '/app/organisms/ODD/ChildNavigation' +import type { ComponentProps, Dispatch } from 'react' import type { PipetteData, Mount } from '@opentrons/api-client' import type { SmallButton } from '/app/atoms/buttons' import type { @@ -23,9 +24,9 @@ import type { interface SelectPipetteProps { onNext: () => void onBack: () => void - exitButtonProps: React.ComponentProps + exitButtonProps: ComponentProps state: QuickTransferWizardState - dispatch: React.Dispatch + dispatch: Dispatch } export function SelectPipette(props: SelectPipetteProps): JSX.Element { @@ -44,9 +45,9 @@ export function SelectPipette(props: SelectPipetteProps): JSX.Element { const rightPipetteSpecs = usePipetteSpecsV2(rightPipette?.instrumentModel) // automatically select 96 channel if it is attached - const [selectedPipette, setSelectedPipette] = React.useState< - Mount | undefined - >(leftPipetteSpecs?.channels === 96 ? LEFT : state.mount) + const [selectedPipette, setSelectedPipette] = useState( + leftPipetteSpecs?.channels === 96 ? LEFT : state.mount + ) const handleClickNext = (): void => { const selectedPipetteSpecs = diff --git a/app/src/organisms/ODD/QuickTransferFlow/SelectSourceLabware.tsx b/app/src/organisms/ODD/QuickTransferFlow/SelectSourceLabware.tsx index 2d4752a5aa1..c51e4782c1d 100644 --- a/app/src/organisms/ODD/QuickTransferFlow/SelectSourceLabware.tsx +++ b/app/src/organisms/ODD/QuickTransferFlow/SelectSourceLabware.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useState } from 'react' import { useTranslation } from 'react-i18next' import { Flex, @@ -15,6 +15,7 @@ import { import { ChildNavigation } from '/app/organisms/ODD/ChildNavigation' import { getCompatibleLabwareByCategory } from './utils' +import type { ComponentProps, Dispatch } from 'react' import type { LabwareDefinition2 } from '@opentrons/shared-data' import type { SmallButton } from '/app/atoms/buttons' import type { LabwareFilter } from '/app/local-resources/labware' @@ -26,9 +27,9 @@ import type { interface SelectSourceLabwareProps { onNext: () => void onBack: () => void - exitButtonProps: React.ComponentProps + exitButtonProps: ComponentProps state: QuickTransferWizardState - dispatch: React.Dispatch + dispatch: Dispatch } export function SelectSourceLabware( @@ -44,11 +45,9 @@ export function SelectSourceLabware( if (state.pipette?.channels === 1) { labwareDisplayCategoryFilters.push('tubeRack') } - const [selectedCategory, setSelectedCategory] = React.useState( - 'all' - ) + const [selectedCategory, setSelectedCategory] = useState('all') - const [selectedLabware, setSelectedLabware] = React.useState< + const [selectedLabware, setSelectedLabware] = useState< LabwareDefinition2 | undefined >(state.source) diff --git a/app/src/organisms/ODD/QuickTransferFlow/SelectSourceWells.tsx b/app/src/organisms/ODD/QuickTransferFlow/SelectSourceWells.tsx index a78ec884560..1a643780e08 100644 --- a/app/src/organisms/ODD/QuickTransferFlow/SelectSourceWells.tsx +++ b/app/src/organisms/ODD/QuickTransferFlow/SelectSourceWells.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useState } from 'react' import { useTranslation } from 'react-i18next' import without from 'lodash/without' import { @@ -14,6 +14,7 @@ import { WellSelection } from '/app/organisms/WellSelection' import { ANALYTICS_QUICK_TRANSFER_WELL_SELECTION_DURATION } from '/app/redux/analytics' import { useTrackEventWithRobotSerial } from '/app/redux-resources/analytics' +import type { ComponentProps, Dispatch, MouseEvent } from 'react' import type { SmallButton } from '/app/atoms/buttons' import type { QuickTransferWizardState, @@ -24,7 +25,7 @@ interface SelectSourceWellsProps { onNext: () => void onBack: () => void state: QuickTransferWizardState - dispatch: React.Dispatch + dispatch: Dispatch } export const CIRCULAR_WELL_96_PLATE_DEFINITION_URI = @@ -42,8 +43,8 @@ export function SelectSourceWells(props: SelectSourceWellsProps): JSX.Element { return { ...acc, [well]: null } }, {}) - const [selectedWells, setSelectedWells] = React.useState(sourceWellGroup) - const [startingTimeStamp] = React.useState(new Date()) + const [selectedWells, setSelectedWells] = useState(sourceWellGroup) + const [startingTimeStamp] = useState(new Date()) const is384WellPlate = state.source?.parameters.format === '384Standard' const handleClickNext = (): void => { @@ -62,10 +63,10 @@ export function SelectSourceWells(props: SelectSourceWellsProps): JSX.Element { onNext() } - const resetButtonProps: React.ComponentProps = { + const resetButtonProps: ComponentProps = { buttonType: 'tertiaryLowLight', buttonText: t('shared:reset'), - onClick: (e: React.MouseEvent) => { + onClick: (e: MouseEvent) => { setSelectedWells({}) e.currentTarget.blur?.() }, diff --git a/app/src/organisms/ODD/QuickTransferFlow/SelectTipRack.tsx b/app/src/organisms/ODD/QuickTransferFlow/SelectTipRack.tsx index e8ebd52d90c..f5fd1abae85 100644 --- a/app/src/organisms/ODD/QuickTransferFlow/SelectTipRack.tsx +++ b/app/src/organisms/ODD/QuickTransferFlow/SelectTipRack.tsx @@ -1,14 +1,15 @@ -import * as React from 'react' +import { useState } from 'react' import { useTranslation } from 'react-i18next' import { - Flex, - SPACING, DIRECTION_COLUMN, + Flex, RadioButton, + SPACING, } from '@opentrons/components' import { getAllDefinitions } from '@opentrons/shared-data' import { ChildNavigation } from '/app/organisms/ODD/ChildNavigation' +import type { ComponentProps, Dispatch } from 'react' import type { LabwareDefinition2 } from '@opentrons/shared-data' import type { SmallButton } from '/app/atoms/buttons' import type { @@ -19,9 +20,9 @@ import type { interface SelectTipRackProps { onNext: () => void onBack: () => void - exitButtonProps: React.ComponentProps + exitButtonProps: ComponentProps state: QuickTransferWizardState - dispatch: React.Dispatch + dispatch: Dispatch } export function SelectTipRack(props: SelectTipRackProps): JSX.Element { @@ -32,7 +33,7 @@ export function SelectTipRack(props: SelectTipRackProps): JSX.Element { const selectedPipetteDefaultTipracks = state.pipette?.liquids.default.defaultTipracks ?? [] - const [selectedTipRack, setSelectedTipRack] = React.useState< + const [selectedTipRack, setSelectedTipRack] = useState< LabwareDefinition2 | undefined >(state.tipRack) diff --git a/app/src/organisms/ODD/QuickTransferFlow/SummaryAndSettings.tsx b/app/src/organisms/ODD/QuickTransferFlow/SummaryAndSettings.tsx index 6ddba5ca50e..2567b703b93 100644 --- a/app/src/organisms/ODD/QuickTransferFlow/SummaryAndSettings.tsx +++ b/app/src/organisms/ODD/QuickTransferFlow/SummaryAndSettings.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useState, useReducer } from 'react' import { useTranslation } from 'react-i18next' import { useNavigate } from 'react-router-dom' import { useQueryClient } from 'react-query' @@ -33,11 +33,12 @@ import { SaveOrRunModal } from './SaveOrRunModal' import { getInitialSummaryState, createQuickTransferFile } from './utils' import { quickTransferSummaryReducer } from './reducers' +import type { ComponentProps } from 'react' import type { SmallButton } from '/app/atoms/buttons' import type { QuickTransferWizardState } from './types' interface SummaryAndSettingsProps { - exitButtonProps: React.ComponentProps + exitButtonProps: ComponentProps state: QuickTransferWizardState analyticsStartTime: Date } @@ -51,18 +52,14 @@ export function SummaryAndSettings( const queryClient = useQueryClient() const host = useHost() const { t } = useTranslation(['quick_transfer', 'shared']) - const [showSaveOrRunModal, setShowSaveOrRunModal] = React.useState( - false - ) + const [showSaveOrRunModal, setShowSaveOrRunModal] = useState(false) const displayCategory: string[] = [ 'overview', 'advanced_settings', 'tip_management', ] - const [selectedCategory, setSelectedCategory] = React.useState( - 'overview' - ) + const [selectedCategory, setSelectedCategory] = useState('overview') const deckConfig = useNotifyDeckConfigurationQuery().data ?? [] const initialSummaryState = getInitialSummaryState({ @@ -71,7 +68,7 @@ export function SummaryAndSettings( state: wizardFlowState, deckConfig, }) - const [state, dispatch] = React.useReducer( + const [state, dispatch] = useReducer( quickTransferSummaryReducer, initialSummaryState ) diff --git a/app/src/organisms/ODD/QuickTransferFlow/TipManagement/ChangeTip.tsx b/app/src/organisms/ODD/QuickTransferFlow/TipManagement/ChangeTip.tsx index 7c72dbe202e..dd3869ff329 100644 --- a/app/src/organisms/ODD/QuickTransferFlow/TipManagement/ChangeTip.tsx +++ b/app/src/organisms/ODD/QuickTransferFlow/TipManagement/ChangeTip.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useState } from 'react' import { useTranslation } from 'react-i18next' import { createPortal } from 'react-dom' @@ -16,6 +16,7 @@ import { getTopPortalEl } from '/app/App/portal' import { useTrackEventWithRobotSerial } from '/app/redux-resources/analytics' import { ChildNavigation } from '/app/organisms/ODD/ChildNavigation' +import type { Dispatch } from 'react' import type { ChangeTipOptions, QuickTransferSummaryState, @@ -25,7 +26,7 @@ import type { interface ChangeTipProps { onBack: () => void state: QuickTransferSummaryState - dispatch: React.Dispatch + dispatch: Dispatch } export function ChangeTip(props: ChangeTipProps): JSX.Element { @@ -53,7 +54,7 @@ export function ChangeTip(props: ChangeTipProps): JSX.Element { const [ selectedChangeTipOption, setSelectedChangeTipOption, - ] = React.useState(state.changeTip) + ] = useState(state.changeTip) const handleClickSave = (): void => { if (selectedChangeTipOption !== state.changeTip) { diff --git a/app/src/organisms/ODD/QuickTransferFlow/TipManagement/TipDropLocation.tsx b/app/src/organisms/ODD/QuickTransferFlow/TipManagement/TipDropLocation.tsx index 6dae428684b..b61a73389cd 100644 --- a/app/src/organisms/ODD/QuickTransferFlow/TipManagement/TipDropLocation.tsx +++ b/app/src/organisms/ODD/QuickTransferFlow/TipManagement/TipDropLocation.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useState } from 'react' import { useTranslation } from 'react-i18next' import { createPortal } from 'react-dom' import { @@ -21,6 +21,7 @@ import { useNotifyDeckConfigurationQuery } from '/app/resources/deck_configurati import { useTrackEventWithRobotSerial } from '/app/redux-resources/analytics' import { ChildNavigation } from '/app/organisms/ODD/ChildNavigation' +import type { Dispatch } from 'react' import type { QuickTransferSummaryState, QuickTransferSummaryAction, @@ -30,7 +31,7 @@ import type { CutoutConfig } from '@opentrons/shared-data' interface TipDropLocationProps { onBack: () => void state: QuickTransferSummaryState - dispatch: React.Dispatch + dispatch: Dispatch } export function TipDropLocation(props: TipDropLocationProps): JSX.Element { @@ -56,7 +57,7 @@ export function TipDropLocation(props: TipDropLocationProps): JSX.Element { const [ selectedTipDropLocation, setSelectedTipDropLocation, - ] = React.useState(state.dropTipLocation) + ] = useState(state.dropTipLocation) const handleClickSave = (): void => { if (selectedTipDropLocation.cutoutId !== state.dropTipLocation.cutoutId) { diff --git a/app/src/organisms/ODD/QuickTransferFlow/TipManagement/index.tsx b/app/src/organisms/ODD/QuickTransferFlow/TipManagement/index.tsx index 03f33c965f2..4a87e67c2c7 100644 --- a/app/src/organisms/ODD/QuickTransferFlow/TipManagement/index.tsx +++ b/app/src/organisms/ODD/QuickTransferFlow/TipManagement/index.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useState, useEffect } from 'react' import { useTranslation } from 'react-i18next' import { ALIGN_CENTER, @@ -21,6 +21,7 @@ import { useTrackEventWithRobotSerial } from '/app/redux-resources/analytics' import { ChangeTip } from './ChangeTip' import { TipDropLocation } from './TipDropLocation' +import type { Dispatch } from 'react' import type { QuickTransferSummaryAction, QuickTransferSummaryState, @@ -28,18 +29,16 @@ import type { interface TipManagementProps { state: QuickTransferSummaryState - dispatch: React.Dispatch + dispatch: Dispatch } export function TipManagement(props: TipManagementProps): JSX.Element | null { const { state, dispatch } = props const { t } = useTranslation(['quick_transfer', 'shared']) const { trackEventWithRobotSerial } = useTrackEventWithRobotSerial() - const [selectedSetting, setSelectedSetting] = React.useState( - null - ) + const [selectedSetting, setSelectedSetting] = useState(null) - React.useEffect(() => { + useEffect(() => { trackEventWithRobotSerial({ name: ANALYTICS_QUICK_TRANSFER_TIP_MANAGEMENT_TAB, properties: {}, diff --git a/app/src/organisms/ODD/QuickTransferFlow/VolumeEntry.tsx b/app/src/organisms/ODD/QuickTransferFlow/VolumeEntry.tsx index 0a6526676d6..58bf2f570e7 100644 --- a/app/src/organisms/ODD/QuickTransferFlow/VolumeEntry.tsx +++ b/app/src/organisms/ODD/QuickTransferFlow/VolumeEntry.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useState, useRef } from 'react' import { useTranslation } from 'react-i18next' import { @@ -14,6 +14,7 @@ import { NumericalKeyboard } from '/app/atoms/SoftwareKeyboard' import { getVolumeRange } from './utils' import { CONSOLIDATE, DISTRIBUTE } from './constants' +import type { ComponentProps, Dispatch } from 'react' import type { SmallButton } from '/app/atoms/buttons' import type { QuickTransferWizardState, @@ -23,17 +24,17 @@ import type { interface VolumeEntryProps { onNext: () => void onBack: () => void - exitButtonProps: React.ComponentProps + exitButtonProps: ComponentProps state: QuickTransferWizardState - dispatch: React.Dispatch + dispatch: Dispatch } export function VolumeEntry(props: VolumeEntryProps): JSX.Element { const { onNext, onBack, exitButtonProps, state, dispatch } = props const { i18n, t } = useTranslation(['quick_transfer', 'shared']) - const keyboardRef = React.useRef(null) + const keyboardRef = useRef(null) - const [volume, setVolume] = React.useState( + const [volume, setVolume] = useState( state.volume ? state.volume.toString() : '' ) const volumeRange = getVolumeRange(state) diff --git a/app/src/organisms/ODD/QuickTransferFlow/__tests__/ConfirmExitModal.test.tsx b/app/src/organisms/ODD/QuickTransferFlow/__tests__/ConfirmExitModal.test.tsx index 4b50c31ca29..18fdc854a85 100644 --- a/app/src/organisms/ODD/QuickTransferFlow/__tests__/ConfirmExitModal.test.tsx +++ b/app/src/organisms/ODD/QuickTransferFlow/__tests__/ConfirmExitModal.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { fireEvent, screen } from '@testing-library/react' import { describe, it, expect, afterEach, vi, beforeEach } from 'vitest' @@ -6,14 +5,16 @@ import { renderWithProviders } from '/app/__testing-utils__' import { i18n } from '/app/i18n' import { ConfirmExitModal } from '../ConfirmExitModal' -const render = (props: React.ComponentProps) => { +import type { ComponentProps } from 'react' + +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, }) } describe('ConfirmExitModal', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { diff --git a/app/src/organisms/ODD/QuickTransferFlow/__tests__/CreateNewTransfer.test.tsx b/app/src/organisms/ODD/QuickTransferFlow/__tests__/CreateNewTransfer.test.tsx index 178086ae401..7d134e0e2be 100644 --- a/app/src/organisms/ODD/QuickTransferFlow/__tests__/CreateNewTransfer.test.tsx +++ b/app/src/organisms/ODD/QuickTransferFlow/__tests__/CreateNewTransfer.test.tsx @@ -1,4 +1,3 @@ -import type * 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' @@ -7,6 +6,7 @@ import { renderWithProviders } from '/app/__testing-utils__' import { i18n } from '/app/i18n' import { CreateNewTransfer } from '../CreateNewTransfer' +import type { ComponentProps } from 'react' import type * as OpentronsComponents from '@opentrons/components' vi.mock('@opentrons/components', async importOriginal => { @@ -16,14 +16,14 @@ vi.mock('@opentrons/components', async importOriginal => { DeckConfigurator: vi.fn(), } }) -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, }) } describe('CreateNewTransfer', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { diff --git a/app/src/organisms/ODD/QuickTransferFlow/__tests__/NameQuickTransfer.test.tsx b/app/src/organisms/ODD/QuickTransferFlow/__tests__/NameQuickTransfer.test.tsx index 363c89cdc82..3b247ce2ef8 100644 --- a/app/src/organisms/ODD/QuickTransferFlow/__tests__/NameQuickTransfer.test.tsx +++ b/app/src/organisms/ODD/QuickTransferFlow/__tests__/NameQuickTransfer.test.tsx @@ -1,10 +1,11 @@ -import type * as React from 'react' import { fireEvent, screen } from '@testing-library/react' import { describe, it, expect, afterEach, vi, beforeEach } from 'vitest' import { renderWithProviders } from '/app/__testing-utils__' import { i18n } from '/app/i18n' import { NameQuickTransfer } from '../NameQuickTransfer' + +import type { ComponentProps } from 'react' import type { InputField } from '@opentrons/components' vi.mock('../utils') @@ -17,14 +18,14 @@ vi.mock('/app/atoms/InputField', async importOriginal => { } }) -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, }) } describe('NameQuickTransfer', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { diff --git a/app/src/organisms/ODD/QuickTransferFlow/__tests__/Overview.test.tsx b/app/src/organisms/ODD/QuickTransferFlow/__tests__/Overview.test.tsx index 242b0f58a92..192d378f97c 100644 --- a/app/src/organisms/ODD/QuickTransferFlow/__tests__/Overview.test.tsx +++ b/app/src/organisms/ODD/QuickTransferFlow/__tests__/Overview.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { screen } from '@testing-library/react' import { describe, it, afterEach, vi, beforeEach } from 'vitest' @@ -6,14 +5,16 @@ import { renderWithProviders } from '/app/__testing-utils__' import { i18n } from '/app/i18n' import { Overview } from '../Overview' -const render = (props: React.ComponentProps) => { +import type { ComponentProps } from 'react' + +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, }) } describe('Overview', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { diff --git a/app/src/organisms/ODD/QuickTransferFlow/__tests__/QuickTransferAdvancedSettings/AirGap.test.tsx b/app/src/organisms/ODD/QuickTransferFlow/__tests__/QuickTransferAdvancedSettings/AirGap.test.tsx index a4cc4a2879a..f1e6245389f 100644 --- a/app/src/organisms/ODD/QuickTransferFlow/__tests__/QuickTransferAdvancedSettings/AirGap.test.tsx +++ b/app/src/organisms/ODD/QuickTransferFlow/__tests__/QuickTransferAdvancedSettings/AirGap.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { fireEvent, screen } from '@testing-library/react' import { describe, it, expect, afterEach, vi, beforeEach } from 'vitest' @@ -8,6 +7,8 @@ import { renderWithProviders } from '/app/__testing-utils__' import { i18n } from '/app/i18n' import { useTrackEventWithRobotSerial } from '/app/redux-resources/analytics' import { AirGap } from '../../QuickTransferAdvancedSettings/AirGap' + +import type { ComponentProps } from 'react' import type { QuickTransferSummaryState } from '../../types' vi.mock('/app/redux-resources/analytics') @@ -21,7 +22,7 @@ vi.mock('@opentrons/components', async importOriginal => { } }) -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, }) @@ -29,7 +30,7 @@ const render = (props: React.ComponentProps) => { let mockTrackEventWithRobotSerial: any describe('AirGap', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { diff --git a/app/src/organisms/ODD/QuickTransferFlow/__tests__/QuickTransferAdvancedSettings/BlowOut.test.tsx b/app/src/organisms/ODD/QuickTransferFlow/__tests__/QuickTransferAdvancedSettings/BlowOut.test.tsx index c75788ac8cd..6921e9daea7 100644 --- a/app/src/organisms/ODD/QuickTransferFlow/__tests__/QuickTransferAdvancedSettings/BlowOut.test.tsx +++ b/app/src/organisms/ODD/QuickTransferFlow/__tests__/QuickTransferAdvancedSettings/BlowOut.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { fireEvent, screen } from '@testing-library/react' import { describe, it, expect, afterEach, vi, beforeEach } from 'vitest' @@ -7,13 +6,15 @@ import { i18n } from '/app/i18n' import { useNotifyDeckConfigurationQuery } from '/app/resources/deck_configuration' import { useTrackEventWithRobotSerial } from '/app/redux-resources/analytics' import { BlowOut } from '../../QuickTransferAdvancedSettings/BlowOut' + +import type { ComponentProps } from 'react' import type { QuickTransferSummaryState } from '../../types' vi.mock('/app/resources/deck_configuration') vi.mock('/app/redux-resources/analytics') vi.mock('../utils') -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, }) @@ -21,7 +22,7 @@ const render = (props: React.ComponentProps) => { let mockTrackEventWithRobotSerial: any describe('BlowOut', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { diff --git a/app/src/organisms/ODD/QuickTransferFlow/__tests__/QuickTransferAdvancedSettings/Delay.test.tsx b/app/src/organisms/ODD/QuickTransferFlow/__tests__/QuickTransferAdvancedSettings/Delay.test.tsx index 957f3eb6e62..32b26e4712f 100644 --- a/app/src/organisms/ODD/QuickTransferFlow/__tests__/QuickTransferAdvancedSettings/Delay.test.tsx +++ b/app/src/organisms/ODD/QuickTransferFlow/__tests__/QuickTransferAdvancedSettings/Delay.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { fireEvent, screen } from '@testing-library/react' import { describe, it, expect, afterEach, vi, beforeEach } from 'vitest' @@ -8,6 +7,8 @@ import { renderWithProviders } from '/app/__testing-utils__' import { i18n } from '/app/i18n' import { useTrackEventWithRobotSerial } from '/app/redux-resources/analytics' import { Delay } from '../../QuickTransferAdvancedSettings/Delay' + +import type { ComponentProps } from 'react' import type { QuickTransferSummaryState } from '../../types' vi.mock('/app/redux-resources/analytics') @@ -21,7 +22,7 @@ vi.mock('@opentrons/components', async importOriginal => { } }) -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, }) @@ -29,7 +30,7 @@ const render = (props: React.ComponentProps) => { let mockTrackEventWithRobotSerial: any describe('Delay', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { diff --git a/app/src/organisms/ODD/QuickTransferFlow/__tests__/QuickTransferAdvancedSettings/FlowRate.test.tsx b/app/src/organisms/ODD/QuickTransferFlow/__tests__/QuickTransferAdvancedSettings/FlowRate.test.tsx index 4b01bb52ebe..413af34ce99 100644 --- a/app/src/organisms/ODD/QuickTransferFlow/__tests__/QuickTransferAdvancedSettings/FlowRate.test.tsx +++ b/app/src/organisms/ODD/QuickTransferFlow/__tests__/QuickTransferAdvancedSettings/FlowRate.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { fireEvent, screen } from '@testing-library/react' import { describe, it, expect, afterEach, vi, beforeEach } from 'vitest' @@ -8,6 +7,8 @@ import { renderWithProviders } from '/app/__testing-utils__' import { i18n } from '/app/i18n' import { useTrackEventWithRobotSerial } from '/app/redux-resources/analytics' import { FlowRateEntry } from '../../QuickTransferAdvancedSettings/FlowRate' + +import type { ComponentProps } from 'react' import type { QuickTransferSummaryState } from '../../types' vi.mock('/app/redux-resources/analytics') @@ -21,7 +22,7 @@ vi.mock('@opentrons/components', async importOriginal => { } }) -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, }) @@ -29,7 +30,7 @@ const render = (props: React.ComponentProps) => { let mockTrackEventWithRobotSerial: any describe('FlowRate', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { diff --git a/app/src/organisms/ODD/QuickTransferFlow/__tests__/QuickTransferAdvancedSettings/Mix.test.tsx b/app/src/organisms/ODD/QuickTransferFlow/__tests__/QuickTransferAdvancedSettings/Mix.test.tsx index c4d1c170be3..298bd040f1c 100644 --- a/app/src/organisms/ODD/QuickTransferFlow/__tests__/QuickTransferAdvancedSettings/Mix.test.tsx +++ b/app/src/organisms/ODD/QuickTransferFlow/__tests__/QuickTransferAdvancedSettings/Mix.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { fireEvent, screen } from '@testing-library/react' import { describe, it, expect, afterEach, vi, beforeEach } from 'vitest' @@ -8,6 +7,8 @@ import { renderWithProviders } from '/app/__testing-utils__' import { i18n } from '/app/i18n' import { useTrackEventWithRobotSerial } from '/app/redux-resources/analytics' import { Mix } from '../../QuickTransferAdvancedSettings/Mix' + +import type { ComponentProps } from 'react' import type { QuickTransferSummaryState } from '../../types' vi.mock('/app/redux-resources/analytics') @@ -21,7 +22,7 @@ vi.mock('@opentrons/components', async importOriginal => { } }) -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, }) @@ -29,7 +30,7 @@ const render = (props: React.ComponentProps) => { let mockTrackEventWithRobotSerial: any describe('Mix', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { diff --git a/app/src/organisms/ODD/QuickTransferFlow/__tests__/QuickTransferAdvancedSettings/PipettePath.test.tsx b/app/src/organisms/ODD/QuickTransferFlow/__tests__/QuickTransferAdvancedSettings/PipettePath.test.tsx index e62571bdc6a..536c14bbbfd 100644 --- a/app/src/organisms/ODD/QuickTransferFlow/__tests__/QuickTransferAdvancedSettings/PipettePath.test.tsx +++ b/app/src/organisms/ODD/QuickTransferFlow/__tests__/QuickTransferAdvancedSettings/PipettePath.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { fireEvent, screen } from '@testing-library/react' import { describe, it, expect, afterEach, vi, beforeEach } from 'vitest' @@ -9,6 +8,8 @@ import { i18n } from '/app/i18n' import { useTrackEventWithRobotSerial } from '/app/redux-resources/analytics' import { PipettePath } from '../../QuickTransferAdvancedSettings/PipettePath' import { useBlowOutLocationOptions } from '../../QuickTransferAdvancedSettings/BlowOut' + +import type { ComponentProps } from 'react' import type { QuickTransferSummaryState } from '../../types' vi.mock('/app/redux-resources/analytics') @@ -23,7 +24,7 @@ vi.mock('@opentrons/components', async importOriginal => { } }) -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, }) @@ -31,7 +32,7 @@ const render = (props: React.ComponentProps) => { let mockTrackEventWithRobotSerial: any describe('PipettePath', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { diff --git a/app/src/organisms/ODD/QuickTransferFlow/__tests__/QuickTransferAdvancedSettings/QuickTransferAdvancedSettings.test.tsx b/app/src/organisms/ODD/QuickTransferFlow/__tests__/QuickTransferAdvancedSettings/QuickTransferAdvancedSettings.test.tsx index 64e400f5a10..aac998fc8dd 100644 --- a/app/src/organisms/ODD/QuickTransferFlow/__tests__/QuickTransferAdvancedSettings/QuickTransferAdvancedSettings.test.tsx +++ b/app/src/organisms/ODD/QuickTransferFlow/__tests__/QuickTransferAdvancedSettings/QuickTransferAdvancedSettings.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { fireEvent, screen } from '@testing-library/react' import { describe, it, expect, afterEach, vi, beforeEach } from 'vitest' @@ -16,6 +15,8 @@ import { TouchTip } from '../../QuickTransferAdvancedSettings/TouchTip' import { AirGap } from '../../QuickTransferAdvancedSettings/AirGap' import { BlowOut } from '../../QuickTransferAdvancedSettings/BlowOut' +import type { ComponentProps } from 'react' + vi.mock('/app/redux-resources/analytics') vi.mock('/app/organisms/ToasterOven') vi.mock('../../QuickTransferAdvancedSettings/PipettePath') @@ -28,7 +29,7 @@ vi.mock('../../QuickTransferAdvancedSettings/AirGap') vi.mock('../../QuickTransferAdvancedSettings/BlowOut') const render = ( - props: React.ComponentProps + props: ComponentProps ): any => { return renderWithProviders(, { i18nInstance: i18n, @@ -38,7 +39,7 @@ let mockTrackEventWithRobotSerial: any let mockMakeSnackbar: any describe('QuickTransferAdvancedSettings', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { diff --git a/app/src/organisms/ODD/QuickTransferFlow/__tests__/QuickTransferAdvancedSettings/TipPosition.test.tsx b/app/src/organisms/ODD/QuickTransferFlow/__tests__/QuickTransferAdvancedSettings/TipPosition.test.tsx index dc109c2c302..02e5022785c 100644 --- a/app/src/organisms/ODD/QuickTransferFlow/__tests__/QuickTransferAdvancedSettings/TipPosition.test.tsx +++ b/app/src/organisms/ODD/QuickTransferFlow/__tests__/QuickTransferAdvancedSettings/TipPosition.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { fireEvent, screen } from '@testing-library/react' import { describe, it, expect, afterEach, vi, beforeEach } from 'vitest' @@ -8,6 +7,8 @@ import { renderWithProviders } from '/app/__testing-utils__' import { i18n } from '/app/i18n' import { useTrackEventWithRobotSerial } from '/app/redux-resources/analytics' import { TipPositionEntry } from '../../QuickTransferAdvancedSettings/TipPosition' + +import type { ComponentProps } from 'react' import type { QuickTransferSummaryState } from '../../types' vi.mock('/app/redux-resources/analytics') @@ -21,7 +22,7 @@ vi.mock('@opentrons/components', async importOriginal => { } }) -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, }) @@ -29,7 +30,7 @@ const render = (props: React.ComponentProps) => { let mockTrackEventWithRobotSerial: any describe('TipPosition', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { diff --git a/app/src/organisms/ODD/QuickTransferFlow/__tests__/QuickTransferAdvancedSettings/TouchTip.test.tsx b/app/src/organisms/ODD/QuickTransferFlow/__tests__/QuickTransferAdvancedSettings/TouchTip.test.tsx index cc30db0a54f..a5338c8aa71 100644 --- a/app/src/organisms/ODD/QuickTransferFlow/__tests__/QuickTransferAdvancedSettings/TouchTip.test.tsx +++ b/app/src/organisms/ODD/QuickTransferFlow/__tests__/QuickTransferAdvancedSettings/TouchTip.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { fireEvent, screen } from '@testing-library/react' import { describe, it, expect, afterEach, vi, beforeEach } from 'vitest' @@ -8,6 +7,8 @@ import { renderWithProviders } from '/app/__testing-utils__' import { i18n } from '/app/i18n' import { useTrackEventWithRobotSerial } from '/app/redux-resources/analytics' import { TouchTip } from '../../QuickTransferAdvancedSettings/TouchTip' + +import type { ComponentProps } from 'react' import type { QuickTransferSummaryState } from '../../types' vi.mock('/app/redux-resources/analytics') @@ -21,7 +22,7 @@ vi.mock('@opentrons/components', async importOriginal => { } }) -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, }) @@ -29,7 +30,7 @@ const render = (props: React.ComponentProps) => { let mockTrackEventWithRobotSerial: any describe('TouchTip', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { diff --git a/app/src/organisms/ODD/QuickTransferFlow/__tests__/SelectDestLabware.test.tsx b/app/src/organisms/ODD/QuickTransferFlow/__tests__/SelectDestLabware.test.tsx index 6448542c14e..38007f7cc58 100644 --- a/app/src/organisms/ODD/QuickTransferFlow/__tests__/SelectDestLabware.test.tsx +++ b/app/src/organisms/ODD/QuickTransferFlow/__tests__/SelectDestLabware.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { fireEvent, screen } from '@testing-library/react' import { describe, it, expect, afterEach, vi, beforeEach } from 'vitest' @@ -6,15 +5,17 @@ import { renderWithProviders } from '/app/__testing-utils__' import { i18n } from '/app/i18n' import { SelectDestLabware } from '../SelectDestLabware' +import type { ComponentProps } from 'react' + vi.mock('@opentrons/react-api-client') -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, }) } describe('SelectDestLabware', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { diff --git a/app/src/organisms/ODD/QuickTransferFlow/__tests__/SelectPipette.test.tsx b/app/src/organisms/ODD/QuickTransferFlow/__tests__/SelectPipette.test.tsx index e32f7645593..b614041b127 100644 --- a/app/src/organisms/ODD/QuickTransferFlow/__tests__/SelectPipette.test.tsx +++ b/app/src/organisms/ODD/QuickTransferFlow/__tests__/SelectPipette.test.tsx @@ -1,4 +1,3 @@ -import type * 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' @@ -8,17 +7,19 @@ import { i18n } from '/app/i18n' import { useIsOEMMode } from '/app/resources/robot-settings/hooks' import { SelectPipette } from '../SelectPipette' +import type { ComponentProps } from 'react' + vi.mock('@opentrons/react-api-client') vi.mock('/app/resources/robot-settings/hooks') -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, }) } describe('SelectPipette', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { diff --git a/app/src/organisms/ODD/QuickTransferFlow/__tests__/SelectSourceLabware.test.tsx b/app/src/organisms/ODD/QuickTransferFlow/__tests__/SelectSourceLabware.test.tsx index 73121ea7b2d..99e53fab15b 100644 --- a/app/src/organisms/ODD/QuickTransferFlow/__tests__/SelectSourceLabware.test.tsx +++ b/app/src/organisms/ODD/QuickTransferFlow/__tests__/SelectSourceLabware.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { fireEvent, screen } from '@testing-library/react' import { describe, it, expect, afterEach, vi, beforeEach } from 'vitest' @@ -6,15 +5,17 @@ import { renderWithProviders } from '/app/__testing-utils__' import { i18n } from '/app/i18n' import { SelectSourceLabware } from '../SelectSourceLabware' +import type { ComponentProps } from 'react' + vi.mock('@opentrons/react-api-client') -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, }) } describe('SelectSourceLabware', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { diff --git a/app/src/organisms/ODD/QuickTransferFlow/__tests__/SelectTipRack.test.tsx b/app/src/organisms/ODD/QuickTransferFlow/__tests__/SelectTipRack.test.tsx index f4ee22bd2b9..1489e744ec6 100644 --- a/app/src/organisms/ODD/QuickTransferFlow/__tests__/SelectTipRack.test.tsx +++ b/app/src/organisms/ODD/QuickTransferFlow/__tests__/SelectTipRack.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { fireEvent, screen } from '@testing-library/react' import { describe, it, expect, afterEach, vi, beforeEach } from 'vitest' @@ -6,15 +5,17 @@ import { renderWithProviders } from '/app/__testing-utils__' import { i18n } from '/app/i18n' import { SelectTipRack } from '../SelectTipRack' +import type { ComponentProps } from 'react' + vi.mock('@opentrons/react-api-client') -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, }) } describe('SelectTipRack', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { diff --git a/app/src/organisms/ODD/QuickTransferFlow/__tests__/SummaryAndSettings.test.tsx b/app/src/organisms/ODD/QuickTransferFlow/__tests__/SummaryAndSettings.test.tsx index 0f5f7c7742c..246fa343260 100644 --- a/app/src/organisms/ODD/QuickTransferFlow/__tests__/SummaryAndSettings.test.tsx +++ b/app/src/organisms/ODD/QuickTransferFlow/__tests__/SummaryAndSettings.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { fireEvent, screen } from '@testing-library/react' import { describe, it, expect, afterEach, vi, beforeEach } from 'vitest' @@ -16,6 +15,8 @@ import { useTrackEventWithRobotSerial } from '/app/redux-resources/analytics' import { SummaryAndSettings } from '../SummaryAndSettings' import { NameQuickTransfer } from '../NameQuickTransfer' import { Overview } from '../Overview' + +import type { ComponentProps } from 'react' import type { NavigateFunction } from 'react-router-dom' const mockNavigate = vi.fn() @@ -41,7 +42,7 @@ vi.mock('../utils/createQuickTransferFile') vi.mock('@opentrons/react-api-client') vi.mock('/app/resources/deck_configuration') -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, }) @@ -49,7 +50,7 @@ const render = (props: React.ComponentProps) => { let mockTrackEventWithRobotSerial: any describe('SummaryAndSettings', () => { - let props: React.ComponentProps + let props: ComponentProps const createProtocol = vi.fn() const createRun = vi.fn() diff --git a/app/src/organisms/ODD/QuickTransferFlow/__tests__/TipManagement/ChangeTip.test.tsx b/app/src/organisms/ODD/QuickTransferFlow/__tests__/TipManagement/ChangeTip.test.tsx index 213633678e5..cfe5b0a5086 100644 --- a/app/src/organisms/ODD/QuickTransferFlow/__tests__/TipManagement/ChangeTip.test.tsx +++ b/app/src/organisms/ODD/QuickTransferFlow/__tests__/TipManagement/ChangeTip.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { fireEvent, screen } from '@testing-library/react' import { describe, it, expect, afterEach, vi, beforeEach } from 'vitest' @@ -8,9 +7,11 @@ import { ANALYTICS_QUICK_TRANSFER_SETTING_SAVED } from '/app/redux/analytics' import { useTrackEventWithRobotSerial } from '/app/redux-resources/analytics' import { ChangeTip } from '../../TipManagement/ChangeTip' +import type { ComponentProps } from 'react' + vi.mock('/app/redux-resources/analytics') -const render = (props: React.ComponentProps): any => { +const render = (props: ComponentProps): any => { return renderWithProviders(, { i18nInstance: i18n, }) @@ -19,7 +20,7 @@ const render = (props: React.ComponentProps): any => { let mockTrackEventWithRobotSerial: any describe('ChangeTip', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { diff --git a/app/src/organisms/ODD/QuickTransferFlow/__tests__/TipManagement/TipDropLocation.test.tsx b/app/src/organisms/ODD/QuickTransferFlow/__tests__/TipManagement/TipDropLocation.test.tsx index aed3d143b31..712e7e2217e 100644 --- a/app/src/organisms/ODD/QuickTransferFlow/__tests__/TipManagement/TipDropLocation.test.tsx +++ b/app/src/organisms/ODD/QuickTransferFlow/__tests__/TipManagement/TipDropLocation.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { fireEvent, screen } from '@testing-library/react' import { describe, it, expect, afterEach, vi, beforeEach } from 'vitest' @@ -9,10 +8,12 @@ import { ANALYTICS_QUICK_TRANSFER_SETTING_SAVED } from '/app/redux/analytics' import { useTrackEventWithRobotSerial } from '/app/redux-resources/analytics' import { TipDropLocation } from '../../TipManagement/TipDropLocation' +import type { ComponentProps } from 'react' + vi.mock('/app/resources/deck_configuration') vi.mock('/app/redux-resources/analytics') -const render = (props: React.ComponentProps): any => { +const render = (props: ComponentProps): any => { return renderWithProviders(, { i18nInstance: i18n, }) @@ -20,7 +21,7 @@ const render = (props: React.ComponentProps): any => { let mockTrackEventWithRobotSerial: any describe('TipDropLocation', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { diff --git a/app/src/organisms/ODD/QuickTransferFlow/__tests__/TipManagement/TipManagement.test.tsx b/app/src/organisms/ODD/QuickTransferFlow/__tests__/TipManagement/TipManagement.test.tsx index 618153a8b53..61f38149a99 100644 --- a/app/src/organisms/ODD/QuickTransferFlow/__tests__/TipManagement/TipManagement.test.tsx +++ b/app/src/organisms/ODD/QuickTransferFlow/__tests__/TipManagement/TipManagement.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { fireEvent, screen } from '@testing-library/react' import { describe, it, expect, afterEach, vi, beforeEach } from 'vitest' @@ -10,11 +9,13 @@ import { ChangeTip } from '../../TipManagement/ChangeTip' import { TipDropLocation } from '../../TipManagement/TipDropLocation' import { TipManagement } from '../../TipManagement/' +import type { ComponentProps } from 'react' + vi.mock('../../TipManagement/ChangeTip') vi.mock('../../TipManagement/TipDropLocation') vi.mock('/app/redux-resources/analytics') -const render = (props: React.ComponentProps): any => { +const render = (props: ComponentProps): any => { return renderWithProviders(, { i18nInstance: i18n, }) @@ -22,7 +23,7 @@ const render = (props: React.ComponentProps): any => { let mockTrackEventWithRobotSerial: any describe('TipManagement', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { diff --git a/app/src/organisms/ODD/QuickTransferFlow/__tests__/VolumeEntry.test.tsx b/app/src/organisms/ODD/QuickTransferFlow/__tests__/VolumeEntry.test.tsx index 8a14b9a5993..e96ea2515cd 100644 --- a/app/src/organisms/ODD/QuickTransferFlow/__tests__/VolumeEntry.test.tsx +++ b/app/src/organisms/ODD/QuickTransferFlow/__tests__/VolumeEntry.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { fireEvent, screen } from '@testing-library/react' import { describe, it, expect, afterEach, vi, beforeEach } from 'vitest' @@ -10,6 +9,8 @@ import { NumericalKeyboard } from '/app/atoms/SoftwareKeyboard' import { getVolumeRange } from '../utils' import { VolumeEntry } from '../VolumeEntry' +import type { ComponentProps } from 'react' + vi.mock('/app/atoms/SoftwareKeyboard') vi.mock('../utils') @@ -21,14 +22,14 @@ vi.mock('@opentrons/components', async importOriginal => { } }) -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, }) } describe('VolumeEntry', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { diff --git a/app/src/organisms/ODD/QuickTransferFlow/index.tsx b/app/src/organisms/ODD/QuickTransferFlow/index.tsx index f7d94a46000..fb0a76b6f47 100644 --- a/app/src/organisms/ODD/QuickTransferFlow/index.tsx +++ b/app/src/organisms/ODD/QuickTransferFlow/index.tsx @@ -1,10 +1,10 @@ -import * as React from 'react' +import { useState, useReducer } from 'react' import { useNavigate } from 'react-router-dom' import { useTranslation } from 'react-i18next' import { - useConditionalConfirm, - StepMeter, POSITION_STICKY, + StepMeter, + useConditionalConfirm, } from '@opentrons/components' import { ANALYTICS_QUICK_TRANSFER_EXIT_EARLY } from '/app/redux/analytics' import { useTrackEventWithRobotSerial } from '/app/redux-resources/analytics' @@ -20,6 +20,7 @@ import { VolumeEntry } from './VolumeEntry' import { SummaryAndSettings } from './SummaryAndSettings' import { quickTransferWizardReducer } from './reducers' +import type { ComponentProps } from 'react' import type { SmallButton } from '/app/atoms/buttons' import type { QuickTransferWizardState } from './types' @@ -30,13 +31,13 @@ export const QuickTransferFlow = (): JSX.Element => { const navigate = useNavigate() const { i18n, t } = useTranslation(['quick_transfer', 'shared']) const { trackEventWithRobotSerial } = useTrackEventWithRobotSerial() - const [state, dispatch] = React.useReducer( + const [state, dispatch] = useReducer( quickTransferWizardReducer, initialQuickTransferState ) - const [currentStep, setCurrentStep] = React.useState(0) + const [currentStep, setCurrentStep] = useState(0) - const [analyticsStartTime] = React.useState(new Date()) + const [analyticsStartTime] = useState(new Date()) const { confirm: confirmExit, @@ -52,7 +53,7 @@ export const QuickTransferFlow = (): JSX.Element => { navigate('/quick-transfer') }, true) - const exitButtonProps: React.ComponentProps = { + const exitButtonProps: ComponentProps = { buttonType: 'tertiaryLowLight', buttonText: i18n.format(t('shared:exit'), 'capitalize'), onClick: confirmExit, diff --git a/app/src/organisms/ODD/QuickTransferFlow/utils/getCompatibleLabwareByCategory.ts b/app/src/organisms/ODD/QuickTransferFlow/utils/getCompatibleLabwareByCategory.ts index cb52a095a33..dd0c0ff7182 100644 --- a/app/src/organisms/ODD/QuickTransferFlow/utils/getCompatibleLabwareByCategory.ts +++ b/app/src/organisms/ODD/QuickTransferFlow/utils/getCompatibleLabwareByCategory.ts @@ -1,4 +1,7 @@ -import { getAllDefinitions } from '@opentrons/shared-data' +import { + getAllDefinitions, + LABWAREV2_DO_NOT_LIST, +} from '@opentrons/shared-data' import { SINGLE_CHANNEL_COMPATIBLE_LABWARE, EIGHT_CHANNEL_COMPATIBLE_LABWARE, @@ -12,7 +15,7 @@ export function getCompatibleLabwareByCategory( pipetteChannels: 1 | 8 | 96, category: LabwareFilter ): LabwareDefinition2[] | undefined { - const allLabwareDefinitions = getAllDefinitions() + const allLabwareDefinitions = getAllDefinitions(LABWAREV2_DO_NOT_LIST) let compatibleLabwareUris: string[] = SINGLE_CHANNEL_COMPATIBLE_LABWARE if (pipetteChannels === 8) { compatibleLabwareUris = EIGHT_CHANNEL_COMPATIBLE_LABWARE diff --git a/app/src/organisms/ODD/RobotDashboard/RecentRunProtocolCard.tsx b/app/src/organisms/ODD/RobotDashboard/RecentRunProtocolCard.tsx index e522fb1dae7..a91bb4fbda1 100644 --- a/app/src/organisms/ODD/RobotDashboard/RecentRunProtocolCard.tsx +++ b/app/src/organisms/ODD/RobotDashboard/RecentRunProtocolCard.tsx @@ -158,15 +158,22 @@ export function ProtocolWithLastRun({ [RUN_STATUS_SUCCEEDED]: t('completed'), [RUN_STATUS_FAILED]: t('failed'), } - // TODO(BC, 2023-06-05): see if addSuffix false allow can remove usage of .replace here - const formattedLastRunTime = formatDistance( - // Fallback to current date if completedAt is null, though this should never happen since runs must be completed to appear in dashboard - new Date(runData.completedAt ?? new Date()), - new Date(), - { - addSuffix: true, + const formattedLastRunTime = + runData.completedAt != null + ? formatDistance(new Date(runData.completedAt), new Date(), { + addSuffix: true, + }).replace('about ', '') + : null + const buildLastRunCopy = (): string => { + if (formattedLastRunTime != null) { + return i18n.format( + `${terminationTypeMap[runData.status] ?? ''} ${formattedLastRunTime}`, + 'capitalize' + ) + } else { + return '' } - ).replace('about ', '') + } return isProtocolFetching || isLookingForHardware ? ( - {i18n.format( - `${terminationTypeMap[runData.status] ?? ''} ${formattedLastRunTime}`, - 'capitalize' - )} + {buildLastRunCopy()} ) diff --git a/app/src/organisms/ODD/RobotDashboard/__tests__/RecentRunProtocolCard.test.tsx b/app/src/organisms/ODD/RobotDashboard/__tests__/RecentRunProtocolCard.test.tsx index 10ee119176e..cb1b541b39b 100644 --- a/app/src/organisms/ODD/RobotDashboard/__tests__/RecentRunProtocolCard.test.tsx +++ b/app/src/organisms/ODD/RobotDashboard/__tests__/RecentRunProtocolCard.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { fireEvent, screen } from '@testing-library/react' import { formatDistance } from 'date-fns' import { MemoryRouter } from 'react-router-dom' @@ -26,6 +25,7 @@ import { useCloneRun, useNotifyAllRunsQuery } from '/app/resources/runs' import { useRerunnableStatusText } from '../hooks' import { RecentRunProtocolCard } from '../' +import type { ComponentProps } from 'react' import type { NavigateFunction } from 'react-router-dom' import type { ProtocolHardware } from '/app/transformations/commands' @@ -103,7 +103,7 @@ const mockBadRunData = { const mockCloneRun = vi.fn() -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders( @@ -120,7 +120,7 @@ const mockTrackProtocolRunEvent = vi.fn( ) describe('RecentRunProtocolCard', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { diff --git a/app/src/organisms/ODD/RobotDashboard/__tests__/RecentRunProtocolCarousel.test.tsx b/app/src/organisms/ODD/RobotDashboard/__tests__/RecentRunProtocolCarousel.test.tsx index 277edf80d87..31026884d3c 100644 --- a/app/src/organisms/ODD/RobotDashboard/__tests__/RecentRunProtocolCarousel.test.tsx +++ b/app/src/organisms/ODD/RobotDashboard/__tests__/RecentRunProtocolCarousel.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { screen } from '@testing-library/react' import { beforeEach, describe, it, vi } from 'vitest' @@ -6,6 +5,7 @@ import { renderWithProviders } from '/app/__testing-utils__' import { useNotifyAllRunsQuery } from '/app/resources/runs' import { RecentRunProtocolCard, RecentRunProtocolCarousel } from '..' +import type { ComponentProps } from 'react' import type { RunData } from '@opentrons/api-client' vi.mock('@opentrons/react-api-client') @@ -30,14 +30,12 @@ const mockRun = { runTimeParameters: [], } -const render = ( - props: React.ComponentProps -) => { +const render = (props: ComponentProps) => { return renderWithProviders() } describe('RecentRunProtocolCarousel', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { diff --git a/app/src/organisms/ODD/RobotDashboard/hooks/__tests__/useHardwareStatusText.test.tsx b/app/src/organisms/ODD/RobotDashboard/hooks/__tests__/useHardwareStatusText.test.tsx index f209e69f5ec..996e00d56c7 100644 --- a/app/src/organisms/ODD/RobotDashboard/hooks/__tests__/useHardwareStatusText.test.tsx +++ b/app/src/organisms/ODD/RobotDashboard/hooks/__tests__/useHardwareStatusText.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { I18nextProvider } from 'react-i18next' import { renderHook } from '@testing-library/react' import { beforeEach, describe, expect, it, vi } from 'vitest' @@ -7,10 +6,12 @@ import { i18n } from '/app/i18n' import { useFeatureFlag } from '/app/redux/config' import { useHardwareStatusText } from '..' +import type { FunctionComponent, ReactNode } from 'react' + vi.mock('/app/redux/config') describe('useHardwareStatusText', () => { - let wrapper: React.FunctionComponent<{ children: React.ReactNode }> + let wrapper: FunctionComponent<{ children: ReactNode }> beforeEach(() => { wrapper = ({ children }) => ( {children} diff --git a/app/src/organisms/ODD/RobotSettingsDashboard/LanguageSetting.tsx b/app/src/organisms/ODD/RobotSettingsDashboard/LanguageSetting.tsx index a935a5571ad..50af850a44f 100644 --- a/app/src/organisms/ODD/RobotSettingsDashboard/LanguageSetting.tsx +++ b/app/src/organisms/ODD/RobotSettingsDashboard/LanguageSetting.tsx @@ -1,7 +1,8 @@ -import * as React from 'react' +import { Fragment, useEffect } from 'react' import { useTranslation } from 'react-i18next' import { useDispatch, useSelector } from 'react-redux' import styled from 'styled-components' +import uuidv1 from 'uuid/v4' import { BORDERS, @@ -14,9 +15,12 @@ import { } from '@opentrons/components' import { LANGUAGES } from '/app/i18n' +import { ANALYTICS_LANGUAGE_UPDATED_ODD_SETTINGS } from '/app/redux/analytics' +import { useTrackEventWithRobotSerial } from '/app/redux-resources/analytics' import { ChildNavigation } from '/app/organisms/ODD/ChildNavigation' import { getAppLanguage, updateConfigValue } from '/app/redux/config' +import type { ChangeEvent } from 'react' import type { Dispatch } from '/app/redux/types' import type { SetSettingOption } from './types' @@ -41,16 +45,31 @@ interface LanguageSettingProps { setCurrentOption: SetSettingOption } +const uuid: () => string = uuidv1 + export function LanguageSetting({ setCurrentOption, }: LanguageSettingProps): JSX.Element { const { t } = useTranslation('app_settings') const dispatch = useDispatch() + const { trackEventWithRobotSerial } = useTrackEventWithRobotSerial() + + let transactionId = '' + useEffect(() => { + transactionId = uuid() + }, []) const appLanguage = useSelector(getAppLanguage) - const handleChange = (event: React.ChangeEvent): void => { + const handleChange = (event: ChangeEvent): void => { dispatch(updateConfigValue('language.appLanguage', event.target.value)) + trackEventWithRobotSerial({ + name: ANALYTICS_LANGUAGE_UPDATED_ODD_SETTINGS, + properties: { + language: event.target.value, + transactionId, + }, + }) } return ( @@ -68,7 +87,7 @@ export function LanguageSetting({ padding={`${SPACING.spacing16} ${SPACING.spacing40} ${SPACING.spacing40} ${SPACING.spacing40}`} > {LANGUAGES.map(lng => ( - + - + ))} diff --git a/app/src/organisms/ODD/RobotSettingsDashboard/NetworkSettings/RobotSettingsJoinOtherNetwork.tsx b/app/src/organisms/ODD/RobotSettingsDashboard/NetworkSettings/RobotSettingsJoinOtherNetwork.tsx index fed06e4572c..37f851d8cbd 100644 --- a/app/src/organisms/ODD/RobotSettingsDashboard/NetworkSettings/RobotSettingsJoinOtherNetwork.tsx +++ b/app/src/organisms/ODD/RobotSettingsDashboard/NetworkSettings/RobotSettingsJoinOtherNetwork.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useState } from 'react' import { useTranslation } from 'react-i18next' import { DIRECTION_COLUMN, Flex } from '@opentrons/components' @@ -6,11 +6,12 @@ import { DIRECTION_COLUMN, Flex } from '@opentrons/components' import { ChildNavigation } from '/app/organisms/ODD/ChildNavigation' import { SetWifiSsid } from '../../NetworkSettings' +import type { Dispatch, SetStateAction } from 'react' import type { SetSettingOption } from '../types' interface RobotSettingsJoinOtherNetworkProps { setCurrentOption: SetSettingOption - setSelectedSsid: React.Dispatch> + setSelectedSsid: Dispatch> } /** @@ -20,10 +21,10 @@ export function RobotSettingsJoinOtherNetwork({ setCurrentOption, setSelectedSsid, }: RobotSettingsJoinOtherNetworkProps): JSX.Element { - const { i18n, t } = useTranslation('device_settings') + const { i18n, t } = useTranslation(['device_settings', 'shared']) - const [inputSsid, setInputSsid] = React.useState('') - const [errorMessage, setErrorMessage] = React.useState(null) + const [inputSsid, setInputSsid] = useState('') + const [errorMessage, setErrorMessage] = useState(null) const handleContinue = (): void => { if (inputSsid.length >= 2 && inputSsid.length <= 32) { @@ -37,7 +38,7 @@ export function RobotSettingsJoinOtherNetwork({ return ( { setCurrentOption('RobotSettingsWifi') diff --git a/app/src/organisms/ODD/RobotSettingsDashboard/NetworkSettings/RobotSettingsSelectAuthenticationType.tsx b/app/src/organisms/ODD/RobotSettingsDashboard/NetworkSettings/RobotSettingsSelectAuthenticationType.tsx index f8ce5e6a205..1e2f7d51cdd 100644 --- a/app/src/organisms/ODD/RobotSettingsDashboard/NetworkSettings/RobotSettingsSelectAuthenticationType.tsx +++ b/app/src/organisms/ODD/RobotSettingsDashboard/NetworkSettings/RobotSettingsSelectAuthenticationType.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { useTranslation } from 'react-i18next' import { DIRECTION_COLUMN, Flex } from '@opentrons/components' @@ -6,6 +5,7 @@ import { DIRECTION_COLUMN, Flex } from '@opentrons/components' import { ChildNavigation } from '/app/organisms/ODD/ChildNavigation' import { SelectAuthenticationType } from '../../NetworkSettings' +import type { Dispatch, SetStateAction } from 'react' import type { WifiSecurityType } from '@opentrons/api-client' import type { SetSettingOption } from '../types' @@ -13,7 +13,7 @@ interface RobotSettingsSelectAuthenticationTypeProps { handleWifiConnect: () => void selectedAuthType: WifiSecurityType setCurrentOption: SetSettingOption - setSelectedAuthType: React.Dispatch> + setSelectedAuthType: Dispatch> } /** @@ -25,12 +25,12 @@ export function RobotSettingsSelectAuthenticationType({ setCurrentOption, setSelectedAuthType, }: RobotSettingsSelectAuthenticationTypeProps): JSX.Element { - const { i18n, t } = useTranslation('device_settings') + const { i18n, t } = useTranslation(['device_settings', 'shared']) return ( { setCurrentOption('RobotSettingsWifi') diff --git a/app/src/organisms/ODD/RobotSettingsDashboard/NetworkSettings/RobotSettingsSetWifiCred.tsx b/app/src/organisms/ODD/RobotSettingsDashboard/NetworkSettings/RobotSettingsSetWifiCred.tsx index 9204f22f5c4..aaafbf5d590 100644 --- a/app/src/organisms/ODD/RobotSettingsDashboard/NetworkSettings/RobotSettingsSetWifiCred.tsx +++ b/app/src/organisms/ODD/RobotSettingsDashboard/NetworkSettings/RobotSettingsSetWifiCred.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { useTranslation } from 'react-i18next' import { DIRECTION_COLUMN, Flex } from '@opentrons/components' @@ -6,13 +5,14 @@ import { DIRECTION_COLUMN, Flex } from '@opentrons/components' import { ChildNavigation } from '/app/organisms/ODD/ChildNavigation' import { SetWifiCred } from '../../NetworkSettings/SetWifiCred' +import type { Dispatch, SetStateAction } from 'react' import type { SetSettingOption } from '../types' interface RobotSettingsSetWifiCredProps { handleConnect: () => void password: string setCurrentOption: SetSettingOption - setPassword: React.Dispatch> + setPassword: Dispatch> } /** diff --git a/app/src/organisms/ODD/RobotSettingsDashboard/NetworkSettings/RobotSettingsWifi.tsx b/app/src/organisms/ODD/RobotSettingsDashboard/NetworkSettings/RobotSettingsWifi.tsx index add3565fe74..67edb5249ff 100644 --- a/app/src/organisms/ODD/RobotSettingsDashboard/NetworkSettings/RobotSettingsWifi.tsx +++ b/app/src/organisms/ODD/RobotSettingsDashboard/NetworkSettings/RobotSettingsWifi.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { useTranslation } from 'react-i18next' import { DIRECTION_COLUMN, Flex } from '@opentrons/components' @@ -6,11 +5,12 @@ import { DIRECTION_COLUMN, Flex } from '@opentrons/components' import { ChildNavigation } from '/app/organisms/ODD/ChildNavigation' import { WifiConnectionDetails } from './WifiConnectionDetails' +import type { Dispatch, SetStateAction } from 'react' import type { WifiSecurityType } from '@opentrons/api-client' import type { SetSettingOption } from '../types' interface RobotSettingsWifiProps { - setSelectedSsid: React.Dispatch> + setSelectedSsid: Dispatch> setCurrentOption: SetSettingOption activeSsid?: string connectedWifiAuthType?: WifiSecurityType diff --git a/app/src/organisms/ODD/RobotSettingsDashboard/NetworkSettings/__tests__/EthernetConnectionDetails.test.tsx b/app/src/organisms/ODD/RobotSettingsDashboard/NetworkSettings/__tests__/EthernetConnectionDetails.test.tsx index ddefd80196d..d034b6b6e7c 100644 --- a/app/src/organisms/ODD/RobotSettingsDashboard/NetworkSettings/__tests__/EthernetConnectionDetails.test.tsx +++ b/app/src/organisms/ODD/RobotSettingsDashboard/NetworkSettings/__tests__/EthernetConnectionDetails.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { fireEvent, screen } from '@testing-library/react' import { describe, it, expect, vi, beforeEach } from 'vitest' import '@testing-library/jest-dom/vitest' @@ -11,13 +10,13 @@ import { getLocalRobot } from '/app/redux/discovery' import { mockConnectedRobot } from '/app/redux/discovery/__fixtures__' import { EthernetConnectionDetails } from '../EthernetConnectionDetails' +import type { ComponentProps } from 'react' + vi.mock('/app/redux/discovery') vi.mock('/app/redux/discovery/selectors') vi.mock('/app/redux/networking/selectors') -const render = ( - props: React.ComponentProps -) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, }) @@ -31,7 +30,7 @@ const mockEthernet = { } describe('EthernetConnectionDetails', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { handleGoBack: vi.fn(), diff --git a/app/src/organisms/ODD/RobotSettingsDashboard/NetworkSettings/__tests__/NetworkDetailsModal.test.tsx b/app/src/organisms/ODD/RobotSettingsDashboard/NetworkSettings/__tests__/NetworkDetailsModal.test.tsx index 76b4c6f1be0..6b4048dbcfc 100644 --- a/app/src/organisms/ODD/RobotSettingsDashboard/NetworkSettings/__tests__/NetworkDetailsModal.test.tsx +++ b/app/src/organisms/ODD/RobotSettingsDashboard/NetworkSettings/__tests__/NetworkDetailsModal.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { fireEvent, screen } from '@testing-library/react' import { describe, it, expect, vi, beforeEach } from 'vitest' import '@testing-library/jest-dom/vitest' @@ -7,16 +6,18 @@ import { i18n } from '/app/i18n' import { renderWithProviders } from '/app/__testing-utils__' import { NetworkDetailsModal } from '../NetworkDetailsModal' +import type { ComponentProps } from 'react' + const mockFn = vi.fn() -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, }) } describe('NetworkDetailsModal', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { diff --git a/app/src/organisms/ODD/RobotSettingsDashboard/NetworkSettings/__tests__/NetworkSettings.test.tsx b/app/src/organisms/ODD/RobotSettingsDashboard/NetworkSettings/__tests__/NetworkSettings.test.tsx index 266778c0c81..37a5db9c3f2 100644 --- a/app/src/organisms/ODD/RobotSettingsDashboard/NetworkSettings/__tests__/NetworkSettings.test.tsx +++ b/app/src/organisms/ODD/RobotSettingsDashboard/NetworkSettings/__tests__/NetworkSettings.test.tsx @@ -1,5 +1,4 @@ /* eslint-disable testing-library/no-node-access */ -import type * as React from 'react' import { screen } from '@testing-library/react' import { describe, it, expect, vi, beforeEach } from 'vitest' import '@testing-library/jest-dom/vitest' @@ -12,6 +11,7 @@ import { WifiConnectionDetails } from '../WifiConnectionDetails' import { EthernetConnectionDetails } from '../EthernetConnectionDetails' import { NetworkSettings } from '..' +import type { ComponentProps } from 'react' import type { DiscoveredRobot } from '/app/redux/discovery/types' import type { WifiNetwork } from '/app/redux/networking/types' @@ -22,14 +22,14 @@ vi.mock('../EthernetConnectionDetails') const mockSetCurrentOption = vi.fn() -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, }) } describe('NetworkSettings', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { diff --git a/app/src/organisms/ODD/RobotSettingsDashboard/NetworkSettings/__tests__/WifiConnectionDetails.test.tsx b/app/src/organisms/ODD/RobotSettingsDashboard/NetworkSettings/__tests__/WifiConnectionDetails.test.tsx index 9650a89b76c..c7311087fe4 100644 --- a/app/src/organisms/ODD/RobotSettingsDashboard/NetworkSettings/__tests__/WifiConnectionDetails.test.tsx +++ b/app/src/organisms/ODD/RobotSettingsDashboard/NetworkSettings/__tests__/WifiConnectionDetails.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { fireEvent, screen } from '@testing-library/react' import { when } from 'vitest-when' import { describe, it, expect, vi, beforeEach } from 'vitest' @@ -10,6 +9,8 @@ import { getLocalRobot } from '/app/redux/discovery' import * as Networking from '/app/redux/networking' import { NetworkDetailsModal } from '../NetworkDetailsModal' import { WifiConnectionDetails } from '../WifiConnectionDetails' + +import type { ComponentProps } from 'react' import type * as Dom from 'react-router-dom' import type { State } from '/app/redux/types' @@ -36,14 +37,14 @@ const initialMockWifi = { type: Networking.INTERFACE_WIFI, } -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, }) } describe('WifiConnectionDetails', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { activeSsid: 'mock wifi ssid', diff --git a/app/src/organisms/ODD/RobotSettingsDashboard/NetworkSettings/index.tsx b/app/src/organisms/ODD/RobotSettingsDashboard/NetworkSettings/index.tsx index db73b89dae3..9afeeb15cc0 100644 --- a/app/src/organisms/ODD/RobotSettingsDashboard/NetworkSettings/index.tsx +++ b/app/src/organisms/ODD/RobotSettingsDashboard/NetworkSettings/index.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { css } from 'styled-components' import { useTranslation } from 'react-i18next' @@ -19,6 +18,7 @@ import { import { ChildNavigation } from '/app/organisms/ODD/ChildNavigation' +import type { ComponentProps } from 'react' import type { IconName, ChipType } from '@opentrons/components' import type { NetworkConnection } from '/app/resources/networking/hooks/useNetworkConnection' import type { SetSettingOption } from '../types' @@ -87,7 +87,7 @@ export function NetworkSettings({ ) } -interface NetworkSettingButtonProps extends React.ComponentProps { +interface NetworkSettingButtonProps extends ComponentProps { buttonTitle: string iconName: IconName chipType: ChipType diff --git a/app/src/organisms/ODD/RobotSettingsDashboard/RobotSettingButton.tsx b/app/src/organisms/ODD/RobotSettingsDashboard/RobotSettingButton.tsx index f777c9fcb77..1a47e3bbe4e 100644 --- a/app/src/organisms/ODD/RobotSettingsDashboard/RobotSettingButton.tsx +++ b/app/src/organisms/ODD/RobotSettingsDashboard/RobotSettingButton.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { css } from 'styled-components' import { @@ -20,6 +19,7 @@ import { TYPOGRAPHY, } from '@opentrons/components' +import type { MouseEventHandler, ReactNode } from 'react' import type { IconName } from '@opentrons/components' const SETTING_BUTTON_STYLE = css` @@ -36,10 +36,10 @@ const SETTING_BUTTON_STYLE = css` interface RobotSettingButtonProps { settingName: string - onClick: React.MouseEventHandler + onClick: MouseEventHandler iconName?: IconName settingInfo?: string - rightElement?: React.ReactNode + rightElement?: ReactNode dataTestId?: string } diff --git a/app/src/organisms/ODD/RobotSettingsDashboard/TouchScreenSleep.tsx b/app/src/organisms/ODD/RobotSettingsDashboard/TouchScreenSleep.tsx index ea3d879088f..3872d79ba21 100644 --- a/app/src/organisms/ODD/RobotSettingsDashboard/TouchScreenSleep.tsx +++ b/app/src/organisms/ODD/RobotSettingsDashboard/TouchScreenSleep.tsx @@ -1,12 +1,12 @@ -import * as React from 'react' +import { useEffect, useRef } from 'react' import { useDispatch, useSelector } from 'react-redux' import { useTranslation } from 'react-i18next' import { DIRECTION_COLUMN, Flex, - SPACING, RadioButton, + SPACING, } from '@opentrons/components' import { ChildNavigation } from '/app/organisms/ODD/ChildNavigation' @@ -14,8 +14,9 @@ import { getOnDeviceDisplaySettings, updateConfigValue, } from '/app/redux/config' -import { SLEEP_NEVER_MS } from '/app/local-resources/config' +import { SLEEP_NEVER_MS } from '/app/local-resources/dom-utils' +import type { ChangeEvent } from 'react' import type { Dispatch } from '/app/redux/types' import type { SetSettingOption } from './types' @@ -31,7 +32,7 @@ export function TouchScreenSleep({ const { t } = useTranslation(['device_settings']) const { sleepMs } = useSelector(getOnDeviceDisplaySettings) ?? SLEEP_NEVER_MS const dispatch = useDispatch() - const screenRef = React.useRef(null) + const screenRef = useRef(null) // Note (kj:02/10/2023) value's unit is ms const settingsButtons = [ @@ -44,7 +45,7 @@ export function TouchScreenSleep({ { label: t('one_hour'), value: SLEEP_TIME_MS * 60 }, ] - const handleChange = (event: React.ChangeEvent): void => { + const handleChange = (event: ChangeEvent): void => { dispatch( updateConfigValue( 'onDeviceDisplaySettings.sleepMs', @@ -53,7 +54,7 @@ export function TouchScreenSleep({ ) } - React.useEffect(() => { + useEffect(() => { if (screenRef.current != null) screenRef.current.scrollIntoView() }, []) diff --git a/app/src/organisms/ODD/RobotSettingsDashboard/UpdateChannel.tsx b/app/src/organisms/ODD/RobotSettingsDashboard/UpdateChannel.tsx index c9668e8a079..642aeae4af3 100644 --- a/app/src/organisms/ODD/RobotSettingsDashboard/UpdateChannel.tsx +++ b/app/src/organisms/ODD/RobotSettingsDashboard/UpdateChannel.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { Fragment } from 'react' import { useTranslation } from 'react-i18next' import { useDispatch, useSelector } from 'react-redux' import styled from 'styled-components' @@ -22,6 +22,7 @@ import { updateConfigValue, } from '/app/redux/config' +import type { ChangeEvent } from 'react' import type { Dispatch } from '/app/redux/types' interface LabelProps { @@ -59,7 +60,7 @@ export function UpdateChannel({ ? channelOptions.filter(option => option.value !== 'alpha') : channelOptions - const handleChange = (event: React.ChangeEvent): void => { + const handleChange = (event: ChangeEvent): void => { dispatch(updateConfigValue('update.channel', event.target.value)) } @@ -87,7 +88,7 @@ export function UpdateChannel({ marginTop={SPACING.spacing24} > {modifiedChannelOptions.map(radio => ( - + ) : null} - + ))} diff --git a/app/src/organisms/ODD/RobotSettingsDashboard/__tests__/DeviceReset.test.tsx b/app/src/organisms/ODD/RobotSettingsDashboard/__tests__/DeviceReset.test.tsx index 9f71205231e..3f273ab2df2 100644 --- a/app/src/organisms/ODD/RobotSettingsDashboard/__tests__/DeviceReset.test.tsx +++ b/app/src/organisms/ODD/RobotSettingsDashboard/__tests__/DeviceReset.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { fireEvent, screen } from '@testing-library/react' import { describe, it, expect, vi, beforeEach } from 'vitest' import '@testing-library/jest-dom/vitest' @@ -9,6 +8,7 @@ import { useDispatchApiRequest } from '/app/redux/robot-api' import { DeviceReset } from '../DeviceReset' +import type { ComponentProps } from 'react' import type { DispatchApiRequestType } from '/app/redux/robot-api' vi.mock('/app/redux/robot-admin') @@ -47,7 +47,7 @@ const mockResetConfigOptions = [ }, ] -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders( , @@ -56,7 +56,7 @@ const render = (props: React.ComponentProps) => { } describe('DeviceReset', () => { - let props: React.ComponentProps + let props: ComponentProps let dispatchApiRequest: DispatchApiRequestType beforeEach(() => { diff --git a/app/src/organisms/ODD/RobotSettingsDashboard/__tests__/LanguageSetting.test.tsx b/app/src/organisms/ODD/RobotSettingsDashboard/__tests__/LanguageSetting.test.tsx index 80d35ebea15..50ce6edc7e6 100644 --- a/app/src/organisms/ODD/RobotSettingsDashboard/__tests__/LanguageSetting.test.tsx +++ b/app/src/organisms/ODD/RobotSettingsDashboard/__tests__/LanguageSetting.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { fireEvent, screen } from '@testing-library/react' import { describe, it, expect, vi, beforeEach } from 'vitest' import '@testing-library/jest-dom/vitest' @@ -10,28 +9,37 @@ import { SIMPLIFIED_CHINESE_DISPLAY_NAME, SIMPLIFIED_CHINESE, } from '/app/i18n' +import { ANALYTICS_LANGUAGE_UPDATED_ODD_SETTINGS } from '/app/redux/analytics' +import { useTrackEventWithRobotSerial } from '/app/redux-resources/analytics' import { getAppLanguage, updateConfigValue } from '/app/redux/config' import { renderWithProviders } from '/app/__testing-utils__' import { LanguageSetting } from '../LanguageSetting' +import type { ComponentProps } from 'react' + vi.mock('/app/redux/config') +vi.mock('/app/redux-resources/analytics') const mockSetCurrentOption = vi.fn() +const mockTrackEvent = vi.fn() -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, }) } describe('LanguageSetting', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { setCurrentOption: mockSetCurrentOption, } vi.mocked(getAppLanguage).mockReturnValue(US_ENGLISH) + vi.mocked(useTrackEventWithRobotSerial).mockReturnValue({ + trackEventWithRobotSerial: mockTrackEvent, + }) }) it('should render text and buttons', () => { @@ -49,6 +57,13 @@ describe('LanguageSetting', () => { 'language.appLanguage', SIMPLIFIED_CHINESE ) + expect(mockTrackEvent).toHaveBeenCalledWith({ + name: ANALYTICS_LANGUAGE_UPDATED_ODD_SETTINGS, + properties: { + language: SIMPLIFIED_CHINESE, + transactionId: expect.anything(), + }, + }) }) it('should call mock function when tapping back button', () => { diff --git a/app/src/organisms/ODD/RobotSettingsDashboard/__tests__/Privacy.test.tsx b/app/src/organisms/ODD/RobotSettingsDashboard/__tests__/Privacy.test.tsx index 03f4a987462..d281ccecf6f 100644 --- a/app/src/organisms/ODD/RobotSettingsDashboard/__tests__/Privacy.test.tsx +++ b/app/src/organisms/ODD/RobotSettingsDashboard/__tests__/Privacy.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { fireEvent, screen } from '@testing-library/react' import { vi, describe, beforeEach, afterEach, expect, it } from 'vitest' @@ -9,17 +8,19 @@ import { getRobotSettings } from '/app/redux/robot-settings' import { Privacy } from '../Privacy' +import type { ComponentProps } from 'react' + vi.mock('/app/redux/analytics') vi.mock('/app/redux/robot-settings') -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, }) } describe('Privacy', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { robotName: 'Otie', diff --git a/app/src/organisms/ODD/RobotSettingsDashboard/__tests__/RobotSystemVersion.test.tsx b/app/src/organisms/ODD/RobotSettingsDashboard/__tests__/RobotSystemVersion.test.tsx index ad30e2539fa..955161149d8 100644 --- a/app/src/organisms/ODD/RobotSettingsDashboard/__tests__/RobotSystemVersion.test.tsx +++ b/app/src/organisms/ODD/RobotSettingsDashboard/__tests__/RobotSystemVersion.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { MemoryRouter } from 'react-router-dom' import { fireEvent, screen } from '@testing-library/react' import { describe, it, expect, vi, beforeEach } from 'vitest' @@ -9,12 +8,14 @@ import { renderWithProviders } from '/app/__testing-utils__' import { RobotSystemVersion } from '../RobotSystemVersion' import { RobotSystemVersionModal } from '../RobotSystemVersionModal' +import type { ComponentProps } from 'react' + vi.mock('/app/redux/shell') vi.mock('../RobotSystemVersionModal') const mockBack = vi.fn() -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders( @@ -26,7 +27,7 @@ const render = (props: React.ComponentProps) => { } describe('RobotSystemVersion', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { diff --git a/app/src/organisms/ODD/RobotSettingsDashboard/__tests__/RobotSystemVersionModal.test.tsx b/app/src/organisms/ODD/RobotSettingsDashboard/__tests__/RobotSystemVersionModal.test.tsx index 0d7a125eb1f..0b8f891f1a5 100644 --- a/app/src/organisms/ODD/RobotSettingsDashboard/__tests__/RobotSystemVersionModal.test.tsx +++ b/app/src/organisms/ODD/RobotSettingsDashboard/__tests__/RobotSystemVersionModal.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { fireEvent, screen } from '@testing-library/react' import { describe, it, expect, vi, beforeEach } from 'vitest' import '@testing-library/jest-dom/vitest' @@ -6,6 +5,8 @@ import '@testing-library/jest-dom/vitest' import { i18n } from '/app/i18n' import { renderWithProviders } from '/app/__testing-utils__' import { RobotSystemVersionModal } from '../RobotSystemVersionModal' + +import type { ComponentProps } from 'react' import type * as Dom from 'react-router-dom' const mockFn = vi.fn() @@ -19,16 +20,14 @@ vi.mock('react-router-dom', async importOriginal => { } }) -const render = ( - props: React.ComponentProps -) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, }) } describe('RobotSystemVersionModal', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { diff --git a/app/src/organisms/ODD/RobotSettingsDashboard/__tests__/TextSize.test.tsx b/app/src/organisms/ODD/RobotSettingsDashboard/__tests__/TextSize.test.tsx index 703323c0d7e..3ca0124810a 100644 --- a/app/src/organisms/ODD/RobotSettingsDashboard/__tests__/TextSize.test.tsx +++ b/app/src/organisms/ODD/RobotSettingsDashboard/__tests__/TextSize.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { fireEvent, screen } from '@testing-library/react' import { describe, it, expect, vi, beforeEach } from 'vitest' @@ -6,15 +5,17 @@ import { i18n } from '/app/i18n' import { renderWithProviders } from '/app/__testing-utils__' import { TextSize } from '../TextSize' +import type { ComponentProps } from 'react' + const mockFunc = vi.fn() -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, }) } describe('TextSize', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { diff --git a/app/src/organisms/ODD/RobotSettingsDashboard/__tests__/TouchScreenSleep.test.tsx b/app/src/organisms/ODD/RobotSettingsDashboard/__tests__/TouchScreenSleep.test.tsx index 990c6bcf436..6c75c584cfe 100644 --- a/app/src/organisms/ODD/RobotSettingsDashboard/__tests__/TouchScreenSleep.test.tsx +++ b/app/src/organisms/ODD/RobotSettingsDashboard/__tests__/TouchScreenSleep.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { fireEvent, screen } from '@testing-library/react' import { describe, it, expect, vi, beforeEach } from 'vitest' import { i18n } from '/app/i18n' @@ -6,19 +5,21 @@ import { updateConfigValue } from '/app/redux/config' import { TouchScreenSleep } from '../TouchScreenSleep' import { renderWithProviders } from '/app/__testing-utils__' +import type { ComponentProps } from 'react' + vi.mock('/app/redux/config') // Note (kj:06/28/2023) this line is to avoid causing errors for scrollIntoView window.HTMLElement.prototype.scrollIntoView = vi.fn() -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, }) } describe('TouchScreenSleep', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { diff --git a/app/src/organisms/ODD/RobotSettingsDashboard/__tests__/TouchscreenBrightness.test.tsx b/app/src/organisms/ODD/RobotSettingsDashboard/__tests__/TouchscreenBrightness.test.tsx index 76993c42300..0dd4c87331d 100644 --- a/app/src/organisms/ODD/RobotSettingsDashboard/__tests__/TouchscreenBrightness.test.tsx +++ b/app/src/organisms/ODD/RobotSettingsDashboard/__tests__/TouchscreenBrightness.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { fireEvent, screen } from '@testing-library/react' import { describe, it, expect, vi, beforeEach } from 'vitest' @@ -10,18 +9,20 @@ import { import { renderWithProviders } from '/app/__testing-utils__' import { TouchscreenBrightness } from '../TouchscreenBrightness' +import type { ComponentProps } from 'react' + vi.mock('/app/redux/config') const mockFunc = vi.fn() -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, }) } describe('TouchscreenBrightness', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { diff --git a/app/src/organisms/ODD/RobotSettingsDashboard/__tests__/UpdateChannel.test.tsx b/app/src/organisms/ODD/RobotSettingsDashboard/__tests__/UpdateChannel.test.tsx index c25e9582a4b..5cf5d34ffde 100644 --- a/app/src/organisms/ODD/RobotSettingsDashboard/__tests__/UpdateChannel.test.tsx +++ b/app/src/organisms/ODD/RobotSettingsDashboard/__tests__/UpdateChannel.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { fireEvent, screen } from '@testing-library/react' import { describe, it, expect, vi, beforeEach } from 'vitest' import '@testing-library/jest-dom/vitest' @@ -13,6 +12,8 @@ import { renderWithProviders } from '/app/__testing-utils__' import { UpdateChannel } from '../UpdateChannel' +import type { ComponentProps } from 'react' + vi.mock('/app/redux/config') const mockChannelOptions = [ @@ -26,14 +27,14 @@ const mockChannelOptions = [ const mockhandleBackPress = vi.fn() -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, }) } describe('UpdateChannel', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { handleBackPress: mockhandleBackPress, diff --git a/app/src/organisms/ODD/RobotSetupHeader/index.tsx b/app/src/organisms/ODD/RobotSetupHeader/index.tsx index 6b7a3fa1049..3e828ddb061 100644 --- a/app/src/organisms/ODD/RobotSetupHeader/index.tsx +++ b/app/src/organisms/ODD/RobotSetupHeader/index.tsx @@ -1,5 +1,3 @@ -import type * as React from 'react' - import { ALIGN_CENTER, Btn, @@ -17,14 +15,15 @@ import { import { SmallButton } from '/app/atoms/buttons' import { InlineNotification } from '/app/atoms/InlineNotification' +import type { MouseEventHandler, ReactNode } from 'react' import type { InlineNotificationProps } from '/app/atoms/InlineNotification' interface RobotSetupHeaderProps { header: string - buttonText?: React.ReactNode + buttonText?: ReactNode inlineNotification?: InlineNotificationProps - onClickBack?: React.MouseEventHandler - onClickButton?: React.MouseEventHandler + onClickBack?: MouseEventHandler + onClickButton?: MouseEventHandler } export function RobotSetupHeader({ diff --git a/app/src/organisms/ODD/RunningProtocol/ConfirmCancelRunModal.tsx b/app/src/organisms/ODD/RunningProtocol/ConfirmCancelRunModal.tsx index 453e3152ad4..13e3e08885d 100644 --- a/app/src/organisms/ODD/RunningProtocol/ConfirmCancelRunModal.tsx +++ b/app/src/organisms/ODD/RunningProtocol/ConfirmCancelRunModal.tsx @@ -112,7 +112,7 @@ export function ConfirmCancelRunModal({ setShowConfirmCancelRunModal(false) }} > - + { } }) -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders( @@ -63,7 +63,7 @@ const ROBOT_NAME = 'otie' const mockFn = vi.fn() describe('ConfirmCancelRunModal', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { diff --git a/app/src/organisms/ODD/RunningProtocol/__tests__/CurrentRunningProtocolCommand.test.tsx b/app/src/organisms/ODD/RunningProtocol/__tests__/CurrentRunningProtocolCommand.test.tsx index 581df7c013c..ceda8df2e42 100644 --- a/app/src/organisms/ODD/RunningProtocol/__tests__/CurrentRunningProtocolCommand.test.tsx +++ b/app/src/organisms/ODD/RunningProtocol/__tests__/CurrentRunningProtocolCommand.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { fireEvent, screen } from '@testing-library/react' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' @@ -12,6 +11,8 @@ import { useRunningStepCounts } from '/app/resources/protocols/hooks' import { useNotifyAllCommandsQuery } from '/app/resources/runs' import { FLEX_ROBOT_TYPE } from '@opentrons/shared-data' +import type { ComponentProps } from 'react' + vi.mock('/app/resources/runs') vi.mock('/app/resources/protocols/hooks') @@ -28,7 +29,7 @@ const mockRunTimer = { } const render = ( - props: React.ComponentProps + props: ComponentProps ) => { return renderWithProviders(, { i18nInstance: i18n, @@ -36,7 +37,7 @@ const render = ( } describe('CurrentRunningProtocolCommand', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { diff --git a/app/src/organisms/ODD/RunningProtocol/__tests__/RunFailedModal.test.tsx b/app/src/organisms/ODD/RunningProtocol/__tests__/RunFailedModal.test.tsx index 8dcfd2e5b88..40e1366d324 100644 --- a/app/src/organisms/ODD/RunningProtocol/__tests__/RunFailedModal.test.tsx +++ b/app/src/organisms/ODD/RunningProtocol/__tests__/RunFailedModal.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { MemoryRouter } from 'react-router-dom' import { fireEvent, screen } from '@testing-library/react' import { beforeEach, describe, expect, it, vi } from 'vitest' @@ -12,6 +11,8 @@ import { RunFailedModal } from '../RunFailedModal' import type { NavigateFunction } from 'react-router-dom' import { RUN_STATUS_FAILED } from '@opentrons/api-client' +import type { ComponentProps } from 'react' + vi.mock('@opentrons/react-api-client') const RUN_ID = 'mock_runID' @@ -82,7 +83,7 @@ vi.mock('react-router-dom', async importOriginal => { } }) -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders( @@ -94,7 +95,7 @@ const render = (props: React.ComponentProps) => { } describe('RunFailedModal', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { diff --git a/app/src/organisms/ODD/RunningProtocol/__tests__/RunningProtocolCommandList.test.tsx b/app/src/organisms/ODD/RunningProtocol/__tests__/RunningProtocolCommandList.test.tsx index 199ae940c3b..5f2ceba8121 100644 --- a/app/src/organisms/ODD/RunningProtocol/__tests__/RunningProtocolCommandList.test.tsx +++ b/app/src/organisms/ODD/RunningProtocol/__tests__/RunningProtocolCommandList.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { fireEvent, screen } from '@testing-library/react' import { beforeEach, describe, expect, it, vi } from 'vitest' @@ -10,20 +9,20 @@ import { i18n } from '/app/i18n' import { mockRobotSideAnalysis } from '/app/molecules/Command/__fixtures__' import { RunningProtocolCommandList } from '../RunningProtocolCommandList' +import type { ComponentProps } from 'react' + const mockPlayRun = vi.fn() const mockPauseRun = vi.fn() const mockShowModal = vi.fn() -const render = ( - props: React.ComponentProps -) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, }) } describe('RunningProtocolCommandList', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { runStatus: RUN_STATUS_RUNNING, diff --git a/app/src/organisms/ODD/RunningProtocol/__tests__/RunningProtocolSkeleton.test.tsx b/app/src/organisms/ODD/RunningProtocol/__tests__/RunningProtocolSkeleton.test.tsx index 656d4d250d1..fcd48819c49 100644 --- a/app/src/organisms/ODD/RunningProtocol/__tests__/RunningProtocolSkeleton.test.tsx +++ b/app/src/organisms/ODD/RunningProtocol/__tests__/RunningProtocolSkeleton.test.tsx @@ -1,18 +1,17 @@ -import type * as React from 'react' import { screen } from '@testing-library/react' import { beforeEach, describe, expect, it } from 'vitest' import { renderWithProviders } from '/app/__testing-utils__' import { RunningProtocolSkeleton } from '../RunningProtocolSkeleton' -const render = ( - props: React.ComponentProps -) => { +import type { ComponentProps } from 'react' + +const render = (props: ComponentProps) => { return renderWithProviders() } describe('RunningProtocolSkeleton', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { diff --git a/app/src/organisms/PipetteWizardFlows/BeforeBeginning.tsx b/app/src/organisms/PipetteWizardFlows/BeforeBeginning.tsx index ffcb72dae18..a69e095a674 100644 --- a/app/src/organisms/PipetteWizardFlows/BeforeBeginning.tsx +++ b/app/src/organisms/PipetteWizardFlows/BeforeBeginning.tsx @@ -107,6 +107,10 @@ export const BeforeBeginning = ( let equipmentList = [CALIBRATION_PROBE] const proceedButtonText = t('move_gantry_to_front') + const hexScrewdriverWithSubtitle = { + ...HEX_SCREWDRIVER, + subtitle: t('provided_with_robot'), + } let bodyTranslationKey: string = '' switch (flowType) { @@ -124,7 +128,7 @@ export const BeforeBeginning = ( equipmentList = [ { ...PIPETTE, displayName: displayName ?? PIPETTE.displayName }, CALIBRATION_PROBE, - HEX_SCREWDRIVER, + hexScrewdriverWithSubtitle, ] } else { equipmentList = [ @@ -133,7 +137,7 @@ export const BeforeBeginning = ( displayName: displayName ?? NINETY_SIX_CHANNEL_PIPETTE.displayName, }, CALIBRATION_PROBE, - HEX_SCREWDRIVER, + hexScrewdriverWithSubtitle, NINETY_SIX_CHANNEL_MOUNTING_PLATE, ] } @@ -148,19 +152,19 @@ export const BeforeBeginning = ( equipmentList = [ { ...NINETY_SIX_CHANNEL_PIPETTE, displayName }, CALIBRATION_PROBE, - HEX_SCREWDRIVER, + hexScrewdriverWithSubtitle, NINETY_SIX_CHANNEL_MOUNTING_PLATE, ] } else { equipmentList = [ { ...PIPETTE, displayName }, CALIBRATION_PROBE, - HEX_SCREWDRIVER, + hexScrewdriverWithSubtitle, ] } } else { bodyTranslationKey = 'get_started_detach' - equipmentList = [HEX_SCREWDRIVER] + equipmentList = [hexScrewdriverWithSubtitle] } break } diff --git a/app/src/organisms/PipetteWizardFlows/CheckPipetteButton.tsx b/app/src/organisms/PipetteWizardFlows/CheckPipetteButton.tsx index 2300204b65b..25438c1442c 100644 --- a/app/src/organisms/PipetteWizardFlows/CheckPipetteButton.tsx +++ b/app/src/organisms/PipetteWizardFlows/CheckPipetteButton.tsx @@ -1,11 +1,12 @@ -import type * as React from 'react' import { useInstrumentsQuery } from '@opentrons/react-api-client' import { PrimaryButton } from '@opentrons/components' import { SmallButton } from '/app/atoms/buttons' +import type { Dispatch, SetStateAction } from 'react' + interface CheckPipetteButtonProps { proceedButtonText: string - setFetching: React.Dispatch> + setFetching: Dispatch> isFetching: boolean isOnDevice: boolean | null proceed?: () => void diff --git a/app/src/organisms/PipetteWizardFlows/ChoosePipette.tsx b/app/src/organisms/PipetteWizardFlows/ChoosePipette.tsx index f8d31f1adec..1a0b45001c2 100644 --- a/app/src/organisms/PipetteWizardFlows/ChoosePipette.tsx +++ b/app/src/organisms/PipetteWizardFlows/ChoosePipette.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useState } from 'react' import { createPortal } from 'react-dom' import { useSelector } from 'react-redux' import { css } from 'styled-components' @@ -46,6 +46,7 @@ import { ExitModal } from './ExitModal' import { FLOWS } from './constants' import { getIsGantryEmpty } from './utils' +import type { Dispatch, SetStateAction, ReactNode } from 'react' import type { StyleProps } from '@opentrons/components' import type { PipetteMount } from '@opentrons/shared-data' import type { SelectablePipettes } from './types' @@ -108,7 +109,7 @@ const SELECTED_OPTIONS_STYLE = css` interface ChoosePipetteProps { proceed: () => void selectedPipette: SelectablePipettes - setSelectedPipette: React.Dispatch> + setSelectedPipette: Dispatch> exit: () => void mount: PipetteMount } @@ -117,10 +118,9 @@ export const ChoosePipette = (props: ChoosePipetteProps): JSX.Element => { const isOnDevice = useSelector(getIsOnDevice) const { t } = useTranslation(['pipette_wizard_flows', 'shared']) const attachedPipettesByMount = useAttachedPipettesFromInstrumentsQuery() - const [ - showExitConfirmation, - setShowExitConfirmation, - ] = React.useState(false) + const [showExitConfirmation, setShowExitConfirmation] = useState( + false + ) const bothMounts = getIsGantryEmpty(attachedPipettesByMount) ? t('ninety_six_channel', { @@ -281,7 +281,7 @@ export const ChoosePipette = (props: ChoosePipetteProps): JSX.Element => { interface PipetteMountOptionProps extends StyleProps { isSelected: boolean onClick: () => void - children: React.ReactNode + children: ReactNode } function PipetteMountOption(props: PipetteMountOptionProps): JSX.Element { const { isSelected, onClick, children, ...styleProps } = props diff --git a/app/src/organisms/PipetteWizardFlows/DetachPipette.tsx b/app/src/organisms/PipetteWizardFlows/DetachPipette.tsx index 46e5f92e389..9d83f3d3e75 100644 --- a/app/src/organisms/PipetteWizardFlows/DetachPipette.tsx +++ b/app/src/organisms/PipetteWizardFlows/DetachPipette.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useState, useMemo } from 'react' import { useTranslation } from 'react-i18next' import { css } from 'styled-components' import { RIGHT, WEIGHT_OF_96_CHANNEL } from '@opentrons/shared-data' @@ -27,12 +27,14 @@ import { Skeleton } from '/app/atoms/Skeleton' import { SmallButton } from '/app/atoms/buttons' import { BODY_STYLE, SECTIONS } from './constants' import { getPipetteAnimations, getPipetteAnimations96 } from './utils' -import type { PipetteWizardStepProps } from './types' + +import type { Dispatch, ReactNode, SetStateAction } from 'react' import type { PipetteData } from '@opentrons/api-client' +import type { PipetteWizardStepProps } from './types' interface DetachPipetteProps extends PipetteWizardStepProps { isFetching: boolean - setFetching: React.Dispatch> + setFetching: Dispatch> } const BACKGROUND_SIZE = '47rem' @@ -82,7 +84,7 @@ export const DetachPipette = (props: DetachPipetteProps): JSX.Element => { flowType, section: SECTIONS.DETACH_PIPETTE, } - const memoizedAttachedPipettes = React.useMemo(() => attachedPipettes, []) + const memoizedAttachedPipettes = useMemo(() => attachedPipettes, []) const is96ChannelPipette = memoizedAttachedPipettes[mount]?.instrumentName === 'p1000_96' const pipetteName = @@ -121,10 +123,9 @@ export const DetachPipette = (props: DetachPipetteProps): JSX.Element => { }) } - const [ - showPipetteStillAttached, - setShowPipetteStillAttached, - ] = React.useState(false) + const [showPipetteStillAttached, setShowPipetteStillAttached] = useState( + false + ) const handleOnClick = (): void => { setFetching(true) @@ -142,7 +143,7 @@ export const DetachPipette = (props: DetachPipetteProps): JSX.Element => { } const channel = memoizedAttachedPipettes[mount]?.data.channels - let bodyText: React.ReactNode =
        + let bodyText: ReactNode =
        if (isFetching) { bodyText = ( <> diff --git a/app/src/organisms/PipetteWizardFlows/MountPipette.tsx b/app/src/organisms/PipetteWizardFlows/MountPipette.tsx index 9ba1b036785..b750aee0ad2 100644 --- a/app/src/organisms/PipetteWizardFlows/MountPipette.tsx +++ b/app/src/organisms/PipetteWizardFlows/MountPipette.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { useTranslation } from 'react-i18next' import { SINGLE_MOUNT_PIPETTES, @@ -17,11 +16,13 @@ import { Skeleton } from '/app/atoms/Skeleton' import { CheckPipetteButton } from './CheckPipetteButton' import { BODY_STYLE, SECTIONS } from './constants' import { getPipetteAnimations, getPipetteAnimations96 } from './utils' + +import type { Dispatch, ReactNode, SetStateAction } from 'react' import type { PipetteWizardStepProps } from './types' interface MountPipetteProps extends PipetteWizardStepProps { isFetching: boolean - setFetching: React.Dispatch> + setFetching: Dispatch> } const BACKGROUND_SIZE = '47rem' @@ -47,7 +48,7 @@ export const MountPipette = (props: MountPipetteProps): JSX.Element => { backgroundSize={BACKGROUND_SIZE} /> ) - let bodyText: React.ReactNode =
        + let bodyText: ReactNode =
        if (isFetching) { bodyText = ( <> diff --git a/app/src/organisms/PipetteWizardFlows/Results.tsx b/app/src/organisms/PipetteWizardFlows/Results.tsx index 96ed9098ac9..4ede1cb6828 100644 --- a/app/src/organisms/PipetteWizardFlows/Results.tsx +++ b/app/src/organisms/PipetteWizardFlows/Results.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useState } from 'react' import { css } from 'styled-components' import { useTranslation } from 'react-i18next' import { @@ -22,6 +22,7 @@ import { usePipetteNameSpecs } from '/app/local-resources/instruments' import { CheckPipetteButton } from './CheckPipetteButton' import { FLOWS } from './constants' +import type { Dispatch, SetStateAction } from 'react' import type { LoadedPipette, MotorAxes, @@ -34,7 +35,7 @@ interface ResultsProps extends PipetteWizardStepProps { currentStepIndex: number totalStepCount: number isFetching: boolean - setFetching: React.Dispatch> + setFetching: Dispatch> hasCalData: boolean requiredPipette?: LoadedPipette nextMount?: string @@ -78,7 +79,7 @@ export const Results = (props: ResultsProps): JSX.Element => { usePipetteNameSpecs(requiredPipette?.pipetteName as PipetteName) ?.displayName ?? null - const [numberOfTryAgains, setNumberOfTryAgains] = React.useState(0) + const [numberOfTryAgains, setNumberOfTryAgains] = useState(0) let header: string = 'unknown results screen' let iconColor: string = COLORS.green50 let isSuccess: boolean = true diff --git a/app/src/organisms/PipetteWizardFlows/__tests__/AttachProbe.test.tsx b/app/src/organisms/PipetteWizardFlows/__tests__/AttachProbe.test.tsx index 00120fe438a..0bbab02ebc5 100644 --- a/app/src/organisms/PipetteWizardFlows/__tests__/AttachProbe.test.tsx +++ b/app/src/organisms/PipetteWizardFlows/__tests__/AttachProbe.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { fireEvent, screen, waitFor } from '@testing-library/react' import { describe, it, beforeEach, vi, expect } from 'vitest' @@ -16,7 +15,9 @@ import { FLOWS } from '../constants' import { AttachProbe } from '../AttachProbe' import { useNotifyDeckConfigurationQuery } from '/app/resources/deck_configuration' -const render = (props: React.ComponentProps) => { +import type { ComponentProps } from 'react' + +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, })[0] @@ -24,7 +25,7 @@ const render = (props: React.ComponentProps) => { vi.mock('/app/resources/deck_configuration') describe('AttachProbe', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { mount: LEFT, diff --git a/app/src/organisms/PipetteWizardFlows/__tests__/BeforeBeginning.test.tsx b/app/src/organisms/PipetteWizardFlows/__tests__/BeforeBeginning.test.tsx index a75e8bfe97a..db8b03816c2 100644 --- a/app/src/organisms/PipetteWizardFlows/__tests__/BeforeBeginning.test.tsx +++ b/app/src/organisms/PipetteWizardFlows/__tests__/BeforeBeginning.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { fireEvent, waitFor, screen } from '@testing-library/react' import { describe, it, vi, beforeEach, expect, afterEach } from 'vitest' @@ -19,19 +18,21 @@ import { BeforeBeginning } from '../BeforeBeginning' import { FLOWS } from '../constants' import { getIsGantryEmpty } from '../utils' +import type { ComponentProps } from 'react' + // TODO(jr, 11/3/22): uncomment out the get help link when we have // the correct URL to link it to vi.mock('/app/molecules/InProgressModal/InProgressModal') vi.mock('../utils') -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, })[0] } describe('BeforeBeginning', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { selectedPipette: SINGLE_MOUNT_PIPETTES, diff --git a/app/src/organisms/PipetteWizardFlows/__tests__/Carriage.test.tsx b/app/src/organisms/PipetteWizardFlows/__tests__/Carriage.test.tsx index 17c8140ebe8..389fde2801f 100644 --- a/app/src/organisms/PipetteWizardFlows/__tests__/Carriage.test.tsx +++ b/app/src/organisms/PipetteWizardFlows/__tests__/Carriage.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { fireEvent, screen } from '@testing-library/react' import { describe, it, beforeEach, vi, expect } from 'vitest' @@ -11,14 +10,16 @@ import { RUN_ID_1 } from '/app/resources/runs/__fixtures__' import { FLOWS } from '../constants' import { Carriage } from '../Carriage' -const render = (props: React.ComponentProps) => { +import type { ComponentProps } from 'react' + +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, })[0] } describe('Carriage', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { mount: LEFT, diff --git a/app/src/organisms/PipetteWizardFlows/__tests__/CheckPipetteButton.test.tsx b/app/src/organisms/PipetteWizardFlows/__tests__/CheckPipetteButton.test.tsx index e5dfc5fe3de..76c60357b46 100644 --- a/app/src/organisms/PipetteWizardFlows/__tests__/CheckPipetteButton.test.tsx +++ b/app/src/organisms/PipetteWizardFlows/__tests__/CheckPipetteButton.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { fireEvent, waitFor, screen } from '@testing-library/react' import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' @@ -7,14 +6,16 @@ import { useInstrumentsQuery } from '@opentrons/react-api-client' import { renderWithProviders } from '/app/__testing-utils__' import { CheckPipetteButton } from '../CheckPipetteButton' -const render = (props: React.ComponentProps) => { +import type { ComponentProps } from 'react' + +const render = (props: ComponentProps) => { return renderWithProviders()[0] } vi.mock('@opentrons/react-api-client') describe('CheckPipetteButton', () => { - let props: React.ComponentProps + let props: ComponentProps const refetch = vi.fn(() => Promise.resolve()) beforeEach(() => { props = { diff --git a/app/src/organisms/PipetteWizardFlows/__tests__/ChoosePipette.test.tsx b/app/src/organisms/PipetteWizardFlows/__tests__/ChoosePipette.test.tsx index bda196f388c..ab14c846013 100644 --- a/app/src/organisms/PipetteWizardFlows/__tests__/ChoosePipette.test.tsx +++ b/app/src/organisms/PipetteWizardFlows/__tests__/ChoosePipette.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { LEFT, NINETY_SIX_CHANNEL, @@ -17,18 +16,20 @@ import { useAttachedPipettesFromInstrumentsQuery } from '/app/resources/instrume import { ChoosePipette } from '../ChoosePipette' import { getIsGantryEmpty } from '../utils' +import type { ComponentProps } from 'react' + vi.mock('../utils') vi.mock('/app/resources/instruments') vi.mock('/app/redux/config') -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, })[0] } describe('ChoosePipette', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { vi.mocked(getIsOnDevice).mockReturnValue(false) vi.mocked(getIsGantryEmpty).mockReturnValue(true) diff --git a/app/src/organisms/PipetteWizardFlows/__tests__/DetachPipette.test.tsx b/app/src/organisms/PipetteWizardFlows/__tests__/DetachPipette.test.tsx index d7777ed368c..a8f85ef3d73 100644 --- a/app/src/organisms/PipetteWizardFlows/__tests__/DetachPipette.test.tsx +++ b/app/src/organisms/PipetteWizardFlows/__tests__/DetachPipette.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { fireEvent, screen } from '@testing-library/react' import { describe, it, beforeEach, vi, expect } from 'vitest' @@ -19,17 +18,19 @@ import { RUN_ID_1 } from '/app/resources/runs/__fixtures__' import { FLOWS } from '../constants' import { DetachPipette } from '../DetachPipette' +import type { ComponentProps } from 'react' + vi.mock('../CheckPipetteButton') vi.mock('/app/molecules/InProgressModal/InProgressModal') -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, })[0] } describe('DetachPipette', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { selectedPipette: SINGLE_MOUNT_PIPETTES, diff --git a/app/src/organisms/PipetteWizardFlows/__tests__/DetachProbe.test.tsx b/app/src/organisms/PipetteWizardFlows/__tests__/DetachProbe.test.tsx index 059846aebb5..755c795e0d4 100644 --- a/app/src/organisms/PipetteWizardFlows/__tests__/DetachProbe.test.tsx +++ b/app/src/organisms/PipetteWizardFlows/__tests__/DetachProbe.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { fireEvent, screen } from '@testing-library/react' import { describe, it, beforeEach, vi, expect } from 'vitest' @@ -12,16 +11,18 @@ import { RUN_ID_1 } from '/app/resources/runs/__fixtures__' import { FLOWS } from '../constants' import { DetachProbe } from '../DetachProbe' +import type { ComponentProps } from 'react' + vi.mock('/app/molecules/InProgressModal/InProgressModal') -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, })[0] } describe('DetachProbe', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { selectedPipette: SINGLE_MOUNT_PIPETTES, diff --git a/app/src/organisms/PipetteWizardFlows/__tests__/ExitModal.test.tsx b/app/src/organisms/PipetteWizardFlows/__tests__/ExitModal.test.tsx index a30407379a2..b09319fcb94 100644 --- a/app/src/organisms/PipetteWizardFlows/__tests__/ExitModal.test.tsx +++ b/app/src/organisms/PipetteWizardFlows/__tests__/ExitModal.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { fireEvent, screen } from '@testing-library/react' import { describe, it, beforeEach, vi, expect } from 'vitest' @@ -7,14 +6,16 @@ import { i18n } from '/app/i18n' import { FLOWS } from '../constants' import { ExitModal } from '../ExitModal' -const render = (props: React.ComponentProps) => { +import type { ComponentProps } from 'react' + +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, })[0] } describe('ExitModal', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { diff --git a/app/src/organisms/PipetteWizardFlows/__tests__/MountPipette.test.tsx b/app/src/organisms/PipetteWizardFlows/__tests__/MountPipette.test.tsx index 4c5d9dda2e0..24e8bb926b8 100644 --- a/app/src/organisms/PipetteWizardFlows/__tests__/MountPipette.test.tsx +++ b/app/src/organisms/PipetteWizardFlows/__tests__/MountPipette.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { fireEvent, screen } from '@testing-library/react' import { describe, it, expect, beforeEach, vi } from 'vitest' @@ -16,16 +15,18 @@ import { FLOWS } from '../constants' import { CheckPipetteButton } from '../CheckPipetteButton' import { MountPipette } from '../MountPipette' +import type { ComponentProps } from 'react' + vi.mock('../CheckPipetteButton') -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, })[0] } describe('MountPipette', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { selectedPipette: SINGLE_MOUNT_PIPETTES, diff --git a/app/src/organisms/PipetteWizardFlows/__tests__/MountingPlate.test.tsx b/app/src/organisms/PipetteWizardFlows/__tests__/MountingPlate.test.tsx index 38744ad1bab..2393d1080d5 100644 --- a/app/src/organisms/PipetteWizardFlows/__tests__/MountingPlate.test.tsx +++ b/app/src/organisms/PipetteWizardFlows/__tests__/MountingPlate.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { fireEvent, waitFor, screen } from '@testing-library/react' import { describe, it, expect, beforeEach, vi } from 'vitest' @@ -10,14 +9,16 @@ import { RUN_ID_1 } from '/app/resources/runs/__fixtures__' import { FLOWS } from '../constants' import { MountingPlate } from '../MountingPlate' -const render = (props: React.ComponentProps) => { +import type { ComponentProps } from 'react' + +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, })[0] } describe('MountingPlate', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { mount: LEFT, diff --git a/app/src/organisms/PipetteWizardFlows/__tests__/Results.test.tsx b/app/src/organisms/PipetteWizardFlows/__tests__/Results.test.tsx index 3382ac401a0..df53d95cdb7 100644 --- a/app/src/organisms/PipetteWizardFlows/__tests__/Results.test.tsx +++ b/app/src/organisms/PipetteWizardFlows/__tests__/Results.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { act, fireEvent, screen, waitFor } from '@testing-library/react' import { describe, it, expect, vi, beforeEach } from 'vitest' @@ -18,19 +17,20 @@ import { RUN_ID_1 } from '/app/resources/runs/__fixtures__' import { Results } from '../Results' import { FLOWS } from '../constants' +import type { ComponentProps } from 'react' import type { Mock } from 'vitest' vi.mock('@opentrons/react-api-client') vi.mock('/app/resources/robot-settings/hooks') -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, })[0] } describe('Results', () => { - let props: React.ComponentProps + let props: ComponentProps let pipettePromise: Promise let mockRefetchInstruments: Mock beforeEach(() => { diff --git a/app/src/organisms/PipetteWizardFlows/__tests__/UnskippableModal.test.tsx b/app/src/organisms/PipetteWizardFlows/__tests__/UnskippableModal.test.tsx index bc738d0caf3..f2d51af9a5f 100644 --- a/app/src/organisms/PipetteWizardFlows/__tests__/UnskippableModal.test.tsx +++ b/app/src/organisms/PipetteWizardFlows/__tests__/UnskippableModal.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { fireEvent, screen } from '@testing-library/react' import { describe, it, expect, vi } from 'vitest' @@ -6,14 +5,16 @@ import { renderWithProviders } from '/app/__testing-utils__' import { i18n } from '/app/i18n' import { UnskippableModal } from '../UnskippableModal' -const render = (props: React.ComponentProps) => { +import type { ComponentProps } from 'react' + +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, })[0] } describe('UnskippableModal', () => { - let props: React.ComponentProps + let props: ComponentProps it('returns the correct information for unskippable modal, pressing return button calls goBack prop', () => { props = { goBack: vi.fn(), diff --git a/app/src/organisms/PipetteWizardFlows/__tests__/hooks.test.tsx b/app/src/organisms/PipetteWizardFlows/__tests__/hooks.test.tsx index f44bd96fc6e..996ec520af2 100644 --- a/app/src/organisms/PipetteWizardFlows/__tests__/hooks.test.tsx +++ b/app/src/organisms/PipetteWizardFlows/__tests__/hooks.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { describe, it, beforeEach, vi, afterEach, expect } from 'vitest' import { I18nextProvider } from 'react-i18next' import { renderHook } from '@testing-library/react' @@ -16,6 +15,8 @@ import { import { FLOWS } from '../constants' import { usePipetteFlowWizardHeaderText } from '../hooks' +import type { FunctionComponent, ReactNode } from 'react' + const BASE_PROPS_FOR_RUN_SETUP = { flowType: FLOWS.CALIBRATE, selectedPipette: SINGLE_MOUNT_PIPETTES, @@ -23,7 +24,7 @@ const BASE_PROPS_FOR_RUN_SETUP = { } describe('usePipetteFlowWizardHeaderText', () => { - let wrapper: React.FunctionComponent<{ children: React.ReactNode }> + let wrapper: FunctionComponent<{ children: ReactNode }> beforeEach(() => { wrapper = ({ children }) => ( {children} diff --git a/app/src/organisms/PipetteWizardFlows/constants.ts b/app/src/organisms/PipetteWizardFlows/constants.ts index e4ddd762d95..1d0b94878c1 100644 --- a/app/src/organisms/PipetteWizardFlows/constants.ts +++ b/app/src/organisms/PipetteWizardFlows/constants.ts @@ -18,6 +18,8 @@ export const FLOWS = { DETACH: 'DETACH', CALIBRATE: 'CALIBRATE', } + +// note: we will not be translating these item titles to be consistent with manuals export const CALIBRATION_PROBE_DISPLAY_NAME = 'Calibration Probe' export const HEX_SCREWDRIVER_DISPLAY_NAME = '2.5 mm Hex Screwdriver' export const PIPETTE_DISPLAY_NAME = '1- or 8-Channel Pipette' @@ -33,9 +35,6 @@ export const CALIBRATION_PROBE = { export const HEX_SCREWDRIVER = { loadName: 'hex_screwdriver', displayName: HEX_SCREWDRIVER_DISPLAY_NAME, - // TODO(jr, 4/3/23): add this subtitle to i18n - subtitle: - 'Provided with the robot. Using another size can strip the instruments’s screws.', } export const PIPETTE = { loadName: 'flex_pipette', diff --git a/app/src/organisms/PipetteWizardFlows/index.tsx b/app/src/organisms/PipetteWizardFlows/index.tsx index ed11df4352d..be3c22cc566 100644 --- a/app/src/organisms/PipetteWizardFlows/index.tsx +++ b/app/src/organisms/PipetteWizardFlows/index.tsx @@ -281,15 +281,19 @@ export const PipetteWizardFlows = ( /> ) - let onExit - if (currentStep == null) return null - let modalContent: JSX.Element =
        UNASSIGNED STEP
        - // These flows often have custom error messaging, so this fallback modal is shown only in specific circumstances. - if ( + if (currentStep == null) { + return null + } + + const isFatalError = (isExiting && errorMessage != null) || maintenanceRunData?.data.status === RUN_STATUS_FAILED || (errorMessage != null && createdMaintenanceRunId == null) - ) { + + let onExit: () => void + let modalContent: JSX.Element =
        UNASSIGNED STEP
        + // These flows often have custom error messaging, so this fallback modal is shown only in specific circumstances. + if (isFatalError) { modalContent = ( void) => { + if (isFatalError || showConfirmExit) { + return handleCleanUpAndClose + } else { + return onExit + } } const progressBarForCalError = @@ -418,13 +418,13 @@ export const PipetteWizardFlows = ( const wizardHeader = ( ) diff --git a/app/src/organisms/PipetteWizardFlows/types.ts b/app/src/organisms/PipetteWizardFlows/types.ts index a8785e8a31c..3870141ab6e 100644 --- a/app/src/organisms/PipetteWizardFlows/types.ts +++ b/app/src/organisms/PipetteWizardFlows/types.ts @@ -1,6 +1,7 @@ -import type { SECTIONS, FLOWS } from './constants' +import type { Dispatch, SetStateAction } from 'react' import type { useCreateCommandMutation } from '@opentrons/react-api-client' import type { PipetteMount, CreateCommand } from '@opentrons/shared-data' +import type { SECTIONS, FLOWS } from './constants' import type { AttachedPipettesFromInstrumentsQuery } from '/app/resources/instruments' export type PipetteWizardStep = @@ -78,7 +79,7 @@ export interface PipetteWizardStepProps { isRobotMoving: boolean maintenanceRunId?: string attachedPipettes: AttachedPipettesFromInstrumentsQuery - setShowErrorMessage: React.Dispatch> + setShowErrorMessage: Dispatch> errorMessage: string | null selectedPipette: SelectablePipettes isOnDevice: boolean | null diff --git a/app/src/organisms/TakeoverModal/MaintenanceRunStatusProvider.tsx b/app/src/organisms/TakeoverModal/MaintenanceRunStatusProvider.tsx index 6ba2707752b..293340a9d85 100644 --- a/app/src/organisms/TakeoverModal/MaintenanceRunStatusProvider.tsx +++ b/app/src/organisms/TakeoverModal/MaintenanceRunStatusProvider.tsx @@ -1,7 +1,8 @@ -import * as React from 'react' - +import { useState, useEffect, useMemo, createContext } from 'react' import { useNotifyCurrentMaintenanceRun } from '/app/resources/maintenance_runs' +import type { ReactNode } from 'react' + interface MaintenanceRunIds { currentRunId: string | null oddRunId: string | null @@ -12,19 +13,19 @@ export interface MaintenanceRunStatus { setOddRunIds: (state: MaintenanceRunIds) => void } -export const MaintenanceRunContext = React.createContext({ +export const MaintenanceRunContext = createContext({ getRunIds: () => ({ currentRunId: null, oddRunId: null }), setOddRunIds: () => {}, }) interface MaintenanceRunProviderProps { - children?: React.ReactNode + children?: ReactNode } export function MaintenanceRunStatusProvider( props: MaintenanceRunProviderProps ): JSX.Element { - const [oddRunIds, setOddRunIds] = React.useState({ + const [oddRunIds, setOddRunIds] = useState({ currentRunId: null, oddRunId: null, }) @@ -33,14 +34,14 @@ export function MaintenanceRunStatusProvider( refetchInterval: 5000, }).data?.data.id - React.useEffect(() => { + useEffect(() => { setOddRunIds(prevState => ({ ...prevState, currentRunId: currentRunIdQueryResult ?? null, })) }, [currentRunIdQueryResult]) - const maintenanceRunStatus = React.useMemo( + const maintenanceRunStatus = useMemo( () => ({ getRunIds: () => oddRunIds, setOddRunIds, diff --git a/app/src/organisms/TakeoverModal/MaintenanceRunTakeover.tsx b/app/src/organisms/TakeoverModal/MaintenanceRunTakeover.tsx index b4cef390203..bb2b380cef1 100644 --- a/app/src/organisms/TakeoverModal/MaintenanceRunTakeover.tsx +++ b/app/src/organisms/TakeoverModal/MaintenanceRunTakeover.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useState, useEffect } from 'react' import { useTranslation } from 'react-i18next' import { useDeleteMaintenanceRunMutation } from '@opentrons/react-api-client' @@ -7,8 +7,10 @@ import { TakeoverModal } from './TakeoverModal' import { MaintenanceRunStatusProvider } from './MaintenanceRunStatusProvider' import { useMaintenanceRunTakeover } from './useMaintenanceRunTakeover' +import type { ReactNode } from 'react' + interface MaintenanceRunTakeoverProps { - children: React.ReactNode + children: ReactNode } export function MaintenanceRunTakeover({ @@ -22,18 +24,18 @@ export function MaintenanceRunTakeover({ } interface MaintenanceRunTakeoverModalProps { - children: React.ReactNode + children: ReactNode } export function MaintenanceRunTakeoverModal( props: MaintenanceRunTakeoverModalProps ): JSX.Element { const { i18n, t } = useTranslation(['shared', 'branded']) - const [isLoading, setIsLoading] = React.useState(false) + const [isLoading, setIsLoading] = useState(false) const [ showConfirmTerminateModal, setShowConfirmTerminateModal, - ] = React.useState(false) + ] = useState(false) const { oddRunId, currentRunId } = useMaintenanceRunTakeover().getRunIds() const isMaintenanceRunCurrent = currentRunId != null @@ -50,7 +52,7 @@ export function MaintenanceRunTakeoverModal( } } - React.useEffect(() => { + useEffect(() => { if (currentRunId == null) { setIsLoading(false) setShowConfirmTerminateModal(false) diff --git a/app/src/organisms/TakeoverModal/TakeoverModal.tsx b/app/src/organisms/TakeoverModal/TakeoverModal.tsx index 8f6441124a7..9c6f42a944d 100644 --- a/app/src/organisms/TakeoverModal/TakeoverModal.tsx +++ b/app/src/organisms/TakeoverModal/TakeoverModal.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { createPortal } from 'react-dom' import { useTranslation } from 'react-i18next' @@ -18,12 +17,13 @@ import { getTopPortalEl } from '/app/App/portal' import { SmallButton } from '/app/atoms/buttons' import { OddModal } from '/app/molecules/OddModal' +import type { Dispatch, SetStateAction } from 'react' import type { OddModalHeaderBaseProps } from '/app/molecules/OddModal/types' interface TakeoverModalProps { title: string showConfirmTerminateModal: boolean - setShowConfirmTerminateModal: React.Dispatch> + setShowConfirmTerminateModal: Dispatch> confirmTerminate: () => void terminateInProgress: boolean } @@ -48,7 +48,7 @@ export function TakeoverModal(props: TakeoverModalProps): JSX.Element { showConfirmTerminateModal ? ( // confirm terminate modal - + {t('branded:confirm_terminate')} @@ -79,6 +79,7 @@ export function TakeoverModal(props: TakeoverModalProps): JSX.Element { gridGap={SPACING.spacing40} alignItems={ALIGN_CENTER} justifyContent={ALIGN_CENTER} + width="100%" > null, } -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, }) } describe('MaintenanceRunTakeover', () => { - let props: React.ComponentProps + let props: ComponentProps const testComponent =
        {'Test Component'}
        beforeEach(() => { diff --git a/app/src/organisms/TakeoverModal/__tests__/TakeoverModal.test.tsx b/app/src/organisms/TakeoverModal/__tests__/TakeoverModal.test.tsx index a902544e4a0..4f3ffd78894 100644 --- a/app/src/organisms/TakeoverModal/__tests__/TakeoverModal.test.tsx +++ b/app/src/organisms/TakeoverModal/__tests__/TakeoverModal.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { fireEvent, screen } from '@testing-library/react' import { describe, it, expect, vi, beforeEach } from 'vitest' import '@testing-library/jest-dom/vitest' @@ -6,14 +5,16 @@ import { i18n } from '/app/i18n' import { renderWithProviders } from '/app/__testing-utils__' import { TakeoverModal } from '../TakeoverModal' -const render = (props: React.ComponentProps) => { +import type { ComponentProps } from 'react' + +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, })[0] } describe('TakeoverModal', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { showConfirmTerminateModal: false, diff --git a/app/src/organisms/ToasterOven/ToasterOven.tsx b/app/src/organisms/ToasterOven/ToasterOven.tsx index 4f29be519ae..c3130793750 100644 --- a/app/src/organisms/ToasterOven/ToasterOven.tsx +++ b/app/src/organisms/ToasterOven/ToasterOven.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useState } from 'react' import { useSelector } from 'react-redux' import { v4 as uuidv4 } from 'uuid' @@ -17,6 +17,7 @@ import { import { getIsOnDevice } from '/app/redux/config' import { ToasterContext } from './ToasterContext' +import type { ReactNode } from 'react' import type { SnackbarProps } from '@opentrons/components' import type { ToastProps, @@ -25,7 +26,7 @@ import type { import type { MakeSnackbarOptions, MakeToastOptions } from './ToasterContext' interface ToasterOvenProps { - children: React.ReactNode + children: ReactNode } /** @@ -34,8 +35,8 @@ interface ToasterOvenProps { * @returns */ export function ToasterOven({ children }: ToasterOvenProps): JSX.Element { - const [toasts, setToasts] = React.useState([]) - const [snackbar, setSnackbar] = React.useState(null) + const [toasts, setToasts] = useState([]) + const [snackbar, setSnackbar] = useState(null) const isOnDevice = useSelector(getIsOnDevice) ?? null const displayType: 'desktop' | 'odd' = diff --git a/app/src/organisms/UpdateRobotSoftware/ErrorUpdateSoftware.tsx b/app/src/organisms/UpdateRobotSoftware/ErrorUpdateSoftware.tsx index f1629e15b64..ab1b44991b6 100644 --- a/app/src/organisms/UpdateRobotSoftware/ErrorUpdateSoftware.tsx +++ b/app/src/organisms/UpdateRobotSoftware/ErrorUpdateSoftware.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { useTranslation } from 'react-i18next' import { @@ -14,9 +13,11 @@ import { TYPOGRAPHY, } from '@opentrons/components' +import type { ReactNode } from 'react' + interface ErrorUpdateSoftwareProps { errorMessage: string - children: React.ReactNode + children: ReactNode } export function ErrorUpdateSoftware({ errorMessage, diff --git a/app/src/organisms/UpdateRobotSoftware/__tests__/CompleteUpdateSoftware.test.tsx b/app/src/organisms/UpdateRobotSoftware/__tests__/CompleteUpdateSoftware.test.tsx index b6b91424b92..be6d9056cc0 100644 --- a/app/src/organisms/UpdateRobotSoftware/__tests__/CompleteUpdateSoftware.test.tsx +++ b/app/src/organisms/UpdateRobotSoftware/__tests__/CompleteUpdateSoftware.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { screen } from '@testing-library/react' import { describe, it, expect, vi, beforeEach } from 'vitest' import '@testing-library/jest-dom/vitest' @@ -6,16 +5,18 @@ import { i18n } from '/app/i18n' import { renderWithProviders } from '/app/__testing-utils__' import { CompleteUpdateSoftware } from '../CompleteUpdateSoftware' +import type { ComponentProps } from 'react' + vi.mock('/app/redux/robot-admin') -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, }) } describe('CompleteUpdateSoftware', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { diff --git a/app/src/organisms/UpdateRobotSoftware/__tests__/ErrorUpdateSoftware.test.tsx b/app/src/organisms/UpdateRobotSoftware/__tests__/ErrorUpdateSoftware.test.tsx index d1706a4bf18..6f585d31db0 100644 --- a/app/src/organisms/UpdateRobotSoftware/__tests__/ErrorUpdateSoftware.test.tsx +++ b/app/src/organisms/UpdateRobotSoftware/__tests__/ErrorUpdateSoftware.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { screen } from '@testing-library/react' import { describe, it, beforeEach } from 'vitest' import '@testing-library/jest-dom/vitest' @@ -6,14 +5,16 @@ import { renderWithProviders } from '/app/__testing-utils__' import { i18n } from '/app/i18n' import { ErrorUpdateSoftware } from '../ErrorUpdateSoftware' -const render = (props: React.ComponentProps) => { +import type { ComponentProps } from 'react' + +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, }) } describe('ErrorUpdateSoftware', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { diff --git a/app/src/organisms/UpdateRobotSoftware/__tests__/UpdateSoftware.test.tsx b/app/src/organisms/UpdateRobotSoftware/__tests__/UpdateSoftware.test.tsx index 93deeb27956..76344df2c4c 100644 --- a/app/src/organisms/UpdateRobotSoftware/__tests__/UpdateSoftware.test.tsx +++ b/app/src/organisms/UpdateRobotSoftware/__tests__/UpdateSoftware.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { screen } from '@testing-library/react' import { describe, it, beforeEach } from 'vitest' import '@testing-library/jest-dom/vitest' @@ -6,14 +5,16 @@ import { renderWithProviders } from '/app/__testing-utils__' import { i18n } from '/app/i18n' import { UpdateSoftware } from '../UpdateSoftware' -const render = (props: React.ComponentProps) => { +import type { ComponentProps } from 'react' + +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, }) } describe('UpdateSoftware', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { updateType: 'downloading', diff --git a/app/src/organisms/WellSelection/Selection384Wells.tsx b/app/src/organisms/WellSelection/Selection384Wells.tsx index d0fcddef752..94e2eaf9e74 100644 --- a/app/src/organisms/WellSelection/Selection384Wells.tsx +++ b/app/src/organisms/WellSelection/Selection384Wells.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useState, useEffect } from 'react' import { useTranslation } from 'react-i18next' import flatten from 'lodash/flatten' @@ -15,6 +15,7 @@ import { import { IconButton } from '/app/atoms/buttons/IconButton' +import type { Dispatch, SetStateAction, ReactNode } from 'react' import type { WellGroup } from '@opentrons/components' import type { LabwareDefinition2, @@ -26,7 +27,7 @@ interface Selection384WellsProps { channels: PipetteChannels definition: LabwareDefinition2 deselectWells: (wells: string[]) => void - labwareRender: React.ReactNode + labwareRender: ReactNode selectWells: (wellGroup: WellGroup) => unknown } @@ -43,18 +44,18 @@ export function Selection384Wells({ labwareRender, selectWells, }: Selection384WellsProps): JSX.Element { - const [selectBy, setSelectBy] = React.useState<'columns' | 'wells'>('columns') + const [selectBy, setSelectBy] = useState<'columns' | 'wells'>('columns') - const [lastSelectedIndex, setLastSelectedIndex] = React.useState< - number | null - >(null) + const [lastSelectedIndex, setLastSelectedIndex] = useState( + null + ) - const [startingWellState, setStartingWellState] = React.useState< + const [startingWellState, setStartingWellState] = useState< Record >({ A1: false, A2: false, B1: false, B2: false }) // to reset last selected index and starting well state on page-level selected well reset - React.useEffect(() => { + useEffect(() => { if (Object.keys(allSelectedWells).length === 0) { setLastSelectedIndex(null) if (channels === 96) { @@ -180,8 +181,8 @@ export function Selection384Wells({ interface SelectByProps { selectBy: 'columns' | 'wells' - setSelectBy: React.Dispatch> - setLastSelectedIndex: React.Dispatch> + setSelectBy: Dispatch> + setLastSelectedIndex: Dispatch> } function SelectBy({ @@ -244,8 +245,8 @@ function StartingWell({ deselectWells: (wells: string[]) => void selectWells: (wellGroup: WellGroup) => void startingWellState: Record - setStartingWellState: React.Dispatch< - React.SetStateAction> + setStartingWellState: Dispatch< + SetStateAction> > wells: string[] }): JSX.Element { @@ -255,7 +256,7 @@ function StartingWell({ channels === 8 ? ['A1', 'B1'] : ['A1', 'A2', 'B1', 'B2'] // on mount, select A1 well group for 96-channel - React.useEffect(() => { + useEffect(() => { // deselect all wells on mount; clears well selection when navigating back within quick transfer flow // otherwise, selected wells and lastSelectedIndex pointer will be out of sync deselectWells(wells) diff --git a/app/src/organisms/WellSelection/SelectionRect.tsx b/app/src/organisms/WellSelection/SelectionRect.tsx index 7c9d1ac0357..6d241d5f0c0 100644 --- a/app/src/organisms/WellSelection/SelectionRect.tsx +++ b/app/src/organisms/WellSelection/SelectionRect.tsx @@ -1,19 +1,20 @@ -import * as React from 'react' +import { useState, useRef, useCallback, useEffect } from 'react' import { Flex, JUSTIFY_CENTER } from '@opentrons/components' +import type { MouseEventHandler, ReactNode, TouchEventHandler } from 'react' import type { DragRect, GenericRect } from './types' interface SelectionRectProps { onSelectionMove?: (rect: GenericRect) => void onSelectionDone?: (rect: GenericRect) => void - children?: React.ReactNode + children?: ReactNode } export function SelectionRect(props: SelectionRectProps): JSX.Element { const { onSelectionMove, onSelectionDone, children } = props - const [positions, setPositions] = React.useState(null) - const parentRef = React.useRef(null) + const [positions, setPositions] = useState(null) + const parentRef = useRef(null) const getRect = (args: DragRect): GenericRect => { const { xStart, yStart, xDynamic, yDynamic } = args @@ -25,7 +26,7 @@ export function SelectionRect(props: SelectionRectProps): JSX.Element { } } - const handleDrag = React.useCallback( + const handleDrag = useCallback( (e: TouchEvent | MouseEvent): void => { let xDynamic: number let yDynamic: number @@ -55,7 +56,7 @@ export function SelectionRect(props: SelectionRectProps): JSX.Element { [onSelectionMove] ) - const handleDragEnd = React.useCallback( + const handleDragEnd = useCallback( (e: TouchEvent | MouseEvent): void => { if (!(e instanceof TouchEvent) && !(e instanceof MouseEvent)) { return @@ -70,7 +71,7 @@ export function SelectionRect(props: SelectionRectProps): JSX.Element { [onSelectionDone, positions] ) - const handleTouchStart: React.TouchEventHandler = e => { + const handleTouchStart: TouchEventHandler = e => { const touch = e.touches[0] setPositions({ xStart: touch.clientX, @@ -80,7 +81,7 @@ export function SelectionRect(props: SelectionRectProps): JSX.Element { }) } - const handleMouseDown: React.MouseEventHandler = e => { + const handleMouseDown: MouseEventHandler = e => { setPositions({ xStart: e.clientX, xDynamic: e.clientX, @@ -89,7 +90,7 @@ export function SelectionRect(props: SelectionRectProps): JSX.Element { }) } - React.useEffect(() => { + useEffect(() => { document.addEventListener('touchmove', handleDrag) document.addEventListener('touchend', handleDragEnd) document.addEventListener('mousemove', handleDrag) diff --git a/app/src/organisms/WellSelection/index.tsx b/app/src/organisms/WellSelection/index.tsx index 06daf9536a5..27949aa14e6 100644 --- a/app/src/organisms/WellSelection/index.tsx +++ b/app/src/organisms/WellSelection/index.tsx @@ -1,5 +1,6 @@ import { useState } from 'react' import reduce from 'lodash/reduce' +import pick from 'lodash/pick' import { COLORS, Labware, RobotCoordinateSpace } from '@opentrons/components' import { @@ -29,6 +30,8 @@ interface WellSelectionProps { pipetteNozzleDetails?: NozzleLayoutDetails /* Whether highlighting and selectWells() updates are permitted. */ allowSelect?: boolean + /* Whether selecting more than the channel count of well locations is permitted. */ + allowMultiDrag?: boolean } export function WellSelection(props: WellSelectionProps): JSX.Element { @@ -40,6 +43,7 @@ export function WellSelection(props: WellSelectionProps): JSX.Element { channels, pipetteNozzleDetails, allowSelect = true, + allowMultiDrag = true, } = props const [highlightedWells, setHighlightedWells] = useState({}) @@ -61,16 +65,21 @@ export function WellSelection(props: WellSelectionProps): JSX.Element { }) if (!wellSet) { return acc + } else if (allowMultiDrag) { + return { ...acc, [wellSet[0]]: null } + } else { + return { [wellSet[0]]: null } } - return { ...acc, [wellSet[0]]: null } }, {} ) return primaryWells + } else { + // single-channel or ingred selection mode + return allowMultiDrag + ? selectedWells + : pick(selectedWells, Object.keys(selectedWells)[0]) } - - // single-channel or ingred selection mode - return selectedWells } const _getWellsFromRect: (rect: GenericRect) => WellGroup = rect => { diff --git a/app/src/pages/Desktop/AppSettings/GeneralSettings.tsx b/app/src/pages/Desktop/AppSettings/GeneralSettings.tsx index db948403fd0..2c579ace421 100644 --- a/app/src/pages/Desktop/AppSettings/GeneralSettings.tsx +++ b/app/src/pages/Desktop/AppSettings/GeneralSettings.tsx @@ -1,8 +1,9 @@ // app info card with version and updated -import { useState } from 'react' +import { useState, useEffect } from 'react' import { createPortal } from 'react-dom' import { useTranslation } from 'react-i18next' import { useSelector, useDispatch } from 'react-redux' +import uuidv1 from 'uuid/v4' import { ALIGN_CENTER, @@ -41,12 +42,9 @@ import { import { useTrackEvent, ANALYTICS_APP_UPDATE_NOTIFICATIONS_TOGGLED, + ANALYTICS_LANGUAGE_UPDATED_DESKTOP_APP_SETTINGS, } from '/app/redux/analytics' -import { - getAppLanguage, - updateConfigValue, - useFeatureFlag, -} from '/app/redux/config' +import { getAppLanguage, updateConfigValue } from '/app/redux/config' import { UpdateAppModal } from '/app/organisms/Desktop/UpdateAppModal' import { PreviousVersionModal } from '/app/organisms/Desktop/AppSettings/PreviousVersionModal' import { ConnectRobotSlideout } from '/app/organisms/Desktop/AppSettings/ConnectRobotSlideout' @@ -59,6 +57,7 @@ const GITHUB_LINK = 'https://github.com/Opentrons/opentrons/blob/edge/app-shell/build/release-notes.md' const ENABLE_APP_UPDATE_NOTIFICATIONS = 'Enable app update notifications' +const uuid: () => string = uuidv1 export function GeneralSettings(): JSX.Element { const { t } = useTranslation(['app_settings', 'shared', 'branded']) @@ -70,12 +69,21 @@ export function GeneralSettings(): JSX.Element { ] = useState(false) const updateAvailable = Boolean(useSelector(getAvailableShellUpdate)) - const enableLocalization = useFeatureFlag('enableLocalization') const appLanguage = useSelector(getAppLanguage) const currentLanguageOption = LANGUAGES.find(lng => lng.value === appLanguage) - + let transactionId = '' + useEffect(() => { + transactionId = uuid() + }, []) const handleDropdownClick = (value: string): void => { dispatch(updateConfigValue('language.appLanguage', value)) + trackEvent({ + name: ANALYTICS_LANGUAGE_UPDATED_DESKTOP_APP_SETTINGS, + properties: { + language: value, + transactionId, + }, + }) } const [showUpdateBanner, setShowUpdateBanner] = useState( @@ -277,11 +285,12 @@ export function GeneralSettings(): JSX.Element {
        - {enableLocalization && currentLanguageOption != null ? ( + {currentLanguageOption != null ? ( <> => { ) } +const mockTrackEvent = vi.fn() + describe('GeneralSettings', () => { beforeEach(() => { vi.mocked(Shell.getAvailableShellUpdate).mockReturnValue(null) vi.mocked(getAlertIsPermanentlyIgnored).mockReturnValue(false) vi.mocked(getAppLanguage).mockReturnValue(US_ENGLISH) - when(vi.mocked(useFeatureFlag)) - .calledWith('enableLocalization') - .thenReturn(true) + vi.mocked(useTrackEvent).mockReturnValue(mockTrackEvent) }) afterEach(() => { vi.resetAllMocks() @@ -126,5 +125,12 @@ describe('GeneralSettings', () => { 'language.appLanguage', SIMPLIFIED_CHINESE ) + expect(mockTrackEvent).toHaveBeenCalledWith({ + name: ANALYTICS_LANGUAGE_UPDATED_DESKTOP_APP_SETTINGS, + properties: { + language: SIMPLIFIED_CHINESE, + transactionId: expect.anything(), + }, + }) }) }) diff --git a/app/src/pages/Desktop/Labware/__tests__/hooks.test.tsx b/app/src/pages/Desktop/Labware/__tests__/hooks.test.tsx index 5fe55c260dc..c2edcacd08a 100644 --- a/app/src/pages/Desktop/Labware/__tests__/hooks.test.tsx +++ b/app/src/pages/Desktop/Labware/__tests__/hooks.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { Provider } from 'react-redux' import { createStore } from 'redux' import { vi, it, describe, expect, beforeEach, afterEach } from 'vitest' @@ -20,6 +19,7 @@ import { import { useLabwareFailure, useNewLabwareName } from '../hooks' import { useAllLabware } from '/app/local-resources/labware' +import type { FunctionComponent, ReactNode } from 'react' import type { Store } from 'redux' import type { State } from '/app/redux/types' import type { FailedLabwareFile } from '/app/redux/custom-labware/types' @@ -39,7 +39,7 @@ describe('useAllLabware hook', () => { }) it('should return object with only definition and modified date', () => { - const wrapper: React.FunctionComponent<{ children: React.ReactNode }> = ({ + const wrapper: FunctionComponent<{ children: ReactNode }> = ({ children, }) => {children} const { result } = renderHook(() => useAllLabware('reverse', 'all'), { @@ -53,7 +53,7 @@ describe('useAllLabware hook', () => { expect(labware2.definition).toBe(mockValidLabware.definition) }) it('should return alphabetically sorted list', () => { - const wrapper: React.FunctionComponent<{ children: React.ReactNode }> = ({ + const wrapper: FunctionComponent<{ children: ReactNode }> = ({ children, }) => {children} const { result } = renderHook(() => useAllLabware('alphabetical', 'all'), { @@ -67,7 +67,7 @@ describe('useAllLabware hook', () => { expect(labware1.definition).toBe(mockValidLabware.definition) }) it('should return no labware if not the right filter', () => { - const wrapper: React.FunctionComponent<{ children: React.ReactNode }> = ({ + const wrapper: FunctionComponent<{ children: ReactNode }> = ({ children, }) => {children} const { result } = renderHook(() => useAllLabware('reverse', 'reservoir'), { @@ -80,7 +80,7 @@ describe('useAllLabware hook', () => { expect(labware2).toBe(undefined) }) it('should return labware with wellPlate filter', () => { - const wrapper: React.FunctionComponent<{ children: React.ReactNode }> = ({ + const wrapper: FunctionComponent<{ children: ReactNode }> = ({ children, }) => {children} const { result } = renderHook(() => useAllLabware('reverse', 'wellPlate'), { @@ -94,7 +94,7 @@ describe('useAllLabware hook', () => { expect(labware2.definition).toBe(mockValidLabware.definition) }) it('should return custom labware with customLabware filter', () => { - const wrapper: React.FunctionComponent<{ children: React.ReactNode }> = ({ + const wrapper: FunctionComponent<{ children: ReactNode }> = ({ children, }) => {children} const { result } = renderHook( @@ -127,7 +127,7 @@ describe('useLabwareFailure hook', () => { vi.restoreAllMocks() }) it('should return invalid labware definition', () => { - const wrapper: React.FunctionComponent<{ children: React.ReactNode }> = ({ + const wrapper: FunctionComponent<{ children: ReactNode }> = ({ children, }) => ( @@ -147,7 +147,7 @@ describe('useLabwareFailure hook', () => { errorMessage: null, }) - const wrapper: React.FunctionComponent<{ children: React.ReactNode }> = ({ + const wrapper: FunctionComponent<{ children: ReactNode }> = ({ children, }) => ( @@ -170,7 +170,7 @@ describe('useLabwareFailure hook', () => { errorMessage: null, }) - const wrapper: React.FunctionComponent<{ children: React.ReactNode }> = ({ + const wrapper: FunctionComponent<{ children: ReactNode }> = ({ children, }) => ( @@ -190,7 +190,7 @@ describe('useLabwareFailure hook', () => { errorMessage: 'error', }) - const wrapper: React.FunctionComponent<{ children: React.ReactNode }> = ({ + const wrapper: FunctionComponent<{ children: ReactNode }> = ({ children, }) => ( @@ -217,7 +217,7 @@ describe('useNewLabwareName hook', () => { }) it('should return filename as a string', () => { - const wrapper: React.FunctionComponent<{ children: React.ReactNode }> = ({ + const wrapper: FunctionComponent<{ children: ReactNode }> = ({ children, }) => {children} const { result } = renderHook(useNewLabwareName, { wrapper }) diff --git a/app/src/pages/Desktop/Labware/index.tsx b/app/src/pages/Desktop/Labware/index.tsx index 83f9dd94f3f..5d8959a5861 100644 --- a/app/src/pages/Desktop/Labware/index.tsx +++ b/app/src/pages/Desktop/Labware/index.tsx @@ -63,6 +63,7 @@ const labwareDisplayCategoryFilters: LabwareFilter[] = [ 'wellPlate', ] +// note: we've decided not to translate these categories const FILTER_OPTIONS: DropdownOption[] = labwareDisplayCategoryFilters.map( category => ({ name: startCase(category), diff --git a/app/src/pages/Desktop/Protocols/ProtocolDetails/index.tsx b/app/src/pages/Desktop/Protocols/ProtocolDetails/index.tsx index 66402416da7..bd301c526f4 100644 --- a/app/src/pages/Desktop/Protocols/ProtocolDetails/index.tsx +++ b/app/src/pages/Desktop/Protocols/ProtocolDetails/index.tsx @@ -2,7 +2,11 @@ import { useEffect } from 'react' import { useParams, Navigate } from 'react-router-dom' import { useDispatch, useSelector } from 'react-redux' -import { fetchProtocols, getStoredProtocol } from '/app/redux/protocol-storage' +import { + fetchProtocols, + getStoredProtocol, + getStoredProtocolGroupedCommands, +} from '/app/redux/protocol-storage' import { ProtocolDetails as ProtocolDetailsContents } from '/app/organisms/Desktop/ProtocolDetails' import type { Dispatch, State } from '/app/redux/types' @@ -17,13 +21,18 @@ export function ProtocolDetails(): JSX.Element { const storedProtocol = useSelector((state: State) => getStoredProtocol(state, protocolKey) ) - + const groupedCommands = useSelector((state: State) => + getStoredProtocolGroupedCommands(state, protocolKey) + ) useEffect(() => { dispatch(fetchProtocols()) }, [dispatch]) return storedProtocol != null ? ( - + ) : ( ) diff --git a/app/src/pages/ODD/ChooseLanguage/__tests__/ChooseLanguage.test.tsx b/app/src/pages/ODD/ChooseLanguage/__tests__/ChooseLanguage.test.tsx index 8508a7b4d08..fa5c793e2d2 100644 --- a/app/src/pages/ODD/ChooseLanguage/__tests__/ChooseLanguage.test.tsx +++ b/app/src/pages/ODD/ChooseLanguage/__tests__/ChooseLanguage.test.tsx @@ -1,10 +1,12 @@ -import { vi, it, describe, expect } from 'vitest' +import { vi, it, describe, expect, beforeEach } from 'vitest' import { fireEvent, screen } from '@testing-library/react' import { MemoryRouter } from 'react-router-dom' import { renderWithProviders } from '/app/__testing-utils__' import { i18n } from '/app/i18n' -import { updateConfigValue } from '/app/redux/config' +import { ANALYTICS_LANGUAGE_UPDATED_ODD_UNBOXING_FLOW } from '/app/redux/analytics' +import { useTrackEventWithRobotSerial } from '/app/redux-resources/analytics' +import { updateConfigValue, getAppLanguage } from '/app/redux/config' import { ChooseLanguage } from '..' import type { NavigateFunction } from 'react-router-dom' @@ -18,6 +20,9 @@ vi.mock('react-router-dom', async importOriginal => { } }) vi.mock('/app/redux/config') +vi.mock('/app/redux-resources/analytics') + +const mockTrackEvent = vi.fn() const render = () => { return renderWithProviders( @@ -31,6 +36,12 @@ const render = () => { } describe('ChooseLanguage', () => { + beforeEach(() => { + vi.mocked(useTrackEventWithRobotSerial).mockReturnValue({ + trackEventWithRobotSerial: mockTrackEvent, + }) + vi.mocked(getAppLanguage).mockReturnValue('en-US') + }) it('should render text, language options, and continue button', () => { render() screen.getByText('Choose your language') @@ -54,6 +65,12 @@ describe('ChooseLanguage', () => { it('should call mockNavigate when tapping continue', () => { render() fireEvent.click(screen.getByRole('button', { name: 'Continue' })) + expect(mockTrackEvent).toHaveBeenCalledWith({ + name: ANALYTICS_LANGUAGE_UPDATED_ODD_UNBOXING_FLOW, + properties: { + language: 'en-US', + }, + }) expect(mockNavigate).toHaveBeenCalledWith('/welcome') }) }) diff --git a/app/src/pages/ODD/ChooseLanguage/index.tsx b/app/src/pages/ODD/ChooseLanguage/index.tsx index d0110e68591..8ecb87451f7 100644 --- a/app/src/pages/ODD/ChooseLanguage/index.tsx +++ b/app/src/pages/ODD/ChooseLanguage/index.tsx @@ -13,6 +13,8 @@ import { TYPOGRAPHY, } from '@opentrons/components' +import { ANALYTICS_LANGUAGE_UPDATED_ODD_UNBOXING_FLOW } from '/app/redux/analytics' +import { useTrackEventWithRobotSerial } from '/app/redux-resources/analytics' import { MediumButton } from '/app/atoms/buttons' import { LANGUAGES, US_ENGLISH } from '/app/i18n' import { RobotSetupHeader } from '/app/organisms/ODD/RobotSetupHeader' @@ -24,6 +26,7 @@ export function ChooseLanguage(): JSX.Element { const { i18n, t } = useTranslation(['app_settings', 'shared']) const navigate = useNavigate() const dispatch = useDispatch() + const { trackEventWithRobotSerial } = useTrackEventWithRobotSerial() const appLanguage = useSelector(getAppLanguage) @@ -69,6 +72,12 @@ export function ChooseLanguage(): JSX.Element { { + trackEventWithRobotSerial({ + name: ANALYTICS_LANGUAGE_UPDATED_ODD_UNBOXING_FLOW, + properties: { + language: appLanguage, + }, + }) navigate('/welcome') }} width="100%" diff --git a/app/src/pages/ODD/ConnectViaEthernet/__tests__/DisplayConnectionStatus.test.tsx b/app/src/pages/ODD/ConnectViaEthernet/__tests__/DisplayConnectionStatus.test.tsx index 9efbfd5c3dc..0caa3a6ba10 100644 --- a/app/src/pages/ODD/ConnectViaEthernet/__tests__/DisplayConnectionStatus.test.tsx +++ b/app/src/pages/ODD/ConnectViaEthernet/__tests__/DisplayConnectionStatus.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { vi, it, describe, expect, beforeEach } from 'vitest' import { fireEvent, screen } from '@testing-library/react' @@ -6,6 +5,7 @@ import { renderWithProviders } from '/app/__testing-utils__' import { i18n } from '/app/i18n' import { DisplayConnectionStatus } from '../DisplayConnectionStatus' +import type { ComponentProps } from 'react' import type { NavigateFunction } from 'react-router-dom' const mockFunc = vi.fn() @@ -18,16 +18,14 @@ vi.mock('react-router-dom', async importOriginal => { } }) -const render = ( - props: React.ComponentProps -) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, }) } describe('DisplayConnectionStatus', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { diff --git a/app/src/pages/ODD/ConnectViaEthernet/__tests__/TitleHeader.test.tsx b/app/src/pages/ODD/ConnectViaEthernet/__tests__/TitleHeader.test.tsx index cffa8e9c63a..8c8667619b9 100644 --- a/app/src/pages/ODD/ConnectViaEthernet/__tests__/TitleHeader.test.tsx +++ b/app/src/pages/ODD/ConnectViaEthernet/__tests__/TitleHeader.test.tsx @@ -1,10 +1,10 @@ -import type * as React from 'react' import { vi, it, describe, expect, beforeEach } from 'vitest' import { fireEvent, screen } from '@testing-library/react' import { renderWithProviders } from '/app/__testing-utils__' import { TitleHeader } from '../TitleHeader' +import type { ComponentProps } from 'react' import type { NavigateFunction } from 'react-router-dom' const mockNavigate = vi.fn() @@ -16,12 +16,12 @@ vi.mock('react-router-dom', async importOriginal => { } }) -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders() } describe('TitleHeader', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { diff --git a/app/src/pages/ODD/ConnectViaEthernet/index.tsx b/app/src/pages/ODD/ConnectViaEthernet/index.tsx index 644d0616e1e..94018c4e32e 100644 --- a/app/src/pages/ODD/ConnectViaEthernet/index.tsx +++ b/app/src/pages/ODD/ConnectViaEthernet/index.tsx @@ -21,7 +21,7 @@ import type { State, Dispatch } from '/app/redux/types' const STATUS_REFRESH_MS = 5000 export function ConnectViaEthernet(): JSX.Element { - const { t } = useTranslation('device_settings') + const { t } = useTranslation(['device_settings', 'shared']) const localRobot = useSelector(getLocalRobot) const robotName = localRobot?.name != null ? localRobot.name : 'no name' const dispatch = useDispatch() diff --git a/app/src/pages/ODD/ConnectViaWifi/JoinOtherNetwork.tsx b/app/src/pages/ODD/ConnectViaWifi/JoinOtherNetwork.tsx index 4abd146238a..1068a65dd9a 100644 --- a/app/src/pages/ODD/ConnectViaWifi/JoinOtherNetwork.tsx +++ b/app/src/pages/ODD/ConnectViaWifi/JoinOtherNetwork.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useState } from 'react' import { useTranslation } from 'react-i18next' import { Flex, DIRECTION_COLUMN } from '@opentrons/components' @@ -6,21 +6,22 @@ import { Flex, DIRECTION_COLUMN } from '@opentrons/components' import { SetWifiSsid } from '/app/organisms/ODD/NetworkSettings' import { RobotSetupHeader } from '/app/organisms/ODD/RobotSetupHeader' +import type { Dispatch, SetStateAction } from 'react' import type { WifiScreenOption } from './' interface JoinOtherNetworkProps { setCurrentOption: (option: WifiScreenOption) => void - setSelectedSsid: React.Dispatch> + setSelectedSsid: Dispatch> } export function JoinOtherNetwork({ setCurrentOption, setSelectedSsid, }: JoinOtherNetworkProps): JSX.Element { - const { i18n, t } = useTranslation('device_settings') + const { i18n, t } = useTranslation(['device_settings', 'shared']) - const [inputSsid, setInputSsid] = React.useState('') - const [errorMessage, setErrorMessage] = React.useState(null) + const [inputSsid, setInputSsid] = useState('') + const [errorMessage, setErrorMessage] = useState(null) const handleContinue = (): void => { if (inputSsid.length >= 2 && inputSsid.length <= 32) { @@ -34,7 +35,7 @@ export function JoinOtherNetwork({ return ( { setCurrentOption('WifiList') diff --git a/app/src/pages/ODD/ConnectViaWifi/SelectAuthenticationType.tsx b/app/src/pages/ODD/ConnectViaWifi/SelectAuthenticationType.tsx index 34f8e11e788..c9044f10ff0 100644 --- a/app/src/pages/ODD/ConnectViaWifi/SelectAuthenticationType.tsx +++ b/app/src/pages/ODD/ConnectViaWifi/SelectAuthenticationType.tsx @@ -21,12 +21,12 @@ export function SelectAuthenticationType({ setCurrentOption, setSelectedAuthType, }: SelectAuthenticationTypeProps): JSX.Element { - const { i18n, t } = useTranslation('device_settings') + const { i18n, t } = useTranslation(['device_settings', 'shared']) return ( { setCurrentOption('WifiList') diff --git a/app/src/pages/ODD/ConnectViaWifi/SetWifiCred.tsx b/app/src/pages/ODD/ConnectViaWifi/SetWifiCred.tsx index e2716fc2282..2d81286d058 100644 --- a/app/src/pages/ODD/ConnectViaWifi/SetWifiCred.tsx +++ b/app/src/pages/ODD/ConnectViaWifi/SetWifiCred.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { useTranslation } from 'react-i18next' import { Flex, DIRECTION_COLUMN } from '@opentrons/components' @@ -6,13 +5,14 @@ import { Flex, DIRECTION_COLUMN } from '@opentrons/components' import { SetWifiCred as SetWifiCredComponent } from '/app/organisms/ODD/NetworkSettings' import { RobotSetupHeader } from '/app/organisms/ODD/RobotSetupHeader' +import type { Dispatch, SetStateAction } from 'react' import type { WifiScreenOption } from './' interface SetWifiCredProps { handleConnect: () => void password: string setCurrentOption: (option: WifiScreenOption) => void - setPassword: React.Dispatch> + setPassword: Dispatch> } export function SetWifiCred({ diff --git a/app/src/pages/ODD/DeckConfiguration/index.tsx b/app/src/pages/ODD/DeckConfiguration/index.tsx index 2de5de02c46..71b8b5905bc 100644 --- a/app/src/pages/ODD/DeckConfiguration/index.tsx +++ b/app/src/pages/ODD/DeckConfiguration/index.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useState } from 'react' import { createPortal } from 'react-dom' import { useTranslation } from 'react-i18next' import { useNavigate } from 'react-router-dom' @@ -20,6 +20,7 @@ import { useNotifyDeckConfigurationQuery, } from '/app/resources/deck_configuration' +import type { ComponentProps } from 'react' import type { SmallButton } from '/app/atoms/buttons' export function DeckConfigurationEditor(): JSX.Element { @@ -32,7 +33,7 @@ export function DeckConfigurationEditor(): JSX.Element { const [ showSetupInstructionsModal, setShowSetupInstructionsModal, - ] = React.useState(false) + ] = useState(false) const isOnDevice = true const { @@ -41,10 +42,9 @@ export function DeckConfigurationEditor(): JSX.Element { addFixtureModal, } = useDeckConfigurationEditingTools(isOnDevice) - const [ - showDiscardChangeModal, - setShowDiscardChangeModal, - ] = React.useState(false) + const [showDiscardChangeModal, setShowDiscardChangeModal] = useState( + false + ) const deckConfig = useNotifyDeckConfigurationQuery().data ?? [] @@ -52,7 +52,7 @@ export function DeckConfigurationEditor(): JSX.Element { navigate(-1) } - const secondaryButtonProps: React.ComponentProps = { + const secondaryButtonProps: ComponentProps = { onClick: () => { setShowSetupInstructionsModal(true) }, diff --git a/app/src/pages/ODD/InitialLoadingScreen/index.tsx b/app/src/pages/ODD/InitialLoadingScreen/index.tsx index f35fad13b56..003d37e549e 100644 --- a/app/src/pages/ODD/InitialLoadingScreen/index.tsx +++ b/app/src/pages/ODD/InitialLoadingScreen/index.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { useSelector } from 'react-redux' import { ALIGN_CENTER, @@ -12,10 +11,12 @@ import { import { useRobotSettingsQuery } from '@opentrons/react-api-client' import { getIsShellReady } from '/app/redux/shell' +import type { ReactNode } from 'react' + export function InitialLoadingScreen({ children, }: { - children?: React.ReactNode + children?: ReactNode }): JSX.Element { const isShellReady = useSelector(getIsShellReady) diff --git a/app/src/pages/ODD/InstrumentDetail/InstrumentDetailOverflowMenu.tsx b/app/src/pages/ODD/InstrumentDetail/InstrumentDetailOverflowMenu.tsx index 96ea37d14d3..c49ef3d4f34 100644 --- a/app/src/pages/ODD/InstrumentDetail/InstrumentDetailOverflowMenu.tsx +++ b/app/src/pages/ODD/InstrumentDetail/InstrumentDetailOverflowMenu.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useState } from 'react' import NiceModal, { useModal } from '@ebay/nice-modal-react' import { useTranslation } from 'react-i18next' import { createPortal } from 'react-dom' @@ -26,6 +26,7 @@ import { FLOWS } from '/app/organisms/PipetteWizardFlows/constants' import { GRIPPER_FLOW_TYPES } from '/app/organisms/GripperWizardFlows/constants' import { getTopPortalEl } from '/app/App/portal' +import type { ComponentProps, MouseEventHandler } from 'react' import type { PipetteData, GripperData, @@ -55,13 +56,13 @@ const InstrumentDetailsOverflowMenu = NiceModal.create( const { instrument, host, enableDTWiz } = props const { t } = useTranslation('robot_controls') const modal = useModal() - const [wizardProps, setWizardProps] = React.useState< - | React.ComponentProps - | React.ComponentProps + const [wizardProps, setWizardProps] = useState< + | ComponentProps + | ComponentProps | null >(null) const sharedGripperWizardProps: Pick< - React.ComponentProps, + ComponentProps, 'attachedGripper' | 'closeFlow' > = { attachedGripper: instrument, @@ -75,7 +76,7 @@ const InstrumentDetailsOverflowMenu = NiceModal.create( instrument.mount !== 'extension' && instrument.data?.channels === 96 - const handleRecalibrate: React.MouseEventHandler = () => { + const handleRecalibrate: MouseEventHandler = () => { if (instrument?.ok) { setWizardProps( instrument.mount === 'extension' diff --git a/app/src/pages/ODD/InstrumentsDashboard/__tests__/InstrumentsDashboard.test.tsx b/app/src/pages/ODD/InstrumentsDashboard/__tests__/InstrumentsDashboard.test.tsx index 7f2687e07b7..5e78f28b4c9 100644 --- a/app/src/pages/ODD/InstrumentsDashboard/__tests__/InstrumentsDashboard.test.tsx +++ b/app/src/pages/ODD/InstrumentsDashboard/__tests__/InstrumentsDashboard.test.tsx @@ -183,7 +183,7 @@ describe('InstrumentsDashboard', () => { }, } as any) render('/instruments') - screen.getByText('Left+Right Mounts') + screen.getByText('Left + Right Mounts') screen.getByText('extension Mount') }) }) diff --git a/app/src/pages/ODD/InstrumentsDashboard/index.tsx b/app/src/pages/ODD/InstrumentsDashboard/index.tsx index acc88714979..49268c0ec51 100644 --- a/app/src/pages/ODD/InstrumentsDashboard/index.tsx +++ b/app/src/pages/ODD/InstrumentsDashboard/index.tsx @@ -7,6 +7,7 @@ import { AttachedInstrumentMountItem } from '/app/organisms/ODD/InstrumentMountI import { GripperWizardFlows } from '/app/organisms/GripperWizardFlows' import { getShowPipetteCalibrationWarning } from '/app/transformations/instruments' import { PipetteRecalibrationODDWarning } from '/app/organisms/ODD/PipetteRecalibrationODDWarning' +import type { ComponentProps } from 'react' import type { GripperData, PipetteData } from '@opentrons/api-client' const FETCH_PIPETTE_CAL_POLL = 10000 @@ -16,8 +17,8 @@ export const InstrumentsDashboard = (): JSX.Element => { refetchInterval: FETCH_PIPETTE_CAL_POLL, }) const [wizardProps, setWizardProps] = useState< - | React.ComponentProps - | React.ComponentProps + | ComponentProps + | ComponentProps | null >(null) diff --git a/app/src/pages/ODD/NameRobot/index.tsx b/app/src/pages/ODD/NameRobot/index.tsx index b0162041963..1417adbbf37 100644 --- a/app/src/pages/ODD/NameRobot/index.tsx +++ b/app/src/pages/ODD/NameRobot/index.tsx @@ -23,6 +23,7 @@ import { TYPOGRAPHY, } from '@opentrons/components' import { useUpdateRobotNameMutation } from '@opentrons/react-api-client' +import { FLEX_ROBOT_TYPE } from '@opentrons/shared-data' import { removeRobot, @@ -166,6 +167,7 @@ export function NameRobot(): JSX.Element { properties: { previousRobotName: previousName, newRobotName: newRobotName, + robotType: FLEX_ROBOT_TYPE, }, }) handleSubmit(onSubmit)() diff --git a/app/src/pages/ODD/ProtocolDashboard/PinnedProtocol.tsx b/app/src/pages/ODD/ProtocolDashboard/PinnedProtocol.tsx index 226000381ad..27db6210d68 100644 --- a/app/src/pages/ODD/ProtocolDashboard/PinnedProtocol.tsx +++ b/app/src/pages/ODD/ProtocolDashboard/PinnedProtocol.tsx @@ -1,7 +1,6 @@ -import * as React from 'react' +import { useEffect } from 'react' import { useTranslation } from 'react-i18next' import { useNavigate } from 'react-router-dom' -import { formatDistance } from 'date-fns' import styled, { css } from 'styled-components' import { @@ -22,7 +21,9 @@ import { import { LongPressModal } from './LongPressModal' import { formatTimeWithUtcLabel } from '/app/resources/runs' +import { useUpdatedLastRunTime } from './hooks' +import type { Dispatch, SetStateAction } from 'react' import type { UseLongPressResult } from '@opentrons/components' import type { ProtocolResource } from '@opentrons/shared-data' @@ -63,7 +64,7 @@ const cardStyleBySize: { interface PinnedProtocolProps { protocol: ProtocolResource - longPress: React.Dispatch> + longPress: Dispatch> setShowDeleteConfirmationModal: (showDeleteConfirmationModal: boolean) => void setTargetProtocolId: (targetProtocolId: string) => void cardSize?: CardSizeType @@ -87,6 +88,8 @@ export function PinnedProtocol(props: PinnedProtocolProps): JSX.Element { const protocolName = protocol.metadata.protocolName ?? protocol.files[0].name const { t } = useTranslation('protocol_info') + const updatedLastRun = useUpdatedLastRunTime(lastRun) + // ToDo (kk:06/18/2024) this will be removed later const handleProtocolClick = ( longpress: UseLongPressResult, @@ -96,7 +99,7 @@ export function PinnedProtocol(props: PinnedProtocolProps): JSX.Element { navigate(`/protocols/${protocolId}`) } } - React.useEffect(() => { + useEffect(() => { if (longpress.isLongPressed) { longPress(true) } @@ -155,14 +158,12 @@ export function PinnedProtocol(props: PinnedProtocolProps): JSX.Element { color={COLORS.grey60} > - {lastRun !== undefined - ? `${formatDistance(new Date(lastRun), new Date(), { - addSuffix: true, - }).replace('about ', '')}` - : t('no_history')} + {t('last_run_time', { time: updatedLastRun })} - {formatTimeWithUtcLabel(protocol.createdAt)} + {t('date_added_date', { + date: formatTimeWithUtcLabel(protocol.createdAt), + })} {longpress.isLongPressed && ( diff --git a/app/src/pages/ODD/ProtocolDashboard/PinnedProtocolCarousel.tsx b/app/src/pages/ODD/ProtocolDashboard/PinnedProtocolCarousel.tsx index 59e9fc5c117..267b77a5cea 100644 --- a/app/src/pages/ODD/ProtocolDashboard/PinnedProtocolCarousel.tsx +++ b/app/src/pages/ODD/ProtocolDashboard/PinnedProtocolCarousel.tsx @@ -1,4 +1,4 @@ -import type * as React from 'react' +import styled from 'styled-components' import { ALIGN_FLEX_START, DIRECTION_ROW, @@ -9,13 +9,13 @@ import { import { useNotifyAllRunsQuery } from '/app/resources/runs' import { PinnedProtocol } from './PinnedProtocol' +import type { Dispatch, SetStateAction } from 'react' import type { ProtocolResource } from '@opentrons/shared-data' import type { CardSizeType } from './PinnedProtocol' -import styled from 'styled-components' interface PinnedProtocolCarouselProps { pinnedProtocols: ProtocolResource[] - longPress: React.Dispatch> + longPress: Dispatch> setShowDeleteConfirmationModal: (showDeleteConfirmationModal: boolean) => void setTargetProtocolId: (targetProtocolId: string) => void isRequiredCSV?: boolean diff --git a/app/src/pages/ODD/ProtocolDashboard/ProtocolCard.tsx b/app/src/pages/ODD/ProtocolDashboard/ProtocolCard.tsx index 61a0bcb9061..6786205d889 100644 --- a/app/src/pages/ODD/ProtocolDashboard/ProtocolCard.tsx +++ b/app/src/pages/ODD/ProtocolDashboard/ProtocolCard.tsx @@ -35,6 +35,7 @@ import { LongPressModal } from './LongPressModal' import { formatTimeWithUtcLabel } from '/app/resources/runs' import { useUpdatedLastRunTime } from '/app/pages/ODD/ProtocolDashboard/hooks' +import type { Dispatch, SetStateAction } from 'react' import type { UseLongPressResult } from '@opentrons/components' import type { ProtocolResource } from '@opentrons/shared-data' import type { OddModalHeaderBaseProps } from '/app/molecules/OddModal/types' @@ -43,7 +44,7 @@ const REFETCH_INTERVAL = 5000 interface ProtocolCardProps { protocol: ProtocolResource - longPress: React.Dispatch> + longPress: Dispatch> setShowDeleteConfirmationModal: (showDeleteConfirmationModal: boolean) => void setTargetProtocolId: (targetProtocolId: string) => void lastRun?: string diff --git a/app/src/pages/ODD/ProtocolDashboard/__tests__/DeleteProtocolConfirmationModal.test.tsx b/app/src/pages/ODD/ProtocolDashboard/__tests__/DeleteProtocolConfirmationModal.test.tsx index 1c55405dbde..059e62f85e5 100644 --- a/app/src/pages/ODD/ProtocolDashboard/__tests__/DeleteProtocolConfirmationModal.test.tsx +++ b/app/src/pages/ODD/ProtocolDashboard/__tests__/DeleteProtocolConfirmationModal.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { vi, it, describe, expect, beforeEach, afterEach } from 'vitest' import { when } from 'vitest-when' import { act, fireEvent, screen } from '@testing-library/react' @@ -10,6 +9,8 @@ import { useHost, useProtocolQuery } from '@opentrons/react-api-client' import { i18n } from '/app/i18n' import { useToaster } from '/app/organisms/ToasterOven' import { DeleteProtocolConfirmationModal } from '../DeleteProtocolConfirmationModal' + +import type { ComponentProps } from 'react' import type { HostConfig } from '@opentrons/api-client' vi.mock('@opentrons/api-client') @@ -22,7 +23,7 @@ const mockMakeSnackbar = vi.fn() const MOCK_HOST_CONFIG = {} as HostConfig const render = ( - props: React.ComponentProps + props: ComponentProps ) => { return renderWithProviders(, { i18nInstance: i18n, @@ -30,7 +31,7 @@ const render = ( } describe('DeleteProtocolConfirmationModal', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { diff --git a/app/src/pages/ODD/ProtocolDashboard/__tests__/LongPressModal.test.tsx b/app/src/pages/ODD/ProtocolDashboard/__tests__/LongPressModal.test.tsx index 777e8d92a39..68ff6dcc9bf 100644 --- a/app/src/pages/ODD/ProtocolDashboard/__tests__/LongPressModal.test.tsx +++ b/app/src/pages/ODD/ProtocolDashboard/__tests__/LongPressModal.test.tsx @@ -26,7 +26,7 @@ const render = (longPress: UseLongPressResult) => { diff --git a/app/src/pages/ODD/ProtocolDashboard/__tests__/PinnedProtocol.test.tsx b/app/src/pages/ODD/ProtocolDashboard/__tests__/PinnedProtocol.test.tsx index f92904573d0..a98f0478d6d 100644 --- a/app/src/pages/ODD/ProtocolDashboard/__tests__/PinnedProtocol.test.tsx +++ b/app/src/pages/ODD/ProtocolDashboard/__tests__/PinnedProtocol.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { vi, it, describe, expect, beforeEach } from 'vitest' import { act, fireEvent, screen } from '@testing-library/react' import { MemoryRouter } from 'react-router-dom' @@ -10,9 +9,10 @@ import { i18n } from '/app/i18n' import { useFeatureFlag } from '/app/redux/config' import { PinnedProtocol } from '../PinnedProtocol' +import type { ComponentProps } from 'react' +import type { NavigateFunction } from 'react-router-dom' import type { Chip } from '@opentrons/components' import type { ProtocolResource } from '@opentrons/shared-data' -import type { NavigateFunction } from 'react-router-dom' const mockNavigate = vi.fn() @@ -50,7 +50,7 @@ const mockProtocol: ProtocolResource = { key: '26ed5a82-502f-4074-8981-57cdda1d066d', } -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders( @@ -62,7 +62,7 @@ const render = (props: React.ComponentProps) => { } describe('Pinned Protocol', () => { - let props: React.ComponentProps + let props: ComponentProps vi.useFakeTimers() beforeEach(() => { diff --git a/app/src/pages/ODD/ProtocolDashboard/__tests__/ProtocolCard.test.tsx b/app/src/pages/ODD/ProtocolDashboard/__tests__/ProtocolCard.test.tsx index c26b429e29e..5f1587242d8 100644 --- a/app/src/pages/ODD/ProtocolDashboard/__tests__/ProtocolCard.test.tsx +++ b/app/src/pages/ODD/ProtocolDashboard/__tests__/ProtocolCard.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { vi, it, describe, expect, beforeEach } from 'vitest' import { act, fireEvent, screen } from '@testing-library/react' import { MemoryRouter } from 'react-router-dom' @@ -14,6 +13,7 @@ import { i18n } from '/app/i18n' import { useFeatureFlag } from '/app/redux/config' import { ProtocolCard } from '../ProtocolCard' +import type { ComponentProps } from 'react' import type { NavigateFunction } from 'react-router-dom' import type { UseQueryResult } from 'react-query' import type { @@ -77,7 +77,7 @@ const mockProtocolWithCSV: ProtocolResource = { key: '26ed5a82-502f-4074-8981-57cdda1d066d', } -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders( @@ -89,7 +89,7 @@ const render = (props: React.ComponentProps) => { } describe('ProtocolCard', () => { - let props: React.ComponentProps + let props: ComponentProps vi.useFakeTimers() beforeEach(() => { diff --git a/app/src/pages/ODD/ProtocolDetails/__tests__/Deck.test.tsx b/app/src/pages/ODD/ProtocolDetails/__tests__/Deck.test.tsx index a99e2c3f82e..902cd1d3b8c 100644 --- a/app/src/pages/ODD/ProtocolDetails/__tests__/Deck.test.tsx +++ b/app/src/pages/ODD/ProtocolDetails/__tests__/Deck.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { vi, it, describe, expect, beforeEach, afterEach } from 'vitest' import { screen } from '@testing-library/react' import { when } from 'vitest-when' @@ -12,6 +11,7 @@ import { import { i18n } from '/app/i18n' import { Deck } from '../Deck' +import type { ComponentProps } from 'react' import type { UseQueryResult } from 'react-query' import type { CompletedProtocolAnalysis } from '@opentrons/shared-data' import type { Protocol } from '@opentrons/api-client' @@ -141,14 +141,14 @@ const MOCK_PROTOCOL_ANALYSIS = { ], } -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, }) } describe('Deck', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { protocolId: MOCK_PROTOCOL_ID, diff --git a/app/src/pages/ODD/ProtocolDetails/__tests__/EmptySection.test.tsx b/app/src/pages/ODD/ProtocolDetails/__tests__/EmptySection.test.tsx index 32b388ef1f3..9c1832fc08f 100644 --- a/app/src/pages/ODD/ProtocolDetails/__tests__/EmptySection.test.tsx +++ b/app/src/pages/ODD/ProtocolDetails/__tests__/EmptySection.test.tsx @@ -1,18 +1,19 @@ -import type * as React from 'react' import { it, describe } from 'vitest' import { screen } from '@testing-library/react' import { renderWithProviders } from '/app/__testing-utils__' import { i18n } from '/app/i18n' import { EmptySection } from '../EmptySection' -const render = (props: React.ComponentProps) => { +import type { ComponentProps } from 'react' + +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, })[0] } describe('EmptySection', () => { - let props: React.ComponentProps + let props: ComponentProps it('should render text for labware', () => { props = { diff --git a/app/src/pages/ODD/ProtocolDetails/__tests__/Hardware.test.tsx b/app/src/pages/ODD/ProtocolDetails/__tests__/Hardware.test.tsx index a8f78c8a121..7d03ce6ba3d 100644 --- a/app/src/pages/ODD/ProtocolDetails/__tests__/Hardware.test.tsx +++ b/app/src/pages/ODD/ProtocolDetails/__tests__/Hardware.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { vi, it, describe, beforeEach, afterEach } from 'vitest' import { screen } from '@testing-library/react' import { when } from 'vitest-when' @@ -12,19 +11,21 @@ import { i18n } from '/app/i18n' import { useRequiredProtocolHardware } from '/app/resources/protocols' import { Hardware } from '../Hardware' +import type { ComponentProps } from 'react' + vi.mock('/app/resources/protocols') vi.mock('/app/redux/config') const MOCK_PROTOCOL_ID = 'mock_protocol_id' -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, }) } describe('Hardware', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { protocolId: MOCK_PROTOCOL_ID, diff --git a/app/src/pages/ODD/ProtocolDetails/__tests__/Labware.test.tsx b/app/src/pages/ODD/ProtocolDetails/__tests__/Labware.test.tsx index 0c1aab61196..14568a6ceb3 100644 --- a/app/src/pages/ODD/ProtocolDetails/__tests__/Labware.test.tsx +++ b/app/src/pages/ODD/ProtocolDetails/__tests__/Labware.test.tsx @@ -1,10 +1,6 @@ -import type * as React from 'react' import { vi, it, describe, beforeEach, afterEach } from 'vitest' +import { screen } from '@testing-library/react' import { when } from 'vitest-when' -import { renderWithProviders } from '/app/__testing-utils__' -import { i18n } from '/app/i18n' -import { useRequiredProtocolLabware } from '/app/resources/protocols' -import { Labware } from '../Labware' import { fixtureTiprack10ul, @@ -12,21 +8,26 @@ import { fixture96Plate, } from '@opentrons/shared-data' +import { renderWithProviders } from '/app/__testing-utils__' +import { i18n } from '/app/i18n' +import { useRequiredProtocolLabware } from '/app/resources/protocols' +import { Labware } from '../Labware' + +import type { ComponentProps } from 'react' import type { LabwareDefinition2 } from '@opentrons/shared-data' -import { screen } from '@testing-library/react' vi.mock('/app/resources/protocols') const MOCK_PROTOCOL_ID = 'mock_protocol_id' -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, }) } describe('Labware', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { protocolId: MOCK_PROTOCOL_ID, diff --git a/app/src/pages/ODD/ProtocolDetails/__tests__/Liquids.test.tsx b/app/src/pages/ODD/ProtocolDetails/__tests__/Liquids.test.tsx index be019dd6cf1..23233c6c50d 100644 --- a/app/src/pages/ODD/ProtocolDetails/__tests__/Liquids.test.tsx +++ b/app/src/pages/ODD/ProtocolDetails/__tests__/Liquids.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { vi, it, describe, beforeEach } from 'vitest' import { when } from 'vitest-when' import { screen } from '@testing-library/react' @@ -16,6 +15,7 @@ import { renderWithProviders } from '/app/__testing-utils__' import { i18n } from '/app/i18n' import { Liquids } from '../Liquids' +import type { ComponentProps } from 'react' import type { UseQueryResult } from 'react-query' import type { Protocol } from '@opentrons/api-client' import type * as SharedData from '@opentrons/shared-data' @@ -180,14 +180,14 @@ const MOCK_LABWARE_INFO_BY_LIQUID_ID = { ], } -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, }) } describe('Liquids', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { protocolId: MOCK_PROTOCOL_ID, diff --git a/app/src/pages/ODD/ProtocolDetails/__tests__/Parameters.test.tsx b/app/src/pages/ODD/ProtocolDetails/__tests__/Parameters.test.tsx index f41115cf638..b2d60c57c92 100644 --- a/app/src/pages/ODD/ProtocolDetails/__tests__/Parameters.test.tsx +++ b/app/src/pages/ODD/ProtocolDetails/__tests__/Parameters.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { when } from 'vitest-when' import { it, describe, beforeEach, vi } from 'vitest' import { screen } from '@testing-library/react' @@ -9,17 +8,19 @@ import { useRunTimeParameters } from '/app/resources/protocols' import { Parameters } from '../Parameters' import { mockRunTimeParameterData } from '/app/organisms/ODD/ProtocolSetup/__fixtures__' +import type { ComponentProps } from 'react' + vi.mock('/app/organisms/ToasterOven') vi.mock('/app/resources/protocols') -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, }) } const MOCK_MAKE_SNACK_BAR = vi.fn() describe('Parameters', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { diff --git a/app/src/pages/ODD/ProtocolDetails/__tests__/ProtocolDetails.test.tsx b/app/src/pages/ODD/ProtocolDetails/__tests__/ProtocolDetails.test.tsx index 675d8b038a8..23fc58bfc57 100644 --- a/app/src/pages/ODD/ProtocolDetails/__tests__/ProtocolDetails.test.tsx +++ b/app/src/pages/ODD/ProtocolDetails/__tests__/ProtocolDetails.test.tsx @@ -13,7 +13,7 @@ import { } from '@opentrons/react-api-client' import { i18n } from '/app/i18n' import { useHardwareStatusText } from '/app/organisms/ODD/RobotDashboard/hooks' -import { useOffsetCandidatesForAnalysis } from '/app/organisms/ApplyHistoricOffsets/hooks/useOffsetCandidatesForAnalysis' +import { useOffsetCandidatesForAnalysis } from '/app/organisms/LegacyApplyHistoricOffsets/hooks/useOffsetCandidatesForAnalysis' import { useRunTimeParameters } from '/app/resources/protocols' import { ProtocolSetupParameters } from '/app/organisms/ODD/ProtocolSetup/ProtocolSetupParameters' import { mockRunTimeParameterData } from '/app/organisms/ODD/ProtocolSetup/__fixtures__' @@ -35,7 +35,7 @@ vi.mock('@opentrons/api-client') vi.mock('@opentrons/react-api-client') vi.mock('/app/organisms/ODD/RobotDashboard/hooks') vi.mock( - '/app/organisms/ApplyHistoricOffsets/hooks/useOffsetCandidatesForAnalysis' + '/app/organisms/LegacyApplyHistoricOffsets/hooks/useOffsetCandidatesForAnalysis' ) vi.mock('/app/resources/protocols') vi.mock('/app/transformations/commands') diff --git a/app/src/pages/ODD/ProtocolDetails/index.tsx b/app/src/pages/ODD/ProtocolDetails/index.tsx index 133210ff218..7cf14e07803 100644 --- a/app/src/pages/ODD/ProtocolDetails/index.tsx +++ b/app/src/pages/ODD/ProtocolDetails/index.tsx @@ -46,7 +46,7 @@ import { getPinnedProtocolIds, updateConfigValue, } from '/app/redux/config' -import { useOffsetCandidatesForAnalysis } from '/app/organisms/ApplyHistoricOffsets/hooks/useOffsetCandidatesForAnalysis' +import { useOffsetCandidatesForAnalysis } from '/app/organisms/LegacyApplyHistoricOffsets/hooks/useOffsetCandidatesForAnalysis' import { useRunTimeParameters } from '/app/resources/protocols' import { useMissingProtocolHardware } from '/app/transformations/commands' import { ProtocolSetupParameters } from '/app/organisms/ODD/ProtocolSetup/ProtocolSetupParameters' @@ -164,19 +164,19 @@ const ProtocolHeader = ({ } const protocolSectionTabOptions = [ - 'Summary', - 'Parameters', - 'Hardware', - 'Labware', - 'Liquids', - 'Deck', + 'summary', + 'parameters', + 'hardware', + 'labware', + 'liquids', + 'deck', ] as const const protocolSectionTabOptionsWithoutParameters = [ - 'Summary', - 'Hardware', - 'Labware', - 'Liquids', - 'Deck', + 'summary', + 'hardware', + 'labware', + 'liquids', + 'deck', ] as const type TabOption = @@ -192,11 +192,12 @@ const ProtocolSectionTabs = ({ currentOption, setCurrentOption, }: ProtocolSectionTabsProps): JSX.Element => { + const { t, i18n } = useTranslation('protocol_details') return ( ({ - text: option, + text: i18n.format(t(option), 'capitalize'), onClick: () => { setCurrentOption(option) }, @@ -266,7 +267,7 @@ const ProtocolSectionContent = ({ let protocolSection: JSX.Element | null = null switch (currentOption) { - case 'Summary': + case 'summary': protocolSection = ( ) break - case 'Parameters': + case 'parameters': protocolSection = break - case 'Hardware': + case 'hardware': protocolSection = break - case 'Labware': + case 'labware': protocolSection = break - case 'Liquids': + case 'liquids': protocolSection = break - case 'Deck': + case 'deck': protocolSection = break } return ( {protocolSection} diff --git a/app/src/pages/ODD/ProtocolSetup/ConfirmSetupStepsCompleteModal.tsx b/app/src/pages/ODD/ProtocolSetup/ConfirmSetupStepsCompleteModal.tsx index eafffd9a8f0..82c87c9f44f 100644 --- a/app/src/pages/ODD/ProtocolSetup/ConfirmSetupStepsCompleteModal.tsx +++ b/app/src/pages/ODD/ProtocolSetup/ConfirmSetupStepsCompleteModal.tsx @@ -36,7 +36,11 @@ export function ConfirmSetupStepsCompleteModal({ return ( - + {t('you_havent_confirmed', { missingSteps: new Intl.ListFormat('en', { diff --git a/app/src/pages/ODD/ProtocolSetup/__tests__/ConfirmAttachedModal.test.tsx b/app/src/pages/ODD/ProtocolSetup/__tests__/ConfirmAttachedModal.test.tsx index f06e36df20d..5a9b2ea98bc 100644 --- a/app/src/pages/ODD/ProtocolSetup/__tests__/ConfirmAttachedModal.test.tsx +++ b/app/src/pages/ODD/ProtocolSetup/__tests__/ConfirmAttachedModal.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { vi, it, describe, expect, beforeEach } from 'vitest' import { fireEvent, screen } from '@testing-library/react' @@ -7,17 +6,19 @@ import { renderWithProviders } from '/app/__testing-utils__' import { i18n } from '/app/i18n' import { ConfirmAttachedModal } from '../ConfirmAttachedModal' +import type { ComponentProps } from 'react' + const mockOnCloseClick = vi.fn() const mockOnConfirmClick = vi.fn() -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, }) } describe('ConfirmAttachedModal', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { diff --git a/app/src/pages/ODD/ProtocolSetup/__tests__/ProtocolSetup.test.tsx b/app/src/pages/ODD/ProtocolSetup/__tests__/ProtocolSetup.test.tsx index 438f856b41d..5863d70ba93 100644 --- a/app/src/pages/ODD/ProtocolSetup/__tests__/ProtocolSetup.test.tsx +++ b/app/src/pages/ODD/ProtocolSetup/__tests__/ProtocolSetup.test.tsx @@ -41,7 +41,7 @@ import { getUnmatchedModulesForProtocol, getIncompleteInstrumentCount, } from '/app/organisms/ODD/ProtocolSetup' -import { useLaunchLPC } from '/app/organisms/LabwarePositionCheck/useLaunchLPC' +import { useLaunchLegacyLPC } from '/app/organisms/LegacyLabwarePositionCheck/useLaunchLegacyLPC' import { ConfirmCancelRunModal } from '/app/organisms/ODD/RunningProtocol' import { mockProtocolModuleInfo } from '/app/organisms/ODD/ProtocolSetup/ProtocolSetupInstruments/__fixtures__' import { @@ -89,7 +89,7 @@ vi.mock('react-router-dom', async importOriginal => { }) vi.mock('@opentrons/react-api-client') -vi.mock('/app/organisms/LabwarePositionCheck/useLaunchLPC') +vi.mock('/app/organisms/LegacyLabwarePositionCheck/useLaunchLegacyLPC') vi.mock('/app/organisms/ODD/ProtocolSetup', async importOriginal => { const ACTUALS = ['ProtocolSetupStep'] const actual = await importOriginal() @@ -300,11 +300,11 @@ describe('ProtocolSetup', () => { when(vi.mocked(useAllPipetteOffsetCalibrationsQuery)) .calledWith() .thenReturn({ data: { data: [] } } as any) - when(vi.mocked(useLaunchLPC)) + when(vi.mocked(useLaunchLegacyLPC)) .calledWith(RUN_ID, FLEX_ROBOT_TYPE, PROTOCOL_NAME) .thenReturn({ - launchLPC: mockLaunchLPC, - LPCWizard:
        mock LPC Wizard
        , + launchLegacyLPC: mockLaunchLPC, + LegacyLPCWizard:
        mock LPC Wizard
        , }) vi.mocked(useIsHeaterShakerInProtocol).mockReturnValue(false) vi.mocked(useDoorQuery).mockReturnValue({ data: mockDoorStatus } as any) @@ -576,7 +576,6 @@ describe('ProtocolSetup', () => { render(`/runs/${RUN_ID}/setup/`) fireEvent.click(screen.getByRole('button', { name: 'play' })) - expect(mockTrackProtocolRunEvent).toBeCalledTimes(1) expect(mockTrackProtocolRunEvent).toHaveBeenCalledWith({ name: ANALYTICS_PROTOCOL_RUN_ACTION.START, properties: {}, diff --git a/app/src/pages/ODD/ProtocolSetup/index.tsx b/app/src/pages/ODD/ProtocolSetup/index.tsx index 03a03626a55..1df659c633b 100644 --- a/app/src/pages/ODD/ProtocolSetup/index.tsx +++ b/app/src/pages/ODD/ProtocolSetup/index.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useEffect, useState } from 'react' import last from 'lodash/last' import { useSelector } from 'react-redux' import { useTranslation } from 'react-i18next' @@ -56,7 +56,7 @@ import { getIncompleteInstrumentCount, ViewOnlyParameters, } from '/app/organisms/ODD/ProtocolSetup' -import { useLaunchLPC } from '/app/organisms/LabwarePositionCheck/useLaunchLPC' +import { useLaunchLegacyLPC } from '/app/organisms/LegacyLabwarePositionCheck/useLaunchLegacyLPC' import { ConfirmCancelRunModal } from '/app/organisms/ODD/RunningProtocol' import { useRunControls } from '/app/organisms/RunTimeControl/hooks' import { useToaster } from '/app/organisms/ToasterOven' @@ -67,7 +67,7 @@ import { ANALYTICS_PROTOCOL_RUN_ACTION, useTrackEvent, } from '/app/redux/analytics' -import { getIsHeaterShakerAttached } from '/app/redux/config' +import { getIsHeaterShakerAttached, useFeatureFlag } from '/app/redux/config' import { ConfirmAttachedModal } from './ConfirmAttachedModal' import { ConfirmSetupStepsCompleteModal } from './ConfirmSetupStepsCompleteModal' import { getLatestCurrentOffsets } from '/app/transformations/runs' @@ -82,7 +82,14 @@ import { useProtocolAnalysisErrors, } from '/app/resources/runs' import { useScrollPosition } from '/app/local-resources/dom-utils' +import { + getLabwareSetupItemGroups, + getProtocolUsesGripper, + useRequiredProtocolHardwareFromAnalysis, + useMissingProtocolHardwareFromAnalysis, +} from '/app/transformations/commands' +import type { Dispatch, SetStateAction } from 'react' import type { Run } from '@opentrons/api-client' import type { CutoutFixtureId, CutoutId } from '@opentrons/shared-data' import type { OnDeviceRouteParams } from '/app/App/types' @@ -92,19 +99,13 @@ import type { ProtocolHardware, ProtocolFixture, } from '/app/transformations/commands' -import { - getLabwareSetupItemGroups, - getProtocolUsesGripper, - useRequiredProtocolHardwareFromAnalysis, - useMissingProtocolHardwareFromAnalysis, -} from '/app/transformations/commands' const FETCH_DURATION_MS = 5000 const ANALYSIS_POLL_MS = 5000 interface PrepareToRunProps { runId: string - setSetupScreen: React.Dispatch> + setSetupScreen: Dispatch> confirmAttachment: () => void confirmStepsComplete: () => void play: () => void @@ -147,7 +148,7 @@ function PrepareToRun({ const [ isPollingForCompletedAnalysis, setIsPollingForCompletedAnalysis, - ] = React.useState(mostRecentAnalysisSummary?.status !== 'completed') + ] = useState(mostRecentAnalysisSummary?.status !== 'completed') const { data: mostRecentAnalysis = null, @@ -165,7 +166,7 @@ function PrepareToRun({ navigate('/protocols') } - React.useEffect(() => { + useEffect(() => { if (mostRecentAnalysis?.status === 'completed') { setIsPollingForCompletedAnalysis(false) } else { @@ -229,10 +230,9 @@ function PrepareToRun({ parameter.type === 'csv_file' || parameter.value !== parameter.default ) - const [ - showConfirmCancelModal, - setShowConfirmCancelModal, - ] = React.useState(false) + const [showConfirmCancelModal, setShowConfirmCancelModal] = useState( + false + ) const deckConfigCompatibility = useDeckConfigurationCompatibility( robotType, @@ -658,6 +658,7 @@ export function ProtocolSetup(): JSX.Element { const { runId } = useParams< keyof OnDeviceRouteParams >() as OnDeviceRouteParams + const isNewLpc = useFeatureFlag('lpcRedesign') const { data: runRecord } = useNotifyRunQuery(runId, { staleTime: Infinity }) const { analysisErrors } = useProtocolAnalysisErrors(runId) const { t } = useTranslation(['protocol_setup']) @@ -670,7 +671,7 @@ export function ProtocolSetup(): JSX.Element { const [ showAnalysisFailedModal, setShowAnalysisFailedModal, - ] = React.useState(true) + ] = useState(true) const robotType = useRobotType(robotName) const attachedModules = useAttachedModules({ @@ -684,7 +685,7 @@ export function ProtocolSetup(): JSX.Element { const [ isPollingForCompletedAnalysis, setIsPollingForCompletedAnalysis, - ] = React.useState(mostRecentAnalysisSummary?.status !== 'completed') + ] = useState(mostRecentAnalysisSummary?.status !== 'completed') const { data: mostRecentAnalysis = null, @@ -699,7 +700,7 @@ export function ProtocolSetup(): JSX.Element { const areLiquidsInProtocol = (mostRecentAnalysis?.liquids?.length ?? 0) > 0 - React.useEffect(() => { + useEffect(() => { if (mostRecentAnalysis?.status === 'completed') { setIsPollingForCompletedAnalysis(false) } else { @@ -735,12 +736,24 @@ export function ProtocolSetup(): JSX.Element { protocolRecord?.data.files[0].name ?? '' - const { launchLPC, LPCWizard } = useLaunchLPC(runId, robotType, protocolName) + const { launchLegacyLPC, LegacyLPCWizard } = useLaunchLegacyLPC( + runId, + robotType, + protocolName + ) + + const { trackProtocolRunEvent } = useTrackProtocolRunEvent(runId, robotName) + const robotAnalyticsData = useRobotAnalyticsData(robotName) + const handleProceedToRunClick = (): void => { trackEvent({ name: ANALYTICS_PROTOCOL_PROCEED_TO_RUN, properties: { robotSerialNumber }, }) + trackProtocolRunEvent({ + name: ANALYTICS_PROTOCOL_RUN_ACTION.START, + properties: robotAnalyticsData ?? {}, + }) play() } const configBypassHeaterShakerAttachmentConfirmation = useSelector( @@ -754,14 +767,14 @@ export function ProtocolSetup(): JSX.Element { handleProceedToRunClick, !configBypassHeaterShakerAttachmentConfirmation ) - const [cutoutId, setCutoutId] = React.useState(null) - const [providedFixtureOptions, setProvidedFixtureOptions] = React.useState< + const [cutoutId, setCutoutId] = useState(null) + const [providedFixtureOptions, setProvidedFixtureOptions] = useState< CutoutFixtureId[] >([]) // TODO(jh 10-31-24): Refactor the below to utilize useMissingStepsModal. - const [labwareConfirmed, setLabwareConfirmed] = React.useState(false) - const [liquidsConfirmed, setLiquidsConfirmed] = React.useState(false) - const [offsetsConfirmed, setOffsetsConfirmed] = React.useState(false) + const [labwareConfirmed, setLabwareConfirmed] = useState(false) + const [liquidsConfirmed, setLiquidsConfirmed] = useState(false) + const [offsetsConfirmed, setOffsetsConfirmed] = useState(false) const missingSteps = [ !offsetsConfirmed ? t('applied_labware_offsets') : null, !labwareConfirmed ? t('labware_placement') : null, @@ -779,9 +792,7 @@ export function ProtocolSetup(): JSX.Element { const isHeaterShakerInProtocol = useIsHeaterShakerInProtocol() // orchestrate setup subpages/components - const [setupScreen, setSetupScreen] = React.useState( - 'prepare to run' - ) + const [setupScreen, setSetupScreen] = useState('prepare to run') const setupComponentByScreen = { 'prepare to run': ( ), labware: ( diff --git a/app/src/pages/ODD/QuickTransferDashboard/PinnedTransfer.tsx b/app/src/pages/ODD/QuickTransferDashboard/PinnedTransfer.tsx index dcb282be9d7..ad01dedc2d9 100644 --- a/app/src/pages/ODD/QuickTransferDashboard/PinnedTransfer.tsx +++ b/app/src/pages/ODD/QuickTransferDashboard/PinnedTransfer.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useEffect } from 'react' import { useNavigate } from 'react-router-dom' import styled, { css } from 'styled-components' @@ -20,6 +20,7 @@ import { import { LongPressModal } from './LongPressModal' import { formatTimeWithUtcLabel } from '/app/resources/runs' +import type { Dispatch, SetStateAction } from 'react' import type { UseLongPressResult } from '@opentrons/components' import type { ProtocolResource } from '@opentrons/shared-data' @@ -59,7 +60,7 @@ const cardStyleBySize: { export function PinnedTransfer(props: { transfer: ProtocolResource - longPress: React.Dispatch> + longPress: Dispatch> setShowDeleteConfirmationModal: (showDeleteConfirmationModal: boolean) => void setTargetTransferId: (targetProtocolId: string) => void cardSize?: CardSizeType @@ -83,7 +84,7 @@ export function PinnedTransfer(props: { navigate(`/quick-transfer/${transferId}`) } } - React.useEffect(() => { + useEffect(() => { if (longpress.isLongPressed) { longPress(true) } diff --git a/app/src/pages/ODD/QuickTransferDashboard/PinnedTransferCarousel.tsx b/app/src/pages/ODD/QuickTransferDashboard/PinnedTransferCarousel.tsx index 8b6554c3aa1..fc5b127b437 100644 --- a/app/src/pages/ODD/QuickTransferDashboard/PinnedTransferCarousel.tsx +++ b/app/src/pages/ODD/QuickTransferDashboard/PinnedTransferCarousel.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import styled from 'styled-components' import { ALIGN_FLEX_START, @@ -11,12 +10,13 @@ import { import { PinnedTransfer } from './PinnedTransfer' +import type { Dispatch, SetStateAction } from 'react' import type { ProtocolResource } from '@opentrons/shared-data' import type { CardSizeType } from './PinnedTransfer' export function PinnedTransferCarousel(props: { pinnedTransfers: ProtocolResource[] - longPress: React.Dispatch> + longPress: Dispatch> setShowDeleteConfirmationModal: (showDeleteConfirmationModal: boolean) => void setTargetTransferId: (targetTransferId: string) => void }): JSX.Element { diff --git a/app/src/pages/ODD/QuickTransferDashboard/QuickTransferCard.tsx b/app/src/pages/ODD/QuickTransferDashboard/QuickTransferCard.tsx index a0f99a4367e..fe4a939d543 100644 --- a/app/src/pages/ODD/QuickTransferDashboard/QuickTransferCard.tsx +++ b/app/src/pages/ODD/QuickTransferDashboard/QuickTransferCard.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useEffect, useState } from 'react' import { useNavigate } from 'react-router-dom' import { Trans, useTranslation } from 'react-i18next' import { useQueryClient } from 'react-query' @@ -36,6 +36,7 @@ import { OddModal } from '/app/molecules/OddModal' import { LongPressModal } from './LongPressModal' import { formatTimeWithUtcLabel } from '/app/resources/runs' +import type { Dispatch, SetStateAction } from 'react' import type { UseLongPressResult } from '@opentrons/components' import type { ProtocolResource } from '@opentrons/shared-data' import type { OddModalHeaderBaseProps } from '/app/molecules/OddModal/types' @@ -44,7 +45,7 @@ const REFETCH_INTERVAL = 5000 export function QuickTransferCard(props: { quickTransfer: ProtocolResource - longPress: React.Dispatch> + longPress: Dispatch> setShowDeleteConfirmationModal: (showDeleteConfirmationModal: boolean) => void setTargetTransferId: (targetTransferId: string) => void }): JSX.Element { @@ -55,11 +56,11 @@ export function QuickTransferCard(props: { setTargetTransferId, } = props const navigate = useNavigate() - const [showIcon, setShowIcon] = React.useState(false) + const [showIcon, setShowIcon] = useState(false) const [ showFailedAnalysisModal, setShowFailedAnalysisModal, - ] = React.useState(false) + ] = useState(false) const { t, i18n } = useTranslation(['quick_transfer', 'branded']) const transferName = quickTransfer.metadata.protocolName ?? quickTransfer.files[0].name @@ -113,7 +114,7 @@ export function QuickTransferCard(props: { } } - React.useEffect(() => { + useEffect(() => { if (longpress.isLongPressed) { longPress(true) setTargetTransferId(quickTransfer.id) diff --git a/app/src/pages/ODD/QuickTransferDashboard/__tests__/DeleteTransferConfirmationModal.test.tsx b/app/src/pages/ODD/QuickTransferDashboard/__tests__/DeleteTransferConfirmationModal.test.tsx index 1b42a6a5e3e..e7cb5ad92e6 100644 --- a/app/src/pages/ODD/QuickTransferDashboard/__tests__/DeleteTransferConfirmationModal.test.tsx +++ b/app/src/pages/ODD/QuickTransferDashboard/__tests__/DeleteTransferConfirmationModal.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { vi, it, describe, expect, beforeEach, afterEach } from 'vitest' import { when } from 'vitest-when' import { act, fireEvent, screen } from '@testing-library/react' @@ -11,6 +10,7 @@ import { i18n } from '/app/i18n' import { useToaster } from '/app/organisms/ToasterOven' import { DeleteTransferConfirmationModal } from '../DeleteTransferConfirmationModal' +import type { ComponentProps } from 'react' import type { NavigateFunction } from 'react-router-dom' import type { HostConfig } from '@opentrons/api-client' @@ -33,7 +33,7 @@ const mockMakeSnackbar = vi.fn() const MOCK_HOST_CONFIG = {} as HostConfig const render = ( - props: React.ComponentProps + props: ComponentProps ) => { return renderWithProviders(, { i18nInstance: i18n, @@ -41,7 +41,7 @@ const render = ( } describe('DeleteTransferConfirmationModal', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { diff --git a/app/src/pages/ODD/QuickTransferDashboard/__tests__/LongPressModal.test.tsx b/app/src/pages/ODD/QuickTransferDashboard/__tests__/LongPressModal.test.tsx index 2c55020d32a..e7e743e19ae 100644 --- a/app/src/pages/ODD/QuickTransferDashboard/__tests__/LongPressModal.test.tsx +++ b/app/src/pages/ODD/QuickTransferDashboard/__tests__/LongPressModal.test.tsx @@ -26,7 +26,7 @@ const render = (longPress: UseLongPressResult) => { diff --git a/app/src/pages/ODD/QuickTransferDetails/__tests__/Deck.test.tsx b/app/src/pages/ODD/QuickTransferDetails/__tests__/Deck.test.tsx index e15296037d5..415a25fdc65 100644 --- a/app/src/pages/ODD/QuickTransferDetails/__tests__/Deck.test.tsx +++ b/app/src/pages/ODD/QuickTransferDetails/__tests__/Deck.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { vi, it, describe, expect, beforeEach, afterEach } from 'vitest' import { screen } from '@testing-library/react' import { when } from 'vitest-when' @@ -12,6 +11,7 @@ import { import { i18n } from '/app/i18n' import { Deck } from '../Deck' +import type { ComponentProps } from 'react' import type { UseQueryResult } from 'react-query' import type { CompletedProtocolAnalysis } from '@opentrons/shared-data' import type { Protocol } from '@opentrons/api-client' @@ -141,14 +141,14 @@ const MOCK_PROTOCOL_ANALYSIS = { ], } -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, }) } describe('Deck', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { transferId: MOCK_PROTOCOL_ID, diff --git a/app/src/pages/ODD/QuickTransferDetails/__tests__/Hardware.test.tsx b/app/src/pages/ODD/QuickTransferDetails/__tests__/Hardware.test.tsx index 2b4af1a5178..a5f707fdf85 100644 --- a/app/src/pages/ODD/QuickTransferDetails/__tests__/Hardware.test.tsx +++ b/app/src/pages/ODD/QuickTransferDetails/__tests__/Hardware.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { vi, it, describe, beforeEach, afterEach } from 'vitest' import { screen } from '@testing-library/react' import { when } from 'vitest-when' @@ -12,20 +11,22 @@ import { i18n } from '/app/i18n' import { useRequiredProtocolHardware } from '/app/resources/protocols' import { Hardware } from '../Hardware' +import type { ComponentProps } from 'react' + vi.mock('/app/transformations/commands') vi.mock('/app/resources/protocols') vi.mock('/app/redux/config') const MOCK_PROTOCOL_ID = 'mock_protocol_id' -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, }) } describe('Hardware', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { transferId: MOCK_PROTOCOL_ID, diff --git a/app/src/pages/ODD/QuickTransferDetails/__tests__/Labware.test.tsx b/app/src/pages/ODD/QuickTransferDetails/__tests__/Labware.test.tsx index 7dd49d4f279..09f09f73718 100644 --- a/app/src/pages/ODD/QuickTransferDetails/__tests__/Labware.test.tsx +++ b/app/src/pages/ODD/QuickTransferDetails/__tests__/Labware.test.tsx @@ -1,32 +1,31 @@ -import type * as React from 'react' import { vi, it, describe, beforeEach, afterEach } from 'vitest' import { when } from 'vitest-when' -import { renderWithProviders } from '/app/__testing-utils__' -import { i18n } from '/app/i18n' -import { useRequiredProtocolLabware } from '/app/resources/protocols' -import { Labware } from '../Labware' - +import { screen } from '@testing-library/react' import { fixtureTiprack10ul, fixtureTiprack300ul, fixture96Plate, } from '@opentrons/shared-data' +import { renderWithProviders } from '/app/__testing-utils__' +import { i18n } from '/app/i18n' +import { useRequiredProtocolLabware } from '/app/resources/protocols' +import { Labware } from '../Labware' +import type { ComponentProps } from 'react' import type { LabwareDefinition2 } from '@opentrons/shared-data' -import { screen } from '@testing-library/react' vi.mock('/app/resources/protocols') const MOCK_PROTOCOL_ID = 'mock_protocol_id' -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, }) } describe('Labware', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { transferId: MOCK_PROTOCOL_ID, diff --git a/app/src/pages/ODD/QuickTransferDetails/__tests__/QuickTransferDetails.test.tsx b/app/src/pages/ODD/QuickTransferDetails/__tests__/QuickTransferDetails.test.tsx index 33203e4dc4f..dac836a90ff 100644 --- a/app/src/pages/ODD/QuickTransferDetails/__tests__/QuickTransferDetails.test.tsx +++ b/app/src/pages/ODD/QuickTransferDetails/__tests__/QuickTransferDetails.test.tsx @@ -13,7 +13,7 @@ import { import { renderWithProviders } from '/app/__testing-utils__' import { i18n } from '/app/i18n' import { useHardwareStatusText } from '/app/organisms/ODD/RobotDashboard/hooks' -import { useOffsetCandidatesForAnalysis } from '/app/organisms/ApplyHistoricOffsets/hooks/useOffsetCandidatesForAnalysis' +import { useOffsetCandidatesForAnalysis } from '/app/organisms/LegacyApplyHistoricOffsets/hooks/useOffsetCandidatesForAnalysis' import { useMissingProtocolHardware } from '/app/transformations/commands' import { formatTimeWithUtcLabel } from '/app/resources/runs' import { @@ -36,7 +36,7 @@ vi.mock('@opentrons/react-api-client') vi.mock('/app/organisms/ODD/RobotDashboard/hooks') vi.mock('/app/redux-resources/analytics') vi.mock( - '/app/organisms/ApplyHistoricOffsets/hooks/useOffsetCandidatesForAnalysis' + '/app/organisms/LegacyApplyHistoricOffsets/hooks/useOffsetCandidatesForAnalysis' ) vi.mock('../../QuickTransferDashboard/DeleteTransferConfirmationModal') vi.mock('/app/transformations/commands') diff --git a/app/src/pages/ODD/QuickTransferDetails/index.tsx b/app/src/pages/ODD/QuickTransferDetails/index.tsx index 5989ec1f29a..64b410e084d 100644 --- a/app/src/pages/ODD/QuickTransferDetails/index.tsx +++ b/app/src/pages/ODD/QuickTransferDetails/index.tsx @@ -50,7 +50,7 @@ import { ANALYTICS_QUICK_TRANSFER_RUN_FROM_DETAILS, } from '/app/redux/analytics' import { useTrackEventWithRobotSerial } from '/app/redux-resources/analytics' -import { useOffsetCandidatesForAnalysis } from '/app/organisms/ApplyHistoricOffsets/hooks/useOffsetCandidatesForAnalysis' +import { useOffsetCandidatesForAnalysis } from '/app/organisms/LegacyApplyHistoricOffsets/hooks/useOffsetCandidatesForAnalysis' import { useMissingProtocolHardware } from '/app/transformations/commands' import { DeleteTransferConfirmationModal } from '../QuickTransferDashboard/DeleteTransferConfirmationModal' import { Deck } from './Deck' @@ -180,10 +180,10 @@ const QuickTransferHeader = ({ } const transferSectionTabOptions = [ - 'Summary', - 'Hardware', - 'Labware', - 'Deck', + 'summary', + 'hardware', + 'labware', + 'deck', ] as const type TabOption = typeof transferSectionTabOptions[number] @@ -197,13 +197,14 @@ const TransferSectionTabs = ({ currentOption, setCurrentOption, }: TransferSectionTabsProps): JSX.Element => { + const { t, i18n } = useTranslation('protocol_details') const options = transferSectionTabOptions return ( ({ - text: option, + text: i18n.format(t(option), 'capitalize'), onClick: () => { setCurrentOption(option) }, @@ -263,7 +264,7 @@ const TransferSectionContent = ({ let protocolSection: JSX.Element | null = null switch (currentOption) { - case 'Summary': + case 'summary': protocolSection = ( ) break - case 'Hardware': + case 'hardware': protocolSection = break - case 'Labware': + case 'labware': protocolSection = break - case 'Deck': + case 'deck': protocolSection = break } return ( {protocolSection} diff --git a/app/src/pages/ODD/RobotDashboard/__tests__/WelcomeModal.test.tsx b/app/src/pages/ODD/RobotDashboard/__tests__/WelcomeModal.test.tsx index 7fc9bf46ea2..8258f29f281 100644 --- a/app/src/pages/ODD/RobotDashboard/__tests__/WelcomeModal.test.tsx +++ b/app/src/pages/ODD/RobotDashboard/__tests__/WelcomeModal.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { vi, it, describe, expect, beforeEach } from 'vitest' import { fireEvent, screen } from '@testing-library/react' @@ -9,6 +8,7 @@ import { i18n } from '/app/i18n' import { updateConfigValue } from '/app/redux/config' import { WelcomeModal } from '../WelcomeModal' +import type { ComponentProps } from 'react' import type { SetStatusBarCreateCommand } from '@opentrons/shared-data' vi.mock('/app/redux/config') @@ -17,14 +17,14 @@ vi.mock('@opentrons/react-api-client') const mockFunc = vi.fn() const WELCOME_MODAL_IMAGE_NAME = 'welcome_dashboard_modal.png' -const render = (props: React.ComponentProps) => { +const render = (props: ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, }) } describe('WelcomeModal', () => { - let props: React.ComponentProps + let props: ComponentProps let mockCreateLiveCommand = vi.fn() beforeEach(() => { diff --git a/app/src/pages/ODD/RobotSettingsDashboard/RobotSettingsList.tsx b/app/src/pages/ODD/RobotSettingsDashboard/RobotSettingsList.tsx index cbc0d68e353..54e25a65c1e 100644 --- a/app/src/pages/ODD/RobotSettingsDashboard/RobotSettingsList.tsx +++ b/app/src/pages/ODD/RobotSettingsDashboard/RobotSettingsList.tsx @@ -20,7 +20,7 @@ import { TYPOGRAPHY, } from '@opentrons/components' -import { LANGUAGES } from '/app/i18n' +import { LANGUAGES, US_ENGLISH_DISPLAY_NAME } from '/app/i18n' import { getLocalRobot, getRobotApiVersion } from '/app/redux/discovery' import { getRobotUpdateAvailable } from '/app/redux/robot-update' import { useErrorRecoverySettingsToggle } from '/app/resources/errorRecovery' @@ -61,6 +61,7 @@ export function RobotSettingsList(props: RobotSettingsListProps): JSX.Element { 'app_settings', 'branded', ]) + const isNewLpc = useFeatureFlag('lpcRedesign') const dispatch = useDispatch() const localRobot = useSelector(getLocalRobot) const robotName = localRobot?.name != null ? localRobot.name : 'no name' @@ -90,7 +91,6 @@ export function RobotSettingsList(props: RobotSettingsListProps): JSX.Element { const appLanguage = useSelector(getAppLanguage) const currentLanguageOption = LANGUAGES.find(lng => lng.value === appLanguage) - const enableLocalization = useFeatureFlag('enableLocalization') return ( @@ -143,18 +143,18 @@ export function RobotSettingsList(props: RobotSettingsListProps): JSX.Element { } /> - {enableLocalization ? ( - { - setCurrentOption('LanguageSetting') - }} - iconName="language" - /> - ) : null} + { + setCurrentOption('LanguageSetting') + }} + iconName="language" + /> - } - onClick={() => dispatch(toggleHistoricOffsets())} - /> + {!isNewLpc && ( + } + onClick={() => dispatch(toggleHistoricOffsets())} + /> + )} { toggleERSettings: mockToggleER, }) vi.mocked(getAppLanguage).mockReturnValue(MOCK_DEFAULT_LANGUAGE) - when(vi.mocked(useFeatureFlag)) - .calledWith('enableLocalization') - .thenReturn(true) }) afterEach(() => { diff --git a/app/src/pages/ODD/RunSummary/index.tsx b/app/src/pages/ODD/RunSummary/index.tsx index 9f84523505d..57c6ffe96ee 100644 --- a/app/src/pages/ODD/RunSummary/index.tsx +++ b/app/src/pages/ODD/RunSummary/index.tsx @@ -39,6 +39,7 @@ import { useProtocolQuery, useDeleteRunMutation, useRunCommandErrors, + useErrorRecoverySettings, } from '@opentrons/react-api-client' import { useRunControls } from '/app/organisms/RunTimeControl/hooks' import { onDeviceDisplayFormatTimestamp } from '/app/transformations/runs' @@ -67,15 +68,13 @@ import { EMPTY_TIMESTAMP, useCurrentRunCommands, } from '/app/resources/runs' -import { - useTipAttachmentStatus, - handleTipsAttachedModal, -} from '/app/organisms/DropTipWizardFlows' +import { handleTipsAttachedModal } from '/app/organisms/DropTipWizardFlows' +import { useTipAttachmentStatus } from '/app/resources/instruments' import { lastRunCommandPromptedErrorRecovery } from '/app/local-resources/commands' import type { IconName } from '@opentrons/components' import type { OnDeviceRouteParams } from '/app/App/types' -import type { PipetteWithTip } from '/app/organisms/DropTipWizardFlows' +import type { PipetteWithTip } from '/app/resources/instruments' export function RunSummary(): JSX.Element { const { runId } = useParams< @@ -236,24 +235,20 @@ export function RunSummary(): JSX.Element { } = useTipAttachmentStatus({ runId, runRecord: runRecord ?? null, - host, }) - - // Determine tip status on initial render only. Error Recovery always handles tip status, so don't show it twice. + const { data } = useErrorRecoverySettings() + const isEREnabled = data?.data.enabled ?? true const runSummaryNoFixit = useCurrentRunCommands({ includeFixitCommands: false, pageLength: 1, }) + useEffect(() => { - if ( - isRunCurrent && - runSummaryNoFixit != null && - runSummaryNoFixit.length > 0 && - !lastRunCommandPromptedErrorRecovery(runSummaryNoFixit) - ) { + // Only run tip checking if it wasn't *just* handled during Error Recovery. + if (!lastRunCommandPromptedErrorRecovery(runSummaryNoFixit, isEREnabled)) { void determineTipStatus() } - }, [runSummaryNoFixit, isRunCurrent]) + }, [isRunCurrent, runSummaryNoFixit, isEREnabled]) const returnToQuickTransfer = (): void => { closeCurrentRunIfValid(() => { diff --git a/app/src/pages/ODD/RunningProtocol/__tests__/RunningProtocol.test.tsx b/app/src/pages/ODD/RunningProtocol/__tests__/RunningProtocol.test.tsx index 33d6f79e9a7..cbb45387a5a 100644 --- a/app/src/pages/ODD/RunningProtocol/__tests__/RunningProtocol.test.tsx +++ b/app/src/pages/ODD/RunningProtocol/__tests__/RunningProtocol.test.tsx @@ -161,6 +161,7 @@ describe('RunningProtocol', () => { vi.mocked(useErrorRecoveryFlows).mockReturnValue({ isERActive: false, failedCommand: {} as any, + runLwDefsByUri: {} as any, }) vi.mocked(useInterventionModal).mockReturnValue({ showModal: false, @@ -224,6 +225,7 @@ describe('RunningProtocol', () => { vi.mocked(useErrorRecoveryFlows).mockReturnValue({ isERActive: true, failedCommand: {} as any, + runLwDefsByUri: {} as any, }) render(`/runs/${RUN_ID}/run`) screen.getByText('MOCK ERROR RECOVERY') diff --git a/app/src/pages/ODD/RunningProtocol/index.tsx b/app/src/pages/ODD/RunningProtocol/index.tsx index 33d9d515930..eee50894ebb 100644 --- a/app/src/pages/ODD/RunningProtocol/index.tsx +++ b/app/src/pages/ODD/RunningProtocol/index.tsx @@ -122,7 +122,10 @@ export function RunningProtocol(): JSX.Element { const { trackProtocolRunEvent } = useTrackProtocolRunEvent(runId, robotName) const robotAnalyticsData = useRobotAnalyticsData(robotName) const robotType = useRobotType(robotName) - const { isERActive, failedCommand } = useErrorRecoveryFlows(runId, runStatus) + const { isERActive, failedCommand, runLwDefsByUri } = useErrorRecoveryFlows( + runId, + runStatus + ) const { showModal: showIntervention, modalProps: interventionProps, @@ -169,6 +172,7 @@ export function RunningProtocol(): JSX.Element { runStatus={runStatus} runId={runId} unvalidatedFailedCommand={failedCommand} + runLwDefsByUri={runLwDefsByUri} protocolAnalysis={robotSideAnalysis} /> ) : null} diff --git a/app/src/redux-resources/analytics/hooks/__tests__/useProtocolRunAnalyticsData.test.tsx b/app/src/redux-resources/analytics/hooks/__tests__/useProtocolRunAnalyticsData.test.tsx index b99b66c8816..1f0501e0c96 100644 --- a/app/src/redux-resources/analytics/hooks/__tests__/useProtocolRunAnalyticsData.test.tsx +++ b/app/src/redux-resources/analytics/hooks/__tests__/useProtocolRunAnalyticsData.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { vi, it, expect, describe, beforeEach, afterEach } from 'vitest' import { when } from 'vitest-when' import { renderHook, waitFor } from '@testing-library/react' @@ -14,6 +13,8 @@ import { useStoredProtocolAnalysis } from '/app/resources/analysis' import { useProtocolMetadata } from '/app/resources/protocols' import { formatInterval } from '/app/transformations/commands' import { mockConnectableRobot } from '/app/redux/discovery/__fixtures__' + +import type { FunctionComponent, ReactNode } from 'react' import type { Store } from 'redux' vi.mock('/app/redux/analytics/hash') @@ -23,7 +24,7 @@ vi.mock('/app/resources/analysis') vi.mock('/app/resources/runs') vi.mock('/app/transformations/commands') -let wrapper: React.FunctionComponent<{ children: React.ReactNode }> +let wrapper: FunctionComponent<{ children: ReactNode }> let store: Store = createStore(vi.fn(), {}) const RUN_ID = '1' diff --git a/app/src/redux-resources/analytics/hooks/__tests__/useRobotAnalyticsData.test.tsx b/app/src/redux-resources/analytics/hooks/__tests__/useRobotAnalyticsData.test.tsx index a781d71c495..bdb7407f843 100644 --- a/app/src/redux-resources/analytics/hooks/__tests__/useRobotAnalyticsData.test.tsx +++ b/app/src/redux-resources/analytics/hooks/__tests__/useRobotAnalyticsData.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { vi, it, expect, describe, beforeEach, afterEach } from 'vitest' import { when } from 'vitest-when' import { renderHook } from '@testing-library/react' @@ -18,6 +17,7 @@ import { getRobotSerialNumber, } from '/app/redux/discovery' +import type { FunctionComponent, ReactNode } from 'react' import type { Store } from 'redux' import type { DiscoveredRobot } from '/app/redux/discovery/types' import type { AttachedPipettesByMount } from '/app/redux/pipettes/types' @@ -41,7 +41,7 @@ const ATTACHED_PIPETTES = { } const ROBOT_SERIAL_NUMBER = 'OT123' -let wrapper: React.FunctionComponent<{ children: React.ReactNode }> +let wrapper: FunctionComponent<{ children: ReactNode }> let store: Store = createStore(vi.fn(), {}) describe('useRobotAnalyticsData hook', () => { diff --git a/app/src/redux-resources/analytics/hooks/__tests__/useTrackProtocolRunEvent.test.tsx b/app/src/redux-resources/analytics/hooks/__tests__/useTrackProtocolRunEvent.test.tsx index 3172c8d1fbc..b387efa58fd 100644 --- a/app/src/redux-resources/analytics/hooks/__tests__/useTrackProtocolRunEvent.test.tsx +++ b/app/src/redux-resources/analytics/hooks/__tests__/useTrackProtocolRunEvent.test.tsx @@ -1,8 +1,7 @@ -import type * as React from 'react' import { createStore } from 'redux' import { Provider } from 'react-redux' import { QueryClient, QueryClientProvider } from 'react-query' -import { vi, it, expect, describe, beforeEach, afterEach } from 'vitest' +import { vi, it, expect, describe, beforeEach } from 'vitest' import { when } from 'vitest-when' import { waitFor, renderHook } from '@testing-library/react' @@ -12,9 +11,11 @@ import { useTrackEvent, ANALYTICS_PROTOCOL_RUN_ACTION, } from '/app/redux/analytics' +import { getAppLanguage } from '/app/redux/config' import { mockConnectableRobot } from '/app/redux/discovery/__fixtures__' import { useRobot } from '/app/redux-resources/robots' +import type { FunctionComponent, ReactNode } from 'react' import type { Store } from 'redux' import type { Mock } from 'vitest' @@ -23,6 +24,7 @@ vi.mock('../useProtocolRunAnalyticsData') vi.mock('/app/redux/discovery') vi.mock('/app/redux/pipettes') vi.mock('/app/redux/analytics') +vi.mock('/app/redux/config') vi.mock('/app/redux/robot-settings') const RUN_ID = 'runId' @@ -31,7 +33,7 @@ const PROTOCOL_PROPERTIES = { protocolType: 'python' } let mockTrackEvent: Mock let mockGetProtocolRunAnalyticsData: Mock -let wrapper: React.FunctionComponent<{ children: React.ReactNode }> +let wrapper: FunctionComponent<{ children: ReactNode }> let store: Store = createStore(vi.fn(), {}) describe('useTrackProtocolRunEvent hook', () => { @@ -55,6 +57,7 @@ describe('useTrackProtocolRunEvent hook', () => { ) vi.mocked(useRobot).mockReturnValue(mockConnectableRobot) vi.mocked(useTrackEvent).mockReturnValue(mockTrackEvent) + vi.mocked(getAppLanguage).mockReturnValue('en-US') when(vi.mocked(useProtocolRunAnalyticsData)) .calledWith(RUN_ID, mockConnectableRobot) @@ -63,10 +66,6 @@ describe('useTrackProtocolRunEvent hook', () => { }) }) - afterEach(() => { - vi.resetAllMocks() - }) - it('returns trackProtocolRunEvent function', () => { const { result } = renderHook( () => useTrackProtocolRunEvent(RUN_ID, ROBOT_NAME), @@ -92,7 +91,11 @@ describe('useTrackProtocolRunEvent hook', () => { ) expect(mockTrackEvent).toHaveBeenCalledWith({ name: ANALYTICS_PROTOCOL_RUN_ACTION.START, - properties: PROTOCOL_PROPERTIES, + properties: { + ...PROTOCOL_PROPERTIES, + transactionId: RUN_ID, + appLanguage: 'en-US', + }, }) }) diff --git a/app/src/redux-resources/analytics/hooks/useTrackProtocolRunEvent.ts b/app/src/redux-resources/analytics/hooks/useTrackProtocolRunEvent.ts index 2f9f085fd64..0603994d4b4 100644 --- a/app/src/redux-resources/analytics/hooks/useTrackProtocolRunEvent.ts +++ b/app/src/redux-resources/analytics/hooks/useTrackProtocolRunEvent.ts @@ -1,5 +1,7 @@ +import { useSelector } from 'react-redux' import { useTrackEvent } from '/app/redux/analytics' import { useProtocolRunAnalyticsData } from './useProtocolRunAnalyticsData' +import { getAppLanguage } from '/app/redux/config' import { useRobot } from '/app/redux-resources/robots' interface ProtocolRunAnalyticsEvent { @@ -21,7 +23,7 @@ export function useTrackProtocolRunEvent( runId, robot ) - + const appLanguage = useSelector(getAppLanguage) const trackProtocolRunEvent: TrackProtocolRunEvent = ({ name, properties = {}, @@ -34,6 +36,10 @@ export function useTrackProtocolRunEvent( ...properties, ...protocolRunAnalyticsData, runTime, + // It's sometimes unavoidable (namely on the desktop app) to prevent sending an event multiple times. + // In these circumstances, we need an idempotency key to accurately filter events in Mixpanel. + transactionId: runId, + appLanguage, }, }) }) diff --git a/app/src/redux-resources/config/__tests__/useIsUnboxingFlowOngoing.test.tsx b/app/src/redux-resources/config/__tests__/useIsUnboxingFlowOngoing.test.tsx index d7610096015..cc65ca15100 100644 --- a/app/src/redux-resources/config/__tests__/useIsUnboxingFlowOngoing.test.tsx +++ b/app/src/redux-resources/config/__tests__/useIsUnboxingFlowOngoing.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { renderHook } from '@testing-library/react' import { describe, it, expect, vi, beforeEach } from 'vitest' import { Provider } from 'react-redux' @@ -7,6 +6,7 @@ import { createStore } from 'redux' import { getIsOnDevice, getOnDeviceDisplaySettings } from '/app/redux/config' import { useIsUnboxingFlowOngoing } from '../useIsUnboxingFlowOngoing' +import type { FunctionComponent, ReactNode } from 'react' import type { Store } from 'redux' import type { State } from '/app/redux/types' @@ -22,7 +22,7 @@ const mockDisplaySettings = { } describe('useIsUnboxingFlowOngoing', () => { - let wrapper: React.FunctionComponent<{ children: React.ReactNode }> + let wrapper: FunctionComponent<{ children: ReactNode }> beforeEach(() => { wrapper = ({ children }) => {children} vi.mocked(getOnDeviceDisplaySettings).mockReturnValue(mockDisplaySettings) diff --git a/app/src/redux-resources/robots/hooks/__tests__/useIsFlex.test.tsx b/app/src/redux-resources/robots/hooks/__tests__/useIsFlex.test.tsx index acb68840dfe..19ca25dff24 100644 --- a/app/src/redux-resources/robots/hooks/__tests__/useIsFlex.test.tsx +++ b/app/src/redux-resources/robots/hooks/__tests__/useIsFlex.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { vi, it, expect, describe, beforeEach, afterEach } from 'vitest' import { when } from 'vitest-when' import { Provider } from 'react-redux' @@ -9,6 +8,8 @@ import { QueryClient, QueryClientProvider } from 'react-query' import { getRobotModelByName } from '/app/redux/discovery' import { useIsFlex } from '..' + +import type { FunctionComponent, ReactNode } from 'react' import type { Store } from 'redux' vi.mock('/app/redux/discovery/selectors') @@ -16,7 +17,7 @@ vi.mock('/app/redux/discovery/selectors') const store: Store = createStore(vi.fn(), {}) describe('useIsFlex hook', () => { - let wrapper: React.FunctionComponent<{ children: React.ReactNode }> + let wrapper: FunctionComponent<{ children: ReactNode }> beforeEach(() => { const queryClient = new QueryClient() wrapper = ({ children }) => ( diff --git a/app/src/redux-resources/robots/hooks/__tests__/useIsRobotViewable.test.tsx b/app/src/redux-resources/robots/hooks/__tests__/useIsRobotViewable.test.tsx index 8b659d25656..38e908b8452 100644 --- a/app/src/redux-resources/robots/hooks/__tests__/useIsRobotViewable.test.tsx +++ b/app/src/redux-resources/robots/hooks/__tests__/useIsRobotViewable.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { vi, it, expect, describe, beforeEach, afterEach } from 'vitest' import { when } from 'vitest-when' import { Provider } from 'react-redux' @@ -13,6 +12,8 @@ import { mockUnreachableRobot, } from '/app/redux/discovery/__fixtures__' import { useIsRobotViewable } from '../useIsRobotViewable' + +import type { FunctionComponent, ReactNode } from 'react' import type { Store } from 'redux' vi.mock('/app/redux/discovery') @@ -20,7 +21,7 @@ vi.mock('/app/redux/discovery') const store: Store = createStore(vi.fn(), {}) describe('useIsRobotViewable hook', () => { - let wrapper: React.FunctionComponent<{ children: React.ReactNode }> + let wrapper: FunctionComponent<{ children: ReactNode }> beforeEach(() => { const queryClient = new QueryClient() wrapper = ({ children }) => ( diff --git a/app/src/redux-resources/robots/hooks/__tests__/useRobot.test.tsx b/app/src/redux-resources/robots/hooks/__tests__/useRobot.test.tsx index df7e05347dd..f99452c7994 100644 --- a/app/src/redux-resources/robots/hooks/__tests__/useRobot.test.tsx +++ b/app/src/redux-resources/robots/hooks/__tests__/useRobot.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { when } from 'vitest-when' import { vi, it, expect, describe, beforeEach, afterEach } from 'vitest' import { Provider } from 'react-redux' @@ -11,6 +10,7 @@ import { mockConnectableRobot } from '/app/redux/discovery/__fixtures__' import { useRobot } from '..' +import type { FunctionComponent, ReactNode } from 'react' import type { Store } from 'redux' vi.mock('/app/redux/discovery') @@ -18,7 +18,7 @@ vi.mock('/app/redux/discovery') const store: Store = createStore(vi.fn(), {}) describe('useRobot hook', () => { - let wrapper: React.FunctionComponent<{ children: React.ReactNode }> + let wrapper: FunctionComponent<{ children: ReactNode }> beforeEach(() => { const queryClient = new QueryClient() wrapper = ({ children }) => ( diff --git a/app/src/redux/analytics/__tests__/make-event.test.ts b/app/src/redux/analytics/__tests__/make-event.test.ts index 70506dc162a..8cc56e18950 100644 --- a/app/src/redux/analytics/__tests__/make-event.test.ts +++ b/app/src/redux/analytics/__tests__/make-event.test.ts @@ -1,5 +1,7 @@ import { vi, describe, it, expect, beforeEach } from 'vitest' +import { OT2_ROBOT_TYPE } from '@opentrons/shared-data' + import { makeEvent } from '../make-event' import * as selectors from '../selectors' @@ -49,6 +51,7 @@ describe('analytics events map', () => { name: 'pipetteOffsetCalibrationStarted', properties: { ...action.payload, + robotType: OT2_ROBOT_TYPE, }, }) }) @@ -65,6 +68,7 @@ describe('analytics events map', () => { name: 'tipLengthCalibrationStarted', properties: { ...action.payload, + robotType: OT2_ROBOT_TYPE, }, }) }) @@ -77,6 +81,7 @@ describe('analytics events map', () => { robotName: 'my-robot', sessionId: 'seshid', command: { command: 'calibration.exitSession' }, + robotType: OT2_ROBOT_TYPE, }, } as any vi.mocked(selectors.getAnalyticsSessionExitDetails).mockReturnValue({ @@ -86,7 +91,7 @@ describe('analytics events map', () => { return expect(makeEvent(action, state)).resolves.toEqual({ name: 'my-session-typeExit', - properties: { step: 'session-step' }, + properties: { step: 'session-step', robotType: OT2_ROBOT_TYPE }, }) }) @@ -117,12 +122,11 @@ describe('analytics events map', () => { properties: { pipetteModel: 'my-pipette-model', tipRackDisplayName: 'some display name', + robotType: OT2_ROBOT_TYPE, }, }) }) - }) - describe('events with calibration data', () => { it('analytics:RESOURCE_MONITOR_REPORT -> resourceMonitorReport event', () => { const state = {} as any const action = { diff --git a/app/src/redux/analytics/constants.ts b/app/src/redux/analytics/constants.ts index cde9b0a1d59..aadeb7c6696 100644 --- a/app/src/redux/analytics/constants.ts +++ b/app/src/redux/analytics/constants.ts @@ -103,3 +103,15 @@ export const ANALYTICS_QUICK_TRANSFER_RERUN = 'quickTransferReRunFromSummary' */ export const ANALYTICS_RESOURCE_MONITOR_REPORT: 'analytics:RESOURCE_MONITOR_REPORT' = 'analytics:RESOURCE_MONITOR_REPORT' + +/** + * Internationalization Analytics + */ +export const ANALYTICS_LANGUAGE_UPDATED_ODD_UNBOXING_FLOW: 'languageUpdatedOddUnboxingFlow' = + 'languageUpdatedOddUnboxingFlow' +export const ANALYTICS_LANGUAGE_UPDATED_ODD_SETTINGS: 'languageUpdatedOddSettings' = + 'languageUpdatedOddSettings' +export const ANALYTICS_LANGUAGE_UPDATED_DESKTOP_APP_MODAL: 'languageUpdatedDesktopAppModal' = + 'languageUpdatedDesktopAppModal' +export const ANALYTICS_LANGUAGE_UPDATED_DESKTOP_APP_SETTINGS: 'languageUpdatedDesktopAppSettings' = + 'languageUpdatedDesktopAppSettings' diff --git a/app/src/redux/analytics/make-event.ts b/app/src/redux/analytics/make-event.ts index bc5c8955104..608915c4112 100644 --- a/app/src/redux/analytics/make-event.ts +++ b/app/src/redux/analytics/make-event.ts @@ -13,6 +13,7 @@ import { getAnalyticsSessionExitDetails, getSessionInstrumentAnalyticsData, } from './selectors' +import { OT2_ROBOT_TYPE } from '@opentrons/shared-data' import type { State, Action } from '../types' import type { AnalyticsEvent } from './types' @@ -180,6 +181,7 @@ export function makeEvent( name: `${sessionDetails.sessionType}Exit`, properties: { step: sessionDetails.step, + robotType: OT2_ROBOT_TYPE, }, } : null @@ -203,6 +205,7 @@ export function makeEvent( 'tiprackDefinition' in commandData ? commandData.tiprackDefinition.metadata.displayName : null, + robotType: OT2_ROBOT_TYPE, }, } : null @@ -234,6 +237,7 @@ export function makeEvent( name: 'pipetteOffsetCalibrationStarted', properties: { ...action.payload, + robotType: OT2_ROBOT_TYPE, }, }) } @@ -243,6 +247,7 @@ export function makeEvent( name: 'tipLengthCalibrationStarted', properties: { ...action.payload, + robotType: OT2_ROBOT_TYPE, }, }) } diff --git a/app/src/redux/analytics/mixpanel.ts b/app/src/redux/analytics/mixpanel.ts index aa5ad5a7893..5304bbbe563 100644 --- a/app/src/redux/analytics/mixpanel.ts +++ b/app/src/redux/analytics/mixpanel.ts @@ -1,9 +1,9 @@ -// mixpanel actions import mixpanel from 'mixpanel-browser' -import { createLogger } from '../../logger' +import { createLogger } from '/app/logger' import { CURRENT_VERSION } from '../shell' +import type { Config as MixpanelConfig } from 'mixpanel-browser' import type { AnalyticsEvent, AnalyticsConfig } from './types' const log = createLogger(new URL('', import.meta.url).pathname) @@ -11,7 +11,7 @@ const log = createLogger(new URL('', import.meta.url).pathname) // pulled in from environment at build time const MIXPANEL_ID = process.env.OT_APP_MIXPANEL_ID -const MIXPANEL_OPTS = { +const MIXPANEL_OPTS: Partial = { // opt out by default opt_out_tracking_by_default: true, // user details are persisted in our own config store @@ -27,9 +27,13 @@ export function initializeMixpanel( isOnDevice: boolean | null ): void { if (MIXPANEL_ID != null) { - initMixpanelInstanceOnce(config) - setMixpanelTracking(config, isOnDevice) - trackEvent({ name: 'appOpen', properties: {} }, config) + try { + initMixpanelInstanceOnce(config) + setMixpanelTracking(config, isOnDevice) + trackEvent({ name: 'appOpen', properties: {} }, config) + } catch (error) { + console.error('Failed to initialize Mixpanel:', error) + } } else { log.warn('MIXPANEL_ID not found; this is a bug if build is production') } @@ -43,11 +47,15 @@ export function trackEvent( log.debug('Trackable event', { event, optedIn }) if (MIXPANEL_ID != null && optedIn) { - if (event.superProperties != null) { - mixpanel.register(event.superProperties) - } - if ('name' in event && event.name != null) { - mixpanel.track(event.name, event.properties) + try { + if (event.superProperties != null) { + mixpanel.register(event.superProperties) + } + if ('name' in event && event.name != null) { + mixpanel.track(event.name, event.properties) + } + } catch (error) { + console.error('Failed to track event:', error) } } } @@ -58,19 +66,23 @@ export function setMixpanelTracking( ): void { if (MIXPANEL_ID != null) { initMixpanelInstanceOnce(config) - if (config.optedIn) { - log.debug('User has opted into analytics; tracking with Mixpanel') - mixpanel.identify(config.appId) - mixpanel.opt_in_tracking() - mixpanel.register({ - appVersion: CURRENT_VERSION, - appId: config.appId, - appMode: Boolean(isOnDevice) ? 'ODD' : 'Desktop', - }) - } else { - log.debug('User has opted out of analytics; stopping tracking') - mixpanel.opt_out_tracking?.() - mixpanel.reset?.() + try { + if (config.optedIn) { + log.debug('User has opted into analytics; tracking with Mixpanel') + mixpanel.identify(config.appId) + mixpanel.opt_in_tracking() + mixpanel.register({ + appVersion: CURRENT_VERSION, + appId: config.appId, + appMode: Boolean(isOnDevice) ? 'ODD' : 'Desktop', + }) + } else { + log.debug('User has opted out of analytics; stopping tracking') + mixpanel.opt_out_tracking() + mixpanel.reset() + } + } catch (error) { + console.error('Failed to set Mixpanel tracking:', error) } } } @@ -82,9 +94,13 @@ function initializeMixpanelInstanceOnce( return function (config: AnalyticsConfig): undefined { if (!hasBeenInitialized && MIXPANEL_ID != null) { - hasBeenInitialized = true - log.debug('Initializing Mixpanel', { config }) - mixpanel.init(MIXPANEL_ID, MIXPANEL_OPTS) + try { + hasBeenInitialized = true + log.debug('Initializing Mixpanel', { config }) + mixpanel.init(MIXPANEL_ID, MIXPANEL_OPTS) + } catch (error) { + console.error('Failed to initialize Mixpanel instance:', error) + } } } } diff --git a/app/src/redux/analytics/selectors.ts b/app/src/redux/analytics/selectors.ts index 4751a8eb99e..962f2c55832 100644 --- a/app/src/redux/analytics/selectors.ts +++ b/app/src/redux/analytics/selectors.ts @@ -1,24 +1,24 @@ import * as Sessions from '../sessions' -import { getViewableRobots, getRobotApiVersion } from '../discovery' +import { FLEX_ROBOT_TYPE, OT2_ROBOT_TYPE } from '@opentrons/shared-data' +import { getViewableRobots, getRobotApiVersion } from '../discovery' import { getRobotUpdateVersion, getRobotUpdateRobot, getRobotUpdateSession, getRobotSystemType, } from '../robot-update' - import { getRobotSessionById } from '../sessions/selectors' import type { State } from '../types' - import type { AnalyticsConfig, BuildrootAnalyticsData, AnalyticsSessionExitDetails, SessionInstrumentAnalyticsData, } from './types' +import type { RobotType } from '@opentrons/shared-data' export function getBuildrootAnalyticsData( state: State, @@ -40,12 +40,29 @@ export function getBuildrootAnalyticsData( const currentVersion = getRobotApiVersion(robot) ?? 'unknown' const currentSystem = getRobotSystemType(robot) ?? 'unknown' + const getRobotType = (): RobotType | undefined => { + switch (currentSystem) { + case 'flex': + return FLEX_ROBOT_TYPE + case 'ot2-buildroot': + case 'ot2-balena': + return OT2_ROBOT_TYPE + case 'unknown': + return undefined + default: { + console.error('Unexpected system type: ', currentSystem) + return undefined + } + } + } + return { currentVersion, currentSystem, updateVersion: updateVersion ?? 'unknown', error: session != null && 'error' in session ? session.error : null, robotSerialNumber, + robotType: getRobotType(), } } diff --git a/app/src/redux/analytics/types.ts b/app/src/redux/analytics/types.ts index d27c2955fe2..95b320e88ab 100644 --- a/app/src/redux/analytics/types.ts +++ b/app/src/redux/analytics/types.ts @@ -1,4 +1,4 @@ -import type { PipetteMount as Mount } from '@opentrons/shared-data' +import type { PipetteMount as Mount, RobotType } from '@opentrons/shared-data' import type { CalibrationCheckComparisonsPerCalibration } from '../sessions/types' import type { DeckCalibrationStatus } from '../calibration/types' import type { Config } from '../config/types' @@ -42,6 +42,7 @@ export interface BuildrootAnalyticsData { updateVersion: string error: string | null robotSerialNumber: string | null + robotType: RobotType | undefined } export interface PipetteOffsetCalibrationAnalyticsData { diff --git a/app/src/redux/config/__tests__/selectors.test.ts b/app/src/redux/config/__tests__/selectors.test.ts index 18262108c0a..00ac8f42ed3 100644 --- a/app/src/redux/config/__tests__/selectors.test.ts +++ b/app/src/redux/config/__tests__/selectors.test.ts @@ -246,7 +246,7 @@ describe('shell selectors', () => { sleepMs: 25200000, brightness: 4, textSize: 1, - unfinishedUnboxingFlowRoute: '/welcome', + unfinishedUnboxingFlowRoute: '/choose-language', }, }, } as any @@ -254,7 +254,7 @@ describe('shell selectors', () => { sleepMs: 25200000, brightness: 4, textSize: 1, - unfinishedUnboxingFlowRoute: '/welcome', + unfinishedUnboxingFlowRoute: '/choose-language', }) }) }) diff --git a/app/src/redux/config/constants.ts b/app/src/redux/config/constants.ts index 4cd981093fc..683f3613980 100644 --- a/app/src/redux/config/constants.ts +++ b/app/src/redux/config/constants.ts @@ -4,9 +4,11 @@ export const DEV_INTERNAL_FLAGS: DevInternalFlag[] = [ 'forceHttpPolling', 'protocolStats', 'enableRunNotes', + 'lpcRedesign', 'protocolTimeline', 'enableLabwareCreator', - 'enableLocalization', + 'reactQueryDevtools', + 'reactScan', ] // action type constants diff --git a/app/src/redux/config/schema-types.ts b/app/src/redux/config/schema-types.ts index a38ed931ec9..a917bdbb3dd 100644 --- a/app/src/redux/config/schema-types.ts +++ b/app/src/redux/config/schema-types.ts @@ -12,9 +12,11 @@ export type DevInternalFlag = | 'forceHttpPolling' | 'protocolStats' | 'enableRunNotes' + | 'lpcRedesign' | 'protocolTimeline' | 'enableLabwareCreator' - | 'enableLocalization' + | 'reactQueryDevtools' + | 'reactScan' export type FeatureFlags = Partial> @@ -283,4 +285,8 @@ export type ConfigV25 = Omit & { } } -export type Config = ConfigV25 +export type ConfigV26 = Omit & { + version: 26 +} + +export type Config = ConfigV26 diff --git a/app/src/redux/config/selectors.ts b/app/src/redux/config/selectors.ts index dbac2cd3c05..cdbe9bb22c6 100644 --- a/app/src/redux/config/selectors.ts +++ b/app/src/redux/config/selectors.ts @@ -1,5 +1,5 @@ import { createSelector } from 'reselect' -import { SLEEP_NEVER_MS } from '/app/local-resources/config' +import { SLEEP_NEVER_MS } from '/app/local-resources/dom-utils' import type { State } from '../types' import type { Config, @@ -148,7 +148,7 @@ export const getOnDeviceDisplaySettings: ( sleepMs: SLEEP_NEVER_MS, brightness: 4, textSize: 1, - unfinishedUnboxingFlowRoute: '/welcome', + unfinishedUnboxingFlowRoute: '/choose-language', } }) diff --git a/app/src/redux/protocol-storage/selectors.ts b/app/src/redux/protocol-storage/selectors.ts index 4684443918b..c351f07b708 100644 --- a/app/src/redux/protocol-storage/selectors.ts +++ b/app/src/redux/protocol-storage/selectors.ts @@ -1,7 +1,8 @@ import { createSelector } from 'reselect' +import { getGroupedCommands } from './utils' import type { State } from '../types' -import type { StoredProtocolData } from './types' +import type { GroupedCommands, StoredProtocolData } from './types' export const getStoredProtocols: ( state: State @@ -27,3 +28,26 @@ export const getIsProtocolAnalysisInProgress: ( protocolKey: string ) => boolean = (state, protocolKey) => state.protocolStorage.inProgressAnalysisProtocolKeys.includes(protocolKey) + +export const getStoredProtocolGroupedCommands: ( + state: State, + protocolKey?: string | null +) => GroupedCommands | null = (state, protocolKey) => { + const storedProtocolData = + protocolKey != null + ? state.protocolStorage.filesByProtocolKey[protocolKey] ?? null + : null + + if (storedProtocolData == null) { + return null + } + const mostRecentAnalysis = storedProtocolData.mostRecentAnalysis + const groupedCommands = + mostRecentAnalysis != null && + mostRecentAnalysis.commandAnnotations != null && + mostRecentAnalysis.commandAnnotations.length > 0 + ? getGroupedCommands(mostRecentAnalysis) + : [] + + return groupedCommands +} diff --git a/app/src/redux/protocol-storage/types.ts b/app/src/redux/protocol-storage/types.ts index e406cee24ca..5297c09c417 100644 --- a/app/src/redux/protocol-storage/types.ts +++ b/app/src/redux/protocol-storage/types.ts @@ -1,6 +1,9 @@ // common types -import type { ProtocolAnalysisOutput } from '@opentrons/shared-data' +import type { + ProtocolAnalysisOutput, + RunTimeCommand, +} from '@opentrons/shared-data' export type ProtocolSort = | 'alphabetical' @@ -10,6 +13,18 @@ export type ProtocolSort = | 'flex' | 'ot2' +interface ParentNode { + annotationIndex: number + subCommands: LeafNode[] + isHighlighted: boolean +} +export interface LeafNode { + command: RunTimeCommand + isHighlighted: boolean +} + +export type GroupedCommands = Array + export interface StoredProtocolDir { dirPath: string modified: number diff --git a/app/src/redux/protocol-storage/utils.ts b/app/src/redux/protocol-storage/utils.ts new file mode 100644 index 00000000000..8d1094aee5d --- /dev/null +++ b/app/src/redux/protocol-storage/utils.ts @@ -0,0 +1,47 @@ +import type { + CompletedProtocolAnalysis, + ProtocolAnalysisOutput, +} from '@opentrons/shared-data' +import type { GroupedCommands } from './types' + +export const getGroupedCommands = ( + mostRecentAnalysis: ProtocolAnalysisOutput | CompletedProtocolAnalysis +): GroupedCommands => { + const annotations = mostRecentAnalysis?.commandAnnotations ?? [] + return mostRecentAnalysis.commands.reduce((acc, c) => { + const foundAnnotationIndex = annotations.findIndex( + a => c.key != null && a.commandKeys.includes(c.key) + ) + const lastAccNode = acc[acc.length - 1] + if ( + acc.length > 0 && + c.key != null && + 'annotationIndex' in lastAccNode && + lastAccNode.annotationIndex != null && + annotations[lastAccNode.annotationIndex]?.commandKeys.includes(c.key) + ) { + return [ + ...acc.slice(0, -1), + { + ...lastAccNode, + subCommands: [ + ...lastAccNode.subCommands, + { command: c, isHighlighted: false }, + ], + isHighlighted: false, + }, + ] + } else if (foundAnnotationIndex >= 0) { + return [ + ...acc, + { + annotationIndex: foundAnnotationIndex, + subCommands: [{ command: c, isHighlighted: false }], + isHighlighted: false, + }, + ] + } else { + return [...acc, { command: c, isHighlighted: false }] + } + }, []) +} diff --git a/app/src/redux/robot-update/__tests__/hooks.test.tsx b/app/src/redux/robot-update/__tests__/hooks.test.tsx index 7528bf290bc..bf9cee3afbc 100644 --- a/app/src/redux/robot-update/__tests__/hooks.test.tsx +++ b/app/src/redux/robot-update/__tests__/hooks.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { vi, describe, it, expect, beforeEach } from 'vitest' import { createStore } from 'redux' import { renderHook } from '@testing-library/react' @@ -9,11 +8,12 @@ import { i18n } from '/app/i18n' import { useDispatchStartRobotUpdate } from '../hooks' import { startRobotUpdate, clearRobotUpdateSession } from '../actions' +import type { FunctionComponent, ReactNode } from 'react' import type { Store } from 'redux' import type { State } from '../../types' describe('useDispatchStartRobotUpdate', () => { - let wrapper: React.FunctionComponent<{ children: React.ReactNode }> + let wrapper: FunctionComponent<{ children: ReactNode }> let store: Store const mockRobotName = 'robotName' const mockSystemFile = 'systemFile' diff --git a/app/src/redux/robot-update/__tests__/selectors.test.ts b/app/src/redux/robot-update/__tests__/selectors.test.ts index e4f3e8f8283..079244b5f32 100644 --- a/app/src/redux/robot-update/__tests__/selectors.test.ts +++ b/app/src/redux/robot-update/__tests__/selectors.test.ts @@ -474,10 +474,10 @@ describe('robot update selectors', () => { expect(result).toMatchObject({ autoUpdateDisabledReason: expect.stringMatching( - /update server is not responding/ + /update_server_unavailable/ ), updateFromFileDisabledReason: expect.stringMatching( - /update server is not responding/ + /update_server_unavailable/ ), }) }) @@ -495,10 +495,10 @@ describe('robot update selectors', () => { expect(result).toMatchObject({ autoUpdateDisabledReason: expect.stringMatching( - /update server is not responding/ + /update_server_unavailable/ ), updateFromFileDisabledReason: expect.stringMatching( - /update server is not responding/ + /update_server_unavailable/ ), }) }) @@ -526,11 +526,9 @@ describe('robot update selectors', () => { const result = selectors.getRobotUpdateDisplayInfo(state, robotName) expect(result).toMatchObject({ - autoUpdateDisabledReason: expect.stringMatching( - /updating a different robot/ - ), + autoUpdateDisabledReason: expect.stringMatching(/other_robot_updating/), updateFromFileDisabledReason: expect.stringMatching( - /updating a different robot/ + /other_robot_updating/ ), }) }) @@ -547,9 +545,7 @@ describe('robot update selectors', () => { expect(result).toEqual({ autoUpdateAction: expect.stringMatching(/unavailable/i), - autoUpdateDisabledReason: expect.stringMatching( - /unable to retrieve update/i - ), + autoUpdateDisabledReason: expect.stringMatching(/no_update_files/i), updateFromFileDisabledReason: null, }) }) diff --git a/app/src/redux/robot-update/selectors.ts b/app/src/redux/robot-update/selectors.ts index 0570a9dd2c8..a2427dfefb5 100644 --- a/app/src/redux/robot-update/selectors.ts +++ b/app/src/redux/robot-update/selectors.ts @@ -19,15 +19,6 @@ import type { RobotUpdateTarget, } from './types' -// TODO(mc, 2020-08-02): i18n -const UPDATE_SERVER_UNAVAILABLE = - "Unable to update because your robot's update server is not responding." -const OTHER_ROBOT_UPDATING = - 'Unable to update because the app is currently updating a different robot.' -const NO_UPDATE_FILES = - 'Unable to retrieve update for this robot. Ensure your computer is connected to the internet and try again later.' -const UNAVAILABLE = 'Update unavailable' - export const getRobotUpdateTarget: ( state: State, robotName: string @@ -198,6 +189,7 @@ export function getRobotUpdateAvailable( : getRobotUpdateType(currentVersion, updateVersion) } +// this util returns i18n keys in device_settings export const getRobotUpdateDisplayInfo: ( state: State, robotName: string @@ -212,21 +204,21 @@ export const getRobotUpdateDisplayInfo: ( (robot, currentUpdatingRobot, updateVersion) => { const robotVersion = robot ? getRobotApiVersion(robot) : null const autoUpdateType = getRobotUpdateType(robotVersion, updateVersion) - const autoUpdateAction = autoUpdateType ?? UNAVAILABLE + const autoUpdateAction = autoUpdateType ?? 'update_unavailable' let autoUpdateDisabledReason = null let updateFromFileDisabledReason = null if (robot?.serverHealthStatus !== HEALTH_STATUS_OK) { - autoUpdateDisabledReason = UPDATE_SERVER_UNAVAILABLE - updateFromFileDisabledReason = UPDATE_SERVER_UNAVAILABLE + autoUpdateDisabledReason = 'update_server_unavailable' + updateFromFileDisabledReason = 'update_server_unavailable' } else if ( currentUpdatingRobot !== null && currentUpdatingRobot.name !== robot?.name ) { - autoUpdateDisabledReason = OTHER_ROBOT_UPDATING - updateFromFileDisabledReason = OTHER_ROBOT_UPDATING + autoUpdateDisabledReason = 'other_robot_updating' + updateFromFileDisabledReason = 'other_robot_updating' } else if (autoUpdateType === null) { - autoUpdateDisabledReason = NO_UPDATE_FILES + autoUpdateDisabledReason = 'no_update_files' } return { diff --git a/app/src/redux/shell/index.ts b/app/src/redux/shell/index.ts index d4f88d9a8c9..06f4199f081 100644 --- a/app/src/redux/shell/index.ts +++ b/app/src/redux/shell/index.ts @@ -5,5 +5,6 @@ export * from './selectors' export * from './update' export * from './is-ready/actions' export * from './is-ready/selectors' +export * from './types' export const CURRENT_VERSION: string = _PKG_VERSION_ diff --git a/app/src/redux/shell/types.ts b/app/src/redux/shell/types.ts index a3f887b4108..ca20ddab53e 100644 --- a/app/src/redux/shell/types.ts +++ b/app/src/redux/shell/types.ts @@ -1,4 +1,6 @@ import type { IpcMainEvent } from 'electron' +import type { UpdateFileInfo } from 'electron-updater' +import type { ReleaseNoteInfo } from 'builder-util-runtime' import type { Error } from '../types' import type { RobotSystemAction } from './is-ready/types' @@ -9,6 +11,8 @@ export interface Remote { on: (channel: string, listener: IpcListener) => void off: (channel: string, listener: IpcListener) => void } + /* The renderer process isn't allowed the file path for security reasons. */ + getFilePathFrom: (file: File) => Promise } export type IpcListener = ( @@ -31,16 +35,11 @@ export type NotifyBrokerResponses = NotifyRefetchData | NotifyUnsubscribeData export type NotifyNetworkError = 'ECONNFAILED' | 'ECONNREFUSED' export type NotifyResponseData = NotifyBrokerResponses | NotifyNetworkError -interface File { - sha512: string - url: string - [key: string]: unknown -} export interface UpdateInfo { version: string - files: File[] + files: UpdateFileInfo[] releaseDate?: string - releaseNotes?: string + releaseNotes?: string | null | ReleaseNoteInfo[] } export interface ShellUpdateState { diff --git a/app/src/redux/system-info/constants.ts b/app/src/redux/system-info/constants.ts index 1502d4bba07..a79b6ffb7bb 100644 --- a/app/src/redux/system-info/constants.ts +++ b/app/src/redux/system-info/constants.ts @@ -22,12 +22,3 @@ export const USB_DEVICE_REMOVED: 'systemInfo:USB_DEVICE_REMOVED' = export const NETWORK_INTERFACES_CHANGED: 'systemInfo:NETWORK_INTERFACES_CHANGED' = 'systemInfo:NETWORK_INTERFACES_CHANGED' - -// copy -// TODO(mc, 2020-05-11): i18n -export const U2E_DRIVER_OUTDATED_MESSAGE = - 'There is an updated Realtek USB-to-Ethernet adapter driver available for your computer.' -export const U2E_DRIVER_DESCRIPTION = - 'The OT-2 uses this adapter for its USB connection to the Opentrons App.' -export const U2E_DRIVER_OUTDATED_CTA = - "Please update your computer's driver to ensure a reliable connection to your OT-2." diff --git a/app/src/redux/types.ts b/app/src/redux/types.ts index d3f502cdc40..c87c2969e6b 100644 --- a/app/src/redux/types.ts +++ b/app/src/redux/types.ts @@ -119,3 +119,5 @@ export type Epic = ( ) => Observable export type Error = Partial<{ name: string; message: string }> + +export * from './shell/types' diff --git a/app/src/resources/analysis/hooks/__tests__/useStoredProtocolAnalysis.test.tsx b/app/src/resources/analysis/hooks/__tests__/useStoredProtocolAnalysis.test.tsx index 2437ed6e1a7..8bebe10c0e7 100644 --- a/app/src/resources/analysis/hooks/__tests__/useStoredProtocolAnalysis.test.tsx +++ b/app/src/resources/analysis/hooks/__tests__/useStoredProtocolAnalysis.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { vi, it, expect, describe, beforeEach, afterEach } from 'vitest' import { when } from 'vitest-when' import { QueryClient, QueryClientProvider } from 'react-query' @@ -25,6 +24,7 @@ import { } from '../__fixtures__/storedProtocolAnalysis' import { useNotifyRunQuery } from '/app/resources/runs' +import type { FunctionComponent, ReactNode } from 'react' import type { Store } from 'redux' import type { UseQueryResult } from 'react-query' import type { Protocol, Run } from '@opentrons/api-client' @@ -63,7 +63,7 @@ const PROTOCOL_ID = 'the_protocol_id' const PROTOCOL_KEY = 'the_protocol_key' describe('useStoredProtocolAnalysis hook', () => { - let wrapper: React.FunctionComponent<{ children: React.ReactNode }> + let wrapper: FunctionComponent<{ children: ReactNode }> beforeEach(() => { const queryClient = new QueryClient() wrapper = ({ children }) => ( diff --git a/app/src/resources/calibration/__tests__/useDeckCalibrationStatus.test.tsx b/app/src/resources/calibration/__tests__/useDeckCalibrationStatus.test.tsx index 768c4152ff2..3cf71852f01 100644 --- a/app/src/resources/calibration/__tests__/useDeckCalibrationStatus.test.tsx +++ b/app/src/resources/calibration/__tests__/useDeckCalibrationStatus.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { vi, it, expect, describe, beforeEach, afterEach } from 'vitest' import { when } from 'vitest-when' import { Provider } from 'react-redux' @@ -12,6 +11,8 @@ import { getDiscoverableRobotByName } from '/app/redux/discovery' import { useDeckCalibrationStatus } from '..' import { mockConnectableRobot } from '/app/redux/discovery/__fixtures__' + +import type { FunctionComponent, ReactNode } from 'react' import type { Store } from 'redux' vi.mock('@opentrons/react-api-client') @@ -21,7 +22,7 @@ vi.mock('/app/redux/discovery') const store: Store = createStore(vi.fn(), {}) describe('useDeckCalibrationStatus hook', () => { - let wrapper: React.FunctionComponent<{ children: React.ReactNode }> + let wrapper: FunctionComponent<{ children: ReactNode }> beforeEach(() => { const queryClient = new QueryClient() wrapper = ({ children }) => ( diff --git a/app/src/resources/deck_configuration/hooks.tsx b/app/src/resources/deck_configuration/hooks.tsx index 79a2e80124b..935c81a4cb2 100644 --- a/app/src/resources/deck_configuration/hooks.tsx +++ b/app/src/resources/deck_configuration/hooks.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useState } from 'react' import { getInitialAndMovedLabwareInSlots } from '@opentrons/components' import { FLEX_ROBOT_TYPE, @@ -15,6 +15,7 @@ import { SINGLE_SLOT_FIXTURES, } from '@opentrons/shared-data' +import type { ReactNode } from 'react' import type { CompletedProtocolAnalysis, CutoutConfigProtocolSpec, @@ -118,7 +119,7 @@ interface DeckConfigurationEditingTools { cutoutId: CutoutId, cutoutFixtureId: CutoutFixtureId ) => void - addFixtureModal: React.ReactNode + addFixtureModal: ReactNode } export function useDeckConfigurationEditingTools( isOnDevice: boolean @@ -129,9 +130,7 @@ export function useDeckConfigurationEditingTools( refetchInterval: DECK_CONFIG_REFETCH_INTERVAL, }).data ?? [] const { updateDeckConfiguration } = useUpdateDeckConfigurationMutation() - const [targetCutoutId, setTargetCutoutId] = React.useState( - null - ) + const [targetCutoutId, setTargetCutoutId] = useState(null) const addFixtureToCutout = (cutoutId: CutoutId): void => { setTargetCutoutId(cutoutId) diff --git a/app/src/resources/devices/hooks/__tests__/useLights.test.tsx b/app/src/resources/devices/hooks/__tests__/useLights.test.tsx index 7485a61f757..e190902f24b 100644 --- a/app/src/resources/devices/hooks/__tests__/useLights.test.tsx +++ b/app/src/resources/devices/hooks/__tests__/useLights.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { vi, it, expect, describe, beforeEach, afterEach } from 'vitest' import { Provider } from 'react-redux' import { createStore } from 'redux' @@ -11,6 +10,7 @@ import { import { useLights } from '../useLights' +import type { FunctionComponent, ReactNode } from 'react' import type { Store } from 'redux' import type { Mock } from 'vitest' @@ -19,7 +19,7 @@ vi.mock('@opentrons/react-api-client') const store: Store = createStore(vi.fn(), {}) describe('useLights hook', () => { - let wrapper: React.FunctionComponent<{ children: React.ReactNode }> + let wrapper: FunctionComponent<{ children: ReactNode }> let setLights: Mock beforeEach(() => { diff --git a/app/src/resources/instruments/__tests__/useAttachedPipetteCalibrations.test.tsx b/app/src/resources/instruments/__tests__/useAttachedPipetteCalibrations.test.tsx index ef5309a52d5..02316923bd3 100644 --- a/app/src/resources/instruments/__tests__/useAttachedPipetteCalibrations.test.tsx +++ b/app/src/resources/instruments/__tests__/useAttachedPipetteCalibrations.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { when } from 'vitest-when' import { vi, it, expect, describe, beforeEach } from 'vitest' import { Provider } from 'react-redux' @@ -21,6 +20,8 @@ import { } from '/app/redux/calibration/tip-length/__fixtures__' import { useAttachedPipetteCalibrations } from '..' + +import type { FunctionComponent, ReactNode } from 'react' import type { Store } from 'redux' import type { State } from '/app/redux/types' @@ -43,7 +44,7 @@ const PIPETTE_CALIBRATIONS = { } describe('useAttachedPipetteCalibrations hook', () => { - let wrapper: React.FunctionComponent<{ children: React.ReactNode }> + let wrapper: FunctionComponent<{ children: ReactNode }> beforeEach(() => { const queryClient = new QueryClient() wrapper = ({ children }) => ( diff --git a/app/src/resources/instruments/__tests__/useAttachedPipettes.test.tsx b/app/src/resources/instruments/__tests__/useAttachedPipettes.test.tsx index 35dbd023839..30f9922a1a7 100644 --- a/app/src/resources/instruments/__tests__/useAttachedPipettes.test.tsx +++ b/app/src/resources/instruments/__tests__/useAttachedPipettes.test.tsx @@ -8,7 +8,8 @@ import { pipetteResponseFixtureLeft, pipetteResponseFixtureRight, } from '@opentrons/api-client' -import type * as React from 'react' + +import type { FunctionComponent, ReactNode } from 'react' import type { UseQueryResult } from 'react-query' import type { FetchPipettesResponseBody } from '@opentrons/api-client' import type { PipetteModelSpecs } from '@opentrons/shared-data' @@ -17,7 +18,7 @@ vi.mock('@opentrons/react-api-client') vi.mock('@opentrons/shared-data') describe('useAttachedPipettes hook', () => { - let wrapper: React.FunctionComponent<{ children: React.ReactNode }> + let wrapper: FunctionComponent<{ children: ReactNode }> beforeEach(() => { vi.mocked(getPipetteModelSpecs).mockReturnValue({ name: 'mockName', diff --git a/app/src/resources/instruments/__tests__/useAttachedPipettesFromInstrumentsQuery.test.ts b/app/src/resources/instruments/__tests__/useAttachedPipettesFromInstrumentsQuery.test.ts index a0ae4757582..cc2bdbdc081 100644 --- a/app/src/resources/instruments/__tests__/useAttachedPipettesFromInstrumentsQuery.test.ts +++ b/app/src/resources/instruments/__tests__/useAttachedPipettesFromInstrumentsQuery.test.ts @@ -7,7 +7,7 @@ import { } from '@opentrons/api-client' import { useIsOEMMode } from '/app/resources/robot-settings/hooks' import { useAttachedPipettesFromInstrumentsQuery } from '..' -import type * as React from 'react' +import type { FunctionComponent, ReactNode } from 'react' vi.mock('@opentrons/react-api-client') vi.mock('/app/resources/robot-settings/hooks') @@ -17,7 +17,7 @@ describe('useAttachedPipettesFromInstrumentsQuery hook', () => { vi.mocked(useIsOEMMode).mockReturnValue(false) }) - let wrapper: React.FunctionComponent<{ children: React.ReactNode }> + let wrapper: FunctionComponent<{ children: ReactNode }> it('returns attached pipettes', () => { vi.mocked(useInstrumentsQuery).mockReturnValue({ data: { diff --git a/app/src/resources/instruments/__tests__/useTipAttachmentStatus.test.ts b/app/src/resources/instruments/__tests__/useTipAttachmentStatus.test.ts new file mode 100644 index 00000000000..6d0de5c6d05 --- /dev/null +++ b/app/src/resources/instruments/__tests__/useTipAttachmentStatus.test.ts @@ -0,0 +1,277 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { act, renderHook, waitFor } from '@testing-library/react' + +import { + getCommands, + getInstruments, + getRunCurrentState, +} from '@opentrons/api-client' +import { getPipetteModelSpecs } from '@opentrons/shared-data' +import { useHost } from '@opentrons/react-api-client' + +import { mockPipetteInfo } from '/app/redux/pipettes/__fixtures__' +import { useTipAttachmentStatus } from '../useTipAttachmentStatus' + +import type { PipetteModelSpecs } from '@opentrons/shared-data' +import type { PipetteData } from '@opentrons/api-client' + +vi.mock('@opentrons/shared-data', async importOriginal => { + const actual = await importOriginal() + return { + ...actual, + getPipetteModelSpecs: vi.fn(), + } +}) +vi.mock('@opentrons/api-client') +vi.mock('@opentrons/react-api-client') + +const MOCK_HOST = { ip: '1.2.3.4', port: 31950 } as any +const MOCK_RUN_ID = 'run-123' + +const MOCK_ACTUAL_PIPETTE = { + ...mockPipetteInfo.pipetteSpecs, + model: 'model', + tipLength: { + value: 20, + }, +} as PipetteModelSpecs + +const mockPipetteData: PipetteData = { + mount: 'left', + instrumentType: 'pipette', + instrumentModel: 'p1000_single_v3.6', + ok: true, +} as any + +const mockSecondPipetteData: PipetteData = { + ...mockPipetteData, + mount: 'right', +} + +const mockRunRecord = { + data: { + pipettes: [ + { id: 'pipette-1', mount: 'left' }, + { id: 'pipette-2', mount: 'right' }, + ], + }, +} as any + +const mockTipStates = { + 'pipette-1': { hasTip: true }, + 'pipette-2': { hasTip: true }, +} + +describe('useTipAttachmentStatus', () => { + beforeEach(() => { + vi.mocked(useHost).mockReturnValue(MOCK_HOST) + vi.mocked(getPipetteModelSpecs).mockReturnValue(MOCK_ACTUAL_PIPETTE) + + vi.mocked(getInstruments).mockResolvedValue({ + data: { data: [mockPipetteData, mockSecondPipetteData] }, + } as any) + + vi.mocked(getRunCurrentState).mockResolvedValue({ + data: { data: { tipStates: mockTipStates } }, + } as any) + + vi.mocked(getCommands).mockResolvedValue({ + data: { data: [{ commandType: 'mockType' }] }, + } as any) + }) + + const renderTipAttachmentStatus = () => { + return renderHook(() => + useTipAttachmentStatus({ + runId: MOCK_RUN_ID, + runRecord: mockRunRecord, + }) + ) + } + + it('should return the correct initial state', async () => { + const { result } = renderTipAttachmentStatus() + + await waitFor(() => { + expect(result.current).toBeDefined() + }) + + expect(result.current.areTipsAttached).toBe(false) + expect(result.current.aPipetteWithTip).toEqual(null) + expect(result.current.initialPipettesWithTipsCount).toEqual(null) + }) + + it('should determine tip status and update state accordingly', async () => { + const { result } = renderTipAttachmentStatus() + + await waitFor(() => { + expect(result.current).toBeDefined() + }) + + await act(async () => { + await result.current.determineTipStatus() + }) + + expect(result.current.areTipsAttached).toBe(true) + expect(result.current.aPipetteWithTip).toMatchObject({ + mount: 'left', + specs: MOCK_ACTUAL_PIPETTE, + }) + expect(result.current.initialPipettesWithTipsCount).toBe(2) + }) + + it('should handle network errors', async () => { + vi.mocked(getInstruments).mockRejectedValueOnce(new Error('Error')) + const { result } = renderTipAttachmentStatus() + + await waitFor(() => { + expect(result.current).toBeDefined() + }) + + await act(async () => { + await result.current.determineTipStatus() + }) + + expect(result.current.areTipsAttached).toBe(false) + expect(result.current.aPipetteWithTip).toBeNull() + }) + + it('should reset tip status', async () => { + const { result } = renderTipAttachmentStatus() + + await waitFor(() => { + expect(result.current).toBeDefined() + }) + + await act(async () => { + await result.current.determineTipStatus() + }) + + act(() => { + result.current.resetTipStatus() + }) + + expect(result.current.areTipsAttached).toBe(false) + expect(result.current.aPipetteWithTip).toEqual(null) + expect(result.current.initialPipettesWithTipsCount).toEqual(null) + }) + + it('should set tip status resolved and a state', async () => { + const { result } = renderTipAttachmentStatus() + + await waitFor(() => { + expect(result.current).toBeDefined() + }) + + await act(async () => { + await result.current.determineTipStatus() + }) + + expect(result.current.aPipetteWithTip).toMatchObject({ + mount: 'left', + specs: MOCK_ACTUAL_PIPETTE, + }) + + act(() => { + result.current.setTipStatusResolved() + }) + + await waitFor(() => + expect(result.current.aPipetteWithTip?.mount).toBe('right') + ) + }) + + it('should call onEmptyCache callback when cache becomes empty', async () => { + vi.mocked(getRunCurrentState).mockResolvedValueOnce({ + data: { + data: { + tipStates: { + 'pipette-1': { hasTip: true }, + 'pipette-2': { hasTip: false }, + }, + }, + }, + } as any) + + const onEmptyCacheMock = vi.fn() + const { result } = renderTipAttachmentStatus() + + await waitFor(() => { + expect(result.current).toBeDefined() + }) + + await act(async () => { + await result.current.determineTipStatus() + }) + + expect(result.current.aPipetteWithTip).toMatchObject({ + mount: 'left', + specs: MOCK_ACTUAL_PIPETTE, + }) + + act(() => { + result.current.setTipStatusResolved(onEmptyCacheMock) + }) + + await waitFor(() => { + expect(onEmptyCacheMock).toHaveBeenCalled() + }) + }) + + it('should handle tipPhysicallyMissing error by assuming tip is attached', async () => { + vi.mocked(getCommands).mockResolvedValueOnce({ + data: { + data: [ + { + error: { + errorType: 'tipPhysicallyMissing', + }, + }, + ], + }, + } as any) + + const { result } = renderTipAttachmentStatus() + + await waitFor(() => { + expect(result.current).toBeDefined() + }) + + await act(async () => { + await result.current.determineTipStatus() + }) + + expect(result.current.areTipsAttached).toBe(true) + }) + + it('should call onTipsDetected callback when tips remain after resolution', async () => { + const onTipsDetectedMock = vi.fn() + const { result } = renderTipAttachmentStatus() + + await waitFor(() => { + expect(result.current).toBeDefined() + }) + + await act(async () => { + await result.current.determineTipStatus() + }) + + expect(result.current.aPipetteWithTip).toMatchObject({ + mount: 'left', + specs: MOCK_ACTUAL_PIPETTE, + }) + + act(() => { + result.current.setTipStatusResolved(undefined, onTipsDetectedMock) + }) + + await waitFor(() => { + expect(onTipsDetectedMock).toHaveBeenCalled() + }) + + expect(result.current.aPipetteWithTip).toMatchObject({ + mount: 'right', + specs: MOCK_ACTUAL_PIPETTE, + }) + }) +}) diff --git a/app/src/resources/instruments/index.ts b/app/src/resources/instruments/index.ts index 16fae1ecad8..d88a2c7215f 100644 --- a/app/src/resources/instruments/index.ts +++ b/app/src/resources/instruments/index.ts @@ -1,3 +1,4 @@ export * from './useAttachedPipettes' export * from './useAttachedPipetteCalibrations' export * from './useAttachedPipettesFromInstrumentsQuery' +export * from './useTipAttachmentStatus' diff --git a/app/src/resources/instruments/useTipAttachmentStatus.ts b/app/src/resources/instruments/useTipAttachmentStatus.ts new file mode 100644 index 00000000000..b08d5ea5270 --- /dev/null +++ b/app/src/resources/instruments/useTipAttachmentStatus.ts @@ -0,0 +1,234 @@ +import { useState, useCallback } from 'react' +import head from 'lodash/head' + +import { useHost } from '@opentrons/react-api-client' +import { + getCommands, + getInstruments, + getRunCurrentState, +} from '@opentrons/api-client' +import { getPipetteModelSpecs } from '@opentrons/shared-data' + +import type { + HostConfig, + Mount, + PipetteData, + Run, + RunCommandSummary, +} from '@opentrons/api-client' +import type { PipetteModelSpecs } from '@opentrons/shared-data' + +export interface PipetteWithTip { + mount: Mount + specs: PipetteModelSpecs +} + +export interface PipetteTipState { + specs: PipetteModelSpecs | null + mount: Mount + hasTip: boolean +} + +export interface TipAttachmentStatusParams { + runId: string + runRecord: Run | null +} + +export interface TipAttachmentStatusResult { + /** Updates the pipettes with tip cache. Determine whether tips are likely attached on one or more pipettes, assuming + * tips are attached when there's uncertainty. + * + * NOTE: This function makes a few network requests on each invocation! + * */ + determineTipStatus: () => Promise + /* Whether tips are likely attached on *any* pipette. Typically called after determineTipStatus() */ + areTipsAttached: boolean + /* Resets the cached pipettes with tip statuses to null. */ + resetTipStatus: () => void + /** Removes the first element from the tip attached cache if present. + * @param {Function} onEmptyCache After removing the pipette from the cache, if the attached tip cache is empty, invoke this callback. + * @param {Function} onTipsDetected After removing the pipette from the cache, if the attached tip cache is not empty, invoke this callback. + * */ + setTipStatusResolved: ( + onEmptyCache?: () => void, + onTipsDetected?: () => void + ) => Promise + /* Relevant pipette information for a pipette with a tip attached. If both pipettes have tips attached, return the left pipette. */ + aPipetteWithTip: PipetteWithTip | null + /* The initial number of pipettes with tips. Null if there has been no tip check yet. */ + initialPipettesWithTipsCount: number | null +} + +// Returns various utilities for interacting with the cache of pipettes with tips attached. +export function useTipAttachmentStatus( + params: TipAttachmentStatusParams +): TipAttachmentStatusResult { + const { runId, runRecord } = params + const host = useHost() + const [pipettesWithTip, setPipettesWithTip] = useState([]) + const [initialPipettesCount, setInitialPipettesCount] = useState< + number | null + >(null) + + const aPipetteWithTip = head(pipettesWithTip) ?? null + const areTipsAttached = + pipettesWithTip.length > 0 && head(pipettesWithTip)?.specs != null + + const determineTipStatus = useCallback((): Promise => { + return Promise.all([ + getInstruments(host as HostConfig), + getRunCurrentState(host as HostConfig, runId), + getCommands(host as HostConfig, runId, { + includeFixitCommands: false, + pageLength: 1, + }), + ]) + .then(([attachedInstruments, currentState, commandsData]) => { + const { tipStates } = currentState.data.data + + const pipetteInfo = validatePipetteInfo( + attachedInstruments?.data.data as PipetteData[] + ) + + const pipetteInfoById = createPipetteInfoById(runRecord, pipetteInfo) + const pipettesWithTipsData = getPipettesWithTipsData( + // eslint-disable-next-line + tipStates, + pipetteInfoById, + commandsData.data.data as RunCommandSummary[] + ) + const pipettesWithTipAndSpecs = filterPipettesWithTips( + pipettesWithTipsData + ) + + setPipettesWithTip(pipettesWithTipAndSpecs) + + if (initialPipettesCount === null) { + setInitialPipettesCount(pipettesWithTipAndSpecs.length) + } + + return Promise.resolve(pipettesWithTipAndSpecs) + }) + .catch(e => { + console.error(`Error during tip status check: ${e.message}`) + return Promise.resolve([]) + }) + }, [host, initialPipettesCount, runId, runRecord]) + + const resetTipStatus = (): void => { + setPipettesWithTip([]) + setInitialPipettesCount(null) + } + + const setTipStatusResolved = ( + onEmptyCache?: () => void, + onTipsDetected?: () => void + ): Promise => { + return new Promise(resolve => { + setPipettesWithTip(prevPipettesWithTip => { + const newState = [...prevPipettesWithTip.slice(1)] + if (newState.length === 0) { + onEmptyCache?.() + } else { + onTipsDetected?.() + } + + resolve(newState[0]) + return newState + }) + }) + } + + return { + areTipsAttached, + determineTipStatus, + resetTipStatus, + aPipetteWithTip, + setTipStatusResolved, + initialPipettesWithTipsCount: initialPipettesCount, + } +} + +// Return good pipettes from instrument data. +const validatePipetteInfo = ( + attachedInstruments: PipetteData[] | null +): PipetteData[] => { + const goodPipetteInfo = + attachedInstruments?.filter( + instr => instr.instrumentType === 'pipette' && instr.ok + ) ?? null + + if (goodPipetteInfo == null) { + throw new Error( + 'Attached instrument pipettes differ from current state pipettes.' + ) + } + + return goodPipetteInfo +} + +// Associate pipette info with a pipette id. +const createPipetteInfoById = ( + runRecord: Run | null, + pipetteInfo: PipetteData[] +): Record => { + const pipetteInfoById: Record = {} + + runRecord?.data.pipettes.forEach(p => { + const pipetteInfoForThisPipette = pipetteInfo.find( + goodPipette => p.mount === goodPipette.mount + ) + if (pipetteInfoForThisPipette != null) { + pipetteInfoById[p.id] = pipetteInfoForThisPipette + } + }) + + return pipetteInfoById +} + +const getPipettesWithTipsData = ( + tipStates: Record, + pipetteInfoById: Record, + commands: RunCommandSummary[] +): PipetteTipState[] => { + return Object.entries(tipStates).map(([pipetteId, tipInfo]) => { + const pipetteInfo = pipetteInfoById[pipetteId] + const specs = getPipetteModelSpecs(pipetteInfo.instrumentModel) + return { + specs, + mount: pipetteInfo.mount, + hasTip: getMightHaveTipGivenCommands(Boolean(tipInfo.hasTip), commands), + } + }) +} + +const PICK_UP_TIP_COMMAND_TYPES: Array = [ + 'pickUpTip', +] as const + +// Sometimes, the robot and the tip status util have different ideas of when tips are attached. +// For example, if a pickUpTip command fails, the robot does not think a tip is attached. However, we want to be +// conservative and prompt drop tip wizard in case there are tips attached unexpectedly. +const getMightHaveTipGivenCommands = ( + hasTip: boolean, + commands: RunCommandSummary[] +): boolean => { + const lastRunProtocolCommand = commands[commands.length - 1] + + if ( + PICK_UP_TIP_COMMAND_TYPES.includes(lastRunProtocolCommand.commandType) || + lastRunProtocolCommand?.error?.errorType === 'tipPhysicallyMissing' + ) { + return true + } else { + return hasTip + } +} + +const filterPipettesWithTips = ( + pipettesWithTipsData: PipetteTipState[] +): PipetteWithTip[] => { + return pipettesWithTipsData.filter( + pipette => pipette.specs != null && pipette.hasTip + ) as PipetteWithTip[] +} diff --git a/app/src/resources/modules/hooks/__tests__/useAttachedModules.test.tsx b/app/src/resources/modules/hooks/__tests__/useAttachedModules.test.tsx index 49fba0eeaa0..6eec9e0996f 100644 --- a/app/src/resources/modules/hooks/__tests__/useAttachedModules.test.tsx +++ b/app/src/resources/modules/hooks/__tests__/useAttachedModules.test.tsx @@ -4,13 +4,14 @@ import { mockModulesResponse } from '@opentrons/api-client' import { useModulesQuery } from '@opentrons/react-api-client' import { useAttachedModules } from '..' +import type { FunctionComponent, ReactNode } from 'react' import type { UseQueryResult } from 'react-query' import type { Modules } from '@opentrons/api-client' vi.mock('@opentrons/react-api-client') describe('useAttachedModules hook', () => { - let wrapper: React.FunctionComponent<{ children: React.ReactNode }> + let wrapper: FunctionComponent<{ children: ReactNode }> it('returns attached modules', () => { vi.mocked(useModulesQuery).mockReturnValue({ diff --git a/app/src/resources/networking/hooks/__tests__/useCanDisconnect.test.tsx b/app/src/resources/networking/hooks/__tests__/useCanDisconnect.test.tsx index 3985dc36ca8..d8788bf7ee1 100644 --- a/app/src/resources/networking/hooks/__tests__/useCanDisconnect.test.tsx +++ b/app/src/resources/networking/hooks/__tests__/useCanDisconnect.test.tsx @@ -1,18 +1,18 @@ -import type * as React from 'react' import { when } from 'vitest-when' import { describe, it, expect, vi, beforeEach } from 'vitest' import { createStore } from 'redux' import { Provider } from 'react-redux' -import { SECURITY_WPA_EAP } from '@opentrons/api-client' import { renderHook } from '@testing-library/react' +import { SECURITY_WPA_EAP } from '@opentrons/api-client' import { getRobotApiVersionByName } from '/app/redux/discovery' import { useIsFlex } from '/app/redux-resources/robots' import { useCanDisconnect } from '../useCanDisconnect' import { useWifiList } from '../useWifiList' -import type { WifiNetwork } from '@opentrons/api-client' +import type { FunctionComponent, ReactNode } from 'react' import type { Store } from 'redux' +import type { WifiNetwork } from '@opentrons/api-client' import type { State } from '/app/redux/types' vi.mock('../useWifiList') @@ -21,9 +21,9 @@ vi.mock('/app/redux/discovery') const store: Store = createStore(state => state, {}) -const wrapper: React.FunctionComponent<{ children: React.ReactNode }> = ({ - children, -}) => {children} +const wrapper: FunctionComponent<{ children: ReactNode }> = ({ children }) => ( + {children} +) const mockWifiNetwork: WifiNetwork = { ssid: 'linksys', diff --git a/app/src/resources/networking/hooks/__tests__/useNetworkConnection.test.tsx b/app/src/resources/networking/hooks/__tests__/useNetworkConnection.test.tsx index 2c0e6257e42..9335c7eafea 100644 --- a/app/src/resources/networking/hooks/__tests__/useNetworkConnection.test.tsx +++ b/app/src/resources/networking/hooks/__tests__/useNetworkConnection.test.tsx @@ -1,5 +1,3 @@ -import type * as React from 'react' - import { when } from 'vitest-when' import { describe, it, expect, vi, beforeEach } from 'vitest' import { Provider } from 'react-redux' @@ -14,6 +12,8 @@ import * as Fixtures from '/app/redux/networking/__fixtures__' import { getNetworkInterfaces } from '/app/redux/networking' import { useNetworkConnection } from '../useNetworkConnection' + +import type { FunctionComponent, ReactNode } from 'react' import type { Store } from 'redux' vi.mock('/app/redux/networking/selectors') @@ -48,7 +48,7 @@ const store: Store = createStore(vi.fn(), {}) // ToDo (kj:0202/2023) USB test cases will be added when USB is out describe('useNetworkConnection', () => { - let wrapper: React.FunctionComponent<{ children: React.ReactNode }> + let wrapper: FunctionComponent<{ children: ReactNode }> beforeEach(() => { wrapper = ({ children }) => ( diff --git a/app/src/resources/protocols/hooks/__tests__/useProtocolMetadata.test.tsx b/app/src/resources/protocols/hooks/__tests__/useProtocolMetadata.test.tsx index 48ced67d25f..bea57c80284 100644 --- a/app/src/resources/protocols/hooks/__tests__/useProtocolMetadata.test.tsx +++ b/app/src/resources/protocols/hooks/__tests__/useProtocolMetadata.test.tsx @@ -1,5 +1,4 @@ // tests for the HostConfig context and hook -import type * as React from 'react' import { vi, it, expect, describe, beforeEach, afterEach } from 'vitest' import { when } from 'vitest-when' import { Provider } from 'react-redux' @@ -8,6 +7,7 @@ import { renderHook } from '@testing-library/react' import { useCurrentProtocol } from '../useCurrentProtocol' import { useProtocolMetadata } from '../useProtocolMetadata' +import type { FunctionComponent, ReactNode } from 'react' import type { Store } from 'redux' import type { State } from '/app/redux/types' @@ -39,7 +39,7 @@ describe('useProtocolMetadata', () => { }) it('should return author, lastUpdated, method, description, and robot type', () => { - const wrapper: React.FunctionComponent<{ children: React.ReactNode }> = ({ + const wrapper: FunctionComponent<{ children: ReactNode }> = ({ children, }) => {children} const { result } = renderHook(useProtocolMetadata, { wrapper }) diff --git a/app/src/resources/runs/__tests__/useCloneRun.test.tsx b/app/src/resources/runs/__tests__/useCloneRun.test.tsx index 9323bbf8073..cf9de675f67 100644 --- a/app/src/resources/runs/__tests__/useCloneRun.test.tsx +++ b/app/src/resources/runs/__tests__/useCloneRun.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { when } from 'vitest-when' import { renderHook } from '@testing-library/react' import { QueryClient, QueryClientProvider } from 'react-query' @@ -13,6 +12,7 @@ import { import { useCloneRun } from '../useCloneRun' import { useNotifyRunQuery } from '../useNotifyRunQuery' +import type { FunctionComponent, ReactNode } from 'react' import type { HostConfig } from '@opentrons/api-client' vi.mock('@opentrons/react-api-client') @@ -23,7 +23,7 @@ const RUN_ID_NO_RTP: string = 'run_id_no_rtp' const RUN_ID_RTP: string = 'run_id_rtp' describe('useCloneRun hook', () => { - let wrapper: React.FunctionComponent<{ children: React.ReactNode }> + let wrapper: FunctionComponent<{ children: ReactNode }> beforeEach(() => { when(vi.mocked(useHost)).calledWith().thenReturn(HOST_CONFIG) @@ -90,8 +90,8 @@ describe('useCloneRun hook', () => { } as any) const queryClient = new QueryClient() - const clientProvider: React.FunctionComponent<{ - children: React.ReactNode + const clientProvider: FunctionComponent<{ + children: ReactNode }> = ({ children }) => ( {children} ) diff --git a/app/src/resources/runs/__tests__/useLPCDisabledReason.test.tsx b/app/src/resources/runs/__tests__/useLPCDisabledReason.test.tsx index 0be19bc19d3..349804ac58b 100644 --- a/app/src/resources/runs/__tests__/useLPCDisabledReason.test.tsx +++ b/app/src/resources/runs/__tests__/useLPCDisabledReason.test.tsx @@ -1,13 +1,14 @@ -import type * as React from 'react' import { renderHook } from '@testing-library/react' import { Provider } from 'react-redux' import { I18nextProvider } from 'react-i18next' import { createStore } from 'redux' import { vi, it, expect, describe, beforeEach, afterEach } from 'vitest' + import { getLoadedLabwareDefinitionsByUri, simple_v6 as _uncastedSimpleV6Protocol, } from '@opentrons/shared-data' + import { i18n } from '/app/i18n' import { RUN_ID_1 } from '..//__fixtures__' import { useStoredProtocolAnalysis } from '/app/resources/analysis' @@ -17,6 +18,7 @@ import { useRunCalibrationStatus } from '../useRunCalibrationStatus' import { useMostRecentCompletedAnalysis } from '../useMostRecentCompletedAnalysis' import { useRunHasStarted } from '../useRunHasStarted' +import type { FunctionComponent, ReactNode } from 'react' import type { Store } from 'redux' import type * as SharedData from '@opentrons/shared-data' import type { State } from '/app/redux/types' @@ -38,7 +40,7 @@ const simpleV6Protocol = (_uncastedSimpleV6Protocol as unknown) as SharedData.Pr describe('useLPCDisabledReason', () => { const store: Store = createStore(vi.fn(), {}) - const wrapper: React.FunctionComponent<{ children: React.ReactNode }> = ({ + const wrapper: FunctionComponent<{ children: ReactNode }> = ({ children, }) => ( diff --git a/app/src/resources/runs/__tests__/useModuleCalibrationStatus.test.tsx b/app/src/resources/runs/__tests__/useModuleCalibrationStatus.test.tsx index 5691230dbaf..fef1217b173 100644 --- a/app/src/resources/runs/__tests__/useModuleCalibrationStatus.test.tsx +++ b/app/src/resources/runs/__tests__/useModuleCalibrationStatus.test.tsx @@ -1,4 +1,5 @@ -import type * as React from 'react' +import { Provider } from 'react-redux' +import { createStore } from 'redux' import { QueryClient, QueryClientProvider } from 'react-query' import { renderHook } from '@testing-library/react' import { vi, it, expect, describe, beforeEach, afterEach } from 'vitest' @@ -10,15 +11,13 @@ import { useIsFlex } from '/app/redux-resources/robots' import { mockMagneticModuleGen2 } from '/app/redux/modules/__fixtures__' +import type { FunctionComponent, ReactNode } from 'react' import type { ModuleModel, ModuleType } from '@opentrons/shared-data' -import { Provider } from 'react-redux' -import { createStore } from 'redux' - vi.mock('/app/redux-resources/robots') vi.mock('../useModuleRenderInfoForProtocolById') -let wrapper: React.FunctionComponent<{ children: React.ReactNode }> +let wrapper: FunctionComponent<{ children: ReactNode }> const mockMagneticModuleDefinition = { moduleId: 'someMagneticModule', diff --git a/app/src/resources/runs/__tests__/useRunCalibrationStatus.test.tsx b/app/src/resources/runs/__tests__/useRunCalibrationStatus.test.tsx index d0a81ff3e50..a93a3fe5d7a 100644 --- a/app/src/resources/runs/__tests__/useRunCalibrationStatus.test.tsx +++ b/app/src/resources/runs/__tests__/useRunCalibrationStatus.test.tsx @@ -1,10 +1,11 @@ -import type * as React from 'react' +import { Provider } from 'react-redux' +import { createStore } from 'redux' import { QueryClient, QueryClientProvider } from 'react-query' import { renderHook } from '@testing-library/react' import { vi, it, expect, describe, beforeEach } from 'vitest' import { when } from 'vitest-when' -import { mockTipRackDefinition } from '/app/redux/custom-labware/__fixtures__' +import { mockTipRackDefinition } from '/app/redux/custom-labware/__fixtures__' import { useRunCalibrationStatus, useRunPipetteInfoByMount, @@ -13,9 +14,8 @@ import { import { useDeckCalibrationStatus } from '/app/resources/calibration' import { useIsFlex } from '/app/redux-resources/robots' +import type { FunctionComponent, ReactNode } from 'react' import type { PipetteInfo } from '/app/redux/pipettes' -import { Provider } from 'react-redux' -import { createStore } from 'redux' vi.mock('../useRunPipetteInfoByMount') vi.mock('../useNotifyRunQuery') @@ -23,7 +23,7 @@ vi.mock('/app/resources/calibration') vi.mock('/app/resources/analysis') vi.mock('/app/redux-resources/robots') -let wrapper: React.FunctionComponent<{ children: React.ReactNode }> +let wrapper: FunctionComponent<{ children: ReactNode }> describe('useRunCalibrationStatus hook', () => { beforeEach(() => { diff --git a/app/src/resources/runs/__tests__/useRunLoadedLabwareDefintionsByUri.test.ts b/app/src/resources/runs/__tests__/useRunLoadedLabwareDefintionsByUri.test.ts new file mode 100644 index 00000000000..b1a7420d8c7 --- /dev/null +++ b/app/src/resources/runs/__tests__/useRunLoadedLabwareDefintionsByUri.test.ts @@ -0,0 +1,31 @@ +import { describe, expect, it, beforeEach, vi } from 'vitest' +import { renderHook } from '@testing-library/react' + +import { useRunLoadedLabwareDefinitions } from '@opentrons/react-api-client' +import { fixture96Plate } from '@opentrons/shared-data' + +import { useRunLoadedLabwareDefinitionsByUri } from '/app/resources/runs' + +import type { LabwareDefinition2 } from '@opentrons/shared-data' + +vi.mock('@opentrons/react-api-client') + +const mockLabwareDef = fixture96Plate as LabwareDefinition2 + +describe('useRunLoadedLabwareDefinitionsByUri', () => { + beforeEach(() => { + vi.mocked(useRunLoadedLabwareDefinitions).mockReturnValue({ + data: { data: [mockLabwareDef] }, + } as any) + }) + + it('returns a record of labware definitions keyed by URI', () => { + const { result } = renderHook(() => + useRunLoadedLabwareDefinitionsByUri('mockId') + ) + + expect(result.current).toEqual({ + 'fixture/fixture_96_plate/1': mockLabwareDef, + }) + }) +}) diff --git a/app/src/resources/runs/index.ts b/app/src/resources/runs/index.ts index d1d84d3bc1b..18a48fd533e 100644 --- a/app/src/resources/runs/index.ts +++ b/app/src/resources/runs/index.ts @@ -30,3 +30,4 @@ export * from './useProtocolAnalysisErrors' export * from './useLastRunCommand' export * from './useRunStatuses' export * from './useUpdateRecoveryPolicyWithStrategy' +export * from './useRunLoadedLabwareDefinitionsByUri' diff --git a/app/src/resources/runs/useRunLoadedLabwareDefinitionsByUri.ts b/app/src/resources/runs/useRunLoadedLabwareDefinitionsByUri.ts new file mode 100644 index 00000000000..9a785cb99df --- /dev/null +++ b/app/src/resources/runs/useRunLoadedLabwareDefinitionsByUri.ts @@ -0,0 +1,46 @@ +import { useMemo } from 'react' + +import { useRunLoadedLabwareDefinitions } from '@opentrons/react-api-client' +import { getLabwareDefURI } from '@opentrons/shared-data' + +import type { UseQueryOptions } from 'react-query' +import type { AxiosError } from 'axios' +import type { + HostConfig, + RunLoadedLabwareDefinitions, +} from '@opentrons/api-client' +import type { LabwareDefinition2 } from '@opentrons/shared-data' + +export type RunLoadedLabwareDefinitionsByUri = Record< + string, + LabwareDefinition2 +> + +// Returns a record of labware definitions keyed by URI for the labware that +// has been loaded with a "loadLabware" command. Errors if the run is not the current run. +// Returns null if the network request is pending. +export function useRunLoadedLabwareDefinitionsByUri( + runId: string | null, + options: UseQueryOptions = {}, + hostOverride?: HostConfig +): RunLoadedLabwareDefinitionsByUri | null { + const { data } = useRunLoadedLabwareDefinitions(runId, options, hostOverride) + + return useMemo(() => { + const result: Record = {} + + if (data == null) { + return null + } else { + // @ts-expect-error TODO(jh, 10-12-24): Update the app's typing to support LabwareDefinition3. + data?.data.forEach((def: LabwareDefinition2) => { + if ('schemaVersion' in def) { + const lwUri = getLabwareDefURI(def) + result[lwUri] = def + } + }) + + return result + } + }, [data]) +} diff --git a/app/src/resources/runs/utils.ts b/app/src/resources/runs/utils.ts index d9b6781f4b2..1ab7c205158 100644 --- a/app/src/resources/runs/utils.ts +++ b/app/src/resources/runs/utils.ts @@ -1,6 +1,6 @@ import { format } from 'date-fns' -import type * as React from 'react' +import type { Dispatch, SetStateAction } from 'react' import type { UseMutateAsyncFunction } from 'react-query' import type { CommandData } from '@opentrons/api-client' import type { CreateCommand } from '@opentrons/shared-data' @@ -11,7 +11,7 @@ export const chainRunCommandsRecursive = ( commands: CreateCommand[], createRunCommand: CreateRunCommand, continuePastCommandFailure: boolean = true, - setIsLoading: React.Dispatch> + setIsLoading: Dispatch> ): Promise => { if (commands.length < 1) { return Promise.reject(new Error('no commands to execute')) @@ -57,7 +57,7 @@ export const chainLiveCommandsRecursive = ( CreateLiveCommandMutateParams >, continuePastCommandFailure: boolean = true, - setIsLoading: React.Dispatch> + setIsLoading: Dispatch> ): Promise => { if (commands.length < 1) { return Promise.reject(new Error('no commands to execute')) @@ -100,7 +100,7 @@ export const chainMaintenanceCommandsRecursive = ( commands: CreateCommand[], createMaintenanceCommand: CreateMaintenanceCommand, continuePastCommandFailure: boolean = true, - setIsLoading: React.Dispatch> + setIsLoading: Dispatch> ): Promise => { if (commands.length < 1) { return Promise.reject(new Error('no commands to execute')) diff --git a/app/src/transformations/commands/hooks/__tests__/useMissingProtocolHardware.test.tsx b/app/src/transformations/commands/hooks/__tests__/useMissingProtocolHardware.test.tsx index 99ce33a9de3..54fd39d607f 100644 --- a/app/src/transformations/commands/hooks/__tests__/useMissingProtocolHardware.test.tsx +++ b/app/src/transformations/commands/hooks/__tests__/useMissingProtocolHardware.test.tsx @@ -1,8 +1,7 @@ import omitBy from 'lodash/omitBy' import { vi, it, describe, expect, beforeEach, afterEach } from 'vitest' -import type { UseQueryResult } from 'react-query' import { renderHook } from '@testing-library/react' -import type { Protocol } from '@opentrons/api-client' + import { useProtocolQuery, useProtocolAnalysisAsDocumentQuery, @@ -14,14 +13,19 @@ import { WASTE_CHUTE_RIGHT_ADAPTER_NO_COVER_FIXTURE, fixtureTiprack300ul, } from '@opentrons/shared-data' + +import { useNotifyDeckConfigurationQuery } from '/app/resources/deck_configuration/useNotifyDeckConfigurationQuery' +import { useMissingProtocolHardware } from '../useMissingProtocolHardware' +import { mockHeaterShaker } from '/app/redux/modules/__fixtures__' + +import type { FunctionComponent, ReactNode } from 'react' +import type { UseQueryResult } from 'react-query' +import type { Protocol } from '@opentrons/api-client' import type { CompletedProtocolAnalysis, DeckConfiguration, LabwareDefinition2, } from '@opentrons/shared-data' -import { useNotifyDeckConfigurationQuery } from '/app/resources/deck_configuration/useNotifyDeckConfigurationQuery' -import { useMissingProtocolHardware } from '../useMissingProtocolHardware' -import { mockHeaterShaker } from '/app/redux/modules/__fixtures__' vi.mock('@opentrons/react-api-client') vi.mock('/app/resources/deck_configuration/useNotifyDeckConfigurationQuery') @@ -161,7 +165,7 @@ const PROTOCOL_ANALYSIS = { runTimeParameters: mockRTPData, } as any describe.only('useMissingProtocolHardware', () => { - let wrapper: React.FunctionComponent<{ children: React.ReactNode }> + let wrapper: FunctionComponent<{ children: ReactNode }> beforeEach(() => { vi.mocked(useInstrumentsQuery).mockReturnValue({ data: { data: [] }, diff --git a/app/src/transformations/commands/transformations/getLabwareSetupItemGroups.ts b/app/src/transformations/commands/transformations/getLabwareSetupItemGroups.ts index ebc31e37ccc..10e255470ec 100644 --- a/app/src/transformations/commands/transformations/getLabwareSetupItemGroups.ts +++ b/app/src/transformations/commands/transformations/getLabwareSetupItemGroups.ts @@ -27,18 +27,6 @@ export interface GroupedLabwareSetupItems { export function getLabwareSetupItemGroups( commands: RunTimeCommand[] ): GroupedLabwareSetupItems { - let beyondInitialLoadCommands = false - - const LABWARE_ACCESS_COMMAND_TYPES = [ - 'moveToWell', - 'aspirate', - 'dispense', - 'blowout', - 'pickUpTip', - 'dropTip', - 'touchTip', - ] - const [offDeckItems, onDeckItems] = partition( commands.reduce((acc, c) => { if ( @@ -77,12 +65,7 @@ export function getLabwareSetupItemGroups( return [ ...acc, { - // NOTE: for the purposes of the labware setup step, anything loaded after - // the initial load commands will be treated as "initially off deck" - // even if technically loaded directly onto the deck later in the protocol - initialLocation: beyondInitialLoadCommands - ? 'offDeck' - : c.params.location, + initialLocation: c.params.location, definition, moduleModel, moduleLocation, @@ -90,17 +73,7 @@ export function getLabwareSetupItemGroups( labwareId: c.result?.labwareId, }, ] - } else if ( - !beyondInitialLoadCommands && - LABWARE_ACCESS_COMMAND_TYPES.includes(c.commandType) && - !( - c.commandType === 'moveLabware' && - c.params.strategy === 'manualMoveWithoutPause' - ) - ) { - beyondInitialLoadCommands = true } - return acc }, []), ({ initialLocation }) => initialLocation === 'offDeck' diff --git a/app/tsconfig.json b/app/tsconfig.json index 92921aa2b1c..1a24f98b959 100644 --- a/app/tsconfig.json +++ b/app/tsconfig.json @@ -35,7 +35,7 @@ "outDir": "lib", "paths": { "/app/*": ["./src/*"] - } + }, }, "include": ["typings", "src"], "exclude": ["**/*.stories.tsx"] diff --git a/app/vite.config.mts b/app/vite.config.mts index f10fedf4f7e..4828283b107 100644 --- a/app/vite.config.mts +++ b/app/vite.config.mts @@ -19,6 +19,7 @@ export default defineConfig( build: { // Relative to the root outDir: 'dist', + sourcemap: true }, plugins: [ react({ diff --git a/components/.npmignore b/components/.npmignore index b8cffed3c08..e69de29bb2d 100644 --- a/components/.npmignore +++ b/components/.npmignore @@ -1,3 +0,0 @@ -src -dist -*.tgz diff --git a/components/README.md b/components/README.md index 03680fccf37..be6918d11ae 100644 --- a/components/README.md +++ b/components/README.md @@ -18,7 +18,7 @@ export default function CowButton(props) { Usage requirements for dependent projects: -- Node v18 and yarn +- Node v22.11.0+ and yarn - The following `dependencies` (peer dependencies of `@opentrons/components`) - `react`: `17.0.1`, - `react-router-dom`: `^4.2.2`, diff --git a/components/package.json b/components/package.json index 7d63ed25e44..c8d9292c6e3 100644 --- a/components/package.json +++ b/components/package.json @@ -5,7 +5,7 @@ "source": "src/index.ts", "types": "lib/index.d.ts", "style": "src/index.module.css", - "main": "lib/index.mjs", + "main": "src/index.ts", "module": "src/index.ts", "repository": { "type": "git", diff --git a/components/src/alerts/AlertItem.tsx b/components/src/alerts/AlertItem.tsx index 708f44f746c..4ef5eb04a8c 100644 --- a/components/src/alerts/AlertItem.tsx +++ b/components/src/alerts/AlertItem.tsx @@ -1,9 +1,9 @@ -import type * as React from 'react' import cx from 'classnames' import { Icon } from '../icons' import { IconButton } from '../buttons' import styles from './alerts.module.css' +import type { ReactNode } from 'react' import type { IconProps } from '../icons' export type AlertType = 'success' | 'warning' | 'error' | 'info' @@ -12,9 +12,9 @@ export interface AlertItemProps { /** name constant of the icon to display */ type: AlertType /** title/main message of colored alert bar */ - title: React.ReactNode + title: ReactNode /** Alert message body contents */ - children?: React.ReactNode + children?: ReactNode /** Additional class name */ className?: string /** optional handler to show close button/clear alert */ diff --git a/components/src/atoms/Checkbox/__tests__/Checkbox.test.tsx b/components/src/atoms/Checkbox/__tests__/Checkbox.test.tsx index cfee4c22835..1bc8265ad4f 100644 --- a/components/src/atoms/Checkbox/__tests__/Checkbox.test.tsx +++ b/components/src/atoms/Checkbox/__tests__/Checkbox.test.tsx @@ -1,16 +1,17 @@ -import type * as React from 'react' import { describe, beforeEach, afterEach, vi, expect, it } from 'vitest' import { fireEvent, screen } from '@testing-library/react' import '@testing-library/jest-dom/vitest' import { renderWithProviders } from '../../../testing/utils' import { Checkbox } from '..' -const render = (props: React.ComponentProps) => { +import type { ComponentProps } from 'react' + +const render = (props: ComponentProps) => { return renderWithProviders()[0] } describe('Checkbox', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { diff --git a/components/src/atoms/Checkbox/index.tsx b/components/src/atoms/Checkbox/index.tsx index 02fa36da6d4..c5763665351 100644 --- a/components/src/atoms/Checkbox/index.tsx +++ b/components/src/atoms/Checkbox/index.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { css } from 'styled-components' import { COLORS, BORDERS } from '../../helix-design-system' import { Flex } from '../../primitives' @@ -13,7 +12,8 @@ import { } from '../../styles' import { RESPONSIVENESS, SPACING } from '../../ui-style-constants' import { StyledText } from '../StyledText' -import { truncateString } from '../../utils' + +import type { MouseEventHandler } from 'react' export interface CheckboxProps { /** checkbox is checked if value is true */ @@ -21,7 +21,7 @@ export interface CheckboxProps { /** label text that describes the option */ labelText: string /** callback click/tap handler */ - onClick: React.MouseEventHandler + onClick: MouseEventHandler /** html tabindex property */ tabIndex?: number /** if disabled is true, mouse events will not trigger onClick callback */ @@ -41,7 +41,6 @@ export function Checkbox(props: CheckboxProps): JSX.Element { width = FLEX_MAX_CONTENT, type = 'round', } = props - const truncatedLabel = truncateString(labelText, 25) const CHECKBOX_STYLE = css` width: ${width}; @@ -50,7 +49,7 @@ export function Checkbox(props: CheckboxProps): JSX.Element { align-items: ${ALIGN_CENTER}; flex-direction: ${DIRECTION_ROW}; color: ${isChecked ? COLORS.white : COLORS.black90}; - background-color: ${isChecked ? COLORS.blue50 : COLORS.blue35}; + background-color: ${isChecked ? COLORS.blue50 : COLORS.blue30}; border-radius: ${type === 'round' ? BORDERS.borderRadiusFull : BORDERS.borderRadius8}; @@ -70,6 +69,9 @@ export function Checkbox(props: CheckboxProps): JSX.Element { background-color: ${COLORS.grey35}; color: ${COLORS.grey50}; } + &:hover { + background-color: ${isChecked ? COLORS.blue55 : COLORS.blue35}; + } @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { padding: ${SPACING.spacing20}; @@ -89,7 +91,7 @@ export function Checkbox(props: CheckboxProps): JSX.Element { css={CHECKBOX_STYLE} > - {truncatedLabel} + {labelText} @@ -99,9 +101,10 @@ export function Checkbox(props: CheckboxProps): JSX.Element { interface CheckProps { isChecked: boolean color?: string + disabled?: boolean } export function Check(props: CheckProps): JSX.Element { - const { isChecked, color = COLORS.white } = props + const { isChecked, color = COLORS.white, disabled = false } = props return isChecked ? ( @@ -109,7 +112,7 @@ export function Check(props: CheckProps): JSX.Element { ) : ( ) diff --git a/components/src/atoms/CheckboxField/__tests__/CheckboxField.test.tsx b/components/src/atoms/CheckboxField/__tests__/CheckboxField.test.tsx index 06fc3153d9e..b3b0e561711 100644 --- a/components/src/atoms/CheckboxField/__tests__/CheckboxField.test.tsx +++ b/components/src/atoms/CheckboxField/__tests__/CheckboxField.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { describe, beforeEach, afterEach, vi, expect, it } from 'vitest' import { fireEvent, screen } from '@testing-library/react' import '@testing-library/jest-dom/vitest' @@ -8,12 +7,14 @@ import { BORDERS, COLORS } from '../../../helix-design-system' import { TYPOGRAPHY, SPACING } from '../../../ui-style-constants' import { CheckboxField } from '..' -const render = (props: React.ComponentProps) => { +import type { ComponentProps } from 'react' + +const render = (props: ComponentProps) => { return renderWithProviders()[0] } describe('CheckboxField', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { diff --git a/components/src/atoms/CheckboxField/index.tsx b/components/src/atoms/CheckboxField/index.tsx index 9f02ef52bc0..1be78e36c1a 100644 --- a/components/src/atoms/CheckboxField/index.tsx +++ b/components/src/atoms/CheckboxField/index.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { css } from 'styled-components' import { SPACING, TYPOGRAPHY } from '../../ui-style-constants' import { COLORS, BORDERS } from '../../helix-design-system' @@ -11,21 +10,23 @@ import { JUSTIFY_CENTER, } from '../../styles' +import type { ChangeEventHandler, ComponentProps, ReactNode } from 'react' + export interface CheckboxFieldProps { /** change handler */ - onChange: React.ChangeEventHandler + onChange: ChangeEventHandler /** checkbox is checked if value is true */ value?: boolean /** name of field in form */ name?: string /** label text for checkbox */ - label?: React.ReactNode + label?: ReactNode /** checkbox is disabled if value is true */ disabled?: boolean /** html tabindex property */ tabIndex?: number /** props passed into label div. TODO IMMEDIATELY what is the Flow type? */ - labelProps?: React.ComponentProps<'div'> + labelProps?: ComponentProps<'div'> /** if true, render indeterminate icon */ isIndeterminate?: boolean } diff --git a/components/src/atoms/Chip/__tests__/Chip.test.tsx b/components/src/atoms/Chip/__tests__/Chip.test.tsx index 65de215160f..9fb190e665b 100644 --- a/components/src/atoms/Chip/__tests__/Chip.test.tsx +++ b/components/src/atoms/Chip/__tests__/Chip.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { describe, it, expect, beforeEach } from 'vitest' import { screen } from '@testing-library/react' import { BORDERS, COLORS } from '../../../helix-design-system' @@ -6,12 +5,14 @@ import { SPACING } from '../../../ui-style-constants' import { renderWithProviders } from '../../../testing/utils' import { Chip } from '..' -const render = (props: React.ComponentProps) => { +import type { ComponentProps } from 'react' + +const render = (props: ComponentProps) => { return renderWithProviders() } describe('Chip Touchscreen', () => { - let props: React.ComponentProps + let props: ComponentProps it('should render text, icon, bgcolor with success colors', () => { props = { @@ -214,7 +215,7 @@ describe('Chip Touchscreen', () => { }) describe('Chip Web', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { Object.defineProperty(window, 'innerWidth', { diff --git a/components/src/atoms/Divider/index.tsx b/components/src/atoms/Divider/index.tsx index 7de6a3757f4..df9a9e9af07 100644 --- a/components/src/atoms/Divider/index.tsx +++ b/components/src/atoms/Divider/index.tsx @@ -1,7 +1,8 @@ -import type * as React from 'react' import { Box, COLORS, SPACING } from '../..' -type Props = React.ComponentProps +import type { ComponentProps } from 'react' + +type Props = ComponentProps export function Divider(props: Props): JSX.Element { return ( diff --git a/components/src/atoms/InputField/__tests__/InputField.test.tsx b/components/src/atoms/InputField/__tests__/InputField.test.tsx index f53d4f4163a..87b3e752272 100644 --- a/components/src/atoms/InputField/__tests__/InputField.test.tsx +++ b/components/src/atoms/InputField/__tests__/InputField.test.tsx @@ -1,15 +1,16 @@ -import type * as React from 'react' import { describe, it, vi, beforeEach, expect } from 'vitest' import { screen, fireEvent } from '@testing-library/react' import { renderWithProviders } from '../../../testing/utils' import { InputField } from '..' -const render = (props: React.ComponentProps) => { +import type { ComponentProps } from 'react' + +const render = (props: ComponentProps) => { return renderWithProviders()[0] } describe('HeaterShakerSlideout', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { type: 'number', diff --git a/components/src/atoms/InputField/index.tsx b/components/src/atoms/InputField/index.tsx index f5c126a15ec..2064bac4d75 100644 --- a/components/src/atoms/InputField/index.tsx +++ b/components/src/atoms/InputField/index.tsx @@ -1,10 +1,11 @@ -import * as React from 'react' +import { forwardRef } from 'react' import styled, { css } from 'styled-components' import { Flex } from '../../primitives' import { ALIGN_CENTER, DIRECTION_COLUMN, + DIRECTION_ROW, NO_WRAP, TEXT_ALIGN_RIGHT, } from '../../styles' @@ -14,7 +15,16 @@ import { RESPONSIVENESS, SPACING, TYPOGRAPHY } from '../../ui-style-constants' import { Tooltip } from '../Tooltip' import { useHoverTooltip } from '../../tooltips' import { StyledText } from '../StyledText' + +import type { + ChangeEventHandler, + FocusEvent, + MouseEvent, + MutableRefObject, + ReactNode, +} from 'react' import type { IconName } from '../../icons' + export const INPUT_TYPE_NUMBER = 'number' as const export const LEGACY_INPUT_TYPE_TEXT = 'text' as const export const LEGACY_INPUT_TYPE_PASSWORD = 'password' as const @@ -24,7 +34,7 @@ export interface InputFieldProps { /** field is disabled if value is true */ disabled?: boolean /** change handler */ - onChange?: React.ChangeEventHandler + onChange?: ChangeEventHandler /** name of field in form */ name?: string /** optional ID of element */ @@ -32,7 +42,7 @@ export interface InputFieldProps { /** placeholder text */ placeholder?: string /** optional suffix component, appears to the right of input text */ - units?: React.ReactNode + units?: ReactNode /** current value of text in box, defaults to '' */ value?: string | number | null /** if included, InputField will use error style and display error instead of caption */ @@ -49,11 +59,11 @@ export interface InputFieldProps { | typeof LEGACY_INPUT_TYPE_PASSWORD | typeof INPUT_TYPE_NUMBER /** mouse click handler */ - onClick?: (event: React.MouseEvent) => unknown + onClick?: (event: MouseEvent) => unknown /** focus handler */ - onFocus?: (event: React.FocusEvent) => unknown + onFocus?: (event: FocusEvent) => unknown /** blur handler */ - onBlur?: (event: React.FocusEvent) => unknown + onBlur?: (event: FocusEvent) => unknown /** makes input field read-only */ readOnly?: boolean /** html tabindex property */ @@ -63,8 +73,8 @@ export interface InputFieldProps { /** if true, clear out value and add '-' placeholder */ isIndeterminate?: boolean /** if input type is number, these are the min and max values */ - max?: number - min?: number + max?: number | string + min?: number | string /** horizontal text alignment for title, input, and (sub)captions */ textAlign?: | typeof TYPOGRAPHY.textAlignLeft @@ -72,14 +82,22 @@ export interface InputFieldProps { /** small or medium input field height, relevant only */ size?: 'medium' | 'small' /** react useRef to control input field instead of react event */ - ref?: React.MutableRefObject + ref?: MutableRefObject + /** optional IconName to display icon aligned to left of input field */ leftIcon?: IconName + /** if true, show delete icon aligned to right of input field */ showDeleteIcon?: boolean + /** callback passed to optional delete icon onClick */ onDelete?: () => void + /** if true, style the background of input field to error state */ hasBackgroundError?: boolean + /** optional prop to override input field border radius */ + borderRadius?: string + /** optional prop to override input field padding */ + padding?: string } -export const InputField = React.forwardRef( +export const InputField = forwardRef( (props, ref): JSX.Element => { const { placeholder, @@ -90,6 +108,9 @@ export const InputField = React.forwardRef( tabIndex = 0, showDeleteIcon = false, hasBackgroundError = false, + onDelete, + borderRadius, + padding, ...inputProps } = props const hasError = props.error != null @@ -111,8 +132,10 @@ export const InputField = React.forwardRef( const INPUT_FIELD = css` display: flex; background-color: ${hasBackgroundError ? COLORS.red30 : COLORS.white}; - border-radius: ${BORDERS.borderRadius4}; - padding: ${SPACING.spacing8}; + border-radius: ${borderRadius != null + ? borderRadius + : BORDERS.borderRadius4}; + padding: ${padding != null ? padding : SPACING.spacing8}; border: ${hasBackgroundError ? 'none' : `1px ${BORDERS.styleSolid} @@ -254,7 +277,11 @@ export const InputField = React.forwardRef( > {title != null ? ( - + ( ) : null} ) : null} - + ( {showDeleteIcon ? ( @@ -331,7 +363,10 @@ export const InputField = React.forwardRef( ) : null} {hasError ? ( - + {props.error} ) : null} diff --git a/components/src/atoms/ListButton/ListButtonChildren/ListButtonAccordion.tsx b/components/src/atoms/ListButton/ListButtonChildren/ListButtonAccordion.tsx index 04c42ba654b..2d874f61c99 100644 --- a/components/src/atoms/ListButton/ListButtonChildren/ListButtonAccordion.tsx +++ b/components/src/atoms/ListButton/ListButtonChildren/ListButtonAccordion.tsx @@ -1,11 +1,12 @@ -import type * as React from 'react' import { Flex } from '../../../primitives' import { DIRECTION_COLUMN } from '../../../styles' import { SPACING } from '../../../ui-style-constants' import { StyledText } from '../../StyledText' +import type { ReactNode } from 'react' + interface ListButtonAccordionProps { - children: React.ReactNode + children: ReactNode // determines if the accordion is expanded or not isExpanded?: boolean // is it nested into another accordion? diff --git a/components/src/atoms/ListButton/ListButtonChildren/ListButtonAccordionContainer.tsx b/components/src/atoms/ListButton/ListButtonChildren/ListButtonAccordionContainer.tsx index 99fde7dd81f..d0e8f19821f 100644 --- a/components/src/atoms/ListButton/ListButtonChildren/ListButtonAccordionContainer.tsx +++ b/components/src/atoms/ListButton/ListButtonChildren/ListButtonAccordionContainer.tsx @@ -1,9 +1,10 @@ -import type * as React from 'react' import { Flex } from '../../../primitives' import { DIRECTION_COLUMN } from '../../../styles' +import type { ReactNode } from 'react' + interface ListButtonAccordionContainerProps { - children: React.ReactNode + children: ReactNode id: string } /* @@ -16,7 +17,7 @@ export function ListButtonAccordionContainer( const { id, children } = props return ( - + {children} ) diff --git a/components/src/atoms/ListButton/ListButtonChildren/ListButtonRadioButton.tsx b/components/src/atoms/ListButton/ListButtonChildren/ListButtonRadioButton.tsx index 52a58e5f4ec..366075df05e 100644 --- a/components/src/atoms/ListButton/ListButtonChildren/ListButtonRadioButton.tsx +++ b/components/src/atoms/ListButton/ListButtonChildren/ListButtonRadioButton.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import styled, { css } from 'styled-components' import { SPACING } from '../../../ui-style-constants' import { BORDERS, COLORS } from '../../../helix-design-system' @@ -6,12 +5,13 @@ import { Flex } from '../../../primitives' import { StyledText } from '../../StyledText' import { CURSOR_POINTER } from '../../../styles' +import type { ChangeEventHandler, MouseEvent } from 'react' import type { StyleProps } from '../../../primitives' interface ListButtonRadioButtonProps extends StyleProps { buttonText: string buttonValue: string | number - onChange: React.ChangeEventHandler + onChange: ChangeEventHandler setNoHover?: () => void setHovered?: () => void disabled?: boolean @@ -34,48 +34,11 @@ export function ListButtonRadioButton( id = buttonText, } = props - const SettingButton = styled.input` - display: none; - ` - - const AVAILABLE_BUTTON_STYLE = css` - background: ${COLORS.white}; - color: ${COLORS.black90}; - - &:hover { - background-color: ${COLORS.grey10}; - } - ` - - const SELECTED_BUTTON_STYLE = css` - background: ${COLORS.blue50}; - color: ${COLORS.white}; - - &:active { - background-color: ${COLORS.blue60}; - } - ` - - const DISABLED_STYLE = css` - color: ${COLORS.grey40}; - background-color: ${COLORS.grey10}; - ` - - const SettingButtonLabel = styled.label` - border-radius: ${BORDERS.borderRadius8}; - cursor: ${CURSOR_POINTER}; - padding: 14px ${SPACING.spacing12}; - width: 100%; - - ${isSelected ? SELECTED_BUTTON_STYLE : AVAILABLE_BUTTON_STYLE} - ${disabled && DISABLED_STYLE} - ` - return ( { + onClick={(e: MouseEvent) => { e.stopPropagation() }} > @@ -89,6 +52,8 @@ export function ListButtonRadioButton( /> ) } + +const SettingButton = styled.input` + display: none; +` + +const AVAILABLE_BUTTON_STYLE = css` + background: ${COLORS.white}; + color: ${COLORS.black90}; + + &:hover { + background-color: ${COLORS.grey10}; + } +` + +const SELECTED_BUTTON_STYLE = css` + background: ${COLORS.blue50}; + color: ${COLORS.white}; + + &:active { + background-color: ${COLORS.blue60}; + } +` + +const DISABLED_STYLE = css` + color: ${COLORS.grey40}; + background-color: ${COLORS.grey10}; +` + +interface ButtonLabelProps { + isSelected: boolean + disabled: boolean +} + +const SettingButtonLabel = styled.label` + border-radius: ${BORDERS.borderRadius8}; + cursor: ${CURSOR_POINTER}; + padding: 14px ${SPACING.spacing12}; + width: 100%; + + ${({ isSelected }) => + isSelected ? SELECTED_BUTTON_STYLE : AVAILABLE_BUTTON_STYLE} + ${({ disabled }) => disabled && DISABLED_STYLE} +` diff --git a/components/src/atoms/ListButton/__tests__/ListButton.test.tsx b/components/src/atoms/ListButton/__tests__/ListButton.test.tsx index e7ba460b5e2..fe4e3fb7349 100644 --- a/components/src/atoms/ListButton/__tests__/ListButton.test.tsx +++ b/components/src/atoms/ListButton/__tests__/ListButton.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { vi, describe, it, expect, beforeEach } from 'vitest' import '@testing-library/jest-dom/vitest' import { fireEvent, screen } from '@testing-library/react' @@ -7,11 +6,13 @@ import { COLORS } from '../../../helix-design-system' import { ListButton } from '..' -const render = (props: React.ComponentProps) => +import type { ComponentProps } from 'react' + +const render = (props: ComponentProps) => renderWithProviders() describe('ListButton', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { diff --git a/components/src/atoms/ListButton/__tests__/ListButtonAccordion.test.tsx b/components/src/atoms/ListButton/__tests__/ListButtonAccordion.test.tsx index 29a2673c773..07ce7452549 100644 --- a/components/src/atoms/ListButton/__tests__/ListButtonAccordion.test.tsx +++ b/components/src/atoms/ListButton/__tests__/ListButtonAccordion.test.tsx @@ -1,15 +1,16 @@ -import type * as React from 'react' import { describe, it, beforeEach } from 'vitest' import { screen } from '@testing-library/react' import { renderWithProviders } from '../../../testing/utils' import { ListButtonAccordion } from '..' -const render = (props: React.ComponentProps) => +import type { ComponentProps } from 'react' + +const render = (props: ComponentProps) => renderWithProviders() describe('ListButtonAccordion', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { diff --git a/components/src/atoms/ListButton/__tests__/ListButtonRadioButton.test.tsx b/components/src/atoms/ListButton/__tests__/ListButtonRadioButton.test.tsx index e9448ffabdf..95520a9691b 100644 --- a/components/src/atoms/ListButton/__tests__/ListButtonRadioButton.test.tsx +++ b/components/src/atoms/ListButton/__tests__/ListButtonRadioButton.test.tsx @@ -1,15 +1,16 @@ -import type * as React from 'react' import { describe, it, beforeEach, vi, expect } from 'vitest' import { fireEvent, screen } from '@testing-library/react' import { renderWithProviders } from '../../../testing/utils' import { ListButtonRadioButton } from '..' -const render = (props: React.ComponentProps) => +import type { ComponentProps } from 'react' + +const render = (props: ComponentProps) => renderWithProviders() describe('ListButtonRadioButton', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { diff --git a/components/src/atoms/ListButton/index.tsx b/components/src/atoms/ListButton/index.tsx index 3f26c830a50..fd4d0cce052 100644 --- a/components/src/atoms/ListButton/index.tsx +++ b/components/src/atoms/ListButton/index.tsx @@ -1,9 +1,10 @@ -import type * as React from 'react' import { css } from 'styled-components' import { Flex } from '../../primitives' import { SPACING } from '../../ui-style-constants' import { BORDERS, COLORS } from '../../helix-design-system' import { CURSOR_POINTER } from '../../styles' + +import type { ReactNode } from 'react' import type { StyleProps } from '../../primitives' export * from './ListButtonChildren/index' @@ -12,7 +13,7 @@ export type ListButtonType = 'noActive' | 'connected' | 'notConnected' interface ListButtonProps extends StyleProps { type: ListButtonType - children: React.ReactNode + children: ReactNode disabled?: boolean onClick?: () => void } diff --git a/components/src/atoms/ListItem/ListItemChildren/ListItemCustomize.tsx b/components/src/atoms/ListItem/ListItemChildren/ListItemCustomize.tsx index aa04dd91722..7eb96e0bac4 100644 --- a/components/src/atoms/ListItem/ListItemChildren/ListItemCustomize.tsx +++ b/components/src/atoms/ListItem/ListItemChildren/ListItemCustomize.tsx @@ -18,6 +18,8 @@ interface ListItemCustomizeProps { label?: string dropdown?: DropdownMenuProps tag?: TagProps + /** optional placement of the menu */ + menuPlacement?: 'auto' | 'top' | 'bottom' } export function ListItemCustomize(props: ListItemCustomizeProps): JSX.Element { @@ -29,6 +31,7 @@ export function ListItemCustomize(props: ListItemCustomizeProps): JSX.Element { linkText, dropdown, tag, + menuPlacement = 'auto', } = props return ( @@ -49,7 +52,9 @@ export function ListItemCustomize(props: ListItemCustomizeProps): JSX.Element { {label} ) : null} - {dropdown != null ? : null} + {dropdown != null ? ( + + ) : null} {tag != null ? : null} {onClick != null && linkText != null ? ( diff --git a/components/src/atoms/ListItem/ListItemChildren/ListItemDescriptor.tsx b/components/src/atoms/ListItem/ListItemChildren/ListItemDescriptor.tsx index dcedecaa9f8..7b7620457c2 100644 --- a/components/src/atoms/ListItem/ListItemChildren/ListItemDescriptor.tsx +++ b/components/src/atoms/ListItem/ListItemChildren/ListItemDescriptor.tsx @@ -1,7 +1,9 @@ import { Flex } from '../../../primitives' import { ALIGN_FLEX_START, + DIRECTION_COLUMN, DIRECTION_ROW, + JUSTIFY_FLEX_START, JUSTIFY_SPACE_BETWEEN, } from '../../../styles' import { SPACING } from '../../../ui-style-constants' @@ -10,19 +12,27 @@ interface ListItemDescriptorProps { type: 'default' | 'large' description: JSX.Element content: JSX.Element + changeFlexDirection?: boolean } export const ListItemDescriptor = ( props: ListItemDescriptorProps ): JSX.Element => { - const { description, content, type } = props + const { description, content, type, changeFlexDirection = false } = props + let justifyContent = 'none' + if (type === 'default' && changeFlexDirection) { + justifyContent = JUSTIFY_FLEX_START + } else if (type === 'default') { + justifyContent = JUSTIFY_SPACE_BETWEEN + } + return ( {description} diff --git a/components/src/atoms/ListItem/__tests__/ListItem.test.tsx b/components/src/atoms/ListItem/__tests__/ListItem.test.tsx index 9cb34e3524f..7b603d85868 100644 --- a/components/src/atoms/ListItem/__tests__/ListItem.test.tsx +++ b/components/src/atoms/ListItem/__tests__/ListItem.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { vi, describe, it, expect, beforeEach } from 'vitest' import '@testing-library/jest-dom/vitest' import { fireEvent, screen } from '@testing-library/react' @@ -7,11 +6,13 @@ import { BORDERS, COLORS } from '../../../helix-design-system' import { ListItem } from '..' -const render = (props: React.ComponentProps) => +import type { ComponentProps } from 'react' + +const render = (props: ComponentProps) => renderWithProviders() describe('ListItem', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { @@ -33,7 +34,7 @@ describe('ListItem', () => { render(props) screen.getByText('mock listitem content') const listItem = screen.getByTestId('ListItem_noActive') - expect(listItem).toHaveStyle(`backgroundColor: ${COLORS.grey30}`) + expect(listItem).toHaveStyle(`backgroundColor: ${COLORS.grey20}`) expect(listItem).toHaveStyle(`borderRadius: ${BORDERS.borderRadius4}`) }) it('should render correct style - success', () => { diff --git a/components/src/atoms/ListItem/index.tsx b/components/src/atoms/ListItem/index.tsx index cb61f0a4d3c..d1d9b8d1312 100644 --- a/components/src/atoms/ListItem/index.tsx +++ b/components/src/atoms/ListItem/index.tsx @@ -1,20 +1,26 @@ -import type * as React from 'react' import { css } from 'styled-components' import { Flex } from '../../primitives' import { RESPONSIVENESS, SPACING } from '../../ui-style-constants' import { BORDERS, COLORS } from '../../helix-design-system' import { FLEX_MAX_CONTENT } from '../../styles' + +import type { ReactNode } from 'react' import type { StyleProps } from '../../primitives' export * from './ListItemChildren' -export type ListItemType = 'error' | 'noActive' | 'success' | 'warning' +export type ListItemType = + | 'error' + | 'noActive' + | 'success' + | 'warning' + | 'unavailable' interface ListItemProps extends StyleProps { /** ListItem state type */ type: ListItemType /** ListItem contents */ - children: React.ReactNode + children: ReactNode onClick?: () => void onMouseEnter?: () => void onMouseLeave?: () => void @@ -22,13 +28,13 @@ interface ListItemProps extends StyleProps { const LISTITEM_PROPS_BY_TYPE: Record< ListItemType, - { backgroundColor: string } + { backgroundColor: string; color?: string } > = { error: { backgroundColor: COLORS.red35, }, noActive: { - backgroundColor: COLORS.grey30, + backgroundColor: COLORS.grey20, }, success: { backgroundColor: COLORS.green35, @@ -36,6 +42,10 @@ const LISTITEM_PROPS_BY_TYPE: Record< warning: { backgroundColor: COLORS.yellow35, }, + unavailable: { + backgroundColor: COLORS.grey20, + color: COLORS.grey40, + }, } /* @@ -54,6 +64,7 @@ export function ListItem(props: ListItemProps): JSX.Element { const LIST_ITEM_STYLE = css` background-color: ${listItemProps.backgroundColor}; + color: ${listItemProps.color ?? COLORS.black90}; width: 100%; height: ${FLEX_MAX_CONTENT}; border-radius: ${BORDERS.borderRadius4}; diff --git a/components/src/atoms/MenuList/MenuItem.tsx b/components/src/atoms/MenuList/MenuItem.tsx index cd34c7c7f44..94857fc56dd 100644 --- a/components/src/atoms/MenuList/MenuItem.tsx +++ b/components/src/atoms/MenuList/MenuItem.tsx @@ -16,6 +16,8 @@ export const MenuItem = styled.button` padding: ${SPACING.spacing8} ${SPACING.spacing12} ${SPACING.spacing8} ${SPACING.spacing12}; border: ${props => (props.border != null ? props.border : 'inherit')}; + border-radius: ${props => + props.borderRadius != null ? props.borderRadius : 'inherit'}; &:hover { background-color: ${COLORS.blue10}; diff --git a/components/src/atoms/MenuList/OverflowBtn.tsx b/components/src/atoms/MenuList/OverflowBtn.tsx index ec5958746f4..0414f95f7d3 100644 --- a/components/src/atoms/MenuList/OverflowBtn.tsx +++ b/components/src/atoms/MenuList/OverflowBtn.tsx @@ -1,22 +1,24 @@ -import * as React from 'react' +import { forwardRef } from 'react' import { css } from 'styled-components' import { Btn } from '../../primitives' import { BORDERS, COLORS } from '../../helix-design-system' import { SPACING } from '../../ui-style-constants' -interface OverflowBtnProps extends React.ComponentProps { +import type { ComponentProps, ForwardedRef, ReactNode } from 'react' + +interface OverflowBtnProps extends ComponentProps { fillColor?: string } export const OverflowBtn: ( props: OverflowBtnProps, - ref: React.ForwardedRef -) => React.ReactNode = React.forwardRef( + ref: ForwardedRef +) => ReactNode = forwardRef( ( props: OverflowBtnProps, - ref: React.ForwardedRef + ref: ForwardedRef ): JSX.Element => { - const { fillColor } = props + const { fillColor, ...restProps } = props return ( ) => { +import type { ComponentProps } from 'react' + +const render = (props: ComponentProps) => { return renderWithProviders()[0] } describe('MenuItem', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { diff --git a/components/src/atoms/MenuList/__tests__/MenuList.test.tsx b/components/src/atoms/MenuList/__tests__/MenuList.test.tsx index e38e145eceb..7b2ccffc42b 100644 --- a/components/src/atoms/MenuList/__tests__/MenuList.test.tsx +++ b/components/src/atoms/MenuList/__tests__/MenuList.test.tsx @@ -1,18 +1,19 @@ -import type * as React from 'react' import { describe, it, expect, vi, beforeEach } from 'vitest' import { fireEvent, screen } from '@testing-library/react' import { renderWithProviders } from '../../../testing/utils' import { MenuList } from '..' -const render = (props: React.ComponentProps) => { +import type { ComponentProps } from 'react' + +const render = (props: ComponentProps) => { return renderWithProviders()[0] } const mockBtn =
        mockBtn
        describe('MenuList', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { children: mockBtn, diff --git a/components/src/atoms/MenuList/__tests__/OverflowBtn.test.tsx b/components/src/atoms/MenuList/__tests__/OverflowBtn.test.tsx index 00f080fae2c..af60a617c41 100644 --- a/components/src/atoms/MenuList/__tests__/OverflowBtn.test.tsx +++ b/components/src/atoms/MenuList/__tests__/OverflowBtn.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { vi, it, expect, describe } from 'vitest' import { fireEvent, screen } from '@testing-library/react' @@ -6,7 +5,9 @@ import { COLORS } from '../../../helix-design-system' import { renderWithProviders } from '../../../testing/utils' import { OverflowBtn } from '../OverflowBtn' -const render = (props: React.ComponentProps) => { +import type { ComponentProps } from 'react' + +const render = (props: ComponentProps) => { return renderWithProviders()[0] } diff --git a/components/src/atoms/MenuList/index.tsx b/components/src/atoms/MenuList/index.tsx index 7b930243ba0..f06fe983523 100644 --- a/components/src/atoms/MenuList/index.tsx +++ b/components/src/atoms/MenuList/index.tsx @@ -1,5 +1,3 @@ -import type * as React from 'react' - import { BORDERS, COLORS } from '../../helix-design-system' import { DIRECTION_COLUMN, @@ -10,10 +8,12 @@ import { Flex } from '../../primitives' import { SPACING } from '../../ui-style-constants' import { ModalShell } from '../../modals' +import type { MouseEventHandler, ReactNode } from 'react' + interface MenuListProps { - children: React.ReactNode + children: ReactNode isOnDevice?: boolean - onClick?: React.MouseEventHandler + onClick?: MouseEventHandler } export const MenuList = (props: MenuListProps): JSX.Element | null => { diff --git a/components/src/atoms/Snackbar/__tests__/Snackbar.test.tsx b/components/src/atoms/Snackbar/__tests__/Snackbar.test.tsx index a4ae1f8f6b0..0f093548cdc 100644 --- a/components/src/atoms/Snackbar/__tests__/Snackbar.test.tsx +++ b/components/src/atoms/Snackbar/__tests__/Snackbar.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { describe, it, expect, vi, beforeEach } from 'vitest' import '@testing-library/jest-dom/vitest' import { screen, act } from '@testing-library/react' @@ -6,12 +5,14 @@ import { renderWithProviders } from '../../../testing/utils' import { Snackbar } from '..' import { COLORS } from '../../../helix-design-system' -const render = (props: React.ComponentProps) => { +import type { ComponentProps } from 'react' + +const render = (props: ComponentProps) => { return renderWithProviders()[0] } describe('Snackbar', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { message: 'test message', diff --git a/components/src/atoms/StyledText/LegacyStyledText.tsx b/components/src/atoms/StyledText/LegacyStyledText.tsx index 83df2c69e06..7816a8967f9 100644 --- a/components/src/atoms/StyledText/LegacyStyledText.tsx +++ b/components/src/atoms/StyledText/LegacyStyledText.tsx @@ -2,11 +2,11 @@ import styled, { css } from 'styled-components' import { Text } from '../../primitives' import { TYPOGRAPHY, RESPONSIVENESS } from '../../ui-style-constants' -import type * as React from 'react' +import type { ComponentProps, ReactNode } from 'react' import type { FlattenSimpleInterpolation } from 'styled-components' -export interface LegacyProps extends React.ComponentProps { - children?: React.ReactNode +export interface LegacyProps extends ComponentProps { + children?: ReactNode } const styleMap: { [tag: string]: FlattenSimpleInterpolation } = { diff --git a/components/src/atoms/StyledText/StyledText.tsx b/components/src/atoms/StyledText/StyledText.tsx index fc33536da9a..df16b3f8b0a 100644 --- a/components/src/atoms/StyledText/StyledText.tsx +++ b/components/src/atoms/StyledText/StyledText.tsx @@ -3,7 +3,7 @@ import { Text } from '../../primitives' import { TYPOGRAPHY, RESPONSIVENESS } from '../../ui-style-constants' import { TYPOGRAPHY as HELIX_TYPOGRAPHY } from '../../helix-design-system/product' -import type * as React from 'react' +import type { ComponentProps, ReactNode } from 'react' import type { FlattenSimpleInterpolation } from 'styled-components' const helixProductStyleMap = { @@ -290,10 +290,10 @@ const ODDStyleMap = { }, } as const -export interface Props extends React.ComponentProps { +export interface Props extends ComponentProps { oddStyle?: ODDStyles desktopStyle?: HelixStyles - children?: React.ReactNode + children?: ReactNode } export const ODD_STYLES = Object.keys(ODDStyleMap) export const HELIX_STYLES = Object.keys(helixProductStyleMap) diff --git a/components/src/atoms/Tag/__tests__/Tag.test.tsx b/components/src/atoms/Tag/__tests__/Tag.test.tsx index dcb25c77d27..9c7ba83731d 100644 --- a/components/src/atoms/Tag/__tests__/Tag.test.tsx +++ b/components/src/atoms/Tag/__tests__/Tag.test.tsx @@ -1,16 +1,17 @@ -import type * as React from 'react' import { describe, it, expect } from 'vitest' import { screen } from '@testing-library/react' import { COLORS } from '../../../helix-design-system' import { renderWithProviders } from '../../../testing/utils' import { Tag } from '../index' -const render = (props: React.ComponentProps) => { +import type { ComponentProps } from 'react' + +const render = (props: ComponentProps) => { return renderWithProviders() } describe('Tag', () => { - let props: React.ComponentProps + let props: ComponentProps it('should render text, icon with default', () => { props = { diff --git a/components/src/atoms/Tag/index.tsx b/components/src/atoms/Tag/index.tsx index c41025dd25b..74c72da486e 100644 --- a/components/src/atoms/Tag/index.tsx +++ b/components/src/atoms/Tag/index.tsx @@ -1,7 +1,7 @@ import { css } from 'styled-components' import { BORDERS, COLORS } from '../../helix-design-system' import { Flex } from '../../primitives' -import { ALIGN_CENTER, DIRECTION_ROW } from '../../styles' +import { ALIGN_CENTER, DIRECTION_ROW, FLEX_MAX_CONTENT } from '../../styles' import { RESPONSIVENESS, SPACING, TYPOGRAPHY } from '../../ui-style-constants' import { Icon } from '../../icons' import { LegacyStyledText } from '../StyledText' @@ -19,6 +19,7 @@ export interface TagProps { iconPosition?: 'left' | 'right' /** Tagicon */ iconName?: IconName + shrinkToContent?: boolean } const defaultColors = { @@ -42,11 +43,12 @@ const TAG_PROPS_BY_TYPE: Record< } export function Tag(props: TagProps): JSX.Element { - const { iconName, type, text, iconPosition } = props + const { iconName, type, text, iconPosition, shrinkToContent = false } = props const DEFAULT_CONTAINER_STYLE = css` padding: ${SPACING.spacing2} ${SPACING.spacing8}; border-radius: ${BORDERS.borderRadius4}; + width: ${shrinkToContent ? FLEX_MAX_CONTENT : 'none'}; @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { border-radius: ${BORDERS.borderRadius8}; padding: ${SPACING.spacing8} ${SPACING.spacing12}; diff --git a/components/src/atoms/Toast/__tests__/ODDToast.test.tsx b/components/src/atoms/Toast/__tests__/ODDToast.test.tsx index d198df7f03e..3999fc8ec6e 100644 --- a/components/src/atoms/Toast/__tests__/ODDToast.test.tsx +++ b/components/src/atoms/Toast/__tests__/ODDToast.test.tsx @@ -1,16 +1,17 @@ -import type * as React from 'react' import { describe, it, expect, vi, beforeEach } from 'vitest' import '@testing-library/jest-dom/vitest' import { act, fireEvent, screen } from '@testing-library/react' import { renderWithProviders } from '../../../testing/utils' import { Toast, TOAST_ANIMATION_DURATION } from '..' -const render = (props: React.ComponentProps) => { +import type { ComponentProps } from 'react' + +const render = (props: ComponentProps) => { return renderWithProviders()[0] } describe('Toast', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { id: '1', diff --git a/components/src/atoms/Toast/__tests__/Toast.test.tsx b/components/src/atoms/Toast/__tests__/Toast.test.tsx index 5133651ecd6..2d4e189e895 100644 --- a/components/src/atoms/Toast/__tests__/Toast.test.tsx +++ b/components/src/atoms/Toast/__tests__/Toast.test.tsx @@ -1,16 +1,17 @@ -import type * as React from 'react' import { describe, it, expect, vi, beforeEach } from 'vitest' import '@testing-library/jest-dom/vitest' import { act, fireEvent, screen } from '@testing-library/react' import { renderWithProviders } from '../../../testing/utils' import { Toast, TOAST_ANIMATION_DURATION } from '..' -const render = (props: React.ComponentProps) => { +import type { ComponentProps } from 'react' + +const render = (props: ComponentProps) => { return renderWithProviders()[0] } describe('Toast', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { id: '1', diff --git a/components/src/atoms/ToggleGroup/__tests__/ToggleGroup.test.tsx b/components/src/atoms/ToggleGroup/__tests__/ToggleGroup.test.tsx index f7beb737696..f754467da5e 100644 --- a/components/src/atoms/ToggleGroup/__tests__/ToggleGroup.test.tsx +++ b/components/src/atoms/ToggleGroup/__tests__/ToggleGroup.test.tsx @@ -1,15 +1,16 @@ -import type * as React from 'react' import { describe, it, expect, vi } from 'vitest' import { fireEvent, screen } from '@testing-library/react' import { renderWithProviders } from '../../../testing/utils' import { ToggleGroup } from '../index' -const render = (props: React.ComponentProps) => { +import type { ComponentProps } from 'react' + +const render = (props: ComponentProps) => { return renderWithProviders() } describe('ToggleGroup', () => { - let props: React.ComponentProps + let props: ComponentProps it('should render text and buttons', () => { props = { diff --git a/components/src/atoms/ToggleGroup/index.tsx b/components/src/atoms/ToggleGroup/index.tsx index 110c3a243d5..99a5b24f73b 100644 --- a/components/src/atoms/ToggleGroup/index.tsx +++ b/components/src/atoms/ToggleGroup/index.tsx @@ -1,9 +1,9 @@ import { css } from 'styled-components' import { BORDERS, COLORS } from '../../helix-design-system' import { Flex } from '../../primitives' -import { fontWeightRegular } from '../../ui-style-constants/typography' import { PrimaryButton } from '../../atoms/buttons/PrimaryButton' -import { spacing6, spacing8 } from '../../ui-style-constants/spacing' +import { spacing8 } from '../../ui-style-constants/spacing' +import { StyledText } from '../StyledText' interface ToggleGroupProps { leftText: string @@ -24,7 +24,7 @@ export const ToggleGroup = (props: ToggleGroupProps): JSX.Element => { onClick={leftClick} data-testid="toggleGroup_leftButton" > - {leftText} + {leftText} { onClick={rightClick} data-testid="toggleGroup_rightButton" > - {rightText} + {rightText}
        ) @@ -44,14 +44,10 @@ const BUTTON_GROUP_STYLES = css` width: fit-content; button { - height: 1.75rem; + height: 2.25rem; width: auto; - font-weight: ${fontWeightRegular}; - font-size: 11px; - line-height: 14px; box-shadow: none; - padding-top: ${spacing6}; - padding-bottom: ${spacing8}; + padding: ${spacing8}; &:focus { box-shadow: none; color: ${COLORS.white}; @@ -82,16 +78,13 @@ const BUTTON_GROUP_STYLES = css` ` const ACTIVE_STYLE = css` - padding-left: ${spacing8}; - padding-right: ${spacing8}; background-color: ${COLORS.blue50}; color: ${COLORS.white}; pointer-events: none; + border: 1px ${COLORS.blue50} solid; ` const DEFAULT_STYLE = css` - padding-left: ${spacing8}; - padding-right: ${spacing8}; background-color: ${COLORS.white}; color: ${COLORS.black90}; border: 1px ${COLORS.grey30} solid; diff --git a/components/src/atoms/Tooltip/__tests__/Tooltip.test.tsx b/components/src/atoms/Tooltip/__tests__/Tooltip.test.tsx index ddc1e4f6a1a..18fe3fcce85 100644 --- a/components/src/atoms/Tooltip/__tests__/Tooltip.test.tsx +++ b/components/src/atoms/Tooltip/__tests__/Tooltip.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { describe, it, expect, vi, beforeEach } from 'vitest' import '@testing-library/jest-dom/vitest' import { screen } from '@testing-library/react' @@ -11,7 +10,9 @@ import { POSITION_ABSOLUTE } from '../../../styles' import { renderWithProviders } from '../../../testing/utils' import { Tooltip } from '..' -const render = (props: React.ComponentProps) => { +import type { ComponentProps, ReactNode } from 'react' + +const render = (props: ComponentProps) => { return renderWithProviders()[0] } @@ -39,11 +40,11 @@ const MockTooltipProps = { } describe('Tooltip', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { - children: 'mock children' as React.ReactNode, + children: 'mock children' as ReactNode, tooltipProps: MockTooltipProps, key: 'mock key', } diff --git a/components/src/atoms/Tooltip/index.tsx b/components/src/atoms/Tooltip/index.tsx index 6bc04add77c..9ba18b99e1e 100644 --- a/components/src/atoms/Tooltip/index.tsx +++ b/components/src/atoms/Tooltip/index.tsx @@ -1,15 +1,14 @@ -import type * as React from 'react' - import { COLORS } from '../../helix-design-system' import { TYPOGRAPHY } from '../../ui-style-constants' import { LegacyTooltip } from '../../tooltips' import { FLEX_MAX_CONTENT } from '../../styles' +import type { ReactNode } from 'react' import type { UseTooltipResultTooltipProps } from '../../tooltips' import type { StyleProps } from '../../primitives' export interface TooltipProps extends StyleProps { - children: React.ReactNode + children: ReactNode tooltipProps: UseTooltipResultTooltipProps & { visible: boolean } key?: string } diff --git a/components/src/atoms/buttons/EmptySelectorButton.tsx b/components/src/atoms/buttons/EmptySelectorButton.tsx index 8a897e3569b..da34a8ba710 100644 --- a/components/src/atoms/buttons/EmptySelectorButton.tsx +++ b/components/src/atoms/buttons/EmptySelectorButton.tsx @@ -1,20 +1,18 @@ import styled from 'styled-components' import { Flex } from '../../primitives' import { - BORDERS, - COLORS, + ALIGN_CENTER, CURSOR_DEFAULT, CURSOR_POINTER, + FLEX_MAX_CONTENT, Icon, - SPACING, - StyledText, JUSTIFY_CENTER, JUSTIFY_START, - ALIGN_CENTER, - FLEX_MAX_CONTENT, -} from '../..' -import type { IconName } from '../..' - + SPACING, + StyledText, +} from '../../index' +import { BORDERS, COLORS } from '../../helix-design-system' +import type { IconName } from '../../index' interface EmptySelectorButtonProps { onClick: () => void text: string @@ -29,27 +27,14 @@ export function EmptySelectorButton( ): JSX.Element { const { onClick, text, iconName, textAlignment, disabled = false } = props - const StyledButton = styled.button` - border: none; - width: ${FLEX_MAX_CONTENT}; - height: ${FLEX_MAX_CONTENT}; - cursor: ${disabled ? CURSOR_DEFAULT : CURSOR_POINTER}; - &:focus-visible { - outline: 2px solid ${COLORS.white}; - box-shadow: 0 0 0 4px ${COLORS.blue50}; - border-radius: ${BORDERS.borderRadius8}; - } - ` - return ( - + ) } + +interface ButtonProps { + disabled: boolean +} + +const StyledButton = styled.button` + border: none; + width: ${FLEX_MAX_CONTENT}; + height: ${FLEX_MAX_CONTENT}; + cursor: ${CURSOR_POINTER}; + background-color: ${COLORS.blue30}; + border-radius: ${BORDERS.borderRadius8}; + + &:focus-visible { + outline: 2px solid ${COLORS.white}; + box-shadow: 0 0 0 4px ${COLORS.blue50}; + border-radius: ${BORDERS.borderRadius8}; + } + &:hover { + background-color: ${COLORS.blue35}; + } + &:disabled { + background-color: ${COLORS.grey20}; + cursor: ${CURSOR_DEFAULT}; + } +` diff --git a/components/src/atoms/buttons/LargeButton.tsx b/components/src/atoms/buttons/LargeButton.tsx index 37be7d8c34e..7bfcf4b6533 100644 --- a/components/src/atoms/buttons/LargeButton.tsx +++ b/components/src/atoms/buttons/LargeButton.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { css } from 'styled-components' import { Btn } from '../../primitives' @@ -16,6 +15,7 @@ import { } from '../..' import { Icon } from '../../icons' +import type { ReactNode } from 'react' import type { StyleProps } from '../../primitives' import type { IconName } from '../../icons' @@ -126,7 +126,7 @@ interface LargeButtonProps extends StyleProps { type?: 'submit' onClick?: () => void buttonType?: LargeButtonTypes - buttonText: React.ReactNode + buttonText: ReactNode iconName?: IconName disabled?: boolean /** aria-disabled for displaying snack bar. */ @@ -217,6 +217,7 @@ export function LargeButton(props: LargeButtonProps): JSX.Element { color: ${LARGE_BUTTON_PROPS_BY_TYPE[buttonType].disabledColor}; background-color: ${LARGE_BUTTON_PROPS_BY_TYPE[buttonType] .disabledBackgroundColor}; + border: none; } &[aria-disabled='true'] { diff --git a/components/src/atoms/buttons/RadioButton.tsx b/components/src/atoms/buttons/RadioButton.tsx index ff991941fe4..8b371912e6a 100644 --- a/components/src/atoms/buttons/RadioButton.tsx +++ b/components/src/atoms/buttons/RadioButton.tsx @@ -1,27 +1,26 @@ -import type * as React from 'react' import styled, { css } from 'styled-components' import { Flex } from '../../primitives' +import { COLORS, BORDERS } from '../../helix-design-system' +import { RESPONSIVENESS, SPACING } from '../../ui-style-constants' import { - ALIGN_CENTER, - BORDERS, - COLORS, CURSOR_DEFAULT, - CURSOR_NOT_ALLOWED, CURSOR_POINTER, + CURSOR_NOT_ALLOWED, DIRECTION_ROW, + ALIGN_CENTER, Icon, - RESPONSIVENESS, - SPACING, StyledText, -} from '../..' +} from '../../index' -import type { IconName } from '../..' +import type { ChangeEventHandler, ReactNode } from 'react' +import type { FlattenSimpleInterpolation } from 'styled-components' +import type { IconName } from '../../icons' import type { StyleProps } from '../../primitives' interface RadioButtonProps extends StyleProps { - buttonLabel: string | React.ReactNode + buttonLabel: string | ReactNode buttonValue: string | number - onChange: React.ChangeEventHandler + onChange: ChangeEventHandler disabled?: boolean iconName?: IconName isSelected?: boolean @@ -56,13 +55,8 @@ export function RadioButton(props: RadioButtonProps): JSX.Element { setHovered, setNoHover, } = props - const isLarge = radioButtonType === 'large' - const SettingButton = styled.input` - display: none; - ` - const AVAILABLE_BUTTON_STYLE = css` background: ${COLORS.blue35}; @@ -82,47 +76,6 @@ export function RadioButton(props: RadioButtonProps): JSX.Element { } ` - const DISABLED_BUTTON_STYLE = css` - background-color: ${COLORS.grey35}; - color: ${COLORS.grey50}; - - &:hover, - &:active { - background-color: ${COLORS.grey35}; - } - - @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { - cursor: ${CURSOR_NOT_ALLOWED}; - } - ` - - const SettingButtonLabel = styled.label` - border-radius: ${!largeDesktopBorderRadius - ? BORDERS.borderRadius40 - : BORDERS.borderRadius8}; - cursor: ${CURSOR_POINTER}; - padding: ${SPACING.spacing12} ${SPACING.spacing16}; - width: 100%; - - ${isSelected ? SELECTED_BUTTON_STYLE : AVAILABLE_BUTTON_STYLE} - ${disabled && DISABLED_BUTTON_STYLE} - - &:focus-visible { - outline: 2px solid ${COLORS.blue55}; - } - - @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { - cursor: ${CURSOR_DEFAULT}; - padding: ${isLarge ? SPACING.spacing24 : SPACING.spacing20}; - border-radius: ${BORDERS.borderRadius16}; - display: ${maxLines != null ? '-webkit-box' : undefined}; - -webkit-line-clamp: ${maxLines ?? undefined}; - -webkit-box-orient: ${maxLines != null ? 'vertical' : undefined}; - word-wrap: break-word; - word-break: break-all; - } - ` - const SUBBUTTON_LABEL_STYLE = css` color: ${disabled ? COLORS.grey50 @@ -131,6 +84,15 @@ export function RadioButton(props: RadioButtonProps): JSX.Element { : COLORS.grey60}; ` + const getButtonStyle = ( + isSelected: boolean, + disabled: boolean + ): FlattenSimpleInterpolation => { + if (disabled) return DISABLED_BUTTON_STYLE + if (isSelected) return SELECTED_BUTTON_STYLE + return AVAILABLE_BUTTON_STYLE + } + return ( ) } + +const DISABLED_BUTTON_STYLE = css` + background-color: ${COLORS.grey35}; + color: ${COLORS.grey50}; + + &:hover, + &:active { + background-color: ${COLORS.grey35}; + } + + @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { + cursor: ${CURSOR_NOT_ALLOWED}; + } +` + +const SettingButton = styled.input` + display: none; +` + +interface SettingsButtonLabelProps { + isSelected: boolean + disabled: boolean + largeDesktopBorderRadius: boolean + isLarge: boolean + maxLines?: number | null +} + +const SettingButtonLabel = styled.label` + border-radius: ${({ largeDesktopBorderRadius }) => + !largeDesktopBorderRadius ? BORDERS.borderRadius40 : BORDERS.borderRadius8}; + cursor: ${CURSOR_POINTER}; + padding: ${SPACING.spacing12} ${SPACING.spacing16}; + width: 100%; + + ${({ disabled }) => disabled && DISABLED_BUTTON_STYLE} + &:focus-visible { + outline: 2px solid ${COLORS.blue55}; + } + + @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { + cursor: ${CURSOR_DEFAULT}; + padding: ${({ largeDesktopBorderRadius }) => + largeDesktopBorderRadius ? SPACING.spacing24 : SPACING.spacing20}; + border-radius: ${BORDERS.borderRadius16}; + display: ${({ maxLines }) => (maxLines != null ? '-webkit-box' : 'none')}; + -webkit-line-clamp: ${({ maxLines }) => maxLines ?? 'none'}; + -webkit-box-orient: ${({ maxLines }) => + maxLines != null ? 'vertical' : 'none'}; + word-wrap: break-word; + word-break: break-all; + } +` diff --git a/components/src/atoms/buttons/__tests__/AlertPrimaryButton.test.tsx b/components/src/atoms/buttons/__tests__/AlertPrimaryButton.test.tsx index cad0175d981..61d1b07729c 100644 --- a/components/src/atoms/buttons/__tests__/AlertPrimaryButton.test.tsx +++ b/components/src/atoms/buttons/__tests__/AlertPrimaryButton.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { describe, it, beforeEach, expect } from 'vitest' import { screen } from '@testing-library/react' import '@testing-library/jest-dom/vitest' @@ -8,12 +7,14 @@ import { TYPOGRAPHY, SPACING } from '../../../ui-style-constants' import { AlertPrimaryButton } from '../AlertPrimaryButton' -const render = (props: React.ComponentProps) => { +import type { ComponentProps } from 'react' + +const render = (props: ComponentProps) => { return renderWithProviders()[0] } describe('AlertPrimaryButton', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { diff --git a/components/src/atoms/buttons/__tests__/AltPrimaryButton.test.tsx b/components/src/atoms/buttons/__tests__/AltPrimaryButton.test.tsx index f65613d0561..7697336c219 100644 --- a/components/src/atoms/buttons/__tests__/AltPrimaryButton.test.tsx +++ b/components/src/atoms/buttons/__tests__/AltPrimaryButton.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { describe, it, beforeEach, expect } from 'vitest' import { screen } from '@testing-library/react' import '@testing-library/jest-dom/vitest' @@ -7,12 +6,14 @@ import { BORDERS, COLORS } from '../../../helix-design-system' import { TYPOGRAPHY, SPACING } from '../../../ui-style-constants' import { AltPrimaryButton } from '../AltPrimaryButton' -const render = (props: React.ComponentProps) => { +import type { ComponentProps } from 'react' + +const render = (props: ComponentProps) => { return renderWithProviders()[0] } describe('AltPrimaryButton', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { diff --git a/components/src/atoms/buttons/__tests__/EmptySelectorButton.test.tsx b/components/src/atoms/buttons/__tests__/EmptySelectorButton.test.tsx index 6fea2d8d297..bf5e2953086 100644 --- a/components/src/atoms/buttons/__tests__/EmptySelectorButton.test.tsx +++ b/components/src/atoms/buttons/__tests__/EmptySelectorButton.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import '@testing-library/jest-dom/vitest' import { fireEvent, screen } from '@testing-library/react' import { describe, it, expect, vi, beforeEach } from 'vitest' @@ -6,12 +5,14 @@ import { renderWithProviders } from '../../../testing/utils' import { JUSTIFY_CENTER, JUSTIFY_START } from '../../../styles' import { EmptySelectorButton } from '../EmptySelectorButton' -const render = (props: React.ComponentProps) => { +import type { ComponentProps } from 'react' + +const render = (props: ComponentProps) => { return renderWithProviders()[0] } describe('EmptySelectorButton', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { onClick: vi.fn(), diff --git a/components/src/atoms/buttons/__tests__/LargeButton.test.tsx b/components/src/atoms/buttons/__tests__/LargeButton.test.tsx index 52f6c9e71f6..628dd88b541 100644 --- a/components/src/atoms/buttons/__tests__/LargeButton.test.tsx +++ b/components/src/atoms/buttons/__tests__/LargeButton.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { describe, it, expect, vi, beforeEach } from 'vitest' import '@testing-library/jest-dom/vitest' import { fireEvent, screen } from '@testing-library/react' @@ -7,12 +6,14 @@ import { renderWithProviders } from '../../../testing/utils' import { COLORS } from '../../../helix-design-system' import { LargeButton } from '../LargeButton' -const render = (props: React.ComponentProps) => { +import type { ComponentProps } from 'react' + +const render = (props: ComponentProps) => { return renderWithProviders()[0] } describe('LargeButton', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { onClick: vi.fn(), diff --git a/components/src/atoms/buttons/__tests__/PrimaryButton.test.tsx b/components/src/atoms/buttons/__tests__/PrimaryButton.test.tsx index 558f4a595eb..8f465d4bc63 100644 --- a/components/src/atoms/buttons/__tests__/PrimaryButton.test.tsx +++ b/components/src/atoms/buttons/__tests__/PrimaryButton.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { describe, it, beforeEach, expect } from 'vitest' import { fireEvent, screen } from '@testing-library/react' import '@testing-library/jest-dom/vitest' @@ -7,12 +6,14 @@ import { BORDERS, COLORS } from '../../../helix-design-system' import { TYPOGRAPHY, SPACING } from '../../../ui-style-constants' import { PrimaryButton } from '../PrimaryButton' -const render = (props: React.ComponentProps) => { +import type { ComponentProps } from 'react' + +const render = (props: ComponentProps) => { return renderWithProviders()[0] } describe('PrimaryButton', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { diff --git a/components/src/atoms/buttons/__tests__/RadioButton.test.tsx b/components/src/atoms/buttons/__tests__/RadioButton.test.tsx index 95aaf6532fc..b259bcc97fb 100644 --- a/components/src/atoms/buttons/__tests__/RadioButton.test.tsx +++ b/components/src/atoms/buttons/__tests__/RadioButton.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import '@testing-library/jest-dom/vitest' import { screen, queryByAttribute } from '@testing-library/react' import { describe, it, expect, vi, beforeEach } from 'vitest' @@ -7,12 +6,14 @@ import { COLORS } from '../../../helix-design-system' import { SPACING } from '../../../ui-style-constants' import { RadioButton } from '../RadioButton' -const render = (props: React.ComponentProps) => { +import type { ComponentProps } from 'react' + +const render = (props: ComponentProps) => { return renderWithProviders()[0] } describe('RadioButton', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { onChange: vi.fn(), diff --git a/components/src/atoms/buttons/__tests__/SecondaryButton.test.tsx b/components/src/atoms/buttons/__tests__/SecondaryButton.test.tsx index 8887e679268..c7fdcc3229f 100644 --- a/components/src/atoms/buttons/__tests__/SecondaryButton.test.tsx +++ b/components/src/atoms/buttons/__tests__/SecondaryButton.test.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import { describe, it, beforeEach, expect } from 'vitest' import { screen } from '@testing-library/react' import '@testing-library/jest-dom/vitest' @@ -8,12 +7,14 @@ import { BORDERS, COLORS } from '../../../helix-design-system' import { SecondaryButton } from '../SecondaryButton' -const render = (props: React.ComponentProps) => { +import type { ComponentProps } from 'react' + +const render = (props: ComponentProps) => { return renderWithProviders()[0] } describe('SecondaryButton', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { diff --git a/components/src/buttons/Button.tsx b/components/src/buttons/Button.tsx index 749c4e603a5..3c159a3b759 100644 --- a/components/src/buttons/Button.tsx +++ b/components/src/buttons/Button.tsx @@ -1,4 +1,3 @@ -import type * as React from 'react' import cx from 'classnames' import omit from 'lodash/omit' @@ -7,6 +6,7 @@ import styles from './buttons.module.css' import { BUTTON_TYPE_BUTTON } from '../primitives' +import type { ComponentType, MouseEventHandler, ReactNode } from 'react' import type { BUTTON_TYPE_SUBMIT, BUTTON_TYPE_RESET } from '../primitives' import type { IconName } from '../icons' import type { UseHoverTooltipTargetProps } from '../tooltips' @@ -15,7 +15,7 @@ export interface ButtonProps { /** id attribute */ id?: string /** click handler */ - onClick?: React.MouseEventHandler + onClick?: MouseEventHandler /** name attribute */ name?: string /** title attribute */ @@ -31,7 +31,7 @@ export interface ButtonProps { /** inverts the default color/background/border of default button style */ inverted?: boolean /** contents of the button */ - children?: React.ReactNode + children?: ReactNode /** type of button (default "button") */ type?: | typeof BUTTON_TYPE_SUBMIT @@ -40,7 +40,7 @@ export interface ButtonProps { /** ID of form that button is for */ form?: string /** custom element or component to use instead of `