Wiki
節點之間傳數據
本文由簡體中文內容確定性轉換,並受版本化術語表保護。
1.用 Ports 與 Blackboard 傳遞機械人目標
對應 BehaviorTree.CPP 3.8 官方文檔:Tutorial - Basics 中的 Blackboard and ports 和 Ports with generic types,並參考 3.8.7 tree_node.h、basic_types.h 與 blackboard.h。
上一篇已經給出“檢查電量 → 選擇目標 → 打印目標”的完整工程。 如果還沒有運行,可以先按上一篇的實踐部分建立工程; 本篇另建一個獨立示例,保留前一個工程,方便對照。
接下來把字符串目標換成機械人常用的座標數據,集中解決幾個相關問題:不同名稱的端口怎樣連接,XML 文本怎樣變成 C++ 類型,節點怎樣讀取並修改同一份數據,外部程序怎樣與樹交換數據。
本篇仍使用同步節點計算和打印數據,實際移動與導航留到“持續任務與中止”學習。
2.從外部位姿計算一個任務目標
假設移動機械人當前位姿為 (1.0, 2.0, 0.0),
需要沿當前朝向前進 2 米,
再給目標的 x 座標增加 0.5 米的示例偏移:
外部程序初始化 Blackboard
├── robot_pose = Pose2D{1.0, 2.0, 0.0}
└── travel_distance = 2.0
↓
Sequence
├── CalculateGoal 根据起点、朝向和距离计算目标,写入 mission_goal
├── OffsetGoal 读取 mission_goal,修改后写回同一个条目
├── PrintGoal 从 mission_goal 读取计算结果
└── PrintGoal 从 XML 固定文本读取另一组坐标
↓
外部程序读取 mission_goal
這裏的 Pose2D 是自行定義的 C++ 結構體,包含 x、y、yaw,不依賴 ROS 消息類型。
位置單位為米,朝向單位為弧度,示例中的座標使用同一個參考座標系。
計算時使用:
goal.x = start.x + distance × cos(start.yaw)
goal.y = start.y + distance × sin(start.yaw)
goal.yaw = start.yaw
起始位姿(1.0, 2.0, 0.0),起始朝向為 0,所以朝前(是x)2米,先得到 (3.0, 2.0, 0.0);
加上 x 偏移後,最終任務目標為 (3.5, 2.0, 0.0)。這是座標計算示例,不包含路徑規劃、碰撞檢查或導航控制。
3.端口名稱與 Blackboard 鍵各管一層
本篇用以下連接傳遞目標:
<CalculateGoal start="{robot_pose}" distance="{travel_distance}" goal="{mission_goal}"/>
<OffsetGoal goal="{mission_goal}" offset_x="0.5"/>
<PrintGoal target="{mission_goal}"/>
讀這段 XML 時,把節點接口和存儲位置分開看:
| 名稱 | 所屬位置 | 作用 |
|---|---|---|
CalculateGoal 的 goal |
輸出端口 | 計算節點寫出目標的接口 |
OffsetGoal 的 goal |
雙向端口 | 偏移節點讀取並更新目標的接口 |
PrintGoal 的 target |
輸入端口 | 打印節點讀取目標的接口 |
mission_goal |
Blackboard 鍵 | 保存這幾個端口共同使用的目標數據 |
生產者的端口叫 goal,消費者的端口叫 target,仍然可以連接,因為 XML 把它們映射到同一個 {mission_goal},而且使用相同的 Pose2D 類型。
C++ 中使用的是節點自己的端口名稱:setOutput("goal", ...) 或 getInput<Pose2D>("target")。
將來把 Blackboard 鍵改為 delivery_goal,只需統一調整 XML 中的映射,節點實現不必跟着改名。
3.1.三種端口方向
| 聲明 | 讀寫方法 | 適合的職責 |
|---|---|---|
BT::InputPort<T> |
getInput<T>() |
讀取起點、距離、目標等輸入 |
BT::OutputPort<T> |
setOutput() |
產生並寫出一個結果 |
BT::BidirectionalPort<T> |
先讀取,再按需寫出 | 讀取已有目標,修改後寫回 |
雙向端口同時提供讀寫接口;它不會自動完成修改,也不保證條目已經有值。
本例先執行 CalculateGoal,再執行 OffsetGoal,就是為了在讀寫目標前先產生有效數據。
getInput() 將值讀到本地變量,修改這個變量不會直接改變 Blackboard。
偏移節點需要明確調用 setOutput() 寫回結果。
4.XML 文本與 C++ 類型怎樣對應
4.1.數值可以來自固定文本、Blackboard 或默認值
計算節點的距離輸入聲明為 double,下面三種配置對應三種來源:
| XML 配置 | 取值方式 |
|---|---|
distance="2.0" |
將固定文本轉換成 double |
distance="{travel_distance}" |
從 Blackboard 讀取對應條目 |
省略 distance 屬性 |
使用節點聲明的默認值,本例為 1.0 |
BehaviorTree.CPP 已經提供常用數值類型的文本轉換。
默認值通過 providedPorts() 聲明;
下面完整程序使用 3.8 的三參數寫法 BT::InputPort<double>("distance", 1.0, "前进距离,单位为米")。
默認值用於沒有顯式配置該輸入的情況。
如果 XML 已經寫成 {unknown_distance},但該條目沒有有效值,讀取仍會失敗,不會自動退回默認值。
4.2.自定義類型需要約定文本格式
本篇約定 Pose2D 的文本格式為三個用分號分隔的實數:
x;y;yaw
2.0;3.0;1.57
因此,<PrintGoal target="2.0;3.0;1.57"/> 需要把字符串轉換成 Pose2D。
做法是在 BT 命名空間中,為 convertFromString<Pose2D>() 提供模板特化,再分別轉換三個數值。
特化放在使用該類型聲明端口的節點類之前。
兩條數據路徑有所區別:
C++ 计算出的 Pose2D → setOutput() → Blackboard → getInput<Pose2D>()
直接读取对应类型的值
XML 中的 "2.0;3.0;1.57" → getInput<Pose2D>() → convertFromString<Pose2D>()
先解析文本,再得到值
Blackboard 可以保存結構體,不需要把所有數據先轉成字符串。 XML 文本轉換也不會讓任意兩個 C++ 類型自動互相兼容;生產者、消費者和外部程序仍應統一數據類型。
5.沿用框架建立數據傳遞工程
本篇繼續使用上一篇的 BT 練習版,目錄來自 CMake工程模板, 構建方式見 CMakePresets與構建安裝。 需要支持 C++20 的編譯器、CMake 3.22 或更新版本和 Ninja。 Ubuntu 22.04 可以繼續使用 CMake 3.22.1,沿用上一篇的工程即可;C++20 和預設格式版本 3 都直接沿用當前模板。
如果還沒有建立上一篇的框架工程,先按上一篇準備好工程文件。
本篇直接複製整個 bt_first_tree 文件夾,再把副本重命名為 bt_ports,保留原來的工程供後續對照。
在系統文件管理器中完成下面幾步:
- 打開
bt_first_tree所在的父目錄,複製並粘貼整個文件夾,將副本重命名為bt_ports,讓兩個工程位於同一層目錄。 - 在新副本
bt_ports中,刪除複製過來的build和install文件夾(如果存在)。其中的構建緩存和安裝結果屬於舊工程,新工程稍後會重新生成。 - 在 VSCode 中選擇 文件 → 新建窗口,再選擇 文件 → 打開文件夾,打開新的
bt_ports。
cmake、src、trees 目錄及其中的文件都已隨工程複製過來,不需要重新建立。
稍後直接修改頂層項目名,並用本篇內容替換已有的 src/main.cpp 和 trees/mission.xml。

新工程的構建目錄會在 Configure、Build 時由自己的 preset 重新生成。參與本篇學習的文件為:
bt_ports/
├── CMakeLists.txt
├── CMakePresets.json
├── cmake/
│ ├── ProjectOptions.cmake
│ └── BehaviorTree.cmake
├── src/
│ ├── CMakeLists.txt
│ └── main.cpp
└── trees/
└── mission.xml
在新的頂層 CMakeLists.txt 中,將 project(...) 一行改為:
project(bt_ports VERSION 1.0.0 LANGUAGES C CXX)
其他配置原樣沿用:BehaviorTree.cmake 繼續優先使用已安裝的 3.8.7,未找到時再獲取同版本源碼;
src/CMakeLists.txt 自動以新項目名創建 bt_ports,並將新工程的 trees/mission.xml 路徑交給 BT_XML_PATH。
在 VSCode 編輯器中,用下面的完整 C++ 和 XML 分別替換 src/main.cpp 與 trees/mission.xml 的原有內容,再保存。本篇繼續使用 CMake Tools 配置、構建和運行。
5.1.src/main.cpp
#include "behaviortree_cpp_v3/bt_factory.h"
#include <cmath>
#include <exception>
#include <iomanip>
#include <iostream>
#include <string>
struct Pose2D
{
double x;
double y;
double yaw;
};
// 统一坐标的打印方式,便于观察每一步的数据
std::ostream& operator<<(std::ostream& stream, const Pose2D& pose)
{
return stream << '(' << pose.x << ", " << pose.y << ", " << pose.yaw << ')';
}
namespace BT
{
template <>
inline Pose2D convertFromString<Pose2D>(StringView text)
{
// XML 中的位姿格式为 x;y;yaw,朝向单位为弧度
const auto parts = splitString(text, ';');
if (parts.size() != 3)
{
throw RuntimeError("Pose2D requires x;y;yaw");
}
Pose2D pose{
convertFromString<double>(parts[0]),
convertFromString<double>(parts[1]),
convertFromString<double>(parts[2])};
if (!std::isfinite(pose.x) || !std::isfinite(pose.y) || !std::isfinite(pose.yaw))
{
throw RuntimeError("Pose2D values must be finite");
}
return pose;
}
} // namespace BT
class CalculateGoal : public BT::SyncActionNode
{
public:
CalculateGoal(const std::string& name, const BT::NodeConfiguration& config)
: BT::SyncActionNode(name, config)
{}
static BT::PortsList providedPorts()
{
return {
BT::InputPort<Pose2D>("start"),
BT::InputPort<double>("distance", 1.0, "前进距离,单位为米"),
BT::OutputPort<Pose2D>("goal")};
}
BT::NodeStatus tick() override
{
const auto start = getInput<Pose2D>("start");
const auto distance = getInput<double>("distance");
if (!start)
{
throw BT::RuntimeError("Cannot read input [start]: ", start.error());
}
if (!distance)
{
throw BT::RuntimeError("Cannot read input [distance]: ", distance.error());
}
if (!std::isfinite(distance.value()) || distance.value() < 0.0)
{
throw BT::RuntimeError("Input [distance] must be finite and non-negative");
}
// 沿起始朝向计算目标,计算结果仍是一个 Pose2D 对象
const Pose2D& origin = start.value();
const Pose2D goal{
origin.x + distance.value() * std::cos(origin.yaw),
origin.y + distance.value() * std::sin(origin.yaw),
origin.yaw};
const auto written = setOutput<Pose2D>("goal", goal);
if (!written)
{
throw BT::RuntimeError("Cannot write output [goal]: ", written.error());
}
std::cout << "[CalculateGoal] distance=" << distance.value()
<< ", goal=" << goal << '\n';
return BT::NodeStatus::SUCCESS;
}
};
class OffsetGoal : public BT::SyncActionNode
{
public:
OffsetGoal(const std::string& name, const BT::NodeConfiguration& config)
: BT::SyncActionNode(name, config)
{}
static BT::PortsList providedPorts()
{
return {
BT::BidirectionalPort<Pose2D>("goal"),
BT::InputPort<double>("offset_x", 0.0, "目标在参考坐标系 x 方向的偏移")};
}
BT::NodeStatus tick() override
{
const auto input = getInput<Pose2D>("goal");
const auto offset = getInput<double>("offset_x");
if (!input)
{
throw BT::RuntimeError("Cannot read input [goal]: ", input.error());
}
if (!offset)
{
throw BT::RuntimeError("Cannot read input [offset_x]: ", offset.error());
}
if (!std::isfinite(offset.value()))
{
throw BT::RuntimeError("Input [offset_x] must be finite");
}
// 先修改本地副本,再通过双向端口写回同一个条目
Pose2D goal = input.value();
goal.x += offset.value();
const auto written = setOutput<Pose2D>("goal", goal);
if (!written)
{
throw BT::RuntimeError("Cannot write output [goal]: ", written.error());
}
std::cout << "[OffsetGoal] goal=" << goal << '\n';
return BT::NodeStatus::SUCCESS;
}
};
class PrintGoal : public BT::SyncActionNode
{
public:
PrintGoal(const std::string& name, const BT::NodeConfiguration& config)
: BT::SyncActionNode(name, config)
{}
static BT::PortsList providedPorts()
{
return {BT::InputPort<Pose2D>("target")};
}
BT::NodeStatus tick() override
{
const auto target = getInput<Pose2D>("target");
if (!target)
{
throw BT::RuntimeError("Cannot read input [target]: ", target.error());
}
std::cout << "[PrintGoal:" << name() << "] target=" << target.value() << '\n';
return BT::NodeStatus::SUCCESS;
}
};
int main()
{
try
{
// 在编辑器中修改这个模拟距离,再构建运行;XML 路径由构建配置提供
const double distance = 2.0;
std::cout << std::fixed << std::setprecision(2);
// 外部程序在创建树之前,准备带有明确 C++ 类型的输入
auto blackboard = BT::Blackboard::create();
blackboard->set<Pose2D>("robot_pose", Pose2D{1.0, 2.0, 0.0});
blackboard->set<double>("travel_distance", distance);
BT::BehaviorTreeFactory factory;
factory.registerNodeType<CalculateGoal>("CalculateGoal");
factory.registerNodeType<OffsetGoal>("OffsetGoal");
factory.registerNodeType<PrintGoal>("PrintGoal");
auto tree = factory.createTreeFromFile(BT_XML_PATH, blackboard);
const BT::NodeStatus result = tree.tickRootWhileRunning();
std::cout << "Tree result: " << BT::toStr(result) << '\n';
if (result == BT::NodeStatus::SUCCESS)
{
// 树执行后,外部程序可以从同一块 Blackboard 取得输出
const Pose2D goal = blackboard->get<Pose2D>("mission_goal");
std::cout << "[main] mission_goal=" << goal << '\n';
}
return result == BT::NodeStatus::SUCCESS ? 0 : 1;
}
catch (const std::exception& error)
{
std::cerr << "Error: " << error.what() << '\n';
return 2;
}
}
getInput<T>() 返回的是帶有成功或錯誤信息的結果;檢查有效後,再用 .value() 取得值。setOutput() 同樣有返回結果,本例也進行檢查;類型衝突等情況還可能拋出異常,由 main() 統一顯示。
本例把缺少必要輸入、文本格式錯誤等作為配置或數據錯誤報告,退出碼為 2。實際任務中可以根據節點職責選擇返回 FAILURE 或使用默認值,不必把所有情況都處理成異常。
5.2.創建、寫入和讀取 Blackboard 的 API
上一篇介紹了節點內部的 getInput() 和 setOutput()。
本篇在 main() 中直接訪問 Blackboard,使用的是另一組接口。
下面列出本例用到的函數原型;聲明用於閲讀,不用再複製到工程中。
// Blackboard 类中的成员声明
static BT::Blackboard::Ptr create(BT::Blackboard::Ptr parent = {});
template<typename T>
void set(const std::string& key, const T& value);
template<typename T>
T get(const std::string& key) const;
| 調用 | 參數 | 返回值與作用 |
|---|---|---|
BT::Blackboard::create() |
本例不傳參數;可選參數 parent 指定父 Blackboard,默認空指針 |
返回 BT::Blackboard::Ptr,也就是 std::shared_ptr<BT::Blackboard>,用於持有這塊 Blackboard |
blackboard->set<Pose2D>("robot_pose", Pose2D{1.0, 2.0, 0.0}) |
模板參數 Pose2D 指定值類型;第一個參數是鍵名,第二個參數是要保存的值 |
返回 void,創建或更新條目;類型不兼容等錯誤可能拋出異常 |
blackboard->get<Pose2D>("mission_goal") |
模板參數指定期望讀取的類型;唯一的函數參數是鍵名 | 返回一個 Pose2D 值;本例使用的這個重載在鍵不存在或類型轉換失敗時拋出異常 |
這裏的 key 是 Blackboard 鍵。
節點內部的 getInput("target")、setOutput("goal", value) 接收的則是 節點端口名,由 XML 決定它們連接哪個鍵。
factory.createTreeFromFile(BT_XML_PATH, blackboard) 的第一個參數仍是 XML 路徑,第二個參數把剛準備好的 Blackboard 交給根樹。
這裏傳遞的是共享指針,因此節點執行時讀寫的就是 main() 中持有的同一塊數據。
get<Pose2D>() 返回值的副本,修改取得的 goal 變量不會自動更新 Blackboard;
需要更新時仍應調用 set()。
5.3.帶默認值的輸入端口與雙向端口
本篇第一次使用 InputPort<T>() 的三參數重載:
template<typename T = void>
std::pair<std::string, BT::PortInfo> InputPort(
BT::StringView name,
const T& default_value,
BT::StringView description);
以 BT::InputPort<double>("distance", 1.0, "前进距离,单位为米") 為例:
| 項目 | 含義 |
|---|---|
模板參數 double |
輸入值的 C++ 類型 |
第一個參數 "distance" |
節點自己的端口名稱,對應 XML 中的 distance 屬性 |
第二個參數 1.0 |
沒有配置該輸入時使用的默認值 |
| 第三個參數 | 端口的文字説明,用於描述接口,不參與距離計算 |
| 返回值 | 一項端口聲明,由 providedPorts() 收集後交給工廠 |
在 3.8.7 中,這個默認值重載需要三個參數;沒有説明文字時可以把第三個參數寫成 ""。前面見過的兩參數形式 InputPort<T>(name, description) 中,第二個參數是説明文字,不能把兩種重載混為一談。
BT::BidirectionalPort<Pose2D>("goal") 聲明雙向端口:模板參數指定 Pose2D 類型,第一個參數指定端口名;可選的第二個參數也是説明文字。它同樣返回一項端口聲明。OffsetGoal 隨後用 getInput<Pose2D>("goal") 取得原值,再用 setOutput("goal", goal) 寫回修改後的值;聲明雙向端口本身不會執行讀寫。
5.4.拆分文本與轉換自定義類型的 API
下面兩個接口來自 BT 命名空間:
std::vector<BT::StringView> splitString(
const BT::StringView& strToSplit, char delimeter);
template<typename T>
T convertFromString(BT::StringView str);
| 調用 | 參數 | 返回值 |
|---|---|---|
splitString(text, ';') |
第一個參數是待拆分的文本;第二個參數是分隔字符 ';' |
std::vector<BT::StringView>,本例應得到 x、y、yaw 三個文本片段 |
convertFromString<double>(parts[0]) |
模板參數 double 指定目標類型;函數參數是一個數值文本片段 |
轉換後的 double;無效文本會報告轉換錯誤 |
convertFromString<Pose2D>(text) |
模板參數 Pose2D 指定自定義類型;函數參數是完整的位姿文本 |
本例特化產生的 Pose2D;字段數量錯誤、數值無效等情況拋出異常 |
BT::StringView 是不擁有字符數據的字符串視圖,拆分得到的片段仍引用原始文本。本例在轉換函數內立即使用它們,不把這些視圖保存為節點成員。
template <> 表示為 Pose2D 提供這個函數模板的特化。節點讀取 XML 固定文本時,getInput<Pose2D>() 會通過庫的轉換機制調用它,主程序不需要再手動轉換一次;從 Blackboard 讀取已經保存的 Pose2D 時則直接取得對應類型的值。
5.5.連續驅動行為樹與輔助函數
tree.tickRootWhileRunning() 把重複 tick 的循環放在庫內部,其原型為:
BT::NodeStatus tickRootWhileRunning(
std::chrono::milliseconds sleep_time = std::chrono::milliseconds(10));
唯一的可選參數 sleep_time 表示兩次 tick 之間的最長等待時間,默認 10 毫秒。本例省略它。
函數先執行一次根 tick;只有結果為 RUNNING 時才等待並繼續 tick,直到得到最終結果,返回 BT::NodeStatus。
本例全部是同步節點,正常情況下第一次根 tick 就得到 SUCCESS。
程序中另外幾個輔助接口可以按下面理解:
| 寫法 | 參數與結果 |
|---|---|
name() |
節點成員函數,無參數,返回當前實例名稱的 const std::string&;本例兩個打印節點分別叫 computed 和 literal |
std::isfinite(value) |
傳入一個浮點數,返回 bool,檢查它是否為有限值 |
std::cos(yaw)、std::sin(yaw) |
傳入以弧度表示的角度,返回餘弦、正弦值,用於分解前進距離 |
std::fixed、std::setprecision(2) |
前者設置固定小數格式,後者的參數 2 指定顯示兩位小數;只改變輸出格式,不改變 Blackboard 中的數據 |
5.6.trees/mission.xml
<root main_tree_to_execute="MainTree">
<BehaviorTree ID="MainTree">
<Sequence name="goal_pipeline">
<CalculateGoal start="{robot_pose}"
distance="{travel_distance}"
goal="{mission_goal}"/>
<OffsetGoal goal="{mission_goal}" offset_x="0.5"/>
<PrintGoal name="computed" target="{mission_goal}"/>
<PrintGoal name="literal" target="2.0;3.0;1.57"/>
</Sequence>
</BehaviorTree>
</root>
兩個 PrintGoal 是同一 C++ 類型的不同實例。name 幫助區分輸出,target 則分別配置為 Blackboard 引用和固定文本。
6.在 VSCode 中運行並跟蹤數據變化
保存所有修改後,在新窗口的 CMake 面板中選擇 Linux Debug,對應 linux-debug Configure 預設,再點擊 Configure。確認配置成功後,選擇同名的 Build 預設並點擊 Build。
截圖預留:bt_ports 配置和構建成功。 展示當前
linux-debug預設、構建成功信息,以及新工程名。
在 Launch 中選擇 bt_ports,點擊運行按鈕,在集成終端中查看輸出。新窗口的運行目標應是 bt_ports,與上一篇的 bt_first_tree 區分開。
截圖預留:選擇 bt_ports 運行目標。 展示 CMake Tools 的 Launch 目標和運行按鈕。
預期輸出:
[CalculateGoal] distance=2.00, goal=(3.00, 2.00, 0.00)
[OffsetGoal] goal=(3.50, 2.00, 0.00)
[PrintGoal:computed] target=(3.50, 2.00, 0.00)
[PrintGoal:literal] target=(2.00, 3.00, 1.57)
Tree result: SUCCESS
[main] mission_goal=(3.50, 2.00, 0.00)

完成工程後,分別檢查固定值、默認值、缺失輸入和錯誤格式等情況,確認端口類型、轉換行為與錯誤輸出符合對應示例的説明。
數據變化可以對應到每一步:
| 執行位置 | 讀取什麼 | 寫入或使用什麼 |
|---|---|---|
main() 初始化 |
編輯器中設置的模擬距離、示例位姿 | robot_pose 和 travel_distance |
CalculateGoal |
起點與距離 | 寫入 mission_goal = (3.0, 2.0, 0.0) |
OffsetGoal |
已有目標與 x 偏移 | 更新 mission_goal = (3.5, 2.0, 0.0) |
PrintGoal:computed |
mission_goal |
打印當前值 |
PrintGoal:literal |
XML 固定文本 | 轉換後打印,不修改 mission_goal |
main() 讀取結果 |
mission_goal |
取得樹計算後的目標 |
所有節點都是快速同步動作,本次序列在一次根節點 tick 內完成。Sequence 負責先後順序,Ports 與 Blackboard 負責傳值;共同映射到一個鍵不會自動安排生產者先執行。
與上一篇相同,程序使用構建配置提供的 BT_XML_PATH,自動讀取當前源碼工程的 trees/mission.xml。在 CMake Tools 中啓動程序時,直接使用同一份文件,無需另外設置 XML 路徑參數。
6.1.改變外部輸入,節點取得新的數值
保持 XML 不變,在編輯器中將 src/main.cpp 的 main() 內模擬距離改為:
const double distance = 4.0;
保存後點擊 Build → Launch。這裏改的是 C++,因此需要先重新構建。
這次先計算出 (5.0, 2.0, 0.0),再偏移到 (5.5, 2.0, 0.0)。computed 實例和 main() 都取得新結果,literal 實例仍打印 XML 中的固定座標。

對照完成後,將模擬距離恢復為 2.0 並點擊 Build,後面繼續使用初始設置。
這裏每次運行程序都會重新建立 Blackboard。 程序持續運行時,也可以由外部更新輸入,再發起下一次任務; 應讓更新與樹執行有明確的調度關係,本例沒有引入併發讀寫。
6.2.在 XML 中切換距離來源
將 CalculateGoal 的 distance 屬性改為固定文本:
<CalculateGoal start="{robot_pose}" distance="3.0" goal="{mission_goal}"/>
保存 trees/mission.xml 後再次點擊 Launch,計算節點使用 3 米,最終目標為 (4.5, 2.0, 0.0)。外部程序仍然寫入 travel_distance,但該節點已不再從這個條目讀取距離。
如果完全省略 distance 屬性:
<CalculateGoal start="{robot_pose}" goal="{mission_goal}"/>
則使用聲明的默認值 1 米,最終目標為 (2.5, 2.0, 0.0)。這兩種修改都只調整 XML,保存後點擊 Launch 即可,無需手動先點 Build。繼續後面的示例前,可以恢復主示例的 distance="{travel_distance}"。

6.3.用斷點觀察目標被讀取和寫回
恢復主示例的距離和 XML 配置,保存並點擊 Build。
在 CalculateGoal::tick() 打印目標的語句處,以及 OffsetGoal::tick() 打印目標的語句處分別設置斷點,再點擊 CMake Tools 的 Debug。
第一次暫停時,在變量窗口展開本地變量 goal,可以看到 (3.0, 2.0, 0.0);按 F5 繼續到偏移節點的斷點,同名本地變量的 x 座標已經變成 3.5。再繼續執行,終端會顯示完整結果。兩次斷點所在的函數不同,本地變量也分別屬於各自回調。


7.輸入錯誤要在讀取位置明確處理
本例常見的數據問題可以按發生位置區分:
| 修改 | 發生什麼 | 本例的處理 |
|---|---|---|
將 target 改為 {unknown_goal} |
找不到有效的輸入值 | getInput() 失敗,報告讀取錯誤 |
將固定座標改為 2.0;3.0 |
字段數量不符合約定 | 轉換函數報告 Pose2D requires x;y;yaw |
將距離改為 distance="far" |
文本無法轉換成 double |
讀取失敗,報告轉換錯誤 |
把 OffsetGoal 移到 CalculateGoal 前面 |
目標還沒有被寫入 | 讀取雙向端口失敗,後面的節點不再執行 |
這些 XML 對照都在編輯器中修改並保存,再點擊 Launch,在集成終端中查看錯誤信息。

工廠可以在創建樹時建立端口和條目的關係,但這不代表每個輸入已經有業務值。數據是否準備好,仍要看外部初始化與前面節點的執行結果。
外部讀取也要使用對應類型。這裏調用 blackboard->get<Pose2D>("mission_goal"),不能因為打印出來像文本,就改成讀取 std::string。直接訪問 Blackboard 的 get() 與節點的 getInput() 是不同接口,不應假定它會執行相同的文本轉換。
8.Blackboard 數據不會隨節點狀態自動清空
本例在 CalculateGoal 中產生目標,在 OffsetGoal 中更新目標;樹任務結束後,main() 仍能讀到最終值。
節點結束或被重置到 IDLE,不會自動刪除這些 Blackboard 條目。
這也意味着,上一次任務留下的值不能自動代表下一次任務已經準備好。 組織重複任務時,應明確哪些輸入由外部更新,哪些輸出由當前任務重新產生,以及失敗後是否允許繼續使用已有結果。
本例中的輸出節點每次成功執行都會重新寫入目標,再由後續節點讀取。後面使用子樹時,還需要明確不同 Blackboard 範圍之間的數據映射;共享一個鍵的寫法不能直接套用到所有子樹。
9.當前學習進度
本篇把輸入、輸出、雙向端口、自定義類型轉換以及外部 Blackboard 讀寫串成了完整工程。
下一篇學習持续任务与中止:用 StatefulActionNode 模擬導航,分別處理啓動、運行檢查和取消,讓任務能夠跨越多次 tick。