
这次来把一个很实际的问题解决掉ROS2 小车底盘驱动写好了总不能每次都在终端里ros2 topic pub手搓速度指令也不适合一直敲键盘控制。最顺手的方式是拿一个游戏手柄直接遥控小车前进、后退、转向。本篇是 ROS2 系列教程的第二十三篇处理的就是遥控手柄控制小车运动这条完整链路。这条链路不复杂关键节点有三个手柄驱动节点joy_node、速度映射节点teleop_twist_joy、底盘统一速度话题/cmd_vel。传感器数据从手柄按键和摇杆产生先转换成 ROS2 的sensor_msgs/msg/Joy消息再经过映射节点转换成geometry_msgs/msg/Twist速度指令最后发给差速底盘驱动、Gazebo 仿真器或者 Nav2 导航切换器。理解这条数据流之后不管是 USB 手柄还是蓝牙手柄不管是自研小车还是仿真模型都能用同样的思路接入。硬件门槛很低一台装了 Ubuntu 22.04 和 ROS2 Humble 的电脑一个 USB 或蓝牙游戏手柄不需要独立显卡CPU 就能带得动。遥控手柄控制本身不涉及 AI 推理不吃显存真正需要关注的反而是一些容易忽略的细节手柄设备权限、轴号和键号映射、使能按键、死区、速度缩放、话题 QoS 匹配。这篇文章会带读者完成以下内容检查手柄设备、启动joy节点、查看 Joy 消息格式、配置teleop_twist_joy参数、验证/cmd_vel输出、接入 Gazebo 仿真或自研底盘驱动、用命名空间扩展多机器人控制最后整理一份常见问题排查清单。全程给可复制命令和配置示例读者照着做就能跑通。1. 核心能力速览本文要解决的问题属于 ROS2 应用层开发从功能形态上看更像一个“输入设备接入 话题映射”的工程任务不需要 GPU。核心能力可以从下面几个维度看能力项说明项目类型ROS2 机器人遥控控制方案技术栈ROS2、joy、teleop_twist_joy、/cmd_vel核心功能手柄按键和摇杆映射为机器人速度指令硬件门槛一个 USB/蓝牙游戏手柄一台运行 ROS2 的电脑GPU/显存需求无特殊需求CPU 计算即可推荐系统Ubuntu 22.04 ROS2 Humble其他新版本类似启动方式命令行多终端启动或 launch 文件一键启动核心通信话题/joy、/cmd_vel速度消息类型sensor_msgs/msg/Joy、geometry_msgs/msg/Twist批量/多机扩展可通过命名空间和话题重映射控制多台机器人适合场景小车遥控调试、遥控建图、手动接管 Nav2、仿真验证整条数据流可以整理成下面这样手柄硬件 - Linux 输入子系统/dev/input/jsX - joy_node发布 /joy消息类型 sensor_msgs/msg/Joy - teleop_twist_joy读取轴值/键值做映射 - /cmd_vel消息类型 geometry_msgs/msg/Twist - 差速底盘驱动 / Gazebo 仿真 / 麦克纳姆轮控制器需要注意teleop_twist_joy做的事情并不是 ROS2 内部的“黑魔法”它本质上是订阅/joy话题从axes数组取摇杆值、从buttons数组取按键值然后计算Twist。所以即使不装这个包自己写一个 Python 订阅节点也能做到同样效果。用teleop_twist_joy的目的是省去重复造轮子并且它已经处理了死区、速度缩放、使能按键、涡轮加速这些常见逻辑。2. 适用场景与使用边界遥控手柄控制小车运动适合下面这些场景底盘驱动调试阶段验证电机正反转、PID 参数整定、轮速反馈是否正常。手动建图用 Cartographer 或 SLAM Toolbox 建图时手柄控制比键盘控制更细腻转向速度过渡更平滑。Nav2 手动接管导航异常或需要人工干预时从自动模式切换到手动遥控。仿真验证在 Gazebo、ArduPilot/ROS2 仿真或者 AirSim 场景中先用虚拟小车跑通手柄映射再上真车。算法对比测试同一台小车用不同速度配置跑同一段轨迹观察底盘响应。使用边界也要说清楚手柄遥控只是“输入映射”不包含路径规划、避障、限速保护。真正的安全逻辑必须写在底盘驱动层。不要在人群密集、无人监管的开放空间直接跑大功率底盘建议先在仿真中验证映射关系。如果手柄带有急停按键或使能按键一定要把该按键映射成速度输出开关不要省略。涉及人脸、声音、地图原始数据等采集任务时注意数据来源合法性和隐私边界。版权和授权问题同样适用于机器人场景使用特定品牌手柄的按键图标、协议库时注意授权范围采集地图数据、图像数据用于发布时要确保不侵犯第三方权益。一句话总结手柄遥控适合做“测试工具”和“人工接管工具”不适合做无人值守的“自动驾驶输入源”。3. 环境准备与前置条件3.1 操作系统与 ROS2 版本推荐组合是 Ubuntu 22.04 ROS2 Humble。这个组合在社区资料最多排查问题最容易。Ubuntu 24.04 Jazzy 或者其他发行版的思路完全一致只是 apt 包名中的发行版代号需要替换。如果还没有安装 ROS2需要先完成基础安装。以 Humble 为例安装桌面版sudo apt update sudo apt install ros-humble-desktop -y这里有一个常见问题如果执行sudo apt install ros-humble-desktop提示E: Unable to locate package ros-humble-desktop大概率是 ROS2 apt 源没有正确添加。需要先添加 ROS2 官方 apt 源再执行sudo apt update之后才能搜索到ros-humble-*系列包。也可以在安装时使用社区的一键安装脚本但建议先理解官方步骤出问题时知道从哪里排查。3.2 安装 joy 与 teleop_twist_joyjoy是 ROS2 官方驱动包负责读取/dev/input/jsX设备并发布/joy话题。teleop_twist_joy负责把/joy映射成/cmd_vel。sudo apt install ros-humble-joy ros-humble-teleop-twist-joy -y安装完成后可以用下面的命令确认包是否可用ros2 pkg list | grep joy正常会看到joy和teleop_twist_joy两个包。3.3 手柄硬件准备建议准备一个有摇杆、有多个肩部按键的手柄。Xbox 手柄、PS 手柄、北通等国产手柄都能用关键在于 Linux 内核是否能识别为标准的输入设备。连接方式有两种USB 有线连接最简单即插即用。蓝牙连接需要先让系统和手柄配对一般通过bluetoothctl或系统蓝牙设置完成。连接完成后检查设备节点是否出现ls /dev/input/js*常见输出为/dev/input/js0。如果目录为空说明手柄没有被 Linux 输入子系统识别。为了查看按键数据可以安装joystick工具sudo apt install joystick -y jstest /dev/input/js0运行jstest后拨动摇杆、按按键终端会有实时数据反馈。这一步很关键它能帮读者确定手柄的哪个摇杆对应哪个 axis哪个按键对应哪个 button。3.4 设备权限问题如果jstest提示权限不足需要把当前用户加入input组sudo usermod -a -G input $USER然后注销重新登录或者重启一次系统让组权限生效。3.5 WSL2 注意事项如果读者使用的是 WSL2 Ubuntu/dev/input/jsX默认并不会出现。WSL2 需要把 USB 设备透传进 Linux 环境常用工具是usbipd-win。如果不想折腾设备透传建议直接在物理机 Linux 上测试或者先在 Gazebo 仿真环境里验证手柄映射逻辑再放到真实小车。4. 安装部署与启动方式4.1 启动 joy_node先启动joy节点ros2 run joy joy_node启动后可以查看话题列表确认/joy是否出现ros2 topic list正常会看到/joy话题。再开一个终端查看 Joy 消息内容ros2 topic echo /joy此时操作手柄终端会打印类似下面的数据header: stamp: sec: 1234 nanosec: 567890123 frame_id: joy axes: - 0.0 - 1.0 - 0.0 ... buttons: - 0 - 1 - 0 ...其中axes数组按顺序对应摇杆的各个轴向buttons数组对应各个按键。拨动左摇杆、按 LB/RB 肩键观察对应索引的变化把结果记录下来。这份“键位表”后面配置teleop_twist_joy时要反复用到。4.2 启动 teleop_twist_joy再开一个终端ros2 run teleop_twist_joy teleop_node此时如果直接拨动摇杆/cmd_vel大概率没有输出因为teleop_twist_joy默认需要按住使能按键才会输出速度。用下面的命令查看ros2 topic echo /cmd_vel按住使能按键的同时拨动摇杆终端才会出现Twist数据。常见的默认配置中使能按键是5左摇杆 Y 轴是axis_linear左摇杆 X 轴是axis_angular。但不同手柄键位差异很大所以更好的做法是显式指定参数。4.3 用 launch 文件一键启动每次开两个终端太麻烦可以把两个节点封装到 launch 文件里。在 ROS2 工作空间新建功能包ros2 pkg create joy_teleop_demo --build-type ament_python --destination-directory src进入功能包目录创建launch文件夹写一个 Python launch 文件from launch import LaunchDescription from launch_ros.actions import Node def generate_launch_description(): return LaunchDescription([ Node( packagejoy, executablejoy_node, namejoy_node, parameters[{ dev: /dev/input/js0, deadzone: 0.05, }], outputscreen ), Node( packageteleop_twist_joy, executableteleop_node, nameteleop_node, parameters[{ axis_linear: 1, axis_angular: 0, enable_button: 5, scale_linear: 0.5, scale_angular: 1.0, }], outputscreen ), ])然后在setup.py中添加 launch 文件安装配置或者直接把 launch 文件放到share目录下。编译后运行colcon build --packages-select joy_teleop_demo source install/setup.bash ros2 launch joy_teleop_demo joy_teleop.launch.py注意上面的参数只是常见配置真正使用时要根据自己手柄的键位表调整axis_linear、axis_angular、enable_button。4.4 参数说明teleop_twist_joy中几个关键参数参数作用常见默认值axis_linear线速度对应的摇杆轴索引1axis_angular角速度对应的摇杆轴索引0enable_button输出使能按键索引5enable_turbo_button涡轮加速按键索引4scale_linear线速度缩放系数0.5scale_angular角速度缩放系数1.0scale_linear_turbo涡轮模式下线速度缩放1.0scale_angular_turbo涡轮模式下的角速度缩放1.5不同版本默认值可能有差异建议在启动后打印参数确认ros2 param get /teleop_node axis_linear5. 功能测试与效果验证5.1 测试一手柄设备识别测试目的确认手柄被 Linux 正确识别。操作步骤ls /dev/input/js* jstest /dev/input/js0预期结果jstest界面能显示摇杆和按键数值变化。判断标准拨动左摇杆时Axes 数值在 -32767 到 32767 之间变化按下按键时 Buttons 数值变为 1。失败原因设备节点不存在、权限不足、手柄没连接好。5.2 测试二Joy 话题数据接收测试目的确认joy_node能把底层输入转换为 ROS2 话题。操作步骤ros2 run joy joy_node ros2 topic echo /joy预期结果操作手柄时/joy话题实时打印axes和buttons。判断标准数据更新有实时性摇杆回正后数值归零。失败原因joy_node没有权限读取设备或者设备节点路径不是/dev/input/js0。5.3 测试三轴映射与按键映射测试目的确定手柄每个轴向和按键的真实编号。操作步骤在ros2 topic echo /joy状态下把左摇杆向上推观察axes哪个索引变化。把左摇杆向左推观察axes哪个索引变化。按住 LB 肩键观察buttons哪个索引变为 1。记录一份映射表形如左摇杆上下: axes[1] 左摇杆左右: axes[0] LB 按键: buttons[4] RB 按键: buttons[5]判断标准映射记录清晰配置参数时能直接对应。5.4 测试四cmd_vel 输出验证测试目的确认teleop_twist_joy能输出有效的速度指令。操作步骤ros2 run teleop_twist_joy teleop_node ros2 topic echo /cmd_vel按住使能按键同时推动左摇杆。预期结果/cmd_vel中linear.x随摇杆前后变化angular.z随摇杆左右变化。判断标准松开使能按键后/cmd_vel不再更新摇杆回正后输出速度为零。失败原因使能按键没按住。axis_linear/axis_angular配置错误。手柄摇杆有漂移死区设置太小。5.5 测试五接入 Gazebo 仿真如果读者已经有一个 ROS2 小车仿真环境最简单的验证方式是直接让仿真底盘订阅/cmd_vel。操作步骤# 终端1启动仿真世界 ros2 launch my_robot_gazebo robot.launch.py # 终端2启动手柄控制 ros2 launch joy_teleop_demo joy_teleop.launch.py # 终端3观察话题 ros2 topic info /cmd_vel -v预期结果仿真小车在按住使能按键后开始运动。判断标准仿真环境中机器人位置发生变化速度变化符合摇杆输入方向。失败原因仿真底盘节点没有订阅/cmd_vel。话题名称不一致比如底盘订阅的是/mobile_base/cmd_vel需要重映射。仿真中的底盘驱动没有启动。5.6 测试六接入自研差速底盘如果读者是自己写底盘驱动直接订阅/cmd_vel并把速度转换为左右轮速即可。下面是一个最小 Python 订阅节点示例import rclpy from rclpy.node import Node from geometry_msgs.msg import Twist class SimpleChassisDriver(Node): def __init__(self): super().__init__(simple_chassis_driver) self.subscription self.create_subscription( Twist, cmd_vel, self.cmd_vel_callback, 10 ) def cmd_vel_callback(self, msg): linear msg.linear.x angular msg.angular.z # 差速模型v (v_right v_left) / 2 # omega (v_right - v_left) / wheel_base # 这里只是示例实际要结合轮径、轮距和电机控制周期 self.get_logger().info( flinear.x{linear:.3f}, angular.z{angular:.3f} ) def main(argsNone): rclpy.init(argsargs) node SimpleChassisDriver() rclpy.spin(node) node.destroy_node() rclpy.shutdown() if __name__ __main__: main()把这个节点编译运行后手柄推动摇杆终端会持续打印线速度和角速度。之后再根据底盘运动学把Twist转成左右轮速指令发给电机驱动板。6. 话题接口与多机器人批量控制在 ROS2 里/cmd_vel就是底盘的标准速度接口。手柄遥控的最终目的就是往这个接口写入速度。反过来也可以用命令行工具模拟手柄输入这在调试底盘驱动时非常好用。6.1 用 ros2 topic pub 模拟手柄输入如果不插手柄也能手动发布速度ros2 topic pub /cmd_vel geometry_msgs/msg/Twist {linear: {x: 0.2}, angular: {z: 0.0}}这种方式适合做纯底盘测试但缺点是一次性发布速度不会自动归零。如果要持续发布带频率的速度指令可以加上--rate参数ros2 topic pub --rate 10 /cmd_vel geometry_msgs/msg/Twist {linear: {x: 0.2}, angular: {z: 0.0}}这种模拟输入的方法非常适合验证底盘驱动节点是否正常工作把手柄问题从底盘中剥离出来。6.2 自定义 Python 接口节点除了直接用teleop_twist_joy也可以在自定义节点里订阅/joy并发布/cmd_vel实现更复杂的映射逻辑。比如按键切换运动模式、摇杆映射为非线性速度曲线、按时间记录手柄轨迹等。import rclpy from rclpy.node import Node from sensor_msgs.msg import Joy from geometry_msgs.msg import Twist class CustomJoyMapper(Node): def __init__(self): super().__init__(custom_joy_mapper) self.joy_sub self.create_subscription( Joy, joy, self.joy_callback, 10 ) self.cmd_pub self.create_publisher( Twist, cmd_vel, 10 ) def joy_callback(self, msg): twist Twist() twist.linear.x msg.axes[1] * 0.3 twist.angular.z msg.axes[0] * 1.0 # 可根据 buttons 实现急停、慢速模式、运动模式切换 self.cmd_pub.publish(twist) def main(argsNone): rclpy.init(argsargs) node CustomJoyMapper() rclpy.spin(node) node.destroy_node() rclpy.shutdown() if __name__ __main__: main()这段代码把左摇杆上下映射为线速度左右映射为角速度速度缩放因子固定为 0.3 和 1.0。注意如果直接替换teleop_twist_joy一定要自己处理死区和使能按键否则容易误触。6.3 多机器人命名空间控制如果同时控制多台机器人的底盘可以用命名空间区分话题。假设有两台机器人robot1和robot2手柄 A 控制robot1手柄 B 控制robot2ros2 run joy joy_node --ros-args -r __ns:/robot1 -r joy:joy ros2 run teleop_twist_joy teleop_node --ros-args -r __ns:/robot1 -r joy:joy -r cmd_vel:cmd_vel这样/robot1/joy和/robot1/cmd_vel就与/robot2命名空间隔离开。每个底盘驱动只订阅自己命名空间下的cmd_vel。如果只有一个手柄但想切换控制不同机器人可以做一个“目标选择”话题按下某个按键把手柄输入转发到不同命名空间的cmd_vel。这种架构适合编队调试场景。6.4 与 Nav2 的切换问题实际建图或者导航时手柄遥控和 Nav2 自动导航会争抢/cmd_vel。常见方案有三种用生命周期节点管理底盘驱动按需启停。用话题合并节点根据一个mode变量选择数据源。用不同的cmd_vel子话题内部做仲裁。最简单的做法是启动一个“模式切换”节点按键 A 切换手柄控制按键 B 切换 Nav2 控制二者通过一个std_msgs/msg/Bool话题切换发布源。这样能避免两个节点同时向/cmd_vel发布速度导致导航冲突。7. 资源占用与性能观察遥控手柄控制不是重计算任务CPU 占用非常低但这不代表不需要观察性能。遥控手柄控制小车运动的性能和稳定性主要体现在话题频率、延迟和资源消耗三个方面。7.1 观察话题发布频率用ros2 topic hz查看话题频率ros2 topic hz /joy ros2 topic hz /cmd_vel/joy的话题频率通常和设备驱动与系统配置有关常见在 30Hz 到 100Hz 之间。/cmd_vel的输出频率取决于teleop_twist_joy节点的发布频率设置。频率过低会导致控制不跟手频率过高会增加 CPU 占用。如果发现/cmd_vel频率波动很大优先检查系统负载、USB 中断频率和手柄蓝牙信号强度。蓝牙手柄在信号差的时候/joy话题会出现明显卡顿进而导致速度指令断续。7.2 CPU 占用观察用htop观察节点 CPU 占用sudo apt install htop -y htop正常情况joy_node和teleop_node的 CPU 占用率都应该是个位数。如果 CPU 占用异常高优先检查是否有多个节点重复启动、回调循环是否存在死循环、日志输出是否过于频繁。7.3 QoS 匹配检查ROS2 中sensor_msgs/msg/Joy默认使用传感器数据 QoS而geometry_msgs/msg/Twist在不同发布者中 QoS 策略可能有差异。自定义订阅节点如果出现“话题看得到但收不到数据”很可能是 QoS 不匹配。常用的排查方式ros2 topic info /cmd_vel -v这条命令会打印发布者和订阅者的 QoS 配置。如果发布端是Reliable订阅端是BestEffort在部分组合下无法通信需要显式指定 QoS 策略。7.4 延迟控制手柄遥控对延迟比较敏感。延迟主要来自三个方面手柄蓝牙传输延迟。系统 USB 轮询间隔。ROS2 话题传输和回调处理延迟。如果发现手柄操作“发飘”先检查物理链路是否稳定再看系统负载是否过高。把不必要的可视化节点如 RViz2 实时高频率刷新关掉一部分能明显降低端到端延迟。7.5 降低资源占用的方法降低teleop_twist_joy的发布频率。关闭调试日志输出或把日志级别调整为 WARN。不要在回调里做复杂计算。不要在同一个终端叠加太多ros2 topic echo否则输出本身会占用 CPU。8. 常见问题与排查方法问题现象可能原因排查方式解决方案找不到/dev/input/js0手柄没连接、驱动未加载、WSL2 未透传设备lsusb检查 USB 设备dmesg查看内核日志重新插拔手柄、安装驱动、配置 USB 透传jstest提示权限不足当前用户不在input组id查看用户组sudo usermod -a -G input $USER后重新登录joy_node启动报错设备节点路径错误ros2 run joy joy_node --ros-args -p dev:/dev/input/js1修改设备路径参数/joy话题有数据但/cmd_vel没有未按住使能按键查看teleop_twist_joy参数中enable_button的值按住对应按键或修改配置按下按键和摇杆方向错乱轴映射或按键映射配置错误用ros2 topic echo /joy记录真实编号修改axis_linear、axis_angular、enable_button手柄摇杆回正后小车还在缓慢运动摇杆漂移、死区太小查看axes是否为非零值调大joy_node的deadzone参数配置为 0.05 或更高/cmd_vel有数据但底盘不动底盘驱动没有订阅同名话题、QoS 不匹配ros2 topic info /cmd_vel -v查看发布订阅关系重映射话题名、统一 QoS 策略蓝牙手柄偶尔丢数据蓝牙信号干扰、电池电量低观察/joy频率波动靠近接收器、更换电池、改用有线连接多个节点同时发布/cmd_vel导致小车乱跑缺少话题仲裁或模式切换ros2 topic info /cmd_vel -v查看发布者数量增加模式切换节点禁止多个发布者直连底盘安装ros-humble-desktop时提示找不到包ROS2 apt 源未添加或apt update未执行apt search ros-humble添加 ROS2 源后sudo apt update再安装排查的时候建议按下述顺序走先确认设备节点再确认joy话题再确认cmd_vel话题最后看底盘驱动。一层一层定位不要上来就改参数。9. 最佳实践与使用建议9.1 安全限速无论手柄操作多顺手都要在底盘驱动层做速度限幅。比如线速度最大 0.5 m/s角速度最大 1.0 rad/s防止误触摇杆导致小车高速冲出去。这个限幅建议写在底盘驱动代码里而不是依赖teleop_twist_joy的缩放系数。9.2 必须有急停逻辑手柄的使能按键相当于“速度输出开关”。更稳妥的设计是底盘驱动在连续 200ms 到 500ms 没有收到新的/cmd_vel消息时自动停车。这样即使手柄断连、节点崩溃底盘也不会一直保持最后的速度。self.last_cmd_time self.get_clock().now() def timer_callback(self): now self.get_clock().now() if (now - self.last_cmd_time).nanoseconds 500_000_000: self.publish_zero_velocity()这种超时停车逻辑在真实机器人上非常关键强烈建议加上。9.3 参数化配置手柄键位、速度缩放、死区这些参数不要硬编码在 launch 文件里建议写成 YAML 参数文件方便不同手柄切换。joy_node: ros__parameters: dev: /dev/input/js0 deadzone: 0.05 teleop_node: ros__parameters: axis_linear: 1 axis_angular: 0 enable_button: 5 scale_linear: 0.5 scale_angular: 1.0更换手柄时只需要改一个 YAML 文件不用改代码。9.4 目录与日志管理把代码、launch 文件、参数文件、手柄映射记录表分目录管理。尤其是手柄映射表建议写成 markdown 文件放在docs目录方便后续查找。9.5 仿真先行新配置首次运行时先接入 Gazebo 仿真验证不要直接上真车。仿真环境能发现映射错误、方向反了、速度过快等问题而且不会损坏硬件。9.6 批量任务与自动化脚本如果遥控实验需要重复若干次可以用 ROS2 的 bag 记录功能ros2 bag record /cmd_vel /joy录制完成后回放 bag 数据就能复现相同的手柄输入序列适合做对比测试。这种方式相当于把手柄操作变成了可重复执行的“批量任务”。10. 总结与下一步这篇教程把遥控手柄控制小车运动的完整链路拆开讲了硬件设备识别、joy节点、teleop_twist_joy映射、/cmd_vel输出、底盘驱动接入、多机器人命名空间、常见问题排查。整个过程不需要 GPU门槛低但要注意的细节不少最容易踩的坑就两个一个是手柄键位映射配置错误另一个是忘记按住使能按键。对于刚入门的读者最先要验证的功能是ros2 topic echo /joy能实时打印手柄数据然后按住使能按键看ros2 topic echo /cmd_vel是否有输出。这两步通了后面的底盘驱动接入基本不会有大问题。下一步可以扩展的方向很多把手柄遥控和 Nav2 自动导航做模式切换用遥控手柄配合 Cartographer 做手动建图把joy_node接入 AirSim 或 PX4 仿真环境测试无人机遥控或者用多手柄加命名空间实现多车编队控制。手柄遥控只是输入层的开始真正有价值的是它和建图、导航、底盘控制组成的完整机器人工作流。