ROS机器人入门第二课:ROS通信编程和工作空间

66f533229f56950a4bf1b1f6b8aa947e

大家好,我是老王,网名叫”老王搞机器人”。92年的,现在在深圳一家做工业机器人的小公司搬砖。自从大学学了自动化专业之后,就一直在机器人这个坑里爬不出来了,平时没事就喜欢折腾各种机器人项目。

上次给大家分享了ROS机器人的基础概念,今天咱们来点实在的——手把手教你怎么用ROS写代码。别担心,我会尽量讲得通俗易懂,毕竟当年我刚学ROS的时候也是一脸懵逼。

今天要学什么?

这节课主要讲四个部分:

  1. 创建工作空间 – 这就像给你的项目安个家
  2. ROS通信编程 – 话题、服务、动作,这三个是ROS机器人通信的三板斧
  3. 分布式通信 – 让多台电脑一起干活
  4. 关键工具 – Launch文件、TF坐标变换、可视化工具这些好东西

我主要会用C++来演示,但别慌,Python的逻辑是一样的。我的建议是:先理解方法论,再纠结具体代码。

第一部分:搞定ROS工作空间

7b45709ea5b1d17320dca0bbc4c0e331

什么是工作空间?

说白了,工作空间(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)通信

动作通信适合耗时较长且需要反馈的任务,比如让机器人走到某个目标点,你希望实时知道它走到哪了。

动作比服务多了两个功能:

  1. 周期性反馈 – 告诉你进度
  2. 可以取消 – 中途可以喊停

实战:洗盘子机器人

定义动作文件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

操作技巧:

  1. 左侧点”Add”添加显示插件
  2. 选择要显示的数据类型(如PointCloud2、Image)
  3. 设置话题名称
  4. 调整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),求障碍物相对小车中心的坐标。

学习资源推荐

  1. ROS Wiki官方教程 – http://wiki.ros.org/ROS/Tutorials
  2. 《机器人学导论》 – 坐标变换理论必读

写在最后

今天的内容信息量比较大,涵盖了ROS机器人开发的核心技能。我当年学这些的时候,光话题通信就折腾了一个星期。所以别着急,慢慢来,多动手实践。

记住:ROS不是用来看的,是用来练的。把今天的代码都跑一遍,遇到问题去Google、去论坛问,慢慢就熟了。

下节课我们会讲机器人建模和URDF,到时候你就能在RViz里看到自己的机器人模型了。

有问题随时在评论区留言,咱们一起进步!

原创文章,作者:老王ros机器人,如若转载,请注明出处:https://www.key-iot.com.cn/jssf/1181.html

posted @ 2025-11-14 19:16  星创易联  阅读(11)  评论(0)    收藏  举报