8. การสร้าง Publisher และ Subscriber
Publisher และ Subscriber เป็นกลไกสำคัญของ ROS 2 สำหรับการแลกเปลี่ยนข้อมูลระหว่างโหนด ในบทนี้จะเน้นสาธิตการสร้างไฟล์โหนด Publisher และ Subscriber และให้เห็นภาพหลักการสื่อสารผ่าน Topic ซึ่งเป็นการส่งข้อมูลแบบ message passing ระหว่าง Publisher และ Subscriber ผ่าน Topic ที่กำหนดไว้
8.1. การสร้าง Publisher
เปิด VS Code แล้วเข้าไปในโฟลเดอร์ my_package
สร้างไฟล์ my_publisher.py ใน path ~/ros2_ws/src/my_package/my_package
เขียนโค้ด Python ลงในไฟล์ my_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, 'chatter', 10)
self.timer = self.create_timer(1.0, self.publish_message)
def publish_message(self):
msg = String()
msg.data = 'Hello, ROS2!'
self.publisher_.publish(msg)
self.get_logger().info(f'Publishing: {msg.data}')
def main():
rclpy.init()
node = SimplePublisher()
rclpy.spin(node)
node.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
เปิดไฟล์ setup.py ชึ้นมา หลังจากนั้นให้ทำการเปิด Tab VS Code เพิ่มขึ้นมา 1 Tab
แก้ไขไฟล์ setup.py เพิ่ม 'my_publisher = my_package.my_publisher:main'
Warning
อย่าลืมใส่เครื่องหมาย , (comma) ต่อท้ายแต่ละบรรทัด ในรายการ entry_points ของ setup.py เมื่อมีการเพิ่มโหนดใหม่ หากไม่มี , (comma) อาจเกิดข้อผิดพลาด syntax error
'my_node = my_package.my_node:main',
'my_publisher = my_package.my_publisher:main'
คอมไพล์
cd ~/ros2_ws
colcon build
ถ้าสำเร็จจะขึ้นแบบนี้
รันโหนด my_publisher ใน my_package ด้วยคำสั่งนี้
ros2 run my_package my_publisher
8.2. การสร้าง Subscriber
ขั้นตอนการสร้าง Subscriber คล้ายคลึงกับข้อ 8.1 โดยมีขั้นตอนรวบรัดดังนี้ เปิด VS Code แล้วเข้าไปในโฟลเดอร์ my_package
สร้างไฟล์ my_subscriber.py ใน path ~/ros2_ws/src/my_package/my_package
เขียนโค้ด Python ลงในไฟล์ my_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, # ชนิดของข้อความที่รับ (String)
'chatter', # ชื่อ Topic ที่รับข้อมูลจาก Publisher
self.listener_callback, # ฟังก์ชัน Callback ที่จะถูกเรียกเมื่อมีข้อความส่งเข้ามา
10) # Queue size
self.subscription # ป้องกันไม่ให้ Garbage Collector ลบ Subscription นี้
def listener_callback(self, msg):
self.get_logger().info(f'Received: {msg.data}') # แสดงข้อความที่รับทาง Terminal
def main():
rclpy.init()
node = SimpleSubscriber()
rclpy.spin(node) # ให้ Node ทำงานแบบต่อเนื่องเพื่อรอรับข้อความ
node.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
แก้ไขไฟล์ setup.py เพิ่ม 'my_subscriber = my_package.my_subscriber:main'
คอมไพล์
cd ~/ros2_ws
colcon build
ถ้าสำเร็จจะขึ้นแบบนี้
รันโหนด my_subscriber ของ my_package
ros2 run my_package my_subscriber
กรณีโหนด Publisher ยังไม่ทำงาน
จากภาพด้านล่างจะเห็นว่า ไม่มีข้อความแสดงผล ใน Terminal เนื่องจาก ยังไม่มีโหนด Publisher ทำงาน ทำให้ Subscriber ไม่ได้รับข้อมูลใด ๆ ผ่าน Topic
กรณีโหนด Publisher ทำงาน
จากภาพด้านล่างจะแสดงให้เห็นว่าเมื่อ โหนด Publisher ทำงาน โดยส่งข้อความ "Hello, ROS2!" ไปยัง Topic ที่กำหนดไว้ Subscriber ที่ทำงานอยู่ก็จะรับข้อความนั้นและแสดงผลใน Terminal
8.3. การใช้งานเครื่องมือ RQT ในการตรวจสอบและทดสอบการสื่อสารแบบ Publisher–Subscriber
เครื่องมือ RQT ซึ่งเป็น GUI ของ ROS2 ที่พัฒนาอยู่บนพื้นฐานของ Qt จึงเข้ามาช่วยให้ผู้พัฒนาและผู้เรียนสามารถ
มองเห็นภาพรวมของระบบการสื่อสาร
ตรวจสอบค่า topic แบบเรียลไทม์
และทดลองส่งค่าข้อมูล Message
ในหัวข้อนี้จะมุ่งเน้นการใช้งานเครื่องมือ 3 ตัวหลัก ได้แก่ a) RQT Node Graph, b) RQT Topic Monitor, c) RQT Message Publisher โดยคำสั่งการเปิดเครื่องมือ RQT และการใช้งาน ดังนี้
เปิด Terminal ใหม่ รันคำสั่ง
rqt_graph
ปรากฎหน้าต่าง ดังภาพ
a) RQT Node Graph — การแสดงแผนผังการเชื่อมต่อของ Node และ Topic
ปรากฎหน้าต่าง แผนผังการเชื่อมต่อของ Node และ Topic ดังภาพ
ดังภาพจะเห็นได้ว่า
Node /simple_publisher อยู่ด้านซ้าย (Publisher)
Node /simple_subscriber อยู่ด้านขวา (Subscriber)
มีลูกศรเชื่อมโยงผ่าน Topic /chatter ตรงกลาง
แสดงให้เห็นถึงการส่งต่อข้อมูลจาก Publisher ไปยัง Subscriber โดยสมบูรณ์
b) RQT Topic Monitor — การตรวจสอบข้อมูล Topic แบบเรียลไทม์
คลิกเลือก Topic ที่ต้องการเพื่อดูค่า Message
ในตัวอย่างนี้ Topic /chatter มีค่า Message เป็น ‘Hello, ROS2!’ ซึ่งหมายความว่า Node Publisher กำลังส่งข้อความ “Hello, ROS2!” ออกไปอย่างต่อเนื่อง
c) RQT Message Publisher — การส่งข้อมูลไปยัง Node ผ่าน Topic
คลิกเลือก Topic ที่ต้องการส่ง Message (ในภาพตัวอย่าง ส่งข้อมูลคำว่า ‘My_RQT’ ประเภทข้อมูลเป็น String ที่ความถี่ 1 Hz)
ผลลัพธ์เกิดขึ้นในหน้าต่าง Terminal ของ Node my_subscriber มีการส่งข้อมูล คำว่า ‘My_RQT’ แทรกเพิ่มมาจากเดิมเป็นการส่ง ข้อมูลจาก (Node my_publisher) คำว่า ‘Hello, ROS2!’
สรุปได้ว่า
RQT Message Publisher เพื่อส่งข้อมูลไปยัง Topic /chatter ซึ่งเป็น Topic เดียวกับที่ Node my_subscriber ทำการรับข้อมูลอยู่ จากเดิม Node my_publisher จะส่งข้อความคำว่า “Hello, ROS2!” ออกมาเป็นระยะ ๆ แต่เมื่อมีการเปิดใช้งาน RQT Message Publisher และกำหนดค่าในช่อง expression เป็น My_RQT จะเห็นได้ว่าในหน้าต่าง Terminal ของ Node my_subscriber มีการรับข้อความใหม่เพิ่มเข้ามาว่า“Received: My_RQT” ซึ่งแสดงให้เห็นว่า RQT สามารถทำหน้าที่เป็น Publisher ได้จริง
8.4. แบบฝึกหัด: การใช้งาน std_msgs กับ Node Publisher / Subscriber
การใช้งาน std_msgs กับ Node Publisher / Subscriber ใน ROS2 มีแพ็กเกจพื้นฐานชื่อ std_msgs ซึ่งรวบรวมชนิดของข้อความมาตรฐาน (Standard Message Types) ที่ใช้กันทั่วไป เช่น String, Int64, Float32, Bool, Header, และอื่น ๆ
สามารถศึกษาเพิ่มเติมได้จากเอกสารทางการของ ROS 2 Humble: https://docs.ros.org/en/jazzy/p/std_msgs/
โจทย์ที่ 1 — Node ส่งค่าตัวเลข (Int64)
สร้าง Node ส่งข้อมูลประเภทตัวเลขจำนวนเต็ม (std_msgs/msg/Int64) จาก Publisher ไปยัง Subscriber
รายละเอียด:
Topic:
/number_topicMessage Type:
std_msgs/msg/Int64Node 1: ส่งค่าตัวเลขเพิ่มขึ้นทีละ 1 ทุก ๆ 0.5 วินาที
Node 2: แสดงค่าที่ได้รับทาง Terminal
คลิกเพื่อดูเฉลย (Click to show solution)
Node 1: Publisher (ส่งค่าตัวเลขเพิ่มขึ้นทุก 0.5 วินาที)
ไฟล์: number_publisher.py
import rclpy
from rclpy.node import Node
from std_msgs.msg import Int64
import time
class NumberPublisher(Node):
def __init__(self):
super().__init__('number_publisher')
self.publisher_ = self.create_publisher(Int64, '/number_topic', 10)
self.timer = self.create_timer(0.5, self.publish_number)
self.count = 0
self.get_logger().info('Number Publisher Started')
def publish_number(self):
msg = Int64()
msg.data = self.count
self.publisher_.publish(msg)
self.get_logger().info(f'Publishing: {msg.data}')
self.count += 1
def main():
rclpy.init()
node = NumberPublisher()
rclpy.spin(node)
node.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
Node 2: Subscriber (แสดงค่าที่ได้รับใน Terminal)
ไฟล์: number_subscriber.py
import rclpy
from rclpy.node import Node
from std_msgs.msg import Int64
class NumberSubscriber(Node):
def __init__(self):
super().__init__('number_subscriber')
self.subscription = self.create_subscription(
Int64, '/number_topic', self.listener_callback, 10)
self.get_logger().info('Number Subscriber Started')
def listener_callback(self, msg):
self.get_logger().info(f'Received: {msg.data}')
def main():
rclpy.init()
node = NumberSubscriber()
rclpy.spin(node)
node.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
โจทย์ที่ 2 — Node ส่งค่าตรรกะ (Bool)
สร้าง Node Publisher/Subscriber สำหรับส่งค่าตรรกะ (True / False) โดยใช้ข้อความชนิด std_msgs/msg/Bool
รายละเอียด:
Topic: /bool_status
Message Type: std_msgs/msg/Bool
Node 1: ส่งค่าตรรกะสลับกันทุก 2 วินาที (True / False)
Node 2: แสดงข้อความตามค่า Bool ที่ได้รับ
คลิกเพื่อดูเฉลย (Click to show solution)
Node 1: Publisher (สลับค่า True/False)
ไฟล์: bool_publisher.py
import rclpy
from rclpy.node import Node
from std_msgs.msg import Bool
class BoolPublisher(Node):
def __init__(self):
super().__init__('bool_publisher')
self.publisher_ = self.create_publisher(Bool, '/bool_status', 10)
self.timer = self.create_timer(2.0, self.publish_bool)
self.state = False
self.get_logger().info('Bool Publisher Started')
def publish_bool(self):
self.state = not self.state # สลับค่า True/False
msg = Bool()
msg.data = self.state
self.publisher_.publish(msg)
self.get_logger().info(f'Publishing: {msg.data}')
def main():
rclpy.init()
node = BoolPublisher()
rclpy.spin(node)
node.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
Node 2: Subscriber (แสดงค่าที่ได้รับ)
ไฟล์: bool_publisher.py
import rclpy
from rclpy.node import Node
from std_msgs.msg import Bool
class BoolSubscriber(Node):
def __init__(self):
super().__init__('bool_subscriber')
self.subscription = self.create_subscription(
Bool, '/bool_status', self.listener_callback, 10)
self.get_logger().info('Bool Subscriber Started')
def listener_callback(self, msg):
self.get_logger().info(f'Received: {msg.data}')
def main():
rclpy.init()
node = BoolSubscriber()
rclpy.spin(node)
node.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()