ROS 2边学边练(14)-- 写一个简单的发布者和订阅者(C++)

前言

        前面章节我们已经体验过节点这个概念,一个节点负责一种功能,ROS系统中会有很多的节点通过不同的主题(topic)、服务(service)、动作(action)等通信方式互相协调共同维护一个系统的日常运作,此篇我们就从主题开始,亲手写出一个发布者节点和一个订阅者节点(C++完成,因为日常工作与ROS完全无关,全凭周末及下班后的时间来学习并记录发博文,所以时间和精力都有限,暂未安排python版本,后续如有机缘可再安排续上🥷)。

动动手

创建一个功能包

        首先呢,还是激活我们的ROS 2安装环境,方便让我们能正常使用各种ROS相关的命令及其他一些过程:

$source /opt/ros/iron/setup.bash

我们再进入ros2_ws/src路径,通过下面的命令创建我们的功能包:

$ros2 pkg create --build-type ament_cmake --license Apache-2.0 cpp_pubsub

即将生成一个叫cpp_pubsub的功能包文件夹,但是include和src文件夹里面是空空如也的啊,因为是需要我们手动添加的。

创建完成之后,我们进入该包的src文件夹下,我们准备下一步。

写个发布者节点

        官方教程有点讨厌,都走到这一步了居然还是让我们下载他们写好的源代码文件。没招,接着弄,通过在包的src路径下执行如下命令下载发布者cpp文件:

$wget -O publisher_member_function.cpp https://raw.githubusercontent.com/ros2/examples/iron/rclcpp/topics/minimal_publisher/member_function.cpp

如果有小伙伴出现下载问题,咱们也可以直接复制下面贴出的代码内容(publisher_member_function.cpp):

// Copyright 2016 Open Source Robotics Foundation, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#include <chrono>
#include <functional>
#include <memory>
#include <string>

#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/string.hpp"

using namespace std::chrono_literals;

/* This example creates a subclass of Node and uses std::bind() to register a
 * member function as a callback from the timer. */

class MinimalPublisher : public rclcpp::Node
{
public:
  MinimalPublisher()
  : Node("minimal_publisher"), count_(0)
  {
    publisher_ = this->create_publisher<std_msgs::msg::String>("topic", 10);
    timer_ = this->create_wall_timer(
      500ms, std::bind(&MinimalPublisher::timer_callback, this));
  }

private:
  void timer_callback()
  {
    auto message = std_msgs::msg::String();
    message.data = "Hello, world! " + std::to_string(count_++);
    RCLCPP_INFO(this->get_logger(), "Publishing: '%s'", message.data.c_str());
    publisher_->publish(message);
  }
  rclcpp::TimerBase::SharedPtr timer_;
  rclcpp::Publisher<std_msgs::msg::String>::SharedPtr publisher_;
  size_t count_;
};

int main(int argc, char * argv[])
{
  rclcpp::init(argc, argv);
  rclcpp::spin(std::make_shared<MinimalPublisher>());
  rclcpp::shutdown();
  return 0;
}
分析代码
#include <chrono>
#include <functional>
#include <memory>
#include <string>

#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/string.hpp"

        头四行为我们会调用的C++标准库中的头文件,倒数第二行为ROS 2系统中最为通用功能的头文件,最后一行为ROS 2中我们需要调用的消息结构的头文件(指定发布和订阅数据的结构)。这几行头文件内容是该节点的依赖项,我们也得在package.xml和CMakeLists.txt中添加进去(后面马上会提到)。

class MinimalPublisher : public rclcpp::Node

        该节点(我们取名MinimalPublisher)公共继承自Node这个类(面向对象编程的一个特性,比如我们创建了一个基础类,它定义了一些公共属性,我们在创建有同样属性但又有些差异的具体类时,我们就可以直接公共继承这个基础类,不需再次定义那些属性,只需关心定义那些差异的属性),后面代码里面的this指代的就是这个MinimalPublisher。

public:
  MinimalPublisher()
  : Node("minimal_publisher"), count_(0)
  {
    publisher_ = this->create_publisher<std_msgs::msg::String>("topic", 10);
    timer_ = this->create_wall_timer(
    500ms, std::bind(&MinimalPublisher::timer_callback, this));
  }

        这是MinimalPublisher类的构造函数(类的初始化函数),代入了一个字符串minimal_publisher作为节点的名字,另外还带了一个用于计数的整形变量count_,并初始化为0,构造函数内部,定义了消息类型为String,主题为“topic”,另外还指定了如果需要备份时的消息队列上限大小为10,构造函数的最后初始化了定时器timer_,其定时周期为1秒触发2次,而且还绑定了一个回调函数timer_callback()用来执行任务(发布主题数据)。

private:
  void timer_callback()
  {
    auto message = std_msgs::msg::String();
    message.data = "Hello, world! " + std::to_string(count_++);
    RCLCPP_INFO(this->get_logger(), "Publishing: '%s'", message.data.c_str());
    publisher_->publish(message);
  }

        上面的代码就是这个回调函数的定义,首先定义并初始化了一个消息对象message,接着填充了该消息的内容(message.data)为"Hello, world! count_",count_在调用回调函数时会自增一次,后面就是ROS里面的RCLCPP_INFO调试打印语句了(会输出到控制台,也即我们的终端上),最后即为发布主题消息了。

rclcpp::TimerBase::SharedPtr timer_;
rclcpp::Publisher<std_msgs::msg::String>::SharedPtr publisher_;
size_t count_;

        此三行声明了我们上面代码中看到的定时器变量、发布者变量以及统计变量。

int main(int argc, char * argv[])
{
  rclcpp::init(argc, argv);
  rclcpp::spin(std::make_shared<MinimalPublisher>());
  rclcpp::shutdown();
  return 0;
}

        这个就是我们的主函数了,程序首先会进入main函数,去执行每一行代码。rclcpp::init(argc, argv)为ROS 2的初始化函数,rclcpp::spin(...)从我们创建的节点MinimalPublisher开始处理数据,包括这个定时器的回调函数,而且spin还是个无限循环函数。

添加依赖项package.xml

        

        我们打开cpp_pubsub功能包根路径下的package.xml,将之前提到的description、maintainer、license先填上,比如下面这样:

将下面两行依赖代码粘贴到ament_cmake这行的下面,保存:

<depend>rclcpp</depend>
<depend>std_msgs</depend>

当该源代码(publisher_member_function.cpp)在编译和运行时是需要这些依赖库的,最后效果如下。

添加依赖项CMakeLists.txt

        我们再打开CMakeLists.txt文件,

将下面两行代码加到第9行find_package(ament_cmake REQUIRED)下面,

find_package(rclcpp REQUIRED)
find_package(std_msgs REQUIRED)

将下面的内容加到第14行的后面,我们给生成的节点可执行文件取名为talker(就可以使用ros2 run来启动它),

add_executable(talker src/publisher_member_function.cpp)
ament_target_dependencies(talker rclcpp std_msgs)

最后将下面的代码加到上面内容的后面,这样ros run就能找到talker被安装在什么地方了,

install(TARGETS
  talker
  DESTINATION lib/${PROJECT_NAME})

最最后一步,我们可以删掉多余的注释代码,让整体看起来更简练,最终效果如下:

        目前我们的发布者节点准备工作都做好了,可以进行构建并单独运行了,但是我们的订阅者节点还没完成,等它一起吧,不然显得不合群就完蛋了。

写个订阅者节点

        同样的流程,我们在包的src路径下执行如下命令下载订阅者节点源代码文件(subscriber_member_function.cpp):

// Copyright 2016 Open Source Robotics Foundation, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#include <functional>
#include <memory>

#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/string.hpp"

using std::placeholders::_1;

class MinimalSubscriber : public rclcpp::Node
{
public:
  MinimalSubscriber()
  : Node("minimal_subscriber")
  {
    subscription_ = this->create_subscription<std_msgs::msg::String>(
      "topic", 10, std::bind(&MinimalSubscriber::topic_callback, this, _1));
  }

private:
  void topic_callback(const std_msgs::msg::String & msg) const
  {
    RCLCPP_INFO(this->get_logger(), "I heard: '%s'", msg.data.c_str());
  }
  rclcpp::Subscription<std_msgs::msg::String>::SharedPtr subscription_;
};

int main(int argc, char * argv[])
{
  rclcpp::init(argc, argv);
  rclcpp::spin(std::make_shared<MinimalSubscriber>());
  rclcpp::shutdown();
  return 0;
}
分析代码

        代码内容与发布者节点代码整体相似(构造函数里订阅者节点名字为minimal_subscriber,订阅主题名字为"topic"),只是你会发现没有定时器了,这是因为订阅者只管接收主题的数据(被动的),而且在主函数里面订阅者节点也是调用的spin函数,它会永远徘徊在那个循环里,有数据我就接收打印输出,无数据我就发发呆,多么惬意的生活。

添加依赖项package.xml

        由于订阅者节点和发布者节点都有相同的依赖项,而package.xml文件是针对整个包里面的源文件的,没有区分哪段是xxx.cpp的依赖哪段是yyy.cpp的依赖,所以无需再增加依赖项到package.xml里面了。

添加依赖项CMakeLists.txt

        再次打开CMakeLists.txt,将下面的内容添加到发布者节点的下面,保存好:

add_executable(listener src/subscriber_member_function.cpp)
ament_target_dependencies(listener rclcpp std_msgs)

install(TARGETS
  listener
  DESTINATION lib/${PROJECT_NAME})

完整内容如下:

构建运行

检查依赖

        发布者节点和订阅者节点相关的源文件及配置文件都弄好了,现在我们就可以整体构建并试试结果了,但首先我们还是需要检查下依赖情况(rclcpp和std_msgs已经在我们安装ROS 2的时候一起下载安装过了),进入工作空间根路径下执行如下命令检查:

$rosdep install -i --from-path src --rosdistro iron -y

构建功能包

        同样在工作空间根路径下构建我们的cpp_pubsub功能包:

$colcon build --packages-select cpp_pubsub

在ros2_ws/install/cpp_pubsub/lib/cpp_pubsub路径下生成了我们的目标节点文件listener和talker.

激活工作空间(overlay)环境变量

        另开一个终端,进入工作空间根路径,执行下面的命令激活环境变量,能让ROS 2找到我们生成的talker和listener:

$source install/setup.bash

可能你会问,之前不是先得source /opt/ros/iron/setup.bash再source install/local_setup.bash嘛,你记得没错,但是我们现在偷个懒,这俩环境的激活我们用一个脚本文件就实现了(可以打开这个脚本的内容查看一下是不是这样),所以嘛,省点力气终归是好的。

运行
启动listener
$ros2 run cpp_pubsub lisener
启动talker

        打开另外一个终端source环境变量再执行下面命令:

$ros2 run cpp_pubsub talker

最终效果如下:

订阅者lisener

发布者talker(2次/秒频率发布主题消息数据)

本篇完。

  • 21
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值