ROS机器人入门第二课:ROS通信编程和工作空间
大家好,我是老王,网名叫”老王搞机器人”。92年的,现在在深圳一家做工业机器人的小公司搬砖。自从大学学了自动化专业之后,就一直在机器人这个坑里爬不出来了,平时没事就喜欢折腾各种机器人项目。
上次给大家分享了ROS机器人的基础概念,今天咱们来点实在的——手把手教你怎么用ROS写代码。别担心,我会尽量讲得通俗易懂,毕竟当年我刚学ROS的时候也是一脸懵逼。
今天要学什么?
这节课主要讲四个部分:
- 创建工作空间 – 这就像给你的项目安个家
- ROS通信编程 – 话题、服务、动作,这三个是ROS机器人通信的三板斧
- 分布式通信 – 让多台电脑一起干活
- 关键工具 – Launch文件、TF坐标变换、可视化工具这些好东西
我主要会用C++来演示,但别慌,Python的逻辑是一样的。我的建议是:先理解方法论,再纠结具体代码。
第一部分:搞定ROS工作空间
什么是工作空间?
说白了,工作空间(workspace)就是一个文件夹,你所有ROS机器人项目的代码、配置文件都放这里面。这个文件夹有个规矩,必须包含四个子文件夹:
- src/ – 源代码放这里
- build/ – 编译时的中间文件,一般不用管
- devel/ – 编译好的可执行文件和环境变量配置
- install/ – 可选的安装目录
实战:创建你的第一个工作空间
我当年第一次创建工作空间的时候,在命令行里敲得手忙脚乱的。其实很简单:
# 在home目录下创建工作空间
cd ~
mkdir -p catkin_ws/src
cd catkin_ws/src
# 初始化工作空间
catkin_init_workspace
这个catkin_init_workspace命令会在src目录里生成一个CMakeLists.txt文件,告诉系统:”嘿,这是个ROS工作空间!”
接下来编译一下:
cd ~/catkin_ws
catkin_make
编译完成后,你会看到build和devel两个文件夹自动生成了。
环境变量设置(重要!)
这里要敲黑板了!很多新手遇到的”找不到功能包”问题,90%都是环境变量没配好。
每次打开新终端都要设置环境变量太麻烦,所以我们把它写到配置文件里:
# 如果你用的是bash
gedit ~/.bashrc
# 如果你用的是zsh(像我一样)
gedit ~/.zshrc
在文件最后加上这一行:
source ~/catkin_ws/devel/setup.bash # bash用户
# 或者
source ~/catkin_ws/devel/setup.zsh # zsh用户
保存后,让配置生效:
source ~/.bashrc # 或 source ~/.zshrc
验证一下是否设置成功:
echo $ROS_PACKAGE_PATH
应该能看到你的工作空间路径排在最前面。
创建功能包
在ROS机器人开发中,功能包(package)是实现某个具体功能的最小单元。比如你想搞一个避障功能,就创建一个obstacle_avoidance功能包。
cd ~/catkin_ws/src
catkin_create_pkg learning_communication std_msgs rospy roscpp
这条命令做了什么?
learning_communication– 功能包名字std_msgs rospy roscpp– 这个包依赖的其他包
创建完成后,你会看到learning_communication文件夹里有:
- package.xml – 功能包的”身份证”,记录名字、版本、依赖关系
- CMakeLists.txt – 编译配置文件
- src/ – 放代码的地方
然后回到工作空间根目录编译:
cd ~/catkin_ws
catkin_make
工作空间覆盖机制
这是个容易踩坑的地方。ROS允许多个工作空间存在,但查找功能包时按环境变量顺序从前往后找,找到就停止。
举个例子,如果你在自己的工作空间里修改了一个系统自带的包,ROS会优先用你修改的版本。这很灵活,但也容易出bug。我的建议是:除非必要,别乱覆盖系统包。
第二部分:ROS机器人通信编程三板斧
第一板斧:话题(Topic)通信
话题通信是ROS里最常用的通信方式,就像微信群聊天:有人发消息(发布者),有人看消息(订阅者),互不干扰。
创建发布者(Talker)
我们来写一个发布”Hello World”的节点:
// talker.cpp
#include <ros/ros.h>
#include <std_msgs/String.h>
int main(int argc, char **argv)
{
// 1. 初始化ROS节点
ros::init(argc, argv, "talker");
// 2. 创建节点句柄
ros::NodeHandle nh;
// 3. 创建发布者
ros::Publisher chatter_pub = nh.advertise<std_msgs::String>("chatter", 1000);
// 4. 设置循环频率(10Hz)
ros::Rate loop_rate(10);
int count = 0;
while (ros::ok())
{
// 5. 组装消息
std_msgs::String msg;
msg.data = "Hello World " + std::to_string(count);
// 6. 发布消息
chatter_pub.publish(msg);
ROS_INFO("%s", msg.data.c_str());
// 7. 处理回调和休眠
ros::spinOnce();
loop_rate.sleep();
count++;
}
return 0;
}
我的经验提醒:
- 那个
loop_rate.sleep()千万别忘了,不然CPU会被吃到100% - 队列长度(1000)根据实际情况调整,发布太快会丢帧
创建订阅者(Listener)
// listener.cpp
#include <ros/ros.h>
#include <std_msgs/String.h>
// 回调函数:收到消息时会被调用
void chatterCallback(const std_msgs::String::ConstPtr& msg)
{
ROS_INFO("I heard: [%s]", msg->data.c_str());
}
int main(int argc, char **argv)
{
ros::init(argc, argv, "listener");
ros::NodeHandle nh;
// 订阅chatter话题
ros::Subscriber sub = nh.subscribe("chatter", 1000, chatterCallback);
// 循环等待回调函数
ros::spin();
return 0;
}
订阅者的核心是回调函数机制:一旦有消息到达,自动进入回调函数处理,不需要你手动去查询。
配置编译选项
在功能包的CMakeLists.txt里添加:
add_executable(talker src/talker.cpp)
target_link_libraries(talker ${catkin_LIBRARIES})
add_executable(listener src/listener.cpp)
target_link_libraries(listener ${catkin_LIBRARIES})
然后编译运行:
cd ~/catkin_ws
catkin_make
# 打开三个终端
# 终端1
roscore
# 终端2
rosrun learning_communication talker
# 终端3
rosrun learning_communication listener
自定义消息类型
ROS机器人开发中,系统自带的消息类型不一定够用。比如我想定义一个”人”的数据结构,包含姓名、性别、年龄:
# Person.msg
string name
uint8 sex
uint8 age
uint8 unknown = 0
uint8 male = 1
uint8 female = 2
在功能包下创建msg文件夹,把Person.msg放进去,然后修改:
package.xml添加依赖:
<build_depend>message_generation</build_depend>
<exec_depend>message_runtime</exec_depend>
CMakeLists.txt添加:
find_package(catkin REQUIRED COMPONENTS
message_generation
# 其他依赖...
)
add_message_files(
FILES
Person.msg
)
generate_messages(
DEPENDENCIES
std_msgs
)
编译后就能用了:
catkin_make
rosmsg show learning_communication/Person
第二板斧:服务(Service)通信
服务通信是”问答模式”:客户端发请求,服务端处理后返回结果。就像你去饭店点菜,服务员给你上菜。
实战案例:加法服务
我们实现一个简单的加法服务:客户端发送两个数,服务端返回它们的和。
1. 定义服务文件AddTwoInts.srv
# 请求部分
int64 a
int64 b
---
# 应答部分
int64 sum
三个横杠上面是请求数据,下面是应答数据。
2. 服务端代码
// server.cpp
#include <ros/ros.h>
#include "learning_communication/AddTwoInts.h"
bool add(learning_communication::AddTwoInts::Request &req,
learning_communication::AddTwoInts::Response &res)
{
res.sum = req.a + req.b;
ROS_INFO("Request: a=%ld, b=%ld", req.a, req.b);
ROS_INFO("Sending back response: %ld", res.sum);
return true;
}
int main(int argc, char **argv)
{
ros::init(argc, argv, "add_two_ints_server");
ros::NodeHandle nh;
ros::ServiceServer service = nh.advertiseService("add_two_ints", add);
ROS_INFO("Ready to add two ints.");
ros::spin();
return 0;
}
3. 客户端代码
// client.cpp
#include <ros/ros.h>
#include "learning_communication/AddTwoInts.h"
int main(int argc, char **argv)
{
ros::init(argc, argv, "add_two_ints_client");
if (argc != 3)
{
ROS_INFO("Usage: client X Y");
return 1;
}
ros::NodeHandle nh;
ros::ServiceClient client = nh.serviceClient<learning_communication::AddTwoInts>("add_two_ints");
learning_communication::AddTwoInts srv;
srv.request.a = atoll(argv[1]);
srv.request.b = atoll(argv[2]);
if (client.call(srv))
{
ROS_INFO("Sum: %ld", srv.response.sum);
}
else
{
ROS_ERROR("Failed to call service");
return 1;
}
return 0;
}
我的踩坑经验:client.call()是阻塞式的,如果服务端没响应,客户端会一直卡住。实际项目中要加超时机制。
第三板斧:动作(Action)通信
动作通信适合耗时较长且需要反馈的任务,比如让机器人走到某个目标点,你希望实时知道它走到哪了。
动作比服务多了两个功能:
- 周期性反馈 – 告诉你进度
- 可以取消 – 中途可以喊停
实战:洗盘子机器人
定义动作文件DoDishes.action:
# Goal(目标)
uint32 dishwasher_id
---
# Result(最终结果)
uint32 total_dishes_cleaned
---
# Feedback(周期反馈)
float32 percent_complete
三个横杠分成三部分:目标、结果、反馈。
服务端代码(简化版):
void executeCB(const learning_communication::DoD ishesGoalConstPtr &goal)
{
ros::Rate rate(1); // 1Hz
learning_communication::DoDishesFeedback feedback;
for (int i = 1; i <= 10; i++)
{
feedback.percent_complete = i * 10.0;
as_.publishFeedback(feedback);
ROS_INFO("Dishwasher %d: %.0f%% complete",
goal->dishwasher_id, feedback.percent_complete);
rate.sleep();
}
// 完成后发送结果
result_.total_dishes_cleaned = 10;
as_.setSucceeded(result_);
}
这个案例模拟洗盘子过程,每秒反馈一次进度,10秒后完成。
第三部分:玩转多机通信
ROS机器人的一个杀手锏是分布式架构。比如我在公司做项目时,上位机跑算法,下位机控制硬件,通过ROS无缝协同。
配置步骤
假设有两台电脑:
- 主机(192.168.31.199)- 运行roscore
- 从机(192.168.31.70)- 运行节点
1. 确保网络互通
两台电脑都要编辑/etc/hosts:
sudo gedit /etc/hosts
主机添加:
192.168.31.70 robot_pi
从机添加:
192.168.31.199 my_pc
测试连通性:
ping robot_pi # 在主机上
ping my_pc # 在从机上
2. 设置ROS_MASTER_URI
在从机的~/.bashrc里添加:
export ROS_MASTER_URI=http://my_pc:11311
export ROS_HOSTNAME=robot_pi
生效配置:
source ~/.bashrc
3. 启动测试
主机上:
roscore
rosrun turtlesim turtlesim_node
从机上:
rostopic list # 应该能看到/turtle1下的话题
rostopic pub /turtle1/cmd_vel geometry_msgs/Twist -r 10 -- '[1.0,0.0,0.0]' '[0.0,0.0,0.5]'
如果小乌龟动了,恭喜你,多机通信成功!
第四部分:必会的ROS工具
Launch文件:批量启动节点
每次开多个终端运行节点太麻烦,Launch文件帮你一键搞定。
<!-- turtle_tf.launch -->
<launch>
<!-- 启动小乌龟仿真器 -->
<node pkg="turtlesim" type="turtlesim_node" name="sim"/>
<!-- 启动键盘控制 -->
<node pkg="turtlesim" type="turtle_teleop_key" name="teleop" output="screen"/>
<!-- 启动TF广播(两只乌龟) -->
<node pkg="learning_tf" type="turtle_tf_broadcaster"
args="/turtle1" name="turtle1_tf_broadcaster" />
<node pkg="learning_tf" type="turtle_tf_broadcaster"
args="/turtle2" name="turtle2_tf_broadcaster" />
<!-- 启动TF监听 -->
<node pkg="learning_tf" type="turtle_tf_listener" name="listener" />
</launch>
常用标签:
<node>– 启动节点,必须指定pkg、type、name<param>– 设置参数<remap>– 重映射话题名<include>– 包含其他launch文件
运行launch文件:
roslaunch learning_communication turtle_tf.launch
TF坐标变换:机器人的GPS
TF(Transform)是ROS机器人处理坐标变换的利器。
实战场景
假设小车上有个激光雷达,雷达检测到前方0.3米有障碍物。但这个0.3米是相对雷达坐标系的,你需要转换成相对小车中心坐标系才能用于控制。
广播坐标变换:
tf::TransformBroadcaster br;
tf::Transform transform;
transform.setOrigin(tf::Vector3(msg->x, msg->y, 0.0));
transform.setRotation(tf::createQuaternionFromYaw(msg->theta));
br.sendTransform(tf::StampedTransform(transform, ros::Time::now(),
"world", "turtle1"));
监听坐标变换:
tf::TransformListener listener;
tf::StampedTransform transform;
try {
listener.waitForTransform("/turtle2", "/turtle1", ros::Time(0), ros::Duration(3.0));
listener.lookupTransform("/turtle2", "/turtle1", ros::Time(0), transform);
// 现在transform里就是turtle1相对turtle2的位姿
double distance = sqrt(pow(transform.getOrigin().x(), 2) +
pow(transform.getOrigin().y(), 2));
} catch (tf::TransformException &ex) {
ROS_ERROR("%s", ex.what());
}
RQT工具箱
rqt_console – 日志查看器:
rqt_console
能看到所有节点的日志,支持按级别过滤。
rqt_graph – 计算图可视化:
rqt_graph
一眼看清节点和话题的关系。
rqt_plot – 数据曲线绘制:
rqt_plot /turtle1/pose/x /turtle1/pose/y
RViz:3D可视化神器
RViz是ROS机器人调试的王牌工具,能可视化:
- 机器人模型
- 传感器数据(激光、点云、图像)
- 坐标系
- 路径规划结果
启动RViz:
rviz
操作技巧:
- 左侧点”Add”添加显示插件
- 选择要显示的数据类型(如PointCloud2、Image)
- 设置话题名称
- 调整Fixed Frame(固定坐标系)
Gazebo:物理仿真环境
Gazebo能模拟真实物理环境,包括重力、摩擦力、惯性。我在公司做算法验证时,先在Gazebo里跑通再上真机,省了不少钱。
# 启动空环境
gazebo
# 加载机器人模型
roslaunch gazebo_ros empty_world.launch
课后作业
为了巩固今天的内容,我给大家留几个作业:
作业1:工作空间练习
创建一个名为my_robot_ws的工作空间,在里面创建功能包my_first_pkg。
作业2:话题编程
- 用服务方式在(5,5)位置生成第二只乌龟turtle2
- 订阅turtle2的位置信息并打印
- 发布速度命令让turtle2旋转
作业3:动作编程
实现一个”移动到目标点”的Action,模拟机器人移动过程,每秒反馈一次进度。
作业4:TF应用
小车底盘中心和激光雷达有偏移(X方向10cm,Z方向20cm),雷达检测到障碍物在(0.3, 0, 0),求障碍物相对小车中心的坐标。
学习资源推荐
- ROS Wiki官方教程 – http://wiki.ros.org/ROS/Tutorials
- 《机器人学导论》 – 坐标变换理论必读
写在最后
今天的内容信息量比较大,涵盖了ROS机器人开发的核心技能。我当年学这些的时候,光话题通信就折腾了一个星期。所以别着急,慢慢来,多动手实践。
记住:ROS不是用来看的,是用来练的。把今天的代码都跑一遍,遇到问题去Google、去论坛问,慢慢就熟了。
下节课我们会讲机器人建模和URDF,到时候你就能在RViz里看到自己的机器人模型了。
有问题随时在评论区留言,咱们一起进步!
原创文章,作者:老王ros机器人,如若转载,请注明出处:https://www.key-iot.com.cn/jssf/1181.html



浙公网安备 33010602011771号