遥控与手柄
概述
遥控器和游戏手柄是机器人最直观的控制方式,尤其在调试、遥操作和紧急接管场景中不可或缺。本节介绍常用的遥控方案及其与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)