.. include:: vars.rst .. _section-gsg_intro_keys: Keys and Instances ****************** .. list-table:: :name: TableInstancesKeysPrereqs :widths: 20 80 :header-rows: 0 * - Prerequisites - .. only:: cpp98 or cpp11 * :ref:`section-gsg_intro_datatypes`, including: * Typed data * Interface Definition Language (IDL) * Introduction to data flows * Streaming data * Install CMake® 3.11 or higher from `cmake.org `__ .. only:: csharp or python * :ref:`section-gsg_intro_datatypes`, including: * Typed data * Interface Definition Language (IDL) * Introduction to data flows * Streaming data * Repository cloned from GitHub `here `_ * - 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 |DW|, 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| uses "instances" to represent these real-world objects. (See :numref:`TableInstancesKeysExamples`.) 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. .. tabularcolumns:: |\X{20}{120}|\X{30}{120}|\X{30}{120}|\X{40}{120}| .. list-table:: Examples of Instances and Keys in Distributed Systems :name: TableInstancesKeysExamples :widths: 20 40 30 10 :header-rows: 1 * - Instance - Key - Data Type - Samples * - Commercial flight being tracked - Airline name and flight number, such as: |br| Airline: "United Airlines" |br| Flight number: 901 |br| - .. code-block:: omg-idl @key string airline @key int16 flight_num float latitude float longitude - **UA, 901,** 37.7749, -122.4194 |br| **UA, 901,** 37.7748, -122.4195 * - Sensor sending data, such as an individual temperature sensor - Unique identifier of that sensor, such as: |br| "tempering-machine-1" or "FirstFloorSensor1" - .. code-block:: omg-idl @key string sensor_id int32 temperature - **tempering-machine-1,** temperature = 175 |br| **tempering-machine-1,** temperature = 176 * - Car being monitored - Vehicle identification number (VIN) of the car - .. code-block:: omg-idl @key string VIN float latitude float longitude - **JH4DA9370MS016526,** 37.7749, -122.4194 |br| **JH4DA9370MS016526,** 37.7748, -122.4195 * - Chocolate lot being processed in a factory - Chocolate lot identifier; likely an incrementing number that rolls over - .. code-block:: omg-idl @key uint32 lot_num LotStatusKind state - **1,** waiting for cocoa |br| **1,** waiting for sugar To specify one or more key fields, annotate them with "@key" in the IDL file. For example: .. code-block:: omg-idl // 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 int32 degrees; }; You add an ``@key`` annotation before each field that is part of the unique identifier of your instance. 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** |br| Creating a new instance is lighter-weight than creating a new |DW|/|DR|/*Topic*. For example, if you’re representing airline flights, you could create a new |DW|, |DR|, 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 |DWs| and |DRs| discover each other. (See :ref:`section-gsg_intro_discovery`.) |br| |br| - **Lifecycle** |br| 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 :ref:`section-gsg_instance_lifecycle` below. |br| |br| - **QoS** |br| 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 :ref:`section-gsg_intro_qos`. 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: .. code-block:: omg-idl // 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 int32 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-``. .. only:: cpp11 .. code-block:: C++ // Modify the data to be written here std::stringstream string_stream; // Specify the sensor instance sending the temperature. id is passed // at the command line. Each unique "TemperingMachine-" is a // unique instance. string_stream << "TemperingMachine-" << sensor_id; temperature.sensor_id(string_stream.str()); // Temperature value sent by sensor temperature.degrees(32); ... writer.write(temperature); .. only:: cpp98 .. code-block:: C++ // Modify the data to be written here // Specify the sensor instance sending the temperature. ID is passed at // the command line. Each unique "TemperingMachine-" is a // unique instance. snprintf(temperature.sensor_id, 255, "TemperingMachine-%s", sensor_id); temperature.degrees = 32; DDS_ReturnCode_t retcode = writer->write(temperature, DDS_HANDLE_NIL); .. only:: csharp .. code-block:: C# // Modify the data to be written here // Specify the sensor instance sending the temperature. ID is passed at // the command line. Each unique "TemperingMachine-" is a // unique instance. temperature.sensor_id = "TemperingMachine-" + sensorId; temperature.degrees = 32; writer.Write(temperature) .. only:: python .. code-block:: python # Modify the data to be written here # Specify the sensor instance sending the temperature. ID is passed at # the command line. Each unique "TemperingMachine-" is a # unique instance. temperature.sensor_id = f"TemperingMachine-{sensor_id}" temperature.degrees = 32; writer.write(temperature) Each time you pass a new ID parameter to the application, you have a new instance in your system. Any time a |DW| writes a sample with a unique value in the sample’s key fields, it is writing to a unique instance. A |DW| can write multiple instances just by setting the key fields to unique values. In our current example, each |DW| writes a single instance; however, as shown in :numref:`SamplesWithInstances`, the number of instances is not directly related to the number of |DWs|. One |DW| can write many instances. Or multiple |DWs| can write one or more instances. You have the flexibility to design your system however you want. Any |DW| 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 |DW|’s write method, the |DW| sends a sample. This is true whether you have instances in your system or not. .. figure:: static/keys/intro_samples_instances.png :scale: 50 % :alt: Samples Without Instances :name: SamplesWithoutInstances :align: center Samples without instances |br| |br| .. figure:: static/keys/intro_multiple_instances.png :scale: 50 % :alt: Samples With Instances :name: SamplesWithInstances :align: center Samples with instances 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 |DR|. 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 |DR| calls ``take()`` to retrieve data from the middleware, and then iterates over a collection of all the samples: .. only:: cpp98 .. code-block:: C++ 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++; } .. only:: cpp11 .. code-block:: C++ // Take all samples dds::sub::LoanedSamples samples = reader.take(); for (const auto& sample : samples) { if (sample.info().valid()) { count++; std::cout << sample.data() << std::endl; } } .. only:: csharp .. code-block:: C# // Take all samples using var samples = reader.Take(); foreach (var data in samples) { if (sample.Info.ValidData) { Console.WriteLine(sample.Data); } } .. only:: python .. code-block:: python # Take all samples samples = reader.take_data() for sample in samples: print(sample) .. only:: cpp11 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 ``select()`` call that can be used to retrieve samples for a particular instance. For all the options for accessing data, see :link_connext_dds_api_cpp2:`this section in the Modern C++ API Reference `.) .. only:: cpp98 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 :link_connext_dds_api_cpp:`this section in the Traditional C++ API Reference `.) .. only:: csharp 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 ``Select()`` call that can be used to retrieve samples for a particular instance.) .. only:: python In a system with instances, the ``take_data()`` call will return a sequence of data samples for all of the instances in the system. (There is also a ``select()`` call that can be used to retrieve samples for a particular instance.) .. only:: cpp98 or cpp11 or csharp Take a closer look at that code, and you will notice there is a line where we check that the sample is valid: .. only:: cpp98 .. code-block:: C++ if (!info_seq[i].valid_data) { .. only:: cpp11 .. code-block:: C++ if (sample.info().valid()) { .. only:: csharp .. code-block:: C# if (sample.Info.ValidData) You may be wondering: what does it mean for the sample to *not* contain valid data? The answer is: a sample can contain data or it can contain information about the instance's lifecycle. The ``data_available`` event notifies the |DR| about these updates in the same way, so the "valid" data flag indicates whether the update is a sample containing data or a sample containing an instance lifecycle update. .. _section-gsg_instance_lifecycle: Instance Lifecycle ------------------ An instance can have the following states, which are all part of the instance lifecycle: - **Alive**: A |DR| has received samples for the instance. (A |DW| has written the instance, and the |DW| is still in the system.) .. - **Not alive, disposed**: All |DRs| in the system have been notified via a |DW| API call 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**: The |DR| is no longer receiving samples for the instance. (Every |DW| 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 |DRs| that the instance has changed state, and you can decide what it means in your system. In the next section, :ref:`section-gsg_keys_chocolate_example`, we'll look at one way that you can use instance states in a chocolate factory example. .. only:: cpp98 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 :link_connext_dds_pro_um:`Instance States, in the RTI Connext Core Libraries User's Manual <#users_manual/AccessingManagingInstances.htm#Instance>` 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. .. only:: cpp11 All of the information about an instance's lifecycle is part of the **SampleInfo**, which you access using ``sample.info()``, the same way we do in the previous example code to check if the sample is valid. Take a look at :link_connext_dds_pro_um:`Instance States, in the RTI Connext Core Libraries User's Manual <#users_manual/AccessingManagingInstances.htm#Instance>` to see the state diagram describing the instance lifecycle. To review the state data that is specific to instances, you use the ``sample.info().state().instance_state()`` method—you can query this state to see if the instance is ALIVE, NOT_ALIVE_DISPOSED, or NOT_ALIVE_NO_WRITERS. .. only:: csharp All of the information about an instance's lifecycle is part of the **SampleInfo**, which you access using ``sample.Info``, the same way we do in the previous example code to check if the sample is valid. Take a look at :link_connext_dds_pro_um:`Instance States, in the RTI Connext Core Libraries User's Manual <#users_manual/AccessingManagingInstances.htm#Instance>` to see the state diagram describing the instance lifecycle. To review the state data that is specific to instances, you use the ``sample.Info.State.Instance`` property—you can query this state to see if the instance is ``Alive``, ``NotAliveDisposed``, or ``NotAliveNoWriters``. .. only:: python All of the information about an instance's lifecycle is part of the **SampleInfo**. To obtain the SampleInfo, you will call ``take()`` instead of ``take_data()`` as we've done so far: .. code-block:: python for data, info in reader.take(): ... The call to ``take()`` returns a list of tuples. The first element of each tuple is the data, and the second element is the info. Take a look at :link_connext_dds_pro_um:`Instance States, in the RTI Connext DDS Core Libraries User's Manual <#users_manual/AccessingManagingInstances.htm#Instance>` to see the state diagram describing the instance lifecycle. To review the state data that is specific to instances, you use the ``info.state.Instance`` property—you can query this state to see if the instance is ``dds.InstanceState.ALIVE``, ``dds.InstanceState.NOT_ALIVE_DISPOSED``, or ``dds.InstanceState.NOT_ALIVE_NO_WRITERS``. So, what is the typical lifecycle of an instance? An instance first becomes alive; this happens when a |DR| receives a sample for an instance for the first time. Then the instance may receive updates for some period of time from |DWs| 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. .. list-table:: Instance State Transitions :name: TableInstanceStatesHow :widths: 20 80 :header-rows: 1 * - State - How Change Occurs * - Alive - - Any time the instance is written * - Not alive, disposed - .. only:: cpp98 - Any single |DW| that has written this instance calls ``dispose()`` .. only:: cpp11 or python - Any single |DW| that has written this instance calls ``dispose_instance()`` .. only:: csharp - Any single |DW| that has written this instance calls ``DisposeInstance()`` * - Not alive, no writers - - All |DWs| that have written this instance have been shut down (or lost liveliness; see :link_connext_dds_pro_um:`LIVELINESS QosPolicy, in the RTI Connext Core Libraries User's Manual <#users_manual/LIVELINESS_QosPolicy.htm>`). .. .. only:: cpp98 or cpp11 or python - All |DWs| that have written this instance call ``unregister_instance()``. This API indicates that a |DW| is no longer updating a particular instance. For more information on this and the difference between “unregistered” and “disposed,” see :link_connext_dds_pro_um:`Managing Instances (Working with Keyed Data Types), in the RTI Connext Core Libraries User's Manual <#users_manual/Managing_Data_Instances__Working_with_Ke.htm>`. .. only:: csharp - All |DWs| that have written this instance call ``UnregisterInstance()``. This API indicates that a |DW| is no longer updating a particular instance. For more information on this and the difference between “unregistered” and “disposed,” see :link_connext_dds_pro_um:`Managing Instances (Working with Keyed Data Types), in the RTI Connext Core Libraries User's Manual <#users_manual/Managing_Data_Instances__Working_with_Ke.htm>`. .. _section-gsg_keys_chocolate_example: 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 :ref:`section-gsg_intro_datatypes`. 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. 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*. .. .. figure:: static/keys/factory_stations.png :scale: 50 % :alt: Chocolate Lot Stations :name: ChocolateLotStations :align: center There are three applications in this chocolate factory. We will illustrate only the Tempering Application and the Monitoring/Control Application for now. 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: .. code-block:: omg-idl 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). .. only:: cpp98 When a chocolate lot finishes, the tempering application disposes the instance with ``lot_status_writer.dispose()``. .. only:: cpp11 or python When a chocolate lot finishes, the tempering application disposes the instance with ``lot_status_writer.dispose_instance()``. .. only:: csharp When a chocolate lot finishes, the tempering application disposes the instance with ``lotStatusWriter.DisposeInstance()``. .. 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 :link_connext_dds_pro_um:`Instance Memory Management, in the RTI Connext DDS Core Libraries User's Manual <#users_manual/InstanceMemoryMgmt.htm>`. .. _section-gsg_keys_handson1: 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 |DR| or |DW|, as you can see below: .. figure:: static/keys/multiple_dwdr.png :scale: 50 % :alt: Multiple Readers and Writers :name: MultipleReadersWriters :align: center Our applications have more than one |DR| or |DW|. Let's look at the "ChocolateLotState" |DWs| and |DRs| more closely: .. figure:: static/keys/multiple_dwdr_half.png :scale: 50 % :alt: Multiple Readers and Writers - Cross-Application :name: MultipleReadersWritersCrossApp :align: center In our example, the |DW| in each application communicates with two |DRs|—one in its own application and one in another application Here's a view of the "ChocolateLotState" |DWs| and |DRs| with samples: .. figure:: static/keys/multiple_dwdr_anno.png :scale: 50 % :alt: Multiple Readers and Writers - Details :name: MultipleReadersWritersDetails :align: center Notice that both |DWs| are communicating with both |DRs| (Actually, the |DR| in the Tempering application only cares about the chocolate lot state if the next station is itself. Right now, the code tells that |DR| to ignore any next station state that isn't the Tempering application, but later we will use content filtering to do this instead.) .. note:: :numref:`MultipleReadersWritersCrossApp` and :numref:`MultipleReadersWritersDetails` demonstrate that it doesn't matter where the matching |DWs| and |DRs| are. *They could be in the same application or different applications.* As long as they match in *Topic* and Quality of Service (more on :ref:`section-gsg_intro_qos` later), they can communicate. .. only:: cpp98 or cpp11 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 :link_connext_dds_pro_pn:`RTI Connext Core Libraries Platform Notes <>`.) .. _section-gsg_keys_compile_apps: Build the Applications ---------------------- .. only:: cpp98 In this exercise, you’ll be working in the directory ``4_keys_instances/c++98``. (See :ref:`section-gsg_clone`.) We’ll start by building the applications using CMake. .. only:: cpp11 In this exercise, you’ll be working in the directory ``4_keys_instances/c++11``. (See :ref:`section-gsg_clone`.) We’ll start by building the applications using CMake. .. only:: csharp In this exercise, you’ll be working in the directory ``4_keys_instances/csharp``. This directory was created when you cloned the getting started repository from GitHub in the first module, in :ref:`section-gsg_clone`. Unlike the previous examples where we built a single application, this directory contains a solution (``KeysInstances.sln``) made of three C# projects: - ``MonitoringCtrlApplication/MonitoringCtrlApplication.csproj`` - ``TemperingApplication/TemperingApplication.csproj`` - ``ChocolateFactoryTypes/ChocolateFactoryTypes.csproj`` The first two build an application each, and reference the third one, which builds a library with the types we will generate from ``chocolate_factory.idl``. .. only:: python In this exercise, you’ll be working in the directory ``4_keys_instances/python``. (See :ref:`section-gsg_clone`.) .. only:: cpp98 or cpp11 .. tip:: You don't need to run ``rtisetenv_`` like you did in the previous modules because the CMake script finds the |CONNEXT| 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. |br| |br| #. Create build files using CMake. .. only:: cpp98 In the ``4_keys_instances/c++98`` directory, type the following, depending on your operating system: .. only:: cpp11 In the ``4_keys_instances/c++11`` directory, type the following, depending on your operating system: .. tabs:: .. group-tab:: Linux .. code-block:: console $ mkdir build $ cd build $ cmake .. Make sure "cmake" is in your path. If |CONNEXT| is not installed in the default location (see :ref:`section-gsg_paths`), specify ``cmake`` as follows: ``cmake -DCONNEXTDDS_DIR= ..``. .. group-tab:: macOS .. code-block:: console $ mkdir build $ cd build $ cmake .. Make sure "cmake" is in your path. If |CONNEXT| is not installed in the default location (see :ref:`section-gsg_paths`), specify ``cmake`` as follows: ``cmake -DCONNEXTDDS_DIR= ..``. .. group-tab:: Windows #. Enter the following commands: .. code-block:: doscon > 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". |br| |br| #. From within the ``build`` directory, create the build files: - If you have a 64-bit Windows® machine, add the option ``-A x64``. For example: .. code-block:: doscon > "c:\program files\CMake\bin\cmake" -G "Visual Studio 15 2017" -DCONNEXTDDS_ARCH=x64Win64VS2017 -A x64 .. - If you have a 32-bit Windows machine, add the option ``-A Win32``. For example: .. code-block:: doscon > "c:\program files\CMake\bin\cmake" -G "Visual Studio 15 2017" -DCONNEXTDDS_ARCH=i86Win32VS2017 -A Win32 .. .. note:: If you are using Visual Studio 2019, specify ``-G "Visual Studio 16 2019"``, but use 2017 in the architecture name: ``-DCONNEXTDDS_ARCH=x64Win64VS2017`` or ``-DCONNEXTDDS_ARCH=i86Win32VS2017``. - If |CONNEXT| is not installed in the default location (see :ref:`section-gsg_paths`), add the option ``-DCONNEXTDDS_DIR=``. For example: .. code-block:: doscon > "c:\program files\CMake\bin\cmake" -G "Visual Studio 15 2017" -DCONNEXTDDS_ARCH=x64Win64VS2017 -A x64 -DCONNEXTDDS_DIR= .. #. From within the ``build`` directory, compile the code: .. tabs:: .. group-tab:: Linux .. code-block:: console $ make .. group-tab:: macOS .. code-block:: console $ make .. group-tab:: Windows #. 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. |br| |br| #. Right-click ``ALL_BUILD`` and choose Build. (See ``2_hello_world\\README_.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. .. only:: csharp #. Run the Code Generator (*rtiddsgen*) to generate the C# code for the IDL file: .. tabs:: .. group-tab:: Linux .. code-block:: console $ cd 4_keys_instances $ rtiddsgen -language c# -d csharp/ChocolateFactoryTypes chocolate_factory.idl .. group-tab:: macOS .. code-block:: console $ cd 4_keys_instances $ rtiddsgen -language c# -d csharp/ChocolateFactoryTypes chocolate_factory.idl .. group-tab:: Windows .. code-block:: doscon > cd 4_keys_instances > rtiddsgen -language c# -d csharp/ChocolateFactoryTypes -ppDisable chocolate_factory.idl ``-ppDisable`` disables the preprocessor. It is necessary for running *rtiddsgen* on a Windows system if the preprocessor is not in your path. You can only use ``-ppDisable`` if your IDL is simple, as it is here—otherwise you must add the preprocessor to your path. See :link_codegen_um:`Command-Line Arguments for rtiddsgen, in the RTI Connext DDS Code Generator User's Manual <#code_generator/users_manual/CommandLineArgs.htm>` if you want more information. We're generating the code in the ``csharp/ChocolateFactoryTypes`` directory, which already contains a project file (``ChocolateFactoryTypes.csproj``) that will compile the types as a .NET library. For more information, see :ref:`section-gsg_intro_data_runcodegen` in the previous example. |br| |br| #. Configure the NuGet package source. In this exercise the ``rtiddsgen`` command we just ran only generates the type files, not the project files or ``NuGet.Config``. In order to have ``dotnet`` find the |CONNEXT| package, you have two options: - Copy the NuGet.Config file that you generated in the previous exercise from ``2_hello_world/csharp`` or ``3_streaming_data/csharp`` to ``4_keys_instances/csharp``, or |br| |br| - Configure NuGet globally by running the following command: .. code-block:: console $ dotnet nuget add source /lib/dotnet --name RTI If you don't configure the package source, the package will be fetched from `nuget.org `__ (which requires a license to run). |br| |br| #. Using the dotnet CLI from ``4_keys_instances/csharp``, enter: .. code-block:: console $ dotnet build These application projects are configured to build .NET 5 applications. If you want to build, for example, .NET Core 3.1 applications, edit ``MonitoringCtrlApplication.csproj`` and ``TemperingApplication.csproj`` and change ``net5`` to ``netcoreapp3.1``. .. only:: python Run the Code Generator (*rtiddsgen*) to generate the Python code for the IDL file: .. tabs:: .. group-tab:: Linux .. code-block:: console $ cd 4_keys_instances $ rtiddsgen -language python -d python chocolate_factory.idl .. group-tab:: macOS .. code-block:: console $ cd 4_keys_instances $ rtiddsgen -language python -d python chocolate_factory.idl .. group-tab:: Windows .. code-block:: doscon > cd 4_keys_instances > rtiddsgen -language python -d python -ppDisable chocolate_factory.idl ``-ppDisable`` disables the preprocessor. It is necessary for running *rtiddsgen* on a Windows system if the preprocessor is not in your path. You can only use ``-ppDisable`` if your IDL is simple, as it is here—otherwise you must add the preprocessor to your path. See :link_codegen_um:`Command-Line Arguments for rtiddsgen, in the RTI Connext DDS Code Generator User's Manual <#code_generator/users_manual/CommandLineArgs.htm>` if you want more information. For more information, see :ref:`section-gsg_intro_data_runcodegen` in the previous example. |br| |br| 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: .. only:: cpp98 or cpp11 .. tabs:: .. group-tab:: Linux #. Open a terminal window and, from within the ``build`` directory, run the Tempering application with a sensor ID as a parameter: .. code-block:: console $ ./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: .. code-block:: console $ ./tempering_application -i 2 .. group-tab:: macOS #. Open a terminal window and, from within the ``build`` directory, run the Tempering application with a sensor ID as a parameter: .. code-block:: console $ ./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: .. code-block:: console $ ./tempering_application -i 2 .. group-tab:: Windows #. Open a command prompt and, from within the ``build`` directory, run the Tempering application with a sensor ID as a parameter: .. code-block:: doscon > 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: .. code-block:: doscon > Debug\tempering_application.exe -i 2 .. only:: csharp #. Open a terminal window and run the Tempering application with a sensor ID as a parameter: .. code-block:: console $ dotnet run --project TemperingApplication -- --sensor-id 1 #. Open a second terminal window and run the Tempering application with a different sensor ID as a parameter: .. code-block:: console $ dotnet run --project TemperingApplication -- --sensor-id 2 .. note:: You should run from the ``4_keys_instances/csharp`` directory because the examples use Quality of Service (QoS) information from the file ``USER_QOS_PROFILES.xml`` in that directory. We'll talk more about QoS in a later module. Note that ``--project TemperingApplication`` tells dotnet to run the project in the ``TemperingApplication`` directory. Arguments to the application (``--sensor-id 2``) are specified after ``--``. .. only:: python #. Open a terminal window and run the Tempering application with a sensor ID as a parameter: .. code-block:: console $ python tempering_application.py --sensor-id 1 #. Open a second terminal window and run the Tempering application with a different sensor ID as a parameter: .. code-block:: console $ python tempering_application.py --sensor-id 2 .. note:: You should run from the ``4_keys_instances/python`` directory because the examples use Quality of Service (QoS) information from the file ``USER_QOS_PROFILES.xml`` in that directory. We'll talk more about QoS in a later module. You should see output similar to the following in both windows: .. only:: cpp98 .. code-block:: text 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 .. only:: cpp11 or csharp or python .. code-block:: text ChocolateTemperature Sensor with ID: 1 starting Waiting for lot Waiting for lot Waiting for lot Waiting for lot Waiting for lot Waiting for lot Waiting for lot Waiting for lot View the Data in Admin Console ------------------------------ #. Make sure your two Tempering applications from the previous step are still running. |br| |br| #. Like you did in :ref:`section-gsg_view_data`, open up *Admin Console* and switch to the Data Visualization Perspective. .. figure:: static/keys/ac_datavis_open.png :figwidth: 90 % :alt: Data Visualization Icon in Admin Console :name: DataVisualizationIcon :align: center (If the Data Visualization Perspective is grayed out, you may already be in that view.) |br| |br| #. Select the "ChocolateTemperature" *Topic* in the Logical View: .. figure:: static/keys/ac_choctemp.png :figwidth: 50 % :alt: Select ChocolateTemperature in Logical View :name: SelectChocolateTemperature :align: center .. note:: Don't worry about the warnings. We'll look at those in :ref:`section-keys-debug`. #. Subscribe to the "ChocolateTemperature" *Topic*. Click the Subscribe button: .. figure:: static/keys/ac_key_subscribe.png :figwidth: 50 % :alt: Subscribe in Admin Console :name: SubscribeAC :align: center Then click OK: .. figure:: static/keys/ac_key_ok.png :figwidth: 50 % :alt: Click OK in Admin Console :name: OKAC :align: center #. 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*: .. figure:: static/keys/ac_single_instance_anno.png :figwidth: 80 % :alt: Single Instance in Admin Console :name: SingleInstanceAC :align: center With instances, however, you see multiple rows in *Admin Console*: .. figure:: static/keys/ac_instances_anno.png :figwidth: 90 % :alt: Multiple Instances in Admin Console :name: MultipleInstancesAC :align: center 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. |br| |br| #. Click Unsubscribe. In :ref:`section-keys-debug`, we will debug your system using *Admin Console*. We do not want *Admin Console* subscribing to any data for that exercise. Hands-On 2: Run Both Applications ================================= 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. |br| |br| #. .. only:: cpp98 or cpp11 From any command prompt window, run the Monitoring/Control application, which you already built in :ref:`section-gsg_keys_compile_apps`: .. tabs:: .. group-tab:: Linux From within the ``build`` directory: .. code-block:: console $ ./monitoring_ctrl_application .. group-tab:: macOS From within the ``build`` directory: .. code-block:: console $ ./monitoring_ctrl_application .. group-tab:: Windows From within the ``build`` directory: .. code-block:: doscon > Debug\monitoring_ctrl_application.exe .. only:: csharp From any command prompt window, run the Monitoring/Control application, which you already built in :ref:`section-gsg_keys_compile_apps`: .. code-block:: console $ dotnet run --project MonitoringCtrlApplication .. only:: python From any command prompt window, run the Monitoring/Control application. .. code-block:: console $ python monitoring_ctrl_application.py You should see output like the following: .. only:: cpp98 or csharp .. code-block:: text 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] .. only:: cpp11 .. code-block:: text Starting lot: [lot_id: 0 next_station: StationKind::TEMPERING_CONTROLLER ] Starting lot: [lot_id: 1 next_station: StationKind::TEMPERING_CONTROLLER ] Received Lot Update: [lot_id: 1, station: StationKind::INVALID_CONTROLLER , next_station: StationKind::TEMPERING_CONTROLLER , lot_status: LotStatusKind::WAITING ] Starting lot: [lot_id: 2 next_station: StationKind::TEMPERING_CONTROLLER ] Received Lot Update: [lot_id: 2, station: StationKind::INVALID_CONTROLLER , next_station: StationKind::TEMPERING_CONTROLLER , lot_status: LotStatusKind::WAITING ] Starting lot: [lot_id: 3 next_station: StationKind::TEMPERING_CONTROLLER ] Received Lot Update: [lot_id: 3, station: StationKind::INVALID_CONTROLLER , next_station: StationKind::TEMPERING_CONTROLLER , lot_status: LotStatusKind::WAITING ] .. only:: python .. code-block:: text Starting lot: [lot_id: 0, next_station: StationKind.TEMPERING_CONTROLLER] Received lot update: ChocolateLotState(lot_id=0, station=, next_station=, lot_status=) Starting lot: [lot_id: 1, next_station: StationKind.TEMPERING_CONTROLLER] Received lot update: ChocolateLotState(lot_id=1, station=, next_station=, lot_status=) Starting lot: [lot_id: 2, next_station: StationKind.TEMPERING_CONTROLLER] Received lot update: ChocolateLotState(lot_id=2, station=, next_station=, lot_status=) 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"): .. only:: cpp98 .. code-block:: text Starting lot: [lot_id: 1 next_station: TEMPERING_CONTROLLER] .. only:: cpp11 or csharp .. code-block:: text Starting lot: [lot_id: 1 next_station: StationKind::TEMPERING_CONTROLLER] .. only:: python .. code-block:: text Starting lot: [lot_id: 1, next_station: StationKind.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"). .. only:: cpp98 .. code-block:: text Received lot update: [lot_id: 1, station: INVALID_CONTROLLER, next_station: TEMPERING_CONTROLLER, lot_status: WAITING] .. only:: cpp11 or csharp .. code-block:: text [lot_id: 1, station: StationKind::INVALID_CONTROLLER , next_station: StationKind::TEMPERING_CONTROLLER , lot_status: LotStatusKind::WAITING ] .. only:: python .. code-block:: text Received lot update: ChocolateLotState(lot_id=1, station=, next_station=, lot_status=) 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. |br| |br| #. In your other command prompt window, run the Tempering application with a sensor ID as a parameter: .. only:: cpp98 or cpp11 .. tabs:: .. group-tab:: Linux From within the ``build`` directory: .. code-block:: console $ ./tempering_application -i 1 .. group-tab:: macOS From within the ``build`` directory: .. code-block:: console $ ./tempering_application -i 1 .. group-tab:: Windows From within the ``build`` directory: .. code-block:: doscon > Debug\tempering_application.exe -i 1 .. only:: csharp .. code-block:: console $ dotnet run --project TemperingApplication -- --sensor-id 1 .. only:: python .. code-block:: console $ python tempering_application.py --sensor-id 1 You should see output like the following: .. only:: cpp98 .. code-block:: text 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 .. only:: cpp11 or csharp or python .. code-block:: text Waiting for lot ChocolateTemperature Sensor with ID: 1 starting Processing lot #16 Waiting for lot Waiting for lot Processing lot #17 Waiting for lot Waiting for lot Processing lot #18 .. note:: You may notice that the Monitoring/Control application starts with lot #0, but the Tempering application's |DR| 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 :ref:`section-gsg_intro_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: .. only:: cpp98 .. code-block:: text :emphasize-lines: 5,6,12,13 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] .. only:: cpp11 or csharp .. code-block:: text :emphasize-lines: 7,8,9,10,18,19,20,21 Starting lot: [lot_id: 16 next_station: StationKind::TEMPERING_CONTROLLER ] Received Lot Update: [lot_id: 16, station: StationKind::INVALID_CONTROLLER , next_station: StationKind::TEMPERING_CONTROLLER , lot_status: LotStatusKind::WAITING ] Received Lot Update: [lot_id: 16, station: StationKind::TEMPERING_CONTROLLER , next_station: StationKind::INVALID_CONTROLLER , lot_status: LotStatusKind::PROCESSING ] Starting lot: [lot_id: 17 next_station: StationKind::TEMPERING_CONTROLLER ] Received Lot Update: [lot_id: 17, station: StationKind::INVALID_CONTROLLER , next_station: StationKind::TEMPERING_CONTROLLER , lot_status: LotStatusKind::WAITING ] Received Lot Update: [lot_id: 17, station: StationKind::TEMPERING_CONTROLLER , next_station: StationKind::INVALID_CONTROLLER , lot_status: LotStatusKind::PROCESSING ] .. only:: python .. code-block:: text Starting lot: [lot_id: 10, next_station: StationKind.TEMPERING_CONTROLLER] Received lot update: ChocolateLotState(lot_id=10, station=, next_station=, lot_status=) Received lot update: ChocolateLotState(lot_id=10, station=, next_station=, lot_status=) Starting lot: [lot_id: 11, next_station: StationKind.TEMPERING_CONTROLLER] Received lot update: ChocolateLotState(lot_id=11, station=, next_station=, lot_status=) Received lot update: ChocolateLotState(lot_id=11, station=, next_station=, lot_status=) #. Quit both applications. Review the Tempering Application Code ------------------------------------- .. only:: cpp98 or cpp11 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 |DWs| and |DRs|. 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). .. only:: csharp 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 |DWs| and |DRs|. Let's start by examining the ``TemperingApplication.cs`` code. Some of this file should look familiar to you. This application is writing temperature data in the ``PublishTemperature`` method. 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). .. only:: python 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 |DWs| and |DRs|. Let's start by examining the ``tempering_application.py`` 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). .. only:: cpp98 .. code-block:: C++ 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); } .. only:: cpp11 .. code-block:: C++ void publish_temperature( dds::pub::DataWriter& writer, const std::string& sensor_id) { // Create temperature sample for writing Temperature temperature; while (!shutdown_requested) { // Modify the data to be written here temperature.sensor_id(sensor_id); temperature.degrees(rand() % 3 + 30); // Random value between 30 and 32 writer.write(temperature); rti::util::sleep(dds::core::Duration::from_millisecs(100)); } } .. only:: csharp .. code-block:: C# private void PublishTemperature( DataWriter writer, string sensorId) { // Create temperature sample for writing var temperature = new Temperature(); while (!shutdownRequested) { // Modify the data to be written here temperature.sensor_id = sensorId; temperature.degrees = rand.Next(30, 33); // Random value between 30 and 32 writer.Write(temperature); Thread.Sleep(100); } } .. only:: python .. code-block:: python def publish_temperature(writer, sensor_id): # Create temperature sample for writing temperature = Temperature() try: while True: # Modify the data to be written here temperature.sensor_id = sensor_id temperature.degrees = random.randint(30, 32) writer.write(temperature) time.sleep(0.1) except KeyboardInterrupt: pass .. only:: cpp98 or cpp11 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 |DW| and a |DR| instead of just a |DR|. This function calls ``take()`` to retrieve data from the |DR|’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. .. only:: csharp Now take a look at the ``ProcessLot`` method. This looks similar to the ``ProcessData`` methods you have seen in the previous hands-on exercises. However, this function takes both a |DW| and a |DR| instead of just a |DR|. This function calls ``Take()`` to retrieve data from the |DR|’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. .. only:: python 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 |DW| and a |DR| instead of just a |DR|. This function calls ``take_data()`` to retrieve data from the |DR|'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. .. only:: cpp98 .. code-block:: C++ // 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; } } .. only:: cpp11 .. code-block:: C++ // Take all samples. Samples are loaned to application, loan is // returned when LoanedSamples destructor called. dds::sub::LoanedSamples samples = lot_state_reader.take(); // Process lots waiting for tempering for (const auto& sample : samples) { if (sample.info().valid() && sample.data().next_station() == StationKind::TEMPERING_CONTROLLER) { std::cout << "Processing lot #" << sample.data().lot_id() << std::endl; // Send an update that the tempering station is processing lot ChocolateLotState updated_state(sample.data()); updated_state.lot_status(LotStatusKind::PROCESSING); updated_state.next_station(StationKind::INVALID_CONTROLLER); updated_state.station(StationKind::TEMPERING_CONTROLLER); lot_state_writer.write(updated_state); // "Processing" the lot. rti::util::sleep(dds::core::Duration(5)); // Exercise #3.1: Since this is the last step in processing, // notify the monitoring application that the lot is complete // using a dispose } } .. only:: csharp .. code-block:: C# // Take all samples. Samples are loaned to application, loan is // returned when LoanedSamples is Disposed. ValidData iterates only over // samples such that sample.Info.ValidData is true. using var samples = lotStateReader.Take(); foreach (var sample in samples.ValidData()) { if (sample.next_station == StationKind.TEMPERING_CONTROLLER) { Console.WriteLine("Processing lot #" + sample.lot_id); // Send an update that the tempering station is processing lot var updatedState = new ChocolateLotState(sample) { lot_status = LotStatusKind.PROCESSING, next_station = StationKind.INVALID_CONTROLLER, station = StationKind.TEMPERING_CONTROLLER }; lotStateWriter.Write(updatedState); // "Processing" the lot. Thread.Sleep(5000); // Exercise #3.1: Since this is the last step in processing, // notify the monitoring application that the lot is complete // using a dispose } } .. only:: python .. code-block:: python # Process lots waiting for tempering for data in lot_state_reader.take_data(): if data.next_station == StationKind.TEMPERING_CONTROLLER: print(f"Processing lot #{data.lot_id}") # Send an update that the tempering station is processing lot updated_state = ChocolateLotState( lot_id=data.lot_id, lot_status=LotStatusKind.PROCESSING, station=StationKind.TEMPERING_CONTROLLER, next_station=StationKind.INVALID_CONTROLLER) lot_state_writer.write(updated_state) # "Processing" the lot. time.sleep(5) # Exercise #3.1: Since this is the last step in processing, # notify the monitoring application that the lot is complete # using a dispose 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. Add Code to Tempering Application to Dispose ChocolateLotState -------------------------------------------------------------- .. only:: cpp98 Add a call to ``dispose()`` the ChocolateLotState data in the Tempering application, since it is the last step in the chocolate factory. .. only:: cpp11 or python Add a call to ``dispose_instance()`` for the ChocolateLotState data in the Tempering application, since it is the last step in the chocolate factory. .. only:: csharp Add a call to ``DisposeInstance()`` for the ChocolateLotState data in the Tempering application, since it is the last step in the chocolate factory. #. .. only:: cpp98 or cpp11 In ``tempering_application.cxx``, find the comment: .. code-block:: C++ // Exercise #3.1: Since this is the last step in processing, // notify the monitoring application that the lot is complete // using a dispose .. only:: csharp In ``TemperingApplication.cs``, find the comment: .. code-block:: C# // Exercise #3.1: Since this is the last step in processing, // notify the monitoring application that the lot is complete // using a dispose .. only:: python In ``tempering_application.py``, find the comment: .. code-block:: python # 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: .. only:: cpp98 .. code-block:: C++ retcode = lot_state_writer->dispose( updated_state, DDS_HANDLE_NIL); std::cout << "Lot completed" << std::endl; .. only:: cpp11 .. code-block:: C++ dds::core::InstanceHandle instance_handle = lot_state_writer.lookup_instance(updated_state); lot_state_writer.dispose_instance(instance_handle); std::cout << "Lot completed" << std::endl; .. only:: csharp .. code-block:: C# lotStateWriter.DisposeInstance( lotStateWriter.LookupInstance(updatedState)); Console.WriteLine("Lot completed"); .. only:: python .. code-block:: python lot_state_writer.dispose_instance( lot_state_writer.lookup_instance(updated_state)) print("Lot completed") .. _section-gsg_keys_dispose_code: Detect the Dispose in the Monitoring Application ------------------------------------------------- .. only:: cpp98 or cpp11 Open ``monitoring_ctrl_application.cxx`` to look at the Monitoring/Control application. Note that it does two things right now: .. only:: csharp Open ``MonitoringCtrlApplication.cs`` to look at the Monitoring/Control application. Note that it does two things right now: .. only:: python Open ``monitoring_ctrl_application.py`` 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: .. only:: cpp98 .. code-block:: C++ 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); ... } .. only:: cpp11 .. code-block:: C++ void publish_start_lot( dds::pub::DataWriter& writer, unsigned int& lots_to_process) { ... // 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(LotStatusKind::WAITING); sample.next_station(StationKind::TEMPERING_CONTROLLER); std::cout << std::endl << "Start lot with ID " << sample.lot_id() << " and next_station: " << sample.next_station() << std::endl; // Send an update to station that there is a lot waiting for tempering writer.write(sample); ... } .. only:: csharp .. code-block:: C# private void PublishStartLot( DataWriter writer, uint lotsToProcess) { ... sample.lot_id = count % 100; sample.lot_status = LotStatusKind.WAITING; sample.next_station = StationKind.TEMPERING_CONTROLLER; Console.WriteLine("Starting lot:"); Console.WriteLine($"[lot_id: {sample.lot_id} next_station: {sample.next_station}]"); writer.Write(sample); ... } .. only:: python .. code-block:: python def publish_start_lot(writer: dds.DataWriter, lots_to_process: int): sample = ChocolateLotState() try: for count in range(lots_to_process): # 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 = LotStatusKind.WAITING sample.next_station = StationKind.TEMPERING_CONTROLLER print("\nStarting lot:") print(f"[lot_id: {sample.lot_id}, next_station: {str(sample.next_station)}]") # Send an update to station that there is a lot waiting for tempering writer.write(sample) time.sleep(10) except KeyboardInterrupt: pass #. Monitors the lot state, and prints out the current state of the lots, as you can see in the ``monitor_lot_state`` function: .. only:: cpp98 .. code-block:: C++ // 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; } .. only:: cpp11 .. code-block:: C++ unsigned int monitor_lot_state(dds::sub::DataReader& reader) { // Take all samples. Samples are loaned to application, loan is // returned when LoanedSamples destructor called. unsigned int samples_read = 0; dds::sub::LoanedSamples samples = reader.take(); // Receive updates from stations about the state of current lots for (const auto& sample : samples) { if (sample.info().valid()) { std::cout << sample.data() << std::endl; samples_read++; } else { // Exercise #3.2: Detect that a lot is complete by checking for // the disposed state. } } return samples_read; } .. only:: csharp .. code-block:: C# private int MonitorLotState(DataReader reader) { int samplesRead = 0; using var samples = reader.Take(); foreach (var sample in samples) { Console.WriteLine("Received Lot Update: "); if (sample.Info.ValidData) { Console.WriteLine(sample.Data); samplesRead++; } else { // Exercise #3.2: Detect that a lot is complete by checking for // the disposed state. } } return samplesRead; } .. only:: python .. code-block:: python def monitor_lot_state(reader: dds.DataReader) -> int: # Receive updates from stations about the state of current lots samples_read = 0 # Exercise #3.2: Detect that a lot is complete by checking for # the disposed state. for data in reader.take_data(): print("Received lot update: ") print(data) samples_read += 1 return samples_read Add code to check the instance state and print the ID of the completed lot: .. only:: cpp98 or cpp11 or csharp #. .. only:: cpp98 or cpp11 In ``monitoring_ctrl_application.cxx``, in the ``monitor_lot_state`` function, find the comment: .. code-block:: C++ // Exercise #3.2: Detect that a lot is complete by checking for // the disposed state. .. only:: csharp In ``MonitoringCtrlApplication.cs``, in the ``MonitorLotState`` method, find the comment: .. code-block:: C# // Exercise #3.2: Detect that a lot is complete by checking for // the disposed state. #. Add the following code after the comment: .. only:: cpp98 .. code-block:: C++ 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; } .. only:: cpp11 .. code-block:: C++ if (sample.info().state().instance_state() == dds::sub::status::InstanceState::not_alive_disposed()) { ChocolateLotState key_holder; // Fills in only the key field values associated with the // instance reader.key_value(key_holder, sample.info().instance_handle()); std::cout << "[lot_id: " << key_holder.lot_id() << " is completed]" << std::endl; } .. only:: csharp .. code-block:: C# if (sample.Info.State.Instance == InstanceState.NotAliveDisposed) { // Create a sample to fill in the key values associated // with the instance var keyHolder = new ChocolateLotState(); reader.GetKeyValue(keyHolder, sample.Info.InstanceHandle); Console.WriteLine($"[lot_id: {keyHolder.lot_id} is completed]"); } .. only:: python #. Under ``Exercise #3.2``, modify the call to ``take_data()`` and the loop to also take the **SampleInfo**; the info object contains instante state information, among other meta-data. .. code-block:: python :emphasize-lines: 3,5 # Exercise #3.2: Detect that a lot is complete by checking for # the disposed state. for data, info in reader.take(): print("Received lot update:") if info.valid: print(data) samples_read += 1 Note that ``take()`` returns a collection of tuples. The first element is the data, as before, and the second element is the sample info. When you use ``take()``, some samples may not contain valid data, but only state updates. #. Add the following code: .. code-block:: python :emphasize-lines: 8-10 # Exercise #3.2: Detect that a lot is complete by checking for # the disposed state. for data, info in reader.take(): print("Received lot update:") if info.valid: print(data) samples_read += 1 elif info.state.instance_state == dds.InstanceState.NOT_ALIVE_DISPOSED: key_holder = reader.key_value(info.instance_handle) print(f"[Lot {key_holder.lot_id} is completed]") The code you just added: - Checks that the instance state was NOT_ALIVE_DISPOSED. - .. only:: cpp98 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. .. only:: cpp11 Creates a temporary ChocolateLotState object, and passes it to ``reader.key_value()`` to get the value of the key fields associated with the instance handle. .. only:: csharp Creates a temporary ChocolateLotState object, and passes it to ``reader.GetKeyValue()`` to get the value of the key fields associated with the instance handle. .. only:: python Calls ``reader.key_value()`` to get a ChocolateLotState object containing the values of the key fields associated with the instance handle. - Prints out the ID of the lot that is completed. Run the Applications -------------------- .. only:: cpp98 or cpp11 #. Recompile the **monitoring_ctrl_application** and the **tempering_application**. .. tabs:: .. group-tab:: Linux From within the ``build`` directory: .. code-block:: console $ make .. group-tab:: macOS In one command terminal, from within the ``build`` directory: .. code-block:: console $ make .. group-tab:: Windows In one command prompt window, from within the ``build`` directory: #. In Visual Studio, right-click ``ALL_BUILD`` and choose Build. (See ``2_hello_world\\README_.txt`` if you need help.) |br| |br| #. Since "Debug" is the default option at the top of Visual Studio, a Debug directory will be created with the compiled files. #. Run the applications: .. tabs:: .. group-tab:: Linux In one command terminal, from within the ``build`` directory: .. code-block:: console $ ./monitoring_ctrl_application In another terminal, from within the ``build`` directory: .. code-block:: console $ ./tempering_application -i 1 .. group-tab:: macOS In one command terminal, from within the ``build`` directory: .. code-block:: console $ ./monitoring_ctrl_application In another terminal, from within the ``build`` directory: .. code-block:: console $ ./tempering_application -i 1 .. group-tab:: Windows In one command prompt window, from within the ``build`` directory: .. code-block:: doscon > Debug\monitoring_ctrl_application.exe In another command prompt window, from within the ``build`` directory: .. code-block:: doscon > Debug\tempering_application.exe -i 1 .. only:: csharp In one command terminal: .. code-block:: console $ dotnet run --project MonitoringCtrlApplication In another terminal: .. code-block:: console $ dotnet run --project TemperingApplication -- --sensor-id 1 .. only:: python In one command terminal: .. code-block:: console $ python monitoring_ctrl_application.py In another terminal: .. code-block:: console $ python tempering_application.py --sensor-id 1 You should see the following output from the Tempering application: .. only:: cpp98 or csharp .. code-block:: text :emphasize-lines: 3,7 Waiting for lot Processing lot #1 Lot completed Waiting for lot Waiting for lot Processing lot #2 Lot completed .. only:: cpp11 or python .. code-block:: text :emphasize-lines: 3,6 Waiting for lot Processing lot #1 Lot completed Waiting for lot Processing lot #2 Lot completed You should see the following output from the Monitoring/Control application: .. only:: cpp98 .. code-block:: text :emphasize-lines: 7,15 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] .. only:: cpp11 or csharp .. code-block:: text :emphasize-lines: 11,12,24,25 Starting lot: [lot_id: 1 next_station: StationKind::TEMPERING_CONTROLLER ] Received Lot Update: [lot_id: 1, station: StationKind::INVALID_CONTROLLER , next_station: StationKind::TEMPERING_CONTROLLER , lot_status: LotStatusKind::WAITING ] Received Lot Update: [lot_id: 1, station: StationKind::TEMPERING_CONTROLLER , next_station: StationKind::INVALID_CONTROLLER , lot_status: LotStatusKind::PROCESSING ] Received Lot Update: [lot_id: 1 is completed] Starting lot: [lot_id: 2 next_station: StationKind::TEMPERING_CONTROLLER ] Received Lot Update: [lot_id: 2, station: StationKind::INVALID_CONTROLLER , next_station: StationKind::TEMPERING_CONTROLLER , lot_status: LotStatusKind::WAITING ] Received Lot Update: [lot_id: 2, station: StationKind::TEMPERING_CONTROLLER , next_station: StationKind::INVALID_CONTROLLER , lot_status: LotStatusKind::PROCESSING ] Received Lot Update: [lot_id: 2 is completed] .. only:: python .. code-block:: text :emphasize-lines: 7,8,16,17 Starting lot: [lot_id: 1, next_station: StationKind.TEMPERING_CONTROLLER] Received lot update: ChocolateLotState(lot_id=1, station=, next_station=, lot_status=) Received lot update: ChocolateLotState(lot_id=1, station=, next_station=, lot_status=) Received lot update: [Lot 1 is completed] Starting lot: [lot_id: 2, next_station: StationKind.TEMPERING_CONTROLLER] Received lot update: ChocolateLotState(lot_id=2, station=, next_station=, lot_status=) Received lot update: ChocolateLotState(lot_id=2, station=, next_station=, lot_status=) Received lot update: [Lot 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. .. _section-keys-debug: 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 |DRs| and |DWs| in an application. This will help you as you continue with upcoming modules, and the exercises become more complex. 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. |br| |br| #. Like you did in :ref:`section-gsg_view_data`, in the first module, open up the *Admin Console* tool. |br| |br| #. Click Unsubscribe if you haven't already in Hands-On 1. |br| |br| #. Choose the Administration Perspective toolbar icon in the top right corner of the window. .. figure:: static/keys/ac_admin_view_anno.png :figwidth: 40 % :alt: Administration Perspective in Admin Console :name: AdministrationPerspective :align: center You should see a Logical View pane in the upper left. |br| |br| #. Notice that one of your *Topics* has a warning: .. figure:: static/keys/ac_choctemp.png :figwidth: 50 % :alt: Select ChocolateTemperature in Logical View :name: SelectChocolateTemperature2 :align: center #. Click on the *Topic* with a warning, and you should see a visualization of that *Topic* and |DW| in the main window: .. figure:: static/keys/ac_topic_visualization.png :figwidth: 50 % :width: 454px :alt: Topic Visualization in Admin Console :name: TopicVisualization :align: center #. Hover your mouse over the writer (DW). You should see this warning message: .. figure:: static/keys/ac_warning_msg.png :figwidth: 40 % :width: 206px :alt: Writer Warning Message in Admin Console :name: WriterWarningMessage :align: center The reason for this warning is because there are no |DRs| reading the "ChocolateTemperature" *Topic*. .. figure:: static/keys/datareader_missing.png :scale: 50 % :alt: Missing DataReader :name: MissingDataReader :align: center Using Admin Console, we identified that the |DR| for the "ChocolateTemperature" Topic is missing from the Monitoring/Control Application. .. only:: cpp98 or cpp11 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 |DR| to read the "ChocolateTemperature" *Topic*! |br| |br| .. only:: csharp If you look more carefully at the code in ``MonitoringCtrlApplication.cs``, you'll see that although the Monitoring/Control application reads and writes to the "ChocolateLotState" *Topic*, it never creates a |DR| to read the "ChocolateTemperature" *Topic*! |br| |br| .. only:: python If you look more carefully at the code in ``monitoring_ctrl_application.py``, you'll see that although the Monitoring/Control application reads and writes to the "ChocolateLotState" *Topic*, it never creates a |DR| to read the "ChocolateTemperature" *Topic*! |br| |br| #. Quit the applications. Add the ChocolateTemperature DataReader --------------------------------------- In this exercise, you will add the missing |DR|. .. only:: cpp98 or cpp11 First, review the ``run_example`` function in ``monitoring_ctrl_application.cxx``. Notice that it uses a single |DP| to create a *Publisher* and a *Subscriber*. (See :ref:`section-gsg_pub_sub_dp`.) .. only:: csharp First, review the ``RunExample`` function in ``MonitoringCtrlApplication.cs``. Notice that it uses a single |DP| to create a *Publisher* and a *Subscriber*. (See :ref:`section-gsg_pub_sub_dp`.) .. only:: python First, review the ``run_example`` function in ``monitoring_ctrl_application.py``. Notice that it uses a single |DP| to create a *Publisher* and a *Subscriber*. (See :ref:`section-gsg_pub_sub_dp`.) .. only:: cpp98 .. code-block:: C++ // 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); } .. only:: cpp11 .. code-block:: C++ dds::domain::DomainParticipant participant(domain_id); ... // A Publisher allows an application to create one or more DataWriters // Publisher QoS is configured in USER_QOS_PROFILES.xml dds::pub::Publisher publisher(participant); ... // A Subscriber allows an application to create one or more DataReaders // Subscriber QoS is configured in USER_QOS_PROFILES.xml dds::sub::Subscriber subscriber(participant); .. only:: csharp .. code-block:: C# DomainParticipant participant = DomainParticipantFactory.Instance .CreateParticipant(domainId); ... // A Publisher allows an application to create one or more DataWriters // Publisher QoS is configured in USER_QOS_PROFILES.xml Publisher publisher = participant.CreatePublisher(); ... // A Subscriber allows an application to create one or more DataReaders // Subscriber QoS is configured in USER_QOS_PROFILES.xml Subscriber subscriber = participant.CreateSubscriber(); .. only:: python .. code-block:: python participant = dds.DomainParticipant(domain_id) ... # A Publisher allows an application to create one or more DataWriters # Publisher QoS is configured in USER_QOS_PROFILES.xml publisher = dds.Publisher(participant) ... # A Subscriber allows an application to create one or more DataReaders # Subscriber QoS is configured in USER_QOS_PROFILES.xml subscriber = dds.Subscriber(participant) In the following steps, you're going to add a second |DR| to the Monitoring/Control application that reads the ChocolateTemperature *Topic*. In :ref:`section-gsg_datatypes_2dw` (in :numref:`section-gsg_intro_datatypes`), you used a single *Publisher* to create multiple |DWs|. In this example, you will use a single *Subscriber* to create multiple |DRs|. To add a second |DR| to the Monitoring/Control application: .. only:: cpp98 #. Create a "ChocolateTemperature" *Topic* that the |DR| 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 |DR|.) In ``monitoring_ctrl_application.cxx``, find this comment in the code: .. code-block:: C++ // 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: .. code-block:: C++ // 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 |DW| and |DR| 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: .. code-block:: omg-idl 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 |DR|. Now that we have a "ChocolateTemperature" *Topic*, we can add a new |DR|. Find this comment in the code: .. code-block:: C++ // Exercise #4.2: Add a DataReader for Temperature to this application Remember from :ref:`section-gsg_intro_rcvdata` (in :numref:`section-gsg_intro_cpp11`) 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 |DR| and set up its StatusCondition by adding this code immediately after the comment: .. code-block:: C++ // 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 |DR|’s status condition with the WaitSet. Find this comment in the code: .. code-block:: C++ // Exercise #4.3: Add the new DataReader's StatusCondition to the Waitset Add this code below the comment: .. code-block:: C++ retcode = waitset.attach_condition(temperature_status_condition); if (retcode != DDS_RETCODE_OK) { shutdown(participant, "attach_condition error", EXIT_FAILURE); } #. Convert from a generic |DR| to a TemperatureDataReader. Find this comment in the code: .. code-block:: C++ // Exercise #4.4: Cast from a generic DataReader to a TemperatureDataReader Add this code below the comment: .. code-block:: C++ TemperatureDataReader *temperature_reader = TemperatureDataReader::narrow(temperature_generic_reader); if (temperature_reader == NULL) { shutdown(participant, "DataReader narrow error", EXIT_FAILURE); } #. Check if the Temperature |DR| received data. Find this comment in the code: .. code-block:: C++ // Exercise #4.5: Check if the temperature DataReader received // DATA_AVAILABLE event notification Add this code below the comment: .. code-block:: C++ 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: .. code-block:: C++ // 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: .. code-block:: C++ 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); } .. only:: cpp11 #. Create a "ChocolateTemperature" *Topic* that the |DR| 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 |DR|.) In ``monitoring_ctrl_application.cxx``, find this comment in the code: .. code-block:: C++ // 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: .. code-block:: C++ dds::topic::Topic temperature_topic( participant, CHOCOLATE_TEMPERATURE_TOPIC); .. 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 |DW| and |DR| 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: .. code-block:: omg-idl 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 |DR|. Now that we have a "ChocolateTemperature" *Topic*, we can add a new |DR|. Find this comment in the code: .. code-block:: C++ // Exercise #4.2: Add a DataReader for Temperature to this application Remember from :ref:`section-gsg_intro_rcvdata` (in :numref:`section-gsg_intro_cpp11`) 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 |DR| and set up its StatusCondition by adding this code immediately after the comment: .. code-block:: C++ dds::sub::DataReader temperature_reader( subscriber, temperature_topic); // Obtain the DataReader's Status Condition dds::core::cond::StatusCondition temperature_status_condition( temperature_reader); // Enable the 'data available' status. temperature_status_condition.enabled_statuses( dds::core::status::StatusMask::data_available()); // Associate a handler with the status condition. This will run when the // condition is triggered, in the context of the dispatch call (see below) temperature_status_condition.extensions().handler([&temperature_reader]() { monitor_temperature(temperature_reader); }); #. Associate the new |DR|’s status condition with the WaitSet. Find this comment in the code: .. code-block:: C++ // Exercise #4.3: Add the new DataReader's StatusCondition to the Waitset Add this line below the comment: .. code-block:: C++ waitset += temperature_status_condition; #. Add a function that processes the temperature data and prints a message if it ever exceeds 32 degrees. Find this comment in the code: .. code-block:: C++ // Exercise #4.4: Add monitor_temperature function Add the following function to print out a message if the temperature exceeds 32 degrees immediately after that comment: .. code-block:: C++ void monitor_temperature(dds::sub::DataReader& reader) { // Take all samples. Samples are loaned to application, loan is // returned when LoanedSamples destructor called. dds::sub::LoanedSamples samples = reader.take(); // Receive updates from tempering station about chocolate temperature. // Only an error if over 32 degrees Fahrenheit. for (const auto& sample : samples) { if (sample.info().valid()) { if (sample.data().degrees() > 32) { std::cout << "Temperature high: " << sample.data() << std::endl; } } } } .. only:: csharp #. Create a "ChocolateTemperature" *Topic* that the |DR| 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 |DR|.) In ``MonitoringCtrlApplication.cs``, find this comment in the code: .. code-block:: C# // 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: .. code-block:: C# Topic temperatureTopic = participant.CreateTopic( CHOCOLATE_TEMPERATURE_TOPIC.Value); .. 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 |DW| and |DR| 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: .. code-block:: omg-idl 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 |DR|. Now that we have a "ChocolateTemperature" *Topic*, we can add a new |DR|. Find this comment in the code: .. code-block:: C# // Exercise #4.2: Add a DataReader for Temperature to this application Remember from :ref:`section-gsg_intro_rcvdata` (in :numref:`section-gsg_intro_cpp11`) 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 |DR| and set up its StatusCondition by adding this code immediately after the comment: .. code-block:: C# DataReader temperatureReader = subscriber.CreateDataReader(temperatureTopic); // Obtain the DataReader's Status Condition StatusCondition temperatureStatusCondition = temperatureReader.StatusCondition; // Enable the 'data available' status. temperatureStatusCondition.EnabledStatuses = StatusMask.DataAvailable; // Associate a handler with the status condition. This will run when the // condition is triggered, in the context of the dispatch call (see below) temperatureStatusCondition.Triggered += _ => MonitorTemperature(temperatureReader); #. Associate the new |DR|’s status condition with the WaitSet. Find this comment in the code: .. code-block:: C# // Exercise #4.3: Add the new DataReader's StatusCondition to the Waitset Add this line below the comment: .. code-block:: C# waitset.AttachCondition(temperatureStatusCondition); #. Add a function that processes the temperature data and prints a message if it ever exceeds 32 degrees. Find this comment in the code: .. code-block:: C# // Exercise #4.4: Add monitor_temperature function Add the following function to print out a message if the temperature exceeds 32 degrees immediately after that comment: .. code-block:: C# private void MonitorTemperature(DataReader reader) { using var samples = reader.Take(); foreach (var data in samples.ValidData()) { // Receive updates from tempering station about chocolate temperature. // Only an error if over 32 degrees Fahrenheit. if (data.degrees > 32) { Console.WriteLine("Temperature high: " + data); } } } .. only:: python #. Create a "ChocolateTemperature" *Topic* that the |DR| 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 |DR|.) In ``monitoring_ctrl_application.py``, find this comment in the code: .. code-block:: python # 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: .. code-block:: python temperature_topic = dds.Topic( participant, CHOCOLATE_TEMPERATURE_TOPIC, Temperature) .. 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 |DW| and |DR| 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: .. code-block:: omg-idl 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 |DR| to monitor high temperature. Now that we have a "ChocolateTemperature" *Topic*, we can add a new |DR|. Find this comment in the code: .. code-block:: python # Exercise #4.2: Add a DataReader for Temperature to this application Remember from :ref:`section-gsg_intro_rcvdata` (in :numref:`section-gsg_intro_cpp11`) 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 |DR| and set up its StatusCondition so that it runs the function ``monitor_temperature``. Add this code after the comment: .. code-block:: python temperature_reader = dds.DataReader(subscriber, temperature_topic) # Obtain the DataReader's Status condition and enable the 'data available' # estatus. temperature_status_condition = dds.StatusCondition(temperature_reader) temperature_status_condition.enabled_statuses = dds.StatusMask.DATA_AVAILABLE # Define a function that processes the temperature data and prints a message # if it exceeds the 32 degrees. def monitor_temperature(_: dds.Condition): high_temperature = filter( lambda t: t.degrees > 32, temperature_reader.take_data()) for t in high_temperature: print(f"Temperature high: {t.degrees}") # Associate monitor_temperature to the status condition temperature_status_condition.set_handler(monitor_temperature) #. Associate the new |DR|’s status condition with the WaitSet. Find this comment in the code: .. code-block:: python # Exercise #4.3: Add the new DataReader's StatusCondition to the Waitset Add this line below the comment: .. code-block:: python waitset += temperature_status_condition .. _keys-recompile_run_apps-2: Run the Applications -------------------- .. only:: cpp11 or cpp98 or csharp Recompile and run the Tempering and Monitoring/Control applications again. .. only:: python 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 |DR| 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 |DR| to an application. Next Steps ========== Next we will look at how to configure the behavior of your |DWs| and |DRs| using Quality of Service.