A flexible and configurable ROS2 package for interfacing game controllers/joysticks with robots. This package uses pygame to read joystick inputs and publishes them as ROS2 topics, making it easy to control your robot with any standard game controller.
- Lifecycle Node Architecture: Proper state management with ROS2 lifecycle nodes
- Hot-plug Support: Automatically detects joystick connection and disconnection
- Fully Configurable: Button and axis mappings can be customized via YAML configuration
- Customizable Topic Names: Publish to any topic names you prefer
- Dead Man Switch: Safety feature for robot control
- Deadband Support: Configurable deadzones for analog sticks to prevent drift
- Default DualSense (PS5) Support: Pre-configured for PlayStation 5 DualSense controller
Demo video showing the node launch, lifecycle configuration, and topic publishing in action.
- Installation
- Quick Start
- Configuration
- Published Topics
- Parameters
- Supported Controllers
- Troubleshooting
- Lifecycle Management
- Directory Structure
- Custom Message Types
- Dependencies
- Contributing
- License
- ROS2 (Humble or later)
- Python 3
- pygame
pip3 install pygamecd ~/your_ros2_workspace
colcon build --packages-select joystick_bot
source install/setup.bashros2 launch joystick_bot js.launch.pyThe node starts in the unconfigured state. To use it, you need to configure and activate it:
# Configure the node
ros2 lifecycle set /joystick_interface configure
# Activate the node
ros2 lifecycle set /joystick_interface activate# View velocity commands
ros2 topic echo /cmd_vel_joy
# View button states
ros2 topic echo /button_state
# View dead man switch state
ros2 topic echo /dead_man_switchThe main configuration file is located at config/js_config.yaml. You can customize all aspects of the joystick interface.
The package comes pre-configured with default values suitable for the PlayStation 5 DualSense controller. If you're using a DualSense controller, you can use the package without any configuration changes.
/**/joystick_interface:
ros__parameters:
device_input: "/dev/input/js0"
linear_deadband: 0.02
angular_deadband: 0.02
# Topic names
cmd_vel_topic: "cmd_vel_joy"
button_state_topic: "button_state"
dead_man_switch_topic: "dead_man_switch"
# Button mappings (pygame button indices)
# Default values configured for DualSense (PS5) controller
button_mapping:
x_button: 0
a_button: 1
b_button: 2
y_button: 3
l1_button: 4
r1_button: 5
l2_button: 6
r2_button: 7
select_button: 8
start_button: 9
dead_man_button: 4 # L1 button by default
# Axis mappings (pygame axis indices)
# Default values configured for DualSense (PS5) controller
axis_mapping:
linear_x_axis: 1 # Left stick vertical
linear_y_axis: 0 # Left stick horizontal
angular_y_axis: 5 # Right stick vertical
angular_z_axis: 2 # Right stick horizontalYou can specify a custom configuration file when launching:
ros2 launch joystick_bot js.launch.py config_file:=/path/to/your/config.yaml| Topic | Type | Description |
|---|---|---|
/cmd_vel_joy (default) |
geometry_msgs/Twist |
Velocity commands from joystick axes |
/button_state (default) |
joystick_bot/ControllerButtonsState |
Current state of all controller buttons |
/dead_man_switch (default) |
std_msgs/Bool |
Dead man switch state (L1 button by default) |
| Parameter | Type | Default | Description |
|---|---|---|---|
device_input |
string | /dev/input/js0 |
Device path for the joystick |
linear_deadband |
double | 0.02 | Deadband threshold for linear axes |
angular_deadband |
double | 0.02 | Deadband threshold for angular axes |
| Parameter | Type | Default | Description |
|---|---|---|---|
cmd_vel_topic |
string | cmd_vel_joy |
Topic name for velocity commands |
button_state_topic |
string | button_state |
Topic name for button states |
dead_man_switch_topic |
string | dead_man_switch |
Topic name for dead man switch |
All button mappings are pygame button indices (integers). Default values are for DualSense (PS5) controller:
| Parameter | Default | Description |
|---|---|---|
button_mapping.x_button |
0 | X/Square button |
button_mapping.a_button |
1 | A/Cross button |
button_mapping.b_button |
2 | B/Circle button |
button_mapping.y_button |
3 | Y/Triangle button |
button_mapping.l1_button |
4 | L1/Left bumper |
button_mapping.r1_button |
5 | R1/Right bumper |
button_mapping.l2_button |
6 | L2/Left trigger |
button_mapping.r2_button |
7 | R2/Right trigger |
button_mapping.select_button |
8 | Select/Share button |
button_mapping.start_button |
9 | Start/Options button |
button_mapping.dead_man_button |
4 | Dead man switch button (L1) |
All axis mappings are pygame axis indices (integers). Default values are for DualSense (PS5) controller:
| Parameter | Default | Description |
|---|---|---|
axis_mapping.linear_x_axis |
1 | Left stick vertical (forward/backward) |
axis_mapping.linear_y_axis |
0 | Left stick horizontal (left/right strafe) |
axis_mapping.angular_y_axis |
5 | Right stick vertical (pitch) |
axis_mapping.angular_z_axis |
2 | Right stick horizontal (yaw/rotation) |
The following diagram illustrates the standard button naming and position conventions used in this package. Use this as a reference when configuring your own controller:
If your controller has a different layout, you can customize the button and axis mappings through the js_config.yaml file.
The default configuration is optimized for the DualSense (PS5) controller. Simply connect your controller via USB or Bluetooth and launch the node.
| Control | Function | Parameter | Index |
|---|---|---|---|
| Left Stick | Robot movement (forward/backward, left/right) | linear_x_axis, linear_y_axis |
Axes 1, 0 |
| Right Stick | Robot rotation (yaw/pitch) | angular_z_axis, angular_y_axis |
Axes 2, 5 |
| Cross (X) | A Button | a_button |
Button 1 |
| Circle (O) | B Button | b_button |
Button 2 |
| Square | X Button | x_button |
Button 0 |
| Triangle | Y Button | y_button |
Button 3 |
| L1 | Dead man switch / L1 Button | dead_man_button, l1_button |
Button 4 |
| R1 | R1 Button | r1_button |
Button 5 |
| L2 | L2 Trigger | l2_button |
Button 6 |
| R2 | R2 Trigger | r2_button |
Button 7 |
| Share | Select Button | select_button |
Button 8 |
| Options | Start Button | start_button |
Button 9 |
| D-pad | Directional buttons | HAT 0 | HAT values |
To use other controllers (Xbox, Logitech, etc.), you'll need to determine the button and axis mappings for your specific controller.
The easiest way to find button and axis mappings is using the jstest command-line tool:
# Install jstest if not already available
sudo apt-get install joystick
# Monitor joystick events in real-time
jstest /dev/input/js0Press buttons and move sticks to see which button/axis indices are triggered. The output will show:
- Button numbers when pressed/released
- Axis numbers and their values when sticks are moved
Once you identify the indices for your controller, update the config/js_config.yaml file with your custom mappings.
-
Check if the joystick is recognized by the system:
ls /dev/input/js* -
Verify pygame can detect it:
python3 -c "import pygame; pygame.init(); pygame.joystick.init(); print(f'Joysticks found: {pygame.joystick.get_count()}')" -
Check permissions:
sudo chmod a+rw /dev/input/js0
- If using Bluetooth, ensure the controller is properly paired
- Try reconnecting the controller (USB or Bluetooth)
- Check
dmesgfor any USB/input device errors - The node supports hot-plugging, so you can disconnect and reconnect while running
- Increase the deadband values in the configuration:
linear_deadband: 0.05 angular_deadband: 0.05
- Use the button/axis discovery scripts above to find correct indices
- Update
config/js_config.yamlwith your controller's mappings
This node uses ROS2 lifecycle management with the following states:
- Unconfigured: Initial state, no resources allocated
- Inactive: Configured but not publishing (safe state)
- Active: Fully operational, reading joystick and publishing data
- Finalized: Cleaned up and shut down
# Configure the node
ros2 lifecycle set /joystick_interface configure
# Activate the node
ros2 lifecycle set /joystick_interface activate
# Deactivate (keeps configuration)
ros2 lifecycle set /joystick_interface deactivate
# Cleanup (returns to unconfigured)
ros2 lifecycle set /joystick_interface cleanup
# Shutdown
ros2 lifecycle set /joystick_interface shutdownjoystick_bot/
├── joystick_bot/ # Python package directory
│ └── __init__.py # Package initialization
├── config/ # Configuration files
│ └── js_config.yaml # Joystick configuration parameters
├── docs/ # Documentation files
│ └── images/ # Images and media
│ ├── controller_layout.png # Controller button reference diagram
│ └── running_example.mp4 # Demo video
├── launch/ # Launch files
│ └── js.launch.py # Joystick interface launcher
├── msg/ # Custom message definitions
│ └── ControllerButtonsState.msg # Button state message
├── scripts/ # Executable scripts
│ └── js_node.py # Main joystick interface node
├── CMakeLists.txt # CMake build configuration
├── package.xml # ROS2 package manifest
├── README.md # This file
└── README_br.md # Portuguese version of README
bool a_button
bool b_button
bool x_button
bool y_button
bool start_button
bool select_button
bool up_button # D-pad
bool down_button # D-pad
bool left_button # D-pad
bool right_button # D-pad
bool l1_button
bool l2_button
bool r1_button
bool r2_button
rclpy- Python ROS2 client libraryrclcpp- C++ ROS2 client librarystd_msgs- Standard message typesgeometry_msgs- Geometry message typesrosidl_default_generators- Interface generation
pygame- Joystick input handling library- Linux input subsystem (
/dev/input/js*)
Contributions are welcome! Please feel free to submit issues or pull requests.
- Fork the repository
- Create a feature branch
- Make your changes
- Test thoroughly
- Submit a pull request
This project is licensed under the MIT License - see the LICENSE file for details.
Made with ❤️ in Brazil
