Lifecycle Node:撰寫自己的生命週期節點
- ros2
- lifecycle-node
- system-design
- rclpy
問題定義
第 6.1 節介紹過 Nav2 的伺服器都是 Lifecycle Node,並展示了怎麼用 lifecycle_manager 管理既有的節點,但沒有深入「怎麼自己寫一個」。當你的系統裡有節點需要管理有代價的資源(連接硬體、載入模型、建立網路連線),且需要明確的「目前到底有沒有在真正運作」狀態,一般節點(第 2.1 節介紹的標準 Node)沒有內建這種機制——只要行程還在跑,它就會處理收到的請求,沒有「已建立但尚未啟用」這種中間狀態。Lifecycle Node 補上了這一塊。
核心概念說明
狀態機:四個主要狀態,加上過渡狀態
Lifecycle Node 的核心狀態機有四個主要(穩定)狀態:
Unconfigured → Inactive → Active
↑ │
└───────────┘
(deactivate)
- Unconfigured:節點剛建立,還沒配置任何資源。
- Inactive:資源已配置好(例如 publisher 已建立),但還沒真正對外生效(例如 timer 還沒啟動,不會真的發布資料)。
- Active:完全運作中。
- Finalized:終止狀態,節點生命週期結束,無法再恢復。
每個主要狀態之間的轉換,會呼叫節點裡對應命名的回呼函式:on_configure(Unconfigured → Inactive)、on_activate(Inactive → Active)、on_deactivate(Active → Inactive)、on_cleanup(Inactive → Unconfigured)、on_shutdown(任何狀態 → Finalized)。
為什麼要把「配置」跟「啟用」分成兩個獨立步驟
這是理解 Lifecycle Node 設計理念的關鍵。以一個需要連接感測器硬體的節點為例:on_configure 負責建立跟硬體的連線、驗證硬體回應正常,但不開始真正讀取資料發布出去;只有等到 on_activate 被呼叫,才啟動定時讀取、真正開始發布資料。這個分離讓系統可以先把所有節點都配置好(確認每個節點的資源都準備就緒、沒有連線失敗),再統一啟用整組系統,而不是每個節點各自獨立地「建立就立刻開始運作」,這正是第 6.1 節提到 lifecycle_manager 能有序管理一整組伺服器的基礎。
實作範例:一個管理「模擬硬體連線」的 Lifecycle Node
檔案位置:~/ros2_ws/src/my_package/my_package/sensor_lifecycle_node.py
import rclpy
from rclpy.lifecycle import Node as LifecycleNode
from rclpy.lifecycle import State, TransitionCallbackReturn
from std_msgs.msg import Float64
class SensorLifecycleNode(LifecycleNode):
def __init__(self):
super().__init__("sensor_lifecycle_node")
self.publisher_ = None
self.timer = None
self.hardware_connected = False
def on_configure(self, state: State) -> TransitionCallbackReturn:
self.get_logger().info("正在連接感測器硬體...")
try:
self._connect_to_hardware()
except ConnectionError as e:
self.get_logger().error(f"硬體連線失敗: {e}")
return TransitionCallbackReturn.FAILURE
self.publisher_ = self.create_lifecycle_publisher(
Float64, "sensor_reading", 10
)
self.get_logger().info("配置完成,等待啟用")
return TransitionCallbackReturn.SUCCESS
def on_activate(self, state: State) -> TransitionCallbackReturn:
self.get_logger().info("啟用中,開始發布感測器資料")
self.timer = self.create_timer(0.5, self.publish_reading)
return super().on_activate(state)
def on_deactivate(self, state: State) -> TransitionCallbackReturn:
self.get_logger().info("停用中,停止發布")
self.destroy_timer(self.timer)
self.timer = None
return super().on_deactivate(state)
def on_cleanup(self, state: State) -> TransitionCallbackReturn:
self.get_logger().info("清理資源,斷開硬體連線")
self._disconnect_hardware()
self.destroy_publisher(self.publisher_)
self.publisher_ = None
return TransitionCallbackReturn.SUCCESS
def on_shutdown(self, state: State) -> TransitionCallbackReturn:
self.get_logger().info("節點終止")
if self.hardware_connected:
self._disconnect_hardware()
return TransitionCallbackReturn.SUCCESS
def publish_reading(self):
msg = Float64()
msg.data = 23.5
self.publisher_.publish(msg)
def _connect_to_hardware(self):
self.hardware_connected = True
def _disconnect_hardware(self):
self.hardware_connected = False
def main():
rclpy.init()
node = SensorLifecycleNode()
try:
rclpy.spin(node)
finally:
node.destroy_node()
rclpy.shutdown()
if __name__ == "__main__":
main()
留意 on_activate/on_deactivate 最後呼叫了 super().on_activate(state)——基底類別的實作負責啟用/停用透過 create_lifecycle_publisher 建立的 publisher(讓它們真正開始/停止對外送出資料,即使程式邏輯上仍然呼叫了 publish()),這是使用 create_lifecycle_publisher 而不是一般 create_publisher 的意義:publisher 本身的「作用中」狀態,會跟著節點的生命週期狀態自動連動。
執行並觀察轉換
ros2 run my_package sensor_lifecycle_node
另開終端機手動觸發轉換:
ros2 lifecycle set /sensor_lifecycle_node configure
[INFO] [sensor_lifecycle_node]: 正在連接感測器硬體...
[INFO] [sensor_lifecycle_node]: 配置完成,等待啟用
Transitioning successful
ros2 lifecycle set /sensor_lifecycle_node activate
[INFO] [sensor_lifecycle_node]: 啟用中,開始發布感測器資料
Transitioning successful
這時候用 ros2 topic echo /sensor_reading 才會開始看到資料——on_configure 執行完成後,節點雖然已經有了 publisher,但實際上沒有 timer 在推動發布,資料是等到 on_activate 之後才真正開始流動。
常見錯誤與除錯技巧
錯誤一:on_configure 裡忘記處理硬體連線失敗的情況,導致節點狀態卡在不明確的地方
現象:硬體連線失敗,但 on_configure 仍然回傳 TransitionCallbackReturn.SUCCESS(例如忘記檢查連線是否真的成功),節點被錯誤地標記為 Inactive,之後 on_activate 執行時才真正發現硬體根本沒連上。
原因:轉換回呼的回傳值是系統判斷這次轉換是否成功的唯一依據,如果邏輯上明明失敗了卻回傳 SUCCESS,系統會信任這個回傳值,繼續讓後續流程建立在一個實際上無效的假設上。
排除方式:確保每個可能失敗的操作都有明確檢查並對應回傳 TransitionCallbackReturn.FAILURE(如範例程式碼所示),讓失敗盡早在正確的轉換階段被發現,而不是延後到後面的階段才爆發,讓除錯範圍不必要地擴大。
錯誤二:ros2 lifecycle set 觸發 activate,但完全沒有效果,也沒有錯誤訊息
原因:常見是節點目前根本不在 Inactive 狀態(例如還沒 configure 就直接嘗試 activate),這種不合法的轉換請求會被系統拒絕,但如果沒有仔細看終端機輸出,容易誤以為是程式邏輯的問題。
排除方式:呼應第 6.1 節提到的除錯順序,先用 ros2 lifecycle get 確認目前實際狀態,再用 ros2 lifecycle list 確認該狀態底下有哪些合法的轉換選項可用:
ros2 lifecycle get /sensor_lifecycle_node
unconfigured [1]
看到 unconfigured 就能立刻知道,要先 configure 才能 activate,不能跳過中間狀態。
小結
Lifecycle Node 把「資源配置」跟「真正啟用」拆成兩個獨立步驟,讓系統能有序地把一整組節點準備就緒後再統一啟用,這是第 6 章 Nav2 大量伺服器能被 lifecycle_manager 協調管理的底層機制。轉換回呼的回傳值是系統判斷成敗的唯一依據,務必讓每個可能失敗的操作明確反映在回傳值上。下一節要更深入 QoS 設定裡幾個較少用到、但在正式系統設計時很關鍵的進階選項:Deadline 與 Liveliness。
延伸閱讀
常見問題
- 一般節點什麼時候應該改寫成 Lifecycle Node?
- 當這個節點的初始化過程本身有風險或代價(例如要連接硬體、載入大型模型、跟外部服務建立連線),且你希望能明確控制『這個節點現在到底有沒有在真正運作』時,Lifecycle Node 就值得考慮。單純做資料轉換、沒有額外資源需要管理的節點,改成 Lifecycle Node 通常只是增加複雜度,不一定有實際好處——第 6 章 Nav2 的伺服器之所以全部採用 Lifecycle Node,是因為它們牽涉大量彼此依賴的資源初始化順序,這正是 Lifecycle Node 設計要解決的問題。
- on_configure 裡可以做的事,跟 __init__ 建構子有什麼不同?
- __init__ 建構子在節點物件被建立的當下就會執行,這時候整個系統可能還沒準備好接受這個節點開始運作;on_configure 則是明確被 lifecycle_manager 或使用者觸發才會執行,可以放心地在裡面做『準備好但還不對外生效』的初始化(例如建立 publisher,但還不開始送資料),等到 on_activate 才真正開始運作。這種分離讓一個節點可以被建立、但暫不生效,之後才決定何時真正啟用,比什麼都塞在建構子裡靈活很多。
- 轉換回呼裡拋出例外,系統會怎麼處理?
- 拋出未預期的例外,系統會把這次轉換視為失敗,節點通常會被轉移到 ErrorProcessing 這個過渡狀態,最終落到 Finalized(終止),需要重新建立節點才能恢復。這也是為什麼轉換回呼裡建議明確捕捉可預期的失敗情境、回傳對應的 TransitionCallbackReturn.FAILURE,而不是讓例外意外往外拋,兩者對系統造成的影響差異很大——前者是優雅的『這次沒成功,還可以再試』,後者是直接進入需要人工介入的終止狀態。