Introducing AXCIOMA

Event Receiver

In our first introduction article we have created the familiar "Hello World" type of example. In this second article we are going to demonstrate how easy it is to make a component that receives events. For the realization of the event based interaction we are going to use the DDS based interaction support.

Prerequisites

In addition to the prerequisites mentioned in the first article this second article assumes some basic knowledge of DDS.

Definition

In this article we are going to use the well known DDS Shapes interoperability demo. We want to create a component that receives DDS Shapes and prints these to the console. In AXCIOMA terminology this means we are going to monitor the ShapeType. The component model for this example is shown in Figure 1.

Directory tree
Figure 1. Example component model

The step is to define the ShapeType datatype. In order to guarantee interoperability we have to make sure we define it exactly as the existing DDS Shapes example does. In IDL the type definitions look like this.

data/shapes.idl
typedef string<128> color_type;

@nested(FALSE)
struct ShapeType {
  color_type color; //@key
  long x;
  long y;
  long shapesize;
};

These IDL definitions are placed in the file shapes.idl which we store in a data subdirectory of our project folder according to "AXCIOMA Best Practices".

Directory tree
Figure 2. Example project directory tree

Figure 2 shows the complete directory tree for this example project.

Based on the defined datatype we can now go about creating the other parts of our implementation like the actual code for the datatype, the connector, and the component.

The first recipe to create is the project recipe placed in a file called aprc. It’s primary function is to mark the root of the project directory tree and to specify a number of project global settings. For this example project we create the following project recipe.

aprc
# common IDL include folder(s)
prj.idl_includes %w{ data }

This specified the data subdirectory as a common include directory for IDL compilation tasks.

Next thing we do is create a connector recipe for the implementation of our chosen event interaction pattern for the ShapeType datatype. The implementation of choice is a DDS4CCM connector so we create the following connector recipe in the file connectors/event_dds4ccm_conn.aprc.

connectors/event_dds4ccm_conn.aprc
connector 'shapes' do |conn|
  conn.idl %w{ shapes.idl }
  conn.port_type :dds4ccm do |tp|
    tp.topic 'ShapeType'
  end
end

This specifies a connector recipe named shapes which is to use (include) the IDL file shapes.idl (to be found by searching any defined IDL include paths like the one specified in the project recipe) to define a DDS4CCM connector for the topic ShapeType interface. The topic type definition must be defined in the scope of the included IDL file(s). In AXCIOMA this is all that is needed to create a completely implemented connector as you will see later on!

Going on we move to creating a component recipe for the component receiving ShapeType samples. This recipe will be placed in the file components/receiver/receiver.aprc.

components/receiver/receiver.aprc
component 'shapes_receiver' do |comp|
  comp.idl %w{ shapes.idl}
  comp.define 'receiver' do |intf|
    intf.port 'shape' do |p|
      p.monitors 'ShapeType'
    end
  end
end

This specifies a component recipe named shapes_receiver for a component using the IDL file shapes.idl. You may notice there is no mention of an IDL file defining the interface of the component. This is because BRIX11 APC recipes allow the definition of that interface in the recipe through the comp.define …​ construct. This recipe definition directs BRIX11 APC to generate an appropriate IDL file when needed. In this case that would be an IDL file named shapes_receiver.idl including a definition for a component receiver having a single uses port definition shape with extended port type ShapeTypeInterface::DDS_Listen. The generated IDL will look like this.

Generated shapes_receiver.idl
#include <Components.idl>
#include "shapesSE.idl"

component receiver
{
  port  ShapeTypeInterface::DDS_Listen shape;
};

After creation of the type IDL file and the BRIX11 APC recipes our project tree looks like the picture shown in Figure 3.

Directory tree
Figure 3. Example project directory tree

Notice that up to this moment we only defined ShapeType datatype in IDL and described the basic properties of our component and connector using fairly simple recipe directives.

In AXCIOMA that is all it takes to be able to start a new project because the BRIX11 APC commands are able to fill in the required blanks and generate all other information/files required for your project after which you will have only relatively little coding left to do to complete your components as you will see in the next section.

Preparation

Now that we have defined the datatype and how to organize our component and connector we can use BRIX11 APC to set up the details.

First we will have BRIX11 APC create a full set of project (build) files for our chosen development environment. In our case this means generating GNU makefiles to drive IDL and code compilation and linking but when using an AXCIOMA package for Microsoft Visual C++ on Windows BRIX11 APC will (by default) generate solution files. To execute this step we run the following command anywhere from the project directory tree (BRIX11 APC will always automatically determine the project root by looking up the project recipe file aprc).

$ brix11 apc prepare

When execution of this command finishes our project tree will contain all necessary project build files as shown in Figure 4.

Directory tree
Figure 4. Example project directory tree

As you can see the brix11 apc prepare command not only generated the project files though. In the component directory we now also find the IDL file defining the component as specified in the component recipe we created.

The generated project files take into account all the dependencies and build steps BRIX11 APC has been able to analyze from the IDL type definition files used as well as the directives of the collective recipes.

APC will generate also all needed dependencies and build steps for the necessary DDS based support. As user you don’t need to manually invoke the DDS vendor IDL compiler and compile all source files generated.

With the project files generated we are ready to start coding and building but instead of coding ourselves we will have the AXCIOMA tooling take care of a large part of that.

A large part of AXCIOMA application implementations is created through automatic code generation by the RIDL parser/generator included with AXCIOMA. The project files generated by BRIX11 APC have been set up to take full advantage of the possibilities of RIDL.

Executing the buildsteps defined by the project files will create (and build) a complete set of code files for the application including full datatype and connector implementations as well as skeleton implementations for the component. The executor code generated for the receiver component is furthermore provided with regeneration markers. The buildsteps configured for this code will regenerate this code when needed (when IDL changes) protecting any user code between regeneration markers. The generated skeleton code for the component provides complete yet empty implementations designed to be able to validate the structural dependencies within the project by building and deploying.

Using BRIX11 APC we do not even need to worry about the actual build tools to use for the environment we are working in. Executing the following command will have BRIX11 APC take care of running the correct tools to compile IDL and code files and build the resulting binaries.

$ brix11 apc build

As with the brix11 apc prepare command this command too can be executed from anywhere within the project directory tree.

Directory tree
Figure 5. Example project directory tree

When execution of this command finishes our project tree will be filled with a large collection of code files and build artifacts and (if everything went well) all runtime binaries (shared libraries) collected under the lib subdirectory as shown in Figure 5.

Most of the generated code can be found in separate subdirectories which (by default) are called generated_code (you will find these below the data, connectors and component directories). The code in these directories is to be considered transient. The AXCIOMA build commands configured in the generated project files will regenerate these files as needed based on IDL definitions and recipe directives disregarding any changes made (additionally the standard cleanup commands of the build tools will delete these files and directories).

The exception to this rule concerns the component executor code. This is the part where the user (you) will have to implement the required business logic. The files containing this code are generated (by default) in the directory holding the component recipes (and IDL files) with names derived from the recipe id appended with _exec.{cpp,h}. Figure 6 shows how this looks like for the receiver component directory.

Directory tree
Figure 6. shapes receiver component directory

The executor code itself is provided with regeneration markers at appropriate locations in the code for the user to add there own definitions and implementations in a way that safeguards these changes for regeneration of the executor code.

The following sample shows how this looks like for the declaration of the executor implementation class of the shape_data_listener_exec_i facet of the receiver component.

shape_data_listener executor implementation class
/// Executor implementation class for shape_data_listener facet
class shape_data_listener_exec_i final
  : public IDL::traits< ::ShapeTypeInterface::CCM_Listener>::base_type
{
public:

  //@@{__RIDL_REGEN_MARKER__} - BEGIN : receiver_Impl::shape_data_listener_exec_i[ctor]
  /// Constructor
  /// @param[in] context Component context
  shape_data_listener_exec_i (
      IDL::traits< CCM_receiver_Context>::ref_type context);
  //@@{__RIDL_REGEN_MARKER__} - END : receiver_Impl::shape_data_listener_exec_i[ctor]

  /// Destructor
  ~shape_data_listener_exec_i () override;

  /** @name Operations from ::ShapeTypeInterface::CCM_Listener */
  //@{

  void
  on_one_data (
      const ::ShapeType& datum,
      const ::CCM_DDS::ReadInfo& info) override;

  void
  on_many_data (
      const ::ShapeTypeSeq& data,
      const ::CCM_DDS::ReadInfoSeq& infos) override;
  //@}

  /** @name User defined public operations. */
  //@{
  //@@{__RIDL_REGEN_MARKER__} - BEGIN : receiver_Impl::shape_data_listener_exec_i[user_public_ops]
  // Your code here
  //@@{__RIDL_REGEN_MARKER__} - END : receiver_Impl::shape_data_listener_exec_i[user_public_ops]
  //@}

private:
  /// Context for component instance. Used for all middleware communication.
  IDL::traits< CCM_receiver_Context>::ref_type context_;

  /** @name User defined members. */
  //@{
  //@@{__RIDL_REGEN_MARKER__} - BEGIN : receiver_Impl::shape_data_listener_exec_i[user_members]
  // Your code here
  //@@{__RIDL_REGEN_MARKER__} - END : receiver_Impl::shape_data_listener_exec_i[user_members]
  //@}

  /** @name User defined private operations. */
  //@{
  //@@{__RIDL_REGEN_MARKER__} - BEGIN : receiver_Impl::shape_data_listener_exec_i[user_private_ops]
  // Your code here
  //@@{__RIDL_REGEN_MARKER__} - END : receiver_Impl::shape_data_listener_exec_i[user_private_ops]
  //@}
};

As mentioned earlier the built binary artifacts (shared libraries) could even in this basic implementation be deployed to check structural integrity and deployment setup. We are however not going to do that here but instead go right on to implementing the business logic for the component executors as shown in the next section.

Implementation

To make sure we can see this example actually doing something we will need to add some "business logic" to the component.

At the moment the DDS4CCM connector will receive data from DDS it will callback to the shape_data_listener_exec_i facet executor. Here we need to add code to the on_one_data method of the facet executor.

For this example we’ll add some logging to let an observer know that the method was called and showing the ShapeType received.

facet executor on_one_data method implementation (from components/receiver/shapes_receiver_exec.cpp)
/** Operations and attributes from shape_data_listener */
void
shape_data_listener_exec_i::on_one_data (
    const ::ShapeType& datum,
    const ::CCM_DDS::ReadInfo& info)
{
  //@@{__RIDL_REGEN_MARKER__} - BEGIN : receiver_Impl::shape_data_listener_exec_i::on_one_data[_datum_info]
  X11_UNUSED_ARG(info);
  CIAOX11_TEST_INFO << "Receiver, on_one_data " << IDL::traits< ShapeType>::write (datum) << std::endl;
  //@@{__RIDL_REGEN_MARKER__} - END : receiver_Impl::shape_data_listener_exec_i::on_one_data[_datum_info]
}

Our (simple) implementation replaced the default generated placeholder code which was originally between the regeneration markers and will now be preserved whenever the executor files are regenerated.

The CIAOX11_TEST_INFO logger stream is a special logstream provided for easy testing purposes. To use it we need to include a standard header from AXCIOMA. For this purpose there exists a regen section at the head of the shapes_receiver_exec.cpp where we insert the include as follows.

test logger include (from components/receiver/shapes_receiver_exec.cpp)
//@@{__RIDL_REGEN_MARKER__} - BEGIN : receiver_Impl[user_includes]
#include <ciaox11/testlib/ciaox11_testlog.h>
//@@{__RIDL_REGEN_MARKER__} - END : receiver_Impl[user_includes]

Again we simply replace the placeholder code between the markers to protect our include declaration against regeneration.

We do need to inform the DDS4CCM connector that we want to receive the DDS samples one by one. For this we need to add a little bit of code to the ccm_activate method of the receiver component. This is done through the DataListenerControl port of the connected DDS4CCM connector.

ccm_activate method (from components/receiver/shapes_receiver_exec.cpp)
void receiver_exec_i::ccm_activate ()
{
  //@@{__RIDL_REGEN_MARKER__} - BEGIN : receiver_Impl::receiver_exec_i[ccm_activate]
  IDL::traits< ::CCM_DDS::DataListenerControl>::ref_type lc =
    this->context_->get_connection_shape_data_control ();
  lc->mode (::CCM_DDS::ListenerMode::ONE_BY_ONE);
  //@@{__RIDL_REGEN_MARKER__} - END : receiver_Impl::receiver_exec_i[ccm_activate]
}

This is all the user supplied coding needed for this simple example to finish the receiver component. All the (default) code for supporting the AXCIOMA infrastructure has been generated and does not need any changes in this case.

Re-executing the brix11 apc build command will update the binary artifacts and than we are ready to deploy!

Deployment

For this simple example application we need a script to execute the deployment process as well as deployment plan to direct said process. We can easily derive these from the multitude of examples and tests included in the AXCIOMA distribution and place them in the descriptors subdirectory of the project tree.

In addition to the standard OMG D&C XML based deployment plans AXCIOMA also supports a custom, simpler and better humanly readable declaration format. We use this format to define a deployment plan according to our original model at the start of this text. The result is shown below.

descriptors/plan.config
# This plan deploys 1 node; Node1 for Receiver with its connector
# both with their respective connectors

#=====================================================
# Definitions for Node1
#-----------------------------------------------------

# ReceiverComponent instance
nl.remedy.it.CCM.Component ReceiverComponent shapes_receiver_exec create_receiver_Impl
    nl.remedy.it.DnCX11.ExecParameter nl.remedy.it.DnCX11.Servant.Artifact "shapes_receiver_svnt"
    nl.remedy.it.DnCX11.ExecParameter nl.remedy.it.DnCX11.Servant.Factory "create_receiver_Servant"
    nl.remedy.it.DnCX11.Node "Node1"

# DDS4CCM_Receiver_Connector instance
nl.remedy.it.CCM.Component DDS4CCM_Receiver_Connector shapes_dds_conn create_ShapeTypeInterface_DDS_Event_Impl
    nl.remedy.it.DnCX11.ExecParameter nl.remedy.it.DnCX11.Servant.Artifact "shapes_dds_conn"
    nl.remedy.it.DnCX11.ExecParameter nl.remedy.it.DnCX11.Servant.Factory "create_ShapeTypeInterface_DDS_Event_Servant"
    nl.remedy.it.DnCX11.Node "Node1"
    nl.remedy.it.DnCX11.ConfigProperty topic_name "Square"
    nl.remedy.it.DnCX11.ConfigProperty domain_id int32:0
    nl.remedy.it.DnCX11.Connection ReceiverComponent
      push_consumer_data_control > shape_data_control
      push_consumer_data_listener < shape_data_listener

As you can see this deployment plan describes the deployment of the ReceiverComponent component (a single instance thereof) on the first node (Node1) together with an instance of the DDS4CCM event connector for the ShapeType datatype. Between the DDS4CCM event connector and the ReceiverComponent we connect two ports. The DDS4CCM connector is going to use the topic Square and use the DDS domain 0.

In order to publish some DDS ShapeType samples we have downloaded a DDS Shapes UI from one of the DDS vendors website. As first step we start this application and let it publish Square Shapes.

We copied (and adapted it for one node) the test execution scripts from our previous example into descriptors/run_test.pl. Executing this script deploys the application according to the deployment plan described above, provides it some time (seconds) to perform its function and then shuts the deployment down (controlled). Executing the following command from within the descriptors directory will run the script.

$ brix11 run test

The output should look like this.

BRIX11 - > perl run_test.pl
Starting Naming Service with  -ORBEndpoint iiop://10.4.0.153:60003 -o ns.ior
Invoking node daemon
Run dancex11_deployment_manager with --handler dancex11_node_dm_handler -p 60001 -N -n Node1=Node1.ior --deployment-nc corbaloc:iiop:10.4.0.153:60003/NameService
Invoking domain deployment manager (dancex11_deployment_manager --handler dancex11_domain_dm_handler) with -l plan.config
[LP_INFO]    - 12:03:28.573809 - Receiver, on_one_data ShapeType{color=BLUE,x=45,y=65,shapesize=30}
[LP_INFO]    - 12:03:28.624279 - Receiver, on_one_data ShapeType{color=BLUE,x=47,y=67,shapesize=30}
[LP_INFO]    - 12:03:28.675087 - Receiver, on_one_data ShapeType{color=BLUE,x=49,y=69,shapesize=30}
[LP_INFO]    - 12:03:28.725843 - Receiver, on_one_data ShapeType{color=BLUE,x=51,y=71,shapesize=30}
[LP_INFO]    - 12:03:28.776768 - Receiver, on_one_data ShapeType{color=BLUE,x=53,y=72,shapesize=30}
[LP_INFO]    - 12:03:28.827595 - Receiver, on_one_data ShapeType{color=BLUE,x=54,y=74,shapesize=30}
[LP_INFO]    - 12:03:28.878118 - Receiver, on_one_data ShapeType{color=BLUE,x=56,y=76,shapesize=30}
[LP_INFO]    - 12:03:28.928413 - Receiver, on_one_data ShapeType{color=BLUE,x=58,y=78,shapesize=30}
[LP_INFO]    - 12:03:28.978768 - Receiver, on_one_data ShapeType{color=BLUE,x=60,y=80,shapesize=30}
[LP_INFO]    - 12:03:29.029151 - Receiver, on_one_data ShapeType{color=BLUE,x=62,y=82,shapesize=30}

You have now designed, implemented and run your first AXCIOMA application that uses the DDS based event interaction support.

All code for this example can be found at GitHub.