【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 表达式熟悉且追求简洁代码风格时可以使用。