跳转至

BehaviorTree.ROS2 开发者指南

本文档详细解析 BehaviorTree.ROS2 项目的架构设计、XML 决策树如何导入 ROS2 系统,以及如何新建自定义决策树和节点。


目录


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 消息

核心目标:

  1. 最小化样板代码 — 用户只需实现关键回调
  2. 异步非阻塞 — 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 是整个系统的入口,它:

  1. 托管 ROS Action Server:接收 ExecuteTree 类型的 Goal
  2. 管理 BehaviorTreeFactory:注册插件和 XML 树
  3. 执行行为树:在独立线程中按频率 tick
  4. 提供扩展钩子:允许用户通过派生来定制行为

可覆写的虚函数:

虚函数 调用时机 用途
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

uint8 IDLE = 0
uint8 RUNNING = 1
uint8 SUCCESS = 2
uint8 FAILURE = 3
uint8 SKIPPED = 4

uint8 status

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() 解析为:

ament_index_cpp::get_package_share_directory("package_name") + "/subfolder"

即实际路径为 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 客户端)

头文件:bt_action_node.hpp

模板参数: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 客户端)

头文件:bt_service_node.hpp

必须实现的回调:

回调 签名 说明
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 发布者)

头文件:bt_topic_pub_node.hpp

必须实现的回调:

回调 签名 说明
setMessage bool setMessage(TopicT& msg) 设置要发布的消息内容

4.4 RosTopicSubNode(Topic 订阅者)

头文件:bt_topic_sub_node.hpp

必须实现的回调:

回调 签名 说明
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,在树创建时启动:

p_->groot_publisher = std::make_shared<BT::Groot2Publisher>(
    p_->tree, p_->params.groot2_port);

使用 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 未启动,或名称不匹配

检查方法:

# 查看可用的 Action Server
ros2 action list
# 检查 Action Server 类型
ros2 action info /sleep_service

确认:XML 中 action_name 值是否与实际 Action Server 名称一致。

Q3:SubTree 找不到

原因:引用的树 XML 未加载

检查:

  • 子树 XML 是否在 behavior_trees 目录中
  • 子树 XML 中 <BehaviorTree ID="xxx"> 的 ID 是否与 <SubTree ID="xxx"/> 匹配
  • YAML 配置是否包含该目录

Q4:节点超时

调整参数:

ros_plugins_timeout: 5000  # 增大超时值 (ms)

或在代码中设置:

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 示例自定义执行器