MCA Logo
MCA Online Reference Documentation
Main Index

Chapter 1. Implementing Modules

Table of Contents
1.1. Modules with fix IOs
1.1.1. Module Interfaces
1.1.2. Methods Sense and Control
1.1.3. Parameter
1.2. Modules with variable IOs or variable parameters
1.3. Inherit from modules

All Methods in MCA are implemented in modules. These modules contain unified interfaces for input and output of values (IOs). These simple interfaces can be connected in a simple, fast and common way, so that outputs of some modules are transferred in inputs of other modules. Implementing methods in unified frames (modules) and connecting them using already existing mechanisms (edges) is the basic idea behind MCA. The simpler modules are designed the larger is the possibility to be able to reuse them in other contexts. We focused our efforts in order to increase reusability and clearness of all modules during the development of the basic parts of MCA.

The standard way to create a module is to define its IOs and methods that act on these IOs. In next versions we will distinguish between static and dynamic modules. Dynamic modules may then change there IOs while the system is executed. This feature is not implemented yet. Only static modules are supported so far. Static modules build their IOs in a construction phase before starting the execution loop. As modules are implemented as C++ classes this is in most cases done within the constructors. The simpliest modules have IOs with fix dimension. These modules are called fix modules. In some cases the dimensions of one ore more IOs depend on constructor parameters. Such modules are called variable modules. We will first describe how to build the more simple fix modules and then later come to the differences to variable modules.

1.1. Modules with fix IOs

A modules implementation consists of its definition in a header file (ModuleName.h) and its implementation in a corresponding source file (ModuleName.C). An empty standard frame for a new module can be created using the script newModule. It needs a base path and a module name as parameters. It creates the two necessary files with all usually used variables and methods of module.

1.1.1. Module Interfaces

Before implementing the methods of a module, the designer has to think about and define the necessary IOs of a module. Best way is to look at the new module as a blackbox and just define how this blockbox has to handle inputs and what outputs it has to generate. In MCA information is transported in two directions. The control direction transports data from user interface through modules down to actors. The sense direction transports data from sensors through modules up to user interface. Therefore each module has two methods: Control() and Sense(). Each Method has its own input and output interfaces, what results in a total number of four IOs for each module. The first step in creating a new module is to define these four interfaces:

controller input

Contains the predetermined desired values of the control method.

controller output

After a call of control() this interface contains the control values as result of the control method

sensor input

The underlying system can be seen as a closed loop controlled system. This interface contains the loop back sensor values.

sensor output

A module may infrom about its status, a control deviation or it may work as a sensor filter. These values are results and written to the sensor output interface when the sense function is called.

As a summary: the control method reads from controller input and writes to controller output. The sense function reads from sensor input and writes to sensor output.

In the following we will implement a module that implements direct and inverse kinematics of a simple 2 DOF robot arm. It gets x and y values as sollvorgaben (where the tool center point (TCP) of the robot has to move to) at its controller input interface and produces corresponding alpha and beta angle values for the robot joints. Actual (real) joint angle values are input for the direct kinematics and therefore read from sensor input in order to generate x,y koordinates of the robots actual TCP position, which are written to sensor output.

For a better readability of the source code we recommend to use enumerations in order to access IOs. These enumerations are defined in the modules header file as they are later also used when connecting different modules interfaces with edges.

After creating a module named Kinemtatic using the newModule script a file mKinemtaic.h exists. There we fill in our defined IOs into the corresponding enumerations:

Example 1-1. Define module IOs

class mKinematic:public tBaseModule
{
public:
  /*!
    Anonymous enumeration typ which contains the indices of the
    controller inputs.
   */
  _DESCR(mKinemtaic::ci_description,4,Natural,cDATA_VECTOR_END_MARKER)
  enum {
    eCI_X,        /*!< desired x-value for TCP */
    eCI_Y,        /*!< desired y-value for TCP */
    eCI_DIMENSION /*!< Endmarker and Dimension */
  };

  /*!
    Anonymous enumeration typ which contains the indices of the
    controller outputs.
   */
  _DESCR(mKinematic::co_description,4,Natural,cDATA_VECTOR_END_MARKER)
  enum {
    eCO_ALPHA,    /*!< needed angle value for first joint*/
    eCO_BETA,     /*!< needed angle value for second joint*/
    eCO_DIMENSION /*!< Endmarker and Dimension */
  };

  /*!
    Anonymous enumeration typ which contains the indices of the
    sensor inputs.
   */
  _DESCR(mKinematic::si_description,4,Natural,cDATA_VECTOR_END_MARKER)
  enum {
    eSI_ACTUAL_ALPHA,  /*!< messured angle value of first joint*/
    eSI_ACTUAL_BETA,   /*!< messured angle value of second joint*/
    eSI_DIMENSION /*!< Endmarker and Dimension */
  };

  /*!
    Anonymous enumeration typ which contains the indices of the
    sensor outputs.
   */
  _DESCR(mKinematic::so_description,4,Natural,cDATA_VECTOR_END_MARKER)
  enum {
    eSO_ACTUAL_X,       /*!< actual x coordinate of TCP */
    eSO_ACTUAL_Y,       /*!< actual y coordinate of TCP */
    eSO_DIMENSION /*!< Endmarker and Dimension */
  };

Enum values are only used in order to adress the different IOs. An MCA programm can later easily visualized with the tool MCAadmin. As this tool is able to display IO values it needs descriptions for every IO as then the user is able to identify and interpret the different IOs easier. These descriptions are generated automatically with the build_descr tool. The _DESCR-lines in your header file are parsed by this tool and describe how your enum definitions are converted into strings. The result of this automatic description generation is in this example:

#include "mKinematic.h"
const tDescription mKinematic::ci_description[]={
  "X",
  "Y",
  cDATA_VECTOR_END_MARKER};
const tDescription mKinematic::co_description[]={
  "Alpha",
  "Beta",
  cDATA_VECTOR_END_MARKER};
const tDescription mKinematic::si_description[]={
  "Actual Alpha",
  "Actual Beta",
  cDATA_VECTOR_END_MARKER};
const tDescription mKinematic::so_description[]={
  "Actual X",
  "Actual Y",
  cDATA_VECTOR_END_MARKER};

If a module does not use a specific IO, if e.g. there is no need for control IO as a module only needs a Sense function (e.g. a sensor filter), the corresponding enum and description entries rest empty.

1.1.2. Methods Sense and Control

After defining IOs the methods are implemented. Within a method IOs can be read and written using the functions ControllerInput(), ControllerOutput(), SensorInput() and SensorOutput(). They all take an enum value as parameter and return a reference to the corresponding floating point value.

The direct kinematic, which is implemented in the Sense() function will then look like this:

Example 1-2. Example of Sense() function

void mKinematic::Sense()
{
  if (SensorInputChanged())
  {
     ClearSensorInput();
     SensorOutput(eSO_ACTUAL_X)=
         length_a*cos(SensorInput(eSI_ACTUAL_ALPHA))
        +length_b*sin(SensorInput(eSI_ACTUAL_BETA));
     SensorOutput(eSO_ACTUAL_Y)=
         length_a*sin(SensorInput(eSI_ACTUAL_ALPHA))
        +length_b*cos(SensorInput(eSI_ACTUAL_BETA));
     SetSensorOutputChanged();
  }
  else
     ClearSensorOutput();
}

If sensor input values are not changed before Sense() is called again, there is no need to calculate the output values again. The function SensorInputChanged() returns true if one of the sensor input values changed, otherwise it return false. The sensor input internally uses a flag, that is set whenever a value changed. This flag can be cleared using the function ClearSensorInput(). The sensor output interface contains this flag too. Whenever an output value is changed within the Sense() function the sensor output must be informed about this change. Setting this flag is not done automatically as in most cases many values are written which only need one collective setting of the changed flag. This flag must of course be cleared, if the sensor output rests untouched.

Of course, the control IOs also own these changed flags and the corresponding functions. The inverse kinematic is more komplex but its implementation has the same structure as the direct kinemtaics:

Example 1-3. Exmaple of Control() function

void mKinematic::Control()
{
  if (ControllerInputChanged())
  {
     ClearControllerInput();
     ControllerOutput(eCO_ALPHA)=
         length_a*acos(ControllerInput(eCI_Y))
        +length_b*asin(ControllerInput(eSI_X));
     ControllerOutput(eCO_BETA)=
         length_a*asin(ControllerInput(eSI_Y))
        +length_b*acos(ControllerInput(eSI_X));
     SetControllerOutputChanged();
  }
  else
     ClearControllerOutput();
}

The kinematics example needs two constants in order to calculate the inverse and direct kinematics of the 2 DOF robot arm: The lengths of the arm segements. If multiple robot arms exist that only differ in these values, its better to store these in variables of the modules and set them during the initialization of the module using construcor parameters. As this values are don't change during execution of the main loop this is a practicable and effective way:

Example 1-4. module constructor

mKinematic::mKinematic(tParent *parent,float arm_length_a,float arm_length_b,
                       tDescription name,bool fixit)
 :tModuleBase(parent,name,
	       eSI_DIMENSION,eSO_DIMENSION,eCI_DIMENSION,eCO_DIMENSION,
	       si_description,so_description,ci_description,co_description,
	       fixit)
{
   length_a=arm_length_a;
   length_b=arm_length_b;
}

1.1.3. Parameter

Sometimes it is necessary to test different parameter sets in a module. E.g the kr,tn,tr1 and tr2 parameters of a PID controller are often changed and fine tuned when a system is put into operation.

In order to avoid restart or even recompile cycles, module internal parameters can be defined that are accessible through MCAadmin while the system is executed.

These parameters are defined in a similar way as the control and sense IOs. First enums have to be defined for all parameters in the corresponding header file. The example will show how parameters of an PID controller are handled:

Example 1-5. Parameter definition

  /*!
    Anonymous enumeration typ which contains the indices of the
    parameters.
  */
  enum {
    ePAR_KR, /*!< Kr-factor of the controller. */
    ePAR_TN, /*!< Tn-factor of the controller. */
    ePAR_TR1, /*!< Tr1-factor of the controller. */
    ePAR_TR2, /*!< Tr2-factor of the controller. */
    ePAR_DIMENSION /*!< Endmarker and Dimension */
  };

Within the constructor of our module, we create the parameters:

Example 1-6. Parameter initialization

 mPIDController::mPIDController(tParent *parent,tDescription name,bool fixit)
  :tModuleBase(parent,name,
               eSI_DIMENSION,eSO_DIMENSION,eCI_DIMENSION,eCO_DIMENSION,
	       si_description,so_description,ci_description,co_description,
	       fixit)
  {
     InitParameter(ePAR_DIMENSION,
                   new tParameterFloat("KR",0,1000,0),
                   new tParameterFloat("TN",0,1000,0),
                   new tParameterFloat("TR1",0,1000,0),
                   new tParameterFloat("TR2",0,1000,0));
  }

These Parameters may be changed from outside the controle program (in most cases using the MCAadmin tool). If a parameter of a module changed a flag is set and the Exception funtion is called with eET_ParameterChanged as parameter. Then internal variabels can be set accordingly:

Example 1-7. Exceptions


void tPIDController::Exception(tExceptionType type)
{
  switch (type)
    {
    case eET_ParameterChanged:

     a1=period+2*ParameterFloat(ePAR_TN))/(period+ParameterFloat(ePAR_TN)));
     a2=-ParameterFloat(ePAR_TN)/(period+ParameterFloat(ePAR_TN)));
     b0=(ParameterFloat(ePAR_KR)*(period+ParameterFloat(ePAR_TR1))
        *(period+ParameterFloat(ePAR_TR2)))
	 /(period+ParameterFloat(ePAR_TN));
     b1=-ParameterFloat(ePAR_KR)/
	   (period+ParameterFloat(ePAR_TN))
            *(((ParameterFloat(ePAR_TR1)+ParameterFloat(ePAR_TR2))*period)
	    +(2*ParameterFloat(ePAR_TR1)*ParameterFloat(ePAR_TR2)))));
     b2=(ParameterFloat(ePAR_KR)/
         (period+ParameterFloat(ePAR_TN)))*(ParameterFloat(ePAR_TR1)
	  *ParameterFloat(ePAR_TR2)));

     ClearParameter();
     break;
    case eET_PrepareForFirstSense:
     break;
    case eET_PrepareForFirstControl:
     break;
    case eET_PrepareForBreak:
     break;
    case eET_PrepareForRestart:
     break;
    case eET_SegFault:
     break;
    }
}

The variables a1,a2,b0,b1 and b2 are of course private variables of your controller class and are used within the control function in order to realise the control method.

Beside the used parameter type float there are also bool, int and string types available.

The int and float types need minimum and maximum values (as in the example above) beside the standard default value (last parameter in the parameters construtors). Whenever the parameter value is changed, new new value is bordered to these values. The bool type only has the two states true and false and therfore does not need such limits. The string type contains a character array with specified maximum length. New content is always shortent to this maximum length.

Note, that the parameters have also to be read with the corresponing functions: ParameterBool(), ParameterInt(), ParameterFloat() and ParameterString().

The ClearParameter() function clears the flag that indicates the parameter change state. If you don't clear this flag the Exeption function will be called again before the next call of the modules Sense() function.

In the example above all parameters where set to 0 as default. Of course they may also be set to other values. We recommend not to specify default parameter values using constructor parameters. Instead we propose to use the SetParameter functions from outside the class:

Example 1-8. parameter initialization

  mPIDController *pid_control=new mPIDController(this,"shoulder");
  pid_control->SetParameters(mPIDController::ePAR_DIMENSION,
                             mPIDController::ePAR_KR, float(1),
                             mPIDController::ePAR_TN, 2.3,
                             mPIDController::ePAR_TR1, 0.1
                             mPIDController::ePAR_TR2, 4.1);

The SetParameters function uses ellipses. It reads the corrsponding parameters type from the stack. This is the reason, why you have to be careful with bool, int and float values. One possibility is to write the dot in all float values and use "false" and "true" for boolean vaules. Or you specify the values explicitly as bool, int or float using the standard constructors of these types. If type conflicts occure during parameter settings, error messages are written to standard output.

Of course also individual parameter settings are possible. Here also bool, float and int values must be distinguished carefully.

  pid_control->SetParameter(mPIDController::ePAR_KR, 1.0);