6. Writing a publisher and subscriber

Your first nodes — one that sends and one that receives.

The publisher

Create ~/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')
        # the 10 is the queue depth — buffer 10 messages if the receiver lags
        self.pub = self.create_publisher(Twist, '/cmd_vel', 10)
        # every 0.1 s = 10 Hz
        self.timer = self.create_timer(0.1, self.tick)

    def tick(self):
        msg = Twist()
        msg.linear.x = 0.2      # forward at 0.2 m/s
        msg.angular.z = 0.0     # no rotation
        self.pub.publish(msg)


def main():
    rclpy.init()
    node = SpeedSender()
    try:
        rclpy.spin(node)        # loop until Ctrl+C
    except KeyboardInterrupt:
        pass
    finally:
        node.destroy_node()
        rclpy.shutdown()


if __name__ == '__main__':
    main()

Important

Do not write your own while True: with sleep() — use create_timer.

rclpy.spin() dispatches all of the node’s work: publishing, receiving, and timers. Your own loop blocks spin, so incoming messages are never processed. The symptom is a subscriber that stays silent while the code looks correct.

The subscriber

Create ~/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(
            'forward %.2f m/s · turn %.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

Use self.get_logger().info() rather than print().

You get a timestamp and the node name, the output appears on /rosout, and it is written to a log file automatically — so you can go back and look.

Register them — edit setup.py

Writing the file is not enough; it has to be registered.

In ~/ros2_ws/src/my_robot/setup.py, edit entry_points:

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

Read it as: command name = module:function.

Warning

🔴 This is the most-forgotten step in this topic. The code is written, the build succeeds, and running gives:

No executable found

The cause is a missing line in entry_points — and because the build succeeded, nothing told you.

Build and run

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

Two terminals, both sourced:

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

Terminal 2 prints every 0.1 s:

[INFO] [speed_watcher]: forward 0.20 m/s · turn 0.00 rad/s

Check it with the tools from topic 4

Without stopping anything, in a third terminal:

ros2 topic info -v /cmd_vel     # expect Publisher 1, Subscription 1
ros2 topic hz /cmd_vel          # expect about 10 Hz
ros2 topic echo /cmd_vel        # the same data speed_watcher sees

Note

Stop speed_sender and run ros2 topic info /cmd_vel again — Publisher count drops to 0 and speed_watcher simply goes quiet, with no error.

Remember what that looks like; on a real robot it looks exactly the same.

Using this on a real robot

speed_sender above works on a real AGV as-is, because /cmd_vel and Twist are the same standard.

Danger

Before running it on real hardware — this code commands forward motion the moment it starts and never stops.

  • Lift the wheels off the ground

  • Know where the emergency stop is

  • Start slower than this, say 0.05

And in a real system /cmd_vel should pass through a speed limiter and an obstacle-based stop, not go straight to the wheels.


Previous: 5. Workspaces · Next: 7. Launch files and parameters