For this example, make sure to position the robot with 2m of clear space on all sides, as you will be able to command walking in translation and rotation and arm stowing and unstowing.
ros2 run spot_examples arm_wasdIf you launched the driver with a namespace, use the following command instead:
ros2 run spot_examples arm_wasd --robot <spot_name>The interface will automatically:
- Claim the robot
- Power on the robot
- Wait for user commands
The interface has two control modes that you can toggle between:
BASE MOVEMENT MODE (default):
- Controls the robot's body movement
- Walking, turning, standing, sitting
ARM CONTROL MODE:
- Controls the robot's arm in 6 degrees of freedom
- Arm positioning and end-effector rotation
- [T]: Toggle between BASE and ARM control modes
- [Tab]: Quit the program
- [ESC]: Emergency stop
- [p]: Toggle power on/off
- [f]: Stand up
- [v]: Sit down
- [r]: Self-right (if robot is on its side)
- [b]: Battery change pose (rollover)
- [y]: Unstow arm (deploy from stowed position)
- [h]: Stow arm (return to stowed position)
- [n]: Open gripper
- [m]: Close gripper
- [w]: Move forward
- [s]: Move backward
- [a]: Strafe left
- [d]: Strafe right
- [q]: Turn left
- [e]: Turn right
- [w]: Move arm forward/out
- [s]: Move arm backward/in
- [a]: Rotate arm counter-clockwise
- [d]: Rotate arm clockwise
- [q]: Move arm up
- [e]: Move arm down
- [i/k]: Rotate around Y-axis (+/-)
- [u/o]: Rotate around X-axis (+/-)
- [j/l]: Rotate around Z-axis (+/-)
- Emergency stop: ESC key stops all movement immediately
- Arm locking: When arm is unstowed and base commands are issued, arm locks in position
- Service call blocking: Prevents conflicting commands during service calls
- Graceful shutdown: On exit, automatically sits robot and powers down
- Always test in a safe environment with adequate space
- Keep emergency stop ready: Know where the ESC key is
- Monitor robot status: Watch the power and battery indicators
- Start with small movements: Test each control before making large movements
- Unstow arm carefully: Ensure adequate clearance before deploying arm
The WASD interface is built using a modular architecture with clear separation of concerns:
WasdInterface
├── ROS 2 Integration (publishers, subscribers, service clients)
├── Control Mode Management (base vs arm)
├── Command Processing (keyboard input handling)
├── Safety Systems (emergency stop, arm locking)
└── UI Management (curses-based display)
WasdInterface - Main controller class
- Manages robot connection and control
- Handles user input and command routing
- Provides safety mechanisms
ExitCheck - Signal handling utility
- Captures SIGTERM/SIGINT for graceful shutdown
- Provides clean exit mechanism
SimpleSpotCommander - Robot command wrapper
- Abstracts basic robot operations
- Provides simplified interface for common commands
self._base_command_dictionary = {
ord("w"): self._move_forward,
ord("s"): self._move_backward,
# ... more commands
}Each key maps to a specific command function, allowing easy extension and modification of controls.
self.control_mode = "base" # or "arm"
self.is_arm_unstowed = False
self.is_arm_locked_in_position = FalseTracks robot and interface state to ensure safe operation.
# Publishers for sending commands
self.pub_cmd_vel = self.node.create_publisher(Twist, ...)
self.pub_arm_vel = self.node.create_publisher(ArmVelocityCommandRequest, ...)
# Subscribers for status updates
self.sub_status_power_state = self.node.create_subscription(PowerState, ...)
self.sub_battery_state = self.node.create_subscription(BatteryStateArray, ...)Velocity Control:
- Uses
geometry_msgs/Twistmessages - Continuous velocity commands with timeout
- Linear and angular velocity components
def _velocity_cmd_helper(self, desc: str = "", v_x: float = 0.0, v_y: float = 0.0, v_rot: float = 0.0):
twist = Twist()
twist.linear.x = v_x
twist.linear.y = v_y
twist.angular.z = v_rot
# Publish continuously for duration
start_time = time.time()
while time.time() - start_time < VELOCITY_CMD_DURATION:
self.pub_cmd_vel.publish(twist)
time.sleep(0.01)6-DOF Arm Control:
- Uses cylindrical coordinate system (r, theta, z)
- End-effector rotation control
- Velocity-based control with acceleration limits
def arm_velocity_cmd_helper(self, r=0.0, theta=0.0, z=0.0,
end_effector_x=0.0, end_effector_y=0.0, end_effector_z=0.0):
arm_cmd = ArmVelocityCommandRequest()
# Set cylindrical velocities
arm_cmd.command.cylindrical_velocity.linear_velocity.r = r
arm_cmd.command.cylindrical_velocity.linear_velocity.theta = theta
arm_cmd.command.cylindrical_velocity.linear_velocity.z = z
# Set end-effector angular velocities
arm_cmd.angular_velocity_of_hand_rt_odom_in_hand.x = end_effector_x
# ... publish with duration controldef _stop(self) -> None:
self.cli_stop.call_async(Trigger.Request())Immediately stops all robot movement.
def _velocity_cmd_helper(self, ...):
if(self.is_arm_unstowed):
if(not self.is_arm_locked_in_position):
result = self.lock_arm_in_position()
# Lock arm before base movementWhen arm is deployed, it's locked in position before allowing base movement.
if(self.service_call_in_progress):
self.add_message("Service call in progress, cannot send velocity command")
returnPrevents conflicting commands during ongoing operations.
JOY_TELEOP_QOS = QoSProfile(
reliability=ReliabilityPolicy.BEST_EFFORT,
history=HistoryPolicy.KEEP_LAST,
depth=1,
durability=DurabilityPolicy.VOLATILE,
lifespan=rclpy.duration.Duration(seconds=1.0),
)Optimized for real-time teleop control with low latency.
The interface uses multiple ROS 2 service clients for robot operations:
- Power control (
power_on,power_off) - Posture control (
sit,stand,self_right) - Arm control (
arm_stow,arm_unstow) - Gripper control (
open_gripper,close_gripper)
def _drive_draw(self, stdscr: curses.window) -> None:
stdscr.clear()
stdscr.addstr(0, 0, f"robot name: {self.robot_name}")
stdscr.addstr(1, 0, f"CONTROL MODE: {self.control_mode.upper()}")
# ... display status and help textdef _drive_cmd(self, key: int) -> None:
if self.control_mode == "base":
cmd_function = self._base_command_dictionary[key]
else:
cmd_function = self._arm_command_dictionary[key]
cmd_function()VELOCITY_BASE_SPEED = 0.3 # m/s - base linear velocity
VELOCITY_BASE_ANGULAR = 0.5 # rad/sec - base angular velocity
ARM_VELOCITY_NORMALIZED = 0.4 # m/s - arm linear velocity
ARM_VELOCITY_ANGULAR_NORMALIZED = 0.3 # rad/s - arm angular velocityVELOCITY_CMD_DURATION = 1.0 # Duration to send base velocity commands
ARM_VELOCITY_CMD_DURATION = 0.25 # Duration to send arm velocity commands
COMMAND_INPUT_RATE = 0.1 # Rate limiting for input processingThe interface continuously monitors:
- Battery status: Charge levels for all battery packs
- Power state: Motor power on/off status
- Arm state: Stowed/unstowed status
- Service call status: Prevents command conflicts
- Add key mapping to appropriate command dictionary
- Implement command function
- Update help text in
_drive_draw()
- Extend
control_modestate management - Create new command dictionary
- Add mode switching logic
- Update UI display
Modify the constants at the top of the file to adjust:
- Movement speeds
- Command durations
- Acceleration limits
- Input rates
This architecture provides a robust, extensible foundation for robot teleoperation while maintaining safety and usability.