diff --git a/api/src/opentrons/protocol_api/execute.py b/api/src/opentrons/protocol_api/execute.py index f88efd3fe01..e7e9d89e12b 100644 --- a/api/src/opentrons/protocol_api/execute.py +++ b/api/src/opentrons/protocol_api/execute.py @@ -120,9 +120,7 @@ def load_pipettes_from_json( model = props.get('model') mount = props.get('mount') - # TODO: Ian 2018-11-06 remove this fallback to 'model' when - # backwards-compatability for JSON protocols with versioned - # pipettes is dropped (next JSON protocol schema major bump) + # NOTE: 'name' is only used by v1 and v2 JSON protocols name = props.get('name') if not name: name = model.split('_v')[0] @@ -134,9 +132,31 @@ def load_pipettes_from_json( return pipettes_by_id -def load_labware_from_json( +def load_labware_from_json_defs( ctx: ProtocolContext, protocol: Dict[Any, Any]) -> Dict[str, labware.Labware]: + protocol_labware = protocol.get('labware', {}) + definitions = protocol.get('labwareDefinitions', {}) + loaded_labware = {} + + for labware_id, props in protocol_labware.items(): + slot = props.get('slot') + definition = definitions.get(props.get('definitionId')) + loaded_labware[labware_id] = ctx.load_labware( + labware.Labware( + definition, + ctx.deck.position_for(slot), + props.get('displayName') + ), + slot) + + return loaded_labware + + +def load_labware_from_json_loadnames( + ctx: ProtocolContext, + protocol: Dict[Any, Any]) -> Dict[str, labware.Labware]: + # NOTE: this is only used by v1 and v2 JSON protocols data = protocol.get('labware', {}) loaded_labware = {} bc = BCLabware(ctx) @@ -171,9 +191,9 @@ def _get_well(loaded_labware: Dict[str, labware.Labware], return plate.wells_by_index()[well] -def _get_bottom_offset(command_type: str, - params: Dict[str, Any], - default_values: Dict[str, float]) -> Optional[float]: +def _get_bottom_offset_v1(command_type: str, + params: Dict[str, Any], + default_values: Dict[str, float]) -> Optional[float]: # default offset from bottom for aspirate/dispense commands offset_default = default_values.get( '{}-mm-from-bottom'.format(command_type)) @@ -185,17 +205,17 @@ def _get_bottom_offset(command_type: str, return offset_from_bottom -def _get_location_with_offset(loaded_labware: Dict[str, labware.Labware], - command_type: str, - params: Dict[str, Any], - default_values: Dict[str, float]) -> Location: +def _get_location_with_offset_v1(loaded_labware: Dict[str, labware.Labware], + command_type: str, + params: Dict[str, Any], + default_values: Dict[str, float]) -> Location: well = _get_well(loaded_labware, params) # Never move to the bottom of the fixed trash if 'fixedTrash' in labware.quirks_from_any_parent(well): return well.top() - offset_from_bottom = _get_bottom_offset( + offset_from_bottom = _get_bottom_offset_v1( command_type, params, default_values) bot = well.bottom() @@ -208,9 +228,27 @@ def _get_location_with_offset(loaded_labware: Dict[str, labware.Labware], return with_offs +def _get_location_with_offset_v3(loaded_labware: Dict[str, labware.Labware], + command_type: str, + params: Dict[str, Any]) -> Location: + well = _get_well(loaded_labware, params) + + # Never move to the bottom of the fixed trash + if 'fixedTrash' in labware.quirks_from_any_parent(well): + return well.top() + + offset_from_bottom = params.get('offsetFromBottomMm') + if None is offset_from_bottom: + raise RuntimeError('"offsetFromBottomMm" is required for {}' + .format(command_type)) + + bottom = well.bottom() + return bottom.move(Point(z=offset_from_bottom)) + + # TODO (Ian 2018-08-22) once Pipette has more sensible way of managing # flow rate value (eg as an argument in aspirate/dispense fns), remove this -def _set_flow_rate( +def _set_flow_rate_v1( pipette_name, pipette, command_type, params, default_values): """ Set flow rate in uL/mm, to value obtained from command's params, @@ -244,10 +282,47 @@ def _set_flow_rate( } -def dispatch_json(context: ProtocolContext, # noqa(C901) - protocol_data: Dict[Any, Any], - instruments: Dict[str, InstrumentContext], - labware: Dict[str, labware.Labware]): +# TODO (Ian 2019-04-05) once Pipette commands allow flow rate as an +# absolute value (not % value) as an argument in +# aspirate/dispense/blowout/air_gap fns, remove this +def _set_flow_rate_v3( + pipette_name, pipette, command_type, params): + """ + Set flow rate in uL/mm, to value obtained from command's params. + """ + flow_rate_param = params.get('flowRate') + + pipette.flow_rate = { + 'aspirate': flow_rate_param, + 'dispense': flow_rate_param + } + + +def get_protocol_schema_version(protocol_json: Dict[Any, Any]) -> int: + # v3 and above uses `schemaVersion: integer` + version = protocol_json.get('schemaVersion') + if None is not version: + return version + # v1 uses 1.x.x and v2 uses 2.x.x + legacyKebabVersion = protocol_json.get('protocol-schema') + # No minor/patch schemas ever were released, + # do not permit protocols with nonexistent schema versions to load + if (legacyKebabVersion == '1.0.0'): + return 1 + if (legacyKebabVersion == '2.0.0'): + return 2 + if (legacyKebabVersion is not None): + raise RuntimeError(('No such schema version: "{}". Did you mean ' + + '"1.0.0" or "2.0.0"?').format(legacyKebabVersion)) + raise RuntimeError( + 'Could not determine schema version for protcol. ' + + 'Make sure there is a version number under "schemaVersion"') + + +def dispatch_json_v1(context: ProtocolContext, # noqa(C901) + protocol_data: Dict[Any, Any], + instruments: Dict[str, InstrumentContext], + loaded_labware: Dict[str, labware.Labware]): subprocedures = [ p.get('subprocedure', []) for p in protocol_data.get('procedure', [])] @@ -281,42 +356,42 @@ def dispatch_json(context: ProtocolContext, # noqa(C901) context.delay(seconds=wait) elif command_type == 'blowout': - well = _get_well(labware, params) + well = _get_well(loaded_labware, params) pipette.blow_out(well) # type: ignore elif command_type == 'pick-up-tip': - well = _get_well(labware, params) + well = _get_well(loaded_labware, params) pipette.pick_up_tip(well) # type: ignore elif command_type == 'drop-tip': - well = _get_well(labware, params) + well = _get_well(loaded_labware, params) pipette.drop_tip(well) # type: ignore elif command_type == 'aspirate': - location = _get_location_with_offset( - labware, 'aspirate', params, default_values) + location = _get_location_with_offset_v1( + loaded_labware, 'aspirate', params, default_values) volume = params['volume'] - _set_flow_rate( + _set_flow_rate_v1( pipette_name, pipette, command_type, params, default_values) pipette.aspirate(volume, location) # type: ignore elif command_type == 'dispense': - location = _get_location_with_offset( - labware, 'dispense', params, default_values) + location = _get_location_with_offset_v1( + loaded_labware, 'dispense', params, default_values) volume = params['volume'] - _set_flow_rate( + _set_flow_rate_v1( pipette_name, pipette, command_type, params, default_values) pipette.dispense(volume, location) # type: ignore elif command_type == 'touch-tip': - well = _get_well(labware, params) - offset = default_values.get('touch-tip-mm-from-top', -1) - pipette.touch_tip(location, v_offset=offset) # type: ignore + well = _get_well(loaded_labware, params) + offset = default_values.get('touchTipMmFromTop', -1) + pipette.touch_tip(well, v_offset=offset) # type: ignore elif command_type == 'move-to-slot': slot = params.get('slot') if slot not in [str(s+1) for s in range(12)]: - raise ValueError('Invalid "slot" for "move-to-slot": {}' + raise ValueError('Invalid "slot" for "moveToSlot": {}' .format(slot)) slot_obj = context.deck.position_for(slot) @@ -328,10 +403,102 @@ def dispatch_json(context: ProtocolContext, # noqa(C901) pipette.move_to( # type: ignore slot_obj.move(offsetPoint), - force_direct=params.get('force-direct'), - minimum_z_height=params.get('minimum-z-height')) + force_direct=params.get('forceDirect'), + minimum_z_height=params.get('minimumZHeight')) else: - MODULE_LOG.warning("Bad command type {}".format(command_type)) + raise RuntimeError( + "Unsupported command type {}".format(command_type)) + + +def dispatch_json_v3(context: ProtocolContext, # noqa(C901) + protocol_data: Dict[Any, Any], + instruments: Dict[str, InstrumentContext], + loaded_labware: Dict[str, labware.Labware]): + commands = protocol_data.get('commands', []) + + for command_item in commands: + command_type = command_item.get('command') + params = command_item.get('params', {}) + pipette = instruments.get(params.get('pipette')) + protocol_pipette_data = protocol_data\ + .get('pipettes', {})\ + .get(params.get('pipette'), {}) + pipette_name = protocol_pipette_data.get('name') + + if (not pipette_name): + # TODO: Ian 2018-11-06 remove this fallback to 'model' when + # backwards-compatability for JSON protocols with versioned + # pipettes is dropped (next JSON protocol schema major bump) + pipette_name = protocol_pipette_data.get('model') + + if command_type == 'delay': + wait = params.get('wait') + if wait is None: + raise ValueError('Delay cannot be null') + elif wait is True: + message = params.get('message', 'Pausing until user resumes') + context.pause(msg=message) + else: + context.delay(seconds=wait) + + elif command_type == 'blowout': + well = _get_well(loaded_labware, params) + _set_flow_rate_v3( + pipette_name, pipette, command_type, params) + pipette.blow_out(well) # type: ignore + + elif command_type == 'pickUpTip': + well = _get_well(loaded_labware, params) + pipette.pick_up_tip(well) # type: ignore + + elif command_type == 'dropTip': + well = _get_well(loaded_labware, params) + pipette.drop_tip(well) # type: ignore + + elif command_type == 'aspirate': + location = _get_location_with_offset_v3( + loaded_labware, 'aspirate', params) + volume = params['volume'] + _set_flow_rate_v3( + pipette_name, pipette, command_type, params) + pipette.aspirate(volume, location) # type: ignore + + elif command_type == 'dispense': + location = _get_location_with_offset_v3( + loaded_labware, 'dispense', params) + volume = params['volume'] + _set_flow_rate_v3( + pipette_name, pipette, command_type, params) + pipette.dispense(volume, location) # type: ignore + + elif command_type == 'touchTip': + location = _get_location_with_offset_v3( + loaded_labware, 'dispense', params) + well = _get_well(loaded_labware, params) + # convert mmFromBottom to v_offset + v_offset = location.point.z - well.top().point.z + pipette.touch_tip(well, v_offset=v_offset) # type: ignore + + elif command_type == 'moveToSlot': + slot = params.get('slot') + if slot not in [str(s+1) for s in range(12)]: + raise ValueError('Invalid "slot" for "moveToSlot": {}' + .format(slot)) + slot_obj = context.deck.position_for(slot) + + offset = params.get('offset', {}) + offsetPoint = Point( + offset.get('x', 0), + offset.get('y', 0), + offset.get('z', 0)) + + pipette.move_to( # type: ignore + slot_obj.move(offsetPoint), + force_direct=params.get('forceDirect'), + minimum_z_height=params.get('minimumZHeight')) + else: + raise RuntimeError( + "Unsupported command type {}".format(command_type)) def run_protocol(protocol_code: Any = None, @@ -364,8 +531,18 @@ def run_protocol(protocol_code: Any = None, if None is not protocol_code: _run_python(protocol_code, true_context) elif None is not protocol_json: - lw = load_labware_from_json(true_context, protocol_json) + protocol_version = get_protocol_schema_version(protocol_json) + if (protocol_version) > 3: + raise RuntimeError('JSON Protocol version {} is not yet supported \ + in this version of the API'.format(protocol_version)) + ins = load_pipettes_from_json(true_context, protocol_json) - dispatch_json(true_context, protocol_json, ins, lw) + + if (protocol_version >= 3): + lw = load_labware_from_json_defs(true_context, protocol_json) + dispatch_json_v3(true_context, protocol_json, ins, lw) + else: + lw = load_labware_from_json_loadnames(true_context, protocol_json) + dispatch_json_v1(true_context, protocol_json, ins, lw) else: raise RuntimeError("run_protocol must have either code or json") diff --git a/api/tests/opentrons/protocol_api/test_execute.py b/api/tests/opentrons/protocol_api/test_execute.py index 9571f052bf6..dffe0bd7721 100644 --- a/api/tests/opentrons/protocol_api/test_execute.py +++ b/api/tests/opentrons/protocol_api/test_execute.py @@ -1,5 +1,5 @@ import pytest - +import json from opentrons.types import Point from opentrons.protocol_api import execute, ProtocolContext @@ -119,7 +119,7 @@ async def test_load_pipettes(loop, protocol_data): @pytest.mark.parametrize('command_type', ['aspirate', 'dispense']) -def test_get_location_with_offset(loop, command_type): +def test_get_location_with_offset_v1(loop, command_type): ctx = ProtocolContext(loop=loop) plate = ctx.load_labware_by_name("generic_96_wellplate_380_ul", 1) well = "B2" @@ -140,10 +140,10 @@ def test_get_location_with_offset(loop, command_type): "well": well, "offsetFromBottomMm": offset } - offs = execute._get_bottom_offset( + offs = execute._get_bottom_offset_v1( command_type, command_params, default_values) assert offs == offset - result = execute._get_location_with_offset( + result = execute._get_location_with_offset_v1( loaded_labware, command_type, command_params, default_values) assert result.labware == plate.wells_by_index()[well] assert result.point\ @@ -155,16 +155,16 @@ def test_get_location_with_offset(loop, command_type): } # no command-specific offset, use default - result = execute._get_location_with_offset( + result = execute._get_location_with_offset_v1( loaded_labware, command_type, command_params, default_values) default = default_values['{}-mm-from-bottom'.format(command_type)] - assert execute._get_bottom_offset( + assert execute._get_bottom_offset_v1( command_type, command_params, default_values) == default assert result.point\ == plate.wells_by_index()[well].bottom().point + Point(z=default) -def test_load_labware(loop): +def test_load_labware_v1(loop): ctx = ProtocolContext(loop=loop) data = { "labware": { @@ -185,7 +185,7 @@ def test_load_labware(loop): }, } } - loaded_labware = execute.load_labware_from_json(ctx, data) + loaded_labware = execute.load_labware_from_json_loadnames(ctx, data) # objects in loaded_labware should be same objs as labware objs in the deck assert loaded_labware['sourcePlateId'] == ctx.loaded_labwares[10] @@ -196,7 +196,37 @@ def test_load_labware(loop): assert 'Test Plate' in str(loaded_labware['oldPlateId']) -def test_load_labware_trash(loop): +def test_load_labware_v2(loop): + ctx = ProtocolContext(loop=loop) + # trough def with arbitrary ID + data = { + "labwareDefinitions": { + "someTroughDef": json.loads("""{"ordering":[["A1"],["A2"],["A3"],["A4"],["A5"],["A6"],["A7"],["A8"],["A9"],["A10"],["A11"],["A12"]],"otId":"THIS IS A CUSTOM ID","deprecated":false,"metadata":{"displayName":"CUSTOM 12 Channel Trough","displayVolumeUnits":"mL","displayCategory":"trough"},"cornerOffsetFromSlot":{"x":0,"y":0.32,"z":0},"dimensions":{"overallLength":127.76,"overallWidth":85.8,"overallHeight":44.45},"parameters":{"format":"trough","isTiprack":false,"isMagneticModuleCompatible":false,"loadName":"usa_scientific_12_trough_22_ml","quirks":["centerMultichannelOnWells"]},"wells":{"A1":{"shape":"rectangular","depth":42.16,"length":8.33,"width":71.88,"totalLiquidVolume":22000,"x":13.94,"y":42.9,"z":2.29},"A2":{"shape":"rectangular","depth":42.16,"length":8.33,"width":71.88,"totalLiquidVolume":22000,"x":23.03,"y":42.9,"z":2.29},"A3":{"shape":"rectangular","depth":42.16,"length":8.33,"width":71.88,"totalLiquidVolume":22000,"x":32.12,"y":42.9,"z":2.29},"A4":{"shape":"rectangular","depth":42.16,"length":8.33,"width":71.88,"totalLiquidVolume":22000,"x":41.21,"y":42.9,"z":2.29},"A5":{"shape":"rectangular","depth":42.16,"length":8.33,"width":71.88,"totalLiquidVolume":22000,"x":50.3,"y":42.9,"z":2.29},"A6":{"shape":"rectangular","depth":42.16,"length":8.33,"width":71.88,"totalLiquidVolume":22000,"x":59.39,"y":42.9,"z":2.29},"A7":{"shape":"rectangular","depth":42.16,"length":8.33,"width":71.88,"totalLiquidVolume":22000,"x":68.48,"y":42.9,"z":2.29},"A8":{"shape":"rectangular","depth":42.16,"length":8.33,"width":71.88,"totalLiquidVolume":22000,"x":77.57,"y":42.9,"z":2.29},"A9":{"shape":"rectangular","depth":42.16,"length":8.33,"width":71.88,"totalLiquidVolume":22000,"x":86.66,"y":42.9,"z":2.29},"A10":{"shape":"rectangular","depth":42.16,"length":8.33,"width":71.88,"totalLiquidVolume":22000,"x":95.75,"y":42.9,"z":2.29},"A11":{"shape":"rectangular","depth":42.16,"length":8.33,"width":71.88,"totalLiquidVolume":22000,"x":104.84,"y":42.9,"z":2.29},"A12":{"shape":"rectangular","depth":42.16,"length":8.33,"width":71.88,"totalLiquidVolume":22000,"x":113.93,"y":42.9,"z":2.29}},"brand":{"brand":"USA Scientific","brandId":["1061-8150"]}}""") # noqa + }, + "labware": { + "sourcePlateId": { + "slot": "10", + "definitionId": "someTroughDef", + "displayName": "Source (Buffer)" + }, + "destPlateId": { + "slot": "11", + "definitionId": "someTroughDef" + }, + } + } + loaded_labware = execute.load_labware_from_json_defs(ctx, data) + + # objects in loaded_labware should be same objs as labware objs in the deck + assert loaded_labware['sourcePlateId'] == ctx.loaded_labwares[10] + # use the displayName from protocol's labware.labwareId.displayName + assert 'Source (Buffer)' in str(loaded_labware['sourcePlateId']) + assert loaded_labware['destPlateId'] == ctx.loaded_labwares[11] + # use the metadata.displayName from embedded def + assert 'CUSTOM 12 Channel Trough' in str(loaded_labware['destPlateId']) + + +def test_load_labware_trash_v1(loop): ctx = ProtocolContext(loop=loop) data = { "labware": { @@ -206,18 +236,13 @@ def test_load_labware_trash(loop): } } } - result = execute.load_labware_from_json(ctx, data) + result = execute.load_labware_from_json_loadnames(ctx, data) assert result['someTrashId'] == ctx.fixed_trash -def test_blank_protocol(loop): - # Check that this doesn’t throw an exception - ctx = ProtocolContext(loop=loop) - execute.run_protocol(protocol_json={}, context=ctx) - - -protocol_data = { +protocol_v1_data = { + "protocol-schema": "1.0.0", "default-values": { "aspirate-flow-rate": { "p300_single_v1": 101 @@ -266,7 +291,7 @@ def test_blank_protocol(loop): } -def test_dispatch_commands(monkeypatch, loop): +def test_dispatch_commands_v1(monkeypatch, loop): ctx = ProtocolContext(loop=loop) cmd = [] flow_rates = [] @@ -283,7 +308,7 @@ def mock_dispense(volume, location): def mock_set_flow_rate(mount, aspirate=None, dispense=None): flow_rates.append((aspirate, dispense)) - insts = execute.load_pipettes_from_json(ctx, protocol_data) + insts = execute.load_pipettes_from_json(ctx, protocol_v1_data) source_plate = ctx.load_labware_by_name('generic_96_wellplate_380_ul', '1') dest_plate = ctx.load_labware_by_name('generic_96_wellplate_380_ul', '2') @@ -299,8 +324,8 @@ def mock_set_flow_rate(mount, aspirate=None, dispense=None): mock_set_flow_rate) monkeypatch.setattr(ctx, 'delay', mock_sleep) - execute.dispatch_json( - ctx, protocol_data, insts, loaded_labware) + execute.dispatch_json_v1( + ctx, protocol_v1_data, insts, loaded_labware) assert cmd == [ ("aspirate", 5, source_plate.wells_by_index()['A1'].bottom()), diff --git a/shared-data/labware-json-schema/labware-schema.json b/shared-data/labware-json-schema/labware-schema.json index 4bd0138d493..36b37445310 100644 --- a/shared-data/labware-json-schema/labware-schema.json +++ b/shared-data/labware-json-schema/labware-schema.json @@ -1,4 +1,5 @@ { + "$id": "opentronsLabwareSchemaV2", "$schema": "http://json-schema.org/draft-07/schema#", "definitions": { "positiveNumber": { diff --git a/shared-data/protocol-json-schema/protocolSchemaV3.json b/shared-data/protocol-json-schema/protocolSchemaV3.json new file mode 100644 index 00000000000..ad24d97245e --- /dev/null +++ b/shared-data/protocol-json-schema/protocolSchemaV3.json @@ -0,0 +1,383 @@ +{ + "$id": "opentronsProtocolSchemaV3", + "$schema": "http://json-schema.org/draft-07/schema#", + + "definitions": { + "pipetteName": { + "description": "Name of a pipette. Does not contain info about specific model/version. Should match keys in pipetteNameSpecs.json", + "type": "string", + "enum": [ + "p10_single", + "p10_multi", + "p50_single", + "p50_multi", + "p300_single", + "p300_multi", + "p1000_single", + "p1000_multi" + ] + }, + + "mmOffset": { + "description": "Millimeters for pipette location offsets", + "type": "number" + }, + + "flowRateForPipettes": { + "description": "Flow rate in mm/sec for each pipette used in the protocol, by pipette name", + "type": "object", + "propertyNames": {"$ref": "#/definitions/pipetteName"}, + "patternProperties": {".*": {"type": "number", "minimum": 0}}, + "additionalProperties": false + }, + + "offsetFromBottomMm": { + "description": "Offset from bottom of well in millimeters", + "required": ["offsetFromBottomMm"], + "properties": { + "offsetFromBottomMm": {"$ref": "#/definitions/mmOffset"} + } + }, + + "pipetteAccessParams": { + "required": ["pipette", "labware", "well"], + "properties": { + "pipette": { + "type": "string" + }, + "labware": { + "type": "string" + }, + "well": { + "type": "string" + } + } + }, + + "volumeParams": { + "required": ["volume"], + "volume": { + "type": "number" + } + }, + + "flowRate": { + "required": ["flowRate"], + "properties": { + "flowRate": { + "description": "Flow rate in uL/sec", + "type": "number" + } + } + }, + + "slot": { + "description": "Slot on the deck of an OT-2 robot", + "type": "string", + "enum": ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12"] + } + }, + + "type": "object", + "additionalProperties": false, + "required": [ + "schemaVersion", + "metadata", + "robot", + "pipettes", + "labware", + "labwareDefinitions", + "commands" + ], + "properties": { + "schemaVersion": { + "description": "Schema version of a protocol is a single integer", + "enum": [3] + }, + + "metadata": { + "description": "Optional metadata about the protocol", + "type": "object", + + "properties": { + "protocolName": { + "description": "A short, human-readable name for the protocol", + "type": "string" + }, + "author": { + "description": "The author or organization who created the protocol", + "type": "string" + }, + "description": { + "description": "A text description of the protocol.", + "type": ["string", "null"] + }, + + "created": { + "description": "UNIX timestamp when this file was created", + "type": "number" + }, + "lastModified": { + "description": "UNIX timestamp when this file was last modified", + "type": ["number", "null"] + }, + + "category": { + "description": "Category of protocol (eg, \"Basic Pipetting\")", + "type": ["string", "null"] + }, + "subcategory": { + "description": "Subcategory of protocol (eg, \"Cell Plating\")", + "type": ["string", "null"] + }, + "tags": { + "description": "Tags to be used in searching for this protocol", + "type": "array", + "items": { + "type": "string" + } + } + } + }, + + "designerApplication": { + "description": "Optional data & metadata not required to execute the protocol, used by the application that created this protocol", + "type": "object", + "properties": { + "name": { + "description": "Name of the application that created the protocol. Should be namespaced under the organization or individual who owns the organization, eg \"opentrons/protocol-designer\"", + "type": "string" + }, + "version": { + "description": "Version of the application that created the protocol", + "type": "string" + }, + "data": { + "description": "Any data used by the application that created this protocol", + "type": "object" + } + } + }, + + "robot": { + "required": ["model"], + "properties": { + "model": { + "description": "Model of the robot this protocol is written for (currently only OT-2 Standard is supported)", + "type": "string", + "enum": ["OT-2 Standard"] + } + } + }, + + "pipettes": { + "description": "The pipettes used in this protocol, keyed by an arbitrary unique ID", + "additionalProperties": false, + "patternProperties": { + ".+": { + "description": "Fields describing an individual pipette", + "type": "object", + "required": ["mount", "name"], + "additionalProperties": false, + "properties": { + "mount": { + "description": "Where the pipette is mounted", + "type": "string", + "enum": ["left", "right"] + }, + "name": { + "$ref": "#/definitions/pipetteName" + } + } + } + } + }, + + "labwareDefinitions": { + "description": "All labware definitions used by labware in this protocol, keyed by UUID", + "patternProperties": { + ".+": { + "$ref": "opentronsLabwareSchemaV2" + } + } + }, + + "labware": { + "description": "All types of labware used in this protocol, and references to their definitions", + "patternProperties": { + ".+": { + "description": "Fields describing a single labware on the deck", + "type": "object", + "required": ["slot", "definitionId"], + "additionalProperties": false, + "properties": { + "slot": {"$ref": "#/definitions/slot"}, + "definitionId": { + "description": "reference to this labware's ID in \"labwareDefinitions\"", + "type": "string" + }, + "displayName": { + "description": "An optional human-readable nickname for this labware. Eg \"Buffer Trough\"", + "type": "string" + } + } + } + } + }, + + "commands": { + "description": "An array of command objects representing steps to be executed on the robot", + "type": "array", + "items": { + "anyOf": [ + { + "description": "Aspirate / dispense / air gap commands", + "type": "object", + "required": ["command", "params"], + "additionalProperties": false, + "properties": { + "command": { + "enum": ["aspirate", "dispense", "airGap"] + }, + "params": { + "allOf": [ + {"$ref": "#/definitions/flowRate"}, + {"$ref": "#/definitions/pipetteAccessParams"}, + {"$ref": "#/definitions/volumeParams"}, + {"$ref": "#/definitions/offsetFromBottomMm"} + ] + } + } + }, + + { + "description": "Blowout command", + "type": "object", + "required": ["command", "params"], + "additionalProperties": false, + "properties": { + "command": { + "enum": ["blowout"] + }, + "params": { + "allOf": [ + {"$ref": "#/definitions/flowRate"}, + {"$ref": "#/definitions/pipetteAccessParams"}, + {"$ref": "#/definitions/offsetFromBottomMm"} + ] + } + } + }, + + { + "description": "Touch tip commands", + "type": "object", + "required": ["command", "params"], + "additionalProperties": false, + "properties": { + "command": { + "enum": ["touchTip"] + }, + "params": { + "allOf": [ + {"$ref": "#/definitions/pipetteAccessParams"}, + {"$ref": "#/definitions/offsetFromBottomMm"} + ] + } + } + }, + + + { + "description": "Pick up tip / drop tip commands", + "type": "object", + "required": ["command", "params"], + "additionalProperties": false, + "properties": { + "command": { + "enum": ["pickUpTip", "dropTip"] + }, + "params": { + "allOf": [ + {"$ref": "#/definitions/pipetteAccessParams"} + ] + } + } + }, + + { + "description": "Move to slot command. NOTE: this is an EXPERIMENTAL command, its behavior is subject to change in future releases.", + "type": "object", + "required": ["command", "params"], + "additionalProperties": false, + "properties": { + "command": {"enum": ["moveToSlot"]}, + "params": { + "type": "object", + "required": ["pipette", "slot"], + "additionalProperties": false, + "properties": { + "pipette": {"type": "string"}, + "slot": {"$ref": "#/definitions/slot"}, + "offset": { + "description": "Optional offset from slot bottom left corner, in mm", + "type": "object", + "required": ["x", "y", "z"], + "properties": { + "x": {"type": "number"}, + "y": {"type": "number"}, + "z": {"type": "number"} + } + }, + "minimumZHeight": { + "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. Specifying this for movements that would not arc (moving within the same well in the same labware) will cause an arc movement instead. This param only supported in API v2, API v1 will ignore it.", + "type": "number", + "minimum": 0 + }, + "forceDirect": { + "description": "Default is false. 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. In APIv1, this will use strategy='direct', which moves first in X/Y plane and then in Z. In API v2, a 'direct' movement is in X/Y/Z simultaneously", + "type": "boolean" + } + } + } + } + }, + + { + "description": "Delay command", + "type": "object", + "required": ["command", "params"], + "additionalProperties": false, + "properties": { + "command": { + "enum": ["delay"] + }, + "params": { + "type": "object", + "additionalProperties": false, + "required": ["wait"], + "properties": { + "wait": { + "description": "either a number of seconds to wait (fractional values OK), or `true` to wait indefinitely until the user manually resumes the protocol", + "anyOf": [ + {"type": "number"}, + {"enum": [true]} + ] + }, + "message": { + "description": "optional message describing the delay" + } + } + } + } + } + ] + } + }, + + "commandAnnotations": { + "description": "An optional object of annotations associated with commands. Its usage has not yet been defined, so its shape is not enforced by this schema.", + "type": "object" + } + } +}