第2章:ROS基础回顾——话题与服务通信、TF坐标变换、URDF模型加载、Launch文件编写

各位同学,欢迎来到第二章。在正式进入MoveIt的传感器融合之前,咱们得先把ROS的几块地基夯实了。我见过不少新手,一上来就搞运动规划,结果被TF和URDF折腾得死去活来。说白了,这些基础工具就像你工具箱里的扳手和螺丝刀——不趁手,后面的活根本干不利索。

这一章,我会带你快速过一遍ROS通信、TF坐标变换、URDF模型加载和Launch文件。嗯,都是干货,咱们直接开整。

2.1 话题与服务通信:ROS的“神经”与“指令”

ROS的通信机制,我习惯把它分成两类:话题(Topic)和服务(Service)。你可以把话题想象成“广播电台”——发布者一直往外发,订阅者想听就听,双方不需要知道对方是谁。而服务更像是“打电话”——你问一句,对方答一句,有来有回。

2.1.1 话题通信:异步、持续、一对多

在传感器融合里,话题用得最多。比如激光雷达的数据,它每秒发几十帧,你不可能每帧都去“请求”一次,对吧?所以用话题最合适。

我写一个最简单的发布者示例,你感受一下:

// 发布者示例(C++)
ros::Publisher pub = nh.advertise<sensor_msgs::LaserScan>("/scan", 10);
sensor_msgs::LaserScan msg;
msg.ranges.resize(360);
// ... 填充数据 ...
pub.publish(msg);

这里有个坑,我当年刚入行时踩过——队列大小。你设成10,如果订阅者处理得慢,消息就会丢。在传感器融合场景下,丢一帧数据可能让运动规划直接跳变。我个人习惯设大一点,比如100,或者用 ros::TransportHints().tcpNoDelay() 来降低延迟。

小提示: 如果你用Python写话题,记得在循环里加 rospy.sleep(0.01),不然CPU会飙到100%。我在一个项目里吃过这个亏,工控机直接过热关机。

2.1.2 服务通信:同步、请求-响应、一对一

服务适合那些“问一次,答一次”的场景。比如你让机械臂去抓一个杯子,你发一个“抓取请求”,它返回“成功/失败”。

看个服务客户端的例子:

// 服务客户端示例
ros::ServiceClient client = nh.serviceClient<moveit_msgs::GetPositionFK>("compute_fk");
moveit_msgs::GetPositionFK srv;
srv.request.header.frame_id = "base_link";
if (client.call(srv)) {
    ROS_INFO("正运动学计算成功");
} else {
    ROS_ERROR("服务调用失败");
}

为什么会失败?我遇到过最奇葩的情况是——服务端还没启动,客户端就开始调用了。所以,记得用 client.waitForExistence() 等一等。

2.2 TF坐标变换:机器人的“空间感知”

TF(Transform)是ROS里最核心的模块之一。没有TF,你根本不知道激光雷达的数据对应到机器人底盘上的哪个位置。说白了,TF就是帮你把不同坐标系下的数据统一到同一个参考系下。

2.2.1 TF树与坐标帧

每个TF都有一个“父帧”和“子帧”。比如 base_link 是父帧,laser_frame 是子帧。它们之间的变换关系,就是平移加旋转。

我画了一张图,帮你理解TF树的结构:

map odom base_link laser_frame camera_frame gripper_frame 静态TF 动态TF 静态TF 静态TF 静态TF

你看,map 到 odom 是动态TF(由定位模块更新),而 base_link 到 laser_frame 是静态TF(因为激光雷达固定在机器人上,位置不变)。

2.2.2 监听与广播TF

在代码里监听TF,我常用 tf::TransformListener:

tf::TransformListener listener;
tf::StampedTransform transform;
try {
    listener.lookupTransform("base_link", "laser_frame", ros::Time(0), transform);
} catch (tf::TransformException &ex) {
    ROS_ERROR("%s", ex.what());
}
注意: 监听TF时一定要加 try-catch。我曾经在一个演示现场,因为TF树还没构建好就直接查询,结果程序崩溃了……嗯,场面一度非常尴尬。

2.3 URDF模型加载:给机器人“画骨架”

URDF(Unified Robot Description Format)就是用XML描述机器人的关节、连杆和传感器。MoveIt全靠URDF来理解机器人的运动学结构。

一个简单的URDF片段长这样:

<robot name="my_robot">
  <link name="base_link">
    <visual>
      <geometry>
        <box size="0.4 0.4 0.1"/>
      </geometry>
    </visual>
  </link>
  
  <joint name="base_to_laser" type="fixed">
    <parent link="base_link"/>
    <child link="laser_frame"/>
    <origin xyz="0.2 0 0.15" rpy="0 0 0"/>
  </joint>
</robot>

这里有个关键点:关节类型。固定关节(fixed)用于传感器,旋转关节(revolute)用于机械臂。我见过有人把传感器关节设成 continuous,结果TF树里激光雷达一直在转圈……

核心经验: 在写URDF时,尽量把传感器帧单独作为一个link,不要直接挂在其他link的origin上。这样后续做传感器融合时,TF查询会更清晰。

2.4 Launch文件编写:一键启动的“总控台”

Launch文件,说白了就是帮你一次性启动多个节点的脚本。没有它,你每次都要开好几个终端,手动敲 rosrun,烦不烦?

一个标准的Launch文件:

<launch>
  <!-- 启动机器人模型 -->
  <param name="robot_description" command="$(find xacro)/xacro $(find my_robot)/urdf/my_robot.urdf.xacro"/>
  
  <!-- 启动TF广播 -->
  <node name="robot_state_publisher" pkg="robot_state_publisher" type="robot_state_publisher"/>
  
  <!-- 启动激光雷达驱动 -->
  <node name="laser_driver" pkg="hokuyo_node" type="hokuyo_node">
    <param name="port" value="/dev/ttyUSB0"/>
  </node>
  
  <!-- 启动Rviz可视化 -->
  <node name="rviz" pkg="rviz" type="rviz" args="-d $(find my_robot)/config/display.rviz"/>
</launch>

我个人习惯在Launch文件里加 output="screen" 和 respawn="true"。前者让你看到节点的打印信息,后者让节点崩溃后自动重启。嗯,调试的时候特别有用。

避坑指南: 我曾经在Launch文件里忘了加 type 参数,结果ROS默认用C++版本,而我的代码是Python的……报错信息看了半小时才反应过来。所以,type 一定要写对。

2.5 本章小结

这一章我们回顾了ROS的四大基础模块:话题与服务通信、TF坐标变换、URDF模型加载、Launch文件编写。这些内容看似基础,但我在实际项目中见过太多因为TF树没配好、URDF关节类型写错而导致的诡异问题。

记住一句话:基础不牢,地动山摇。下一章开始,我们就要正式进入传感器数据融合的核心了。到时候你会发现,今天讲的这些,全是后面实战的“弹药”。