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')
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()}")
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)
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}")
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)
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)
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)
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")
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
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
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)
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)
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
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" ))
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)
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']})
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)
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}
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)
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"})
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)
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
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)
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
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')
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')
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, '')
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)
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!"
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