ROS2 Foxy实战:用C++构建带进度反馈的Fibonacci动作服务器
在机器人开发中,异步任务处理是常见需求。想象一个场景:机器人需要执行一个耗时计算任务,同时还要实时向用户反馈进度。这正是ROS2 Action机制的用武之地。本文将带你从零实现一个完整的Fibonacci数列计算服务,重点解析如何通过C++构建支持进度反馈的动作服务器。
1. 环境准备与项目创建
首先确保你的开发环境满足以下要求:
- Ubuntu 20.04 LTS
- ROS2 Foxy Fitzroy
- VSCode(推荐)或其他C++ IDE
创建工作空间和包:
bash复制mkdir -p ~/action_ws/src
cd ~/action_ws/src
ros2 pkg create --build-type ament_cmake --dependencies action_tutorials_interfaces rclcpp rclcpp_action rclcpp_components -- action_tutorials_cpp
关键依赖说明:
rclcpp_action:ROS2 C++动作库action_tutorials_interfaces:包含Fibonacci.action定义rclcpp_components:支持节点组件化
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 动作服务器核心实现
2.1 服务器类定义
创建fibonacci_action_server.cpp文件,开始构建服务器核心:
cpp复制#include <functional>
#include <memory>
#include <thread>
#include "action_tutorials_interfaces/action/fibonacci.hpp"
#include "rclcpp/rclcpp.hpp"
#include "rclcpp_action/rclcpp_action.hpp"
class FibonacciActionServer : public rclcpp::Node {
public:
using Fibonacci = action_tutorials_interfaces::action::Fibonacci;
using GoalHandle = rclcpp_action::ServerGoalHandle<Fibonacci>;
explicit FibonacciActionServer(const rclcpp::NodeOptions & options = rclcpp::NodeOptions())
: Node("fibonacci_action_server", options) {
using namespace std::placeholders;
this->action_server_ = rclcpp_action::create_server<Fibonacci>(
this,
"fibonacci",
std::bind(&FibonacciActionServer::handle_goal, this, _1, _2),
std::bind(&FibonacciActionServer::handle_cancel, this, _1),
std::bind(&FibonacciActionServer::handle_accepted, this, _1));
}
private:
rclcpp_action::Server<Fibonacci>::SharedPtr action_server_;
// 后续添加回调函数实现...
};
2.2 三大回调函数实现
目标处理回调
cpp复制rclcpp_action::GoalResponse handle_goal(
const rclcpp_action::GoalUUID & uuid,
std::shared_ptr<const Fibonacci::Goal> goal) {
RCLCPP_INFO(this->get_logger(), "收到Fibonacci计算请求,阶数: %d", goal->order);
(void)uuid;
// 简单验证输入有效性
if (goal->order < 1 || goal->order > 50) {
RCLCPP_WARN(this->get_logger(), "无效的阶数输入");
return rclcpp_action::GoalResponse::REJECT;
}
return rclcpp_action::GoalResponse::ACCEPT_AND_EXECUTE;
}
取消请求处理
cpp复制rclcpp_action::CancelResponse handle_cancel(
const std::shared_ptr<GoalHandle> goal_handle) {
RCLCPP_INFO(this->get_logger(), "收到取消请求");
(void)goal_handle;
return rclcpp_action::CancelResponse::ACCEPT;
}
任务执行线程
cpp复制void execute(const std::shared_ptr<GoalHandle> goal_handle) {
RCLCPP_INFO(this->get_logger(), "开始计算Fibonacci序列");
rclcpp::Rate loop_rate(1); // 1Hz反馈频率
auto goal = goal_handle->get_goal();
auto feedback = std::make_shared<Fibonacci::Feedback>();
auto & sequence = feedback->partial_sequence;
// 初始化序列
sequence.push_back(0);
sequence.push_back(1);
auto result = std::make_shared<Fibonacci::Result>();
for (int i = 1; i < goal->order && rclcpp::ok(); ++i) {
// 检查取消请求
if (goal_handle->is_canceling()) {
result->sequence = sequence;
goal_handle->canceled(result);
RCLCPP_INFO(this->get_logger(), "计算已取消");
return;
}
// 计算下一项并更新反馈
sequence.push_back(sequence[i] + sequence[i-1]);
goal_handle->publish_feedback(feedback);
RCLCPP_INFO(this->get_logger(), "发布进度反馈");
loop_rate.sleep();
}
// 完成计算
if (rclcpp::ok()) {
result->sequence = sequence;
goal_handle->succeed(result);
RCLCPP_INFO(this->get_logger(), "计算完成");
}
}
3. 编译与运行配置
3.1 CMakeLists.txt配置
cmake复制find_package(ament_cmake REQUIRED)
find_package(rclcpp REQUIRED)
find_package(rclcpp_action REQUIRED)
find_package(rclcpp_components REQUIRED)
find_package(action_tutorials_interfaces REQUIRED)
add_library(fibonacci_server SHARED
src/fibonacci_action_server.cpp
)
target_include_directories(fibonacci_server PRIVATE
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:include>
)
ament_target_dependencies(fibonacci_server
"action_tutorials_interfaces"
"rclcpp"
"rclcpp_action"
"rclcpp_components"
)
rclcpp_components_register_node(fibonacci_server
PLUGIN "action_tutorials_cpp::FibonacciActionServer"
EXECUTABLE fibonacci_action_server
)
install(TARGETS
fibonacci_server
ARCHIVE DESTINATION lib
LIBRARY DESTINATION lib
RUNTIME DESTINATION bin
)
3.2 编译与运行
编译工作空间:
bash复制cd ~/action_ws
colcon build --packages-select action_tutorials_cpp
运行动作服务器:
bash复制source install/setup.bash
ros2 run action_tutorials_cpp fibonacci_action_server
4. 动作客户端实现
为了完整测试我们的服务器,下面实现一个简单的客户端:
cpp复制#include <rclcpp/rclcpp.hpp>
#include <rclcpp_action/rclcpp_action.hpp>
#include <action_tutorials_interfaces/action/fibonacci.hpp>
class FibonacciActionClient : public rclcpp::Node {
public:
using Fibonacci = action_tutorials_interfaces::action::Fibonacci;
using GoalHandle = rclcpp_action::ClientGoalHandle<Fibonacci>;
explicit FibonacciActionClient() : Node("fibonacci_client") {
client_ = rclcpp_action::create_client<Fibonacci>(this, "fibonacci");
// 发送测试请求
send_goal(10);
}
private:
rclcpp_action::Client<Fibonacci>::SharedPtr client_;
void send_goal(int order) {
if (!client_->wait_for_action_server()) {
RCLCPP_ERROR(this->get_logger(), "动作服务器不可用");
return;
}
auto goal = Fibonacci::Goal();
goal.order = order;
auto send_goal_options = rclcpp_action::Client<Fibonacci>::SendGoalOptions();
send_goal_options.goal_response_callback =
[this](std::shared_future<GoalHandle::SharedPtr> future) {
auto goal_handle = future.get();
if (!goal_handle) {
RCLCPP_ERROR(this->get_logger(), "目标被拒绝");
} else {
RCLCPP_INFO(this->get_logger(), "目标已接受");
}
};
send_goal_options.feedback_callback =
[this](GoalHandle::SharedPtr, const std::shared_ptr<const Fibonacci::Feedback> feedback) {
std::stringstream ss;
ss << "当前进度: ";
for (auto number : feedback->partial_sequence) {
ss << number << " ";
}
RCLCPP_INFO(this->get_logger(), ss.str().c_str());
};
send_goal_options.result_callback =
[this](const GoalHandle::WrappedResult & result) {
switch (result.code) {
case rclcpp_action::ResultCode::SUCCEEDED:
break;
case rclcpp_action::ResultCode::ABORTED:
RCLCPP_ERROR(this->get_logger(), "计算被中止");
return;
case rclcpp_action::ResultCode::CANCELED:
RCLCPP_ERROR(this->get_logger(), "计算被取消");
return;
default:
RCLCPP_ERROR(this->get_logger(), "未知错误");
return;
}
std::stringstream ss;
ss << "最终结果: ";
for (auto number : result.result->sequence) {
ss << number << " ";
}
RCLCPP_INFO(this->get_logger(), ss.str().c_str());
rclcpp::shutdown();
};
client_->async_send_goal(goal, send_goal_options);
}
};
5. 高级技巧与调试
5.1 多线程注意事项
ROS2 Executor默认是单线程的,长时间运行的任务必须放在独立线程中:
cpp复制void handle_accepted(const std::shared_ptr<GoalHandle> goal_handle) {
std::thread([this, goal_handle]() {
this->execute(goal_handle);
}).detach();
}
5.2 反馈频率控制
通过rclcpp::Rate控制反馈频率,避免过度占用系统资源:
cpp复制rclcpp::Rate rate(2); // 2Hz反馈频率
while (/* 条件 */) {
// 发布反馈
goal_handle->publish_feedback(feedback);
rate.sleep();
}
5.3 错误处理最佳实践
| 错误类型 | 处理方式 | 日志级别 |
|---|---|---|
| 无效输入 | 拒绝目标 | WARN |
| 计算中断 | 返回部分结果 | INFO |
| 系统错误 | 中止任务 | ERROR |
6. 性能优化建议
- 对象复用:重用
feedback和result对象减少内存分配 - 线程池:对于高频任务,考虑使用线程池替代每次创建新线程
- 反馈批处理:对于极高频率更新,可以积累多个更新后批量发送
cpp复制// 线程池示例
#include <thread>
#include <vector>
#include <queue>
#include <mutex>
#include <condition_variable>
class ThreadPool {
public:
ThreadPool(size_t threads) : stop(false) {
for(size_t i = 0; i < threads; ++i)
workers.emplace_back([this] {
for(;;) {
std::function<void()> task;
{
std::unique_lock<std::mutex> lock(this->queue_mutex);
this->condition.wait(lock,
[this]{ return this->stop || !this->tasks.empty(); });
if(this->stop && this->tasks.empty())
return;
task = std::move(this->tasks.front());
this->tasks.pop();
}
task();
}
});
}
// ... 其他成员函数 ...
};
实现一个完整的ROS2动作服务器需要考虑许多细节,从基本的回调函数实现到线程安全、性能优化等高级话题。本文展示的方案已在Ubuntu 20.04 + ROS2 Foxy环境下完整测试,你可以直接应用于自己的项目中。
