第一次接触ROS手柄控制时,最让人头疼的就是硬件连接问题。我测试过市面上常见的几款手柄,包括Xbox 360、北通阿修罗和小米蓝牙手柄,发现虽然连接方式不同,但在ROS中的处理逻辑基本一致。有线手柄直接插入USB接口就能识别,无线手柄则需要通过蓝牙配对。记得有次用北通手柄时,蓝牙连接死活不成功,后来发现是手柄需要先长按模式键进入配对状态,这个小细节卡了我整整一下午。
连接成功后,在终端输入以下命令检查设备:
bash复制ls /dev/input/
正常情况下会看到js0或js1这样的设备文件。为了验证手柄是否正常工作,可以安装jstest-gtk工具:
bash复制sudo apt-get install jstest-gtk
jstest /dev/input/js0
这时候拨动摇杆或按下按键,会看到数值实时变化。不同手柄的按键映射差异很大,比如Xbox的LT/RT键在某些手柄上是轴输入,在另一些手柄上却是按钮输入。建议新建一个mapping.yaml文件记录这些映射关系,后面写控制代码时会省事很多。
joy_node是ROS官方提供的手柄驱动节点,它会把原始手柄数据转换成标准化的ROS消息。启动方法很简单:
bash复制rosrun joy joy_node
但实际使用时有几个关键参数需要注意:
通过rostopic查看/joy话题消息:
bash复制rostopic echo /joy
你会看到axes数组对应各摇杆的偏移量(-1到1),buttons数组则是按键状态(0或1)。有个坑要注意:某些手柄的摇杆初始位置不在0点,这时需要手动校准或者在代码里做偏移补偿。
turtlesim是ROS的经典示例,我们先分析它的控制机制。启动小乌龟仿真:
bash复制rosrun turtlesim turtlesim_node
rosrun turtlesim turtle_teleop_key
用rqt_graph查看节点关系,会发现teleop_turtle节点向/turtle1/cmd_vel发布geometry_msgs/Twist消息。这个消息类型包含线速度和角速度:
cpp复制geometry_msgs::Vector3 linear # x/y/z方向线速度(m/s)
geometry_msgs::Vector3 angular # x/y/z方向角速度(rad/s)
对小乌龟来说,只需要设置linear.x(前进速度)和angular.z(转向速度)。我建议先用rostopic手动发布消息感受下:
bash复制rostopic pub /turtle1/cmd_vel geometry_msgs/Twist "linear:
x: 0.5
y: 0.0
z: 0.0
angular:
x: 0.0
y: 0.0
z: 0.3"
结合前两节内容,我们可以编写手柄控制节点。核心思路是订阅/joy话题,转换数据后发布到cmd_vel。先创建功能包:
bash复制catkin_create_pkg teleop roscpp sensor_msgs geometry_msgs joy
关键代码逻辑如下:
cpp复制// 初始化发布者和订阅者
pub_ = nh_.advertise<geometry_msgs::Twist>("/turtle1/cmd_vel", 1);
sub_ = nh_.subscribe<sensor_msgs::Joy>("joy", 10, &TeleopTurtle::joyCallback, this);
// 回调函数处理逻辑
void joyCallback(const sensor_msgs::Joy::ConstPtr& joy)
{
geometry_msgs::Twist twist;
twist.linear.x = scale_linear_ * joy->axes[axis_linear_];
twist.angular.z = scale_angular_ * joy->axes[axis_angular_];
pub_.publish(twist);
}
完整代码需要处理几个实际问题:
一个好的launch文件能让调试效率翻倍。建议采用如下结构:
xml复制<launch>
<node pkg="turtlesim" type="turtlesim_node" name="turtle"/>
<node pkg="joy" type="joy_node" name="joy">
<param name="dev" value="/dev/input/js0"/>
<param name="deadzone" value="0.05"/>
</node>
<node pkg="teleop" type="teleop_turtle" name="teleop" output="screen">
<param name="axis_linear" value="1"/>
<param name="axis_angular" value="0"/>
<param name="scale_linear" value="2"/>
<param name="scale_angular" value="3"/>
<param name="enable_button" value="4"/>
</node>
</launch>
参数说明:
在实际项目中遇到过各种奇葩问题,这里分享几个典型案例:
手柄无响应
控制延迟严重
小乌龟运动不连续
这是因为只在摇杆变化时才发布消息。改进方案是加入定时器:
cpp复制ros::Timer timer = nh_.createTimer(ros::Duration(0.1),
[this](const ros::TimerEvent&){
if(enable_ && (last_linear_!=0 || last_angular_!=0)){
publishCmdVel(last_linear_, last_angular_);
}
});
当基本功能跑通后,可以考虑以下优化:
多手柄支持
xml复制<group ns="player1">
<node pkg="joy" type="joy_node" name="joy">
<param name="dev" value="/dev/input/js0"/>
</node>
</group>
<group ns="player2">
<node pkg="joy" type="joy_node" name="joy">
<param name="dev" value="/dev/input/js1"/>
</node>
</group>
按键映射可视化
用rqt_plot实时显示按键状态:
bash复制rqt_plot /joy/axes[0] /joy/axes[1] /joy/buttons[0]
运动轨迹记录
cpp复制nav_msgs::Path path;
path.header.frame_id = "world";
geometry_msgs::PoseStamped pose;
pose.pose.position.x = turtle_pose_.x;
pose.pose.position.y = turtle_pose_.y;
path.poses.push_back(pose);
path_pub_.publish(path);
最后提醒一点,实际机器人控制一定要加入急停开关和安全检查逻辑,我在实验室就见过因为手柄漂移导致机器人撞墙的事故。建议在代码中加入速度限幅和障碍物检测,这些经验都是用惨痛教训换来的。