3. ROS2 基础实战:节点、话题、服务、动作的创建与通信
好,咱们直接进入正题。这一章是 ROS2 的“内功心法”。你想想看,机器人系统里那么多模块——传感器、控制器、规划器——它们怎么互相说话?靠的就是节点、话题、服务、动作这四样东西。
我个人习惯把这四样比作一个团队协作的场景:节点是干活的人,话题是广播喇叭(一个人说,多人听),服务是打电话问答案(一问一答),动作是派发长期任务(边干边汇报进度)。
下面咱们就动手,把这四个家伙都跑一遍。
3.1 节点:ROS2 的最小执行单元
节点说白了就是一个独立的可执行程序。每个节点只干一件事,比如读取激光雷达数据,或者控制电机。我刚开始学的时候,总想把所有功能塞进一个节点里——后来发现调试起来简直要命,一个地方崩了全完蛋。
创建节点的标准姿势:
// 文件名: my_first_node.cpp
#include "rclcpp/rclcpp.hpp"
int main(int argc, char **argv)
{
rclcpp::init(argc, argv);
// 创建节点,名字叫 "minimal_node"
auto node = std::make_shared<rclcpp::Node>("minimal_node");
RCLCPP_INFO(node->get_logger(), "节点已启动: %s", node->get_name());
rclcpp::spin(node);
rclcpp::shutdown();
return 0;
}
laser_scan_node。别用驼峰,ROS2 社区约定俗成的规矩。
3.2 话题:一对多的异步通信
话题是最常用的通信方式。发布者只管往外扔数据,订阅者只管收。双方不需要知道对方的存在——这就是解耦。
我曾经在一个项目中,需要把摄像头数据从相机节点传给视觉处理节点。用话题就特别合适,因为图像数据是持续产生的,订阅者随时加入都能收到最新帧。
发布者示例:
// 发布 "chatter" 话题,类型为 std_msgs::msg::String
auto publisher = node->create_publisher<std_msgs::msg::String>("chatter", 10);
std_msgs::msg::String message;
message.data = "Hello ROS2!";
publisher->publish(message);
订阅者示例:
auto subscription = node->create_subscription<std_msgs::msg::String>(
"chatter", 10,
[](const std_msgs::msg::String::SharedPtr msg) {
RCLCPP_INFO(rclcpp::get_logger("subscriber"), "收到: '%s'", msg->data.c_str());
});
3.3 服务:同步的请求-响应模式
服务适合那种“你问我答”的场景。比如你让机器人抓取一个物体,你发出请求,它返回成功或失败。注意,服务是同步的——客户端发完请求后会阻塞等待响应。
服务端定义:
// 服务类型: example_interfaces::srv::AddTwoInts
auto service = node->create_service<example_interfaces::srv::AddTwoInts>(
"add_two_ints",
[](const std::shared_ptr<example_interfaces::srv::AddTwoInts::Request> req,
std::shared_ptr<example_interfaces::srv::AddTwoInts::Response> res) {
res->sum = req->a + req->b;
RCLCPP_INFO(rclcpp::get_logger("service"), "返回: %ld + %ld = %ld", req->a, req->b, res->sum);
});
客户端调用:
auto client = node->create_client<example_interfaces::srv::AddTwoInts>("add_two_ints");
auto request = std::make_shared<example_interfaces::srv::AddTwoInts::Request>();
request->a = 3;
request->b = 5;
auto future = client->async_send_request(request);
// 等待结果
if (rclcpp::spin_until_future_complete(node, future) == rclcpp::FutureReturnCode::SUCCESS)
{
RCLCPP_INFO(node->get_logger(), "结果: %ld", future.get()->sum);
}
3.4 动作:带反馈的异步任务
动作是 ROS2 里最强大的通信机制。它结合了话题和服务的特点:你给机器人下达一个目标(比如“移动到坐标 (1,2)”),它一边执行一边给你反馈进度(“已走 30%”),最后告诉你结果(“到达目标”)。
我记得第一次用动作写导航程序时,发现它天然支持取消任务——用户随时可以喊停。这在真实机器人上太重要了,万一撞墙呢?
动作服务端骨架:
// 动作类型: nav2_msgs::action::NavigateToPose
class MyActionServer : public rclcpp::Node
{
public:
MyActionServer() : Node("action_server_node")
{
using namespace std::placeholders;
action_server_ = rclcpp_action::create_server<nav2_msgs::action::NavigateToPose>(
this, "navigate",
std::bind(&MyActionServer::handle_goal, this, _1, _2),
std::bind(&MyActionServer::handle_cancel, this, _1),
std::bind(&MyActionServer::handle_accepted, this, _1));
}
private:
rclcpp_action::Server<nav2_msgs::action::NavigateToPose>::SharedPtr action_server_;
// 处理目标、取消、接受的回调函数...
};
动作客户端调用:
auto action_client = rclcpp_action::create_client<nav2_msgs::action::NavigateToPose>(node, "navigate");
auto goal_msg = nav2_msgs::action::NavigateToPose::Goal();
goal_msg.pose.pose.position.x = 1.0;
goal_msg.pose.pose.position.y = 2.0;
auto send_goal_options = rclcpp_action::Client<nav2_msgs::action::NavigateToPose>::SendGoalOptions();
send_goal_options.feedback_callback = [](auto, auto feedback) {
RCLCPP_INFO(rclcpp::get_logger("client"), "进度: %.2f%%", feedback->distance_remaining);
};
action_client->async_send_goal(goal_msg, send_goal_options);
3.5 四种通信方式的对比
| 通信方式 | 模式 | 典型场景 | 是否阻塞 | 是否支持取消 |
|---|---|---|---|---|
| 话题 (Topic) | 发布/订阅 | 传感器数据流、状态广播 | 否 | 不适用 |
| 服务 (Service) | 请求/响应 | 查询参数、触发一次性操作 | 是 | 否 |
| 动作 (Action) | 目标/反馈/结果 | 导航、机械臂控制、长时间任务 | 否(异步) | 是 |
嗯,到这里四种通信方式就讲完了。你想想看,其实它们各有各的脾气:话题适合“一直说”,服务适合“问一句答一句”,动作适合“派个活慢慢干”。
我个人建议,刚开始写代码时先用话题把数据流打通,等需要“请求-响应”时再引入服务,最后遇到长时间任务再上动作。别一上来就全用动作,杀鸡焉用牛刀嘛。
下一节咱们会把这些知识点串起来,写一个完整的机器人控制节点。到时候你就知道,这些通信方式是怎么配合工作的了。