Create a ROS2 Workspace

Create a ROS2 Workspace #

ROS2 plays a crucial role in the RACCOON OS Userspace. It’s used as the topic-based publish/subscribe/service middleware and also as the build system for your mission-specific deployment. In this tutorial we will create and run an example ROS2 workspace on our development machine, and in the next step we will compile it for a target platform.

Create the Workspace Structure #

This is basically the same process as described in the ROS2 colcon tutorial. We optionally use an official ros container image to simplify the ROS2 installation process, but if your OS is supported by ROS2 (i.e. Ubuntu 22.04, RHEL 8 or Windows 10) it’s recommended to use those binary packages instead of a container.

Create a New Folder and Initialize Git #

$ mkdir -p ros2-example-workspace/src
$ cd ros2-example-workspace
$ git init

container.sh #

For convenience, we can create a simple script that will run whatever commands it is given inside of the ROS2 container.

#!/bin/sh

# Check if the 'ros' pod exists, if not create it
if ! podman pod exists ros; then
    podman pod create --name ros --userns keep-id
fi

podman run \
    -it \
    --pod ros \
    --workdir /work \
    -v $(pwd):/work \
    docker.io/ros:humble-ros-base \
    "$@"

Instead of docker we use podman because of its simplicity and lack of need to run a privileged daemon. It also makes it easy to run as the same user inside the container, instead of as root, using the --userns=keep-id flag. This alleviates the typical permissions problem that you may encounter when using build containers.

Make the script executable and commit it:

$ chmod +x container.sh
$ git add container.sh
$ git commit -m "Add container.sh"
[master (root-commit) c3f47cf] Add container.sh
 1 file changed, 7 insertions(+)
 create mode 100755 container.sh

Add a Simple C++ Publisher Node #

We follow similar steps to those described in the ROS2 C++ tutorial. While we mostly want to write the applications for RACCOON OS in Rust, we’ll add a simple publisher node in C++ to show the process of cross-compiling to the target in a simple way first. Preparing a Rust package for cross-compilation is a slightly more advanced workflow that is described in this tutorial. Run the following command from the src directory of your ROS2 workspace (optionally prefixed by ../container.sh):

src/ $ ../container.sh ros2 pkg create --build-type ament_cmake cpp_simple_publisher
going to create a new package
package name: cpp_simple_publisher
destination directory: /work
package format: 3
version: 0.0.0
description: TODO: Package description
maintainer: ['jdiez <jdiez@todo.todo>']
licenses: ['TODO: License declaration']
build type: ament_cmake
dependencies: []
creating folder ./cpp_simple_publisher
creating ./cpp_simple_publisher/package.xml
creating source and include folder
creating folder ./cpp_simple_publisher/src
creating folder ./cpp_simple_publisher/include/src/cpp_simple_publisher
creating ./cpp_simple_publisher/CMakeLists.txt

Now let’s add the code for the publisher:

src/cpp_simple_publisher/src/main.cc
#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;
}

Finally, we need to tell CMake how to build the publisher. Add this to src/simple_cpp_publisher/CMakeLists.txt:

src/simple_cpp_publisher/CMakeLists.txt
cmake_minimum_required(VERSION 3.5)
project(cpp_simple_publisher)

# Default to C++14
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_package(ament_cmake REQUIRED)
find_package(rclcpp REQUIRED)
find_package(std_msgs REQUIRED)

add_executable(talker src/main.cc)
ament_target_dependencies(talker rclcpp std_msgs)

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

ament_package()

Let’s build it and run it (from the top level directory of your workspace):

$ ./container.sh colcon build
Starting >>> cpp_simple_publisher
Finished <<< cpp_simple_publisher [4.97s]

Summary: 1 package finished [5.10s]
$ ./container.sh bash
jdiez@ros:~$ source install/setup.sh
jdiez@ros:~$ ros2 run cpp_simple_publisher talker
[INFO] [1723064903.495775843] [minimal_publisher]: Publishing: 'Hello, world! 0'
[INFO] [1723064903.995789024] [minimal_publisher]: Publishing: 'Hello, world! 1'
[INFO] [1723064904.495774612] [minimal_publisher]: Publishing: 'Hello, world! 2'
[INFO] [1723064904.995864134] [minimal_publisher]: Publishing: 'Hello, world! 3'

Note how we are first running source install.sh inside the container shell, and then we are using ros2 run to start the talker node.

In a new terminal we can run ros2 topic echo /topic to confirm that ROS2 messages are being published:

$ ./container.sh ros2 topic echo /topic
data: Hello, world! 18
---
data: Hello, world! 19
---
data: Hello, world! 20
---
data: Hello, world! 21

Note how there is no docker-compose required here. Such is the power of podman pods!