8. การสร้าง Publisher และ Subscriber

Publisher และ Subscriber เป็นกลไกสำคัญของ ROS 2 สำหรับการแลกเปลี่ยนข้อมูลระหว่างโหนด ในบทนี้จะเน้นสาธิตการสร้างไฟล์โหนด Publisher และ Subscriber และให้เห็นภาพหลักการสื่อสารผ่าน Topic ซึ่งเป็นการส่งข้อมูลแบบ message passing ระหว่าง Publisher และ Subscriber ผ่าน Topic ที่กำหนดไว้

8.1. การสร้าง Publisher

เปิด VS Code แล้วเข้าไปในโฟลเดอร์ my_package

_images/8.1.1.png

สร้างไฟล์ my_publisher.py ใน path ~/ros2_ws/src/my_package/my_package

_images/8.1.2.png

เขียนโค้ด 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()
_images/8.1.3.png

เปิดไฟล์ setup.py ชึ้นมา หลังจากนั้นให้ทำการเปิด Tab VS Code เพิ่มขึ้นมา 1 Tab

_images/8.1.4.png _images/8.1.5.png

แก้ไขไฟล์ setup.py เพิ่ม 'my_publisher = my_package.my_publisher:main'

_images/8.1.6.png

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

ถ้าสำเร็จจะขึ้นแบบนี้

_images/8.1.7.png

รันโหนด my_publisher ใน my_package ด้วยคำสั่งนี้

ros2 run my_package my_publisher
_images/8.1.8.png

8.2. การสร้าง Subscriber

ขั้นตอนการสร้าง Subscriber คล้ายคลึงกับข้อ 8.1 โดยมีขั้นตอนรวบรัดดังนี้ เปิด VS Code แล้วเข้าไปในโฟลเดอร์ my_package

_images/8.2.1.png

สร้างไฟล์ my_subscriber.py ใน path ~/ros2_ws/src/my_package/my_package

_images/8.2.2.png

เขียนโค้ด 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()
_images/8.2.3.png

แก้ไขไฟล์ setup.py เพิ่ม 'my_subscriber = my_package.my_subscriber:main'

_images/8.2.4.png

คอมไพล์

cd ~/ros2_ws
colcon build

ถ้าสำเร็จจะขึ้นแบบนี้

_images/8.1.7.png

รันโหนด my_subscriber ของ my_package

ros2 run my_package my_subscriber

กรณีโหนด Publisher ยังไม่ทำงาน

จากภาพด้านล่างจะเห็นว่า ไม่มีข้อความแสดงผล ใน Terminal เนื่องจาก ยังไม่มีโหนด Publisher ทำงาน ทำให้ Subscriber ไม่ได้รับข้อมูลใด ๆ ผ่าน Topic

_images/8.2.5.png

กรณีโหนด Publisher ทำงาน

จากภาพด้านล่างจะแสดงให้เห็นว่าเมื่อ โหนด Publisher ทำงาน โดยส่งข้อความ "Hello, ROS2!" ไปยัง Topic ที่กำหนดไว้ Subscriber ที่ทำงานอยู่ก็จะรับข้อความนั้นและแสดงผลใน Terminal

_images/8.2.6.png

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

ปรากฎหน้าต่าง ดังภาพ

_images/8.3.1.png

a) RQT Node Graph — การแสดงแผนผังการเชื่อมต่อของ Node และ Topic

_images/8.3.2.png

ปรากฎหน้าต่าง แผนผังการเชื่อมต่อของ Node และ Topic ดังภาพ

_images/8.3.3.png

ดังภาพจะเห็นได้ว่า

  • Node /simple_publisher อยู่ด้านซ้าย (Publisher)

  • Node /simple_subscriber อยู่ด้านขวา (Subscriber)

  • มีลูกศรเชื่อมโยงผ่าน Topic /chatter ตรงกลาง

  • แสดงให้เห็นถึงการส่งต่อข้อมูลจาก Publisher ไปยัง Subscriber โดยสมบูรณ์

b) RQT Topic Monitor — การตรวจสอบข้อมูล Topic แบบเรียลไทม์

_images/8.3.4.png

คลิกเลือก Topic ที่ต้องการเพื่อดูค่า Message

_images/8.3.5.png

ในตัวอย่างนี้ Topic /chatter มีค่า Message เป็น ‘Hello, ROS2!’ ซึ่งหมายความว่า Node Publisher กำลังส่งข้อความ “Hello, ROS2!” ออกไปอย่างต่อเนื่อง

c) RQT Message Publisher — การส่งข้อมูลไปยัง Node ผ่าน Topic

_images/8.3.6.png

คลิกเลือก Topic ที่ต้องการส่ง Message (ในภาพตัวอย่าง ส่งข้อมูลคำว่า ‘My_RQT’ ประเภทข้อมูลเป็น String ที่ความถี่ 1 Hz)

_images/8.3.7.png

ผลลัพธ์เกิดขึ้นในหน้าต่าง Terminal ของ Node my_subscriber มีการส่งข้อมูล คำว่า ‘My_RQT’ แทรกเพิ่มมาจากเดิมเป็นการส่ง ข้อมูลจาก (Node my_publisher) คำว่า ‘Hello, ROS2!’

_images/8.3.8.png

สรุปได้ว่า

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_topic

  • Message Type: std_msgs/msg/Int64

  • Node 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()