赵虚左Ros2-核心篇讲义学习-第二三章 ROS2通信机制核心

2023-12-28 12:06:12

好资料参考:?

https://mp.weixin.qq.com/mp/appmsgalbum?search_click_id=7832261026941590639-1701139587153-4569371651&__biz=MzU3Mjg0NDkxNw==&action=getalbum&album_id=3095145891872931841&uin=&key=&devicetype=Windows+10+x64&version=6309080f&lang=zh_CN&ascene=1

ROS 2 基本命令总结_ros2启动命令-CSDN博客

第二章? Ros2通信机制核心

1 通信机制简介

????????在ROS2中通信方式虽然有多种,但是不同通信方式的组成要素都是类似的,比如:通信是双方或多方行为、通信时都需要将不同的通信对象关联、都有各自的模型、交互数据时也必然涉及到数据载体等等。本节将会介绍通信中涉及到的一些术语。

1.1节点(通信主体)

????????在通信时,不论采用何种方式,通信对象的构建都依赖于节点(Node),(Node对象或者Node子类的对象)在ROS2中,一般情况下每个节点都对应某一单一的功能模块(例如:雷达驱动节点可能负责发布雷达消息,摄像头驱动节点可能负责发布图像消息)。一个完整的机器人系统可能由许多协同工作的节点组成,ROS2中的单个可执行文件(C++程序或Python程序)可以包含一个或多个节点。

1.2.话题

????????话题(Topic)是一个纽带,具有相同话题的节点可以关联在一起(跟人一样),而这正是通信的前提。并且ROS2是跨语言的,有的节点可能是使用C++实现,有的节点可能是使用Python实现的,但是只要二者使用了相同的话题,就可以实现数据的交互。

1.3 通信模型

????????不同的通信对象(节点)通过(话题)关联到一起之后,以何种方式实现通信呢?在ROS2中,常用的通信模型有四种:

1.话题通信:是一种单向通信模型,在通信双方中,发布方发布数据,订阅方订阅数据,数据流单向的由发布方传输到订阅方。

2.服务通信:是一种基于请求响应的通信模型,在通信双方中,客户端发送请求数据到服务端,服务端响应结果给客户端。

3.动作通信:是一种带有连续反馈的通信模型,在通信双方中,客户端发送请求数据到服务端,服务端响应结果给客户端,但是在服务端接收到请求到产生最终响应的过程中,会发送中间连续的反馈(进度)信息到客户端。

4.参数服务:是一种基于共享的通信模型,在通信双方中,服务端可以设置数据,而客户端可以连接服务端并操作服务端数据。

1.4.接口

????????在通信过程中,需要传输数据,就必然涉及到数据载体,也即要以特定格式传输数据。在ROS2中,数据载体称之为接口(interfaces)。通信时使用的数据载体一般需要使用接口文件定义。常用的接口文件有三种:msg文件(话题通信)、srv文件(服务通信)与action(动作通信)文件。每种文件都可以按照一定格式定义特定数据类型的“变量”。

1.4.1.msg文件

msg文件是用于定义话题通信中数据载体的接口文件,一个典型的.msg文件示例如下。

int64 num1
int64 num2

在文件中声明了一些被传输的类似于C++变量的数据。

1.4.2.srv文件

????????srv(service)文件是用于定义服务通信中数据载体的接口文件,一个典型的.srv文件示例如下。

int64 num1
int64 num2
---
int64 sum

????????文件中声明的数据被---分割为两部分,上半部分用于声明请求数据,下半部分用于声明响应数据。

1.4.3.action文件

action文件使用用于定义动作通信中数据载体的接口文件,一个典型的.action文件示例如下。

int64 num
---
int64 sum
---
float64 progress

文件中声明的数据被---分割为三部分,上半部分用于声明请求数据,中间部分用于声明响应数据,下半部分用于声明连续反馈数据。

1.4.4.变量类型

????????不管是何种接口文件,在文件中每行声明的数据都由字段类型和字段名称组成,可以使用的字段类型有:

  • int8, int16, int32, int64 (或者无符号类型: uint*)

  • float32, float64

  • string

  • time, duration

  • 其他msg文件

  • 变长数组和定长数组

????????ROS中还有一种特殊类型:Header,标头包含时间戳和ROS2中常用的坐标帧信息。许多接口文件的第一行包含Header标头。

另外,需要说明的是:

参数通信的数据无需定义接口文件,参数通信时数据会被封装为参数对象,参数客户端和服务端操作的都是参数对象。

本阶段大家对数据载体做简单的了解即可,其具体使用后续章节有详细介绍。


1.5准备工作

1.请先创建工作空间ws01_plumbing,本章以及第3章代码部分内容存储在该工作空间下。

mkdir -p ws01_plumbing/src #创建工作空间以及子级目录 src,工作空间名称可以自定义

cd ws01_plumbing #进入工作空间

colcon build #编译

2.实际应用中一般建议创建专门的接口功能包定义接口文件,当前教程也遵循这一建议,预先创建教程所需使用的接口功能包(需要注意的是,目前为止无法在Python功能包中定义接口文件),终端下进入工作空间的src目录,执行如下命令:

ros2 pkg create --build-type ament_cmake base_interfaces_demo

该功能包将用于保存本章教程中自定义的接口文件。

2 话题通信
2.1 话题通信简介
场景

????????话题通信是ROS中使用频率最高的一种通信模式,话题通信是基于发布订阅模式的,也即:一个节点发布消息,另一个节点订阅该消息。话题通信的应用场景也极其广泛,比如如下场景:

????????机器人在执行导航功能,使用的传感器是激光雷达,机器人会采集激光雷达感知到的信息并计算,然后生成运动控制信息驱动机器人底盘运动。

在该场景中,就不止一次使用到了话题通信。

  • 以激光雷达信息的采集处理为例,在ROS中有一个节点需要实时的发布当前雷达采集到的数据,导航模块中也有节点会订阅并解析雷达数据。

  • 再以运动消息的发布为例,导航模块会综合多方面数据实时计算出运动控制信息并发布给底盘驱动模块,底盘驱动有一个节点订阅运动信息并将其转换成控制电机的脉冲信号。

????????以此类推,像雷达、摄像头、GPS.... 等等一些传感器数据的采集,也都是使用了话题通信,话题通信适用于不断更新的数据传输相关的应用场景。

概念

????????话题通信是一种以发布订阅的方式实现不同节点之间数据传输的通信模型。数据发布对象称为发布方,数据订阅对象称之为订阅方,发布方和订阅方通过话题相关联,发布方将消息发布在话题上,订阅方则从该话题订阅消息,消息的流向是单向的。

????????话题通信的发布方与订阅方是一种多对多的关系,也即,同一话题下可以存在多个发布方,也可以存在多个订阅方,这意味着数据会出现交叉传输的情况,当然如果没有订阅方,数据传输也会出现丢失的情况。

作用

????????话题通信一般应用于不断更新的、少逻辑处理的数据传输场景。

关于消息接口

关于消息接口的使用有多种方式:

  1. 在ROS2中通过功能包 std_msgs封装了一些原生的数据类型,比如:String、Int8、Int16、Int32、Int64、Float32、Float64、Char、Bool、Empty.... 这些原生数据类型也可以作为话题通信的载体,不过这些数据一般只包含一个 data 字段,而std_msgs包中其他的接口文件也比较简单,结构的单一意味着功能上的局限性,当传输一些结构复杂的数据时,就显得力不从心了;

  2. 在ROS2中还预定义了许多标准话题消息接口,这在实际工作中有着广泛的应用,比如:sensor_msgs包中定义了许多关于传感器消息的接口(雷达、摄像头、点云......),geometry_msgs包中则定义了许多几何消息相关的接口(坐标点、坐标系、速度指令......);

  3. 如果上述接口文件都不能满足我们的需求,那么就可以自定义接口消息;

具体如何选型,大家可以根据具体情况具体分析。

2.2案例以及案例分析
2.2.1.案例需求

需求1:编写话题通信实现,发布方以某个频率发布一段文本,订阅方订阅消息,并输出在终端。

需求2:编写话题通信实现,发布方以某个频率发布自定义接口消息,订阅方订阅消息,并输出在终端。

2.2.2.案例分析

在上述案例中,需要关注的要素有三个:

  1. 发布方;

  2. 订阅方;

  3. 消息载体。

案例1和案例2的主要区别在于消息载体,前者可以使用原生的数据类型(string),后者需要自定义接口消息。

2.2.3.流程简介

案例2需要先自定义接口消息,除此之外的实现流程与案例1一致,主要步骤如下:

  1. 编写发布方实现;

  2. 编写订阅方实现;

  3. 编辑配置文件;

  4. 编译;

  5. 执行。

案例我们会采用C++和Python分别实现,二者都遵循上述实现流程。

2.2.4.准备工作

终端下进入工作空间的src目录,调用如下两条命令分别创建C++功能包和Python功能包。

ros2 pkg create cpp01_topic --build-type ament_cmake --dependencies rclcpp std_msgs base_interfaces_demo
ros2 pkg create py01_topic --build-type ament_python --dependencies rclpy std_msgs base_interfaces_demo

注意:上面依赖了三个功能包

2.3话题通信之内置消息(C++)
2.3.1.发布方实现

????????功能包cpp01_topic的src目录下,新建C++文件demo01_talker_str.cpp,并编辑文件,输入如下内容:

/*  
  需求:以某个固定频率发送文本“hello world!”,文本后缀编号,每发送一条消息,编号递增1。
  步骤:
    1.包含头文件;
    2.初始化 ROS2 客户端 rclcpp;
    3.定义节点类;
      3-1.创建发布方;
      3-2.创建定时器;
      3-3.组织消息并发布。
    4.调用spin函数,并传入节点对象指针;
    5.释放资源。
*/

// 1.包含头文件;
#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/string.hpp"

using namespace std::chrono_literals;//关于设置定时器的持续时间单位

// 3.定义节点类;
class MinimalPublisher : public rclcpp::Node
{
  public:
    MinimalPublisher()
    : Node("minimal_publisher"), count_(0)
    {
      // 3-1.创建发布方;
      //返回值是个智能指针;模板是消息类型,用的 std_msgs中的内置string
      publisher_ = this->create_publisher<std_msgs::msg::String>("topic_name", 10);//10是qos的参数,10指的是一个队列的长度
      // 3-2.创建定时器;返回值是定时器对象的指针
      timer_ = this->create_wall_timer(500ms, std::bind(&MinimalPublisher::timer_callback, this));
    }

  private:
    void timer_callback()
    {
      // 3-3.组织消息并发布。
      auto message = std_msgs::msg::String();
      message.data = "Hello, world! " + std::to_string(count_++);
      //如果不用这个字符串对象呢?
      RCLCPP_INFO(this->get_logger(), "发布的消息:'%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[])
{
  // 2.初始化 ROS2 客户端;
  rclcpp::init(argc, argv);
  // 4.调用spin函数,并传入发布方节点对象指针。
  rclcpp::spin(std::make_shared<MinimalPublisher>());
  // 5.释放资源;
  rclcpp::shutdown();
  return 0;
}
  • spin函数:阻塞等待执行回调函数(如果没有spin函数,回调函数就不能被处理到);传入参是定义的节点对象的智能指针

订阅方实现之前,可以进行命令行测试-发布方消息是否真的正常发出去了:

使用ros2终端命令工具进行调试:ros2 topic echo命令?浏览在指定话题上发布的数据:

ros2 topic echo /topic_name

浏览上下,不难看出,发布方和订阅方练习的纽带就是同一个话题名称 topic_name

2.3.2.订阅方实现

功能包cpp01_topic的src目录下,新建C++文件demo02_listener_str.cpp,并编辑文件,输入如下内容:

/*  
    需求:订阅发布方发布的消息,并输出到终端。
    步骤:
        1.包含头文件;
        2.初始化 ROS2 客户端;
        3.定义节点类;
            3-1.创建订阅方;
            3-2.处理订阅到的消息。
        4.调用spin函数,并传入节点对象指针;
        5.释放资源。
*/

// 1.包含头文件;
#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/string.hpp"
using std::placeholders::_1;

// 3.定义节点类;
class MinimalSubscriber : public rclcpp::Node
{
  public:
    MinimalSubscriber()
    : Node("minimal_subscriber")
    {
      // 3-1.创建订阅方;
      subscription_ = this->create_subscription<std_msgs::msg::String>("topic_name", 10, std::bind(&MinimalSubscriber::topic_callback, this, _1));
    }

  private:
    // 3-2.处理订阅到的消息;
    void topic_callback(const std_msgs::msg::String & msg) const
    {
      RCLCPP_INFO(this->get_logger(), "订阅的消息: '%s'", msg.data.c_str());
    }
    rclcpp::Subscription<std_msgs::msg::String>::SharedPtr subscription_;
};

int main(int argc, char * argv[])
{
  // 2.初始化 ROS2 客户端;
  rclcpp::init(argc, argv);
  // 4.调用spin函数,并传入节点对象指针。
  rclcpp::spin(std::make_shared<MinimalSubscriber>());
  // 5.释放资源;
  rclcpp::shutdown();
  return 0;
}
2.3.3.编辑配置文件

在C++功能包中,配置文件主要关注package.xml与CMakeLists.txt。

1.package.xml

在创建功能包时,所依赖的功能包已经自动配置了,配置内容如下:

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

需要说明的是<depend>base_interfaces_demo</depend>在本案例中不是必须的。

2.CMakeLists.txt

CMakeLists.txt中发布和订阅程序核心配置如下:

find_package(rclcpp REQUIRED)
find_package(std_msgs REQUIRED)
find_package(base_interfaces_demo REQUIRED)

add_executable(demo01_talker_str src/demo01_talker_str.cpp)
ament_target_dependencies(
  demo01_talker_str
  "rclcpp"
  "std_msgs"
)

add_executable(demo02_listener_str src/demo02_listener_str.cpp)
ament_target_dependencies(
  demo02_listener_str
  "rclcpp"
  "std_msgs"
)

install(TARGETS 
  demo01_talker_str
  demo02_listener_str
  DESTINATION lib/${PROJECT_NAME})
2.3.4.编译

终端中进入当前工作空间,编译功能包:

colcon build --packages-select cpp01_topic
2.3.5.执行

当前工作空间下,启动两个终端,终端1执行发布程序,终端2执行订阅程序。

终端1输入如下指令:

. install/setup.bash
ros2 run cpp01_topic demo01_talker_str

终端2输入如下指令:

. install/setup.bash 
ros2 run cpp01_topic demo02_listener_str

最终运行结果与案例1类似。

2.4?话题通信自定义接口消息之--创建接口文件功能包

自定义接口消息的流程与在功能包中编写可执行程序的流程类似,主要步骤如下:

  1. 创建并编辑?.msg 接口文件

  2. 编辑配置文件;

  3. 编译;

  4. 测试。

接下来,我们可以参考案例2编译一个msg文件,该文件中包含学生的姓名、年龄、身高等字段。

2.4.1.创建并编辑 .msg 文件

功能包base_interfaces_demo下新建 msg 文件夹msg文件夹下新建Student.msg文件,文件中输入如下内容:

string   name
int32    age
float64  height

个人理解:相当于是ros2消息版的 c/c++结构体?的抽象实现

2.4.2.编辑配置文件

1.package.xml文件

在package.xml中需要添加一些依赖包,具体内容如下:

<!--编译依赖-->
<build_depend>rosidl_default_generators</build_depend>

<!--执行依赖-->
<exec_depend>rosidl_default_runtime</exec_depend>

<!--声明当前包所属的功能包组-->
<member_of_group>rosidl_interface_packages</member_of_

2.CMakeLists.txt文件

为了将.msg文件转换成对应的C++和Python代码,还需要在CMakeLists.txt中添加如下配置:

find_package(rosidl_default_generators REQUIRED)

rosidl_generate_interfaces(${PROJECT_NAME}
  "msg/Student.msg"
)
2.4.3.编译

终端中进入当前工作空间,编译功能包:

colcon build --packages-select base_interfaces_demo
2.4.4.测试

编译完成之后,在工作空间下的install目录下将生成Student.msg文件对应的C++和Python文件。

我们也可以在终端下进入工作空间,通过如下命令查看文件定义以及编译是否正常:

. install/setup.bash
ros2 interface show base_interfaces_demo/msg/Student

正常情况下,终端将会输出与Student.msg文件一致的内容。

总结:接口文件 msg? ?c/c+ 中的结构体? 自定义消息

2.5 话题通信之自定义消息(C++)

准备

C++文件中包含自定义消息相关头文件时,可能会抛出异常,可以配置VSCode中c_cpp_properties.json文件,在文件中的 includePath属性下添加一行:"${workspaceFolder}/install/base_interfaces_demo/include/**"

添加完毕后,包含相关头文件时,就不会抛出异常了,其他接口文件或接口包的使用也与此同理。

2.5.1.发布方实现

????????功能包cpp01_topic的src目录下,新建C++文件demo02_talker_stu.cpp,并编辑文件,输入如下内容:

/*  
  需求:以某个固定频率发送文本学生信息,包含学生的姓名、年龄、身高等数据。
*/

// 1.包含头文件;
#include "rclcpp/rclcpp.hpp"
#include "base_interfaces_demo/msg/student.hpp"

using namespace std::chrono_literals;
using base_interfaces_demo::msg::Student;
// 3.定义节点类;
class MinimalPublisher : public rclcpp::Node
{
  public:
    MinimalPublisher()
    : Node("student_publisher"), count_(0)
    {
      // 3-1.创建发布方;
      publisher_ = this->create_publisher<Student>("topic_stu", 10);
      // 3-2.创建定时器;
      timer_ = this->create_wall_timer(500ms, std::bind(&MinimalPublisher::timer_callback, this));
    }

  private:
    void timer_callback()
    {
      // 3-3.组织消息并发布。
      auto stu = Student();
      stu.name = "张三";
      stu.age = count_++;
      stu.height = 1.65;
      //发布方自己也能看到发布的消息
      RCLCPP_INFO(this->get_logger(), "学生信息:name=%s,age=%d,height=%.2f", stu.name.c_str(),stu.age,stu.height);
      publisher_->publish(stu);

    }
    rclcpp::TimerBase::SharedPtr timer_;
    rclcpp::Publisher<Student>::SharedPtr publisher_;
    size_t count_;
};

int main(int argc, char * argv[])
{
  // 2.初始化 ROS2 客户端;
  rclcpp::init(argc, argv);
  // 4.调用spin函数,并传入节点对象指针。
  rclcpp::spin(std::make_shared<MinimalPublisher>());
  // 5.释放资源;
  rclcpp::shutdown();
  return 0;
}

关键点:

1、头文件包含:#include "base_interfaces_demo/msg/student.hpp"

这个hpp里面放着自定义的消息结构体 数据类型 <base_interfaces_demo::msg::Student>,创建发布方以及回调函数初始化消息时候都会用到。

2.5.2.订阅方实现

功能包cpp01_topic的src目录下,新建C++文件demo02_listener_stu.cpp,并编辑文件,输入如下内容:

/*  
    需求:订阅发布方发布的学生消息,并输出到终端。
*/

// 1.包含头文件;
#include "rclcpp/rclcpp.hpp"
#include "base_interfaces_demo/msg/student.hpp"

using std::placeholders::_1;
using base_interfaces_demo::msg::Student;
// 3.定义节点类;
class MinimalSubscriber : public rclcpp::Node
{
  public:
    MinimalSubscriber()
    : Node("student_subscriber")
    {
      // 3-1.创建订阅方;
      subscription_ = this->create_subscription<Student>("topic_stu", 10, std::bind(&MinimalSubscriber::topic_callback, this, _1));
    }

  private:
    // 3-2.处理订阅到的消息;
    void topic_callback(const Student & msg) const
    {
      RCLCPP_INFO(this->get_logger(), "订阅的学生消息:name=%s,age=%d,height=%.2f", msg.name.c_str(),msg.age, msg.height);
    }
    rclcpp::Subscription<Student>::SharedPtr subscription_;
};

int main(int argc, char * argv[])
{
  // 2.初始化 ROS2 客户端;
  rclcpp::init(argc, argv);
  // 4.调用spin函数,并传入节点对象指针。
  rclcpp::spin(std::make_shared<MinimalSubscriber>());
  // 5.释放资源;
  rclcpp::shutdown();
  return 0;
}
2.5.3.编辑配置文件

package.xml无需修改(为什么?demo01已经加了吗),CMakeLists.txt文件需要添加如下内容:

add_executable(demo02_talker_stu src/demo02_talker_stu.cpp)
ament_target_dependencies(
  demo02_talker_stu
  "rclcpp"
  "std_msgs"
  "base_interfaces_demo"
)

add_executable(demo02_listener_stu src/demo02_listener_stu.cpp)
ament_target_dependencies(
  demo02_listener_stu
  "rclcpp"
  "std_msgs"
  "base_interfaces_demo"
)

文件中install修改为如下内容:

install(TARGETS 
  demo01_talker_string
  demo01_listener_string
  demo02_talker_stu
  demo02_listener_stu
  DESTINATION lib/${PROJECT_NAME})
2.5.4.编译

终端中进入当前工作空间,编译功能包:

colcon build --packages-select cpp01_topic
2.5.5.执行

当前工作空间下,启动两个终端,终端1执行发布程序,终端2执行订阅程序。

终端1输入如下指令:

. install/setup.bash
ros2 run cpp01_topic demo02_talker_stu

终端2输入如下指令:

. install/setup.bash 
ros2 run cpp01_topic demo02_listener_stu

最终运行结果与案例2类似。

一对python的发布订阅、一对c++的发布订阅(话题通信案例功能包中的四个可执行文件/节点),实现了结点之间的多对多话题通信。

也可以使用 rqt 查看节点之间的通信关系计算图:

3 服务通信
3.1?服务通信简介

场景:

????????服务通信也是ROS中一种极其常用的通信模式,服务通信是基于服务端-客户端模式的,是一种应答机制。也即:一个节点A向另一个节点B发送请求,B接收处理请求并产生响应结果返回给A。比如如下场景:

机器人巡逻过程中,控制系统分析传感器数据发现可疑物体或人... 此时需要请求拍摄照片并留存。

在上述场景中,就使用到了服务通信。

  • 数据分析节点A需要向相机相关节点B发送图片存储请求,节点B处理请求,并返回处理结果。

与上述应用类似的,服务通信更适用于对实时性有要求、具有一定逻辑处理的应用场景

概念:

????????服务通信中,服务端与客户端是一对多的关系,也即,同一服务话题下,存在多个客户端,每个客户端都可以向服务端发送请求。

作用:

????????用于偶然的、对实时性有要求、有一定逻辑处理需求的数据传输场景。

3.2案例以及案例分析
3.2.1.案例需求

需求:编写服务通信,客户端可以提交两个整数到服务端,服务端接收请求并解析两个整数求和,然后将结果响应回客户端。

3.2.2.案例分析

在上述案例中,需要关注的要素有三个:

  1. 客户端;

  2. 服务端;

  3. 消息载体。

3.2.3.流程简介

案例实现前,需要先自定义服务接口,接口准备完毕后,服务实现主要步骤如下:

  1. 编写服务端实现;

  2. 编写客户端实现;

  3. 编辑配置文件;

  4. 编译;

  5. 执行。

3.2.4.准备工作

终端下进入工作空间的src目录,调用如下两条命令分别创建C++功能包和Python功能包。

ros2 pkg create cpp02_service --build-type ament_cmake --dependencies rclcpp base_interfaces_demo
ros2 pkg create py02_service --build-type ament_python --dependencies rclpy base_interfaces_demo
3.3服务通信接口消息

直接就自定义消息??

定义服务接口消息与定义话题接口消息流程类似,主要步骤如下:

  1. 创建并编辑?.srv文件;

  2. 编辑配置文件;

  3. 编译;

  4. 测试。

接下来,我们可以参考案例编写一个srv文件,该文件中包含请求数据(两个整型字段)与响应数据(一个整型字段)。

3.3.1.创建并编辑 .srv 文件

????????功能包base_interfaces_demo下新建srv文件夹,srv文件夹下新建AddInts.srv(文件名开头要求大写)文件,文件中输入如下内容:

int32 num1
int32 num2
---
int32 sum
3.3.2.编辑配置文件

1.package.xml 文件

????????srv文件与msg文件的包依赖一致,如果你是新建的功能包添加srv文件,那么直接参考定义msg文件时package.xml 配置即可。由于我们使用的是base_interfaces_demo该包已经为msg文件配置过了依赖包,所以package.xml不需要做修改。

2.CMakeLists.txt 文件

????????如果是新建的功能包,与之前定义msg文件同理,为了将.srv文件转换成对应的C++和Python代码,还需要在CMakeLists.txt中添加如下配置:

find_package(rosidl_default_generators REQUIRED)

rosidl_generate_interfaces(${PROJECT_NAME}
  "srv/AddInts.srv"
)

????????不过,我们当前使用的base_interfaces_demo包,那么你只需要修改rosidl_generate_interfaces函数即可,修改后的内容如下:

rosidl_generate_interfaces(${PROJECT_NAME}
  "msg/Student.msg"
  "srv/AddInts.srv"
)
3.3.3.编译

终端中进入当前工作空间,编译功能包:

colcon build --packages-select base_interfaces_demo
3.3.4.测试

????????编译完成之后,在工作空间下的 install 目录下将生成AddInts.srv文件对应的C++和Python文件。

我们也可以在终端下进入工作空间,通过如下命令查看文件定义以及编译是否正常:

. install/setup.bash
ros2 interface show base_interfaces_demo/srv/AddInts

????????正常情况下,终端将会输出与AddInts.srv文件一致的内容。

?

3.4服务通信(C++)
3.4.1.服务端实现

????????功能包cpp02_service的src目录下,新建C++文件demo01_server.cpp,并编辑文件,输入如下内容:

/*  
  需求:编写服务端,接收客户端发送请求,提取其中两个整型数据,相加后将结果响应回客户端。
  步骤:
    1.包含头文件;
    2.初始化 ROS2 客户端;
    3.定义节点类;
      3-1.创建服务端;
      3-2.处理请求数据并响应结果。
    4.调用spin函数,并传入节点对象指针;
    5.释放资源。
*/

// 1.包含头文件;
#include "rclcpp/rclcpp.hpp"
#include "base_interfaces_demo/srv/add_ints.hpp"

using base_interfaces_demo::srv::AddInts;

using std::placeholders::_1;
using std::placeholders::_2;

// 3.定义节点类;
class MinimalService: public rclcpp::Node{
  public:
    MinimalService():Node("minimal_service"){
      // 3-1.创建服务端;
        //参数2里面有add回调函数
      server = this->create_service<AddInts>("add_ints",std::bind(&MinimalService::add, this, _1, _2));
      RCLCPP_INFO(this->get_logger(),"add_ints 服务端启动完毕,等待请求提交...");
    }
  private:
    rclcpp::Service<AddInts>::SharedPtr server;
    // 3-2.处理请求数据并响应结果。
    void add(const AddInts::Request::SharedPtr req,const AddInts::Response::SharedPtr res){
      res->sum = req->num1 + req->num2;
      RCLCPP_INFO(this->get_logger(),"请求数据:(%d,%d),响应结果:%d", req->num1, req->num2, res->sum);
    }
};

int main(int argc, char const *argv[])
{
  // 2.初始化 ROS2 客户端;
  rclcpp::init(argc,argv);

  // 4.调用spin函数,并传入节点对象指针;
  auto server = std::make_shared<MinimalService>();
  rclcpp::spin(server);

  // 5.释放资源。
  rclcpp::shutdown();
  return 0;
}

命令行测试工具-测试服务端代码:

3.4.2.客户端实现

????????功能包cpp02_service的src目录下,新建C++文件demo01_client.cpp,并编辑文件,输入如下内容:

/*  
  需求:编写客户端,发送两个整型变量作为请求数据,并处理响应结果。
  步骤:
    1.包含头文件;
    2.初始化 ROS2 客户端;
    3.定义节点类;
      3-1.创建客户端;
      3-2.连接服务器;
      3-3.组织请求数据并发送;
    4.创建对象指针调用其功能,并处理响应;
    5.释放资源。

*/
// 1.包含头文件;
#include "rclcpp/rclcpp.hpp"
#include "base_interfaces_demo/srv/add_ints.hpp"

using base_interfaces_demo::srv::AddInts;
using namespace std::chrono_literals;

// 3.定义节点类;
class MinimalClient: public rclcpp::Node{
  public:
    MinimalClient():Node("minimal_client"){
      // 3-1.创建客户端;
      client = this->create_client<AddInts>("add_ints");
      RCLCPP_INFO(this->get_logger(),"客户端创建,等待连接服务端!");
    }
    // 3-2.等待服务连接;
    bool connect_server(){
      while (!client->wait_for_service(1s))
      {//小bug:如果没有这个if分支,长久等待后,用户终止连接,日志还是会不断打印
        if (!rclcpp::ok()) //注意这个语法:判断是否有来自键盘的ctrl等结束进程信号,强制结束连接
        {
          RCLCPP_INFO(rclcpp::get_logger("rclcpp"),"强制退出!");
          return false;
        }

        RCLCPP_INFO(this->get_logger(),"服务连接中,请稍候...");
      }
      return true;
    }
    // 3-3.组织请求数据并发送;
    rclcpp::Client<AddInts>::FutureAndRequestId send_request(int32_t num1, int32_t num2){
     //发送数据时候,需要专门创建一个请求对象
      auto request = std::make_shared<AddInts::Request>();
      request->num1 = num1;
      request->num2 = num2;
      return client->async_send_request(request);
    }


  private:
    rclcpp::Client<AddInts>::SharedPtr client;
};

int main(int argc, char ** argv)
{
  if (argc != 3){
    RCLCPP_INFO(rclcpp::get_logger("rclcpp"),"请提交两个整型数据!");
    return 1;
  }

  // 2.初始化 ROS2 客户端;
  rclcpp::init(argc,argv);

  // 4.创建对象指针并调用其功能;
  auto client = std::make_shared<MinimalClient>();
  bool flag = client->connect_server();
  if (!flag)
  {
    RCLCPP_INFO(rclcpp::get_logger("rclcpp"),"服务连接失败!");
    return 0;
  }

  auto response = client->send_request(atoi(argv[1]),atoi(argv[2]));

  // 处理响应,比对响应结束的状态码
  if (rclcpp::spin_until_future_complete(client,response) == rclcpp::FutureReturnCode::SUCCESS)
  {
    RCLCPP_INFO(client->get_logger(),"请求正常处理");
    RCLCPP_INFO(client->get_logger(),"响应结果:%d!", response.get()->sum);

  } else {
    RCLCPP_INFO(client->get_logger(),"请求异常");
  }

  // 5.释放资源。
  rclcpp::shutdown();
  return 0;
}
3.4.3.编辑配置文件

1.packages.xml

在创建功能包时,所依赖的功能包已经自动配置了,配置内容如下:

<depend>rclcpp</depend>
<depend>base_interfaces_demo</depend>

2.CMakeLists.txt

CMakeLists.txt 中服务端和客户端程序核心配置如下:

find_package(ament_cmake REQUIRED)
find_package(rclcpp REQUIRED)
find_package(base_interfaces_demo REQUIRED)

add_executable(demo01_server src/demo01_server.cpp)
ament_target_dependencies(
  demo01_server
  "rclcpp"
  "base_interfaces_demo"
)

add_executable(demo01_client src/demo01_client.cpp)
ament_target_dependencies(
  demo01_client
  "rclcpp"
  "base_interfaces_demo"
)

install(TARGETS 
  demo01_server
  demo02_client
  DESTINATION lib/${PROJECT_NAME})
3.4.4.编译

终端中进入当前工作空间,编译功能包:

colcon build --packages-select cpp02_service
3.4.5.执行

当前工作空间下,启动两个终端,终端1执行服务端程序,终端2执行客户端程序。

终端1输入如下指令:

. install/setup.bash
ros2 run cpp02_service demo01_server

终端2输入如下指令:

. install/setup.bash
ros2 run cpp02_service demo02_client 100 200

最终运行结果与案例类似。

4 动作通信
4.1?动作通信简介
场景:

????????机器人导航到某个目标点:导航过程中,可以连续反馈当前机器人状态信息(类似于话题通信);当导航终止时,再返回最终的执行结果(类似于服务通信)。

????????在ROS中,该实现策略称之为:action 通信。

概念:

????????动作通信适用于长时间运行的任务

????????就结构而言动作通信由目标、反馈和结果三部分组成;

????????就功能而言,动作通信类似于服务通信,动作客户端可以发送请求到动作服务端,并接收动作服务端响应的最终结果,不过动作通信可以在请求响应过程中获取连续反馈,并且也可以向动作服务端发送任务取消请求;

????????就底层实现而言,动作通信是建立在话题通信和服务通信之上的,目标发送实现是对服务通信的封装,结果的获取也是对服务通信的封装,而连续反馈则是对话题通信的封装。

过程分析如下:

作用:

一般适用于耗时的请求响应场景,用以获取连续的状态反馈。

4.2?案例以及案例分析
1.案例需求

????????需求:编写动作通信,动作客户端提交一个整型数据N,动作服务端接收请求数据并累加1-N之间的所有整数,将最终结果返回给动作客户端,且每累加一次都需要计算当前运算进度并反馈给动作客户端。

图片

2.案例分析

在上述案例中,需要关注的要素有三个:

  1. 动作客户端;

  2. 动作服务端;

  3. 消息载体。

3.流程简介

案例实现前需要先自定义动作接口,接口准备完毕后,动作通信实现主要步骤如下:

  1. 编写动作服务端实现;

  2. 编写动作客户端实现;

  3. 编辑配置文件;

  4. 编译;

  5. 执行。

案例我们会采用C++和Python分别实现,二者都遵循上述实现流程。

4.准备工作

终端下进入工作空间的src目录,调用如下两条命令分别创建C++功能包和Python功能包。

ros2 pkg create cpp03_action --build-type ament_cmake --dependencies rclcpp rclcpp_action base_interfaces_demo
ros2 pkg create py03_action --build-type ament_python --dependencies rclpy base_interfaces_demo
4.3?动作通信接口消息

定义动作接口消息与定义话题或服务接口消息流程类似,主要步骤如下:

  1. 创建并编辑.action文件;

  2. 编辑配置文件;

  3. 编译;

  4. 测试。

接下来,我们可以参考案例编写一个action文件,该文件中包含请求数据(一个整型字段)、响应数据(一个整型字段)和连续反馈数据(一个浮点型字段)。

1.创建并编辑 .action 文件

功能包base_interfaces_demo下新建action文件夹,action文件夹下新建Progress.action文件,文件中输入如下内容:

int64 num
---
int64 sum
---
float64 progress

请求数据

---

响应结果

---

进度

2.编辑配置文件

1.package.xml

如果单独构建action功能包,需要在package.xml中需要添加一些依赖包,具体内容如下:

<buildtool_depend>rosidl_default_generators</buildtool_depend>
<depend>action_msgs</depend>
<member_of_group>rosidl_interface_packages</member_of_group>

当前使用的是 base_interfaces_demo 功能包,已经为 msg 、srv 文件添加过了一些依赖,所以 package.xml 中添加如下内容即可:

<buildtool_depend>rosidl_default_generators</buildtool_depend>
<depend>action_msgs</depend>

2.CMakeLists.txt

如果是新建的功能包,与之前定义msg、srv文件同理,为了将.action文件转换成对应的C++和Python代码,还需要在CMakeLists.txt 中添加如下配置:

find_package(rosidl_default_generators REQUIRED)

rosidl_generate_interfaces(${PROJECT_NAME}
  "action/Progress.action"
)

不过,我们当前使用的base_interfaces_demo包,那么只需要修改rosidl_generate_interfaces函数即可,修改后的内容如下:

rosidl_generate_interfaces(${PROJECT_NAME}
  "msg/Student.msg"
  "srv/AddInts.srv"
  "action/Progress.action"
)
3.编译

终端中进入当前工作空间,编译功能包:

colcon build --packages-select base_interfaces_demo
4.测试

编译完成之后,在工作空间下的 install 目录下将生成Progress.action文件对应的C++和Python文件,我们也可以在终端下进入工作空间,通过如下命令查看文件定义以及编译是否正常:

. install/setup.bash
ros2 interface show base_interfaces_demo/action/Progress

正常情况下,终端将会输出与Progress.action文件一致的内容。

(暂时看到这)4.4?动作通信(C++)
1.动作服务端实现

????????功能包cpp03_action的src目录下,新建C++文件demo01_action_server.cpp,并编辑文件,输入如下内容:

/*  
  需求:编写动作服务端实习,可以提取客户端请求提交的整型数据,并累加从1到该数据之间的所有整数以求和,
       每累加一次都计算当前运算进度并连续反馈回客户端,最后,在将求和结果返回给客户端。
  步骤:
    1.包含头文件;
    2.初始化 ROS2 客户端;
    3.定义节点类;
      3-1.创建动作服务端;
      3-2.处理请求数据;
      3-3.处理取消任务请求;
      3-4.生成连续反馈。
    4.调用spin函数,并传入节点对象指针;
    5.释放资源。

*/
// 1.包含头文件;
#include "rclcpp/rclcpp.hpp"
#include "rclcpp_action/rclcpp_action.hpp" //依赖的这个功能包主要就是创建动作通信对象的
#include "base_interfaces_demo/action/progress.hpp"

using namespace std::placeholders;
using base_interfaces_demo::action::Progress;
using GoalHandleProgress = rclcpp_action::ServerGoalHandle<Progress>;

// 3.定义节点类;
class MinimalActionServer : public rclcpp::Node
{
public:

  explicit MinimalActionServer(const rclcpp::NodeOptions & options = rclcpp::NodeOptions())
  : Node("minimal_action_server", options)
  {
    // 3-1.创建动作服务端;
    //动作通信节点的创建不再是 rclcpp::Node 中的成员函数,而是 rclcpp_action中的库函数
    this->action_server_ = rclcpp_action::create_server<Progress>(
      this,
      "get_sum",
      //处理请求、接受取消、过程与结果反馈全是由回调函数实现。
      std::bind(&MinimalActionServer::handle_goal, this, _1, _2),
      std::bind(&MinimalActionServer::handle_cancel, this, _1),
      std::bind(&MinimalActionServer::handle_accepted, this, _1));
    RCLCPP_INFO(this->get_logger(),"动作服务端创建,等待请求...");
  }

private:
  rclcpp_action::Server<Progress>::SharedPtr action_server_;

  // 3-2.处理请求数据;
  rclcpp_action::GoalResponse handle_goal(const rclcpp_action::GoalUUID & uuid,std::shared_ptr<const Progress::Goal> goal)
  {
    (void)uuid;
    RCLCPP_INFO(this->get_logger(), "接收到动作客户端请求,请求数字为 %ld", goal->num);
    if (goal->num < 1) {
      return rclcpp_action::GoalResponse::REJECT;
    }
    return rclcpp_action::GoalResponse::ACCEPT_AND_EXECUTE;
  }

  // 3-3.处理取消任务请求;
  rclcpp_action::CancelResponse handle_cancel(
    const std::shared_ptr<GoalHandleProgress> goal_handle)
  {
    (void)goal_handle;
    RCLCPP_INFO(this->get_logger(), "接收到任务取消请求");
    return rclcpp_action::CancelResponse::ACCEPT;
  }

  void execute(const std::shared_ptr<GoalHandleProgress> goal_handle)
  {
    RCLCPP_INFO(this->get_logger(), "开始执行任务");
    rclcpp::Rate loop_rate(10.0);
    const auto goal = goal_handle->get_goal();
    auto feedback = std::make_shared<Progress::Feedback>();
    auto result = std::make_shared<Progress::Result>();
    int64_t sum= 0;
    for (int i = 1; (i <= goal->num) && rclcpp::ok(); i++) {
      sum += i;
      // Check if there is a cancel request
      if (goal_handle->is_canceling()) {
        result->sum = sum;
        goal_handle->canceled(result);
        RCLCPP_INFO(this->get_logger(), "任务取消");
        return;
      }
      feedback->progress = (double_t)i / goal->num;
      goal_handle->publish_feedback(feedback);
      RCLCPP_INFO(this->get_logger(), "连续反馈中,进度:%.2f", feedback->progress);

      loop_rate.sleep();
    }

    if (rclcpp::ok()) {
      result->sum = sum;
      //结果反馈和过程反馈放在一块
      goal_handle->succeed(result);
      RCLCPP_INFO(this->get_logger(), "任务完成!");
    }
  }

  // 3-4.生成连续反馈。
  void handle_accepted(const std::shared_ptr<GoalHandleProgress> goal_handle)
  {
    std::thread{std::bind(&MinimalActionServer::execute, this, _1), goal_handle}.detach();
  }
}; 

int main(int argc, char ** argv)
{
  // 2.初始化 ROS2 客户端;
  rclcpp::init(argc, argv);
  // 4.调用spin函数,并传入节点对象指针;
  auto action_server = std::make_shared<MinimalActionServer>();
  rclcpp::spin(action_server);
  // 5.释放资源。
  rclcpp::shutdown();
  return 0;
}
2.动作客户端实现

????????功能包cpp03_action的src目录下,新建C++文件demo02_action_client.cpp,并编辑文件,输入如下内容:

/*  
  需求:编写动作客户端实现,可以提交一个整型数据到服务端,并处理服务端的连续反馈以及最终返回结果。
  步骤:
    1.包含头文件;
    2.初始化 ROS2 客户端;
    3.定义节点类;
      3-1.创建动作客户端;
      3-2.发送请求;
      3-3.处理目标发送后的反馈;
      3-4.处理连续反馈;
      3-5.处理最终响应。
    4.调用spin函数,并传入节点对象指针;
    5.释放资源。
*/
// 1.包含头文件;
#include "rclcpp/rclcpp.hpp"
#include "rclcpp_action/rclcpp_action.hpp"
#include "base_interfaces_demo/action/progress.hpp"

using base_interfaces_demo::action::Progress;
using GoalHandleProgress = rclcpp_action::ClientGoalHandle<Progress>;
using namespace std::placeholders;

// 3.定义节点类;
class MinimalActionClient : public rclcpp::Node
{
public:

  explicit MinimalActionClient(const rclcpp::NodeOptions & node_options = rclcpp::NodeOptions())
  : Node("minimal_action_client", node_options)
  {
    // 3-1.创建动作客户端;
    this->client_ptr_ = rclcpp_action::create_client<Progress>(this,"get_sum");
  }

  // 3-2.发送请求;
  void send_goal(int64_t num)
  {

    if (!this->client_ptr_) {
      RCLCPP_ERROR(this->get_logger(), "动作客户端未被初始化。");
    }

    if (!this->client_ptr_->wait_for_action_server(std::chrono::seconds(10))) {
      RCLCPP_ERROR(this->get_logger(), "服务连接失败!");
      return;
    }

    auto goal_msg = Progress::Goal();
    goal_msg.num = num;
    RCLCPP_INFO(this->get_logger(), "发送请求数据!");

    auto send_goal_options = rclcpp_action::Client<Progress>::SendGoalOptions();
    send_goal_options.goal_response_callback =std::bind(&MinimalActionClient::goal_response_callback, this, _1);
    send_goal_options.feedback_callback =std::bind(&MinimalActionClient::feedback_callback, this, _1, _2);
    send_goal_options.result_callback =std::bind(&MinimalActionClient::result_callback, this, _1);
    auto goal_handle_future = this->client_ptr_->async_send_goal(goal_msg, send_goal_options);
  }

private:
  rclcpp_action::Client<Progress>::SharedPtr client_ptr_;

  // 3-3.处理目标发送后的反馈;
  void goal_response_callback(GoalHandleProgress::SharedPtr goal_handle)
  {
    if (!goal_handle) {
      RCLCPP_ERROR(this->get_logger(), "目标请求被服务器拒绝!");
    } else {
      RCLCPP_INFO(this->get_logger(), "目标被接收,等待结果中");
    }
  }

  // 3-4.处理连续反馈;
  void feedback_callback(GoalHandleProgress::SharedPtr,const std::shared_ptr<const Progress::Feedback> feedback)
  {
    int32_t progress = (int32_t)(feedback->progress * 100);
    RCLCPP_INFO(this->get_logger(), "当前进度: %d%%", progress);
  }

  // 3-5.处理最终响应。
  void result_callback(const GoalHandleProgress::WrappedResult & result)
  {
    switch (result.code) {
      case rclcpp_action::ResultCode::SUCCEEDED:
        break;
      case rclcpp_action::ResultCode::ABORTED:
        RCLCPP_ERROR(this->get_logger(), "任务被中止");
        return;
      case rclcpp_action::ResultCode::CANCELED:
        RCLCPP_ERROR(this->get_logger(), "任务被取消");
        return;
      default:
        RCLCPP_ERROR(this->get_logger(), "未知异常");
        return;
    }

    RCLCPP_INFO(this->get_logger(), "任务执行完毕,最终结果: %ld", result.result->sum);
  }
}; 

int main(int argc, char ** argv)
{
  // 2.初始化 ROS2 客户端;
  rclcpp::init(argc, argv);

  // 4.调用spin函数,并传入节点对象指针;
  auto action_client = std::make_shared<MinimalActionClient>();
  action_client->send_goal(10);
  rclcpp::spin(action_client);
  // 5.释放资源。
  rclcpp::shutdown();
  return 0;
}
3.编辑配置文件

1.packages.xml

????????在创建功能包时,所依赖的其他功能包已经自动配置了,配置内容如下:

<depend>rclcpp</depend>
<depend>rclcpp_action</depend>
<depend>base_interfaces_demo</depend>

2.CMakeLists.txt

CMakeLists.txt中服务端和客户端程序核心配置如下:

find_package(rclcpp REQUIRED)
find_package(rclcpp_action REQUIRED)
find_package(base_interfaces_demo REQUIRED)

add_executable(demo01_action_server src/demo01_action_server.cpp)
ament_target_dependencies(
  demo01_action_server
  "rclcpp"
  "rclcpp_action"
  "base_interfaces_demo"
)

add_executable(demo02_action_client src/demo02_action_client.cpp)
ament_target_dependencies(
  demo02_action_client
  "rclcpp"
  "rclcpp_action"
  "base_interfaces_demo"
)

install(TARGETS 
  demo01_action_server
  demo02_action_client
  DESTINATION lib/${PROJECT_NAME})
4.编译

终端中进入当前工作空间,编译功能包:

colcon build --packages-select cpp03_action
5.执行

当前工作空间下,启动两个终端,终端1执行动作服务端程序,终端2执行动作客户端程序。

终端1输入如下指令:

. install/setup.bash
ros2 run cpp03_action demo01_action_server

终端2输入如下指令:

. install/setup.bash
ros2 run cpp03_action demo02_action_client

最终运行结果与案例类似。

整个回顾一下接口文件!!!
5 参数服务
5.1 概念简介
场景

在机器人系统中不同的功能模块可能会使用到一些相同的数据,比如:

导航实现时,会进行路径规划,路径规划主要包含, 全局路径规划和本地路径规划,所谓全局路径规划就是设计一个从出发点到目标点的大致路径,而本地路径规划,则是根据车辆当前路况生成实时的行进路径。两种路径规划实现,都会使用到车辆的尺寸数据——长度、宽度、高度等。那么这些通用数据在程序中应该如何存储、调用呢?

上述场景中,就可以使用参数服务实现,在一个节点下保存车辆尺寸数据,其他节点可以访问该节点并操作这些数据。

概念

参数服务是以共享的方式实现不同节点之间数据交互的一种通信模式。保存参数的节点称之为参数服务端,调用参数的节点称之为参数客户端。参数客户端与参数服务端的交互是基于请求响应的,且参数通信的实现本质上对服务通信的进一步封装。

作用

参数服务保存的数据类似于编程中“全局变量”的概念,可以在不同的节点之间共享数据。

5.2?案例以及案例分析
1.案例需求

需求:在参数服务端设置一些参数,参数客户端访问服务端并操作这些参数。

图片

2.案例分析

在上述案例中,需要关注的要素有三个:

  1. 参数客户端;

  2. 参数服务端;

  3. 参数。

3.流程简介

案例实现前需要先了解ROS2中参数的相关API,无论是客户端还是服务端都会使用到参数,而参数服务案例实现主要步骤如下:

  1. 编写参数服务端实现;

  2. 编写参数客户端实现;

  3. 编辑配置文件;

  4. 编译;

  5. 执行。

案例我们会采用C++和Python分别实现,二者都遵循上述实现流程。

4.准备工作

终端下进入工作空间的src目录,调用如下两条命令分别创建C++功能包和Python功能包。

ros2 pkg create cpp04_param --build-type ament_cmake --dependencies rclcpp
ros2 pkg create py04_param --build-type ament_python --dependencies rclpy

和描述符三部分组成,其中键是字符串类型,值可以是bool、int64、float64、string、byte[]、bool[]、int64[]、float64[]、string[]中的任一类型,描述符默认情况下为空,但是可以设置参数描述、参数数据类型、取值范围或其他约束等信息。

为了方便操作,参数被封装为了相关类,其中C++客户端对应的类是rclcpp::Parameter,Python客户端对应的类是rclpy.Parameter。借助于相关API,我们可以实现参数对象创建以及参数属性解析等操作。以下代码提供了参数相关API基本使用的示例。

C++示例:

...
// 创建参数对象
rclcpp::Parameter p1("car_name","Tiger"); //参数值为字符串类型
rclcpp::Parameter p2("width",0.15); //参数值为浮点类型
rclcpp::Parameter p3("wheels",2); //参数值为整型

// 获取参数值并转换成相应的数据类型
RCLCPP_INFO(rclcpp::get_logger("rclcpp"),"car_name = %s", p1.as_string().c_str());
RCLCPP_INFO(rclcpp::get_logger("rclcpp"),"width = %.2f", p2.as_double());
RCLCPP_INFO(rclcpp::get_logger("rclcpp"),"wheels = %ld", p3.as_int());

// 获取参数的键
RCLCPP_INFO(rclcpp::get_logger("rclcpp"),"p1 name = %s", p1.get_name().c_str());
// 获取参数数据类型
RCLCPP_INFO(rclcpp::get_logger("rclcpp"),"p1 type_name = %s", p1.get_type_name().c_str());
// 将参数值转换成字符串类型
RCLCPP_INFO(rclcpp::get_logger("rclcpp"),"p1 value_to_msg = %s", p1.value_to_string().c_str());
...

关于参数具体的API使用,在后续案例中会有介绍。

5.3?参数服务(C++)
1.参数服务端

功能包cpp04_param的src目录下,新建C++文件demo01_param_server.cpp,并编辑文件,输入如下内容:

/*
    需求:编写参数服务端,设置并操作参数。
    步骤:
        1.包含头文件;
        2.初始化 ROS2 客户端;
        3.定义节点类;
            3-1.声明参数;
            3-2.查询参数;
            3-3.修改参数;
            3-4.删除参数。
        4.创建节点对象指针,调用参数操作函数,并传递给spin函数;
        5.释放资源。

*/

// 1.包含头文件;
#include "rclcpp/rclcpp.hpp"

// 3.定义节点类;
class MinimalParamServer: public rclcpp::Node{
    public:
        MinimalParamServer():Node("minimal_param_server",rclcpp::NodeOptions()
                .allow_undeclared_parameters(true)
                ){       
        }
        // 3-1.声明参数;
        void declare_param(){
            // 声明参数并设置默认值
            this->declare_parameter("car_type","Tiger"); 
            this->declare_parameter("height",1.50); 
            this->declare_parameter("wheels",4);   
            // 需要设置 rclcpp::NodeOptions().allow_undeclared_parameters(true),否则非法 
            this->set_parameter(rclcpp::Parameter("undcl_test",100));
        }
        // 3-2.查询参数
        void get_param(){
            RCLCPP_INFO(this->get_logger(),"------------------查----------------");
            // 获取指定
            rclcpp::Parameter car_type = this->get_parameter("car_type");
            RCLCPP_INFO(this->get_logger(),"car_type:%s",car_type.as_string().c_str());
            RCLCPP_INFO(this->get_logger(),"height:%.2f",this->get_parameter("height").as_double());
            RCLCPP_INFO(this->get_logger(),"wheels:%ld",this->get_parameter("wheels").as_int());
            RCLCPP_INFO(this->get_logger(),"undcl_test:%ld",this->get_parameter("undcl_test").as_int());
            // 判断包含
            RCLCPP_INFO(this->get_logger(),"包含car_type? %d",this->has_parameter("car_type"));
            RCLCPP_INFO(this->get_logger(),"包含car_typesxxxx? %d",this->has_parameter("car_typexxxx"));
            // 获取所有
            auto params = this->get_parameters({"car_type","height","wheels"});
            for (auto &param : params)
            {
                RCLCPP_INFO(this->get_logger(),"name = %s, value = %s", param.get_name().c_str(), param.value_to_string().c_str());

            }
        }
        // 3-3.修改参数
        void update_param(){
            RCLCPP_INFO(this->get_logger(),"------------------改----------------");
            this->set_parameter(rclcpp::Parameter("height",1.75));
            RCLCPP_INFO(this->get_logger(),"height:%.2f",this->get_parameter("height").as_double());
        }
        // 3-4.删除参数
        void del_param(){
            RCLCPP_INFO(this->get_logger(),"------------------删----------------");
            // this->undeclare_parameter("car_type");
            // RCLCPP_INFO(this->get_logger(),"删除操作后,car_type还存在马? %d",this->has_parameter("car_type"));
            RCLCPP_INFO(this->get_logger(),"删除操作前,undcl_test存在马? %d",this->has_parameter("undcl_test"));
            this->undeclare_parameter("undcl_test");
            RCLCPP_INFO(this->get_logger(),"删除操作前,undcl_test存在马? %d",this->has_parameter("undcl_test"));
        }
};

int main(int argc, char ** argv)
{
    // 2.初始化 ROS2 客户端;
    rclcpp::init(argc,argv);

    // 4.创建节点对象指针,调用参数操作函数,并传递给spin函数;
    auto paramServer= std::make_shared<MinimalParamServer>();
    paramServer->declare_param();
    paramServer->get_param();
    paramServer->update_param();
    paramServer->del_param();
    rclcpp::spin(paramServer);

    // 5.释放资源。
    rclcpp::shutdown();
    return 0;
}
2.参数客户端

功能包cpp04_param的src目录下,新建C++文件demo02_param_client.cpp,并编辑文件,输入如下内容:

/*
    需求:编写参数客户端,获取或修改服务端参数。
    步骤:
        1.包含头文件;
        2.初始化 ROS2 客户端;
        3.定义节点类;
            3-1.查询参数;
            3-2.修改参数;
        4.创建节点对象指针,调用参数操作函数;
        5.释放资源。
*/

// 1.包含头文件;
#include "rclcpp/rclcpp.hpp"

using namespace std::chrono_literals;

// 3.定义节点类;
class MinimalParamClient: public rclcpp::Node {
    public:
        MinimalParamClient():Node("paramDemoClient_node"){
            paramClient = std::make_shared<rclcpp::SyncParametersClient>(this,"minimal_param_server");
        }
        bool connect_server(){
            // 等待服务连接
            while (!paramClient->wait_for_service(1s))
            {
                if (!rclcpp::ok())
                {
                   return false;
                }  
                RCLCPP_INFO(this->get_logger(),"服务未连接");
            }

            return true;

        }

        // 3-1.查询参数;
        void get_param(){
            RCLCPP_INFO(this->get_logger(),"-----------参数客户端查询参数-----------");
            double height = paramClient->get_parameter<double>("height");
            RCLCPP_INFO(this->get_logger(),"height = %.2f", height);
            RCLCPP_INFO(this->get_logger(),"car_type 存在吗?%d", paramClient->has_parameter("car_type"));
            auto params = paramClient->get_parameters({"car_type","height","wheels"});
            for (auto &param : params)
            {
                RCLCPP_INFO(this->get_logger(),"%s = %s", param.get_name().c_str(),param.value_to_string().c_str());
            }


        }
        // 3-2.修改参数;
        void update_param(){
            RCLCPP_INFO(this->get_logger(),"-----------参数客户端修改参数-----------");
            paramClient->set_parameters({rclcpp::Parameter("car_type","Mouse"),
            rclcpp::Parameter("height",2.0),
            //这是服务端不存在的参数,只有服务端设置了rclcpp::NodeOptions().allow_undeclared_parameters(true)时,
            // 这个参数才会被成功设置。
            rclcpp::Parameter("width",0.15),
            rclcpp::Parameter("wheels",6)});
        }

    private:
        rclcpp::SyncParametersClient::SharedPtr paramClient;
};

int main(int argc, char const *argv[])
{
    // 2.初始化 ROS2 客户端;
    rclcpp::init(argc,argv);

    // 4.创建节点对象指针,调用参数操作函数;
    auto paramClient = std::make_shared<MinimalParamClient>();
    bool flag = paramClient->connect_server();
    if(!flag){
        return 0;
    }
    paramClient->get_param();
    paramClient->update_param();
    paramClient->get_param();

    // 5.释放资源。
    rclcpp::shutdown();
    return 0;
}
3.编辑配置文件

1.packages.xml

在创建功能包时,所依赖的功能包已经自动配置了,配置内容如下:

<depend>rclcpp</depend>

2.CMakeLists.txt

CMakeLists.txt中参数服务端和参数客户端程序核心配置如下:

find_package(rclcpp REQUIRED)

add_executable(demo01_param_server src/demo01_param_server.cpp)
ament_target_dependencies(
  demo01_param_server
  "rclcpp"
)
add_executable(demo02_param_client src/demo02_param_client.cpp)
ament_target_dependencies(
  demo02_param_client
  "rclcpp"
)

install(TARGETS 
  demo01_param_server
  demo02_param_client
  DESTINATION lib/${PROJECT_NAME})
4.编译

终端中进入当前工作空间,编译功能包:

colcon build --packages-select cpp04_param
5.执行

当前工作空间下,启动两个终端,终端1执行参数服务端程序,终端2执行参数客户端程序。

终端1输入如下指令:

. install/setup.bash
ros2 run cpp04_param demo01_param_server

终端2输入如下指令:

. install/setup.bash
ros2 run cpp04_param demo02_param_client

最终运行结果与案例类似。

6 本章小结

本章主要介绍了ROS2中常用的四种通信机制:

  • 话题通信;

  • 服务通信;

  • 动作通信;

  • 参数服务。

????????无论何种通信机制,他们的实现框架都是类似的。比如:通信必然涉及到双方,双方需要通过“话题”关联,通信还都必然涉及到数据,一般可以通过接口文件来定义数据格式(参数服务是通过参数类封装数据)。

????????不同的通信机制其实现模型也存在明显差异。话题通信是基于广播的单向数据交互模式;服务通信是基于请求响应的问答式交数据互模式;动作通信则是在请求响应的过程中又包含连续反馈的数据交互模式;参数服务是基于服务通信的,可以在不同节点间实现数据共享。实现模型的差异也决定着他们有着不同的应用场景,大家可以根据自己的实际需求灵活选择。

第三章? Ros2通信机制补充

后续章节:

https://blog.csdn.net/weixin_46697509/article/details/135081379

文章来源:https://blog.csdn.net/weixin_46697509/article/details/135000917
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。