Reachy Mini documentation

Joystick Controller

Hugging Face's logo
Join the Hugging Face community

and get access to the augmented documentation experience

to get started

Joystick Controller

This example demonstrates how to control Reachy Mini’s head yaw angle using a joystick (PS4 or Xbox controller). The left joystick controls the head’s left-right rotation, providing intuitive real-time control of the robot.

Controls:

  • LEFT JOYSTICK (Left/Right): Control head yaw angle
  • CIRCLE / B BUTTON: Quit the application safely
  • CTRL-C: Quit the application

Requirements:

  • Install pygame: pip install pygame
  • Connect a PS4 or Xbox controller to your computer

Controller mappings:

  • PS4: Button 1 = Circle (O), Axis 0 = Left Stick Horizontal
  • Xbox: Button 1 = B, Axis 0 = Left Stick Horizontal

# Standard library imports
import os
import sys
import time

# Third-party imports
import numpy as np
import pygame

# Local application/library-specific imports
from reachy_mini import ReachyMini, utils

# --- Configuration ---
CONTROL_LOOP_RATE = 0.02
# Maximum yaw angle. The joystick's -1 to 1 input will be mapped to this range.
YAW_ANGLE_LIMIT = np.pi / 4 * 1.3  # Radians

# To use pygame "headlessly" (without a GUI window).
os.environ["SDL_VIDEODRIVER"] = "dummy"

# --- Controller Bindings Comment ---
# PS4 controller:
# Button 1 = O (Circle)
# Axis 0: Left Joy Left/Right   (-1 left, 1 right)
# Axis 3 or 4: Right Joy Left/Right
#
# XBOX controller:
# Button 1 = B
# Axis 0: Left Joy Left/Right
# Axis 2 or 3: Right Joy Left/Right


class Controller:
    """Handle joystick input using pygame."""

    def __init__(self, deadzone: float = 0.08):
        """Initialize the controller and find the first joystick.

        Args:
            deadzone (float): Axis value below which input is ignored.

        Raises:
            IOError: If no joystick is found.

        """
        pygame.init()
        pygame.joystick.init()

        if pygame.joystick.get_count() < 1:
            raise IOError("No joystick controller found.")

        self.joystick: pygame.joystick.Joystick = pygame.joystick.Joystick(0)
        self.deadzone = deadzone
        print(f"Initialized joystick: {self.joystick.get_name()}")

    def _apply_deadzone(self, value: float) -> float:
        """Apply a deadzone to a joystick axis value."""
        return value if abs(value) > self.deadzone else 0.0

    def get_horizontal_inputs(self) -> tuple[float, float]:
        """Read the horizontal axes of the left and right joysticks.

        Returns:
            tuple[float, float]: (left_joy_h, right_joy_h) from -1.0 to 1.0.

        """
        pygame.event.pump()  # Update pygame's internal event state.

        left_joy_h = self._apply_deadzone(self.joystick.get_axis(0))

        # Right joystick horizontal axis can be 2, 3 or 4 depending on controller
        right_joy_h = 0.0
        if self.joystick.get_numaxes() > 3:
            right_joy_h = self._apply_deadzone(self.joystick.get_axis(3))
        elif self.joystick.get_numaxes() > 2:
            right_joy_h = self._apply_deadzone(self.joystick.get_axis(2))

        return left_joy_h, right_joy_h

    def check_for_quit(self) -> bool:
        """Check pygame events for a quit signal.

        Returns:
            bool: True if the designated quit button (Circle/B) is pressed.

        """
        for event in pygame.event.get():
            if event.type == pygame.JOYBUTTONDOWN:
                if self.joystick.get_button(1):  # Button 1 is Circle/B
                    print("\nQuit button pressed.")
                    return True
        return False


def main() -> None:
    """Run the main joystick control loop."""
    try:
        controller = Controller()
    except IOError as e:
        print(f"Error: {e}", file=sys.stderr)
        return

    print("Connecting to Reachy Mini...")
    try:
        # The 'with' statement ensures the robot is properly handled on exit
        with ReachyMini(automatic_body_yaw=True) as mini:
            print("Robot connected.")

            print("\n" + "=" * 50)
            print("  Reachy Head Yaw Joystick Controller")
            print("  CONTROLS: [Left Stick] to turn | [Circle/B] to quit")
            print("=" * 50 + "\n")

            while True:
                if controller.check_for_quit():
                    break

                # Get scaled joystick values
                left_joy, right_joy = controller.get_horizontal_inputs()

                # Map joystick input (-1 to 1) to the desired angle range
                target_yaw = left_joy * YAW_ANGLE_LIMIT

                target_body_yaw = right_joy * YAW_ANGLE_LIMIT

                # Define the target pose: x,y,z and roll,pitch,yaw
                target_position = np.array([0, 0, 0.0])
                target_orientation = np.array([0, 0, target_yaw])

                # Create and send the command to the robot
                mini.set_target(
                    utils.create_head_pose(
                        x=target_position[0],
                        y=target_position[1],
                        z=target_position[2],
                        roll=target_orientation[0],
                        pitch=target_orientation[1],
                        yaw=target_orientation[2],
                        degrees=False,
                    ),
                    body_yaw=target_body_yaw,
                )

                # Print status, overwriting the line
                print(
                    f"\rHead Yaw: {target_yaw:6.2f} rad | "
                    f"Body Yaw: {target_body_yaw:6.2f} rad",
                    end="",
                )
                sys.stdout.flush()

                time.sleep(CONTROL_LOOP_RATE)

    except KeyboardInterrupt:
        print("\nCTRL+C detected. Shutting down...")
    except Exception as e:
        print(f"\nAn unexpected error occurred: {e}", file=sys.stderr)
    finally:
        print("\n\nApplication finished. Robot will go to sleep.")
        pygame.quit()


if __name__ == "__main__":
    main()
Update on GitHub