Skip to content

Latest commit

 

History

History
 
 

icedelivery

icedelivery - Transfer data between POSIX applications

Introduction

This example showcases a data transmission setup with zero-copy inter-process communication (IPC) on iceoryx. It provides publisher and subscriber applications. They come in two C++ API flavours (untyped and typed). Check icedelivery_on_c for the C API

Run icedelivery

Create different terminals and run one command in each of them. Choose at least one publisher and one subscriber for having a data communication. You can also mix the typed and untyped versions. And if you feel like crazy today you start several publishers and subscribers from icedelivery and icedelivery_on_c (needs the default n:m communication, not possible if you build with the ONE_TO_MANY option)

# If installed and available in PATH environment variable
iox-roudi
# If build from scratch with script in tools
$ICEORYX_ROOT/build/iox-roudi


build/iceoryx_examples/icedelivery/iox-ex-publisher-untyped
# The untyped publisher is an alternative
build/iceoryx_examples/icedelivery/iox-ex-publisher-typed


build/iceoryx_examples/icedelivery/iox-ex-subscriber-untyped
# The untyped subscriber is an alternative
build/iceoryx_examples/icedelivery/iox-ex-subscriber-typed

Expected output

The counter can differ depending on startup of the applications.

RouDi application

2020-12-20 15:55:18.616 [ Info  ]: No config file provided and also not found at '/etc/iceoryx/roudi_config.toml'. Falling back to built-in config.
Log level set to: [Warning]
Reserving 64244064 bytes in the shared memory [/iceoryx_mgmt]
[ Reserving shared memory successful ]
Reserving 149134400 bytes in the shared memory [/user]
[ Reserving shared memory successful ]
RouDi is ready for clients

Publisher application

2020-12-20 16:05:01.837 [ Debug ]: Application registered management segment 0x7fd6d39e3000 with size 64244064 to id 1
2020-12-20 16:26:42.791 [ Info  ]: Application registered payload segment 0x7f377c4e6000 with size 149134400 to id 2
Sent {five,two} times value: 1
Sent {five,two} times value: 2
Sent {five,two} times value: 3

Subscriber application (typed)

2020-12-20 16:26:58.839 [ Debug ] Application registered management segment 0x7f6353c04000 with size 64244064 to id 1
2020-12-20 16:26:58.839 [ Info  ] Application registered payload segment 0x7f634ab8c000 with size 149134400 to id 2
Not subscribed!
Got value: 2
Got value: 2
Got value: 2
Got value: 2
Got value: 2

Got value: 3
Got value: 3
Got value: 3
Got value: 3
Got value: 3

Subscriber application (untyped)

2020-12-20 16:26:58.839 [ Debug ] Application registered management segment 0x7f6353c04000 with size 64244064 to id 1
2020-12-20 16:26:58.839 [ Info  ] Application registered payload segment 0x7f634ab8c000 with size 149134400 to id 2
Not subscribed!
Got value: 2
Got value: 2


Got value: 3
Got value: 3

Code walkthrough

This example makes use of two kind of API flavours. With the untyped API you have the most flexibility. It enables you to put higher level APIs with different look and feel on top of iceoryx. E.g. the ara::com API of AUTOSAR Adaptive or the ROS2 API. It is not meant to be used by developers in daily life, the assumption is that there will always be a higher abstraction. A simple example how such an abstraction could look like is given in the second step with the typed example.

Publisher application (untyped)

First off, let's include the publisher and the runtime:

#include "iceoryx_posh/popo/untyped_publisher.hpp"
#include "iceoryx_posh/runtime/posh_runtime.hpp"

You might be wondering what the publisher application is sending? It's this struct.

struct RadarObject
{
    RadarObject() noexcept
    {
    }
    RadarObject(double x, double y, double z) noexcept
        : x(x)
        , y(y)
        , z(z)
    {
    }
    double x = 0.0;
    double y = 0.0;
    double z = 0.0;
};

It is included by:

#include "topic_data.hpp"

For the communication with RouDi a runtime object is created. The parameter of the method initRuntime() contains a unique string identifier for this publisher.

iox::runtime::PoshRuntime::initRuntime("iox-ex-publisher-typed");

Now that RouDi knows our publisher application is existing, let's create a publisher instance and offer our charming struct to everyone:

iox::popo::UntypedPublisher untypedPublisher({"Radar", "FrontLeft", "Object"});
untypedPublisher.offer();

The strings inside the first parameter of the constructor of iox::popo::Publisher are of the type capro::ServiceDescription. capro stands for canionical protocol and is used to abstract different SoA protocols. Radar is the service name, FrontLeft an instance of the service Radar and the third string the specific event Object of the instance. In iceoryx a publisher and a subscriber only match if all the three IDs match.

Now comes the work mode. Data needs to be created. But hang on.. we need memory first! Let's reserve a memory chunk which fits our RadarObject struct

auto result = untypedPublisher.loan(sizeof(RadarObject));

Two different ways of handling the returned cxx::expected are possible. Either you save the result in a variable and do the error check with an if-condition (#1):

auto result = untypedPublisher.loan(sizeof(RadarObject));
if (!result.has_error())
{
    // ...
}
else
{
    // ...
}

Or try the functional way (#2) by concatenating and_then and or_else. Read it like a story in a book: "Loan memory and then if it succeeds, fill it with some data or else if it fails, handle the error"

untypedPublisher.loan(sizeof(RadarObject))
    .and_then([&](auto& sample)
    {
        // ...
    }
    .or_else([&](iox::popo::AllocationError error)
    {
        // ...
    });

If choosing #1, please mind the additional step to unwrap the cxx::expected with value()

if (!result.has_error())
{
    auto& sample = result.value();
    // ...
}

One might wonder what the type of the variable sample is? It is iox::popo::Sample<void>. This class behaves similar to a std::unique_ptr and makes sure that the ownership handling is done automatically and memory is freed when going out of scope on subscriber side. One slight difference is, if you want to take the ownership of the pointer, Sample::release() does not return the pointer.

Whichever way you choose, the untyped API will be bare-metal! A void* is contained inside the iox::popo::Sample. Hence, the pointer needs to be casted to RadarObject*

auto object = static_cast<RadarObject*>(sample.get());

Then we can write a new RadarObject value with an incremented counter in the shared memory

*object = RadarObject(ct, ct, ct);

Finanlly, in both ways, the value is made available to other subscribers with

sample.publish();

The incrementation and sending of the data is done in a loop every second till the user pressed Ctrl-C. It is captured with the signal handler and stops the loop.

Subscriber application (untyped)

How can the subscriber application receive the data the publisher application just transmitted?

Similar to the publisher we need to include the runtime and the subscriber as well as the topic data header:

#include "iceoryx_posh/popo/subscriber.hpp"
#include "iceoryx_posh/runtime/posh_runtime.hpp"
#include "topic_data.hpp"

To make RouDi aware of the subscriber an runtime object is created, once again with a unique identifier string:

iox::runtime::PoshRuntime::initRuntime("iox-ex-subscriber-untyped");

For quality of service a popo::SubscriberOptions object is created and the queueCapacity is set. This parameter specifies how many samples the queue of the subscriber object can hold. If the queue would encounter an overflow, the oldest sample is released to create space for the newest one, which is then stored.

iox::popo::SubscriberOptions subscriberOptions;
subscriberOptions.queueCapacity = 10U;

In the next step a subscriber object is created, matching exactly the capro::ServiceDescription that the publisher offered. Additionally, the previously created subscriber options are passed to the constructor. If no subscriber options are created, a default value will be used which sets the queueCapacity to the maximum value:

iox::popo::UntypedSubscriber untypedSubscriber({"Radar", "FrontLeft", "Object"}, subscriberOptions);

After the creation, the subscriber object subscribes to the offered data

untypedSubscriber.subscribe();

When using the default n:m communication philosophy, the SubscriptionState is immediately SUBSCRIBED. However, when restricting iceoryx to the 1:n communication philosophy before being in the state SUBSCRIBED, the state is change to SUBSCRIBE_REQUESTED.

Again in a while-loop we do the following: First check for the SubscriptionState

while (!killswitch)
{
    if (untypedSubscriber.getSubscriptionState() == iox::SubscribeState::SUBSCRIBED)
    {

The killswitch will be used to stop the programm execution.

Once the publisher has sent data, we can receive the data

untypedSubscriber.take()
    .and_then([](iox::cxx::optional<iox::popo::Sample<const void>>& sample)
    {
        // ...
    })
    .if_empty([]
    {
        // ...
    })
    .or_else([](iox::popo::ChunkReceiveError error)
    {
        std::cout << "Error receiving chunk." << std::endl;
    });

Well, that's a bit of a lambda jungle. Let's translate it into a story again: "Take the data and then if this succeeds, work with the sample, if the sample is empty do something different, or else if an error occured, print the string 'Error receiving chunk.'" Of course you don't need to take care about all cases, but it is advised to do so.

In the and_then case the content of the sample is printed to the command line:

auto object = static_cast<const RadarObject*>(sample->get());
std::cout << "Got value: " << object->x << std::endl;

Please note the static_cast before reading out the data. It is necessary, because the untyped subscriber is unaware of the type of the transmitted data.

If the untypedSubscriber was not yet subscribed

std::cout << "Not subscribed!" << std::endl;

is printed.

The subscriber runs 10x times faster than the publisher, to make sure that all data samples are received.

Publisher application (typed)

The typed publisher application is an example for a high-level user API and does the same thing as the publisher described before. In this summary, just the differences to the prior publisher application are described.

Starting again with the includes, there is now a different one:

#include "iceoryx_posh/popo/typed_publisher.hpp"

When it comes to the runtime, things are the same as in the untyped publisher. However, a typed publisher object is created

iox::popo::TypedPublisher<RadarObject> typedPublisher({"Radar", "FrontLeft", "Object"});

A similar while-loop is used to send the data to the subscriber. In contrast to the untyped publisher the typed one offers two additional possibilities

// #3
auto object = RadarObject(ct, ct, ct);
typedPublisher.publishCopyOf(object);

#3 should only be used for small data types, as otherwise copies can lead to a larger runtime.

// #4
typedPublisher.publishResultOf(getRadarObject, ct);
// OR
typedPublisher.publishResultOf([&ct](RadarObject* object) { new (object) RadarObject(ct, ct, ct); });

If you have a callable e.g. a function should be always called, #4 could be a good solution for you.

Another difference compared to the untyped publisher, is the easier handling of iox::popo::Sample. There is no need for any casts with the typed publisher, as the type of the stored data is know. One can directly access the data with the operator->().

Subscriber application (typed)

As with the typed publisher application there is an different include compared to the untyped subscriber:

#include "iceoryx_posh/popo/typed_subscriber.hpp"

An instance of TypedSubscriber is created:

iox::popo::TypedSubscriber<RadarObject> typedSubscriber({"Radar", "FrontLeft", "Object"}, subscriberOptions);

Everything else is nearly the same. However, there is one crucial difference which makes the TypedSubscriber typed.

Compare this line from the UntypedSubscriber

.and_then([](iox::popo::Sample<const RadarObject>& object)
{
    // ...
})

with

.and_then([](iox::popo::Sample<const void>& sample)
{
    // ...
})

The difference is the type that is contained in iox::popo::Sample. In case of the TypedSubscriber it is a const RadarObject instead of const void.