diff --git a/api/src/opentrons/api/session.py b/api/src/opentrons/api/session.py index 490f90717ff..bc3c6ea9f49 100755 --- a/api/src/opentrons/api/session.py +++ b/api/src/opentrons/api/session.py @@ -9,7 +9,7 @@ from opentrons.commands import tree, types as command_types from opentrons.commands.commands import is_new_loc, listify from opentrons.config import feature_flags as ff -from opentrons.protocols.types import JsonProtocol, PythonProtocol +from opentrons.protocols.types import JsonProtocol, PythonProtocol, APIVersion from opentrons.protocols.parse import parse from opentrons.types import Location, Point from opentrons.protocol_api import (ProtocolContext, @@ -277,12 +277,12 @@ def on_command(message): def refresh(self): self._reset() - self.api_level = 2 if ff.use_protocol_api_v2() else 1 # self.metadata is exposed via jrpc if isinstance(self._protocol, PythonProtocol): + self.api_level = self._protocol.api_level self.metadata = self._protocol.metadata if ff.use_protocol_api_v2()\ - and self._protocol.api_level == '1'\ + and self._protocol.api_level == APIVersion(1, 0)\ and not ff.enable_back_compat(): raise RuntimeError( 'This protocol targets Protocol API V1, but the robot is ' @@ -294,6 +294,10 @@ def refresh(self): log.info(f"Protocol API version: {self._protocol.api_level}") else: + if ff.use_protocol_api_v2(): + self.api_level = APIVersion(2, 0) + else: + self.api_level = APIVersion(1, 0) self.metadata = {} log.info(f"JSON protocol") diff --git a/api/src/opentrons/execute.py b/api/src/opentrons/execute.py index 0682613525a..db5e8422e01 100644 --- a/api/src/opentrons/execute.py +++ b/api/src/opentrons/execute.py @@ -18,7 +18,7 @@ from opentrons import commands from opentrons.config import feature_flags as ff from opentrons.protocols.parse import parse -from opentrons.protocols.types import JsonProtocol +from opentrons.protocols.types import JsonProtocol, APIVersion from opentrons.hardware_control import API _HWCONTROL: Optional[API] = None @@ -179,7 +179,7 @@ def execute(protocol_file: TextIO, contents = protocol_file.read() protocol = parse(contents, protocol_file.name) if isinstance(protocol, JsonProtocol)\ - or protocol.api_level == '2'\ + or protocol.api_level >= APIVersion(2, 0)\ or (ff.enable_back_compat() and ff.use_protocol_api_v2()): context = get_protocol_api( bundled_labware=getattr(protocol, 'bundled_labware', None), diff --git a/api/src/opentrons/protocol_api/execute.py b/api/src/opentrons/protocol_api/execute.py index 6ee76df42b6..0ee9bf3e591 100644 --- a/api/src/opentrons/protocol_api/execute.py +++ b/api/src/opentrons/protocol_api/execute.py @@ -9,7 +9,7 @@ from . import execute_v3, legacy_wrapper from opentrons import config -from opentrons.protocols.types import PythonProtocol, Protocol +from opentrons.protocols.types import PythonProtocol, Protocol, APIVersion MODULE_LOG = logging.getLogger(__name__) @@ -169,9 +169,9 @@ def run_protocol(protocol: Protocol, raise RuntimeError( 'Will not automatically generate hardware controller') if isinstance(protocol, PythonProtocol): - if protocol.api_level == '2': + if protocol.api_level >= APIVersion(2, 0): _run_python(protocol, true_context) - elif protocol.api_level == '1': + elif protocol.api_level == APIVersion(1, 0): _run_python_legacy(protocol, true_context) else: raise RuntimeError( diff --git a/api/src/opentrons/protocols/parse.py b/api/src/opentrons/protocols/parse.py index 14cc6dc98db..608e9b3b02f 100644 --- a/api/src/opentrons/protocols/parse.py +++ b/api/src/opentrons/protocols/parse.py @@ -6,6 +6,7 @@ import itertools import json import pkgutil +import re from io import BytesIO from zipfile import ZipFile from typing import Any, Dict, Union @@ -13,9 +14,12 @@ import jsonschema # type: ignore from opentrons.config import feature_flags as ff -from .types import Protocol, PythonProtocol, JsonProtocol, Metadata +from .types import Protocol, PythonProtocol, JsonProtocol, Metadata, APIVersion from .bundle import extract_bundle +# match e.g. "2.0" but not "hi", "2", "2.0.1" +API_VERSION_RE = re.compile(r'^(\d+)\.(\d+)$') + def _parse_json( protocol_contents: str, filename: str = None) -> JsonProtocol: @@ -46,7 +50,7 @@ def _parse_python( filename=ast_filename) metadata = extract_metadata(parsed) protocol = compile(parsed, filename=ast_filename, mode='exec') - version = infer_version(metadata, parsed) + version = get_version(metadata, parsed) result = PythonProtocol( text=protocol_contents, @@ -78,7 +82,7 @@ def _parse_bundle(bundle: ZipFile, filename: str = None) -> PythonProtocol: # n contents.bundled_data, contents.bundled_python) - if result.api_level != '2': + if result.api_level < APIVersion(2, 0): raise RuntimeError('Bundled protocols must use Protocol API V2, ' + f'got {result.api_level}') @@ -155,7 +159,7 @@ def extract_metadata(parsed: ast.Module) -> Metadata: return metadata -def infer_version_from_imports(parsed: ast.Module) -> str: +def infer_version_from_imports(parsed: ast.Module) -> APIVersion: # Imports in the form of `import opentrons.robot` will have an entry in # parsed.body[i].names[j].name in the form "opentrons.robot". Find those # imports and transform them to strip away the 'opentrons.' part. @@ -182,12 +186,31 @@ def infer_version_from_imports(parsed: ast.Module) -> str: v1_markers = set(('robot', 'instruments', 'modules', 'containers')) v1evidence = v1_markers.intersection(opentrons_imports) if v1evidence: - return '1' + return APIVersion(1, 0) else: - return '2' + raise RuntimeError('Cannot infer API level') + +def version_from_metadata(metadata: Metadata) -> APIVersion: + """ Build an API version from metadata, if we can. -def infer_version(metadata: Metadata, parsed: ast.Module) -> str: + If there is no apiLevel key, raise a KeyError. + If the apiLevel value is malformed, raise a ValueError. + """ + if 'apiLevel' not in metadata: + raise KeyError('apiLevel') + requested_level = str(metadata['apiLevel']) + if requested_level == '1': + return APIVersion(1, 0) + matches = API_VERSION_RE.match(requested_level) + if not matches: + raise ValueError( + f'apiLevel {requested_level} is incorrectly formatted. It should ' + 'major.minor, where both major and minor are numbers.') + return APIVersion(major=int(matches.group(1)), minor=int(matches.group(2))) + + +def get_version(metadata: Metadata, parsed: ast.Module) -> APIVersion: """ Infer protocol API version based on a combination of metadata and imports. @@ -203,10 +226,17 @@ def infer_version(metadata: Metadata, parsed: ast.Module) -> str: APIv2 protocol (note that 'labware' is not in this list, as there is a valid APIv2 import named 'labware'). """ - level = str(metadata.get('apiLevel')) - if level in ('1', '2'): - return level - return infer_version_from_imports(parsed) + try: + return version_from_metadata(metadata) + except KeyError: # No apiLevel key, may be apiv1 + pass + try: + return infer_version_from_imports(parsed) + except RuntimeError: + raise RuntimeError( + 'If this is not an API v1 protocol, you must specify the target ' + 'api level in the apiLevel key of the metadata. For instance, ' + 'metadata={"apiLevel": "2.0"}') def _get_protocol_schema_version(protocol_json: Dict[Any, Any]) -> int: diff --git a/api/src/opentrons/protocols/types.py b/api/src/opentrons/protocols/types.py index a0ebb6ed84d..4ac56b6fe31 100644 --- a/api/src/opentrons/protocols/types.py +++ b/api/src/opentrons/protocols/types.py @@ -3,6 +3,14 @@ Metadata = Dict[str, Union[str, int]] +class APIVersion(NamedTuple): + major: int + minor: int + + def __str__(self): + return f'{self.major}.{self.minor}' + + class JsonProtocol(NamedTuple): text: str filename: Optional[str] @@ -15,7 +23,7 @@ class PythonProtocol(NamedTuple): filename: Optional[str] contents: Any # This is the output of compile() which we can't type metadata: Metadata - api_level: str # For now, should be '1' or '2' + api_level: APIVersion # these 'bundled_' attrs should only be included when the protocol is a zip bundled_labware: Optional[Dict[str, Dict[str, Any]]] bundled_data: Optional[Dict[str, bytes]] diff --git a/api/src/opentrons/simulate.py b/api/src/opentrons/simulate.py index 6016c86ceab..23d7a259496 100644 --- a/api/src/opentrons/simulate.py +++ b/api/src/opentrons/simulate.py @@ -21,7 +21,7 @@ from opentrons import protocol_api from opentrons.protocols import parse, bundle from opentrons.protocols.types import ( - JsonProtocol, PythonProtocol, BundleContents) + JsonProtocol, PythonProtocol, BundleContents, APIVersion) from opentrons.protocol_api import execute from .util.entrypoint_util import labware_from_paths, datafiles_from_paths @@ -249,7 +249,7 @@ def simulate(protocol_file: TextIO, extra_data=extra_data) if isinstance(protocol, JsonProtocol)\ - or protocol.api_level == '2'\ + or protocol.api_level >= APIVersion(2, 0)\ or (ff.enable_back_compat() and ff.use_protocol_api_v2()): context = get_protocol_api(protocol) scraper = CommandScraper(stack_logger, log_level, context.broker) diff --git a/api/tests/opentrons/api/test_session.py b/api/tests/opentrons/api/test_session.py index 70d9c5d5181..f6dcadf2a57 100755 --- a/api/tests/opentrons/api/test_session.py +++ b/api/tests/opentrons/api/test_session.py @@ -88,9 +88,22 @@ async def test_async_notifications(main_router): assert res == {'name': 'foo', 'payload': {'bar': 'baz'}} -def test_load_protocol_with_error(session_manager): +@pytest.mark.api2_only +def test_load_protocol_with_error_v2(session_manager, hardware): + with pytest.raises(Exception) as e: + session = session_manager.create( + name='', contents='metadata={"apiLevel": "2.0"}; blah') + assert session is None + + args, = e.value.args + assert args == "name 'blah' is not defined" + + +@pytest.mark.api1_only +def test_load_protocol_with_error_v1(session_manager, hardware): with pytest.raises(Exception) as e: - session = session_manager.create(name='', contents='blah') + session = session_manager.create( + name='', contents='metadata={"apiLevel": "1.0"}; blah') assert session is None args, = e.value.args @@ -231,7 +244,8 @@ async def test_get_instruments_and_containers(labware_setup, instruments, containers, modules, interactions = \ _accumulate([_get_labware(command) for command in commands]) - session = session_manager.create(name='', contents='') + session = session_manager.create( + name='', contents='from opentrons import instruments') # We are calling dedupe directly for testing purposes. # Normally it is called from within a session session._instruments.extend(_dedupe(instruments)) @@ -357,7 +371,7 @@ async def test_session_create_error(main_router): with pytest.raises(SyntaxError): main_router.session_manager.create( name='', - contents='syntax error ;(') + contents='from opentrons import instruments; syntax error ;(') with pytest.raises(TimeoutError): # No state change is expected @@ -366,20 +380,22 @@ async def test_session_create_error(main_router): with pytest.raises(ZeroDivisionError): main_router.session_manager.create( name='', - contents='1/0') + contents='from opentrons import instruments; 1/0') with pytest.raises(TimeoutError): # No state change is expected await main_router.wait_until(lambda _: True) -async def test_session_metadata(main_router): +@pytest.mark.api1_only +async def test_session_metadata_v1(main_router): expected = { 'hello': 'world', 'what?': 'no' } prot = """ +from opentrons import instruments this = 0 that = 1 metadata = { @@ -387,6 +403,31 @@ async def test_session_metadata(main_router): 'hello': 'world' } print('wat?') +""" + + session = main_router.session_manager.create( + name='', + contents=prot) + assert session.metadata == expected + + +@pytest.mark.api2_only +async def test_session_metadata_v2(main_router): + expected = { + 'hello': 'world', + 'what?': 'no', + 'apiLevel': '2.0' + } + + prot = """ +this = 0 +that = 1 +metadata = { +'what?': 'no', +'hello': 'world', +'apiLevel': '2.0' +} +print('wat?') def run(ctx): print('hi there') diff --git a/api/tests/opentrons/conftest.py b/api/tests/opentrons/conftest.py index 4d7d7b3cd01..2f93f9d3799 100755 --- a/api/tests/opentrons/conftest.py +++ b/api/tests/opentrons/conftest.py @@ -646,7 +646,8 @@ def _get_bundle_protocol_fixture(fixture_name): result['bundled_python'] = {} # NOTE: this is copy-pasted from the .py fixture file - result['metadata'] = {'author': 'MISTER FIXTURE'} + result['metadata'] = {'author': 'MISTER FIXTURE', + 'apiLevel': '2.0'} # make binary zipfile binary_zipfile = io.BytesIO() diff --git a/api/tests/opentrons/data/testosaur-gen2-v2.py b/api/tests/opentrons/data/testosaur-gen2-v2.py index 8faa616a826..50654a20813 100644 --- a/api/tests/opentrons/data/testosaur-gen2-v2.py +++ b/api/tests/opentrons/data/testosaur-gen2-v2.py @@ -4,7 +4,8 @@ 'protocolName': 'Testosaur', 'author': 'Opentrons ', 'description': 'A variant on "Dinosaur" for testing', - 'source': 'Opentrons Repository' + 'source': 'Opentrons Repository', + 'apiLevel': '2.0' } diff --git a/api/tests/opentrons/data/testosaur_v2.py b/api/tests/opentrons/data/testosaur_v2.py index b404d038cdc..dbe06d79b45 100644 --- a/api/tests/opentrons/data/testosaur_v2.py +++ b/api/tests/opentrons/data/testosaur_v2.py @@ -4,7 +4,8 @@ 'protocolName': 'Testosaur', 'author': 'Opentrons ', 'description': 'A variant on "Dinosaur" for testing', - 'source': 'Opentrons Repository' + 'source': 'Opentrons Repository', + 'apiLevel': '2.0', } diff --git a/api/tests/opentrons/protocol_api/test_execute.py b/api/tests/opentrons/protocol_api/test_execute.py index d4253247a04..c7fff521e81 100644 --- a/api/tests/opentrons/protocol_api/test_execute.py +++ b/api/tests/opentrons/protocol_api/test_execute.py @@ -6,11 +6,14 @@ ("""from opentrons import labware, instruments, robot"""), (""" metadata = { - "apiLevel": '1' + "apiLevel": '1.0' } import opentrons opentrons.robot - """)] + """), + (""" +metadata = {"apiLevel": '1'} +""")] def test_api2_runfunc(): @@ -57,17 +60,28 @@ def test_execute_v1_imports(protocol, ensure_api2): def test_bad_protocol(ensure_api2, loop): ctx = ProtocolContext(loop) - no_run = parse('print("hi")') + no_run = parse(''' +metadata={"apiLevel": "2.0"} +print("hi") +''') with pytest.raises(execute.MalformedProtocolError) as e: execute.run_protocol(no_run, context=ctx) assert "No function 'run" in str(e.value) - no_args = parse('def run(): pass') + no_args = parse(''' +metadata={"apiLevel": "2.0"} +def run(): + pass +''') with pytest.raises(execute.MalformedProtocolError) as e: execute.run_protocol(no_args, context=ctx) assert "Function 'run()' does not take any parameters" in str(e.value) - many_args = parse('def run(a, b): pass') + many_args = parse(''' +metadata={"apiLevel": "2.0"} +def run(a, b): + pass +''') with pytest.raises(execute.MalformedProtocolError) as e: execute.run_protocol(many_args, context=ctx) assert "must be called with more than one argument" in str(e.value) @@ -75,7 +89,8 @@ def test_bad_protocol(ensure_api2, loop): def test_proto_with_exception(ensure_api2, loop): ctx = ProtocolContext(loop) - exc_in_root = ''' + exc_in_root = '''metadata={"apiLevel": "2.0"} + def run(ctx): raise Exception("hi") ''' @@ -84,7 +99,7 @@ def run(ctx): execute.run_protocol( protocol, context=ctx) - assert 'Exception [line 3]: hi' in str(e.value) + assert 'Exception [line 4]: hi' in str(e.value) nested_exc = ''' import ast @@ -94,6 +109,8 @@ def this_throws(): def run(ctx): this_throws() + +metadata={"apiLevel": "2.0"}; ''' protocol = parse(nested_exc) with pytest.raises(execute.ExceptionInProtocolError) as e: diff --git a/api/tests/opentrons/protocols/fixtures/bundled_protocols/simple_bundle/protocol.py b/api/tests/opentrons/protocols/fixtures/bundled_protocols/simple_bundle/protocol.py index 6a1238db2cc..d1fe6ded84c 100755 --- a/api/tests/opentrons/protocols/fixtures/bundled_protocols/simple_bundle/protocol.py +++ b/api/tests/opentrons/protocols/fixtures/bundled_protocols/simple_bundle/protocol.py @@ -1,4 +1,5 @@ -metadata = {'author': 'MISTER FIXTURE'} +metadata = {'author': 'MISTER FIXTURE', + 'apiLevel': '2.0'} def run(protocol_context): diff --git a/api/tests/opentrons/protocols/test_parse.py b/api/tests/opentrons/protocols/test_parse.py index 1acb7955c93..401bf055590 100644 --- a/api/tests/opentrons/protocols/test_parse.py +++ b/api/tests/opentrons/protocols/test_parse.py @@ -7,8 +7,11 @@ from opentrons.protocols.parse import (extract_metadata, _get_protocol_schema_version, validate_json, - parse) -from opentrons.protocols.types import (JsonProtocol, PythonProtocol) + parse, + version_from_metadata) +from opentrons.protocols.types import (JsonProtocol, + PythonProtocol, + APIVersion) def test_extract_metadata(): @@ -43,17 +46,17 @@ def test_extract_metadata(): from opentrons import instruments p = instruments.P10_Single(mount='right') -""", '1'), +""", APIVersion(1, 0)), (""" import opentrons.instruments p = instruments.P10_Single(mount='right') -""", '1'), +""", APIVersion(1, 0)), (""" from opentrons import instruments as instr p = instr.P10_Single(mount='right') -""", '1'), +""", APIVersion(1, 0)), (""" from opentrons import instruments @@ -62,22 +65,26 @@ def test_extract_metadata(): } p = instruments.P10_Single(mount='right') -""", '1'), +""", APIVersion(1, 0)), (""" from opentrons import instruments metadata = { - 'apiLevel': '2' + 'apiLevel': '2.0' } p = instruments.P10_Single(mount='right') -""", '2'), +""", APIVersion(2, 0)), (""" from opentrons import types +metadata = { + 'apiLevel': '2.0' +} + def run(ctx): right = ctx.load_instrument('p300_single', types.Mount.RIGHT) -""", '2'), +""", APIVersion(2, 0)), (""" from opentrons import types @@ -87,44 +94,74 @@ def run(ctx): def run(ctx): right = ctx.load_instrument('p300_single', types.Mount.RIGHT) -""", '1'), +""", APIVersion(1, 0)), (""" from opentrons import types metadata = { - 'apiLevel': '2' + 'apiLevel': '2.0' } def run(ctx): right = ctx.load_instrument('p300_single', types.Mount.RIGHT) -""", '2'), +""", APIVersion(2, 0)), (""" from opentrons import labware, instruments p = instruments.P10_Single(mount='right') - """, '1'), + """, APIVersion(1, 0)), (""" from opentrons import types, containers - """, '1'), + """, APIVersion(1, 0)), (""" from opentrons import types, instruments p = instruments.P10_Single(mount='right') - """, '1'), + """, APIVersion(1, 0)), (""" from opentrons import instruments as instr p = instr.P300_Single('right') - """, '1') + """, APIVersion(1, 0)) ] @pytest.mark.parametrize('proto,version', infer_version_cases) -def test_infer_version(proto, version): +def test_get_version(proto, version): parsed = parse(proto) assert parsed.api_level == version +test_valid_metadata = [ + ({'apiLevel': '1'}, APIVersion(1, 0)), + ({'apiLevel': '1.0'}, APIVersion(1, 0)), + ({'apiLevel': '1.2'}, APIVersion(1, 2)), + ({'apiLevel': '2.0'}, APIVersion(2, 0)), + ({'apiLevel': '2.6'}, APIVersion(2, 6)), + ({'apiLevel': '10.23123151'}, APIVersion(10, 23123151)) +] + + +test_invalid_metadata = [ + ({}, KeyError), + ({'sasdaf': 'asdaf'}, KeyError), + ({'apiLevel': '2'}, ValueError), + ({'apiLevel': '2.0.0'}, ValueError), + ({'apiLevel': 'asda'}, ValueError) +] + + +@pytest.mark.parametrize('metadata,version', test_valid_metadata) +def test_valid_version_from_metadata(metadata, version): + assert version_from_metadata(metadata) == version + + +@pytest.mark.parametrize('metadata,exc', test_invalid_metadata) +def test_invalid_version_from_metadata(metadata, exc): + with pytest.raises(exc): + version_from_metadata(metadata) + + def test_get_protocol_schema_version(): assert _get_protocol_schema_version({'protocol-schema': '1.0.0'}) == 1 assert _get_protocol_schema_version({'protocol-schema': '2.0.0'}) == 2 @@ -171,16 +208,25 @@ def test_parse_python_details( assert isinstance(parsed, PythonProtocol) assert parsed.text == protocol.text assert isinstance(parsed.text, str) - version = '2' if '2' in protocol.filename else '1' - assert parsed.api_level == version fname = fake_fname if fake_fname else '' assert parsed.filename == fname - assert parsed.metadata == { - 'protocolName': 'Testosaur', - 'author': 'Opentrons ', - 'description': 'A variant on "Dinosaur" for testing', - 'source': 'Opentrons Repository' - } + if '2' in protocol.filename: + assert parsed.api_level == APIVersion(2, 0) + assert parsed.metadata == { + 'protocolName': 'Testosaur', + 'author': 'Opentrons ', + 'description': 'A variant on "Dinosaur" for testing', + 'source': 'Opentrons Repository', + 'apiLevel': '2.0' + } + else: + assert parsed.api_level == APIVersion(1, 0) + assert parsed.metadata == { + 'protocolName': 'Testosaur', + 'author': 'Opentrons ', + 'description': 'A variant on "Dinosaur" for testing', + 'source': 'Opentrons Repository', + } assert parsed.contents == compile( protocol.text, filename=fname, @@ -223,7 +269,7 @@ def test_parse_bundle_details(get_bundle_fixture, ensure_api2): assert parsed.bundled_python == fixture['bundled_python'] assert parsed.bundled_data == fixture['bundled_data'] assert parsed.metadata == fixture['metadata'] - assert parsed.api_level == '2' + assert parsed.api_level == APIVersion(2, 0) @pytest.mark.parametrize('protocol_file', diff --git a/app/src/components/CalibrateLabware/ConfirmModalContents.js b/app/src/components/CalibrateLabware/ConfirmModalContents.js index 976bd2388de..d21d350a16a 100644 --- a/app/src/components/CalibrateLabware/ConfirmModalContents.js +++ b/app/src/components/CalibrateLabware/ConfirmModalContents.js @@ -61,6 +61,6 @@ function mapStateToProps(state, ownProps: OP): SP { const calibrator = pipettes.find(i => i.mount === calibratorMount) || robotSelectors.getCalibrator(state) - const useCenteredTroughs = robotSelectors.getApiLevel(state) > 1 + const useCenteredTroughs = robotSelectors.getApiLevel(state)[0] > 1 return { calibrator, useCenteredTroughs } } diff --git a/app/src/robot/actions.js b/app/src/robot/actions.js index 7578954e3e1..b1b4b4cbc1f 100755 --- a/app/src/robot/actions.js +++ b/app/src/robot/actions.js @@ -133,7 +133,7 @@ export type SessionResponseAction = {| name: string, protocolText: string, metadata?: ?$PropertyType, - apiLevel: number, + apiLevel: [number, number], |}, meta: {| freshUpload: boolean |}, |} diff --git a/app/src/robot/api-client/client.js b/app/src/robot/api-client/client.js index 6e57085f429..a390347249a 100755 --- a/app/src/robot/api-client/client.js +++ b/app/src/robot/api-client/client.js @@ -471,7 +471,17 @@ export default function client(dispatch) { ) } - update.apiLevel = apiSession.api_level || 1 + if (Array.isArray(apiSession.api_level)) { + update.apiLevel = apiSession.api_level + } else if (apiSession.api_level) { + // if we're connected to a robot on older software that still expresses + // its api level as a single int, it's the major version + update.apiLevel = [apiSession.api_level, 0] + } else { + // if we're connected to a robot on software sufficiently old that it + // doesn't send us its api level at all, it's on API v1 + update.apiLevel = [1, 0] + } dispatch(actions.sessionResponse(null, update, freshUpload)) } catch (error) { diff --git a/app/src/robot/reducer/session.js b/app/src/robot/reducer/session.js index e1328bcf060..10e17593603 100644 --- a/app/src/robot/reducer/session.js +++ b/app/src/robot/reducer/session.js @@ -46,7 +46,7 @@ export type SessionState = { remoteTimeCompensation: number | null, startTime: ?number, runTime: number, - apiLevel: number, + apiLevel: [number, number], } // TODO(mc, 2018-01-11): replace actionType constants with Flow types @@ -83,7 +83,7 @@ const INITIAL_STATE: SessionState = { remoteTimeCompensation: null, startTime: null, runTime: 0, - apiLevel: 1, + apiLevel: [1, 0], } export default function sessionReducer( diff --git a/app/src/robot/test/api-client.test.js b/app/src/robot/test/api-client.test.js index 503f121a1b9..66e2bbbdca1 100755 --- a/app/src/robot/test/api-client.test.js +++ b/app/src/robot/test/api-client.test.js @@ -323,7 +323,7 @@ describe('api client', () => { pipettesByMount: {}, labwareBySlot: {}, modulesBySlot: {}, - apiLevel: 1, + apiLevel: [1, 0], }, false ) diff --git a/app/src/robot/test/session-reducer.test.js b/app/src/robot/test/session-reducer.test.js index b0ea05eb22a..1939f5cc0a4 100644 --- a/app/src/robot/test/session-reducer.test.js +++ b/app/src/robot/test/session-reducer.test.js @@ -33,7 +33,7 @@ describe('robot reducer - session', () => { remoteTimeCompensation: null, startTime: null, runTime: 0, - apiLevel: 1, + apiLevel: [1, 0], }) })