赞
踩
写在前面:本文章是基于赵老师的视频后,所做笔记
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程序,且两种语言的实现流程大致一致,主要包含如下步骤:
- 创建功能包;2. 编辑源文件;3. 编辑配置文件;4. 编译;5. 执行。
终端下,进入ws00_helloworld/src目录,使用如下指令创建一个C++功能包:
ros2 pkg create pkg01_helloworld_cpp --build-type ament_cmake --dependencies rclcpp --node-name helloworld
进入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;
- }
在步骤1创建功能包时所使用的指令已经默认生成且配置了配置文件,不过实际应用中经常需要自己编辑配置文件,所以在此对相关内容做简单介绍,所使用的配置文件主要有两个,分别是功能包下的package.xml与CMakeLists.txt。
- <?xml version="1.0"?>
- <?xml-model href="http://download.ros.org/schema/package_format3.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?>
- <package format="3">
- <name>pkg01_helloworld_cpp</name>
- <version>0.0.0</version>
- <description>TODO: Package description</description>
- <maintainer email="ros2@todo.todo">ros2</maintainer>
- <license>TODO: License declaration</license>
-
- <buildtool_depend>ament_cmake</buildtool_depend>
-
- <!-- 所需要依赖 -->
- <depend>rclcpp</depend>
-
- <test_depend>ament_lint_auto</test_depend>
- <test_depend>ament_lint_common</test_depend>
-
- <export>
- <build_type>ament_cmake</build_type>
- </export>
- </package>

文件内容如下:
- 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()

终端下进入到工作空间,执行如下指令:
colcon build
终端下进入到工作空间,执行如下指令:
- . install/setup.bash
- ros2 run pkg01_helloworld_cpp helloworld
效果:
可以看到在最后一行成功输出 hello world!了
每次终端中执行工作空间下的节点时,都需要调用. install/setup.bash
指令,使用不便,优化策略是,可以将该指令的调用添加进 ~/setup.bash
,操作格式如下:
待添加....
7. 在VScode下配置
然后打开修复
修改的实质是.vscode配置文件
使用快捷键:ctrl+~ 快捷打开终端 ctrl+s保存,然后进行编译,运行
注意 : 节点不能有空格
立足系统架构,如下图所示,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)
是指开发者构建的应用程序,在应用程序中是以功能包为核心的,在功能包中可以包含源码、数据定义、接口等内容。
功能包是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<MyNode>();
- rclcpp::shutdown();
- return 0;
- }

举例:
问题:初始化与资源释放在程序中启一个什么样的作用
初始化——> 节点对象——>日志输出——>数据发布——>数据订阅——>....——>资源释放
2. 不同步骤或阶段之间涉及到数据的传递
3. 怎么实现不同数据的传递呢?
使用Context(上下文)对象。这是一个容器,可以存储数据,也可以从中读取数据
4. 初始化其实就是要创建Context对象,资源释放就是要销毁Context对象
经常需要开发者编辑一些配置文件以设置功能包的构建信息,功能包类型不同,所需修改的配置文件也有所不同。C++功能包的构建信息主要包含在package.xml与CMakeLists.txt中,Python功能包的构建信息则主要包含在package.xml和setup.py中,接下来我们就简单了解一下这些配置文件。
1. package.xml
- <?xml version="1.0"?>
- <?xml-model href="http://download.ros.org/schema/package_format3.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?>
- <package format="3">
- <name>pkg01_helloworld_cpp</name>
- <version>0.0.0</version>
- <description>TODO: Package description</description>
- <maintainer email="ros2@todo.todo">ros2</maintainer>
- <license>TODO: License declaration</license>
-
- <buildtool_depend>ament_cmake</buildtool_depend>
- <depend>rclcpp</depend>
-
- <test_depend>ament_lint_auto</test_depend>
- <test_depend>ament_lint_common</test_depend>
-
- <export>
- <build_type>ament_cmake</build_type>
- </export>
- </package>

1.根标签
- <package>:该标签为整个xml文件的根标签,format属性用来声明文件的格式版本。
2.元信息标签
- <name>:包名;
- <version>:包的版本号;
- <description>:包的描述信息;
- <maintainer>:维护者信息;
- <license>:软件协议;
- <url>:包的介绍网址;
- <author>:包的作者信息。
3.依赖项
- <buildtool_depend>:声明编译工具依赖;
- <build_depend>:声明编译依赖;
- <build_export_depend>:声明根据此包构建库所需依赖;
- <exec_depend>:声明执行时依赖;
- <depend>:相当于<build_depend>、<build_export_depend>、<exec_depend>三者的集成;
- <test_depend>:声明测试依赖;
- <doc_depend>:声明构建文档依赖。
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
来实现功能包的创建、编译、查找与执行等相关操作。
新建功能包语法如下:
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
colcon build
- ros2 pkg executables [包名] # 输出所有功能包或指定功能包下的可执行程序。
- ros2 pkg list # 列出所有功能包
- ros2 pkg prefix 包名 # 列出功能包路径
- ros2 pkg xml # 输出功能包的package.xml内容
ros2 run 功能包 可执行程序 参数
例:ros2 run pkg01_helloworld_cpp helloworld
通信模块是整个ROS2架构中的重中之重,比如你可能想要了解在ROS2中是如何控制机器人底盘运动的?雷达、摄像头、imu、GPS等这些传感器数据是如何传输到ROS2系统的?人机交互时调用者如何下发指令,机器人又是如何反馈数据的?导航、机械臂等系统性实现不同模块之间是如何交互数据的......等等,其实这些都离不开通信模块。另外,开发者构建应用程序时,通信部分在工作内容中占有相当大的比重。
ROS官方或社区提供的功能包可以很方便的通过二进制方式安装,安装命令如下:
sudo apt install ros-ROS2版本代号-功能包名称
git clone 仓库地址
开发者按照业务需求自己编写功能包实现。
ROS2是一个分布式架构,不同的ROS2设备之间可以方便的实现通信,这在多机器人设备协同中是极其重要的。
在ROS2中提供了丰富的命令行工具,可以方便的调试程序、提高开发效率。
rqt是一个图形化工具,它的功能与命令行工具类似,但是图形化的交互方式更为友好。
在通信时,不论采用何种方式,通信对象的构建都依赖于节点(Node),在ROS2中,一般情况下每个节点都对应某一单一的功能模块(例如:雷达驱动节点可能负责发布雷达消息,摄像头驱动节点可能负责发布图像消息)。一个完整的机器人系统可能由许多协同工作的节点组成,ROS2中的单个可执行文件(C++程序或Python程序)可以包含一个或多个节点。
话题(Topic)是一个纽带,具有相同话题的节点可以关联在一起,而这正是通信的前提。并且ROS2是跨语言的,有的节点可能是使用C++实现,有的节点可能是使用Python实现的,但是只要二者使用了相同的话题,就可以实现数据的交互。
不同的通信对象通过话题关联到一起之后,以何种方式实现通信呢?在ROS2中,常用的通信模型有四种:
1.话题通信:是一种单向通信模型,在通信双方中,发布方发布数据,订阅方订阅数据,数据流单向的由发布方传输到订阅方。
2.服务通信:是一种基于请求响应的通信模型,在通信双方中,客户端发送请求数据到服务端,服务端响应结果给客户端。
3.动作通信:是一种带有连续反馈的通信模型,在通信双方中,客户端发送请求数据到服务端,服务端响应结果给客户端,但是在服务端接收到请求到产生最终响应的过程中,会发送连续的反馈信息到客户端。
4.参数服务:是一种基于共享的通信模型,在通信双方中,服务端可以设置数据,而客户端可以连接服务端并操作服务端数据。
在通信过程中,需要传输数据,就必然涉及到数据载体,也即要以特定格式传输数据。在ROS2中,数据载体称之为接口(interfaces)。通信时使用的数据载体一般需要使用接口文件定义。常用的接口文件有三种:msg文件、srv文件与action文件。每种文件都可以按照一定格式定义特定数据类型的“变量”。
msg文件是用于定义话题通信中数据载体的接口文件,一个典型的.msg
文件示例如下。
- int64 num1
- int64 num2
srv文件是用于定义服务通信中数据载体的接口文件,一个典型的.srv
文件示例如下。
- int64 num1
- int64 num2
- ---
- int64 sum
文件中声明的数据被---
分割为两部分,上半部分用于声明请求数据,下半部分用于声明响应数据。
action文件使用用于定义动作通信中数据载体的接口文件,一个典型的.action
文件示例如下。
- int64 num
- ---
- int64 sum
- ---
- float64 progress
文件中声明的数据被---
分割为三部分,上半部分用于声明请求数据,中间部分用于声明响应数据,下半部分用于声明连续反馈数据。
不管是何种接口文件,在文件中每行声明的数据都由字段类型和字段名称组成,可以使用的字段类型有:
int8, int16, int32, int64 (或者无符号类型: uint*)
float32, float64
string
time, duration
其他msg文件
变长数组和定长数组
ROS中还有一种特殊类型:Header
,标头包含时间戳和ROS2中常用的坐标帧信息。许多接口文件的第一行包含Header
标头。
话题通信是一种以发布订阅的方式实现不同节点之间数据传输的通信模型。数据发布对象称为发布方,数据订阅对象称之为订阅方,发布方和订阅方通过话题相关联,发布方将消息发布在话题上,订阅方则从该话题订阅消息,消息的流向是单向的。
话题通信的发布方与订阅方是一种多对多的关系,也即,同一话题下可以存在多个发布方,也可以存在多个订阅方,这意味着数据会出现交叉传输的情况,当然如果没有订阅方,数据传输也会出现丢失的情况。
话题通信一般应用于不断更新的、少逻辑处理的数据传输场景。
在写程序时,需要关注的要素有三个:
需要先自定义接口消息,除此之外的实现流程与案例1一致,主要步骤如下:
案例我们会采用C++实现,遵循上述实现流程。
终端下进入工作空间的src目录,调用如下两条命令分别创建C++功能包
创建功能包:
- 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
上面为C++, 下面为python 按需选择
源码解析:
- /*
- 需求:以某个固定频率发送文本“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<std_msgs::msg::String>("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<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;
- }

编译操作:
1. colcon build 2.. install/setup.bash 3. ros2 run 包+文件
验证:--------我们确实接收到了消息:
- . install/setup.bash
- ros2 topic echo /chatter(发布方)
效果:
功能包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<std_msgs::msg::String>("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<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;
- }

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

终端中进入当前工作空间,编译功能包:
colcon build --packages-select cpp01_topic
当前工作空间下,启动两个终端,终端1执行发布程序,终端2执行订阅程序。
终端1输入如下指令:
- . install/setup.bash
- ros2 run cpp01_topic demo01_talker_str
终端2输入如下指令:
- . install/setup.bash
- ros2 run cpp01_topic demo02_listener_str
效果:
自定义接口消息的流程与在功能包中编写可执行程序的流程类似,主要步骤如下:
.msg
文件;接下来,我们可以参考案例2编译一个msg文件,该文件中包含学生的姓名、年龄、身高等字段。
功能包 base_interfaces_demo 下新建 msg 文件夹,msg文件夹下新建Student.msg文件,文件中输入如下内容:
- string name
- int32 age
- float64 height
在package.xml中需要添加一些依赖包,具体内容如下:
<build_depend>rosidl_default_generators</build_depend> <exec_depend>rosidl_default_runtime</exec_depend> <member_of_group>rosidl_interface_packages</member_of_group>
为了将.msg
文件转换成对应的C+代码,还需要在CMakeLists.txt中添加如下配置:
- find_package(rosidl_default_generators REQUIRED)
-
- # 为接口文件生成源代码
- rosidl_generate_interfaces(${PROJECT_NAME}
- "msg/Student.msg"
- )
终端中进入当前工作空间,编译功能包:
colcon build --packages-select base_interfaces_demo
编译完成之后,在工作空间下的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})
功能包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<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 "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;
- }

也是ROS中一种极其常用的通信模式,服务通信是基于请求响应模式的,是一种应答机制。也即:一个节点A向另一个节点B发送请求,B接收处理请求并产生响应结果返回给A。
编写服务通信,客户端可以提交两个整数到服务端,服务端接收请求并解析两个整数求和,然后将结果响应回客户端。
在上述案例中,需要关注的要素有三个:
案例实现前需要先自定义服务接口,接口准备完毕后,服务实现主要步骤如下:
终端下进入工作空间的src目录,调用如下两条命令分别创建C++功能包
ros2 pkg create cpp02_service --build-type ament_cmake --dependencies rclcpp base_interfaces_demo
定义服务接口消息与定义话题接口消息流程类似,主要步骤如下:
.srv
文件;接下来,我们可以参考案例编写一个srv文件,该文件中包含请求数据(两个整型字段)与响应数据(一个整型字段)。
功能包base_interfaces_demo下新建srv文件夹,srv文件夹下新建AddInts.srv文件,文件中输入如下内容:
- int32 num1
- int32 num2
- ---
- int32 sum
srv文件与msg文件的包依赖一致,如果你是新建的功能包添加srv文件,那么直接参考定义msg文件时package.xml 配置即可。由于我们使用的是base_interfaces_demo该包已经为msg文件配置过了依赖包,所以package.xml不需要做修改。
如果是新建的功能包,与之前定义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"
- )
终端中进入当前工作空间,编译功能包:
colcon build --packages-select base_interfaces_demo
编译完成之后,在工作空间下的 install 目录下将生成AddInts.srv
文件对应的C++和Python文件,我们也可以在终端下进入工作空间,通过如下命令查看文件定义以及编译是否正常:
- . install/setup.bash
- ros2 interface show base_interfaces_demo/srv/AddInts
正常情况下,终端将会输出与AddInts.srv
文件一致的内容
功能包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<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;
- }

功能包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<AddInts>("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<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;
- }

在创建功能包时,所依赖的功能包已经自动配置了,配置内容如下:
- <depend>rclcpp</depend>
- <depend>base_interfaces_demo</depend>
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})

终端中进入当前工作空间,编译功能包:
colcon build --packages-select cpp02_service
当前工作空间下,启动两个终端,终端1执行服务端程序,终端2执行客户端程序。
终端1输入如下指令:
- . install/setup.bash
- ros2 run cpp02_service demo01_server
终端2输入如下指令:
- . install/setup.bash
- ros2 run cpp02_service demo02_client 100 200
关于action通信,我们先从之前导航中的应用场景开始介绍,描述如下:
机器人导航到某个目标点,此过程需要一个节点A发布目标信息,然后一个节点B接收到请求并控制移动,最终响应目标达成状态信息。
乍一看,这好像是服务通信实现,因为需求中要A发送目标,B执行并返回结果,这是一个典型的基于请求响应的应答模式,不过,如果只是使用基本的服务通信实现,存在一个问题:导航是一个过程,是耗时操作,如果使用服务通信,那么只有在导航结束时,才会产生响应结果,而在导航过程中,节点A是不会获取到任何反馈的,从而可能出现程序"假死"的现象,过程的不可控意味着不良的用户体验,以及逻辑处理的缺陷(比如:导航中止的需求无法实现)。更合理的方案应该是:导航过程中,可以连续反馈当前机器人状态信息,当导航终止时,再返回最终的执行结果。在ROS中,该实现策略称之为:action 通信。
动作通信适用于长时间运行的任务。就结构而言动作通信由目标、反馈和结果三部分组成;就功能而言动作通信类似于服务通信,动作客户端可以发送请求到动作服务端,并接收动作服务端响应的最终结果,不过动作通信可以在请求响应过程中获取连续反馈,并且也可以向动作服务端发送任务取消请求;就底层实现而言动作通信是建立在话题通信和服务通信之上的,目标发送实现是对服务通信的封装,结果的获取也是对服务通信的封装,而连续反馈则是对话题通信的封装。
我们需要关注的要素有三个:
案例实现前需要先自定义动作接口,接口准备完毕后,动作通信实现主要步骤如下:
案例我们会采用C+实现,遵循上述实现流程。
终端下进入工作空间的src目录,调用如下两条命令分别创建C++功能包。
ros2 pkg create cpp03_action --build-type ament_cmake --dependencies rclcpp rclcpp_action base_interfaces_demo
定义动作接口消息与定义话题或服务接口消息流程类似,主要步骤如下:
.action
文件;接下来,我们可以参考案例编写一个action文件,该文件中包含请求数据(一个整型字段)、响应数据(一个整型字段)和连续反馈数据(一个浮点型字段)。
功能包base_interfaces_demo下新建action文件夹,action文件夹下新建Progress.action文件,文件中输入如下内容:
- int64 num
- ---
- int64 sum
- ---
- float64 progress
如果单独构建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>
如果是新建的功能包,与之前定义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"
- )
终端中进入当前工作空间,编译功能包:
colcon build --packages-select base_interfaces_demo
编译完成之后,在工作空间下的 install 目录下将生成Progress.action
文件对应的C++和Python文件,我们也可以在终端下进入工作空间,通过如下命令查看文件定义以及编译是否正常:
- . install/setup.bash
- ros2 interface show base_interfaces_demo/action/Progress
正常情况下,终端将会输出与Progress.action
文件一致的内容。
功能包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.创建动作服务端;
- 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;
- }

功能包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;
- }

在创建功能包时,所依赖的功能包已经自动配置了,配置内容如下:
- <depend>rclcpp</depend>
- <depend>rclcpp_action</depend>
- <depend>base_interfaces_demo</depend>
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})

终端中进入当前工作空间,编译功能包:
colcon build --packages-select cpp03_action
当前工作空间下,启动两个终端,终端1执行动作服务端程序,终端2执行动作客户端程序。
终端1输入如下指令:
- . install/setup.bash
- ros2 run cpp03_action demo01_action_server
终端2输入如下指令:
- . install/setup.bash
- ros2 run cpp03_action demo02_action_client
在机器人系统中不同的功能模块可能会使用到一些相同的数据,比如:
导航实现时,会进行路径规划,路径规划主要包含, 全局路径规划和本地路径规划,所谓全局路径规划就是设计一个从出发点到目标点的大致路径,而本地路径规划,则是根据车辆当前路况生成实时的行进路径。两种路径规划实现,都会使用到车辆的尺寸数据——长度、宽度、高度等。那么这些通用数据在程序中应该如何存储、调用呢?
上述场景中,就可以使用参数服务实现,在一个节点下保存车辆尺寸数据,其他节点可以访问该节点并操作这些数据。
参数服务是以共享的方式实现不同节点之间数据交互的一种通信模式。保存参数的节点称之为参数服务端,调用参数的节点称之为参数客户端。参数客户端与参数服务端的交互是基于请求响应的,且参数通信的实现本质上对服务通信的进一步封装。
参数服务保存的数据类似于编程中“全局变量”的概念,可以在不同的节点之间共享数据。
需求:在参数服务端设置一些参数,参数客户端访问服务端并操作这些参数。
在上述案例中,需要关注的要素有三个:
案例实现前需要先了解ROS2中参数的相关API,无论是客户端还是服务端都会使用到参数,而参数服务案例实现主要步骤如下:
案例我们会采用C++和Python分别实现,二者都遵循上述实现流程。
终端下进入工作空间的src目录,调用如下两条命令分别创建C++功能包和Python功能包。
ros2 pkg create cpp04_param --build-type ament_cmake --dependencies rclcpp
在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());
- ...

功能包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<MinimalParamServer>();
- paramServer->declare_param();
- paramServer->get_param();
- paramServer->update_param();
- paramServer->del_param();
- rclcpp::spin(paramServer);
-
- // 5.释放资源。
- rclcpp::shutdown();
- return 0;
- }

功能包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 ¶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<MinimalParamClient>();
- bool flag = paramClient->connect_server();
- if(!flag){
- return 0;
- }
- paramClient->get_param();
- paramClient->update_param();
- paramClient->get_param();
-
- // 5.释放资源。
- rclcpp::shutdown();
- return 0;
- }

在创建功能包时,所依赖的功能包已经自动配置了,配置内容如下:
<depend>rclcpp</depend>
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})

终端中进入当前工作空间,编译功能包:
colcon build --packages-select cpp04_param
当前工作空间下,启动两个终端,终端1执行参数服务端程序,终端2执行参数客户端程序。
终端1输入如下指令:
- . install/setup.bash
- ros2 run cpp04_param demo01_param_server
终端2输入如下指令:
- . install/setup.bash
- ros2 run cpp04_param demo02_param_client
在许多机器人相关的应用场景中都涉及到多台ROS2设备协作,比如:无人车编队、无人机编队、远程控制等等,那么不同的ROS2设备之间是如何实现通信的呢?
分布式通信是指可以通过网络在不同主机之间实现数据交互的一种通信策略。
ROS2本身是一个分布式通信框架,可以很方便的实现不同设备之间的通信,ROS2所基于的中间件是DDS,当处于同一网络中时,通过DDS的域ID机制(ROS_DOMAIN_ID)可以实现分布式通信,大致流程是:在启动节点之前,可以设置域ID的值,不同节点如果域ID相同,那么可以自由发现并通信,反之,如果域ID值不同,则不能实现。默认情况下,所有节点启动时所使用的域ID为0,换言之,只要保证在同一网络,你不需要做任何配置,不同ROS2设备上的不同节点即可实现分布式通信。
分布式通信的应用场景是较为广泛的,如上所述:机器人编队时,机器人可能需要获取周边机器人的速度、位置、运行轨迹的相关信息,远程控制时,则可能需要控制端获取机器人采集的环境信息并下发控制指令...... 这些数据的交互都依赖于分布式通信。
多机通信时,可以通过域ID对节点进行分组,组内的节点之间可以自由通信,不同组之间的节点则不可通信。如果所有节点都属于同一组,那么直接使用默认域ID即可,如果要将不同节点划分为多个组,那么可以在终端中启动节点前设置该节点的域ID(比如设置为6),具体执行命令为:
export ROS_DOMAIN_ID=6
上述指令执行后,该节点将被划分到ID为6的域内。
如果要为当前设备下的所有节点设置统一的域ID,那么可以执行如下指令:
echo "export ROS_DOMAIN_ID=6" >> ~/.bashrc
执行完毕后再重新启动终端,运行的所有节点将自动被划分到ID为6的域内。
完成一个系统性的功能,可能涉及到多个功能包,比如实现了机器人导航模块,该模块下有地图、定位、路径规划...等不同的子级功能包。那么调用者安装该模块时,需要逐一的安装每一个功能包吗?
显而易见的,逐一安装功能包的效率低下,在ROS2中,提供了一种方式可以将不同的功能包打包成一个功能包,当安装某个功能模块时,直接调用打包后的功能包即可,该包又称之为元功能包(metapackage)。
MetaPackage是Linux的一个文件管理系统的概念。是 ROS2 中的一个虚包,里面没有实质性的内容,但是它依赖了其他的软件包,通过这种方法可以把其他包组合起来,我们可以认为它是一本书的目录索引,告诉我们这个包集合中有哪些子包,并且该去哪里下载。
例如:
方便用户的安装,我们只需要这一个包就可以把其他相关的软件包组织到一起安装了。
1.新建一个功能包
ros2 pkg create tutorails_plumbing
2.修改 package.xml 文件,添加执行时所依赖的包:
- <?xml version="1.0"?>
- <?xml-model href="http://download.ros.org/schema/package_format3.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?>
- <package format="3">
- <name>tutorails_plumbing</name>
- <version>0.0.0</version>
- <description>TODO: Package description</description>
- <maintainer email="ros2@todo.todo">ros2</maintainer>
- <license>TODO: License declaration</license>
-
- <buildtool_depend>ament_cmake</buildtool_depend>
-
- <exec_depend>base_interfaces_demo</exec_depend>
- <exec_depend>cpp01_topic</exec_depend>
- <exec_depend>cpp02_service</exec_depend>
- <exec_depend>cpp03_action</exec_depend>
- <exec_depend>cpp04_param</exec_depend>
- <exec_depend>py01_topic</exec_depend>
- <exec_depend>py02_service</exec_depend>
- <exec_depend>py03_action</exec_depend>
- <exec_depend>py04_param</exec_depend>
-
-
- <export>
- <build_type>ament_cmake</build_type>
- </export>
- </package>

3.文件CMakeLists.txt内容如下:
- cmake_minimum_required(VERSION 3.8)
- project(tutorails_plumbing)
-
- if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
- add_compile_options(-Wall -Wextra -Wpedantic)
- endif()
-
- find_package(ament_cmake REQUIRED)
-
- ament_package()
语法:ros2 run 包名 节点名 --ros-args --remap __ns:=命名空间
示例:
ros2 run turtlesim turtlesim_node --ros-args --remap __ns:=/t1
使用ros2 node list
查看节点信息,显示结果:
/t1/turtlesim
语法: ros2 run 包名 节点名 --ros-args --remap __name:=新名称
或
ros2 run 包名 节点名 --ros-args --remap __node:=新名称
示例:
ros2 run turtlesim turtlesim_node --ros-args --remap __name:=turtle1
使用ros2 node list
查看节点信息,显示结果:
/turtle1
语法: ros2 run 包名 节点名 --ros-args --remap __ns:=新名称 --remap __name:=新名称
ros2 run turtlesim turtlesim_node --ros-args --remap __ns:=/t1 --remap __name:=turtle1
使用ros2 node list
查看节点信息,显示结果:
/t1/turtle1
在ROS2中launch文件可以由Python、XML或YAML三种语言编写(关于launch文件的基本使用可以参考4.1 启动文件launch简介),每种实现方式都可以设置节点的命名空间或为节点起别名。
在 XML 方式实现的 launch 文件中,可以通过 node 标签中 name 和 namespace 属性来设置节点的名称与命名空间,使用示例如下:
- <launch>
- <node pkg="turtlesim" exec="turtlesim_node" name="turtle1" />
- <node pkg="turtlesim" exec="turtlesim_node" namespace="t1" />
- <node pkg="turtlesim" exec="turtlesim_node" namespace="t1" name="turtle1" />
- </launch>
在 YAML 方式实现的 launch 文件中,可以通过 node 属性中 name 和 namespace 属性来设置节点的名称与命名空间,使用示例如下:
- launch:
- - node:
- pkg: turtlesim
- exec: turtlesim_node
- name: turtle1
- - node:
- pkg: turtlesim
- exec: turtlesim_node
- namespace: t1
- - node:
- pkg: turtlesim
- exec: turtlesim_node
- namespace: t1
- name: turtle1
节点名称可能出现重名的情况,同理话题名称也可能重名,不过与节点重名不同的是,有些场景下需要避免话题重名的情况,但有些场景下又需要将不同的不同的话题名称修改为相同。
在 ROS2 不同的节点之间通信都依赖于话题,话题名称也可能出现重名的情况,话题重名时,系统虽然不会抛出异常,但是通过相同话题名称关联到一起的节点可能并不属于同一通信逻辑,从而导致通信错乱,甚至出现异常。这种情况下可能就需要将相同的话题名称设置为不同。
又或者,两个节点是属于同一通信逻辑的,但是节点之间话题名称不同,导致通信失败。这种情况下就需要将两个节点的话题名称由不同修改为相同。
那么如何修改话题名称呢?
与节点重名的解决思路类似的,为了避免话题重名问题,一般有两种策略:
需要注意的是,通过命名空间设置话题名称时,需要保证话题是非全局话题。
与节点重名解决方案类似的,修改话题名称的方式主要有如下三种:
该实现与3.4.1 ros2 run设置节点名称中演示的语法使用一致。
语法:ros2 run 包名 节点名 --ros-args --remap __ns:=命名空间
示例:
ros2 run turtlesim turtlesim_node --ros-args --remap __ns:=/t1
使用ros2 topic list
查看节点信息,显示结果:
- /t1/turtle1/cmd_vel
- /t1/turtle1/color_sensor
- /t1/turtle1/pose
节点下的话题已经添加了命名空间前缀。
语法: ros2 run 包名 节点名 --ros-args --remap 原话题名称:=新话题名称
示例:
ros2 run turtlesim turtlesim_node --ros-args --remap /turtle1/cmd_vel:=/cmd_vel
使用ros2 topic list
查看节点信息,显示结果:
- /cmd_vel
- /turtle1/color_sensor
- /turtle1/pose
节点下的话题/turtle1/cmd_vel已经被修改为了/cmd_vel。
注意:
当为节点添加命名空间时,节点下的所有非全局话题都会前缀命名空间,而重映射的方式只是修改指定话题。
在 XML 方式实现的 launch 文件中,可以通过 node 标签的子标签 remap(属性from取值为被修改的话题名称,属性to的取值为修改后的话题名称) 修改话题名称,使用示例如下:
- <launch>
- <node pkg="turtlesim" exec="turtlesim_node" namespace="t1" />
- <node pkg="turtlesim" exec="turtlesim_node">
- <remap from="/turtle1/cmd_vel" to="/cmd_vel" />
- </node>
- </launch>
在 YAML 方式实现的 launch 文件中,可以通过 node 属性中 remap(属性from取值为被修改的话题名称,属性to的取值为修改后的话题名称) 修改话题名称,使用示例如下:
- launch:
- - node:
- pkg: turtlesim
- exec: turtlesim_node
- namespace: t1
- - node:
- pkg: turtlesim
- exec: turtlesim_node
- remap:
- -
- from: "/turtle1/cmd_vel"
- to: "/cmd_vel"
上述三种方式在修改话题名称时虽然语法不同,但是实现功能类似,都是启动了两个turtlesim_node
节点,一个节点添加了命名空间,另一个节点将话题从/turtle1/cmd_vel
映射到了/cmd_vel
。使用ros2 topic list
查看节点信息,显示结果:
添加命名空间的节点对应的话题为:
- /t1/turtle1/cmd_vel
- /t1/turtle1/color_sensor
- /t1/turtle1/pose
重映射的节点对应的话题为:
- /cmd_vel
- /turtle1/color_sensor
- /turtle1/pose
第2章话题通信案例中,要求话题发布方按照一定的频率发布消息,我们实现时是通过定时器来控制发布频率的,其实,除了定时器之外,ROS2 中还提供了 Rate 类,通过该类对象也可以控制程序的运行频率。
示例:周期性输出一段文本。
- #include "rclcpp/rclcpp.hpp"
-
- using namespace std::chrono_literals;
-
- int main(int argc, char ** argv)
- {
- rclcpp::init(argc,argv);
- auto node = rclcpp::Node::make_shared("rate_demo");
- // rclcpp::Rate rate(1000ms); // 创建 Rate 对象方式1
- rclcpp::Rate rate(1.0); // 创建 Rate 对象方式2
- while (rclcpp::ok())
- {
- RCLCPP_INFO(node->get_logger(),"hello rate");
- // 休眠
- rate.sleep();
- }
-
- rclcpp::shutdown();
- return 0;
- }

示例:创建 Time 对象,并调用其函数。
- #include "rclcpp/rclcpp.hpp"
-
- int main(int argc, char const *argv[])
- {
- rclcpp::init(argc,argv);
- auto node = rclcpp::Node::make_shared("time_demo");
-
- // 创建 Time 对象
- rclcpp::Time t1(10500000000L);
- rclcpp::Time t2(2,1000000000L);
- // 通过节点获取当前时刻。
- // rclcpp::Time roght_now = node->get_clock()->now();
- rclcpp::Time roght_now = node->now();
- RCLCPP_INFO(node->get_logger(),"s = %.2f, ns = %ld",t1.seconds(),t1.nanoseconds());
- RCLCPP_INFO(node->get_logger(),"s = %.2f, ns = %ld",t2.seconds(),t2.nanoseconds());
- RCLCPP_INFO(node->get_logger(),"s = %.2f, ns = %ld",roght_now.seconds(),roght_now.nanoseconds());
-
- rclcpp::shutdown();
-
- return 0;
- }

示例:创建 Duration 对象,并调用其函数。
- #include "rclcpp/rclcpp.hpp"
-
- using namespace std::chrono_literals;
-
- int main(int argc, char const *argv[])
- {
- rclcpp::init(argc,argv);
- auto node = rclcpp::Node::make_shared("duration_node");
-
- // 创建 Duration 对象
- rclcpp::Duration du1(1s);
- rclcpp::Duration du2(2,500000000);
-
- RCLCPP_INFO(node->get_logger(),"s = %.2f, ns = %ld", du2.seconds(),du2.nanoseconds());
-
- rclcpp::shutdown();
- return 0;
- }

本节主要介绍通信机制相关的一些练习,这些练习基于turtlesim功能包,练习类型覆盖了话题、服务、动作、参数这四种通信机制。
终端下进入工作空间的src目录,调用如下命令创建C++功能包。
ros2 pkg create cpp07_exercise --build-type ament_cmake --dependencies rclcpp turtlesim base_interfaces_demo geometry_msgs rclcpp_action
功能包下新建launch目录以备用。
------------------
需求:启动两个turtlesim_node节点,节点2中的乌龟自动调头180°,我们可以通过键盘控制节点1中的乌龟运动,但是不能控制节点2的乌龟,需要自实现功能:可以根据乌龟1的速度生成并发布控制乌龟2运动的速度指令,最终两只乌龟做镜像运动。
在上述案例中,主要需要关注的问题有三个:
思路:
主要步骤如下:
11
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。