3、ROS2进阶通信:Service与Client编程、Action通信机制、参数服务器、生命周期节点
聊完了Topic,咱们得聊聊ROS2里另外几个“狠角色”。
Topic是“发布-订阅”,说白了就是单向广播。你发你的,我收我的,谁也不等谁。但实际工程里,哪有这么简单?你想想看,你让底盘转个弯,总得知道它转没转好吧?你让相机拍张照,总得拿到照片吧?这时候,Service和Action就派上用场了。
3.1 Service与Client:一问一答的“请求-响应”模式
Service,我习惯叫它“服务”。它跟Topic最大的区别就是:有来有回。
Client发一个请求,Service处理完,再给一个响应。就像你打电话问客服:“我家网断了,帮我查查?”客服查完告诉你:“先生,您欠费了。”——这就是一个完整的Service调用。
核心区别一句话总结:
- Topic:单向流,适合高频、持续的数据(比如激光雷达点云、摄像头图像)。
- Service:双向交互,适合低频、一次性的任务(比如拍照、设置参数、查询状态)。
我在项目中遇到过一个问题:用Topic去请求一个传感器校准结果。结果发现,因为Topic是异步的,我发完请求就跑了,根本不知道校准啥时候结束。后来改成Service,发请求后直接阻塞等待响应,代码逻辑一下子就清晰了。
3.1.1 定义.srv文件
Service的接口定义在.srv文件里。格式很简单:请求部分和响应部分用---隔开。
# 例:拍照服务
# CaptureImage.srv
string camera_name # 请求:相机名称
---
bool success # 响应:是否成功
sensor_msgs/Image image # 响应:图像数据
3.1.2 编写Service端(C++)
嗯,这里要注意。Service端的回调函数,必须返回一个bool值,表示是否处理成功。
// service_server.cpp
#include "rclcpp/rclcpp.hpp"
#include "your_package/srv/capture_image.hpp"
class CaptureService : public rclcpp::Node {
public:
CaptureService() : Node("capture_service") {
srv_ = this->create_service<your_package::srv::CaptureImage>(
"capture_image",
std::bind(&CaptureService::handle_capture, this, std::placeholders::_1, std::placeholders::_2));
}
private:
bool handle_capture(
const std::shared_ptr<your_package::srv::CaptureImage::Request> req,
std::shared_ptr<your_package::srv::CaptureImage::Response> res) {
RCLCPP_INFO(this->get_logger(), "收到拍照请求,相机:%s", req->camera_name.c_str());
// 模拟拍照
res->success = true;
// res->image = ... 填充图像数据
return true;
}
rclcpp::Service<your_package::srv::CaptureImage>::SharedPtr srv_;
};
3.1.3 编写Client端(C++)
Client端调用Service时,我建议你加上超时判断。否则网络一卡,程序就卡死了。
// service_client.cpp
#include "rclcpp/rclcpp.hpp"
#include "your_package/srv/capture_image.hpp"
auto client = node->create_client<your_package::srv::CaptureImage>("capture_image");
auto request = std::make_shared<your_package::srv::CaptureImage::Request>();
request->camera_name = "front_camera";
// 等待服务上线
while (!client->wait_for_service(std::chrono::seconds(1))) {
RCLCPP_WARN(node->get_logger(), "等待拍照服务上线...");
}
// 异步发送请求
auto future = client->async_send_request(request);
// 等待结果(带超时)
if (future.wait_for(std::chrono::seconds(5)) == std::future_status::ready) {
auto response = future.get();
if (response->success) {
RCLCPP_INFO(node->get_logger(), "拍照成功!");
}
} else {
RCLCPP_ERROR(node->get_logger(), "拍照超时!");
}
避坑指南:我曾经在Client端忘了检查wait_for_service,结果Service还没启动,Client就发请求了,直接崩溃。记住:Service不是Topic,没有“晚加入”机制。Client必须确保Service在线,才能调用。
3.2 Action通信机制:带反馈的“长期任务”
Service虽然好用,但它有个硬伤:阻塞。你发一个请求,必须等响应回来才能干别的事。如果这个任务要执行10秒钟呢?你让整个程序卡10秒?
Action就是来解决这个问题的。它本质上是一个“带反馈的Service”。
- Goal:你下达的目标(比如“导航到A点”)。
- Feedback:任务执行过程中的状态反馈(比如“已行驶50%”)。
- Result:最终结果(比如“到达A点”)。
你想想看,自动驾驶里的路径规划、泊车入库,哪个不是长期任务?Action简直就是为这些场景量身定做的。
3.2.1 定义.action文件
# 例:导航到目标点
# NavigateToPose.action
# Goal
geometry_msgs/PoseStamped target_pose
---
# Result
bool success
float32 duration_seconds
---
# Feedback
float32 progress_percentage # 0.0 ~ 100.0
3.2.2 Action Server端(C++)
Server端需要实现三个回调:handle_goal、handle_cancel、handle_accepted。
// action_server.cpp
#include "rclcpp/rclcpp.hpp"
#include "rclcpp_action/rclcpp_action.hpp"
#include "your_package/action/navigate_to_pose.hpp"
class NavigateServer : public rclcpp::Node {
public:
using NavigateAction = your_package::action::NavigateToPose;
using GoalHandle = rclcpp_action::ServerGoalHandle<NavigateAction>;
NavigateServer() : Node("navigate_server") {
action_server_ = rclcpp_action::create_server<NavigateAction>(
this, "navigate_to_pose",
std::bind(&NavigateServer::handle_goal, this, std::placeholders::_1, std::placeholders::_2),
std::bind(&NavigateServer::handle_cancel, this, std::placeholders::_1),
std::bind(&NavigateServer::handle_accepted, this, std::placeholders::_1));
}
private:
rclcpp_action::GoalResponse handle_goal(const rclcpp_action::GoalUUID &uuid,
std::shared_ptr<const NavigateAction::Goal> goal) {
RCLCPP_INFO(this->get_logger(), "收到导航目标");
// 可以在这里做合法性检查
return rclcpp_action::GoalResponse::ACCEPT_AND_EXECUTE;
}
rclcpp_action::CancelResponse handle_cancel(std::shared_ptr<GoalHandle> goal_handle) {
RCLCPP_INFO(this->get_logger(), "收到取消请求");
return rclcpp_action::CancelResponse::ACCEPT;
}
void handle_accepted(std::shared_ptr<GoalHandle> goal_handle) {
// 启动一个线程去执行任务,避免阻塞
std::thread{std::bind(&NavigateServer::execute, this, goal_handle)}.detach();
}
void execute(std::shared_ptr<GoalHandle> goal_handle) {
auto feedback = std::make_shared<NavigateAction::Feedback>();
auto result = std::make_shared<NavigateAction::Result>();
// 模拟导航过程
for (int i = 0; i <= 100; i += 10) {
if (goal_handle->is_canceling()) {
result->success = false;
goal_handle->canceled(result);
return;
}
feedback->progress_percentage = i;
goal_handle->publish_feedback(feedback);
std::this_thread::sleep_for(std::chrono::milliseconds(500));
}
result->success = true;
result->duration_seconds = 5.0;
goal_handle->succeed(result);
}
rclcpp_action::Server<NavigateAction>::SharedPtr action_server_;
};
注意:Action Server的execute函数里,一定要检查is_canceling()。我曾经写过一个导航Action,忘了检查取消标志,结果用户点了取消,机器人还在傻乎乎地往前开……差点撞墙。
3.3 参数服务器:节点的“配置文件”
参数服务器,说白了就是ROS2里一个全局的键值对存储。你可以在启动时加载参数,也可以在运行时动态修改。
我个人习惯把传感器参数、PID系数、算法开关都放在参数服务器里。这样改参数不用重新编译代码,重启节点就行。
3.3.1 声明与获取参数
// 声明参数(带默认值)
this->declare_parameter<double>("pid.kp", 1.0);
this->declare_parameter<double>("pid.ki", 0.1);
this->declare_parameter<double>("pid.kd", 0.05);
// 获取参数
double kp = this->get_parameter("pid.kp").as_double();
3.3.2 动态参数回调
如果你想在参数改变时立刻响应,可以注册一个回调。
// 参数改变回调
rclcpp::ParameterCallbackHandle::SharedPtr cb_handle_;
cb_handle_ = this->add_on_set_parameters_callback(
[this](const std::vector<rclcpp::Parameter>& params) -> rcl_interfaces::msg::SetParametersResult {
for (auto ¶m : params) {
if (param.get_name() == "pid.kp") {
RCLCPP_INFO(this->get_logger(), "Kp 更新为: %f", param.as_double());
// 更新控制器
}
}
rcl_interfaces::msg::SetParametersResult result;
result.successful = true;
return result;
});
小技巧:我建议你把参数名用“.”分层,比如pid.kp、sensor.lidar.range。这样在YAML配置文件里结构清晰,也方便用ros2 param list查看。
3.4 生命周期节点:让节点“活”起来
这个功能,说实话,我第一次看到时觉得有点多余。直到我在实际项目中,需要控制多个传感器的启动顺序——先启动激光雷达,再启动相机,最后启动融合算法。如果不用生命周期节点,你得自己写一堆状态机代码。
生命周期节点,把节点的生命周期分成了几个明确的状态:
| 状态 | 说明 |
|---|---|
| Unconfigured | 未配置,节点刚创建 |
| Inactive | 已配置,但未激活(不处理数据) |
| Active | 激活状态,正常运行 |
| Finalized | 销毁前清理 |
节点在这几个状态之间切换,需要调用对应的接口:configure()、activate()、deactivate()、cleanup()、shutdown()。
3.4.1 编写生命周期节点
#include "rclcpp_lifecycle/lifecycle_node.hpp"
class MyLifecycleNode : public rclcpp_lifecycle::LifecycleNode {
public:
MyLifecycleNode() : LifecycleNode("my_lifecycle_node") {}
// 配置阶段:分配资源、读取参数
rclcpp_lifecycle::node_interfaces::LifecycleNodeInterface::CallbackReturn
on_configure(const rclcpp_lifecycle::State &) override {
RCLCPP_INFO(get_logger(), "配置中...");
// 创建Publisher、Subscriber等
return rclcpp_lifecycle::node_interfaces::LifecycleNodeInterface::CallbackReturn::SUCCESS;
}
// 激活阶段:开始处理数据
rclcpp_lifecycle::node_interfaces::LifecycleNodeInterface::CallbackReturn
on_activate(const rclcpp_lifecycle::State &) override {
RCLCPP_INFO(get_logger(), "激活中...");
return rclcpp_lifecycle::node_interfaces::LifecycleNodeInterface::CallbackReturn::SUCCESS;
}
// 清理阶段:释放资源
rclcpp_lifecycle::node_interfaces::LifecycleNodeInterface::CallbackReturn
on_cleanup(const rclcpp_lifecycle::State &) override {
RCLCPP_INFO(get_logger(), "清理中...");
return rclcpp_lifecycle::node_interfaces::LifecycleNodeInterface::CallbackReturn::SUCCESS;
}
};
核心价值:生命周期节点让系统具备了“可控性”。你可以通过ros2 lifecycle set /node_name configure这样的命令,精确控制每个节点的行为。在自动驾驶系统中,这简直是救命的功能——你可以先让所有传感器就绪,再统一激活感知算法,避免数据错乱。
3.5 本章知识体系总览
下面这张图,是我自己梳理的。它把本章四个核心通信机制的关系画清楚了。
这张图里,我把Service、Action、参数服务器、生命周期节点并列展示。你仔细看,它们其实解决的是不同维度的问题:Service和Action解决“交互模式”,参数服务器解决“配置管理”,生命周期节点解决“节点状态控制”。
在实际的自动驾驶项目中,这四个机制经常混合使用。比如:用生命周期节点控制传感器启动顺序,用参数服务器加载算法参数,用Service触发拍照,用Action执行路径规划。嗯,这才是工程级的做法。
我的建议:初学者可以先从Service和参数服务器入手,这两个最常用。Action稍微复杂一点,但一旦你写过一次,就会发现它比Service优雅得多。生命周期节点嘛,等你需要做系统集成时,自然会爱上它。
好了,这一章的内容就到这儿。记住,通信机制是ROS2的骨架,把骨架搭好了,后面的感知和规划算法才能跑得稳。
可可精品资料库