示例#1
0
    def __init__(self, robot):
        self._velocity_speed = VELOCITY_BASE_SPEED
        self._velocity_angular = VELOCITY_BASE_ANGULAR
        self.height = 0.5
        self.roll = 0.0
        self.pitch = 0.0
        self.yaw = 0.0

        self._robot = robot
        # Create clients -- do not use the for communication yet.
        self._lease_client = robot.ensure_client(LeaseClient.default_service_name)
        self._estop_client = robot.ensure_client(EstopClient.default_service_name)
        self._estop_endpoint = EstopEndpoint(self._estop_client, 'wasd', 9.0)
        self._power_client = robot.ensure_client(PowerClient.default_service_name)
        self._robot_state_client = robot.ensure_client(RobotStateClient.default_service_name)
        self._robot_command_client = robot.ensure_client(RobotCommandClient.default_service_name)
        self._robot_metrics_task = AsyncMetrics(self._robot_state_client)
        self._robot_state_task = AsyncRobotState(self._robot_state_client)
        self._image_task = AsyncImageCapture(robot)
        self._async_tasks = AsyncTasks(
            [self._robot_metrics_task, self._robot_state_task, self._image_task])
        self._lock = threading.Lock()
        self._command_dictionary = {
            ord('\t'): self._quit_program,
            ord('T'): self._toggle_time_sync,
            ord('\\'): self._toggle_estop,
            ord('r'): self._self_right,
            ord('P'): self._toggle_power,
            ord('1'): self._sit,
            ord('2'): self._stand,
            ord('w'): self._move_forward,
            ord('s'): self._move_backward,
            ord('a'): self._strafe_left,
            ord('d'): self._strafe_right,
            ord('q'): self._turn_left,
            ord('e'): self._turn_right,
            ord('.'): self._speed_up,
            ord(','): self._slow_down,
            ord('>'): self._speed_up,
            ord('<'): self._slow_down,
            ord('z'): self._lean_left,
            ord('c'): self._lean_right,
            ord('f'): self._twist_left,
            ord('h'): self._twist_right,
            ord('t'): self._tilt_down,
            ord('g'): self._tilt_up,
            ord('3'): self._stand_lower,
            ord('4'): self._stand_higher,
            ord('I'): self._image_task.take_image,
            ord('O'): self._image_task.toggle_video_mode,
        }
        self._locked_messages = ['', '', '']  # string: displayed message for user
        self._estop_keepalive = None
        self._exit_check = None

        # Stuff that is set in start()
        self._robot_id = None
        self._lease = None
示例#2
0
    def __init__(self, robot):
        self._robot = robot
        # Create clients -- do not use the for communication yet.
        self._lease_client = robot.ensure_client(
            LeaseClient.default_service_name)
        try:
            self._estop_client = self._robot.ensure_client(
                EstopClient.default_service_name)
            self._estop_endpoint = EstopEndpoint(self._estop_client,
                                                 'GNClient', 9.0)
        except:
            # Not the estop.
            self._estop_client = None
            self._estop_endpoint = None
        self._power_client = robot.ensure_client(
            PowerClient.default_service_name)
        self._robot_state_client = robot.ensure_client(
            RobotStateClient.default_service_name)
        self._robot_command_client = robot.ensure_client(
            RobotCommandClient.default_service_name)
        self._robot_state_task = AsyncRobotState(self._robot_state_client)
        self._image_task = AsyncImageCapture(robot)
        self._async_tasks = AsyncTasks(
            [self._robot_state_task, self._image_task])
        self._lock = threading.Lock()
        self._command_dictionary = {
            27: self._stop,  # ESC key
            ord('\t'): self._quit_program,
            ord('T'): self._toggle_time_sync,
            ord(' '): self._toggle_estop,
            ord('r'): self._self_right,
            ord('P'): self._toggle_power,
            ord('v'): self._sit,
            ord('b'): self._battery_change_pose,
            ord('f'): self._stand,
            ord('w'): self._move_forward,
            ord('s'): self._move_backward,
            ord('a'): self._strafe_left,
            ord('d'): self._strafe_right,
            ord('q'): self._turn_left,
            ord('e'): self._turn_right,
            ord('I'): self._image_task.take_image,
            ord('O'): self._image_task.toggle_video_mode,
            ord('u'): self._unstow,
            ord('j'): self._stow,
            ord('l'): self._toggle_lease
        }
        self._locked_messages = ['', '',
                                 '']  # string: displayed message for user
        self._estop_keepalive = None
        self._exit_check = None

        # Stuff that is set in start()
        self._robot_id = None
        self._lease = None
        self._lease_keepalive = None
示例#3
0
def main(argv):
    # The last argument should be the IP address of the robot. The app will use the directory to find
    # the velodyne and start getting data from it.
    parser = argparse.ArgumentParser()
    bosdyn.client.util.add_common_arguments(parser)
    options = parser.parse_args(argv)

    sdk = bosdyn.client.create_standard_sdk('VelodyneClient')
    robot = sdk.create_robot(options.hostname)
    robot.authenticate(options.username, options.password)
    robot.sync_with_directory()

    _point_cloud_client = robot.ensure_client('velodyne-point-cloud')
    _robot_state_client = robot.ensure_client(
        RobotStateClient.default_service_name)
    _point_cloud_task = AsyncPointCloud(_point_cloud_client)
    _robot_state_task = AsyncRobotState(_robot_state_client)
    _task_list = [_point_cloud_task, _robot_state_task]
    _async_tasks = AsyncTasks(_task_list)
    print('Connected.')

    update_thread = threading.Thread(target=_update_thread,
                                     args=[_async_tasks])
    update_thread.daemon = True
    update_thread.start()

    # Wait for the first responses.
    while any(task.proto is None for task in _task_list):
        time.sleep(0.1)
    fig = plt.figure()

    body_tform_butt = SE3Pose(-0.5, 0, 0, Quat())
    body_tform_head = SE3Pose(0.5, 0, 0, Quat())

    # Plot the point cloud as an animation.
    ax = fig.add_subplot(111, projection='3d')
    aggregate_data = collections.deque(maxlen=5)
    while True:
        if _point_cloud_task.proto[0].point_cloud:
            data = np.fromstring(_point_cloud_task.proto[0].point_cloud.data,
                                 dtype=np.float32)
            aggregate_data.append(data)
            plot_data = np.concatenate(aggregate_data)
            ax.clear()
            ax.set_xlabel('X (m)')
            ax.set_ylabel('Y (m)')
            ax.set_zlabel('Z (m)')

            # Draw robot position and orientation on plot
            if _robot_state_task.proto:
                odom_tform_body = get_odom_tform_body(
                    _robot_state_task.proto.kinematic_state.transforms_snapshot
                ).to_proto()
                helper_se3 = SE3Pose.from_obj(odom_tform_body)
                odom_tform_butt = helper_se3.mult(body_tform_butt)
                odom_tform_head = helper_se3.mult(body_tform_head)
                ax.plot([odom_tform_butt.x], [odom_tform_butt.y],
                        [odom_tform_butt.z],
                        'o',
                        color=SPOT_YELLOW,
                        markersize=7,
                        label='robot_butt')
                ax.plot([odom_tform_head.x], [odom_tform_head.y],
                        [odom_tform_head.z],
                        'o',
                        color=SPOT_YELLOW,
                        markersize=7,
                        label='robot_head')
                ax.text(odom_tform_butt.x,
                        odom_tform_butt.y,
                        odom_tform_butt.z,
                        'Spot Butt',
                        size=TEXT_SIZE,
                        zorder=1,
                        color='k')
                ax.text(odom_tform_head.x,
                        odom_tform_head.y,
                        odom_tform_head.z,
                        'Spot Head',
                        size=TEXT_SIZE,
                        zorder=1,
                        color='k')
                ax.plot([odom_tform_butt.x, odom_tform_head.x],
                        [odom_tform_butt.y, odom_tform_head.y],
                        zs=[odom_tform_butt.z, odom_tform_head.z],
                        linewidth=6,
                        color=SPOT_YELLOW)

            # Plot point cloud data
            ax.plot(plot_data[0::3], plot_data[1::3], plot_data[2::3], '.')
            set_axes_equal(ax)
            plt.draw()
            plt.pause(0.016)
            if window_closed(ax):
                sys.exit(0)
示例#4
0
    def __init__(self, robot, download_filepath):
        self._robot = robot

        # Flag indicating whether mission is currently being recorded
        self._recording = False

        # Filepath for the location to put the downloaded graph and snapshots.
        self._download_filepath = download_filepath

        # List of visited waypoints in current mission
        self._waypoint_path = []

        # Current waypoint id
        self._waypoint_id = 'NONE'

        # Create clients -- do not use the for communication yet.
        self._lease_client = robot.ensure_client(
            LeaseClient.default_service_name)
        try:
            self._estop_client = self._robot.ensure_client(
                EstopClient.default_service_name)
            self._estop_endpoint = EstopEndpoint(self._estop_client,
                                                 'GNClient', 9.0)
        except:
            # Not the estop.
            self._estop_client = None
            self._estop_endpoint = None

        self._power_client = robot.ensure_client(
            PowerClient.default_service_name)
        self._robot_state_client = robot.ensure_client(
            RobotStateClient.default_service_name)
        self._robot_command_client = robot.ensure_client(
            RobotCommandClient.default_service_name)
        self._world_object_client = robot.ensure_client(
            WorldObjectClient.default_service_name)

        # Setup the recording service client.
        self._recording_client = self._robot.ensure_client(
            GraphNavRecordingServiceClient.default_service_name)

        # Setup the graph nav service client.
        self._graph_nav_client = robot.ensure_client(
            GraphNavClient.default_service_name)

        self._robot_state_task = AsyncRobotState(self._robot_state_client)
        self._async_tasks = AsyncTasks([self._robot_state_task])
        self._lock = threading.Lock()
        self._command_dictionary = {
            ord('\t'): self._quit_program,
            ord('T'): self._toggle_time_sync,
            ord(' '): self._toggle_estop,
            ord('r'): self._self_right,
            ord('P'): self._toggle_power,
            ord('v'): self._sit,
            ord('f'): self._stand,
            ord('w'): self._move_forward,
            ord('s'): self._move_backward,
            ord('a'): self._strafe_left,
            ord('d'): self._strafe_right,
            ord('q'): self._turn_left,
            ord('e'): self._turn_right,
            ord('m'): self._start_recording,
            ord('l'): self._relocalize,
            ord('g'): self._generate_mission,
        }
        self._locked_messages = ['', '',
                                 '']  # string: displayed message for user
        self._estop_keepalive = None
        self._exit_check = None

        # Stuff that is set in start()
        self._robot_id = None
        self._lease = None
示例#5
0
class RecorderInterface(object):
    """A curses interface for recording robot missions."""
    def __init__(self, robot, download_filepath):
        self._robot = robot

        # Flag indicating whether mission is currently being recorded
        self._recording = False

        # Filepath for the location to put the downloaded graph and snapshots.
        self._download_filepath = download_filepath

        # List of visited waypoints in current mission
        self._waypoint_path = []

        # Current waypoint id
        self._waypoint_id = 'NONE'

        # Create clients -- do not use the for communication yet.
        self._lease_client = robot.ensure_client(
            LeaseClient.default_service_name)
        try:
            self._estop_client = self._robot.ensure_client(
                EstopClient.default_service_name)
            self._estop_endpoint = EstopEndpoint(self._estop_client,
                                                 'GNClient', 9.0)
        except:
            # Not the estop.
            self._estop_client = None
            self._estop_endpoint = None

        self._power_client = robot.ensure_client(
            PowerClient.default_service_name)
        self._robot_state_client = robot.ensure_client(
            RobotStateClient.default_service_name)
        self._robot_command_client = robot.ensure_client(
            RobotCommandClient.default_service_name)
        self._world_object_client = robot.ensure_client(
            WorldObjectClient.default_service_name)

        # Setup the recording service client.
        self._recording_client = self._robot.ensure_client(
            GraphNavRecordingServiceClient.default_service_name)

        # Setup the graph nav service client.
        self._graph_nav_client = robot.ensure_client(
            GraphNavClient.default_service_name)

        self._robot_state_task = AsyncRobotState(self._robot_state_client)
        self._async_tasks = AsyncTasks([self._robot_state_task])
        self._lock = threading.Lock()
        self._command_dictionary = {
            ord('\t'): self._quit_program,
            ord('T'): self._toggle_time_sync,
            ord(' '): self._toggle_estop,
            ord('r'): self._self_right,
            ord('P'): self._toggle_power,
            ord('v'): self._sit,
            ord('f'): self._stand,
            ord('w'): self._move_forward,
            ord('s'): self._move_backward,
            ord('a'): self._strafe_left,
            ord('d'): self._strafe_right,
            ord('q'): self._turn_left,
            ord('e'): self._turn_right,
            ord('m'): self._start_recording,
            ord('l'): self._relocalize,
            ord('g'): self._generate_mission,
        }
        self._locked_messages = ['', '',
                                 '']  # string: displayed message for user
        self._estop_keepalive = None
        self._exit_check = None

        # Stuff that is set in start()
        self._robot_id = None
        self._lease = None

    def start(self):
        """Begin communication with the robot."""
        self._lease = self._lease_client.acquire()

        self._robot_id = self._robot.get_id()
        if self._estop_endpoint is not None:
            self._estop_endpoint.force_simple_setup(
            )  # Set this endpoint as the robot's sole estop.

        # Clear existing graph nav map
        self._graph_nav_client.clear_graph()

    def shutdown(self):
        """Release control of robot as gracefully as posssible."""
        LOGGER.info("Shutting down WasdInterface.")
        if self._estop_keepalive:
            # This stops the check-in thread but does not stop the robot.
            self._estop_keepalive.shutdown()
        if self._lease:
            _grpc_or_log("returning lease",
                         lambda: self._lease_client.return_lease(self._lease))
            self._lease = None

    def __del__(self):
        self.shutdown()

    def flush_and_estop_buffer(self, stdscr):
        """Manually flush the curses input buffer but trigger any estop requests (space)"""
        key = ''
        while key != -1:
            key = stdscr.getch()
            if key == ord(' '):
                self._toggle_estop()

    def add_message(self, msg_text):
        """Display the given message string to the user in the curses interface."""
        with self._lock:
            self._locked_messages = [msg_text] + self._locked_messages[:-1]

    def message(self, idx):
        """Grab one of the 3 last messages added."""
        with self._lock:
            return self._locked_messages[idx]

    @property
    def robot_state(self):
        """Get latest robot state proto."""
        return self._robot_state_task.proto

    def drive(self, stdscr):
        """User interface to control the robot via the passed-in curses screen interface object."""
        with LeaseKeepAlive(self._lease_client) as lease_keep_alive, \
             ExitCheck() as self._exit_check:
            curses_handler = CursesHandler(self)
            curses_handler.setLevel(logging.INFO)
            LOGGER.addHandler(curses_handler)

            stdscr.nodelay(True)  # Don't block for user input.
            stdscr.resize(26, 96)
            stdscr.refresh()

            # for debug
            curses.echo()

            try:
                while not self._exit_check.kill_now:
                    self._async_tasks.update()
                    self._drive_draw(stdscr, lease_keep_alive)

                    try:
                        cmd = stdscr.getch()
                        # Do not queue up commands on client
                        self.flush_and_estop_buffer(stdscr)
                        self._drive_cmd(cmd)
                        time.sleep(COMMAND_INPUT_RATE)
                    except Exception:
                        # On robot command fault, sit down safely before killing the program.
                        self._safe_power_off()
                        time.sleep(2.0)
                        self.shutdown()
                        raise

            finally:
                LOGGER.removeHandler(curses_handler)

    def _count_visible_fiducials(self):
        """Return number of fiducials visible to robot."""
        request_fiducials = [world_object_pb2.WORLD_OBJECT_APRILTAG]
        fiducial_objects = self._world_object_client.list_world_objects(
            object_type=request_fiducials).world_objects
        return len(fiducial_objects)

    def _drive_draw(self, stdscr, lease_keep_alive):
        """Draw the interface screen at each update."""
        stdscr.clear()  # clear screen
        stdscr.resize(26, 96)
        stdscr.addstr(
            0, 0, '{:20s} {}'.format(self._robot_id.nickname,
                                     self._robot_id.serial_number))
        stdscr.addstr(1, 0, self._lease_str(lease_keep_alive))
        stdscr.addstr(2, 0, self._battery_str())
        stdscr.addstr(3, 0, self._estop_str())
        stdscr.addstr(4, 0, self._power_state_str())
        stdscr.addstr(5, 0, self._time_sync_str())
        stdscr.addstr(6, 0, self._waypoint_str())
        stdscr.addstr(7, 0, self._fiducial_str())
        for i in range(3):
            stdscr.addstr(9 + i, 2, self.message(i))
        stdscr.addstr(13, 0,
                      "Commands: [TAB]: quit                               ")
        stdscr.addstr(14, 0,
                      "          [T]: Time-sync, [SPACE]: Estop, [P]: Power")
        stdscr.addstr(15, 0,
                      "          [v]: Sit, [f]: Stand, [r]: Self-right     ")
        stdscr.addstr(16, 0,
                      "          [wasd]: Directional strafing              ")
        stdscr.addstr(17, 0,
                      "          [qe]: Turning                             ")
        stdscr.addstr(18, 0,
                      "          [m]: Start recording mission              ")
        stdscr.addstr(19, 0,
                      "          [l]: Add fiducial localization to mission ")
        stdscr.addstr(20, 0,
                      "          [g]: Stop recording and generate mission  ")

        stdscr.refresh()

    def _drive_cmd(self, key):
        """Run user commands at each update."""
        try:
            cmd_function = self._command_dictionary[key]
            cmd_function()

        except KeyError:
            if key and key != -1 and key < 256:
                self.add_message("Unrecognized keyboard command: '{}'".format(
                    chr(key)))

    def _try_grpc(self, desc, thunk):
        try:
            return thunk()
        except (ResponseError, RpcError) as err:
            self.add_message("Failed {}: {}".format(desc, err))
            return None

    def _quit_program(self):
        self._robot.power_off()
        self.shutdown()
        if self._exit_check is not None:
            self._exit_check.request_exit()

    def _toggle_time_sync(self):
        if self._robot.time_sync.stopped:
            self._robot.time_sync.start()
        else:
            self._robot.time_sync.stop()

    def _toggle_estop(self):
        """toggle estop on/off. Initial state is ON"""
        if self._estop_client is not None and self._estop_endpoint is not None:
            if not self._estop_keepalive:
                self._estop_keepalive = EstopKeepAlive(self._estop_endpoint)
            else:
                self._try_grpc("stopping estop", self._estop_keepalive.stop)
                self._estop_keepalive.shutdown()
                self._estop_keepalive = None

    def _start_robot_command(self, desc, command_proto, end_time_secs=None):
        def _start_command():
            self._robot_command_client.robot_command(
                lease=None, command=command_proto, end_time_secs=end_time_secs)

        self._try_grpc(desc, _start_command)

    def _self_right(self):
        self._start_robot_command('self_right',
                                  RobotCommandBuilder.selfright_command())

    def _sit(self):
        self._start_robot_command('sit', RobotCommandBuilder.sit_command())

    def _stand(self):
        self._start_robot_command('stand', RobotCommandBuilder.stand_command())

    def _move_forward(self):
        self._velocity_cmd_helper('move_forward', v_x=VELOCITY_BASE_SPEED)

    def _move_backward(self):
        self._velocity_cmd_helper('move_backward', v_x=-VELOCITY_BASE_SPEED)

    def _strafe_left(self):
        self._velocity_cmd_helper('strafe_left', v_y=VELOCITY_BASE_SPEED)

    def _strafe_right(self):
        self._velocity_cmd_helper('strafe_right', v_y=-VELOCITY_BASE_SPEED)

    def _turn_left(self):
        self._velocity_cmd_helper('turn_left', v_rot=VELOCITY_BASE_ANGULAR)

    def _turn_right(self):
        self._velocity_cmd_helper('turn_right', v_rot=-VELOCITY_BASE_ANGULAR)

    def _velocity_cmd_helper(self, desc='', v_x=0.0, v_y=0.0, v_rot=0.0):
        self._start_robot_command(
            desc,
            RobotCommandBuilder.velocity_command(v_x=v_x, v_y=v_y,
                                                 v_rot=v_rot),
            end_time_secs=time.time() + VELOCITY_CMD_DURATION)

    def _return_to_origin(self):
        self._start_robot_command(
            'fwd_and_rotate',
            RobotCommandBuilder.trajectory_command(
                goal_x=0.0,
                goal_y=0.0,
                goal_heading=0.0,
                frame_name=ODOM_FRAME_NAME,
                params=None,
                body_height=0.0,
                locomotion_hint=spot_command_pb2.HINT_SPEED_SELECT_TROT),
            end_time_secs=time.time() + 20)

    def _toggle_power(self):
        power_state = self._power_state()
        if power_state is None:
            self.add_message(
                'Could not toggle power because power state is unknown')
            return

        if power_state == robot_state_proto.PowerState.STATE_OFF:
            self._try_grpc("powering-on", self._request_power_on)
        else:
            self._try_grpc("powering-off", self._safe_power_off)

    def _request_power_on(self):
        bosdyn.client.power.power_on(self._power_client)

    def _safe_power_off(self):
        self._start_robot_command('safe_power_off',
                                  RobotCommandBuilder.safe_power_off_command())

    def _power_state(self):
        state = self.robot_state
        if not state:
            return None
        return state.power_state.motor_power_state

    def _lease_str(self, lease_keep_alive):
        alive = 'RUNNING' if lease_keep_alive.is_alive() else 'STOPPED'
        lease = '{}:{}'.format(self._lease.lease_proto.resource,
                               self._lease.lease_proto.sequence)
        return 'Lease {} THREAD:{}'.format(lease, alive)

    def _power_state_str(self):
        power_state = self._power_state()
        if power_state is None:
            return ''
        state_str = robot_state_proto.PowerState.MotorPowerState.Name(
            power_state)
        return 'Power: {}'.format(state_str[6:])  # get rid of STATE_ prefix

    def _estop_str(self):
        if not self._estop_client:
            thread_status = 'NOT ESTOP'
        else:
            thread_status = 'RUNNING' if self._estop_keepalive else 'STOPPED'
        estop_status = '??'
        state = self.robot_state
        if state:
            for estop_state in state.estop_states:
                if estop_state.type == estop_state.TYPE_SOFTWARE:
                    estop_status = estop_state.State.Name(
                        estop_state.state)[6:]  # s/STATE_//
                    break
        return 'Estop {} (thread: {})'.format(estop_status, thread_status)

    def _time_sync_str(self):
        if not self._robot.time_sync:
            return 'Time sync: (none)'
        if self._robot.time_sync.stopped:
            status = 'STOPPED'
            exception = self._robot.time_sync.thread_exception
            if exception:
                status = '{} Exception: {}'.format(status, exception)
        else:
            status = 'RUNNING'
        try:
            skew = self._robot.time_sync.get_robot_clock_skew()
            if skew:
                skew_str = 'offset={}'.format(duration_str(skew))
            else:
                skew_str = "(Skew undetermined)"
        except (TimeSyncError, RpcError) as err:
            skew_str = '({})'.format(err)
        return 'Time sync: {} {}'.format(status, skew_str)

    def _waypoint_str(self):
        state = self._graph_nav_client.get_localization_state()
        try:
            self._waypoint_id = state.localization.waypoint_id
            if self._waypoint_id == '':
                self._waypoint_id = 'NONE'

        except:
            self._waypoint_id = 'ERROR'

        if self._recording and self._waypoint_id != 'NONE' and self._waypoint_id != 'ERROR':
            if (self._waypoint_path
                    == []) or (self._waypoint_id != self._waypoint_path[-1]):
                self._waypoint_path += [self._waypoint_id]
            return 'Current waypoint: {} [ RECORDING ]'.format(
                self._waypoint_id)

        return 'Current waypoint: {}'.format(self._waypoint_id)

    def _fiducial_str(self):
        return 'Visible fiducials: {}'.format(
            str(self._count_visible_fiducials()))

    def _battery_str(self):
        if not self.robot_state:
            return ''
        battery_state = self.robot_state.battery_states[0]
        status = battery_state.Status.Name(battery_state.status)
        status = status[7:]  # get rid of STATUS_ prefix
        if battery_state.charge_percentage.value:
            bar_len = int(battery_state.charge_percentage.value) // 10
            bat_bar = '|{}{}|'.format('=' * bar_len, ' ' * (10 - bar_len))
        else:
            bat_bar = ''
        time_left = ''
        if battery_state.estimated_runtime:
            time_left = ' ({})'.format(
                secs_to_hms(battery_state.estimated_runtime.seconds))
        return 'Battery: {}{}{}'.format(status, bat_bar, time_left)

    def _fiducial_visible(self):
        """Return True if robot can see fiducial."""
        request_fiducials = [world_object_pb2.WORLD_OBJECT_APRILTAG]
        fiducial_objects = self._world_object_client.list_world_objects(
            object_type=request_fiducials).world_objects
        self.add_message("Fiducial objects: " + str(fiducial_objects))

        if len(fiducial_objects) > 0:
            return True

        return False

    def _start_recording(self):
        """Start recording a map."""
        if self._count_visible_fiducials() == 0:
            self.add_message(
                "ERROR: Can't start recording -- No fiducials in view.")
            return

        self._waypoint_path = []
        status = self._recording_client.start_recording()
        if status != recording_pb2.StartRecordingResponse.STATUS_OK:
            self.add_message("Start recording failed.")
        else:
            self.add_message("Successfully started recording a map.")
            self._recording = True

    def _stop_recording(self):
        """Stop or pause recording a map."""
        if not self._recording:
            return True
        self._recording = False

        try:
            status = self._recording_client.stop_recording()
            if status != recording_pb2.StopRecordingResponse.STATUS_OK:
                self.add_message("Stop recording failed.")
                return False

            self.add_message("Successfully stopped recording a map.")
            return True

        except NotLocalizedToEndError:
            self.add_message(
                "ERROR: Move to final waypoint before stopping recording.")
            return False

    def _relocalize(self):
        """Insert localization node into mission."""
        if not self._recording:
            print('Not recording mission.')
            return False

        self.add_message("Adding fiducial localization to mission.")
        self._waypoint_path += ['LOCALIZE']
        return True

    def _generate_mission(self):
        """Save graph map and mission file."""

        # Check whether mission has been recorded
        if not self._recording:
            self.add_message("ERROR: No mission recorded.")
            return

        # Stop recording mission
        if not self._stop_recording():
            self.add_message("ERROR: Error while stopping recording.")
            return

        # Save graph map
        os.mkdir(self._download_filepath)
        self._download_full_graph()

        # Generate mission
        mission = self._make_mission()

        # Save mission file
        os.mkdir(self._download_filepath + "/missions")
        mission_filepath = self._download_filepath + "/missions/autogenerated"
        write_mission(mission, mission_filepath)

        # Quit program
        self._quit_program()

    def _download_full_graph(self):
        """Download the graph and snapshots from the robot."""
        graph = self._graph_nav_client.download_graph()
        if graph is None:
            self.add_message("Failed to download the graph.")
            return
        self._write_full_graph(graph)
        self.add_message(
            "Graph downloaded with {} waypoints and {} edges".format(
                len(graph.waypoints), len(graph.edges)))
        # Download the waypoint and edge snapshots.
        self._download_and_write_waypoint_snapshots(graph.waypoints)
        self._download_and_write_edge_snapshots(graph.edges)

    def _write_full_graph(self, graph):
        """Download the graph from robot to the specified, local filepath location."""
        graph_bytes = graph.SerializeToString()
        write_bytes(self._download_filepath, '/graph', graph_bytes)

    def _download_and_write_waypoint_snapshots(self, waypoints):
        """Download the waypoint snapshots from robot to the specified, local filepath location."""
        num_waypoint_snapshots_downloaded = 0
        for waypoint in waypoints:
            try:
                waypoint_snapshot = self._graph_nav_client.download_waypoint_snapshot(
                    waypoint.snapshot_id)
            except Exception:
                # Failure in downloading waypoint snapshot. Continue to next snapshot.
                self.add_message("Failed to download waypoint snapshot: " +
                                 waypoint.snapshot_id)
                continue
            write_bytes(self._download_filepath + '/waypoint_snapshots',
                        '/' + waypoint.snapshot_id,
                        waypoint_snapshot.SerializeToString())
            num_waypoint_snapshots_downloaded += 1
            self.add_message(
                "Downloaded {} of the total {} waypoint snapshots.".format(
                    num_waypoint_snapshots_downloaded, len(waypoints)))

    def _download_and_write_edge_snapshots(self, edges):
        """Download the edge snapshots from robot to the specified, local filepath location."""
        num_edge_snapshots_downloaded = 0
        for edge in edges:
            try:
                edge_snapshot = self._graph_nav_client.download_edge_snapshot(
                    edge.snapshot_id)
            except Exception:
                # Failure in downloading edge snapshot. Continue to next snapshot.
                self.add_message("Failed to download edge snapshot: " +
                                 edge.snapshot_id)
                continue
            write_bytes(self._download_filepath + '/edge_snapshots',
                        '/' + edge.snapshot_id,
                        edge_snapshot.SerializeToString())
            num_edge_snapshots_downloaded += 1
            self.add_message(
                "Downloaded {} of the total {} edge snapshots.".format(
                    num_edge_snapshots_downloaded, len(edges)))

    def _make_mission(self):
        """ Create a mission that visits each waypoint on stored path."""
        # Create a Sequence that visits all the waypoints.
        sequence = nodes_pb2.Sequence()
        sequence.children.add().CopyFrom(self._make_localize_node())

        for waypoint_id in self._waypoint_path:
            if waypoint_id == 'LOCALIZE':
                sequence.children.add().CopyFrom(self._make_localize_node())
            else:
                sequence.children.add().CopyFrom(
                    self._make_goto_node(waypoint_id))

        # Return a Node with the Sequence.
        ret = nodes_pb2.Node()
        ret.name = "visit %d waypoints" % len(self._waypoint_path)
        ret.impl.Pack(sequence)
        return ret

    def _make_goto_node(self, waypoint_id):
        """ Create a leaf node that will go to the waypoint. """
        ret = nodes_pb2.Node()
        ret.name = "goto %s" % waypoint_id
        impl = nodes_pb2.BosdynNavigateTo()
        impl.destination_waypoint_id = waypoint_id
        ret.impl.Pack(impl)
        return ret

    def _make_localize_node(self):
        """Make localization node."""
        loc = nodes_pb2.Node()
        loc.name = "localize robot"
        impl = nodes_pb2.BosdynGraphNavLocalize()
        loc.impl.Pack(impl)
        return loc
示例#6
0
class WasdInterface(object):
    """A curses interface for driving the robot."""
    def __init__(self, robot):
        self._robot = robot
        # Create clients -- do not use the for communication yet.
        self._lease_client = robot.ensure_client(
            LeaseClient.default_service_name)
        self._estop_client = robot.ensure_client(
            EstopClient.default_service_name)
        self._estop_endpoint = EstopEndpoint(self._estop_client, 'wasd', 9.0)
        self._power_client = robot.ensure_client(
            PowerClient.default_service_name)
        self._robot_state_client = robot.ensure_client(
            RobotStateClient.default_service_name)
        self._robot_command_client = robot.ensure_client(
            RobotCommandClient.default_service_name)
        self._robot_metrics_task = AsyncMetrics(self._robot_state_client)
        self._robot_state_task = AsyncRobotState(self._robot_state_client)
        self._image_task = AsyncImageCapture(robot)
        self._async_tasks = AsyncTasks([
            self._robot_metrics_task, self._robot_state_task, self._image_task
        ])
        self._lock = threading.Lock()
        self._command_dictionary = {
            ord('\t'): self._quit_program,
            ord('T'): self._toggle_time_sync,
            ord(' '): self._toggle_estop,
            ord('r'): self._self_right,
            ord('P'): self._toggle_power,
            ord('v'): self._sit,
            ord('f'): self._stand,
            ord('w'): self._move_forward,
            ord('s'): self._move_backward,
            ord('a'): self._strafe_left,
            ord('d'): self._strafe_right,
            ord('q'): self._turn_left,
            ord('e'): self._turn_right,
            ord('I'): self._image_task.take_image,
            ord('O'): self._image_task.toggle_video_mode,
        }
        self._locked_messages = ['', '',
                                 '']  # string: displayed message for user
        self._estop_keepalive = None
        self._exit_check = None

        # Stuff that is set in start()
        self._robot_id = None
        self._lease = None

    def start(self):
        """Begin communication with the robot."""
        self._robot_id = self._robot.get_id()
        self._lease = self._lease_client.acquire()
        self._estop_endpoint.force_simple_setup(
        )  # Set this endpoint as the robot's sole estop.

    def shutdown(self):
        """Release control of robot as gracefully as posssible."""
        LOGGER.info("Shutting down WasdInterface.")
        if self._robot.time_sync:
            self._robot.time_sync.stop()
        if self._estop_keepalive:
            _grpc_or_log("stopping estop", self._estop_keepalive.stop)
            self._estop_keepalive = None
        if self._lease:
            _grpc_or_log("returning lease",
                         lambda: self._lease_client.return_lease(self._lease))
            self._lease = None

    def __del__(self):
        self.shutdown()

    def flush_and_estop_buffer(self, stdscr):
        """Manually flush the curses input buffer but trigger any estop requests (space)"""
        key = ''
        while key != -1:
            key = stdscr.getch()
            if key == ord(' '):
                self._toggle_estop()

    def add_message(self, msg_text):
        """Display the given message string to the user in the curses interface."""
        with self._lock:
            self._locked_messages = [msg_text] + self._locked_messages[:-1]

    def message(self, idx):
        """Grab one of the 3 last messages added."""
        with self._lock:
            return self._locked_messages[idx]

    @property
    def robot_state(self):
        """Get latest robot state proto."""
        return self._robot_state_task.proto

    @property
    def robot_metrics(self):
        """Get latest robot metrics proto."""
        return self._robot_metrics_task.proto

    def drive(self, stdscr):
        """User interface to control the robot via the passed-in curses screen interface object."""
        with ExitCheck(exit_delay_secs=2.0) as self._exit_check, \
              LeaseKeepAlive(self._lease_client,
                             keep_running_cb=lambda: self._exit_check.keep_alive) \
                             as lease_keep_alive:
            curses_handler = CursesHandler(self)
            curses_handler.setLevel(logging.INFO)
            LOGGER.addHandler(curses_handler)

            stdscr.nodelay(True)  # Don't block for user input.

            # for debug
            curses.echo()

            try:
                while self._exit_check.keep_alive:
                    self._async_tasks.update()
                    self._drive_draw(stdscr, lease_keep_alive)

                    if not self._exit_check.exit_requested:
                        try:
                            cmd = stdscr.getch()
                            # Do not queue up commands on client
                            self.flush_and_estop_buffer(stdscr)
                            self._drive_cmd(cmd)

                        except Exception:
                            # On robot command fault, sit down safely before killing the program.
                            self._safe_power_off()
                            time.sleep(2.0)
                            raise

                    time.sleep(COMMAND_INPUT_RATE)
            finally:
                LOGGER.removeHandler(curses_handler)

    def _drive_draw(self, stdscr, lease_keep_alive):
        """Draw the interface screen at each update."""
        stdscr.clear()  # clear screen
        stdscr.addstr(
            0, 0, '{:20s} {}'.format(self._robot_id.nickname,
                                     self._robot_id.serial_number))
        stdscr.addstr(1, 0, self._lease_str(lease_keep_alive))
        stdscr.addstr(2, 0, self._battery_str())
        stdscr.addstr(3, 0, self._estop_str())
        stdscr.addstr(4, 0, self._power_state_str())
        stdscr.addstr(5, 0, self._time_sync_str())
        for i in range(3):
            stdscr.addstr(7 + i, 2, self.message(i))
        self._show_metrics(stdscr)
        stdscr.addstr(10, 0, "Commands: [TAB]: quit")
        stdscr.addstr(11, 0,
                      "          [T]: Time-sync, [SPACE]: Estop, [P]: Power")
        stdscr.addstr(12, 0, "          [I]: Take image, [O]: Video mode")
        stdscr.addstr(13, 0, "          [v]: Sit, [f]: Stand, [r]: Self-right")
        stdscr.addstr(14, 0, "          [wasd]: Directional strafing")
        stdscr.addstr(15, 0, "          [qe]: Turning")

        # print as many lines of the image as will fit on the curses screen
        if self._image_task.ascii_image != None:
            max_y, _max_x = stdscr.getmaxyx()
            for y_i, img_line in enumerate(self._image_task.ascii_image):
                if y_i + 16 >= max_y:
                    break
                stdscr.addstr('\n' + img_line)

        stdscr.refresh()

    def _drive_cmd(self, key):
        """Run user commands at each update."""
        try:
            cmd_function = self._command_dictionary[key]
            cmd_function()

        except KeyError:
            if key and key != -1 and key < 256:
                self.add_message("Unrecognized keyboard command: '{}'".format(
                    chr(key)))

    def _try_grpc(self, desc, thunk):
        try:
            return thunk()
        except (ResponseError, RpcError) as err:
            self.add_message("Failed {}: {}".format(desc, err))
            return None

    def _quit_program(self):
        self._sit()
        self._exit_check.request_exit()

    def _toggle_time_sync(self):
        if self._robot.time_sync.stopped:
            self._robot.time_sync.start()
        else:
            self._robot.time_sync.stop()

    def _toggle_estop(self):
        """toggle estop on/off. Initial state is ON"""
        if not self._estop_keepalive:
            self._estop_keepalive = EstopKeepAlive(self._estop_endpoint)
        else:
            self._try_grpc("stopping estop", self._estop_keepalive.stop)
            self._estop_keepalive.shutdown()
            self._estop_keepalive = None

    def _start_robot_command(self, desc, command_proto, end_time_secs=None):
        def _start_command():
            self._robot_command_client.robot_command(
                lease=None, command=command_proto, end_time_secs=end_time_secs)

        self._try_grpc(desc, _start_command)

    def _self_right(self):
        self._start_robot_command('self_right',
                                  RobotCommandBuilder.selfright_command())

    def _sit(self):
        self._start_robot_command('sit', RobotCommandBuilder.sit_command())

    def _stand(self):
        self._start_robot_command('stand', RobotCommandBuilder.stand_command())

    def _move_forward(self):
        self._velocity_cmd_helper('move_forward', v_x=VELOCITY_BASE_SPEED)

    def _move_backward(self):
        self._velocity_cmd_helper('move_backward', v_x=-VELOCITY_BASE_SPEED)

    def _strafe_left(self):
        self._velocity_cmd_helper('strafe_left', v_y=VELOCITY_BASE_SPEED)

    def _strafe_right(self):
        self._velocity_cmd_helper('strafe_right', v_y=-VELOCITY_BASE_SPEED)

    def _turn_left(self):
        self._velocity_cmd_helper('turn_left', v_rot=VELOCITY_BASE_ANGULAR)

    def _turn_right(self):
        self._velocity_cmd_helper('turn_right', v_rot=-VELOCITY_BASE_ANGULAR)

    def _velocity_cmd_helper(self, desc='', v_x=0.0, v_y=0.0, v_rot=0.0):
        self._start_robot_command(
            desc,
            RobotCommandBuilder.velocity_command(v_x=v_x, v_y=v_y,
                                                 v_rot=v_rot),
            end_time_secs=time.time() + VELOCITY_CMD_DURATION)

    #  This command makes the robot move very fast and the 'origin' is very unpredictable.
    def _return_to_origin(self):
        self._start_robot_command(
            'fwd_and_rotate',
            RobotCommandBuilder.trajectory_command(
                goal_x=0.0,
                goal_y=0.0,
                goal_heading=0.0,
                frame=geometry_pb2.Frame(base_frame=geometry_pb2.FRAME_KO),
                params=None,
                body_height=0.0,
                locomotion_hint=spot_command_pb2.HINT_SPEED_SELECT_TROT),
            end_time_secs=time.time() + 20)

    def _take_ascii_image(self):
        source_name = "frontright_fisheye_image"
        image_response = self._image_client.get_image_from_sources(
            [source_name])
        image = Image.open(io.BytesIO(image_response[0].shot.image.data))
        ascii_image = self._ascii_converter.convert_to_ascii(image,
                                                             new_width=70)
        self._last_image_ascii = ascii_image

    def _toggle_ascii_video(self):
        if self._video_mode:
            self._video_mode = False
        else:
            self._video_mode = True

    # def _clear_behavior_faults(self):
    #     cmd = robot_state_proto.GetRobotState()
    #     self._start_robot_command(cmd)
    #     robot_command_pb2.clear_behavior_fault(behavior_fault_id = , lease = self._lease)

    def _toggle_power(self):
        power_state = self._power_state()
        if power_state is None:
            self.add_message(
                'Could not toggle power because power state is unknown')
            return

        if power_state == robot_state_proto.PowerState.STATE_OFF:
            self._try_grpc("powering-on", self._request_power_on)
        else:
            self._try_grpc("powering-off", self._safe_power_off)

    def _request_power_on(self):
        bosdyn.client.power.power_on(self._power_client)

    def _safe_power_off(self):
        self._start_robot_command('safe_power_off',
                                  RobotCommandBuilder.safe_power_off_command())

    def _show_metrics(self, stdscr):
        metrics = self.robot_metrics
        if not metrics:
            return
        for idx, metric in enumerate(metrics.metrics):
            stdscr.addstr(2 + idx, 50, format_metric(metric))

    def _power_state(self):
        state = self.robot_state
        if not state:
            return None
        return state.power_state.motor_power_state

    def _lease_str(self, lease_keep_alive):
        alive = 'RUNNING' if lease_keep_alive.is_alive() else 'STOPPED'
        lease = '{}:{}'.format(self._lease.lease_proto.resource,
                               self._lease.lease_proto.sequence)
        return 'Lease {} THREAD:{}'.format(lease, alive)

    def _power_state_str(self):
        power_state = self._power_state()
        if power_state is None:
            return ''
        state_str = robot_state_proto.PowerState.MotorPowerState.Name(
            power_state)
        return 'Power: {}'.format(state_str[6:])  # get rid of STATE_ prefix

    def _estop_str(self):
        thread_status = 'RUNNING' if self._estop_keepalive else 'STOPPED'
        estop_status = '??'
        state = self.robot_state
        if state:
            for estop_state in state.estop_states:
                if estop_state.type == estop_state.TYPE_SOFTWARE:
                    estop_status = estop_state.State.Name(
                        estop_state.state)[6:]  # s/STATE_//
                    break
        return 'Estop {} (thread: {})'.format(estop_status, thread_status)

    def _time_sync_str(self):
        if not self._robot.time_sync:
            return 'Time sync: (none)'
        if self._robot.time_sync.stopped:
            status = 'STOPPED'
            exception = self._robot.time_sync.thread_exception
            if exception:
                status = '{} Exception: {}'.format(status, exception)
        else:
            status = 'RUNNING'
        try:
            skew = self._robot.time_sync.get_robot_clock_skew()
            if skew:
                skew_str = 'offset={}'.format(duration_str(skew))
            else:
                skew_str = "(Skew undetermined)"
        except (TimeSyncError, RpcError) as err:
            skew_str = '({})'.format(err)
        return 'Time sync: {} {}'.format(status, skew_str)

    def _battery_str(self):
        if not self.robot_state:
            return ''
        battery_state = self.robot_state.battery_states[0]
        status = battery_state.Status.Name(battery_state.status)
        status = status[7:]  # get rid of STATUS_ prefix
        if battery_state.charge_percentage.value:
            bar_len = int(battery_state.charge_percentage.value) // 10
            bat_bar = '|{}{}|'.format('=' * bar_len, ' ' * (10 - bar_len))
        else:
            bat_bar = ''
        time_left = ''
        if battery_state.estimated_runtime:
            time_left = ' ({})'.format(
                secs_to_hms(battery_state.estimated_runtime.seconds))
        return 'Battery: {}{}{}'.format(status, bat_bar, time_left)
示例#7
0
    def __init__(self,
                 username,
                 password,
                 hostname,
                 logger,
                 rates={},
                 callbacks={}):
        self._username = username
        self._password = password
        self._hostname = hostname
        self._logger = logger
        self._rates = rates
        self._callbacks = callbacks
        self._keep_alive = True
        self._valid = True

        self._mobility_params = RobotCommandBuilder.mobility_params()
        self._is_standing = False
        self._is_sitting = True
        self._is_moving = False
        self._last_stand_command = None
        self._last_sit_command = None
        self._last_motion_command = None
        self._last_motion_command_time = None

        self._front_image_requests = []
        for source in front_image_sources:
            self._front_image_requests.append(
                build_image_request(
                    source, image_format=image_pb2.Image.Format.FORMAT_RAW))

        self._side_image_requests = []
        for source in side_image_sources:
            self._side_image_requests.append(
                build_image_request(
                    source, image_format=image_pb2.Image.Format.FORMAT_RAW))

        self._rear_image_requests = []
        for source in rear_image_sources:
            self._rear_image_requests.append(
                build_image_request(
                    source, image_format=image_pb2.Image.Format.FORMAT_RAW))

        try:
            self._sdk = create_standard_sdk('ros_spot')
        except Exception as e:
            self._logger.error("Error creating SDK object: %s", e)
            self._valid = False
            return

        self._robot = self._sdk.create_robot(self._hostname)

        try:
            self._robot.authenticate(self._username, self._password)
            self._robot.start_time_sync()
        except RpcError as err:
            self._logger.error("Failed to communicate with robot: %s", err)
            self._valid = False
            return

        if self._robot:
            # Clients
            try:
                self._robot_state_client = self._robot.ensure_client(
                    RobotStateClient.default_service_name)
                self._robot_command_client = self._robot.ensure_client(
                    RobotCommandClient.default_service_name)
                self._graph_nav_client = self._robot.ensure_client(
                    GraphNavClient.default_service_name)
                self._power_client = self._robot.ensure_client(
                    PowerClient.default_service_name)
                self._lease_client = self._robot.ensure_client(
                    LeaseClient.default_service_name)
                self._lease_wallet = self._lease_client.lease_wallet
                self._image_client = self._robot.ensure_client(
                    ImageClient.default_service_name)
                self._estop_client = self._robot.ensure_client(
                    EstopClient.default_service_name)
            except Exception as e:
                self._logger.error("Unable to create client service: %s", e)
                self._valid = False
                return

            # Store the most recent knowledge of the state of the robot based on rpc calls.
            self._current_graph = None
            self._current_edges = dict(
            )  #maps to_waypoint to list(from_waypoint)
            self._current_waypoint_snapshots = dict(
            )  # maps id to waypoint snapshot
            self._current_edge_snapshots = dict()  # maps id to edge snapshot
            self._current_annotation_name_to_wp_id = dict()

            # Async Tasks
            self._async_task_list = []
            self._robot_state_task = AsyncRobotState(
                self._robot_state_client, self._logger,
                max(0.0, self._rates.get("robot_state", 0.0)),
                self._callbacks.get("robot_state", lambda: None))
            self._robot_metrics_task = AsyncMetrics(
                self._robot_state_client, self._logger,
                max(0.0, self._rates.get("metrics", 0.0)),
                self._callbacks.get("metrics", lambda: None))
            self._lease_task = AsyncLease(
                self._lease_client, self._logger,
                max(0.0, self._rates.get("lease", 0.0)),
                self._callbacks.get("lease", lambda: None))
            self._front_image_task = AsyncImageService(
                self._image_client, self._logger,
                max(0.0, self._rates.get("front_image", 0.0)),
                self._callbacks.get("front_image", lambda: None),
                self._front_image_requests)
            self._side_image_task = AsyncImageService(
                self._image_client, self._logger,
                max(0.0, self._rates.get("side_image", 0.0)),
                self._callbacks.get("side_image", lambda: None),
                self._side_image_requests)
            self._rear_image_task = AsyncImageService(
                self._image_client, self._logger,
                max(0.0, self._rates.get("rear_image", 0.0)),
                self._callbacks.get("rear_image", lambda: None),
                self._rear_image_requests)
            self._idle_task = AsyncIdle(self._robot_command_client,
                                        self._logger, 10.0, self)

            self._estop_endpoint = None

            self._async_tasks = AsyncTasks([
                self._robot_state_task, self._robot_metrics_task,
                self._lease_task, self._front_image_task,
                self._side_image_task, self._rear_image_task, self._idle_task
            ])

            self._robot_id = None
            self._lease = None
示例#8
0
class SpotWrapper():
    """Generic wrapper class to encompass release 1.1.4 API features as well as maintaining leases automatically"""
    def __init__(self,
                 username,
                 password,
                 hostname,
                 logger,
                 rates={},
                 callbacks={}):
        self._username = username
        self._password = password
        self._hostname = hostname
        self._logger = logger
        self._rates = rates
        self._callbacks = callbacks
        self._keep_alive = True
        self._valid = True

        self._mobility_params = RobotCommandBuilder.mobility_params()
        self._is_standing = False
        self._is_sitting = True
        self._is_moving = False
        self._last_stand_command = None
        self._last_sit_command = None
        self._last_motion_command = None
        self._last_motion_command_time = None

        self._front_image_requests = []
        for source in front_image_sources:
            self._front_image_requests.append(
                build_image_request(
                    source, image_format=image_pb2.Image.Format.FORMAT_RAW))

        self._side_image_requests = []
        for source in side_image_sources:
            self._side_image_requests.append(
                build_image_request(
                    source, image_format=image_pb2.Image.Format.FORMAT_RAW))

        self._rear_image_requests = []
        for source in rear_image_sources:
            self._rear_image_requests.append(
                build_image_request(
                    source, image_format=image_pb2.Image.Format.FORMAT_RAW))

        try:
            self._sdk = create_standard_sdk('ros_spot')
        except Exception as e:
            self._logger.error("Error creating SDK object: %s", e)
            self._valid = False
            return

        self._robot = self._sdk.create_robot(self._hostname)

        try:
            self._robot.authenticate(self._username, self._password)
            self._robot.start_time_sync()
        except RpcError as err:
            self._logger.error("Failed to communicate with robot: %s", err)
            self._valid = False
            return

        if self._robot:
            # Clients
            try:
                self._robot_state_client = self._robot.ensure_client(
                    RobotStateClient.default_service_name)
                self._robot_command_client = self._robot.ensure_client(
                    RobotCommandClient.default_service_name)
                self._graph_nav_client = self._robot.ensure_client(
                    GraphNavClient.default_service_name)
                self._power_client = self._robot.ensure_client(
                    PowerClient.default_service_name)
                self._lease_client = self._robot.ensure_client(
                    LeaseClient.default_service_name)
                self._lease_wallet = self._lease_client.lease_wallet
                self._image_client = self._robot.ensure_client(
                    ImageClient.default_service_name)
                self._estop_client = self._robot.ensure_client(
                    EstopClient.default_service_name)
            except Exception as e:
                self._logger.error("Unable to create client service: %s", e)
                self._valid = False
                return

            # Store the most recent knowledge of the state of the robot based on rpc calls.
            self._current_graph = None
            self._current_edges = dict(
            )  #maps to_waypoint to list(from_waypoint)
            self._current_waypoint_snapshots = dict(
            )  # maps id to waypoint snapshot
            self._current_edge_snapshots = dict()  # maps id to edge snapshot
            self._current_annotation_name_to_wp_id = dict()

            # Async Tasks
            self._async_task_list = []
            self._robot_state_task = AsyncRobotState(
                self._robot_state_client, self._logger,
                max(0.0, self._rates.get("robot_state", 0.0)),
                self._callbacks.get("robot_state", lambda: None))
            self._robot_metrics_task = AsyncMetrics(
                self._robot_state_client, self._logger,
                max(0.0, self._rates.get("metrics", 0.0)),
                self._callbacks.get("metrics", lambda: None))
            self._lease_task = AsyncLease(
                self._lease_client, self._logger,
                max(0.0, self._rates.get("lease", 0.0)),
                self._callbacks.get("lease", lambda: None))
            self._front_image_task = AsyncImageService(
                self._image_client, self._logger,
                max(0.0, self._rates.get("front_image", 0.0)),
                self._callbacks.get("front_image", lambda: None),
                self._front_image_requests)
            self._side_image_task = AsyncImageService(
                self._image_client, self._logger,
                max(0.0, self._rates.get("side_image", 0.0)),
                self._callbacks.get("side_image", lambda: None),
                self._side_image_requests)
            self._rear_image_task = AsyncImageService(
                self._image_client, self._logger,
                max(0.0, self._rates.get("rear_image", 0.0)),
                self._callbacks.get("rear_image", lambda: None),
                self._rear_image_requests)
            self._idle_task = AsyncIdle(self._robot_command_client,
                                        self._logger, 10.0, self)

            self._estop_endpoint = None

            self._async_tasks = AsyncTasks([
                self._robot_state_task, self._robot_metrics_task,
                self._lease_task, self._front_image_task,
                self._side_image_task, self._rear_image_task, self._idle_task
            ])

            self._robot_id = None
            self._lease = None

    @property
    def is_valid(self):
        """Return boolean indicating if the wrapper initialized successfully"""
        return self._valid

    @property
    def id(self):
        """Return robot's ID"""
        return self._robot_id

    @property
    def robot_state(self):
        """Return latest proto from the _robot_state_task"""
        return self._robot_state_task.proto

    @property
    def metrics(self):
        """Return latest proto from the _robot_metrics_task"""
        return self._robot_metrics_task.proto

    @property
    def lease(self):
        """Return latest proto from the _lease_task"""
        return self._lease_task.proto

    @property
    def front_images(self):
        """Return latest proto from the _front_image_task"""
        return self._front_image_task.proto

    @property
    def side_images(self):
        """Return latest proto from the _side_image_task"""
        return self._side_image_task.proto

    @property
    def rear_images(self):
        """Return latest proto from the _rear_image_task"""
        return self._rear_image_task.proto

    @property
    def is_standing(self):
        """Return boolean of standing state"""
        return self._is_standing

    @property
    def is_sitting(self):
        """Return boolean of standing state"""
        return self._is_sitting

    @property
    def is_moving(self):
        """Return boolean of walking state"""
        return self._is_moving

    @property
    def time_skew(self):
        """Return the time skew between local and spot time"""
        return self._robot.time_sync.endpoint.clock_skew

    def robotToLocalTime(self, timestamp):
        """Takes a timestamp and an estimated skew and return seconds and nano seconds

        Args:
            timestamp: google.protobuf.Timestamp
        Returns:
            google.protobuf.Timestamp
        """

        rtime = Timestamp()

        rtime.seconds = timestamp.seconds - self.time_skew.seconds
        rtime.nanos = timestamp.nanos - self.time_skew.nanos
        if rtime.nanos < 0:
            rtime.nanos = rtime.nanos + 1000000000
            rtime.seconds = rtime.seconds - 1

        # Workaround for timestamps being incomplete
        if rtime.seconds < 0:
            rtime.seconds = 0
            rtime.nanos = 0

        return rtime

    def claim(self):
        """Get a lease for the robot, a handle on the estop endpoint, and the ID of the robot."""
        try:
            self._robot_id = self._robot.get_id()
            self.getLease()
            self.resetEStop()
            return True, "Success"
        except (ResponseError, RpcError) as err:
            self._logger.error("Failed to initialize robot communication: %s",
                               err)
            return False, str(err)

    def updateTasks(self):
        """Loop through all periodic tasks and update their data if needed."""
        self._async_tasks.update()

    def resetEStop(self):
        """Get keepalive for eStop"""
        self._estop_endpoint = EstopEndpoint(self._estop_client, 'ros', 9.0)
        self._estop_endpoint.force_simple_setup(
        )  # Set this endpoint as the robot's sole estop.
        self._estop_keepalive = EstopKeepAlive(self._estop_endpoint)

    def assertEStop(self, severe=True):
        """Forces the robot into eStop state.

        Args:
            severe: Default True - If true, will cut motor power immediately.  If false, will try to settle the robot on the ground first
        """
        try:
            if severe:
                self._estop_endpoint.stop()
            else:
                self._estop_endpoint.settle_then_cut()

            return True, "Success"
        except:
            return False, "Error"

    def releaseEStop(self):
        """Stop eStop keepalive"""
        if self._estop_keepalive:
            self._estop_keepalive.stop()
            self._estop_keepalive = None
            self._estop_endpoint = None

    def getLease(self):
        """Get a lease for the robot and keep the lease alive automatically."""
        self._lease = self._lease_client.acquire()
        self._lease_keepalive = LeaseKeepAlive(self._lease_client)

    def releaseLease(self):
        """Return the lease on the body."""
        if self._lease:
            self._lease_client.return_lease(self._lease)
            self._lease = None

    def release(self):
        """Return the lease on the body and the eStop handle."""
        try:
            self.releaseLease()
            self.releaseEStop()
            return True, "Success"
        except Exception as e:
            return False, str(e)

    def disconnect(self):
        """Release control of robot as gracefully as posssible."""
        if self._robot.time_sync:
            self._robot.time_sync.stop()
        self.releaseLease()
        self.releaseEStop()

    def _robot_command(self, command_proto, end_time_secs=None):
        """Generic blocking function for sending commands to robots.

        Args:
            command_proto: robot_command_pb2 object to send to the robot.  Usually made with RobotCommandBuilder
            end_time_secs: (optional) Time-to-live for the command in seconds
        """
        try:
            id = self._robot_command_client.robot_command(
                lease=None, command=command_proto, end_time_secs=end_time_secs)
            return True, "Success", id
        except Exception as e:
            return False, str(e), None

    def stop(self):
        """Stop the robot's motion."""
        response = self._robot_command(RobotCommandBuilder.stop_command())
        return response[0], response[1]

    def self_right(self):
        """Have the robot self-right itself."""
        response = self._robot_command(RobotCommandBuilder.selfright_command())
        return response[0], response[1]

    def sit(self):
        """Stop the robot's motion and sit down if able."""
        response = self._robot_command(RobotCommandBuilder.sit_command())
        self._last_sit_command = response[2]
        return response[0], response[1]

    def stand(self, monitor_command=True):
        """If the e-stop is enabled, and the motor power is enabled, stand the robot up."""
        response = self._robot_command(
            RobotCommandBuilder.synchro_stand_command(
                params=self._mobility_params))
        if monitor_command:
            self._last_stand_command = response[2]
        return response[0], response[1]

    def safe_power_off(self):
        """Stop the robot's motion and sit if possible.  Once sitting, disable motor power."""
        response = self._robot_command(
            RobotCommandBuilder.safe_power_off_command())
        return response[0], response[1]

    def power_on(self):
        """Enble the motor power if e-stop is enabled."""
        try:
            power.power_on(self._power_client)
            return True, "Success"
        except:
            return False, "Error"

    def set_mobility_params(self, mobility_params):
        """Set Params for mobility and movement

        Args:
            mobility_params: spot.MobilityParams, params for spot mobility commands.
        """
        self._mobility_params = mobility_params

    def get_mobility_params(self):
        """Get mobility params
        """
        return self._mobility_params

    def velocity_cmd(self, v_x, v_y, v_rot, cmd_duration=0.1):
        """Send a velocity motion command to the robot.

        Args:
            v_x: Velocity in the X direction in meters
            v_y: Velocity in the Y direction in meters
            v_rot: Angular velocity around the Z axis in radians
            cmd_duration: (optional) Time-to-live for the command in seconds.  Default is 125ms (assuming 10Hz command rate).
        """
        end_time = time.time() + cmd_duration
        self._robot_command(RobotCommandBuilder.synchro_velocity_command(
            v_x=v_x, v_y=v_y, v_rot=v_rot, params=self._mobility_params),
                            end_time_secs=end_time)
        self._last_motion_command_time = end_time

    def list_graph(self, upload_path):
        """List waypoint ids of garph_nav
        Args:
          upload_path : Path to the root directory of the map.
        """
        ids, eds = self._list_graph_waypoint_and_edge_ids()
        # skip waypoint_ for v2.2.1, skip waypiont for < v2.2
        return [
            v for k, v in sorted(
                ids.items(),
                key=lambda id: int(id[0].replace('waypoint_', '')))
        ]

    def navigate_to(self,
                    upload_path,
                    navigate_to,
                    initial_localization_fiducial=True,
                    initial_localization_waypoint=None):
        """ navigate with graph nav.

        Args:
           upload_path : Path to the root directory of the map.
           navigate_to : Waypont id string for where to goal
           initial_localization_fiducial : Tells the initializer whether to use fiducials
           initial_localization_waypoint : Waypoint id string of current robot position (optional)
        """
        # Filepath for uploading a saved graph's and snapshots too.
        if upload_path[-1] == "/":
            upload_filepath = upload_path[:-1]
        else:
            upload_filepath = upload_path

        # Boolean indicating the robot's power state.
        power_state = self._robot_state_client.get_robot_state().power_state
        self._started_powered_on = (
            power_state.motor_power_state == power_state.STATE_ON)
        self._powered_on = self._started_powered_on

        # FIX ME somehow,,,, if the robot is stand, need to sit the robot before starting garph nav
        if self.is_standing and not self.is_moving:
            self.sit()

        # TODO verify estop  / claim / power_on
        self._clear_graph()
        self._upload_graph_and_snapshots(upload_filepath)
        if initial_localization_fiducial:
            self._set_initial_localization_fiducial()
        if initial_localization_waypoint:
            self._set_initial_localization_waypoint(
                [initial_localization_waypoint])
        self._list_graph_waypoint_and_edge_ids()
        self._get_localization_state()
        resp = self._navigate_to([navigate_to])

        return resp

    ## copy from spot-sdk/python/examples/graph_nav_command_line/graph_nav_command_line.py
    def _get_localization_state(self, *args):
        """Get the current localization and state of the robot."""
        state = self._graph_nav_client.get_localization_state()
        print('Got localization: \n%s' % str(state.localization))
        odom_tform_body = get_odom_tform_body(
            state.robot_kinematics.transforms_snapshot)
        print('Got robot state in kinematic odometry frame: \n%s' %
              str(odom_tform_body))

    def _set_initial_localization_fiducial(self, *args):
        """Trigger localization when near a fiducial."""
        robot_state = self._robot_state_client.get_robot_state()
        current_odom_tform_body = get_odom_tform_body(
            robot_state.kinematic_state.transforms_snapshot).to_proto()
        # Create an empty instance for initial localization since we are asking it to localize
        # based on the nearest fiducial.
        localization = nav_pb2.Localization()
        self._graph_nav_client.set_localization(
            initial_guess_localization=localization,
            ko_tform_body=current_odom_tform_body)

    def _set_initial_localization_waypoint(self, *args):
        """Trigger localization to a waypoint."""
        # Take the first argument as the localization waypoint.
        if len(args) < 1:
            # If no waypoint id is given as input, then return without initializing.
            print("No waypoint specified to initialize to.")
            return
        destination_waypoint = graph_nav_util.find_unique_waypoint_id(
            args[0][0], self._current_graph,
            self._current_annotation_name_to_wp_id)
        if not destination_waypoint:
            # Failed to find the unique waypoint id.
            return

        robot_state = self._robot_state_client.get_robot_state()
        current_odom_tform_body = get_odom_tform_body(
            robot_state.kinematic_state.transforms_snapshot).to_proto()
        # Create an initial localization to the specified waypoint as the identity.
        localization = nav_pb2.Localization()
        localization.waypoint_id = destination_waypoint
        localization.waypoint_tform_body.rotation.w = 1.0
        self._graph_nav_client.set_localization(
            initial_guess_localization=localization,
            # It's hard to get the pose perfect, search +/-20 deg and +/-20cm (0.2m).
            max_distance=0.2,
            max_yaw=20.0 * math.pi / 180.0,
            fiducial_init=graph_nav_pb2.SetLocalizationRequest.
            FIDUCIAL_INIT_NO_FIDUCIAL,
            ko_tform_body=current_odom_tform_body)

    def _list_graph_waypoint_and_edge_ids(self, *args):
        """List the waypoint ids and edge ids of the graph currently on the robot."""

        # Download current graph
        graph = self._graph_nav_client.download_graph()
        if graph is None:
            print("Empty graph.")
            return
        self._current_graph = graph

        localization_id = self._graph_nav_client.get_localization_state(
        ).localization.waypoint_id

        # Update and print waypoints and edges
        self._current_annotation_name_to_wp_id, self._current_edges = graph_nav_util.update_waypoints_and_edges(
            graph, localization_id)
        return self._current_annotation_name_to_wp_id, self._current_edges

    def _upload_graph_and_snapshots(self, upload_filepath):
        """Upload the graph and snapshots to the robot."""
        print("Loading the graph from disk into local storage...")
        with open(upload_filepath + "/graph", "rb") as graph_file:
            # Load the graph from disk.
            data = graph_file.read()
            self._current_graph = map_pb2.Graph()
            self._current_graph.ParseFromString(data)
            print("Loaded graph has {} waypoints and {} edges".format(
                len(self._current_graph.waypoints),
                len(self._current_graph.edges)))
        for waypoint in self._current_graph.waypoints:
            # Load the waypoint snapshots from disk.
            with open(
                    upload_filepath +
                    "/waypoint_snapshots/{}".format(waypoint.snapshot_id),
                    "rb") as snapshot_file:
                waypoint_snapshot = map_pb2.WaypointSnapshot()
                waypoint_snapshot.ParseFromString(snapshot_file.read())
                self._current_waypoint_snapshots[
                    waypoint_snapshot.id] = waypoint_snapshot
        for edge in self._current_graph.edges:
            # Load the edge snapshots from disk.
            with open(
                    upload_filepath +
                    "/edge_snapshots/{}".format(edge.snapshot_id),
                    "rb") as snapshot_file:
                edge_snapshot = map_pb2.EdgeSnapshot()
                edge_snapshot.ParseFromString(snapshot_file.read())
                self._current_edge_snapshots[edge_snapshot.id] = edge_snapshot
        # Upload the graph to the robot.
        print("Uploading the graph and snapshots to the robot...")
        self._graph_nav_client.upload_graph(lease=self._lease.lease_proto,
                                            graph=self._current_graph)
        # Upload the snapshots to the robot.
        for waypoint_snapshot in self._current_waypoint_snapshots.values():
            self._graph_nav_client.upload_waypoint_snapshot(waypoint_snapshot)
            print("Uploaded {}".format(waypoint_snapshot.id))
        for edge_snapshot in self._current_edge_snapshots.values():
            self._graph_nav_client.upload_edge_snapshot(edge_snapshot)
            print("Uploaded {}".format(edge_snapshot.id))

        # The upload is complete! Check that the robot is localized to the graph,
        # and it if is not, prompt the user to localize the robot before attempting
        # any navigation commands.
        localization_state = self._graph_nav_client.get_localization_state()
        if not localization_state.localization.waypoint_id:
            # The robot is not localized to the newly uploaded graph.
            print("\n")
            print("Upload complete! The robot is currently not localized to the map; please localize", \
                   "the robot using commands (2) or (3) before attempting a navigation command.")

    def _navigate_to(self, *args):
        """Navigate to a specific waypoint."""
        # Take the first argument as the destination waypoint.
        if len(args) < 1:
            # If no waypoint id is given as input, then return without requesting navigation.
            print("No waypoint provided as a destination for navigate to.")
            return

        self._lease = self._lease_wallet.get_lease()
        destination_waypoint = graph_nav_util.find_unique_waypoint_id(
            args[0][0], self._current_graph,
            self._current_annotation_name_to_wp_id)
        if not destination_waypoint:
            # Failed to find the appropriate unique waypoint id for the navigation command.
            return
        if not self.toggle_power(should_power_on=True):
            print(
                "Failed to power on the robot, and cannot complete navigate to request."
            )
            return

        # Stop the lease keepalive and create a new sublease for graph nav.
        self._lease = self._lease_wallet.advance()
        sublease = self._lease.create_sublease()
        self._lease_keepalive.shutdown()

        # Navigate to the destination waypoint.
        is_finished = False
        nav_to_cmd_id = -1
        while not is_finished:
            # Issue the navigation command about twice a second such that it is easy to terminate the
            # navigation command (with estop or killing the program).
            nav_to_cmd_id = self._graph_nav_client.navigate_to(
                destination_waypoint, 1.0, leases=[sublease.lease_proto])
            time.sleep(
                .5)  # Sleep for half a second to allow for command execution.
            # Poll the robot for feedback to determine if the navigation command is complete. Then sit
            # the robot down once it is finished.
            is_finished = self._check_success(nav_to_cmd_id)

        self._lease = self._lease_wallet.advance()
        self._lease_keepalive = LeaseKeepAlive(self._lease_client)

        # Update the lease and power off the robot if appropriate.
        if self._powered_on and not self._started_powered_on:
            # Sit the robot down + power off after the navigation command is complete.
            self.toggle_power(should_power_on=False)

        status = self._graph_nav_client.navigation_feedback(nav_to_cmd_id)
        if status.status == graph_nav_pb2.NavigationFeedbackResponse.STATUS_REACHED_GOAL:
            return True, "Successfully completed the navigation commands!"
        elif status.status == graph_nav_pb2.NavigationFeedbackResponse.STATUS_LOST:
            return False, "Robot got lost when navigating the route, the robot will now sit down."
        elif status.status == graph_nav_pb2.NavigationFeedbackResponse.STATUS_STUCK:
            return False, "Robot got stuck when navigating the route, the robot will now sit down."
        elif status.status == graph_nav_pb2.NavigationFeedbackResponse.STATUS_ROBOT_IMPAIRED:
            return False, "Robot is impaired."
        else:
            return False, "Navigation command is not complete yet."

    def _navigate_route(self, *args):
        """Navigate through a specific route of waypoints."""
        if len(args) < 1:
            # If no waypoint ids are given as input, then return without requesting navigation.
            print("No waypoints provided for navigate route.")
            return
        waypoint_ids = args[0]
        for i in range(len(waypoint_ids)):
            waypoint_ids[i] = graph_nav_util.find_unique_waypoint_id(
                waypoint_ids[i], self._current_graph,
                self._current_annotation_name_to_wp_id)
            if not waypoint_ids[i]:
                # Failed to find the unique waypoint id.
                return

        edge_ids_list = []
        all_edges_found = True
        # Attempt to find edges in the current graph that match the ordered waypoint pairs.
        # These are necessary to create a valid route.
        for i in range(len(waypoint_ids) - 1):
            start_wp = waypoint_ids[i]
            end_wp = waypoint_ids[i + 1]
            edge_id = self._match_edge(self._current_edges, start_wp, end_wp)
            if edge_id is not None:
                edge_ids_list.append(edge_id)
            else:
                all_edges_found = False
                print("Failed to find an edge between waypoints: ", start_wp,
                      " and ", end_wp)
                print(
                    "List the graph's waypoints and edges to ensure pairs of waypoints has an edge."
                )
                break

        self._lease = self._lease_wallet.get_lease()
        if all_edges_found:
            if not self.toggle_power(should_power_on=True):
                print(
                    "Failed to power on the robot, and cannot complete navigate route request."
                )
                return

            # Stop the lease keepalive and create a new sublease for graph nav.
            self._lease = self._lease_wallet.advance()
            sublease = self._lease.create_sublease()
            self._lease_keepalive.shutdown()

            # Navigate a specific route.
            route = self._graph_nav_client.build_route(waypoint_ids,
                                                       edge_ids_list)
            is_finished = False
            while not is_finished:
                # Issue the route command about twice a second such that it is easy to terminate the
                # navigation command (with estop or killing the program).
                nav_route_command_id = self._graph_nav_client.navigate_route(
                    route, cmd_duration=1.0, leases=[sublease.lease_proto])
                time.sleep(
                    .5
                )  # Sleep for half a second to allow for command execution.
                # Poll the robot for feedback to determine if the route is complete. Then sit
                # the robot down once it is finished.
                is_finished = self._check_success(nav_route_command_id)

            self._lease = self._lease_wallet.advance()
            self._lease_keepalive = LeaseKeepAlive(self._lease_client)

            # Update the lease and power off the robot if appropriate.
            if self._powered_on and not self._started_powered_on:
                # Sit the robot down + power off after the navigation command is complete.
                self.toggle_power(should_power_on=False)

    def _clear_graph(self, *args):
        """Clear the state of the map on the robot, removing all waypoints and edges."""
        return self._graph_nav_client.clear_graph(
            lease=self._lease.lease_proto)

    def toggle_power(self, should_power_on):
        """Power the robot on/off dependent on the current power state."""
        is_powered_on = self.check_is_powered_on()
        if not is_powered_on and should_power_on:
            # Power on the robot up before navigating when it is in a powered-off state.
            power_on(self._power_client)
            motors_on = False
            while not motors_on:
                future = self._robot_state_client.get_robot_state_async()
                state_response = future.result(
                    timeout=10
                )  # 10 second timeout for waiting for the state response.
                if state_response.power_state.motor_power_state == robot_state_pb2.PowerState.STATE_ON:
                    motors_on = True
                else:
                    # Motors are not yet fully powered on.
                    time.sleep(.25)
        elif is_powered_on and not should_power_on:
            # Safe power off (robot will sit then power down) when it is in a
            # powered-on state.
            safe_power_off(self._robot_command_client,
                           self._robot_state_client)
        else:
            # Return the current power state without change.
            return is_powered_on
        # Update the locally stored power state.
        self.check_is_powered_on()
        return self._powered_on

    def check_is_powered_on(self):
        """Determine if the robot is powered on or off."""
        power_state = self._robot_state_client.get_robot_state().power_state
        self._powered_on = (
            power_state.motor_power_state == power_state.STATE_ON)
        return self._powered_on

    def _check_success(self, command_id=-1):
        """Use a navigation command id to get feedback from the robot and sit when command succeeds."""
        if command_id == -1:
            # No command, so we have not status to check.
            return False
        status = self._graph_nav_client.navigation_feedback(command_id)
        if status.status == graph_nav_pb2.NavigationFeedbackResponse.STATUS_REACHED_GOAL:
            # Successfully completed the navigation commands!
            return True
        elif status.status == graph_nav_pb2.NavigationFeedbackResponse.STATUS_LOST:
            print(
                "Robot got lost when navigating the route, the robot will now sit down."
            )
            return True
        elif status.status == graph_nav_pb2.NavigationFeedbackResponse.STATUS_STUCK:
            print(
                "Robot got stuck when navigating the route, the robot will now sit down."
            )
            return True
        elif status.status == graph_nav_pb2.NavigationFeedbackResponse.STATUS_ROBOT_IMPAIRED:
            print("Robot is impaired.")
            return True
        else:
            # Navigation command is not complete yet.
            return False

    def _match_edge(self, current_edges, waypoint1, waypoint2):
        """Find an edge in the graph that is between two waypoint ids."""
        # Return the correct edge id as soon as it's found.
        for edge_to_id in current_edges:
            for edge_from_id in current_edges[edge_to_id]:
                if (waypoint1 == edge_to_id) and (waypoint2 == edge_from_id):
                    # This edge matches the pair of waypoints! Add it the edge list and continue.
                    return map_pb2.Edge.Id(from_waypoint=waypoint2,
                                           to_waypoint=waypoint1)
                elif (waypoint2 == edge_to_id) and (waypoint1 == edge_from_id):
                    # This edge matches the pair of waypoints! Add it the edge list and continue.
                    return map_pb2.Edge.Id(from_waypoint=waypoint1,
                                           to_waypoint=waypoint2)
        return None
示例#9
0
def main(argv):
    """Command line interface.

    Args:
        argv: List of command-line arguments passed to the program.
    """

    parser = argparse.ArgumentParser()
    parser.add_argument(
        "--model-path",
        required=True,
        help=
        "Local file path to the Tensorflow model, example pre-trained models \
                            can be found at \
                            https://github.com/tensorflow/models/blob/master/research/object_detection/g3doc/detection_model_zoo.md"
    )
    parser.add_argument(
        "--classes",
        default='/classes.json',
        type=str,
        help="File containing json mapping of object class IDs to class names")
    parser.add_argument(
        "--number-tensorflow-processes",
        default=1,
        type=int,
        help="Number of Tensorflow processes to run in parallel")
    parser.add_argument(
        "--detection-threshold",
        default=0.7,
        type=float,
        help="Detection threshold to use for Tensorflow detections")
    parser.add_argument(
        "--sleep-between-capture",
        default=1.0,
        type=float,
        help=
        "Seconds to sleep between each image capture loop iteration, which captures "
        + "an image from all cameras")
    parser.add_argument(
        "--detection-class",
        default=1,
        type=int,
        help="Detection classes to use in the" +
        "Tensorflow model; Default is to use 1, which is a person in the Coco dataset"
    )
    parser.add_argument(
        "--max-processing-delay",
        default=7.0,
        type=float,
        help="Maximum allowed delay for processing an image; " +
        "any image older than this value will be skipped")

    bosdyn.client.util.add_common_arguments(parser)
    options = parser.parse_args(argv)
    try:
        # Make sure the model path is a valid file
        if not _check_model_path(options.model_path):
            return False

        # Check for classes json file, otherwise use the COCO class dictionary
        _check_and_load_json_classes(options.classes)

        global TENSORFLOW_PROCESS_BARRIER  # pylint: disable=global-statement
        TENSORFLOW_PROCESS_BARRIER = Barrier(
            options.number_tensorflow_processes + 1)
        # Start Tensorflow processes
        start_tensorflow_processes(options.number_tensorflow_processes,
                                   options.model_path, options.detection_class,
                                   options.detection_threshold,
                                   options.max_processing_delay)

        # sleep to give the Tensorflow processes time to initialize
        try:
            TENSORFLOW_PROCESS_BARRIER.wait()
        except BrokenBarrierError as exc:
            print(
                f'Error waiting for Tensorflow processes to initialize: {exc}')
            return False
        # Start the API related things

        # Create robot object with a world object client
        sdk = bosdyn.client.create_standard_sdk('SpotFollowClient')
        robot = sdk.create_robot(options.hostname)
        robot.authenticate(options.username, options.password)
        #Time sync is necessary so that time-based filter requests can be converted
        robot.time_sync.wait_for_sync()

        # Verify the robot is not estopped and that an external application has registered and holds
        # an estop endpoint.
        verify_estop(robot)

        # Create the sdk clients
        robot_state_client = robot.ensure_client(
            RobotStateClient.default_service_name)
        robot_command_client = robot.ensure_client(
            RobotCommandClient.default_service_name)
        lease_client = robot.ensure_client(LeaseClient.default_service_name)
        image_client = robot.ensure_client(ImageClient.default_service_name)
        source_list = get_source_list(image_client)
        image_task = AsyncImage(image_client, source_list)
        robot_state_task = AsyncRobotState(robot_state_client)
        task_list = [image_task, robot_state_task]
        _async_tasks = AsyncTasks(task_list)
        print('Detect and follow client connected.')

        lease = lease_client.take()
        lease_keep = LeaseKeepAlive(lease_client)
        # Power on the robot and stand it up
        resp = robot.power_on()
        try:
            blocking_stand(robot_command_client)
        except CommandFailedError as exc:
            print(
                f'Error ({exc}) occurred while trying to stand. Check robot surroundings.'
            )
            return False
        except CommandTimedOutError as exc:
            print(f'Stand command timed out: {exc}')
            return False
        print('Robot powered on and standing.')
        params_set = get_mobility_params()

        # This thread starts the async tasks for image and robot state retrieval
        update_thread = Thread(target=_update_thread, args=[_async_tasks])
        update_thread.daemon = True
        update_thread.start()
        # Wait for the first responses.
        while any(task.proto is None for task in task_list):
            time.sleep(0.1)

        # Start image capture process
        image_capture_thread = Thread(target=capture_images,
                                      args=(
                                          image_task,
                                          options.sleep_between_capture,
                                      ))
        image_capture_thread.start()
        while True:
            # This comes from the tensorflow processes and limits the rate of this loop
            entry = PROCESSED_BOXES_QUEUE.get()
            # find the highest confidence bounding box
            highest_conf_source = _find_highest_conf_source(entry)
            if highest_conf_source is None:
                # no boxes or scores found
                continue
            capture_to_use = entry[highest_conf_source]
            raw_time = capture_to_use['raw_image_time']
            time_gap = time.time() - raw_time
            if time_gap > options.max_processing_delay:
                continue  # Skip image due to delay

            # Find the transform to highest confidence object using the depth sensor
            world_tform_object = get_object_position(
                capture_to_use['world_tform_cam'],
                capture_to_use['visual_image'], capture_to_use['depth_image'],
                capture_to_use['boxes'][0],
                ROTATION_ANGLES[capture_to_use['source']])

            # get_object_position can fail if there is insufficient depth sensor information
            if not world_tform_object:
                continue

            scores = capture_to_use['scores']
            print(
                f'Transform for object with confidence {scores[0]}: {world_tform_object}'
            )
            print(
                f'Process latency: {time.time() - capture_to_use["system_cap_time"]}'
            )
            tag_cmd = get_go_to(world_tform_object, robot_state_task.proto,
                                params_set)
            end_time = 15.0
            if tag_cmd is not None:
                robot_command_client.robot_command(lease=None,
                                                   command=tag_cmd,
                                                   end_time_secs=time.time() +
                                                   end_time)

        return True
    except Exception as exc:  # pylint: disable=broad-except
        LOGGER.error("Spot Tensorflow Detector threw an exception: %s", exc)
        return False
示例#10
0
class SpotWrapper():
    """Generic wrapper class to encompass release 1.1.4 API features as well as maintaining leases automatically"""
    def __init__(self,
                 username,
                 password,
                 hostname,
                 logger,
                 rates={},
                 callbacks={}):
        self._username = username
        self._password = password
        self._hostname = hostname
        self._logger = logger
        self._rates = rates
        self._callbacks = callbacks
        self._keep_alive = True
        self._valid = True

        self._mobility_params = RobotCommandBuilder.mobility_params()
        self._is_standing = False
        self._is_sitting = True
        self._is_moving = False
        self._last_stand_command = None
        self._last_sit_command = None
        self._last_motion_command = None
        self._last_motion_command_time = None

        self._front_image_requests = []
        for source in front_image_sources:
            self._front_image_requests.append(
                build_image_request(
                    source, image_format=image_pb2.Image.Format.FORMAT_RAW))

        self._side_image_requests = []
        for source in side_image_sources:
            self._side_image_requests.append(
                build_image_request(
                    source, image_format=image_pb2.Image.Format.FORMAT_RAW))

        self._rear_image_requests = []
        for source in rear_image_sources:
            self._rear_image_requests.append(
                build_image_request(
                    source, image_format=image_pb2.Image.Format.FORMAT_RAW))

        try:
            self._sdk = create_standard_sdk('ros_spot')
        except Exception as e:
            self._logger.error("Error creating SDK object: %s", e)
            self._valid = False
            return

        self._robot = self._sdk.create_robot(self._hostname)

        try:
            self._robot.authenticate(self._username, self._password)
            self._robot.start_time_sync()
        except RpcError as err:
            self._logger.error("Failed to communicate with robot: %s", err)
            self._valid = False
            return

        if self._robot:
            # Clients
            try:
                self._robot_state_client = self._robot.ensure_client(
                    RobotStateClient.default_service_name)
                self._robot_command_client = self._robot.ensure_client(
                    RobotCommandClient.default_service_name)
                self._power_client = self._robot.ensure_client(
                    PowerClient.default_service_name)
                self._lease_client = self._robot.ensure_client(
                    LeaseClient.default_service_name)
                self._image_client = self._robot.ensure_client(
                    ImageClient.default_service_name)
                self._estop_client = self._robot.ensure_client(
                    EstopClient.default_service_name)
            except Exception as e:
                self._logger.error("Unable to create client service: %s", e)
                self._valid = False
                return

            # Async Tasks
            self._async_task_list = []
            self._robot_state_task = AsyncRobotState(
                self._robot_state_client, self._logger,
                max(0.0, self._rates.get("robot_state", 0.0)),
                self._callbacks.get("robot_state", lambda: None))
            self._robot_metrics_task = AsyncMetrics(
                self._robot_state_client, self._logger,
                max(0.0, self._rates.get("metrics", 0.0)),
                self._callbacks.get("metrics", lambda: None))
            self._lease_task = AsyncLease(
                self._lease_client, self._logger,
                max(0.0, self._rates.get("lease", 0.0)),
                self._callbacks.get("lease", lambda: None))
            self._front_image_task = AsyncImageService(
                self._image_client, self._logger,
                max(0.0, self._rates.get("front_image", 0.0)),
                self._callbacks.get("front_image", lambda: None),
                self._front_image_requests)
            self._side_image_task = AsyncImageService(
                self._image_client, self._logger,
                max(0.0, self._rates.get("side_image", 0.0)),
                self._callbacks.get("side_image", lambda: None),
                self._side_image_requests)
            self._rear_image_task = AsyncImageService(
                self._image_client, self._logger,
                max(0.0, self._rates.get("rear_image", 0.0)),
                self._callbacks.get("rear_image", lambda: None),
                self._rear_image_requests)
            self._idle_task = AsyncIdle(self._robot_command_client,
                                        self._logger, 10.0, self)

            self._estop_endpoint = None

            self._async_tasks = AsyncTasks([
                self._robot_state_task, self._robot_metrics_task,
                self._lease_task, self._front_image_task,
                self._side_image_task, self._rear_image_task, self._idle_task
            ])

            self._robot_id = None
            self._lease = None

    @property
    def is_valid(self):
        """Return boolean indicating if the wrapper initialized successfully"""
        return self._valid

    @property
    def id(self):
        """Return robot's ID"""
        return self._robot_id

    @property
    def robot_state(self):
        """Return latest proto from the _robot_state_task"""
        return self._robot_state_task.proto

    @property
    def metrics(self):
        """Return latest proto from the _robot_metrics_task"""
        return self._robot_metrics_task.proto

    @property
    def lease(self):
        """Return latest proto from the _lease_task"""
        return self._lease_task.proto

    @property
    def front_images(self):
        """Return latest proto from the _front_image_task"""
        return self._front_image_task.proto

    @property
    def side_images(self):
        """Return latest proto from the _side_image_task"""
        return self._side_image_task.proto

    @property
    def rear_images(self):
        """Return latest proto from the _rear_image_task"""
        return self._rear_image_task.proto

    @property
    def is_standing(self):
        """Return boolean of standing state"""
        return self._is_standing

    @property
    def is_sitting(self):
        """Return boolean of standing state"""
        return self._is_sitting

    @property
    def is_moving(self):
        """Return boolean of walking state"""
        return self._is_moving

    @property
    def time_skew(self):
        """Return the time skew between local and spot time"""
        return self._robot.time_sync.endpoint.clock_skew

    def robotToLocalTime(self, timestamp):
        """Takes a timestamp and an estimated skew and return seconds and nano seconds

        Args:
            timestamp: google.protobuf.Timestamp
        Returns:
            google.protobuf.Timestamp
        """

        rtime = Timestamp()

        rtime.seconds = timestamp.seconds - self.time_skew.seconds
        rtime.nanos = timestamp.nanos - self.time_skew.nanos
        if rtime.nanos < 0:
            rtime.nanos = rtime.nanos + 1000000000
            rtime.seconds = rtime.seconds - 1

        # Workaround for timestamps being incomplete
        if rtime.seconds < 0:
            rtime.seconds = 0
            rtime.nanos = 0

        return rtime

    def claim(self):
        """Get a lease for the robot, a handle on the estop endpoint, and the ID of the robot."""
        try:
            self._robot_id = self._robot.get_id()
            self.getLease()
            self.resetEStop()
            return True, "Success"
        except (ResponseError, RpcError) as err:
            self._logger.error("Failed to initialize robot communication: %s",
                               err)
            return False, str(err)

    def updateTasks(self):
        """Loop through all periodic tasks and update their data if needed."""
        self._async_tasks.update()

    def resetEStop(self):
        """Get keepalive for eStop"""
        self._estop_endpoint = EstopEndpoint(self._estop_client, 'ros', 9.0)
        self._estop_endpoint.force_simple_setup(
        )  # Set this endpoint as the robot's sole estop.
        self._estop_keepalive = EstopKeepAlive(self._estop_endpoint)

    def assertEStop(self, severe=True):
        """Forces the robot into eStop state.

        Args:
            severe: Default True - If true, will cut motor power immediately.  If false, will try to settle the robot on the ground first
        """
        try:
            if severe:
                self._estop_endpoint.stop()
            else:
                self._estop_endpoint.settle_then_cut()

            return True, "Success"
        except:
            return False, "Error"

    def releaseEStop(self):
        """Stop eStop keepalive"""
        if self._estop_keepalive:
            self._estop_keepalive.stop()
            self._estop_keepalive = None
            self._estop_endpoint = None

    def getLease(self):
        """Get a lease for the robot and keep the lease alive automatically."""
        self._lease = self._lease_client.acquire()
        self._lease_keepalive = LeaseKeepAlive(self._lease_client)

    def releaseLease(self):
        """Return the lease on the body."""
        if self._lease:
            self._lease_client.return_lease(self._lease)
            self._lease = None

    def release(self):
        """Return the lease on the body and the eStop handle."""
        try:
            self.releaseLease()
            self.releaseEStop()
            return True, "Success"
        except Exception as e:
            return False, str(e)

    def disconnect(self):
        """Release control of robot as gracefully as posssible."""
        if self._robot.time_sync:
            self._robot.time_sync.stop()
        self.releaseLease()
        self.releaseEStop()

    def _robot_command(self, command_proto, end_time_secs=None):
        """Generic blocking function for sending commands to robots.

        Args:
            command_proto: robot_command_pb2 object to send to the robot.  Usually made with RobotCommandBuilder
            end_time_secs: (optional) Time-to-live for the command in seconds
        """
        try:
            id = self._robot_command_client.robot_command(
                lease=None, command=command_proto, end_time_secs=end_time_secs)
            return True, "Success", id
        except Exception as e:
            return False, str(e), None

    def stop(self):
        """Stop the robot's motion."""
        response = self._robot_command(RobotCommandBuilder.stop_command())
        return response[0], response[1]

    def self_right(self):
        """Have the robot self-right itself."""
        response = self._robot_command(RobotCommandBuilder.selfright_command())
        return response[0], response[1]

    def sit(self):
        """Stop the robot's motion and sit down if able."""
        response = self._robot_command(RobotCommandBuilder.sit_command())
        self._last_sit_command = response[2]
        return response[0], response[1]

    def stand(self, monitor_command=True):
        """If the e-stop is enabled, and the motor power is enabled, stand the robot up."""
        response = self._robot_command(
            RobotCommandBuilder.stand_command(params=self._mobility_params))
        if monitor_command:
            self._last_stand_command = response[2]
        return response[0], response[1]

    def safe_power_off(self):
        """Stop the robot's motion and sit if possible.  Once sitting, disable motor power."""
        response = self._robot_command(
            RobotCommandBuilder.safe_power_off_command())
        return response[0], response[1]

    def power_on(self):
        """Enble the motor power if e-stop is enabled."""
        try:
            power.power_on(self._power_client)
            return True, "Success"
        except:
            return False, "Error"

    def set_mobility_params(self,
                            body_height=0,
                            footprint_R_body=EulerZXY(),
                            locomotion_hint=1,
                            stair_hint=False,
                            external_force_params=None):
        """Define body, locomotion, and stair parameters.

        Args:
            body_height: Body height in meters
            footprint_R_body: (EulerZXY) – The orientation of the body frame with respect to the footprint frame (gravity aligned framed with yaw computed from the stance feet)
            locomotion_hint: Locomotion hint
            stair_hint: Boolean to define stair motion
        """
        self._mobility_params = RobotCommandBuilder.mobility_params(
            body_height, footprint_R_body, locomotion_hint, stair_hint,
            external_force_params)

    def velocity_cmd(self, v_x, v_y, v_rot, cmd_duration=0.1):
        """Send a velocity motion command to the robot.

        Args:
            v_x: Velocity in the X direction in meters
            v_y: Velocity in the Y direction in meters
            v_rot: Angular velocity around the Z axis in radians
            cmd_duration: (optional) Time-to-live for the command in seconds.  Default is 125ms (assuming 10Hz command rate).
        """
        end_time = time.time() + cmd_duration
        self._robot_command(RobotCommandBuilder.velocity_command(
            v_x=v_x, v_y=v_y, v_rot=v_rot, params=self._mobility_params),
                            end_time_secs=end_time)
        self._last_motion_command_time = end_time
示例#11
0
def main(argv):
    parser = argparse.ArgumentParser()
    bosdyn.client.util.add_common_arguments(parser)
    parser.add_argument('--protocol',
                        default='tcp',
                        type=str,
                        choices=['tcp', 'udp'],
                        help='IP Protocel to use, either tcp or udp.')
    parser.add_argument('--server-port',
                        default=5201,
                        type=int,
                        help='Port number of iperf3 server')
    parser.add_argument('--server-hostname',
                        default='127.0.0.1',
                        type=str,
                        help='IP address of ieprf3 server')
    options = parser.parse_args(argv)

    sdk = bosdyn.client.create_standard_sdk('CommsTestingClient',
                                            [MissionClient])
    robot = sdk.create_robot(options.hostname)  #ROBOT_IP
    robot.authenticate(options.username, options.password)
    robot.sync_with_directory()

    _robot_state_client = robot.ensure_client(
        RobotStateClient.default_service_name)
    _mission_client = robot.ensure_client(MissionClient.default_service_name)
    _robot_state_task = AsyncRobotState(_robot_state_client)
    _mission_state_task = AsyncMissionState(_mission_client)
    _task_list = [_robot_state_task, _mission_state_task]
    _async_tasks = AsyncTasks(_task_list)
    print('Connected.')

    update_thread = threading.Thread(target=_update_thread,
                                     args=[_async_tasks])
    update_thread.daemon = True
    update_thread.start()

    # Wait for the first responses.
    while any(task.proto is None for task in _task_list):
        time.sleep(0.1)

    # list to hold all the data
    data_list = []
    curr_fname = ''
    try:
        while True:
            if _mission_state_task.proto.status != mission_proto.State.STATUS_RUNNING:
                # Write out data if it exists
                if len(data_list) > 0:
                    print(
                        f'Finished a mission, data can be found in {curr_fname}'
                    )
                    data_list.clear()
                print(_mission_state_task.proto)
                time.sleep(1)

            else:
                if _robot_state_task.proto:

                    # This script currently uses odometry positioning, which is based on the robot's
                    # position at boot time. Runs across boots will not be easily comparable
                    odom_tform_body = get_odom_tform_body(
                        _robot_state_task.proto.kinematic_state.
                        transforms_snapshot).to_proto()
                    helper_se3 = SE3Pose.from_obj(odom_tform_body)

                    #check latency
                    ping_ret = check_ping(options.server_hostname)
                    ping = -1 if ping_ret is None else ping_ret['avg']

                    # Run iperf3 client
                    client = iperf3.Client()
                    client.duration = 1
                    client.server_hostname = options.server_hostname
                    client.protocol = options.protocol
                    client.port = options.server_port
                    result = client.run()

                    # update list of data points
                    if result.error:
                        print(result.error)
                    else:
                        data_entry = {
                            'loc_x': helper_se3.x,
                            'loc_y': helper_se3.y,
                            'loc_z': helper_se3.z,
                            'time': datetime.datetime.now(),
                            'latency(ms)': ping
                        }
                        if options.protocol == 'udp':
                            data_entry.update({
                                'send throughput(Mbps)': result.Mbps,
                                'recv throughput(Mbps)': -1,
                                'jitter(ms)': result.jitter_ms,
                                'lost(%)': result.lost_percent,
                                'retransmits': -1
                            })
                        elif options.protocol == 'tcp':
                            data_entry.update({
                                'send throughput(Mbps)': result.sent_Mbps,
                                'recv throughput(Mbps)': result.received_Mbps,
                                'jitter(ms)': -1,
                                'lost(%)': -1,
                                'retransmits': result.retransmits
                            })
                        data_list.append(data_entry)
                        print(data_list[-1])

                        # Create csv with header if it doesn't exist, then write latest row
                        keys = data_list[0].keys()
                        if curr_fname == '':
                            curr_fname = create_csv_filename(options.protocol)
                            with open(curr_fname, 'w') as output_file:
                                header_writer = csv.DictWriter(
                                    output_file, keys)
                                header_writer.writeheader()
                        with open(curr_fname, 'a') as output_file:
                            dict_writer = csv.DictWriter(output_file, keys)
                            dict_writer.writerow(data_list[-1])

                    del client

    except KeyboardInterrupt:
        print("Caught KeyboardInterrupt, exiting")
        return True
示例#12
0
    def __init__(self,
                 username,
                 password,
                 token,
                 hostname,
                 logger,
                 rates={},
                 callbacks={}):
        self._username = username
        self._password = password
        self._token = token
        self._hostname = hostname
        self._logger = logger
        self._rates = rates
        self._callbacks = callbacks
        self._keep_alive = True
        self._valid = True

        self._mobility_params = RobotCommandBuilder.mobility_params()
        self._is_standing = False
        self._is_sitting = True
        self._is_moving = False
        self._last_stand_command = None
        self._last_sit_command = None
        self._last_motion_command = None
        self._last_motion_command_time = None

        self._front_image_requests = []
        for source in front_image_sources:
            self._front_image_requests.append(
                build_image_request(
                    source, image_format=image_pb2.Image.Format.FORMAT_RAW))

        self._side_image_requests = []
        for source in side_image_sources:
            self._side_image_requests.append(
                build_image_request(
                    source, image_format=image_pb2.Image.Format.FORMAT_RAW))

        self._rear_image_requests = []
        for source in rear_image_sources:
            self._rear_image_requests.append(
                build_image_request(
                    source, image_format=image_pb2.Image.Format.FORMAT_RAW))

        try:
            self._sdk = create_standard_sdk('ros_spot')
        except Exception as e:
            self._logger.error("Error creating SDK object: %s", e)
            self._valid = False
            return
        try:
            self._sdk.load_app_token(self._token)
        except Exception as e:
            self._logger.error("Error loading developer token: %s", e)
            self._valid = False
            return

        self._robot = self._sdk.create_robot(self._hostname)

        try:
            self._robot.authenticate(self._username, self._password)
            self._robot.start_time_sync()
        except RpcError as err:
            self._logger.error("Failed to communicate with robot: %s", err)
            self._valid = False
            return

        if self._robot:
            # Clients
            try:
                self._robot_state_client = self._robot.ensure_client(
                    RobotStateClient.default_service_name)
                self._robot_command_client = self._robot.ensure_client(
                    RobotCommandClient.default_service_name)
                self._power_client = self._robot.ensure_client(
                    PowerClient.default_service_name)
                self._lease_client = self._robot.ensure_client(
                    LeaseClient.default_service_name)
                self._image_client = self._robot.ensure_client(
                    ImageClient.default_service_name)
                self._estop_client = self._robot.ensure_client(
                    EstopClient.default_service_name)
            except Exception as e:
                self._logger.error("Unable to create client service: %s", e)
                self._valid = False
                return

            # Async Tasks
            self._async_task_list = []
            self._robot_state_task = AsyncRobotState(
                self._robot_state_client, self._logger,
                max(0.0, self._rates.get("robot_state", 0.0)),
                self._callbacks.get("robot_state", lambda: None))
            self._robot_metrics_task = AsyncMetrics(
                self._robot_state_client, self._logger,
                max(0.0, self._rates.get("metrics", 0.0)),
                self._callbacks.get("metrics", lambda: None))
            self._lease_task = AsyncLease(
                self._lease_client, self._logger,
                max(0.0, self._rates.get("lease", 0.0)),
                self._callbacks.get("lease", lambda: None))
            self._front_image_task = AsyncImageService(
                self._image_client, self._logger,
                max(0.0, self._rates.get("front_image", 0.0)),
                self._callbacks.get("front_image", lambda: None),
                self._front_image_requests)
            self._side_image_task = AsyncImageService(
                self._image_client, self._logger,
                max(0.0, self._rates.get("side_image", 0.0)),
                self._callbacks.get("side_image", lambda: None),
                self._side_image_requests)
            self._rear_image_task = AsyncImageService(
                self._image_client, self._logger,
                max(0.0, self._rates.get("rear_image", 0.0)),
                self._callbacks.get("rear_image", lambda: None),
                self._rear_image_requests)
            self._idle_task = AsyncIdle(self._robot_command_client,
                                        self._logger, 10.0, self)

            self._estop_endpoint = EstopEndpoint(self._estop_client, 'ros',
                                                 9.0)

            self._async_tasks = AsyncTasks([
                self._robot_state_task, self._robot_metrics_task,
                self._lease_task, self._front_image_task,
                self._side_image_task, self._rear_image_task, self._idle_task
            ])

            self._robot_id = None
            self._lease = None
示例#13
0
class RecorderInterface(object):
    """A curses interface for recording robot missions."""
    def __init__(self, robot, download_filepath):
        self._robot = robot

        # Flag indicating whether mission is currently being recorded
        self._recording = False

        # Flag indicating whether robot is in feature desert mode
        self._desert_mode = False

        # Filepath for the location to put the downloaded graph and snapshots.
        self._download_filepath = download_filepath

        # List of waypoint commands
        self._waypoint_commands = []

        # Dictionary indicating which waypoints are deserts
        self._desert_flag = {}

        # Current waypoint id
        self._waypoint_id = 'NONE'

        # Create clients -- do not use the for communication yet.
        self._lease_client = robot.ensure_client(
            LeaseClient.default_service_name)
        try:
            self._estop_client = self._robot.ensure_client(
                EstopClient.default_service_name)
            self._estop_endpoint = EstopEndpoint(self._estop_client,
                                                 'GNClient', 9.0)
        except:
            # Not the estop.
            self._estop_client = None
            self._estop_endpoint = None

        self._map_processing_client = robot.ensure_client(
            MapProcessingServiceClient.default_service_name)
        self._power_client = robot.ensure_client(
            PowerClient.default_service_name)
        self._robot_state_client = robot.ensure_client(
            RobotStateClient.default_service_name)
        self._robot_command_client = robot.ensure_client(
            RobotCommandClient.default_service_name)
        self._world_object_client = robot.ensure_client(
            WorldObjectClient.default_service_name)

        # Setup the recording service client.
        self._recording_client = self._robot.ensure_client(
            GraphNavRecordingServiceClient.default_service_name)

        # Setup the graph nav service client.
        self._graph_nav_client = robot.ensure_client(
            GraphNavClient.default_service_name)

        # Local copy of the graph.
        self._graph = None
        self._all_graph_wps_in_order = []

        self._robot_state_task = AsyncRobotState(self._robot_state_client)
        self._async_tasks = AsyncTasks([self._robot_state_task])
        self._lock = threading.Lock()
        self._command_dictionary = {
            27: self._stop,  # ESC key
            ord('\t'): self._quit_program,
            ord('T'): self._toggle_time_sync,
            ord(' '): self._toggle_estop,
            ord('r'): self._self_right,
            ord('P'): self._toggle_power,
            ord('v'): self._sit,
            ord('f'): self._stand,
            ord('w'): self._move_forward,
            ord('s'): self._move_backward,
            ord('a'): self._strafe_left,
            ord('c'): self._auto_close_loops,
            ord('d'): self._strafe_right,
            ord('q'): self._turn_left,
            ord('e'): self._turn_right,
            ord('m'): self._start_recording,
            ord('l'): self._relocalize,
            ord('z'): self._enter_desert,
            ord('x'): self._exit_desert,
            ord('g'): self._generate_mission
        }
        self._locked_messages = ['', '',
                                 '']  # string: displayed message for user
        self._estop_keepalive = None
        self._exit_check = None

        # Stuff that is set in start()
        self._robot_id = None
        self._lease = None

    def start(self):
        """Begin communication with the robot."""
        self._lease = self._lease_client.acquire()

        self._robot_id = self._robot.get_id()
        if self._estop_endpoint is not None:
            self._estop_endpoint.force_simple_setup(
            )  # Set this endpoint as the robot's sole estop.

        # Clear existing graph nav map
        self._graph_nav_client.clear_graph()

    def shutdown(self):
        """Release control of robot as gracefully as possible."""
        LOGGER.info("Shutting down WasdInterface.")
        if self._estop_keepalive:
            # This stops the check-in thread but does not stop the robot.
            self._estop_keepalive.shutdown()
        if self._lease:
            _grpc_or_log("returning lease",
                         lambda: self._lease_client.return_lease(self._lease))
            self._lease = None

    def __del__(self):
        self.shutdown()

    def flush_and_estop_buffer(self, stdscr):
        """Manually flush the curses input buffer but trigger any estop requests (space)"""
        key = ''
        while key != -1:
            key = stdscr.getch()
            if key == ord(' '):
                self._toggle_estop()

    def add_message(self, msg_text):
        """Display the given message string to the user in the curses interface."""
        with self._lock:
            self._locked_messages = [msg_text] + self._locked_messages[:-1]

    def message(self, idx):
        """Grab one of the 3 last messages added."""
        with self._lock:
            return self._locked_messages[idx]

    @property
    def robot_state(self):
        """Get latest robot state proto."""
        return self._robot_state_task.proto

    def drive(self, stdscr):
        """User interface to control the robot via the passed-in curses screen interface object."""
        with LeaseKeepAlive(self._lease_client) as lease_keep_alive, \
             ExitCheck() as self._exit_check:
            curses_handler = CursesHandler(self)
            curses_handler.setLevel(logging.INFO)
            LOGGER.addHandler(curses_handler)

            stdscr.nodelay(True)  # Don't block for user input.
            stdscr.resize(26, 96)
            stdscr.refresh()

            # for debug
            curses.echo()

            try:
                while not self._exit_check.kill_now:
                    self._async_tasks.update()
                    self._drive_draw(stdscr, lease_keep_alive)

                    try:
                        cmd = stdscr.getch()
                        # Do not queue up commands on client
                        self.flush_and_estop_buffer(stdscr)
                        self._drive_cmd(cmd)
                        time.sleep(COMMAND_INPUT_RATE)
                    except Exception:
                        # On robot command fault, sit down safely before killing the program.
                        self._safe_power_off()
                        time.sleep(2.0)
                        self.shutdown()
                        raise

            finally:
                LOGGER.removeHandler(curses_handler)

    def _count_visible_fiducials(self):
        """Return number of fiducials visible to robot."""
        request_fiducials = [world_object_pb2.WORLD_OBJECT_APRILTAG]
        fiducial_objects = self._world_object_client.list_world_objects(
            object_type=request_fiducials).world_objects
        return len(fiducial_objects)

    def _drive_draw(self, stdscr, lease_keep_alive):
        """Draw the interface screen at each update."""
        stdscr.clear()  # clear screen
        stdscr.resize(26, 96)
        stdscr.addstr(
            0, 0, '{:20s} {}'.format(self._robot_id.nickname,
                                     self._robot_id.serial_number))
        stdscr.addstr(1, 0, self._lease_str(lease_keep_alive))
        stdscr.addstr(2, 0, self._battery_str())
        stdscr.addstr(3, 0, self._estop_str())
        stdscr.addstr(4, 0, self._power_state_str())
        stdscr.addstr(5, 0, self._time_sync_str())
        stdscr.addstr(6, 0, self._waypoint_str())
        stdscr.addstr(7, 0, self._fiducial_str())
        stdscr.addstr(8, 0, self._desert_str())
        for i in range(3):
            stdscr.addstr(10 + i, 2, self.message(i))
        stdscr.addstr(14, 0,
                      "Commands: [TAB]: quit                               ")
        stdscr.addstr(15, 0,
                      "          [T]: Time-sync, [SPACE]: Estop, [P]: Power")
        stdscr.addstr(16, 0,
                      "          [v]: Sit, [f]: Stand, [r]: Self-right     ")
        stdscr.addstr(17, 0,
                      "          [wasd]: Directional strafing              ")
        stdscr.addstr(18, 0,
                      "          [qe]: Turning, [ESC]: Stop                ")
        stdscr.addstr(19, 0,
                      "          [m]: Start recording mission              ")
        stdscr.addstr(20, 0,
                      "          [l]: Add fiducial localization to mission ")
        stdscr.addstr(21, 0,
                      "          [z]: Enter desert mode                    ")
        stdscr.addstr(22, 0,
                      "          [x]: Exit desert mode                     ")
        stdscr.addstr(23, 0,
                      "          [g]: Stop recording and generate mission  ")

        stdscr.refresh()

    def _drive_cmd(self, key):
        """Run user commands at each update."""
        try:
            cmd_function = self._command_dictionary[key]
            cmd_function()

        except KeyError:
            if key and key != -1 and key < 256:
                self.add_message("Unrecognized keyboard command: '{}'".format(
                    chr(key)))

    def _try_grpc(self, desc, thunk):
        try:
            return thunk()
        except (ResponseError, RpcError) as err:
            self.add_message("Failed {}: {}".format(desc, err))
            return None

    def _quit_program(self):
        if self._recording:
            self._stop_recording()
        self._robot.power_off()
        self.shutdown()
        if self._exit_check is not None:
            self._exit_check.request_exit()

    def _toggle_time_sync(self):
        if self._robot.time_sync.stopped:
            self._robot.time_sync.start()
        else:
            self._robot.time_sync.stop()

    def _toggle_estop(self):
        """toggle estop on/off. Initial state is ON"""
        if self._estop_client is not None and self._estop_endpoint is not None:
            if not self._estop_keepalive:
                self._estop_keepalive = EstopKeepAlive(self._estop_endpoint)
            else:
                self._try_grpc("stopping estop", self._estop_keepalive.stop)
                self._estop_keepalive.shutdown()
                self._estop_keepalive = None

    def _start_robot_command(self, desc, command_proto, end_time_secs=None):
        def _start_command():
            self._robot_command_client.robot_command(
                lease=None, command=command_proto, end_time_secs=end_time_secs)

        self._try_grpc(desc, _start_command)

    def _self_right(self):
        self._start_robot_command('self_right',
                                  RobotCommandBuilder.selfright_command())

    def _sit(self):
        self._start_robot_command('sit',
                                  RobotCommandBuilder.synchro_sit_command())

    def _stand(self):
        self._start_robot_command('stand',
                                  RobotCommandBuilder.synchro_stand_command())

    def _move_forward(self):
        self._velocity_cmd_helper('move_forward', v_x=VELOCITY_BASE_SPEED)

    def _move_backward(self):
        self._velocity_cmd_helper('move_backward', v_x=-VELOCITY_BASE_SPEED)

    def _strafe_left(self):
        self._velocity_cmd_helper('strafe_left', v_y=VELOCITY_BASE_SPEED)

    def _strafe_right(self):
        self._velocity_cmd_helper('strafe_right', v_y=-VELOCITY_BASE_SPEED)

    def _turn_left(self):
        self._velocity_cmd_helper('turn_left', v_rot=VELOCITY_BASE_ANGULAR)

    def _turn_right(self):
        self._velocity_cmd_helper('turn_right', v_rot=-VELOCITY_BASE_ANGULAR)

    def _stop(self):
        self._start_robot_command('stop', RobotCommandBuilder.stop_command())

    def _velocity_cmd_helper(self, desc='', v_x=0.0, v_y=0.0, v_rot=0.0):
        self._start_robot_command(
            desc,
            RobotCommandBuilder.synchro_velocity_command(v_x=v_x,
                                                         v_y=v_y,
                                                         v_rot=v_rot),
            end_time_secs=time.time() + VELOCITY_CMD_DURATION)

    def _return_to_origin(self):
        self._start_robot_command(
            'fwd_and_rotate',
            RobotCommandBuilder.synchro_se2_trajectory_point_command(
                goal_x=0.0,
                goal_y=0.0,
                goal_heading=0.0,
                frame_name=ODOM_FRAME_NAME,
                params=None,
                body_height=0.0,
                locomotion_hint=spot_command_pb2.HINT_SPEED_SELECT_TROT),
            end_time_secs=time.time() + 20)

    def _toggle_power(self):
        power_state = self._power_state()
        if power_state is None:
            self.add_message(
                'Could not toggle power because power state is unknown')
            return

        if power_state == robot_state_proto.PowerState.STATE_OFF:
            self._try_grpc("powering-on", self._request_power_on)
        else:
            self._try_grpc("powering-off", self._safe_power_off)

    def _request_power_on(self):
        bosdyn.client.power.power_on(self._power_client)

    def _safe_power_off(self):
        self._start_robot_command('safe_power_off',
                                  RobotCommandBuilder.safe_power_off_command())

    def _power_state(self):
        state = self.robot_state
        if not state:
            return None
        return state.power_state.motor_power_state

    def _lease_str(self, lease_keep_alive):
        alive = 'RUNNING' if lease_keep_alive.is_alive() else 'STOPPED'
        lease = '{}:{}'.format(self._lease.lease_proto.resource,
                               self._lease.lease_proto.sequence)
        return 'Lease {} THREAD:{}'.format(lease, alive)

    def _power_state_str(self):
        power_state = self._power_state()
        if power_state is None:
            return ''
        state_str = robot_state_proto.PowerState.MotorPowerState.Name(
            power_state)
        return 'Power: {}'.format(state_str[6:])  # get rid of STATE_ prefix

    def _estop_str(self):
        if not self._estop_client:
            thread_status = 'NOT ESTOP'
        else:
            thread_status = 'RUNNING' if self._estop_keepalive else 'STOPPED'
        estop_status = '??'
        state = self.robot_state
        if state:
            for estop_state in state.estop_states:
                if estop_state.type == estop_state.TYPE_SOFTWARE:
                    estop_status = estop_state.State.Name(
                        estop_state.state)[6:]  # s/STATE_//
                    break
        return 'Estop {} (thread: {})'.format(estop_status, thread_status)

    def _time_sync_str(self):
        if not self._robot.time_sync:
            return 'Time sync: (none)'
        if self._robot.time_sync.stopped:
            status = 'STOPPED'
            exception = self._robot.time_sync.thread_exception
            if exception:
                status = '{} Exception: {}'.format(status, exception)
        else:
            status = 'RUNNING'
        try:
            skew = self._robot.time_sync.get_robot_clock_skew()
            if skew:
                skew_str = 'offset={}'.format(duration_str(skew))
            else:
                skew_str = "(Skew undetermined)"
        except (TimeSyncError, RpcError) as err:
            skew_str = '({})'.format(err)
        return 'Time sync: {} {}'.format(status, skew_str)

    def _waypoint_str(self):
        state = self._graph_nav_client.get_localization_state()
        try:
            self._waypoint_id = state.localization.waypoint_id
            if self._waypoint_id == '':
                self._waypoint_id = 'NONE'

        except:
            self._waypoint_id = 'ERROR'

        if self._recording and self._waypoint_id != 'NONE' and self._waypoint_id != 'ERROR':
            if self._waypoint_id not in self._desert_flag:
                self._desert_flag[self._waypoint_id] = self._desert_mode
            return 'Current waypoint: {} [ RECORDING ]'.format(
                self._waypoint_id)

        return 'Current waypoint: {}'.format(self._waypoint_id)

    def _fiducial_str(self):
        return 'Visible fiducials: {}'.format(
            str(self._count_visible_fiducials()))

    def _desert_str(self):
        if self._desert_mode:
            return '[ FEATURE DESERT MODE ]'
        else:
            return ''

    def _battery_str(self):
        if not self.robot_state:
            return ''
        battery_state = self.robot_state.battery_states[0]
        status = battery_state.Status.Name(battery_state.status)
        status = status[7:]  # get rid of STATUS_ prefix
        if battery_state.charge_percentage.value:
            bar_len = int(battery_state.charge_percentage.value) // 10
            bat_bar = '|{}{}|'.format('=' * bar_len, ' ' * (10 - bar_len))
        else:
            bat_bar = ''
        time_left = ''
        if battery_state.estimated_runtime:
            time_left = ' ({})'.format(
                secs_to_hms(battery_state.estimated_runtime.seconds))
        return 'Battery: {}{}{}'.format(status, bat_bar, time_left)

    def _fiducial_visible(self):
        """Return True if robot can see fiducial."""
        request_fiducials = [world_object_pb2.WORLD_OBJECT_APRILTAG]
        fiducial_objects = self._world_object_client.list_world_objects(
            object_type=request_fiducials).world_objects
        self.add_message("Fiducial objects: " + str(fiducial_objects))

        if len(fiducial_objects) > 0:
            return True

        return False

    def _start_recording(self):
        """Start recording a map."""
        if self._count_visible_fiducials() == 0:
            self.add_message(
                "ERROR: Can't start recording -- No fiducials in view.")
            return

        if self._waypoint_id is None:
            self.add_message("ERROR: Not localized to waypoint.")
            return

        # Tell graph nav to start recording map
        status = self._recording_client.start_recording()
        if status != recording_pb2.StartRecordingResponse.STATUS_OK:
            self.add_message("Start recording failed.")
            return

        self.add_message("Started recording map.")
        self._graph = None
        self._all_graph_wps_in_order = []
        state = self._graph_nav_client.get_localization_state()
        if '' == state.localization.waypoint_id:
            self.add_message("No localization after start recording.")
            self._recording_client.stop_recording()
            return
        self._waypoint_commands = []
        # Treat this sort of like RPN / postfix, where operators follow their operands.
        # waypoint_id LOCALIZE means "localize to waypoint_id".
        # INITIALIZE takes no argument.
        # Implicitly, we make routes between pairs of waypoint ids as they occur.
        # `w0 w1 LOCALIZE w2` will give a route w0->w1, whereupon we localize,
        # and then a route w1->w2.
        self._waypoint_commands.append('INITIALIZE')
        # Start with the first waypoint.
        self._waypoint_commands.append(state.localization.waypoint_id)
        self._recording = True

    def _stop_recording(self):
        """Stop or pause recording a map."""
        if not self._recording:
            return True
        self._recording = False

        if self._waypoint_id != self._waypoint_commands[-1]:
            self._waypoint_commands += [self._waypoint_id]

        try:
            status = self._recording_client.stop_recording()
            if status != recording_pb2.StopRecordingResponse.STATUS_OK:
                self.add_message("Stop recording failed.")
                return False

            self.add_message("Successfully stopped recording a map.")
            return True

        except NotLocalizedToEndError:
            self.add_message(
                "ERROR: Move to final waypoint before stopping recording.")
            return False

    def _relocalize(self):
        """Insert localization node into mission."""
        if not self._recording:
            print('Not recording mission.')
            return False

        self.add_message("Adding fiducial localization to mission.")
        self._waypoint_commands += [self._waypoint_id]
        self._waypoint_commands += ['LOCALIZE']

        # Return to waypoint (in case localization shifted to another waypoint)
        self._waypoint_commands += [self._waypoint_id]
        return True

    def _enter_desert(self):
        self._desert_mode = True
        if self._recording:
            if self._waypoint_commands == [] or self._waypoint_commands[
                    -1] != self._waypoint_id:
                self._waypoint_commands += [self._waypoint_id]

    def _exit_desert(self):
        self._desert_mode = False
        if self._recording:
            if self._waypoint_commands == [] or self._waypoint_commands[
                    -1] != self._waypoint_id:
                self._waypoint_commands += [self._waypoint_id]

    def _generate_mission(self):
        """Save graph map and mission file."""

        # Check whether mission has been recorded
        if not self._recording:
            self.add_message("ERROR: No mission recorded.")
            return

        # Check for empty mission
        if len(self._waypoint_commands) == 0:
            self.add_message("ERROR: No waypoints in mission.")
            return

        # Stop recording mission
        if not self._stop_recording():
            self.add_message("ERROR: Error while stopping recording.")
            return

        # Save graph map
        os.mkdir(self._download_filepath)
        if not self._download_full_graph():
            self.add_message("ERROR: Error downloading graph.")
            return

        # Generate mission
        mission = self._make_mission()

        # Save mission file
        os.mkdir(os.path.join(self._download_filepath, "missions"))
        mission_filepath = os.path.join(self._download_filepath, "missions",
                                        "autogenerated")
        write_mission(mission, mission_filepath)

        # Quit program
        self._quit_program()

    def _make_desert(self, waypoint):
        waypoint.annotations.scan_match_region.empty.CopyFrom(
            map_pb2.Waypoint.Annotations.LocalizeRegion.Empty())

    def _download_full_graph(self, overwrite_desert_flag=None):
        """Download the graph and snapshots from the robot."""
        graph = self._graph_nav_client.download_graph()
        if graph is None:
            self.add_message("Failed to download the graph.")
            return False

        if overwrite_desert_flag is not None:
            for wp in graph.waypoints:
                if wp.id in overwrite_desert_flag:
                    # Overwrite anything that we passed in.
                    self._desert_flag[wp.id] = overwrite_desert_flag[wp.id]
                elif wp.id not in self._desert_flag:
                    self._desert_flag[wp.id] = False

        # Mark desert waypoints
        for waypoint in graph.waypoints:
            if self._desert_flag[waypoint.id]:
                self._make_desert(waypoint)

        # Write graph map
        self._write_full_graph(graph)
        self.add_message(
            "Graph downloaded with {} waypoints and {} edges".format(
                len(graph.waypoints), len(graph.edges)))

        # Download the waypoint and edge snapshots.
        self._download_and_write_waypoint_snapshots(graph.waypoints)
        self._download_and_write_edge_snapshots(graph.edges)

        # Cache a list of all graph waypoints, ordered by creation time.
        # Because we only record one (non-branching) chain, all possible routes are contained
        # in ranges in this list.
        self._graph = graph
        wp_to_time = []
        for wp in graph.waypoints:
            time = wp.annotations.creation_time.seconds + wp.annotations.creation_time.nanos / 1e9
            wp_to_time.append((wp.id, time))
        # Switch inner and outer grouping and grab only the waypoint names after sorting by time.
        self._all_graph_wps_in_order = list(
            zip(*sorted(wp_to_time, key=lambda x: x[1])))[0]

        return True

    def _write_full_graph(self, graph):
        """Download the graph from robot to the specified, local filepath location."""
        graph_bytes = graph.SerializeToString()
        write_bytes(self._download_filepath, 'graph', graph_bytes)

    def _download_and_write_waypoint_snapshots(self, waypoints):
        """Download the waypoint snapshots from robot to the specified, local filepath location."""
        num_waypoint_snapshots_downloaded = 0
        for waypoint in waypoints:
            try:
                waypoint_snapshot = self._graph_nav_client.download_waypoint_snapshot(
                    waypoint.snapshot_id)
            except Exception:
                # Failure in downloading waypoint snapshot. Continue to next snapshot.
                self.add_message("Failed to download waypoint snapshot: " +
                                 waypoint.snapshot_id)
                continue
            write_bytes(
                os.path.join(self._download_filepath, 'waypoint_snapshots'),
                waypoint.snapshot_id, waypoint_snapshot.SerializeToString())
            num_waypoint_snapshots_downloaded += 1
            self.add_message(
                "Downloaded {} of the total {} waypoint snapshots.".format(
                    num_waypoint_snapshots_downloaded, len(waypoints)))

    def _download_and_write_edge_snapshots(self, edges):
        """Download the edge snapshots from robot to the specified, local filepath location."""
        num_edge_snapshots_downloaded = 0
        num_to_download = 0
        for edge in edges:
            if len(edge.snapshot_id) == 0:
                continue
            num_to_download += 1
            try:
                edge_snapshot = self._graph_nav_client.download_edge_snapshot(
                    edge.snapshot_id)
            except Exception:
                # Failure in downloading edge snapshot. Continue to next snapshot.
                self.add_message("Failed to download edge snapshot: " +
                                 edge.snapshot_id)
                continue
            write_bytes(
                os.path.join(self._download_filepath, 'edge_snapshots'),
                edge.snapshot_id, edge_snapshot.SerializeToString())
            num_edge_snapshots_downloaded += 1
            self.add_message(
                "Downloaded {} of the total {} edge snapshots.".format(
                    num_edge_snapshots_downloaded, num_to_download))

    def _auto_close_loops(self):
        """Automatically find and close all loops in the graph."""
        close_fiducial_loops = True
        close_odometry_loops = True
        response = self._map_processing_client.process_topology(
            params=map_processing_pb2.ProcessTopologyRequest.Params(
                do_fiducial_loop_closure=wrappers.BoolValue(
                    value=close_fiducial_loops),
                do_odometry_loop_closure=wrappers.BoolValue(
                    value=close_odometry_loops)),
            modify_map_on_server=True)
        self.add_message("Created {} new edge(s).".format(
            len(response.new_subgraph.edges)))

    def _make_mission(self):
        """ Create a mission that visits each waypoint on stored path."""

        self.add_message("Mission: " + str(self._waypoint_commands))

        # Create a Sequence that visits all the waypoints.
        sequence = nodes_pb2.Sequence()

        prev_waypoint_command = None
        first_waypoint = True
        for waypoint_command in self._waypoint_commands:
            if waypoint_command == 'LOCALIZE':
                if prev_waypoint_command is None:
                    raise RuntimeError(f'No prev waypoint; LOCALIZE')
                sequence.children.add().CopyFrom(
                    self._make_localize_node(prev_waypoint_command))
            elif waypoint_command == 'INITIALIZE':
                # Initialize to any fiducial.
                sequence.children.add().CopyFrom(self._make_initialize_node())
            else:
                if first_waypoint:
                    # Go to the beginning of the mission using any path. May be a no-op.
                    sequence.children.add().CopyFrom(
                        self._make_goto_node(waypoint_command))
                else:
                    if prev_waypoint_command is None:
                        raise RuntimeError(
                            f'No prev waypoint; route to {waypoint_command}')
                    sequence.children.add().CopyFrom(
                        self._make_go_route_node(prev_waypoint_command,
                                                 waypoint_command))
                prev_waypoint_command = waypoint_command
                first_waypoint = False

        # Return a Node with the Sequence.
        ret = nodes_pb2.Node()
        ret.name = "Visit %d goals" % len(self._waypoint_commands)
        ret.impl.Pack(sequence)
        return ret

    def _make_goto_node(self, waypoint_id):
        """ Create a leaf node that will go to the waypoint. """
        ret = nodes_pb2.Node()
        ret.name = "goto %s" % waypoint_id
        vel_limit = NAV_VELOCITY_LIMITS

        # We don't actually know which path we will plan. It could have many feature deserts.
        # So, let's just allow feature deserts.
        tolerance = graph_nav_pb2.TravelParams.TOLERANCE_IGNORE_POOR_FEATURE_QUALITY
        travel_params = graph_nav_pb2.TravelParams(
            velocity_limit=vel_limit, feature_quality_tolerance=tolerance)

        impl = nodes_pb2.BosdynNavigateTo(travel_params=travel_params)
        impl.destination_waypoint_id = waypoint_id
        ret.impl.Pack(impl)
        return ret

    def _make_go_route_node(self, prev_waypoint_id, waypoint_id):
        """ Create a leaf node that will go to the waypoint along a route. """
        ret = nodes_pb2.Node()
        ret.name = "go route %s -> %s" % (prev_waypoint_id, waypoint_id)
        vel_limit = NAV_VELOCITY_LIMITS

        if prev_waypoint_id not in self._all_graph_wps_in_order:
            raise RuntimeError(
                f'{prev_waypoint_id} (FROM) not in {self._all_graph_wps_in_order}'
            )
        if waypoint_id not in self._all_graph_wps_in_order:
            raise RuntimeError(
                f'{waypoint_id} (TO) not in {self._all_graph_wps_in_order}')
        prev_wp_idx = self._all_graph_wps_in_order.index(prev_waypoint_id)
        wp_idx = self._all_graph_wps_in_order.index(waypoint_id)

        impl = nodes_pb2.BosdynNavigateRoute()
        backwards = False
        if wp_idx >= prev_wp_idx:
            impl.route.waypoint_id[:] = self._all_graph_wps_in_order[
                prev_wp_idx:wp_idx + 1]
        else:
            backwards = True
            # Switch the indices and reverse the output order.
            impl.route.waypoint_id[:] = reversed(
                self._all_graph_wps_in_order[wp_idx:prev_wp_idx + 1])
        edge_ids = [e.id for e in self._graph.edges]
        for i in range(len(impl.route.waypoint_id) - 1):
            eid = map_pb2.Edge.Id()
            # Because we only record one (non-branching) chain, and we only create routes from
            # older to newer waypoints, we can just follow the time-sorted list of all waypoints.
            if backwards:
                from_i = i + 1
                to_i = i
            else:
                from_i = i
                to_i = i + 1
            eid.from_waypoint = impl.route.waypoint_id[from_i]
            eid.to_waypoint = impl.route.waypoint_id[to_i]
            impl.route.edge_id.extend([eid])
            if eid not in edge_ids:
                raise RuntimeError(
                    f'Was map recorded in linear chain? {eid} not in graph')

        if any(self._desert_flag[wp_id] for wp_id in impl.route.waypoint_id):
            tolerance = graph_nav_pb2.TravelParams.TOLERANCE_IGNORE_POOR_FEATURE_QUALITY
        else:
            tolerance = graph_nav_pb2.TravelParams.TOLERANCE_DEFAULT

        travel_params = graph_nav_pb2.TravelParams(
            velocity_limit=vel_limit, feature_quality_tolerance=tolerance)
        impl.travel_params.CopyFrom(travel_params)
        ret.impl.Pack(impl)
        return ret

    def _make_localize_node(self, waypoint_id):
        """Make localization node."""
        loc = nodes_pb2.Node()
        loc.name = "localize robot"

        impl = nodes_pb2.BosdynGraphNavLocalize()
        impl.localization_request.fiducial_init = graph_nav_pb2.SetLocalizationRequest.FIDUCIAL_INIT_NEAREST_AT_TARGET
        impl.localization_request.initial_guess.waypoint_id = waypoint_id

        loc.impl.Pack(impl)
        return loc

    def _make_initialize_node(self):
        """Make initialization node."""
        loc = nodes_pb2.Node()
        loc.name = "initialize robot"

        impl = nodes_pb2.BosdynGraphNavLocalize()
        impl.localization_request.fiducial_init = graph_nav_pb2.SetLocalizationRequest.FIDUCIAL_INIT_NEAREST

        loc.impl.Pack(impl)
        return loc