示例#1
0
    def tip_probe(self, instrument):
        inst = instrument._instrument
        log.info('Probing tip with {}'.format(instrument.name))
        self._set_state('probing')

        if ff.use_protocol_api_v2():
            mount = Mount[instrument._instrument.mount.upper()]
            assert instrument.tip_racks,\
                'No known tipracks for {}'.format(instrument)
            tip_length = inst._tip_length_for(
                instrument.tip_racks[0]._container)
            # TODO (tm, 2019-04-22): This warns "coroutine not awaited" in
            # TODO: test. The test fixture probably needs to be modified to get
            # TODO: a synchronous adapter instead of a raw hardware_control API
            measured_center = self._hardware.locate_tip_probe_center(
                mount, tip_length)
        else:
            measured_center = calibration_functions.probe_instrument(
                instrument=inst, robot=inst.robot)

        log.info('Measured probe top center: {0}'.format(measured_center))

        if ff.use_protocol_api_v2():
            self._hardware.update_instrument_offset(
                Mount[instrument._instrument.mount.upper()],
                from_tip_probe=measured_center)
            config = self._hardware.config
        else:
            config = calibration_functions.update_instrument_config(
                instrument=inst, measured_center=measured_center)

        log.info('New config: {0}'.format(config))

        self.move_to_front(instrument)
        self._set_state('ready')
示例#2
0
def initialize_robot(loop):
    packed_smoothie_fw_file, packed_smoothie_fw_ver = _find_smoothie_file()
    try:
        if ff.use_protocol_api_v2():
            hardware.connect(force=True)
        else:
            hardware.connect()
    except Exception as e:
        # The most common reason for this exception (aside from hardware
        # failures such as a disconnected smoothie) is that the smoothie
        # is in programming mode. If it is, then we still want to update
        # it (so it can boot again), but we don’t have to do the GPIO
        # manipulations that _put_ it in programming mode
        log.exception("Error while connecting to motor driver: {}".format(e))
        fw_version = None
    else:
        if ff.use_protocol_api_v2():
            fw_version = loop.run_until_complete(hardware.fw_version)
        else:
            fw_version = hardware.fw_version
    log.info("Smoothie FW version: {}".format(fw_version))
    if fw_version != packed_smoothie_fw_ver:
        log.info("Executing smoothie update: current vers {}, packed vers {}".
                 format(fw_version, packed_smoothie_fw_ver))
        loop.run_until_complete(
            _do_fw_update(packed_smoothie_fw_file, packed_smoothie_fw_ver))
        if ff.use_protocol_api_v2():
            hardware.connect(force=True)
        else:
            hardware.connect()
    else:
        log.info("FW version OK: {}".format(packed_smoothie_fw_ver))
    log.info(f"Name: {name()}")
示例#3
0
async def reset(request: web.Request) -> web.Response:  # noqa(C901)
    """ Execute a reset of the requested parts of the user configuration.
    """
    data = await request.json()
    ok, bad_key = _check_reset(data)
    if not ok:
        return web.json_response(
            {'message': '{} is not a valid reset option'.format(bad_key)},
            status=400)
    log.info("Reset requested for {}".format(', '.join(data.keys())))
    if data.get('tipProbe'):
        config = rc.load()
        if ff.use_protocol_api_v2():
            config = config._replace(
                instrument_offset=rc.build_fallback_instrument_offset({}))
        else:
            config.tip_length.clear()
        rc.save_robot_settings(config)
    if data.get('labwareCalibration'):
        labware.clear_calibrations()
        if not ff.use_protocol_api_v2():
            db.reset()

    if data.get('customLabware'):
        labware.delete_all_custom_labware()

    if data.get('bootScripts'):
        if IS_ROBOT:
            if os.path.exists('/data/boot.d'):
                shutil.rmtree('/data/boot.d')
        else:
            log.debug('Not on pi, not removing /data/boot.d')
    return web.json_response({}, status=200)
示例#4
0
    def switch_mounts(self):
        if not feature_flags.use_protocol_api_v2():
            self.select_home(self._current_mount)
            r_pipette = right
            l_pipette = left
        else:
            r_pipette = types.Mount.RIGHT
            l_pipette = types.Mount.LEFT
            axes = right if self._current_mount == r_pipette else left
            self.select_home(axes)

        if self._current_mount == r_pipette:
            self._current_mount = l_pipette
        else:
            self._current_mount = r_pipette
        self.move_to_safe_height()
        if self._pipettes[self._current_mount]:
            self._tip_length =\
                self._pipettes[self._current_mount]._fallback_tip_length
            self.model_offset =\
                self._pipettes[self._current_mount].model_offset
            if not feature_flags.use_protocol_api_v2():
                self._expected_points = self.set_deck_height_expected_points(
                    self._tip_length)
                self._test_points = self.set_deck_height_test_points(
                    self._tip_length)
            return f"Switched mount to {self._current_mount}"
        else:
            return ("Switched mount, but please add pipette\n"
                    f"to {self._current_mount}")
示例#5
0
async def get_attached_pipettes(request):
    """
    Query robot for model strings on 'left' and 'right' mounts, and return a
    dict with the results keyed by mount. By default, this endpoint provides
    cached values, which will not interrupt a running session. WARNING: if the
    caller supplies the "refresh=true" query parameter, this method will
    interrupt a sequence of Smoothie operations that are in progress, such as a
    protocol run.

    Example:

    ```
    {
      'left': {
        'model': 'p300_single_v1',
        'name': 'p300_single',
        'tip_length': 51.7,
        'mount_axis': 'z',
        'plunger_axis': 'b',
        'id': '<pipette id string>'
      },
      'right': {
        'model': 'p10_multi_v1',
        'name': 'p10_multi',
        'tip_length': 40,
        'mount_axis': 'a',
        'plunger_axis': 'c',
        'id': '<pipette id string>'
      }
    }
    ```

    If a pipette is "uncommissioned" (e.g.: does not have a model string
    written to on-board memory), or if no pipette is present, the corresponding
    mount will report `'model': null`
    """
    hw = hw_from_req(request)
    if request.url.query.get('refresh') == 'true':
        if ff.use_protocol_api_v2():
            await hw.cache_instruments()
        else:
            hw.cache_instrument_models()
    response = {}
    if ff.use_protocol_api_v2():
        attached = await hw.get_attached_pipettes()
    else:
        attached = hw.get_attached_pipettes()
    for mount, data in attached.items():
        response[mount] = {
            'model': data['model'],
            'name': data['name'],
            'mount_axis': str(data['mount_axis']).lower(),
            'plunger_axis': str(data['plunger_axis']).lower(),
            'id': data['id']
        }
        if 'tip_length' in data:
            response[mount]['tip_length'] = data.get('tip_length', 0)
    return web.json_response(response, status=200)
示例#6
0
async def home(request):
    """
    This initializes a call to pipette.home() which, as a side effect will:
        1. Check the pipette is actually connected (will throw an error if you
        try to home a non-connected pipette)
        2. Re-engages the motor
    :param request: Information obtained from a POST request.
        The content type is application/json.
        The correct packet form should be as follows:
        {
        'target': Can be, 'robot' or 'pipette'
        'mount': 'left' or 'right', only used if target is pipette
        }
    :return: A success or non-success message.
    """
    hw = hw_from_req(request)
    req = await request.text()
    data = json.loads(req)
    target = data.get('target')
    if target == 'robot':
        if ff.use_protocol_api_v2():
            await hw.home()
        else:
            hw.home()
        status = 200
        message = "Homing robot."
    elif target == 'pipette':
        mount = data.get('mount')
        if mount in ['left', 'right']:
            if ff.use_protocol_api_v2():
                await hw.home([Axis.by_mount(Mount[mount.upper()])])
                await hw.home_plunger(Mount[mount.upper()])
                status = 200
                message = 'Pipette on {} homed successfuly'.format(mount)
            else:
                pipette, should_remove = _fetch_or_create_pipette(hw,
                                                                  mount)
                pipette.home()
                if should_remove:
                    hw.remove_instrument(mount)
                status = 200
                message = "Pipette on {} homed successfully.".format(mount)
        else:
            status = 400
            message = "Expected 'left' or 'right' as values for mount" \
                      "got {} instead.".format(mount)
    else:
        status = 400
        message = "Expected 'robot' or 'pipette' got {}.".format(target)

    return web.json_response({"message": message}, status=status)
示例#7
0
def run(hardware, **kwargs):  # noqa(C901)
    """
    This function was necessary to separate from main() to accommodate for
    server startup path on system 3.0, which is server.main. In the case where
    the api is on system 3.0, server.main will redirect to this function with
    an additional argument of 'patch_old_init'. kwargs are hence used to allow
    the use of different length args
    """
    loop = asyncio.get_event_loop()

    if ff.use_protocol_api_v2():
        robot_conf = loop.run_until_complete(hardware.get_config())
    else:
        robot_conf = hardware.config

    logging_config.log_init(robot_conf.log_level)

    log.info("API server version:  {}".format(__version__))
    if not os.environ.get("ENABLE_VIRTUAL_SMOOTHIE"):
        initialize_robot(loop, hardware)
        if ff.use_protocol_api_v2():
            loop.run_until_complete(hardware.cache_instruments())
        if not ff.disable_home_on_boot():
            log.info("Homing Z axes")
            if ff.use_protocol_api_v2():
                loop.run_until_complete(hardware.home_z())
            else:
                hardware.home_z()
        try:
            udev.setup_rules_file()
        except Exception:
            log.exception(
                "Could not setup udev rules, modules may not be detected")

    if kwargs.get('hardware_server'):
        if ff.use_protocol_api_v2():
            loop.run_until_complete(
                install_hardware_server(kwargs['hardware_server_socket'],
                                        hardware._api))
        else:
            log.warning(
                "Hardware server requested but apiv1 selected, not starting")
    server.run(
        hardware,
        kwargs.get('hostname'),
        kwargs.get('port'),
        kwargs.get('path'),
        loop)
示例#8
0
def main():
    """ The main entrypoint for the Opentrons robot API server stack.

    This function
    - creates and starts the server for both the RPC routes
      handled by :py:mod:`opentrons.server.rpc` and the HTTP routes handled
      by :py:mod:`opentrons.server.http`
    - initializes the hardware interaction handled by either
      :py:mod:`opentrons.legacy_api` or :py:mod:`opentrons.hardware_control`

    This function does not return until the server is brought down.
    """

    arg_parser = ArgumentParser(description="Opentrons robot software",
                                parents=[build_arg_parser()])
    arg_parser.add_argument('--hardware-server',
                            action='store_true',
                            help='Run a jsonrpc server allowing rpc to the'
                            ' hardware controller. Only works on buildroot '
                            'because extra dependencies are required.')
    arg_parser.add_argument('--hardware-server-socket',
                            action='store',
                            default='/var/run/opentrons-hardware.sock',
                            help='Override for the hardware server socket')
    args = arg_parser.parse_args()

    if ff.use_protocol_api_v2():
        checked_hardware = adapters.SingletonAdapter(asyncio.get_event_loop())
    else:
        checked_hardware = opentrons.hardware
    run(checked_hardware, **vars(args))
    arg_parser.exit(message="Stopped\n")
示例#9
0
async def test_health(virtual_smoothie_env, loop, async_client):

    expected = json.dumps({
        'name':
        'opentrons-dev',
        'api_version':
        __version__,
        'fw_version':
        'Virtual Smoothie',
        'board_revision':
        '2.1',
        'logs': ['/logs/serial.log', '/logs/api.log'],
        'system_version':
        '0.0.0',
        'protocol_api_version':
        list(protocol_api.MAX_SUPPORTED_VERSION)
        if ff.use_protocol_api_v2() else [1, 0],
        "links": {
            "apiLog": "/logs/api.log",
            "serialLog": "/logs/serial.log",
            "apiSpec": "/openapi"
        }
    })
    resp = await async_client.get('/health')
    text = await resp.text()
    assert resp.status == 200
    assert text == expected
示例#10
0
 def update_pipette_models(self):
     if feature_flags.use_protocol_api_v2():
         self.hardware.cache_instruments()
         cached = self.hardware.get_attached_instruments()
         pip_func = None
     else:
         from opentrons import instruments
         self.hardware.cache_instrument_models()
         cached = self.hardware.get_attached_pipettes()
         pip_func = instruments.pipette_by_name
     for mount, attached in cached.items():
         if mount == 'left':
             mount_key = left
         elif mount == 'right':
             mount_key = right
         else:
             mount_key = mount
         if attached.get('name') and pip_func:
             if not self._pipettes.get(mount_key):
                 self._pipettes[mount_key] = pip_func(
                     mount, attached['name'])
         elif attached.get('name'):
             self._pipettes[mount_key] =\
                 self.hardware._attached_instruments[mount_key]
         else:
             self._pipettes[mount_key] = None
示例#11
0
async def detach_tip(data):
    """
    Detach the tip from the current pipette

    :param data: Information obtained from a POST request.
    The content type is application/json.
    The correct packet form should be as follows:
    {
      'token': UUID token from current session start
      'command': 'detach tip'
    }
    """
    global session

    if not feature_flags.use_protocol_api_v2():
        pipette = session.pipettes[session.current_mount]
        if not pipette.tip_attached:
            log.warning('detach tip called with no tip')
        pipette._remove_tip(session.tip_length)
    else:
        session.adapter.remove_tip(session.current_mount)
        if session.cp == CriticalPoint.TIP:
            session.cp = CriticalPoint.NOZZLE
    session.tip_length = None

    return web.json_response({'message': "Tip removed"}, status=200)
示例#12
0
async def save_z(data):
    """
    Save the current Z height value for the calibration data

    :param data: Information obtained from a POST request.
    The content type is application/json.
    The correct packet form should be as follows:
    {
      'token': UUID token from current session start
      'command': 'save z'
    }
    """
    if not session.tip_length:
        message = "Tip length must be set before calibrating"
        status = 400
    else:
        if not feature_flags.use_protocol_api_v2():
            mount = 'Z' if session.current_mount == 'left' else 'A'
            actual_z = position(mount, session.adapter)[-1]
            length_offset = pipette_config.load(
                session.current_model, session.pipette_id).model_offset[-1]
            session.z_value = actual_z - session.tip_length + length_offset
        else:
            session.z_value = position(session.current_mount, session.adapter,
                                       session.cp)[-1]

        session.current_transform[2][3] = session.z_value

        session.adapter.update_config(gantry_calibration=list(
            map(lambda i: list(i), session.current_transform)))

        message = "Saved z: {}".format(session.z_value)
        status = 200
    return web.json_response({'message': message}, status=status)
示例#13
0
def init_pipette():
    """
    Finds pipettes attached to the robot currently and chooses the correct one
    to add to the session.

    :return: The pipette type and mount chosen for deck calibration
    """
    global session
    pipette_info = set_current_mount(session)
    pipette = pipette_info['pipette']
    res = {}
    if pipette:
        session.current_model = pipette_info['model']
        if not feature_flags.use_protocol_api_v2():
            mount = pipette.mount
            session.current_mount = mount
        else:
            mount = pipette.get('mount')
            session.current_mount = mount_by_name[mount]
        session.pipettes[mount] = pipette
        res = {'mount': mount, 'model': pipette_info['model']}

    log.info("Pipette info {}".format(session.pipettes))

    return res
示例#14
0
async def get_health(
        hardware: HardwareAPILike = Depends(get_hardware)) -> Health:
    static_paths = ['/logs/serial.log', '/logs/api.log']
    # This conditional handles the case where we have just changed the
    # use protocol api v2 feature flag, so it does not match the type
    # of hardware we're actually using.
    fw_version = hardware.fw_version  # type: ignore
    if inspect.isawaitable(fw_version):
        fw_version = await fw_version

    if feature_flags.use_protocol_api_v2():
        max_supported = protocol_api.MAX_SUPPORTED_VERSION
    else:
        max_supported = APIVersion(1, 0)

    return Health(name=config.name(),
                  api_version=__version__,
                  fw_version=fw_version,
                  board_revision=hardware.board_revision,
                  logs=static_paths,
                  system_version=config.OT_SYSTEM_VERSION,
                  protocol_api_version=list(max_supported),
                  links=Links(
                      apiLog='/logs/api.log',
                      serialLog='/logs/serial.log',
                      apiSpec="/openapi.json"
                  ))
示例#15
0
    def validate(self, point: Tuple[float, float, float], point_num: int,
                 pipette_mount) -> str:
        """
        :param point: Expected values from mechanical drawings
        :param point_num: The current position attempting to be validated
        :param pipette: 'Z' for left mount or 'A' for right mount

        :return:
        """
        self._current_point = point_num
        if not feature_flags.use_protocol_api_v2():
            _, _, cz = self._position()
            if cz < SAFE_HEIGHT:
                self.move_to_safe_height()
            tx, ty, tz = self._deck_to_driver_coords(point)
            _, _, moz = self.model_offset
            self.hardware._driver.move({'X': tx, 'Y': ty})
            self.hardware._driver.move({self._current_mount: tz - moz})
        else:
            self._current_mount = pipette_mount
            self.move_to_safe_height()
            pt1 = types.Point(x=point[0], y=point[1], z=SAFE_HEIGHT)
            pt2 = types.Point(*point)
            self.hardware.move_to(self._current_mount, pt1)
            self.hardware.move_to(self._current_mount, pt2)
        return 'moved to point {}'.format(point)
示例#16
0
async def get_rail_lights(request):
    hw = hw_from_req(request)
    if ff.use_protocol_api_v2():
        on = await hw.get_lights()
    else:
        on = hw.get_lights()
    return web.json_response({'on': on['rails']})
示例#17
0
def driver_import(monkeypatch, robot):
    from opentrons import tools
    if ff.use_protocol_api_v2():
        monkeypatch.setattr(
            tools, 'driver',
            robot._ctx._hw_manager._current._backend._smoothie_driver)
    else:
        monkeypatch.setattr(tools, 'driver', robot._driver)
示例#18
0
def set_current_mount(session: SessionManager):
    """
    Choose the pipette in which to execute commands. If there is no pipette,
    or it is uncommissioned, the pipette is not mounted.

    :attached_pipettes attached_pipettes: Information obtained from the current
    pipettes attached to the robot. This looks like the following:
    :dict with keys 'left' and 'right' and a model string for each
    mount, or 'uncommissioned' if no model string available
    :return: The selected pipette
    """

    pipette = None
    right_channel = None
    left_channel = None
    right_pipette, left_pipette = get_pipettes(session)
    if right_pipette:
        if not feature_flags.use_protocol_api_v2():
            right_channel = right_pipette.channels
        else:
            right_channel = right_pipette.get('channels')
            right_pipette['mount'] = 'right'

    if left_pipette:
        if not feature_flags.use_protocol_api_v2():
            left_channel = left_pipette.channels
        else:
            left_channel = left_pipette.get('channels')
            left_pipette['mount'] = 'left'

    if right_channel == 1:
        pipette = right_pipette
        session.cp = CriticalPoint.NOZZLE
    elif left_channel == 1:
        pipette = left_pipette
        session.cp = CriticalPoint.NOZZLE
    elif right_pipette:
        pipette = right_pipette
        session.cp = CriticalPoint.FRONT_NOZZLE
    elif left_pipette:
        pipette = left_pipette
        session.cp = CriticalPoint.FRONT_NOZZLE

    model, pip_id = _get_model_name(pipette, session.adapter)
    session.pipette_id = pip_id
    return {'pipette': pipette, 'model': model}
示例#19
0
 def move_to_safe_height(self):
     cx, cy, _ = self._position()
     if not feature_flags.use_protocol_api_v2():
         _, _, sz = self._deck_to_driver_coords((cx, cy, SAFE_HEIGHT))
         self.hardware._driver.move({self._current_mount: sz})
     else:
         pt = types.Point(x=cx, y=cy, z=SAFE_HEIGHT)
         self.hardware.move_to(self._current_mount, pt)
示例#20
0
async def identify(request):
    hw = hw_from_req(request)
    blink_time = int(request.query.get('seconds', '10'))
    if ff.use_protocol_api_v2():
        asyncio.ensure_future(hw.identify(blink_time))
    else:
        Thread(target=lambda: hw.identify(blink_time)).start()
    return web.json_response({"message": "identifying"})
示例#21
0
async def available_resets(request: web.Request) -> web.Response:
    """ Indicate what parts of the user configuration are available for reset.
    """
    if ff.use_protocol_api_v2():
        to_use = _settings_reset_options + _apiv2_settings_reset_options
    else:
        to_use = _settings_reset_options
    return web.json_response({'options': to_use}, status=200)
示例#22
0
    def refresh(self):
        self._reset()
        self._is_json_protocol = self.name.endswith('.json')

        if self._is_json_protocol:
            # TODO Ian 2018-05-16 use protocol JSON schema to raise
            # warning/error here if the protocol_text doesn't follow the schema
            self._protocol = json.loads(self.protocol_text)
            version = 'JSON'
        else:
            parsed = ast.parse(self.protocol_text, filename=self.name)
            self.metadata = extract_metadata(parsed)
            self._protocol = compile(parsed, filename=self.name, mode='exec')
            version = infer_version(self.metadata, parsed)

        self.api_level = 2 if ff.use_protocol_api_v2() else 1

        if ff.use_protocol_api_v2() and version == '1':
            raise RuntimeError(
                'This protocol targets Protocol API V1, but the robot is set '
                'to Protocol API V2. If this is actually a V2 protocol, '
                'please set the \'apiLevel\' to \'2\' in the metadata. If you '
                'do not want to be on API V2, please disable the \'Use '
                'Protocol API version 2\' toggle in the robot\'s Advanced '
                'Settings and restart the robot.')

        log.info(f"Protocol API version: {version}")

        try:
            self._broker.set_logger(self._sim_logger)
            commands = self._simulate()
        except Exception:
            raise
        finally:
            self._broker.set_logger(self._default_logger)

        self.commands = tree.from_list(commands)

        self.containers = self.get_containers()
        self.instruments = self.get_instruments()
        self.modules = self.get_modules()
        self.startTime = None
        self.set_state('loaded')
        return self
示例#23
0
 def save_z_value(self) -> str:
     actual_z = self._position()[-1]
     if not feature_flags.use_protocol_api_v2():
         expected_z = self.current_transform[2][3] + self._tip_length
         new_z = self.current_transform[2][3] + actual_z - expected_z
     else:
         new_z = self.current_transform[2][3] + actual_z
     log.debug("Saving z value: {}".format(new_z))
     self.current_transform[2][3] = new_z
     return 'saved Z-Offset: {}'.format(new_z)
示例#24
0
    def resume(self):
        if ff.use_protocol_api_v2():
            self._hardware.resume()
        # robot.resume in the legacy API will publish commands to the broker
        # use the broker-less execute_resume instead
        else:
            self._hardware.execute_resume()

        self.set_state('running')
        return self
示例#25
0
 def return_tip(self, instrument):
     inst = instrument._instrument
     log.info('Returning tip from {}'.format(instrument.name))
     self._set_state('moving')
     if ff.use_protocol_api_v2():
         with instrument._context.temp_connect(self._hardware):
             instrument._context.location_cache = None
             inst.return_tip()
     else:
         inst.return_tip()
     self._set_state('ready')
示例#26
0
 def home(self, instrument):
     inst = instrument._instrument
     log.info('Homing {}'.format(instrument.name))
     self._set_state('moving')
     if ff.use_protocol_api_v2():
         with instrument._context.temp_connect(self._hardware):
             instrument._context.location_cache = None
             inst.home()
     else:
         inst.home()
     self._set_state('ready')
示例#27
0
def _check_reset(reset_req: Dict[str, str]) -> Tuple[bool, str]:

    if ff.use_protocol_api_v2():
        to_use = _settings_reset_options + _apiv2_settings_reset_options
    else:
        to_use = _settings_reset_options
    for requested_reset in reset_req.keys():
        if requested_reset not in [opt['id'] for opt in to_use]:
            log.error('Bad reset option {} requested'.format(requested_reset))
            return (False, requested_reset)
    return (True, '')
示例#28
0
async def get_robot_settings(request: web.Request) -> web.Response:
    """
    Handles a GET request and returns a body that is the JSON
    representation of all internal robot settings and gantry calibration
    """

    hw = request.app['com.opentrons.hardware']

    if ff.use_protocol_api_v2():
        conf = await hw.config
    else:
        conf = hw.config
    return web.json_response(conf._asdict(), status=200)
示例#29
0
 def try_pickup_tip(self):
     pipette = self._pipettes[self._current_mount]
     self._tip_length = pipette._fallback_tip_length
     # Check that pipette does not have tip attached, if it does remove it.
     self._clear_tips(pipette)
     if not feature_flags.use_protocol_api_v2():
         top = self._position()
         self._helper_pickup(pipette, top)
         pipette._add_tip(pipette._tip_length)
     else:
         self.hardware.pick_up_tip(self._current_mount,
                                   tip_length=self._tip_length)
     return "Picked up tip!"
示例#30
0
    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.metadata = self._protocol.metadata
            if ff.use_protocol_api_v2()\
               and self._protocol.api_level == '1'\
               and not ff.enable_back_compat():
                raise RuntimeError(
                    'This protocol targets Protocol API V1, but the robot is '
                    'set to Protocol API V2. If this is actually a V2 '
                    'protocol, please set the \'apiLevel\' to \'2\' in the '
                    'metadata. If you do not want to be on API V2, please '
                    'disable the \'Use Protocol API version 2\' toggle in the '
                    'robot\'s Advanced Settings and restart the robot.')

            log.info(f"Protocol API version: {self._protocol.api_level}")
        else:
            self.metadata = {}
            log.info(f"JSON protocol")

        try:
            self._broker.set_logger(self._sim_logger)
            commands = self._simulate()
        except Exception:
            raise
        finally:
            self._broker.set_logger(self._default_logger)

        self.commands = tree.from_list(commands)

        self.containers = self.get_containers()
        self.instruments = self.get_instruments()
        self.modules = self.get_modules()
        self.startTime = None
        self.set_state('loaded')
        return self