Getting Started with ROS on Embedded Systems - User Guide - C++ - Package set up

From RidgeRun Developer Wiki




Previous: C++ User Guide Index Next: User Guide/C++/Initialization




Introduction

This serves as an introduction on how to create and build simple packages using ROS with the colcon build system.

An ROS package is simply one folder located under a workspace that can be constructed by using a package manager, for example, colcon or catkin. This guide will use the colcon build system.

A package can be created using the ROS2 command, like:

ros2 pkg create --license Apache-2.0 <pkg-name> --dependencies [deps]

A project setup for colcon generally will look like:

root@vision:/test# tree .
.
├── CMakeLists.txt
├── include
│   └── test
├── LICENSE
├── package.xml
└── src

3 directories, 3 files

To test our test package, we can build it:

cd /test
colcon build

If all goes well, you should see:

root@vision:/test# colcon build
Starting >>> test    
Finished <<< test [5.37s]                  

Summary: 1 package finished [5.74s]d

Sample publisher

We are going to make a simple text publisher and receiver, using the following guide. The first step in configuring the package is to modify the package.xml. You will have entries similar to the following:

<?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>test</name>
  <version>0.0.0</version>
  <description>TODO: Package description</description>
  <maintainer email="root@todo.todo">root</maintainer>
  <license>Apache-2.0</license>

  <buildtool_depend>ament_cmake</buildtool_depend>

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

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

Now replace the TODOs with your info. Then add the following dependencies:

  <depend>rclcpp</depend>
  <depend>std_msgs</depend>

Now let's add the publisher sources, create the file src/sample_pub.cpp and add the following:

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

#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/string.hpp"

using namespace std::chrono_literals;

/* This example creates a subclass of Node and uses std::bind() to register a
* member function as a callback from the timer. */

class MinimalPublisher : public rclcpp::Node
{
  public:
    MinimalPublisher()
    : Node("minimal_publisher"), count_(0)
    {
      publisher_ = this->create_publisher<std_msgs::msg::String>("topic", 10);
      timer_ = this->create_wall_timer(
      500ms, std::bind(&MinimalPublisher::timer_callback, this));
    }

  private:
    void timer_callback()
    {
      auto message = std_msgs::msg::String();
      message.data = "Hello, world! " + std::to_string(count_++);
      RCLCPP_INFO(this->get_logger(), "Publishing: '%s'", message.data.c_str());
      publisher_->publish(message);
    }
    rclcpp::TimerBase::SharedPtr timer_;
    rclcpp::Publisher<std_msgs::msg::String>::SharedPtr publisher_;
    size_t count_;
};

int main(int argc, char * argv[])
{
  rclcpp::init(argc, argv);
  rclcpp::spin(std::make_shared<MinimalPublisher>());
  rclcpp::shutdown();
  return 0;
}

The code has 3 main sections:

  • ROS initialization:
  rclcpp::init(argc, argv);
  • Node declaration, where we declare our ROS node that extends from the base rclcpp::Node class.
  • Main thread initialization:
  rclcpp::spin(std::make_shared<MinimalPublisher>());

Now we can add our listener, create the file src/sample_listener.cpp and add the following:

#include <memory>

#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/string.hpp"
using std::placeholders::_1;

class MinimalSubscriber : public rclcpp::Node
{
  public:
    MinimalSubscriber()
    : Node("minimal_subscriber")
    {
      subscription_ = this->create_subscription<std_msgs::msg::String>(
      "topic", 10, std::bind(&MinimalSubscriber::topic_callback, this, _1));
    }

  private:
    void topic_callback(const std_msgs::msg::String & msg) const
    {
      RCLCPP_INFO(this->get_logger(), "I heard: '%s'", msg.data.c_str());
    }
    rclcpp::Subscription<std_msgs::msg::String>::SharedPtr subscription_;
};

int main(int argc, char * argv[])
{
  rclcpp::init(argc, argv);
  rclcpp::spin(std::make_shared<MinimalSubscriber>());
  rclcpp::shutdown();
  return 0;
}

We have a similar structure to the publisher code, with the main three sections, but the node is set to listen to a topic, instead of writing to it with:

create_subscription<std_msgs::msg::String>("topic", 10, std::bind(&MinimalSubscriber::topic_callback, this, _1));

Where the node will listen to "topic".
After that we can also take a look at the CMakeList.txt file:

cmake_minimum_required(VERSION 3.8)
project(test)

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)
# uncomment the following section in order to fill in
# further dependencies manually.
# find_package(<dependency> 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()

ament_package()

First we add the dependencies on the dependencies section:

find_package(rclcpp REQUIRED)
find_package(std_msgs REQUIRED)

Now we can add our sources and set the executable targets after the find_packages statements:

# publiser code
add_executable(talker src/publisher_member_function.cpp)
ament_target_dependencies(talker rclcpp std_msgs)
# listener code
add_executable(listener src/sample_listener.cpp)
ament_target_dependencies(listener rclcpp std_msgs)

Now we can fetch the dependencies:

rosdep install -i --from-path src --rosdistro humble -y

After that finishes, we can compile it:

colcon build

Now we can either use the executable directly on the build/test folder or install them using the install script like ". install/setup.bash".
After that we can open two terminals and execute one executable on each. Remember to do "source /ros_entrypoint.sh" on every new terminal that is opened. If we run the talker on one and the listener on the other, we will see something like:


Simple listener and publisher example


Previous: C++ User Guide Index Next: User Guide/C++/Initialization