Writing ROS2 Nodes

Week 12 • CMPSC 304 Robotic Agents

Press Space or to advance

Course Progression

SLAM
Build maps
Nav2
Click goals
Today
Write nodes
P4 Part 3
Nav goals
Today's plan: Learn to publish velocity commands directly (topics & publishers), then learn to send navigation goals programmatically (actions). Activity 8 practices direct velocity control; P4 Part 3 uses goal-based navigation.

Why Write Your Own Nodes?

Using ROS2 Tools

  • CLI: ros2 topic pub
  • RViz: click goals one at a time
  • Requires a human at the screen
  • No conditional logic
  • Great for testing & exploration

Writing ROS2 Code

  • Publish commands on a timer
  • Send multiple waypoints in sequence
  • Fully autonomous — no human needed
  • React to sensor data or events
  • Required for real robots
Velocity commands (Activity 8) → goal-based navigation (P4 Part 3).

Anatomy of a ROS2 Python Node

import rclpy
from rclpy.node import Node

class MyNode(Node):
    def __init__(self):
        super().__init__('my_node_name')
        self.get_logger().info('Node started!')

def main(args=None):
    rclpy.init(args=args)        # Initialize ROS2
    node = MyNode()               # Create the node
    rclpy.spin(node)              # Keep it running
    rclpy.shutdown()              # Clean up
  • rclpy — the ROS2 Python client library
  • Node — base class that gives you publishers, subscribers, timers, etc.
  • spin() — event loop that processes callbacks (like Arduino's loop())

Communication: Topics Review

Topics are one-way data streams: publishers send, subscribers receive. No response expected.
# Publishing velocity commands
from geometry_msgs.msg import Twist

self.publisher = self.create_publisher(Twist, '/turtle1/cmd_vel', 10)

msg = Twist()
msg.linear.x = 1.0   # Forward at 1.0 m/s
msg.angular.z = 0.0   # No rotation
self.publisher.publish(msg)
Subscribing is the mirror image: self.create_subscription(LaserScan, '/scan', callback, 10) — your callback fires whenever new data arrives.

Your First Node: A Velocity Publisher

Combine a publisher and a timer to send velocity commands repeatedly.
class DriveForward(Node):
    def __init__(self):
        super().__init__('drive_forward')
        self.pub = self.create_publisher(
            Twist, '/turtle1/cmd_vel', 10)
        self.timer = self.create_timer(0.1, self.on_timer)
        self.state = 'forward'  # simple state machine

    def on_timer(self):
        msg = Twist()
        if self.state == 'forward':
            msg.linear.x = 1.0   # drive forward
        elif self.state == 'turn':
            msg.angular.z = 1.0  # rotate in place
        self.pub.publish(msg)
Activity 8 uses this pattern — you'll run a node like this with TurtleSim and watch the turtle draw patterns on screen.

From Direct Control to Smart Navigation

Activity 8: Direct Control

  • Publish Twist to /cmd_vel
  • "Go forward 1 m/s, turn 1 rad/s"
  • No map, no sensors, no obstacles
  • Robot hits walls and stops
  • ROS2 pattern: topic (publisher)

P4 Part 3: Goal-Based Nav

  • Send NavigateToPose goal
  • "Go to position (2.0, −1.0)"
  • Uses map, costmaps, path planning
  • Nav2 avoids obstacles & recovers
  • ROS2 pattern: action (client)
The upgrade: Instead of telling the robot how to move, you tell it where to go. Nav2 figures out the rest. This requires a new ROS2 pattern: actions.

Communication: Actions

An action is for long-running tasks with feedback. The client sends a goal; the server executes it and reports progress.
Your
Node
Goal
Feedback + Result
Nav2
Server
PartDirectionExample
GoalClient → Server"Navigate to (2, 3)"
FeedbackServer → Client"At (1.5, 2.1), ETA 5s"
ResultServer → Client"Reached goal" / "Failed"
Compare to topics: with direct velocity control, you publish and get nothing back. Actions give you feedback and a result.

The NavigateToPose Action

Nav2 exposes a NavigateToPose action — the same thing RViz uses when you click a goal.
from rclpy.action import ActionClient
from nav2_msgs.action import NavigateToPose
from geometry_msgs.msg import PoseStamped

self._client = ActionClient(self, NavigateToPose, 'navigate_to_pose')

goal = NavigateToPose.Goal()
goal.pose = PoseStamped()
goal.pose.header.frame_id = 'map'
goal.pose.pose.position.x = 2.0
goal.pose.pose.position.y = -1.0

self._client.wait_for_server()
future = self._client.send_goal_async(goal)

Handling the Response (Callbacks)

Actions are asynchronous — you send the goal and get callbacks when things happen.
def goal_response_callback(self, future):
    goal_handle = future.result()
    if not goal_handle.accepted:
        self.get_logger().warn('Goal rejected!')
        return
    self.get_logger().info('Goal accepted!')
    result_future = goal_handle.get_result_async()
    result_future.add_done_callback(self.result_callback)

def result_callback(self, future):
    self.get_logger().info('Goal reached!')
    # Send the next waypoint here...
Key pattern: Send goal → get acceptance → wait for result → send next goal.

Pattern: Sequential Waypoints

class WaypointNavigator(Node):
    def __init__(self):
        super().__init__('waypoint_navigator')
        self._client = ActionClient(
            self, NavigateToPose, 'navigate_to_pose')
        self.waypoints = [
            (0.5, -1.0, 0.0),    # (x, y, yaw)
            (2.0, -1.0, 1.57),
            (2.0,  0.5, 3.14),
            (0.0,  0.0, 0.0),    # Back to start
        ]
        self.current = 0

    def result_callback(self, future):
        self.current += 1
        if self.current < len(self.waypoints):
            self.send_next_goal()
        else:
            self.get_logger().info('Route complete!')
This is the pattern for P4 Part 3. Full starter code is in the project README.

Specifying Orientation

Goal poses include orientation as a quaternion. For 2D robots, only yaw matters.
import math
yaw = 1.57  # radians (90 degrees)
orientation_z = math.sin(yaw / 2.0)
orientation_w = math.cos(yaw / 2.0)
goal.pose.pose.orientation.z = orientation_z
goal.pose.pose.orientation.w = orientation_w
Yaw (rad)DirectionDegrees
0.0Facing +x (right)
1.57Facing +y (up)90°
3.14Facing -x (left)180°
-1.57Facing -y (down)-90°

How to Find Coordinates

Where should your waypoints be? Use RViz to find coordinates on your map.
  • Hover method: Hover over the map in RViz — coordinates appear at the bottom
  • Topic method: Click a "Nav2 Goal" and check the terminal output
  • Echo method: ros2 topic echo /goal_pose to see goals as they're sent
Coordinate frame: All positions are in the map frame. Origin (0, 0) is defined in your map.yaml.

Creating a ROS2 Python Package

# Create workspace and package
mkdir -p ~/ros2_ws/src && cd ~/ros2_ws/src
ros2 pkg create --build-type ament_python waypoint_nav

# Structure: waypoint_nav/
# ├── package.xml, setup.py, setup.cfg
# └── waypoint_nav/
#     ├── __init__.py
#     └── navigate_waypoints.py  ← your node
# Register in setup.py → entry_points:
'console_scripts': [
    'navigate_waypoints = waypoint_nav.navigate_waypoints:main',
]

# Build and run:
cd ~/ros2_ws && colcon build --packages-select waypoint_nav
source install/setup.bash
ros2 run waypoint_nav navigate_waypoints

Common Pitfalls

Mistakes

  • Forgetting wait_for_server()
  • Wrong frame_id (must be 'map')
  • Waypoint inside an obstacle
  • Not sourcing the workspace
  • Forgetting to set initial pose first

Best Practices

  • Test waypoints in RViz first
  • Always handle goal rejection
  • Start with 2 waypoints, add more once working
  • Source workspace after every colcon build
  • Use self.get_logger() for debugging

Arduino vs. ROS2: A Comparison

AspectArduino (P1)ROS2 (P4)
MovementanalogWrite(motorPin, 200)Publish to /cmd_vel or Nav2 action
SensingdigitalRead(sensorPin)Subscribe to /scan
Control loopvoid loop()rclpy.spin() + callbacks
AbstractionDirect hardware controlHigh-level goals ("go to (2,3)")
ReusabilityCustom to your robotWorks on any ROS2 robot
The progression: Arduino → PLC → FANUC → ROS2. Each step increases abstraction and reusability.

Summary

ConceptWhat It DoesUsed In
rclpy NodePython process in the ROS2 graphActivity 8 & P4
Publisher + TimerSend velocity commands on a loopActivity 8
TopicsOne-way data streams (pub/sub)Both
Actions + CallbacksLong-running goals with feedbackP4 Part 3
NavigateToPoseNav2 action for waypoint navigationP4 Part 3
Activity 8: Publisher + timer with TurtleSim → Activity 8
P4 Part 3: Actions + NavigateToPose → Project 4 README