1.4.3. Fast DDS - Vulcanexus Topic Intercommunication

This tutorial presents a step-by-step demonstration on how to intercommunicate Vulcanexus applications with native Fast DDS applications.

1.4.3.1. Background

Being Fast DDS the default Vulcanexus middleware enables the possibility of intercommunicating Vulcanexus applications with native Fast DDS ones. This is of special interest when integrating pre-existing systems with each other, such as interfacing with a third-party software which exposes a DDS API. Since both Fast DDS’ and Vulcanexus’ backbone is DDS, it is possible to intercommunicate full-blown systems running a Vulcanexus stack with smaller systems for which Vulcanexus is either unnecessary or unfit, such as more constrained environments or applications that would not require any Vulcanexus functionality other than the middleware.

package "Controller MCU" {
    [Fast DDS Motor controller] as controller
}

package "Robot's main MCU" {
    [Vulcanexus path planner] => controller : <<DDS>>
}

1.4.3.2. Prerequisites

For convenience, this tutorial is built and run within a Docker environment, although Docker is not required. The tutorial focuses on the explanations regarding message type and topic name compatibilities rather than given an in depth explanation about the code used. Create a clean workspace and download the Vulcanexus - Fast DDS Topic Intercommunication project:

# Create directory structure
mkdir ~/vulcanexus_dds_ws
cd ~/vulcanexus_dds_ws
mkdir fastdds_app idl vulcanexus_app
# Download project source code
wget -O CMakeLists.txt https://raw.githubusercontent.com/eProsima/vulcanexus/iron/docs/resources/tutorials/core/deployment/dds2vulcanexus/topic/CMakeLists.txt
wget -O Dockerfile https://raw.githubusercontent.com/eProsima/vulcanexus/iron/docs/resources/tutorials/core/deployment/dds2vulcanexus/topic/Dockerfile
wget -O package.xml https://raw.githubusercontent.com/eProsima/vulcanexus/iron/docs/resources/tutorials/core/deployment/dds2vulcanexus/topic/package.xml
wget -O README.md https://raw.githubusercontent.com/eProsima/vulcanexus/iron/docs/resources/tutorials/core/deployment/dds2vulcanexus/topic/README.md
wget -O fastdds_app/subscriber.cpp https://raw.githubusercontent.com/eProsima/vulcanexus/iron/docs/resources/tutorials/core/deployment/dds2vulcanexus/topic/fastdds_app/subscriber.cpp
wget -O idl/HelloWorld.idl https://raw.githubusercontent.com/eProsima/vulcanexus/iron/docs/resources/tutorials/core/deployment/dds2vulcanexus/topic/idl/HelloWorld.idl
wget -O vulcanexus_app/publisher.cpp https://raw.githubusercontent.com/eProsima/vulcanexus/iron/docs/resources/tutorials/core/deployment/dds2vulcanexus/topic/vulcanexus_app/publisher.cpp

The resulting directory structure should be:

~/vulcanexus_dds_ws/
├── CMakeLists.txt
├── Dockerfile
├── fastdds_app
│   └── subscriber.cpp
├── idl
│   └── HelloWorld.idl
├── package.xml
├── README.md
└── vulcanexus_app
    └── publisher.cpp

Finally, the Docker image can be built with:

cd ~/vulcanexus_dds_ws
docker build -f Dockerfile -t dds2vulcanexus .

1.4.3.3. IDL type definition

Although the msg format used to be the preferred way to describe topic types in ROS 2 (just to ease the migration from ROS types), they get converted into IDL under the hood before the actual topic type related code is generated on the CMake call to rosidl_generate_interfaces. This means that the topic type definitions can be written as IDL files directly, allowing for a straight forward type compatibility with native DDS applications, since the standardized type definition format in DDS is in fact IDL. For a complete correspondence matrix between msg and IDL types (referred as DDS Types in the table), please refer to Field types.

This tutorial leverages ROS 2 capabilities of describing types in IDL to define a HelloWorld.idl that will be used by both the Vulcanexus and native Fast DDS applications. The HelloWorld.idl, and its msg equivalent is as follows:

module dds2vulcanexus
{
    module idl
    {
        struct HelloWorld
        {
            unsigned long index;
            string message;
        };
    };
};

It is important to note that rosidl_generate_interfaces converts the simple HelloWorld.msg into and IDL containing the structure (which is named after the msg file name) within 2 nested modules, the outermost being the package name (in this case dds2vulcanexus), and the innermost being the name of the directory in which the file is located. Mind that in the aforementioned directory structure, the IDL file is placed within an idl directory, hence the name of the innermost module.

The following sections detail how to incorporate the IDL message definition into both the Vulcanexus and native Fast DDS applications, covering both the C++ and CMake sides.

1.4.3.4. Vulcanexus Application

On this tutorial, the Vulcanexus application consists on a simple publisher node which will publish messages to the HelloWorld topic once a second.

1.4.3.4.1. Vulcanexus Application - Type generation

Inspecting the CMakeLists.txt file downloaded in Prerequisites, the following CMake code pertains the Vulcanexus publisher:

#####################################################################
# Vulcanexus application
#####################################################################
message(STATUS "Configuring Vulcanexus application...")
find_package(ament_cmake REQUIRED)
find_package(rclcpp REQUIRED)
find_package(rosidl_default_generators REQUIRED)

set(type_files
  "idl/HelloWorld.idl"
)
rosidl_generate_interfaces(${PROJECT_NAME}
  ${type_files}
)

ament_export_dependencies(rosidl_default_runtime)

add_executable(vulcanexus_publisher vulcanexus_app/publisher.cpp)
ament_target_dependencies(vulcanexus_publisher rclcpp)
rosidl_get_typesupport_target(cpp_typesupport_target ${PROJECT_NAME} "rosidl_typesupport_cpp")
target_link_libraries(vulcanexus_publisher "${cpp_typesupport_target}")

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

ament_package()

In particular, the type related code is generated in:

set(type_files
  "idl/HelloWorld.idl"
)
rosidl_generate_interfaces(${PROJECT_NAME}
  ${type_files}
)

1.4.3.4.2. Vulcanexus Application - C++

The simple Vulcanexus publisher node is as follows:

// Copyright 2022 Open Source Robotics Foundation, Inc.
//
// 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.

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

#include "rclcpp/qos.hpp"
#include "rclcpp/rclcpp.hpp"

#include "dds2vulcanexus/idl/hello_world.hpp"

using namespace std::chrono_literals;

class HelloWorldPublisher : public rclcpp::Node
{
public:
  HelloWorldPublisher()
  : Node("helloworld_publisher")
  {
    sample_.index = 0;
    sample_.message = "Hello from Vulcanexus";

    publisher_ = this->create_publisher<dds2vulcanexus::idl::HelloWorld>("HelloWorld", 10);

    auto timer_callback =
      [this]() -> void {
        sample_.index++;
        RCLCPP_INFO(
          this->get_logger(), "Publishing: '%s %u'",
          sample_.message.c_str(), sample_.index);
        this->publisher_->publish(sample_);
      };
    timer_ = this->create_wall_timer(1s, timer_callback);
  }

private:
  rclcpp::TimerBase::SharedPtr timer_;
  rclcpp::Publisher<dds2vulcanexus::idl::HelloWorld>::SharedPtr publisher_;
  dds2vulcanexus::idl::HelloWorld sample_;
};

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

To use the type generated from the IDL, three things are done:

  1. Include the generated type header:

    #include "dds2vulcanexus/idl/hello_world.hpp"
    
  2. Create a publisher in a HelloWorld topic which uses the generated type.

    1. First, the HelloWorldPublisher Node class stores a shared pointer to the publisher:

      rclcpp::Publisher<dds2vulcanexus::idl::HelloWorld>::SharedPtr publisher_;
      
    2. Then, upon construction, it instantiates the publisher, assigning it to the shared pointer class data member:

      publisher_ = this->create_publisher<dds2vulcanexus::idl::HelloWorld>("HelloWorld", 10);
      
  3. Publish data on the topic. In this case, the HelloWorldPublisher is using a wall timer to have periodic publications:

    1. HelloWorldPublisher has a data member for reusing the sample:

      dds2vulcanexus::idl::HelloWorld sample_;
      
    2. HelloWorldPublisher, upon construction, creates said wall timer, which is used to publish data:

      auto timer_callback =
        [this]() -> void {
          sample_.index++;
          RCLCPP_INFO(
            this->get_logger(), "Publishing: '%s %u'",
            sample_.message.c_str(), sample_.index);
          this->publisher_->publish(sample_);
        };
      timer_ = this->create_wall_timer(1s, timer_callback);
      

1.4.3.5. Fast DDS Application

Much like the Vulcanexus application, the native Fast DDS one consists on two parts:

  1. The generated type related code (a.k.a type support).

  2. The application code

1.4.3.5.1. Fast DDS Application - Type generation

In the case of Fast DDS, the type support is generated from the HelloWorld.idl file using Fast DDS-Gen.

In this tutorial, the Fast DDS type support is generated within the CMakeLists.txt file for the sake of completion and simplicity, but it can be generated as a pre-build step instead. Inspecting the CMakeLists.txt file downloaded in Prerequisites, the following CMake code pertains the native Fast DDS subscriber:

#####################################################################
# Fast DDS application
#####################################################################
message(STATUS "Configuring Fast DDS application...")
find_package(fastcdr REQUIRED)
find_package(fastrtps REQUIRED)
find_program(FASTDDSGEN fastddsgen)
set(
  GENERATED_TYPE_SUPPORT_FILES
  ${CMAKE_SOURCE_DIR}/fastdds_app/HelloWorld.h
  ${CMAKE_SOURCE_DIR}/fastdds_app/HelloWorld.cxx
  ${CMAKE_SOURCE_DIR}/fastdds_app/HelloWorldPubSubTypes.h
  ${CMAKE_SOURCE_DIR}/fastdds_app/HelloWorldPubSubTypes.cxx
)
add_custom_command(
  OUTPUT ${GENERATED_TYPE_SUPPORT_FILES}
  COMMAND ${FASTDDSGEN}
  -replace
  -typeros2
  -d ${CMAKE_SOURCE_DIR}/fastdds_app
  ${CMAKE_SOURCE_DIR}/idl/HelloWorld.idl
  DEPENDS ${CMAKE_SOURCE_DIR}/idl/HelloWorld.idl
  COMMENT "Fast DDS type support generation" VERBATIM
)

add_executable(
  fastdds_subscriber
  ${CMAKE_SOURCE_DIR}/fastdds_app/subscriber.cpp
  ${GENERATED_TYPE_SUPPORT_FILES}
)
target_link_libraries(fastdds_subscriber fastrtps fastcdr)
install(TARGETS
  fastdds_subscriber
  DESTINATION lib/${PROJECT_NAME}
)

In particular, the type generation related code is:

find_program(FASTDDSGEN fastddsgen)
set(
  GENERATED_TYPE_SUPPORT_FILES
  ${CMAKE_SOURCE_DIR}/fastdds_app/HelloWorld.h
  ${CMAKE_SOURCE_DIR}/fastdds_app/HelloWorld.cxx
  ${CMAKE_SOURCE_DIR}/fastdds_app/HelloWorldPubSubTypes.h
  ${CMAKE_SOURCE_DIR}/fastdds_app/HelloWorldPubSubTypes.cxx
)
add_custom_command(
  OUTPUT ${GENERATED_TYPE_SUPPORT_FILES}
  COMMAND ${FASTDDSGEN}
  -replace
  -typeros2
  -d ${CMAKE_SOURCE_DIR}/fastdds_app
  ${CMAKE_SOURCE_DIR}/idl/HelloWorld.idl
  DEPENDS ${CMAKE_SOURCE_DIR}/idl/HelloWorld.idl
  COMMENT "Fast DDS type support generation" VERBATIM
)

The call to Fast DDS-Gen within add_custom_command will generate the type support in the fastdds_app directory, leaving the file names in a convenient GENERATED_TYPE_SUPPORT_FILES CMake variable that is later used to add the source files to the executable. It is important to note the Fast DDS-Gen is called with the -typeros2 flag, so it generates ROS 2 compatible type names.

1.4.3.5.2. Fast DDS Application - C++

The native Fast DDS subscriber is as follows:

// Copyright 2022 Proyectos y Sistemas de Mantenimiento SL (eProsima).
//
// 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.

#include <condition_variable>
#include <csignal>
#include <mutex>

#include <fastdds/dds/domain/DomainParticipant.hpp>
#include <fastdds/dds/domain/DomainParticipantFactory.hpp>
#include <fastdds/dds/domain/qos/DomainParticipantQos.hpp>
#include <fastdds/dds/subscriber/DataReader.hpp>
#include <fastdds/dds/subscriber/DataReaderListener.hpp>
#include <fastdds/dds/subscriber/InstanceState.hpp>
#include <fastdds/dds/subscriber/qos/DataReaderQos.hpp>
#include <fastdds/dds/subscriber/qos/SubscriberQos.hpp>
#include <fastdds/dds/subscriber/Subscriber.hpp>
#include <fastrtps/subscriber/SampleInfo.h>
#include <fastrtps/types/TypesBase.h>

#include "HelloWorldPubSubTypes.h"

class HelloWorldSubscriber : public eprosima::fastdds::dds::DataReaderListener
{
public:

    HelloWorldSubscriber()
        : participant_(nullptr)
        , subscriber_(nullptr)
        , topic_(nullptr)
        , reader_(nullptr)
        , type_(new dds2vulcanexus::idl::HelloWorldPubSubType())
    {
    }

    virtual ~HelloWorldSubscriber()
    {
        if (reader_ != nullptr)
        {
            subscriber_->delete_datareader(reader_);
        }
        if (topic_ != nullptr)
        {
            participant_->delete_topic(topic_);
        }
        if (subscriber_ != nullptr)
        {
            participant_->delete_subscriber(subscriber_);
        }
        eprosima::fastdds::dds::DomainParticipantFactory::get_instance()->delete_participant(participant_);
    }

    bool init()
    {
        auto factory = eprosima::fastdds::dds::DomainParticipantFactory::get_instance();
        participant_ = factory->create_participant(0, eprosima::fastdds::dds::PARTICIPANT_QOS_DEFAULT);
        if (participant_ == nullptr)
        {
            return false;
        }

        participant_->register_type(type_);

        topic_ = participant_->create_topic("rt/HelloWorld",
                        type_.get_type_name(), eprosima::fastdds::dds::TOPIC_QOS_DEFAULT);
        if (topic_ == nullptr)
        {
            return false;
        }

        subscriber_ = participant_->create_subscriber(eprosima::fastdds::dds::SUBSCRIBER_QOS_DEFAULT);
        if (subscriber_ == nullptr)
        {
            return false;
        }

        reader_ = subscriber_->create_datareader(topic_, eprosima::fastdds::dds::DATAREADER_QOS_DEFAULT, this);
        if (reader_ == nullptr)
        {
            return false;
        }

        return true;
    }

    void on_data_available(
            eprosima::fastdds::dds::DataReader* reader) override
    {
        eprosima::fastdds::dds::SampleInfo info;
        if (reader->take_next_sample(&sample_, &info) == eprosima::fastrtps::types::ReturnCode_t::RETCODE_OK)
        {
            if (info.instance_state == eprosima::fastdds::dds::InstanceStateKind::ALIVE_INSTANCE_STATE)
            {
                std::cout << "Receiving: '" << sample_.message() << " " << sample_.index() << "'" << std::endl;
            }
        }
    }

private:

    eprosima::fastdds::dds::DomainParticipant* participant_;
    eprosima::fastdds::dds::Subscriber* subscriber_;
    eprosima::fastdds::dds::Topic* topic_;
    eprosima::fastdds::dds::DataReader* reader_;
    eprosima::fastdds::dds::TypeSupport type_;
    dds2vulcanexus::idl::HelloWorld sample_;
};

std::condition_variable cv;
std::mutex mtx;
std::atomic_bool running;

void signal_handler_callback(
        int signum)
{
    std::cout << std::endl << "Caught signal " << signum << "; closing down..." << std::endl;
    running.store(false);
    cv.notify_one();
}

int main()
{
    signal(SIGINT, signal_handler_callback);

    HelloWorldSubscriber subscriber;
    running.store(subscriber.init());

    std::unique_lock<std::mutex> lck(mtx);
    cv.wait(lck, [&]()
            {
                return !running.load();
            });
}

There are several things to unpack in this application:

  1. HelloWorldSubscriber holds both a reference to the type support, to create the topic, and a HelloWorld sample instance for reusing it upon reception.

    eprosima::fastdds::dds::TypeSupport type_;
    dds2vulcanexus::idl::HelloWorld sample_;
    
    1. The type support is instantiated upon construction:

      HelloWorldSubscriber()
          : participant_(nullptr)
          , subscriber_(nullptr)
          , topic_(nullptr)
          , reader_(nullptr)
          , type_(new dds2vulcanexus::idl::HelloWorldPubSubType())
      
    2. Then, it is registered in the DomainParticipant for further use:

      participant_->register_type(type_);
      
  2. The topic is created with name rt/HelloWorld. Mind that this topic name is different from the one set in the Vulcanexus publisher (HelloWorld). This is because Vulcanexus appends rt/ to the topic name passed when creating a Publisher or Subscription, where rt stands for ROS Topic, as services and actions have different prefixes (please refer to ROS 2 design documentation regarding Topic and Service name mapping to DDS). Another important detail is the type name, which in this example is extracted from the type support directly, as the type is generated with ROS 2 naming compatibility (see Fast DDS Application - Type generation).

    topic_ = participant_->create_topic("rt/HelloWorld",
                    type_.get_type_name(), eprosima::fastdds::dds::TOPIC_QOS_DEFAULT);
    if (topic_ == nullptr)
    {
        return false;
    }
    
  3. A DataReader is created in the topic, setting the very HelloWorldSubscriber as listener, since it inherits from DataReaderListener, overriding the on_data_available callback:

    subscriber_ = participant_->create_subscriber(eprosima::fastdds::dds::SUBSCRIBER_QOS_DEFAULT);
    if (subscriber_ == nullptr)
    {
        return false;
    }
    
    reader_ = subscriber_->create_datareader(topic_, eprosima::fastdds::dds::DATAREADER_QOS_DEFAULT, this);
    if (reader_ == nullptr)
    {
        return false;
    }
    
  4. Finally, when a new sample arrives, Fast DDS calls the implementation of on_data_available, which print the data to the STDOUT:

    void on_data_available(
            eprosima::fastdds::dds::DataReader* reader) override
    {
        eprosima::fastdds::dds::SampleInfo info;
        if (reader->take_next_sample(&sample_, &info) == eprosima::fastrtps::types::ReturnCode_t::RETCODE_OK)
        {
            if (info.instance_state == eprosima::fastdds::dds::InstanceStateKind::ALIVE_INSTANCE_STATE)
            {
                std::cout << "Receiving: '" << sample_.message() << " " << sample_.index() << "'" << std::endl;
            }
        }
    }
    

1.4.3.6. Run the demo

Once the Docker image is built, running the demo simply require two terminals. The image can be run in each of them with:

docker run -it --rm dds2vulcanexus

Then, run the publisher on one of the containers and the subscriber on the other:

vulcanexus_publisher