Using ros2 doctor to Identify Issues – ROS 2 Jazzy

In this tutorial, we will explore how to use the ros2 doctor command to identify and diagnose issues in your ROS 2 system.

The purpose of ros2 doctor is to check the health and configuration of your ROS 2 setup. It analyzes various aspects such as environment variables, network configuration, and running systems to identify potential issues or warnings.

Real-World Applications

ros2 doctor is useful when:

  • Setting up a new ROS 2 environment to ensure proper configuration
  • Troubleshooting issues or errors in your ROS 2 system
  • Collaborating on ROS 2 projects to ensure consistent setups across team members

Prerequisites

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

Check Your Setup

Open a new terminal window.

Run the following command:

ros2 doctor

Review the report for any warnings or errors.

If ros2 doctor finds no warnings, you’ll see “All <n> checks passed”

You don’t have to worry about UserWarnings. UserWarnings indicate non-ideal configurations, but the setup is still usable.

What you do need to pay attention to are errors. If any errors are found, you will see “UserWarning: ERROR:”. In this case, the check is considered failed, and you should address the errors.

Check a System

Let’s use ros2 doctor to analyze an actual system.

We will launch the robot and use ros2 doctor to analyze the running system.

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

Open another terminal window, and move the robot around:

ros2 run yahboom_rosmaster_system_tests square_mecanum_controller

In a separate terminal, display the messages that are broadcasting over the /cmd_vel topic:

ros2 topic echo /mecanum_drive_controller/cmd_vel

While the robot is running, open another terminal and run:

ros2 doctor

Review the report to see if there are any warnings or errors related to the running nodes, topics, or services.

Get a Full Report

To get a more detailed report from ros2 doctor, use the –report argument.

Open a terminal window, and run the following command:

ros2 doctor --report

The report will provide detailed information categorized into five groups:

  • NETWORK CONFIGURATION
  • PLATFORM INFORMATION
  • RMW MIDDLEWARE
  • ROS 2 INFORMATION
  • TOPIC LIST

Cross-reference the information in the report with the warnings from running ros2 doctor to gain deeper insights into the issues.

That’s it. Keep building!

Working With Miscellaneous Topics in C++

In this tutorial, we will explore miscellaneous useful features in C++.

Prerequisites

Defining Variables

Let’s cover how to define variables in C++. Understanding variable types and how to use them effectively is fundamental for programming in robotics, where you need to handle various types of data, from sensor readings to actuator commands.

Defining Enums 

Let’s explore enums in C++, which allow you to create named constants for better code readability and type safety in robotics applications.

Open a terminal window, and type this: 

cd ~/Documents/cpp_tutorial 
code . 

Create a new C++ file and name it enum_example.cpp.

Type the following code into the editor:

#include <iostream>

enum class RobotState {
    IDLE,
    MOVING,
    GRASPING,
    ERROR
};

void print_robot_state(RobotState state) {
    switch (state) {
        case RobotState::IDLE:
            std::cout << "Robot is idle." << std::endl;
            break;
        case RobotState::MOVING:
            std::cout << "Robot is moving." << std::endl;
            break;
        case RobotState::GRASPING:
            std::cout << "Robot is grasping an object." << std::endl;
            break;
        case RobotState::ERROR:
            std::cout << "Robot encountered an error." << std::endl;
            break;
    }
}

int main() {
    RobotState current_state = RobotState::IDLE;
    print_robot_state(current_state);

    current_state = RobotState::MOVING;
    print_robot_state(current_state);

    return 0;
}

In this code, we include the necessary header: iostream for input/output operations.

We define an enum class named RobotState to represent different states of a robot. The enum constants are IDLE, MOVING, GRASPING, and ERROR. Using an enum class provides strong typing and prevents naming conflicts.

We define a function print_robot_state() that takes a RobotState as a parameter. Inside the function, we use a switch statement to match the state and print the corresponding message.

In the main function, we create a variable current_state of type RobotState and initialize it with RobotState::IDLE. We pass this state to the print_robot_state() function, which prints the appropriate message.

We then change the current_state to RobotState::MOVING and call print_robot_state() again with the updated state.

Run the code.

1-enum-example

You should see the messages corresponding to the idle and moving states of the robot printed in the terminal.

Enums provide a way to create named constants, making your code more expressive and self-explanatory. They are particularly useful for representing states, modes, or options in robotic systems, improving code readability and reducing the chances of errors.

Generating Random Numbers

Let’s explore how to generate random numbers in C++, a useful technique for various robotics applications, such as simulations, sensor noise modeling, and path planning algorithms.

Let’s create a new C++ file called random_numbers_example.cpp.

Type the following code into the editor:

#include <iostream>
#include <random>
#include <ctime>

int main() {
    // Seed the random number generator
    std::srand(static_cast<unsigned int>(std::time(nullptr)));

    // Generate random integers
    int random_int = std::rand();
    std::cout << "Random integer: " << random_int << std::endl;

    // Generate random floats between 0 and 1
    float random_float = static_cast<float>(std::rand()) / static_cast<float>(RAND_MAX);
    std::cout << "Random float between 0 and 1: " << random_float << std::endl;

    // Generate random numbers within a range
    int min_value = 10;
    int max_value = 50;
    int random_range = min_value + (std::rand() % (max_value - min_value + 1));
    std::cout << "Random integer between " << min_value << " and " << max_value << ": " << random_range << std::endl;

    // Using the Mersenne Twister random number engine
    std::random_device rd;
    std::mt19937 gen(rd());
    std::uniform_real_distribution<double> dis(0.0, 1.0);
    double random_double = dis(gen);
    std::cout << "Random double between 0 and 1 (Mersenne Twister): " << random_double << std::endl;

    return 0;
}

In this example, we include the necessary headers: iostream for input/output operations, random for the Mersenne Twister random number engine, and ctime for seeding the random number generator.

First, we seed the random number generator using std::srand and the current time from std::time(nullptr). This ensures that we get a different sequence of random numbers each time we run the program.

We then generate a random integer using std::rand() and print it to the console.

Next, we generate a random float between 0 and 1 by dividing a random integer from std::rand() by the maximum value RAND_MAX.

To generate a random number within a specific range, we use the modulo operator % to get a random value between min_value and max_value and add min_value to shift the range.

For more advanced random number generation, we use the Mersenne Twister algorithm, which provides better statistical properties than the standard std::rand() function.

We create a std::random_device object rd to obtain a seed value, initialize a std::mt19937 generator gen with the seed, and create a std::uniform_real_distribution object dis to generate random doubles between 0 and 1. 

Finally, we generate a random double using dis(gen) and print it to the console.

Run the code.

2-random-numbers-example

You will see the random integers, floats, and doubles generated using different techniques printed in the terminal.

We’ve covered a lot of ground in these tutorials, from basic C++ concepts to advanced topics. Each of these features plays an important role in robotics programming, whether you’re working on motion control, sensor processing, or complex autonomous systems. 

Remember that writing good robotics code is about more than just making things work – it’s about writing clean, efficient, and maintainable code that can be reliably deployed in real-world applications. 

Don’t be afraid to experiment with combining different features to solve complex problems. The skills you’ve learned in these C++ tutorials will serve as a strong foundation for your robotics programming journey.

Thanks for following along with these tutorials.

Keep building!

How to Use Multithreading and Time Functions in C++

In this tutorial, we will explore multithreading and time functions in C++.

Prerequisites

Implementing Threads

Let’s explore how to implement threads in C++ for robotics projects. Threads allow multiple tasks to run simultaneously, which is essential for parallel processing in robotics applications. 

Open a terminal window, and type this: 

cd ~/Documents/cpp_tutorial 
code . 

Create a new C++ file and name it threads.cpp

Type the following code into the editor:

#include <iostream>
#include <thread>
#include <chrono>

void sensorTask(const std::string& sensorName, int duration) {
    std::cout << "Sensor task started: " << sensorName << std::endl;
    std::this_thread::sleep_for(std::chrono::seconds(duration));
    std::cout << "Sensor task completed: " << sensorName << std::endl;
}

void controlTask(const std::string& controllerName, int duration) {
    std::cout << "Control task started: " << controllerName << std::endl;
    std::this_thread::sleep_for(std::chrono::seconds(duration));
    std::cout << "Control task completed: " << controllerName << std::endl;
}

int main() {
    std::cout << "Main thread started" << std::endl;

    // Create threads for sensor tasks
    std::thread sensorThread1(sensorTask, "TemperatureSensor", 2);
    std::thread sensorThread2(sensorTask, "HumiditySensor", 3);

    // Create a thread for control task
    std::thread controlThread(controlTask, "MotionController", 4);

    // Wait for the threads to finish
    sensorThread1.join();
    sensorThread2.join();
    controlThread.join();

    std::cout << "Main thread completed" << std::endl;

    return 0;
}

In this code, we define two functions: sensorTask() and controlTask(). These functions simulate tasks performed by sensors and controllers in a robotics system. Each task prints a message when it starts and completes, and sleeps for a specified duration using std::this_thread::sleep_for().

In the main() function, we create three threads:

  • Two threads for sensor tasks: sensorThread1 and sensorThread2. These threads execute the sensorTask() function with different sensor names and durations.
  • One thread for the control task: controlThread. This thread executes the controlTask() function with a controller name and duration.

After creating the threads, we use join() to wait for each thread to finish before proceeding further in the main thread.

Run the code.

1-threads

The output displays the messages from each thread, indicating when they start and complete. The threads execute concurrently, and the main thread waits for all the threads to finish before completing.

Using Time Functions

Let’s explore how to use time functions in C++ to enhance your robotics projects. Time functions are essential for tasks such as measuring elapsed time, creating delays, and synchronizing actions in robotic systems.

We will use the chrono library, which provides a set of convenient functions to work with time in C++.

Create a new C++ file and name it time_example.cpp.

Type the following code into the editor:

#include <iostream>
#include <chrono>
#include <thread>

int main() {
    auto start_time = std::chrono::steady_clock::now();

    std::this_thread::sleep_for(std::chrono::seconds(2));

    auto end_time = std::chrono::steady_clock::now();
    std::chrono::duration<double> elapsed_seconds = end_time - start_time;

    std::cout << "Elapsed time: " << elapsed_seconds.count() << " seconds\n";
    return 0;
}

In this code, we include the necessary headers: iostream for input/output operations, chrono for time functions, and thread for creating delays. 

Then, in the main function, we capture the start time using std::chrono::steady_clock::now().

Next, we create a 2-second delay using std::this_thread::sleep_for(std::chrono::seconds(2)).

Finally, we capture the end time and calculate the elapsed time by subtracting start_time from end_time. We then print the elapsed time using std::cout.

Run the code.

2-time-example

You should see the elapsed time printed in the terminal.

Thanks, and I’ll see you in the next tutorial.

Keep building!