Record and Play Back Data Using ROS 2 Bag – ROS 2 Jazzy

In this tutorial, we will explore ROS 2 Bag, a powerful tool for recording and playing back data in a ROS 2 system. We will learn about the purpose of ROS 2 Bag, its real-world applications, and how to use it effectively. By the end of this tutorial, you will be able to record data from your ROS 2 system, analyze the recorded data, play it back for various purposes, and convert it to a CSV file (i.e. comma-separated text file).

Prerequisites

All my code for this project is located here on GitHub.

Purpose of ROS 2 Bag

ROS 2 Bag allows you to record data from various topics and services in your ROS 2 system and store it in a file format called a “bag.” This recorded data can be later played back, analyzed, or used for debugging and testing purposes. 

ROS 2 Bag is particularly useful when you need to capture data from sensors, robot states, or any other relevant information during runtime.

Real-World Applications

The following are several real-world applications of ROS 2 bag:

  • Autonomous Vehicle Development: ROS 2 Bag can be used to record sensor data, such as LIDAR scans, camera images, and GPS information, during autonomous vehicle testing. This recorded data can be used for offline analysis, algorithm development, and simulation purposes.
  • Robot Debugging: When troubleshooting issues with a robot, ROS 2 Bag can be used to record data from various topics during the occurrence of the problem. This recorded data can be analyzed later to identify the root cause of the issue and facilitate debugging efforts.
  • Dataset Creation: ROS 2 Bag is often used to create datasets for machine learning and computer vision applications. By recording data from sensors and ground truth information, researchers can build datasets for tasks such as object detection, semantic segmentation, and localization.

I will be working with the mobile robot I set up in this tutorial. I want to record the velocity commands, and then play them back.

Setup

Open a terminal window, and launch your robot.

I will type the following command:

x3 

or

bash ~/ros2_ws/src/yahboom_rosmaster/yahboom_rosmaster_bringup/scripts/rosmaster_x3_gazebo.sh

Choose a Topic

In another terminal window, type:

cd ~/Downloads/
mkdir bag_files && cd bag_files

Check out the active topics:

ros2 topic list

The /mecanum_drive_controller/cmd_vel topic contains the velocity messages.

ros2 bag record

Open a new terminal window, and move the robot in a square-shaped pattern using this command:

ros2 run yahboom_rosmaster_system_tests square_mecanum_controller

To start recording data, use the ros2 bag record command followed by the topic(s) you want to record. 

For example, to record the /mecanum_drive_controller/cmd_vel topic, run:

ros2 bag record /mecanum_drive_controller/cmd_vel

The recording will start, and you will see output indicating the number of messages recorded for each topic.

1-record-ros2-bag

To stop the recording, press Ctrl + C in the terminal where the ros2 bag record command is running.

Close down the robot and the square mecanum controller as well using CTRL + C.

The recorded data will be saved in a bag file with a name format of rosbag2_year_month_day-hour_minute_second. This folder contains a metadata.yaml folder and the bag file.

dir

Here is what I see:

rosbag2_2024_11_26-07_00_48

The metadata.yaml file provides important information about the recorded bag file, such as the duration of the recording, the starting time, the number of messages recorded, and the details of each recorded topic. This metadata helps in understanding the contents of the bag file and can be useful for analysis and playback purposes.

If you want to record multiple topics and change the name of the topic, type the following command:

ros2 bag record -o <bag_file_name> <topic1> <topic2> ... <topicN>

If you want to record all topics, you would type this command:

ros2 bag record -a

You can find other options here at the ROS 2 Bag GitHub page.

ros2 bag info

To view information about a recorded bag file, use the `ros2 bag info` command followed by the path to the bag file. For example:

ros2 bag info rosbag2_2024_11_26-07_00_48

The command will display information such as the duration of the recording, the number of messages recorded for each topic, and the start and end times of the recording.

2-ros2-bag-info

ros2 bag play

Open a terminal window, and launch your robot.

I will type the following command:

x3 

or

bash ~/ros2_ws/src/yahboom_rosmaster/yahboom_rosmaster_bringup/scripts/rosmaster_x3_gazebo.sh

To play back a recorded bag file, use the ros2 bag play command followed by the path to the bag file. 

cd ~/Downloads/bag_files

Run the bag file:

ros2 bag play rosbag2_2024_11_26-07_00_48

You can use the –loop option to play the bag file in a loop:

ros2 bag play rosbag2_2024_11_26-07_00_48 --loop

Let’s play it in a loop.

The recorded data will be played back, and you will see the messages being published to the respective topic.

ros2 topic echo /mecanum_drive_controller/cmd_vel

Your robot will start moving as well.

To pause the playback, press the spacebar in the terminal where the ros2 bag play command is running. Your robot will continue based on the last velocity message it received.

Press the spacebar again to resume playback.

To stop the playback, press Ctrl+C in all terminal windows.

Convert the ROS 2 Bag File to a CSV File

Let’s convert the ROS 2 bag file into CSV format. 

cd ~/Downloads/bag_files/rosbag2_2024_11_26-07_00_48

If you take a look inside the ROS 2 bag folder, you see two files.

  • rosbag2_2024_11_26-07_00_48_0.mcap: The ROS 2 bag file
  • metadata.yaml: This is a supplementary file that provides extra detail about the data in the .mcap file. It is written in YAML format to make it easy for both humans and computers to read and understand.

Now let’s convert the ROS 2 bag file (in mcap format, the default for ROS 2) into .csv format.

First, type this command to install the package we need (it is usually already installed):

sudo apt-get install ros-${ROS_DISTRO}-rosbag2-storage-mcap
cd ~/Downloads/bag_files/

Create a new file called ros2bag_to_csv.py.

Add this code.

#!/usr/bin/env python3
"""
Convert a ROS 2 bag file to CSV format.

This script reads messages from a ROS 2 bag file and converts them to CSV format,
creating separate CSV files for each topic in the bag.

Usage:
  python3 ros2bag_to_csv.py <input_bag_path> <output_folder>

Subscription Topics:
    None (reads from bag file)

Publishing Topics:
    None (writes to CSV files)

:author: Addison Sears-Collins
:date: November 26, 2024
"""

import argparse
import csv
import os

from rclpy.serialization import deserialize_message
from rosidl_runtime_py.utilities import get_message
import rosbag2_py
from std_msgs.msg import String


def read_messages(input_bag: str):
    """
    Read messages from a ROS 2 bag file.

    :param input_bag: Path to the input bag file.
    :return: A generator yielding (topic, message, timestamp) tuples.
    """
    # Create a SequentialReader to read messages from the bag file
    reader = rosbag2_py.SequentialReader()

    # Open the bag file for reading
    reader.open(
        rosbag2_py.StorageOptions(uri=input_bag, storage_id="mcap"),
        rosbag2_py.ConverterOptions(
            input_serialization_format="cdr", output_serialization_format="cdr"
        ),
    )

    # Get all the topic types available in the bag file
    topic_types = reader.get_all_topics_and_types()

    def typename(topic_name):
        """
        Get the message type for a given topic.

        :param topic_name: The name of the topic.
        :return: The message type as a string.
        :raises ValueError: If the topic is not found in the bag.
        """
        # Iterate through the topic types to find the matching topic name
        for topic_type in topic_types:
            if topic_type.name == topic_name:
                return topic_type.type
        raise ValueError(f"topic {topic_name} not in bag")

    # Iterate through the messages in the bag file
    while reader.has_next():
        topic, data, timestamp = reader.read_next()
        msg_type = get_message(typename(topic))
        msg = deserialize_message(data, msg_type)
        yield topic, msg, timestamp

    # Clean up the reader
    del reader


def main():
    """
    Main function to parse arguments and convert ROS 2 bag to CSV.
    """
    # Create an argument parser
    parser = argparse.ArgumentParser(description="Convert ROS 2 bag to CSV")
    parser.add_argument("input", help="Input bag path (folder) to read from")
    parser.add_argument("output", help="Output folder to save CSV files")

    # Parse the command-line arguments
    args = parser.parse_args()

    # Create the output folder if it doesn't exist
    if not os.path.exists(args.output):
        os.makedirs(args.output)

    # Initialize dictionaries to store topic files and writers
    topic_files = {}
    topic_writers = {}

    # Iterate through the messages in the bag file
    for topic, msg, timestamp in read_messages(args.input):
        # If the topic hasn't been processed before, create a new CSV file and writer
        if topic not in topic_files:
            output_file = os.path.join(args.output, topic.replace('/', '_') + '.csv')
            topic_files[topic] = open(output_file, mode='w',
                                      newline='', encoding='utf-8')
            topic_writers[topic] = csv.writer(topic_files[topic])
            topic_writers[topic].writerow(['timestamp', 'data'])

        # Write the message data to the corresponding CSV file
        if isinstance(msg, String):
            topic_writers[topic].writerow([timestamp, msg.data])
        else:
            topic_writers[topic].writerow([timestamp, str(msg)])

    # Close all the topic files
    for file in topic_files.values():
        file.close()


if __name__ == "__main__":
    main()

Save the file.

Change the file’s permissions:

chmod +x ros2bag_to_csv.py 

Run the script:

python3 ros2bag_to_csv.py rosbag2_2024_11_26-07_00_48 myros2bag

Here is the syntax:

python3 ros2bag_to_csv.py <ROS 2 bag folder> <desired output folder>
cd myros2bag

Inside this folder you will see your .csv file.

3-csv-file-from-ros2-bag

That’s it!

You can now use this knowledge to capture and analyze data from your ROS 2 system, debug issues, and create datasets for various applications.

Keep building!

How to Create an Action – ROS 2 Jazzy

In this tutorial, we will create a ROS 2 action. A ROS 2 action is a communication interface that allows a node to send a goal to another node to perform a long-running task and receive feedback while the task is in progress and a result when the task is completed.

You will build this by the end of this tutorial:

rotating-mecanum-wheel-robot-gazebo

A ROS 2 action is useful for tasks that take a while to complete and where you want to know the progress and be able to cancel the task if needed. Let’s take a look at some real-world examples.

Real-World Applications

  • Robot Vacuum Cleaner: Consider a robot vacuum cleaner. Instead of just starting to clean, let’s say we want to clean a specific room. This might take some time, and we want to get updates on the cleaning progress. We create an action called clean_room. This action will give us updates like “10% complete”, “50% complete”, and “finished”, and we can even cancel the cleaning if we change our mind.
  • Autonomous Docking to a Charging Station: Consider a mobile robot that needs to autonomously dock to recharge its batteries. This process involves multiple steps and precise positioning. We create an action called dock_to_charger. The action provides continuous feedback about the robot’s progress, such as “approaching charging station”, “aligning with dock”, “making final approach”, and “charging connection established”. The operation can be cancelled if obstacles are detected or if alignment fails. This is particularly useful because docking requires careful coordination and monitoring – the robot needs to detect the charging station, align itself precisely, and verify a successful connection, all while providing status updates and handling potential failures.
  • Kitchen Robot Chef: Imagine a mobile robot with an arm that prepares meals in a kitchen. The robot needs to move around, grab ingredients, and use cooking equipment. We create an action called prepare_dish. This action provides feedback like “getting ingredients”, “chopping vegetables”, “stirring pot”, and “plating food”. The cooking process can be cancelled if ingredients are missing or if safety limits are exceeded.

When to Use Actions vs. Publishers and Subscribers?

Publishers and subscribers are the basic tools for continuous, one-way communication in ROS 2. For example, a temperature sensor constantly publishing the current temperature or a motor constantly receiving speed commands. 

However, sometimes we need to monitor the progress of a long-running task and have the ability to cancel it if needed. ROS 2 actions work well for this use case.

Prerequisites

All my code for this project is located here on GitHub.

Create a Package

Navigate to the ~/ros2_ws/src/yahboom_rosmaster directory and create the yahboom_rosmaster_msgs package.

cd ~/ros2_ws/src/yahboom_rosmaster
ros2 pkg create --build-type ament_cmake \
                --license BSD-3-Clause \
                --maintainer-name ubuntu \
                --maintainer-email automaticaddison@todo.com \
                yahboom_rosmaster_msgs

Format of an Action

In ROS 2, an action is defined in a .action file, which consists of three parts: 

  • The goal (what you want to happen)
  • The result (what happened in the end)
  • The feedback (updates while it’s happening)

Each part is separated by a line with three dashes (—).

# Goal (Request)
---
# Result
---
# Feedback

The first part, the goal message, describes exactly what you want the robot or system to do. Think of it like giving instructions to someone – you need to be clear about what you want.

The second part, the result message, tells you what happened after everything is done. It’s like getting a report card after finishing a task – it shows the final outcome.

The third part, the feedback message, gives you regular updates while the action is running. Imagine tracking a package delivery – you get updates like “package picked up”, “in transit”, “out for delivery”.

When you use an action, it works like this: First, you (the action client) send your request (goal). Then, the system that handles the task (action server) starts working on it and sends you progress updates (feedback) so you know what’s happening. When it’s all done, you get the final report (result) telling you how everything went.

Define the Action

Let’s create a file called TimedRotation.action that defines an action for performing a timed rotation (of a mobile robot) with a specified angular velocity and duration, providing feedback on the progress and status of the rotation, and returning the result indicating the success and actual duration of the rotation.

Open a terminal window, and type:

cd ~/ros2_ws/src/yahboom_rosmaster/yahboom_rosmaster_msgs
mkdir action && cd action

Within the action directory, create a file called TimedRotation.action with the following contents:

# Request parameters for the timed rotation
float64 angular_velocity    # Desired angular velocity in rad/s (positive for counterclockwise, negative for clockwise)
float64 duration           # Desired duration of the rotation in seconds
---
# Result of the completed rotation
bool success               # Indicates if the rotation was completed successfully
float64 actual_duration    # Actual duration of the rotation in seconds
---
# Continuous feedback during rotation
float64 elapsed_time  # Elapsed time since the start of the rotation in seconds
string status         # Current status of the rotation:
                      # GOAL_RECEIVED: The action server has received a goal
                      # ROTATING: The rotation is in progress
                      # GOAL_SUCCEEDED: The rotation has been completed successfully
                      # GOAL_ABORTED: The rotation has been aborted due to an error or exceptional condition
                      # GOAL_CANCELED: The client has canceled the rotation goal

The goal message includes the desired angular velocity (in radians per second) and the duration of the rotation (in seconds).

The feedback message provides information about the progress of the rotation, including the elapsed time since the start of the rotation and the current status of the rotation, which can be one of the following: GOAL_RECEIVED, ROTATING, GOAL_SUCCEEDED, GOAL_ABORTED, or GOAL_CANCELED.

The result message indicates whether the rotation was completed successfully and provides the actual duration of the rotation.

Edit CMakeLists.txt

Here’s the complete CMakeLists.txt file with the necessary additions to include the TimedRotation.action in your ROS 2 package:

cmake_minimum_required(VERSION 3.8)
project(yahboom_rosmaster_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)

rosidl_generate_interfaces(${PROJECT_NAME}
  "action/TimedRotation.action"
)

if(BUILD_TESTING)
  find_package(ament_lint_auto REQUIRED)
  # the following line skips the linter which checks for copyrights
  # comment the line when a copyright and license is added to all source files
  set(ament_cmake_copyright_FOUND TRUE)
  # the following line skips cpplint (only works in a git repo)
  # comment the line when this package is in a git repo and when
  # a copyright and license is added to all source files
  set(ament_cmake_cpplint_FOUND TRUE)
  ament_lint_auto_find_test_dependencies()
endif()

ament_package()

Edit package.xml

Add the necessary dependencies to your package.xml file:

<?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>yahboom_rosmaster_msgs</name>
  <version>0.0.0</version>
  <description>Custom messages, services, and actions for the Yahboom ROSMaster robot</description>
  <maintainer email="automaticaddison@todo.com">ubuntu</maintainer>
  <license>BSD-3-Clause</license>

  <buildtool_depend>ament_cmake</buildtool_depend>
  <buildtool_depend>rosidl_default_generators</buildtool_depend>

  <exec_depend>rosidl_default_runtime</exec_depend>

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

  <member_of_group>rosidl_interface_packages</member_of_group>

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

Build the action

Open a terminal window, and type:

cd ~/ros2_ws
rosdep install --from-paths src --ignore-src -y
colcon build && source ~/.bashrc

Or you can type the following command if you have been following my tutorials:

build 

Check the action definition exists:

ros2 interface show yahboom_rosmaster_msgs/action/TimedRotation
1-show-ros2-action-interface

If the action definition exists and is properly generated, the command will display the contents of the TimedRotation.action file, like this:

If the action definition doesn’t exist or is not properly generated, you will see an error message indicating that the interface could not be found.

Writing an Action Server

Let’s write an action server node in C++. This node will handle long-running tasks with feedback and the ability to cancel the task while it’s running. We will follow the official ROS 2 examples.

The server should publish to the /mecanum_drive_controller/cmd_vel topic to control the robot’s rotation. The action server receives the goal from the action client, which specifies the desired angular velocity and duration for the rotation.

The server then publishes velocity commands to the /mecanum_drive_controller/cmd_vel topic to execute the rotation. Here’s an explanation of the action server’s behavior:

  1. The action server subscribes to the action topic to receive goals from the action client.
  2. When a goal is received, the action server extracts the desired angular velocity and duration from the goal message.
  3. The action server publishes velocity commands to the /mecanum_drive_controller/cmd_vel topic to initiate and maintain the robot’s rotation. It sets the linear velocities (in both the x and y directions) to zero and the angular velocity around z to the specified value from the goal.
  4. While the rotation is in progress, the action server publishes feedback messages to the action topic, providing the elapsed time since the start of the rotation and the status.
  5. The action server continues publishing velocity commands to the /mecanum_drive_controller/cmd_vel topic until the specified duration is reached.
  6. Once the duration is reached, the action server stops publishing velocity commands and sends a result message to the action topic, indicating the success of the rotation and the actual duration of the rotation.
  7. If any errors occur during the rotation, the action server stops publishing velocity commands and sends a result message indicating the failure.

Open a terminal window, and type:

cd ~/ros2_ws/src/yahboom_rosmaster/yahboom_rosmaster_system_tests/src

Create a new file called timed_rotation_action_server.cpp.

Add the following code:

/**
 * @file timed_rotation_action_server.cpp
 * @brief This ROS 2 node implements an action server for the TimedRotation action.
 *
 * This program implements a ROS 2 action server that executes timed rotation actions for the Yahboom RosMaster robot.
 * It receives goals from action clients, publishes timestamped velocity commands to rotate the robot,
 * and provides feedback on the rotation progress.
 *
 * Subscription Topics:
 *   None
 *
 * Publishing Topics:
 *   /mecanum_drive_controller/cmd_vel (geometry_msgs/TwistStamped):
 *     Timestamped velocity command for the robot:
 *     - header.stamp: Current ROS time when command is published
 *     - header.frame_id: "base_link" (robot's base frame)
 *     - twist.linear.x: Always 0 (no forward/backward motion)
 *     - twist.linear.y: Always 0 (no left/right motion)
 *     - twist.angular.z: Rotation speed in radians/second (positive=counterclockwise, negative=clockwise)
 *
 * Action Server:
 *   timed_rotation (yahboom_rosmaster_msgs/action/TimedRotation):
 *     Executes timed rotation actions with the following:
 *     - Goal: angular_velocity (rad/s) and duration (seconds)
 *     - Feedback: elapsed_time and status updates
 *     - Result: success flag and actual_duration
 *
 * @author Addison Sears-Collins
 * @date November 25, 2024
 */

#include <memory>
#include <thread>
#include <chrono>

#include "rclcpp/rclcpp.hpp"
#include "rclcpp_action/rclcpp_action.hpp"
#include "yahboom_rosmaster_msgs/action/timed_rotation.hpp"
#include "geometry_msgs/msg/twist_stamped.hpp"

using namespace std::chrono_literals;
using TimedRotation = yahboom_rosmaster_msgs::action::TimedRotation;
using GoalHandleTimedRotation = rclcpp_action::ServerGoalHandle<TimedRotation>;

/**
 * @class TimedRotationActionServer
 * @brief ROS 2 action server for the TimedRotation action.
 *
 * This class implements the action server functionality for the TimedRotation action.
 * It receives goals from an action client, executes the timed rotation, provides feedback,
 * and handles goal cancellation requests. The server maintains rotation by continuously
 * publishing timestamped velocity commands until the specified duration is reached or the goal is canceled.
 */
class TimedRotationActionServer : public rclcpp::Node
{
public:
  /**
   * @brief Constructor for the TimedRotationActionServer class.
   *
   * Initializes the action server and the timestamped velocity command publisher.
   * Sets up callback bindings for handling goals, cancellation requests,
   * and accepted goals.
   */
  TimedRotationActionServer()
  : Node("timed_rotation_action_server_cpp")
  {
    using namespace std::placeholders;

    // Create the action server with callbacks for goal handling, cancellation, and execution
    action_server_ = rclcpp_action::create_server<TimedRotation>(
      this,
      "timed_rotation",
      std::bind(&TimedRotationActionServer::handle_goal, this, _1, _2),
      std::bind(&TimedRotationActionServer::handle_cancel, this, _1),
      std::bind(&TimedRotationActionServer::handle_accepted, this, _1));

    // Create publisher for timestamped velocity commands
    cmd_vel_publisher_ = this->create_publisher<geometry_msgs::msg::TwistStamped>(
      "/mecanum_drive_controller/cmd_vel",
      10);  // Queue size of 10 messages
  }

private:
  // Class member variables
  rclcpp_action::Server<TimedRotation>::SharedPtr action_server_;  ///< The action server instance
  rclcpp::Publisher<geometry_msgs::msg::TwistStamped>::SharedPtr cmd_vel_publisher_;  ///< Publisher for timestamped velocity commands

  /**
   * @brief Callback function for handling new goal requests.
   *
   * This function is called when a new goal is received from an action client.
   * Currently accepts all goals without any validation.
   *
   * @param uuid Unique identifier for the goal request.
   * @param goal The goal request message containing angular_velocity and duration.
   * @return rclcpp_action::GoalResponse indicating if the goal is accepted or rejected.
   */
  rclcpp_action::GoalResponse handle_goal(
    const rclcpp_action::GoalUUID & uuid,
    [[maybe_unused]] std::shared_ptr<const TimedRotation::Goal> goal)
  {
    RCLCPP_INFO(this->get_logger(), "Received goal request for a timed rotation action");
    (void)uuid;
    return rclcpp_action::GoalResponse::ACCEPT_AND_EXECUTE;
  }

  /**
   * @brief Callback function for handling cancel requests.
   *
   * This function is called when a client requests to cancel an ongoing goal.
   * Currently accepts all cancellation requests.
   *
   * @param goal_handle The goal handle associated with the goal to be canceled.
   * @return rclcpp_action::CancelResponse indicating if the cancellation is accepted.
   */
  rclcpp_action::CancelResponse handle_cancel(
    const std::shared_ptr<GoalHandleTimedRotation> goal_handle)
  {
    RCLCPP_INFO(this->get_logger(), "Received cancel request for timed rotation action");
    (void)goal_handle;
    return rclcpp_action::CancelResponse::ACCEPT;
  }

  /**
   * @brief Callback function for handling accepted goals.
   *
   * This function is called when a goal has been accepted and needs to be executed.
   * Spawns a new thread to execute the goal to avoid blocking the main thread.
   *
   * @param goal_handle The goal handle associated with the accepted goal.
   */
  void handle_accepted(const std::shared_ptr<GoalHandleTimedRotation> goal_handle)
  {
    // Create a new thread using proper bind syntax
    std::thread{
      [this, goal_handle]() {
        this->execute(goal_handle);
      }
    }.detach();
  }

  /**
   * @brief Executes the timed rotation action.
   *
   * This function runs in a separate thread and performs the following:
   * 1. Publishes initial feedback with GOAL_RECEIVED status
   * 2. Creates and sends timestamped velocity commands based on goal parameters
   * 3. Continuously publishes feedback during rotation
   * 4. Handles cancellation requests during execution
   * 5. Stops rotation and publishes final result
   *
   * @param goal_handle The goal handle associated with the goal to be executed.
   */
  void execute(const std::shared_ptr<GoalHandleTimedRotation> goal_handle)
  {
    RCLCPP_INFO(this->get_logger(), "Executing goal for timed rotation action...");

    // Get the goal message
    const auto goal = goal_handle->get_goal();

    // Create result and feedback messages
    auto result = std::make_shared<TimedRotation::Result>();
    auto feedback = std::make_shared<TimedRotation::Feedback>();

    // Send initial feedback
    feedback->status = "GOAL_RECEIVED";
    goal_handle->publish_feedback(feedback);

    // Create timestamped velocity command message
    geometry_msgs::msg::TwistStamped twist_stamped_msg;
    twist_stamped_msg.twist.angular.z = goal->angular_velocity;  // Set rotation speed
    twist_stamped_msg.header.frame_id = "base_link";  // Set the reference frame
    // linear.x and linear.y are automatically initialized to 0

    // Record start time for duration tracking
    auto start_time = this->now();

    // Main execution loop
    while (rclcpp::ok())
    {
      // Check for cancellation requests
      if (goal_handle->is_canceling())
      {
        goal_handle->canceled(result);
        RCLCPP_INFO(this->get_logger(), "Goal canceled");
        feedback->status = "GOAL_CANCELED";
        goal_handle->publish_feedback(feedback);

        // Stop the robot with timestamped command
        twist_stamped_msg.twist.angular.z = 0.0;
        twist_stamped_msg.header.stamp = this->now();
        cmd_vel_publisher_->publish(twist_stamped_msg);
        return;
      }

      // Update timestamp and publish velocity command
      twist_stamped_msg.header.stamp = this->now();
      cmd_vel_publisher_->publish(twist_stamped_msg);

      // Calculate elapsed time
      auto elapsed_time = (this->now() - start_time).seconds();

      // Publish feedback
      feedback->elapsed_time = elapsed_time;
      feedback->status = "ROTATING";
      goal_handle->publish_feedback(feedback);

      // Check if we've reached the desired duration
      if (elapsed_time >= goal->duration)
      {
        break;
      }

      // Sleep to control update rate (5Hz)
      std::this_thread::sleep_for(200ms);
    }

    // Stop the robot with final timestamped command
    twist_stamped_msg.twist.angular.z = 0.0;
    twist_stamped_msg.header.stamp = this->now();
    cmd_vel_publisher_->publish(twist_stamped_msg);

    // Set and send result
    result->success = true;
    result->actual_duration = (this->now() - start_time).seconds();

    // Mark the goal as succeeded
    goal_handle->succeed(result);

    // Send final feedback
    feedback->status = "GOAL_SUCCEEDED";
    goal_handle->publish_feedback(feedback);

    RCLCPP_INFO(this->get_logger(), "Goal succeeded");
  }
};

/**
 * @brief Main function for the timed rotation action server node.
 *
 * Initializes ROS 2, creates an instance of the TimedRotationActionServer,
 * and spins the node to process callbacks.
 *
 * @param argc Number of command-line arguments.
 * @param argv Array of command-line arguments.
 * @return int Exit status of the program.
 */
int main(int argc, char ** argv)
{
  rclcpp::init(argc, argv);
  auto action_server = std::make_shared<TimedRotationActionServer>();
  rclcpp::spin(action_server);
  rclcpp::shutdown();
  return 0;
}

Save the code and close the editor.

Edit your package.xml file.

cd ~/ros2_ws/src/yahboom_rosmaster/yahboom_rosmaster_system_tests

Open the package.xml file.

Save the file and close the editor.

Open CMakeLists.txt.

Make sure your CMakeLists.txt file looks like this.

Save the file, and close the editor.

Build the workspace:

build

Testing the Action Server

Now let’s launch the mobile robot.

Open a terminal window, and type the following command:

x3 

or

bash ~/ros2_ws/src/yahboom_rosmaster/yahboom_rosmaster_bringup/scripts/rosmaster_x3_gazebo.sh

Now, let’s launch the action server. Open a new terminal window, and type:

ros2 run yahboom_rosmaster_system_tests timed_rotation_action_server 

To see the list of actions a node provides, /timed_rotation_action_server  in this case, open a new terminal and run the command:

ros2 node info /timed_rotation_action_server_cpp

Which will return a list of /timed_rotation_action_server_cpp’s subscribers, publishers, services, action servers and action clients:

Let’s see all the active actions:

ros2 action list -t
2-active-actions

If you want to check the action type for the action, run the command:

ros2 action type /timed_rotation

Take a closer look at the action with this command:

ros2 action info /timed_rotation
3-ros2-action-info

To send a goal to the server and receive feedback, type:.

ros2 action send_goal /timed_rotation yahboom_rosmaster_msgs/action/TimedRotation "{duration: 15.0, angular_velocity: -0.25}" --feedback
4-rotate-with-feedback

Observe the velocity messages:

ros2 topic echo /mecanum_drive_controller/cmd_vel

The robot will rotate for 15 seconds.

Press CTRL + C in all windows to close everything.

Writing an Action Client

Now that we’ve created and tested our action server for controlling the robot’s rotation, let’s create an action client that can send goals to it. The action client will allow us to programmatically request timed rotations, monitor their progress through feedback, and receive the final results. This is particularly useful when you want to integrate the rotation behavior into a larger automated system or control sequence.

Open a terminal window and navigate to the src directory of your package:

cd ~/ros2_ws/src/yahboom_rosmaster/yahboom_rosmaster_system_tests/src

Create a new file named timed_rotation_action_client.cpp and open it in your preferred text editor:

Add the following code to timed_rotation_action_client.cpp:

/**
 * @file timed_rotation_action_client.cpp
 * @brief ROS 2 action client node for sending timed rotation goals.
 *
 * This program implements a ROS 2 action client node that sends timed rotation goals
 * to a corresponding action server. It subscribes to the /timed_rotation_mode topic
 * to determine when to send or cancel goals.
 *
 * Subscription Topics:
 *   /timed_rotation_mode (std_msgs/Bool): Topic indicating whether timed rotation mode is active
 *
 * Publishing Topics:
 *   None
 *
 * Action Client:
 *   timed_rotation (yahboom_rosmaster_msgs/action/TimedRotation):
 *     Sends goals for timed rotation and receives feedback and results:
 *     - Goal: angular_velocity (rad/s) and duration (seconds)
 *     - Feedback: elapsed_time (seconds) and status string
 *     - Result: success flag and actual_duration (seconds)
 *
 * @author Addison Sears-Collins
 * @date November 25, 2024
 */

#include <memory>
#include <thread>

#include "rclcpp/rclcpp.hpp"
#include "rclcpp_action/rclcpp_action.hpp"
#include "yahboom_rosmaster_msgs/action/timed_rotation.hpp"
#include "std_msgs/msg/bool.hpp"

using TimedRotation = yahboom_rosmaster_msgs::action::TimedRotation;
using GoalHandleTimedRotation = rclcpp_action::ClientGoalHandle<TimedRotation>;

/**
 * @class TimedRotationActionClient
 * @brief ROS 2 action client node for sending timed rotation goals.
 *
 * This class implements a ROS 2 action client that sends timed rotation goals and
 * processes feedback and results from the action server. It can be controlled via
 * a boolean topic to start and stop rotations.
 */
class TimedRotationActionClient : public rclcpp::Node
{
public:
  using TimedRotation = yahboom_rosmaster_msgs::action::TimedRotation;
  using GoalHandleTimedRotation = rclcpp_action::ClientGoalHandle<TimedRotation>;

  /**
   * @brief Constructor for the TimedRotationActionClient class.
   * @param options ROS 2 node options.
   */
  explicit TimedRotationActionClient(const rclcpp::NodeOptions& options)
  : Node("timed_rotation_action_client_cpp", options)
  {
    // Create the action client
    action_client_ = rclcpp_action::create_client<TimedRotation>(
      this,
      "timed_rotation"
    );

    // Create subscription to control rotation mode
    timed_rotation_mode_sub_ = create_subscription<std_msgs::msg::Bool>(
      "/timed_rotation_mode",
      10,
      std::bind(&TimedRotationActionClient::timed_rotation_mode_callback, this, std::placeholders::_1)
    );

    // Declare parameters with default values
    this->declare_parameter("angular_velocity", 0.5);  // rad/s
    this->declare_parameter("duration", 10.0);         // seconds
  }

private:
  /**
   * @brief Callback function for the /timed_rotation_mode topic.
   *
   * Controls the action client based on the received boolean value:
   * - True: Starts a new rotation if none is active
   * - False: Cancels the current rotation if one is active
   *
   * @param msg The received boolean message.
   */
  void timed_rotation_mode_callback(const std_msgs::msg::Bool::SharedPtr msg)
  {
    if (!timed_rotation_mode_ && msg->data)
    {
      // Transition from False to True - Start rotation
      send_goal();
    }
    else if (timed_rotation_mode_ && !msg->data)
    {
      // Transition from True to False - Cancel if active
      if (goal_handle_ && goal_handle_->get_status() == rclcpp_action::GoalStatus::STATUS_ACCEPTED)
      {
        cancel_goal();
      }
    }
    timed_rotation_mode_ = msg->data;
  }

  /**
   * @brief Callback function for goal response.
   * @param goal_handle The goal handle returned by the action server.
   */
  void goal_response_callback(const GoalHandleTimedRotation::SharedPtr& goal_handle)
  {
    if (!goal_handle)
    {
      RCLCPP_ERROR(get_logger(), "Goal was rejected by server");
      return;
    }

    goal_handle_ = goal_handle;
    RCLCPP_INFO(get_logger(), "Goal accepted by server");
  }

  /**
   * @brief Callback function for feedback.
   * @param goal_handle The goal handle associated with the feedback.
   * @param feedback The feedback message received from the action server.
   */
  void feedback_callback(
    const GoalHandleTimedRotation::SharedPtr& goal_handle,
    const std::shared_ptr<const TimedRotation::Feedback>& feedback)
  {
    (void)goal_handle;  // Unused parameter
    RCLCPP_INFO(get_logger(), "Feedback received - Elapsed time: %.2f seconds, Status: %s",
                feedback->elapsed_time,
                feedback->status.c_str());
  }

  /**
   * @brief Callback function for getting the result.
   * @param result The result message received from the action server.
   */
  void get_result_callback(const GoalHandleTimedRotation::WrappedResult& result)
  {
    switch (result.code)
    {
      case rclcpp_action::ResultCode::SUCCEEDED:
        RCLCPP_INFO(get_logger(), "Goal succeeded! Actual duration: %.2f seconds",
                    result.result->actual_duration);
        break;
      case rclcpp_action::ResultCode::ABORTED:
        RCLCPP_ERROR(get_logger(), "Goal was aborted");
        return;
      case rclcpp_action::ResultCode::CANCELED:
        RCLCPP_INFO(get_logger(), "Goal was canceled");
        return;
      default:
        RCLCPP_ERROR(get_logger(), "Unknown result code");
        return;
    }
  }

  /**
   * @brief Cancel the current goal.
   *
   * Attempts to cancel the currently executing goal. Checks for a valid goal handle
   * and waits for the cancellation response from the server.
   */
  void cancel_goal()
  {
    RCLCPP_INFO(get_logger(), "Requesting to cancel goal");
    if (!goal_handle_)
    {
        RCLCPP_ERROR(get_logger(), "Goal handle is null");
        return;
    }

    auto future = action_client_->async_cancel_goal(goal_handle_);

    if (rclcpp::spin_until_future_complete(this->get_node_base_interface(), future) !=
        rclcpp::FutureReturnCode::SUCCESS)
    {
        RCLCPP_ERROR(get_logger(), "Failed to cancel goal");
        return;
    }
    RCLCPP_INFO(get_logger(), "Goal cancellation request sent");
  }

  /**
   * @brief Send a goal to the action server.
   *
   * Sends a new rotation goal using parameters from the parameter server.
   * If the server isn't available, it will wait for it to become available.
   */
  void send_goal()
  {
    RCLCPP_INFO(get_logger(), "Waiting for action server...");
    if (!action_client_->wait_for_action_server(std::chrono::seconds(5)))
    {
      RCLCPP_ERROR(get_logger(), "Action server not available after 5 seconds");
      return;
    }

    // Get parameters for the goal
    auto angular_velocity = this->get_parameter("angular_velocity").as_double();
    auto duration = this->get_parameter("duration").as_double();

    auto goal_msg = TimedRotation::Goal();
    goal_msg.angular_velocity = angular_velocity;
    goal_msg.duration = duration;

    RCLCPP_INFO(get_logger(),
                "Sending goal: angular_velocity=%.2f rad/s, duration=%.2f seconds",
                angular_velocity, duration);

    auto send_goal_options = rclcpp_action::Client<TimedRotation>::SendGoalOptions();
    send_goal_options.goal_response_callback =
      std::bind(&TimedRotationActionClient::goal_response_callback, this, std::placeholders::_1);
    send_goal_options.feedback_callback =
      std::bind(&TimedRotationActionClient::feedback_callback, this, std::placeholders::_1, std::placeholders::_2);
    send_goal_options.result_callback =
      std::bind(&TimedRotationActionClient::get_result_callback, this, std::placeholders::_1);

    action_client_->async_send_goal(goal_msg, send_goal_options);
  }

  // Class member variables
  rclcpp_action::Client<TimedRotation>::SharedPtr action_client_;        ///< Action client for timed rotation
  rclcpp::Subscription<std_msgs::msg::Bool>::SharedPtr timed_rotation_mode_sub_;  ///< Subscription to timed rotation mode
  bool timed_rotation_mode_{false};                                      ///< Current state of timed rotation mode
  GoalHandleTimedRotation::SharedPtr goal_handle_;                      ///< Handle for the current goal
};

/**
 * @brief Main function for the timed rotation action client node.
 *
 * Initializes ROS 2, creates an instance of the TimedRotationActionClient,
 * and spins the node to process callbacks.
 *
 * @param argc Number of command-line arguments.
 * @param argv Array of command-line arguments.
 * @return int Exit status of the program.
 */
int main(int argc, char** argv)
{
  rclcpp::init(argc, argv);
  auto action_client = std::make_shared<TimedRotationActionClient>(rclcpp::NodeOptions());
  rclcpp::spin(action_client);
  rclcpp::shutdown();
  return 0;
}

Save the code and close the file.

Edit your CMakeLists.txt file to add the new executable:

cd ~/ros2_ws/src/yahboom_rosmaster/yahboom_rosmaster_system_tests/

Open CMakeLists.txt.

Add your new node.

Save the file, and close it.

Open a terminal window and build the package:

build

Testing the Action Client – C++

Now let’s launch the mobile robot.

Open a terminal window, and type the following command:

x3 

or

bash ~/ros2_ws/src/yahboom_rosmaster/yahboom_rosmaster_bringup/scripts/rosmaster_x3_gazebo.sh

Now, let’s launch the action server. Open a new terminal window, and type:

ros2 run yahboom_rosmaster_system_tests timed_rotation_action_server 

Launch the action client in another terminal window.

ros2 run yahboom_rosmaster_system_tests timed_rotation_action_client

To see how many servers and clients you have on the /timed_rotation action, type:

ros2 action info /timed_rotation

Check out the topics.

ros2 topic list

You will see a topic called /timed_rotation_mode. This is the topic we will use to trigger the timed rotation action.

Trigger this action using the following command:

ros2 topic pub /timed_rotation_mode std_msgs/msg/Bool "data: true" -1

Observe the velocity messages:

ros2 topic echo /mecanum_drive_controller/cmd_vel

Once that finishes, trigger it again:

ros2 topic pub /timed_rotation_mode std_msgs/msg/Bool "data: false" -1
ros2 topic pub /timed_rotation_mode std_msgs/msg/Bool "data: true" -1

To stop the timed rotation before it completes, you can publish a message with the value set to False:

ros2 topic pub /timed_rotation_mode std_msgs/msg/Bool "data: false" -1

This will cause the action client to send a cancellation request to the action server if the rotation is currently in progress.

Press CTRL + C in all windows to close everything.

Congratulations! You have created a ROS 2 action. 

That’s it! Keep building!

Customize Your Robot with Parameters in C++ – ROS 2 Jazzy

In this tutorial, we’ll explore the power of parameters in ROS 2. Parameters are important for creating flexible and dynamic robot software. 

By the end of this tutorial, you will be able to use parameters to make your robot move like this:

mecanum-robot-rotate-counter-clockwise

You’ll learn how to define, access, and use parameters within your nodes, enabling you to customize your robot’s behavior without altering its code. 

Join me as we dive into the essentials of parameters, setting the foundation for more complex robotics applications.

Real-World Applications

Here are some real-world examples of how ROS 2 parameters can be used in robotic applications:

  • Joint Limits and Velocities: Parameters can be used to define the safe operating range (minimum and maximum) for each joint of the arm in terms of angles or positions. 
  • Payload Capacity: This parameter specifies the maximum weight the arm can safely carry while maintaining stability and avoiding damage to its motors or gears. 
  • Path Planning Parameters: These parameters influence how the arm plans its trajectory to reach a target point. 
  • End-effector Calibration: Parameters can store calibration data for the gripper or any tool attached to the end of the arm.

Pretty much any variable that applies to your robot can be declared as a parameter in ROS 2. Parameters are used to configure nodes at startup (and during runtime), without changing the code.

Remember, a ROS 2 node is a mini-program (written in either Python or C++) that performs a specific task within a robot system. 

Prerequisites

All my code for this project is located here on GitHub.

Write the Code

Let’s start by creating a ROS 2 node that declares four parameters: 

  • robot_name (string): Represents the name of the robot.
  • robot_id (integer): The unique ID number of the robot
  • target_linear_velocity (floating-point value): How fast we want the robot to move forward or sideways in meters per second.
  • target_angular_velocity (floating-point value): How fast we want the robot to rotate in radians per second.

This node will include a function that executes any time one of the parameters changes. The node will also publish velocity messages (geometry_msgs/msg/TwistStamped message) to the /mecanum_drive_controller/cmd_vel topic that has as inputs the target_linear_velocity (in the x and y direction) and target_angular_velocity (rotation around the z-axis) parameters. 

This publisher publishes a new velocity message at a rate of 2 Hz (two times per second), and the default velocity is 0 meters per second (for linear velocities) and 0 radians per second (for angular velocity).  

Because we are parameterizing the velocity values, you can change the linear and angular velocity parameters at runtime, and the values will automatically update.

Open a new terminal window, and type:

cd ~/ros2_ws/src/yahboom_rosmaster/yahboom_rosmaster_system_tests/src

Create a new file called mecanum_parameters_node.cpp.

Type the following code inside mecanum_parameters_node.cpp:

/**
 * @file mecanum_parameters_node.cpp
 * @brief This ROS 2 node demonstrates how to declare parameters and publish velocity messages.
 *
 * This program implements a ROS 2 node that declares parameters for a mecanum wheel robot 
 * and publishes velocity commands. It demonstrates parameter declaration, callback
 * registration for parameter changes, and publishing to the velocity topic.
 *
 * Subscription Topics:
 *   None
 *
 * Publishing Topics:
 *   /mecanum_drive_controller/cmd_vel (geometry_msgs/TwistStamped): Velocity command for the robot
 *   (linear.x, linear.y, and angular.z)
 *
 * Parameters:
 *   robot_name (string): The name of the robot
 *   robot_id (int): The unique ID number of the robot
 *   target_linear_velocity_x (double): Target linear velocity in x direction (m/s)
 *   target_linear_velocity_y (double): Target linear velocity in y direction (m/s)
 *   target_angular_velocity (double): Target angular velocity in radians per second
 *
 * @author Addison Sears-Collins
 * @date November 24, 2024
 */

#include <rclcpp/rclcpp.hpp>
#include <rcl_interfaces/msg/parameter_descriptor.hpp>
#include <rcl_interfaces/msg/set_parameters_result.hpp>
#include <geometry_msgs/msg/twist_stamped.hpp>

/**
 * @class MecanumParameters
 * @brief A ROS 2 node that demonstrates parameter handling and velocity publishing.
 */
class MecanumParameters : public rclcpp::Node
{
public:
  /**
   * @brief Constructor for the MecanumParameters node.
   *
   * Initializes the node, declares parameters, sets up parameter change callback,
   * and creates a publisher and timer for velocity messages.
   */
  MecanumParameters() : Node("mecanum_parameters_node")
  {
    // Describe parameters
    rcl_interfaces::msg::ParameterDescriptor robot_name_descriptor;
    robot_name_descriptor.description = "The name of the robot";
    rcl_interfaces::msg::ParameterDescriptor robot_id_descriptor;
    robot_id_descriptor.description = "The unique ID number of the robot";
    rcl_interfaces::msg::ParameterDescriptor target_linear_velocity_x_descriptor;
    target_linear_velocity_x_descriptor.description = "Target linear velocity in x direction (meters per second)";
    rcl_interfaces::msg::ParameterDescriptor target_linear_velocity_y_descriptor;
    target_linear_velocity_y_descriptor.description = "Target linear velocity in y direction (meters per second)";
    rcl_interfaces::msg::ParameterDescriptor target_angular_velocity_descriptor;
    target_angular_velocity_descriptor.description = "Target angular velocity in radians per second";

    // Declare parameters using declare_parameter()
    this->declare_parameter("robot_name", "Automatic Addison Bot", robot_name_descriptor);
    this->declare_parameter("robot_id", 1, robot_id_descriptor);
    this->declare_parameter("target_linear_velocity_x", 0.0, target_linear_velocity_x_descriptor);
    this->declare_parameter("target_linear_velocity_y", 0.0, target_linear_velocity_y_descriptor);
    this->declare_parameter("target_angular_velocity", 0.0, target_angular_velocity_descriptor);

    // Register a callback function for parameter changes using the newer method
    param_callback_handle_ = this->add_on_set_parameters_callback(
      std::bind(&MecanumParameters::parameter_change_callback, this, std::placeholders::_1));

    // Create a publisher for the /mecanum_drive_controller/cmd_vel topic
    velocity_publisher_ = this->create_publisher<geometry_msgs::msg::TwistStamped>("/mecanum_drive_controller/cmd_vel", 10);

    // Create a timer that will call the publish_velocity function every 0.5 seconds (2 Hz)
    timer_ = this->create_wall_timer(std::chrono::milliseconds(500), std::bind(&MecanumParameters::publish_velocity, this));
  }

private:
  /**
   * @brief Callback function for parameter changes.
   *
   * @param params A vector of rclcpp::Parameter objects representing the parameters that are
   *               being attempted to change.
   * @return rcl_interfaces::msg::SetParametersResult Object indicating whether the change was successful.
   */
  rcl_interfaces::msg::SetParametersResult parameter_change_callback(const std::vector<rclcpp::Parameter> & params)
  {
    rcl_interfaces::msg::SetParametersResult result;
    result.successful = true;

    for (const auto & param : params)
    {
      if (param.get_name() == "robot_name" && param.get_type() == rclcpp::ParameterType::PARAMETER_STRING)
      {
        RCLCPP_INFO(this->get_logger(), "Parameter robot_name has changed. The new value is: %s", param.as_string().c_str());
      }
      else if (param.get_name() == "robot_id" && param.get_type() == rclcpp::ParameterType::PARAMETER_INTEGER)
      {
        RCLCPP_INFO(this->get_logger(), "Parameter robot_id has changed. The new value is: %ld", param.as_int());  // Changed %d to %ld
      }
      else if (param.get_name() == "target_linear_velocity_x" && param.get_type() == rclcpp::ParameterType::PARAMETER_DOUBLE)
      {
        RCLCPP_INFO(this->get_logger(), "Parameter target_linear_velocity_x has changed. The new value is: %f", param.as_double());
      }
      else if (param.get_name() == "target_linear_velocity_y" && param.get_type() == rclcpp::ParameterType::PARAMETER_DOUBLE)
      {
        RCLCPP_INFO(this->get_logger(), "Parameter target_linear_velocity_y has changed. The new value is: %f", param.as_double());
      }
      else if (param.get_name() == "target_angular_velocity" && param.get_type() == rclcpp::ParameterType::PARAMETER_DOUBLE)
      {
        RCLCPP_INFO(this->get_logger(), "Parameter target_angular_velocity has changed. The new value is: %f", param.as_double());
      }
      else
      {
        result.successful = false;
        result.reason = "Unknown parameter: " + param.get_name();
        break;
      }
    }
    return result;
  }

  /**
   * @brief Publishes velocity messages to the /mecanum_drive_controller/cmd_vel topic.
   *
   * This function is called periodically by the timer to publish the current
   * target linear and angular velocities.
   */
  void publish_velocity()
  {
    double target_linear_velocity_x = this->get_parameter("target_linear_velocity_x").as_double();
    double target_linear_velocity_y = this->get_parameter("target_linear_velocity_y").as_double();
    double target_angular_velocity = this->get_parameter("target_angular_velocity").as_double();

    geometry_msgs::msg::TwistStamped velocity_msg;
    velocity_msg.header.stamp = this->get_clock()->now();
    velocity_msg.header.frame_id = "base_link";
    velocity_msg.twist.linear.x = target_linear_velocity_x;
    velocity_msg.twist.linear.y = target_linear_velocity_y;
    velocity_msg.twist.angular.z = target_angular_velocity;

    velocity_publisher_->publish(velocity_msg);
  }

  rclcpp::Publisher<geometry_msgs::msg::TwistStamped>::SharedPtr velocity_publisher_;  ///< Publisher for velocity commands
  rclcpp::TimerBase::SharedPtr timer_;  ///< Timer for periodic velocity publishing
  OnSetParametersCallbackHandle::SharedPtr param_callback_handle_;  ///< Handle for the parameter callback
};

/**
 * @brief Main function to start the ROS 2 node.
 *
 * Initializes ROS 2, creates an instance of the MecanumParameters node,
 * and keeps it running to process callbacks and publish messages.
 *
 * @param argc Number of command-line arguments.
 * @param argv Array of command-line argument strings.
 * @return int Return code (0 for normal exit).
 */
int main(int argc, char ** argv)
{
  rclcpp::init(argc, argv);
  auto mecanum_parameters_node = std::make_shared<MecanumParameters>();
  rclcpp::spin(mecanum_parameters_node);
  rclcpp::shutdown();
  return 0;
}

Save the file, and close it.

Edit package.xml

Now let’s modify our package.xml file to update the package dependencies. Either open the package.xml file if you are using VS Code, or type this:

cd ~/ros2_ws/src/yahboom_rosmaster/yahboom_rosmaster_system_tests/
gedit package.xml

Add this line in the dependencies section:

<depend>rcl_interfaces</depend>

Save the file, and close it.

Update CMakeLists.txt

Now we need to edit the CMakeLists.txt file. 

Open CMakeLists.txt, and make sure it looks like this:

Be sure to add the mecanum_parameters_node.cpp program.

cmake_minimum_required(VERSION 3.8)
project(yahboom_rosmaster_system_tests)

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(rclcpp REQUIRED)
find_package(geometry_msgs REQUIRED)
find_package(std_msgs REQUIRED)
find_package(rcl_interfaces REQUIRED)

# Copy necessary files to designated locations in the project
install (
  DIRECTORY config launch
  DESTINATION share/${PROJECT_NAME}
)

# Add the square mecanum controller executable
add_executable(square_mecanum_controller
  src/square_mecanum_controller.cpp
)

# Add the mecanum parameters node executable
add_executable(mecanum_parameters_node
  src/mecanum_parameters_node.cpp
)

# Specify dependencies for the target
ament_target_dependencies(square_mecanum_controller
  rclcpp
  geometry_msgs
  std_msgs
)

# Specify dependencies for the mecanum parameters node
ament_target_dependencies(mecanum_parameters_node
  rclcpp
  geometry_msgs
  std_msgs
  rcl_interfaces
)

# Install the executables
install(TARGETS
  square_mecanum_controller
  mecanum_parameters_node
  DESTINATION lib/${PROJECT_NAME}
)

if(BUILD_TESTING)
  find_package(ament_lint_auto REQUIRED)
  # the following line skips the linter which checks for copyrights
  # comment the line when a copyright and license is added to all source files
  set(ament_cmake_copyright_FOUND TRUE)
  # the following line skips cpplint (only works in a git repo)
  # comment the line when this package is in a git repo and when
  # a copyright and license is added to all source files
  set(ament_cmake_cpplint_FOUND TRUE)
  ament_lint_auto_find_test_dependencies()
endif()

ament_package()

Save the file, and close it.

Build the Workspace

Open a terminal window, and type:

cd ~/ros2_ws/
colcon build && source ~/.bashrc

Or you can type my alias:

build

Launch the Robot

Now let’s launch the mobile robot.

Open a terminal window, and type the following command:

x3 

or

bash ~/ros2_ws/src/yahboom_rosmaster/yahboom_rosmaster_bringup/scripts/rosmaster_x3_gazebo.sh

Run the node you created earlier. Open a new terminal and type:

ros2 run yahboom_rosmaster_system_tests mecanum_parameters_node

Now, press Enter.

Open a new terminal window.

What are the currently active nodes?

ros2 node list
1-ros2-node-list

List all the parameters for all nodes in the ROS2 system.

ros2 param list

Retrieve the value of a specific parameter for a node.

ros2 param get /mecanum_parameters_node robot_name

String value is: Automatic Addison Bot

ros2 param get /mecanum_parameters_node robot_id

Integer value is: 1

ros2 param get /mecanum_parameters_node target_linear_velocity_x

Double value is: 0.0

ros2 param get /mecanum_parameters_node target_linear_velocity_y

Double value is: 0.0

ros2 param get /mecanum_parameters_node target_angular_velocity

Double value is: 0.0

Let’s change the name of the robot:

ros2 param set /mecanum_parameters_node robot_name "New Automatic Addison Bot"

Set parameter successful

Double check the new value:

ros2 param get /mecanum_parameters_node robot_name

String value is: New Automatic Addison Bot

Let’s make the robot rotate clockwise:

ros2 param set /mecanum_parameters_node target_angular_velocity -0.5
ros2 param get /mecanum_parameters_node target_angular_velocity

Double value is: -0.5

Now go back to the terminals where your scripts are running and press CTRL + C to stop the execution.

To clear the terminal window, type:

clear

Create Your Own YAML File to Set Parameters in a Launch File

Now we will create a YAML file containing our desired parameters. We will then import that YAML file into a launch file and launch the robot in Gazebo.

Create the Launch File

Go to this folder:

cd ~/ros2_ws/src/yahboom_rosmaster/yahboom_rosmaster_system_tests/
mkdir launch
cd launch

Create a new file called mecanum_parameters.launch.py.

Open a new terminal window, and type the following code:

#!/usr/bin/env python3
"""
Launch file for mecanum_parameters_node.

This launch file launches the mecanum_parameters_node with configurable parameters.

:author: Addison Sears-Collins
:date: November 24, 2024
"""

import os
from launch import LaunchDescription
from launch.actions import DeclareLaunchArgument
from launch.substitutions import LaunchConfiguration
from launch_ros.actions import Node
from launch_ros.substitutions import FindPackageShare


def generate_launch_description():
    """
    Generate a launch description for the mecanum_parameters_node.

    :return: A LaunchDescription object containing the actions to execute.
    :rtype: LaunchDescription
    """

    # Constants for paths to different files and folders
    package_name_system_tests = 'yahboom_rosmaster_system_tests'

    mecanum_params_path = 'config/mecanum_parameters.yaml'

    # Set the path to different files and folders
    pkg_share_system_tests = FindPackageShare(
        package=package_name_system_tests).find(package_name_system_tests)

    mecanum_params_path = os.path.join(pkg_share_system_tests, mecanum_params_path)

    # Launch configuration variables
    mecanum_params = LaunchConfiguration('mecanum_params')

    # Declare the launch arguments
    declare_mecanum_params_cmd = DeclareLaunchArgument(
        name='mecanum_params',
        default_value=mecanum_params_path,
        description='Full path to parameters for the parameters file.')

    # Launch the mecanum_parameters_node
    start_mecanum_parameters_node_cmd = Node(
        package=package_name_system_tests,
        executable='mecanum_parameters_node',
        parameters=[mecanum_params],
        output='screen')

    # Create the launch description and populate it
    ld = LaunchDescription()

    # Declare the launch options
    ld.add_action(declare_mecanum_params_cmd)

    # Add actions to the launch description
    ld.add_action(start_mecanum_parameters_node_cmd)

    return ld

Save the file, and close it.

Create the YAML File

Now let’s create the configuration file.

cd ~/ros2_ws/src/yahboom_rosmaster/yahboom_rosmaster_system_tests/
mkdir config
cd config

Create a new file called mecanum_parameters.yaml.

# Desired parameter values
/mecanum_parameters_node:
  ros__parameters:
    robot_name: "New Automatic Addison Bot"
    robot_id: 82
    target_linear_velocity_x: 0.0
    target_linear_velocity_y: 0.0
    target_angular_velocity: 0.23

Save the file, and close it.

Update CMakeLists.txt

Update your CMakeLists.txt with the new launch and config folders.

# Copy necessary files to designated locations in the project
install (
  DIRECTORY config launch
  DESTINATION share/${PROJECT_NAME}
)

Build the Workspace

Open a terminal window, and type:

build

(Note this “build” is my alias for “cd ~/ros2_ws/ && colcon build && source ~/.bashrc)

Launch the Robot Again

To launch the robot with the default parameter values, you can do this:

x3

In another terminal window, type:

ros2 launch yahboom_rosmaster_system_tests mecanum_parameters.launch.py

The robot should start to rotate counter-clockwise.

2-robot-rotate

Let’s check the velocity values:

ros2 topic echo /mecanum_drive_controller/cmd_vel
3-check-velocity-values

Press CTRL + C to stop the robot.

That’s it! Keep building!