How to integrate OpenCV with a ROS2 C++ node

How to integrate OpenCV with a ROS2 C++ node

In this post, you will learn how to integrate the OpenCV library with a ROS2 C++ node. The example shown builds into a “hello-world” binary for ROS2 integration with OpenCV that publishes an image to the ROS network.

After going through this post, you would be able to use OpenCV to do things related to image processing and computer vision and make the results available to other ROS2 nodes. The example uses ROS2 Humble.

Step 1: Fire up a system with ROS2 installation

“Hey, do I have to install ros2 first?” Absolutely not! Just log in to The Construct to get access to virtual machines pre-installed with ROS.

Once logged in, click on My Rosjects, then Create a New Rosject, supply the information as shown in the image below, and click Create. Then RUN the rosject.

Create a new Rosject

You might also want to try this on a local PC if you have ros2 installed. However, please note that we cannot support local PCs and you will have to fix any errors you run into on your own. The rest of the instruction assumes that you are working on The Construct; please adapt them to your local PC and ros2 installation.

Step 2: Verify that OpenCV is installed

All ROS installs include OpenCV, so verify whether OpenCV has been installed.

Open Code Editor
Open a web shell

Open a web shell and run the following command:

pkg-config --modversion opencv4

You should get a version number similar to this:

4.5.4

If the above output is similar to what you see, you are set and ready, everything should work. Otherwise, please install OpenCV using the following command:

sudo apt install libopencv-dev python3-opencv

Step 3: Create a ROS2 C++ node integrating the OpenCV library

First, we need to create a package. We need the following dependencies for the package:

  • rclcpp – this is the ros2 C++ API we’ll use to create the ros2 node
  • std_msgs – needed for sending message header while sending the image
  • sensor_msgs – needed for sending the image itself
  • cv_bridge – converts from the OpenCV image format to the ros2 image format
  • image_transport – compresses the image for transport within the ros2 network
  • OpenCV – generates the image we want to send

Run the following command in the terminal you used in Step 2, to create the package:

cd ~/ros2_ws/src
ros2 pkg create my_opencv_demo --dependencies rclcpp std_msgs sensor_msgs cv_bridge image_transport OpenCV

Now go to the src folder of the package you just created and create the C++ file that will define the node:

cd ~/ros2_ws/src/my_opencv_demo/src
touch minimal_opencv_ros2_node.cpp

Open the Code Editor, locate the C++ file you just created, and paste the code indicated below. Explanations are given as comments within the code.

Open the Code Editor
#include "rclcpp/rclcpp.hpp"
#include "sensor_msgs/msg/image.hpp"
#include "std_msgs/msg/header.hpp"
#include <chrono&gt;
#include <cv_bridge/cv_bridge.h&gt; // cv_bridge converts between ROS 2 image messages and OpenCV image representations.
#include <image_transport/image_transport.hpp&gt; // Using image_transport allows us to publish and subscribe to compressed image streams in ROS2
#include <opencv2/opencv.hpp&gt; // We include everything about OpenCV as we don't care much about compilation time at the moment.

using namespace std::chrono_literals;

class MinimalImagePublisher : public rclcpp::Node {
public:
  MinimalImagePublisher() : Node("opencv_image_publisher"), count_(0) {
    publisher_ =
        this-&gt;create_publisher<sensor_msgs::msg::Image&gt;("random_image", 10);
    timer_ = this-&gt;create_wall_timer(
        500ms, std::bind(&amp;MinimalImagePublisher::timer_callback, this));
  }

private:
  void timer_callback() {
    // Create a new 640x480 image
    cv::Mat my_image(cv::Size(640, 480), CV_8UC3);

    // Generate an image where each pixel is a random color
    cv::randu(my_image, cv::Scalar(0, 0, 0), cv::Scalar(255, 255, 255));

    // Write message to be sent. Member function toImageMsg() converts a CvImage
    // into a ROS image message
    msg_ = cv_bridge::CvImage(std_msgs::msg::Header(), "bgr8", my_image)
               .toImageMsg();

    // Publish the image to the topic defined in the publisher
    publisher_-&gt;publish(*msg_.get());
    RCLCPP_INFO(this-&gt;get_logger(), "Image %ld published", count_);
    count_++;
  }
  rclcpp::TimerBase::SharedPtr timer_;
  sensor_msgs::msg::Image::SharedPtr msg_;
  rclcpp::Publisher<sensor_msgs::msg::Image&gt;::SharedPtr publisher_;
  size_t count_;
};

int main(int argc, char *argv[]) {
  rclcpp::init(argc, argv);
  // create a ros2 node
  auto node = std::make_shared<MinimalImagePublisher&gt;();

  // process ros2 callbacks until receiving a SIGINT (ctrl-c)
  rclcpp::spin(node);
  rclcpp::shutdown();
  return 0;
}

Finally, edit the package’s CMakeLists.txt (~/ros2_ws/src/my_opencv_demo/CMakeLists.txt) file to recognize the node. Copy the lines of code shown below and paste them before the invocation of ament_package():

add_executable(minimal_opencv_ros2_node src/minimal_opencv_ros2_node.cpp)
ament_target_dependencies(minimal_opencv_ros2_node rclcpp std_msgs sensor_msgs cv_bridge image_transport OpenCV)
​
install(TARGETS
   minimal_opencv_ros2_node
   DESTINATION lib/${PROJECT_NAME}
 )

The CMakeLists.txt file should now look like this:

cmake_minimum_required(VERSION 3.8)
project(my_opencv_demo)

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(std_msgs REQUIRED)
find_package(sensor_msgs REQUIRED)
find_package(cv_bridge REQUIRED)
find_package(image_transport REQUIRED)
find_package(OpenCV REQUIRED)

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()

add_executable(minimal_opencv_ros2_node src/minimal_opencv_ros2_node.cpp)
ament_target_dependencies(minimal_opencv_ros2_node rclcpp std_msgs sensor_msgs cv_bridge image_transport OpenCV)

install(TARGETS
  minimal_opencv_ros2_node
  DESTINATION lib/${PROJECT_NAME}
)

ament_package()

Congratulations! You have created a ros2 C++ node integrating OpenCV. Now, we need to see if it works!

Step 4: Compile and test ROS2 C++ node integrating the OpenCV library

Generate the ros2 executable by compiling and sourcing the package:

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

If you got to this point, great! Now we will run the node:

ros2 run my_opencv_demo minimal_opencv_ros2_node

You should see some output similar to this:

[INFO] [1677071986.446315963] [opencv_image_publisher]: Image 0 published
[INFO] [1677071986.941745471] [opencv_image_publisher]: Image 1 published
[INFO] [1677071987.442009334] [opencv_image_publisher]: Image 2 published
[INFO] [1677071987.941677164] [opencv_image_publisher]: Image 3 published
[INFO] [1677071988.441115565] [opencv_image_publisher]: Image 4 published
[INFO] [1677071988.940492910] [opencv_image_publisher]: Image 5 published
[INFO] [1677071989.441007118] [opencv_image_publisher]: Image 6 published

Now let’s run ros2 topic list to confirm the existence of the image topic. Leave the node running in the current terminal and run the following command in a new terminal:

source ~/ros2_ws/install/setup.bash
ros2 topic list

The output should inclide the /random_image topic:

/parameter_events
/random_image
/rosout

Finally lets us see what the image produced by OpenCV looks like. Run the following in the same terminal where you ran ros2 topic list. If you get the error, (image_view:8839): Gdk-ERROR **: 13:30:34.498: The program 'image_view' received an X Window System error, just run the command again.

ros2 run image_view image_view --ros-args --remap image:=/random_image

You should now see something like this pop up on your screen. Yahoo!

Random image output from the OpenCV node

Great job! You have successfully created a ros2 C++ node integrating OpenCV!

Step 5: Extra: add text to the Image

Copy the lines of code shown below and paste them inside the timer callback function just before writing the message to be sent:

    // Declare the text position
    cv::Point text_position(15, 40);
    
    // Declare the size and color of the font
    int font_size = 1;
    cv::Scalar font_color(255, 255, 255);

    // Declare the font weight
    int font_weight = 2;

    // Put the text in the image
    cv::putText(my_image, "ROS2 + OpenCV", text_position, cv::FONT_HERSHEY_COMPLEX, font_size, font_color, font_weight);

Stop the currently running node, recompile and source the package, and re-run the node. You should now see something like this:

Random image with text from the OpenCV node

Step 6: Check your learning

Do you understand how to create a ros2 C++ node integrating OpenCV? If you don’t know it yet, please go over the post again, more carefully this time.

(Extra) Step 7: Watch the video to understand how to integrate the OpenCV library with a ROS2 C++ node

Here you go:

If you want to learn more about ROS 2 and dive deeper into robotics, check out the Robotics Developer Masterclass, where you’ll master robotics development from scratch and get 100% job-ready to work at robotics companies.




Feedback

Did you like this post? Do you have any questions about how to integrate the OpenCV library with a ROS2 C++ node? Please leave a comment in the comments section below, so we can interact and learn from each other.

If you want to learn about other ROS2 topics, please let us know in the comments area and we will do a video or post about it.

How to introspect ROS 2 executables

How to introspect ROS 2 executables

In this post, you will learn how to introspect ROS 2 executables by identifying publishers and subscribers in the executables. This answers this question posted on ROS Answers.

Step 1: Copy sample packages containing ROS 2 executables

“Hey, do I have to install ros2 first?” Absolutely not! We will be using The Construct to get access to virtual machines pre-installed with ROS.

Click here to copy the ROS2 TurtleBot3 sandbox packages. Once copied, click the red RUN button to launch the packages in a virtual machine. Please be patient while the environment loads.

PS: You will need to login or create an account to copy the packages.


You might also want to try this on a local PC if you have ros2 and some executables installed. However, please note that we cannot support local PCs and you will have to fix any errors you run into on your own. The rest of the instruction assumes that you are working on The Construct; please adapt them to your local PC and ros2 installation.

Step 2: Explore the source code of an ament_python package

You can know what topics are being used by an executable (that is, introspect it) without running it by checking its source code, looking for publishers and subscribers.

Now head over to the Code Editor to make to explore the source code of the packages you just copied.

Open the Code Editor

First, we look at the turtlebot3_teleop package.

Turtblebot3 Teleop package
turtlebot3_teleop package

Let’s look for the executable file for this package. We can find that in the setup.py file.

turtlebot3_teleop/setup.py

from setuptools import find_packages
from setuptools import setup

package_name = 'turtlebot3_teleop'

setup(
    name=package_name,
    version='2.1.2',
    packages=find_packages(exclude=[]),
    data_files=[
        ('share/ament_index/resource_index/packages', ['resource/' + package_name]),
        ('share/' + package_name, ['package.xml']),
    ],
    install_requires=[
        'setuptools',
    ],
    zip_safe=True,
    author='Darby Lim',
    author_email='thlim@robotis.com',
    maintainer='Will Son',
    maintainer_email='willson@robotis.com',
    keywords=['ROS'],
    classifiers=[
        'Intended Audience :: Developers',
        'License :: OSI Approved :: Apache Software License',
        'Programming Language :: Python',
        'Topic :: Software Development',
    ],
    description=(
        'Teleoperation node using keyboard for TurtleBot3.'
    ),
    license='Apache License, Version 2.0',
    tests_require=['pytest'],
    entry_points={
        'console_scripts': [
            'teleop_keyboard = turtlebot3_teleop.script.teleop_keyboard:main'
        ],
    },
)

Looking at the entry_points part of the file, and then the console_scripts, we find the executable file is in the turtlebot3_teleop/script/teleop_keyboard.py file. Let’s find the publishers and/subscribers for this executable.

turtlebot3_teleop/script/teleop_keyboard.py

def main():
    settings = None
    if os.name != 'nt':
        settings = termios.tcgetattr(sys.stdin)

    rclpy.init()

    qos = QoSProfile(depth=10)
    node = rclpy.create_node('teleop_keyboard')
    pub = node.create_publisher(Twist, 'cmd_vel', qos)

    status = 0
    target_linear_velocity = 0.0
    target_angular_velocity = 0.0
    control_linear_velocity = 0.0
    control_angular_velocity = 0.0

    try:
        print(msg)
        while(1):
            key = get_key(settings)
            if key == 'w':
                target_linear_velocity =\
                    check_linear_limit_velocity(target_linear_velocity + LIN_VEL_STEP_SIZE)
                status = status + 1
                print_vels(target_linear_velocity, target_angular_velocity)
            elif key == 'x':
                target_linear_velocity =\
                    check_linear_limit_velocity(target_linear_velocity - LIN_VEL_STEP_SIZE)
                status = status + 1
                print_vels(target_linear_velocity, target_angular_velocity)
            elif key == 'a':
                target_angular_velocity =\
                    check_angular_limit_velocity(target_angular_velocity + ANG_VEL_STEP_SIZE)
                status = status + 1
                print_vels(target_linear_velocity, target_angular_velocity)
            elif key == 'd':
                target_angular_velocity =\
                    check_angular_limit_velocity(target_angular_velocity - ANG_VEL_STEP_SIZE)
                status = status + 1
                print_vels(target_linear_velocity, target_angular_velocity)
            elif key == ' ' or key == 's':
                target_linear_velocity = 0.0
                control_linear_velocity = 0.0
                target_angular_velocity = 0.0
                control_angular_velocity = 0.0
                print_vels(target_linear_velocity, target_angular_velocity)
            else:
                if (key == '\x03'):
                    break

            if status == 20:
                print(msg)
                status = 0

            twist = Twist()

            control_linear_velocity = make_simple_profile(
                control_linear_velocity,
                target_linear_velocity,
                (LIN_VEL_STEP_SIZE / 2.0))

            twist.linear.x = control_linear_velocity
            twist.linear.y = 0.0
            twist.linear.z = 0.0

            control_angular_velocity = make_simple_profile(
                control_angular_velocity,
                target_angular_velocity,
                (ANG_VEL_STEP_SIZE / 2.0))

            twist.angular.x = 0.0
            twist.angular.y = 0.0
            twist.angular.z = control_angular_velocity

            pub.publish(twist)

    except Exception as e:
        print(e)

    finally:
        twist = Twist()
        twist.linear.x = 0.0
        twist.linear.y = 0.0
        twist.linear.z = 0.0

        twist.angular.x = 0.0
        twist.angular.y = 0.0
        twist.angular.z = 0.0

        pub.publish(twist)

        if os.name != 'nt':
            termios.tcsetattr(sys.stdin, termios.TCSADRAIN, settings)


if __name__ == '__main__':
    main()

Success! On line 148 we can find that the executable creates a publisher to the /cmd_vel topic, so we know this is a topic used by the executable.

Are there other topics used by this executable? Find out!

Step 3: Explore the source code of an ament_cmake package

Let explore the turtlebot3_node package.

Turtlebot3_node package
turtlebot3_node package

We can find the main executable in the CMakeLists.txt file in the add_executable line (line 75).

turtlebot3_node/CMakeLists.txt

################################################################################
# Set minimum required version of cmake, project name and compile options
################################################################################
cmake_minimum_required(VERSION 3.5)
project(turtlebot3_node)

if(NOT CMAKE_CXX_STANDARD)
  set(CMAKE_CXX_STANDARD 14)
endif()

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

################################################################################
# Find ament packages and libraries for ament and system dependencies
################################################################################
find_package(ament_cmake REQUIRED)
find_package(dynamixel_sdk REQUIRED)
find_package(geometry_msgs REQUIRED)
find_package(message_filters REQUIRED)
find_package(nav_msgs REQUIRED)
find_package(rclcpp REQUIRED)
find_package(rcutils REQUIRED)
find_package(sensor_msgs REQUIRED)
find_package(std_msgs REQUIRED)
find_package(std_srvs REQUIRED)
find_package(tf2 REQUIRED)
find_package(tf2_ros REQUIRED)
find_package(turtlebot3_msgs REQUIRED)

################################################################################
# Build
################################################################################
include_directories(
  include
)

add_library(${PROJECT_NAME}_lib
  "src/devices/motor_power.cpp"
  "src/devices/sound.cpp"
  "src/devices/reset.cpp"

  "src/diff_drive_controller.cpp"
  "src/dynamixel_sdk_wrapper.cpp"
  "src/odometry.cpp"
  "src/turtlebot3.cpp"

  "src/sensors/battery_state.cpp"
  "src/sensors/imu.cpp"
  "src/sensors/joint_state.cpp"
  "src/sensors/sensor_state.cpp"
)

set(DEPENDENCIES
  "dynamixel_sdk"
  "geometry_msgs"
  "message_filters"
  "nav_msgs"
  "rclcpp"
  "rcutils"
  "sensor_msgs"
  "std_msgs"
  "std_srvs"
  "tf2"
  "tf2_ros"
  "turtlebot3_msgs"
)

target_link_libraries(${PROJECT_NAME}_lib)
ament_target_dependencies(${PROJECT_NAME}_lib ${DEPENDENCIES})

set(EXECUTABLE_NAME "turtlebot3_ros")

add_executable(${EXECUTABLE_NAME} src/node_main.cpp)
target_link_libraries(${EXECUTABLE_NAME} ${PROJECT_NAME}_lib)
ament_target_dependencies(${EXECUTABLE_NAME} ${DEPENDENCIES})

################################################################################
# Install
################################################################################
install(DIRECTORY param
  DESTINATION share/${PROJECT_NAME}
)

install(TARGETS ${EXECUTABLE_NAME}
  DESTINATION lib/${PROJECT_NAME}
)

################################################################################
# Macro for ament package
################################################################################
ament_export_include_directories(include)
ament_package()

So we see that the main executable is src/node_main.cpp. Let’s examine it.

turtlebot3_node/src/node_main.cpp

// Copyright 2019 ROBOTIS CO., LTD.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// Author: Darby Lim

#include <rcutils/cmdline_parser.h>
#include <rclcpp/rclcpp.hpp>

#include <chrono>
#include <memory>
#include <string>

#include "turtlebot3_node/diff_drive_controller.hpp"
#include "turtlebot3_node/turtlebot3.hpp"

void help_print()
{
  printf("For turtlebot3 node : \n");
  printf("turtlebot3_node [-i usb_port] [-h]\n");
  printf("options:\n");
  printf("-h : Print this help function.\n");
  printf("-i usb_port: Connected USB port with OpenCR.");
}

int main(int argc, char * argv[])
{
  setvbuf(stdout, NULL, _IONBF, BUFSIZ);

  if (rcutils_cli_option_exist(argv, argv + argc, "-h")) {
    help_print();
    return 0;
  }

  rclcpp::init(argc, argv);

  std::string usb_port = "/dev/ttyACM0";
  char * cli_options;
  cli_options = rcutils_cli_get_option(argv, argv + argc, "-i");
  if (nullptr != cli_options) {
    usb_port = std::string(cli_options);
  }

  rclcpp::executors::SingleThreadedExecutor executor;

  auto turtlebot3 = std::make_shared<robotis::turtlebot3::TurtleBot3>(usb_port);
  auto diff_drive_controller =
    std::make_shared<robotis::turtlebot3::DiffDriveController>(
    turtlebot3->get_wheels()->separation,
    turtlebot3->get_wheels()->radius);

  executor.add_node(turtlebot3);
  executor.add_node(diff_drive_controller);
  executor.spin();

  rclcpp::shutdown();

  return 0;
}

There’s no reference to a publisher or subscriber in this file, but it references some other files. Let’s look at the turtlebot3_node/turtlebot3.cpp file referenced on line 25.

turtlebot3_node/src/turtlebot3.cpp

void TurtleBot3::cmd_vel_callback()
{
  auto qos = rclcpp::QoS(rclcpp::KeepLast(10));
  cmd_vel_sub_ = this->create_subscription<geometry_msgs::msg::Twist>(
    "cmd_vel",
    qos,
    [this](const geometry_msgs::msg::Twist::SharedPtr msg) -> void
    {
      std::string sdk_msg;

      union Data {
        int32_t dword[6];
        uint8_t byte[4 * 6];
      } data;

      data.dword[0] = static_cast<int32_t>(msg->linear.x * 100);
      data.dword[1] = 0;
      data.dword[2] = 0;
      data.dword[3] = 0;
      data.dword[4] = 0;
      data.dword[5] = static_cast<int32_t>(msg->angular.z * 100);

      uint16_t start_addr = extern_control_table.cmd_velocity_linear_x.addr;
      uint16_t addr_length =
      (extern_control_table.cmd_velocity_angular_z.addr -
      extern_control_table.cmd_velocity_linear_x.addr) +
      extern_control_table.cmd_velocity_angular_z.length;

      uint8_t * p_data = &amp;data.byte[0];

      dxl_sdk_wrapper_->set_data_to_device(start_addr, addr_length, p_data, &amp;sdk_msg);

      RCLCPP_DEBUG(
        this->get_logger(),
        "lin_vel: %f ang_vel: %f msg : %s", msg->linear.x, msg->angular.z, sdk_msg.c_str());
    }
  );
}

Success! We see the /cmd_vel is also referenced on line 313. Can you find other files linked to the main file and it’s linked files, where other topics are referenced?

Step 4: Check your learning

Do you understand how to introspect ROS 2 executables? If you don’t know it yet, please go over the post again, more carefully this time.

(Extra) Step 5: Watch the video to understand how to introspect ROS 2 executables

Here you go:

Feedback

Did you like this post? Do you have any questions about how to introspect ROS 2 executables? Please leave a comment in the comments section below, so we can interact and learn from each other.

If you want to learn about other ROS2 topics, please let us know in the comments area and we will do a video or post about it.

How to create a ros2 C++ Library

How to create a ros2 C++ Library

In this post, you will learn how to create and use your own ros2 C++ library. I’ll show you how to create the library and then use it in a demo package.

Step 1: Fire up a system with ROS2 installation

“Hey, do you mean I have to install ros2 first?” Absolutely not! Just log in to The Construct to get access to virtual machines pre-installed with ROS.

Once logged in, click on My Rosjects, then Create a New Rosject, supply the information as shown in the video and click Create. Then RUN the rosject.

Create a new Rosject

You might also want to try this on a local PC if you have ros2 installed. However, please note that we cannot support local PCs and you will have to fix any errors you run into on your own. The rest of the instruction assumes that you are working on The Construct; please adapt them to your local PC and ros2 installation.

Step 2: Create a new package that contains the library’s source code (logic)

Open Code Editor
Open a web shell

Open a web shell and run the following commands to create the package.

cd ros2_ws/src
source /opt/ros/humble/setup.bash/
ros2 pkg create my_value_converter_library --build-type ament_cmake --license BSD-3-Clause

Now that your package is created, we need to create a header file for the library. Move inside the include/my_value_converter_library directory and create a header file named library_header.h.  

cd ~/ros2_ws/src/my_value_converter_library/include/my_value_converter_library
touch library_header.h

Now head over to the Code Editor to make changes to the header file. Check the image below for how to open the Code Editor.

Open the Code Editor

Locate the header file in the code editor: ros2_ws > src > my_value_converter_library > include > my_value_converter_library > library_header.h and paste in the following code.

#ifndef MAP_VALUE_H
#define MAP_VALUE_H

double map_value(double x, double in_min, double in_max, double out_min, double out_max);

#endif

Next, we create the C++ source code file for the library. In the same web shell:

cd ~/ros2_ws/src/my_value_converter_library/src
touch my_value_converter_library.cpp

Locate the my_value_converter_library.cpp file in the code editor and paste in the following code:

#include "my_value_converter_library/library_header.h"

double map_value(double x, double in_min, double in_max, double out_min, double out_max) {
  return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min;
}

Now we need to edit the CMakeLists.txt file of our package to recognize the source code we have just added. Open the file ros2_ws > src > my_value_converter_library > CMakeLists.txt and add the following lines before the ament_package() call.

# let the compiler search for headers in the include folder
include_directories(include)

# define a library target called my_value_converter_library
add_library(my_value_converter_library src/my_value_converter_library.cpp) 

# this line to exports the library
ament_export_targets(my_value_converter_library HAS_LIBRARY_TARGET)

# install the include/my_cpp_library directory to the install/include/my_cpp_library
install(
  DIRECTORY include/my_value_converter_library
  DESTINATION include
)

install(
  TARGETS my_value_converter_library
  EXPORT my_value_converter_library
  LIBRARY DESTINATION lib
  ARCHIVE DESTINATION lib
  RUNTIME DESTINATION bin
  INCLUDES DESTINATION include
)

So much for all the hard work – now is the time to see if it works. Time to compile the code. In the same web shell, run the following commands:

cd ~/ros2_ws
colcon build

Success! We have now created our library. Next, we are going to use it!

PS: if your code did not compile correctly, please go over the instructions and ensure you have created the files in the exact locations specified.

Step 3: Create a new package that uses the library you just created

Create the new package in the open shell. Note that my_value_converter_library is passed as a dependency during the creation of the new package. This will simplify some of the work that needs to be done when modifying the CMakeLists.txt file.

cd ros2_ws/src
ros2 pkg create my_value_converter_node --build-type ament_cmake --license BSD-3-Clause --dependencies rclcpp std_msgs my_value_converter_library

Now verify that the dependencies have been properly added. If the build runs successfully, it is so. If not, please correct the errors before you proceed.

cd ~/ros2_ws
colcon build

If you got to this point, great! Now we will create the C++ source code that will use your library.

cd ~/ros2_ws/src/my_value_converter_node/src
touch my_value_converter_node.cpp

Now locate the my_value_converter_node.cpp file in the code editor and paste in the following source code.

#include <memory>
#include <string>

#include "my_value_converter_library/library_header.h"
#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/float32.hpp"

class ConverterNode : public rclcpp::Node {
public:
  explicit ConverterNode() : Node("converter_node") {

    auto callback =
        [this](const std_msgs::msg::Float32::SharedPtr input_msg) -> void {
      RCLCPP_DEBUG(this->get_logger(), "I heard: [%f]", input_msg->data);

      // Use the library
      // Map an value from 0 - 1023 range to -1.0 to +1.0 range
      double new_value = map_value(input_msg->data, 0, 1023, -1.0, 1.0);

      output_msg_ = std::make_unique<std_msgs::msg::Float32>();
      output_msg_->data = new_value;
      RCLCPP_DEBUG(this->get_logger(), "Publishing: '%f'", output_msg_->data);
      pub_->publish(std::move(output_msg_));
    };

    sub_ = create_subscription<std_msgs::msg::Float32>("output_topic", 10,
                                                       callback);
    // Create a publisher
    rclcpp::QoS qos(rclcpp::KeepLast(7));
    pub_ = this->create_publisher<std_msgs::msg::Float32>("input_topic", qos);
  }

private:
  std::unique_ptr<std_msgs::msg::Float32> output_msg_;
  rclcpp::Publisher<std_msgs::msg::Float32>::SharedPtr pub_;
  rclcpp::Subscription<std_msgs::msg::Float32>::SharedPtr sub_;
};

int main(int argc, char *argv[]) {
  rclcpp::init(argc, argv);
  auto node = std::make_shared<ConverterNode>();
  RCLCPP_INFO(node->get_logger(), "My value converter node started.");
  rclcpp::spin(node);
  rclcpp::shutdown();
  return 0;
}

As you might have expected, we need to modify the CMakeLists.txt file for our new package. Locate the ros2_ws > src > my_value_converter_node > CMakeLists.txt in the code editor and paste in the following code before the ament_package() call.

# define the binary to be built and identify the source files with with which to build it
add_executable(main src/my_value_converter_node.cpp)
# tell CMake that the executable "main" depends on the library "my_value_converter_library"
ament_target_dependencies(main my_value_converter_library rclcpp std_msgs)
# install the executable in the lib folder to make it detectable through setup.bash
install(TARGETS 
  main
  DESTINATION lib/${PROJECT_NAME}/
)

Now build and use your package!

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

ros2 run my_value_converter_node main

Step 4: Check your learning

Do you understand how to create a ros2 C++ library? If you don’t know it yet, please go over the post again, more carefully this time.

(Extra) Step 5: Watch the video to understand how to install a ros2 binary package

Here you go:

Feedback

Did you like this post? Do you have any questions about how to create a ros2 C++ library? Please leave a comment in the comments section below, so we can interact and learn from each other.

If you want to learn about other ROS2 topics, please let us know in the comments area and we will do a video or post about it.

How to install a ros2 binary package

How to install a ros2 binary package

In this post, you will learn how to install a ros2 binary package and make it available for use. This post was written in response to this question on ROS Answers.

Step 1: Fire up a system with ROS2 installation

“Hey, do you mean I have to install ros2 first?” Absolutely not! Just login to The Construct to get access to virtual machines pre-installed with ROS.

Once logged in, click on My Rosjects, then Create a New Rosject, supply the information as shown in the video and click Create. Then RUN the rosject.

Create a new Rosject

You might also want to try this on a local PC if you have ros2 installed. However, please note that we cannot support local PCs and you will have to fix any errors you run into on your own. The rest of the instruction assumes that you are working on The Construct; please adapt them to your local PC and ros2 installation.

Step 2: Check the ros2 binaries currently installed

Open Code Editor
Open a web shell

Open a web shell and run the following commands. You should see a long list of many binaries!

cd /opt/ros/foxy/share/
ls -al

ROS binaries are stored in /opt/ros/{distro}/share/ If you would like to check if some binary is already installed without going through the long list, use grep. In this case we want to check binaries related to joint_state.

user:/opt/ros/foxy/share$ ls -al | grep joint_state
drwxr-xr-x  1 root root 4096 Sep 17  2021 joint_state_broadcaster
drwxr-xr-x  1 root root 4096 Sep 17  2021 joint_state_controller
drwxr-xr-x  1 root root 4096 Sep 17  2021 joint_state_publisher
drwxr-xr-x  1 root root 4096 Sep 17  2021 joint_state_publisher_gui

Heck, if you were the user in this post, you wouldn’t need to install anything. “What would I do then?!” Read on to find out!

Step 3: Install the ros2 binary package

We will demo this with the same package the user was struggling with: ros-foxy-joint-state-publisher-gui. Since the package is already installed in our case, we need to uninstall it first, just to demonstrate. Run the following on the current shell:

sudo apt update -y
sudo apt remove ros-foxy-joint-state-publisher-gui -y

Now verify that this GUI binary is no longer present.

user:/opt/ros/foxy/share$ ros2 pkg list | grep joint_state
joint_state_broadcaster
joint_state_controller
joint_state_publisher

Gone! Now we will install this package again.

sudo apt install ros-foxy-joint-state-publisher-gui -y

Before anything else, and this is probably what was missing for our friend who also installed the package successfully, SOURCE the ros2 distribution!

source /opt/ros/foxy/setup.bash

And that’s it! Installation complete. We are going to use the package now, but let’s check that it’s in the list of binaries.

user:/opt/ros/foxy/share$ ls -al | grep joint_state
drwxr-xr-x  1 root root 4096 Sep 17  2021 joint_state_broadcaster
drwxr-xr-x  1 root root 4096 Sep 17  2021 joint_state_controller
drwxr-xr-x  1 root root 4096 Sep 17  2021 joint_state_publisher
drwxr-xr-x  2 root root 4096 Oct 11 15:10 joint_state_publisher_gui

Step 4: Use the ros2 binary package

The user got an error when they tried to launch the GUI binary. Let’s see what we get (fingers crossed).

ros2 run joint_state_publisher_gui joint_state_publisher_gui

You should get a GUI like the image below pop up, confirming that the binary is really there. Cheers!

Hello, I am Joint State GUI

Step 5: Check your learning

Do you understand how to install a ros2 binary package and make it available for use? The magic word here is source.

If you didn’t get the point above, please go over the post again, more carefully this time.

(Extra) Step 6: Watch the video to understand how to install a ros2 binary package

Here you go:

Feedback

Did you like this post? Do you have any questions about how to install a ros2 binary package? Please leave a comment on the comments section below, so we can interact and learn from each other.

If you want to learn about other ROS2 topics, please let us know in the comments area and we will do a video or post about it.

Pin It on Pinterest