4. Introduction to Keys and Instances

Prerequisites
  • Introduction to Data Types, including:
    • Typed data
    • Interface Definition Language (IDL)
    • Introduction to data flows
    • Streaming data
  • Repository cloned from GitHub here
  • Install CMake® 3.11 or higher from cmake.org
Time to complete 1 hour
Concepts covered in this module
  • Definition of an instance
  • Benefits of using instances
  • How key fields identify an instance
  • How to write a new instance
  • Instance lifecycles

So far, we’ve talked about samples. A DataWriter, for example, publishes samples of a particular Topic. Sometimes, we want to use one Topic to publish samples of data for several different objects, such as flights or sensors. Connext DDS uses “instances” to represent these real-world objects. (See Table 4.2.)

When you need to represent multiple objects within a DDS Topic, you use a key to establish instances. A key in DDS is similar to a primary key in a database—it is a unique identifier of something within your data. An instance is the object identified by the key. A key can be composed of multiple fields in your data as long as they uniquely identify the object you are representing. For example, in an air traffic control system, the key fields might be the airline name and flight number. Samples would be the updated locations of each flight “instance.” See other examples of keys, instances, and samples in the following table.

Table 4.2 Examples of Instances and Keys in Distributed Systems
Instance Key Data Type Samples
Commercial flight being tracked Airline name and flight number, such as:
Airline: “United Airlines”
Flight number: 901
string airline
short flight_num
float lat
float long
UA, 901, 37.7749, -122.4194
UA, 901, 37.7748, -122.4195
Sensor sending data, such as an individual temperature sensor Unique identifier of that sensor, such as:
“tempering-machine-1” or “FirstFloorSensor1”
string sensor_id
long temperature
tempering-machine-1, temperature = 175
tempering-machine-1, temperature = 176
Car being monitored Vehicle identification number (VIN) of the car string VIN
float lat
float long
JH4DA9370MS016526, 37.7749, -122.4194
JH4DA9370MS016526, 37.7748, -122.4195
Chocolate lot being processed in a factory Chocolate lot identifier; likely an incrementing number that rolls over uint32 lot_num
LotStatusKind state
1, waiting for cocoa
1, waiting for sugar

To specify one or more key fields, annotate them with “@key” in the IDL file. For example:

// Temperature data type
struct Temperature {

    // Unique ID of the sensor sending the temperature. Each time a sample is
    // written with a new ID, it represents a new sensor instance.
    @key
    string<256> sensor_id;

    // Degrees in Celsius
    long degrees;

};

You add an @key annotation before each field that is part of the unique identifier of your instance.

4.1. Why and How Do We Use Instances?

Not every data model requires that you use instances. You’re already dividing up your data into multiple Topics. So why use instances at all?

  • Less memory and discovery time
    Creating a new instance is lighter-weight than creating a new DataWriter/DataReader/Topic. For example, if you’re representing airline flights, you could create a new DataWriter, DataReader, and Topic each time a new flight takes off. At a major airport, that’s over a thousand flights per day! The problem with a one-Topic-per-flight system is that it uses more memory than necessary, and it takes more time for discovery. Using instances to represent unique flights requires less memory, and the instances do not need to be discovered the way DataWriters and DataReaders discover each other. (More on discovery in a later module.)

  • Lifecycle
    Instances allow you to model the behavior of real-world objects that come and go. For example, you can use the instance lifecycle to detect an event such as a flight landing or a chocolate lot finishing. We will go into this in more detail when we talk about the Instance Lifecycle below.

  • QoS
    Several Quality of Service (QoS) policies are applied per-instance. This is a huge benefit, and we’ll talk about this in greater detail later in Basic QoS.

4.1.1. Writing an Instance

To send a sample of an instance, all you need to do is make sure the key fields are set to the unique ID of your instance. Let’s refer back to the following example type, representing temperature sensor data:

// Temperature data type
struct Temperature {

    // Unique ID of the sensor sending the temperature. Each time a sample is
    // written with a new ID, it represents a new sensor instance.
    @key
    string<256> sensor_id;

    // Degrees in Celsius
    long degrees;

};

For the purposes of this example, assume that each physical sensor in our distributed system has a unique ID assigned to it, and this ID maps to the sensor_id field in our type. By marking sensor_id with the @key annotation, we have marked it a key field, and therefore each unique sensor_id string we write will represent a different DDS instance.

If we want to write values for multiple sensors, we can change our code so the application takes an ID as a command-line parameter. Then, it can use that ID from the command line as part of the string that becomes the unique sensor ID—for example, TemperingMachine-<id>.

// Modify the data to be written here
// Specify the sensor instance sending the temperature. ID is passed at
// the command line. Each unique "TemperingMachine-<id>" is a
// unique instance.
snprintf(sample.sensor_id, 255, "TemperingMachine-%s", sensor_id);
sample.degrees = 32;

DDS_ReturnCode_t retcode = writer->write(sample, DDS_HANDLE_NIL);

Each time you pass a new ID parameter to the application, you have a new instance in your system. Any time a DataWriter writes a sample with a unique value in the sample’s key fields, it is writing to a unique instance. A DataWriter can write multiple instances just by setting the key fields to unique values.

In our current example, each DataWriter writes a single instance; however, as shown in Figure 4.2, the number of instances is not directly related to the number of DataWriters. One DataWriter can write many instances. Or multiple DataWriters can write one or more instances.

You have the flexibility to design your system however you want. Any DataWriter of a given Topic can be responsible for updating one or more instances within that Topic, depending on your requirements.

Tip

Remember: a “sample” is a single update of data. Every time your application calls the DataWriter’s write method, the DataWriter sends a sample. This is true whether you have instances in your system or not.

Samples Without Instances

Figure 4.1 Samples without instances

Samples With Instances

Figure 4.2 Samples with instances

4.1.2. Reading an Instance

An instance lifecycle event will set the data_available status to true, similar to what we have previously seen when a new sample is available for a DataReader. In the code we have seen so far, the WaitSet wakes when the data_available status becomes true and when we can process the sample. When the application gets the data_available notification from an instance lifecycle event, retrieving an instance is identical to retrieving a sample (except that you have some additional options). You may remember the code from “Hello World”, where a DataReader calls take() to retrieve data from the middleware, and then iterates over a collection of all the samples:

TemperatureSeq data_seq;
DDS_SampleInfoSeq info_seq;
unsigned int samples_read = 0;

// Take available data from DataReader's queue
DDS_ReturnCode_t retcode = temperature_reader->take(data_seq, info_seq);

...

// Iterate over all available data
for (int i = 0; i < data_seq.length(); ++i) {
    // Check if a sample is an instance lifecycle event
    if (!info_seq[i].valid_data) {
        std::cout << "Received instance state notification" << std::endl;
        continue;
    }
    // Print data
    TemperatureTypeSupport::print_data(&data_seq[i]);
    samples_read++;
}

In a system with instances, the take() call will return a sequence of samples for all of the instances in the system. (There is also a take_instance() call that can be used to retrieve samples for a particular instance. For all the options for accessing data, see this section in the Traditional C++ API Reference.)

Take a closer look at that code, and you will notice there is a line where we check that the sample is valid:

if (!info_seq[i].valid_data) {

You may be wondering: what does it mean for the sample to not be valid? The answer is: a sample can contain data (the sample is valid) or it can contain information about the instance’s lifecycle (the sample is not valid). The data_available event notifies the DataReader about these updates in the same way, so “valid” indicates whether the update is a sample containing data or a sample containing an instance lifecycle update.

4.1.3. Instance Lifecycle

An instance can be in the following states, which are all part of the instance lifecycle:

  • Alive: A DataWriter has written the instance, and the DataWriter is still in the system.
  • Not alive, disposed: A DataWriter made an API call to notify all the DataReaders in the system that this instance has been “disposed.” For example, you might set up a system in which once a flight has landed, it is disposed (we don’t need to track it anymore).
  • Not alive, no writers: Every DataWriter that wrote that instance has left the system (or declared it is no longer writing that instance).

We can use the state of instances in our application (i.e., the instance lifecycle) to trigger specific logic or to track specific events. What does it mean at a system level when an instance is not alive because of no writers, or if it is disposed? This depends on your system—DDS notifies the DataReaders that the instance has changed state, and you can decide what it means in your system. In the next section, Example: Chocolate Factory, we’ll look at one way that you can use instance states in a chocolate factory example.

All of the information about an instance’s lifecycle is part of the SampleInfo, which can be found in the DDS_SampleInfoSeq that you passed to the take() call. The DDS_SampleInfo at a specific position in the sequence corresponds to the sample in the same position in the data sequence. You can see an example of this above in the code where we check if the sample is valid. Take a look at Instance States, in the RTI Connext DDS Core Libraries User’s Manual to see the state diagram describing the instance lifecycle. To review the state data that is specific to instances, you use the info_seq[i].instance_state field—you can query this state to see if the instance is ALIVE, NOT_ALIVE_DISPOSED, or NOT_ALIVE_NO_WRITERS.

So, what is the typical lifecycle of an instance? An instance first becomes alive; this happens when a DataWriter writes an instance for the first time. Then the instance may receive updates for some period of time from DataWriters publishing to that instance. If an instance becomes not alive, the instance will transition to either “Not alive, no writers” or “Not alive, disposed”. An instance may become not alive for a variety of reasons, which are detailed in the following table.

Table 4.3 Instance State Transitions
State How Change Occurs
Alive
  • Any time the instance is written
Not alive, disposed
  • Any single DataWriter that has written this instance calls dispose()
Not alive, no writers

4.2. Example: Chocolate Factory

This example illustrates the use of instance states in the context of a chocolate factory, where different stations add ingredients to a chocolate lot, and where the final stage is a tempering station that heats and then cools the chocolate to a specific temperature.

In the chocolate factory that we are creating, there will be two data types: Temperature and ChocolateLotState. We saw these types earlier, in Introduction to Data Types.

Since a temperature reading doesn’t “go away” like a flight does, we will not use the instance lifecycle for the “Temperature” Topic. Instead, we will focus on the “ChocolateLotState” Topic, which can utilize instance lifecycle events.

4.2.1. Chocolate Factory: System Overview

We will start building this system with the following applications:

  • Monitoring/Control application:
    • Starts off processing a chocolate lot by writing a sample of the “ChocolateLotState” Topic, saying that a chocolate lot is ready to be processed.
    • Monitors the “ChocolateLotState” Topic as it’s updated by the different stations.
    • Finally, reads the “ChocolateTemperature” Topic to check that tempering is done correctly.
  • Tempering Station application:
    • Writes to the “ChocolateTemperature” Topic to let the Monitoring/Control application know the current tempering temperature.
    • Monitors the “ChocolateLotState” Topic to see if it needs to process a lot.
    • Processes the lot and updates the state of the lot by writing to the “ChocolateLotState” Topic.
  • Ingredient Station application (not implemented until a later module):
    • Monitors the “ChocolateLotState” Topic to see if it needs to process a lot.
    • Processes the lot (adds an ingredient) and updates the state of the lot by writing to the “ChocolateLotState” Topic.
Chocolate Lot Stations

Figure 4.3 There are three applications in this chocolate factory. We will illustrate only the Tempering Application and the Monitoring/Control Application for now.

4.2.2. Chocolate Factory: Data Overview

A chocolate lot is processed by various stations described above. At each station, the chocolate lot transitions through three states:

  • Waiting at station
  • Processing at station
  • Completed by station

To represent the state of each chocolate lot that we are processing, we are going to be using a more complex version of the ChocolateLotState data type than we saw in the last module.

Recall that in the IDL file, the ChocolateLotState data type uses a lot ID (lot_id) as the key field:

struct ChocolateLotState {
    // Unique ID of the chocolate lot being produced.
    // rolls over each day.
    @key
    int32 lot_id;

    // Which station is producing the status
    StationKind station;

    // This will be the same as the current station if the station producing
    // the status is currently processing the lot.
    StationKind next_station;

    // Current status of the chocolate lot: Waiting/Processing/Completed
    LotStatusKind lot_status;

};

As a chocolate lot receives ingredients from stations in the factory, the stations update the “ChocolateLotState” Topic.

The fields station and next_station in the IDL file represent the current station processing the lot, and the next station that should process the lot. If there is no current station (because the lot is waiting for the first station), station will be INVALID_CONTROLLER. If the lot is processing at a station and is not ready to be sent to the next station, the next_station field will be INVALID_CONTROLLER. The stations are represented by an enumeration in the IDL (not shown here).

The lot_status field describes the status of the lot at a given controller: WAITING, PROCESSING, or COMPLETED. This status is represented in an enumeration in the IDL (not shown here).

When a chocolate lot finishes, the tempering application disposes the instance with lot_status_writer.dispose().

Note

Disposing an instance does not necessarily free up memory. There is some subtlety about memory management when using instances, so when you get past the basics, it’s a good idea to review Instance Memory Management, in the RTI Connext DDS Core Libraries User’s Manual.

4.3. Hands-On 1: Build the Applications and View in Admin Console

To keep this example from getting too complex, we are focusing on just the Monitoring/Control and Tempering applications right now. These applications have more than a single DataReader or DataWriter, as you can see below:

Multiple Readers and Writers

Figure 4.4 Our applications have more than one DataReader or DataWriter.

Let’s look at the “ChocolateLotState” DataWriters and DataReaders more closely:

Multiple Readers and Writers - Cross-Application

Figure 4.5 In our example, the DataWriter in each application communicates with two DataReaders—one in its own application and one in another application

Here’s a view of the “ChocolateLotState” DataWriters and DataReaders with samples:

Multiple Readers and Writers - Details

Figure 4.6 Notice that both DataWriters are communicating with both DataReaders

(Actually, the DataReader in the Tempering application only cares about the chocolate lot state if the next station is itself. Right now, the code tells that DataReader to ignore any next station state that isn’t the Tempering application, but later we will use content filtering to do this instead.)

Note

Figure 4.5 and Figure 4.6 demonstrate that it doesn’t matter where the matching DataWriters and DataReaders are. They could be in the same application or different applications. As long as they match in Topic and Quality of Service (more on Basic QoS later), they can communicate.

Since our Tempering application and Monitoring/Control application each write and read data, we will have to move past the simple makefiles generated by RTI Code Generator (rtiddsgen). To support this, we are using CMake for the rest of these exercises. If you can’t use CMake or prefer to work directly with makefiles, you can use rtiddsgen to create makefiles and then edit them. (If you want to create your own makefiles, find information on compilation and linker settings for your architecture in the RTI Connext DDS Core Libraries Platform Notes.)

4.3.1. Compile the Applications

In this exercise, you’ll be working in the directory 4_keys_instances/c++98. This directory was created when you cloned the getting started repository from github in the first module, in Clone Repository. We’ll start by building the applications using CMake.

Tip

You don’t need to run rtisetenv_<arch> like you did in the previous modules because the CMake script finds the Connext DDS installation directory and sets up the environment for you.

  1. If you do not have CMake already, download a binary distribution of CMake from cmake.org.

    Note

    You must have CMake version 3.11 or higher.

  2. Open a command prompt.

  3. Create build files using CMake.

    In the 4_keys_instances/c++98 directory, type the following, depending on your operating system:

    $ mkdir build
    $ cd build
    $ cmake ..
    

    Make sure “cmake” is in your path.

    If Connext DDS is not installed in the default location (see Paths Mentioned in Documentation), specify cmake as follows: cmake -DCONNEXTDDS_DIR=<installation dir> ...

    $ mkdir build
    $ cd build
    $ cmake ..
    

    Make sure “cmake” is in your path.

    If Connext DDS is not installed in the default location (see Paths Mentioned in Documentation), specify cmake as follows: cmake -DCONNEXTDDS_DIR=<installation dir> ...

    1. Enter the following commands:

      > mkdir build
      > cd build
      > "c:\program files\CMake\bin\cmake" --help
      

      Your path name may vary. Substitute "c:\program files\CMake\bin\cmake" with the correct path for your CMake installation.

      --help will list all the compilers you can generate project files for. Choose an installed compiler version, such as “Visual Studio 15 2017”.

    2. From within the build directory, create the build files:

      • If you have a 64-bit Windows® machine, add the option -A x64. For example:

        > "c:\program files\CMake\bin\cmake" -G "Visual Studio 15 2017" -D CONNEXTDDS_ARCH=x64Win64VS2017 -A x64 ..
        
      • If you have a 32-bit Windows machine, add the option -A Win32. For example:

        > "c:\program files\CMake\bin\cmake" -G "Visual Studio 15 2017" -D CONNEXTDDS_ARCH=i86Win32VS2017 -A Win32 ..
        

      Note

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

      • If Connext DDS is not installed in the default location (see Paths Mentioned in Documentation), add the option -DCONNEXTDDS_DIR=<install dir>. For example:

        > c:\program files\CMake\bin\cmake" -G "Visual Studio 15 2017" -DCONNEXTDDS_ARCH=x64Win64VS2017 -A x64 -DCONNEXTDDS_DIR=<install dir> ..
        
  4. From within the build directory, compile the code:

    $ make
    
    $ make
    
    1. Open rticonnextdds-getting-started-keys-instances.sln in Visual Studio by entering rticonnextdds-getting-started-keys-instances.sln at the command prompt or using File > Open Project in Visual Studio.

    2. Right-click ALL_BUILD and choose Build. (See 2_hello_world\<language>\README_<architecture>.txt if you need more help.)

      Since “Debug” is the default option at the top of Visual Studio, a Debug directory will be created with the compiled files.

4.3.2. Run Multiple Copies of the Tempering Application

For now, we will focus on visualizing the Tempering application’s instances. Run multiple copies of the Tempering application. Be sure to specify different sensor IDs at the command line:

  1. Open a terminal window and, from within the build directory, run the Tempering application with a sensor ID as a parameter:

    $ ./tempering_application -i 1
    
  2. Open a second terminal window and, from within the build directory, run the Tempering application with a different sensor ID as a parameter:

    $ ./tempering_application -i 2
    
  1. Open a terminal window and, from within the build directory, run the Tempering application with a sensor ID as a parameter:

    $ ./tempering_application -i 1
    
  2. Open a second terminal window and, from within the build directory, run the Tempering application with a different sensor ID as a parameter:

    $ ./tempering_application -i 2
    
  1. Open a command prompt and, from within the build directory, run the Tempering application with a sensor ID as a parameter:

    > Debug\tempering_application.exe -i 1
    
  2. Open a second command prompt and, from within the build directory, run the Tempering application with a different sensor ID as a parameter:

    > Debug\tempering_application.exe -i 2
    

You should see output similar to the following in both windows:

ChocolateTemperature Sensor with ID: 1 starting
Waiting for lot
Wait timed out after 10 seconds.
Waiting for lot
Wait timed out after 10 seconds.
Waiting for lot
Wait timed out after 10 seconds.
Waiting for lot
Wait timed out after 10 seconds.
Waiting for lot
Wait timed out after 10 seconds.
Waiting for lot
Wait timed out after 10 seconds.
Waiting for lot
Wait timed out after 10 seconds.
Waiting for lot

4.3.3. View the Data in Admin Console

  1. Make sure your two Tempering applications from the previous step are still running.

  2. Like you did in Hands-On 2: Viewing Your Data, open up Admin Console and switch to the Data Visualization Perspective.

    Data Visualization Icon in Admin Console

    (If the Data Visualization Perspective is grayed out, you may already be in that view.)

  3. Select the “ChocolateTemperature” Topic in the Logical View:

    Select ChocolateTemperature in Logical View

    Note

    Don’t worry about the warnings. We’ll look at those in Hands-On 4: Debugging the System and Completing the Application.

  4. Subscribe to the “ChocolateTemperature” Topic.

    Click the Subscribe button:

    Subscribe in Admin Console

    Then click OK:

    Click OK in Admin Console
  5. View the samples coming in.

    Recall that in the “Hello World” hands-on exercise, we did not yet have instances. Every sample updated in a single row in Admin Console:

    Single Instance in Admin Console

    With instances, however, you see multiple rows in Admin Console:

    Multiple Instances in Admin Console

    Figure 4.12 Since sensor_id is a key field for the ChocolateTemperature data type, Admin Console understands that every sample is an update to a particular sensor_id instance. Therefore, Admin Console can display each instance separately, and you can see the degrees value from each sensor displayed separately.

    Admin Console can show you one row per instance because it recognizes each instance as a different object.

  6. Click Unsubscribe.

    In Hands-On 4: Debugging the System and Completing the Application, we will debug your system using Admin Console. We do not want Admin Console subscribing to any data for that exercise.

4.4. Hands-On 2: Run Both Applications

4.4.1. Run Monitoring and Tempering Applications

  1. In the previous hands-on, you ran multiple copies of the tempering application—quit them now if they’re still running.

  2. From any command prompt window, run the Monitoring/Control application, which you already built in Compile the Applications:

    From within the build directory:

    $ ./monitoring_ctrl_application
    

    From within the build directory:

    $ ./monitoring_ctrl_application
    

    From within the build directory:

    > Debug\monitoring_ctrl_application.exe
    

    You should see output like the following:

    Starting lot:
    [lot_id: 0 next_station: TEMPERING_CONTROLLER]
    
    Starting lot:
    [lot_id: 1 next_station: TEMPERING_CONTROLLER]
    Received lot update:
    [lot_id: 1, station: INVALID_CONTROLLER, next_station: TEMPERING_CONTROLLER, lot_status: WAITING]
    
    Starting lot:
    [lot_id: 2 next_station: TEMPERING_CONTROLLER]
    Received lot update:
    [lot_id: 2, station: INVALID_CONTROLLER, next_station: TEMPERING_CONTROLLER, lot_status: WAITING]
    

    This output shows the two things that the Monitoring/Control application is doing:

    • Starts the chocolate lots by writing a sample, which indicates that the next station is the TEMPERING_CONTROLLER. This line shows the application in its control capacity (“I’m starting the lot”):

      Starting lot:
      [lot_id: 1 next_station: TEMPERING_CONTROLLER]
      
    • Reads the current state of the chocolate lot and prints that to the console. This line shows the application in its monitoring capacity (“right now, the lot is starting”).

      Received lot update:
      [lot_id: 1, station: INVALID_CONTROLLER, next_station: TEMPERING_CONTROLLER, lot_status: WAITING]
      

    Since the Monitoring/Control application is the only application running right now, the lots will always be WAITING. In the next step, this will change when you start the Tempering application.

  3. In your other command prompt window, run the Tempering application with a sensor ID as a parameter:

    From within the build directory:

    $ ./tempering_application -i 1
    

    From within the build directory:

    $ ./tempering_application -i 1
    

    From within the build directory:

    > Debug\tempering_application.exe -i 1
    

    You should see output like the following:

    ChocolateTemperature Sensor with ID: 1 starting
    Waiting for lot
    Processing lot #13
    Waiting for lot
    Waiting for lot
    Processing lot #14
    Waiting for lot
    Waiting for lot
    Processing lot #15
    

    Note

    You may notice that the Monitoring/Control application starts with lot #0, but the Tempering application’s DataReader does not receive notifications about lot #0, or any lots from before it starts up. We will talk about this more when we talk about the Durability QoS in Basic QoS.

  4. Review the output of the Monitoring/Control application now that you have started the Tempering application. You can see that the lot states changed to “PROCESSING” at the tempering station:

    Starting lot:
    [lot_id: 13 next_station: TEMPERING_CONTROLLER]
    Received lot update:
    [lot_id: 13, station: INVALID_CONTROLLER, next_station: TEMPERING_CONTROLLER, lot_status: WAITING]
    Received lot update:
    [lot_id: 13, station: TEMPERING_CONTROLLER, next_station: INVALID_CONTROLLER, lot_status: PROCESSING]
    
    Starting lot:
    [lot_id: 14 next_station: TEMPERING_CONTROLLER]
    Received lot update:
    [lot_id: 14, station: INVALID_CONTROLLER, next_station: TEMPERING_CONTROLLER, lot_status: WAITING]
    Received lot update:
    [lot_id: 14, station: TEMPERING_CONTROLLER, next_station: INVALID_CONTROLLER, lot_status: PROCESSING]
    
  5. Quit both applications.

4.4.2. Review the Tempering Application Code

This code is significantly more complex than the previous examples, because we are starting to build applications that are closer to real-world applications, with multiple DataWriters and DataReaders. Let’s start by examining the tempering_application.cxx code.

Some of this file should look familiar to you. This application is writing temperature data in the publish_temperature function. Even though a real tempering machine would raise the temperature of chocolate and then lower it to around freezing, in this example we’re just sending a “temperature” that’s close to freezing (in Fahrenheit).

void publish_temperature(const TemperatureWriteData *write_data)
{
    TemperatureDataWriter *writer = write_data->writer;

    // Create temperature sample for writing
    Temperature temperature;
    temperature.sensor_id = DDS_String_alloc(256);

    while (!shutdown_requested) {
        // Modify the data to be written here
        snprintf(temperature.sensor_id, 255, "%s", write_data->sensor_id);
        temperature.degrees = rand() % 3 + 30;  // Random num between 30 and 32

        DDS_ReturnCode_t retcode = writer->write(temperature, DDS_HANDLE_NIL);
        if (retcode != DDS_RETCODE_OK) {
            std::cerr << "write error " << retcode << std::endl;
        }

        // Update temperature every 100 ms
        DDS_Duration_t send_period = { 0, 100000000 };
        NDDSUtility::sleep(send_period);
    }
    DDS_String_free(temperature.sensor_id);
}

Now take a look at the process_lot function. This looks similar to the process_data functions you have seen in the previous hands-on exercises. However, this function takes both a DataWriter and a DataReader instead of just a DataReader. This function calls take() to retrieve data from the DataReader’s queue just like we did in the previous hands-on exercises. However, after taking that data, it updates the state of the lot to PROCESSING. In this example, instead of actually processing the lot, we’re going to sleep for 5 seconds to represent the processing.

// Take available data from DataReader's queue
DDS_ReturnCode_t retcode = lot_state_reader->take(data_seq, info_seq);

if (retcode != DDS_RETCODE_OK && retcode != DDS_RETCODE_NO_DATA) {
    std::cerr << "take error " << retcode << std::endl;
    return;
}

// Process lots waiting for tempering
for (int i = 0; i < data_seq.length(); ++i) {
    // Check if a sample is an instance lifecycle event
    if (info_seq[i].valid_data) {
        if (data_seq[i].next_station == TEMPERING_CONTROLLER) {
            std::cout << "Processing lot #" << data_seq[i].lot_id
                      << std::endl;

            // Send an update that the tempering station is processing lot
            ChocolateLotState updated_state(data_seq[i]);
            updated_state.lot_status = PROCESSING;
            updated_state.next_station = INVALID_CONTROLLER;
            updated_state.station = TEMPERING_CONTROLLER;
            DDS_ReturnCode_t retcode =
                    lot_state_writer->write(updated_state, DDS_HANDLE_NIL);
            if (retcode != DDS_RETCODE_OK) {
                std::cerr << "write error " << retcode << std::endl;
            }

            // "Processing" the lot.
            DDS_Duration_t processing_time = { 5, 0 };
            NDDSUtility::sleep(processing_time);

            // Exercise #3.1: Since this is the last step in processing,
            // notify the monitoring application that the lot is complete
            // using a dispose
        }
    } else {
        // Received instance state notification
        continue;
    }
}

4.5. Hands-On 3: Dispose the ChocolateLotState

The Tempering application will use the NOT_ALIVE_DISPOSED instance state to indicate that a chocolate lot has finished processing.

4.5.1. Add Code to Tempering Application to Dispose ChocolateLotState

Add a call to dispose() the ChocolateLotState data in the Tempering application, since it is the last step in the chocolate factory.

  1. In tempering_application.cxx, find the comment:

    // Exercise #3.1: Since this is the last step in processing,
    // notify the monitoring application that the lot is complete
    // using a dispose
    
  2. Add the following code after the comment to dispose the ChocolateLotState:

    retcode = lot_state_writer->dispose(
            updated_state,
            DDS_HANDLE_NIL);
    std::cout << "Lot completed" << std::endl;
    

4.5.2. Detect the Dispose in the Monitoring Application

Open monitoring_ctrl_application.cxx to look at the Monitoring/Control application. Note that it does two things right now:

  1. Kicks off processing chocolate lots, by sending the first update to the “ChocolateLotState” Topic. Since we are skipping all the ingredient station steps (for now) in this example, the Monitoring/Control application sends the lot directly to wait at the tempering machine, as you can see in the publish_start_lot function:

    void publish_start_lot(StartLotThreadData *thread_data)
    {
    
        ...
        // Set the values for a chocolate lot that is going to be sent to wait
        // at the tempering station
        sample.lot_id = count % 100;
        sample.lot_status = WAITING;
        sample.next_station = TEMPERING_CONTROLLER;
    
        std::cout << std::endl
                  << "Starting lot: " << std::endl
                  << "[lot_id: " << sample.lot_id << " next_station: ";
        print_station_kind(sample.next_station);
        std::cout << "]" << std::endl;
    
        // Send an update to station that there is a lot waiting for tempering
        DDS_ReturnCode_t retcode = writer->write(sample, DDS_HANDLE_NIL);
        ...
    }
    
  2. Monitors the lot state, and prints out the current state of the lots, as you can see in the monitor_lot_state function:

    // Process data. Returns number of samples processed.
    unsigned int monitor_lot_state(ChocolateLotStateDataReader *lot_state_reader)
    {
        ChocolateLotStateSeq data_seq;
        DDS_SampleInfoSeq info_seq;
        unsigned int samples_read = 0;
    
        // Take available data from DataReader's queue
        DDS_ReturnCode_t retcode = lot_state_reader->take(data_seq, info_seq);
        if (retcode != DDS_RETCODE_OK && retcode != DDS_RETCODE_NO_DATA) {
            std::cerr << "take error " << retcode << std::endl;
            return 0;
        }
    
        // Iterate over all available data
        for (int i = 0; i < data_seq.length(); ++i) {
            // Check if a sample is an instance lifecycle event
            if (info_seq[i].valid_data) {
                std::cout << "Received lot update:" << std::endl;
                application::print_chocolate_lot_data(&data_seq[i]);
                samples_read++;
            } else {
                // Exercise #3.2: Detect that a lot is complete by checking for
                // the disposed state.
            }
        }
        // Data sequence was loaned from middleware for performance.
        // Return loan when application is finished with data.
        retcode = lot_state_reader->return_loan(data_seq, info_seq);
        if (retcode != DDS_RETCODE_OK) {
            std::cerr << "return_loan error " << retcode << std::endl;
        }
    
        return samples_read;
    }
    

Add code to check the instance state and print the ID of the completed lot:

  1. In monitoring_ctrl_application.cxx, in the monitor_lot_state function, find the comment:

    // Exercise #3.2: Detect that a lot is complete by checking for
    // the disposed state.
    
  2. Add the following code after the comment:

    if (info_seq[i].instance_state ==
            DDS_NOT_ALIVE_DISPOSED_INSTANCE_STATE) {
        ChocolateLotState key_holder;
        // Fills in only the key field values associated with the
        // instance
        lot_state_reader->get_key_value(
                key_holder, info_seq[i].instance_handle);
        std::cout << "[lot_id: " << key_holder.lot_id
                  << " is completed]" << std::endl;
    }
    

    The code you just added:

    • Checks that the instance state was NOT_ALIVE_DISPOSED.
    • Creates a temporary ChocolateLotState object, and passes it to lot_state_reader->get_key_value() to get the value of the key fields associated with the instance handle.
    • Prints out the ID of the lot that is completed.
  3. Save your changes.

4.5.3. Recompile and Run the Applications

  1. Recompile the monitoring_ctrl_application and the tempering_application.

    From within the build directory:

    $ make
    

    In one command terminal, from within the build directory:

    $ make
    

    In one command prompt window, from within the build directory:

    1. In Visual Studio, right-click ALL_BUILD and choose Build. (See 2_hello_world\<language>\README_<architecture>.txt if you need help.)

    2. Since “Debug” is the default option at the top of Visual Studio, a Debug directory will be created with the compiled files.
  2. Run the applications:

    In one command terminal, from within the build directory:

    $ ./monitoring_ctrl_application
    

    In another terminal, from within the build directory:

    $ ./tempering_application -i 1
    

    In one command terminal, from within the build directory:

    $ ./monitoring_ctrl_application
    

    In another terminal, from within the build directory:

    $ ./tempering_application -i 1
    

    In one command prompt window, from within the build directory:

    > Debug\monitoring_ctrl_application.exe
    

    In another command prompt window, from within the build directory:

    > Debug\tempering_application.exe -i 1
    

You should see the following output from the Tempering application:

Waiting for lot
Processing lot #1
Lot completed
Waiting for lot
Waiting for lot
Processing lot #2
Lot completed

You should see the following output from the Monitoring/Control application:

Starting lot:
[lot_id: 1 next_station: TEMPERING_CONTROLLER]
Received lot update:
[lot_id: 1, station: INVALID_CONTROLLER, next_station: TEMPERING_CONTROLLER, lot_status: WAITING]
Received lot update:
[lot_id: 1, station: TEMPERING_CONTROLLER, next_station: INVALID_CONTROLLER, lot_status: PROCESSING]
[lot_id: 1 is completed]

Starting lot:
[lot_id: 2 next_station: TEMPERING_CONTROLLER]
Received lot update:
[lot_id: 2, station: INVALID_CONTROLLER, next_station: TEMPERING_CONTROLLER, lot_status: WAITING]
Received lot update:
[lot_id: 2, station: TEMPERING_CONTROLLER, next_station: INVALID_CONTROLLER, lot_status: PROCESSING]
[lot_id: 2 is completed]

Notice that when we first ran the Monitoring/Control application, we saw WAITING and PROCESSING statuses. Now we also see a “completed” status. The “completed” status occurs because we added the code to dispose the instance in the Tempering application and because we checked for the disposed status in the Monitoring/Control application.

4.6. Hands-On 4: Debugging the System and Completing the Application

This hands-on is not specific to using instances, but it will make you more familiar with how to create multiple DataReaders and DataWriters in an application. This will help you as you continue with upcoming modules, and the exercises become more complex.

4.6.1. Debug in Admin Console

So far, we have only used Admin Console for data visualization. Now we will use it for system debugging.

  1. Make sure your applications (monitoring_ctrl_application and tempering_application) are running.

  2. Like you did in Hands-On 2: Viewing Your Data, in the first module, open up the Admin Console tool.

  3. Click Unsubscribe if you haven’t already in Hands-On 1.

  4. Choose the Administration Perspective toolbar icon in the top right corner of the window.

    Administration Perspective in Admin Console

    You should see a Logical View pane in the upper left.

  5. Notice that one of your Topics has a warning:

    Select ChocolateTemperature in Logical View
  6. Click on the Topic with a warning, and you should see a visualization of that Topic in the main window:

    Topic Visualization in Admin Console
  7. Notice that the warning message is on the Topic and DataWriter. Hover your mouse over the writer (with the ac_writer_warning icon), and you should see this warning message:

    Writer Warning Message in Admin Console

    The reason for this warning is because there are no DataReaders reading the “ChocolateTemperature” Topic.

    Missing DataReader

    Figure 4.17 Using Admin Console, we identified that the DataReader for the “ChocolateTemperature” Topic is missing from the Monitoring/Control Application.

    If you look more carefully at the code in monitoring_ctrl_application.cxx, you’ll see that although the Monitoring/Control application reads and writes to the “ChocolateLotState” Topic, it never creates a DataReader to read the “ChocolateTemperature” Topic!

  8. Quit the applications.

4.6.2. Add the ChocolateTemperature DataReader

In this exercise, you will add the missing DataReader.

First, review the run_example function in monitoring_ctrl_application.cxx. Notice that it uses a single DomainParticipant to create a Publisher and a Subscriber. (See Publishers, Subscribers, and DomainParticipants.)

// Connext DDS Setup
// -----------------
// A DomainParticipant allows an application to begin communicating in
// a DDS domain. Typically there is one DomainParticipant per application.
// DomainParticipant QoS is configured in USER_QOS_PROFILES.xml
DDSDomainParticipant *participant =
        DDSTheParticipantFactory->create_participant(
                domain_id,
                DDS_PARTICIPANT_QOS_DEFAULT,
                NULL /* listener */,
                DDS_STATUS_MASK_NONE);
if (participant == NULL) {
    shutdown(participant, "create_participant error", EXIT_FAILURE);
}

...
// Create Publisher and DataWriter

// A Publisher allows an application to create one or more DataWriters
// Publisher QoS is configured in USER_QOS_PROFILES.xml
DDSPublisher *publisher = participant->create_publisher(
        DDS_PUBLISHER_QOS_DEFAULT,
        NULL /* listener */,
        DDS_STATUS_MASK_NONE);
if (publisher == NULL) {
    return shutdown(participant, "create_publisher error", EXIT_FAILURE);
}

...
// Create Subscriber and DataReaders

// A Subscriber allows an application to create one or more DataReaders
// Subscriber QoS is configured in USER_QOS_PROFILES.xml
DDSSubscriber *subscriber = participant->create_subscriber(
        DDS_SUBSCRIBER_QOS_DEFAULT,
        NULL /* listener */,
        DDS_STATUS_MASK_NONE);
if (subscriber == NULL) {
    shutdown(participant, "create_subscriber error", EXIT_FAILURE);
}

In the following steps, you’re going to add a second DataReader to the Monitoring/Control application that reads the ChocolateTemperature Topic. In Hands-On 2: Add a Second DataWriter (in Section 3), you used a single Publisher to create multiple DataWriters. In this example, you will use a single Subscriber to create multiple DataReaders.

To add a second DataReader to the Monitoring/Control application:

  1. Create a “ChocolateTemperature” Topic that the DataReader will be reading. (Right now, this application only has the “ChocolateLotState” Topic defined, so you must add a “ChocolateTemperature” Topic to be used by the new DataReader.)

    In monitoring_ctrl_application.cxx, find this comment in the code:

    // Exercise #4.1: Add a Topic for Temperature to this application
    

    Add the following code immediately after that comment to create a “ChocolateTemperature” Topic, which uses the Temperature data type:

    // Register the datatype to use when creating the Topic
    const char *temperature_type_name =
            TemperatureTypeSupport::get_type_name();
    retcode = TemperatureTypeSupport::register_type(
            participant,
            temperature_type_name);
    if (retcode != DDS_RETCODE_OK) {
        shutdown(participant, "register_type error", EXIT_FAILURE);
    }
    // A Topic has a name and a datatype. Create a Topic called
    // "ChocolateTemperature" with your registered data type
    DDSTopic *temperature_topic = participant->create_topic(
            CHOCOLATE_TEMPERATURE_TOPIC,
            temperature_type_name,
            DDS_TOPIC_QOS_DEFAULT,
            NULL /* listener */,
            DDS_STATUS_MASK_NONE);
    if (temperature_topic == NULL) {
        shutdown(participant, "create_topic error", EXIT_FAILURE);
    }
    

    Tip

    It is a best practice to use a variable defined inside the IDL file while creating the Topic instead of passing a string literal. Using a variable ensures that our Topic names are identical in our applications. (Recall that if they’re not identical, our DataWriter and DataReader won’t communicate.) It is convenient to use the IDL file to define the variable, since the IDL file is used by all of our applications. Here is what the Topic name looks like in the IDL file, which can be found in the 4_keys_instances directory:

    const string CHOCOLATE_TEMPERATURE_TOPIC = "ChocolateTemperature";
    

    Recall that one data type can be associated with several Topic names. Therefore, you may be defining more Topic names than data types in your IDL.

  2. Add a new DataReader.

    Now that we have a “ChocolateTemperature” Topic, we can add a new DataReader. Find this comment in the code:

    // Exercise #4.2: Add a DataReader for Temperature to this application
    

    Remember from Details of Receiving Data (in Section 2) that the StatusCondition defines a condition we can attach to a WaitSet. The WaitSet will wake when the StatusCondition becomes true.

    Add a new temperature DataReader and set up its StatusCondition by adding this code immediately after the comment:

    // This DataReader reads data of type Temperature on Topic
    // "ChocolateTemperature". DataReader QoS is configured in
    // USER_QOS_PROFILES.xml
    DDSDataReader *temperature_generic_reader = subscriber->create_datareader(
            temperature_topic,
            DDS_DATAREADER_QOS_DEFAULT,
            NULL,
            DDS_STATUS_MASK_NONE);
    if (temperature_generic_reader == NULL) {
        shutdown(participant, "create_datareader error", EXIT_FAILURE);
    }
    
    // Get status condition: Each entity has a Status Condition, which
    // gets triggered when a status becomes true
    DDSStatusCondition *temperature_status_condition =
            temperature_generic_reader->get_statuscondition();
    if (temperature_status_condition == NULL) {
       shutdown(participant, "get_statuscondition error", EXIT_FAILURE);
    }
    
    // Enable only the status we are interested in:
    //   DDS_DATA_AVAILABLE_STATUS
    retcode = temperature_status_condition->set_enabled_statuses(
            DDS_DATA_AVAILABLE_STATUS);
    if (retcode != DDS_RETCODE_OK) {
        shutdown(participant, "set_enabled_statuses error", EXIT_FAILURE);
    }
    
  3. Associate the new DataReader’s status condition with the WaitSet. Find this comment in the code:

    // Exercise #4.3: Add the new DataReader's StatusCondition to the Waitset
    

    Add this code below the comment:

    retcode = waitset.attach_condition(temperature_status_condition);
    if (retcode != DDS_RETCODE_OK) {
        shutdown(participant, "attach_condition error", EXIT_FAILURE);
    }
    
  4. Convert from a generic DataReader to a TemperatureDataReader. Find this comment in the code:

    // Exercise #4.4: Cast from a generic DataReader to a TemperatureDataReader
    

    Add this code below the comment:

    TemperatureDataReader *temperature_reader =
            TemperatureDataReader::narrow(temperature_generic_reader);
    if (temperature_reader == NULL) {
        shutdown(participant, "DataReader narrow error", EXIT_FAILURE);
    }
    
  5. Check if the Temperature DataReader received data. Find this comment in the code:

    // Exercise #4.5: Check if the temperature DataReader received
    // DATA_AVAILABLE event notification
    

    Add this code below the comment:

    triggeredmask = temperature_reader->get_status_changes();
    if (triggeredmask & DDS_DATA_AVAILABLE_STATUS) {
        monitor_temperature(temperature_reader);
    }
    
  6. Add a function that processes the temperature data and prints a message if it ever exceeds 32 degrees. Find this comment in the code:

    // Exercise #4.6: Add monitor_temperature function
    

    Add the following function to print out a message if the temperature exceeds 32 degrees immediately after that comment:

    void monitor_temperature(TemperatureDataReader *temperature_reader)
    {
        TemperatureSeq data_seq;
        DDS_SampleInfoSeq info_seq;
    
        // Take available data from DataReader's queue
        DDS_ReturnCode_t retcode = temperature_reader->take(data_seq, info_seq);
    
        if (retcode != DDS_RETCODE_OK && retcode != DDS_RETCODE_NO_DATA) {
            std::cerr << "take error " << retcode << std::endl;
            return;
        }
    
        // Iterate over all available data
        for (int i = 0; i < data_seq.length(); ++i) {
            // Check if a sample is an instance lifecycle event
            if (!info_seq[i].valid_data) {
                std::cout << "Received instance state notification" << std::endl;
                continue;
            }
            // Print data
            if (data_seq[i].degrees > 32) {
                std::cout << "Temperature high: ";
                TemperatureTypeSupport::print_data(&data_seq[i]);
            }
        }
        // Data sequence was loaned from middleware for performance.
        // Return loan when application is finished with data.
        temperature_reader->return_loan(data_seq, info_seq);
    
    }
    

4.6.3. Recompile and Run the Applications

Recompile and run the Tempering and Monitoring/Control applications again.

Notice that Admin Console no longer shows a warning, because the Monitoring/Control application now has a DataReader that is reading the “ChocolateTemperature” Topic that the Tempering application is writing.

Note

We’ve intentionally written the applications in a way that you won’t actually see temperature output printed to the console, to keep the code simpler, but we’ll change this in a later exercise.

Congratulations! In this module, you have learned to do the following:

  • Change an instance’s state to NOT_ALIVE_DISPOSED.
  • Get notified of the state change.
  • Debug your system using Admin Console.
  • Add a new DataReader to an application.

4.7. Next Steps

Next we will look at how to configure the behavior of your DataWriters and DataReaders using Quality of Service.