# การสร้าง Publisher และ Subscriber Publisher และ Subscriber เป็นกลไกสำคัญของ ROS 2 สำหรับการแลกเปลี่ยนข้อมูลระหว่างโหนด ในบทนี้จะเน้นสาธิตการสร้างไฟล์โหนด Publisher และ Subscriber และให้เห็นภาพหลักการสื่อสารผ่าน Topic ซึ่งเป็นการส่งข้อมูลแบบ message passing ระหว่าง Publisher และ Subscriber ผ่าน Topic ที่กำหนดไว้ ## การสร้าง Publisher เปิด VS Code แล้วเข้าไปในโฟลเดอร์ my_package ```{image} images/c8/8.1.1.png :width: 80% :align: center ``` **สร้างไฟล์ my_publisher.py ใน path ~/ros2_ws/src/my_package/my_package** ```{image} images/c8/8.1.2.png :width: 80% :align: center ``` **เขียนโค้ด Python ลงในไฟล์ my_publisher.py** ```{code-block} python 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() ``` ```{image} images/c8/8.1.3.png :width: 80% :align: center ``` **เปิดไฟล์ setup.py ชึ้นมา หลังจากนั้นให้ทำการเปิด Tab VS Code เพิ่มขึ้นมา 1 Tab** ```{image} images/c8/8.1.4.png :width: 80% :align: center ``` ```{image} images/c8/8.1.5.png :width: 80% :align: center ``` **แก้ไขไฟล์ setup.py เพิ่ม 'my_publisher = my_package.my_publisher:main'** ```{image} images/c8/8.1.6.png :width: 80% :align: center ``` ```{warning} อย่าลืมใส่เครื่องหมาย , (comma) ต่อท้ายแต่ละบรรทัด ในรายการ entry_points ของ setup.py เมื่อมีการเพิ่มโหนดใหม่ หากไม่มี , (comma) อาจเกิดข้อผิดพลาด syntax error `'my_node = my_package.my_node:main',` `'my_publisher = my_package.my_publisher:main'` ``` **คอมไพล์** ```{code-block} bash cd ~/ros2_ws colcon build ``` **ถ้าสำเร็จจะขึ้นแบบนี้** ```{image} images/c8/8.1.7.png :width: 80% :align: center ``` **รันโหนด my_publisher ใน my_package ด้วยคำสั่งนี้** ```{code-block} bash ros2 run my_package my_publisher ``` ```{image} images/c8/8.1.8.png :width: 80% :align: center ``` ## การสร้าง Subscriber ขั้นตอนการสร้าง Subscriber คล้ายคลึงกับข้อ 8.1 โดยมีขั้นตอนรวบรัดดังนี้ **เปิด VS Code แล้วเข้าไปในโฟลเดอร์ my_package** ```{image} images/c8/8.2.1.png :width: 80% :align: center ``` **สร้างไฟล์ my_subscriber.py ใน path ~/ros2_ws/src/my_package/my_package** ```{image} images/c8/8.2.2.png :width: 80% :align: center ``` **เขียนโค้ด Python ลงในไฟล์ my_subscriber.py** ```{code-block} python 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() ``` ```{image} images/c8/8.2.3.png :width: 80% :align: center ``` **แก้ไขไฟล์ setup.py เพิ่ม 'my_subscriber = my_package.my_subscriber:main'** ```{image} images/c8/8.2.4.png :width: 80% :align: center ``` **คอมไพล์** ```{code-block} bash cd ~/ros2_ws colcon build ``` **ถ้าสำเร็จจะขึ้นแบบนี้** ```{image} images/c8/8.1.7.png :width: 80% :align: center ``` **รันโหนด my_subscriber ของ my_package** ```{code-block} bash ros2 run my_package my_subscriber ``` **กรณีโหนด Publisher ยังไม่ทำงาน** จากภาพด้านล่างจะเห็นว่า ไม่มีข้อความแสดงผล ใน Terminal เนื่องจาก ยังไม่มีโหนด Publisher ทำงาน ทำให้ Subscriber ไม่ได้รับข้อมูลใด ๆ ผ่าน Topic ```{image} images/c8/8.2.5.png :width: 80% :align: center ``` **กรณีโหนด Publisher ทำงาน** จากภาพด้านล่างจะแสดงให้เห็นว่าเมื่อ โหนด Publisher ทำงาน โดยส่งข้อความ "Hello, ROS2!" ไปยัง Topic ที่กำหนดไว้ Subscriber ที่ทำงานอยู่ก็จะรับข้อความนั้นและแสดงผลใน Terminal ```{image} images/c8/8.2.6.png :width: 80% :align: center ``` ## การใช้งานเครื่องมือ RQT ในการตรวจสอบและทดสอบการสื่อสารแบบ Publisher–Subscriber เครื่องมือ RQT ซึ่งเป็น GUI ของ ROS2 ที่พัฒนาอยู่บนพื้นฐานของ Qt จึงเข้ามาช่วยให้ผู้พัฒนาและผู้เรียนสามารถ * มองเห็นภาพรวมของระบบการสื่อสาร * ตรวจสอบค่า topic แบบเรียลไทม์ * และทดลองส่งค่าข้อมูล Message ในหัวข้อนี้จะมุ่งเน้นการใช้งานเครื่องมือ 3 ตัวหลัก ได้แก่ a) RQT Node Graph, b) RQT Topic Monitor, c) RQT Message Publisher โดยคำสั่งการเปิดเครื่องมือ RQT และการใช้งาน ดังนี้ **เปิด Terminal ใหม่ รันคำสั่ง** ```{code-block} bash rqt_graph ``` **ปรากฎหน้าต่าง ดังภาพ** ```{image} images/c8/8.3.1.png :width: 80% :align: center ``` **a) RQT Node Graph — การแสดงแผนผังการเชื่อมต่อของ Node และ Topic** ```{image} images/c8/8.3.2.png :width: 80% :align: center ``` **ปรากฎหน้าต่าง แผนผังการเชื่อมต่อของ Node และ Topic ดังภาพ** ```{image} images/c8/8.3.3.png :width: 80% :align: center ``` ดังภาพจะเห็นได้ว่า * Node /simple_publisher อยู่ด้านซ้าย (Publisher) * Node /simple_subscriber อยู่ด้านขวา (Subscriber) * มีลูกศรเชื่อมโยงผ่าน Topic /chatter ตรงกลาง * แสดงให้เห็นถึงการส่งต่อข้อมูลจาก Publisher ไปยัง Subscriber โดยสมบูรณ์ **b) RQT Topic Monitor — การตรวจสอบข้อมูล Topic แบบเรียลไทม์** ```{image} images/c8/8.3.4.png :width: 80% :align: center ``` **คลิกเลือก Topic ที่ต้องการเพื่อดูค่า Message** ```{image} images/c8/8.3.5.png :width: 80% :align: center ``` ในตัวอย่างนี้ Topic /chatter มีค่า Message เป็น ‘Hello, ROS2!’ ซึ่งหมายความว่า Node Publisher กำลังส่งข้อความ “Hello, ROS2!” ออกไปอย่างต่อเนื่อง **c) RQT Message Publisher — การส่งข้อมูลไปยัง Node ผ่าน Topic** ```{image} images/c8/8.3.6.png :width: 80% :align: center ``` **คลิกเลือก Topic ที่ต้องการส่ง Message (ในภาพตัวอย่าง ส่งข้อมูลคำว่า ‘My_RQT’ ประเภทข้อมูลเป็น String ที่ความถี่ 1 Hz)** ```{image} images/c8/8.3.7.png :width: 80% :align: center ``` ผลลัพธ์เกิดขึ้นในหน้าต่าง Terminal ของ Node my_subscriber มีการส่งข้อมูล คำว่า ‘My_RQT’ แทรกเพิ่มมาจากเดิมเป็นการส่ง ข้อมูลจาก (Node my_publisher) คำว่า ‘Hello, ROS2!’ ```{image} images/c8/8.3.8.png :width: 80% :align: center ``` **สรุปได้ว่า** 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 ได้จริง ## แบบฝึกหัด: การใช้งาน 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_topic` * **Message Type:** `std_msgs/msg/Int64` * **Node 1:** ส่งค่าตัวเลขเพิ่มขึ้นทีละ 1 ทุก ๆ 0.5 วินาที * **Node 2:** แสดงค่าที่ได้รับทาง Terminal ```{only} latex **หมายเหตุ:** สำหรับเฉลย สามารถดูได้ที่เวอร์ชันออนไลน์ (Interactive Web) ``` ````{only} html :::{dropdown} **คลิกเพื่อดูเฉลย (Click to show solution)** :color: primary :icon: check Node 1: Publisher (ส่งค่าตัวเลขเพิ่มขึ้นทุก 0.5 วินาที)\ ไฟล์: number_publisher.py ```{code-block} python 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 ```{code-block} python 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 ที่ได้รับ ```{only} latex **หมายเหตุ:** สำหรับเฉลย สามารถดูได้ที่เวอร์ชันออนไลน์ (Interactive Web) ``` ````{only} html :::{dropdown} **คลิกเพื่อดูเฉลย (Click to show solution)** :color: primary :icon: check Node 1: Publisher (สลับค่า True/False)\ ไฟล์: bool_publisher.py ```{code-block} python 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 ```{code-block} python 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() ``` ::: ````