quinta-feira, 7 de abril de 2022

ROS 2: Publisher / Subscriber With Interface In The Same Package

    Now, let's remake the pub/sub example using a message interface to transfer more complex messages using ROS.

    As we did before, first let's create a new package called complex_pubsub

ros2 pkg create --build-type ament_cmake complex_pubsub --dependencies rclcpp

  

  Then, let's make a folder msg to hold message formats

mkdir msg

 

   Now let's create a file called UserData.msg


bool FEMALE=true
bool MALE=false
string first_name
string last_name
bool gender
uint8 age


    Next, go to CMakeLists.txt and let's setup the package for interface building, adding:

find_package(rosidl_default_generators REQUIRED)

set(msg_files
"msg/UserData.msg"
)

rosidl_generate_interfaces(${PROJECT_NAME}
${msg_files}
)

ament_export_dependencies(rosidl_default_runtime)


    Also, let's add the IDL compiler references to the package.xml


<buildtool_depend>rosidl_default_generators</buildtool_depend>
<exec_depend>rosidl_default_runtime</exec_depend>
<member_of_group>rosidl_interface_packages</member_of_group>


    Nice. So now we can create a very small and simple publisher publisher.cpp, without any handling logic, just to make it compile:


#include <memory>
#include <string>

#include "rclcpp/rclcpp.hpp"

class UserPublisher : public rclcpp::Node
{

};

int main(int argc, char * argv[])
{
rclcpp::init(argc, argv);
rclcpp::shutdown();
return 0;
}


    Now we can build the package:

colcon build --packages-select complex_pubsub

    After finish building the package, we have now a useful hpp file with our data struct and we can go back to the publisher code and include it. If you're using Visual Studio Code and it doesn't recognize this path for autocomplete, you can close this IDE and reload it.


#include "complex_pubsub/msg/user_data.hpp"


    Now we can  complete our publisher's code on file user_publisher.cpp:


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

#include "rclcpp/rclcpp.hpp"
#include "complex_pubsub/msg/user_data.hpp"

using namespace std::chrono_literals;

class UserPublisher : public rclcpp::Node
{
private:
rclcpp::Publisher<complex_pubsub::msg::UserData>::SharedPtr publisher;
rclcpp::TimerBase::SharedPtr timer;

public:
UserPublisher() : Node("user_publisher")
{
publisher = this->create_publisher<complex_pubsub::msg::UserData>(
        "user_data_topic", 10);

timer = this->create_wall_timer(1s, [this]() -> void {
auto msg = complex_pubsub::msg::UserData();
msg.first_name = "Users first name";
msg.last_name = "Users last name";
msg.age = 40;
msg.gender = msg.MALE;
RCLCPP_INFO(this->get_logger(), "publishing user data\n");

this->publisher->publish(msg);
});
}
};

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


    This code is very simple. We have basically to create the publisher and publish a message to it. The message must follow the format that we specify when creating the publisher. In this case, complex_pubsub::msg::UserData

    The method create_wall_timer will create a forever repeater that will execute the lambda code in 
[this]() -> void { (code) }

    Now, just to be able to test it, let's write a subscriber that will print the user's data being published. So create a file called user_subscriber.cpp with:


#include <memory>
#include <string>

#include "rclcpp/rclcpp.hpp"
#include "complex_pubsub/msg/user_data.hpp"

using std::placeholders::_1;

#define GENDER_OUTP_MALE "male"
#define GENDER_OUTP_FEMALE "female"

class UserSubscriber : public rclcpp::Node
{
private:
rclcpp::Subscription<complex_pubsub::msg::UserData>::SharedPtr subscriber;

void read_message(const complex_pubsub::msg::UserData &msg)
{
RCLCPP_INFO(this->get_logger(),
           "received user data: { first-name: '%s', last-name: '%s', " +
            "age: '%u', gender:'%s' }",
msg.first_name.c_str(),
msg.last_name.c_str(),
msg.age,
(msg.gender == msg.MALE) ? GENDER_OUTP_MALE : GENDER_OUTP_FEMALE
);
}

public:
UserSubscriber() : Node("user_publisher")
{
subscriber = this->create_subscription<complex_pubsub::msg::UserData>(
"user_data_topic", 10, std::bind(&UserSubscriber::read_message,
           this, _1));
}
};

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



     Now we change our CMakeLists.txt to support having msg / srv  in itself and to put together things to form the two executables: the publisher and the subscriber


cmake_minimum_required(VERSION 3.8)
project(complex_pubsub)

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

set(msg_files
"msg/UserData.msg"
)

rosidl_generate_interfaces(${PROJECT_NAME}
${msg_files}
)

add_executable(publisher
src/user_publisher.cpp
)

add_executable(subscriber
src/user_subscriber.cpp
)

ament_target_dependencies(publisher
rclcpp
)

ament_target_dependencies(subscriber
rclcpp
)

rosidl_target_interfaces(publisher
${PROJECT_NAME} "rosidl_typesupport_cpp")

rosidl_target_interfaces(subscriber
${PROJECT_NAME} "rosidl_typesupport_cpp")

install(TARGETS publisher subscriber
DESTINATION lib/${PROJECT_NAME})

ament_export_dependencies(rosidl_default_runtime)


ament_package()


Let's now test it!

1. Open another terminal and then please execute

. install/local_setup.bash
ros2 run complex_pubsub publisher
2. Check that the message is being published on the topic by using a ROS2 tool

. install/setup.bash
ros2 topic echo /user_data_topic
ros2 topic echo /user_data_topic
first_name: Users first name
last_name: Users last name
gender: false
age: 40
---
first_name: Users first name
last_name: Users last name
gender: false
age: 40
---

3. Execute the subscriber to see it receiving the published messages:

. install/local_setup.bash
ros2 run complex_pubsub subscriber
[INFO] [1649384531.571994547] [user_publisher]: received user data: { first-name: 'Users first name', last-name: 'Users last name', age: '40', gender:'male' }
[INFO] [1649384532.572024766] [user_publisher]: received user data: { first-name: 'Users first name', last-name: 'Users last name', age: '40', gender:'male' }
[INFO] [1649384533.572154201] [user_publisher]: received user data: { first-name: 'Users first name', last-name: 'Users last name', age: '40', gender:'male' }


quarta-feira, 6 de abril de 2022

ROS 2: Creating a Simple Service


    ROS Services work with a client-server model to provide an interface for IPC (interprocess communication) based on request / wait / check response  async programming.

    This type of programming is not new, being one of the most common models in computer programming. It allows us to request another part of the robot to do something and then go process something else for later on, check the result of that request. 

    This is not a callback based response, rather a "check later" based response, which is suitable for very fast tasks or  for some other use cases. It is good because its very simple and easy to understand, so if the async model will not require a complex multi-threading event-driven based request/response model, one should be just fine using this approach.

    A  service is therefore comprised of three parts, at least:

  • A server, responsible for creating the service and responding to it's request.
  • A client, which will consume the service.
  • A service message format which will define what is the format of the data being shared between those nodes

    ROS provides a very easy way of building this message format. All we need to do is to create one file: a .srv file, which will specify the message's format for a service.

In our example, we'll define a service that returns the greatest of two int64 numbers

ros2 pkg create --build-type ament_cmake greater2_interface --dependencies rclcpp
    Now, create a folder called srv and inside it, a file called GreaterInt.srv


int64 a
int64 b
---
int64 greater


    This file will specify a service message which will receive two int64 a and b and return another int64 greater

    Then, we need to teach cmake how to compile our code. Edit the CMakeLists.txt file and add


find_package(rosidl_default_generators REQUIRED)

rosidl_generate_interfaces(${PROJECT_NAME}
"src/srv/GreaterInt.srv"
)



    Then, edit the package.xml file and add


<build_depend>rosidl_default_generators</build_depend>
<exec_depend>rosidl_default_runtime</exec_depend>
<member_of_group>rosidl_interface_packages</member_of_group>


    Now, we can compile this package to import it later

colcon build --packages-select greater2_interface
    We can also check if the interface is ok by issuing:
. install/setup.bash
ros2 interface show greater2_interface/srv/GreaterInt
int64 a
int64 b
---
int64 greater

    That means our interface is prep and ready to be imported as a model to a service. Now, let's create a package to implement the server and the client to that server:

ros2 pkg create --build-type ament_cmake greater2_service --dependencies rclcpp greater2_interface


     Then, let's create the server file greater_int_server.cpp


#include <memory>

#include "rclcpp/rclcpp.hpp"
#include "greater2_interface/srv/greater_int.hpp"

void greater(
const std::shared_ptr<greater2_interface::srv::GreaterInt::Request> request,
const std::shared_ptr<greater2_interface::srv::GreaterInt::Response> response)
{
response->greater = request->a > request -> b ? request->a : request->b;
}

int main (int argc, char **argv) {
rclcpp::init(argc, argv);

auto node = rclcpp::Node::make_shared("greater_int_server");
auto service = node->create_service<greater2_interface::srv::GreaterInt>(
        "greater_int", &greater);

RCLCPP_INFO(rclcpp::get_logger("rclcpp"), "GreaterInt service is up");

rclcpp::spin(node);
rclcpp::shutdown();
}


    We can see that creating a service is very easy. The #include directive will include the generated struct for the data format we expect to handle. ROS also generates Request / Response interfaces for us.

    Then, we create a method to implement our logic and attach it to a service.

    Finally, we create a node and put ROS to spin it (run it) so it can handle service requests.


    The client is also very straightforward to implement. Let's check it out:


#include <chrono>
#include <cstdlib>
#include <memory>

#include "rclcpp/rclcpp.hpp"
#include "greater2_interface/srv/greater_int.hpp"

using namespace std::chrono_literals;

int main (int argc, char **argv) {
rclcpp::init(argc, argv);

if (argc != 3) {
RCLCPP_INFO(rclcpp::get_logger("rclcpp"), "usage %s [int] [int]",
            argv[0]);
return 1;
}

auto node = rclcpp::Node::make_shared("greater_int_client");
    auto client = node->create_client<greater2_interface::srv::GreaterInt>(
        "greater_int");

auto request =
        std::make_shared<greater2_interface::srv::GreaterInt::Request>();

request->a = atoll(argv[1]);
request->b = atoll(argv[2]);

while (!client->wait_for_service(1s)) {

if (!rclcpp::ok()) {
RCLCPP_ERROR(rclcpp::get_logger("rclcpp"),
                "Interrupted while waiting for the service. Exiting.");
return 0;
}

RCLCPP_INFO(rclcpp::get_logger("rclcpp"),
            "Service not available [yet]");
}

auto result_req = client->async_send_request(request);

auto result_code = rclcpp::spin_until_future_complete(node, result_req);

if (result_code == rclcpp::FutureReturnCode::SUCCESS) {
RCLCPP_INFO(rclcpp::get_logger("rclcpp"), "Greater: %ld",
            result_req.get()->greater);
} else {
RCLCPP_ERROR(rclcpp::get_logger("rclcpp"),
            "Failed to call service add_2");
}

rclcpp::shutdown();
return 0;
}



    First, we build a node object to be able to execute this client code as a node on ROS.

    Next, we create a client object which will allow us to send async requests to the specified service name

    After that, we build the request object itself, with the data that we want to send to the service.

    And then we use the client object to build a async request object. This object will only send the data once it gets spinned by ROS. We do so by using rclcpp::spin_until_future_complete ( )

    Finally, we check the result code and show the result data.

    This architecture is very similar to the async / await achitecture for those who program javascript.


    In order to compile our service package, we still need to set it up by editing CMakeFiles.txt and packages.xml for it (remember that we did it already, but for the interface package)

    Our CMakeFiles.txt will look like this:


cmake_minimum_required(VERSION 3.8)
project(greater2_service)

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

add_executable(server src/greater_int_server.cpp)
ament_target_dependencies(server
rclcpp
greater2_interface)

add_executable(client src/greater_int_client.cpp)
ament_target_dependencies(client
rclcpp
greater2_interface)

install(TARGETS
server
client
DESTINATION lib/${PROJECT_NAME})

ament_package()


    Notice that we are instructing cmake to find packages greater2_interface in find_package () and then we're inserting it when compiling our exec file in add_executable( )

    To our packages.xml, we just need to have the dependencies right:


<depend>rclcpp</depend>
<depend>greater2_interface</depend>


    If everything is done right, we should be able to execute the server and the client, both in different terminals and check the results:

. install/setup.bash
ros2 run greater2_service server
[INFO] [1649290854.198941166] [rclcpp]: GreaterInt service is up
. install/setup.bash
ros2 run greater2_service client 3 7

[INFO] [1649290900.712837482] [rclcpp]: Greater: 7


ROS 2: Creating a Simple Publisher / Subscriber

     We'll create a simple example of a publisher and a subscriber like we did for DDS, but this time using ROS to manage the process communication for us.

    By the end of this example, we'll have two nodes running, one of them will be publishing Hello World! and the other will be receiving it.


    First we create a new package called hello_pubsub

ros2 pkg create --build-type ament_cmake hello_pubsub

    

    Now, let's create the HelloPublisher class on a file called hello_publisher.cpp


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

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

using namespace std::chrono_literals;

#define MAX_MESSAGES_QUEUE 10

class HelloPublisher : public rclcpp::Node {
private:
size_t count;
rclcpp::TimerBase::SharedPtr timer;
rclcpp::Publisher<std_msgs::msg::String>::SharedPtr publisher;

void send_message () {
auto msg = std_msgs::msg::String();
msg.data = "Hello World! " + std::to_string(count);
count++;
publisher->publish(msg);

RCLCPP_INFO(this->get_logger(), "message #%d sent", count);
}

public:
HelloPublisher() : Node("hello_publisher"), count(0) {
publisher = this->create_publisher<std_msgs::msg::String>(
                "hello_world_messages_topic", MAX_MESSAGES_QUEUE);

/* A timer will be sending a message each 500ms */
timer = this->create_wall_timer(500ms,
                std::bind(&HelloPublisher::send_message, this));
}
};

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



    The publisher is very straighforward. ROS handles all the trouble here. All we have to do is to create an object of type rclcpp::Publisher that is typed to msg::String, because it is what we're sending.
    
    After that, we need to periodically send our message (because this is our USE CASE for this testing project)

    And that's it.

    The holding class, HelloPublisher, must inherits from the Node class, in order to be a "node" in the ROS process tree,

    
Now that we've created the publisher, let's do the same thing for the subscriber. We could repeat all of this and create it in another package, but for now let's create it on the same package as the publisher

    We'll name it hello_subscriber.cpp


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

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

using std::placeholders::_1;

#define MAX_MESSAGES_QUEUE 10

class HelloSubscriber : public rclcpp::Node {
private:
rclcpp::Subscription<std_msgs::msg::String>::SharedPtr subscriber;

void read_message (const std_msgs::msg::String & msg) {
RCLCPP_INFO(this->get_logger(), "received: '%s'",
                msg.data.c_str());
}

public:
HelloSubscriber() : Node("hello_subscriber") {
subscriber = this->create_subscription<std_msgs::msg::String>(
                "hello_world_messages_topic", MAX_MESSAGES_QUEUE,
                std::bind(&HelloSubscriber::read_message, this, _1));
}
};

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


The subscriber is also very straighforward. ROS handles all the trouble here too. 

    All we have to do is to create an object of type rclcpp::Subscription that is typed to msg::String, because it is what we're expecting, set the topic we wish to observe and then define a callback function that get's called whenever something is published on the topic .
    
    And that's it.


    To finish our example, we need to set up things to run.  

    We need to edit the package.xml file to add dependencies and the CMakeList.txt to teach the compiler how to compile our code.

    On package.xml, we add the dependecies for the standard message format and the rclcpp:

  <export>
<build_type>ament_cmake</build_type>
<depend>rclcpp</depend>
<depend>std_msgs</depend>
</export>


    On CMakeLists.txt, we also add dependencies for those packages, and add executable configurations. Another thing that we add is the install( )  method, which is responsible for defining a path to the output of the building.


cmake_minimum_required(VERSION 3.8)
project(hello_pubsub)

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)

add_executable(publisher src/hello_publisher.cpp)
add_executable(subscriber src/hello_subscriber.cpp)

ament_target_dependencies(publisher rclcpp std_msgs)
ament_target_dependencies(subscriber rclcpp std_msgs)

install(TARGETS
publisher
subscriber
DESTINATION lib/${PROJECT_NAME})

ament_package()



    All set. Now, let's download the dependencies by issuing:

rosdep install -i --from-path src --rosdistro galactic -y
    And then, we compile our new package

colcon build --packages-select hello_pubsub
    Finally, let's open two separate consoles, one running the publisher and the other running the subscriber


Publisher
. install/setup.bash
ros2 run hello_pubsub publisher
Subscriber
. install/setup.bash
ros2 run hello_pubsub subscriber
[INFO] [1649219711.222277763] [hello_publisher]: message #86 sent
[INFO] [1649219711.722307938] [hello_publisher]: message #87 sent
[INFO] [1649219712.222268412] [hello_publisher]: message #88 sent
[INFO] [1649219712.722280066] [hello_publisher]: message #89 sent
[INFO] [1649219713.222285281] [hello_publisher]: message #90 sent
(...)

[INFO] [1649219711.722458435] [hello_subscriber]: received: 'Hello World! 86'
[INFO] [1649219712.222437726] [hello_subscriber]: received: 'Hello World! 87'
[INFO] [1649219712.722470352] [hello_subscriber]: received: 'Hello World! 88'
[INFO] [1649219713.222397218] [hello_subscriber]: received: 'Hello World! 89'
(..)

terça-feira, 5 de abril de 2022

ROS 2: Hello World!




Package Creation

    Well, it's time to do the same code as we did for the DDS layer, but now for ROS 2 instead.

    The first thing we need to do to create ROS-2 code is to build a package. A package works as a container for your code to be run and shared among other ROS-2 users / machines / projects.

    To create a package, first we need to have a workspace set up. 


    Workspace

        First you need to choose a path for your workspace. For this example, let's make one called ws. The name doesn't matter.

mkdir -p ws/src              
cd ws/

        Notice that we also created a subdir called src. This dir is to be holding our package code. It is considered good practice having a subdir called src to hold the packages in a workspace.

        Next, let's activate ROS-2 shell env

source /opt/ros/galactic/setup.bash

    Package building


    
    Nice, ok, so now let's move on to the package creation and building.  For C++ programming, we need to have at least two files :

  • package.xml    -   meta information about our package
  • CMakeList.txt -   describes how to compile our package's code
        Let's do it all by issuing the following command

ros2 pkg create --build-type ament_cmake --node-name hello_node hello_world
        This creates the basic structure of a package named hello_world for a node called hello_node for us. 




        Now let's build the package to test if everything is working as expected. The following command will build every package you have in your workspace. That's fine for now, because we only have our hello_world package

cd ..
colcon build               
Starting >>> hello_world
Finished <<< hello_world [1.42s]                     

Summary: 1 package finished [1.77s]

        If you wish to build only a specific package, you should use the command bellow. Let's try it now too.

colcon build --packages-select (package name) 


    
    Finally, let's test our package by running it on a node. Remember that we are simulating a robot processing unit such as a microcontroller, so the terminal (which is a OS process) will work as that processor to run our node.

        In order to differentiate it from other processes that are potentially running in parallel in our robot, we'll setup our terminal to point out to our hello_world package / hello_node node  "session", then we'll ask ros to run the code


. install/local_setup.bash
ros2 run hello_world hello_node
        ROS ran our code and then returned it's output:  "hello world hello_world package"

        Let's go and change it so we get a better hello world message. In hello_node.cpp:

#include <cstdio>

int main(int argc, char ** argv)
{
(void) argc;
(void) argv;

printf("Hello world from ROS!\n");
return 0;
}


      Now rebuild and run it to test it:

colcon build --packages-select --allow-overriding hello_world
ros2 run hello_world hello_node
Hello world from ROS!



segunda-feira, 4 de abril de 2022

DDS: Data Distribution Service

 What is DDS?

    DDS means Data Distribution Service and its a standard for machine-to-machine real-time systems communication that uses a publish-subscribe pattern to allow complex high dependable systems to handle their internal communication in a scalable and interoperable way.

    It works as a middleware that simplifies complex network programming. As we've discoursed on the topic about ROS 2 fundamentals,  a pub/sub architecture is a sort of re-reading of the classical producer-consumer problem, where we have n producers and m consumers.

    Each node can be consuming or producing a message, and we could have a lot of messages dealing with a lot of things running through the system in parallel.

    In order to keep things organized, every "parallel" messaging queue is called a topic.

    So a pub/sub system works as follows: 

  • A publisher is a node that intends to produce a message. This publisher will publish the message in a topic.
  • A consumer is a node whose job is to listen to any message of a specific topic and react to it appropriately. So a consumer will subscribe to a topic in order to be alerted by the pub/sub system that a message is available to be read.
    This architecture has a concept of a QoS (quality of service) which are some parameters to alter the expected behavior of this middleware up-from. For example, you could make it more reliable (but slower), because you can't afford to lose messages. 

 Eclipse Cyclone DDS

    The Eclipse Cyclone DDS is an implementation of the OMG specification that is a part of the Eclipse Foundation's IoT project and that is currently (Galactic version) the default middleware for ROS 2.

    The Cyclone DDS aims at full coverage of the specs and today already covers most of this. Some optional parts are still missing. With references to the individual OMG specifications:

  • DCPS the base specification
    • zero configuration discovery (if multicast works)
    • publish/subscribe messaging
    • configurable storage of data in subscribers
    • many QoS settings - liveliness monitoring, deadlines, historical data, 
    • ...
    • coverage includes the Minimum, Ownership and (partially) Content profiles
  • DDS Security - providing authentication, access control and encryption
  • DDS C++ API
  • DDS XTypes - the structural type system (some caveats here)
  • DDSI-RTPS - the interoperable network protocol
Let's dig into it

    The tutorial bellow will allow us to better understand DDS. It is not required to run ROS 2, but it's good to understand how it works.

    First and foremost, we need to install the Cyclone DDS and then we can take a look at a code as an example of using this lib.

    1. Cloning the latest version
    
        To test this DDS implementation, lets first clone it and build it from source
git clone https://github.com/eclipse-cyclonedds/cyclonedds.git
cd cyclonedds

    2. Building and installing iceoryx.io
    
        Iceoryx.io is a lib to allow message communication using shared memory, instead of using UDP. First, we install the dependencies by issuing
sudo apt install gcc g++ cmake libacl1-dev libncurses5-dev pkg-config

        Then, we'll download the source code and compile / install it
git clone https://github.com/eclipse-iceoryx/iceoryx.git
cd iceoryx
cmake -Bbuild -Hiceoryx_meta -DCMAKE_PREFIX_PATH=$(PWD)/build/dependencies/

        Next, compile the source code and install it to the system
cmake --build build
sudo cmake --build build --target install

    3. Building Cyclone DDS from the source
    
        Finally, let's build the Cyclone DDS from the source
mkdir build
cd build
cmake -DCMAKE_INSTALL_PREFIX=/opt/cyclonedds -S..
cmake --build .
cmake --build . --target install

 Analyzing the Hello World example

    Let's first analyze a code that comes with the source code that implements a "hello world" for this DDS.  For this implementation, we'll have a publisher that will send a Hello World message once a subscriber shows up.

    We'll also have the subscriber to subscribe, consume and show this message.

    The first thing we have is to define a message format using the IDL language. This language is responsible for defining an interface. The "source code" for this definition should be compiled to C to be integrated into the final binary file

So, go to the folder examples/helloworld and take a look at the file HelloWorldData.idl

module HelloWorldData
{
  struct Msg
  {
    @key
    long userID;
    string message;
  };
};


To compile it, we issue the following command
/opt/cyclonedds/bin/idlc ./HelloWorldData.idl

The above command will generate two files: HelloWorldData.c, HelloWorldData.h implementing this structure.


Then, we can join those two files with the publisher.c code and create the hello world publisher
gcc -o publisher publisher.c HelloWorldData.c -I/opt/cyclonedds/include/ -I. -L/opt/cyclonedds/lib -lddsc

After that, we build the subscriber
gcc -o subscriber subscriber.c HelloWorldData.c -I/opt/cyclonedds/include/ -I. -L/opt/cyclonedds/lib -lddsc

Finally, we need to link the libs on /opt/cyclone/lib to /usr/lib and link those libraries.
sudo ln -s /opt/cyclonedds/lib/libddsc.so.0 /usr/lib/libddsc.so.0
ldconfig -v

Now, we can run those examples and see the results
./publisher 
=== [Publisher]  Waiting for a reader to be discovered ...

./subscriber 
=== [Subscriber] Waiting for a sample ...
=== [Subscriber] Received : Message (1, Hello World)

The Publisher Code

The publisher code is very straightforward. First, we create a participant, which is an actor in the DDS world


/* Create a Participant. */
participant = dds_create_participant (DDS_DOMAIN_DEFAULT, NULL, NULL);
if (participant < 0)
DDS_FATAL("dds_create_participant: %s\n", dds_strretcode(-participant));


Then, we create a topic to publish to

topic = dds_create_topic (
participant, &HelloWorldData_Msg_desc, "HelloWorldData_Msg", NULL, NULL);
if (topic < 0)
DDS_FATAL("dds_create_topic: %s\n", dds_strretcode(-topic));


Next, we create a writer, which is the method to create a publisher implicitly.


writer = dds_create_writer (participant, topic, NULL, NULL);
if (writer < 0)
DDS_FATAL("dds_create_writer: %s\n", dds_strretcode(-writer));



After that, we call the dds_set_status_mask( ) function which is responsible for enabling actions based on status mask. In this example, the chosen mask is DDS_PUBLICATION_MATCHED _STATUS which means that the writer has found a reader that matches the topic and has a compatible QoS


rc = dds_set_status_mask(writer, DDS_PUBLICATION_MATCHED_STATUS);
if (rc != DDS_RETCODE_OK)
DDS_FATAL("dds_set_status_mask: %s\n", dds_strretcode(-rc));



Finally we put the publisher to sleep, waiting for a subscriber to subscribe to a topic. 
while(!(status & DDS_PUBLICATION_MATCHED_STATUS))
{
rc = dds_get_status_changes (writer, &status);
if (rc != DDS_RETCODE_OK)
DDS_FATAL("dds_get_status_changes: %s\n", dds_strretcode(-rc));

/* Polling sleep. */
dds_sleepfor (DDS_MSECS (20));
}


When the subscriber comes to live, we execute dds_write ( ) to send the message.
  msg.userID = 1;
msg.message = "Hello World";

printf ("=== [Publisher] Writing : ");
printf ("Message (%"PRId32", %s)\n", msg.userID, msg.message);
fflush (stdout);

rc = dds_write (writer, &msg);
if (rc != DDS_RETCODE_OK)
DDS_FATAL("dds_write: %s\n", dds_strretcode(-rc));



The Subscriber Code

    For the subscriber code, the code is also straightforward and very similar to the publisher.

    First, we also need to create a participant, which in this case is the subscriber.

participant = dds_create_participant (DDS_DOMAIN_DEFAULT, NULL, NULL);
if (participant < 0)
DDS_FATAL("dds_create_participant: %s\n", dds_strretcode(-participant));

  
   Then, we create a topic to subscribe to
topic = dds_create_topic (
participant, &HelloWorldData_Msg_desc, "HelloWorldData_Msg", NULL, NULL);
if (topic < 0)
DDS_FATAL("dds_create_topic: %s\n", dds_strretcode(-topic));



    Next, we set QoS and config it to be a reliable reader. A reliable reader will check data for transmission error, assuring that it is well transmitted (much like a TCP connection would do).

  
qos = dds_create_qos ();
dds_qset_reliability (qos, DDS_RELIABILITY_RELIABLE, DDS_SECS (10));
  

  Finally, we create a reader which implicitly creates a subscriber to a topic.


reader = dds_create_reader (participant, topic, qos, NULL);
if (reader < 0)
DDS_FATAL("dds_create_reader: %s\n", dds_strretcode(-reader));
dds_delete_qos(qos);



    Then, we read the incoming message using the dds_reader ( ) function.


  rc = dds_read (reader, samples, infos, MAX_SAMPLES, MAX_SAMPLES);
if (rc < 0)
DDS_FATAL("dds_read: %s\n", dds_strretcode(-rc));


Final Thoughts

    DDS is a very nice pub/sub specification to allow decoupling modules in a complex system. 

    We chose the current official implementation of DDS on ROS  2 as an example, but you can find other examples to study. Most of them have a pretty similar structure.

    I hope you've learned something on this post. Let's move on.... there's yet a lot to learn!

ROS2: Executables, Plugins and Components

    Introduction       In ROS-2, we have basically three types of code-run generation:  an executable, a plugin and a component.       An ex...