← All modules

1.6 ROS2 Practical Getting Started

Draft — not verified
Download lecture slides (PPTX)
CH1.6 The ROS 2 Blueprint
CH1.6-The_ROS_2_Blueprint.pptx
Left-to-right workspace build flow: source tree flows through colcon build into the generated build, install, and log directories, then source install/setup.bash exposes packages to the shell
The colcon workflow: author-maintained source under src/ is compiled into build/install/log; sourcing install/setup.bash makes the packages available to the current shell.

Theoretical Background: ROS 2 Development Practice

Module 1 Theory: ROS 2 Practical Getting Started

1.6.1 Setting Up the ROS 2 Environment

Before writing any robotic application, the development environment must be configured. ROS 2 distributions follow a naming convention tied to release year and alphabetical order. The recommended distribution for this course is ROS 2 Humble Hawksbill (LTS, supported through 2027) or ROS 2 Jazzy Jalisco, both targeting Ubuntu 22.04 or 24.04 respectively.

Installation Overview

ROS 2 is installed from pre-built Debian packages on Ubuntu. The installation adds the ROS 2 apt repository, installs the desired configuration (ros-humble-desktop includes RViz2, Gazebo integration, and demo packages), and makes ROS 2 libraries and executables available system-wide. The desktop installation provides everything needed for development including visualization and simulation tools.

Workspace Creation

A ROS 2 workspace is a directory that contains your custom packages. The convention is to create a workspace with a src subdirectory where all source packages reside:

# Create a new workspace
        mkdir -p ~/ros2_ws/src
        cd ~/ros2_ws

The workspace is separate from the ROS 2 system installation. Packages in the workspace overlay the system packages, allowing you to develop and test modified versions of any ROS 2 package without affecting the system installation.

Sourcing

ROS 2 uses shell environment variables to locate packages, libraries, and executables. Two sourcing steps are required:

# Source the system ROS 2 installation (add to ~/.bashrc for persistence)
        source /opt/ros/humble/setup.bash
# After building, source the workspace overlay
        source ~/ros2_ws/install/setup.bash

Sourcing the system installation makes all standard ROS 2 packages available. Sourcing the workspace overlay adds your custom packages on top. Forgetting to source is the most common cause of “package not found” errors and is the first thing to check when a node fails to launch.

Environment Verification

After sourcing, verify the environment is correctly configured:

# Check ROS 2 distribution
        echo $ROS_DISTRO
        
        # Verify ROS 2 commands are available
        ros2 --help
        
        # List available packages
        ros2 pkg list

1.6.2 Creating Your First Package

A ROS 2 package is the fundamental unit of code organization. Every node, message definition, launch file, and configuration file belongs to a package.

Package Build Types

ROS 2 supports two build types:

  • ament_cmake: For C++ packages. Uses CMakeLists.txt to define build targets, dependencies, and installation rules. Provides maximum performance and is used for computationally intensive nodes (perception, control loops).
  • ament_python: For Python packages. Uses setup.py and setup.cfg to define entry points and dependencies. Provides rapid development and is used for high-level logic, launch files, and prototyping.

Creating a Python Package

# Navigate to workspace source directory
        cd ~/ros2_ws/src
        
        # Create a new Python package with dependencies
        ros2 pkg create --build-type ament_python \
        --dependencies rclpy geometry_msgs nav_msgs my_robot_pkg

This command generates the package scaffolding:

# Generated package structure
        my_robot_pkg/
        package.xml              # package metadata: name, version, dependencies
        setup.py                 # Python package configuration
        setup.cfg                # entry point mapping
        resource/
        my_robot_pkg           # ament resource index marker
        my_robot_pkg/
        __init__.py            # Python module initializer

package.xml declares the package name, version, description, maintainer, license, and all build and runtime dependencies. Every ROS 2 message type or library your code uses must be listed here.

setup.py defines the Python package metadata and, critically, the console_scripts entry points that register your Python scripts as executable ROS 2 nodes:

# Entry point definition in setup.py
        entry_points={
            'console_scripts': [
            'cmd_vel_publisher = my_robot_pkg.cmd_vel_publisher:main',
            'odom_subscriber   = my_robot_pkg.odom_subscriber:main',
            ],
        },

Each entry point maps a command name (used to run the node) to a Python function (module_name:function_name).

Creating a C++ Package

# Create a C++ package
        ros2 pkg create --build-type ament_cmake \
        --dependencies rclcpp geometry_msgs my_cpp_pkg

The generated CMakeLists.txt file defines how to compile the source files, link against ROS 2 libraries, and install the resulting executables. The key CMake commands are find_package (locating dependencies), add_executable (defining build targets), ament_target_dependencies (linking ROS 2 packages), and install (placing executables where ROS 2 can find them).

1.6.3 Writing a Publisher Node

The publisher node is the most fundamental ROS 2 program. The following example creates a node that publishes velocity commands to control a differential drive robot.

# cmd_vel_publisher.py -- publishes velocity commands at 10 Hz
        import rclpy
        from rclpy.node import Node
        from geometry_msgs.msg import Twist
        import math
        
        class CmdVelPublisher(Node):
        def __init__(self):
        super().__init__('cmd_vel_publisher')
        
        # Declare parameters with default values
        self.declare_parameter('linear_speed', 0.5)
        self.declare_parameter('angular_speed', 0.0)
        self.declare_parameter('publish_rate',  10.0)
        
        # Create publisher on /cmd_vel topic with queue depth 10
        self.publisher = self.create_publisher(Twist, '/cmd_vel', 10)
        
        # Create timer for periodic publishing
        rate = self.get_parameter('publish_rate').value
        self.timer = self.create_timer(1.0 / rate, self.timer_callback)
        
        self.get_logger().info('CmdVel publisher started')
        
        def timer_callback(self):
        msg = Twist()
        msg.linear.x  = self.get_parameter('linear_speed').value
        msg.angular.z = self.get_parameter('angular_speed').value
        self.publisher.publish(msg)
        
        def main(args=None):
        rclpy.init(args=args)
        node = CmdVelPublisher()
        try:
        rclpy.spin(node)
        except KeyboardInterrupt:
        pass
        finally:
        node.destroy_node()
        rclpy.shutdown()
        
        if __name__ == '__main__':
        main()

Code Walkthrough:

  • Node initialization: The constructor calls super().__init__() with the node name, which registers this node with the ROS 2 system under the name cmd_vel_publisher.
  • Parameter declaration: Parameters are declared with default values. These can be overridden at launch time through launch files, command-line arguments, or YAML configuration files.
  • Publisher creation: create_publisher specifies the message type (Twist), topic name (/cmd_vel), and queue depth (10). The queue depth determines how many outgoing messages are buffered if subscribers cannot keep up.
  • Timer creation: create_timer schedules the callback function at the specified rate. The timer is managed by the executor and fires regardless of whether any subscriber is connected.
  • Spin loop: rclpy.spin enters the executor loop, which processes timer callbacks and incoming messages until the node is shut down via Ctrl+C or a programmatic shutdown signal.

1.6.4 Writing a Subscriber Node

The subscriber node receives messages from a topic and processes them. The following example subscribes to odometry data to monitor the robot’s position and orientation.

# odom_subscriber.py -- subscribes to odometry and logs robot pose
        import rclpy
        from rclpy.node import Node
        from nav_msgs.msg import Odometry
        import math
        
        class OdomSubscriber(Node):
        def __init__(self):
        super().__init__('odom_subscriber')
        
        # Create subscription to /odom topic
        self.subscription = self.create_subscription(
        Odometry,
        '/odom',
        self.odom_callback,
        10
        )
        
        self.get_logger().info('Odometry subscriber started')
        
        def odom_callback(self, msg):
        # Extract position from odometry message
        x = msg.pose.pose.position.x
        y = msg.pose.pose.position.y
        
        # Extract yaw angle from quaternion
        qz  = msg.pose.pose.orientation.z
        qw  = msg.pose.pose.orientation.w
        yaw = 2.0 * math.atan2(qz, qw)
        
        # Extract velocities
        linear_vel  = msg.twist.twist.linear.x
        angular_vel = msg.twist.twist.angular.z
        
        self.get_logger().info(
        f'Pose: ({x:.2f}, {y:.2f}, {math.degrees(yaw):.1f} deg) '
        f'Vel: ({linear_vel:.2f} m/s, {angular_vel:.2f} rad/s)'
        )
        
        def main(args=None):
        rclpy.init(args=args)
        node = OdomSubscriber()
        try:
        rclpy.spin(node)
        except KeyboardInterrupt:
        pass
        finally:
        node.destroy_node()
        rclpy.shutdown()
        
        if __name__ == '__main__':
        main()

Code Walkthrough:

  • Subscription creation: create_subscription specifies the message type (Odometry), topic name (/odom), callback function (odom_callback), and queue depth (10). The callback is invoked by the executor each time a new message arrives on the topic.
  • Quaternion to yaw conversion: The Odometry message stores orientation as a quaternion (). For a ground robot that only rotates about the vertical axis, the yaw angle can be extracted using the simplified formula yaw = 2 * atan2(qz, qw). This avoids importing a full quaternion library for the common case.
  • Message field access: The Odometry message contains nested structures. Position is accessed through msg.pose.pose.position, orientation through msg.pose.pose.orientation, and velocities through msg.twist.twist. The double nesting (pose.pose) exists because the outer layer includes a covariance matrix alongside the pose.

1.6.5 Using Launch Files

Launch files start multiple nodes simultaneously with configured parameters, topic remappings, and conditional logic. ROS 2 uses Python-based launch files for maximum flexibility.

# robot_launch.py -- launches publisher and subscriber with parameters
        from launch import LaunchDescription
        from launch.actions import DeclareLaunchArgument
        from launch.substitutions import LaunchConfiguration
        from launch_ros.actions import Node
        
        def generate_launch_description():
        
        # Declare launch arguments (can be overridden from command line)
        speed_arg = DeclareLaunchArgument(
        'linear_speed',
        default_value='0.3',
        description='Forward speed in m/s'
        )
        
        # Define the publisher node
        publisher_node = Node(
        package='my_robot_pkg',
        executable='cmd_vel_publisher',
        name='cmd_vel_publisher',
        parameters=[{
            'linear_speed': LaunchConfiguration('linear_speed'),
            'angular_speed': 0.0,
            'publish_rate':  10.0,
        }],
        output='screen'
        )
        
        # Define the subscriber node
        subscriber_node = Node(
        package='my_robot_pkg',
        executable='odom_subscriber',
        name='odom_subscriber',
        output='screen'
        )
        
        return LaunchDescription([
        speed_arg,
        publisher_node,
        subscriber_node,
        ])

Running the Launch File:

# Launch with default parameters
        ros2 launch my_robot_pkg robot_launch.py
        
        # Launch with overridden parameter
        ros2 launch my_robot_pkg robot_launch.py linear_speed:=0.8

Parameter Files

For complex systems with many parameters, YAML files provide a cleaner configuration approach:

# params.yaml -- parameter configuration file
        cmd_vel_publisher:
        ros__parameters:
        linear_speed: 0.5
        angular_speed: 0.1
        publish_rate:  20.0
        
        odom_subscriber:
        ros__parameters:
        log_frequency: 5.0

The YAML file is referenced in the launch file by passing it to the parameters argument of the Node action. This separates configuration from code, allowing the same nodes to be reused across different robots by swapping parameter files.

1.6.6 Visualizing with RViz2

RViz2 is the primary visualization tool for ROS 2. It renders 3D representations of robot models, sensor data, maps, planned paths, and coordinate frames by subscribing to standard ROS 2 topics.

Launching RViz2

# Start RViz2
        rviz2
        
        # Start RViz2 with a saved configuration
        rviz2 -d path/to/config.rviz

Key Display Types:

  • RobotModel: Renders the robot’s URDF (Unified Robot Description Format) model in 3D, showing links, joints, and their current configuration. Requires the robot_state_publisher node to broadcast joint transforms on tf2.
  • LaserScan: Visualizes LiDAR data from /scan as a point cloud or line display. Each range measurement is rendered as a point at the corresponding distance and angle, providing a visual representation of the robot’s surroundings.
  • TF: Displays coordinate frame axes and their parent-child relationships. Essential for verifying that sensor frames are correctly positioned relative to the robot base and that the odom-to-base_link transform is updating correctly.
  • Map: Renders occupancy grid maps from /map, showing free space (white), occupied cells (black), and unknown regions (gray).
  • Path: Displays planned paths from Nav2 as lines overlaid on the map.
  • Odometry: Renders the robot’s pose history as a trail of arrows, showing the trajectory the robot has followed.

Debugging with RViz2: When the robot behaves unexpectedly, RViz2 is the first diagnostic tool. If the robot model appears at the wrong location, the tf2 tree has an error. If LiDAR points do not align with walls in the map, the sensor transform is misconfigured. If the planned path passes through obstacles, the costmap is not receiving sensor data. Visual debugging through RViz2 is far more efficient than inspecting raw numeric data in terminal output.

1.6.7 Connecting to Gazebo Simulation

Gazebo provides a physics-based simulation environment where the complete robot software stack can be tested without hardware. The simulation publishes sensor data and subscribes to commands using the same topics and message types as the real robot, making the transition from simulation to hardware seamless.

The ros_gz_bridge

Gazebo and ROS 2 use different internal communication systems. The ros_gz_bridge package translates messages between them, mapping Gazebo topics to ROS 2 topics and vice versa. Each bridge is configured with the Gazebo topic name, ROS 2 topic name, and message type mapping.

# Launch a bridge for velocity commands and odometry
        ros2 run ros_gz_bridge parameter_bridge \
        /cmd_vel@geometry_msgs/msg/Twist@gz.msgs.Twist \
        /odom@nav_msgs/msg/Odometry@gz.msgs.Odometry

The @ symbols separate the topic name, ROS 2 message type, and Gazebo message type. The bridge direction is inferred from the message type pairing.

Spawning a Robot in Gazebo

A robot is defined by a URDF or SDF (Simulation Description Format) model that describes its geometry, mass properties, joints, sensors, and plugins. The model is spawned into a running Gazebo simulation:

# Launch Gazebo with an empty world
        ros2 launch ros_gz_sim gz_sim.launch.py
        
        # Spawn a robot model from a URDF file
        ros2 run ros_gz_sim create -file path/to/robot.urdf -name my_robot -z 0.1

Sending Commands to the Simulated Robot

Once the bridge is running, the simulated robot responds to the same /cmd_vel commands as a real robot:

# Drive the simulated robot forward
        ros2 topic pub /cmd_vel geometry_msgs/msg/Twist "{linear: {x: 0.5}, angular: {z: 0.0}}"
        
        # Verify odometry is being published
        ros2 topic echo /odom

The simulation loop operates as follows: the controller node publishes a Twist message on /cmd_vel; the bridge forwards it to Gazebo; Gazebo applies the velocity to the simulated robot model and computes the resulting physics; sensor plugins generate simulated LiDAR scans, camera images, and IMU data; the bridge forwards these sensor messages back to ROS 2; perception and planning nodes process the simulated sensor data exactly as they would process real sensor data.

1.6.8 Web-Based Control via rosbridge

The rosbridge suite provides a WebSocket interface to ROS 2, enabling web browsers and non-ROS applications to interact with the robot system. This is particularly useful for building monitoring dashboards, remote teleoperation interfaces, and cross-platform control applications.

Architecture

The rosbridge_server node opens a WebSocket server (default port 9090) that accepts JSON-formatted messages. Web clients connect to this server and can publish messages, subscribe to topics, call services, and interact with actions. The roslibjs JavaScript library provides a convenient API for web applications to communicate through rosbridge.

Starting the rosbridge Server

# Install rosbridge suite
        sudo apt install ros-humble-rosbridge-suite
        
        # Launch the WebSocket server
        ros2 launch rosbridge_server rosbridge_websocket_launch.xml

Web Client Example

A minimal web page that connects to rosbridge and sends velocity commands:

<!-- Include roslibjs library -->
        <script src="https://cdn.jsdelivr.net/npm/roslib/build/roslib.min.js"></script>
        
        <script>
        // Connect to rosbridge WebSocket server
        var ros = new ROSLIB.Ros({ url: 'ws://localhost:9090' });
        
        // Create a topic publisher for /cmd_vel
        var cmdVel = new ROSLIB.Topic({
            ros: ros,
            name: '/cmd_vel',
            messageType: 'geometry_msgs/msg/Twist'
        });
        
        // Publish a velocity command
        var twist = new ROSLIB.Message({
            linear:  { x: 0.5, y: 0.0, z: 0.0 },
            angular: { x: 0.0, y: 0.0, z: 0.3 }
        });
        cmdVel.publish(twist);
        
        // Subscribe to odometry
        var odomListener = new ROSLIB.Topic({
            ros: ros,
            name: '/odom',
            messageType: 'nav_msgs/msg/Odometry'
        });
        odomListener.subscribe(function(message) {
            console.log('Position: ' + message.pose.pose.position.x);
        });
        </script>

Key Consideration: rosbridge adds latency (typically 10–50 ms over WebSocket) and is not suitable for safety-critical real-time control. It is appropriate for supervisory control, monitoring, and teleoperation where moderate latency is acceptable. High-frequency control loops (motor PID, sensor fusion) should always run as native ROS 2 nodes.

Key ROS 2 Commands Reference

The following commands are used daily when developing and debugging ROS 2 applications:

# Build all packages in the workspace
        colcon build
        
        # Build a specific package
        colcon build --packages-select my_robot_pkg
        
        # Source the workspace after building
        source install/setup.bash
        
        # List all active topics
        ros2 topic list
        
        # Show message type and publisher/subscriber counts for a topic
        ros2 topic info /cmd_vel
        
        # Display messages on a topic in real time
        ros2 topic echo /odom
        
        # Publish a single message to a topic
        ros2 topic pub /cmd_vel geometry_msgs/msg/Twist "{linear: {x: 0.5}, angular: {z: 0.0}}"
        
        # Publish at a specific rate (10 Hz)
        ros2 topic pub --rate 10 /cmd_vel geometry_msgs/msg/Twist "{linear: {x: 0.5}, angular: {z: 0.0}}"
        
        # List all active nodes
        ros2 node list
        
        # Show information about a specific node
        ros2 node info /cmd_vel_publisher
        
        # List all available services
        ros2 service list
        
        # Call a service
        ros2 service call /reset_odom std_srvs/srv/Empty
        
        # List parameters of a node
        ros2 param list /cmd_vel_publisher
        
        # Get a parameter value
        ros2 param get /cmd_vel_publisher linear_speed
        
        # Set a parameter value at runtime
        ros2 param set /cmd_vel_publisher linear_speed 0.8
        
        # Record topic data to a bag file
        ros2 bag record -o my_bag /cmd_vel /odom /scan
        
        # Play back a recorded bag file
        ros2 bag play my_bag
        
        # View the tf2 frame tree
        ros2 run tf2_tools view_frames

Integration: Theory to Practice

The practical workflow presented in this module forms the development cycle used throughout the course. Every lab session follows the same pattern: write or modify nodes in the workspace, build with colcon, source the overlay, launch nodes, and verify behavior through RViz2 and command-line introspection. The publisher-subscriber pattern demonstrated here scales directly to full robotic systems: the cmd_vel publisher becomes a navigation controller, the odom subscriber becomes a state estimator, and launch files grow to orchestrate dozens of nodes with complex parameter configurations. Gazebo simulation allows testing before deploying to hardware, catching software errors in a safe environment where crashes cost nothing. The rosbridge interface extends the ROS 2 system to web technologies, enabling monitoring dashboards and teleoperation interfaces that run in any browser without installing ROS 2 on the client machine. Understanding these practical foundations is essential because every theoretical concept in subsequent modules (SLAM, path planning, control) is implemented and tested using exactly these tools and workflows.

Theoretical Design Choices

Why Python for prototyping and C++ for production: Python’s interpreted nature and dynamic typing enable rapid development — a new node can be written and tested in minutes without a compile step. However, Python’s Global Interpreter Lock and interpreted execution make it 10–100 slower than C++ for computationally intensive tasks. The standard practice is to prototype in Python, validate the algorithm, and then rewrite performance-critical nodes in C++. ROS 2’s build system supports mixed-language workspaces, and the topic-based communication means Python and C++ nodes interoperate transparently.

Why colcon over catkin: ROS 1 used catkin, which was tightly coupled to CMake and supported only C++ and Python through custom macros. Colcon is a meta-build tool that delegates to the appropriate build system (CMake, setuptools, or others) for each package. This clean separation allows ROS 2 packages to use standard build tools without ROS-specific modifications, making it easier to integrate non-ROS libraries and to build packages outside of a ROS 2 workspace.

Why simulation-first development: Building and testing directly on hardware is slow (deploy, observe, debug, repeat) and risky (software bugs can cause physical damage). Gazebo simulation provides deterministic, repeatable testing at speeds up to real time. Sensor noise models and physics simulation produce realistic behavior that transfers to hardware. The same code runs in simulation and on the real robot because both use identical ROS 2 topics and message types. The simulation-to-reality gap is managed by validating critical behaviors on hardware after simulation testing passes, not by skipping simulation entirely.

Why rosbridge uses JSON over WebSocket rather than native DDS: Web browsers cannot run DDS natively. WebSocket is the standard protocol for bidirectional communication between browsers and servers, and JSON is the universal data interchange format for web applications. The rosbridge design sacrifices the performance and QoS guarantees of DDS in exchange for universal accessibility from any platform that supports a web browser. For supervisory and monitoring applications, this trade-off is appropriate because the data rates are low and moderate latency is acceptable.