Skip to content

Remote Control and Gamepad

Overview

Remote controllers and game controllers are the most intuitive control methods for robots, especially indispensable during debugging, teleoperation, and emergency takeover scenarios. This section introduces common remote control solutions and their integration with ROS2.

Game Controllers

PS4 DualShock 4 / PS5 DualSense

Parameter DualShock 4 DualSense (PS5)
Connection Bluetooth / USB Bluetooth / USB
Joysticks 2 analog sticks 2 analog sticks
Triggers L2/R2 analog triggers L2/R2 adaptive triggers
Buttons 14 buttons + touchpad 14 buttons + touchpad
Gyroscope Yes Yes
Vibration Yes Haptic feedback
Battery Life 4-8h 12h
Price ~$40-60 ~$60-70

Linux Pairing:

# Install ds4drv (PS4)
pip install ds4drv
sudo ds4drv

# Or use direct Bluetooth connection
bluetoothctl
> scan on
> pair XX:XX:XX:XX:XX:XX
> connect XX:XX:XX:XX:XX:XX
> trust XX:XX:XX:XX:XX:XX

# Verify
ls /dev/input/js*
# Or
cat /dev/input/js0 | xxd

Testing Tools:

# Install joystick tools
sudo apt install joystick

# Test controller
jstest /dev/input/js0

# Graphical test
sudo apt install jstest-gtk
jstest-gtk

Xbox Controller

Parameter Xbox Series Xbox One
Connection Bluetooth/USB/Wireless adapter USB/Wireless adapter
Compatibility Native Linux support (xpad) Native Linux support
Price ~$50-65 ~$35-50

Xbox controllers have the best Linux compatibility, recommended for use:

# Xbox controllers are usually auto-detected
# xpad driver is built into the Linux kernel

# For advanced features
sudo apt install xboxdrv
sudo xboxdrv --detach-kernel-driver

Controller Button Mapping

Standard mapping (Xbox layout):

Button/Axis Index Recommended Function
Left Stick X axes[0] Lateral movement/Steering
Left Stick Y axes[1] Forward/Backward
Right Stick X axes[2] View/Rotation
Right Stick Y axes[3] Lift/Pitch
LT (L2) axes[4] Decelerate
RT (R2) axes[5] Accelerate
A Button buttons[0] Confirm/Start
B Button buttons[1] Cancel/Stop
X Button buttons[2] Function 1
Y Button buttons[3] Function 2
LB (L1) buttons[4] Mode switch
RB (R1) buttons[5] Mode switch
Back buttons[6] Emergency stop
Start buttons[7] Enable teleoperation

RC Controllers

Hobby RC Transmitters

Hobby/RC transmitters are suitable for scenarios requiring long-range, high-reliability control (UAVs, outdoor robots).

Brand/Model Channels Frequency Protocol Price
FlySky FS-i6X 6(10) ch 2.4GHz IBUS/PPM ~$50
FlySky FS-i6S 10 ch 2.4GHz IBUS ~$70
FrSky X9 Lite 16 ch 2.4GHz SBUS/ACCST ~$80
FrSky Taranis X9D 16 ch 2.4GHz SBUS ~$200
RadioMaster TX16S 16 ch 2.4GHz Multi-protocol ~$150

Signal Protocols

Protocol Signal Type Features
PWM One wire per channel, 1-2ms pulse Simplest, many wires
PPM Single wire serial, 1-2ms pulse train One wire for multiple channels
SBUS Inverted UART, 100kbps 16 channels, digital
IBUS UART, 115200bps FlySky proprietary, easy to parse

SBUS Protocol Parsing

SBUS is the standard protocol for FrSky and other high-end transmitters:

  • Baud rate: 100000 bps
  • Data bits: 8bit, even parity, 2 stop bits
  • Signal inverted: Requires hardware inverter or software handling
  • Frame length: 25 bytes, containing 16 channels of 11-bit values
# SBUS parsing (MicroPython/Python)
import serial

# SBUS signal is inverted; some USB-to-TTL chips can handle this
# Or use a hardware inverter (a single NPN transistor is sufficient)
ser = serial.Serial('/dev/ttyAMA0', 100000, 
                     parity=serial.PARITY_EVEN,
                     stopbits=serial.STOPBITS_TWO)

def parse_sbus(frame):
    """Parse 25-byte SBUS frame, return 16 channel values (172-1811)"""
    if len(frame) != 25 or frame[0] != 0x0F:
        return None

    channels = []
    bits = 0
    byte_idx = 1
    bit_idx = 0

    for ch in range(16):
        value = 0
        for bit in range(11):
            if frame[byte_idx] & (1 << bit_idx):
                value |= (1 << bit)
            bit_idx += 1
            if bit_idx >= 8:
                bit_idx = 0
                byte_idx += 1
        channels.append(value)

    return channels

IBUS Protocol Parsing

IBUS is much simpler than SBUS:

import serial

ser = serial.Serial('/dev/ttyUSB0', 115200)

def parse_ibus(frame):
    """Parse IBUS frame, return channel values (1000-2000)"""
    if len(frame) != 32 or frame[0] != 0x20 or frame[1] != 0x40:
        return None

    channels = []
    for i in range(14):
        low = frame[2 + i * 2]
        high = frame[3 + i * 2]
        channels.append(low | (high << 8))

    return channels

Custom Remote Controller (ESP32)

ESP32 + Joystick + Wireless

Building a custom remote controller with ESP32:

// ESP32 Remote Controller - Transmitter
#include <esp_now.h>
#include <WiFi.h>

#define JOY_X  34  // Analog pin
#define JOY_Y  35
#define BTN_A  32
#define BTN_B  33

// Robot's MAC address
uint8_t robotAddress[] = {0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF};

typedef struct {
    int16_t joy_x;
    int16_t joy_y;
    uint8_t btn_a;
    uint8_t btn_b;
} ControlData;

ControlData data;

void setup() {
    WiFi.mode(WIFI_STA);
    esp_now_init();

    esp_now_peer_info_t peerInfo;
    memcpy(peerInfo.peer_addr, robotAddress, 6);
    peerInfo.channel = 0;
    peerInfo.encrypt = false;
    esp_now_add_peer(&peerInfo);
}

void loop() {
    data.joy_x = analogRead(JOY_X) - 2048;  // Center
    data.joy_y = analogRead(JOY_Y) - 2048;
    data.btn_a = !digitalRead(BTN_A);
    data.btn_b = !digitalRead(BTN_B);

    esp_now_send(robotAddress, (uint8_t*)&data, sizeof(data));
    delay(20);  // 50Hz
}

ROS2 joy Package

Installation and Usage

# Install
sudo apt install ros-humble-joy ros-humble-teleop-twist-joy

# Launch joy node (reads controller)
ros2 run joy joy_node

# View controller data
ros2 topic echo /joy

# Launch teleop (maps controller to cmd_vel)
ros2 launch teleop_twist_joy teleop-launch.py

Joy Message Format

# sensor_msgs/msg/Joy
Header header
float32[] axes      # Joystick/trigger values [-1.0, 1.0]
int32[] buttons     # Button states [0 or 1]

Custom Controller-to-Velocity Mapping

#!/usr/bin/env python3
import rclpy
from rclpy.node import Node
from sensor_msgs.msg import Joy
from geometry_msgs.msg import Twist

class JoyTeleop(Node):
    def __init__(self):
        super().__init__('joy_teleop')

        # Parameters
        self.declare_parameter('max_linear', 1.0)
        self.declare_parameter('max_angular', 2.0)
        self.declare_parameter('enable_button', 7)  # Start button

        self.max_linear = self.get_parameter('max_linear').value
        self.max_angular = self.get_parameter('max_angular').value
        self.enable_btn = self.get_parameter('enable_button').value

        self.joy_sub = self.create_subscription(Joy, '/joy', self.joy_cb, 10)
        self.cmd_pub = self.create_publisher(Twist, '/cmd_vel', 10)

        self.enabled = False

    def joy_cb(self, msg):
        # Safety switch: must hold Start button to move
        if msg.buttons[self.enable_btn]:
            self.enabled = True
        else:
            self.enabled = False

        twist = Twist()

        if self.enabled:
            # Left stick Y -> Forward/Backward
            twist.linear.x = msg.axes[1] * self.max_linear
            # Right stick X -> Rotation
            twist.angular.z = msg.axes[2] * self.max_angular

        self.cmd_pub.publish(twist)

def main():
    rclpy.init()
    node = JoyTeleop()
    rclpy.spin(node)

if __name__ == '__main__':
    main()

Launch File

# teleop_launch.py
from launch import LaunchDescription
from launch_ros.actions import Node

def generate_launch_description():
    return LaunchDescription([
        Node(
            package='joy',
            executable='joy_node',
            parameters=[{
                'dev': '/dev/input/js0',
                'deadzone': 0.05,
                'autorepeat_rate': 20.0,
            }]
        ),
        Node(
            package='my_robot_teleop',
            executable='joy_teleop',
            parameters=[{
                'max_linear': 0.5,
                'max_angular': 1.5,
                'enable_button': 7,
            }]
        ),
    ])

Safety Design

Deadman's Switch

Teleoperated robots must implement a deadman's switch -- releasing the button causes the robot to stop immediately:

# Implemented in joy_cb
if not msg.buttons[self.enable_btn]:
    # Enable button not held -> stop immediately
    twist = Twist()  # All zeros
    self.cmd_pub.publish(twist)
    return

Communication Timeout

If the controller signal is lost, the robot should automatically stop:

def __init__(self):
    # ...
    self.last_joy_time = self.get_clock().now()
    self.timeout = 0.5  # 500ms timeout

    self.timer = self.create_timer(0.05, self.check_timeout)

def joy_cb(self, msg):
    self.last_joy_time = self.get_clock().now()
    # ... normal processing

def check_timeout(self):
    dt = (self.get_clock().now() - self.last_joy_time).nanoseconds / 1e9
    if dt > self.timeout:
        # Timeout -> stop
        self.cmd_pub.publish(Twist())

Speed Limiting

# Use trigger as speed scaling
speed_scale = (msg.axes[5] + 1.0) / 2.0  # RT: -1~1 -> 0~1
twist.linear.x = msg.axes[1] * self.max_linear * speed_scale

Solution Selection

Scenario Recommended Solution Reason
ROS2 robot debugging Xbox controller + joy package Best compatibility
Indoor service robot PS4/PS5 controller (Bluetooth) Wireless, good ergonomics
Outdoor/UAV FrSky transmitter (SBUS) Long range, reliable
DIY/Education ESP32 custom remote Flexible, educational
Competition robot RadioMaster TX16S Multi-protocol, professional

References

  • ROS2 joy package: GitHub
  • ds4drv: GitHub
  • FlySky IBUS Protocol Specification
  • FrSky SBUS Protocol Documentation
  • ESP-NOW Documentation (Espressif)

评论 #