跳到正文

Wiki

Groot1

约 19 分钟阅读

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

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

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

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

本篇选择与 3.8.x 配套的 Groot1。它目前处于维护模式,适合延续这套既有工程;本篇使用的是 BT::PublisherZMQ 和 .fbl 日志格式。

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

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

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

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


2.准备并启动 Groot1

Groot1 是桌面程序,需要可显示 Qt 窗口的图形桌面。下面的安装命令在 Ubuntu 22.04 中执行;请在 Ubuntu 图形桌面中打开终端并启动 Groot1。

本篇默认 Groot1 和示例程序运行在同一台 Ubuntu 电脑上,用 127.0.0.1 连接。运行在不同电脑时,使用后文说明的网络地址和端口配置。

2.1.安装依赖并获取源码

在 VSCode 集成终端准备依赖:

sudo apt update
sudo apt install build-essential cmake git ninja-build qtbase5-dev \
    libqt5opengl5-dev libqt5svg5-dev libzmq3-dev libdw-dev

Qt5 提供图形界面;ZeroMQ 用于实时监控通信。libzmq3-dev 也让自行从源码构建的 BehaviorTree.CPP 能启用相应发布接口。继续保留 Ubuntu 22.04 的 CMake 3.22.1 即可。

在自己保存开发工具的目录中获取 Groot1:

git clone --recurse-submodules https://github.com/BehaviorTree/Groot.git
cd Groot
git checkout 70973d004365f16d47a8c7bd9c5d84fa0bb9d05d
git submodule update --init --recursive

这里固定到本文验证过的 Groot1 提交 70973d0,便于对照相同的界面和代码。--recurse-submodules 用于同时获取上游声明的子模块;不要把 Groot1 源码放进 bt_running/src 中,它是独立的工具工程。

2.2.编译并打开界面

在 Groot1 源码根目录的终端中执行:

source /opt/ros/humble/setup.bash
cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release
cmake --build build --target Groot --parallel 2
./build/Groot

加载 Humble 环境后,这份 Groot1 源码会发现现有的 ROS 依赖,配置中可以看到 Compiling with AMENT 和 ZeroMQ found。这里直接使用上游 CMake 工程;BT 示例自己的模板、预设和 C++20 配置在后面继续沿用。

--target Groot 表示只构建图形程序及其依赖,--parallel 2 表示最多同时执行两个构建任务。首次构建完成后,后续直接运行 ./build/Groot 即可。

启动窗口可以选择 Editor、Monitor、Log Replay,选定后点击 START。已经进入主界面时,也可以通过 Tools → Switch Mode... 切换模式。

截图预留:Groot1 构建完成。 展示 ZeroMQ found 和 Groot 目标构建成功的终端输出。

截图预留:Groot1 启动窗口。 展示三个模式以及 START 按钮。


3.复制已有工程,加入监控与日志

直接复制整个 bt_running 文件夹,把副本重命名为 bt_groot,保留原工程。删除新副本中的 build、install 文件夹(如果存在),再用 VSCode 新窗口打开 bt_groot。

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

参与本篇的文件为:

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

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

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

project(bt_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.fbl"
)

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

3.2.trees/mission.xml

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

<root 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> 告诉 Groot1:自定义节点属于什么类型、有哪些端口,以及端口的类型和默认值。

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.src/main.cpp

用下面的完整内容替换 src/main.cpp。节点逻辑沿用原来的例子,重点看 main() 中新增的两个观察对象,以及开始和结束时的等待:

#include "behaviortree_cpp_v3/bt_factory.h"
#include "behaviortree_cpp_v3/loggers/bt_file_logger.h"
#include "behaviortree_cpp_v3/loggers/bt_zmq_publisher.h"

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

class NavigationAllowed : public BT::ConditionNode
{
public:
    NavigationAllowed(const std::string& name, const BT::NodeConfiguration& 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::NodeConfiguration& 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::PublisherZMQ publisher(tree, 25, 1666, 1667);
        BT::FileLogger file_logger(tree, BT_LOG_PATH);

        std::cout << "Groot monitor: publisher=1666, server=1667\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.tickRoot();
            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;
    }
}

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


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

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

4.1.PublisherZMQ 发布运行状态

构造函数原型:

PublisherZMQ(
    const BT::Tree& tree,
    unsigned max_msg_per_second = 25,
    unsigned publisher_port = 1666,
    unsigned server_port = 1667);

作用:为 Groot1 提供当前树的结构和状态变化。程序中的写法是 BT::PublisherZMQ publisher(tree, 25, 1666, 1667);。

参数 本例传入 含义
第一个 tree 已创建的 tree 要观察的行为树,以引用传入
第二个 max_msg_per_second 25 最大发布频率,单位为每秒消息数;不控制行为树的 tick 频率
第三个 publisher_port 1666 发布运行状态数据的端口
第四个 server_port 1667 Groot1 请求树描述等数据时连接的服务端口

这是构造函数,没有普通函数意义上的返回值;执行后得到名为 publisher 的对象。它在存活期间提供监控通信,析构时关闭相应资源。端口占用等初始化错误会进入 main() 的异常报告。

构造参数有默认值,因此 BT::PublisherZMQ publisher(tree); 也使用相同的频率和端口。本篇显式写全,方便与 Groot1 界面逐项对照。

4.2.FileLogger 保存二进制运行日志

构造函数原型:

FileLogger(
    const BT::Tree& tree,
    const char* filename,
    uint16_t buffer_size = 10);

作用:记录节点的状态变化,生成 Groot1 的 Log Replay 可以读取的 .fbl 文件。

参数 本例传入 含义
第一个 tree 已创建的 tree 要记录的行为树
第二个 filename BT_LOG_PATH 日志文件路径;这个宏展开为 CMake 提供的字符串字面量
第三个 buffer_size 省略,使用 10 状态变化记录的缓冲条数,达到阈值后写出;不是每十次根 tick 才记录一次

它同样是构造函数,不返回任务状态,而是创建 file_logger 对象。.fbl 是二进制状态日志,不是终端文本;std::cout 中的目标字符串、进度文字不会作为完整终端日志自动写进去。

两个观察对象都要在树创建后构造,并在整个执行期间保持存活。 如果在一个很快结束的局部代码块里创建它们,退出代码块时对象就被销毁,后面的节点执行也就无法继续发布或记录。本例将它们放在与 tree 相同的 try 作用域中;离开作用域时,先销毁观察对象,再销毁树。

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

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

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

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


5.在 Editor 中查看和修改 XML

先启动 Groot1,选择 Editor 模式。点击左上方 Load Tree,选择新工程的 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,点击 Save Tree 或按 Ctrl+S,保存到当前的 mission.xml。

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

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

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

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


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

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

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

终端开头应显示:

Groot monitor: publisher=1666, server=1667
Log file: .../bt_groot/navigation.fbl
Connect Groot first, then press Enter to start.

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

在 Groot1 中选择 Tools → Switch Mode... → Real-time Monitor,或重新打开程序并选择 Monitor。填写:

界面字段 本例填写 对应程序参数
Server IP 127.0.0.1 运行 bt_groot 的地址
Publisher Port 1666 PublisherZMQ 的第三个参数
Server Port 1667 PublisherZMQ 的第四个参数

点击上方 Connect。连接成功后,Groot1 会从 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 不产生新的状态变化记录。Groot1 也不会自动显示 C++ 成员 completed_ticks_ 的数值,成员值仍可用终端或 VSCode 断点观察。

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

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

最后回到终端,再按一次回车,让程序正常退出。此时日志记录完成。在 VSCode 文件管理器中,把 navigation.fbl 复制并重命名为 navigation_success.fbl,保存这次正常完成的记录。

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

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

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

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


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

确认示例程序已经正常退出、文件已写完。在 Groot1 中选择 Tools → Switch Mode... → Log Replay,点击 Load Log,打开 navigation_success.fbl。

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

先用播放按钮观察整体过程,再通过时间滑块、记录序号或下方状态变化表选择具体时刻。本例重点看:

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

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

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


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

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

程序再次停在开始提示时,在 Groot1 中进入 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()。Groot1 接收到的是这些执行引起的状态变化,具体回调里做了什么仍由 C++ 实现决定。

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

按第二次回车让程序正常退出,将新生成的 navigation.fbl 复制并重命名为 navigation_cancel.fbl,再切到 Log 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 图形桌面启动 Groot1,当前会话是否能正常显示 Qt 窗口
构建提示缺少 Qt5Svg 或 Qt5OpenGL Groot1 所在环境是否安装了对应的 Qt5 开发包
界面没有可用的 Monitor 功能 Groot1 的 Configure 是否找到 ZeroMQ;安装 libzmq3-dev 后重新配置并构建
Editor 提示未知节点或端口不对 TreeNodesModel 是否声明了自定义节点,注册名及端口是否与 C++ 一致
程序启动时报端口占用 上次示例是否仍停在第二次回车提示处;结束旧进程后再启动新程序
Connect 失败 发布程序是否已启动,连接地址、两个 TCP 端口和网络访问配置是否匹配
Monitor 已连接,但没有执行变化 程序是否仍在第一次回车提示处等待;回到 Launch 终端开始执行
日志为空、不完整或无法回放 日志对象是否在 tick 期间存活,程序是否正常退出,是否打开了正确的 .fbl 文件
修改 XML 后运行仍是旧树 是否保存到 BT_XML_PATH 指向的 mission.xml,并退出旧程序后重新 Launch
任务完成后树图又变成 IDLE 查看终端返回结果和历史状态记录;这是本例终态后的重置过程

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


10.当前学习进度

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