ROS2实践总结

目录

  • ROS2简介
  • 与ROS1的主要区别
  • ROS2的改变对实际使用的影响

ROS2简介

新一代机器人操作系统(robot operation system),继承了ROS1大部分优异特点,并对部分功能进行了增强,同时对ROS1中存在的主要问题进行了修复和改进,相关介绍很多,这里就不赘述了。
ROS2实践总结_第1张图片

与ROS1的主要区别

ROS1与ROS2的主体架构对比图:
ROS2实践总结_第2张图片
对以上架构图进行剖析:

  • 去除master节点
    ROS1的整体架构设计采用了中心化设计,在设计模式中也称为中介模式,即通过一个中心代理节点完成系统中所有节点的通信和数据交换,同时也具有一定的管理职能,这种设计有很多优点,例如整体结构简洁、功能清晰,由于中心节点承担了所有数据交换,其他节点都不需要处理通信相关任务等,但中心化设计有一个巨大的弊病,整个系统过于依赖中心节点,中心节点的崩溃会导致整个系统崩溃,而现代机器人对于稳定性的要求越来越高,因此ROS2去除了master节点,采用了完全的去中心化设计(毕竟连货币都开始去中心化了),然而这种设计需要每个节点自己处理数据传输问题,因此诞生了DDS(Data distribution system)。
  • DDS
    由于采用了完全的去中心化设计,因此ROS2增加了一个中间层处理通信问题,这也是ROS2的一大特点,DDS负责处理节点之间的互相发现、数据序列化与反序列化、数据传输以及一些通信质量管理(QOS)等,从以上架构图也可以看出,DDS上有一层接口封装,这也就意味着所有满足ROS2数据接口协议的DDS库可以随意替换,使用户在面对不同需求时采用不同的DDS库,保证了整个系统的灵活性。
    由于DDS的出现,ROS2成为一个完全的分布式系统,在ROS1中,不同机器通信需要设置master节点所在机器IP及名称,同时master所在机器需要知道各节点所在机器IP及名称,此外,从部分测试结果来看,ROS2的通信效率比ROS1也有很大提升,这一点未做详细测试,暂时只作为参考。
  • 多平台支持
    ROS1基本只支持Linux系统,而ROS2则面对多平台设计,个人主要在ubuntu下开发,没有经过太多实践,不多赘述。

整体设计上的区别大概就是以上几点,实际应用中的细节性差异非常多,API基本被全部重写了,ROS1切到ROS2的用户建议仔细阅读官方迁移文档,下面主要看看这些改动对于实际应用的影响。

ROS2的改变对实际使用的影响

  • 编译工具
    ROS1中主要使用catkin系统进行编译管理,一个典型的编译操作如下:
catkin_make -DCMAKE_BUILD_TYPE=Release --install

开发阶段一般会去掉–install选项,这与代码中关于配置以及launch的修改都会实时生效,大大提高了开发效率。
ROS2使用ament系统进行编译管理,按照ROS2官方编译系统的说法,ament系统是catkin系统的升级,修复了ROS1中大家反馈的一些问题,例如为了devel模式增加了大量逻辑等等,细节看官方文档就好,典型操作如下:

colcon build --cmake-args -DCMAKE_BUILD_TYPE=Release

一个直观感受就是colcon的参数指定都需要使用全称,虽然比较麻烦,但是可以减少误操作,使用类似catkin的开发模式只需要加上–symlink-install即可。

  • launch文件
    ROS2同时支持了python和xml格式两种launch文件,但官方建议使用新的python版launch,比较方便的一点是可以使用大量的python库对launch文件进行功能增强,看下ROS1和ROS2的典型launch文件的写法:
    ROS1:
<launch>
   <arg name="topic_name" default="chatter"/>
   <node pkg="demo_nodes_cpp" exec="talker">
      <remap from="chatter" to="$(var topic_name)"/>
   node>
   <node pkg="demo_nodes_cpp" exec="listener">
      <remap from="chatter" to="$(var topic_name)"/>
   node>
launch>

ROS2:

from launch import LaunchDescription
from launch_ros.actions import Node
from launch.actions import DeclareLaunchArgument
from launch.actions import ExecuteProcess
from launch.substitutions import LaunchConfiguration

def generate_launch_description():
    DeclareLaunchArgument(
       'topic_name',
       default_value='chatter',
       description='chatter topic name'),
    return LaunchDescription([
        Node(
            package='demo_nodes_cpp',
            executable='talker',
            remappings=[
               ('chatter', LaunchConfiguration('topic_name')),
            ]
        ),
        Node(
            package='demo_nodes_cpp',
            executable='chatter',
            remappings=[
               ('chatter', LaunchConfiguration('topic_name')),
            ]
        )
    ])

一个非常明显的区别,ROS2的launch文件编写复杂度远远高于ROS1的xml格式,虽然ROS2官方描述也可以支持xml格式,但实测中确实出过一些问题,并且从官方教程到几乎所有的ROS2开源库都使用了python版本的launch文件,文件复杂度不算是一个严重的问题,毕竟现在IDE下编写效率已经很高了,但launch.substitutions这个库着实没有xml中的${var}替换来得好用,举个例子:

from launch import LaunchDescription
from launch_ros.actions import Node
from launch.actions import DeclareLaunchArgument
from launch.actions import ExecuteProcess
from launch.substitutions import LaunchConfiguration

def generate_launch_description():
    DeclareLaunchArgument(
       'urdf_file_name',
       default_value='a.urdf',
       description=''),
    urdf = os.path.join(
        get_package_share_directory('urdf'),
        'urdf',
        LaunchConfiguration('urdf_file_name'))
    with open(urdf, 'r') as infp:
        robot_desc = infp.read()
    return LaunchDescription([
        Node(
            package='robot_state_publisher',
            executable='robot_state_publisher',
            name='robot_state_publisher',
            output='screen',
            parameters=[{'robot_description': robot_desc}],
            #arguments=[urdf]),
    ])

按照这个写法直接运行是会出问题的,因为substitution只有到运行时才会进行真正的替换,而os.path.join的操作在替换之前就进行了,实现同样的功能,替换成ROS1的xml格式不仅简洁明了,并且不会出问题。

  • rosbag
    ROS2把rosbag几乎重写了,使用sqlite数据库管理数据,数据更加标准化,但在实际使用中,仍然存在很多问题,例如,rosbag play无法使用倍速播放,也无法指定从某个时间点播放数据,这对于回放有很大的影响;离线播放时如果需要使用use_sim_time,录制bag的时候必须包含一个clock话题,而这个话题需要自己手动写个节点来发出。
    此外,rosbag C++ API也有很大变化,ROS1的将一条message写入文件的write函数在ROS2 foxy中需要写成以下的样子:
  rosbag2_cpp::StorageOptions storage_options({path, "sqlite3"});
  const rosbag2_cpp::ConverterOptions converter_options(
      {rmw_get_serialization_format(), rmw_get_serialization_format()});
  auto writer = std::make_unique<rosbag2_cpp::writers::SequentialWriter>();
  writer->open(storage_options, converter_options);

  auto serializer = rclcpp::Serialization<sensor_msgs::msg::PointCloud2>();
  auto bag_serialized_msg = std::make_shared<rosbag2_storage::SerializedBagMessage>();
  writer->create_topic({"topic", "sensor_msgs/msg/PointCloud2", rmw_get_serialization_format(), ""});
    
  sensor_msgs::msg::PointCloud2 ros_cloud;
  // cloud -> serialized bag message
  auto rclcpp_serialized_msg = rclcpp::SerializedMessage();
  serializer.serialize_message(&ros_cloud, &rclcpp_serialized_msg);
  bag_serialized_msg->serialized_data =
      std::shared_ptr<rcutils_uint8_array_t>(new rcutils_uint8_array_t, [](rcutils_uint8_array_t* msg) {
        auto fini_return = rcutils_uint8_array_fini(msg);
        delete msg;
        if (fini_return != RCUTILS_RET_OK) {
          std::cerr << "Failed to destroy serialized message " << rcutils_get_error_string().str;
        }
      });
  *tools.bag_serialized_msg->serialized_data = rclcpp_serialized_msg.release_rcl_serialized_message();
  // end convert
  tools.bag_serialized_msg->topic_name = cloud.first;
  writer->write(tools.bag_serialized_msg);

截至目前,galactic虽然对以上过程进行了一定程度的封装,但封装也极为简化,同时read的流程仍没有封装,使用还是很麻烦。

  • 节点的建立
    ROS2使用了完全的面向对象设计,以C++为例,大量的ROS API都封装在了rclcpp::Node这个类中,也就意味着需要使用这些API,你必须定义一个继承自Node的类,这也就强制用户必须使用类的方式构建整个系统,官方的一个简单的例子如下:
class MinimalPublisher : public rclcpp::Node
{
  public:
    MinimalPublisher()
    : Node("minimal_publisher"), count_(0)
    {
      publisher_ = this->create_publisher<std_msgs::msg::String>("topic", 10);
      timer_ = this->create_wall_timer(
      500ms, std::bind(&MinimalPublisher::timer_callback, this));
    }
  private:
    rclcpp::TimerBase::SharedPtr timer_;
    rclcpp::Publisher<std_msgs::msg::String>::SharedPtr publisher_;
    size_t count_;
}

从ROS1转ROS2的开发者可能会稍有不适,ROS1中带上ros命名空间就可以各处使用ros的API了,而ROS2中如果希望将系统分割为很多个类,有两个选择:
1、将所有类都继承自rclcpp::Node
2、将一个外层封装类或者一个核心类继承自rclcpp::Node,其他类构造时接受它的引用或指针,利用这个引用调用ros API。
虽然说使用上比ROS1更麻烦,但让用户养成良好的面向对象变成习惯是一个好的选择。

  • 线程管理方式
    ROS2中提供了SingleThreadedExecutor和MultiThreadedExecutor两个类来管理单线程与多线程的程序,但它与ROS1有一个很大的区别,就是subscriber增加了group的概念,同一group的subscribers的回调函数一定会在一个线程同步运行,一个例子如下:
class MinimalSubscriber : public rclcpp::Node
{
  public:
    MinimalSubscriber()
    : Node("minimal_subscriber")
    {
	   sub1_ = this->create_subscription<std_msgs::msg::String>("topic1", 10, 
	   		std::bind(&MinimalSubscriber::callback1, this, _1));
       sub2_ = this->create_subscription<std_msgs::msg::String>("topic2", 10, 
       		std::bind(&MinimalSubscriber::callback2, this, _1));
    }
  private:
    rclcpp::Subscriber<std_msgs::msg::String>::SharedPtr sub1_;
    rclcpp::Subscriber<std_msgs::msg::String>::SharedPtr sub2_;
}

int main(){
  rclcpp::init(argc, argv);
  rclcpp::ExecutorOptions exec_option;
  rclcpp::executors::MultiThreadedExecutor exec(exec_option, 10);
  exec.add_node(std::make_shared<MinimalSubscriber>());
  exec.spin();
  rclcpp::shutdown();
  return 0;
}

按照以上的写法,callback1和callback2并不会异步运行,即使使用了MultiThreadedExecutor,没有手动指定subscriber的group,程序仍然等于是单线程在运行,想要两个callback异步运行,必须手动将其group指定为不同的group才行,因为一个ros node内部的subscribers默认都属于同一group,而不同ros node的subscribers默认属于不同group。
这种subscriber的管理方式确实会更清晰,但使用起来也更加麻烦。

  • 多机通信
    ROS2采用了完全的分布式设计,因此在多机通信这一块使用非常便利,要实现多机通信,只需要启动节点前设置一个环境变量就可以了:
export ROS_DOMAIN_ID=66

只要在同一ID的机器就可以接受到彼此的消息。

  • yaml配置文件
    比较坑的一个细节,ROS2在yaml使用上有两个变化,如下:
test_node:
  ros__parameters:
  	common:
      topic: "your_topic"

第一行必须跟node的name一致,若launch文件修改name,yaml会无法读取。第二行必须增加ros__parameters,加载二级分级参数时,代码中需要:

node_->declare_parameter("common.topic", "topic");

ROS1中使用/分级。

  • 同一进程加载多节点
    ROS1中nodelet的升级版,ROS2中支持实时加载不同的节点,使用相比ROS1的nodelet更方便了一点,ROS1中使用nodelet要修改CMakeList、package.xml和源代码,并且很多名字必须完全对应,否则就会出问题。
  • python与c++通信
    ROS2的一个大坑,python与c++ API大文件通信会出现巨大延时,原因在于底层数据拷贝的逻辑问题,一个关键PR已经被合入最新版galactic,但foxy还没有更新。具体的测试数据可以参考:https://github.com/karl-schulz/ros2_latency

你可能感兴趣的:(ROS相关,ROS,linux)