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 nodesgeometry_msgs: Package for handling geometric messages such as position and velocity - we’ll use theTwistmessage 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_controlThis 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.py4. 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_controlWhat 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.bashImportant: 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_controlWhat 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 listshould 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:
- Stop (1.0 seconds)
- Forward (1.0 seconds)
- Stop (1.0 seconds)
- Rotate left (2.0 seconds)
- Stop (1.0 seconds)
- Rotate right (2.0 seconds)
- 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