跳转至

遥控与手柄

概述

遥控器和游戏手柄是机器人最直观的控制方式,尤其在调试、遥操作和紧急接管场景中不可或缺。本节介绍常用的遥控方案及其与ROS2的集成方法。

游戏手柄

PS4 DualShock 4 / PS5 DualSense

参数 DualShock 4 DualSense (PS5)
连接方式 Bluetooth / USB Bluetooth / USB
摇杆 2个模拟摇杆 2个模拟摇杆
扳机 L2/R2 模拟扳机 L2/R2 自适应扳机
按键 14个按键 + 触摸板 14个按键 + 触摸板
陀螺仪
振动 触觉反馈
续航 4-8h 12h
价格 ~¥200-350 ~¥400-500

Linux配对

# 安装ds4drv(PS4)
pip install ds4drv
sudo ds4drv

# 或使用蓝牙直连
bluetoothctl
> scan on
> pair XX:XX:XX:XX:XX:XX
> connect XX:XX:XX:XX:XX:XX
> trust XX:XX:XX:XX:XX:XX

# 验证
ls /dev/input/js*
# 或
cat /dev/input/js0 | xxd

测试工具

# 安装 joystick 工具
sudo apt install joystick

# 测试手柄
jstest /dev/input/js0

# 图形化测试
sudo apt install jstest-gtk
jstest-gtk

Xbox控制器

参数 Xbox Series Xbox One
连接方式 Bluetooth/USB/无线适配器 USB/无线适配器
兼容性 Linux原生支持(xpad) Linux原生支持
价格 ~¥350-450 ~¥250-350

Xbox控制器在Linux上兼容性最好,推荐使用:

# Xbox控制器通常自动识别
# xpad驱动内置在Linux内核中

# 如果需要高级功能
sudo apt install xboxdrv
sudo xboxdrv --detach-kernel-driver

手柄按键映射

标准映射(Xbox布局):

按键/轴 索引 推荐功能
左摇杆X axes[0] 左右平移/转向
左摇杆Y axes[1] 前进/后退
右摇杆X axes[2] 视角/旋转
右摇杆Y axes[3] 升降/俯仰
LT (L2) axes[4] 减速
RT (R2) axes[5] 加速
A按键 buttons[0] 确认/启动
B按键 buttons[1] 取消/停止
X按键 buttons[2] 功能1
Y按键 buttons[3] 功能2
LB (L1) buttons[4] 模式切换
RB (R1) buttons[5] 模式切换
Back buttons[6] 紧急停止
Start buttons[7] 启用遥控

RC遥控器

航模遥控器

航模/RC遥控器适合需要远距离、高可靠性控制的场景(无人机、室外机器人)。

品牌/型号 通道数 频率 协议 价格
FlySky FS-i6X 6(10)通道 2.4GHz IBUS/PPM ~¥200
FlySky FS-i6S 10通道 2.4GHz IBUS ~¥300
FrSky X9 Lite 16通道 2.4GHz SBUS/ACCST ~¥400
FrSky Taranis X9D 16通道 2.4GHz SBUS ~¥1000
RadioMaster TX16S 16通道 2.4GHz 多协议 ~¥800

信号协议

协议 信号类型 特点
PWM 每通道一根线,1-2ms脉冲 最简单,线多
PPM 单线串行,1-2ms脉冲串 一根线多通道
SBUS UART反相,100kbps 16通道,数字化
IBUS UART,115200bps FlySky专用,易解析

SBUS协议解析

SBUS是FrSky等高端遥控器的标准协议:

  • 波特率:100000 bps
  • 数据位:8bit,偶校验,2停止位
  • 信号反相:需要硬件反相器或软件处理
  • 帧长:25字节,包含16个11位通道值
# SBUS解析(MicroPython/Python)
import serial

# SBUS信号是反相的,某些USB转TTL芯片可以处理
# 或使用硬件反相器(一个NPN三极管即可)
ser = serial.Serial('/dev/ttyAMA0', 100000, 
                     parity=serial.PARITY_EVEN,
                     stopbits=serial.STOPBITS_TWO)

def parse_sbus(frame):
    """解析25字节SBUS帧,返回16个通道值(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协议解析

IBUS比SBUS简单得多:

import serial

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

def parse_ibus(frame):
    """解析IBUS帧,返回通道值(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

自定义遥控器(ESP32)

ESP32 + 摇杆 + 无线

使用ESP32制作自定义遥控器:

// ESP32 遥控器发送端
#include <esp_now.h>
#include <WiFi.h>

#define JOY_X  34  // 模拟引脚
#define JOY_Y  35
#define BTN_A  32
#define BTN_B  33

// 机器人的MAC地址
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;  // 中心化
    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包

安装与使用

# 安装
sudo apt install ros-humble-joy ros-humble-teleop-twist-joy

# 启动joy节点(读取手柄)
ros2 run joy joy_node

# 查看手柄数据
ros2 topic echo /joy

# 启动teleop(将手柄映射到cmd_vel)
ros2 launch teleop_twist_joy teleop-launch.py

Joy消息格式

# sensor_msgs/msg/Joy
Header header
float32[] axes      # 摇杆/扳机值 [-1.0, 1.0]
int32[] buttons     # 按键状态 [0 或 1]

自定义手柄到速度的映射

#!/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')

        # 参数
        self.declare_parameter('max_linear', 1.0)
        self.declare_parameter('max_angular', 2.0)
        self.declare_parameter('enable_button', 7)  # Start按键

        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):
        # 安全开关:必须按住Start按键才能移动
        if msg.buttons[self.enable_btn]:
            self.enabled = True
        else:
            self.enabled = False

        twist = Twist()

        if self.enabled:
            # 左摇杆Y -> 前进/后退
            twist.linear.x = msg.axes[1] * self.max_linear
            # 右摇杆X -> 旋转
            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文件

# 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,
            }]
        ),
    ])

安全设计

死人开关(Deadman's Switch)

遥控机器人必须实现死人开关——松开按键时机器人立即停止:

# 在joy_cb中实现
if not msg.buttons[self.enable_btn]:
    # 未按住使能键 -> 立即停止
    twist = Twist()  # 全零
    self.cmd_pub.publish(twist)
    return

通信超时

如果手柄信号丢失,机器人应自动停止:

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

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

def joy_cb(self, msg):
    self.last_joy_time = self.get_clock().now()
    # ... 正常处理

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

速度限制

# 使用扳机作为速度缩放
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

方案选型

场景 推荐方案 理由
ROS2机器人调试 Xbox手柄 + joy包 兼容性最好
室内服务机器人 PS4/PS5手柄 (蓝牙) 无线、手感好
室外/无人机 FrSky遥控器 (SBUS) 远距离、可靠
DIY/教育 ESP32自定义遥控 灵活、学习
竞赛机器人 RadioMaster TX16S 多协议、专业

参考资源

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

评论 #