很多入门 ROS2 的人,在把仿真小车跑起来之后,下一个动作往往是买一个游戏手柄。原因很简单:键盘遥控小车看起来不像机器人,用摇杆推着车走,才是大部分人想象中“遥控机器人”的样子。
但真把手柄插到电脑上,很多人会卡在第一关。系统里面明明能看到设备,ROS2 却读不到数据;或者 joy 节点能跑起来,但小车就是一动不动。如果再没有人告诉你按键和摇杆是怎么映射成速度的,你甚至会怀疑是不是手柄型号不兼容。
这篇文章就是来解决这个问题的。
我会以 Ubuntu 22.04 + ROS2 Humble 环境为例,从设备识别、消息理解、按键映射到统一 launch 启动,把“手柄 → 话题 → 小车底盘 → 电机”这条链路完整走通。读完你既能用现成的 teleop_twist_joy 快速上手,也能理解底层原理,以后换任意一个手柄,或者从差速车换成麦克纳姆轮车,都不至于无从下手。
1. 这篇文章真正要解决的问题
先说一下,遥控手柄控制小车这个需求,在 ROS2 里其实是一个非常典型的“中间层”任务。它不涉及复杂的 SLAM、导航、规划,也不涉及电机驱动里的 PID 控制,它只负责一件事:把人的操作意图,翻译成机器人能听懂的速度指令。
听起来很简单,但实际开发中很容易出问题,主要集中在三个环节:
- 设备识别环节:手柄插上之后,系统没有把它识别为
/dev/input/jsX类型的摇杆设备,导致 joy 节点读不到数据。 - 消息转换环节:手柄输出的是传感器消息
sensor_msgs/msg/Joy,它只是一堆按钮(buttons)和摇杆轴(axes)的数字数组。要把这个数组变成geometry_msgs/msg/Twist速度指令,需要做映射。 - 安全控制环节:没有设计“使能按键”和“速度缩放”,小车可能一上电就冲出去,或者摇杆回中后小车还继续往前跑。
如果只看表面,很多人会误以为“手柄控制小车”的核心工作是写一个把 Joy 转成 Twist 的节点,其实真正值得花时间的是后面两件事:理解消息结构,以及设计一套安全、易用的参数映射方案。
这篇文章会从零开始,用最小示例把整条链路跑通,然后给出一个可以直接放进自己功能包的 launch 方案。如果你正打算用手柄遥控仿真或实体小车,这篇文章应该能帮你省下大量的排错时间。
2. 遥控手柄控制小车的基本链路与核心概念
2.1 整条链路是什么
先建立整体认识。手柄控制小车的 ROS2 数据链路,从上到下是这样的:
手柄物理设备 ↓(USB / 蓝牙) /dev/input/js0(系统摇杆设备) ↓(joy 节点读取并发布) /joy 话题(消息类型 sensor_msgs/msg/Joy) ↓(teleop_twist_joy 节点执行映射) /cmd_vel 话题(消息类型 geometry_msgs/msg/Twist) ↓(底盘驱动节点订阅) 小车电机这条链路里,ROS2 官方已经帮我们写好了前两个环节,也就是读取手柄的joy节点,以及做映射的teleop_twist_joy节点。所以我们的工作重心是:把设备接好,把参数配对,把小车的底盘驱动和/cmd_vel对接起来。
如果你的机器人是仿真环境,比如 Gazebo、Webots,或者一些项目里的“小黄鸭机器人”仿真,链路是一样的,只是最后订阅/cmd_vel的是仿真模型,而不是实体电机。
2.2 两个核心消息类型
你可以不用记住一大串 ROS2 API,但两个消息类型必须理解清楚。
第一个是sensor_msgs/msg/Joy,它表示手柄的原始输入。它的结构可以简化为:
header:消息头,包含时间戳和坐标系。axes:一个float32[]数组,表示所有摇杆轴和扳机的位置,取值范围一般是-1.0到1.0。buttons:一个int32[]数组,表示所有按键的状态,0表示松开,1表示按下。
第二个是geometry_msgs/msg/Twist,它表示机器人的速度指令。对差速小车来说,最常用的是:
linear.x:前进和后退的线速度,单位 m/s。angular.z:左右旋转的角速度,单位 rad/s。
手柄控制的核心工作,就是把Joy消息里的摇杆数值和按键状态,翻译成Twist消息里的线速度和角速度。
2.3 涉及的核心节点
我们可以把链路中的角色分清楚:
| 节点 | 作用 | 输入 | 输出 |
|---|---|---|---|
| joy | 读取手柄驱动设备 | /dev/input/js0 | /joy话题 |
| teleop_twist_joy | 根据参数映射按键和摇杆 | 订阅/joy | 发布/cmd_vel |
| 底盘驱动节点 | 控制电机执行速度 | 订阅/cmd_vel | 电机 PWM 或速度环控制 |
这里有个新手容易糊涂的地方:joy 节点只负责把设备数据变成 ROS2 话题,它不会去理解哪个按键是前进。理解按键含义这件事,是在 teleop_twist_joy 节点或者你的自定义节点里做的。
2.4 为什么推荐用现成工具而不是自己写节点
如果你已经会写 ROS2 的订阅发布程序,你当然可以自己写一个从 Joy 转 Twist 的节点。不过在实际项目中,我更建议先基于teleop_twist_joy来做,原因是它解决了很多你一开始想不到的工程问题:
- 支持“使能按键”(enable button),必须按住指定按键,手柄操作才生效。这相当于一个安全锁。
- 支持“Turbo 按键”,按住 Turbo 时用一套更大的速度倍率,方便快速移动和精确控制切换。
- 可以把线性速度和角速度的缩放系数独立配置,不同机器人可以灵活调参。
- 有多个手柄的轴号和按键号映射参数,不需要改代码,改 yaml 即可。
这不代表自己写节点没有意义。如果你想深入理解原理,或者在特殊场景下需要非常规映射,自己写一个几行代码的小节点反而更轻量。我会在第 6 章给出一个完整的自定义节点示例,你可以把它当作原理对照。
3. 环境准备与前置条件
3.1 操作系统与 ROS2 版本
本文以 Ubuntu 22.04 + ROS2 Humble 为例。如果你使用的是其他发行版,比如 ROS2 Foxy、Galactic、Iron 等,操作逻辑完全一致,只是软件包名称中的发行版代号不同。
确认 ROS2 环境已经正确安装,并能够正常使用:
source /opt/ros/humble/setup.bash ros2 --version正常运行会输出类似:
ros2 2.x.x如果你的 ROS2 还没有安装,需要先完成安装。网络上常见的“鱼香ROS一键安装”脚本可以辅助安装,但安装完成后还是要自己确认环境变量和ros2命令可用。
3.2 手柄设备选择
理论上,任何能被 Linux 识别为标准摇杆设备的手柄都可以使用,比如 Xbox 手柄、PS 手柄、北通手柄,以及各种 USB 遥控器。
两个建议:
- 首选有线 USB 手柄。蓝牙手柄虽然方便,但在工控机上容易出现连接不稳定、延迟、权限冲突问题,新手排错成本高。
- 不需要追求昂贵设备。市面上常见的几十元 USB 手柄,只要能识别成
/dev/input/js0,就足够完成遥控小车的需求。
如果你用的是 PS2 遥控手柄加接收器、RC 遥控器的 PPM 信号板等特殊设备,驱动方式会不同,本文不展开。
3.3 基础的 ROS2 操作知识
阅读本文前,你至少应该知道怎么创建工作空间和功能包,怎么使用ros2 run、ros2 topic echo这些基础命令。如果这些还不熟,建议先跑一遍官方的 Talker-Listener 教程,或者跟着任何一篇 ROS2 入门教程把基本概念过一遍。
4. 安装功能包与手柄设备识别
4.1 安装 joy 和 teleop_twist_joy
我们需要的两个功能包分别是joy和teleop_twist_joy。在 Ubuntu 22.04 + ROS2 Humble 下,安装命令如下:
sudo apt install ros-humble-joy ros-humble-teleop-twist-joy如果你的发行版是 Foxy,对应命令是ros-foxy-joy ros-foxy-teleop-twist-joy,其他发行版同理。
安装完成后,可以用下面的命令确认可执行文件是否存在:
ros2 pkg executables joy ros2 pkg executables teleop_twist_joy注意teleop_twist_joy的可执行文件名,不同版本可能略有差异,以这里输出的结果为准。这在后面配置 launch 文件时需要用到。
4.2 检查手柄设备是否被系统识别
把 USB 手柄插到电脑上,然后查看系统摇杆设备:
ls -l /dev/input/js*如果看到类似下面的输出,说明系统已经识别到手柄:
crw-rw---- 1 root input 13, 0 5月 11 10:30 /dev/input/js0如果没有任何/dev/input/jsX设备,先别急着怀疑 ROS2,问题大概率出在系统层面。可以先检查内核是否识别了 USB 设备:
lsusb在输出中找到你的手柄厂商信息。如果lsusb中能看到设备,但/dev/input/jsX不存在,可能是手柄驱动没加载,或者设备被系统当成普通 HID 设备,而不是 joystick。这种情况下可以先换个 USB 口,或者换一台电脑交叉验证。
4.3 用 jstest 验证按键和摇杆
确认设备存在后,建议先用 Linux 自带的工具验证手柄本身是好的。安装joystick工具包:
sudo apt install joystick然后运行测试工具:
jstest /dev/input/js0在jstest的界面里,你会看到多个 Axis 通道,操作摇杆时对应的值会在-32767和32767之间变化;按下按键时对应的 Button 状态会变成on。如果你的手柄按键有反应,说明设备本身没有问题。
这一步非常重要,它可以帮你提前区分“手柄坏了”和“ROS2 配置有问题”。很多新手跳过了设备测试,直接从 ROS2 开始排查,结果绕了一大圈才发现是手柄本身接触不良。
4.4 设备权限问题
当你运行jstest时,如果提示permission denied,说明当前用户没有访问摇杆设备的权限。临时解决办法是用 sudo 运行,但更推荐的做法是把当前用户加入input组:
sudo usermod -aG input $USER重新登录终端后,执行groups命令确认input组已经生效。
如果要让手柄设备固定使用某个设备名,还可以配置 udev 规则,这个放到最佳实践章节详细讲。
5. 核心流程拆解:从手柄到 cmd_vel
在这一章,我们手动把链路跑通。先不写任何 launch 文件,直接用命令行一步步来,目的是让你清楚每一步在发生什么。
5.1 第一步:运行 joy 节点并查看原始消息
打开一个终端,启动 joy 节点:
ros2 run joy joy_node如果一切正常,这个节点会开始发布/joy话题。新开一个终端,查看话题内容:
ros2 topic echo /joy此时你操作摇杆或按下按键,终端会打印类似下面的内容:
header: stamp: sec: 1715402000 nanosec: 123456789 frame_id: joy axes: - 0.0 - -0.003921568859368563 - 0.0 - 1.0 - 0.0 - 0.0 buttons: - 0 - 0 - 1 - 0 - 0 - 0 - 0 - 0 ---这里axes数组里的每一个值,对应一个摇杆轴或扳机;buttons数组里的每一个值,对应一个按键。
这一步的核心目标只有一个:记录你的手柄上,控制前后的摇杆在第几个轴上,控制左右的摇杆在第几个轴上,以及你希望用作安全使能的按键在第几个按钮上。
不同的手柄,轴和按键的索引差异很大。比如 Xbox 手柄常见的布局是左摇杆 X 为axes[0]、左摇杆 Y 为axes[1],但在其他手柄上可能完全相反。不要凭记忆写参数,一定要以ros2 topic echo /joy的实际输出为准。
5.2 第二步:分析并记录按键映射
拿到/joy消息后,可以做一个简单的记录表。比如对于某款 USB 手柄,实际输出可能是:
| 操作 | 消息位置 | 动作 |
|---|---|---|
| 左摇杆左右 | axes[0] | 左右旋转(角速度) |
| 左摇杆前后 | axes[1] | 前进后退(线速度) |
| 右扳机 | buttons[5] | 使能按键 |
| 左扳机 | buttons[4] | Turbo 按键 |
请注意:这个表只是示例,你的设备几乎肯定会有所不同。实际项目中应该用手写或表格记录自己的映射结果,然后写进参数文件。
5.3 第三步:使用 teleop_twist_joy 做参数映射
现在,我们手动启动 teleop_twist_joy 节点,通过命令行参数传递映射配置。
假设你的手柄左摇杆 Y 在axes[1],左摇杆 X 在axes[0],使能按键在buttons[5],那么可以这样启动:
ros2 run teleop_twist_joy teleop_node \ --ros-args \ -p enable_button:=5 \ -p axis_linear:=1 \ -p axis_angular:=0 \ -p scale_linear:=0.5 \ -p scale_angular:=0.5这里需要解释几个参数的含义:
enable_button:使能按键。必须按住这个按键,节点才会发布速度指令;松开后,即使摇杆有输入,也会发布零速度。axis_linear:控制前进后退的摇杆轴索引。axis_angular:控制左右旋转的摇杆轴索引。scale_linear:线速度缩放系数。摇杆拉到最大时,linear.x等于这个值。scale_angular:角速度缩放系数。摇杆推到最大时,angular.z等于这个值。
启动后,如果你现在按住使能按键,并推动左摇杆,然后看一眼/cmd_vel话题:
ros2 topic echo /cmd_vel你应该能看到类似这样的输出:
linear: x: 0.5 y: 0.0 z: 0.0 angular: x: 0.0 y: 0.0 z: -0.08到这里,链路已经通了百分之八十。剩下的工作就是把这些参数固化成配置文件,并用 launch 文件统一启动。
5.4 第四步:确认底盘驱动订阅的是 /cmd_vel
如果你的小车是仿真环境,只需要在仿真中确认模型订阅了/cmd_vel。如果是实体小车,通常你在写底盘驱动节点时,已经设置好了订阅话题名。这里唯一要注意的是话题名一致性,很多小车的底盘驱动默认订阅的是cmd_vel,但也有一些是cmd_vel_stamped,需要仔细确认。
检查小车是否在接收指令,可以用:
ros2 topic info /cmd_vel输出中会显示 Publisher count 和 Subscription count。如果 Subscription count 为 0,说明你的底盘驱动根本没有订阅这个话题,速度指令自然没人执行。
6. 完整示例:参数文件、launch 文件与自定义节点
上一章的命令行方式适合理解原理和临时调试,但在真正的项目里,我们应该把配置整理成文件,用 launch 统一启动。这一章我会给出一个完整可用的示例结构。
6.1 功能包目录结构
建议在你自己的工作空间里新建一个操控功能包,比如叫bot_teleop。目录结构如下:
bot_teleop/ ├── config/ │ └── joy_teleop.yaml ├── launch/ │ └── teleop_joy.launch.py ├── bot_teleop/ │ ├── __init__.py │ └── simple_joy_teleop.py ├── package.xml └── setup.py如果你只是想快速测试,可以只在现有功能包里增加 config 和 launch 目录,不一定立刻新建功能包。新建功能包的方式这里不展开,前面几篇 ROS2 系列文章已经写过创建功能包的流程。
6.2 参数文件 config/joy_teleop.yaml
下面这个 YAML 文件,放在config/目录下,描述的是一个常见的双摇杆手柄映射:
teleop_twist_joy_node: ros__parameters: enable_button: 5 enable_turbo_button: 4 axis_linear: 1 axis_angular: 0 scale_linear: 0.5 scale_angular: 0.5 scale_linear_turbo: 1.0 scale_angular_turbo: 1.0这里有几个细节需要注意:
- 第一行
teleop_twist_joy_node是节点名,用于参数匹配。launch 文件中给节点起的名字必须和这里对应。 scale_linear建议初始值设小一点,比如 0.2 到 0.5,不要第一次测试就设置成 1.0。- 如果你的手柄没有 Turbo 按键,可以不配置
enable_turbo_button和对应的 turbo 缩放参数。
6.3 launch 文件 launch/teleop_joy.launch.py
这个 launch 文件同时启动 joy 节点和 teleop_twist_joy 节点:
import os from ament_index_python.packages import get_package_share_directory from launch import LaunchDescription from launch_ros.actions import Node def generate_launch_description(): pkg_share = get_package_share_directory('bot_teleop') joy_teleop_config = os.path.join(pkg_share, 'config', 'joy_teleop.yaml') return LaunchDescription([ Node( package='joy', executable='joy_node', name='joy_node', parameters=[{ 'dev': '/dev/input/js0', 'deadzone': 0.1, }] ), Node( package='teleop_twist_joy', executable='teleop_node', name='teleop_twist_joy_node', parameters=[joy_teleop_config] ), ])关键点解释:
dev参数指定读取哪个设备文件。如果你的电脑上只有一个手柄,通常/dev/input/js0就可以了。如果有多个手柄,必须确认设备名。deadzone是摇杆死区。这个参数非常实用,因为很多手柄回中后摇杆并不严格是 0,而是有一个微小偏移。不设置死区,小车会自己缓慢漂移。get_package_share_directory是从安装后的 share 目录读取配置。如果你不熟悉安装流程,直接使用os.path.join指定源码路径也是可以的,但规范做法是安装到 share 目录。
需要特别提醒:launch 文件里teleop_node这个可执行文件名,请先用ros2 pkg executables teleop_twist_joy确认。不同版本可能叫teleop_node或teleop_twist_joy_node,以实际为准。
如果你不想新建功能包,也可以直接在终端通过ros2 launch加载现有功能包的方式,但配置文件路径会麻烦一些。更简单的临时启动方式,是在终端里用参数直接启动上一章的节点。
6.4 自定义简单节点:掌握原理
如果你不想依赖 teleop_twist_joy,或者需要在映射逻辑里加入自己的处理,可以写一个这样的小节点。文件路径为bot_teleop/bot_teleop/simple_joy_teleop.py:
import rclpy from rclpy.node import Node from sensor_msgs.msg import Joy from geometry_msgs.msg import Twist class SimpleJoyTeleop(Node): def __init__(self): super().__init__('simple_joy_teleop') self.publisher = self.create_publisher(Twist, 'cmd_vel', 10) self.subscription = self.create_subscription( Joy, 'joy', self.joy_callback, 10 ) self.speed = 0.3 self.turn = 0.5 def joy_callback(self, msg: Joy): twist = Twist() deadzone = 0.1 if len(msg.axes) > 1: linear = msg.axes[1] else: linear = 0.0 if len(msg.axes) > 0: angular = msg.axes[0] else: angular = 0.0 if abs(linear) < deadzone: linear = 0.0 if abs(angular) < deadzone: angular = 0.0 enabled = len(msg.buttons) > 5 and msg.buttons[5] == 1 if enabled: twist.linear.x = linear * self.speed twist.angular.z = angular * self.turn else: twist.linear.x = 0.0 twist.angular.z = 0.0 self.publisher.publish(twist) def main(args=None): rclpy.init(args=args) node = SimpleJoyTeleop() rclpy.spin(node) node.destroy_node() rclpy.shutdown() if __name__ == '__main__': main()这个自定义节点的逻辑和 teleop_twist_joy 的核心思想是一致的:
- 判断轴和按钮是否存在,避免越界。
- 对摇杆做死区判断,回中时输出零。
- 只有使能按键被按下时,才输出速度,否则输出零。
运行方式:
ros2 run bot_teleop simple_joy_teleop如果你不想新建功能包,也可以把上面的代码放到~/ros2_ws/src/bot_teleop/bot_teleop/下,并在setup.py中注册入口点。这一步操作细节较多,建议对照你项目中已有的 Python 节点配置方式。
7. 运行结果与效果验证
7.1 启动 launch 文件
一切配置完成后,用 launch 统一启动:
ros2 launch bot_teleop teleop_joy.launch.py启动成功后,终端不会报错,两个节点会同时运行。
7.2 使用 topic 工具验证话题
新开一个终端,查看话题和节点是否正常:
ros2 node list ros2 topic list正常时应能看到joy_node、teleop_twist_joy_node等节点,以及/joy、/cmd_vel等话题。
接着监听/cmd_vel:
ros2 topic echo /cmd_vel按住使能按键,推动摇杆,应该能看到话题里有数据。松开使能按键,数据会立即变成零。
7.3 用 rqt_graph 查看链路
如果安装了 rqt 工具,可以运行:
ros2 run rqt_graph rqt_graph在图形界面里,你应该能看到joy节点发布/joy,teleop_twist_joy节点订阅/joy并发布/cmd_vel,然后底盘驱动节点订阅/cmd_vel。这个图是用来判断链路是否完整的最直观方式。
7.4 实体小车的安全验证
如果你是在实体小车上测试,请务必先把小车架起来,让驱动轮悬空:
- 先不给电机上电,或者断开电机电源,验证
/cmd_vel输出是否正确。 - 再给电机上电,但把
scale_linear调小,比如 0.2。 - 准备好急停开关,或者随时准备关闭节点。
- 先按住使能按键,缓慢推动摇杆,确认轮子转动方向和你预期一致。
8. 常见问题与排查思路
手柄控制小车这个任务,说简单也简单,说麻烦也麻烦,难点几乎都集中在环境问题和映射参数上。下面是我认为“命中率”最高的一些排查点:
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
/dev/input/jsX不存在 | 手柄没被系统识别 / USB 接触不良 | 执行lsusb和 `dmesg | tail` |
jstest报 permission denied | 当前用户没有权限访问设备 | 执行groups查看用户组 | 将用户加入input组后重新登录 |
| joy 节点启动失败 | 设备名配置错误 | 执行ros2 run joy joy_node --ros-args -p dev:=/dev/input/js0 | 在启动时通过参数指定正确设备 |
/joy话题没有任何输出 | 手柄按键没有触发 / 节点没启动 | 执行ros2 topic list确认话题 | 确认 joy 节点在运行,检查设备线缆 |
/joy有输出但/cmd_vel无输出 | 没有按住使能按键 / 参数映射错误 | 执行ros2 topic echo /cmd_vel | 确认 enable_button 索引正确,按住按键 |
| 放手后小车还继续跑 | 摇杆没有回中 / 没有死区设置 | 查看/joy中 axes 回中是否接近 0 | 设置 deadzone 参数,让节点正确处理死区 |
/cmd_vel有输出但小车不动 | 底盘驱动没有订阅 cmd_vel | 执行ros2 topic info /cmd_vel | 检查底盘驱动代码,统一话题名 |
| launch 提示 config 目录不存在 | 配置文件没有安装到 share 目录 | 检查安装流程和目录结构 | 使用colcon build后重新 source,或改用绝对路径 |
| 按键映射错乱 | 不同手柄轴号不同 | 操作摇杆时对比/joy输出 | 重新记录轴号和按键号,修改参数文件 |
这里再重点说一个问题:很多时候你发现/cmd_vel没输出,不是链路断了,而是 enable_button 没按对。ROS2 的 teleop_twist_joy 节点默认行为是必须按住使能按键才能输出速度,这是安全设计。很多人第一次测试时没有按住使能按键,就以为参数配置错了,这是非常常见的误判。
9. 最佳实践与工程建议
9.1 安全优先:使能按键、速度上限和急停
遥控小车这个任务,安全再怎么强调都不过分。建议至少做到以下三点:
- 强制要求使能按键。无论你用 teleop_twist_joy 还是自定义节点,都要有一个使能按键。它的本质是“人在回路”,只有人持续按住,车才可能动。
- 初始速度设小。第一次测试时,线速度缩放系数最好不要超过
0.3m/s。不要觉得自己控制得住,实体小车冲出去的速度远超你的反应速度。 - 实体测试准备急停。急停可以是硬件开关,也可以是“松开按键即停”的软件机制。无论如何,要有一个能立刻切断速度输出的方式。
9.2 固定设备名:使用 udev 规则
如果你的工控机上插了多个手柄,或者设备有时会被识别成js0、有时js1,建议写一个 udev 规则,根据手柄的idVendor和idProduct固定设备名。
先查询设备信息:
udevadm info -a -n /dev/input/js0然后创建规则文件,例如/etc/udev/rules.d/99-joystick.rules,内容示意如下:
SUBSYSTEM=="input", ATTRS{idVendor}=="1234", ATTRS{idProduct}=="5678", MODE="0666", SYMLINK+="my_joystick"写完规则后执行:
sudo udevadm control --reload-rules sudo udevadm trigger这样你的手柄会固定对应/dev/input/my_joystick,在 launch 文件里直接指定这个路径,就不会因为设备名变化导致启动失败。
9.3 摇杆死区与滤波
很多手柄回中后,摇杆的数值并不是严格的0.0,而是会在0.02、-0.01这类小值附近跳动。如果不处理,小车会表现为“静止时偶尔抖一下”,严重的会缓慢漂移。
joy_node自带deadzone参数,可以设置全局死区。在你自己的自定义节点里,也要做类似处理。比较大的摇杆建议死区设到0.1左右,小摇杆或者质量好的手柄可以适当减小。
9.4 善用 Turbo 按键
teleop_twist_joy 支持 Turbo 按键,也就是按住 Turbo 时使用另一套更大的速度倍率。经典用法是:
- 正常模式下
scale_linear: 0.3,用于精确靠近障碍物或对接。 - Turbo 模式下
scale_linear_turbo: 1.0,用于快速移动和远距离转移。
这个设计在实际项目中比“把速度调来调去”要优雅得多。
9.5 先仿真,后实车
无论你有多想直接看实体车跑起来,我都建议先跑一遍 Gazebo 仿真或至少把车架起来空转。在仿真里,你可以放心测试各个方向的映射是否正确,不会撞坏东西,也不会伤到人。
尤其是从键盘控制切换到遥控手柄控制时,轴的方向、正负号很容易搞反。这些问题在仿真里暴露的成本,远低于在实体车上暴露的成本。
9.6 日志与可视化
调试阶段要养成使用可视化工具的习惯:
rqt_graph可以快速判断话题链路是否完整。rqt_plot或者 PlotJuggler 可以绘制cmd_vel中的linear.x和angular.z变化曲线,对判断遥控手感非常直观。
这些工具不是必须的,但在复杂项目中能大幅减少排错时间。
10. 总结与后续学习方向
遥控手柄控制小车,看起来只是一个 ROS2 的小应用,但它串起了一个很重要的思维模型:机器人系统里的控制指令,本质上都是消息的流动和转换。手柄产生 Joy 消息,映射节点把它变成 Twist 消息,底盘节点再把它变成电机的转动力矩。
本文讲清楚了几个关键点:
- 手柄控制小车的完整链路:设备、话题、节点。
sensor_msgs/msg/Joy和geometry_msgs/msg/Twist两个核心消息。- joy 节点和 teleop_twist_joy 节点的作用与参数含义。
- 从命令行到 launch 文件的标准化配置流程。
- 一个自定义节点的完整代码示例。
- 从设备权限到安全设计的大量排错经验。
你可以先在自己现有的小车上把这一套跑通,再看看下面的可选方向:
- 手动接管导航:在 Nav2 导航过程中,如果机器人偏离路线或需要人工干预,可以用手柄临时接管
/cmd_vel,这正是遥控手柄在真实项目中的典型用途。 - 控制机械臂:手柄控制机械臂的末端移动或关节运动,逻辑和遥控小车类似,只是把 Twist 换成了 JointState 或特定控制消息。
- 麦克纳姆轮车:麦克纳姆轮底盘的
/cmd_vel消息里会增加linear.y的横移分量,映射逻辑会更丰富。 - 多机遥控:通过 ROS2 的多机通信配置,在一台电脑上遥控另一台机器人的小车,链路保持不变,只是话题变成了跨机传输。
建议你现在打开终端,把手柄插上,先运行ros2 run joy joy_node看看/joy消息,然后对照本文一步步把/cmd_vel打通。整个流程跑通一次后,你对方才讲的所有概念都会有更深的体感。遇到问题也不用急,回到第 8 章的排查表格,大多数问题都能在那里找到方向。