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