Wiki
Groot1
本文由簡體中文內容確定性轉換,並受版本化術語表保護。
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++ 調用、局部變量和成員數據;兩種觀察方式可以結合使用。