15  Tutorial - Wrapping vessel simulator in ROS2

Welcome aboard, aspiring ROS 2 navigator! In this tutorial, we’ll embark on an exciting journey through the seas of robotics, transforming our marine craft simulator from the previous tutorial into a full-fledged ROS 2 vessel. In this tutorial we shall learn the art of ROS 2 communication, package creation, odometry publishing, odometry subscribing, and more.

After completing this tutorial, you will be able to:

15.1 Getting Started

We will be using the same repository as the previous tutorial. Once the previous tutorial deadline is over, your instructor will update the upstream repository with the necessary files for this tutorial and raise a pull request on your team repository. You will need to pull in the changes from the upstream repository to your team repository to start this tutorial. The instructions to pull in the changes are given in Appendix B.

After you complete the steps shown in Appendix B to synchronize your repository with the upstream repository, navigate to the root directory of your repository. Pull the latest changes made to your team repository onto your local machine.

cd <your-repository-name>
git pull origin main

The relevant folder structure of the repository for this tutorial is shown below.

<your-repository-name>
├── ros2_ws/
│   ├── src/
│   │   ├── student/
│   │   │   ├── tut_02/
│   │   │   │   └── hello.py
│   │   │   ├── tut_03/
│   │   │   │   ├── hyd.yml
│   │   │   │   ├── input.yml
│   │   │   │   ├── simulate.py
│   │   │   │   ├── read_input.py
│   │   │   │   ├── class_vessel.py
│   │   │   │   ├── module_control.py
│   │   │   │   ├── module_kinematics.py
│   │   │   │   └── plot_results.py
│   │   │   └── tut_04/
│   │   │       └── Tutorial_04.md
├── README.md
├── ros2_run_devdocker.sh
├── ros2_run_devdocker.bat
└── LICENSE

Start your own branch from the main branch.

git checkout -b <your-name>

All your group members can work independently on their own branches. However, you will have to merge your branches back to the main branch before the end of the tutorial. We learnt how to do this in Chapter 13 using pull requests.

15.2 Understanding ROS 2 Topics

ROS 2 can be imagined as a vast ocean of information, with topics as the currents carrying messages between different parts of your robotic system. These topics are named channels that allow nodes (our program’s functional units) to exchange data.

Let’s start by exploring these currents using the command line:

  1. Open a new terminal and navigate to the root directory of your repository.
cd <your-repository-name>
  1. Start the docker container with the following command:
./ros2_run_devdocker.sh
  1. Source your ROS 2 installation:
source /opt/ros/humble/setup.bash
  1. List all active topics:
ros2 topic list
  1. Publish a message to a topic:
ros2 topic pub /sea_conditions std_msgs/String "data: 'Calm seas ahead'"
  1. Now let’s open a new terminal and connect to our docker container
docker exec -it oe3036 bash
  1. Source your ROS 2 installation:
source /opt/ros/humble/setup.bash
  1. Now let’s subscribe to the topic that we published to in the previous terminal:
ros2 topic echo /sea_conditions

You’ll see the message appear in your subscriber terminal. Congratulations, you’ve just sent your first message across the ROS 2 seas!

  1. Open a new terminal and connect to our docker container:
docker exec -it oe3036 bash
  1. Source your ROS 2 installation:
source /opt/ros/humble/setup.bash
  1. Now let’s get ROS2 to tell us what it knows about the topic:
ros2 topic info /sea_conditions --verbose

This should give you some information about the topic, including the type of message it carries and the number of publishers and subscribers. You should see something like this:

Type: std_msgs/msg/String

Publisher count: 1

Node name: _ros2cli_354
Node namespace: /
Topic type: std_msgs/msg/String
Endpoint type: PUBLISHER
GID: 01.0f.54.da.62.01.d6.c2.00.00.00.00.00.00.05.03.00.00.00.00.00.00.00.00
QoS profile:
  Reliability: RELIABLE
  History (Depth): UNKNOWN
  Durability: TRANSIENT_LOCAL
  Lifespan: Infinite
  Deadline: Infinite
  Liveliness: AUTOMATIC
  Liveliness lease duration: Infinite

Subscription count: 1

Node name: _ros2cli_834
Node namespace: /
Topic type: std_msgs/msg/String
Endpoint type: SUBSCRIPTION
GID: 01.0f.54.da.42.03.d5.fb.00.00.00.00.00.00.05.04.00.00.00.00.00.00.00.00
QoS profile:
  Reliability: RELIABLE
  History (Depth): UNKNOWN
  Durability: TRANSIENT_LOCAL
  Lifespan: Infinite
  Deadline: Infinite
  Liveliness: AUTOMATIC
  Liveliness lease duration: Infinite

We can see that there are two nodes - 1 publisher and 1 subscriber to the topic. We can also see that the publisher and subscriber nodes are named _ros2cli_354 and _ros2cli_834 respectively in this output (on your docker container, you will see different node names). These nodes have been created by the ros2 topic command that we used earlier for publishing and subscribing to the topic. Nodes are the entities that can publish and subscribe to topics.

15.3 Creating a ROS 2 Workspace and Package

Now that we understand the basics of communication, let’s construct a ROS 2 package for our marine craft simulator that we developed in the previous tutorial. Here we will create our own nodes using Python to publish and subscribe to the topic. For organizing our code we will create a workspace and a package.

  1. Navigate to the tut_04 directory:
cd /workspaces/mavlab/ros2_ws/src/student/tut_04
  1. Create a new package:
ros2 pkg create --build-type ament_python mav_simulator --dependencies rclpy std_msgs nav_msgs sensor_msgs geometry_msgs class_vessel --license MIT

Let’s break down the command:

  • ros2 pkg create: This command creates a new ROS 2 package.
  • --build-type ament_python: This specifies that we are creating a Python package.
  • mav_simulator: This is the name of our package.
  • --dependencies: This specifies the dependencies for our package.
  • rclpy std_msgs nav_msgs sensor_msgs geometry_msgs class_vessel: These are the dependencies for our package.
  1. Navigate into the package:
cd mav_simulator/mav_simulator

Your folder structure after creating the package should look like this:

ros2_ws/
├── src/
│   └── student/
│       ├── tut_03/
│       │   ├── hyd.yml
│       │   ├── input.yml
│       │   ├── simulate.py
│       │   ├── read_input.py
│       │   ├── class_vessel.py
│       │   ├── module_control.py
│       │   ├── module_kinematics.py
│       │   └── plot_results.py
│       └── tut_04/
│           ├── mav_simulator/
│           │   ├── mav_simulator/
│           │   │   └── __init__.py
│           │   ├── setup.py
│           │   ├── package.xml
│           │   └── setup.cfg
│           └── Tutorial_04.md
├── README.md
├── ros2_run_devdocker.sh
└── ros2_run_devdocker.bat

15.4 Setting Up the Python Node to Publish Odometry Data

Now, let’s adapt our existing marine craft simulator to work with ROS 2. We’ll create a new Python script that will act as our ROS 2 node. The node will be responsible for simulating the vessel and publishing the vessel’s odometry data over the /mav_odom topic.

Create a new file mav_simulate.py in the /workspaces/mavlab/ros2_ws/src/student/tut_04/mav_simulator/mav_simulator directory:

cd /workspaces/mavlab/ros2_ws/src/student/tut_04/mav_simulator/mav_simulator
touch mav_simulate.py

Copy the following content into the file:

Code
import rclpy
from rclpy.node import Node
from nav_msgs.msg import Odometry
from geometry_msgs.msg import Quaternion
from std_msgs.msg import Float64

import sys
sys.path.append('/workspaces/mavlab/')

from ros2_ws.src.student.tut_03.class_vessel import Vessel
from ros2_ws.src.student.tut_03.module_kinematics import eul_to_quat
from ros2_ws.src.student.tut_03.read_input import read_input
import numpy as np

class MavNode(Node):
    def __init__(self, vessel_params, hydrodynamic_data):
        # Initialize the node
        super().__init__('mav_node')

        # Initialize the vessel
        self.vessel = Vessel(vessel_params, hydrodynamic_data, ros_flag=True)
        
        # Create a publisher to publish the vessel odometry
        self.publisher = self.create_publisher(Odometry, 'mav_odom', 10)

        # Create a publisher to publish the rudder angle
        self.rudder_publisher = self.create_publisher(Float64, 'mav_rudder', 10)

        # Create a subscriber to receive the rudder angle
        self.rudder_subscriber = self.create_subscription(Float64, 'mav_rudder_command', self.rudder_callback, 10)

        # Create a timer to call the timer_callback function at the vessel time step
        self.timer = self.create_timer(self.vessel.dt, self.timer_callback)        

    def timer_callback(self):
        
        # Step the vessel forward in time
        self.vessel.step()
        
        # Create an odometry message
        odom_msg = Odometry()
        odom_msg.header.stamp = self.get_clock().now().to_msg()
        odom_msg.header.frame_id = 'odom'
        odom_msg.child_frame_id = 'base_link'
        
        # Set position
        odom_msg.pose.pose.position.x = self.vessel.current_state[6]
        odom_msg.pose.pose.position.y = self.vessel.current_state[7]
        odom_msg.pose.pose.position.z = self.vessel.current_state[8]
        
        # Set orientation
        q = eul_to_quat(self.vessel.current_state[9:12], order='ZYX', deg=False)
        odom_msg.pose.pose.orientation = Quaternion(x=q[1], y=q[2], z=q[3], w=q[0])
        
        # Set linear velocity
        odom_msg.twist.twist.linear.x = self.vessel.current_state[0]
        odom_msg.twist.twist.linear.y = self.vessel.current_state[1]
        odom_msg.twist.twist.linear.z = self.vessel.current_state[2]
        
        # Set angular velocity
        odom_msg.twist.twist.angular.x = self.vessel.current_state[3]
        odom_msg.twist.twist.angular.y = self.vessel.current_state[4]
        odom_msg.twist.twist.angular.z = self.vessel.current_state[5]
        
        # Publish the odometry message
        self.publisher.publish(odom_msg)

        # Publish rudder angle
        rudder_msg = Float64()
        rudder_msg.data = self.vessel.current_state[12] * 180 / np.pi
        self.rudder_publisher.publish(rudder_msg)

        # Log info
        self.get_logger().info(
            f'\nTime: {self.vessel.t:.2f}s\n'
            f'Position (x,y,z): [{self.vessel.current_state[6]:.2f}, {self.vessel.current_state[7]:.2f}, {self.vessel.current_state[8]:.2f}]\n'
            f'Euler angles (phi,theta,psi): [{self.vessel.current_state[9]*180/np.pi:.2f}, {self.vessel.current_state[10]*180/np.pi:.2f}, {self.vessel.current_state[11]*180/np.pi:.2f}]\n'
            f'Velocity (u,v,w): [{self.vessel.current_state[0]:.2f}, {self.vessel.current_state[1]:.2f}, {self.vessel.current_state[2]:.2f}]\n'
            f'Angular velocity (p,q,r): [{self.vessel.current_state[3]:.2f}, {self.vessel.current_state[4]:.2f}, {self.vessel.current_state[5]:.2f}]\n'
            f'Rudder Command: {self.vessel.current_state[12] * 180 / np.pi:.2f} deg\n'
        )

    def rudder_callback(self, msg):
        # Set the commanded rudder angle
        self.vessel.delta_c = msg.data

def main(args=None):
    try:
        # Initialize the ROS 2 node
        rclpy.init(args=args)
        vessel_params, hydrodynamic_data = read_input()
        vessel_node = MavNode(vessel_params, hydrodynamic_data)
        rclpy.spin(vessel_node)

    except KeyboardInterrupt:
        # Destroy the node and shutdown ROS 2
        vessel_node.destroy_node()
        rclpy.shutdown()

    finally:
        # Destroy the node and shutdown ROS 2
        vessel_node.destroy_node()
        rclpy.shutdown()

if __name__ == '__main__':
    main()

Let’s break down the important parts of the code:

The file contains a class MavNode that inherits from the Node class of ROS2. The __init__ method initializes the node, the vessel, and the publishers and subscribers. The timer_callback method is called at the vessel time step and publishes the odometry data. The rudder_callback method is called when a rudder command is received. Note that here we make a distinction between the commanded rudder angle and the actual rudder angle. The commanded rudder angle is the angle that the node will try to achieve, while the actual rudder angle is the angle that the vessel is currently at.

The main function initializes the ROS 2 node, reads the vessel parameters and hydrodynamic data, and spins the node. Spinning the node means that the node will run continuously and handle the communication between the node and the ROS 2 system. Unlike a regular Python script that runs to completion, a spun node will continue to run until the node is stopped.

Note that the MavNode class wraps a Vessel object that is defined in the class_vessel.py file. This Vessel object is the same as the one we defined in the previous tutorial. Instead of looping through the time steps as we did in the previous tutorial, here we use a timer to call the timer_callback method at the vessel time step. This ensures that the vessel is simulated at the correct time step and simulates the vessel in real-time. The node will publish the vessel’s odometry data to the /mav_odom topic and the actual rudder angle to the /mav_rudder topic every time the timer_callback is called.

15.5 Running Our ROS 2 Package

Now that our ship is built and rigged, it’s time to set sail! Let’s update our package configuration and create a launch file to run our node.

  1. Update setup.py in the mav_simulator directory:
Code
from setuptools import setup
import os
from glob import glob

package_name = 'mav_simulator'

setup(
    name=package_name,
    version='0.0.1',
    packages=[package_name],
    data_files=[
        ('share/ament_index/resource_index/packages',
            ['resource/' + package_name]),
        ('share/' + package_name, ['package.xml']),
        (os.path.join('share', package_name, 'launch'), glob('launch/*.py')),
    ],
    install_requires=['setuptools'],
    zip_safe=True,
    maintainer='Abhilash Somayajula',
    maintainer_email='abhilash@iitm.ac.in',
    description='MAV Lab - Vessel simulator for ROS 2',
    license='MIT',
    tests_require=['pytest'],
    entry_points={
        'console_scripts': [
            'mav_simulate = mav_simulator.mav_simulate:main',
            # 'mav_control = mav_simulator.mav_control:main'
        ],
    },
)
  1. Create a launch directory in the mav_simulator package:
cd /workspaces/mavlab/ros2_ws/src/student/tut_04/mav_simulator
mkdir launch
cd launch
  1. Create a launch file mav_launch.py in the launch directory with the following content:
Code
from launch import LaunchDescription
from launch_ros.actions import Node

def generate_launch_description():
    return LaunchDescription([
        Node(
            package='mav_simulator',
            executable='mav_simulate',
            name='mav_simulate',
            output='screen'
        )
    ])
  1. Build and source the workspace:
cd /workspaces/mavlab/ros2_ws
colcon build
source install/setup.bash
  1. Launch the marine craft simulator:
ros2 launch mav_simulator mav_launch.py

This should start the vessel simulator and you should see the vessel’s odometry data being published to the /mav_odom topic. The node will also log the vessel’s current state to the terminal. The terminal should look like this:

[INFO] [launch]: All log files can be found below /home/mavlab/.ros/log/2025-02-10-12-59-45-647703-iit-m-2558
[INFO] [launch]: Default logging verbosity is set to INFO
[INFO] [mav_simulate-1]: process started with pid [2559]
[mav_simulate-1] [INFO] [1739192386.338351042] [mav_simulate]: 
[mav_simulate-1] Time: 0.10s
[mav_simulate-1] Position (x,y,z): [0.05, 0.00, 0.00]
[mav_simulate-1] Euler angles (phi,theta,psi): [0.00, 0.00, 0.00]
[mav_simulate-1] Velocity (u,v,w): [0.50, 0.00, 0.00]
[mav_simulate-1] Angular velocity (p,q,r): [0.00, 0.00, 0.00]
[mav_simulate-1] Rudder Command: 0.00 deg
[mav_simulate-1] 
[mav_simulate-1] [INFO] [1739192386.402897739] [mav_simulate]: 
[mav_simulate-1] Time: 0.20s
[mav_simulate-1] Position (x,y,z): [0.10, 0.00, 0.00]
[mav_simulate-1] Euler angles (phi,theta,psi): [0.00, 0.00, 0.00]
[mav_simulate-1] Velocity (u,v,w): [0.50, 0.00, 0.00]
[mav_simulate-1] Angular velocity (p,q,r): [0.00, 0.00, 0.00]
[mav_simulate-1] Rudder Command: 0.00 deg
[mav_simulate-1]

15.6 Understanding Odometry Messages

As our marine craft sails through the ROS 2 seas, it’s constantly reporting its position and velocity through Odometry messages. These messages are crucial for navigation and localization.

Let’s examine the Odometry message our node is publishing:

  1. In a new terminal connected to our docker container, subscribe to the odometry topic:
source /opt/ros/humble/setup.bash
ros2 topic echo /mav_odom
  1. You’ll see a stream of Odometry messages containing:
  • Header: Timestamp and frame information
  • Pose: Position and orientation of the marine craft and their covariance matrices
  • Twist: Linear and angular velocities and their covariance matrices

This data allows other nodes in our system to track the marine craft’s movement and make decisions based on its current state. Notice that the covariance matrices are zero, which means that we have no uncertainty in our measurements. This is so as we are simulating the vessel in a perfect environment. In reality, the vessel’s position and velocity will need to be estimated from noisy measurements from sensors. The odometry estimate will be obtained by fusing the noisy measurements from the sensors along with the vessel dynamics model through a process called state estimation. In this course we will get a glimpse of this process when we learn about state observers and Kalman filters. However, we will not be implementing Kalman filters in this course.

  1. Check the rate of publication of the odometry messages:
ros2 topic hz /mav_odom

You should see something like this:

average rate: 9.994
        min: 0.100s max: 0.101s std dev: 0.00030s window: 11
average rate: 10.000
        min: 0.100s max: 0.101s std dev: 0.00036s window: 22
average rate: 10.000
        min: 0.100s max: 0.101s std dev: 0.00033s window: 32

This means that the node is publishing the odometry messages at a rate of 10 Hz.

15.7 Setting up the Python Node to Subscribe to the Odometry Topic

So far, we have the simulator publishing the odometry data to the mav_odom topic. The simulator also subscribes to the rudder command from the mav_rudder_command topic. Let’s now create a new node that will subscribe to the odometry topic and publish a rudder command to the mav_rudder_command topic.

  1. Create a new file mav_control.py in the mav_simulator package:
cd /workspaces/mavlab/ros2_ws/src/student/tut_04/mav_simulator/mav_simulator
touch mav_control.py
  1. Copy the following content into the file:
Code
import rclpy
from rclpy.node import Node
from nav_msgs.msg import Odometry
from std_msgs.msg import Float64

import sys
sys.path.append('/workspaces/mavlab/')

from ros2_ws.src.student.tut_03 import module_control as con
from ros2_ws.src.student.tut_03.module_kinematics import quat_to_eul
from ros2_ws.src.student.tut_03.read_input import read_input
import numpy as np

class MAVController(Node):
    def __init__(self, control_mode):
        super().__init__('mav_controller')
        
        # Create subscriber for odometry
        self.odom_sub = self.create_subscription(
            Odometry,
            'mav_odom',
            self.odom_callback,
            10)
            
        # Create publisher for rudder command
        self.rudder_pub = self.create_publisher(
            Float64,
            'mav_rudder_command',
            10)
            
        # Initialize time
        self.start_time = self.get_clock().now()
        
        # Select control mode
        self.control_mode = control_mode
        
        self.get_logger().info('MAV Controller initialized')

    def odom_callback(self, msg):
        # Get current time in seconds
        current_time = self.get_clock().now()
        t = (current_time - self.start_time).nanoseconds / 1e9

        quat = np.array([
            msg.pose.pose.orientation.w,
            msg.pose.pose.orientation.x,
            msg.pose.pose.orientation.y,
            msg.pose.pose.orientation.z
        ])

        eul = quat_to_eul(quat, order='ZYX')
        
        # Create state vector from odometry
        state = np.array([            
            msg.twist.twist.linear.x,
            msg.twist.twist.linear.y,
            msg.twist.twist.linear.z,
            msg.twist.twist.angular.x,
            msg.twist.twist.angular.y,
            msg.twist.twist.angular.z,
            msg.pose.pose.position.x,
            msg.pose.pose.position.y,
            msg.pose.pose.position.z,
            eul[0],
            eul[1],
            eul[2],
            0.0 # rudder angle
        ])
        
        # Calculate control command
        rudder_cmd = self.control_mode(t, state)
        
        # Publish command
        cmd_msg = Float64()
        cmd_msg.data = float(rudder_cmd)
        self.rudder_pub.publish(cmd_msg)
        
        # Log info
        self.get_logger().info(
            f'\nTime: {t:.2f}s\n'            
            f'Rudder Command: {rudder_cmd*180/np.pi:.2f} deg\n'
        )

def main(args=None):
    rclpy.init(args=args)
    
    vessel_params, _ = read_input()
    control_type = vessel_params['control_type']
    
    if control_type == 'switching_rudder':
        control_mode = con.switching_rudder
    elif control_type == 'fixed_rudder':
        control_mode = con.fixed_rudder
    elif control_type == 'zero_rudder':
        control_mode = lambda t, state: con.fixed_rudder(t, state, rudder_angle=0.0)
    else:
        raise ValueError(f"Invalid control type: {control_type}")
    
    controller = MAVController(control_mode)
    rclpy.spin(controller)
    controller.destroy_node()
    rclpy.shutdown()

if __name__ == '__main__':
    main()

Let’s break down the important parts of the code:

The MAVController class inherits from the Node class of ROS2. The __init__ method initializes the node, the subscriber and publisher for the odometry and rudder command topics respectively. The odom_callback method is called when a new odometry message is received. The main function initializes the ROS 2 node, reads the vessel parameters and control type, and spins the node.

The odom_callback method extracts the current time, the vessel’s state from the odometry message, and the control command from the control mode function. The control command is then published to the rudder command topic.

The main function initializes the ROS 2 node, reads the vessel parameters and control type, and spins the node.

Notice that the control_mode is a function that takes the current time and the vessel’s state as arguments and returns the control command. This function is defined in the module_control.py file from the previous tutorial.

  1. Update setup.py in the mav_simulator package to include the new node:
Code
from setuptools import setup
import os
from glob import glob

package_name = 'mav_simulator'

setup(
    name=package_name,
    version='0.0.1',
    packages=[package_name],
    data_files=[
        ('share/ament_index/resource_index/packages',
            ['resource/' + package_name]),
        ('share/' + package_name, ['package.xml']),
        (os.path.join('share', package_name, 'launch'), glob('launch/*.py')),
    ],
    install_requires=['setuptools'],
    zip_safe=True,
    maintainer='Abhilash Somayajula',
    maintainer_email='abhilash@iitm.ac.in',
    description='MAV Lab - Vessel simulator for ROS 2',
    license='MIT',
    tests_require=['pytest'],
    entry_points={
        'console_scripts': [
            'mav_simulate = mav_simulator.mav_simulate:main',
            'mav_control = mav_simulator.mav_control:main'      # <---- ADD THIS LINE
        ],
    },
)
  1. Update the launch file mav_launch.py to include the new node:
Code
from launch import LaunchDescription
from launch_ros.actions import Node

def generate_launch_description():
    return LaunchDescription([
        Node(
            package='mav_simulator',
            executable='mav_simulate',
            name='mav_simulate',
            output='screen'
        ),
        Node(
            package='mav_simulator',
            executable='mav_control',
            name='mav_control',
            output='screen'
        )
    ])
  1. Launch the marine craft simulator node and the control node:
ros2 launch mav_simulator mav_launch.py

You should see the rudder command being published to the mav_rudder_command topic and vessel simulator showing the response of the vessel to the rudder command through the odometry data published to the mav_odom topic. The terminal should be showing the logs from both nodes and should look similar to this:

[mav_simulate-1] [INFO] [1739193929.216482603] [mav_simulate]: 
[mav_simulate-1] Time: 40.30s
[mav_simulate-1] Position (x,y,z): [14.03, -12.03, 0.00]
[mav_simulate-1] Euler angles (phi,theta,psi): [0.00, 0.00, -10.78]
[mav_simulate-1] Velocity (u,v,w): [0.50, 0.02, 0.00]
[mav_simulate-1] Angular velocity (p,q,r): [0.00, 0.00, 0.10]
[mav_simulate-1] Rudder Command: 4.82 deg
[mav_simulate-1] 
[mav_control-2] [INFO] [1739193929.216846193] [mav_control]: 
[mav_control-2] Time: 40.48s
[mav_control-2] Rudder Command: 5.00 deg
[mav_control-2]

Congratulations! You’ve successfully transformed your marine craft simulator into a full-fledged ROS 2 node. You’ve mastered the arts of topic communication, package creation, node programming, odometry publishing, and odometry subscribing. Remember that this is just the beginning of your ROS 2 journey. There are many more aspects of ROS 2 to explore, from services and actions to more complex navigation and control algorithms.

15.8 Testing the Code

To test the code, commit your changes and push them to the remote repository. This will trigger the GitHub Actions workflow to run the tests. You can view the results of the tests by navigating to the “Actions” tab in the GitHub repository. If the tests pass, the workflow will be marked as successful. If the tests fail, you can view the detailed logs to see which tests failed and why. The GitHub workflow will also show the difference between the expected output and the output generated by your code.

15.9 Evaluation

The evaluation of the tutorial will be performed in the classroom. Notice that this is a group activity. You will be working in a group of 4 students. The evaluation will be performed on the group level. In order to pass the tutorial, the group must pass all the tests in the GitHub Actions workflow and also be able to answer all the questions during the evaluation in the classroom. Those who are not present in the classroom on the day of evaluation will not be able to participate in the evaluation and will automatically fail the tutorial.

While one of the objectives of these tutorials is to give you hands on experience with coding and practical implementation, the more important goal is to help you relate what you are doing in your tutorials to the underlying concepts discussed in the class. Therefore, while you are working on the implementation tasks, make sure you are also thinking about the underlying concepts and how they are being applied in the code.

Some questions to think about while you are working on the implementation tasks:

  • Is your code able to pass all the tests in the GitHub Actions workflow?
  • Are you able to show running of the ROS2 wrapped simulator live during the evaluation?
  • What are the topics and nodes that are being published and subscribed to in the simulation?
  • How is a node different from a topic?
  • What is type of ROS2 message used for publishing and subscribing to the odometry data?
  • What is type of ROS2 message used for publishing and subscribing to the rudder command?
  • How did the code from the previous tutorial change in this tutorial to allow for ROS2 communication?
  • How was the simulation code separated into two nodes in this tutorial? What does each node do?

15.10 Instructor Feedback through Pull Requests

Commit your changes to the remote repository often. This will help you to track your progress and also help you to revert to a working version if you make a mistake. In addition, the instructor and TA will be able to provide you with feedback on your code through the Feeback pull request available in your repository. Please do not close this pull request.