跳至主要内容

Publisher/Subscriber

本頁會帶你在 Workspace 與 Package 中建立的 my_ros2_tutorial Package 內,完成一組最小但完整可執行的 Python Publisher 與 Subscriber。

完成後,你會擁有兩個透過 Topic 彼此通訊的 Node,而且整個範例會由你自己建立、Build 並執行。

概述​

Publisher Node(simple_publisher)會每秒在 /tutorial_chatter Topic 發布一筆 std_msgs/msg/String Message。

Subscriber Node(simple_subscriber)會訂閱同一個 Topic,並將收到的內容輸出到 Log。

這就是 ROS 2 核心概念 中介紹的 Topic 通訊模式,以最小可執行形式呈現。

程式碼中使用的是相對 Topic Name tutorial_chatter。當這兩個 Node 如本教學所示執行在 Root Namespace 時,ROS 2 會將它解析為 /tutorial_chatter。

為什麼重要​

許多 ROS 2 Node 都建立在這裡展示的基本模式之上:Node、Publisher 或 Subscription 等通訊 Interface、Callback,以及負責處理這些 Callback 的 Executor。

熟悉這個基本結構後,閱讀與撰寫更複雜的 ROS 2 Node 會容易許多。

前置條件​

  • 已完成 Workspace 與 Package:ROS 2 環境已 source,並已使用 --dependencies rclpy std_msgs 建立 my_ros2_tutorial Package。
  • 執行 ROS 2 的機器或 Container 中具備可用的文字編輯器。

核心概念​

  • Node:繼承自 rclpy.node.Node 的 Python Class。
  • Publisher:透過 self.create_publisher(MsgType, topic_name, qos) 建立;呼叫 .publish(msg) 傳送 Message。
  • Subscriber:透過 self.create_subscription(MsgType, topic_name, callback, qos) 建立;每當收到 Message 時,ROS 2 就會呼叫指定的 callback。
  • Timer:self.create_timer(period_sec, callback) 會以固定時間間隔呼叫 callback;本範例用它每秒發布一次 Message。
  • rclpy.spin(node):會持續執行並處理 Callback(例如 Timer、Subscription),直到 Node 被關閉,例如按下 Ctrl+C。
  • QoS depth(下方範例中的 10):使用預設 Keep Last History Policy 時,這個值表示 Queue 中最多保留多少筆 Message。 Publisher 與 Subscriber 的 QoS 設定不需要完全相同,但必須彼此相容,才能正常通訊。

實作步驟​

1. 建立 Publisher 檔案​

建立 ~/ros2_ws/src/my_ros2_tutorial/my_ros2_tutorial/simple_publisher.py:

import rclpy
from rclpy.node import Node
from std_msgs.msg import String


class SimplePublisher(Node):
def __init__(self):
super().__init__('simple_publisher')
self.publisher_ = self.create_publisher(String, 'tutorial_chatter', 10)
self.timer_period = 1.0 # seconds
self.timer = self.create_timer(self.timer_period, self.timer_callback)
self.count = 0

def timer_callback(self):
msg = String()
msg.data = f'Hello ROS 2: {self.count}'
self.publisher_.publish(msg)
self.get_logger().info(f'Publishing: "{msg.data}"')
self.count += 1


def main(args=None):
rclpy.init(args=args)
node = SimplePublisher()
try:
rclpy.spin(node)
except KeyboardInterrupt:
pass
finally:
node.destroy_node()
rclpy.shutdown()


if __name__ == '__main__':
main()

2. 建立 Subscriber 檔案​

建立 ~/ros2_ws/src/my_ros2_tutorial/my_ros2_tutorial/simple_subscriber.py:

import rclpy
from rclpy.node import Node
from std_msgs.msg import String


class SimpleSubscriber(Node):
def __init__(self):
super().__init__('simple_subscriber')
self.subscription = self.create_subscription(
String,
'tutorial_chatter',
self.listener_callback,
10)
self.subscription # prevent unused variable warning

def listener_callback(self, msg):
self.get_logger().info(f'Received: "{msg.data}"')


def main(args=None):
rclpy.init(args=args)
node = SimpleSubscriber()
try:
rclpy.spin(node)
except KeyboardInterrupt:
pass
finally:
node.destroy_node()
rclpy.shutdown()


if __name__ == '__main__':
main()

3. 在 setup.py 中註冊 Entry Point​

開啟 ~/ros2_ws/src/my_ros2_tutorial/setup.py,修改 entry_points Dictionary,使其如下所示(在 console_scripts 中加入這兩行):

entry_points={
'console_scripts': [
'simple_publisher = my_ros2_tutorial.simple_publisher:main',
'simple_subscriber = my_ros2_tutorial.simple_subscriber:main',
],
},

這些設定會讓:

ros2 run my_ros2_tutorial simple_publisher

能夠對應到上方程式中的 main() Function。

由於建立 Package 時已經將 rclpy 與 std_msgs 宣告為 Dependency,因此不需要再修改 package.xml。

4. Build 並 Source​

cd ~/ros2_ws
colcon build --packages-select my_ros2_tutorial --symlink-install
source install/setup.bash

5. 執行兩個 Node​

在一個已經 source 環境的 Terminal 中:

ros2 run my_ros2_tutorial simple_publisher

在第二個已經 source 環境的 Terminal 中:

ros2 run my_ros2_tutorial simple_subscriber

預期成果​

Publisher Terminal 每秒會輸出一行:

[INFO] [simple_publisher]: Publishing: "Hello ROS 2: 0"
[INFO] [simple_publisher]: Publishing: "Hello ROS 2: 1"
[INFO] [simple_publisher]: Publishing: "Hello ROS 2: 2"

Subscriber Terminal 則會針對每一筆收到的 Message 顯示對應內容:

[INFO] [simple_subscriber]: Received: "Hello ROS 2: 0"
[INFO] [simple_subscriber]: Received: "Hello ROS 2: 1"
[INFO] [simple_subscriber]: Received: "Hello ROS 2: 2"

你也可以在第三個已經 source 環境的 Terminal 中檢查這個 Topic。

查看 Message:

ros2 topic echo /tutorial_chatter

按下 Ctrl+C 停止後,再檢查 Publish Rate:

ros2 topic hz /tutorial_chatter

完成後,使用 Ctrl+C 停止兩個 Node。

常用指令​

# Confirm both nodes are visible in the graph
ros2 node list

# Inspect the topic that connects them
ros2 topic info /tutorial_chatter -v
ros2 interface show std_msgs/msg/String

# Visualize the connection
ros2 run rqt_graph rqt_graph

常見問題​

  • ros2 run my_ros2_tutorial simple_publisher 出現 "No executable found" — setup.py 中的 Entry Point 可能沒有正確加入,或修改後尚未重新 Build Workspace。重新確認 entry_points 區塊,再執行 colcon build --packages-select my_ros2_tutorial。
  • Subscriber 沒有任何輸出 — 確認 Publisher 仍在執行、兩個 Terminal 都 source 了相同的 Overlay,並使用相同的 ROS_DOMAIN_ID(參考 ROS 2 Domain ID)。同時使用 ros2 topic info /tutorial_chatter -v 確認是否同時存在 Publisher 與 Subscriber。
  • ModuleNotFoundError: No module named 'my_ros2_tutorial' — Build 後目前 Terminal 尚未 source Overlay。再次執行 source ~/ros2_ws/install/setup.bash。
  • 修改 .py 檔案後,執行中的 Node 沒有變化 — Workspace 可能不是使用 --symlink-install Build,或修改後沒有重新啟動 Node。先停止 Node(Ctrl+C),必要時重新 Build,再重新執行。
  • 一般 Discovery 問題(所有東西都看不到)— 參考 Troubleshooting 中的完整檢查流程。

重點整理​

  • 最基本的 ROS 2 Node 包含 Class、Publisher 或 Subscriber、Callback,以及 rclpy.spin()。
  • create_timer 用來驅動週期性 Publish;create_subscription 則會在收到 Message 時觸發對應 Callback。
  • setup.py 中的 Entry Point 會讓 ros2 run <package> <executable> 能找到正確的執行程式,因此內容必須與檔案名稱及 Function 完全對應。
  • 開發 Python Node 時使用 --symlink-install,可以避免每次修改 Python 原始碼後都重新 Build。

下一步​

接著前往 Service 與 Action,在相同的 Package 中加入 Request/Response 與長時間執行 Goal 的通訊方式;也可以前往 Parameter 與 Launch,將目前寫死的 Publish Rate 改成可設定的 Parameter。