Wiki
Groot2
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 工程,确认能够观察正常完成与条件中止。本篇在它的基础上加入监控与日志:
- 在文件管理器中复制整个
bt4_running文件夹,将副本重命名为bt4_groot。 - 删除副本中的
build、install文件夹(如果存在),清理持续任务工程的构建缓存和结果。 - 用 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++ 调用、局部变量和成员数据;两种观察方式可以结合使用。