BehaviorTree.ROS2 开发者指南
本文档详细解析 BehaviorTree.ROS2 项目的架构设计、XML 决策树如何导入 ROS2 系统,以及如何新建自定义决策树和节点。
目录
- BehaviorTree.ROS2 开发者指南
- 目录
- 1. 项目概览
- 2. 核心架构
- 3. XML 决策树导入 ROS2 的完整流程
- 4. 四种 ROS 节点封装基类
- 5. 示例深度解析
- 6. 新建自定义决策树完整指南
- 7. 关键设计模式与最佳实践
- 8. 常见问题排查
- 附录:文件快速索引
1. 项目概览
1.1 项目定位
BehaviorTree.ROS2 是一个将 BehaviorTree.CPP 与 ROS2 做深度封装的库。它提供了标准的方式来实现:
- Behavior Tree Executor:通过 ROS Action 接口执行行为树
- Action 客户端节点:在 BT 中异步调用 ROS Action Server
- Service 客户端节点:在 BT 中异步调用 ROS Service
- Topic 订阅节点:在 BT 中接收 Topic 消息
- Topic 发布节点:在 BT 中发布 Topic 消息
核心目标:
- 最小化样板代码 — 用户只需实现关键回调
- 异步非阻塞 — ROS 通信不会阻塞 BT 主循环的
tick()
1.2 仓库结构
BehaviorTree.ROS2/
├── behaviortree_ros2/ # 核心封装库
│ ├── include/behaviortree_ros2/
│ │ ├── bt_action_node.hpp # RosActionNode 基类
│ │ ├── bt_service_node.hpp # RosServiceNode 基类
│ │ ├── bt_topic_pub_node.hpp # RosTopicPubNode 基类
│ │ ├── bt_topic_sub_node.hpp # RosTopicSubNode 基类
│ │ ├── tree_execution_server.hpp# 树执行服务器
│ │ ├── plugins.hpp # 插件注册宏
│ │ ├── ros_node_params.hpp # ROS 节点参数结构
│ │ ├── bt_utils.hpp # 工具函数
│ │ └── bt_ros_logger.hpp # ROS 日志记录器
│ └── src/
│ ├── tree_execution_server.cpp# 树执行服务器实现
│ ├── bt_utils.cpp # 工具函数实现(加载插件/树)
│ ├── bt_ros_logger.cpp # ROS 日志记录器实现
│ ├── bt_ros2.cpp # ROS2 相关实现
│ └── bt_executor_parameters.yaml # 参数声明
│
├── btcpp_ros2_interfaces/ # ROS2 接口定义
│ ├── action/
│ │ ├── ExecuteTree.action # 执行树 Action 定义
│ │ └── Sleep.action # 示例 Sleep Action 定义
│ ├── msg/
│ │ └── NodeStatus.msg # 节点状态消息
│ └── srv/
│ └── GetTrees.srv # 查询已加载树的 Service
│
└── btcpp_ros2_samples/ # 示例代码
├── behavior_trees/ # XML 决策树文件
│ ├── cross_door.xml
│ ├── door_closed.xml
│ └── sleep_action.xml
├── config/
│ └── sample_bt_executor.yaml # 参数配置文件
├── launch/
│ └── sample_bt_executor.launch.xml
└── src/ # 示例节点和执行器
├── sample_bt_executor.cpp # 自定义 TreeExecutionServer
├── sleep_action.hpp/.cpp # SleepAction BT 节点
├── set_bool_node.hpp/.cpp # SetBool BT 节点
├── sleep_client.cpp # 独立客户端示例
└── ...
1.3 依赖与兼容性
| 依赖 | 最低版本 |
|---|---|
| BehaviorTree.CPP | 4.6+ |
| ROS2 | Humble+ |
| rclcpp | Humble |
| rclcpp_action | Humble |
| generate_parameter_library | — |
完整依赖列表见 behaviortree_ros2/package.xml。
2. 核心架构
2.1 整体数据流
┌──────────────────┐
│ Action Client │ ros2 action send_goal
│ (命令行/其他节点)│ /behavior_server ExecuteTree
└────────┬─────────┘ "{target_tree: MyTree}"
│
Goal: target_tree + payload
│
▼
┌────────────────────────────────────────────────────────────────────┐
│ TreeExecutionServer │
│ │
│ ┌──────────────────────────────────────────┐ │
│ │ BehaviorTreeFactory │ │
│ │ ┌─────────────┐ ┌──────────────────┐ │ │
│ │ │ 已注册插件 │ │ 已注册 XML 树 │ │ │
│ │ │ (节点类型) │ │ (树结构定义) │ │ │
│ │ └─────────────┘ └──────────────────┘ │ │
│ └──────────────────────────────────────────┘ │
│ │ │
│ factory.createTree(target_tree) │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────────┐ │
│ │ BT::Tree 实例 │ │
│ │ ┌─────────┐ ┌──────────┐ ┌────────┐ │ │
│ │ │Action │ │Service │ │Topic │ │ tick()循环 │
│ │ │Client │ │Client │ │Sub/Pub │ │───────────────► │
│ │ │节点 │ │节点 │ │节点 │ │ NodeStatus │
│ │ └────┬────┘ └────┬─────┘ └───┬────┘ │ │
│ └───────┼────────────┼────────────┼────────┘ │
│ │ │ │ │
└──────────┼────────────┼────────────┼────────────────────────────────┘
│ │ │
▼ ▼ ▼
ROS Action ROS Service ROS Topics
Servers Servers (pub/sub)
2.2 核心类关系
BT::ActionNodeBase
│
├── BT::RosActionNode<ActionT> # Action 客户端封装
│ └── SleepAction # 用户派生类(示例)
│
├── BT::RosServiceNode<ServiceT> # Service 客户端封装
│ └── SetBoolService # 用户派生类(示例)
│
├── BT::RosTopicPubNode<TopicT> # Topic 发布封装
│
└── BT::RosTopicSubNode<TopicT> # Topic 订阅封装
BT::TreeExecutionServer # 树执行服务器
└── MyActionServer # 用户派生类(示例)
2.3 TreeExecutionServer 详解
TreeExecutionServer 是整个系统的入口,它:
- 托管 ROS Action Server:接收
ExecuteTree类型的 Goal - 管理 BehaviorTreeFactory:注册插件和 XML 树
- 执行行为树:在独立线程中按频率 tick
- 提供扩展钩子:允许用户通过派生来定制行为
可覆写的虚函数:
| 虚函数 | 调用时机 | 用途 |
|---|---|---|
onGoalReceived(tree_name, payload) | 收到 Goal 时,创建树之前 | 验证或拒绝请求 |
onTreeCreated(tree) | 树创建之后 | 初始化日志器/黑板 |
registerNodesIntoFactory(factory) | 注册阶段 | 手动注册额外节点 |
onLoopAfterTick(status) | 每次 tick 之后 | 中断执行(返回有效值则停止) |
onLoopFeedback() | 每次 tick 之后 | 发送反馈消息给 Action Client |
onTreeExecutionCompleted(status, was_cancelled) | 树执行结束 | 自定义返回消息 |
2.4 ROS 通信接口定义
ExecuteTree.action
# 文件: btcpp_ros2_interfaces/action/ExecuteTree.action
#### Request ####
string target_tree # 要执行的树名(对应 XML 中 BehaviorTree ID)
string payload # 可选的自定义负载数据
---
#### Result ####
NodeStatus node_status # 树的最终状态
string return_message # 返回消息或错误信息
---
#### Feedback ####
string message # 执行过程中的反馈消息
NodeStatus.msg
3. XML 决策树导入 ROS2 的完整流程
3.1 配置文件定义加载路径
YAML 配置文件(如 sample_bt_executor.yaml)定义了要加载的插件和行为树:
bt_action_server:
ros__parameters:
action_name: "behavior_server" # Action Server 名称(默认 bt_action_server)
tick_frequency: 100 # tick 频率 Hz(默认 100)
groot2_port: 1667 # Groot2 调试端口(默认 1667)
ros_plugins_timeout: 1000 # ROS 插件超时 ms(默认 1000)
plugins: # 插件目录列表(格式: package_name/subfolder)
- behaviortree_cpp/bt_plugins
- btcpp_ros2_samples/bt_plugins
behavior_trees: # XML 树目录列表(格式: package_name/subfolder)
- btcpp_ros2_samples/behavior_trees
路径解析规则:package_name/subfolder 会被 GetDirectoryPath() 解析为:
即实际路径为 ROS2 包安装目录下的 share/package_name/subfolder。
参数完整说明见 bt_executor_parameters.md。
3.2 启动时自动注册
TreeExecutionServer 构造时,通过一个单次触发的 wall timer 异步调用 executeRegistration():
// tree_execution_server.cpp 构造函数中
p_->single_shot_timer = node_->create_wall_timer(
std::chrono::milliseconds(1), [this]() {
if(!p_->factory_initialized_) {
executeRegistration();
}
p_->single_shot_timer->cancel(); // 只执行一次
});
executeRegistration() 的执行顺序:
void TreeExecutionServer::executeRegistration()
{
p_->factory.clearRegisteredBehaviorTrees();
// 1. 用户自定义注册(虚函数)
registerNodesIntoFactory(p_->factory);
// 2. 从目录加载 .so 插件
RegisterPlugins(p_->params, p_->factory, node_);
// 3. 从目录加载 .xml 行为树
RegisterBehaviorTrees(p_->params, p_->factory, node_);
p_->factory_initialized_ = true;
}
3.3 插件加载机制
RegisterPlugins() 的工作流程:
YAML plugins 列表
│
▼
遍历每个 "package_name/subfolder"
│
▼
GetDirectoryPath() → 解析为完整路径
│
▼
遍历目录中所有 .so 文件
│
▼
LoadPlugin() → 动态加载共享库
│
├── 检查符号 BT_PLUGIN_SYMBOL ("BT_RegisterFromPlugin")
│ └── 调用: func(factory) ← 标准 BT.CPP 插件
│
└── 检查符号 BT_RegisterRosNodeFromPlugin
└── 调用: func(factory, ros_params) ← ROS 插件(带节点参数)
关键代码(bt_utils.cpp):
void LoadPlugin(BT::BehaviorTreeFactory& factory,
const std::filesystem::path& file_path,
BT::RosNodeParams params)
{
BT::SharedLibrary loader(file_path.string());
if(loader.hasSymbol(BT::PLUGIN_SYMBOL)) {
// 标准 BT.CPP 插件
auto func = (Func)loader.getSymbol(BT::PLUGIN_SYMBOL);
func(factory);
}
else if(loader.hasSymbol(BT::ROS_PLUGIN_SYMBOL)) {
// ROS 插件,需要传入 RosNodeParams
auto func = (Func)loader.getSymbol(BT::ROS_PLUGIN_SYMBOL);
func(factory, params);
}
}
3.4 XML 树加载机制
RegisterBehaviorTrees() 的工作流程:
YAML behavior_trees 列表
│
▼
遍历每个 "package_name/subfolder"
│
▼
GetDirectoryPath() → 解析为完整路径
│
▼
LoadBehaviorTrees() → 遍历目录中所有 .xml 文件
│
▼
factory.registerBehaviorTreeFromFile(path)
│
▼
BT.CPP 解析 XML,提取 BehaviorTree ID,注册到工厂
关键代码(bt_utils.cpp):
void LoadBehaviorTrees(BT::BehaviorTreeFactory& factory,
const std::string& directory_path)
{
for(const auto& entry : directory_iterator(directory_path)) {
if(entry.path().extension() == ".xml") {
factory.registerBehaviorTreeFromFile(entry.path().string());
RCLCPP_INFO(kLogger, "Loaded BehaviorTree: %s",
entry.path().filename().c_str());
}
}
}
重要:XML 中 <BehaviorTree ID="xxx"> 的 ID 就是后续 factory.createTree("xxx") 使用的树名。
3.5 运行时执行树
当 Action Client 发送 Goal 时,execute() 方法被调用:
void TreeExecutionServer::execute(
const std::shared_ptr<GoalHandleExecuteTree> goal_handle)
{
const auto goal = goal_handle->get_goal();
// 1. 如果参数有变,重新注册
if(p_->param_listener->is_old(p_->params)) {
executeRegistration();
}
// 2. 创建黑板(父级为全局黑板)
auto root_blackboard = BT::Blackboard::create(p_->global_blackboard);
// 3. 根据 target_tree 名称创建树实例
p_->tree = p_->factory.createTree(goal->target_tree, root_blackboard);
// 4. 调用用户钩子
onTreeCreated(p_->tree);
// 5. 启动 Groot2 调试发布器
p_->groot_publisher = std::make_shared<BT::Groot2Publisher>(p_->tree, ...);
// 6. 循环 tick
while(rclcpp::ok() && status == BT::NodeStatus::RUNNING) {
if(goal_handle->is_canceling()) {
// 处理取消
p_->tree.haltTree();
goal_handle->canceled(action_result);
return;
}
status = p_->tree.tickExactlyOnce();
// 调用用户钩子
if(auto res = onLoopAfterTick(status); res.has_value()) { ... }
if(auto res = onLoopFeedback(); res.has_value()) { ... }
// 按频率休眠
p_->tree.sleep(...);
}
// 7. 返回结果
goal_handle->succeed(action_result); // 或 abort
}
4. 四种 ROS 节点封装基类
4.1 RosActionNode(Action 客户端)
模板参数:ROS2 Action 类型(如 btcpp_ros2_interfaces::action::Sleep)
必须实现的回调:
| 回调 | 签名 | 说明 |
|---|---|---|
setGoal | bool setGoal(Goal& goal) | 设置 Action Goal。返回 false 则触发 onFailure(INVALID_GOAL) |
onResultReceived | NodeStatus onResultReceived(const WrappedResult& result) | 收到结果时调用,返回 SUCCESS 或 FAILURE |
可选回调:
| 回调 | 签名 | 说明 |
|---|---|---|
onFeedback | NodeStatus onFeedback(const shared_ptr<const Feedback>) | 收到反馈时调用,默认返回 RUNNING |
onFailure | NodeStatus onFailure(ActionNodeErrorCode error) | 出错时调用,默认返回 FAILURE |
onHalt | void onHalt() | 节点被暂停时调用,cancelGoal() 会自动调用 |
错误码(ActionNodeErrorCode):
| 枚举值 | 含义 |
|---|---|
SERVER_UNREACHABLE | Action Server 不可达 |
SEND_GOAL_TIMEOUT | 发送 Goal 超时 |
GOAL_REJECTED_BY_SERVER | Goal 被服务器拒绝 |
ACTION_ABORTED | Action 被中止 |
ACTION_CANCELLED | Action 被取消 |
INVALID_GOAL | setGoal() 返回 false |
默认端口:
static PortsList providedBasicPorts(PortsList addition) {
PortsList basic = { InputPort<std::string>("action_name", "", "Action server name") };
basic.insert(addition.begin(), addition.end());
return basic;
}
4.2 RosServiceNode(Service 客户端)
必须实现的回调:
| 回调 | 签名 | 说明 |
|---|---|---|
setRequest | bool setRequest(Request::SharedPtr& request) | 设置 Service Request |
onResponseReceived | NodeStatus onResponseReceived(const Response::SharedPtr&) | 收到响应,返回 SUCCESS 或 FAILURE |
可选回调:
| 回调 | 签名 | 说明 |
|---|---|---|
onFailure | NodeStatus onFailure(ServiceNodeErrorCode error) | 出错时调用 |
错误码(ServiceNodeErrorCode):
| 枚举值 | 含义 |
|---|---|
SERVICE_UNREACHABLE | Service 不可达 |
SERVICE_TIMEOUT | 请求超时 |
INVALID_REQUEST | setRequest() 返回 false |
SERVICE_ABORTED | Service 中止 |
4.3 RosTopicPubNode(Topic 发布者)
必须实现的回调:
| 回调 | 签名 | 说明 |
|---|---|---|
setMessage | bool setMessage(TopicT& msg) | 设置要发布的消息内容 |
4.4 RosTopicSubNode(Topic 订阅者)
必须实现的回调:
| 回调 | 签名 | 说明 |
|---|---|---|
onTick | NodeStatus onTick(const shared_ptr<TopicT>& last_msg) | tick 时处理最新消息 |
可选回调:
| 回调 | 签名 | 说明 |
|---|---|---|
latchLastMessage | bool latchLastMessage() | 返回 true 则锁存上一条消息,否则无新消息时 last_msg 为空 |
5. 示例深度解析
5.1 SleepAction 示例(Action 客户端)
XML 树(sleep_action.xml):
<root BTCPP_format="4">
<BehaviorTree ID="SleepActionSample">
<Sequence>
<SaySomething message="start"/>
<SleepAction name="sleepA" action_name="sleep_service" msec="2000"/>
<SaySomething message="sleep completed"/>
<Fallback>
<Timeout msec="1500">
<SleepAction name="sleepB" action_name="sleep_service" msec="2000"/>
</Timeout>
<SaySomething message="sleep aborted"/>
</Fallback>
</Sequence>
</BehaviorTree>
</root>
节点定义(sleep_action.hpp):
class SleepAction : public RosActionNode<btcpp_ros2_interfaces::action::Sleep>
{
public:
SleepAction(const std::string& name, const NodeConfig& conf,
const RosNodeParams& params)
: RosActionNode<btcpp_ros2_interfaces::action::Sleep>(name, conf, params)
{}
// 声明自定义端口
static BT::PortsList providedPorts() {
return providedBasicPorts({ InputPort<unsigned>("msec") });
}
bool setGoal(Goal& goal) override;
void onHalt() override;
BT::NodeStatus onResultReceived(const WrappedResult& wr) override;
BT::NodeStatus onFailure(ActionNodeErrorCode error) override;
};
节点实现(sleep_action.cpp):
bool SleepAction::setGoal(RosActionNode::Goal& goal) {
auto timeout = getInput<unsigned>("msec");
goal.msec_timeout = timeout.value();
return true;
}
NodeStatus SleepAction::onResultReceived(const RosActionNode::WrappedResult& wr) {
RCLCPP_INFO(logger(), "%s: onResultReceived. Done = %s",
name().c_str(), wr.result->done ? "true" : "false");
return wr.result->done ? NodeStatus::SUCCESS : NodeStatus::FAILURE;
}
NodeStatus SleepAction::onFailure(ActionNodeErrorCode error) {
RCLCPP_ERROR(logger(), "%s: onFailure with error: %s",
name().c_str(), toStr(error));
return NodeStatus::FAILURE;
}
void SleepAction::onHalt() {
RCLCPP_INFO(logger(), "%s: onHalt", name().c_str());
}
// 关键:注册为插件
CreateRosNodePlugin(SleepAction, "SleepAction");
XML 中的 "SleepAction" 必须与 CreateRosNodePlugin 的第二个参数一致。
5.2 SetBoolService 示例(Service 客户端)
节点定义(set_bool_node.hpp):
class SetBoolService : public BT::RosServiceNode<SetBool>
{
public:
SetBoolService(const std::string& name, const BT::NodeConfig& conf,
const BT::RosNodeParams& params)
: RosServiceNode<SetBool>(name, conf, params) {}
static BT::PortsList providedPorts() {
return providedBasicPorts({ BT::InputPort<bool>("value") });
}
bool setRequest(Request::SharedPtr& request) override;
BT::NodeStatus onResponseReceived(const Response::SharedPtr& response) override;
BT::NodeStatus onFailure(BT::ServiceNodeErrorCode error) override;
};
5.3 CrossDoor 示例(子树引用)
主树(cross_door.xml):
<root BTCPP_format="4">
<BehaviorTree ID="CrossDoor">
<Sequence>
<Fallback>
<Inverter>
<IsDoorClosed/>
</Inverter>
<SubTree ID="DoorClosed"/> <!-- 引用另一个 XML 中定义的子树 -->
</Fallback>
<PassThroughDoor/>
</Sequence>
</BehaviorTree>
</root>
子树(door_closed.xml):
<root BTCPP_format="4">
<BehaviorTree ID="DoorClosed">
<Fallback>
<OpenDoor/>
<RetryUntilSuccessful num_attempts="5">
<PickLock/>
</RetryUntilSuccessful>
<SmashDoor/>
</Fallback>
</BehaviorTree>
</root>
关键点:
- 两个 XML 放在同一个
behavior_trees/目录下 - 所有
.xml文件会被自动加载到同一个factory <SubTree ID="DoorClosed"/>可以跨文件引用,只要 ID 匹配
6. 新建自定义决策树完整指南
6.1 总体步骤
1. 设计 XML 决策树 → behavior_trees/my_tree.xml
2. 实现自定义 BT 节点(C++ 类) → src/my_node.hpp + src/my_node.cpp
3. 注册为插件(宏) → 在 .cpp 末尾添加宏
4. 配置 CMakeLists.txt → 编译为 .so 并安装
5. 配置 YAML 参数 → config/my_executor.yaml
6. 编写 Launch 文件 → launch/my_executor.launch.xml
7. (可选) 自定义 TreeExecutionServer → src/my_executor.cpp
8. 编译与运行
6.2 步骤一:设计 XML 决策树
在你的包中创建 behavior_trees/ 目录,放入 XML 文件:
<!-- behavior_trees/patrol.xml -->
<root BTCPP_format="4">
<BehaviorTree ID="PatrolMission">
<Sequence>
<!-- 检查电池 -->
<CheckBattery min_level="20.0"/>
<!-- 获取下一个巡逻点 -->
<GetNextWaypoint waypoint="{target_pose}"/>
<!-- 导航到目标点 -->
<NavigateToGoal goal="{target_pose}" action_name="/navigate_to_pose"/>
<!-- 到达后执行检查 -->
<Fallback>
<PerformInspection service_name="/run_inspection"/>
<SaySomething message="Inspection failed, continuing patrol"/>
</Fallback>
</Sequence>
</BehaviorTree>
</root>
XML 语法要点:
| 要素 | 说明 |
|---|---|
BTCPP_format="4" | 必须,声明 BT.CPP 4.x 格式 |
BehaviorTree ID="xxx" | 树的唯一标识,用于 createTree("xxx") |
action_name="..." | RosActionNode 的 Action Server 名称 |
service_name="..." | RosServiceNode 的 Service 名称 |
{variable} | 黑板变量引用 |
SubTree ID="xxx" | 引用其他已注册的树 |
6.3 步骤二:实现自定义 BT 节点
根据 XML 中使用的节点类型,选择对应的基类来实现:
示例 A:Action 客户端节点
// src/navigate_to_goal.hpp
#pragma once
#include "behaviortree_ros2/bt_action_node.hpp"
#include "nav2_msgs/action/navigate_to_pose.hpp"
#include "geometry_msgs/msg/pose_stamped.hpp"
using NavigateToPose = nav2_msgs::action::NavigateToPose;
class NavigateToGoal : public BT::RosActionNode<NavigateToPose>
{
public:
NavigateToGoal(const std::string& name, const BT::NodeConfig& conf,
const BT::RosNodeParams& params)
: RosActionNode<NavigateToPose>(name, conf, params)
{}
// 声明端口:继承 action_name 端口,再添加 goal 端口
static BT::PortsList providedPorts()
{
return providedBasicPorts({
BT::InputPort<geometry_msgs::msg::PoseStamped>("goal", "Target pose")
});
}
// [必须] 设置 Goal
bool setGoal(Goal& goal) override
{
if(!getInput("goal", goal.pose))
{
RCLCPP_ERROR(logger(), "Missing required input [goal]");
return false;
}
return true;
}
// [必须] 处理结果
BT::NodeStatus onResultReceived(const WrappedResult& result) override
{
if(result.code == rclcpp_action::ResultCode::SUCCEEDED)
return BT::NodeStatus::SUCCESS;
return BT::NodeStatus::FAILURE;
}
// [可选] 处理反馈
BT::NodeStatus onFeedback(const std::shared_ptr<const Feedback> feedback) override
{
RCLCPP_INFO(logger(), "Distance remaining: %.2f",
feedback->distance_remaining);
return BT::NodeStatus::RUNNING;
}
// [可选] 错误处理
BT::NodeStatus onFailure(BT::ActionNodeErrorCode error) override
{
RCLCPP_ERROR(logger(), "Navigation failed: %s", BT::toStr(error));
return BT::NodeStatus::FAILURE;
}
};
示例 B:Service 客户端节点
// src/perform_inspection.hpp
#pragma once
#include "behaviortree_ros2/bt_service_node.hpp"
#include "std_srvs/srv/trigger.hpp"
using Trigger = std_srvs::srv::Trigger;
class PerformInspection : public BT::RosServiceNode<Trigger>
{
public:
PerformInspection(const std::string& name, const BT::NodeConfig& conf,
const BT::RosNodeParams& params)
: RosServiceNode<Trigger>(name, conf, params)
{}
static BT::PortsList providedPorts()
{
return providedBasicPorts({}); // 无额外端口
}
// [必须] 设置 Request
bool setRequest(Request::SharedPtr& request) override
{
// Trigger 类型无额外字段
return true;
}
// [必须] 处理 Response
BT::NodeStatus onResponseReceived(const Response::SharedPtr& response) override
{
if(response->success)
return BT::NodeStatus::SUCCESS;
RCLCPP_WARN(logger(), "Inspection failed: %s", response->message.c_str());
return BT::NodeStatus::FAILURE;
}
};
示例 C:纯逻辑节点(不涉及 ROS 通信)
// src/check_battery.hpp
#pragma once
#include "behaviortree_cpp/action_node.h"
class CheckBattery : public BT::SyncActionNode
{
public:
CheckBattery(const std::string& name, const BT::NodeConfig& conf)
: BT::SyncActionNode(name, conf)
{}
static BT::PortsList providedPorts()
{
return { BT::InputPort<double>("min_level", 20.0, "Minimum battery level") };
}
BT::NodeStatus tick() override
{
double min_level;
getInput("min_level", min_level);
// 从黑板获取电池电量(由 TreeExecutionServer 中的订阅回调更新)
auto battery = config().blackboard->get<float>("battery_level");
if(battery >= min_level)
return BT::NodeStatus::SUCCESS;
return BT::NodeStatus::FAILURE;
}
};
6.4 步骤三:注册为插件
在每个 .cpp 文件末尾添加插件注册宏:
方式 A:单节点一个插件(推荐)
// src/navigate_to_goal.cpp
#include "navigate_to_goal.hpp"
#include "behaviortree_ros2/plugins.hpp"
// 第一个参数:类名;第二个参数:XML 中使用的节点名
CreateRosNodePlugin(NavigateToGoal, "NavigateToGoal");
方式 B:多节点合并到一个插件
// src/my_nodes_plugin.cpp
#include "navigate_to_goal.hpp"
#include "perform_inspection.hpp"
#include "check_battery.hpp"
#include "behaviortree_ros2/plugins.hpp"
BT_REGISTER_ROS_NODES(factory, params)
{
// ROS 节点(需要 params)
factory.registerNodeType<NavigateToGoal>("NavigateToGoal", params);
factory.registerNodeType<PerformInspection>("PerformInspection", params);
// 纯逻辑节点(不需要 params)
factory.registerNodeType<CheckBattery>("CheckBattery");
}
注意:
CreateRosNodePlugin和BT_REGISTER_ROS_NODES两个宏在同一个 .cpp 中只能用一个。
6.5 步骤四:配置 CMakeLists.txt
cmake_minimum_required(VERSION 3.16)
project(my_bt_package)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
find_package(ament_cmake REQUIRED)
find_package(behaviortree_ros2 REQUIRED)
find_package(btcpp_ros2_interfaces REQUIRED)
# 添加你用到的消息包
find_package(nav2_msgs REQUIRED)
find_package(geometry_msgs REQUIRED)
find_package(std_srvs REQUIRED)
set(THIS_PACKAGE_DEPS
behaviortree_ros2::behaviortree_ros2
${btcpp_ros2_interfaces_TARGETS}
${nav2_msgs_TARGETS}
${geometry_msgs_TARGETS}
${std_srvs_TARGETS})
######################################################
# 编译插件库(关键步骤)
######################################################
# 方式 A:每个节点一个插件
add_library(navigate_plugin SHARED src/navigate_to_goal.cpp)
target_compile_definitions(navigate_plugin PRIVATE BT_PLUGIN_EXPORT) # 必须!
target_link_libraries(navigate_plugin ${THIS_PACKAGE_DEPS})
add_library(inspection_plugin SHARED src/perform_inspection.cpp)
target_compile_definitions(inspection_plugin PRIVATE BT_PLUGIN_EXPORT)
target_link_libraries(inspection_plugin ${THIS_PACKAGE_DEPS})
# 方式 B:多节点合并为一个插件
# add_library(my_bt_plugins SHARED src/my_nodes_plugin.cpp)
# target_compile_definitions(my_bt_plugins PRIVATE BT_PLUGIN_EXPORT)
# target_link_libraries(my_bt_plugins ${THIS_PACKAGE_DEPS})
######################################################
# 编译执行器
######################################################
add_executable(my_bt_executor src/my_executor.cpp)
target_link_libraries(my_bt_executor ${THIS_PACKAGE_DEPS})
######################################################
# 安装
######################################################
# 安装可执行文件
install(TARGETS my_bt_executor
DESTINATION lib/${PROJECT_NAME})
# 安装插件到 bt_plugins 目录(关键路径!)
install(TARGETS navigate_plugin inspection_plugin
LIBRARY DESTINATION share/${PROJECT_NAME}/bt_plugins
ARCHIVE DESTINATION share/${PROJECT_NAME}/bt_plugins
RUNTIME DESTINATION share/${PROJECT_NAME}/bt_plugins)
# 安装 XML 树、配置文件和 launch 文件
install(DIRECTORY
behavior_trees
config
launch
DESTINATION share/${PROJECT_NAME}/)
ament_export_dependencies(behaviortree_ros2 btcpp_ros2_interfaces)
ament_package()
关键:
target_compile_definitions(xxx PRIVATE BT_PLUGIN_EXPORT)必须添加,否则符号不会导出。
6.6 步骤五:配置 YAML 参数
# config/my_executor.yaml
bt_action_server:
ros__parameters:
action_name: "my_behavior_server" # Action Server 名称
tick_frequency: 100 # tick 频率
groot2_port: 1667 # Groot2 端口
ros_plugins_timeout: 2000 # ROS 插件超时 (ms)
plugins:
- behaviortree_cpp/bt_plugins # BT.CPP 内置插件(Timeout, Retry 等)
- my_bt_package/bt_plugins # 你的自定义插件
behavior_trees:
- my_bt_package/behavior_trees # 你的 XML 树目录
6.7 步骤六:编写 Launch 文件
<!-- launch/my_executor.launch.xml -->
<launch>
<!-- 启动行为树执行器 -->
<node pkg="my_bt_package" exec="my_bt_executor" output="screen">
<param from="$(find-pkg-share my_bt_package)/config/my_executor.yaml"/>
</node>
<!-- 启动你需要的其他 ROS 节点(Action Server、Service Server 等) -->
<!-- <node pkg="nav2_bt_navigator" exec="bt_navigator" output="screen"/> -->
</launch>
6.8 步骤七:(可选)自定义 TreeExecutionServer
// src/my_executor.cpp
#include <behaviortree_ros2/tree_execution_server.hpp>
#include <behaviortree_cpp/loggers/bt_cout_logger.h>
#include <std_msgs/msg/float32.hpp>
class MyExecutor : public BT::TreeExecutionServer
{
public:
MyExecutor(const rclcpp::NodeOptions& options)
: TreeExecutionServer(options)
{
// 订阅电池电量,更新全局黑板
battery_sub_ = node()->create_subscription<std_msgs::msg::Float32>(
"battery_level", 10,
[this](const std_msgs::msg::Float32::SharedPtr msg) {
globalBlackboard()->set("battery_level", msg->data);
});
}
// 树创建后的回调:添加日志
void onTreeCreated(BT::Tree& tree) override
{
logger_ = std::make_shared<BT::StdCoutLogger>(tree);
}
// 树执行完成后的回调
std::optional<std::string> onTreeExecutionCompleted(
BT::NodeStatus status, bool was_cancelled) override
{
logger_.reset(); // 清理日志
if(was_cancelled) {
return "Mission was cancelled";
}
return std::nullopt; // 使用默认消息
}
// 每次 tick 后的反馈
std::optional<std::string> onLoopFeedback() override
{
return "Mission in progress...";
}
// 手动注册非插件化的节点
void registerNodesIntoFactory(BT::BehaviorTreeFactory& factory) override
{
// 可以在这里注册不想做成插件的节点
// factory.registerNodeType<SomeNode>("SomeNode");
}
private:
std::shared_ptr<BT::StdCoutLogger> logger_;
rclcpp::Subscription<std_msgs::msg::Float32>::SharedPtr battery_sub_;
};
int main(int argc, char* argv[])
{
rclcpp::init(argc, argv);
rclcpp::NodeOptions options;
auto server = std::make_shared<MyExecutor>(options);
rclcpp::executors::MultiThreadedExecutor exec(
rclcpp::ExecutorOptions(), 0, false, std::chrono::milliseconds(250));
exec.add_node(server->node());
exec.spin();
exec.remove_node(server->node());
rclcpp::shutdown();
}
6.9 步骤八:编译与运行
# 编译
cd ~/ros2_ws
colcon build --packages-select my_bt_package
# 运行
source install/setup.bash
ros2 launch my_bt_package my_executor.launch.xml
# 发送执行请求
ros2 action send_goal /my_behavior_server \
btcpp_ros2_interfaces/action/ExecuteTree \
"{target_tree: PatrolMission}"
# 带 payload 的请求
ros2 action send_goal /my_behavior_server \
btcpp_ros2_interfaces/action/ExecuteTree \
"{target_tree: PatrolMission, payload: '{\"zone\": \"A\"}'}"
# 查询已加载的树
ros2 service call /get_loaded_trees btcpp_ros2_interfaces/srv/GetTrees
7. 关键设计模式与最佳实践
7.1 非阻塞异步设计
所有 ROS 通信节点(Action/Service)在 tick() 中都采用异步模式:
tick() 第一次调用 (IDLE → RUNNING)
│
├── 构造 Goal/Request
├── async_send_goal() / async_send_request()
└── 返回 RUNNING
tick() 后续调用 (RUNNING)
│
├── callback_executor.spin_some() ← 处理回调(非阻塞)
├── 检查 future 是否完成
│ ├── 未完成 → 返回 RUNNING
│ └── 已完成 → 调用 onResultReceived() → 返回 SUCCESS/FAILURE
└── 检查超时 → 调用 onFailure()
为什么非阻塞很重要:BT 的 tick() 是顺序执行的,如果一个节点阻塞了,整个树的其他节点(包括 Timeout 装饰器)都无法执行。
7.2 客户端复用机制
框架使用全局注册表来避免重复创建 ROS 客户端:
// 每个 RosActionNode/RosServiceNode 类内部
using ClientsRegistry = std::unordered_map<std::string, std::weak_ptr<ClientInstance>>;
static ClientsRegistry& getRegistry();
键名为 node_fully_qualified_name + "/" + action_name。如果同一个 Action Server 被多个 BT 节点使用,它们会共享同一个客户端实例。
7.3 端口名称解析策略
Action/Service 名称的确定遵循三级优先级:
1. XML 中硬编码的端口值
<SleepAction action_name="sleep_service"/>
2. XML 中的黑板引用(运行时动态解析)
<SleepAction action_name="{my_action_name}"/>
3. RosNodeParams.default_port_value
(通过 YAML 配置或代码设置)
判断逻辑(以 RosActionNode 为例):
RosActionNode::RosActionNode(..., const RosNodeParams& params) {
auto portIt = config().input_ports.find("action_name");
if(portIt != ...) {
const auto& bb_name = portIt->second;
if(isBlackboardPointer(bb_name)) {
action_name_should_be_checked_ = true; // 延迟到 tick 时解析
} else if(!bb_name.empty()) {
createClient(bb_name); // 硬编码值,立即创建
}
}
if(!client_instance_ && !params.default_port_value.empty()) {
createClient(params.default_port_value); // 使用默认值
}
}
7.4 Blackboard 使用
行为树使用两级黑板结构:
全局黑板 (global_blackboard) ← 持久存在,跨树执行
│
└── 根黑板 (root_blackboard) ← 每次执行创建,树销毁时释放
│
└── 子树黑板 (subtree_bb) ← SubTree 自动创建
在 TreeExecutionServer 中:
// 全局黑板可在构造函数中通过订阅者更新
globalBlackboard()->set("battery_level", msg->data);
// 执行时,根黑板继承全局黑板
auto root_blackboard = BT::Blackboard::create(p_->global_blackboard);
p_->tree = p_->factory.createTree(goal->target_tree, root_blackboard);
7.5 Groot2 可视化调试
框架自动集成了 BT::Groot2Publisher,在树创建时启动:
使用 Groot2 工具连接到配置的端口(默认 1667)即可实时查看树执行状态。
8. 常见问题排查
Q1:XML 中的节点名无法识别
原因:插件未正确加载
检查清单:
-
.cpp末尾是否有CreateRosNodePlugin(ClassName, "NodeName")或BT_REGISTER_ROS_NODES宏 - CMakeLists.txt 是否有
target_compile_definitions(xxx PRIVATE BT_PLUGIN_EXPORT) - 插件安装路径是否正确:
share/${PROJECT_NAME}/bt_plugins - YAML 中
plugins列表是否包含package_name/bt_plugins - XML 中节点名是否与注册名完全一致(大小写敏感)
Q2:Action Server 不可达
原因:Action Server 未启动,或名称不匹配
检查方法:
确认:XML 中 action_name 值是否与实际 Action Server 名称一致。
Q3:SubTree 找不到
原因:引用的树 XML 未加载
检查:
- 子树 XML 是否在
behavior_trees目录中 - 子树 XML 中
<BehaviorTree ID="xxx">的 ID 是否与<SubTree ID="xxx"/>匹配 - YAML 配置是否包含该目录
Q4:节点超时
调整参数:
或在代码中设置:
RosNodeParams params;
params.server_timeout = std::chrono::milliseconds(5000);
params.wait_for_server_timeout = std::chrono::milliseconds(5000);
Q5:动态更新树/插件
TreeExecutionServer 支持运行时重新加载。当 ROS 参数被修改时,下一次执行前会自动调用 executeRegistration() 重新加载:
// tree_execution_server.cpp - execute()
if(p_->param_listener->is_old(p_->params)) {
executeRegistration(); // 重新加载插件和树
}
可通过 ros2 param set 动态修改参数来触发重新加载。
附录:文件快速索引
| 文件 | 作用 |
|---|---|
behaviortree_ros2/include/behaviortree_ros2/bt_action_node.hpp | RosActionNode 基类定义与实现 |
behaviortree_ros2/include/behaviortree_ros2/bt_service_node.hpp | RosServiceNode 基类定义与实现 |
behaviortree_ros2/include/behaviortree_ros2/bt_topic_pub_node.hpp | RosTopicPubNode 基类 |
behaviortree_ros2/include/behaviortree_ros2/bt_topic_sub_node.hpp | RosTopicSubNode 基类 |
behaviortree_ros2/include/behaviortree_ros2/tree_execution_server.hpp | TreeExecutionServer 接口 |
behaviortree_ros2/include/behaviortree_ros2/plugins.hpp | 插件注册宏 |
behaviortree_ros2/include/behaviortree_ros2/ros_node_params.hpp | RosNodeParams 参数结构体 |
behaviortree_ros2/src/tree_execution_server.cpp | TreeExecutionServer 实现 |
behaviortree_ros2/src/bt_utils.cpp | 插件/树加载工具函数 |
btcpp_ros2_interfaces/action/ExecuteTree.action | 执行树 Action 接口 |
btcpp_ros2_samples/config/sample_bt_executor.yaml | 示例配置 |
btcpp_ros2_samples/src/sample_bt_executor.cpp | 示例自定义执行器 |