跳到正文

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++ 調用、局部變量和成員數據;兩種觀察方式可以結合使用。