Custom Nodes

In this lesson, we will learn how to create a simple custom node in Python in a ROS2 Humble environment and create a basic program to control a mobile robot. The node publishes geometry_msgs/Twist type messages to the /kachaka/manual_control/cmd_vel topic.

Learning Objectives

By the end of this lesson, you will be able to:

  • Create a ROS 2 package and node from scratch
  • Implement a publisher to send velocity commands
  • Control a robot’s movement using feedforward control
  • Build and run a custom ROS 2 node

1. Program Overview

This program demonstrates basic robot control by:

  • Controlling a mobile robot with simple feedforward control
  • Executing basic movements (forward, stop, rotate) in sequence
  • Publishing velocity commands to control the robot’s motion

2. Prerequisites

Before starting this lesson, make sure you have:

  • ROS2 Humble installed and properly configured
  • Python3 available (Python 3.8 or higher recommended)
  • A ROS2 workspace set up (e.g., ~/ros2_ws)
  • Basic understanding of Python programming
  • Familiarity with terminal/command line usage

Required Packages

The following ROS 2 packages will be used:

  • rclpy: ROS2 Python client library - provides the core functionality for creating nodes
  • geometry_msgs: Package for handling geometric messages such as position and velocity - we’ll use the Twist message type for velocity commands

3. Creating a Package

First, navigate to the src directory of your ROS2 workspace and create a new package. The --node-name option automatically creates a basic node file with the specified name, which saves us from manually setting up the entry point in setup.py.

cd ./ros2_ws/src
ros2 pkg create --build-type ament_python kachaka_simple_control --dependencies rclpy geometry_msgs --node-name kachaka_simple_control

This command generates a package with the following structure:

kachaka_simple_control/
├── resource/
│   └── kachaka_simple_control
├── kachaka_simple_control/
│   ├── __init__.py
│   └── kachaka_simple_control.py
├── test/
├── package.xml
└── setup.py

4. Node Implementation

Now we’ll implement the node logic. If you used the --node-name option when creating the package, a basic node file was already created. Replace its contents with the following code, or create a new kachaka_simple_control.py file in the kachaka_simple_control directory within the kachaka_simple_control package.

#!/usr/bin/env python3
# This line (shebang) tells the system to use Python 3 to execute this script

# Import necessary ROS 2 libraries
import rclpy  # ROS 2 Python client library - provides core ROS 2 functionality
from rclpy.node import Node  # Base class for all ROS 2 nodes
from geometry_msgs.msg import Twist  # Message type for velocity commands (linear and angular)
from rclpy.duration import Duration  # For handling time durations

class KachakaSimpleControl(Node):
    """
    A ROS 2 node class for simple robot control.
    This class inherits from Node, which provides all the ROS 2 node functionality.
    """
    def __init__(self):
        # Initialize the parent Node class with the node name
        super().__init__('kachaka_simple_control')
        
        # Create a Publisher that sends Twist type messages to the /kachaka/manual_control/cmd_vel topic
        # Parameters:
        #   - Twist: The message type (contains linear and angular velocity)
        #   - '/kachaka/manual_control/cmd_vel': The topic name where velocity commands are published
        #   - 10: The queue size (how many messages to buffer if subscribers are slow)
        self.publisher = self.create_publisher(Twist, '/kachaka/manual_control/cmd_vel', 10)
        
        # Log an informational message that the node has started
        self.get_logger().info('Robot control node has started')

    def move_robot(self, linear_x, angular_z, duration):
        """
        Move the robot at specified speed and time using feedforward control.
        
        This method publishes velocity commands continuously for the specified duration.
        In ROS 2, velocity commands must be published regularly; if you stop publishing,
        the robot will stop moving (safety feature).
        
        Parameters:
            linear_x: Forward/backward speed [m/s]
                - Positive values: forward motion
                - Negative values: backward motion
                - Zero: no linear motion
            angular_z: Rotation speed [rad/s]
                - Positive values: counterclockwise rotation (left turn when moving forward)
                - Negative values: clockwise rotation (right turn when moving forward)
                - Zero: no rotation
            duration: Movement duration [seconds]
                - How long to maintain this velocity command
        """
        # Create a new Twist message to hold velocity commands
        vel = Twist()
        # Set the linear velocity in the x direction (forward/backward)
        vel.linear.x = linear_x
        # Set the angular velocity around the z-axis (rotation)
        vel.angular.z = angular_z
        
        # Calculate when the movement should end
        # get_clock().now() gets the current ROS 2 time
        # Duration(seconds=duration) creates a time duration
        # Adding them gives us the target end time
        end_time = self.get_clock().now() + Duration(seconds=duration)
        
        # Continue publishing velocity commands until the specified time
        # This loop ensures commands are sent regularly (every 0.1 seconds)
        while self.get_clock().now() < end_time:
            # Publish the velocity command
            self.publisher.publish(vel)
            # Process any pending callbacks and wait 0.1 seconds
            # This allows ROS 2 to handle other tasks and prevents the loop from running too fast
            rclpy.spin_once(self, timeout_sec=0.1)
        
        # Stop after the movement is complete
        # This ensures the robot doesn't continue moving after the duration expires
        self.stop_robot()
        
        # Log the completed movement for debugging/monitoring
        self.get_logger().info(f'Movement completed: Forward={linear_x}, Rotation={angular_z}, Time={duration} seconds')
    
    def stop_robot(self):
        """
        Stop the robot by publishing zero velocities.
        
        This method explicitly sets all velocities to zero and publishes the command.
        It's important to explicitly stop the robot to ensure it doesn't continue
        moving due to any buffered commands.
        """
        # Create a new Twist message with zero velocities
        vel = Twist()
        vel.linear.x = 0.0   # No forward/backward motion
        vel.angular.z = 0.0  # No rotation
        # Publish the stop command
        self.publisher.publish(vel)
        # Log that the robot has stopped
        self.get_logger().info('Robot has stopped')

def main(args=None):
    """
    Main function that initializes ROS 2 and runs the robot control sequence.
    
    This function:
    1. Initializes ROS 2
    2. Creates the robot control node
    3. Executes a predefined movement sequence
    4. Handles interruptions gracefully
    5. Cleans up resources
    """
    # Initialize ROS 2 - this must be called before creating any nodes
    rclpy.init(args=args)
    
    # Create an instance of our robot control node
    robot_control = KachakaSimpleControl()

    try:
        # Execute the following movement sequence
        # The sequence demonstrates basic movements: forward, rotation, and stopping
        robot_control.move_robot(0.0, 0.0, 1.0)  # Stop for 1 second (initial pause)
        robot_control.move_robot(0.1, 0.0, 1.0)  # Move forward at 0.1 m/s for 1 second
        robot_control.move_robot(0.0, 0.0, 1.0)  # Stop for 1 second
        robot_control.move_robot(0.0, 0.5, 2.0)  # Rotate left at 0.5 rad/s for 2 seconds
        robot_control.move_robot(0.0, 0.0, 1.0)  # Stop for 1 second
        robot_control.move_robot(0.0, -0.5, 2.0)  # Rotate right at -0.5 rad/s for 2 seconds
        robot_control.move_robot(0.0, 0.0, 1.0)  # Final stop for 1 second
    except KeyboardInterrupt:
        # Handle Ctrl+C gracefully
        # This allows the user to stop the program safely
        robot_control.get_logger().info('Program was interrupted')
    finally:
        # Always execute cleanup, even if an error occurred
        # This ensures the robot stops and resources are freed
        robot_control.stop_robot()      # Make sure robot stops
        robot_control.destroy_node()   # Clean up the node
        rclpy.shutdown()               # Shutdown ROS 2

if __name__ == '__main__':
    main()

5. Editing setup.py

Note: If you used the --node-name option when creating the package, the entry point is already configured and you can skip this step.

If you did not use --node-name, you need to manually set the entry point in setup.py at the package root. The entry point tells ROS 2 how to run your node when you execute ros2 run.

entry_points={
    'console_scripts': [
        'kachaka_simple_control = kachaka_simple_control.kachaka_simple_control:main'
    ],
},

6. Building and Running the Package

Building the Package

Navigate to your workspace root and build the package:

cd ~/ros2_ws
colcon build --packages-select kachaka_simple_control

What happens during build:

  • The build system compiles your Python code
  • It checks for dependencies
  • It installs the package so it can be run with ros2 run
  • Any errors in your code will be reported here

Common issues:

  • If you see import errors, make sure all dependencies are listed in package.xml
  • If the build fails, check the error messages carefully

Loading Environment Settings

After building, you must source the setup file to make the package available:

source install/setup.bash

Important: You need to run this command in every new terminal, or add it to your ~/.bashrc file to make it permanent.

Running the Node

Execute the node using the ROS 2 run command:

ros2 run kachaka_simple_control kachaka_simple_control

What to expect:

  • The node will start and log “Robot control node has started”
  • The robot will execute the predefined movement sequence
  • You can stop it with Ctrl+C at any time
  • The robot will stop safely when the program ends

Troubleshooting:

  • If you get “package not found”, make sure you sourced install/setup.bash
  • If the robot doesn’t move, check that the Kachaka robot is running and connected
  • Check topic names match: ros2 topic list should show /kachaka/manual_control/cmd_vel

7. Code Explanation

Understanding the Code Structure

The code is organized into a class that inherits from ROS 2’s Node class. This is the standard pattern for creating ROS 2 nodes in Python.

Classes and Methods

  • KachakaSimpleControl: Main node class
    • __init__: Node initialization and publisher setup
      • Creates the publisher that will send velocity commands
      • Sets up logging for debugging
    • move_robot: Method to move the robot at specified speed and time
      • Takes linear and angular velocities as parameters
      • Publishes commands continuously until the duration is reached
    • stop_robot: Method to stop the robot
      • Publishes zero velocities to bring the robot to a stop

Control Parameters

  • linear_x: Forward speed [m/s]
    • Positive value: Forward
    • Negative value: Backward
    • 0: Stop
  • angular_z: Rotation speed [rad/s]
    • Positive value: Rotate left
    • Negative value: Rotate right
    • 0: No rotation
  • duration: Time to continue the movement [seconds]

Movement Sequence

The program executes the following movement sequence:

  1. Stop (1.0 seconds)
  2. Forward (1.0 seconds)
  3. Stop (1.0 seconds)
  4. Rotate left (2.0 seconds)
  5. Stop (1.0 seconds)
  6. Rotate right (2.0 seconds)
  7. Stop (1.0 seconds)

8. Explanation of Basic ROS2 Concepts

Node

A node is the basic execution unit of a ROS2 system. Each node has a specific function and communicates with other nodes. In this example, the KachakaSimpleControl class functions as a node.

Topic

A topic is a communication channel for exchanging messages between nodes. In this example, we use the /kachaka/manual_control/cmd_vel topic to send velocity commands to the robot.

Message

A message defines the format of data sent through topics. In this example, we use a geometry_msgs/Twist type message to specify the robot’s linear and angular velocities.

Publisher

A publisher is an object for publishing (sending) messages to a specific topic. In this example, self.publisher publishes velocity commands to the /kachaka/manual_control/cmd_vel topic.

Feedforward Control

Feedforward control is a control method that executes predefined commands. In this example, we control the robot’s movement by specifying speed and time. It does not use sensor feedback and executes predetermined movements.

Key Characteristics:

  • Open-loop control (no feedback)
  • Predictable behavior
  • Simple to implement
  • Suitable for well-known environments
  • Cannot adapt to unexpected situations