Skip to content

Intro to ROS 2 Workshop Manual

Review ROS2 Fundamentals

Key Concepts Review

Before starting the exercises, familiarize yourself with these core ROS2 concepts:

  • Nodes: Independent processes that perform specific computations
  • Topics: Named buses for asynchronous message passing between nodes
  • Messages: Typed data structures exchanged over topics
  • Publishers: Nodes that send messages to topics
  • Subscribers: Nodes that receive messages from topics
  • Workspace: Directory structure for organizing ROS2 packages
  • Packages: Organizational units containing nodes, libraries, and configuration files

ROS2 Workspace Structure

ros2_ws/
├── src/              # Source code for packages
├── build/            # Intermediate build files
├── install/          # Compiled binaries and libraries
└── log/              # Build and runtime logs

Part 0 - Environment Setup & Workspace

Objective

Build and run a ROS 2 workspace.

Tasks

  1. Source your ROS 2 installation if not automatically done already:

bash source /opt/ros/humble/setup.bash 2. Create and build a workspace:

bash mkdir -p ~/ros2_ws/src cd ~/ros2_ws colcon build source install/setup.bash

Checkpoint

  • ros2 pkg list runs without errors

Part 1 (Difficulty: 2) - Publisher & Subscriber Nodes

Objective

Understand nodes, topics, and messages through hands-on implementation.

Python Publisher-Subscriber Implementation

Step 1: Create Python Package

  1. In the ROS2 workspace you created, Navigate to the src/ directory: bash cd ~/ros2_ws/src

  2. Create a Python package named my_python_pkg with dependencies on rclpy and std_msgs:

bash ros2 pkg create my_python_pkg --build-type ament_python --dependencies rclpy std_msgs

--build-type ament_python specifies the build system of the package and that it will be a Python-based ROS 2 package.

--dependencies rclpy std_msgs specifies the dependencies the package requires:

  • rclpy is the Python client library for ROS 2, needed to write ROS 2 nodes in Python.
  • std_msgs is a package that provides standard message types, such as String, Int32, and Float64
  1. Examine the generated package structure and identify the purpose of each file/directory.

Generated Package Structure:

```bash
my_python_pkg/
├── package.xml # file containing meta information about the package
├── setup.py # containing instructions for how to install the package
├── setup.cfg # optional; can define entry_points and metadata instead of setup.py
├── resource/
│   └── my_python_pkg # marker file for the package
└── my_python_pkg/ # a directory with the same name as your package, used by ROS 2 tools to find your package, contains __init__.py
    └── __init__.py # used to mark directories on disk as Python package directories
```

Step 2: Write the Publisher Node

In the my_python_pkg folder, create a file named simple_publisher.py, which will be the publisher node.

File: ~/ros2_ws/src/my_python_pkg/my_python_pkg/simple_publisher.py

import rclpy                          # Import ROS2 Python client library
from rclpy.node import Node           # Import Node class to create a ROS2 node
from std_msgs.msg import String       # Import standard String message

class SimplePublisher(Node):
    def __init__(self):
        # Initialize the node with the name 'simple_publisher'
        super().__init__('simple_publisher')

        # Create a publisher of message type: String; topic name: 'topic'
        # And queue size: 10 (buffer size for outgoing messages)
        self.publisher_ = self.create_publisher(String, 'ros2intro', 10)

        # Create a timer that triggers 'timer_callback' every 1 second (1Hz)
        # The timer allows us to publish messages at a fixed interval.
        self.timer = self.create_timer(1.0, self.publish_message)  # 1 second interval

        # Initialize a counter to keep track of how many messages we've sent
        self.count = 0

    def publish_message(self):
        # Called every 1.0 second due to the timer.

        # Create a new String message instance
        msg = String()

        # Set its data field to a friendly message including the counter
        msg.data = f'Hello ROS 2: {self.count}'

        # Publish the message to the topic
        self.publisher_.publish(msg)

        # Log output to the console so we see what is being published
        self.get_logger().info(f'Publishing: "{msg.data}"')

        # Increment counter for the next message
        self.count += 1

def main(args=None):
    # Initialize the ROS client library
    rclpy.init(args=args)

    # Create an instance of our publisher node
    node = SimplePublisher()

    # Keep the node running until it is killed, allowing callbacks (like the timer) to function
    rclpy.spin(node)

    # Once we exit spin (e.g., by pressing Ctrl+C), destroy the node
    node.destroy_node()

    # Shutdown ROS2 gracefully
    rclpy.shutdown()

if __name__ == '__main__':
    main()

Step 3: Write the Subscriber Node

Create the subscriber node as simple_subscriber.py.

File: ~/ros2_ws/src/my_python_pkg/my_python_pkg/simple_subscriber.py

import rclpy                          # Import the ROS2 Python client library for initializing nodes and handling ROS-related operations
from rclpy.node import Node           # Import the Node class, which serves as the base class for all ROS2 node implementations
from std_msgs.msg import String       # Import the standard ROS2 String message type for sending and receiving text data

class SimpleSubscriber(Node):
    def __init__(self):
        # Initialize this class as a ROS2 node named 'simple_subscriber'
        super().__init__('simple_subscriber')

        # Create a subscription to a topic named 'topic' that publishes String messages.
        # Arguments:
        #   String: The message type the subscriber expects (std_msgs/String)
        #   'topic': The name of the topic to subscribe to
        #   self.listener_callback: The callback function that will be triggered when a new message arrives
        #   10: The queue size (message buffer) if messages arrive faster than they can be processed
        self.subscription = self.create_subscription(
            String,
            'ros2intro',
            self.listener_callback,
            10)

        # This line doesn't do anything functionally; it simply prevents a linting or IDE warning about the subscription
        # variable not being used. It's a common practice in ROS2 Python examples.
        self.subscription

    def listener_callback(self, msg):
        # This callback function is triggered each time a new message is received from the 'topic'.
        # 'msg' is a String message, so we access its data field to get the actual text content.
        # We then use the node's built-in logging system to print the received data.
        self.get_logger().info(f'Received: "{msg.data}"')

def main(args=None):
    # Initialize the ROS2 client library, necessary before using any ROS2-related code
    rclpy.init(args=args)

    # Create an instance of the SimpleSubscriber node
    node = SimpleSubscriber()

    try:
        # Spin the node, meaning this will block and process any incoming callbacks indefinitely
        # until an external event (like Ctrl+C) interrupts the process.
        rclpy.spin(node)
    except KeyboardInterrupt:
        # If the user presses Ctrl+C in the terminal, a KeyboardInterrupt exception is raised.
        # Here, we simply pass to allow the program to proceed to the 'finally' block for cleanup.
        pass
    finally:
        # Once we are done spinning or have been interrupted, we destroy the node to clean up resources.
        node.destroy_node()
        if rclpy.ok():  # Only shutdown if not already shutting down; Prevent "shutdown already called" on Ctrl-C
           # Shut down the rclpy library, releasing all ROS2-related resources.
           rclpy.shutdown()

# The standard Python entry point for executable scripts.
# If this script is run directly (e.g., 'python3 subscriber.py'), execute the main() function.
if __name__ == '__main__':
    main()

[!Note] The try/except/finally block is not strictly necessary, but it's considered good practice for graceful shutdown.


Step 4: Update the Setup Configuration

Modify setup.py to point to your nodes.

File: ~/ros2_ws/src/my_python_pkg/setup.py

from setuptools import setup

package_name = 'my_python_pkg'

setup(
    name=package_name,
    version='0.0.0',
    packages=[package_name],
    install_requires=['setuptools'],
    zip_safe=True,
    maintainer='your_name',
    maintainer_email='your_email@example.com',
    description='A simple publisher and subscriber example in ROS 2',
    license='Apache License 2.0',
    tests_require=['pytest'],
    entry_points={
        'console_scripts': [
            'simple_publisher = my_python_pkg.simple_publisher:main',
            'simple_subscriber = my_python_pkg.simple_subscriber:main',
        ],
    },
)

Step 5: Build and Source the Package

Go to the workspace root, build the package, and source it:

cd ~/ros2_ws
colcon build
source install/setup.bash

Replace .bash with the appropriate Shell if you are not using Bash Shell


Step 6: Run the Nodes

In two separate terminals, run the publisher and subscriber:

Terminal 1: Publisher
source ~/ros2_ws/install/setup.bash
ros2 run my_python_pkg simple_publisher
Terminal 2: Subscriber
source ~/ros2_ws/install/setup.bash
ros2 run my_python_pkg simple_subscriber
Expected Output
  • Publisher Terminal:

bash [INFO] [simple_publisher]: Publishing: "Hello ROS 2: 0" [INFO] [simple_publisher]: Publishing: "Hello ROS 2: 1" ...

  • Subscriber Terminal:

bash [INFO] [simple_subscriber]: Received: "Hello ROS 2: 0" [INFO] [simple_subscriber]: Received: "Hello ROS 2: 1" ...


Part 2 (Difficulty: 1) - ROS 2 CLI Introspection

Objective

Learn how to debug and inspect live systems

Tasks

With your nodes running, open a new terminal and execute:

  1. List all active nodes:

bash ros2 node list

  1. Get detailed information about a node:

bash ros2 node info /simple_publisher ros2 node info /simple_subscriber

  1. List all active topics:

bash ros2 topic list

  1. Get information about the ros2intro topic:

bash ros2 topic info /ros2intro

  1. Echo messages from the topic:

bash ros2 topic echo /ros2intro

  1. Check the publishing rate:

bash ros2 topic hz /ros2intro


Part 3 (Difficulty: 4) - Custom Message Types

Overview

Custom message types allow you to define your own data structures for communication between nodes. Instead of using only standard messages like String or Float32, you can create messages that bundle multiple fields together, making your code more organized and meaningful.

Understanding Custom Messages

Why Use Custom Messages?

Standard Message:

# Publishing speed as a Float32
speed_msg = Float32()
speed_msg.data = 25.5

Custom Message:

# Publishing complete vehicle state
vehicle_msg = VehicleState()
vehicle_msg.speed = 25.5
vehicle_msg.steering_angle = 15.0
vehicle_msg.throttle = 0.8
vehicle_msg.timestamp = self.get_clock().now().to_msg()

Custom messages are more descriptive, organized, and maintainable for complex robotic applications.


Creating a Custom Message - Step by Step

Step 1: Create a Package for Custom Messages

It's best practice to create a separate package for custom messages so multiple packages can use them.

cd ~/ros2_ws/src
ros2 pkg create my_custom_msgs --build-type ament_cmake

Why ament_cmake? Message generation requires CMake build system, even if your nodes are in Python.

Step 2: Create Message Directory and File

cd ~/ros2_ws/src/my_custom_msgs
mkdir msg

Create your first custom message file:

File: ~/ros2_ws/src/my_custom_msgs/msg/VehicleState.msg

# VehicleState.msg
# Custom message for vehicle state information

float32 speed                  # Vehicle speed in m/s
float32 steering_angle        # Steering angle in degrees
float32 throttle              # Throttle position (0.0 to 1.0)
builtin_interfaces/Time timestamp  # Timestamp of the measurement

Message Field Types: - Basic types: bool, int8, int16, int32, int64, float32, float64, string - Arrays: float32[] (dynamic), float32[5] (fixed size) - Other messages: builtin_interfaces/Time, std_msgs/Header

Step 3: Update package.xml

Add the required dependencies to ~/ros2_ws/src/my_custom_msgs/package.xml:

<?xml version="1.0"?>
<?xml-model href="http://download.ros.org/schema/package_format3.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?>
<package format="3">
  <name>my_custom_msgs</name>
  <version>0.0.0</version>
  <description>Custom message types for ROS2 tutorial</description>
  <maintainer email="student@example.com">student</maintainer>
  <license>Apache License 2.0</license>

  <buildtool_depend>ament_cmake</buildtool_depend>

  <!-- Add these dependencies for message generation -->
  <build_depend>rosidl_default_generators</build_depend>
  <exec_depend>rosidl_default_runtime</exec_depend>
  <member_of_group>rosidl_interface_packages</member_of_group>

  <!-- Add dependency on builtin_interfaces for Time type -->
  <depend>builtin_interfaces</depend>

  <test_depend>ament_lint_auto</test_depend>
  <test_depend>ament_lint_common</test_depend>

  <export>
    <build_type>ament_cmake</build_type>
  </export>
</package>

Step 4: Update CMakeLists.txt

Update ~/ros2_ws/src/my_custom_msgs/CMakeLists.txt:

cmake_minimum_required(VERSION 3.8)
project(my_custom_msgs)

if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
  add_compile_options(-Wall -Wextra -Wpedantic)
endif()

# Find dependencies
find_package(ament_cmake REQUIRED)
find_package(rosidl_default_generators REQUIRED)
find_package(builtin_interfaces REQUIRED)

# Declare the message files to generate code for
rosidl_generate_interfaces(${PROJECT_NAME}
  "msg/VehicleState.msg"
  DEPENDENCIES builtin_interfaces
)

if(BUILD_TESTING)
  find_package(ament_lint_auto REQUIRED)
  ament_lint_auto_find_test_dependencies()
endif()

ament_package()

Step 5: Build the Message Package

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

Expected Output:

Starting >>> my_custom_msgs
Finished <<< my_custom_msgs [5.23s]

Summary: 1 package finished [5.67s]

Step 6: Verify Message Creation

Check that your message was created:

ros2 interface show my_custom_msgs/msg/VehicleState

Expected Output:

float32 speed
float32 steering_angle
float32 throttle
builtin_interfaces/Time timestamp
        int32 sec
        uint32 nanosec

Using Custom Messages in Python Nodes

Step 1: Update Python Package Dependencies

Update ~/ros2_ws/src/my_python_pkg/package.xml to add the custom message dependency:

<?xml version="1.0"?>
<?xml-model href="http://download.ros.org/schema/package_format3.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?>
<package format="3">
  <name>my_python_pkg</name>
  <version>0.0.0</version>
  <description>A simple publisher and subscriber example in ROS 2</description>
  <maintainer email="student@example.com">student</maintainer>
  <license>Apache License 2.0</license>

  <depend>rclpy</depend>
  <depend>std_msgs</depend>
  <depend>my_custom_msgs</depend>  <!-- Add this line -->

  <test_depend>ament_copyright</test_depend>
  <test_depend>ament_flake8</test_depend>
  <test_depend>ament_pep257</test_depend>
  <test_depend>python3-pytest</test_depend>

  <export>
    <build_type>ament_python</build_type>
  </export>
</package>

Step 2: Create Custom Message Publisher

File: ~/ros2_ws/src/my_python_pkg/my_python_pkg/custom_publisher.py

import rclpy
from rclpy.node import Node
from my_custom_msgs.msg import VehicleState
import random
import math

class CustomPublisher(Node):
    def __init__(self):
        super().__init__('custom_publisher')

        # Create publisher for custom message type
        self.publisher_ = self.create_publisher(
            VehicleState, 
            'vehicle_state', 
            10)

        # Timer to publish at 2 Hz
        self.timer = self.create_timer(0.5, self.publish_vehicle_state)

        # Simulation variables
        self.simulation_time = 0.0

        self.get_logger().info('Custom Publisher started - Publishing VehicleState messages')

    def publish_vehicle_state(self):
        # Create custom message instance
        msg = VehicleState()

        # Simulate varying vehicle state
        self.simulation_time += 0.5

        # Speed varies between 10-30 m/s with some randomness
        msg.speed = 20.0 + 10.0 * math.sin(self.simulation_time * 0.5) + random.uniform(-1, 1)

        # Steering angle varies between -30 and +30 degrees
        msg.steering_angle = 15.0 * math.sin(self.simulation_time * 0.3) + random.uniform(-2, 2)

        # Throttle varies between 0.3 and 1.0
        msg.throttle = 0.65 + 0.35 * math.sin(self.simulation_time * 0.4)

        # Add timestamp
        msg.timestamp = self.get_clock().now().to_msg()

        # Publish the message
        self.publisher_.publish(msg)

        # Log the published data
        self.get_logger().info(
            f'Publishing VehicleState: '
            f'speed={msg.speed:.2f} m/s, '
            f'steering={msg.steering_angle:.2f}°, '
            f'throttle={msg.throttle:.2f}'
        )

def main(args=None):
    rclpy.init(args=args)
    node = CustomPublisher()

    try:
        rclpy.spin(node)
    except KeyboardInterrupt:
        pass
    finally:
        node.destroy_node()
        rclpy.shutdown()

if __name__ == '__main__':
    main()

Step 3: Create Custom Message Subscriber

File: ~/ros2_ws/src/my_python_pkg/my_python_pkg/custom_subscriber.py

import rclpy
from rclpy.node import Node
from my_custom_msgs.msg import VehicleState

class CustomSubscriber(Node):
    def __init__(self):
        super().__init__('custom_subscriber')

        # Create subscription for custom message type
        self.subscription = self.create_subscription(
            VehicleState,
            'vehicle_state',
            self.vehicle_state_callback,
            10)

        self.subscription  # prevent unused variable warning

        self.get_logger().info('Custom Subscriber started - Listening for VehicleState messages')

    def vehicle_state_callback(self, msg):
        # Extract timestamp
        timestamp_sec = msg.timestamp.sec + msg.timestamp.nanosec * 1e-9

        # Analyze the vehicle state
        speed_status = self.analyze_speed(msg.speed)
        steering_status = self.analyze_steering(msg.steering_angle)
        throttle_status = self.analyze_throttle(msg.throttle)

        # Log received data with analysis
        self.get_logger().info(
            f'Received VehicleState:\n'
            f'  Speed: {msg.speed:.2f} m/s ({speed_status})\n'
            f'  Steering: {msg.steering_angle:.2f}° ({steering_status})\n'
            f'  Throttle: {msg.throttle:.2f} ({throttle_status})\n'
            f'  Timestamp: {timestamp_sec:.2f}s'
        )

    def analyze_speed(self, speed):
        """Provide context about speed"""
        if speed < 15:
            return "SLOW"
        elif speed < 25:
            return "MODERATE"
        else:
            return "FAST"

    def analyze_steering(self, angle):
        """Provide context about steering"""
        if abs(angle) < 5:
            return "STRAIGHT"
        elif abs(angle) < 15:
            return "GENTLE TURN"
        else:
            return "SHARP TURN"

    def analyze_throttle(self, throttle):
        """Provide context about throttle"""
        if throttle < 0.4:
            return "LOW"
        elif throttle < 0.7:
            return "MEDIUM"
        else:
            return "HIGH"

def main(args=None):
    rclpy.init(args=args)
    node = CustomSubscriber()

    try:
        rclpy.spin(node)
    except KeyboardInterrupt:
        pass
    finally:
        node.destroy_node()
        rclpy.shutdown()

if __name__ == '__main__':
    main()

Step 4: Update setup.py

Update ~/ros2_ws/src/my_python_pkg/setup.py to include the new executables:

from setuptools import setup

package_name = 'my_python_pkg'

setup(
    name=package_name,
    version='0.0.0',
    packages=[package_name],
    data_files=[
        ('share/ament_index/resource_index/packages',
            ['resource/' + package_name]),
        ('share/' + package_name, ['package.xml']),
    ],
    install_requires=['setuptools'],
    zip_safe=True,
    maintainer='student',
    maintainer_email='student@example.com',
    description='A simple publisher and subscriber example in ROS 2',
    license='Apache License 2.0',
    tests_require=['pytest'],
    entry_points={
        'console_scripts': [
            'simple_publisher = my_python_pkg.simple_publisher:main',
            'simple_subscriber = my_python_pkg.simple_subscriber:main',
            'custom_publisher = my_python_pkg.custom_publisher:main',
            'custom_subscriber = my_python_pkg.custom_subscriber:main',
        ],
    },
)

Step 5: Build and Test

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

Terminal 1 - Run Custom Publisher:

source ~/ros2_ws/install/setup.bash
ros2 run my_python_pkg custom_publisher

Expected Output:

[INFO] [custom_publisher]: Custom Publisher started - Publishing VehicleState messages
[INFO] [custom_publisher]: Publishing VehicleState: speed=20.45 m/s, steering=2.34°, throttle=0.78
[INFO] [custom_publisher]: Publishing VehicleState: speed=24.23 m/s, steering=5.67°, throttle=0.85
[INFO] [custom_publisher]: Publishing VehicleState: speed=28.91 m/s, steering=-1.23°, throttle=0.92
[INFO] [custom_publisher]: Publishing VehicleState: speed=26.45 m/s, steering=-8.45°, throttle=0.88

Terminal 2 - Run Custom Subscriber:

source ~/ros2_ws/install/setup.bash
ros2 run my_python_pkg custom_subscriber

Expected Output:

[INFO] [custom_subscriber]: Custom Subscriber started - Listening for VehicleState messages
[INFO] [custom_subscriber]: Received VehicleState:
  Speed: 20.45 m/s (MODERATE)
  Steering: 2.34° (STRAIGHT)
  Throttle: 0.78 (HIGH)
  Timestamp: 1735411234.56s
[INFO] [custom_subscriber]: Received VehicleState:
  Speed: 24.23 m/s (MODERATE)
  Steering: 5.67° (GENTLE TURN)
  Throttle: 0.85 (HIGH)
  Timestamp: 1735411235.06s

Part 4 (Difficulty: 3) - Launch Files (Multi-Node Systems)

Overview

Launch files allow you to start multiple nodes simultaneously with a single command. Instead of opening multiple terminals and running each node individually, you can configure and launch your entire system at once. This is essential for complex robotic systems

Understanding Launch Files

Why Use Launch Files?

Without Launch Files:

# Terminal 1
ros2 run my_python_pkg simple_publisher

# Terminal 2
ros2 run my_python_pkg simple_subscriber

# Terminal 3
ros2 run my_python_pkg custom_publisher

# Terminal 4
ros2 run my_python_pkg custom_subscriber

With Launch Files:

# Single terminal
ros2 launch my_python_pkg all_nodes_launch.py

Benefits of Launch Files:

  • Start multiple nodes with one command
  • Configure parameters for nodes
  • Set remapping rules for topics
  • Organize complex systems
  • Reproducible system startup
  • Easier testing and debugging

Python Launch Files

Step 1: Create Launch Directory

cd ~/ros2_ws/src/my_python_pkg
mkdir launch

Step 2: Create Simple Launch File

File: ~/ros2_ws/src/my_python_pkg/launch/simple_nodes_launch.py

from launch import LaunchDescription
from launch_ros.actions import Node

def generate_launch_description():
    """Launch simple publisher and subscriber nodes"""

    return LaunchDescription([
        # Launch simple publisher node
        Node(
            package='my_python_pkg',
            executable='simple_publisher',
            name='simple_publisher',
            output='screen',
            emulate_tty=True,
        ),

        # Launch simple subscriber node
        Node(
            package='my_python_pkg',
            executable='simple_subscriber',
            name='simple_subscriber',
            output='screen',
            emulate_tty=True,
        ),
    ])

Explanation of Parameters: - package: The ROS2 package containing the executable - executable: The name of the node executable (from setup.py entry_points) - name: The name the node will use (can override the node's internal name) - output='screen': Display node output in the terminal - emulate_tty=True: Ensures colored output and proper formatting

Step 3: Create Custom Messages Launch File

File: ~/ros2_ws/src/my_python_pkg/launch/custom_nodes_launch.py

from launch import LaunchDescription
from launch_ros.actions import Node

def generate_launch_description():
    """Launch custom message publisher and subscriber nodes"""

    return LaunchDescription([
        # Launch custom publisher node
        Node(
            package='my_python_pkg',
            executable='custom_publisher',
            name='custom_publisher',
            output='screen',
            emulate_tty=True,
        ),

        # Launch custom subscriber node
        Node(
            package='my_python_pkg',
            executable='custom_subscriber',
            name='custom_subscriber',
            output='screen',
            emulate_tty=True,
        ),
    ])

Step 4: Create Comprehensive Launch File (All Nodes)

File: ~/ros2_ws/src/my_python_pkg/launch/all_nodes_launch.py

from launch import LaunchDescription
from launch_ros.actions import Node
from launch.actions import DeclareLaunchArgument, LogInfo
from launch.substitutions import LaunchConfiguration

def generate_launch_description():
    """
    Launch all publisher and subscriber nodes:
    - Simple string message nodes
    - Custom VehicleState message nodes
    """

    return LaunchDescription([
        # Log info message
        LogInfo(msg="Starting all ROS2 tutorial nodes..."),

        # Simple Publisher (String messages)
        Node(
            package='my_python_pkg',
            executable='simple_publisher',
            name='simple_publisher',
            output='screen',
            emulate_tty=True,
            parameters=[],
        ),

        # Simple Subscriber (String messages)
        Node(
            package='my_python_pkg',
            executable='simple_subscriber',
            name='simple_subscriber',
            output='screen',
            emulate_tty=True,
            parameters=[],
        ),

        # Custom Publisher (VehicleState messages)
        Node(
            package='my_python_pkg',
            executable='custom_publisher',
            name='custom_publisher',
            output='screen',
            emulate_tty=True,
            parameters=[],
        ),

        # Custom Subscriber (VehicleState messages)
        Node(
            package='my_python_pkg',
            executable='custom_subscriber',
            name='custom_subscriber',
            output='screen',
            emulate_tty=True,
            parameters=[],
        ),

        LogInfo(msg="All nodes launched successfully!"),
    ])

Step 5: Update setup.py to Include Launch Files

File: ~/ros2_ws/src/my_python_pkg/setup.py

from setuptools import setup
import os
from glob import glob

package_name = 'my_python_pkg'

setup(
    name=package_name,
    version='0.0.0',
    packages=[package_name],
    data_files=[
        ('share/ament_index/resource_index/packages',
            ['resource/' + package_name]),
        ('share/' + package_name, ['package.xml']),
        # Include launch files
        (os.path.join('share', package_name, 'launch'), 
            glob(os.path.join('launch', '*launch.[pxy][yma]*'))),
    ],
    install_requires=['setuptools'],
    zip_safe=True,
    maintainer='student',
    maintainer_email='student@example.com',
    description='A simple publisher and subscriber example in ROS 2',
    license='Apache License 2.0',
    tests_require=['pytest'],
    entry_points={
        'console_scripts': [
            'simple_publisher = my_python_pkg.simple_publisher:main',
            'simple_subscriber = my_python_pkg.simple_subscriber:main',
            'custom_publisher = my_python_pkg.custom_publisher:main',
            'custom_subscriber = my_python_pkg.custom_subscriber:main',
        ],
    },
)

Step 6: Update package.xml for Launch Dependencies

File: ~/ros2_ws/src/my_python_pkg/package.xml

Add these dependencies if not already present:

  <exec_depend>ros2launch</exec_depend>
  <exec_depend>launch</exec_depend>
  <exec_depend>launch_ros</exec_depend>

Complete package.xml should look like:

<?xml version="1.0"?>
<?xml-model href="http://download.ros.org/schema/package_format3.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?>
<package format="3">
  <name>my_python_pkg</name>
  <version>0.0.0</version>
  <description>A simple publisher and subscriber example in ROS 2</description>
  <maintainer email="student@example.com">student</maintainer>
  <license>Apache License 2.0</license>

  <depend>rclpy</depend>
  <depend>std_msgs</depend>
  <depend>my_custom_msgs</depend>

  <!-- Launch file dependencies -->
  <exec_depend>ros2launch</exec_depend>
  <exec_depend>launch</exec_depend>
  <exec_depend>launch_ros</exec_depend>

  <test_depend>ament_copyright</test_depend>
  <test_depend>ament_flake8</test_depend>
  <test_depend>ament_pep257</test_depend>
  <test_depend>python3-pytest</test_depend>

  <export>
    <build_type>ament_python</build_type>
  </export>
</package>

Step 7: Build and Test Launch Files

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

Step 8: Run Launch Files

Launch simple nodes only:

ros2 launch my_python_pkg simple_nodes_launch.py

Expected Output:

[INFO] [launch]: All log files can be found below /home/student/.ros/log/2024-01-02-12-34-56-789012-ubuntu-12345
[INFO] [launch]: Default logging verbosity is set to INFO
[INFO] [simple_publisher-1]: process started with pid [12346]
[INFO] [simple_subscriber-2]: process started with pid [12347]
[simple_publisher-1] [INFO] [1735411234.123456789] [simple_publisher]: Publishing: "Hello ROS 2: 0"
[simple_subscriber-2] [INFO] [1735411234.123456789] [simple_subscriber]: Received: "Hello ROS 2: 0"
[simple_publisher-1] [INFO] [1735411235.123456789] [simple_publisher]: Publishing: "Hello ROS 2: 1"
[simple_subscriber-2] [INFO] [1735411235.123456789] [simple_subscriber]: Received: "Hello ROS 2: 1"

Launch custom message nodes only:

ros2 launch my_python_pkg custom_nodes_launch.py

Launch all nodes:

ros2 launch my_python_pkg all_nodes_launch.py

Expected Output:

[INFO] [launch]: All log files can be found below /home/student/.ros/log/2024-01-02-12-34-56-789012-ubuntu-12345
[INFO] [launch]: Default logging verbosity is set to INFO
[INFO] [launch.user]: Starting all ROS2 tutorial nodes...
[INFO] [simple_publisher-1]: process started with pid [12346]
[INFO] [simple_subscriber-2]: process started with pid [12347]
[INFO] [custom_publisher-3]: process started with pid [12348]
[INFO] [custom_subscriber-4]: process started with pid [12349]
[INFO] [launch.user]: All nodes launched successfully!
[simple_publisher-1] [INFO] [1735411234.123456789] [simple_publisher]: Publishing: "Hello ROS 2: 0"
[simple_subscriber-2] [INFO] [1735411234.123456789] [simple_subscriber]: Received: "Hello ROS 2: 0"
[custom_publisher-3] [INFO] [1735411234.567890123] [custom_publisher]: Publishing VehicleState: speed=22.45 m/s, steering=3.21°, throttle=0.78
[custom_subscriber-4] [INFO] [1735411234.567890123] [custom_subscriber]: Received VehicleState:
[custom_subscriber-4]   Speed: 22.45 m/s (MODERATE)
[custom_subscriber-4]   Steering: 3.21° (STRAIGHT)
[custom_subscriber-4]   Throttle: 0.78 (HIGH)
[custom_subscriber-4]   Timestamp: 1735411234.57s

To stop all nodes: Press Ctrl+C in the terminal


Practice: Multi-Node Communication System - Data Processing Pipeline

Design and implement a simple multi-node system with the following architecture:

[Sensor Node] --> /raw_data --> [Processing Node] --> /processed_data --> [Display Node]

Requirements:

  1. Sensor Node: Publishes simulated or random sensor readings (e.g., temperature values, speed, etc..) to /raw_data
  2. Processing Node: Subscribes to /raw_data, applies a simple transformation (e.g., Celsius to Fahrenheit, kilometers to miles, etc..), and publishes to /processed_data
  3. Display Node: Subscribes to /processed_data and displays the results
  4. Custom Data Type: Create at least one custom data types to be used by one or more of the nodes
  5. Launch File: Create a launch file that runs all of the required nodes and set parameters if needed

Implementation Notes:

  • You can implement this in Python or C++
  • Create these nodes in your existing package or create a new package
  • Use appropriate message types (e.g., std_msgs/Float32 for numeric data)