跳到正文

Wiki

Groot2

約 21 分鐘閱讀

本文由簡體中文內容確定性轉換,並受版本化術語表保護。

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 工程,確認能夠觀察正常完成與條件中止。本篇在它的基礎上加入監控與日誌:

  1. 在檔案管理器中複製整個 bt4_running 資料夾,將副本重新命名為 bt4_groot。
  2. 刪除副本中的 build、install 資料夾(如果存在),清理持續任務工程的構建快取和結果。
  3. 用 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++ 呼叫、區域性變數和成員資料;兩種觀察方式可以結合使用。


v4 學習路線 · 上一篇:全域性Blackboard與資料更新