รูปปกบทความ

1. 🎯 ตอนที่ 12: พื้นฐานการเขียน Subscriber Node ด้วย Python - เงี่ยหูฟังโลกกว้างด้วย Callback Function

สวัสดีครับน้องๆ วิศวกรหุ่นยนต์ทุกคน! ในตอนที่แล้วเราได้สวมบทบาทเป็น “ดีเจสถานีวิทยุ” เขียนโค้ดสร้าง Publisher Node เพื่อกระจายเสียง (ข้อมูล) ออกไปบนคลื่น Topic กันแล้ว แต่สถานีวิทยุจะมีความหมายอะไรถ้าไม่มี “คนฟัง” จริงไหมครับ?

วันนี้พี่จะพาน้องๆ มาสร้างหูให้กับหุ่นยนต์ ด้วยการเขียน Subscriber Node ด้วยภาษา Python (ผ่านไลบรารี rclpy) เราจะมาเจาะลึกกันว่า หุ่นยนต์มันรู้ได้อย่างไรว่ามีข้อมูลพุ่งเข้ามาชนมัน โดยที่ CPU ไม่ต้องทำงานหนักจนไหม้ไปซะก่อน เตรียมตัวให้พร้อมครับ!

2. 📖 เปิดฉาก (The Hook)

สมัยที่เราหัดเขียนโปรแกรมใหม่ๆ ถ้าน้องอยากรู้ว่ามีคนส่งข้อมูลมาให้หรือยัง น้องอาจจะเขียนลูป while(true) เพื่อคอยเช็คค่าตัวแปรตลอดเวลาว่า “มายัง? มายัง? มายัง?” (Polling) ซึ่งการทำแบบนี้เป็นการผลาญทรัพยากรคอมพิวเตอร์อย่างสูญเปล่าสุดๆ ครับ! ลองจินตนาการว่าน้องมารอรับพัสดุ แล้วต้องเดินไปเปิดตู้จดหมายดูทุกๆ 1 วินาทีสิครับ เหนื่อยแย่เลย!

ในโลกของ ROS 2 เราจะทำงานแบบ “Event-driven” ครับ เราไม่ต้องไปคอยชะเง้อมองตู้จดหมาย แต่เราจะจ้าง “เลขาส่วนตัว” เอาไว้ ทันทีที่มีพัสดุ (Message) มาส่งที่ตู้จดหมาย (Topic) เลขาคนนี้จะเดินมาสะกิดเรา แล้วยื่นพัสดุให้เราแกะกล่องทันที! ซึ่งเลขาผู้แสนดีคนนี้ก็คือกลไกที่เรียกว่า “Callback Function” นั่นเองครับ นี่แหละคือพระเอกตัวจริงของการเขียน Subscriber!

3. 🧠 แก่นวิชา (Core Concepts)

โครงสร้างการเขียน Subscriber Node จะใช้การเขียนโปรแกรมเชิงวัตถุ (OOP) คล้ายกับ Publisher เลยครับ โดยมีหัวใจสำคัญอยู่ที่ 3 องค์ประกอบนี้:

  • create_subscription(): คำสั่งสำหรับสร้าง “ผู้ฟัง” โดยเราต้องระบุ 4 อย่างให้ชัดเจน ได้แก่ 1. ชนิดของข้อมูล (Message Type), 2. ชื่อคลื่นสถานีวิทยุ (Topic Name), 3. ชื่อเลขาที่จะรับเรื่อง (Callback Function), และ 4. ขนาดของกล่องรับฝาก (Queue Size)
  • listener_callback(self, msg): นี่คือฟังก์ชันเลขาของเราครับ! ทันทีที่มีข้อมูลวิ่งเข้ามาใน Topic ระบบ ROS 2 จะเรียกฟังก์ชันนี้ทำงานโดยอัตโนมัติ พร้อมกับแนบข้อมูลที่ได้รับมาในรูปแบบของตัวแปร msg ให้เราเอาไปดึงค่าต่อ (เช่น msg.data)
  • rclpy.spin(node): หากไม่มีคำสั่งนี้ Node ของเราจะรันจบแล้วปิดตัวไปเลยทันที! การเรียกใช้ spin() คือการสั่งให้โปรแกรม “หยุดรอและมีชีวิตอยู่ต่อไป” เพื่อคอยรับ Event ใหม่ๆ เมื่อมีข้อมูลเข้ามา มันก็จะส่งไปให้ Callback Function ทำงานสลับกันไปเรื่อยๆ จนกว่าเราจะกดปิดโปรแกรมนั่นเองครับ
รูปประกอบโครงสร้างโค้ด Subscriber

4. 💻 ร่ายมนต์โค้ดและคำสั่ง (Show me the Code)

เรามาดูหน้าตาของโค้ด Subscriber คู่บุญที่จะคอยรับข้อความ “Hello, ROS 2!” จากตอนที่แล้วกันครับ มาชำแหละกันทีละบรรทัดสไตล์วิศวกร!

ตัวอย่างโค้ด ROS 2 Subscriber (ด้วย rclpy):

#!/usr/bin/env python3
import rclpy
from rclpy.node import Node
from std_msgs.msg import String # ต้อง Import Message Type ให้ตรงกับฝั่ง Publisher เป๊ะๆ!

# 1. สร้างคลาสสืบทอดจาก Node ของ ROS 2
class MinimalSubscriber(Node):
    def __init__(self):
        super().__init__('minimal_subscriber') # ตั้งชื่อ Node
        
        # 2. สร้าง Subscriber (จูนคลื่นไปที่ 'topic', รับข้อมูลแบบ String, ฝากคิวไว้ 10)
        # และชี้เป้าหมายไปที่เลขาชื่อ self.listener_callback
        self.subscription = self.create_subscription(
            String,
            'topic',
            self.listener_callback,
            10)
        self.subscription  # บรรทัดนี้แค่ใส่ไว้กัน Python แจ้งเตือน Unused variable ครับ

    # 3. ฟังก์ชันเลขา (Callback Function)
    # ฟังก์ชันนี้จะถูกปลุกให้ตื่น ทุกครั้งที่มีข้อความวิ่งเข้ามาที่ 'topic'
    def listener_callback(self, msg: String):
        # ดึงข้อมูลจากฟิลด์ .data ของกล่องพัสดุ msg มาพ่นออกหน้าจอ
        self.get_logger().info(f'I heard: "{msg.data}"')

# 4. ฟังก์ชันหลักสำหรับรันโปรแกรม
def main(args=None):
    rclpy.init(args=args) # สตาร์ทเครื่องยนต์ ROS 2
    
    minimal_subscriber = MinimalSubscriber() # สร้างอ็อบเจกต์ Node
    
    # 5. สั่งให้ Node สแตนด์บายรอรับข้อมูลไปเรื่อยๆ (ห้ามลืมบรรทัดนี้เด็ดขาด!)
    rclpy.spin(minimal_subscriber)
    
    # หากโดนกด Ctrl+C ให้ออกมาทำลาย Node และปิดระบบ
    minimal_subscriber.destroy_node()
    rclpy.shutdown()

if __name__ == '__main__':
    main()

5. 🛡️ เคล็ดลับจากคัมภีร์ลับ (Under the Hood / Pro-Tips)

ในฐานะที่พี่ตรวจโค้ดของน้องๆ ในทีมมาเยอะ นี่คือจุดตายที่วิศวกรหน้าใหม่มักจะพลาดตอนเขียน Subscriber ครับ:

  • ระวังกับดัก Callback ค้าง (Blocking Callback): กฎเหล็กข้อแรกของการเขียน Callback Function คือ “ต้องทำงานให้เร็วที่สุด แล้วรีบจบฟังก์ชัน” ครับ! ถ้าน้องเอาลอจิกที่ใช้เวลาประมวลผลนานมากๆ (เช่น ประมวลผลภาพ AI หนักๆ หรือใส่คำสั่ง time.sleep()) เข้าไปยัดไว้ใน Callback ผลคือระบบจะค้าง (Block) ทำให้มันไม่สามารถออกไปรับข้อความใหม่ๆ ที่ Topic อื่นได้ หุ่นยนต์ของน้องเฟรมเรตจะตกและเดินชนกำแพงทันที! วิธีแก้คือให้ Callback ทำหน้าที่แค่รับข้อมูลมาเซฟใส่ตัวแปรไว้ แล้วให้ Thread หลักเป็นคนเอาไปคำนวณต่อครับ
  • วิชา Type Hinting ช่วยชีวิต: สังเกตบรรทัด def listener_callback(self, msg: String): ไหมครับ? การใส่ : String ไม่ได้บังคับใน Python แต่มันคือ Best Practice! มันเป็นการบอกโปรแกรม IDE (เช่น VS Code) ว่าตัวแปร msg ก้อนนี้คือคลาส String นะ พอเราพิมพ์ msg. ปุ๊บ โปรแกรมจะ Auto-complete แนะนำฟิลด์ .data ขึ้นมาให้เราเลือกทันที ช่วยลดบั๊กพิมพ์ชื่อตัวแปรผิดไปได้เยอะมากครับ!
  • คิวล้น (Queue Size Drop): ตัวเลข 10 ตอนสร้าง Subscription คือการจองคิวรับฝากข้อความ ถ้าฝั่งกล้องส่งภาพมาเร็วมากระดับ 60 fps แต่ฝั่ง AI น้องประมวลผลภาพได้แค่ 10 fps ข้อความที่ล้นคิวจะถูกระบบ Drop (โยนทิ้ง) อัตโนมัติ นี่คือพฤติกรรมปกติเพื่อป้องกันไม่ให้ RAM คอมพิวเตอร์เต็มจนระบบพังครับ!

6. 🏁 บทสรุป (To be continued…)

จบไปแล้วครับสำหรับการสร้าง “หูและปาก” ให้กับหุ่นยนต์! ตอนนี้น้องๆ สามารถสร้าง Node ที่ส่งข้อมูลหากันได้แล้ว (Publisher / Subscriber) ด้วยกลไกของ Callback Function ที่ทำงานแบบ Event-driven ทำให้ระบบของเราตอบสนองได้รวดเร็วและไม่กินสเปคคอมพิวเตอร์

แต่การสื่อสารผ่าน Topic มันเป็นแบบ “ส่งไปเรื่อยๆ โดยไม่สนใจคนรับ” ครับ ถ้าน้องอยากได้การสื่อสารแบบ “สั่งงานปุ๊บ รอรับผลลัพธ์ปั๊บ” เหมือนการเรียกฟังก์ชัน (Request-Response) เราจะต้องอัปเกรดไปใช้ระบบ Services ครับ! ในตอนหน้า พี่จะพาไปเขียน Service Server และ Client ด้วย Python กันแบบเจาะลึก เตรียมตัวให้พร้อมนะครับ!


ต้องการที่ปรึกษาด้านการออกแบบสถาปัตยกรรมหุ่นยนต์ (Robotics) และระบบ Automation ให้กับองค์กรของคุณ? ทีมงาน WP Solution พร้อมให้บริการออกแบบและพัฒนาซอฟต์แวร์แบบครบวงจร ดูรายละเอียดบริการของเราได้ที่: www.wpsolution2017.com หรือพูดคุยปรึกษาเบื้องต้นได้ที่ Line: wisit.p