【ROS2】ROS 2 中 node 的开发方式示例

ROS2 14 次阅读
【ROS2】ROS 2 中 node 的开发方式示例

node 节点是 ROS 与 ROS2 架构中最基础、最核心的执行单元,也是机器人软件系统的最小功能载体。每一个节点都对应一项独立的业务功能,例如传感器数据采集、算法运算、设备控制、逻辑处理等,彼此之间无需直接关联,依靠话题、服务、动作等标准通信机制实现数据与指令交互,具备典型的松耦合特性。节点支持单独启动、停止与调试,局部故障不会影响整个系统运行,大幅降低开发与维护难度。同时它还支持分布式部署,可跨设备、跨主机运行并完成组网协作。整套机器人应用本质上就是众多功能各异的节点协同工作的集合,无论是功能开发、模块拆分、问题排查,还是后期迭代扩展,都以节点为基本单位开展,贯穿了 ROS 开发的全流程,是整个机器人软件体系不可或缺的组成部分。

node 节点的开发方式,也从 ROS 到 ROS2 发生了比较显著的变化,具体如下示例:

1、ROS1 经典方式

#include <chrono>
#include <string>

#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/string.hpp"

using namespace std::chrono_literals;

int main(int argc, char * argv[])
{
  rclcpp::init(argc, argv);
  auto node = rclcpp::Node::make_shared("minimal_publisher");
  rclcpp::executors::SingleThreadedExecutor executor;
  executor.add_node(node);
  auto publisher = node->create_publisher<std_msgs::msg::String>("topic", 10);
  std_msgs::msg::String message;
  auto publish_count = 0;
  rclcpp::WallRate loop_rate(500ms);

  while (rclcpp::ok()) {
    message.data = "Hello, world! " + std::to_string(publish_count++);
    RCLCPP_INFO(node->get_logger(), "Publishing: '%s'", message.data.c_str());
    try {
      publisher->publish(message);
      executor.spin_some();
    } catch (const rclcpp::exceptions::RCLError & e) {
      RCLCPP_ERROR(
        node->get_logger(),
        "unexpectedly failed with %s",
        e.what());
    }
    loop_rate.sleep();
  }
  rclcpp::shutdown();
  return 0;
}

注意: 官方说明:我们不再推荐这种风格,因为无法在同一个可执行程序中组合多个节点。请参考以下示例,了解我们推荐的 “新” 风格。保留此示例仅为保证内容完整性,因其与 “经典” 的独立 ROS 节点类似。

2、ROS2 标准方式

本示例创建了 Node 的一个子类,并使用 std::bind () 将成员函数注册为定时器的回调函数。

#include <chrono>
#include <functional>
#include <memory>
#include <string>

#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/string.hpp"

using namespace std::chrono_literals;

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:
  void timer_callback()
  {
    auto message = std_msgs::msg::String();
    message.data = "Hello, world! " + std::to_string(count_++);
    RCLCPP_INFO(this->get_logger(), "Publishing: '%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[])
{
  rclcpp::init(argc, argv);
  rclcpp::spin(std::make_shared<MinimalPublisher>());
  rclcpp::shutdown();
  return 0;
}

3、ROS2 lamba方式

此示例创建了 Node 的一个子类,并使用了一个炫酷的 C++11 lambda 表达式缩短回调语法的函数,代价是使代码乍一看有点难以理解。

#include <chrono>
#include <memory>
#include <string>

#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/string.hpp"

using namespace std::chrono_literals;

class MinimalPublisher : public rclcpp::Node
{
public:
  MinimalPublisher()
  : Node("minimal_publisher"), count_(0)
  {
    publisher_ = this->create_publisher<std_msgs::msg::String>("topic", 10);
    auto timer_callback =
      [this]() -> void {
        auto message = std_msgs::msg::String();
        message.data = "Hello, world! " + std::to_string(this->count_++);
        RCLCPP_INFO(this->get_logger(), "Publishing: '%s'", message.data.c_str());
        this->publisher_->publish(message);
      };
    timer_ = this->create_wall_timer(500ms, timer_callback);
  }

private:
  rclcpp::TimerBase::SharedPtr timer_;
  rclcpp::Publisher<std_msgs::msg::String>::SharedPtr publisher_;
  size_t count_;
};

int main(int argc, char * argv[])
{
  rclcpp::init(argc, argv);
  rclcpp::spin(std::make_shared<MinimalPublisher>());
  rclcpp::shutdown();
  return 0;
}

4、三种方式对比

(1)ROS1 经典方式

  • 优点:
    • 对于熟悉 ROS 1 编程风格的开发者来说,这种方式比较容易理解和上手,因为它在一定程度上保留了 ROS 1 中节点创建和消息发布的逻辑结构。
    • 可以较为清晰地看到节点的初始化、消息发布以及消息循环的过程,对于初学者理解 ROS 2 的基本运行机制有一定帮助。
  • 缺点:
    • 正如官方所说,它无法在同一个可执行程序中组合多个节点,这限制了代码的模块化和复用性。
    • 代码结构相对较为冗长,需要手动管理节点的执行器等相关操作,例如创建rclcpp::executors::SingleThreadedExecutor并添加节点等步骤,增加了代码的复杂性。
  • 适用场景:
    • 适用于从 ROS 1 迁移到 ROS 2 的开发者,在初期过渡阶段可以使用这种方式来熟悉 ROS 2 的基本概念和操作,同时保持与 ROS 1 编程习惯的一定连贯性。
    • 对于一些简单的、不需要在一个可执行程序中包含多个节点的项目,这种方式可以快速实现基本的消息发布功能。

(2)ROS2 标准方式

  • 优点:
    • 通过创建Node的子类,将节点的相关功能封装在类中,符合面向对象编程的思想,使代码结构更加清晰和模块化。
    • 使用std::bind将成员函数注册为定时器的回调函数,这种方式的逻辑较为直观,便于理解和维护。不同的功能可以在类的不同成员函数中实现,提高了代码的可读性和可扩展性。
    • 方便在一个可执行程序中创建多个不同功能的节点子类,实现更复杂的系统架构。
  • 缺点:
    • 对于不熟悉 C++ 面向对象编程或者std::bind用法的初学者来说,可能需要一定的时间来理解和掌握这种方式的代码结构和逻辑。
    • 相比 ROS 1 经典方式,代码中涉及到类的定义和成员函数的使用,可能会使代码量稍微增加一些。
  • 适用场景:
    • 适用于大多数 ROS 2 项目开发,尤其是需要构建复杂系统、包含多个不同功能节点的场景。它能够很好地组织代码,方便进行功能扩展和维护。
    • 对于注重代码结构清晰性和可维护性的团队开发,这种方式是一个不错的选择。

(3)ROS2 lambda 方式

  • 优点:
    • 使用 C++11 的 lambda 表达式,使代码更加简洁和紧凑,尤其是在回调函数的定义上,减少了代码量,看起来更加炫酷和现代。
    • 对于熟悉 lambda 表达式的开发者来说,这种方式可以更灵活地定义回调逻辑,并且可以直接在 lambda 函数中捕获类的成员变量,方便访问和操作。
  • 缺点:
    • 对于不熟悉 lambda 表达式的开发者来说,代码可能会比较难以理解,尤其是当 lambda 函数内部逻辑稍微复杂一些时,阅读和调试起来可能会有一定困难。
    • 由于 lambda 表达式的匿名性,在一些情况下可能不利于代码的复用和维护,例如如果有多个类似的回调函数,可能需要重复编写类似的 lambda 表达式。
  • 适用场景:
    • 适用于对 C++ lambda 表达式比较熟悉的开发者,在一些简单的回调逻辑场景中,使用 lambda 表达式可以快速简洁地实现功能,提高代码编写效率。
    • 对于追求代码简洁性和代码风格较为现代的项目,这种方式可以作为一种选择,但需要注意代码的可读性和可维护性,尤其是在团队开发中要确保其他成员也能理解 lambda 表达式的逻辑。

总体而言,ROS2 标准方式是最推荐的方式,它在代码结构、可维护性和扩展性方面都有较好的表现,适用于大多数 ROS 2 开发场景。而 ROS1 经典方式可作为过渡阶段或简单项目的选择,ROS2 lambda 方式则在特定情况下,如对 lambda 表达式熟悉且追求简洁代码风格时可以使用。