Wiki
Groot1
Existing English translations were reused. 110 current source blocks remain in Simplified Chinese, without triggering paid translation.
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 发布运行状态
Constructor prototype:
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 保存二进制运行日志
Constructor prototype:
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++ 调用、局部变量和成员数据;两种观察方式可以结合使用。