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。