3. Publish/Subscribe

Prerequisites

Time to complete

45 minutes

Concepts covered in this module

  • Introduction to publish/subscribe

  • Introduction to DataWriters, and DataReaders, and Topics

  • Using the code generator

  • Using Waitsets

  • Viewing your data in RTI Admin Console

  • Working with the safety-critical SDK

The most basic communication pattern supported by RTI Connext Drive is the publish/subscribe model. Publish/Subscribe is a communications model where data producers “publish” data and data consumers “subscribe” to data. These publishers and subscribers don’t need to know about each other ahead of time; they discover each other dynamically at runtime. The data they share is described by a “topic,” and publishers and subscribers send and receive data only for the topics they are interested in. In this pattern, many publishers may publish the same topic, and many subscribers may subscribe to the same topic. Subscribers receive data from all of the publishers that they share a topic with. Publishers send data directly to subscribers, with no need for a broker or centralized application to mediate communications.

3.1. Introduction to DataWriters, DataReaders, and Topics

In DDS, the objects that actually publish data are called DataWriters, and the objects that subscribe to data are DataReaders. DataWriters and DataReaders are associated with a single Topic object that describes that data. (DDS also has Publisher and Subscriber objects, but we will talk about them later.) An application typically has a combination of DataWriters and DataReaders.

Writers Readers Overview

Figure 3.1 DataWriters write data and DataReaders read data of a Topic. DataWriters of the “ChocolateTemperature” Topic communicate with DataReaders of the “ChocolateTemperature” Topic. DataWriters of the “ChocolateLotState” Topic communicate with DataReaders of the “ChocolateLotState” Topic.

In a chocolate factory, for example, there might be a sensor that measures and publishes the current temperature of the tempering machine. Other applications monitor the temperature by subscribing to it. In this example, your Topic might be “ChocolateTemperature.” The sensor’s DataWriter will be associated with the “ChocolateTemperature” Topic. In a similar way, other DataWriters and DataReaders share different types of data using additional Topics.

Connext is responsible for discovering DataWriters and DataReaders in a system, checking if they have a matching Topic (and compatible quality of service, which we will discuss later) and then providing the communication between those DataWriters and DataReaders. Logically, this means you can visualize your applications as having DataWriters and DataReaders that connect to a “databus,” because your applications are not specifying exactly which other applications they communicate with – they only specify which Topics they read from and write to the databus, and Connext sets up the communication. Note that there is no “databus” object in your system – it is a logical way to visualize systems in which you don’t have to configure each communication path.

Note

We recommend completing the following examples in the order presented, starting with Hands-On 1: Your First DataWriter and DataReader, then Hands-On 2: Viewing Your Data, and finally Hands-On 3: Getting Started with the Safety-Critical SDK.

Alternatively, you can complete Hands-On 1: Your First DataWriter and DataReader and move directly to Hands-On 3: Getting Started with the Safety-Critical SDK if you aren’t interested in the data visualization tools covered in Hands-On 2: Viewing Your Data.

3.2. Hands-On 1: Your First DataWriter and DataReader

We are going to start with a simple “Hello World” application to show how to use the code generator, and how to create a DataWriter and a DataReader.

Tip

By the end of this exercise, a publishing application will send data, and a subscribing application will receive and print it to the console using std::cout.

3.2.1. Get Example Files

The files you need to perform the hands-on exercises are installed in your rti_workspace directory, in an examples directory. You can also get the example files from GitHub. We recommend you use the getting_started examples in your rti_workspace directory, but check for updates in GitHub later.

Note

New languages and modules for this Getting Started Guide may be added to GitHub in between Connext releases.

3.2.1.1. From your Installation

To get the installed getting_started examples, look in the following location:

/home/<your user name>/rti_workspace/6.1.2/examples/getting_started

/Users/<your user name>/rti_workspace/6.1.2/examples/getting_started

<your Windows documents folder>\rti_workspace\6.1.2\examples\getting_started

Where <your Windows documents folder> depends on your version of Windows. For example, on Windows 10, the folder is C:\Users\<your user name>\Documents.

3.2.1.2. From GitHub

To get the getting_started examples from the GitHub repository, do one of the following:

Note

Always use the version of the examples that matches your Connext release. By default, the GitHub repository branch is set to master, which is always the latest release. If your version of Connext is not the latest release, select the release version you want, such as release/6.1.1.

  • Download the repository as a Zip file from the webpage here, for your desired release (e.g., release/6.1.2).

  • Clone the repository from GitHub.

    Use the following command:

    $ git clone https://github.com/rticommunity/rticonnextdds-getting-started.git
    
    $ git clone https://github.com/rticommunity/rticonnextdds-getting-started.git
    
    > git clone https://github.com/rticommunity/rticonnextdds-getting-started.git
    

    If you’re not using the latest Connext release, check out the desired release. For example:

    $ git checkout release/6.1.1
    
    $ git checkout release/6.1.1
    
    > git checkout release/6.1.1
    

3.2.2. Set Up Environment Variables (rtisetenv)

Set up the environment variables for running the code generator and compiling your application:

  1. Open a command prompt window, if you haven’t already.

  2. Run this script:

    $ source <installdir>/resource/scripts/rtisetenv_<architecture>.bash
    

    If you’re using the Z shell, run this:

    $ source <installdir>/resource/scripts/rtisetenv_<architecture>.zsh
    
    $ source <installdir>/resource/scripts/rtisetenv_<architecture>.bash
    

    If you’re using the Z shell, run this:

    $ source <installdir>/resource/scripts/rtisetenv_<architecture>.zsh
    
    > <installdir>\resource\scripts\rtisetenv_<architecture>.bat
    

    When a directory name has a space, enclose the path in quotation marks. For example: "C:\Program Files\rti_connext_dds-6.1.2\resource\scripts\rtisetenv_x64Win64VS2017.bat".

    <installdir> refers to the installation directory for Connext.

    The rtisetenv script adds the location of the SDK libraries (<installdir>/lib/<architecture>) to your library path, sets the <NDDSHOME> environment variable (see Paths Mentioned in the Documentation), and puts the RTI Code Generator tool in your path. You’ll need this tool in the next step.

    Your architecture (such as x64Win64VS2017 or x64Linux3gcc5.4.0) is the combination of a processor, OS, and compiler version that you will use to build your application. The architecture you choose should match the target libraries you installed (see Installing Connext Drive). See also Checking What’s Installed. To see the full list of available architectures, run rtiddsgen -help or see the “RTI Architecture Abbreviation” columns in the RTI Connext DDS Core Libraries Platform Notes.

    If this script does not exist in your installation, it means that you need to install a target. See Installing Connext Drive.

3.2.3. Run Code Generator

Inside the repository you have cloned, there is a directory named 2_hello_world, which contains the HelloWorld type definition in a file named hello_world.idl.

  1. Open hello_world.idl to see the definition for our HelloWorld type:

    // Hello world!
    struct HelloWorld {
    
        // String with maximum length of 256 characters
        string<256> msg;
    
    };
    

    This language-independent interface is written in IDL, the Interface Definition Language. IDL allows you to declare data types used for communication (we’ll cover this more in Data Types). Connext includes a code generator that translates from this language-independent data type into code specific for your language. The generated code serializes and deserializes your data into and out of a network format.

  2. From a terminal or command prompt, run rtiddsgen, which runs the code generator on hello_world.idl:

    $ cd 2_hello_world
    $ rtiddsgen -language c++11 -platform <architecture> -create makefiles -create typefiles -d c++11 hello_world.idl
    
    $ cd 2_hello_world
    $ rtiddsgen -language c++11 -platform <architecture> -create makefiles -create typefiles -d c++11 hello_world.idl
    
    > cd 2_hello_world
    > rtiddsgen -language c++11 -platform <architecture> -create makefiles -create typefiles -d c++11 -ppDisable hello_world.idl
    

    -ppDisable disables the preprocessor. It is necessary for running rtiddsgen on a Windows system if the preprocessor is not in your path. You can only use -ppDisable if your IDL is simple, as it is here—otherwise you must add the preprocessor to your path. See Command-Line Arguments for rtiddsgen, in the RTI Connext DDS Code Generator User’s Manual if you want more information.

    You don’t have to type the full path (<install dir>/bin/rtiddsgen) because you ran rtisetenv_<architecture> earlier, as described in Set Up Environment Variables (rtisetenv).

    Your architecture is the combination of a processor, OS, and compiler version that you will use to build your application. (See Set Up Environment Variables (rtisetenv).)

    -d c++11 specifies the directory where the code will be generated.

    Note

    If you are using Visual Studio 2019, use “VS2017” in your architecture name.

  3. Open the c++11 directory to review the code.

3.2.3.1. Overview of Generated and Example Code

The code you just generated includes the files in Table 3.1, in the c++11 directory. Some of the files are generated by rtiddsgen, some came from the examples directory as described in Get Example Files.

Table 3.1 Generated and Example Code Files

Files

Description

Generated by rtiddsgen?

  • hello_world.hpp

  • hello_world.cxx

  • hello_worldPlugin.hpp

  • hello_worldPlugin.cxx

The C++11 definition of your data type, and the code used to serialize and deserialize it (convert it to a format for the network). This is the type-specific code that will be used in your real application. These files were generated when you specified -create typefiles when running rtiddsgen.

Yes

  • application.hpp

  • hello_world_publisher.cxx

  • hello_world_subscriber.cxx

Example code from the repository you cloned. It includes code for two applications you can read and modify. These will compile into separate applications, hello_world_publisher and hello_world_subscriber.

No 1

  • makefiles, Visual Studio solutions, or project files

Used to build your application. These files were generated because you specified -platform <architecture> -create makefiles when running rtiddsgen. For more information on how to build, see the README_<architecture>.txt file generated with the code. 2

Yes

  • USER_QOS_PROFILES.xml

Configuration file for Quality of Service (to be discussed more later). This file came from the repository you cloned.

No

  • README_<architecture>.txt

Instructions for how to open and modify the files, compile, and run the example for your specific IDE and operating system. This file was generated when you ran rtiddsgen.

Yes

1

rtiddsgen can also generate simple example code, but we are using our own code examples for this Getting Started Guide.

2

These files are generated by rtiddsgen. If you want to create your own project files or makefiles, see the RTI Connext DDS Core Libraries Platform Notes to know what compilation and linker settings to use in them.

3.2.4. Open/Modify Publishing Application

  1. In the 2_hello_world/c++11 directory, open hello_world_publisher.cxx in your IDE of choice. (See the README_<architecture>.txt file generated with the code for information on how to modify this file in your IDE and operating system.)

    This snippet shows how to create a Topic (with a name and data type) and create a DataWriter for that Topic:

    // A Topic has a name and a datatype. Create a Topic named
    // "HelloWorld Topic" with type HelloWorld
    dds::topic::Topic<HelloWorld> topic(participant, "Example HelloWorld");
    
    // A Publisher allows an application to create one or more DataWriters
    // Publisher QoS is configured in USER_QOS_PROFILES.xml
    dds::pub::Publisher publisher(participant);
    
    // This DataWriter will write data on Topic "HelloWorld Topic"
    // DataWriter QoS is configured in USER_QOS_PROFILES.xml
    dds::pub::DataWriter<HelloWorld> writer(publisher, topic);
    

    Tip

    In this example, Topic, Publisher, and DataWriter look like ordinary objects being created on the stack. However, these types are actually reference types that behave like shared pointers. Copying a reference does not copy the entity it is referring to, and creating additional references from the existing reference(s) is a relatively inexpensive operation.

    You can find more information about reference type semantics in the Modern C++ API reference, along with documentation conventions for distinguishing between reference types and value types.

  2. Change the Topic name from “Example HelloWorld” to “HelloWorld Topic”:

    // A Topic has a name and a datatype. Create a Topic named
    // "HelloWorld Topic" with type HelloWorld
    dds::topic::Topic<HelloWorld> topic(participant, "HelloWorld Topic");
    
  3. Modify the code to send the message “Hello world!” with a count.

    The following snippet shows how to write a HelloWorld update using the DataWriter’s write method.

    In the for loop in the snippet, add the highlighted line, just after the comment // Modify the data to be written here. This will set sample.msg to “Hello world!” with a count:

    // Create data sample for writing
    HelloWorld sample;
    for (int count = 0; running && (count < sample_count || sample_count == 0);
         count++) {
        // Modify the data to be written here
        sample.msg("Hello world! " + std::to_string(count));
    
        std::cout << "Writing HelloWorld, count " << count << std::endl;
    
        writer.write(sample);
    
        rti::util::sleep(dds::core::Duration(4));
    }
    

    Recall that your “HelloWorld Topic” describes your data. This Topic is associated with the data type HelloWorld, which is defined in the IDL file (see Run Code Generator). The data type HelloWorld contains a string field named msg. In this step, you have just added code to set a value for the msg field. Now, when the DataWriter writes data, the msg field in the data will contain the string “Hello world! 1”, “Hello world! 2”, etc.

Definition

A sample is a single update to a Topic, such as “Hello world! 1”. Every time an application calls write(), it is “writing a sample.” Every time an application receives data, it is “receiving a sample.”

Note that samples don’t necessarily overwrite each other. For example, if you set up a RELIABLE Quality of Service (QoS) with a History kind of KEEP_ALL, all samples will be saved and accumulate. You can find more details in Basic QoS.

3.2.5. Open/Modify Subscribing Application

The subscriber application also creates a Topic, in hello_world_subscriber.cxx.

  1. Open hello_world_subscriber.cxx.

    This snippet shows how to create a Topic (with a name and data type) and create a DataReader for that Topic:

    // A Topic has a name and a datatype. Create a Topic named
    // "HelloWorld Topic" with type HelloWorld
    dds::topic::Topic<HelloWorld> topic(participant, "Example HelloWorld");
    
    // A Subscriber allows an application to create one or more DataReaders
    // Subscriber QoS is configured in USER_QOS_PROFILES.xml
    dds::sub::Subscriber subscriber(participant);
    
    // This DataReader will read data of type HelloWorld on Topic
    // "HelloWorld Topic". DataReader QoS is configured in
    // USER_QOS_PROFILES.xml
    dds::sub::DataReader<HelloWorld> reader(subscriber, topic);
    
  2. Change the Topic name from “Example HelloWorld” to “HelloWorld Topic”, just as you did in the publishing application.

    Note

    The Topic names must match between the publishing and subscribing applications for the DataWriter and DataReader to communicate.

    // A Topic has a name and a datatype. Create a Topic named
    // "HelloWorld Topic" with type HelloWorld
    dds::topic::Topic<HelloWorld> topic(participant, "HelloWorld Topic");
    

3.2.5.1. Details of Receiving Data

You don’t need to make any changes here, but look at the hello_world_subscriber.cxx code to see how it receives data. The DataReader is being notified of new data using an object called a WaitSet:

// Create a WaitSet and attach the StatusCondition
dds::core::cond::WaitSet waitset;
waitset += status_condition;

while (running && (samples_read < sample_count || sample_count == 0)) {
    // Dispatch will call the handlers associated to the WaitSet conditions
    // when they activate
    std::cout << "HelloWorld subscriber sleeping for 4 sec..."
              << std::endl;

    waitset.dispatch(dds::core::Duration(4));  // Wait up to 4s each time
}

This WaitSet object is a way for the application to sleep until some “condition” becomes true. When the application calls waitset.dispatch(dds::core::Duration(4)), it will sleep for up to the duration time (4 seconds in this example), unless it is woken up due to a condition becoming true.

There are multiple types of conditions that you can attach to a WaitSet, but this example shows a StatusCondition. Here is the code for the StatusCondition:

// Obtain the DataReader's Status Condition
dds::core::cond::StatusCondition status_condition(reader);

// Enable the 'data available' status.
status_condition.enabled_statuses(
        dds::core::status::StatusMask::data_available());

// Associate a handler with the status condition. This will run when the
// condition is triggered.
unsigned int samples_read = 0;
status_condition.extensions().handler([&reader, &samples_read]() {
    samples_read += process_data(reader);
});

In this example, we are saying that we are interested in waking up when the “data available” status becomes true. This means that when data arrives:

  • The reader’s data_available status becomes true

  • The application is woken up from the dispatch call

  • The lambda function is called

A closer look at process_data, called by the lambda function:

unsigned int process_data(dds::sub::DataReader<HelloWorld>& reader)
{
    // Take all samples.  Samples are loaned to application, loan is
    // returned when LoanedSamples destructor called.
    unsigned int samples_read = 0;
    dds::sub::LoanedSamples<HelloWorld> samples = reader.take();
    for (const auto& sample : samples) {
        if (sample.info().valid()) {
            samples_read++;
            std::cout << sample.data() << std::endl;
        }
    }

    return samples_read;
}  // The LoanedSamples destructor returns the loan

This code calls reader.take(), which removes any available samples out of the DataReader queue, and returns them in a collection. If data is arriving quickly, it is likely this collection will contain multiple samples. In this example, the sample’s data is being printed to the screen with std::cout.

3.2.6. Compile Your Changes

Now that you have made changes to both the publisher and subscriber code, compile the code with your modifications. (For general instructions on how to compile on each operating system, see the README_<architecture>.txt file included with your generated code.)

Note

If you see linker errors, you may not have a complete installation. See Installing Connext Drive.

You should see two applications in the objs/<architecture> directory:

  • hello_world_publisher

  • hello_world_subscriber

3.2.7. Run the Applications

  1. Make sure you have run rtisetenv_<architecture> in any new command prompt window, to avoid issues with paths and licensing. See Set Up Environment Variables (rtisetenv).

  2. From within the 2_hello_world/c++11 directory, enter the following full path:

    $ objs/<architecture>/hello_world_publisher
    
    $ objs/<architecture>/hello_world_publisher
    
    > objs\<architecture>\hello_world_publisher.exe
    

    Note

    You must be in the 2_hello_world/<language> directory and type the full path above. Do not run the publisher or subscriber application from within objs/<architecture>. You should run from the 2_hello_world/<language> directory because the examples use Quality of Service (QoS) information from the file USER_QOS_PROFILES.xml in the 2_hello_world/<language> directory. We’ll talk more about QoS in a later module.

    You should see this in the window for the publisher:

    Writing HelloWorld, count 0
    Writing HelloWorld, count 1
    Writing HelloWorld, count 2
    Writing HelloWorld, count 3
    ...
    
  3. Open another command prompt window, run rtisetenv_<architecture> if you haven’t already in that window, and from within the 2_hello_world/c++11 directory, enter the following full path:

    $ objs/<architecture>/hello_world_subscriber
    
    $ objs/<architecture>/hello_world_subscriber
    
    > objs\<architecture>\hello_world_subscriber.exe
    

    You should see this in the window for the subscriber:

    HelloWorld subscriber sleeping for 4 sec...
    [msg: Hello world! 1]
    HelloWorld subscriber sleeping for 4 sec...
    [msg: Hello world! 2]
    HelloWorld subscriber sleeping for 4 sec...
    [msg: Hello world! 3]
    ...
    

    Note

    Since both the publisher and subscriber sleep for up to 4 seconds, you may get the HelloWorld subscriber sleeping for 4 sec... line twice in a row, depending on the timing between the two applications.

    The [msg: Hello world! <count>] line is the data being sent by the DataWriter. If the DataWriter weren’t communicating with the DataReader, you would just see the HelloWorld subscriber sleeping for 4 sec lines and not the “msg” lines. (The subscribing application prints the “sleeping” lines before the WaitSet starts waiting for data, then it prints the “msg:” lines when it receives data from the DataWriter.)

    Note

    If you don’t get the results described here, see Troubleshooting.

3.2.8. Taking It Further

Under the hood, the publishing and subscribing applications are doing a lot of work:

Before communication starts, the DataWriter and DataReader discover each other and check that they have the same Topic name, compatible data types, and compatible QoS. (We will talk more about discovery in a later module). After discovery, the DataWriter sends data directly to the DataReader, with no message broker required.

When you run the applications on the same machine, by default they communicate over shared memory. If you run one on another machine, they communicate over the network using UDP.

3.2.8.1. Start up Multiple Publishing or Subscribing Applications

Try starting up multiple publishing or subscribing applications, and you will see that they will also send or receive data. (Remember to run from the 2_hello_world/<language> directory.)

Hello World Publisher Subscriber

Figure 3.2 Two applications publishing, and two subscribing, to the same Topic. Notice that each subscriber is receiving data from both publishers.

Two Applications Publishing and Subscribing

Figure 3.4 Two applications publishing, and two subscribing, to the same Topic.

3.2.8.2. Publish/Subscribe across Multiple Machines

To publish/subscribe between two machines:

  1. Install or clone the getting_started examples on both machines. See Get Example Files.

  2. In the 2_hello_world directory on one machine, modify and run the publishing application as described in Open/Modify Publishing Application. See also Compile Your Changes and Run the Applications.

  3. In the 2_hello_world directory on the other machine, modify and run the subscribing application as described in Open/Modify Subscribing Application. See also Compile Your Changes and Run the Applications.

    Note

    If you are running both applications and they aren’t communicating (you don’t see the “msg:” lines shown at the end of Run the Applications), see Troubleshooting.

3.2.8.3. Create Multiple DataWriters, DataReaders, and Topics in a Single Application

So far, you have created two applications: one that uses a DataReader to subscribe to “HelloWorld Topic” and one that uses a DataWriter to publish “HelloWorld Topic.” You have seen that these applications automatically discover each other and communicate, and that you can run multiple copies of them.

In Data Types, you’ll add a second DataWriter to an application, and in Keys and Instances, you’ll create an application that contains multiple DataWriters, DataReaders, and Topics.

Tip

Even though these initial example applications each do only one thing, typical real-world applications contain a combination of DataWriters and DataReaders for multiple Topics in a single application. We will illustrate this in later modules.

Oven Application, Multiple Topics

Figure 3.5 Sneak Preview: In a later module, we will create a tempering application that will write a “ChocolateTemperature” Topic and read a “ChocolateLotState” Topic. An application can write and read multiple Topics by creating more DataWriters and DataReaders.

3.3. Troubleshooting

3.3.1. Why aren’t my applications communicating?

If you are running both applications and they aren’t communicating (you don’t see the msg: Hello world lines shown at the end of Run the Applications), here are some things to check:

  • Did you change the Topic name in both applications before compiling? They should match.

  • Are you running both applications from the 2_hello_world/<language> directory, so they load the same USER_QOS_PROFILES.xml file?

Note

You must be in the 2_hello_world/<language> directory when you type objs/<architecture>/hello_world_<publisher or subscriber>[.exe]. Do not run the application from within objs/<architecture>. (When rtiddsgen generates a project for your IDE (e.g., for Eclipse™ or Visual Studio) and you run the application from that IDE, it will run from the correct directory.)

  • If you are running on multiple machines, does your network support multicast? If it does not, see How do I set my discovery peers? below, for how to specify the addresses of the remote machines your application plans to communicate with.

  • Check to see if one or both machines use a firewall. You may need to disable your firewall or configure it to allow multicast traffic for communication to be established.

See also Discovery.

3.3.2. How do I set my discovery peers?

If you are running Connext on multiple machines and your network does not support multicast, specify the address(es) of the remote machine(s) you want to communicate with:

  1. In the 2_hello_world\<language> directory, find USER_QOS_PROFILES.xml.

  2. Identify the following lines in USER_QOS_PROFILES.xml:

    <domain_participant_qos>
        <!--
        The participant name, if it is set, will be displayed in the
        RTI tools, making it easier for you to tell one
        application from another when you're debugging.
        -->
        <participant_name>
            <name>HelloWorldParticipant</name>
        </participant_name>
    
    </domain_participant_qos>
    
  3. Add the <discovery> section as follows, and replace the IP address with the IP address or hostname of the other machine you want to communicate with:

    <domain_participant_qos>
        <!--
        The participant name, if it is set, will be displayed in the
        RTI tools, making it easier for you to tell one
        application from another when you're debugging.
        -->
        <participant_name>
            <name>HelloWorldParticipant</name>
        </participant_name>
        <discovery>
            <initial_peers>
                <!-- Add an element for each machine you want to communicate with -->
                <element>192.168.1.14</element>
            </initial_peers>
        </discovery>
    
    </domain_participant_qos>
    

If you want more information about discovery peers, see Configuring the Peers List Used in Discovery, in the RTI Connext DDS Core Libraries User’s Manual.

3.3.3. Why does the DataReader miss the first samples?

Discovery is not an instantaneous event. It takes some time for the discovery process between applications to complete. The DataWriter and DataReader must discover each other before they can start communicating. Therefore, if you send data immediately after creating the Connext entities, DataReaders will not receive the first few samples because they are still in-process of discovering the DataWriters and vice versa. This is true even when the DataWriters and DataReaders are reliable, because the Reliability QoS on its own does not guarantee delivery to DataReaders that have not been discovered yet.

You can overcome this behavior with the Durability QoS Policy, which can be set to deliver historical samples (that DataWriters already sent) to late-joining DataReaders. The DataReaders will then receive the first samples they originally missed. We’ll talk about this more in Basic QoS.

3.3.4. Why do I get a “No source for License information” error?

If you get the following error, run rtisetenv_<architecture> in your command prompt window:

RTI Connext DDS No source for License information
Please contact support@rti.com with any questions or comments.
Exception in subscriber_main(): Failed to create DomainParticipant

(You may need to run rtisetenv_<architecture> for each new command prompt window you open, especially if you installed an lm package.) See Set Up Environment Variables (rtisetenv). See also Setting Up a License.

3.4. Hands-On 2: Viewing Your Data

Now that you’ve created applications that are publishing data, you can visualize that data using the Admin Console tool.

Note

The applications from Hands-On 1 above should be running while you perform the Admin Console steps below. See Run the Applications.

3.4.1. Open Admin Console

Start by opening Admin Console from RTI Launcher:

Open Admin Console in Launcher

The following sections guide you through Admin Console for the purposes of this exercise, but you can find more information in Admin Console’s online Help:

Admin Console Help

3.4.2. Choose Automatically Join

You may be prompted to automatically or manually join domains when you first open Admin Console. For the purposes of this exercise, choose to automatically join (the default). We’ll discuss domains in a later module.

3.4.3. Switch to Data Visualization Perspective

Select the Data Visualization Perspective entry under the Visualization menu. (You may need to close a Welcome tab first.) If you can’t select the Data Visualization Perspective menu item, you may already be in that mode.

Open Data Visualization

3.4.4. Open Topic Data Tab

From the DDS Logical View, click on the “HelloWorld Topic” to open the Topic View.

Open Topic Data Tab

In the Data Visualization Perspective, the main window will show the Topic Data tab, as seen below:

Topic Data Tab

3.4.5. Subscribe to “HelloWorld Topic”

The Admin Console tool itself is a DDS application, so it must also create DataReaders to subscribe to the “HelloWorld Topic.” (See the Admin Console online Help for more information about this.) When you subscribe to the “HelloWorld Topic” in Admin Console, Admin Console will create a DataReader for this Topic.

Click on the Subscribe… button or right-click on the Topic in the DDS Logical View and select Subscribe. This will open the Create Subscription dialog seen below. Click OK to subscribe to the Topic.

Create Subscription Dialog

3.4.6. Use Topic Data Tab

After the Topic is subscribed to, you will see your “Hello world” message.

View Hello World Message

In the Admin Console tool, you’re also able to inspect DataWriters and DataReaders, and even chart your live data.

3.4.7. Use Admin Console across Machines

Admin Console is like any other Connext application. It discovers what DataWriters and DataReaders are on the network, whether they’re on the same machine as Admin Console or on different machines.

If you have trouble viewing data in Admin Console across machines, the same troubleshooting tips apply to Admin Console as apply to DataWriters and DataReaders in How do I set my discovery peers?. If your network supports multicast, you’ll see data on the other machines in Admin Console; otherwise, specify the IP addresses of the remote machines you want Admin Console to communicate with.

Setting Peers in Admin Console

3.5. Hands-On 3: Getting Started with the Safety-Critical SDK

Now that you’ve created some basic applications with Connext Professional, you can create applications with Connext Micro, using libraries that are safety-certified to automotive standards.

Note

We recommend that you complete Hands-On 1: Your First DataWriter and DataReader before working on this example. The instructions for Hands-On 1 cover some basic concepts that are not explained in detail in this example.

3.5.1. Build the libraries

If you installed a Connext Drive LM (licensed-managed) bundle, the libraries do not need to be built. As explained in Safety-certified libraries, an LM bundle (usually used for evaluation purposes) provides prebuilt libraries for two target architectures:

  • armv8Linux4gcc7.3.0_certprofile

  • x64Linux4gcc7.3.0_certprofile

These are prebuilt Connext Micro libraries that only use features available in Connext Cert. We refer to these as cert-profile libraries, and will use them in this hands-on section.

If you installed a non-license-managed Connext Drive bundle, you will need to compile your own cert-profile libraries. To do so, follow the instructions in the Building and Porting chapter of the Connext Micro 2.4.14 User’s Manual.

Note

The User’s Manual for Connext Micro 2.4.14 refers to Connext Micro as Connext DDS Micro, and to Connext Cert as Connext DDS Micro Cert. These are simply legacy names and do not denote different products.

3.5.2. Define a Data Type

To distribute data using Connext Drive cert-profile libraries, you must first define a data type as described in this section, then run the RTI Code Generator (rtiddsgen) utility as described in Run Code Generator below. This utility will generate the type-specific support code that the cert-profile libraries need and the code that makes calls to publish and subscribe to that data type.

rtiddsgen accepts type-definitions in Interface Definition Language (IDL) format, and generates type-support code using those type-definitions.

For instance, to define a simple type called HelloWorld which contains a string “msg” with a maximum length of 128 characters, create a file called “HelloWorld.idl” with the following content:

struct HelloWorld
{
    string<256> msg;
};

3.5.3. Run Code Generator

To generate an example for Connext Micro, run the following command:

rtiddsgen -micro -ppDisable -example -exampleTemplate cert -language C <file with type-definition>

In the above command, we passed rtiddsgen the following options:

-micro

Necessary to generate type-support code that is specific to the cert-profile libraries.

-ppDisable

Instructs rtiddsgen to not attempt to invoke the C preprocessor on the IDL file prior to generating code.

-example

Generates type files and example files.

-exampleTemplate cert

Generates example files from the cert template, only using APIs compatible with Connext Cert.

-language C

Generates C code.

You can create your own type-definition file, modeled after the one described in Define a data type. The type-definition file name must end in “.idl”.

Note

Code Generator for Connext Micro 2.4.14 is not compatible out-of-the-box with the GCC 11 preprocessor (which is the default in some operating systems, like Ubuntu® 22.04). If your IDL file does not contain a preprocessor directive, we recommend using -ppDisable as in the example above.

Otherwise, you can include the following options in your rtiddsgen call:

-ppOption -P

3.5.3.1. How to compile the generated examples

Note

This section assumes that a CMake bundle is available. Make sure that CMake is installed and available before continuing.

The Connext Drive bundle includes rtime-make. This script is a convenient way to build the examples with the correct options. Note that before calling rtime-make, make sure that <RTIMEHOME>/resource/scripts is in your path environment variable.

For example:

cd "<directory containing your generated example>"

rtime-make --config Release --build --target <cert_arch_name> --source-dir . \
    -G "Unix Makefiles" --delete [-DRTIME_IDL_ADD_REGENERATE_TYPESUPPORT_RULE=true]

For <cert_arch_name>: use either of the two architectures included in the LM bundle, or another architecture that you have built yourself (see Build the libraries):

  • armv8Linux4gcc7.3.0_certprofile

  • x64Linux4gcc7.3.0_certprofile

Note

Since --name is not specified, the value for --target will be used as the name. Since <cert_arch_name> has the suffix “_certprofile” in the name, the rtime-make build system will automatically set the CMake flag RTIME_CERT when compiling. The CMake flag RTIME_CERT instructs the build system to build Connext Micro with Connext Cert compatibility.

The executable can be found in the directory ‘objs’.

3.5.3.2. How to run the generated examples

By default, the example uses all available interfaces to receive samples. This can cause communication problems if the number of available interfaces is greater than the maximum number of interfaces supported by the cert-profile libraries. For this reason, we recommend that you restrict the number of interfaces used by the application; use the option -udp_interface <interface name> when running the example to restrict it to using only the specified network interface.

For example, if the example has been compiled for x64Linux4gcc7.3.0_certprofile, run the subscriber with this command:

objs/x64Linux4gcc7.3.0_certprofile/<type-definition file name>_subscriber [-domain <Domain_ID>] [-peer <address>] \
    [-sleep <sleep_time_in_msec>] [-count <seconds_to_run>] [-udp_intf <interface name>]

and run the publisher with this command:

objs/x64Linux4gcc7.3.0_certprofile/<type-definition file name>_publisher [-domain <Domain_ID> -peer <address>] \
    [-sleep <sleep_time_in_msec>] [-count <seconds_to_run>] [-udp_intf <interface name>]

3.5.4. Configure UDP Transport

You may need to configure the UDP transport component that is pre-registered by the cert-profile libraries. To change the properties of the UDP transport: unregister the UDP component, then update the properties and reregister it.

Example code:

  • Unregister the pre-registered UDP component:

    /* Unregister the pre-registered UDP component */
    if (!RT_Registry_unregister(registry, "_udp", NULL, NULL))
    {
        /* failure */
    }
    
  • Configure UDP transport properties:

    struct UDP_InterfaceFactoryProperty *udp_property = NULL;
    
    udp_property = (struct UDP_InterfaceFactoryProperty *)
       malloc(sizeof(struct UDP_InterfaceFactoryProperty));
    if (udp_property != NULL)
    {
       *udp_property = UDP_INTERFACE_FACTORY_PROPERTY_DEFAULT;
       /* allow_interface: Names of network interfaces allowed to send/receive.
       * Allow one loopback (lo) and one NIC (eth0).
       */
       REDA_StringSeq_set_maximum(&udp_property->allow_interface,2);
       REDA_StringSeq_set_length(&udp_property->allow_interface,2);
    
       *REDA_StringSeq_get_reference(&udp_property->allow_interface,0) = DDS_String_dup("lo");
       *REDA_StringSeq_get_reference(&udp_property->allow_interface,1) = DDS_String_dup("eth0");
    }
    else
    {
       /* failure */
    }
    
  • Re-register UDP component with updated properties:

    if (!RT_Registry_register(registry, "_udp",
                              UDP_InterfaceFactory_get_interface(),
                              (struct RT_ComponentFactoryProperty*)udp_property, NULL))
    {
       /* failure */
    }
    

3.5.5. Create a DomainParticipant and Topic with a registered type

A DomainParticipantFactory creates DomainParticipants, and a DomainParticipant itself is the factory for creating Publishers, Subscribers, and Topics.

When creating a DomainParticipant, you may need to customize DomainParticipantQos, notably for:

  • Resource limits. Default resource limits are set at minimum values.

  • Initial peers. A list of known remote entities that your application should connect to automatically. Entities that are not in this list will still be found using the Discovery process.

  • Discovery. The name of the registered discovery component (typically “dpde” or “dpse”) must be assigned to DiscoveryQosPolicy’s name. Please note that in Connext Cert, only the DPSE discovery plugin is supported.

  • Participant Name. Every DomainParticipant is given the same default name. Must be unique when using DPSE discovery.

Example code:

  • Create a DomainParticipant with configured DomainParticipantQos:

    DDS_DomainParticipant *participant = NULL;
    struct DDS_DomainParticipantQos dp_qos =
        DDS_DomainParticipantQos_INITIALIZER;
    
    /* DDS domain of DomainParticipant */
    DDS_Long domain_id = 0;
    
    /* Name of your registered Discovery component */
    if (!RT_ComponentFactoryId_set_name(&dp_qos.discovery.discovery.name, "dpde"))
    {
        /* failure */
    }
    
    /* Initial peers: use only default multicast peer */
    DDS_StringSeq_set_maximum(&dp_qos.discovery.initial_peers,1);
    DDS_StringSeq_set_length(&dp_qos.discovery.initial_peers,1);
    *DDS_StringSeq_get_reference(&dp_qos.discovery.initial_peers,0) =
        DDS_String_dup("239.255.0.1");
    
    /* Resource limits */
    dp_qos.resource_limits.max_destination_ports = 32;
    dp_qos.resource_limits.max_receive_ports = 32;
    dp_qos.resource_limits.local_topic_allocation = 1;
    dp_qos.resource_limits.local_type_allocation = 1;
    dp_qos.resource_limits.local_reader_allocation = 1;
    dp_qos.resource_limits.local_writer_allocation = 1;
    dp_qos.resource_limits.remote_participant_allocation = 8;
    dp_qos.resource_limits.remote_reader_allocation = 8;
    dp_qos.resource_limits.remote_writer_allocation = 8;
    
    /* Participant name */
    strcpy(dp_qos.participant_name.name, "Participant_1");
    
    participant =
        DDS_DomainParticipantFactory_create_participant(factory,
                                                        domain_id,
                                                        &dp_qos,
                                                        NULL,
                                                        DDS_STATUS_MASK_NONE);
    
    if (participant == NULL)
    {
        /* failure */
    }
    

3.5.5.1. Register type

Your data types that have been generated from IDL need to be registered with the DomainParticipants that will be using them. Each registered type must have a unique name, preferably the same as its IDL defined name.

DDS_ReturnCode_t retcode;

retcode = DDS_DomainParticipant_register_type(participant,
                                              "HelloWorld",
                                              HelloWorldTypePlugin_get());

if (retcode != DDS_RETCODE_OK)
{
    /* failure */
}

3.5.5.2. Create Topic of registered type

DDS Topics encapsulate the types being communicated, and you can create Topics for your type once your type is registered.

A topic is given a name at creation (e.g. “Example HelloWorld”). The type associated with the Topic is specified with its registered name.

DDS_Topic *topic = NULL;

topic = DDS_DomainParticipant_create_topic(participant,
                                           "Example HelloWorld",
                                           "HelloWorld",
                                           &DDS_TOPIC_QOS_DEFAULT,
                                           NULL,
                                           DDS_STATUS_MASK_NONE);

if (topic == NULL)
{
    /* failure */
}

3.5.5.3. DPSE Discovery: assert remote participant

DPSE Discovery relies on the application to specify the other, or remote, DomainParticipants that its local DomainParticipants are allowed to discover. Your application must call a DPSE API for each remote participant to be discovered. The API takes as input the name of the remote participant.

/* Enable discovery of remote participant with name Participant_2 */
retcode = DPSE_RemoteParticipant_assert(participant, "Participant_2");
if (retcode != DDS_RETCODE_OK)
{
    /* failure */
}

For more information, see the Working with DDS Domains in the Core Libraries User’s Manual.

3.5.6. Create Publisher

A publishing application needs to create a DDS Publisher and then a DataWriter for each Topic it wants to publish.

In Connext Drive, PublisherQos in general contains no policies that need to be customized, while DataWriterQos does contain several customizable policies.

  • Create Publisher:

    DDS_Publisher *publisher = NULL;
    publisher = DDS_DomainParticipant_create_publisher(participant,
                                                       &DDS_PUBLISHER_QOS_DEFAULT,
                                                       NULL,
                                                       DDS_STATUS_MASK_NONE);
    
    if (publisher == NULL)
    {
        /* failure */
    }
    

For more information, see the Sending Data section in the Core Libraries User’s Manual.

3.5.7. Create DataWriter

DDS_DataWriter *datawriter = NULL;
struct DDS_DataWriterQos dw_qos = DDS_DataWriterQos_INITIALIZER;
struct DDS_DataWriterListener dw_listener = DDS_DataWriterListener_INITIALIZER;

/* Configure writer Qos */
dw_qos.protocol.rtps_object_id = 100;
dw_qos.reliability.kind = DDS_RELIABLE_RELIABILITY_QOS;
dw_qos.resource_limits.max_samples_per_instance = 2;
dw_qos.resource_limits.max_instances = 2;
dw_qos.resource_limits.max_samples =
    dw_qos.resource_limits.max_samples_per_instance * dw_qos.resource_limits.max_instances;
dw_qos.history.depth = 1;
dw_qos.durability.kind = DDS_VOLATILE_DURABILITY_QOS;
dw_qos.protocol.rtps_reliable_writer.heartbeat_period.sec = 0;
dw_qos.protocol.rtps_reliable_writer.heartbeat_period.nanosec = 250000000;

/* Set enabled listener callbacks */
dw_listener.on_publication_matched = HelloWorldPublisher_on_publication_matched;

datawriter =
    DDS_Publisher_create_datawriter(publisher,
                                    topic,
                                    &dw_qos,
                                    &dw_listener,
                                    DDS_PUBLICATION_MATCHED_STATUS);

if (datawriter == NULL)
{
    /* failure */
}

The DataWriterListener has its callbacks selectively enabled by the DDS status mask. In the example, the mask has set the on_publication_matched status, and accordingly the DataWriterListener has its on_publication_matched assigned to a callback function.

void HelloWorldPublisher_on_publication_matched(void *listener_data,
                                                DDS_DataWriter * writer,
                                                const struct DDS_PublicationMatchedStatus *status)
{
    /* Print on match/unmatch */
    if (status->current_count_change > 0)
    {
        printf("Matched a subscriber\n");
    }
    else
    {
        printf("Unmatched a subscriber\n");
    }
}

3.5.7.1. DPSE Discovery: assert remote subscription

A publishing application using DPSE discovery must specify the other DataReaders that its DataReaders are allowed to discover. Like the API for asserting a remote participant, the DPSE API for asserting a remote subscription must be called for each remote DataReader that a DataWriter may discover.

Whereas asserting a remote participant requires only the remote Participant’s name, asserting a remote subscription requires more configuration, as all QoS policies of the subscription necessary to determine matching must be known and thus specified.

struct DDS_SubscriptionBuiltinTopicData rem_subscription_data =
    DDS_SubscriptionBuiltinTopicData_INITIALIZER;

/* Set Reader's protocol.rtps_object_id */
rem_subscription_data.key.value[DDS_BUILTIN_TOPIC_KEY_OBJECT_ID] = 200;

rem_subscription_data.topic_name = DDS_String_dup("Example HelloWorld");
rem_subscription_data.type_name = DDS_String_dup("HelloWorld");

rem_subscription_data.reliability.kind = DDS_RELIABLE_RELIABILITY_QOS;

retcode = DPSE_RemoteSubscription_assert(participant,
                                         "Participant_2",
                                         &rem_subscription_data,
                                         HelloWorld_get_key_kind(HelloWorldTypePlugin_get(),
                                         NULL)));
if (retcode != DDS_RETCODE_OK)
{
    /* failure */
}

3.5.7.2. Writing samples

Within the generated type support code are declarations of the type-specific DataWriter. For the HelloWorld type, this is the HelloWorldDataWriter.

Writing a HelloWorld sample is done by calling the write API of the HelloWorldDataWriter.

HelloWorldDataWriter *hw_datawriter;
DDS_ReturnCode_t retcode;
HelloWorld *sample = NULL;

/* Create and set sample */
sample = HelloWorld_create();
if (sample == NULL)
{
    /* failure */
}
sprintf(sample->msg, "Hello World!");

/* Write sample */
hw_datawriter = HelloWorldDataWriter_narrow(datawriter);

retcode = HelloWorldDataWriter_write(hw_datawriter, sample, &DDS_HANDLE_NIL);
if (retcode != DDS_RETCODE_OK)
{
    /* failure */
}

For more information, see the Sending Data section in the Core Libraries User’s Manual.

3.5.8. Create Subscriber

A subscribing application needs to create a DDS Subscriber and then a DataReader for each Topic to which it wants to subscribe.

In the cert profile of Connext Drive, SubscriberQos in general contains no policies that need to be customized, while DataReaderQos does contain several customizable policies.

DDS_Subscriber *subscriber = NULL;
subscriber = DDS_DomainParticipant_create_subscriber(participant,
                                                     &DDS_SUBSCRIBER_QOS_DEFAULT,
                                                     NULL,
                                                     DDS_STATUS_MASK_NONE);
if (subscriber == NULL)
{
    /* failure */
}

For more information, see the Receiving Data section in the Core Libraries User’s Manual.

3.5.9. Create DataReader

DDS_DataReader *datareader = NULL;
struct DDS_DataReaderQos dr_qos = DDS_DataReaderQos_INITIALIZER;
struct DDS_DataReaderListener dr_listener = DDS_DataReaderListener_INITIALIZER;

/* Configure Reader Qos */
dr_qos.protocol.rtps_object_id = 200;
dr_qos.resource_limits.max_instances = 2;
dr_qos.resource_limits.max_samples_per_instance = 2;
dr_qos.resource_limits.max_samples =
    dr_qos.resource_limits.max_samples_per_instance * dr_qos.resource_limits.max_instances;
dr_qos.reader_resource_limits.max_remote_writers = 10;
dr_qos.reader_resource_limits.max_remote_writers_per_instance = 10;
dr_qos.history.depth = 1;
dr_qos.durability.kind = DDS_VOLATILE_DURABILITY_QOS;
dr_qos.reliability.kind = DDS_RELIABLE_RELIABILITY_QOS;

/* Set listener callbacks */
dr_listener.on_data_available = HelloWorldSubscriber_on_data_available;
dr_listener.on_subscription_matched = HelloWorldSubscriber_on_subscription_matched;

datareader = DDS_Subscriber_create_datareader(subscriber,
                                              DDS_Topic_as_topicdescription(topic),
                                              &dr_qos,
                                              &dr_listener,
                                              DDS_DATA_AVAILABLE_STATUS | DDS_SUBSCRIPTION_MATCHED_STATUS);
if (datareader == NULL)
{
    /* failure */
}

The DataReaderListener has its callbacks selectively enabled by the DDS status mask. In the example, the mask has set the DDS_SUBSCRIPTION_MATCHED_STATUS and DDS_DATA_AVAILABLE_STATUS statuses, and accordingly the DataReaderListener has its on_subscription_matched and on_data_available assigned to callback functions.

void HelloWorldSubscriber_on_subscription_matched(void *listener_data,
                                                  DDS_DataReader * reader,
                                                  const struct DDS_SubscriptionMatchedStatus *status)
{
    if (status->current_count_change > 0)
    {
        printf("Matched a publisher\n");
    }
    else
    {
        printf("Unmatched a publisher\n");
    }
}
void HelloWorldSubscriber_on_data_available(void* listener_data,
                                            DDS_DataReader* reader)

    {
    HelloWorldDataReader *hw_reader = HelloWorldDataReader_narrow(reader);
    DDS_ReturnCode_t retcode;
    struct DDS_SampleInfo *sample_info = NULL;
    HelloWorld *sample = NULL;

    struct DDS_SampleInfoSeq info_seq =
        DDS_SEQUENCE_INITIALIZER(struct DDS_SampleInfo);
    struct HelloWorldSeq sample_seq =
        DDS_SEQUENCE_INITIALIZER(HelloWorld);

    const DDS_Long TAKE_MAX_SAMPLES = 32;
    DDS_Long i;

    retcode = HelloWorldDataReader_take(hw_reader,
        &sample_seq, &info_seq, TAKE_MAX_SAMPLES,
        DDS_ANY_SAMPLE_STATE, DDS_ANY_VIEW_STATE, DDS_ANY_INSTANCE_STATE);

    if (retcode != DDS_RETCODE_OK)
    {
        printf("failed to take data: %d\n", retcode);
        goto done;
    }

    /* Print each valid sample taken */
    for (i = 0; i < HelloWorldSeq_get_length(&sample_seq); ++i)
    {
        sample_info = DDS_SampleInfoSeq_get_reference(&info_seq, i);

        if (sample_info->valid_data)
        {
            sample = HelloWorldSeq_get_reference(&sample_seq, i);
            printf("\nSample received\n\tmsg: %s\n", sample->msg);
        }
        else
        {
            printf("not valid data\n");
        }
    }

    HelloWorldDataReader_return_loan(hw_reader, &sample_seq, &info_seq);

done:
    HelloWorldSeq_finalize(&sample_seq);
    DDS_SampleInfoSeq_finalize(&info_seq);
}

3.5.9.1. DPSE Discovery: assert remote publication

A subscribing application using DPSE discovery must specify the other DataReaders that its DataReaders are allowed to discover. Like the API for asserting a remote participant, the DPSE API for asserting a remote publication must be called for each remote DataWriter that a DataReader may discover.

struct DDS_PublicationBuiltinTopicData rem_publication_data =
    DDS_PublicationBuiltinTopicData_INITIALIZER;

/* Set Writer's protocol.rtps_object_id */
rem_publication_data.key.value[DDS_BUILTIN_TOPIC_KEY_OBJECT_ID] = 100;

rem_publication_data.topic_name = DDS_String_dup("Example HelloWorld");
rem_publication_data.type_name = DDS_String_dup("HelloWorld");

rem_publication_data.reliability.kind = DDS_RELIABLE_RELIABILITY_QOS;

retcode = DPSE_RemotePublication_assert(participant,
                                        "Participant_1",
                                        &rem_publication_data,
                                        HelloWorld_get_key_kind(HelloWorldTypePlugin_get(),
                                        NULL)));
if (retcode != DDS_RETCODE_OK)
{
    /* failure */
}

Asserting a remote publication requires configuration of all QoS policies necessary to determine matching.

3.5.9.2. Receiving samples

Accessing received samples can be done in a few ways:

  • Polling. Do read or take within a periodic polling loop.

  • Listener. When a new sample is received, the DataReaderListener’s on_data_available is called. Processing is done in the context of the middleware’s receive thread. See the above HelloWorldSubscriber_on_data_available callback for example code.

  • Waitset. Create a waitset, attach it to a status condition with the data_available status enabled, and wait for a received sample to trigger the waitset. Processing is done in the context of the user’s application thread. (Note: the code snippet below is taken from the shipped HelloWorld_dpde_waitset example).

DDS_WaitSet *waitset = NULL;
struct DDS_Duration_t wait_timeout = { 10, 0 }; /* 10 seconds */
DDS_StatusCondition *dr_condition = NULL;
struct DDS_ConditionSeq active_conditions =
    DDS_SEQUENCE_INITIALIZER(struct DDS_ConditionSeq);

if (!DDS_ConditionSeq_initialize(&active_conditions))
{
    /* failure */
}

if (!DDS_ConditionSeq_set_maximum(&active_conditions, 1))
{
    /* failure */
}

waitset = DDS_WaitSet_new();
if (waitset == NULL )
{
    /* failure */
}

dr_condition = DDS_Entity_get_statuscondition(DDS_DataReader_as_entity(datareader));

retcode = DDS_StatusCondition_set_enabled_statuses(dr_condition,
                                                   DDS_DATA_AVAILABLE_STATUS);

if (retcode != DDS_RETCODE_OK)
{
    /* failure */
}

retcode = DDS_WaitSet_attach_condition(waitset,
                                       DDS_StatusCondition_as_condition(dr_condition));

if (retcode != DDS_RETCODE_OK)
{
    /* failure */
}

retcode = DDS_WaitSet_wait(waitset, active_conditions, &wait_timeout);

switch (retcode) {
    case DDS_RETCODE_OK:
    {
       /* This WaitSet only has a single condition attached to it
        * so we can implicitly assume the DataReader's status condition
        * to be active (with the enabled DATA_AVAILABLE status) upon
        * successful return of wait().
        * If more than one conditions were attached to the WaitSet,
        * the returned sequence must be examined using the
        * commented out code instead of the following.
        */

        HelloWorldSubscriber_take_data(HelloWorldDataReader_narrow(datareader));

        /*
        DDS_Long active_len = DDS_ConditionSeq_get_length(&active_conditions);
        for (i = active_len - 1; i >= 0; --i)
        {
            DDS_Condition *active_condition =
                *DDS_ConditionSeq_get_reference(&active_conditions, i);

            if (active_condition ==
                    DDS_StatusCondition_as_condition(dr_condition))
            {
                total_samples += HelloWorldSubscriber_take_data(
                        HelloWorldDataReader_narrow(datareader));
            }
            else if (active_condition == some_other_condition)
            {
                do_something_else();
            }
        }
        */
        break;
    }
    case DDS_RETCODE_TIMEOUT:
    {
        printf("WaitSet_wait timed out\n");
        break;
    }
    default:
    {
        printf("ERROR in WaitSet_wait: retcode=%d\n", retcode);
        break;
    }
}

3.5.9.3. Filtering samples

In lieu of supporting Content-Filtered Topics, a DataReaderListener in Connext Drive provides callbacks to do application-level filtering per sample.

  • on_before_sample_deserialize. Through this callback, a received sample is presented to the application before it has been deserialized or stored in the DataReader’s queue.

  • on_before_sample_commit. Through this callback, a received sample is presented to the application after it has been deserialized but before it has been stored in the DataReader’s queue.

You control the callbacks’ sample_dropped parameter; upon exiting either callback, the DataReader will drop the sample if sample_dropped is true. Consequently, dropped samples are not stored in the DataReader’s queue and are not available to be read or taken.

Neither callback is associated with a DDS Status. Rather, each is enabled when assigned to a non-NULL callback.

Note

Because it is called after the sample has been deserialized, on_before_sample_commit provides an additional sample_info parameter, containing some of the usual sample information that would be available when the sample is read or taken.

The HelloWorld_dpde example’s subscriber has this on_before_sample_commit callback:

DDS_Boolean HelloWorldSubscriber_on_before_sample_commit(
    void *listener_data,
    DDS_DataReader *reader,
    const void *const sample,
    const struct DDS_SampleInfo *const sample_info,
    DDS_Boolean *dropped)
{
    HelloWorld *hw_sample = (HelloWorld *)sample;

    /* Drop samples with even-numbered count in msg */
    HelloWorldSubscriber_filter_sample(hw_sample, dropped);

    if (*dropped)
    {
        printf("\nSample filtered, before commit\n\tDROPPED - msg: %s\n",
               hw_sample->msg);
    }

    return DDS_BOOLEAN_TRUE;
}

...

dr_listener.on_before_sample_commit =
    HelloWorldSubscriber_on_before_sample_commit;

For more information, see the Receiving Data section in the Core Libraries User’s Manual.

3.6. Next Steps

Congratulations! You’ve written your first DDS applications, which publish HelloWorld data. In this exercise, you’ve experienced a quick overview of the development process from defining a data type and using the code generator, to building an example application and using Connext tools to see that data is being published. We’ll continue to build on these skills and to use these tools in more depth in subsequent exercises.

The next section takes a deeper dive into building a successful Next Generation E/E Architecture for automotive systems with Connext Drive. This includes a hands-on example and advice for integration with major industry standards, such as ROS 2, AUTOSAR Classic, and AUTOSAR Adaptive.