Methods vs Operations

RTT 2.0 has unified events, commands and methods in the Operation interface.

Purpose

To allow one component to provide a function and other components, located anywhere, to call it. This is often called 'offering a service'. Orocos component can offer many functions to any number of components.

Component interface

In Orocos, a C or C++ function is managed by the 'RTT::Operation' object. Click below to read the rest of this post.RTT 2.0 has unified events, commands and methods in the Operation interface.

Purpose

To allow one component to provide a function and other components, located anywhere, to call it. This is often called 'offering a service'. Orocos component can offer many functions to any number of components.

Component interface

In Orocos, a C or C++ function is managed by the 'RTT::Operation' object. So the first task is to create such an operation object for each function you want to provide.

This is how a function is added to the component interface:

  #include <rtt/Operation.hpp>;
  using namespace RTT;
 
  class MyTask
    : public RTT::TaskContext
  {
    public:
    string getType() const { return "SpecialTypeB" }
    // ...
 
    MyTask(std::string name)
      : RTT::TaskContext(name),
    {
       // Add the a C++ method to the operation interface:
       addOperation( "getType", &MyTask::getType, this )
                .doc("Read out the name of the system.");
     }
     // ...
  };
 
  MyTask mytask("ATask");

The writer of the component has written a function 'getType()' which returns a string that other components may need. In order to add this operation to the Component's interface, you use the TaskContext's addOperation function. This is a short-hand notation for:

       // Add the C++ method to the operation interface:
       provides()->addOperation( "getType", &MyTask::getType, this )
                .doc("Read out the name of the system.");

Meaning that we add 'getType()' to the component's main interface (also called 'this' interface). addOperation takes a number of parameters: the first one is always the name, the second one a pointer to the function and the third one is the pointer to the object of that function, in our case, MyTask itself. In case the function is a C function, the third parameter may be omitted.

If you don't want to polute the component's this interface, put the operation in a sub-service:

       // Add the C++ method objects to the operation interface:
       provides("type_interface")
            ->addOperation( "getType", &MyTask::getType, this )
                .doc("Read out the name of the system.");

The code above dynamically created a new service object 'type_interface' to which one operation was added: 'getType()'. This is similar to creating an object oriented interface with one function in it.

Calling an Operation in C++

Now another task wants to call this function. There are two ways to do this: from a script or in C++. This section explains how to do it in C++

Your code needs a few things before it can call a component's operation:

  • It needs to be a peer of instance 'ATask' of MyTask.
  • It needs to know the signature of the operation it wishes to call: string (void) (this is the function's declaration without the function's name).
  • It needs to know the name of the operation it wishes to call: "getType"

Combining these three givens, we must create an OperationCaller object that will manage our call to 'getType':

#include <rtt/OperationCaller.hpp>
//...
 
  // In some other component:
  TaskContext* a_task_ptr = getPeer("ATask");
 
  // create a OperationCaller<Signature> object 'getType':
  OperationCaller<string(void)> getType
       = a_task_ptr->getOperation("getType"); // lookup 'string getType(void)'
 
  // Call 'getType' of ATask:
  cout << getType() <<endl;

A lot of work for calling a function no ? The advantages you get are these:

  • ATask may be located on any computer, or in any process.
  • You didn't need to include the header of ATask, so it's very decoupled.
  • If ATask disappears, the OperationCaller object will let you know, instead of crashing your program.
  • The exposed operation is directly available from the scripting interface.

Calling Operations in scripts

In scripts, operations are accessed far more easier. The above C++ part is reduced to:

var string result = "";
set result = ATask.getType();

Tweaking Operation's Execution

In real-time applications, it is important to know which thread will execute which code. By default the caller's thread will execute the operation's function, but you can change this when adding the operation by specifying the ExecutionType:

       // Add the C++ method to the operation interface:
       // Execute function in component's thread:
       provides("type_interface")
            ->addOperation( "getType", &MyTask::getType, this, OwnThread )
                .doc("Read out the name of the system.");

So this causes that when getType() is called, it gets queued for execution in the ATask component, is executed by its ExecutionEngine, and when done, the caller will resume. The caller (ie the OperationCaller object) will not notice this change of execution path. It will wait for the getType function to complete and return the results.

Not blocking when calling operations

In the examples above, the caller always blocked until the operation returns the result. This is not mandatory. A caller can 'send' an operation execution to a component and collect the returned values later. This is done with the 'send' function:

// This first part is equal to the example above:
 
#include <rtt/OperationCaller.hpp>
//...
 
  // In some other component:
  TaskContext* a_task_ptr = getPeer("ATask");
 
  // create a OperationCaller<Signature> object 'getType':
  OperationCaller<string(void)> getType
       = a_task_ptr->getOperation("getType"); // lookup 'string getType(void)'
 
// Here it is different:
 
  // Send 'getType' to ATask:
  SendHandle<string(void)> sh = getType.send();
 
  // Collect the return value 'some time later':
  sh.collect();             // blocks until getType() completes
  cout << sh.retn() <<endl; // prints the return value of getType().

Other variations on the use of SendHandle are possible, for example polling for the result or retrieving more than one result if the arguments are passed by reference. See the Component Builder's Manual for more details.