4. Introduction to Keys and Instances¶
Prerequisites |
|
Time to complete | 1 hour |
Concepts covered in this module |
|
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.
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.
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.
State | How Change Occurs |
---|---|
Alive |
|
Not alive, disposed |
|
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.
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:
Let’s look at the “ChocolateLotState” DataWriters and DataReaders more closely:
Here’s a view of the “ChocolateLotState” DataWriters and DataReaders with samples:
(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.
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.
Open a command prompt.
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> ..
.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”.
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> ..
From within the
build
directory, compile the code:$ make
$ make
Open
rticonnextdds-getting-started-keys-instances.sln
in Visual Studio by enteringrticonnextdds-getting-started-keys-instances.sln
at the command prompt or using File > Open Project in Visual Studio.
Right-click
ALL_BUILD
and choose Build. (See2_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:
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
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
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
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
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
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¶
Make sure your two Tempering applications from the previous step are still running.
Like you did in Hands-On 2: Viewing Your Data, open up Admin Console and switch to the Data Visualization Perspective.
(If the Data Visualization Perspective is grayed out, you may already be in that view.)
Select the “ChocolateTemperature” Topic in the 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.
Subscribe to the “ChocolateTemperature” Topic.
Click the Subscribe button:
Then click OK:
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:
With instances, however, you see multiple rows in Admin Console:
Admin Console can show you one row per instance because it recognizes each instance as a different object.
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¶
In the previous hands-on, you ran multiple copies of the tempering application—quit them now if they’re still running.
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.
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.
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]
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.
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
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:
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); ... }
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:
In
monitoring_ctrl_application.cxx
, in themonitor_lot_state
function, find the comment:// Exercise #3.2: Detect that a lot is complete by checking for // the disposed state.
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.
Save your changes.
4.5.3. Recompile and Run the Applications¶
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:- In Visual Studio, right-click
ALL_BUILD
and choose Build. (See2_hello_world\<language>\README_<architecture>.txt
if you need help.)
- Since “Debug” is the default option at the top of Visual Studio, a Debug directory will be created with the compiled files.
- In Visual Studio, right-click
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.
Make sure your applications (monitoring_ctrl_application and tempering_application) are running.
Like you did in Hands-On 2: Viewing Your Data, in the first module, open up the Admin Console tool.
Click Unsubscribe if you haven’t already in Hands-On 1.
Choose the Administration Perspective toolbar icon in the top right corner of the window.
You should see a Logical View pane in the upper left.
Notice that one of your Topics has a warning:
Click on the Topic with a warning, and you should see a visualization of that Topic in the main window:
Notice that the warning message is on the Topic and DataWriter. Hover your mouse over the writer (with the icon), and you should see this warning message:
The reason for this warning is because there are no DataReaders reading the “ChocolateTemperature” Topic.
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!
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:
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.
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); }
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); }
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); }
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); }
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.