ROS2理论入门

写在前面:本文章是基于赵老师的视频后,所做笔记


1. ROS快速体验

ROS2中涉及的编程语言以C++和Python为主,ROS2中的大多数功能两者都可以实现,在本系列教程中,如无特殊情况,每一个案例也会用C++方案演示。

我们将介绍一个最基本的案例——ROS2版本的HelloWorld,通过学习本节内容,你可以了解ROS2程序的编写、编译与执行流程。

案例:

(1)准备:无论是使用C++还是Python编写ROS2程序,都需要依赖于工作空间,在此,我们先实现工作空间的创建与编译,打开终端,输入如下指令:

mkdir -p ws00_helloworld/src #创建工作空间以及子级目录 src,工作空间名称可以自定义
cd ws00_helloworld #进入工作空间
colcon build #编译

工作空间创建完毕后,我么可以在工作空间下的src目录中编写C++或Python程序,且两种语言的实现流程大致一致,主要包含如下步骤:

  1. 创建功能包;2. 编辑源文件;3. 编辑配置文件;4. 编译;5. 执行。

1. 创建功能包

终端下,进入ws00_helloworld/src目录,使用如下指令创建一个C++功能包:

ros2 pkg create pkg01_helloworld_cpp --build-type ament_cmake --dependencies rclcpp --node-name helloworld

2. 编辑源文件

进入pkg01_helloworld_cpp/src目录,该目录下有一helloworld.cpp文件,修改文件内容如下:

#include "rclcpp/rclcpp.hpp"

int main(int argc, char ** argv)
{
  // 初始化 ROS2
  rclcpp::init(argc,argv);
  // 创建节点
  auto node = rclcpp::Node::make_shared("helloworld_node");
  // 输出文本
  RCLCPP_INFO(node->get_logger(),"hello world!");
  // 释放资源
  rclcpp::shutdown();
  return 0;
}

ROS2理论入门_第1张图片

3. 编译配置文件

在步骤1创建功能包时所使用的指令已经默认生成且配置了配置文件,不过实际应用中经常需要自己编辑配置文件,所以在此对相关内容做简单介绍,所使用的配置文件主要有两个,分别是功能包下的package.xml与CMakeLists.txt。

1. package.xml




  pkg01_helloworld_cpp
  0.0.0
  TODO: Package description
  ros2
  TODO: License declaration

  ament_cmake

  
  rclcpp

  ament_lint_auto
  ament_lint_common

  
    ament_cmake
  

2. CMakeLists.txt

文件内容如下:

cmake_minimum_required(VERSION 3.8)
project(pkg01_helloworld_cpp)

if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
  add_compile_options(-Wall -Wextra -Wpedantic)
endif()

# find dependencies
find_package(ament_cmake REQUIRED)
# 引入外部依赖包
find_package(rclcpp REQUIRED)

# 映射源文件与可执行文件
add_executable(helloworld src/helloworld.cpp)
# 设置目标依赖库
ament_target_dependencies(
  helloworld
  "rclcpp"
)
# 定义安装规则
install(TARGETS helloworld
  DESTINATION lib/${PROJECT_NAME})

if(BUILD_TESTING)
  find_package(ament_lint_auto REQUIRED)
  # the following line skips the linter which checks for copyrights
  # comment the line when a copyright and license is added to all source files
  set(ament_cmake_copyright_FOUND TRUE)
  # the following line skips cpplint (only works in a git repo)
  # comment the line when this package is in a git repo and when
  # a copyright and license is added to all source files
  set(ament_cmake_cpplint_FOUND TRUE)
  ament_lint_auto_find_test_dependencies()
endif()

ament_package()

4. 编译

终端下进入到工作空间,执行如下指令:

colcon build

5. 执行

终端下进入到工作空间,执行如下指令:

. install/setup.bash
ros2 run pkg01_helloworld_cpp helloworld

效果:

可以看到在最后一行成功输出了


6. 运行优化

每次终端中执行工作空间下的节点时,都需要调用. install/setup.bash指令,使用不便,优化策略是,可以将该指令的调用添加进~/setup.bash,操作格式如下

7. 在VScode下配置

ROS2理论入门_第2张图片

然后打开修复

ROS2理论入门_第3张图片

修改的实质是.vscode配置文件

使用快捷键:ctrl+~ 快捷打开终端  ctrl+s保存,然后进行编译,运行

ROS2理论入门_第4张图片

 注意节点不能有空格

二、 ROS2文件系统

立足系统架构,如下图所示,ROS2可以划分为三层:

  • 操作系统层(OS Layer)

    如前所述,ROS虽然称之为机器人操作系统,但实质只是构建机器人应用程序的软件开发工具包,ROS必须依赖于传统意义的操作系统,目前ROS2可以运行在Linux、Windows、Mac或RTOS上。 实质上是个SDK

  • 中间层(Middleware Layer)

    主要由数据分发服务DDS与ROS2封装的关于机器人开发的中间件组成。DDS是一种去中心化的数据通讯方式,ROS2还引入了服务质量管理 (Quality of Service)机制,借助该机制可以保证在某些较差网络环境下也可以具备良好的通讯效果。ROS2中间件则主要由客户端库、DDS抽象层与进程内通讯API构成。

  • 应用层(Application Layer)

    是指开发者构建的应用程序,在应用程序中是以功能包为核心的,在功能包中可以包含源码、数据定义、接口等内容。

1.概览

功能包是ROS2应用程序的核心,但是功能包不能直接构建,必须依赖于工作空间,一个ROS2工作空间的目录结构如下:

WorkSpace --- 自定义的工作空间。
    |--- build:存储中间文件的目录,该目录下会为每一个功能包创建一个单独子目录。
    |--- install:安装目录,该目录下会为每一个功能包创建一个单独子目录。
    |--- log:日志目录,用于存储日志文件。
    |--- src:用于存储功能包源码的目录。
        |-- C++功能包
            |-- package.xml:包信息,比如:包名、版本、作者、依赖项。
            |-- CMakeLists.txt:配置编译规则,比如源文件、依赖项、目标文件。
            |-- src:C++源文件目录。
            |-- include:头文件目录。
            |-- msg:消息接口文件目录。
            |-- srv:服务接口文件目录。
            |-- action:动作接口文件目录。
        |-- Python功能包
            |-- package.xml:包信息,比如:包名、版本、作者、依赖项。
            |-- setup.py:与C++功能包的CMakeLists.txt类似。
            |-- setup.cfg:功能包基本配置文件。
            |-- resource:资源目录。
            |-- test:存储测试相关文件。
            |-- 功能包同名目录:Python源文件目录。

另外,无论是Python功能包还是C++功能包,都可以自定义一些配置文件相关的目录。

|-- C++或Python功能包
    |-- launch:存储launch文件。
    |-- rviz:存储rviz2配置相关文件。
    |-- urdf:存储机器人建模文件。
    |-- params:存储参数文件。
    |-- world:存储仿真环境相关文件。
    |-- map:存储导航所需地图文件。
    |-- ......

源文件说明:

在实现第一个ROS2程序时,都需要创建节点,无论是C++实现还是Python实现,都是直接实例化的Node对象。

C++实例化Node示例如下:(推荐)

#include "rclcpp/rclcpp.hpp"

#创建节点
class MyNode: public rclcpp::Node{
public:
    MyNode():Node("node_name"){
        RCLCPP_INFO(this->get_logger(),"hello world!");
    }

};

int main(int argc, char *argv[])
{
    rclcpp::init(argc,argv);
    auto node = std::make_shared();
    rclcpp::shutdown();
    return 0;
}

举例:

ROS2理论入门_第5张图片

问题:初始化与资源释放在程序中启一个什么样的作用

答: 1.前提:构建的程序可能由若干步骤或阶段组成

      初始化——> 节点对象——>日志输出——>数据发布——>数据订阅——>....——>资源释放

        2. 不同步骤或阶段之间涉及到数据的传递

        3. 怎么实现不同数据的传递呢?

            使用Context(上下文)对象。这是一个容器,可以存储数据,也可以从中读取数据

        4. 初始化其实就是要创建Context对象,资源释放就是要销毁Context对象

    

配置文件说明:

经常需要开发者编辑一些配置文件以设置功能包的构建信息,功能包类型不同,所需修改的配置文件也有所不同。C++功能包的构建信息主要包含在package.xml与CMakeLists.txt中,Python功能包的构建信息则主要包含在package.xml和setup.py中,接下来我们就简单了解一下这些配置文件。

1. package.xml




  pkg01_helloworld_cpp
  0.0.0
  TODO: Package description
  ros2
  TODO: License declaration

  ament_cmake
  rclcpp

  ament_lint_auto
  ament_lint_common

  
    ament_cmake
  

1.根标签

  • :该标签为整个xml文件的根标签,format属性用来声明文件的格式版本。

2.元信息标签

  • :包名;
  • :包的版本号;
  • :包的描述信息;
  • :维护者信息;
  • :软件协议;
  • :包的介绍网址;
  • :包的作者信息。

3.依赖项

  • :声明编译工具依赖;
  • :声明编译依赖;
  • :声明根据此包构建库所需依赖;
  • :声明执行时依赖;
  • :相当于三者的集成;
  • :声明测试依赖;
  • :声明构建文档依赖。
2.CMakeLists.txt

C++功能包中需要配置CMakeLists.txt文件,该文件描述了如何构建C++功能包,一个简单的CMakeLists.txt示例如下:

# 声明cmake的最低版本
cmake_minimum_required(VERSION 3.8)
# 包名,需要与package.xml中的包名一致
project(pkg01_helloworld_cpp)

if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
  add_compile_options(-Wall -Wextra -Wpedantic)
endif()

# find dependencies
find_package(ament_cmake REQUIRED)
# 引入外部依赖包
find_package(rclcpp REQUIRED)

# 映射源文件与可执行文件
add_executable(helloworld src/helloworld.cpp)
# 设置目标依赖库
ament_target_dependencies(
  helloworld
  "rclcpp"
)
# 定义安装规则
install(TARGETS helloworld
  DESTINATION lib/${PROJECT_NAME})

if(BUILD_TESTING)
  find_package(ament_lint_auto REQUIRED)
  # the following line skips the linter which checks for copyrights
  # comment the line when a copyright and license is added to all source files
  set(ament_cmake_copyright_FOUND TRUE)
  # the following line skips cpplint (only works in a git repo)
  # comment the line when this package is in a git repo and when
  # a copyright and license is added to all source files
  set(ament_cmake_cpplint_FOUND TRUE)
  ament_lint_auto_find_test_dependencies()
endif()

ament_package()

3.操作命令

ROS2的文件系统核心是功能包,我们可以通过编译指令colcon和ROS2内置的工具指令ros2来实现功能包的创建、编译、查找与执行等相关操作。

1.创建

新建功能包语法如下:

ros2 pkg create 包名 --build-type 构建类型 --dependencies 依赖列表 --node-name 可执行程序名称

格式解释:

  • --build-type:是指功能包的构建类型,有cmake、ament_cmake、ament_python三种类型可选;
  • --dependencies:所依赖的功能包列表;
  • --node-name:可执行程序的名称,会自动生成对应的源文件并生成配置文件。

例:

ros2 pkg create pkg01_helloworld_cpp --build-type ament_cmake --dependencies rclcpp --node-name helloworld

2.编译

colcon build

3.查找

ros2 pkg executables [包名] # 输出所有功能包或指定功能包下的可执行程序。
ros2 pkg list # 列出所有功能包
ros2 pkg prefix 包名 # 列出功能包路径
ros2 pkg xml # 输出功能包的package.xml内容

4.执行

ros2 run 功能包 可执行程序 参数

例:ros2 run pkg01_helloworld_cpp helloworld

三、核心模块

1. 通信模块

通信模块是整个ROS2架构中的重中之重,比如你可能想要了解在ROS2中是如何控制机器人底盘运动的?雷达、摄像头、imu、GPS等这些传感器数据是如何传输到ROS2系统的?人机交互时调用者如何下发指令,机器人又是如何反馈数据的?导航、机械臂等系统性实现不同模块之间是如何交互数据的......等等,其实这些都离不开通信模块。另外,开发者构建应用程序时,通信部分在工作内容中占有相当大的比重

2.功能包应用

1.二进制安装

ROS官方或社区提供的功能包可以很方便的通过二进制方式安装,安装命令如下:

sudo apt install ros-ROS2版本代号-功能包名称
2.源码安装
git clone 仓库地址
3.自实现

开发者按照业务需求自己编写功能包实现。

3.分布式

ROS2是一个分布式架构,不同的ROS2设备之间可以方便的实现通信,这在多机器人设备协同中是极其重要的。

4.终端命令与rqt

在ROS2中提供了丰富的命令行工具,可以方便的调试程序、提高开发效率。

rqt是一个图形化工具,它的功能与命令行工具类似,但是图形化的交互方式更为友好。

2.1 通信机制简介

1.节点

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

2.话题

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

3.通信模型

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

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

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

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

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

ROS2理论入门_第6张图片

4.接口

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

1.msg文件

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

int64 num1
int64 num2
2.srv文件

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

int64 num1
int64 num2
---
int64 sum

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

3.action文件

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

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

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

4.变量类型

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

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

  • float32, float64

  • string

  • time, duration

  • 其他msg文件

  • 变长数组和定长数组

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

2.2 话题通信

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

ROS2理论入门_第7张图片

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

ROS2理论入门_第8张图片

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

2.使用分析

在写程序时,需要关注的要素有三个:

  1. 发布方;
  2. 订阅方;
  3. 消息载体。

3.流程简介

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

  1. 编写发布方实现;
  2. 编写订阅方实现;
  3. 编辑配置文件;
  4. 编译;
  5. 执行。

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

4.准备工作

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

ROS2理论入门_第9张图片

 创建功能包:

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.2.2 话题通信之原生消息(C++)

源码解析:

/*  
  需求:以某个固定频率发送文本“hello world!”,文本后缀编号,每发送一条消息,编号递增1。
  步骤:
    1.包含头文件;
    2.初始化 ROS2 客户端;
    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 // MinimalPublisher是自定义节点名
{
  public:
   //构造函数
    MinimalPublisher(): Node("minimal_publisher"), //节点名称
    count_(0)
    {
      // 3-1.创建发布方;
      publisher_ = this->create_publisher("topic", 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::SharedPtr publisher_;
    size_t count_;
};

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

显示效果:

1. colcon build 2.. install/setup.bash 3. ros2 run 包+文件

ROS2理论入门_第10张图片

验证我们确实接收到了消息:

. install/setup.bash
ros2 topic echo /chatter(发布方)

效果:

ROS2理论入门_第11张图片

订阅端:

功能包cpp01_topic的src目录下,新建C++文件demo04_listener_stu.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("topic", 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::SharedPtr subscription_;
};

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

3.编辑配置文件

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})

4.编译

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

colcon build --packages-select cpp01_topic

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

效果:

ROS2理论入门_第12张图片

2.2.4 话题通信自定义接口消息

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

  1. 创建并编辑 .msg文件;
  2. 编辑配置文件;
  3. 编译;
  4. 测试。

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

1.创建并编辑 .msg 文件

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

string   name
int32    age
float64  height

2.编辑配置文件

1.package.xml文件

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

rosidl_default_generators
rosidl_default_runtime
rosidl_interface_packages
ROS2理论入门_第13张图片
2.CMakeLists.txt文件

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

find_package(rosidl_default_generators REQUIRED)

# 为接口文件生成源代码
rosidl_generate_interfaces(${PROJECT_NAME}
  "msg/Student.msg"
)

3.编译

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

colcon build --packages-select base_interfaces_demo

4.测试

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

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

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

 1.发布方实现:

编辑配置文件

package.xml无需修改,CMakeLists.txt文件需要添加如下内容:

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

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

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

install(TARGETS 
  demo01_talker_str
  demo02_listener_str
  demo03_talker_stu
  demo04_listener_stu
  DESTINATION lib/${PROJECT_NAME})

ROS2理论入门_第14张图片

功能包cpp01_topic的src目录下,新建C++文件demo01_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("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::SharedPtr publisher_;
    size_t count_;
};

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

订阅方:

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

// 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("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::SharedPtr subscription_;
};

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

ROS2理论入门_第15张图片

ROS2理论入门_第16张图片

服务通信:

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

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

2.分析

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

  1. 客户端;
  2. 服务端;
  3. 消息载体。

3.流程

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

  1. 编写服务端实现;
  2. 编写客户端实现;
  3. 编辑配置文件;
  4. 编译;
  5. 执行。

4.准备工作

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

ros2 pkg create cpp02_service --build-type ament_cmake --dependencies rclcpp base_interfaces_demo

2.3.2 服务通信接口消息

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

  1. 创建并编辑 .srv文件;
  2. 编辑配置文件;
  3. 编译;
  4. 测试。

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

1.创建并编辑 .srv 文件

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

int32 num1
int32 num2
---
int32 sum

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.编译

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

colcon build --packages-select base_interfaces_demo

4.测试

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

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

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

ROS2理论入门_第17张图片


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.创建服务端;
      server = this->create_service("add_ints",std::bind(&MinimalService::add, this, _1, _2));
      RCLCPP_INFO(this->get_logger(),"add_ints 服务端启动完毕,等待请求提交...");
    }
  private:
    rclcpp::Service::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();
  rclcpp::spin(server);

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

2.客户端实现

功能包cpp02_service的src目录下,新建C++文件demo02_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("add_ints");
      RCLCPP_INFO(this->get_logger(),"客户端创建,等待连接服务端!");
    }
    // 3-2.等待服务连接;
    bool connect_server(){
      while (!client->wait_for_service(1s))
      {
        if (!rclcpp::ok())
        {
          RCLCPP_INFO(rclcpp::get_logger("rclcpp"),"强制退出!");
          return false;
        }

        RCLCPP_INFO(this->get_logger(),"服务连接中,请稍候...");
      }
      return true;
    }
    // 3-3.组织请求数据并发送;
    rclcpp::Client::FutureAndRequestId send_request(int32_t num1, int32_t num2){
      auto request = std::make_shared();
      request->num1 = num1;
      request->num2 = num2;
      return client->async_send_request(request);
    }


  private:
    rclcpp::Client::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();
  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.编辑配置文件

1.packages.xml

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

rclcpp
base_interfaces_demo
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(demo02_client src/demo02_client.cpp)
ament_target_dependencies(
  demo02_client
  "rclcpp"
  "base_interfaces_demo"
)

install(TARGETS 
  demo01_server
  demo02_client
  DESTINATION lib/${PROJECT_NAME})

4.编译

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

colcon build --packages-select cpp02_service

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

2.4 动作通信

场景

关于action通信,我们先从之前导航中的应用场景开始介绍,描述如下:

机器人导航到某个目标点,此过程需要一个节点A发布目标信息,然后一个节点B接收到请求并控制移动,最终响应目标达成状态信息。

乍一看,这好像是服务通信实现,因为需求中要A发送目标,B执行并返回结果,这是一个典型的基于请求响应的应答模式,不过,如果只是使用基本的服务通信实现,存在一个问题:导航是一个过程,是耗时操作,如果使用服务通信,那么只有在导航结束时,才会产生响应结果,而在导航过程中,节点A是不会获取到任何反馈的,从而可能出现程序"假死"的现象,过程的不可控意味着不良的用户体验,以及逻辑处理的缺陷(比如:导航中止的需求无法实现)。更合理的方案应该是:导航过程中,可以连续反馈当前机器人状态信息,当导航终止时,再返回最终的执行结果。在ROS中,该实现策略称之为:action 通信

概念

动作通信适用于长时间运行的任务。就结构而言动作通信由目标、反馈和结果三部分组成;就功能而言动作通信类似于服务通信,动作客户端可以发送请求到动作服务端,并接收动作服务端响应的最终结果,不过动作通信可以在请求响应过程中获取连续反馈,并且也可以向动作服务端发送任务取消请求;就底层实现而言动作通信是建立在话题通信和服务通信之上的,目标发送实现是对服务通信的封装,结果的获取也是对服务通信的封装,而连续反馈则是对话题通信的封装。

ROS2理论入门_第18张图片

作用

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

2.4.1 案例以及案例分析

1.案例需求

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

2.案例分析

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

  1. 动作客户端;
  2. 动作服务端;
  3. 消息载体。

3.流程简介

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

  1. 编写动作服务端实现;
  2. 编写动作客户端实现;
  3. 编辑配置文件;
  4. 编译;
  5. 执行。

案例我们会采用C+实现,遵循上述实现流程。

4.准备工作

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

ros2 pkg create cpp03_action --build-type ament_cmake --dependencies rclcpp rclcpp_action base_interfaces_demo

2.4.2 动作通信接口消息

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

  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中需要添加一些依赖包,具体内容如下:

rosidl_default_generators
action_msgs
rosidl_interface_packages

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

rosidl_default_generators
action_msgs
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文件一致的内容。

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;

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

  explicit MinimalActionServer(const rclcpp::NodeOptions & options = rclcpp::NodeOptions())
  : Node("minimal_action_server", options)
  {
    // 3-1.创建动作服务端;
    this->action_server_ = rclcpp_action::create_server(
      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::SharedPtr action_server_;

  // 3-2.处理请求数据;
  rclcpp_action::GoalResponse handle_goal(const rclcpp_action::GoalUUID & uuid,std::shared_ptr 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 goal_handle)
  {
    (void)goal_handle;
    RCLCPP_INFO(this->get_logger(), "接收到任务取消请求");
    return rclcpp_action::CancelResponse::ACCEPT;
  }

  void execute(const std::shared_ptr 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();
    auto result = std::make_shared();
    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 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();
  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;
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(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::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::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 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();
  action_client->send_goal(10);
  rclcpp::spin(action_client);
  // 5.释放资源。
  rclcpp::shutdown();
  return 0;
}

3.编辑配置文件

1.packages.xml

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

rclcpp
rclcpp_action
base_interfaces_demo
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

2.5 参数服务

场景

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

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

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

概念

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

作用

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

1.案例需求

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

ROS2理论入门_第19张图片

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

2.5.2 参数数据类型

在ROS2中,参数由键、值和描述符三部分组成,其中键是字符串类型,值可以是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());
...

2.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 ¶m : 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();
    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(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("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 ¶m : 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();
    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

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

rclcpp
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

11

你可能感兴趣的:(ROS2,CAR,开发语言,c++,单片机)