跳到正文

Wiki

Groot2

约 21 分钟阅读

1.用 Groot2 看清行为树怎样执行

对应官方资料:Groot2 仓库与安装说明、BehaviorTree.CPP 4 XML 格式。 接口按 4.10.0 的 bt_zmq_publisher.h 与 bt_file_logger.h 核对。

前面的基础编程实践已经完成。本篇进入工具实践,继续使用 BehaviorTree.CPP 4.10.0、C++20、Ubuntu 22.04 和 VSCode CMake Tools,通过 Groot2 完成“打开树 → 修改参数 → 观察运行 → 回放日志”的完整过程。

Groot2 是行为树的图形编辑和观察工具。 C++ 继续实现节点逻辑,XML 继续组织树;Groot2 帮助我们编辑结构,并观察程序报告的状态。 用鼠标在画布上创建一个动作,只会创建它的模型和 XML 描述,对应的 C++ 实现仍需自行编写并注册。

本篇固定使用 Groot2 1.9.0,配套 BehaviorTree.CPP 4.10.0 的 BT::Groot2Publisher 和 BT::FileLogger2。监控与日志查看采用免费版能够观察的小树,本例实际执行三个节点。免费版当前限 20 个节点;Blackboard 可视化、交互断点与故障注入属于 PRO,可作为后续选学,不影响本篇完成。

模式 打开什么或连接什么 本篇用它做什么
Editor XML 文件及节点模型 查看树,修改目标和模拟次数,保存 XML
Monitor 正在运行的 C++ 程序 观察条件检查、导航运行和中止引起的状态变化
Log Replay 程序生成的 .btlog 文件 回看已经结束的任务,逐条查看节点状态变化

本篇复用 持续任务与中止 的小树,先聚焦三个实际执行的节点:

ReactiveSequence
├── NavigationAllowed   读取是否允许导航
└── NavigateToGoal      开始任务,运行检查三次后报告成功

仍然用计数模拟导航,没有连接机器人。正常完成时需要一次 onStart() 和三次 onRunning();开启取消后,第二次根 tick 结束时修改条件,第三次根 tick 触发中止。


2.准备并启动 Groot2

Groot2 是独立的 Qt 桌面程序,需要图形桌面。下面以 Ubuntu 22.04 x86_64、Groot2 1.9.0 AppImage 为例。官网下载页同时提供安装包及其他架构的 AppImage,按运行电脑的架构选择。它不是从 Groot1 源码编译出来的程序。

在保存开发工具的目录执行:

curl -fL https://pub-32cef6782a9e411e82222dee82af193e.r2.dev/Groot2-v1.9.0-x86_64.AppImage \
    -o Groot2-v1.9.0-x86_64.AppImage
chmod u+x Groot2-v1.9.0-x86_64.AppImage
./Groot2-v1.9.0-x86_64.AppImage

若系统缺少 FUSE,或运行在不支持 AppImage 挂载的容器中,可以在专门的工具目录解包后启动:

./Groot2-v1.9.0-x86_64.AppImage --appimage-extract
./squashfs-root/AppRun

--appimage-extract 生成当前目录下的 squashfs-root,保留整个目录,不能只复制其中的可执行文件。解包解决的是挂载问题,程序仍需要可用图形会话。

Ubuntu 22.04 若提示缺少 xcb-cursor,安装对应运行库:

sudo apt install libxcb-cursor0

从 Ubuntu 图形桌面的终端启动,避免继承其他 Qt 工程设置的插件搜索路径;AppRun 会指向自己的 Qt 插件。遇到窗口无法启动时,先看终端的缺库、显示连接或平台插件信息。

左侧竖栏前三个图标依次进入 Editor、Monitor 和 Log Replay 三种工作模式。先使用 Editor 打开 XML;需要监控时进入 Monitor,任务结束后使用 Replay 打开日志。图形界面布局可能随版本变化,本篇按 1.9.0 的实际操作说明。

截图预留:Groot2 启动与版本。 展示 1.9.0 界面及 Editor、Monitor、Replay 入口。


3.建立 Groot2 练习工程

先完成本系列 持续任务与中止 中的 bt4_running 工程,确认能够观察正常完成与条件中止。本篇在它的基础上加入监控与日志:

  1. 在文件管理器中复制整个 bt4_running 文件夹,将副本重命名为 bt4_groot。
  2. 删除副本中的 build、install 文件夹(如果存在),清理持续任务工程的构建缓存和结果。
  3. 用 VSCode 新窗口打开 bt4_groot,按下面的步骤修改项目名、CMake 配置、XML 和主程序。

框架来源仍为 CMake工程模板,继续使用 linux-debug / linux-release 预设。复用 Humble 安装的库时,VSCode 的配置、运行、调试进程仍需继承 ROS 2 环境。

参与本篇的文件为:

bt4_groot/
├── CMakeLists.txt
├── CMakePresets.json
├── cmake/
│   ├── ProjectOptions.cmake
│   └── BehaviorTree.cmake
├── src/
│   ├── CMakeLists.txt
│   └── main.cpp
├── trees/
│   └── mission.xml
└── navigation.btlog          程序正常退出后保存的运行日志

navigation.btlog 由程序写出,不需要提前新建。每次启动程序会重新打开并覆盖这个文件;需要保留上次结果时,应先复制或重命名,再启动下一次程序。

顶层 CMakeLists.txt 的项目名改为:

project(bt4_groot VERSION 1.0.0 LANGUAGES C CXX)

3.1.src/CMakeLists.txt

用下面的完整内容替换新工程的 src/CMakeLists.txt:

include("${PROJECT_SOURCE_DIR}/cmake/BehaviorTree.cmake")

add_executable(${PROJECT_NAME}
    ${CMAKE_CURRENT_SOURCE_DIR}/main.cpp
)

target_link_libraries(${PROJECT_NAME}
    PRIVATE
        project_options
        project_warnings
        ${BTCPP_TARGET}
)

# XML 和日志均使用工程目录下的完整路径。
target_compile_definitions(${PROJECT_NAME}
    PRIVATE
        BT_XML_PATH="${PROJECT_SOURCE_DIR}/trees/mission.xml"
        BT_LOG_PATH="${PROJECT_SOURCE_DIR}/navigation.btlog"
)

新增的 BT_LOG_PATH 和已有的 BT_XML_PATH 一样,由 CMake 提供字符串形式的完整路径。因此,无论 VSCode 从哪个工作目录启动,程序都会读取本工程的 XML,并把日志写到本工程根目录。

3.2.trees/mission.xml

用下面的内容替换 trees/mission.xml:

<root BTCPP_format="4" main_tree_to_execute="MainTree">
    <BehaviorTree ID="MainTree">
        <ReactiveSequence name="navigation_guard">
            <NavigationAllowed allowed="{navigation_allowed}"/>
            <NavigateToGoal goal="loading_area" ticks="3"/>
        </ReactiveSequence>
    </BehaviorTree>

    <TreeNodesModel>
        <Condition ID="NavigationAllowed">
            <input_port name="allowed" type="bool"/>
        </Condition>
        <Action ID="NavigateToGoal">
            <input_port name="goal" type="std::string"/>
            <input_port name="ticks" type="int" default="3"/>
        </Action>
    </TreeNodesModel>
</root>

<BehaviorTree> 仍然描述实际执行结构。下面新增的 <TreeNodesModel> 告诉 Groot2:自定义节点属于什么类型、有哪些端口,以及端口的类型和默认值。

XML 模型声明 对应 C++ 内容
<Condition ID="NavigationAllowed"> 注册名为 NavigationAllowed 的条件节点
<Action ID="NavigateToGoal"> 注册名为 NavigateToGoal 的动作节点;持续动作在这里也归为 Action
<input_port name="allowed" type="bool"/> BT::InputPort<bool>("allowed")
<input_port name="goal" type="std::string"/> BT::InputPort<std::string>("goal")
<input_port name="ticks" type="int" default="3"/> 名为 ticks、类型为 int、默认值为 3 的输入端口

这些是给图形工具看的接口说明,不能代替 C++ 的节点注册和 providedPorts()。今后修改接口时,两边应保持一致。模型中的 default="3" 是接口默认值,树实例中的 ticks="3" 是这次任务的实际配置。

3.3.让模型跟随 C++ 声明生成

主示例直接把 TreeNodesModel 写在 XML 中,第一次打开 Editor 就能读取。节点较多时,可以在注册自定义节点之后、创建树之前生成独立模型文件,减少手写接口与 C++ 不一致的机会。下面是可选片段,加入前需包含 xml_parsing.h 和 fstream:

#include "behaviortree_cpp/xml_parsing.h"
#include <fstream>

// 放在注册之后;用工程目录的明确路径保存,以下路径仅为示例。
std::ofstream models("nodes.xml");
if (!models)
{
    throw std::runtime_error("Cannot open node model output");
}
models << BT::writeTreeNodesModelXML(factory);

writeTreeNodesModelXML(factory, include_builtin=false) 第一个参数是已完成注册的工厂引用;第二个可选布尔值控制是否包含内置节点,默认不包含。返回 std::string,是模型 XML 文本,函数本身不写文件。相对的 nodes.xml 路径受启动工作目录影响,实际工程可以像 BT_XML_PATH 一样通过 CMake 提供完整路径。

独立模型文件通过 Groot2 的节点模型导入功能加载,不把它当成带 MainTree 的任务 XML 执行。手写模型和生成模型只需选择一种维护;修改 C++ 端口后应重新生成或更新模型。

3.4.src/main.cpp

用下面的完整内容替换 src/main.cpp。条件检查与计数导航继续使用本系列“持续任务与中止”中介绍的逻辑;本篇增加发布器、日志对象,以及连接前和退出前的终端等待:

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

#include <chrono>
#include <iostream>
#include <stdexcept>
#include <string>

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

    static BT::PortsList providedPorts()
    {
        return {BT::InputPort<bool>("allowed")};
    }

    BT::NodeStatus tick() override
    {
        auto allowed = getInput<bool>("allowed");
        if (!allowed)
        {
            throw BT::RuntimeError("Cannot read input [allowed]: ", allowed.error());
        }
        std::cout << "[NavigationAllowed] allowed="
                  << (allowed.value() ? "true" : "false") << '\n';
        return allowed.value() ? BT::NodeStatus::SUCCESS : BT::NodeStatus::FAILURE;
    }
};

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

    static BT::PortsList providedPorts()
    {
        return {BT::InputPort<std::string>("goal"),
                BT::InputPort<int>("ticks", 3, "完成导航所需的 onRunning 调用次数")};
    }

    BT::NodeStatus onStart() override
    {
        auto goal = getInput<std::string>("goal");
        auto ticks = getInput<int>("ticks");
        if (!goal)
        {
            throw BT::RuntimeError("Cannot read input [goal]: ", goal.error());
        }
        if (!ticks)
        {
            throw BT::RuntimeError("Cannot read input [ticks]: ", ticks.error());
        }
        if (goal.value().empty() || ticks.value() < 1)
        {
            throw std::runtime_error("goal must not be empty and ticks must be positive");
        }

        goal_ = goal.value();             // 保存本次目标,供后续回调使用
        required_ticks_ = ticks.value();  // 保存模拟完成所需的检查次数,本例为 3
        completed_ticks_ = 0;             // 每次开始新任务,都从零次检查开始
        // 真实导航通常在这里发送一次请求,本例只保存模拟任务状态。
        std::cout << "[NavigateToGoal] start goal=" << goal_ << '\n';
        // 结束本次回调并报告“尚未完成”,后续 tick 再进入 onRunning。
        return BT::NodeStatus::RUNNING;
    }

    BT::NodeStatus onRunning() override
    {
        // 每次进入这个回调,模拟检查次数就加一;函数返回后成员计数仍会保留。
        ++completed_ticks_;
        std::cout << "[NavigateToGoal] progress=" << completed_ticks_
                  << '/' << required_ticks_ << '\n';
        // 本例把“检查次数达到要求”当作完成条件,没有查询机器人的真实位置。
        if (completed_ticks_ >= required_ticks_)
        {
            std::cout << "[NavigateToGoal] arrived goal=" << goal_ << '\n';
            // 已满足模拟完成条件,向父节点报告成功,本次函数到这里结束。
            return BT::NodeStatus::SUCCESS;
        }
        // 尚未达到要求,结束本次检查,等待后续 tick 再进入这个回调。
        return BT::NodeStatus::RUNNING;
    }

    void onHalted() override
    {
        // 运行中的任务收到中止要求时,基类才会调用这个回调。
        // 真实导航在这里取消请求、停止当前任务并清理资源。
        std::cout << "[NavigateToGoal] halted goal=" << goal_
                  << ", progress=" << completed_ticks_ << '\n';
        completed_ticks_ = 0;
    }

private:
    std::string goal_;          // 本次目标
    int required_ticks_ = 0;    // 模拟完成所需的运行检查次数,由 onStart 读取
    int completed_ticks_ = 0;   // 已完成的运行检查次数,可跨多次回调保存
};

int main()
{
    // 正常完成后,再改成 true 观察条件失败触发中止。
    const bool cancel_after_two_ticks = false;

    try
    {
        BT::BehaviorTreeFactory factory;
        factory.registerNodeType<NavigationAllowed>("NavigationAllowed");
        factory.registerNodeType<NavigateToGoal>("NavigateToGoal");

        auto blackboard = BT::Blackboard::create();
        blackboard->set("navigation_allowed", true);
        auto tree = factory.createTreeFromFile(BT_XML_PATH, blackboard);

        // 两个观察对象在树创建后构造,并在整个 tick 过程中保持存活。
        BT::Groot2Publisher publisher(tree, 1667);
        BT::FileLogger2 file_logger(tree, BT_LOG_PATH);

        std::cout << "Groot2 monitor: server=1667, events=1668\n";
        std::cout << "Log file: " << BT_LOG_PATH << '\n';
        std::cout << "Connect Groot first, then press Enter to start.\n";
        std::cin.get();

        BT::NodeStatus result = BT::NodeStatus::RUNNING;
        int tick_count = 0;
        while (result == BT::NodeStatus::RUNNING)
        {
            std::cout << "--- tick " << ++tick_count << '\n';
            result = tree.tickExactlyOnce();
            std::cout << "Root returned: " << BT::toStr(result) << '\n';
            if (result != BT::NodeStatus::RUNNING)
            {
                break;
            }

            if (cancel_after_two_ticks && tick_count == 2)
            {
                blackboard->set("navigation_allowed", false);
                std::cout << "[main] navigation is no longer allowed\n";
            }
            // 放慢每次检查,方便在 Groot 中观察节点状态。
            tree.sleep(std::chrono::milliseconds(1000));
        }

        std::cout << "Tree result: " << BT::toStr(result) << '\n';
        std::cout << "Press Enter to exit and finish writing the log.\n";
        std::cin.get();
        return result == BT::NodeStatus::SUCCESS ? 0 : 1;
    }
    catch (const std::exception& error)
    {
        std::cerr << "Error: " << error.what() << '\n';
        return 2;
    }
}

截图预留:bt4_groot 的文件与配置。 展示新项目名、两个路径宏,以及带 TreeNodesModel 的 XML。


4.新增接口的参数与生命周期

本篇没有改变三个导航回调的调用规则。新增的发布器和日志对象订阅节点状态变化;主程序仍通过 tickExactlyOnce() 驱动树,它们不会代替主循环执行节点。

4.1.Groot2Publisher 提供实时连接

构造函数原型:

Groot2Publisher(const BT::Tree& tree, unsigned server_port = 1667);
参数 本例 含义
第一个 tree 已创建的 tree 被观察的树,以引用传入,树必须比发布器存活更久
第二个 server_port 1667 连接服务端口;发布器还使用其后一号端口,本例为 1668

构造函数创建 publisher 对象,没有普通函数意义上的返回值。它在存活期间提供结构、状态和相应监控协议,不执行树,也不设定 tick 周期。端口占用等初始化错误由 main 的 catch 报告。

本例用 BT::Groot2Publisher publisher(tree, 1667); 将服务端口设置为 1667。随后在 Groot2 的 Monitor 界面填写程序所在电脑的地址与这个服务端口;网络上需要让 1667、1668 两个 TCP 端口可达。

4.2.FileLogger2 保存状态日志

构造函数原型:

FileLogger2(const BT::Tree& tree, const std::filesystem::path& filepath);

第一个参数是被记录的树,第二个参数是日志完整路径。本例 BT_LOG_PATH 的字符串会转换为 filesystem::path。文件名必须使用 .btlog 后缀,不能仅把旧 .fbl 文件重命名来转换格式。

它创建 file_logger 对象,订阅状态变化并在写线程中记录。终端打印的进度文字不会作为完整终端日志自动保存;两次 onRunning 都返回 RUNNING 时,也不一定新增一次状态转换。

发布器和日志对象都在树创建之后构造,在整个 tick 过程中保持存活,并先于树析构。 本例同一作用域中的局部变量按构造的相反顺序销毁,先销毁 logger 和 publisher,再销毁 tree。正常退出后再打开日志;不要强制杀进程后假定尾部记录已经完整。

SQLite 日志是另一种记录方案。本篇先把 FileLogger2 的 .btlog 跑通,避免同时引入两套日志读写配置。

4.3.std::cin.get 给连接和日志写出留出时机

本例使用的 std::cin.get() 不传参数,从标准输入读取一个字符,返回读取结果。本例不使用返回值,只利用它等待终端输入的行为:

  • 第一次等待位于首个 tick 之前。先连接 Groot2,再在 VSCode 终端按回车开始,避免程序执行得太快,来不及建立连接。
  • 第二次等待位于任务结束之后。按回车让程序正常离开作用域,FileLogger2 析构时会把剩余记录写入文件,再打开日志回放。

这两次等待都在主程序中,节点回调本身仍然快速返回。请在 CMake Tools 的 Launch 集成终端 中按回车;不要把回车输进 Groot2 的窗口或只读调试输出窗口。


5.在 Editor 中查看和修改 XML

先启动 Groot2,选择 Editor 模式。点击左上角 Project 旁的文件夹图标,或欢迎页的 Open File,选择新工程的 trees/mission.xml。

本例已经把两个自定义节点的模型写在同一个 XML 的 TreeNodesModel 中,因此可以同时加载结构与模型,不需要导入 Nav2 的节点列表。界面中应出现 NavigationAllowed、NavigateToGoal,以及它们配置的端口值。

画布可能显示一个 Root 入口辅助节点;对本例运行中的 BT::Tree 而言,实际根节点仍是 ReactiveSequence,tree.rootNode() 指向它。

截图预留:Editor 加载自定义节点。 展示完整树图、两个自定义节点,以及 goal、ticks、allowed 的配置。

在导航节点的参数区域,将 goal 从 loading_area 改为 inspection_area,将 ticks 从 3 改为 2,按回车或点击参数框外的空白处确认修改,再按 Ctrl+S 保存当前 XML。确认左侧文件名后的未保存星号消失,并在 VSCode 中检查磁盘上的 mission.xml。只创建了未命名新文件时,应先把该文件保存到磁盘,再保存整个 Project。

回到 VSCode,确认磁盘上的 XML 已更新。首次运行时选择 Linux Debug 预设,执行 Configure → Build,将 Launch 目标设置为 bt4_groot,然后运行。在终端按第一次回车开始执行;这一步先不连接 Monitor,也可以直接运行。应看到目标变为 inspection_area,一次启动加两次运行检查后完成;出现退出提示后,再按一次回车结束程序。

后续只修改 XML 时,C++ 无需重新编译,保存后重新 Launch 即可。运行中的树不会因为文件保存而自动重建,要退出旧程序,再次 Launch 才会重新加载。

截图预留:在 Groot2 中修改端口值。 展示修改后的 goal、ticks,以及保存后 VSCode 中对应的 XML 属性。

观察完这个对照后,将目标和次数恢复为 loading_area、3 并保存,后面的监控与回放都按原始设置进行。


6.连接 Monitor 观察一次正常完成

6.1.先启动程序,再连接图形工具

在 VSCode 中选择 Linux Debug 预设,执行 Configure → Build,将 Launch 目标设置为 bt4_groot,然后运行。

终端开头应显示:

Groot2 monitor: server=1667, events=1668
Log file: .../bt4_groot/navigation.btlog
Connect Groot first, then press Enter to start.

日志路径中的 ... 表示你实际的工程父目录。此时发布器已经创建,程序在首个 tick 之前等待输入,先保留这个窗口。

点击左侧第二个图标进入 Monitor。左上角可直接填写 Host 和 Port;1.9.0 初始显示的端口可能为 5555,应改成与本例代码一致的 1667:

连接项 本例填写 对应程序参数
Host 127.0.0.1 运行 bt4_groot 的电脑地址
Port 1667 Groot2Publisher 的第二个参数

点击左上角 Connect。连接成功后,左侧 Trees 出现 MainTree;必要时双击它打开树图。Groot2 会从 C++ 程序读取当前实际创建的树;即使 Editor 刚才显示的是另一份 XML,这里显示的也是程序正在执行的树。

截图预留:等待开始与 Monitor 连接。 同屏展示程序的等待提示、地址和服务端口、已加载的树,以及连接成功后的 Disconnect 按钮。

6.2.回到终端按回车,观察节点状态变化

连接完成后,回到 VSCode 的 Launch 终端,按一次回车。程序每隔约一秒发起下一次根 tick,正常运行的关键输出为:

--- tick 1
[NavigateToGoal] start goal=loading_area
Root returned: RUNNING
--- tick 2
[NavigateToGoal] progress=1/3
Root returned: RUNNING
--- tick 3
[NavigateToGoal] progress=2/3
Root returned: RUNNING
--- tick 4
[NavigateToGoal] progress=3/3
[NavigateToGoal] arrived goal=loading_area
Root returned: SUCCESS
Tree result: SUCCESS
Press Enter to exit and finish writing the log.

这里省略了每次条件检查打印的 allowed=true,完整程序仍会输出它们。对照树图观察:ReactiveSequence 等待导航时,导航节点处于 RUNNING;最后一次计数达到要求,导航报告 SUCCESS。

本例两次运行检查之间,导航状态一直保持 RUNNING,单纯把进度从 1/3 加到 2/3 不产生新的状态变化记录。Groot2 也不会自动显示 C++ 成员 completed_ticks_ 的数值,成员值仍可用终端或 VSCode 断点观察。

截图预留:正常导航的实时状态。 展示导航节点处于 RUNNING 的树图,以及终端对应的进度输出。

成功状态可能只显示很短时间。 控制节点会重置子节点,4.10.0 的 tickExactlyOnce() 也会在返回终态时把根状态重置为 IDLE。因此,任务结束后看到部分或全部节点回到 IDLE,不能据此认定任务没有成功;本次结果看 tickExactlyOnce() 的返回值,历史变化用日志回放查看。

先在 Groot2 Monitor 点击 Disconnect,再回到终端按一次回车,让程序正常退出。这样避免结束发布程序后图形工具仍自动重连,产生 Reply timeout 提示。此时日志记录完成。在 VSCode 文件管理器中,把 navigation.btlog 复制并重命名为 navigation_success.btlog,保存这次正常完成的记录。

6.3.Groot2 与示例不在同一个网络环境时

只有在 Groot2 能访问到发布程序的两个 TCP 端口时,才能建立实时连接:

运行方式 连接地址与条件
两者在同一台电脑 使用 127.0.0.1,端口为 1667、1668
两者位于不同电脑 填写运行示例那台电脑可访问的 IP,并保证两个端口可达

这两个端口用于 ZeroMQ 通信,与 ROS_DOMAIN_ID 无关。本篇观察的是自行编写的 bt4_groot,并没有直接接入 Nav2 的 BT Navigator。


7.用 Log Replay 回看正常完成的记录

确认示例程序已经正常退出、文件已写完。点击左侧第三个图标进入 Log Replay,点击左上角 Log 旁的文件夹图标,打开 navigation_success.btlog。

回放模式会从日志中恢复树和状态变化记录,不要求原来的 C++ 程序继续运行,也不需要连接 1667、1668。

先用底部播放按钮观察整体过程,再通过底部时间滑块或右侧 Transitions 状态变化表选择具体时刻。本例重点看:

对象 日志中应能找到的变化 对应含义
NavigateToGoal 从 IDLE 进入 RUNNING 第一次执行 onStart() 后任务开始
NavigateToGoal 从 RUNNING 进入 SUCCESS 最后一次检查满足计数条件
导航及控制节点 后续回到 IDLE 当前任务结束,节点状态被重置

回放中的 SUCCESS 表示某条状态记录发生时的结果;把滑块拖到最后,可能又看到重置后的 IDLE。查看表格中的前后状态,能避免只看最后一帧产生误解。

截图预留:正常任务的日志回放。 展示 navigation_success.btlog、状态变化表,以及选中导航 RUNNING → SUCCESS 记录时的树图。


8.再观察条件失败与导航中止

先保留好 navigation_success.btlog。在 main() 中把 cancel_after_two_ticks 改成 true,保存后 Build → Launch。

程序再次停在开始提示时,在 Groot2 中进入 Monitor,使用原来的地址和服务端口重新 Connect,然后回到 VSCode 终端按回车。关键输出为:

--- tick 1
[NavigateToGoal] start goal=loading_area
Root returned: RUNNING
--- tick 2
[NavigateToGoal] progress=1/3
Root returned: RUNNING
[main] navigation is no longer allowed
--- tick 3
[NavigationAllowed] allowed=false
[NavigateToGoal] halted goal=loading_area, progress=1
Root returned: FAILURE
Tree result: FAILURE
Press Enter to exit and finish writing the log.

这次只进入一次 onRunning()。第二次根 tick 结束后,主程序修改 Blackboard;第三次根 tick 重新检查条件,条件失败,父节点中止导航并调用它的 onHalted()。Groot2 接收到的是这些执行引起的状态变化,具体回调里做了什么仍由 C++ 实现决定。

截图预留:条件变化触发中止的实时观察。 展示终端中的 allowed=false、halted,以及 Monitor 的树图。

先 Disconnect,再按第二次回车让程序正常退出,将新生成的 navigation.btlog 复制并重命名为 navigation_cancel.btlog,再切到 Replay 打开它。

与成功记录对照:

观察对象 正常完成 条件中止
条件节点 允许导航 出现 FAILURE
导航节点 RUNNING → SUCCESS 仍在运行时被中止,回到 IDLE
终端结果 Tree result: SUCCESS Tree result: FAILURE

中止不是导航返回 FAILURE。 本例 onHalted() 返回 void,导航在父节点处理中止后回到 IDLE;树的失败来自前置条件及 ReactiveSequence 的执行规则。回放时不要要求一定存在导航节点的 RUNNING → FAILURE 记录。

截图预留:中止日志与成功日志对照。 展示条件失败、导航回到 IDLE 的记录,与正常完成的 RUNNING → SUCCESS 对比。

对照完成后,把取消设置恢复为 false 并 Build,保留两份日志供后续观察。


9.常见问题与排查位置

现象 优先检查
Qt 提示无法连接显示器,窗口打不开 是否从 Ubuntu 图形桌面启动 Groot2,当前会话是否能正常显示 Qt 窗口
启动提示缺少 xcb-cursor Ubuntu 22.04 安装 libxcb-cursor0,再从图形桌面终端启动
程序链接不到 Groot2Publisher 使用的 4.10.0 库是否以 BTCPP_GROOT_INTERFACE=ON 构建;只改应用开关不能改变现成库
Editor 提示未知节点或端口不对 TreeNodesModel 是否声明了自定义节点,注册名及端口是否与 C++ 一致
程序启动时报端口占用 上次示例是否仍停在第二次回车提示处;结束旧进程后再启动新程序
Connect 失败 发布程序是否已启动,连接地址、两个 TCP 端口和网络访问配置是否匹配
Monitor 已连接,但没有执行变化 程序是否仍在第一次回车提示处等待;回到 Launch 终端开始执行
日志为空、不完整或无法回放 日志对象是否在 tick 期间存活,程序是否正常退出,是否打开了正确的 .btlog 文件
修改 XML 后运行仍是旧树 是否保存到 BT_XML_PATH 指向的 mission.xml,并退出旧程序后重新 Launch
任务完成后树图又变成 IDLE 查看终端返回结果和历史状态记录;这是本例终态后的重置过程

完成练习后,分别检查自定义节点加载、XML 保存、实时状态接收,以及成功和中止两种日志的回放。图形编辑后的 XML 应交给示例程序重新运行,确认目标和检查次数按修改生效。


10.本篇验证范围

示例在 Ubuntu 22.04 的 Humble 容器中使用 CMake 3.22.1、GCC 11.4、C++20 和 BehaviorTree.CPP 4.10.0 构建运行。Groot2 1.9.0 已实际加载本例自定义模型,编辑保存目标与次数,接收实时 RUNNING 状态,并分别打开成功和条件中止生成的 .btlog。验证使用隔离的虚拟图形显示,文中仍预留由学习时补齐的桌面截图。

这部分验证没有使用 PRO 功能,也没有连接实际机器人或 Nav2 执行器。

11.当前学习进度

v4.10.0 的本篇工具实践到此结束:已经把 XML 编辑、自定义节点模型、实时状态发布和二进制日志回放连成一个完整流程。 Groot2 帮助观察树的结构和状态,VSCode 断点继续用于查看 C++ 调用、局部变量和成员数据;两种观察方式可以结合使用。


v4 学习路线 · 上一篇:全局Blackboard与数据更新