机器人是一种高度复杂的系统性实现,一个完整的机器人应用程序可能由若干功能模块组成,每个功能模块可能又包含若干功能点,在不同功能模块、不同功能点之间需要频繁的进行数据交互。比如以导航中的路径规划模块为例:
路径规划时就需要其他功能模块输入数据,并输出数据以被其他模块调用。
输入的数据有地图服务提供的地图数据、定位模块提供的机器人位姿数据、人机交互模块提供的目标点数据......。
输出的路径信息则被运动控制订阅或是回显在人机交互界面上。
那么这些相对独立的功能模块或功能点之间是如何实现数据交互的呢?在此,我们就需要介绍一下ROS2中的通信机制了。
2.1 通信机制简介
在ROS2中通信方式虽然有多种,但是不同通信方式的组成要素都是类似的,比如:通信是双方或多方行为、通信时都需要将不同的通信对象关联、都有各自的模型、交互数据时也必然涉及到数据载体等等。本节将会介绍通信中涉及到的一些术语。
1.节点
在通信时,不论采用何种方式,通信对象的构建都依赖于节点(Node),在ROS2中,一般情况下每个节点都对应某一单一的功能模块(例如:雷达驱动节点可能负责发布雷达消息,摄像头驱动节点可能负责发布图像消息)。一个完整的机器人系统可能由许多协同工作的节点组成,ROS2中的单个可执行文件(C++程序或Python程序)可以包含一个或多个节点。
2.话题
话题(Topic)是一个纽带,具有相同话题的节点可以关联在一起,而这正是通信的前提。并且ROS2是跨语言的,有的节点可能是使用C++实现,有的节点可能是使用Python实现的,但是只要二者使用了相同的话题,就可以实现数据的交互。
3.通信模型
不同的通信对象通过话题关联到一起之后,以何种方式实现通信呢?在ROS2中,常用的通信模型有四种:
1.话题通信:是一种单向通信模型,在通信双方中,发布方发布数据,订阅方订阅数据,数据流单向的由发布方传输到订阅方。
2.服务通信:是一种基于请求响应的通信模型,在通信双方中,客户端发送请求数据到服务端,服务端响应结果给客户端。
3.动作通信:是一种带有连续反馈的通信模型,在通信双方中,客户端发送请求数据到服务端,服务端响应结果给客户端,但是在服务端接收到请求到产生最终响应的过程中,会发送连续的反馈信息到客户端。
4.参数服务:是一种基于共享的通信模型,在通信双方中,服务端可以设置数据,而客户端可以连接服务端并操作服务端数据。
4.接口
在通信过程中,需要传输数据,就必然涉及到数据载体,也即要以特定格式传输数据。在ROS2中,数据载体称之为接口(interfaces)。通信时使用的数据载体一般需要使用接口文件定义。常用的接口文件有三种:msg文件、srv文件与action文件。每种文件都可以按照一定格式定义特定数据类型的“变量”。
1.msg文件
msg文件是用于定义话题通信中数据载体的接口文件,一个典型的.msg文件示例如下。
int64 num1
int64 num2
在文件中声明了一些被传输的类似于C++变量的数据。
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标头。
另外,需要说明的是:
参数通信的数据无需定义接口文件,参数通信时数据会被封装为参数对象,参数客户端和服务端操作的都是参数对象。
本阶段大家对数据载体做简单的了解即可,其具体使用后续章节有详细介绍。
准备工作
1.请先创建工作空间ws01_plumbing,本章以及第3章代码部分内容存储在该工作空间下。
2.实际应用中一般建议创建专门的接口功能包定义接口文件,当前教程也遵循这一建议,预先创建教程所需使用的接口功能包(需要注意的是,目前为止无法在Python功能包中定义接口文件),终端下进入工作空间的src目录,执行如下命令:
ros2 pkg create --build-type ament_cmake base_interfaces_demo
该功能包将用于保存本章教程中自定义的接口文件。
2.2 话题通信
场景
话题通信是ROS中使用频率最高的一种通信模式,话题通信是基于发布订阅模式的,也即:一个节点发布消息,另一个节点订阅该消息。话题通信的应用场景也极其广泛,比如如下场景:
机器人在执行导航功能,使用的传感器是激光雷达,机器人会采集激光雷达感知到的信息并计算,然后生成运动控制信息驱动机器人底盘运动。
在该场景中,就不止一次使用到了话题通信。
以激光雷达信息的采集处理为例,在ROS中有一个节点需要时时的发布当前雷达采集到的数据,导航模块中也有节点会订阅并解析雷达数据。
再以运动消息的发布为例,导航模块会综合多方面数据实时计算出运动控制信息并发布给底盘驱动模块,底盘驱动有一个节点订阅运动信息并将其转换成控制电机的脉冲信号。
以此类推,像雷达、摄像头、GPS.... 等等一些传感器数据的采集,也都是使用了话题通信,话题通信适用于不断更新的数据传输相关的应用场景。
概念
话题通信是一种以发布订阅的方式实现不同节点之间数据传输的通信模型。数据发布对象称为发布方,数据订阅对象称之为订阅方,发布方和订阅方通过话题相关联,发布方将消息发布在话题上,订阅方则从该话题订阅消息,消息的流向是单向的。

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

作用
话题通信一般应用于不断更新的、少逻辑处理的数据传输场景。
关于消息接口
关于消息接口的使用有多种方式:
在ROS2中通过std_msgs包封装了一些原生的数据类型,比如:String、Int8、Int16、Int32、Int64、Float32、Float64、Char、Bool、Empty.... 这些原生数据类型也可以作为话题通信的载体,不过这些数据一般只包含一个 data 字段,而std_msgs包中其他的接口文件也比较简单,结构的单一意味着功能上的局限性,当传输一些结构复杂的数据时,就显得力不从心了;
在ROS2中还预定义了许多标准话题消息接口,这在实际工作中有着广泛的应用,比如:sensor_msgs包中定义了许多关于传感器消息的接口(雷达、摄像头、点云......),geometry_msgs包中则定义了许多几何消息相关的接口(坐标点、坐标系、速度指令......);
如果上述接口文件都不能满足我们的需求,那么就可以自定义接口消息;
具体如何选型,大家可以根据具体情况具体分析。
2.2.1 案例以及案例分析
1.案例需求
需求1:编写话题通信实现,发布方以某个频率发布一段文本,订阅方订阅消息,并输出在终端。

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

2.案例分析
在上述案例中,需要关注的要素有三个:
发布方;
订阅方;
消息载体。
案例1和案例2的主要区别在于消息载体,前者可以使用原生的数据类型,后者需要自定义接口消息。
3.流程简介
案例2需要先自定义接口消息,除此之外的实现流程与案例1一致,主要步骤如下:
编写发布方实现;
编写订阅方实现;
编辑配置文件;
编译;
执行。
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_demo2.2.2 话题通信之原生消息(C++)
1.发布方实现
功能包cpp01_topic的src目录下,新建C++文件demo01_talker_str.cpp,并编辑文件,输入如下内容:
/*
需求:以某个固定频率发送文本“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
{
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;
}
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", 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;
}
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})
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_str2.2.3 话题通信之原生消息(Python)
1.发布方实现
功能包py01_topic的py01_topic目录下,新建Python文件demo01_talker_str_py.py,并编辑文件,输入如下内容:
"""
需求:以某个固定频率发送文本“hello world!”,文本后缀编号,每发送一条消息,编号递增1。
步骤:
1.导包;
2.初始化 ROS2 客户端;
3.定义节点类;
3-1.创建发布方;
3-2.创建定时器;
3-3.组织消息并发布。
4.调用spin函数,并传入节点对象;
5.释放资源。
"""
# 1.导包;
import rclpy
from rclpy.node import Node
from std_msgs.msg import String
# 3.定义节点类;
class MinimalPublisher(Node):
def __init__(self):
super().__init__('minimal_publisher_py')
# 3-1.创建发布方;
self.publisher_ = self.create_publisher(String, 'topic', 10)
# 3-2.创建定时器;
timer_period = 0.5
self.timer = self.create_timer(timer_period, self.timer_callback)
self.i = 0
# 3-3.组织消息并发布。
def timer_callback(self):
msg = String()
msg.data = 'Hello World(py): %d' % self.i
self.publisher_.publish(msg)
self.get_logger().info('发布的消息: "%s"' % msg.data)
self.i += 1
def main(args=None):
# 2.初始化 ROS2 客户端;
rclpy.init(args=args)
# 4.调用spin函数,并传入节点对象;
minimal_publisher = MinimalPublisher()
rclpy.spin(minimal_publisher)
# 5.释放资源。
rclpy.shutdown()
if __name__ == '__main__':
main()
2.订阅方实现
功能包py01_topic的py01_topic目录下,新建Python文件demo02_listener_str_py.py,并编辑文件,输入如下内容:
"""
需求:订阅发布方发布的消息,并输出到终端。
步骤:
1.导包;
2.初始化 ROS2 客户端;
3.定义节点类;
3-1.创建订阅方;
3-2.处理订阅到的消息。
4.调用spin函数,并传入节点对象;
5.释放资源。
"""
# 1.导包;
import rclpy
from rclpy.node import Node
from std_msgs.msg import String
# 3.定义节点类;
class MinimalSubscriber(Node):
def __init__(self):
super().__init__('minimal_subscriber_py')
# 3-1.创建订阅方;
self.subscription = self.create_subscription(
String,
'topic',
self.listener_callback,
10)
self.subscription
# 3-2.处理订阅到的消息。
def listener_callback(self, msg):
self.get_logger().info('订阅的消息: "%s"' % msg.data)
def main(args=None):
# 2.初始化 ROS2 客户端;
rclpy.init(args=args)
# 4.调用spin函数,并传入节点对象;
minimal_subscriber = MinimalSubscriber()
rclpy.spin(minimal_subscriber)
# 5.释放资源。
rclpy.shutdown()
if __name__ == '__main__':
main()
3.编辑配置文件
在Python功能包中,配置文件主要关注package.xml与setup.py。
1.package.xml
在创建功能包时,所依赖的功能包已经自动配置了,配置内容如下:
<depend>rclcpp</depend>
<depend>std_msgs</depend>
<depend>base_interfaces_demo</depend>
需要说明的是和上一节C++实现一样<depend>base_interfaces_demo</depend>在本案例中不是必须的。
2.setup.py
entry_points字段的console_scripts中添加如下内容:
entry_points={
'console_scripts': [
'demo01_talker_str_py = py01_topic.demo01_talker_str_py:main',
'demo02_listener_str_py = py01_topic.demo02_listener_str_py:main'
],
},
4.编译
终端中进入当前工作空间,编译功能包:
colcon build --packages-select py01_topic
5.执行
当前工作空间下,启动两个终端,终端1执行发布程序,终端2执行订阅程序。
终端1输入如下指令:
. install/setup.bash
ros2 run py01_topic demo01_talker_str_py
终端2输入如下指令:
. install/setup.bash
ros2 run py01_topic demo02_listener_str_py
2.2.4 话题通信自定义接口消息
自定义接口消息的流程与在功能包中编写可执行程序的流程类似,主要步骤如下:
创建并编辑
.msg文件;编辑配置文件;
编译;
测试。
接下来,我们可以参考案例2编译一个msg文件,该文件中包含学生的姓名、年龄、身高等字段。
1.创建并编辑 .msg 文件
功能包base_interfaces_demo下新建 msg 文件夹,msg文件夹下新建Student.msg文件,文件中输入如下内容:
string name
int32 age
float64 height
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_group>
2.CMakeLists.txt文件
为了将.msg文件转换成对应的C++和Python代码,还需要在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文件一致的内容。
2.2.5 话题通信之自定义消息(C++)
准备
C++文件中包含自定义消息相关头文件时,可能会抛出异常,可以配置VSCode中c_cpp_properties.json文件,在文件中的 includePath属性下添加一行:"${workspaceFolder}/install/base_interfaces_demo/include/**"
添加完毕后,包含相关头文件时,就不会抛出异常了,其他接口文件或接口包的使用也与此同理。
1.发布方实现
功能包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;
}
2.订阅方实现
功能包cpp01_topic的src目录下,新建C++文件demo04_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;
}
3.编辑配置文件
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})
4.编译
终端中进入当前工作空间,编译功能包:
colcon build --packages-select cpp01_topic
5.执行
当前工作空间下,启动两个终端,终端1执行发布程序,终端2执行订阅程序。
终端1输入如下指令:
. install/setup.bash
ros2 run cpp01_topic demo03_talker_stu
终端2输入如下指令:
. install/setup.bash
ros2 run cpp01_topic demo04_listener_stu
最终运行结果与案例2类似。
2.2.6 话题通信之自定义消息(Python)
准备
Python文件中导入自定义消息相关的包时,为了方便使用,可以配置VSCode中settings.json文件,在文件中的python.autoComplete.extraPaths和python.analysis.extraPaths属性下添加一行:"${workspaceFolder}/install/base_interfaces_demo/local/lib/python3.10/dist-packages"
添加完毕后,代码可以高亮显示且可以自动补齐,其他接口文件或接口包的使用也与此同理。
1.发布方实现
功能包py01_topic的py01_topic目录下,新建Python文件demo03_talker_stu_py.py,并编辑文件,输入如下内容:
"""
需求:以某个固定频率发送文本学生信息,包含学生的姓名、年龄、身高等数据。
"""
# 1.导包;
import rclpy
from rclpy.node import Node
from base_interfaces_demo.msg import Student
# 3.定义节点类;
class MinimalPublisher(Node):
def __init__(self):
super().__init__('stu_publisher_py')
# 3-1.创建发布方;
self.publisher_ = self.create_publisher(Student, 'topic_stu', 10)
# 3-2.创建定时器;
timer_period = 0.5
self.timer = self.create_timer(timer_period, self.timer_callback)
self.i = 0
# 3-3.组织消息并发布。
def timer_callback(self):
stu = Student()
stu.name = "李四"
stu.age = self.i
stu.height = 1.70
self.publisher_.publish(stu)
self.get_logger().info('发布的学生消息(py): name=%s,age=%d,height=%.2f' % (stu.name, stu.age, stu.height))
self.i += 1
def main(args=None):
# 2.初始化 ROS2 客户端;
rclpy.init(args=args)
# 4.调用spin函数,并传入节点对象;
minimal_publisher = MinimalPublisher()
rclpy.spin(minimal_publisher)
# 5.释放资源。
rclpy.shutdown()
if __name__ == '__main__':
main()
2.订阅方实现
功能包py01_topic的py01_topic目录下,新建Python文件demo04_listener_stu_py.py,并编辑文件,输入如下内容:
"""
需求:订阅发布方发布的学生消息,并输出到终端。
"""
# 1.导包;
import rclpy
from rclpy.node import Node
from base_interfaces_demo.msg import Student
# 3.定义节点类;
class MinimalSubscriber(Node):
def __init__(self):
super().__init__('stu_subscriber_py')
# 3-1.创建订阅方;
self.subscription = self.create_subscription(
Student,
'topic_stu',
self.listener_callback,
10)
self.subscription
# 3-2.处理订阅到的消息。
def listener_callback(self, stu):
self.get_logger().info('订阅的消息(py): name=%s,age=%d,height=%.2f' % (stu.name, stu.age, stu.height))
def main(args=None):
# 2.初始化 ROS2 客户端;
rclpy.init(args=args)
# 4.调用spin函数,并传入节点对象;
minimal_subscriber = MinimalSubscriber()
rclpy.spin(minimal_subscriber)
# 5.释放资源。
rclpy.shutdown()
if __name__ == '__main__':
main()
3.编辑配置文件
package.xml无需修改,需要修改setup.py文件,entry_points字段的console_scripts中修改为如下内容:
entry_points={
'console_scripts': [
'demo01_talker_str_py = py01_topic.demo01_talker_str_py:main',
'demo02_listener_str_py = py01_topic.demo02_listener_str_py:main',
'demo03_talker_stu_py = py01_topic.demo03_talker_stu_py:main',
'demo04_listener_stu_py = py01_topic.demo04_listener_stu_py:main'
],
},
4.编译
终端中进入当前工作空间,编译功能包:
colcon build --packages-select py01_topic
5.执行
当前工作空间下,启动两个终端,终端1执行发布程序,终端2执行订阅程序。
终端1输入如下指令:
. install/setup.bash
ros2 run py01_topic demo03_talker_stu_py
终端2输入如下指令:
. install/setup.bash
ros2 run py01_topic demo04_listener_stu_py
最终运行结果与案例2类似。
2.3 服务通信
场景
服务通信也是ROS中一种极其常用的通信模式,服务通信是基于请求响应模式的,是一种应答机制。也即:一个节点A向另一个节点B发送请求,B接收处理请求并产生响应结果返回给A。比如如下场景:
机器人巡逻过程中,控制系统分析传感器数据发现可疑物体或人... 此时需要拍摄照片并留存。
在上述场景中,就使用到了服务通信。
数据分析节点A需要向相机相关节点B发送图片存储请求,节点B处理请求,并返回处理结果。
与上述应用类似的,服务通信更适用于对实时性有要求、具有一定逻辑处理的应用场景。
概念
服务通信是以请求响应的方式实现不同节点之间数据传输的通信模式。发送请求数据的对象称为客户端,接收请求并发送响应的对象称之为服务端,同话题通信一样,客户端和服务端也通过话题相关联,不同的是服务通信的数据传输是双向交互式的。

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

作用
用于偶然的、对实时性有要求、有一定逻辑处理需求的数据传输场景
2.3.1 案例以及案例分析
1.案例需求
需求:编写服务通信,客户端可以提交两个整数到服务端,服务端接收请求并解析两个整数求和,然后将结果响应回客户端。

2.案例分析
在上述案例中,需要关注的要素有三个:
客户端;
服务端;
消息载体。
3.流程简介
案例实现前需要先自定义服务接口,接口准备完毕后,服务实现主要步骤如下:
编写服务端实现;
编写客户端实现;
编辑配置文件;
编译;
执行。
案例我们会采用C++和Python分别实现,二者都遵循上述实现流程。
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_demo2.3.2 服务通信接口消息
定义服务接口消息与定义话题接口消息流程类似,主要步骤如下:
创建并编辑
.srv文件;编辑配置文件;
编译;
测试。
接下来,我们可以参考案例编写一个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文件一致的内容。
2.3.3 服务通信(C++)
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<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;
}
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<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;
}
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(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.3.4 服务通信(Python)
1.服务端实现
功能包py02_service的py02_service目录下,新建Python文件demo01_server_py.py,并编辑文件,输入如下内容:
"""
需求:编写服务端,接收客户端发送请求,提取其中两个整型数据,相加后将结果响应回客户端。
步骤:
1.导包;
2.初始化 ROS2 客户端;
3.定义节点类;
3-1.创建服务端;
3-2.处理请求数据并响应结果。
4.调用spin函数,并传入节点对象;
5.释放资源。
"""
# 1.导包;
import rclpy
from rclpy.node import Node
from base_interfaces_demo.srv import AddInts
# 3.定义节点类;
class MinimalService(Node):
def __init__(self):
super().__init__('minimal_service_py')
# 3-1.创建服务端;
self.srv = self.create_service(AddInts, 'add_ints', self.add_two_ints_callback)
self.get_logger().info("服务端启动!")
# 3-2.处理请求数据并响应结果。
def add_two_ints_callback(self, request, response):
response.sum = request.num1 + request.num2
self.get_logger().info('请求数据:(%d,%d),响应结果:%d' % (request.num1, request.num2, response.sum))
return response
def main():
# 2.初始化 ROS2 客户端;
rclpy.init()
# 4.调用spin函数,并传入节点对象;
minimal_service = MinimalService()
rclpy.spin(minimal_service)
# 5.释放资源。
rclpy.shutdown()
if __name__ == '__main__':
main()
2.客户端实现
功能包py02_service的py02_service目录下,新建Python文件demo02_client_py.py,并编辑文件,输入如下内容:
"""
需求:编写客户端,发送两个整型变量作为请求数据,并处理响应结果。
步骤:
1.导包;
2.初始化 ROS2 客户端;
3.定义节点类;
3-1.创建客户端;
3-2.等待服务连接;
3-3.组织请求数据并发送;
4.创建对象调用其功能,处理响应结果;
5.释放资源。
"""
# 1.导包;
import sys
import rclpy
from rclpy.node import Node
from base_interfaces_demo.srv import AddInts
# 3.定义节点类;
class MinimalClient(Node):
def __init__(self):
super().__init__('minimal_client_py')
# 3-1.创建客户端;
self.cli = self.create_client(AddInts, 'add_ints')
# 3-2.等待服务连接;
while not self.cli.wait_for_service(timeout_sec=1.0):
self.get_logger().info('服务连接中,请稍候...')
self.req = AddInts.Request()
# 3-3.组织请求数据并发送;
def send_request(self):
self.req.num1 = int(sys.argv[1])
self.req.num2 = int(sys.argv[2])
self.future = self.cli.call_async(self.req)
def main():
# 2.初始化 ROS2 客户端;
rclpy.init()
# 4.创建对象并调用其功能;
minimal_client = MinimalClient()
minimal_client.send_request()
# 处理响应
rclpy.spin_until_future_complete(minimal_client,minimal_client.future)
try:
response = minimal_client.future.result()
except Exception as e:
minimal_client.get_logger().info(
'服务请求失败: %r' % (e,))
else:
minimal_client.get_logger().info(
'响应结果: %d + %d = %d' %
(minimal_client.req.num1, minimal_client.req.num2, response.sum))
# 5.释放资源。
rclpy.shutdown()
if __name__ == '__main__':
main()
3.编辑配置文件
1.package.xml
在创建功能包时,所依赖的功能包已经自动配置了,配置内容如下:
<depend>rclpy</depend>
<depend>base_interfaces_demo</depend>
2.setup.py
entry_points字段的console_scripts中添加如下内容:
entry_points={
'console_scripts': [
'demo01_server_py = py02_service.demo01_server_py:main',
'demo02_client_py = py02_service.demo02_client_py:main'
],
},
4.编译
终端中进入当前工作空间,编译功能包:
colcon build --packages-select py02_service
5.执行
当前工作空间下,启动两个终端,终端1执行服务端程序,终端2执行客户端程序。
终端1输入如下指令:
. install/setup.bash
ros2 run py02_service demo01_server_py
终端2输入如下指令:
. install/setup.bash
ros2 run py02_service demo02_client_py 100 200
最终运行结果与案例类似。
2.4 动作通信
场景
关于action通信,我们先从之前导航中的应用场景开始介绍,描述如下:
机器人导航到某个目标点,此过程需要一个节点A发布目标信息,然后一个节点B接收到请求并控制移动,最终响应目标达成状态信息。
乍一看,这好像是服务通信实现,因为需求中要A发送目标,B执行并返回结果,这是一个典型的基于请求响应的应答模式,不过,如果只是使用基本的服务通信实现,存在一个问题:导航是一个过程,是耗时操作,如果使用服务通信,那么只有在导航结束时,才会产生响应结果,而在导航过程中,节点A是不会获取到任何反馈的,从而可能出现程序"假死"的现象,过程的不可控意味着不良的用户体验,以及逻辑处理的缺陷(比如:导航中止的需求无法实现)。更合理的方案应该是:导航过程中,可以连续反馈当前机器人状态信息,当导航终止时,再返回最终的执行结果。在ROS中,该实现策略称之为:action 通信。
概念
动作通信适用于长时间运行的任务。就结构而言动作通信由目标、反馈和结果三部分组成;就功能而言动作通信类似于服务通信,动作客户端可以发送请求到动作服务端,并接收动作服务端响应的最终结果,不过动作通信可以在请求响应过程中获取连续反馈,并且也可以向动作服务端发送任务取消请求;就底层实现而言动作通信是建立在话题通信和服务通信之上的,目标发送实现是对服务通信的封装,结果的获取也是对服务通信的封装,而连续反馈则是对话题通信的封装。
2.4.1 案例以及案例分析
1.案例需求
需求:编写动作通信,动作客户端提交一个整型数据N,动作服务端接收请求数据并累加1-N之间的所有整数,将最终结果返回给动作客户端,且每累加一次都需要计算当前运算进度并反馈给动作客户端。

2.案例分析
在上述案例中,需要关注的要素有三个:
动作客户端;
动作服务端;
消息载体。
3.流程简介
案例实现前需要先自定义动作接口,接口准备完毕后,动作通信实现主要步骤如下:
编写动作服务端实现;
编写动作客户端实现;
编辑配置文件;
编译;
执行。
案例我们会采用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_demo2.4.2 动作通信接口消息
定义动作接口消息与定义话题或服务接口消息流程类似,主要步骤如下:
创建并编辑
.action文件;编辑配置文件;
编译;
测试。
接下来,我们可以参考案例编写一个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文件一致的内容。
2.4.3 动作通信(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.创建动作服务端;
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
最终运行结果与案例类似。
2.4.4 动作通信(Python)
1.动作服务端实现
功能包py03_action的py03_action目录下,新建Python文件demo01_action_server_py.py,并编辑文件,输入如下内容:
"""
需求:编写动作服务端实习,可以提取客户端请求提交的整型数据,并累加从1到该数据之间的所有整数以求和,
每累加一次都计算当前运算进度并连续反馈回客户端,最后,在将求和结果返回给客户端。
步骤:
1.导包;
2.初始化 ROS2 客户端;
3.定义节点类;
3-1.创建动作服务端;
3-2.生成连续反馈;
3-3.生成最终响应。
4.调用spin函数,并传入节点对象;
5.释放资源。
"""
# 1.导包;
import time
import rclpy
from rclpy.action import ActionServer
from rclpy.node import Node
from base_interfaces_demo.action import Progress
# 3.定义节点类;
class ProgressActionServer(Node):
def __init__(self):
super().__init__('progress_action_server')
# 3-1.创建动作服务端;
self._action_server = ActionServer(
self,
Progress,
'get_sum',
self.execute_callback)
self.get_logger().info('动作服务已经启动!')
def execute_callback(self, goal_handle):
self.get_logger().info('开始执行任务....')
# 3-2.生成连续反馈;
feedback_msg = Progress.Feedback()
sum = 0
for i in range(1, goal_handle.request.num + 1):
sum += i
feedback_msg.progress = i / goal_handle.request.num
self.get_logger().info('连续反馈: %.2f' % feedback_msg.progress)
goal_handle.publish_feedback(feedback_msg)
time.sleep(1)
# 3-3.生成最终响应。
goal_handle.succeed()
result = Progress.Result()
result.sum = sum
self.get_logger().info('任务完成!')
return result
def main(args=None):
# 2.初始化 ROS2 客户端;
rclpy.init(args=args)
# 4.调用spin函数,并传入节点对象;
Progress_action_server = ProgressActionServer()
rclpy.spin(Progress_action_server)
# 5.释放资源。
rclpy.shutdown()
if __name__ == '__main__':
main()
2.动作客户端实现
功能包py03_action的py03_action目录下,新建Python文件demo02_action_client_py.py,并编辑文件,输入如下内容:
"""
需求:编写动作客户端实现,可以提交一个整型数据到服务端,并处理服务端的连续反馈以及最终返回结果。
步骤:
1.导包;
2.初始化 ROS2 客户端;
3.定义节点类;
3-1.创建动作客户端;
3-2.发送请求;
3-3.处理目标发送后的反馈;
3-4.处理连续反馈;
3-5.处理最终响应。
4.调用spin函数,并传入节点对象;
5.释放资源。
"""
# 1.导包;
import rclpy
from rclpy.action import ActionClient
from rclpy.node import Node
from base_interfaces_demo.action import Progress
# 3.定义节点类;
class ProgressActionClient(Node):
def __init__(self):
super().__init__('progress_action_client')
# 3-1.创建动作客户端;
self._action_client = ActionClient(self, Progress, 'get_sum')
def send_goal(self, num):
# 3-2.发送请求;
goal_msg = Progress.Goal()
goal_msg.num = num
self._action_client.wait_for_server()
self._send_goal_future = self._action_client.send_goal_async(goal_msg, feedback_callback=self.feedback_callback)
self._send_goal_future.add_done_callback(self.goal_response_callback)
def goal_response_callback(self, future):
# 3-3.处理目标发送后的反馈;
goal_handle = future.result()
if not goal_handle.accepted:
self.get_logger().info('请求被拒绝')
return
self.get_logger().info('请求被接收,开始执行任务!')
self._get_result_future = goal_handle.get_result_async()
self._get_result_future.add_done_callback(self.get_result_callback)
# 3-5.处理最终响应。
def get_result_callback(self, future):
result = future.result().result
self.get_logger().info('最终计算结果:sum = %d' % result.sum)
# 5.释放资源。
rclpy.shutdown()
# 3-4.处理连续反馈;
def feedback_callback(self, feedback_msg):
feedback = (int)(feedback_msg.feedback.progress * 100)
self.get_logger().info('当前进度: %d%%' % feedback)
def main(args=None):
# 2.初始化 ROS2 客户端;
rclpy.init(args=args)
# 4.调用spin函数,并传入节点对象;
action_client = ProgressActionClient()
action_client.send_goal(10)
rclpy.spin(action_client)
# rclpy.shutdown()
if __name__ == '__main__':
main()
3.编辑配置文件
1.package.xml
在创建功能包时,所依赖的功能包已经自动配置了,配置内容如下:
<depend>rclpy</depend>
<depend>base_interfaces_demo</depend>
2.setup.py
entry_points字段的console_scripts中添加如下内容:
entry_points={
'console_scripts': [
'demo01_action_server_py = py03_action.demo01_action_server_py:main',
'demo02_action_client_py = py03_action.demo02_action_client_py:main'
],
},
4.编译
终端中进入当前工作空间,编译功能包:
colcon build --packages-select py03_action
5.执行
当前工作空间下,启动两个终端,终端1执行动作服务端程序,终端2执行动作客户端程序。
终端1输入如下指令:
. install/setup.bash
ros2 run py03_action demo01_action_server_py
终端2输入如下指令:
. install/setup.bash
ros2 run py03_action demo02_action_client_py
最终运行结果与案例类似。
2.5 参数服务
场景
在机器人系统中不同的功能模块可能会使用到一些相同的数据,比如:
导航实现时,会进行路径规划,路径规划主要包含, 全局路径规划和本地路径规划,所谓全局路径规划就是设计一个从出发点到目标点的大致路径,而本地路径规划,则是根据车辆当前路况生成实时的行进路径。两种路径规划实现,都会使用到车辆的尺寸数据——长度、宽度、高度等。那么这些通用数据在程序中应该如何存储、调用呢?
上述场景中,就可以使用参数服务实现,在一个节点下保存车辆尺寸数据,其他节点可以访问该节点并操作这些数据。
概念
参数服务是以共享的方式实现不同节点之间数据交互的一种通信模式。保存参数的节点称之为参数服务端,调用参数的节点称之为参数客户端。参数客户端与参数服务端的交互是基于请求响应的,且参数通信的实现本质上对服务通信的进一步封装。
作用
参数服务保存的数据类似于编程中“全局变量”的概念,可以在不同的节点之间共享数据。
2.5.1 案例以及案例分析
1.案例需求
需求:在参数服务端设置一些参数,参数客户端访问服务端并操作这些参数。

2.案例分析
在上述案例中,需要关注的要素有三个:
参数客户端;
参数服务端;
参数。
3.流程简介
案例实现前需要先了解ROS2中参数的相关API,无论是客户端还是服务端都会使用到参数,而参数服务案例实现主要步骤如下:
编写参数服务端实现;
编写参数客户端实现;
编辑配置文件;
编译;
执行。
案例我们会采用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 rclpy2.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());
...
Python示例:
# 创建参数对象
p1 = rclpy.Parameter("car_name",value="Horse")
p2 = rclpy.Parameter("length",value=0.5)
p3 = rclpy.Parameter("wheels",value=4)
# 获取参数值
get_logger("rclpy").info("car_name = %s" % p1.value)
get_logger("rclpy").info("length = %.2f" % p2.value)
get_logger("rclpy").info("wheels = %d" % p3.value)
# 获取参数键
get_logger("rclpy").info("p1 name = %s" % p1.name)
关于参数具体的API使用,在后续案例中会有介绍。
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<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 ¶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;
}
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
最终运行结果与案例类似。
2.5.4 参数服务(Python)
1.参数服务端
功能包py04_param的py04_param目录下,新建Python文件demo01_param_server_py.py,并编辑文件,输入如下内容:
"""
需求:编写参数服务端,设置并操作参数。
步骤:
1.导包;
2.初始化 ROS2 客户端;
3.定义节点类;
3-1.声明参数;
3-2.查询参数;
3-3.修改参数;
3-4.删除参数。
4.创建节点对象,调用参数操作函数,并传递给spin函数;
5.释放资源。
"""
# 1.导包;
import rclpy
from rclpy.node import Node
# 3.定义节点类;
class MinimalParamServer(Node):
def __init__(self):
super().__init__("minimal_param_server",allow_undeclared_parameters=True)
self.get_logger().info("参数演示")
# 3-1.声明参数;
def declare_param(self):
self.declare_parameter("car_type","Tiger")
self.declare_parameter("height",1.50)
self.declare_parameter("wheels",4)
self.p1 = rclpy.Parameter("car_type",value = "Mouse")
self.p2 = rclpy.Parameter("undcl_test",value = 100)
self.set_parameters([self.p1,self.p2])
# 3-2.查询参数;
def get_param(self):
self.get_logger().info("--------------查-------------")
# 判断包含
self.get_logger().info("包含 car_type 吗?%d" % self.has_parameter("car_type"))
self.get_logger().info("包含 width 吗?%d" % self.has_parameter("width"))
# 获取指定
car_type = self.get_parameter("car_type")
self.get_logger().info("%s = %s " % (car_type.name, car_type.value))
# 获取所有
params = self.get_parameters(["car_type","height","wheels"])
self.get_logger().info("解析所有参数:")
for param in params:
self.get_logger().info("%s ---> %s" % (param.name, param.value))
# 3-3.修改参数;
def update_param(self):
self.get_logger().info("--------------改-------------")
self.set_parameters([rclpy.Parameter("car_type",value = "horse")])
param = self.get_parameter("car_type")
self.get_logger().info("修改后: car_type = %s" %param.value)
# 3-4.删除参数。
def del_param(self):
self.get_logger().info("--------------删-------------")
self.get_logger().info("删除操作前包含 car_type 吗?%d" % self.has_parameter("car_type"))
self.undeclare_parameter("car_type")
self.get_logger().info("删除操作后包含 car_type 吗?%d" % self.has_parameter("car_type"))
def main():
# 2.初始化 ROS2 客户端;
rclpy.init()
# 4.创建节点对象,调用参数操作函数,并传递给spin函数;
param_server = MinimalParamServer()
param_server.declare_param()
param_server.get_param()
param_server.update_param()
param_server.del_param()
rclpy.spin(param_server)
# 5.释放资源。
rclpy.shutdown()
if __name__ == "__main__":
main()
2.参数客户端
ROS2的Python客户端暂时没有提供参数客户端专用的API,但是参数服务的底层是基于服务通信的,所以可以通过服务通信操作参数服务端的参数。
3.编辑配置文件
1.package.xml
在创建功能包时,所依赖的功能包已经自动配置了,配置内容如下:
<depend>rclpy</depend>
2.setup.py
entry_points字段的console_scripts中添加如下内容:
entry_points={
'console_scripts': [
'demo01_param_server_py = py04_param.demo01_param_server_py:main'
],
},
4.编译
终端中进入当前工作空间,编译功能包:
colcon build --packages-select py04_param
5.执行
当前工作空间下,启动两个终端,终端1执行参数服务端程序,终端2执行参数客户端程序(使用2.5.3中的C++实现)。
终端1输入如下指令:
. install/setup.bash
ros2 run py04_param demo01_param_server_py
终端2输入如下指令:
. install/setup.bash
ros2 run cpp04_param demo02_param_client
最终运行结果与案例类似。
资料:
以服务通信方式操作参数服务端示例代码:
# 1.导包
import rclpy
from rclpy.node import Node
from rcl_interfaces.srv import ListParameters
from rcl_interfaces.srv import GetParameters
from rcl_interfaces.srv import SetParameters
from rcl_interfaces.msg import ParameterType
from rcl_interfaces.msg import Parameter
from rcl_interfaces.msg import ParameterValue
from ros2param.api import get_parameter_value
class MinimalParamClient(Node):
def __init__(self):
super().__init__('minimal_param_client_py')
def list_params(self):
# 3-1.创建客户端;
cli_list = self.create_client(ListParameters, '/minimal_param_server/list_parameters')
# 3-2.等待服务连接;
while not cli_list.wait_for_service(timeout_sec=1.0):
self.get_logger().info('列出参数服务连接中,请稍候...')
req = ListParameters.Request()
future = cli_list.call_async(req)
rclpy.spin_until_future_complete(self,future)
return future.result()
def get_params(self,names):
# 3-1.创建客户端;
cli_get = self.create_client(GetParameters, '/minimal_param_server/get_parameters')
# 3-2.等待服务连接;
while not cli_get.wait_for_service(timeout_sec=1.0):
self.get_logger().info('列出参数服务连接中,请稍候...')
req = GetParameters.Request()
req.names = names
future = cli_get.call_async(req)
rclpy.spin_until_future_complete(self,future)
return future.result()
def set_params(self):
# 3-1.创建客户端;
cli_set = self.create_client(SetParameters, '/minimal_param_server/set_parameters')
# 3-2.等待服务连接;
while not cli_set.wait_for_service(timeout_sec=1.0):
self.get_logger().info('列出参数服务连接中,请稍候...')
req = SetParameters.Request()
p1 = Parameter()
p1.name = "car_type"
# v1 = ParameterValue()
# v1.type = ParameterType.PARAMETER_STRING
# v1.string_value = "Pig"
# p1.value = v1
p1.value = get_parameter_value(string_value="Pig")
p2 = Parameter()
p2.name = "height"
v2 = ParameterValue()
v2.type = ParameterType.PARAMETER_DOUBLE
v2.double_value = 0.3
p2.value = v2
# p2.value = get_parameter_value(string_value="0.3")
req.parameters = [p1, p2]
future = cli_set.call_async(req)
rclpy.spin_until_future_complete(self,future)
return future.result()
def main():
# 2.初始化 ROS2 客户端;
rclpy.init()
# 4.创建对象并调用其功能;
client = MinimalParamClient()
# 获取参数列表
client.get_logger().info("---------获取参数列表---------")
response = client.list_params()
for name in response.result.names:
client.get_logger().info(name)
client.get_logger().info("---------获取参数---------")
names = ["height","car_type"]
response = client.get_params(names)
# print(response.values)
for v in response.values:
if v.type == ParameterType.PARAMETER_STRING:
client.get_logger().info("字符串值:%s" % v.string_value)
elif v.type == ParameterType.PARAMETER_DOUBLE:
client.get_logger().info("浮点值:%.2f" % v.double_value)
client.get_logger().info("---------设置参数---------")
response = client.set_params()
results = response.results
client.get_logger().info("设置了%d个参数" % len(results))
for result in results:
if not result.successful:
client.get_logger().info("参数设置失败")
rclpy.shutdown()
if __name__ == "__main__":
main()2.6 本章小结
本章主要介绍了ROS2中常用的四种通信机制:
话题通信;
服务通信;
动作通信;
参数服务。
无论何种通信机制,他们的实现框架都是类似的。比如:通信必然涉及到双方,双方需要通过“话题”关联,通信还都必然涉及到数据,一般可以通过接口文件来定义数据格式(参数服务是通过参数类封装数据)。
不同的通信机制其实现模型也存在明显差异。话题通信是基于广播的单向数据交互模式;服务通信是基于请求响应的问答式交数据互模式;动作通信则是在请求响应的过程中又包含连续反馈的数据交互模式;参数服务是基于服务通信的,可以在不同节点间实现数据共享。实现模型的差异也决定着他们有着不同的应用场景,大家可以根据自己的实际需求灵活选择。


