跳到正文

Wiki

v3与v4区别及迁移

约 12 分钟阅读

1.从 BehaviorTree.CPP 3.8.7 迁移到 4.10.0

官方资料:Migration from BT.CPP 3.x、4.10.0 源码、版本更新记录。官方迁移网页当前标注 4.8,本篇进一步对照 4.10.0 的声明与实现。

本篇承接已经完成的 v3 基础实践,目标是迁移独立 CMake 程序。先完成最小工程,再迁移组合任务,最后使用 Groot2。示例继续使用 C++20、VSCode 和模拟移动机器人数据。

迁移需要同时检查三件事:能否编译、能否加载 XML、实际执行的目标和状态是否符合原任务。 没有异常不代表语义没有改变,子树固定字符串就是容易漏掉的例子。

2.版本变化先看任务层面

项目 本组 v3.8.7 写法 本组 v4.10.0 写法
CMake 包名 behaviortree_cpp_v3 behaviortree_cpp
头文件目录 behaviortree_cpp_v3/ behaviortree_cpp/
节点配置类型 BT::NodeConfiguration BT::NodeConfig
精确一次根 tick tree.tickRoot() tree.tickExactlyOnce()
库内部驱动 RUNNING tree.tickRootWhileRunning() tree.tickWhileRunning()
可处理唤醒的单次调用 本组未采用此形式 tree.tickOnce(),内部可能多次根 tick
XML root 不需要 v4 格式声明 BTCPP_format="4"
普通 SubTree 传父键 target="pickup_goal" target="{pickup_goal}"
节点状态 IDLE、RUNNING、SUCCESS、FAILURE 增加 SKIPPED,需要检查父节点的组合结果
图形实时连接 PublisherZMQ 配 Groot1 Groot2Publisher 配 Groot2
本组状态文件日志 FileLogger,.fbl FileLogger2,.btlog

节点、Ports、Blackboard、注册工厂以及 StatefulActionNode 的基本分工仍然沿用。端口属于节点,Blackboard 保存键值,XML 决定映射;这层认识不需要推倒重学。

3.先保留原工程,再迁移构建

复制整个 v3 的 bt_first_tree 为 bt4_first_tree,删除副本 build/install,用 VSCode 新窗口打开。修改顶层 project 名称,保留公共 project_options、project_warnings 和 linux-debug/linux-release 预设。

将副本 cmake/BehaviorTree.cmake 替换为 v4 第一个完整程序 中的完整版本。它精确查找 4.10.0,找不到时获取同一标签源码,并开启后续工具实践需要的接口。

src/CMakeLists.txt 继续链接 ${BTCPP_TARGET},继续由 CMake 设置 BT_XML_PATH;顶层不是把整个模板替换成上游库的 CMakeLists.txt。

检查项 迁移后的要求
主程序标准 继续使用 C++20
上游源码标准 4.10.0 库使用 C++17;与主程序的标准设置分开理解
版本来源 Configure 显示精确安装包或固定源码来源,不能混用不确定版本
构建缓存 在新工程重新 Configure,不能继续使用指向旧路径的缓存
运行库 启动与调试应能找到当前链接的动态库

上游独立源码安装可能缺少 ConfigVersion 文件,EXACT 查找会显示 version unknown 并走源码分支;这是版本元信息问题,不能据此放宽成“任意 v4 都能用”。源码作为子工程加入时,4.10.0 用顶层 CMAKE_PROJECT_VERSION 生成版本字符串,教程依赖模块也处理了这一点。

4.头文件与类型逐项改

// v3
#include "behaviortree_cpp_v3/bt_factory.h"
PrintGoal(const std::string& name, const BT::NodeConfiguration& config);

// v4
#include "behaviortree_cpp/bt_factory.h"
PrintGoal(const std::string& name, const BT::NodeConfig& config);

4.10.0 仍保留 NodeConfiguration 的类型别名,所以旧构造参数不一定立即报错;新代码统一采用 NodeConfig。能通过兼容别名编译,只说明这一处名字可用,不代表 XML、执行接口和日志已经完成迁移。

其他旧名称 v4 常用名称 注意
AsyncActionNode ThreadedAction 不等同于推荐把所有节点改成线程动作;本组继续采用 StatefulActionNode
Optional Expected 用值或错误描述输入读取结果,不是仅表示“可能没有值”
SequenceStar SequenceWithMemory 4.10.0 在失败和 halt 后仍保留索引,不能假定 halt 总会从头开始
SubTreePlus SubTree v4 普通 SubTree 使用带花括号的映射语法

部分旧名称受 USE_V3_COMPATIBLE_NAMES 构建选项控制,不能假定不同安装包都开启它。教程迁移使用新名称;自己的插件也应逐个重新编译。

5.用最小程序确认新版本

将以下完整内容保存为迁移副本的 src/main.cpp。它将库版本与执行结果一起打印,不依赖旧节点的实现。

#include "behaviortree_cpp/bt_factory.h"

#include <iostream>
#include <string>

class PrintGoal : public BT::SyncActionNode
{
public:
    PrintGoal(const std::string& name, const BT::NodeConfig& config)
        : BT::SyncActionNode(name, config)
    {}

    static BT::PortsList providedPorts()
    {
        return {BT::InputPort<std::string>("goal")};
    }

    BT::NodeStatus tick() override
    {
        auto goal = getInput<std::string>("goal");
        if (!goal)
        {
            throw BT::RuntimeError("Cannot read input [goal]: ", goal.error());
        }
        std::cout << "[PrintGoal] target=" << goal.value() << '\n';
        return BT::NodeStatus::SUCCESS;
    }
};

int main()
{
    try
    {
        std::cout << "BT.CPP version: " << BT::LibraryVersionString() << '\n';
        BT::BehaviorTreeFactory factory;
        factory.registerNodeType<PrintGoal>("PrintGoal");
        auto blackboard = BT::Blackboard::create();
        blackboard->set("pickup_goal", std::string("loading_area"));
        auto tree = factory.createTreeFromFile(BT_XML_PATH, blackboard);
        const auto result = tree.tickExactlyOnce();
        std::cout << "Tree result: " << BT::toStr(result) << '\n';
        return result == BT::NodeStatus::SUCCESS ? 0 : 1;
    }
    catch (const std::exception& error)
    {
        std::cerr << "Error: " << error.what() << '\n';
        return 2;
    }
}

将 trees/mission.xml 替换为:

<root BTCPP_format="4" main_tree_to_execute="MainTree">
    <BehaviorTree ID="MainTree">
        <SubTree ID="PrintTask" target="{pickup_goal}"/>
    </BehaviorTree>
    <BehaviorTree ID="PrintTask">
        <PrintGoal goal="{target}"/>
    </BehaviorTree>
</root>

Configure、Build,选择 bt4_first_tree 作为 Launch 目标,再运行:

BT.CPP version: 4.10.0
[PrintGoal] target=loading_area
Tree result: SUCCESS

LibraryVersionString() 没有参数,返回库提供的版本字符串指针,用于输出诊断信息,不执行节点。这个结果还应与固定源码来源对应,不能只凭一个可修改的字符串证明所有构建来源正确。

截图预留:最小迁移程序。 展示 Configure 的依赖来源、版本输出和传入的正确目标。

6.SubTree 是最需要检查的 XML 边界

本组 v3 组合任务使用普通 SubTree,它直接将属性值作为父键名:

<!-- v3.8.7 普通 SubTree -->
<SubTree ID="NavigateLeg" target="pickup_goal" reached="pickup_reached"/>

迁移到 v4 后,应明确使用 Blackboard 引用:

<!-- v4.10.0 -->
<SubTree ID="NavigateLeg" target="{pickup_goal}" reached="{pickup_reached}"/>
v4 写法 实际含义
target="{pickup_goal}" 子树 target 映射到父树 pickup_goal
target="loading_area" 子树 target 得到固定字符串 loading_area
target="pickup_goal" 子树 target 得到固定字符串 pickup_goal,不读取父树变量

在最小迁移示例中去掉花括号,再运行,会打印 target=pickup_goal,程序仍可能成功退出。这一对照说明为什么迁移验证要检查业务数据。

输出端口也必须正确映射回父树。遗漏 reached 映射,子树可以写入自己的局部键,但主树未必有结果;不能因为两侧键名相同就认为共享。

多层子树仍然各有 Blackboard。v4 的 _autoremap="true" 可按约定自动映射,但本组迁移先写出必要输入输出,方便审核边界。全局 @ 访问也不应作为批量替代所有映射的办法。

截图预留:不报异常的错误目标。 对照带花括号与不带花括号时 PrintGoal 的实际输出。

7.tick 接口先确定外部需要控制什么

// 需要一次外部调用严格对应一次根 tick,例如在两次调用间修改外部数据。
const auto result = tree.tickExactlyOnce();

// 允许处理本次执行产生的唤醒信号,内部可能多次 tick 根节点。
const auto result_with_wakeup = tree.tickOnce();

// 在库内部重复驱动,根结果不再是 RUNNING 时返回。
const auto completed_result = tree.tickWhileRunning(std::chrono::milliseconds(100));

三者都返回 BT::NodeStatus。前两者没有参数;tickWhileRunning 的可选参数是等待时长,默认 10 毫秒。唤醒信号可使等待提前结束,参数不是严格的固定周期。

本组计数导航为了对照“一次 onStart 加三次 onRunning”,使用 tickExactlyOnce;主程序在两次调用之间更新取消条件。使用 tickWhileRunning 会把循环交给库,如果同一线程要在第二次 tick 后更新数据,就必须重新安排这段调度逻辑。

Retry、Repeat 等节点也可能发出唤醒。一处 API 替换后,终态结果可能相同,但“一次外部函数调用推进了几次根 tick”可能变化。不要拿终端打印的次数直接当成固定运行时间。

4.10.0 在成功或失败的根 tick 后同样会重置根状态到 IDLE。判断本次结果应保存返回值,不能稍后只读取 rootNode()->status()。

8.SKIPPED 与持续动作的关系

v4 的 SKIPPED 表示本次未执行。普通 Sequence 遇到它会继续后面的步骤,全部被跳过时整个序列也会返回 SKIPPED。因此:

Sequence
├── NavigateToGoal [_while="navigation_allowed"]
└── ReportArrival

条件变假时导航被 halt,随后可能跳过并继续报告到达。这个结构不满足“只有真实完成才能报告”的需求。应检查完成标志、明确转换任务结果,或选择原来的 ReactiveSequence+失败条件结构。

_failureIf、_successIf 和 _skipIf 不在 RUNNING 期间反复检查;_while 才会在执行到运行节点时继续检查。不能把它们全部当成旧条件节点的持续监控替代。完整对照见 脚本与前后置条件。

已有 StatefulActionNode 的三个回调仍保留各自职责:onStart 发送一次请求,onRunning 查询进展,onHalted 取消与清理。用脚本写一个 cancelled=true 不会替代取消外部 Action 的实现。

9.迁移已有组合任务

复制 v3 的 bt_mission 为 bt4_mission,清理副本 build/install,按下面逐项修改。完整迁移结果在 v4 组合实际任务,可以对照其完整 C++、mission.xml 和 navigation.xml。

文件 修改
顶层 CMakeLists.txt 改项目名为 bt4_mission
cmake/BehaviorTree.cmake 使用本组精确版本的 v4 依赖配置
src/main.cpp 头文件换为 behaviortree_cpp,配置类型采用 NodeConfig,驱动采用 tickExactlyOnce
trees/mission.xml root 增加 BTCPP_format;两次 SubTree 的目标和结果都采用花括号引用
trees/navigation.xml root 增加 BTCPP_format,保留导航端口与有限重试规则

先保持业务参数与旧工程一致:电量20、两次总尝试、第一次导航失败。应仍然观察到充电、取货点第二次成功、放货点第二次成功,并读回两个结果。

对照场景 迁移后应保留的任务行为
电量80 不执行模拟充电
fail_attempts=0 两段导航各第一次成功
num_attempts=1,fail_attempts=1 第一段耗尽,执行失败报告,不开始第二段
fail_attempts=2,最多两次 尝试耗尽,任务 FAILURE
去掉目标映射花括号 暴露固定字符串目标错误,即使返回 SUCCESS 也不能通过验收
遗漏结果映射 父树缺少结果时报告数据错误

调试时比较两段 NavigateLeg 的 this 指针与 attempt_:子树定义复用,但节点对象与未映射的局部数据独立。只比较最终一个 SUCCESS 字符串,会漏掉目标、重试次数和数据流变化。

截图预留:组合任务迁移前后。 对照两段目标、重试次数及主树读取到的结果。

10.从 Groot1 接到 Groot2

v3 本组使用 PublisherZMQ 和 FileLogger,迁移后的工具篇使用 Groot2Publisher 与 FileLogger2:

#include "behaviortree_cpp/loggers/groot2_publisher.h"
#include "behaviortree_cpp/loggers/bt_file_logger_v2.h"

// 放在 tree 创建之后,并与整个执行过程保持相同作用域。
BT::Groot2Publisher publisher(tree, 1667);
BT::FileLogger2 file_logger(tree, BT_LOG_PATH);

发布器第二个参数是服务端口,不是旧发布器的频率;还需要后一号 TCP 端口。日志路径应使用 .btlog 后缀,旧 .fbl 不能靠改文件名转换。

Groot2 的编辑、监控与回放需要分别验证。编辑器能打开 XML,不等于程序已经发布实时状态;实时能连接,也不等于保存下来的日志能完整回放。完整安装、模型和成功/中止两条路径见 Groot2 教程。

Groot2 的编辑器也支持 BT3 XML,但本篇迁移目标统一采用 v4.10.0 的原生接口,不把两代监控协议混在同一个示例中。

11.独立工程与 ROS 2 系统迁移的边界

完成本篇证明独立示例已迁移。实际 ROS 2/Nav2 系统还需要检查:执行器链接的库、节点插件构建时使用的头文件与 ABI、插件导出和加载方式、现有 XML 的自定义节点、配置及取消处理。

v3 插件动态库不能直接当作 v4 插件加载。不同库版本在磁盘上共存,也不代表能在同一个进程中任意混用。已有 Nav2 应按自身分支和依赖构建,独立 v4 练习不要覆盖其运行库。

本篇迁移学习到此结束。已经学完 v3 时,接下来重点运行 v4 的脚本、全局数据与 Groot2;需要查阅完整程序时,按各篇主题回看 v4 基础实践。