我最近在学习小鱼ROS2,针对学习的内容简单整理了一下,分享给大家供参考。
1.OS层
从原来的只支持linux平台变成了支持windows、mac甚至是嵌入式RTOS平台,这一点要点个赞。
2.MiddleWare中间件层
那么中间层ROS2到底相对于ROS做了哪些优化呢?
1.DDS是什么
数据分发服务DDS(DataDistributionService)是对象管理组织(OMG)在HLA及CORBA等标准的基础上制定的新一代分布式实时通信中间件技术规范,DDS采用发布/订阅体系架构,强调以数据为中心,提供丰富的QoS服务质量策略,能保障数据进行实时、高效、灵活地分发,可满足各种分布式实时通信应用需求。DDS信息分发中间件是一种轻便的、能够提供实时信息传送的中间件技术。
2.DDS的QoS
可靠性
基于时间的过滤
接收端顺序
持久性
DDS实现层其实就是对不同常见的DDS接口进行再次的封装,让其保持统一性,为DDS抽象层提供统一的API。
这一层将DDS实现层进一步的封装,使得DDS更容易使用。原因在于DDS需要大量的设置和配置(分区,主题名称,发现模式,消息创建,…),这些设置都是在ROS2的抽象层中完成的。
RCL(ROS Client Library)ROS客户端库,其实就是ROS的一种API,提供了对ROS话题、服务、参数、Action等接口。
1.GUI和CLI
● GUI(Graphical User Interface)就是平常我们说的图形用户界面,大家用的Windows是就是可视化的,我们可以通过鼠标点击按钮等图形化交互完成任务。
● CLI(Command-Line Interface)就是命令行界面了,我们所用的终端,黑框框就是命令行界面,没有图形化。
2.API是什么
API( Application Programming Interface)应用程序编程接口。比如你写了一个库,里面有很多函数,如果别人要使用你这个库,但是并不知道每个函数内部是怎么实现的。使用的人需要看你的文档或者注释才知道这个函数的入口参数和返回值或者这个函数是用来做什么的。对于使用者来说来说 ,你的这些函数就是API。(摘自知乎)API在不同语言中的表现形式不同,在C和C++表现为头文件,在Python中表现为Python文件。
ROS的客户端库就是上面所说的RCL,不同的语言对应着不同的rcl,但基本功能都是相同的。比如Python语言提供了rclpy来操作ROS2的节点话题服务等,而C++则使用rclcpp提供API操作ROS2的节点话题和服务等。所以后面我们使用Python和C++来编写ROS2节点实现通讯等功能时,我们就会引入rclpy和rclcpp的库。
上面这张图时ROS2,API的实现层级,最新下面的是第三方的DDS,rmw(中间件接口)层是对各家DDS的抽象层,基于rmw实现了rclc,有了rclc,我们就可以实现各个语言的库,大家都知道C语言是各个语言的鼻祖(汇编除外)所以基于rclc,ROS2官方实现了rclpy和rclcpp.
顾名思义
中间件就是介于某两个或者多个节点中间的组件。干嘛用的呢?
就是提供多个节点中间通信用的。
官方解释就比较玄乎了:
中间件是一种独立的系统软件或服务程序,分布式应用软件借助这种软件在不同的技术之间共享资源。中间件位于客户机/ 服务器的操作系统之上,管理计算机资源和网络通讯。是连接两个独立应用程序或独立系统的软件。相连接的系统,即使它们具有不同的接口,但通过中间件相互之间仍能交换信息。执行中间件的一个关键途径是信息传递。通过中间件,应用程序可以工作于多平台或OS环境。
ROS1的中间件是ROS组织自己基于TCP/UDP机制建立的,为了维护该部分ROS1组织花费了大量的精力,但是依然存在很多问题。
ROS2采用了第三方的DDS作为中间件,将DDS服务接口进行了一层抽象,保证了上层应用层调用接口的统一性。
基于DDS的互相发现协议,ROS2终于干掉了ROS1中的Master节点。
ROS2为每家DDS供应商都开发了对应的DDS_Interface即DDS接口层,然后通过DDS Abstract抽象层来统一DDS的API。
ROS2架构中的DDS部分
DDS的模型是非常容易理解,我们可以定义话题的数据结构(类似于ROS2中的接口类型)。下图中的例子:
● Pos:一个编号id的车子的位置x,y
DDS的参与者(Participant)通过发布和订阅主题数据进行通信。
DDS的应用层通过DDS进行数据订阅发布,DDS通过传输层进行数据的收发。
● 发布/订阅模型:简单解耦,可以轻松实现系统解耦
● 性能:在发布/订阅模式中,与请求/回复模式相比,延迟更低,吞吐量更高。
● 远程参与者的自动发现:此机制是 DDS 的主要功能之一。通信是匿名的、解耦的,开发者不必担心远程参与者的本地化。
● 丰富的 Qos 参数集,允许调整通信的各个方面:可靠性、持久性、冗余、寿命、传输设置、资源…
● 实时发布订阅协议 ( RTPS ):该协议几乎可以通过任何传输实现,允许在 UDP、TCP、共享内存和用户传输中使用 DDS,并实现不同 DDS 实现之间的真正互操作性。
● API复杂,DDS 的灵活性是以复杂性为代价的。
● 系统开销相对较大,小鱼实际体会,待数据论证。
● 社区支持问题,但ROS2近两年来使用DDS后社区表现还是不错的。
1.启动海龟模拟器
ros2 run turtlesim turtlesim_node
2.启动海龟遥控器
ros2 run turtlesim turtle_teleop_key
1.可视化命令
rqt_graph
2.查看节点列表
ros2 node list
3.节点的启动
ros2 run
4.查看节点信息
ros2 node info
5.重映射节点名称
ros2 run turtlesim turtlesim_node --ros-args --remap __node:=my_turtle
6.运行节点时设置参数
ros2 run example_parameters_rclcpp parameters_basic --ros-args -p rcl_log_level:=10
7.功能包的安装
sudo apt install ros--package_name
8.创建功能包
ros2 pkg create --build-type {cmake,ament_cmake,ament_python} --dependencies <依赖名字>
9.列出所有功能包的可执行文件
ros2 pkg executables
10.列出指定功能包的所有可执行文件,如下:turtlesim
ros2 pkg executables turtlesim
11.列出所有的功能包
ros2 pkg list
12.列出包的清单描述文件
每一个功能包都有一个标配的manifest.xml文件,用于记录这个包的名字,构建工具,编译信息,拥有者,干啥用的等信息。
通过这个信息,就可以自动为该功能包安装依赖,构建时确定编译顺序等。如turtlesim
ros2 pkg xml turtlesim
13.录bag包
ros2 bag record -a
14.放bag包
ros2 bag play xxxxx --read-ahead-queue-size=1000
15.增加topic的消息类型
ros2 topic list -t
16.查看消息类型中的数据结构
ros2 interface show std_msgs/msg/String
17.手动发布topic命令
ros2 topic pub /chatter std_msgs/msg/String 'data: "123"'
18.查看服务列表
ros2 service list
19.查看功能包下的所有接口信息,以sensor_msgs为例
ros2 interface package sensor_msgs
C++节点
cpp文件
// 包含rclcpp头文件,如果Vscode显示红色的波浪线也没关系
// 我们只是把VsCode当记事本而已,谁会在意记事本对代码的看法呢,不是吗?
#include "rclcpp/rclcpp.hpp"
int main(int argc, char **argv)
{
// 调用rclcpp的初始化函数
rclcpp::init(argc, argv);
// 调用rclcpp的循环运行我们创建的first_node节点
rclcpp::spin(std::make_shared("first_node"));
return 0;
}
CMakeLists.txt
cmake_minimum_required(VERSION 3.22)
project(first_node)
#include_directories 添加特定的头文件搜索路径 ,相当于指定g++编译器的-I参数
include_directories(/opt/ros/humble/include/rclcpp/)
include_directories(/opt/ros/humble/include/rcl/)
include_directories(/opt/ros/humble/include/rcutils/)
include_directories(/opt/ros/humble/include/rcl_yaml_param_parser/)
include_directories(/opt/ros/humble/include/rosidl_runtime_c/)
include_directories(/opt/ros/humble/include/rosidl_typesupport_interface/)
include_directories(/opt/ros/humble/include/rcpputils/)
include_directories(/opt/ros/humble/include/builtin_interfaces/)
include_directories(/opt/ros/humble/include/rmw/)
include_directories(/opt/ros/humble/include/rosidl_runtime_cpp/)
include_directories(/opt/ros/humble/include/tracetools/)
include_directories(/opt/ros/humble/include/rcl_interfaces/)
include_directories(/opt/ros/humble/include/libstatistics_collector/)
include_directories(/opt/ros/humble/include/statistics_msgs/)
# link_directories - 向工程添加多个特定的库文件搜索路径,相当于指定g++编译器的-L参数
link_directories(/opt/ros/humble/lib/)
# add_executable - 生成first_node可执行文件
add_executable(first_node first_ros2_node.cpp)
# target_link_libraries - 为first_node(目标) 添加需要动态链接库,相同于指定g++编译器-l参数
# 下面的语句代替 -lrclcpp -lrcutils
target_link_libraries(first_node rclcpp rcutils)
Python节点
# 导入rclpy库,如果Vscode显示红色的波浪线也没关系
# 我们只是把VsCode当记事本而已,谁会在意记事本对代码的看法呢,不是吗?
import rclpy
from rclpy.node import Node
# 调用rclcpp的初始化函数
rclpy.init()
# 调用rclcpp的循环运行我们创建的second_node节点
rclpy.spin(Node("second_node"))
ROS2提供了四种节点交互方式:
● 话题-topics
● 服务-services
● 动作-Action
● 参数-parameters
●
1.colcon的安装
sudo apt-get install python3-colcon-common-extensions
2.编译命令
colcon build #类似于catkin_make,编译src下的所有功能包
colcon build --packages-select YOUR_PKG_NAME #编译指定的功能包
colcon build --packages-select YOUR_PKG_NAME --cmake-args -DBUILD_TESTING=0 #不编译测试单元
colcon test #运行编译的功能包的测试
colcon build --symlink-install #每次调整python脚本时都不必重新build了
构建完成后,在src同级目录我们应该会看到 build 、 install 和 log 目录:
.
├── build
├── install
├── log
└── src
4 directories, 0 files
● build 目录存储的是中间文件。对于每个包,将创建一个子文件夹,在其中调用例如CMake
● install 目录是每个软件包将安装到的位置。默认情况下,每个包都将安装到单独的子目录中。
● log 目录包含有关每个colcon调用的各种日志信息。
环境变量的source:
source install/setup.bash
colcon build编译命令行参数
--packages-select ,仅生成单个包(或选定的包)。
--packages-up-to,构建选定的包,包括其依赖项。
--packages-above,整个工作区,然后对其中一个包进行了更改。此指令将重构此包以及(递归地)依赖于此包的所有包。
--build-base参数和--install-base,指定构建目录和安装目录。
--merge-install,使用 作为所有软件包的安装前缀,而不是安装基中的软件包特定子目录。--install-base
如果没有此选项,每个包都将提供自己的环境变量路径,从而导致非常长的环境变量值。
使用此选项时,添加到环境变量的大多数路径将相同,从而导致环境变量值更短。
--symlink-install后将不会把文件拷贝到install目录,而是通过创建符号链接的方式。
--continue-on-error,当发生错误的时候继续进行编译。
--log-level可以设置日志级别,比如--log-level info
1.创建功能包
cd d2lros2/chapt2/
mkdir -p chapt2_ws/src/
cd chapt2_ws/src
ros2 pkg create example_cpp --build-type ament_cmake --dependencies rclcpp
创建完后的目录结构:
.
└── src
└── example_cpp
├── CMakeLists.txt
├── include
│ └── example_cpp
├── package.xml
└── src
5 directories, 2 files
2.创建节点
cd chapt2_ws/src/example_cpp/src
创建node_01.cpp文件
############################################################
#include "rclcpp/rclcpp.hpp"
int main(int argc, char **argv)
{
/* 初始化rclcpp */
rclcpp::init(argc, argv);
/*产生一个node_01的节点*/
auto node = std::make_shared("node_01");
// 打印一句自我介绍
RCLCPP_INFO(node->get_logger(), "node_01节点已经启动.");
/* 运行节点,并检测退出信号 Ctrl+C*/
rclcpp::spin(node);
/* 停止运行 */
rclcpp::shutdown();
return 0;
}
###########################################################
3.CMakeLists.txt的修改
add_executable(node_01 src/node_01.cpp)
ament_target_dependencies(node_01 rclcpp)
install(TARGETS
node_01
DESTINATION lib/${PROJECT_NAME}
)
4.编译、source、运行节点
colcon build
source install/setup.bash
ros2 run example_cpp node_01
1.创建功能包
cd chapt2/chapt2_ws/src/
ros2 pkg create example_py --build-type ament_python --dependencies rclpy
创建完后的目录结构
.
├── example_py
│ └── __init__.py
├── package.xml
├── resource
│ └── example_py
├── setup.cfg
├── setup.py
└── test
├── test_copyright.py
├── test_flake8.py
└── test_pep257.py
3 directories, 8 files
2.编码
1. 导入库文件
2. 初始化客户端库
3. 新建节点
4. spin循环节点
5. 关闭客户端库
在example_py下创建node_02.py接着我们开始编写代码
###########################################################
import rclpy
from rclpy.node import Node
def main(args=None):
"""
ros2运行该节点的入口函数
编写ROS2节点的一般步骤
1. 导入库文件
2. 初始化客户端库
3. 新建节点对象
4. spin循环节点
5. 关闭客户端库
"""
rclpy.init(args=args) # 初始化rclpy
node = Node("node_02") # 新建一个节点
node.get_logger().info("大家好,我是node_02.")
rclpy.spin(node) # 保持节点运行,检测是否收到退出指令(Ctrl+C)
rclpy.shutdown() # 关闭rclpy
###########################################################
3.修改配置
代码编写完成用Crtl+S进行保存。接着修改setup.py。
entry_points={
'console_scripts': [
"node_02 = example_py.node_02:main"
],
},
)
声明一个ROS2的节点,声明后使用colcon build才能检测到,从而将其添加到install目录下
4.编译、source、运行
colcon build
source install/setup.bash
ros2 run example_py node_02
C++编程
#include "rclcpp/rclcpp.hpp"
/*
创建一个类节点,名字叫做Node03,继承自Node.
*/
class Node03 : public rclcpp::Node
{
public:
// 构造函数,有一个参数为节点名称
Node03(std::string name) : Node(name)
{
// 打印一句
RCLCPP_INFO(this->get_logger(), "大家好,我是%s.",name.c_str());
}
private:
};
int main(int argc, char **argv)
{
rclcpp::init(argc, argv);
/*产生一个node_03的节点*/
auto node = std::make_shared("node_03");
/* 运行节点,并检测退出信号*/
rclcpp::spin(node);
rclcpp::shutdown();
return 0;
}
Python版本
#!/usr/bin/env python3
import rclpy
from rclpy.node import Node
class Node04(Node):
"""
创建一个Node04节点,并在初始化时输出一个话
"""
def __init__(self,name):
super().__init__(name)
self.get_logger().info("大家好,我是%s!" % name)
def main(args=None):
rclpy.init(args=args) # 初始化rclpy
node = Node04("node_04") # 新建一个节点
rclpy.spin(node) # 保持节点运行,检测是否收到退出指令(Ctrl+C)
rclpy.shutdown() # 关闭rclpy
Multicast Port 多播端口,Unicast Port 单播端口,Domain ID 域ID,Participant ID 参与者ID
如其他地方所解释的,ROS 2用于通讯的默认中间件是DDS。在DDS中,不同逻辑网络共享物理网络的主要机制称为域(Domain) ID。同一域上的ROS 2节点可以自由地相互发现并发送消息,而不同域上的ROS 2节点则不能。所有ROS 2节点默认使用域ID为0。为了避免在同一网络上运行ROS 2的不同计算机组之间互相不干扰,应为每组设置不同的域ID。
Linux
默认情况下,linux内核使用端口32768-60999作为临时端口。这意味着域ID 0-101 和 215-232 可以安全使用,而不会与临时端口发生冲突。临时端口范围可在Linux中通过在 /proc/sys/net/ipv4/ip_local_port_range 中设置自定义值进行配置。如果使用自定义临时端口范围,则可能需要相应地调整上述数字。
参与者约束
对于计算机上运行的每个ROS 2进程,将创建一个DDS “participant” 。由于每个DDS参与者占用计算机上的两个端口,因此在一台计算机上运行120个以上的ROS 2进程可能会溢出到其他域ID或临时端口。
为了解释原因,我们考虑域ID编号1和2。
● 域ID 1使用端口7650和7651进行多播。
● 域ID 2使用端口7900和7901进行多播。
● 在域ID 1中创建第一个进程 (第0个参与者) 时,端口7660和7661用于单播。
● 在域ID 1中创建第120个进程 (第119个参与者) 时,端口7898和7899用于单播。
● 在域ID 1中创建第121个进程 (第120个参与者) 时,端口7900和7901用于单播,并与域ID 2重叠。
如果已知计算机一次只能在一个域ID上,并且域ID足够低,那么创建比这更多的ROS 2进程是安全的。
在选择特定平台域 ID 范围顶部的域 ID 时,还有一个限制因素需要考虑。
例如,假设一台ID为101的Linux计算机:
● 计算机上的第0个ROS 2进程将连接到端口32650、32651、32660和32661。
● 计算机上的第1个ROS 2进程将连接到端口32650、32651、32662和32663。
● 计算机上的第53个ROS 2进程将连接到端口32650、32651、32766和32767。
● 计算机上的第54个ROS 2进程将连接到端口32650、32651、32768和32769,运行在临时端口范围内。
因此,在Linux上使用域ID为101时应创建的最大进程数为54。同样,在Linux上使用域ID为232时应创建的最大进程数为63,因为最大端口号为65535。
域ID到UDP端口号计算器
http://dev.ros2.fishros.com/doc/Concepts/About-Domain-ID.html#domain-id-to-udp-port-calculator
1.什么是ZeroMQ
ZeroMQ非常的轻量,也就是小巧,占用资源少,看名字,Zero Message Queue,零消息队列。ZeroMQ提供了各种(如进程内、进程间、TCP 和多播)消息传输的套接字。
2.PyZmq
了解完ZMQ是啥后,我们再来看看PyZMQ。
● https://pyzmq.readthedocs.io/en/latest/
pyzmq也提供了类似于订阅发布的方式来传递消息,还有更多的使用方法,比如客户端服务端这种,网上有大佬已经探索了,小鱼贴个链接在这里,大家有需要使用的可以去瞅一瞅。
● https://www.php.cn/python-tutorials-459626.html
查看服务列表
ros2 service list
手动调用服务
ros2 service call /add_two_ints example_interfaces/srv/AddTwoInts "{a: 5,b: 10}"
查看服务的接口类型
ros2 service type /add_two_ints
查找使用某个接口类型的所有服务
ros2 service find example_interfaces/srv/AddTwoInts
c++
#include "example_interfaces/srv/add_two_ints.hpp"
#include "rclcpp/rclcpp.hpp"
class ServiceServer01 : public rclcpp::Node {
public:
ServiceServer01(std::string name) : Node(name) {
RCLCPP_INFO(this->get_logger(), "节点已启动:%s.", name.c_str());
// 创建服务
add_ints_server_ =
this->create_service(
"add_two_ints_srv",
std::bind(&ServiceServer01::handle_add_two_ints, this,
std::placeholders::_1, std::placeholders::_2));
}
private:
// 声明一个服务
rclcpp::Service::SharedPtr
add_ints_server_;
// 收到请求的处理函数
void handle_add_two_ints(
const std::shared_ptr request,
std::shared_ptr response) {
RCLCPP_INFO(this->get_logger(), "收到a: %ld b: %ld", request->a,
request->b);
response->sum = request->a + request->b;
};
};
Python
# 导入接口
from example_interfaces.srv import AddTwoInts
class ServiceServer02(Node):
def __init__(self,name):
super().__init__(name)
self.get_logger().info("节点已启动:%s!" % name)
self.add_ints_server_ = self.create_service(AddTwoInts,"add_two_ints_srv", self.handle_add_two_ints)
def handle_add_two_ints(self,request, response):
self.get_logger().info(f"收到请求,计算{request.a} + {request.b}")
response.sum = request.a + request.b
return response
c++
#include "example_interfaces/srv/add_two_ints.hpp"
class ServiceClient01 : public rclcpp::Node {
public:
// 构造函数,有一个参数为节点名称
ServiceClient01(std::string name) : Node(name) {
RCLCPP_INFO(this->get_logger(), "节点已启动:%s.", name.c_str());
// 创建客户端
client_ = this->create_client("add_two_ints_srv");
}
void send_request(int a, int b) {
RCLCPP_INFO(this->get_logger(), "计算%d+%d", a, b);
// 1.等待服务端上线
while (!client_->wait_for_service(std::chrono::seconds(1))) {
//等待时检测rclcpp的状态
if (!rclcpp::ok()) {
RCLCPP_ERROR(this->get_logger(), "等待服务的过程中被打断...");
return;
}
RCLCPP_INFO(this->get_logger(), "等待服务端上线中");
}
// 2.构造请求的
auto request =
std::make_shared();
request->a = a;
request->b = b;
// 3.发送异步请求,然后等待返回,返回时调用回调函数
client_->async_send_request(
request, std::bind(&ServiceClient01::result_callback_, this,
std::placeholders::_1));
};
private:
// 声明客户端
rclcpp::Client::SharedPtr client_;
void result_callback_(
rclcpp::Client::SharedFuture
result_future) {
auto response = result_future.get();
RCLCPP_INFO(this->get_logger(), "计算结果:%ld", response->sum);
}
};
Python
from example_interfaces.srv import AddTwoInts
class ServiceClient02(Node):
def __init__(self,name):
super().__init__(name)
self.get_logger().info("节点已启动:%s!" % name)
self.client_ = self.create_client(AddTwoInts,"add_two_ints_srv")
def result_callback_(self, result_future):
response = result_future.result()
self.get_logger().info(f"收到返回结果:{response.sum}")
def send_request(self, a, b):
while rclpy.ok() and self.client_.wait_for_service(1)==False:
self.get_logger().info(f"等待服务端上线....")
request = AddTwoInts.Request()
request.a = a
request.b = b
self.client_.call_async(request).add_done_callback(self.result_callback_)
def main(args=None):
rclpy.init(args=args) # 初始化rclpy
node = ServiceClient02("service_client_02") # 新建一个节点
# 调用函数发送请求
node.send_request(3,4)
rclpy.spin(node) # 保持节点运行,检测是否收到退出指令(Ctrl+C)
rclpy.shutdown() # 关闭rclpy
1 可以定义的接口三种类型
● 话题-Topics
● 服务-Services
● 动作-Action
● 参数-Parameters
除了参数之外,话题、服务和动作(Action)都支持自定义接口,每一种通信方式所适用的场景各不相同,所定义的接口也被分为话题接口、服务接口、动作接口三种。
2 接口形式
话题接口格式:xxx.msg
int64 num
服务接口格式:xxx.srv
int64 a
int64 b
---
int64 sum
动作接口格式:xxx.action
int32 order
---
int32[] sequence
---
int32[] partial_sequence
3 接口数据类型
根据引用方式不同可以分为基础类型和包装类型两类。
基础类型有(同时后面加上[]可形成数组)
bool byte char float32,float64 int8,uint8 int16,uint16 int32,uint32 int64,uint64 string
包装类型
即在已有的接口类型上进行包含,比如
uint32 id
string image_name
sensor_msgs/Image image
4 接口如何生成代码
通过ROS2的IDL模块将msg、srv、action文件转换为Python和C++的头文件。
创建功能包
ros2 pkg create example_ros2_interfaces --build-type ament_cmake --dependencies rosidl_default_generators geometry_msgs
注意功能包类型必须为:ament_cmake
依赖rosidl_default_generators必须添加,geometry_msgs视内容情况添加(我们这里有geometry_msgs/Pose pose所以要添加)。
接着创建文件夹和文件将3.2中文件写入,注意话题接口放到msg文件夹下,以.msg结尾。服务接口放到srv文件夹下,以srv结尾。
.
├── CMakeLists.txt
├── msg
│ ├── RobotPose.msg
│ └── RobotStatus.msg
├── package.xml
└── srv
└── MoveRobot.srv
2 directories, 5 files
接着修改CMakeLists.txt
find_package(rosidl_default_generators REQUIRED)
find_package(geometry_msgs REQUIRED)
# 添加下面的内容
rosidl_generate_interfaces(${PROJECT_NAME}
"msg/RobotPose.msg"
"msg/RobotStatus.msg"
"srv/MoveRobot.srv"
DEPENDENCIES geometry_msgs
)
保存即可编译
colcon build --packages-select example_ros2_interfaces
编译完成后在chapt3_ws/install/example_ros2_interfaces/include下你应该可以看到C++的头文件。在chapt3_ws/install/example_ros2_interfaces/local/lib/python3.10/dist-packages下应该可以看到Python版本的头文件。接下来的代码里我们就可以通过头文件导入和使用我们定义的接口了。
在ROS1中,节点间的通信是基于TCP的。因为TCP的失败重传机制,在一些网络不稳定的场景,通信会出现延时严重的问题。这大大限制了ROS1的使用场景。
在ROS2中,采用DDS作为通信中间件。ROS2的DDS中间件是可以配置成不同厂家提供的。这些不同的DDS各自有不同的侧重点,可根据项目的不同需求来选择。ROS2 Galactic和Rolling默认采用rmw_cyclonedds_cpp。rmw_cyclonedds_cpp在进程间和多主机间通信的场景下,主要是使用UDP做为通信媒介。
通过正确的服务质量策略配置,ROS2可以像TCP一样可靠,也可以像UDP那样尽力而为。在不稳定的网络环境下,“尽力而为”策略将更合适。在实时性要求高的场景下,设定数据的有效性将是必须的。
针对节点特定的工作负载和使用场景,有倾向地配置Qos将可以使通信质量达到最佳。
我们可以为发布器、订阅器、提供服务的服务器和客户端配置QoS。
因为每个节点的Qos是可以单独配置的,所以如果配置的Qos互相不兼容,节点间的通信将无法建立。
Qos(Quality of Service)有哪些配置项
配置项目
● History
○ Keep last: 只缓存最新的N个数据,N可通过Depth的Queue size配置。
○ Keep all: 缓存所有的数据,但是受限于DDS底层的资源限制。
● Depth
○ Queue size: 当History设置为Keep last时有效。
QoS &
QoS::keep_last(size_t depth)
{
rmw_qos_profile_.history = RMW_QOS_POLICY_HISTORY_KEEP_LAST;
rmw_qos_profile_.depth = depth;
return *this;
}
● Reliability
○ Best effort: 尽力传送数据,但是网络不稳定可能会丢弃一些数据。
○ Reliable: 确保数据被传送到,可能会重传多次,导致数据延时严重。
● Durability
○ Transient local: 为后订阅话题的订阅者保留数据,比如map_server发布map的Qos策略。
○ Volatile: 不为后订阅话题的订阅者保留数据,比如订阅传感器数据的节点。
// Create a publisher using the QoS settings to emulate a ROS1 latched topic
occ_pub_ = create_publisher(
topic_name,
rclcpp::QoS(rclcpp::KeepLast(1)).transient_local().reliable());
● Deadline
○ Duration: 设置数据被发布的间隔时间。比如:像cmd_vel等控制命令就希望是固定间隔时间下发的。
● Lifespan
○ Duration: 设置数据从发布到被接收的最大间隔时间。超过该时间将被认为是过时的数据,直接丢弃了。这对于传感器数据来说是很重要的。因为过时的传感器数据毫无用处。
● Liveliness
○ Automatic: 一个节点可能有多个发布器。只要有一个发布器发布了数据,系统将认为该节点的所有发布器在接下来的lease duration时间段内是活跃的。
○ Manual by topic: 如果手动确认发布器仍然是活跃的,系统将认为该发布器在接下来的lease duration时间段内是活跃的。
● Lease Duration
○ Duration: 在这个时间段内,发布器需发布数据,不然会被系统认为是停止工作了。该参数与Liveliness配合使用。
#define RMW_DURATION_INFINITE {9223372036LL, 854775807LL}
#define RMW_DURATION_UNSPECIFIED {0LL, 0LL}
查询话题的Qos策略
ros2 topic info /scan --verbose
分析一下:
● Reliability = BEST_EFFORT. 这是在传感器节点中的标准设置方式。因为我们感兴趣的是获得大量的数据,如果丢失一两个信息,其实并不重要。
● Durability = Volatile. 这也是传感器节点的标准方式,特别是具有高数据量的传感器。我们并不需要为晚加入的节点保存旧的信息。因为旧信息对它根本没有意义了。
● Liveliness = Automatic. 这是默认的设置,特别是对于传感器。我们认为在lease duration时间段内,节点发布了任何话题,代表节点是活跃的。
● Deadline = “9223372036.854775807” seconds ( INFINITE VALUE ). 这意味着 没有Deadline限制.
● Lifespan = “9223372036.854775807” seconds( INFINITE VALUE ). 这意味着 没有数据有效性限制. 不管数据延时多久被接受到都认为其是有效。这是从gazebo中发出的数据。仿真环境下这么设置应该没啥关系。但在实际场景下则需要根据需求设置一下。
系统层预设的Qos
这里以rclcpp为例。
Qos配置的相关接口维护在rclcpp模块中的qos.cpp和qos.hpp文件中。/opt/ros/galactic/include/rmw/qos_profiles.h中维护了预设的Qos结构数据。
/**
* Sensor Data QoS class
* - History: Keep last,
* - Depth: 5,
* - Reliability: Best effort,
* - Durability: Volatile,
* - Deadline: Default,
* - Lifespan: Default,
* - Liveliness: System default,
* - Liveliness lease duration: default,
* - avoid ros namespace conventions: false
*/
static const rmw_qos_profile_t rmw_qos_profile_sensor_data =
{
RMW_QOS_POLICY_HISTORY_KEEP_LAST,
5,
RMW_QOS_POLICY_RELIABILITY_BEST_EFFORT,
RMW_QOS_POLICY_DURABILITY_VOLATILE,
RMW_QOS_DEADLINE_DEFAULT,
RMW_QOS_LIFESPAN_DEFAULT,
RMW_QOS_POLICY_LIVELINESS_SYSTEM_DEFAULT,
RMW_QOS_LIVELINESS_LEASE_DURATION_DEFAULT,
false
};
/**
* Parameters QoS class
* - History: Keep last,
* - Depth: 1000,
* - Reliability: Reliable,
* - Durability: Volatile,
* - Deadline: Default,
* - Lifespan: Default,
* - Liveliness: System default,
* - Liveliness lease duration: default,
* - Avoid ros namespace conventions: false
*/
static const rmw_qos_profile_t rmw_qos_profile_parameters =
{
RMW_QOS_POLICY_HISTORY_KEEP_LAST,
1000,
RMW_QOS_POLICY_RELIABILITY_RELIABLE,
RMW_QOS_POLICY_DURABILITY_VOLATILE,
RMW_QOS_DEADLINE_DEFAULT,
RMW_QOS_LIFESPAN_DEFAULT,
RMW_QOS_POLICY_LIVELINESS_SYSTEM_DEFAULT,
RMW_QOS_LIVELINESS_LEASE_DURATION_DEFAULT,
false
};
Qos设置示例:
Python版本
qos_profile_publisher = QoSProfile(depth=10)
# Options QoSDurabilityPolicy.VOLATILE, QoSDurabilityPolicy.TRANSIENT_LOCAL,
qos_profile_publisher.durability = QoSDurabilityPolicy.VOLATILE
qos_profile_publisher.deadline = Duration(seconds=2)
# Options QoSLivelinessPolicy.MANUAL_BY_TOPIC, QoSLivelinessPolicy.AUTOMATIC
qos_profile_publisher.liveliness = QoSLivelinessPolicy.AUTOMATIC
qos_profile_publisher.liveliness_lease_duration = Duration(seconds=2)
# Options: QoSReliabilityPolicy.RELIABLE, QoSReliabilityPolicy.BEST_EFFORT
if reliability == "reliable":
qos_profile_publisher.reliability = QoSReliabilityPolicy.RELIABLE
else:
qos_profile_publisher.reliability = QoSReliabilityPolicy.BEST_EFFORT
#创建发布
self.publisher_ = self.create_publisher(msg_type=String,
topic='/qos_test',
qos_profile=qos_profile_publisher,
event_callbacks=event_callbacks)
export RMW_IMPLEMENTATION=rmw_cyclonedds_cpp #指定RMW中间通信件
ros2 run qos_tests_pkg publisher_custom_minimal_qos_exe -reliability reliable