1.5.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.5.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.
1.5.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/jazzy/docs/resources/tutorials/core/deployment/dds2vulcanexus/topic/CMakeLists.txt
wget -O Dockerfile https://raw.githubusercontent.com/eProsima/vulcanexus/jazzy/docs/resources/tutorials/core/deployment/dds2vulcanexus/topic/Dockerfile
wget -O package.xml https://raw.githubusercontent.com/eProsima/vulcanexus/jazzy/docs/resources/tutorials/core/deployment/dds2vulcanexus/topic/package.xml
wget -O README.md https://raw.githubusercontent.com/eProsima/vulcanexus/jazzy/docs/resources/tutorials/core/deployment/dds2vulcanexus/topic/README.md
wget -O fastdds_app/subscriber.cpp https://raw.githubusercontent.com/eProsima/vulcanexus/jazzy/docs/resources/tutorials/core/deployment/dds2vulcanexus/topic/fastdds_app/subscriber.cpp
wget -O idl/HelloWorld.idl https://raw.githubusercontent.com/eProsima/vulcanexus/jazzy/docs/resources/tutorials/core/deployment/dds2vulcanexus/topic/idl/HelloWorld.idl
wget -O vulcanexus_app/publisher.cpp https://raw.githubusercontent.com/eProsima/vulcanexus/jazzy/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.5.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:
Important
If generating the type for a Fast DDS native application with Fast DDS-Gen v4.0.0 (the minimum required for Fast DDS v3.0.0), the extensibility must be explicitly marked as @extensibility(FINAL)
in idl structs.
Starting from Fast DDS-Gen v4.0.1, it can be left unspecified, as it defaults to @final
.
Please, refer to Fast DDS data types extensibility for further information about type extensibility.
module dds2vulcanexus
{
module idl
{
struct HelloWorld
{
unsigned long index;
string message;
};
};
};
uint32 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.5.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.5.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.5.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:
Include the generated type header:
#include "dds2vulcanexus/idl/hello_world.hpp"
Create a publisher in a
HelloWorld
topic which uses the generated type.First, the
HelloWorldPublisher
Node
class stores a shared pointer to the publisher:rclcpp::Publisher<dds2vulcanexus::idl::HelloWorld>::SharedPtr publisher_;
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);
Publish data on the topic. In this case, the
HelloWorldPublisher
is using a wall timer to have periodic publications:HelloWorldPublisher
has a data member for reusing the sample:dds2vulcanexus::idl::HelloWorld sample_;
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.5.3.5. Fast DDS Application¶
Much like the Vulcanexus application, the native Fast DDS one consists on two parts:
The generated type related code (a.k.a type support).
The application code
1.5.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
)
Important
If generating the type for a Fast DDS native application with Fast DDS-Gen v4.0.0 (the minimum required for Fast DDS v3.0.0), the extensibility must be explicitly marked as @extensibility(FINAL)
in idl structs.
Starting from Fast DDS-Gen v4.0.1, it can be left unspecified, as it defaults to @final
.
Please, refer to Fast DDS data types extensibility for further information about type extensibility.
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.5.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:
HelloWorldSubscriber
holds both a reference to the type support, to create the topic, and aHelloWorld
sample instance for reusing it upon reception.eprosima::fastdds::dds::TypeSupport type_; dds2vulcanexus::idl::HelloWorld sample_;
The type support is instantiated upon construction:
HelloWorldSubscriber() : participant_(nullptr) , subscriber_(nullptr) , topic_(nullptr) , reader_(nullptr) , type_(new dds2vulcanexus::idl::HelloWorldPubSubType())
Then, it is registered in the
DomainParticipant
for further use:participant_->register_type(type_);
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 appendsrt/
to the topic name passed when creating aPublisher
orSubscription
, wherert
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; }
A
DataReader
is created in the topic, setting the veryHelloWorldSubscriber
as listener, since it inherits fromDataReaderListener
, overriding theon_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; }
Finally, when a new sample arrives, Fast DDS calls the implementation of
on_data_available
, which print the data to theSTDOUT
: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.5.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
fastdds_subscriber