详细介绍ROS2的节点

详细介绍ROS2的节点

ROS2(Robot Operating System 2)是一个开源的机器人操作系统,主要用于协调机器人的硬件和软件资源。它包括一套工具、库和规则,用来简化创建复杂和健壮的机器人行为。

在ROS2中,节点(Node)是一个可执行的程序,它可以进行计算和处理数据。节点可以发布消息到话题(Topic),订阅话题来接收消息,提供服务(Service),或者调用其他节点的服务。

以下是ROS2节点的一些关键概念:

  1. 节点命名和命名空间:每个节点在ROS2中都有一个唯一的名字,这样其他的节点可以找到并与它进行通信。节点也可以有一个命名空间,这使得在大型系统中组织节点变得更加简单。

  2. 发布者和订阅者:节点可以作为发布者(Publisher)发布消息到一个话题,或者作为订阅者(Subscriber)从话题接收消息。这种发布-订阅模型使得节点可以在不了解其他节点的情况下进行通信,这有助于创建模块化和可复用的代码。

  3. 服务和客户端:节点还可以提供服务,这是一种在节点之间进行同步通信的方式。一个节点(服务提供者)提供服务,其他的节点(服务客户端)可以发送请求并等待回复。

  4. 参数:节点可以有参数,这是一种让用户或其他节点在运行时配置节点行为的方式。

  5. 生命周期:一些特殊的节点,称为生命周期节点(Lifecycle Node),有预定义的状态和转换。这使得更复杂的行为可以在节点中编码,例如初始化和关闭过程。

ROS2通过使用这些和其他工具,使得机器人的开发变得更加简单和灵活。

关于ROS2节点的更多信息,我将详细介绍一下每个部分。

  1. 节点命名和命名空间

每个节点在ROS2中都有一个唯一的名称,这使得其他节点可以找到并与它通信。此外,节点也可以有一个命名空间,使得在大型系统中组织节点更加容易。例如,如果你有一个机器人系统,可以将移动相关的节点放在”移动”命名空间中,将视觉相关的节点放在”视觉”命名空间中。

  1. 发布者和订阅者

在ROS2中,节点可以发布消息到话题,或者从话题中接收消息。发布者节点将数据发布到一个特定的话题,而订阅者节点订阅这个话题以接收数据。这种发布-订阅模型使得节点之间的通信可以在不了解其他节点的具体实现的情况下进行,从而有助于创建模块化和可复用的代码。

  1. 服务和客户端

除了发布和订阅消息,节点还可以提供或使用服务。服务是一种同步通信的形式,允许一个节点(服务提供者)接收请求,处理请求并返回响应。服务客户端是一个发送服务请求并等待响应的节点。

  1. 参数

参数是ROS2节点的一种配置方式,它允许在运行时改变节点的行为。参数可以是各种类型,包括整数、浮点数、字符串、布尔值、数组等。参数可以在启动节点时设置,也可以在节点运行时动态改变。

  1. 生命周期节点

生命周期节点是ROS2中的一个特殊类型的节点,它具有预定义的状态和转换。这些状态包括未配置、已配置、激活、未激活和关闭。生命周期节点的目的是为了提供更细粒度的控制节点的运行状态,特别是对于需要进行初始化和关闭过程的节点。

话题

有关ROS2节点的更深入的讨论可以涉及到如何在实际中创建和使用节点。以下是一个简单的ROS2 C++节点的创建过程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include "rclcpp/rclcpp.hpp"

class MyNode : public rclcpp::Node
{
public:
MyNode() : Node("my_node")
{
}
};

int main(int argc, char **argv)
{
rclcpp::init(argc, argv);

auto node = std::make_shared<MyNode>();

rclcpp::spin(node);

rclcpp::shutdown();

return 0;
}


在这个例子中,我们首先包含了rclcpp/rclcpp.hpp,这是ROS2的C++客户端库。然后我们定义了一个名为MyNode的类,它继承自rclcpp::Node类。在MyNode的构造函数中,我们调用了父类的构造函数,并传入了节点的名字。

在main函数中,我们首先初始化了ROS2,然后创建了一个MyNode实例。rclcpp::spin(node)使得节点开始运行,等待并处理来自其他节点的消息。当节点不再需要时,我们关闭ROS2。

在以上的基本节点创建之后,你可能会想要这个节点实际上做一些事情,例如发布或接收消息。下面是一个ROS2 C++节点的例子,它创建了一个发布者:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/string.hpp"

class MyPublisherNode : public rclcpp::Node
{
public:
MyPublisherNode()
: Node("my_publisher_node"), count_(0)
{
publisher_ = this->create_publisher<std_msgs::msg::String>("topic", 10);
timer_ = this->create_wall_timer(
500ms, std::bind(&MyPublisherNode::publish_message, this));
}

private:
void publish_message()
{
auto message = std_msgs::msg::String();
message.data = "Hello, ROS2! " + std::to_string(count_++);
RCLCPP_INFO(this->get_logger(), "Publishing: '%s'", message.data.c_str());
publisher_->publish(message);
}

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

int main(int argc, char **argv)
{
rclcpp::init(argc, argv);

auto node = std::make_shared<MyPublisherNode>();

rclcpp::spin(node);

rclcpp::shutdown();

return 0;
}


在这个例子中,我们创建了一个MyPublisherNode类,它继承自rclcpp::Node类。在这个类的构造函数中,我们创建了一个发布者和一个定时器。发布者将发布std_msgs::msg::String类型的消息到”topic”话题,而定时器每500毫秒就会调用publish_message函数。

publish_message函数创建了一个新的std_msgs::msg::String消息,设置了它的数据,然后使用发布者将它发布到话题。

这个例子展示了如何在ROS2节点中创建发布者和定时器,以及如何发布消息。这是一个基本的例子,但你可以根据你的需要进行修改,例如你可以发布不同类型的消息,或者发布到不同的话题。

当然,发布消息只是ROS2节点的一部分功能,节点也可以订阅消息。下面是一个ROS2 C++节点的例子,它创建了一个订阅者:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/string.hpp"

class MySubscriberNode : public rclcpp::Node
{
public:
MySubscriberNode()
: Node("my_subscriber_node")
{
subscription_ = this->create_subscription<std_msgs::msg::String>(
"topic", 10, std::bind(&MySubscriberNode::topic_callback, this, std::placeholders::_1));
}

private:
void topic_callback(const std_msgs::msg::String::SharedPtr msg)
{
RCLCPP_INFO(this->get_logger(), "I heard: '%s'", msg->data.c_str());
}

rclcpp::Subscription<std_msgs::msg::String>::SharedPtr subscription_;
};

int main(int argc, char **argv)
{
rclcpp::init(argc, argv);

auto node = std::make_shared<MySubscriberNode>();

rclcpp::spin(node);

rclcpp::shutdown();

return 0;
}

在这个例子中,我们创建了一个MySubscriberNode类,它继承自rclcpp::Node类。在这个类的构造函数中,我们创建了一个订阅者,它将订阅”topic”话题的std_msgs::msg::String类型的消息。

当订阅者接收到消息时,它将调用topic_callback函数。这个函数将打印出接收到的消息的数据。

这个例子展示了如何在ROS2节点中创建订阅者,以及如何接收并处理消息。同样,这是一个基本的例子,你可以根据你的需要进行修改,例如你可以订阅不同类型的消息,或者订阅不同的话题。

服务

在ROS2中,除了发布者和订阅者,节点还可以提供服务或者作为客户端调用服务。以下是一个提供服务的ROS2 C++节点的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#include "rclcpp/rclcpp.hpp"
#include "example_interfaces/srv/add_two_ints.hpp"

class MyServiceNode : public rclcpp::Node
{
public:
MyServiceNode()
: Node("my_service_node")
{
service_ = this->create_service<example_interfaces::srv::AddTwoInts>(
"add_two_ints", std::bind(&MyServiceNode::handle_service, this, std::placeholders::_1, std::placeholders::_2));
}

private:
void handle_service(
const std::shared_ptr<example_interfaces::srv::AddTwoInts::Request> request,
std::shared_ptr<example_interfaces::srv::AddTwoInts::Response> response)
{
response->sum = request->a + request->b;
RCLCPP_INFO(this->get_logger(), "Incoming request\na: %ld" " b: %ld", request->a, request->b);
RCLCPP_INFO(this->get_logger(), "sending back response: [%ld]", (long int)response->sum);
}

rclcpp::Service<example_interfaces::srv::AddTwoInts>::SharedPtr service_;
};

int main(int argc, char **argv)
{
rclcpp::init(argc, argv);

auto node = std::make_shared<MyServiceNode>();

rclcpp::spin(node);

rclcpp::shutdown();

return 0;
}

在这个例子中,我们创建了一个MyServiceNode类,它继承自rclcpp::Node类。在这个类的构造函数中,我们创建了一个服务,它将提供example_interfaces::srv::AddTwoInts类型的服务。

当服务收到请求时,它将调用handle_service函数。这个函数接收一个请求对象和一个响应对象。它将请求对象中的两个整数相加,然后将结果设置为响应对象的sum字段。

这个例子展示了如何在ROS2节点中创建服务,以及如何处理服务请求。和之前的例子一样,这只是一个基本的例子,你可以根据你的需要进行修改,例如你可以提供不同类型的服务。

在上述服务提供者的例子中,我们创建了一个可以接受两个整数并返回它们之和的服务。但是,要使这个服务有用,我们还需要一个服务客户端来调用这个服务。以下是一个ROS2 C++服务客户端节点的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#include "rclcpp/rclcpp.hpp"
#include "example_interfaces/srv/add_two_ints.hpp"

class MyClientNode : public rclcpp::Node
{
public:
MyClientNode()
: Node("my_client_node")
{
client_ = this->create_client<example_interfaces::srv::AddTwoInts>("add_two_ints");
timer_ = this->create_wall_timer(
1000ms, std::bind(&MyClientNode::send_request, this));
}

private:
void send_request()
{
auto request = std::make_shared<example_interfaces::srv::AddTwoInts::Request>();
request->a = 1;
request->b = 2;

auto result = client_->async_send_request(request);
// Handle the response in a callback
result.wait_for(std::chrono::seconds(1));
if (result.valid())
{
RCLCPP_INFO(this->get_logger(), "Sum: %ld", result.get()->sum);
}
}

rclcpp::Client<example_interfaces::srv::AddTwoInts>::SharedPtr client_;
rclcpp::TimerBase::SharedPtr timer_;
};

int main(int argc, char **argv)
{
rclcpp::init(argc, argv);

auto node = std::make_shared<MyClientNode>();

rclcpp::spin(node);

rclcpp::shutdown();

return 0;
}

在这个例子中,我们创建了一个MyClientNode类,它继承自rclcpp::Node类。在这个类的构造函数中,我们创建了一个客户端,它将调用example_interfaces::srv::AddTwoInts类型的服务。我们也创建了一个定时器,它每1000毫秒就会调用send_request函数。

send_request函数创建了一个新的请求对象,设置了它的a和b字段,然后使用客户端发送了这个请求。我们使用async_send_request函数发送请求,并获取一个future对象,这样我们可以在稍后获取响应。

这个例子展示了如何在ROS2节点中创建服务客户端,以及如何发送服务请求并处理响应。这只是一个基本的例子,你可以根据你的需要进行修改,例如你可以调用不同类型的服务,或者发送不同的请求。

节点参数化配置

此外,ROS2节点还可以与其他节点进行参数化配置,这样可以在运行时动态更改节点的行为。在ROS2中,参数是键值对,键是参数的名称,值是参数的值。下面是一个ROS2 C++节点的例子,它使用了参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#include "rclcpp/rclcpp.hpp"

class MyParameterNode : public rclcpp::Node
{
public:
MyParameterNode()
: Node("my_parameter_node")
{
this->declare_parameter<int>("my_parameter", 42);
timer_ = this->create_wall_timer(
1000ms, std::bind(&MyParameterNode::print_parameter, this));
}

private:
void print_parameter()
{
int my_parameter;
this->get_parameter("my_parameter", my_parameter);
RCLCPP_INFO(this->get_logger(), "My parameter is: %d", my_parameter);
}

rclcpp::TimerBase::SharedPtr timer_;
};

int main(int argc, char **argv)
{
rclcpp::init(argc, argv);

auto node = std::make_shared<MyParameterNode>();

rclcpp::spin(node);

rclcpp::shutdown();

return 0;
}

在这个例子中,我们创建了一个MyParameterNode类,它继承自rclcpp::Node类。在这个类的构造函数中,我们声明了一个参数my_parameter,并给它设置了默认值42。我们也创建了一个定时器,它每1000毫秒就会调用print_parameter函数。

print_parameter函数获取my_parameter参数的值,然后打印出这个值。

这个例子展示了如何在ROS2节点中声明和获取参数。这只是一个基本的例子,你可以根据你的需要进行修改,例如你可以声明不同类型的参数,或者处理参数的更改。

动作

在ROS 2中,Action是一种用于执行长时间运行任务的通信机制。它提供了对任务的目标设置、状态反馈和结果返回的功能。Action的设计目的是支持需要长时间运行的任务,如路径规划、图像处理等。

Action由三个主要组件组成:action接口定义、action服务器和action客户端。

Action接口定义:
Action接口定义是定义Action消息类型的ROS 2接口。它包括三个主要消息类型:Goal(目标),Feedback(反馈)和Result(结果)。这些消息类型是使用ROS 2的接口描述语言(IDL)编写的,并在编译时生成相应的代码。以下是一个示例的Action接口定义文件(Fibonacci.action):

1
2
3
4
5
6
7
8
9
10
11
# Example action definition for Fibonacci sequence

# The goal message specifies the order of the Fibonacci sequence to compute
int32 order

# The feedback message provides feedback about the computed sequence
int32[] partial_sequence

# The result message contains the computed Fibonacci sequence
int32[] sequence

Action服务器:
Action服务器是执行Action任务的节点。它接收来自Action客户端的目标请求,并执行相应的任务。在执行任务的过程中,服务器可以提供反馈给客户端,并最终返回结果。以下是一个简单的Action服务器的C++示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
#include "rclcpp/rclcpp.hpp"
#include "example_interfaces/action/fibonacci.hpp"

class MyActionServerNode : public rclcpp::Node
{
public:
MyActionServerNode()
: Node("my_action_server_node")
{
using namespace std::placeholders;

this->action_server_ = rclcpp_action::create_server<example_interfaces::action::Fibonacci>(
this,
"fibonacci",
std::bind(&MyActionServerNode::handle_goal, this, _1, _2),
std::bind(&MyActionServerNode::handle_cancel, this, _1),
std::bind(&MyActionServerNode::handle_accepted, this, _1));
}

private:
rclcpp_action::Server<example_interfaces::action::Fibonacci>::SharedPtr action_server_;

rclcpp_action::GoalResponse handle_goal(
const rclcpp_action::GoalUUID &uuid,
std::shared_ptr<const example_interfaces::action::Fibonacci::Goal> goal)
{
RCLCPP_INFO(this->get_logger(), "Received goal request with order %d", goal->order);
(void)uuid;
// Let's accept the goal
return rclcpp_action::GoalResponse::ACCEPT_AND_EXECUTE;
}

rclcpp_action::CancelResponse handle_cancel(
const std::shared_ptr<rclcpp_action::ServerGoalHandle<example_interfaces::action::Fibonacci>> goal_handle)
{
(void)goal_handle;
// Accept the cancel request
return rclcpp_action::CancelResponse::ACCEPT;
}

void handle_accepted(const std::shared_ptr<rclcpp_action::ServerGoalHandle<example_interfaces::action::Fibonacci>> goal_handle)
{
using namespace std::placeholders;

std::function<void()> execute_callback = std::bind(&MyActionServerNode::execute, this, _1);
this->execute(goal_handle);
}

void execute(const std::shared_ptr<rclcpp_action::ServerGoalHandle<example_interfaces::action::Fibonacci>> gh)
{
// Execute the action
rclcpp::Rate loop_rate(1);
const auto goal = gh->get_goal();
auto feedback = std::make_shared<example_interfaces::action::Fibonacci::Feedback>();
auto &sequence = feedback->sequence;
sequence.push_back(0);
sequence.push_back(1);
auto result = std::make_shared<example_interfaces::action::Fibonacci::Result>();
for (int i = 1; (i < goal->order) && rclcpp::ok(); ++i)
{
// Check if there is a cancel request
if (gh->is_canceling())
{
result->sequence = sequence;
gh->canceled(result);
RCLCPP_INFO(this->get_logger(), "Goal Canceled");
return;
}
// Update sequence
sequence.push_back(sequence[i] + sequence[i - 1]);
// Provide feedback
gh->publish_feedback(feedback);
RCLCPP_INFO(this->get_logger(), "Publish Feedback");

loop_rate.sleep();
}

// Check if goal is done
if (rclcpp::ok())
{
result->sequence = sequence;
gh->succeed(result);
RCLCPP_INFO(this->get_logger(), "Goal Succeeded");
}
}
};

int main(int argc, char **argv)
{
rclcpp::init(argc, argv);
auto node = std::make_shared<MyActionServerNode>();
rclcpp::spin(node);
rclcpp::shutdown();
return 0;
}

在这个示例中,我们创建了一个名为MyActionServerNode的自定义节点,它继承自rclcpp::Node。在构造函数中,我们使用rclcpp_action::create_server函数创建了一个Action服务器。我们提供了一些回调函数来处理目标请求、取消请求和目标接受的事件。在handle_goal回调中,我们接受并执行目标,然后在handle_accepted回调中执行任务。在execute函数中,我们执行了一个简单的Fibonacci序列计算任务,并在每次迭代时提供反馈。如果接收到取消请求,我们通过调用canceled函数取消任务。当任务完成时,我们通过调用succeed函数将结果发送给客户端。

Action客户端:
Action客户端是发送Action目标请求的节点。它与Action服务器进行通信,并接收来自服务器的反馈和结果。以下是一个简单的Action客户端的C++示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
#include "rclcpp/rclcpp.hpp"
#include "example_interfaces/action/fibonacci.hpp"

class MyActionClientNode : public rclcpp::Node
{
public:
MyActionClientNode()
: Node("my_action_client_node")
{
this->action_client_ = rclcpp_action::create_client<example_interfaces::action::Fibonacci>(
this,
"fibonacci");

this->send_goal();
}

private:
rclcpp_action::Client<example_interfaces::action::Fibonacci>::SharedPtr action_client_;

void send_goal()
{
using namespace std::placeholders;

RCLCPP_INFO(this->get_logger(), "Sending goal");

auto send_goal_options = rclcpp_action::Client<example_interfaces.action::Fibonacci>::SendGoalOptions();
send_goal_options.goal_response_callback =
std::bind(&MyActionClientNode::goal_response_callback, this, _1);
send_goal_options.feedback_callback =
std::bind(&MyActionClientNode::feedback_callback, this, _1, _2);
send_goal_options.result_callback =
std::bind(&MyActionClientNode::result_callback, this, _1);
auto goal_msg = std::make_shared<example_interfaces::action::Fibonacci::Goal>();
goal_msg->order = 10; // Set the order of the Fibonacci sequence

// Send the goal to the server
this->action_client_->async_send_goal(goal_msg, send_goal_options);
}

void goal_response_callback(std::shared_future<rclcpp_action::ClientGoalHandle<example_interfaces::action::Fibonacci>::SharedPtr> future)
{
auto goal_handle = future.get();
if (!goal_handle)
{
RCLCPP_ERROR(this->get_logger(), "Goal was rejected by server");
}
else
{
RCLCPP_INFO(this->get_logger(), "Goal accepted by server, waiting for result");
}
}

void feedback_callback(
rclcpp_action::ClientGoalHandle<example_interfaces::action::Fibonacci>::SharedPtr,
const std::shared_ptr<const example_interfaces::action::Fibonacci::Feedback> feedback)
{
RCLCPP_INFO(this->get_logger(), "Received feedback");
// Process the feedback received from the server
// ...
}

void result_callback(const rclcpp_action::ClientGoalHandle<example_interfaces::action::Fibonacci>::WrappedResult &result)
{
switch (result.code)
{
case rclcpp_action::ResultCode::SUCCEEDED:
RCLCPP_INFO(this->get_logger(), "Goal succeeded");
// Process the result received from the server
// ...
break;
case rclcpp_action::ResultCode::ABORTED:
RCLCPP_ERROR(this->get_logger(), "Goal was aborted");
break;
case rclcpp_action::ResultCode::CANCELED:
RCLCPP_ERROR(this->get_logger(), "Goal was canceled");
break;
default:
RCLCPP_ERROR(this->get_logger(), "Unknown result code");
break;
}
rclcpp::shutdown();
}
};

int main(int argc, char **argv)
{
rclcpp::init(argc, argv);
auto node = std::make_shared<MyActionClientNode>();
rclcpp::spin(node);
rclcpp::shutdown();
return 0;
}

在这个示例中,我们创建了一个名为MyActionClientNode的自定义节点,它继承自rclcpp::Node。在构造函数中,我们创建了一个Action客户端并调用了send_goal函数来发送目标请求。在send_goal函数中,我们设置了一些回调函数来处理目标响应、反馈和结果。我们创建了一个目标消息并设置了要计算的Fibonacci序列的顺序。然后,我们使用async_send_goal函数将目标发送给服务器。在回调函数中,我们处理目标响应、反馈和结果。当目标成功完成时,我们打印出”Goal succeeded”,并调用rclcpp::shutdown函数来关闭节点。

这是一个简单的示例,展示了了解Action在ROS 2中的基本概念和用法。你可以根据自己的需求进行进一步的扩展和定制。

需要注意的是,上述示例代码是一个简化的示例,用于说明Action的基本使用方式。实际应用中,可能还需要处理更多的边界情况、错误处理和状态管理等。


详细介绍ROS2的节点
https://qiangsun89.github.io/2023/05/15/详细介绍ROS2的节点/
作者
Qiang Sun
发布于
2023年5月15日
许可协议