コード例 #1
0
def construct_hold_pose_task():
    """ Helper function for holding the pose of the hand
    Use this if you want to hold the position of the hand,
    without leaving constrained manipulation.

    Output:
    + command: api command object
    """
    frame_name = "hand"
    force_lim = 80
    torque_lim = 10
    force_direction = geometry_pb2.Vec3(x=1.0, y=0.0, z=0.0)
    torque_direction = geometry_pb2.Vec3(x=0.0, y=0.0, z=0.0)
    init_wrench_dir = geometry_pb2.Wrench(force=force_direction,
                                          torque=torque_direction)
    task_type = basic_command_pb2.ConstrainedManipulationCommand.Request.TASK_TYPE_HOLD_POSE

    command = RobotCommandBuilder.constrained_manipulation_command(
        task_type=task_type,
        init_wrench_direction_in_frame_name=init_wrench_dir,
        force_limit=force_lim,
        torque_limit=torque_lim,
        tangential_speed=0.0,
        frame_name=frame_name)
    return command
コード例 #2
0
def construct_right_handed_ballvalve_task(velocity_normalized,
                                          force_limit=40,
                                          torque_limit=5):
    """ Helper function for manipulating right-handed ball valves
    Use this when the hand is to the right of the pivot of the ball valve
    And when hand x axis is roughly parallel to the axis of rotation of
    the ball valve

    params:
    + velocity_normalized: normalized task tangential velocity in range [-1.0, 1.0]
    + force_limit (optional): positive value denoting max force robot will exert along task dimension
    + torque_limit (optional): positive value denoting max torque robot will exert along
                            the axis of rotation of the task

    Output:
    + command: api command object

    Notes:
    If the grasp is such that the hand x axis is not parallel to the axis
    of rotation of the ball valve, then use the lever task.
    """
    velocity_normalized = max(min(velocity_normalized, 1.0), -1.0)
    velocity_limit = scale_velocity_lim_given_force_lim(force_limit)
    tangential_velocity = velocity_normalized * velocity_limit
    frame_name = "hand"
    force_lim = force_limit
    torque_lim = torque_limit
    # Force/torque signs are opposite for right-handed ball valve
    force_direction = geometry_pb2.Vec3(x=0.0, y=0.0, z=1.0)
    # The torque vector is provided as additional information denoting the
    # axis of rotation of the task.
    torque_direction = geometry_pb2.Vec3(x=-1.0, y=0.0, z=0.0)
    init_wrench_dir = geometry_pb2.Wrench(force=force_direction,
                                          torque=torque_direction)
    task_type = basic_command_pb2.ConstrainedManipulationCommand.Request.TASK_TYPE_SE3_CIRCLE_FORCE_TORQUE

    command = RobotCommandBuilder.constrained_manipulation_command(
        task_type=task_type,
        init_wrench_direction_in_frame_name=init_wrench_dir,
        force_limit=force_lim,
        torque_limit=torque_lim,
        tangential_speed=tangential_velocity,
        frame_name=frame_name)
    return command
コード例 #3
0
def construct_lever_task(velocity_normalized, force_limit=40, torque_limit=5):
    """ Helper function for manipulating levers

    params:
    + velocity_normalized: normalized task tangential velocity in range [-1.0, 1.0]
    + force_limit (optional): positive value denoting max force robot will exert along task dimension
    + torque_limit (optional): positive value denoting max torque robot will exert along
                            the axis of rotation of the task

    Output:
    + command: api command object

    Notes:
    In this function, we assume the initial motion of the lever is
    along the z axis of the hand (up and down). If the initial
    grasp is such that the initial motion needs to be something else,
    change the force direction.
    This function assumes we don't know the plane_normal (i.e. torque_direction)
    of the lever. If we do know that, we can specify it as torque_direction
    or use the ball valve task types, which assume a specific grasp and specify
    what the initial torque_direction is.
    """
    velocity_normalized = max(min(velocity_normalized, 1.0), -1.0)
    velocity_limit = scale_velocity_lim_given_force_lim(force_limit)
    tangential_velocity = velocity_normalized * velocity_limit
    frame_name = "hand"
    force_lim = force_limit
    torque_lim = torque_limit
    force_direction = geometry_pb2.Vec3(x=0.0, y=0.0, z=1.0)
    torque_direction = geometry_pb2.Vec3(x=0.0, y=0.0, z=0.0)
    init_wrench_dir = geometry_pb2.Wrench(force=force_direction,
                                          torque=torque_direction)
    task_type = basic_command_pb2.ConstrainedManipulationCommand.Request.TASK_TYPE_SE3_CIRCLE_FORCE_TORQUE

    command = RobotCommandBuilder.constrained_manipulation_command(
        task_type=task_type,
        init_wrench_direction_in_frame_name=init_wrench_dir,
        force_limit=force_lim,
        torque_limit=torque_lim,
        tangential_speed=tangential_velocity,
        frame_name=frame_name)
    return command
コード例 #4
0
def construct_crank_task(velocity_normalized, force_limit=40):
    """ Helper function for manipulating cranks with a free to rotate handle

    params:
    + velocity_normalized: normalized task tangential velocity in range [-1.0, 1.0]
    + force_limit (optional): positive value denoting max force robot will exert along task dimension

    Output:
    + command: api command object

    Notes:
    In this function, we assume the initial motion of the crank is
    along the y axis of the hand (left and right). If the initial
    grasp is such that the initial motion needs to be something else,
    change the force direction.
    """
    velocity_normalized = max(min(velocity_normalized, 1.0), -1.0)
    velocity_limit = scale_velocity_lim_given_force_lim(force_limit)
    tangential_velocity = velocity_normalized * velocity_limit
    frame_name = "hand"
    force_lim = force_limit
    # Setting a placeholder value that doesn't matter, since we don't
    # apply a pure torque in this task.
    torque_lim = 5.0
    # This assumes the grasp of crank is such that the crank will initially
    # move along the hand y axis. Change if that is not the case.
    force_direction = geometry_pb2.Vec3(x=0.0, y=1.0, z=0.0)
    torque_direction = geometry_pb2.Vec3(x=0.0, y=0.0, z=0.0)
    init_wrench_dir = geometry_pb2.Wrench(force=force_direction,
                                          torque=torque_direction)
    task_type = basic_command_pb2.ConstrainedManipulationCommand.Request.TASK_TYPE_R3_CIRCLE_EXTRADOF_FORCE

    command = RobotCommandBuilder.constrained_manipulation_command(
        task_type=task_type,
        init_wrench_direction_in_frame_name=init_wrench_dir,
        force_limit=force_lim,
        torque_limit=torque_lim,
        tangential_speed=tangential_velocity,
        frame_name=frame_name)

    return command
コード例 #5
0
def construct_wheel_task(velocity_normalized, force_limit=40):
    """ Helper function for turning wheels while grasping the rim
    Use this when the wheel is grasped on the rim. If the grasp
    is on a handle that is free to rotate, use the crank task type.
    If the handle is not free to rotate, use this task type.

    params:
    + velocity_normalized: normalized task tangential velocity in range [-1.0, 1.0]
    + force_limit (optional): positive value denoting max force robot will exert along task dimension

    Output:
    + command: api command object

    Notes:
    This assumes initial motion will be along the y axis of the hand,
    which is often the case. Change force_direction if that is not true.
    """
    velocity_normalized = max(min(velocity_normalized, 1.0), -1.0)
    velocity_limit = scale_velocity_lim_given_force_lim(force_limit)
    tangential_velocity = velocity_normalized * velocity_limit
    frame_name = "hand"
    force_lim = force_limit
    # Setting a placeholder value that doesn't matter, since we don't
    # apply a pure torque in this task.
    torque_lim = 5.0
    force_direction = geometry_pb2.Vec3(x=0.0, y=1.0, z=0.0)
    torque_direction = geometry_pb2.Vec3(x=0.0, y=0.0, z=0.0)
    init_wrench_dir = geometry_pb2.Wrench(force=force_direction,
                                          torque=torque_direction)
    task_type = basic_command_pb2.ConstrainedManipulationCommand.Request.TASK_TYPE_R3_CIRCLE_FORCE

    command = RobotCommandBuilder.constrained_manipulation_command(
        task_type=task_type,
        init_wrench_direction_in_frame_name=init_wrench_dir,
        force_limit=force_lim,
        torque_limit=torque_lim,
        tangential_speed=tangential_velocity,
        frame_name=frame_name)
    return command
コード例 #6
0
def construct_knob_task(velocity_normalized, torque_limit=5):
    """ Helper function for turning purely rotational knobs
    Use this for turning knobs/valves that do not have a lever arm

    params:
    + velocity_normalized: normalized task rotational velocity in range [-1.0, 1.0]
    + torque_limit (optional): positive value denoting max torque robot will exert along axis of
                            rotation of the task

    Output:
    + command: api command object

    Notes:
    This assumes that the axis of rotation of of the knob is roughly parallel
    to the x axis of the hand. Change torque_direction if that is not the case.
    """
    velocity_normalized = max(min(velocity_normalized, 1.0), -1.0)
    rot_velocity_limit = scale_rot_velocity_lim_given_torque_lim(torque_limit)
    rotational_velocity = velocity_normalized * rot_velocity_limit
    frame_name = "hand"
    # Setting a placeholder value that doesn't matter, since we don't
    # apply a pure force in this task.
    force_lim = 40.0
    torque_lim = torque_limit
    force_direction = geometry_pb2.Vec3(x=0.0, y=0.0, z=0.0)
    torque_direction = geometry_pb2.Vec3(x=1.0, y=0.0, z=0.0)
    init_wrench_dir = geometry_pb2.Wrench(force=force_direction,
                                          torque=torque_direction)
    task_type = basic_command_pb2.ConstrainedManipulationCommand.Request.TASK_TYPE_SE3_ROTATIONAL_TORQUE

    command = RobotCommandBuilder.constrained_manipulation_command(
        task_type=task_type,
        init_wrench_direction_in_frame_name=init_wrench_dir,
        force_limit=force_lim,
        torque_limit=torque_lim,
        rotational_speed=rotational_velocity,
        frame_name=frame_name)
    return command
コード例 #7
0
def construct_drawer_task(velocity_normalized, force_limit=40):
    """ Helper function for opening/closing drawers

    params:
    + velocity_normalized: normalized task tangential velocity in range [-1.0, 1.0]
    + force_limit (optional): positive value denoting max force robot will exert along task dimension

    Output:
    + command: api command object

    Notes:
    In this function, we assume the initial motion of the drawer is
    along the x axis of the hand (forward and backward). If the initial
    grasp is such that the initial motion needs to be something else,
    change the force direction.
    """
    velocity_normalized = max(min(velocity_normalized, 1.0), -1.0)
    velocity_limit = scale_velocity_lim_given_force_lim(force_limit)
    tangential_velocity = velocity_normalized * velocity_limit
    frame_name = "hand"
    force_lim = force_limit
    # Setting a placeholder value that doesn't matter, since we don't
    # apply a pure torque in this task.
    torque_lim = 5.0
    force_direction = geometry_pb2.Vec3(x=1.0, y=0.0, z=0.0)
    torque_direction = geometry_pb2.Vec3(x=0.0, y=0.0, z=0.0)
    init_wrench_dir = geometry_pb2.Wrench(force=force_direction,
                                          torque=torque_direction)
    task_type = basic_command_pb2.ConstrainedManipulationCommand.Request.TASK_TYPE_R3_LINEAR_FORCE

    command = RobotCommandBuilder.constrained_manipulation_command(
        task_type=task_type,
        init_wrench_direction_in_frame_name=init_wrench_dir,
        force_limit=force_lim,
        torque_limit=torque_lim,
        tangential_speed=tangential_velocity,
        frame_name=frame_name)
    return command
コード例 #8
0
def force_wrench(config):
    """Commanding a force / wrench with Spot's arm."""

    # See hello_spot.py for an explanation of these lines.
    bosdyn.client.util.setup_logging(config.verbose)

    sdk = bosdyn.client.create_standard_sdk('ForceTrajectoryClient')
    robot = sdk.create_robot(config.hostname)
    robot.authenticate(config.username, config.password)
    robot.time_sync.wait_for_sync()

    assert robot.has_arm(), "Robot requires an arm to run this example."

    # Verify the robot is not estopped and that an external application has registered and holds
    # an estop endpoint.
    assert not robot.is_estopped(), "Robot is estopped. Please use an external E-Stop client, " \
                                    "such as the estop SDK example, to configure E-Stop."

    lease_client = robot.ensure_client(
        bosdyn.client.lease.LeaseClient.default_service_name)
    lease = lease_client.acquire()
    try:
        with bosdyn.client.lease.LeaseKeepAlive(lease_client):
            # Now, we are ready to power on the robot. This call will block until the power
            # is on. Commands would fail if this did not happen. We can also check that the robot is
            # powered at any point.
            robot.logger.info(
                "Powering on robot... This may take a several seconds.")
            robot.power_on(timeout_sec=20)
            assert robot.is_powered_on(), "Robot power on failed."
            robot.logger.info("Robot powered on.")

            # Tell the robot to stand up. The command service is used to issue commands to a robot.
            # The set of valid commands for a robot depends on hardware configuration. See
            # SpotCommandHelper for more detailed examples on command building. The robot
            # command service requires timesync between the robot and the client.
            robot.logger.info("Commanding robot to stand...")
            command_client = robot.ensure_client(
                RobotCommandClient.default_service_name)
            blocking_stand(command_client, timeout_sec=10)
            robot.logger.info("Robot standing.")

            time.sleep(2.0)

            # Unstow the arm
            unstow = RobotCommandBuilder.arm_ready_command()

            # Issue the command via the RobotCommandClient
            command_client.robot_command(unstow)

            robot.logger.info("Unstow command issued.")
            time.sleep(3.0)

            # Demonstrate an example force trajectory by ramping up and down a vertical force over
            # 10 seconds

            f_x0 = 0  # Newtons
            f_y0 = 0
            f_z0 = 0

            f_x1 = 0  # Newtons
            f_y1 = 0
            f_z1 = -10  # push down

            # We won't have any rotational torques
            torque_x = 0
            torque_y = 0
            torque_z = 0

            # Duration in seconds.
            traj_duration = 5

            # First point on the trajectory
            force0 = geometry_pb2.Vec3(x=f_x0, y=f_y0, z=f_z0)
            torque0 = geometry_pb2.Vec3(x=torque_x, y=torque_y, z=torque_z)

            wrench0 = geometry_pb2.Wrench(force=force0, torque=torque0)
            t0 = seconds_to_duration(0)
            traj_point0 = trajectory_pb2.WrenchTrajectoryPoint(
                wrench=wrench0, time_since_reference=t0)

            # Second point on the trajectory
            force1 = geometry_pb2.Vec3(x=f_x1, y=f_y1, z=f_z1)
            torque1 = geometry_pb2.Vec3(x=torque_x, y=torque_y, z=torque_z)

            wrench1 = geometry_pb2.Wrench(force=force1, torque=torque1)
            t1 = seconds_to_duration(traj_duration)
            traj_point1 = trajectory_pb2.WrenchTrajectoryPoint(
                wrench=wrench1, time_since_reference=t1)

            # Build the trajectory
            trajectory = trajectory_pb2.WrenchTrajectory(
                points=[traj_point0, traj_point1])

            # Build the full request, putting all axes into force mode.
            arm_cartesian_command = arm_command_pb2.ArmCartesianCommand.Request(
                root_frame_name=ODOM_FRAME_NAME,
                wrench_trajectory_in_task=trajectory,
                x_axis=arm_command_pb2.ArmCartesianCommand.Request.
                AXIS_MODE_FORCE,
                y_axis=arm_command_pb2.ArmCartesianCommand.Request.
                AXIS_MODE_FORCE,
                z_axis=arm_command_pb2.ArmCartesianCommand.Request.
                AXIS_MODE_FORCE,
                rx_axis=arm_command_pb2.ArmCartesianCommand.Request.
                AXIS_MODE_FORCE,
                ry_axis=arm_command_pb2.ArmCartesianCommand.Request.
                AXIS_MODE_FORCE,
                rz_axis=arm_command_pb2.ArmCartesianCommand.Request.
                AXIS_MODE_FORCE)
            arm_command = arm_command_pb2.ArmCommand.Request(
                arm_cartesian_command=arm_cartesian_command)
            synchronized_command = synchronized_command_pb2.SynchronizedCommand.Request(
                arm_command=arm_command)
            robot_command = robot_command_pb2.RobotCommand(
                synchronized_command=synchronized_command)

            # Send the request
            command_client.robot_command(robot_command)
            robot.logger.info('Force trajectory command issued...')

            time.sleep(5.0 + traj_duration)

            # Power the robot off. By specifying "cut_immediately=False", a safe power off command
            # is issued to the robot. This will attempt to sit the robot before powering off.
            robot.power_off(cut_immediately=False, timeout_sec=20)
            assert not robot.is_powered_on(), "Robot power off failed."
            robot.logger.info("Robot safely powered off.")
    finally:
        # If we successfully acquired a lease, return it.
        lease_client.return_lease(lease)
コード例 #9
0
def force_wrench(config):
    """Commanding a force / wrench with Spot's arm."""

    # See hello_spot.py for an explanation of these lines.
    bosdyn.client.util.setup_logging(config.verbose)

    sdk = bosdyn.client.create_standard_sdk('ForceWrenchClient')
    robot = sdk.create_robot(config.hostname)
    robot.authenticate(config.username, config.password)
    robot.time_sync.wait_for_sync()

    assert robot.has_arm(), "Robot requires an arm to run this example."

    # Verify the robot is not estopped and that an external application has registered and holds
    # an estop endpoint.
    assert not robot.is_estopped(), "Robot is estopped. Please use an external E-Stop client, " \
                                    "such as the estop SDK example, to configure E-Stop."

    robot_state_client = robot.ensure_client(
        RobotStateClient.default_service_name)

    lease_client = robot.ensure_client(
        bosdyn.client.lease.LeaseClient.default_service_name)
    lease = lease_client.acquire()
    try:
        with bosdyn.client.lease.LeaseKeepAlive(lease_client):
            # Now, we are ready to power on the robot. This call will block until the power
            # is on. Commands would fail if this did not happen. We can also check that the robot is
            # powered at any point.
            robot.logger.info(
                "Powering on robot... This may take a several seconds.")
            robot.power_on(timeout_sec=20)
            assert robot.is_powered_on(), "Robot power on failed."
            robot.logger.info("Robot powered on.")

            # Tell the robot to stand up. The command service is used to issue commands to a robot.
            # The set of valid commands for a robot depends on hardware configuration. See
            # SpotCommandHelper for more detailed examples on command building. The robot
            # command service requires timesync between the robot and the client.
            robot.logger.info("Commanding robot to stand...")
            command_client = robot.ensure_client(
                RobotCommandClient.default_service_name)
            blocking_stand(command_client, timeout_sec=10)
            robot.logger.info("Robot standing.")
            # Send a second stand command with a lowered body height to allow the arm to reach the ground.
            stand_command = RobotCommandBuilder.synchro_stand_command(
                body_height=-0.15)
            command_id = command_client.robot_command(stand_command, timeout=2)
            time.sleep(2.0)

            # Unstow the arm
            unstow = RobotCommandBuilder.arm_ready_command()

            # Issue the command via the RobotCommandClient
            command_client.robot_command(unstow)

            robot.logger.info("Unstow command issued.")
            time.sleep(3.0)

            # Start in gravity-compensation mode (but zero force)
            f_x = 0  # Newtons
            f_y = 0
            f_z = 0

            # We won't have any rotational torques
            torque_x = 0
            torque_y = 0
            torque_z = 0

            # Duration in seconds.
            seconds = 1000

            # Use the helper function to build a single wrench
            command = RobotCommandBuilder.arm_wrench_command(
                f_x, f_y, f_z, torque_x, torque_y, torque_z, BODY_FRAME_NAME,
                seconds)

            # Send the request
            command_client.robot_command(command)
            robot.logger.info('Zero force commanded...')

            time.sleep(5.0)

            # Drive the arm into the ground with a specified force:
            f_z = -5  # Newtons

            command = RobotCommandBuilder.arm_wrench_command(
                f_x, f_y, f_z, torque_x, torque_y, torque_z, BODY_FRAME_NAME,
                seconds)

            # Send the request
            command_client.robot_command(command)

            time.sleep(5.0)

            # --------------- #

            # Hybrid position-force mode and trajectories.
            #
            # We'll move the arm in an X/Y trajectory while maintaining some force on the ground.
            # Hand will start to the left and move to the right.

            hand_x = 0.75  # in front of the robot.
            hand_y_start = 0.25  # to the left
            hand_y_end = -0.25  # to the right
            hand_z = 0  # will be ignored since we'll have a force in the Z axis.

            f_z = -5  # Newtons

            hand_vec3_start = geometry_pb2.Vec3(x=hand_x,
                                                y=hand_y_start,
                                                z=hand_z)
            hand_vec3_end = geometry_pb2.Vec3(x=hand_x, y=hand_y_end, z=hand_z)

            qw = 1
            qx = 0
            qy = 0
            qz = 0
            quat = geometry_pb2.Quaternion(w=qw, x=qx, y=qy, z=qz)

            # Build a position trajectory
            hand_pose1_in_flat_body = geometry_pb2.SE3Pose(
                position=hand_vec3_start, rotation=quat)
            hand_pose2_in_flat_body = geometry_pb2.SE3Pose(
                position=hand_vec3_end, rotation=quat)

            # Convert the poses to the odometry frame.
            robot_state = robot_state_client.get_robot_state()
            odom_T_flat_body = get_a_tform_b(
                robot_state.kinematic_state.transforms_snapshot,
                ODOM_FRAME_NAME, GRAV_ALIGNED_BODY_FRAME_NAME)
            hand_pose1 = odom_T_flat_body * math_helpers.SE3Pose.from_obj(
                hand_pose1_in_flat_body)
            hand_pose2 = odom_T_flat_body * math_helpers.SE3Pose.from_obj(
                hand_pose2_in_flat_body)

            traj_time = 5.0
            time_since_reference = seconds_to_duration(traj_time)

            traj_point1 = trajectory_pb2.SE3TrajectoryPoint(
                pose=hand_pose1.to_proto(),
                time_since_reference=seconds_to_duration(0))
            traj_point2 = trajectory_pb2.SE3TrajectoryPoint(
                pose=hand_pose2.to_proto(),
                time_since_reference=time_since_reference)

            hand_traj = trajectory_pb2.SE3Trajectory(
                points=[traj_point1, traj_point2])

            # We'll use a constant wrench here.  Build a wrench trajectory with a single point.
            # Note that we only need to fill out Z-force because that is the only axis that will
            # be in force mode.
            force_tuple = odom_T_flat_body.rotation.transform_point(x=0,
                                                                    y=0,
                                                                    z=f_z)
            force = geometry_pb2.Vec3(x=force_tuple[0],
                                      y=force_tuple[1],
                                      z=force_tuple[2])
            torque = geometry_pb2.Vec3(x=0, y=0, z=0)

            wrench = geometry_pb2.Wrench(force=force, torque=torque)

            # We set this point to happen at 0 seconds.  The robot will hold the wrench past that
            # time, so we'll end up with a constant value.
            duration = seconds_to_duration(0)

            traj_point = trajectory_pb2.WrenchTrajectoryPoint(
                wrench=wrench, time_since_reference=duration)
            trajectory = trajectory_pb2.WrenchTrajectory(points=[traj_point])

            arm_cartesian_command = arm_command_pb2.ArmCartesianCommand.Request(
                pose_trajectory_in_task=hand_traj,
                root_frame_name=ODOM_FRAME_NAME,
                wrench_trajectory_in_task=trajectory,
                x_axis=arm_command_pb2.ArmCartesianCommand.Request.
                AXIS_MODE_POSITION,
                y_axis=arm_command_pb2.ArmCartesianCommand.Request.
                AXIS_MODE_POSITION,
                z_axis=arm_command_pb2.ArmCartesianCommand.Request.
                AXIS_MODE_FORCE,
                rx_axis=arm_command_pb2.ArmCartesianCommand.Request.
                AXIS_MODE_POSITION,
                ry_axis=arm_command_pb2.ArmCartesianCommand.Request.
                AXIS_MODE_POSITION,
                rz_axis=arm_command_pb2.ArmCartesianCommand.Request.
                AXIS_MODE_POSITION)
            arm_command = arm_command_pb2.ArmCommand.Request(
                arm_cartesian_command=arm_cartesian_command)
            synchronized_command = synchronized_command_pb2.SynchronizedCommand.Request(
                arm_command=arm_command)
            robot_command = robot_command_pb2.RobotCommand(
                synchronized_command=synchronized_command)

            # Send the request
            command_client.robot_command(robot_command)
            robot.logger.info('Running mixed position and force mode.')

            time.sleep(traj_time + 5.0)

            # Power the robot off. By specifying "cut_immediately=False", a safe power off command
            # is issued to the robot. This will attempt to sit the robot before powering off.
            robot.power_off(cut_immediately=False, timeout_sec=20)
            assert not robot.is_powered_on(), "Robot power off failed."
            robot.logger.info("Robot safely powered off.")
    finally:
        # If we successfully acquired a lease, return it.
        lease_client.return_lease(lease)