6. เขียน publisher กับ subscriber

หัวข้อนี้เขียน node แรกที่เป็นของเราเอง — ตัวส่งกับตัวรับ

ตัวส่ง (publisher)

สร้างไฟล์ ~/ros2_ws/src/my_robot/my_robot/speed_sender.py

import rclpy
from rclpy.node import Node
from geometry_msgs.msg import Twist


class SpeedSender(Node):
    def __init__(self):
        super().__init__('speed_sender')
        # ตัวเลข 10 คือ queue — เก็บได้ 10 ข้อความถ้าตัวรับตามไม่ทัน
        self.pub = self.create_publisher(Twist, '/cmd_vel', 10)
        # ส่งทุก 0.1 วินาที = 10 Hz
        self.timer = self.create_timer(0.1, self.tick)

    def tick(self):
        msg = Twist()
        msg.linear.x = 0.2      # เดินหน้า 0.2 เมตร/วินาที
        msg.angular.z = 0.0     # ไม่หมุน
        self.pub.publish(msg)


def main():
    rclpy.init()
    node = SpeedSender()
    try:
        rclpy.spin(node)        # วนรอจนกว่าจะกด Ctrl+C
    except KeyboardInterrupt:
        pass
    finally:
        node.destroy_node()
        rclpy.shutdown()


if __name__ == '__main__':
    main()

Important

อย่าเขียน while True: แล้ว sleep() เอง — ให้ใช้ create_timer

rclpy.spin() เป็นตัวจัดคิวงานทั้งหมดของ node ทั้งการส่ง การรับ และ timer ถ้าเขียนลูปเองจะไปบล็อก spin ทำให้ข้อความที่เข้ามาไม่ถูกประมวลผล อาการคือ subscriber เงียบสนิททั้งที่โค้ดถูก

ตัวรับ (subscriber)

สร้างไฟล์ ~/ros2_ws/src/my_robot/my_robot/speed_watcher.py

import rclpy
from rclpy.node import Node
from geometry_msgs.msg import Twist


class SpeedWatcher(Node):
    def __init__(self):
        super().__init__('speed_watcher')
        self.sub = self.create_subscription(
            Twist, '/cmd_vel', self.on_msg, 10)

    def on_msg(self, msg):
        self.get_logger().info(
            'เดินหน้า %.2f m/s · หมุน %.2f rad/s'
            % (msg.linear.x, msg.angular.z))


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


if __name__ == '__main__':
    main()

Tip

ใช้ self.get_logger().info() แทน print()

ข้อความจะมีเวลาและชื่อ node กำกับ ไปโผล่ใน ros2 topic echo /rosout ด้วย และถูกบันทึกลงไฟล์ log อัตโนมัติ — ตามหาย้อนหลังได้

บอกให้ระบบรู้จัก — แก้ setup.py

เขียนไฟล์แล้วยังเรียกไม่ได้ ต้องลงทะเบียนก่อน

เปิด ~/ros2_ws/src/my_robot/setup.py แล้วแก้ส่วน entry_points

    entry_points={
        'console_scripts': [
            'speed_sender = my_robot.speed_sender:main',
            'speed_watcher = my_robot.speed_watcher:main',
        ],
    },

อ่านว่า: ชื่อคำสั่ง = โมดูล:ฟังก์ชัน

Warning

🔴 ขั้นนี้ลืมกันบ่อยที่สุดในบท เขียนโค้ดเสร็จ build ผ่าน แต่รันแล้วได้

No executable found

สาเหตุคือไม่ได้เพิ่มบรรทัดใน entry_points — และเพราะ build ผ่าน จึงไม่มีอะไรฟ้องว่าลืม

build แล้วลอง

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

เปิด 2 terminal (source ทั้งคู่)

ros2 run my_robot speed_sender     # terminal 1
ros2 run my_robot speed_watcher    # terminal 2

terminal 2 จะพิมพ์ทุก 0.1 วินาที

[INFO] [speed_watcher]: เดินหน้า 0.20 m/s · หมุน 0.00 rad/s

ตรวจด้วยเครื่องมือจากหัวข้อ 4

โดยไม่ต้องหยุดโปรแกรม เปิด terminal ที่ 3

ros2 topic info -v /cmd_vel     # ควรเห็น Publisher 1 · Subscription 1
ros2 topic hz /cmd_vel          # ควรได้ราว 10 Hz
ros2 topic echo /cmd_vel        # เห็นข้อมูลเหมือนที่ speed_watcher เห็น

Note

ลองปิด speed_sender แล้วดู ros2 topic info /cmd_vel อีกครั้ง — Publisher count จะกลายเป็น 0 ส่วน speed_watcher จะเงียบไปเฉย ๆ โดยไม่มี error

จำอาการนี้ไว้ให้ดี เพราะกับหุ่นจริงมันหน้าตาเหมือนกันเป๊ะ

เอาไปใช้กับหุ่นจริง

โค้ด speed_sender ข้างบนใช้กับหุ่น AGV จริงได้ทันที เพราะ /cmd_vel กับ Twist คือมาตรฐานเดียวกัน

Danger

ก่อนรันกับหุ่นจริง — โค้ดนี้สั่งเดินหน้าทันทีที่เปิดและไม่มีตัวหยุด

  • ยกล้อลอยพื้นก่อน

  • รู้ว่าปุ่มหยุดฉุกเฉินอยู่ตรงไหน

  • เริ่มที่ความเร็วต่ำกว่านี้ เช่น 0.05

และในระบบจริง /cmd_vel ควรผ่านตัวจำกัดความเร็วกับตัวหยุดเมื่อเจอสิ่งกีดขวางก่อน ไม่ใช่ต่อตรงเข้าล้อ


ก่อนหน้า: 5. workspace · ต่อไป: 7. launch file กับ parameter