Skip to content

cisstMultiTask concepts

Anton Deguet edited this page Apr 22, 2014 · 19 revisions

1 Introduction

This is an introduction to the features of cisstMultiTask, which provides the component-based framework for the cisst package. It replaces the outdated [http://unittest.lcsr.jhu.edu/cisst/downloads/cisst/current/doc/latex/multiTask-quickstart.pdf cisstMultiTask Quick Start]. The code and examples are available as part of the cisst SVN repository https://svn.lcsr.jhu.edu/cisst/trunk. The library headers are in cisst/cisstMultiTask and the code can be found in cisst/cisstMultiTask/code. Examples can be found on cisst/cisstMultiTask/examples and unit tests are in cisst/cisstMultiTask/tests. To compile your own code, remember to include cisstMultiTask.h (which includes all public header files) or the required set of individual files (e.g., cisstMultiTask/mtsXXXX.h). Per convention, all the symbols starting with mts are defined in cisstMultiTask. Symbols prefixed with cmn, osa and vct come from cisstCommon, cisstOSAbstraction and cisstVector, respectively.

1.1 Component Model

cisstMultiTask component

A cisstMultiTask component can include "provided interfaces", "required interfaces", "input interfaces", and "output interfaces". The base component class mtsComponent provides all the base implementation to add interfaces but doesn't manage any thread nor thread safety mechanism. If a thread is needed for a given component, this component should be derived from one of mtsTask derived class (mtsTaskContinuous, mtsTaskPeriodic, mtsTaskFromSignal, mtsTaskFromCallback, ...). Libraries based on cisstMultiTask can also define their own base component type (e.g. cisstStereovision filters are components derived from svlFilterBase). Internally, a component may contain "state tables" to manage important component data. The component may have a state associated with it (e.g., initialized, paused, started). Note that every component contains at least one "provided interface" -- this is an internal interface that is connected to the "component manager".

1.2 Components In Use

cisstMultiTask components in use

The above figure shows a hypothetical deployment of components for visual servo control of a robot. A video stream (bottom) captures stereo images; it includes a Find Target filter that locates the target for visual servoing. The coordinates of this target are made available to the Visual Servo control component (a periodic task) via a provided interface. The Visual Servo component also relies on (has a Required interface for) a lower-level Joint Servo controller. The App component provides the overall application control (e.g., turning visual servo on/off) and can overlay information on the video output by invoking commands in the Provided interface of the Overlay filter.

An important point to consider is how to achieve "plug & play" capabilities within the system. For example, it is desirable for this application to work for any robot that can provide a low-level Joint Servo controller. This requires some standardization -- in the cisst library, we have chosen to standardize at the command level; specifically, by standardizing the name of a command (a string) and its payload (a data type). For example, the command to retrieve the joint positions should be named "!GetPositionJoint" and it should return the joint positions in an instance of the prmPositionJointGet class (see cisstParameterTypes library). The cisst library does not enforce this standard, however, so users can create components with any command names and data types, but will lose the benefits of "plug & play".

1.3 Provided and Required Interfaces

cisstMultiTask interfaces

All components can have multiple ''provided interfaces'' (mtsInterfaceProvided) and ''required interfaces'' (mtsInterfaceRequired). A component that contains only provided interfaces can be called a ''Server'', whereas a component that contains only required interfaces can be called a ''Client''. Of course, the more typical case is that a component will have both provided and required interfaces.

Each provided interface can have multiple command objects which encapsulate the available services, as well as event generators that broadcast events with or without payloads. Several command object classes are defined to handle commands with no parameters, one input parameter, one output parameter, or one of each, as described [#Commands below]. Provided interfaces are created and added to a component using AddInterfaceProvided("name"). Once created, they can be populated with commands and events.

Each required interface has multiple function objects that are bound to command objects to use the services that the connected command objects provide. It may also have event receivers or handlers to respond to events generated by the connected component. As with the command objects, several corresponding function object classes are defined. When two interfaces are connected to each other, all function objects in the required interface are bound to the corresponding command objects in the provided interface, and event handlers/receivers in the required interface become observers of the events generated by the provided interface. Required interfaces are created and added to a component using AddInterfaceRequired("name"). Once created, they can be populated with functions and event handlers/receivers.

It is important to note that during the connection between a required and a provided interface the following rules apply:

  • Required interfaces don't have to use all the commands of the provided interfaces
  • Required interfaces don't have to provide event handlers or receivers for all the events of the provided interface
  • All event handlers added to a required interface must match an event of the provided interface
  • Functions added to the required interface can be marked as "optional" to permit a connection even if the provided interface doesn't provide a matching command. All functions marked as "required" must match a provided command.

Interfaces automatically create message queues when needed to ensure thread safety (for task components) for both commands and events. This behavior can be overloaded at the interface or command/event level if the user has a more efficient way to enforce thread safety.

1.4 Output and Input Interfaces

All components can have multiple ''output interfaces'' (mtsInterfaceOutput). An output interface provides an output data port (e.g., a video source). It is created and added to a component using AddInterfaceOutput("name").

All components can have multiple ''input interfaces'' (mtsInterfaceOutput). An input interface is used to accept data, such as a video image. It is created and added to component using AddInterfaceInput("name").

For further details, see the [wiki:cisstStereoVisionTutorial cisstStereoVision Tutorial].

1.5 Technical concepts

To provide a flexible programming interface (API), the cisstMultiTask library uses the “command pattern”. In this design, an object API is not defined by its public methods but rather by a list of pointers on methods. The list of methods pointers (commands) can be defined and queried at run-time which provides much more flexibility than compile-time binding. The matching between commands is performed using a string compare.

Commands are grouped in "provided interfaces" corresponding to the list of provided functionalities. A component can have multiple provided interfaces to group its functionalities. In other words, a '''component''' can have multiple '''provided interfaces''' and each '''provided interface''' can have multiple '''commands'''. To use the provided functionalities of a given component (similar to a "server"), one needs to create a "client" component with a list of required "functions" grouped in a "required interface" . In other words a '''component''' can have multiple '''required interfaces''' and each '''required interface''' can have multiple '''functions'''. For the "client" component to use the "server" features, its required interface needs to be connected to the "server"'s provided interface.

When the two interfaces get connected, the "client" required functions get bound to the "server" provided commands based on their names (strings). At runtime, a call to the required function causes an invocation of the provided command and ultimately runs the underlying method. Once the initial matching based on strings is performed, all calls within a single process rely on method pointers and are therefore quite efficient.

To allow some communication from the "server"'s provided interface to the "client"'s required interface, cisstMultiTask supports events. Each required interface can contain a list of events to observe and for each observed event must provide an event handler or an event receiver.

In this introduction, we used the terms "client" and "server". These terms can be used to illustrate a single connection between "componentA::!InterfaceRequired" and "componentB::!InterfaceProvided". Since cisstMultiTask allows any component to have both required and provided interfaces, the reader should know that a component can be either "client" or "server" depending on which interface is being used.

The main classes of the cisstMultiTask library are:

  • Component: mtsComponent, mtsTask, mtsTaskContinuous, mtsTaskFromSignal, mtsTaskFromCallback, mtsTaskPeriodic, ...
  • Interfaces: mtsInterfaceProvided and mtsInterfaceRequired
  • Commands: mtsCommandVoid, mtsCommandWrite, mtsCommandRead, mtsCommandQualifiedRead
  • Events
  • State tables: mtsStateTable
  • Component manager

2 Components and tasks

As mentioned in the introduction, cisstMultiTask provides different base components are provided and can be used as a base class depending on the user's application:

  • mtsComponent: this is the base class of all components. It provides all the mechanisms to create interfaces but doesn't handle any threading. It doesn't own a thread nor use thread-safe mechanisms by default.
  • mtsTask: this class is not intended to be used directly. It assumes a thread will be used for all derived components and adds mechanisms to ensure thread safety when using commands to communicate between components
  • mtsTaskContinuous: base class for a task running as fast as possible. It defines a pure virtual method called Run which will be called over and over, as fast as possible
  • mtsTaskPeriodic: base class for a task running at a given periodicity (e.g. should wake up every 100 milliseconds). It defines a pure virtual method called Run which will be called when needed to maintain the periodicity. The jitter (variation between actual and desired period) depends on the operating system. Best performances are achieved using a real time operating system (Linux RTAI, QNX, ...).
  • mtsTaskFromSignal: base class for a task that should run only upon receipt of a command. This task is appropriate for event based applications -- the Run method gets called only when a command is queued.
  • mtsTaskFromCallback: base class for a task using an external trigger to start any computation. The Run method is used as a callback attached to an external library, driver, ...
  • svlFilterBase: this class is defined in the [wiki:cisstStereoVisionTutorial cisstStereoVision (SVL) library]; it is the base class for all SVL filters. It does not contain a thread, but is intended to be added to an SVL "stream" that is managed by the svlStreamManager component. The svlStreamManager provides the execution thread (or thread pool). See the [wiki:cisstStereoVisionTutorial cisstStereoVision Tutorial] for further details.

All tasks derived from mtsTask must define the following methods which are pure virtual in the base type:

  • void Startup(void): Initialization that will be executed after the thread is started but only once.
  • void Cleanup(void): Cleanup code that will be executed just before the thread is stopped.
  • void Run(void): Method called multiple times between Startup and Cleanup. This method is where the core logic of the component should be implemented.

For all threaded components, it is important to remember that most messages (commands and events) coming from other components are queued by default to ensure thread safety. The different queues of messages should be emptied by the receiving component. For most components, emptying all queues when the Run method is called is sufficient. This can be done using:

  • ProcessQueuedCommands will dequeue all commands of all provided interfaces
  • ProcessQueuedEvents will dequeue all events of all required interfaces For a component with both required and provided interfaces, the Run method is likely to look like:
void Run(void) {
    this->ProcessQueuedCommands();
    this->ProcessQueuedEvents() ;
    // perform our computation
    ...
}

Both methods return the number of commands/events dequeued.

3 Interfaces

3.1 Provided interfaces

Provided interfaces can be added to any existing component using the method mtsComponent::AddInterfaceProvided. The result of AddInterfaceProvided is a pointer on the newly create interface. It is the caller's responsability to verify that it is a valid pointer (the most likely cause of error is an attempt to create to interfaces with the same name).

// in the scope of a component
mtsInterfaceProvided * provided = AddInterfaceProvided("interfaceName");
if (provided == 0) {
     // handle error case
} else {
     // proceed to populate interface
}

It is not necessary to keep a pointer on the provided interface. The component cleanup procedure will free the allocate memory and if a pointer on the interface is needed later, one can call GetInterfaceProvided("interfaceName").

Internally, cisstMultiTask configures the provided interface differently based on the component itself. The main distinction is between components with a separate thread or not. If the component owns a thread (i.e. all components derived from mtsTask), the provided interface created using AddInterfaceProvided will assumes that void and write commands are queued by default. It is possible to override this behavior using AddInterfaceProvided("interfaceName", <interface_queueing_policy>) where the interface queueing policy can be one of:

  • MTS_COMPONENT_POLICY: let the component type defines the queueuing policy.
  • MTS_COMMANDS_SHOULD_NOT_BE_QUEUED: never queue a command. In this case, make sure all commands added to the interface are thread safe.
  • MTS_COMMANDS_SHOULD_BE_QUEUED: queue commands by default. In this case, make sure you empty the queues in your own thread. For a task derived from mtsTaskFromSignal, the provided interface is created with an added callback associated to the queue. This callback allows to wake up the task's thread right after a command is queued.

All queues for a given provided interface can be configured using a combination of SetMailBoxSize, SetArgumentQueuesSize and/or SetMailBoxAndArgumentQueuesSize (see Doxygen reference manual for details).

3.2 Required interfaces

Required interfaces are very similar to provided interfaces. They can be added to any existing component using the method mtsComponent::AddInterfaceRequired which returns a pointer on a new required interface.

// in the scope of a component
mtsInterfaceRequired * required = AddInterfaceRequired("interfaceName");
if (required == 0) {
    // handle error case
} else {
    // proceed to populate interface
}

To retrieve an existing required interface using, one can call GetInterfaceRequired("interfaceName").

One significant difference between provide and required interfaces is that the component manager checks by default that all required interfaces of a given component are connected before this component can be started. This can be tailored to your application using AddInterfaceRequired("interfaceName", MTS_OPTIONAL). The default behavior is AddInterfaceRequired("interfaceName", MTS_REQUIRED). The term required here means two different things:

  • In required interface, it means required to use a provided interface from a given component
  • In MTS_REQUIRED, it means required for the component to behave as expected

cisstMultiTask uses queues to ensure thread safeties when messages are sent to an interface. This applies to provided interfaces' commands as well as required interfaces's event handler. When a component register an event handler to observe a given event, the default behavior for components with a separate thread is to queue the event (actually, it queues the pointer on the event handler). As for provided interfaces, this can be changed using AddInterfaceRequired("interfaceName", <interface_queueing_policy>) where the interface queueing policy can be one of MTS_COMPONENT_POLICY, MTS_COMMANDS_SHOULD_NOT_BE_QUEUED or MTS_COMMANDS_SHOULD_BE_QUEUED.

As for provided interfaces, all queues of a required interface can be configured using a combination of SetMailBoxSize, SetArgumentQueuesSize and/or SetMailBoxAndArgumentQueuesSize (see Doxygen reference manual for details).

4 Commands and functions

4.1 Types available

During initialization, a given component has to populate its provided interfaces with pointers to existing methods. Within cisstMultiTask, the following types of commands are available (as internally the commands are C++ method pointers or C function pointers, a limited number of signatures is supported):

Name Signature Default queuing Synchronicity Example
mtsCommandVoid void method(void) Yes for tasks Non blocking by default robot->Stop()
mtsCommandVoidReturn void method(& result) Yes for tasks Always blocking robot->Start(status)
mtsCommandWrite void method(const & message) Yes for tasks Non blocking by default light->SetColor(blue)
mtsCommandWriteReturn void method(const & message, & result) Yes for tasks Always blocking robot->MoveTo(desired, actual)
mtsCommandRead void method(& result) const Never queued Always blocking robot->GetStatus(status)
mtsCommandQualifiedRead void method(const & qualifier, & result) const Never queued Always blocking robot->GetAxisStatus(axis, status)
  • All void and write commands are queued by default, i.e. the execution of the actual method or function will occur in the thread space of the task who dequeues the command.
    • Void and write commands without a return value are not blocking by default. It is possible to wait for the end of execution using ExecuteBlocking().
    • Void and write commands with a return value are always blocking.
  • All read and qualified read commands are not queued, i.e. they are executed in the thread space of the caller. Programmers will have to be careful when creating read commands not based on the state table (see next section).
  • The significant difference between mtsCommandVoidReturn and mtsCommandRead is that the later is ''const''. A read command must not change the state of the component with the provided interface ("server"), it must only read. On the other hand, a void return command is used to perform an action on the "server" side and get the result of this action. This also applies to mtsCommandWriteReturn and mtsCommandQualifiedRead.
  • Read methods can be used to:
  • Read the "server"'s configuration. In this case, thread safety is fairly easy to enforce as the component is not continuously updating this data
  • Read the "server"'s state. In this case, as the state changes continuously, a thread safe mechanism should be used. See mtsStateTable in state table section.
  • Execute a "static" method provided by the server side. Since "static" methods don't use any state data, they should be relatively thread safe (e.g. robot->ForwardKinematics(joints, pose))

For each command type, there is a matching function object:

  • mtsFunctionVoid
  • mtsFunctionVoidReturn
  • mtsFunctionWrite
  • mtsFunctionWriteReturn
  • mtsFunctionRead
  • mtsFunctionQualifiedRead

All function classes have a method Execute which can be called to trigger the command execution or queueing as well as the overloaded operator (). This permits to write code very similar to actual C function calls. Finally for commands which are not blocking by default (void and write), one can make them blocking using ExecuteBlocking():

mtsFunctionVoid StopRobot; // declaration in class header for a required interface
...
StopRobot(); // use in Run method for example
StopRobot.Execute(); // equivalent to ()
StopRobot.ExecuteBlocking(); // make sure the command has been executed on server side

4.2 Declaration

4.2.1 Commands

To add a command to a provided interface, one must first define the corresponding C global function or C++ method. In most cases, a command rely on a C++ method therefore all the following examples will rely on methods. We must first provide a few C++ methods corresponding to the provided commands.

class MyServerClass: public mtsTaskContinous
{
    ... // add constructor, destructor, ...
protected:
    // declaration of methods used by the commands
    void VoidMethod(void);
    void VoidReturnMethod(mtsBool & resultPlaceHolder);
    void WriteMethod(const mtsDouble & payload);
    void WriteReturnMethod(const mtsDouble & payload, mtsBool & resultPlaceHolder);
    void ReadMethod(mtsDouble & placeHolder) const;
    void QualifiedReadMethod(const mtsInt & qualifier, mtsDouble & placeHolder) const;
}

When the component is being built, these methods can be associated to provided commands:

bool MyServerClass::SetupInterfaces(void) {
    mtsInterfaceProvided * provided = this->AddInterfaceProvided("MyInterfaceProvided");
    if (!provided) {
        return false;
    }
    if (!(provided->AddCommandVoid(&MyServerClass::VoidMethod, this, "VoidCommand"))) {
        return false;
    }
    // etc. using:
    // AddCommandVoidReturn
    // AddCommandWrite
    // AddCommandWriteReturn
    // AddCommandRead
    // AddCommandQualifiedRead
}

Notes:

  • Commands can use method of different classes (i.e. not methods of MyServerClass). For example, one can use an array of objects to provide different interfaces and then use:
// declare a subclass
class MyTool {
    std::string Name;
    void VoidMethod(void);
}
...
// declare an array
MyTool Tools[5];
...
// create multiple provided interfaces
for (unsigned int index = 0; index < 5; ++index) {
    provided = this->AddInterfaceProvided(Tools[i].Name); // use the tool name
    provided->AddCommandVoid(&MyTool::VoidMethod, &(Tools[i]), "VoidCommand"); // use pointer on MyTool instance instead of `this`
}
  • By default, the command queueing policy is defined by the type of component used. As mentioned earlier, it is possible to change that behavior at the interface level using AddInterfaceProvided("name", policy). It is also possible to override the interface queueing policy and force a single command to not be queued (it is not possible to force queueing if the interface has been created without a mailbox/queue). To do so, one can use:
provided->AddCommandVoid(&MyServerClass::MethodVoid, this, "VoidCommand", MTS_COMMAND_NOT_QUEUED);
  • For all non queued commands, the programmer must make sure that the underlying C++ method is thread safe.

4.2.2 Functions

Functions in cisstMultiTask are objects added to required interface. These functions are ultimately bound to commands which are bound to C functions or C++ methods. A cisstMultiTask function can be declared using:

class MyClientClass: public mtsTaskPeriodic
{
    ... // add constructor, destructor, ...
protected:
    // declaration of function objects
    mtsFunctionVoid FunctionVoid;
    mtsFunctionVoidReturnMethod FunctionVoidReturn;
    mtsFunctionWrite FunctionWrite;
    mtsFunctionWriteReturn FunctionWriteReturn;
    mtsFunctionRead FunctionRead;
    mtsFunctionQualifiedRead FunctionQualifiedRead;
}

When the component is being built, these functions can be added to the required interface. The name associated to each function should match the name of the provided command from the provided interface which will be connected to the required interface:

bool MyClientClass::SetupInterfaces(void) {
    mtsInterfaceRequired * required = this->AddInterfaceRequired("MyInterfaceRequired");
    if (!required) {
        return false;
    }
    if (!(required->AddFunction("VoidCommand", this->FunctionVoid))) {
        return false;
    }
    // etc. using: AddFunction for all types of functions
}

Notes:

  • When a required interface is connected to a provided interface, the default behavior is to check that all functions in the required interface match commands in the provided interface. If there is any missing command from the provided interface, the connection fails. It is possible to override the behavior and mark a function as optional.
// in class declaration
mtsFunctionVoid FunctionVoidOptional;
...
// in code to setup required interface
required->AddFunction("OptionalCommand", FunctionVoidOptional, MTS_OPTIONAL); // default is MTS_REQUIRED

This allows to bind to extra features. The user can check if the function was bound at runtime using:

if (!(FunctionVoidOptional.GetCommand())) {
   // set some flag telling the component that the extra command is not available
}

4.3 Runtime

4.3.1 Execution result

At runtime, the "client" component can use the function objects to trigger the commands provided by the "server" component. All functions return an '''execution result''' of type mtsExecutionResult:

mtsExecutionResult result;
result = FunctionVoid(); // remember, operator () is overloaded
if (!result.IsOK()) {
   // do some error handling
   CMN_LOG_CLASS_RUN_ERROR << "execution failed, result is \"" << result << "\"" << std::endl;
}

The method IsOK() can be used to make sure the everything is working as expected. If an error is detected, the execution result can be used for testing and debugging. The most useful values of result.GetResult() are:

  • mtsExecutionResult::COMMAND_SUCCEEDED: for non queued or blocking commands, the command has been executed and succeeded
  • mtsExecutionResult::COMMAND_QUEUED: for a queued command, it has been queued successfully (but no guarantee it has been dequeued and executed on "server" side)
  • mtsExecutionResult::FUNCTION_NOT_BOUND: the function is not bound to a command, either because the required interface is not yet connected or because the provided interface doesn't have a matching command
  • mtsExecutionResult::INTERFACE_COMMAND_MAILBOX_FULL or COMMAND_ARGUMENT_QUEUE_FULL: the "server" component is not emptying its provided interface queues fast enough or not at all
  • mtsExecutionResult::INVALID_INPUT_TYPE: when using the function object, the parameter types don't match those of the provided command
  • mtsExecutionResult::METHOD_OR_FUNCTION_FAILED: for non queued commands (read and qualified read), it is actually possible to use C++ methods returning bool and not void (e.g. bool ReadMethod(mtsDouble & placeHolder)). In this case, the function object checks the returned boolean and if it is set to false returns METHOD_OR_FUNCTION_FAILED. All other codes are used mostly to debug the cisstMultiTask library itself.

4.3.2 Single process

This section presents a high level view of the command execution. The goal is to help users with a deeper interest in the internal mechanisms to understand the different steps involved.

  • Read and qualified read commands. Read and qualified read commands are never queued by default, including for components with a separate thread (e.g. all derived from mtsTask). It is the programmer's responsibility to make sure the underlying C++ method is thread safe. To ease this task, cisstMultiTask relies on circular buffers (see mtsStateTable). The execution goes as follow, all commands are executed in the caller's thread space:
  1. Function object checks that it is bound to a command, returns FUNCTION_NOT_BOUND otherwise
  2. Command object checks that it is enabled, returns COMMAND_DISABLED otherwise
  3. Based on underlying C++ method signature:
  4. For void method(...) const, calls the method and returns COMMAND_SUCCEEDED
  5. For bool method(...) const, calls the method and if the method itself return true, return COMMAND_SUCCEEDED, otherwise returns METHOD_OR_FUNCTION_FAILED
  • Void and write commands
  • If the command (in provided interface) is not queued, the sequence is very similar to the one described above except that there is only support for methods returning void. The reason is that we wish to maintain consistency with non blocking queued commands for which it is much harder to return a result.
  • If the command (in provided interface) is queued, the execution starts in the caller's thread space, continues in the callee's thread space and, for blocking commands, ends in the caller's thread space.
  1. Caller's thread space: 1. Function object checks that it is bound to a command, returns FUNCTION_NOT_BOUND otherwise 1. Command object checks that it is enabled, returns COMMAND_DISABLED otherwise 1. If the command is a write command, queues the argument (i.e. Write or WriteReturn). If the argument queue is full, return COMMAND_ARGUMENT_QUEUE_FULL 1. If the command expects a return value (i.e. VoidReturn or WriteReturn), give the address of the return placeholder to the command 1. If the command is blocking (i.e. VoidReturn and WriteReturn or Void and Write with the command ExecuteBlocking), set a flag telling the command this is a blocking call 1. Queue the command itself. If the queue is full, returns INTERFACE_COMMAND_MAILBOX_FULL 1. Finally: 1. If the command is blocking, put the current thread to sleep 1. If the command is not blocking, returns COMMAND_QUEUED
  2. Callee's thread space: 1. Call ProcessQueuedCommands. 1. Get argument (for write commands), blocking flag, address of return value (for return commands) and command pointer 1. Execute command using argument and return pointer as needed 1. If the command is blocking, wake up caller's thread
  3. For blocking commands only, caller's thread space 1. Return COMMAND_SUCCEEDED

4.3.3 Inter-process

5 Events and event handlers

5.1 Types available

Two types of events are available, void events don't carry any payload while write events do.

Name Event handler signature Example
Void void method(void) this->LostPower()
Write void method(const & message) this->ReachedJointLimit(jointIndex)

5.2 Declaration

5.2.1 Events

To add an event to a provided interface it is recommended to use a cisstMultiTask function as a way to trigger the event.

class MyServerClass: public mtsTaskContinous
{
    ... // add constructor, destructor, ...
protected:
    mtsFunctionVoid VoidEventTrigger;
    mtsFunctionWrite WriteEventTrigger;
}

When the component is being built, these functions can be bound to events:

bool MyServerClass::SetupInterfaces(void) {
    mtsInterfaceProvided * provided = this->AddInterfaceProvided("MyInterfaceProvided");
    if (!provided) {
        return false;
    }
    if (!(provided->AddEventVoid(this->EventVoidTrigger, "EventVoid"))) {
        return false;
    }
    if (!(provided->AddEventWrite(this->EventWriteTrigger, "EventWrite"))) {
        return false;
    }
}

5.2.2 Event handlers

To add an event handler to a required interface, one must first define the corresponding C global function or C++ method. We must first provide a few C++ methods corresponding to the event handlers.

class MyClientClass: public mtsTaskContinous
{
    ... // add constructor, destructor, ...
protected:
    // declaration of methods used as event handlers
    void VoidEventHandler(void);
    void WriteEventHandler(const mtsDouble & payload);
}

When the component is being built, these methods can be used as event handlers:

bool MyClientClass::SetupInterfaces(void) {
    mtsInterfaceRequired * required = this->AddInterfaceRequired("MyInterfaceRequired");
    if (!required) {
        return false;
    }
    if (!(required->AddEventHandlerVoid(&MyClientClass::VoidEventHandler, this, "EventVoid")) {
        return false;
    }
    if (!(required->AddEventHandlerWrite(&MyClientClass::WriteEventHandler, this, "EventWrite")) {
        return false;
    }
}

Notes:

  • As for commands, the method pointers don't have to point to methods of the component itself.
  • By default, the event handler queueing policy is defined by the type of component used. It means that for all components with a separate thread (i.e. derived frommtsTask), the event is queued until the client decides to process its queues of events using ProcessQueuedEvents(). This default can be overridden at two levels:
  • For the whole required interface using component->AddInterfaceRequired("interfaceName", MTS_COMMANDS_SHOULD_NOT_BE_QUEUED),
  • For a given event handler only using required->AddEventHandlerVoid(&MyClientClass::VoidEventHandler, this, "EventVoid", MTS_EVENT_NOT_QUEUED)
  • For all non queued event handlers, the programmer must make sure that the underlying C++ method is thread safe.

6 State data and table

Each task owns a default state table (mtsStateTable) which can be used to store the state of the task (the data member is mtsTask::StateTable). The table is a matrix indexed by time. At each iteration, one or more data objects used to define the state (mtsStateData) are saved in the table. At any given time, the task can write in the last row while anyone can safely read the previous states (including from other threads/tasks). The state table length is fixed to avoid dynamic re-allocation. Its size is defined by a mtsTask constructor parameter. The table will not overflow because it is implemented as a circular buffer. The class mtsStateData provides easy ways to create commands to access the state table.

Users can add more state tables to their components.

7 Connecting component interfaces

Once all the components are defined, it is necessary to connect them. Each component can have multiple provided interfaces. Reciprocally, a component may have multiple required interfaces, i.e. a component “user” can connect to multiple provided interfaces provided by one or more “resource” component. For each interface provided by a resource, a user task must define a required interface. To manage the components and their connections, use the cisstMultiTask class mtsComponentManager.

8 Using Custom Data Types in cisstMultiTask

8.1 Hand written code

If you want to use custom data types in cisstMultiTask interfaces, there currently are two strategies: (1) Inherit from mtsGenericObject and invoke a few macros in the header and implementation files, or (2) Define some "helper" functions to enable cisstMultiTask to use a "wrapper" object (mtsGenericObjectProxy) for your type.

The cisstMultiTask library itself uses both methods. For example, there is a class mtsDouble3 that inherits from mtsGenericObject and vctFixedSizeVector<double,3> (this is one of the few places in the cisst libraries where multiple inheritance is used). But, you can also directly use vctDouble3 (and its typedef vct3) because cisstMultiTask defines all the "helper" functions that are needed by the wrapper object, mtsGenericObjectProxy<vct3> (which is not the same as mtsDouble3).

8.1.1 Inheriting from mtsGenericObject

For example, if you have the following class:

MyClass.h

class MyClass {
  public:
    MyClass();
};

MyClass.cpp

#include <MyClass.h>

MyClass::MyClass() { }

In order to use this in a cisstMultiTask interface, you would need to make the following modifications:

MyClass.h

// CISST Includes
#include <cisstCommon/cmnGenericObject.h>
#include <cisstCommon/cmnClassServices.h>
#include <cisstCommon/cmnClassRegisterMacros.h>

class MyClass : public mtsGenericObject {
  CMN_DECLARE_SERVICES(CMN_NO_DYNAMIC_CREATION, CMN_LOG_ALLOW_DEFAULT);

  public:
    MyClass();
};

CMN_DECLARE_SERVICES_INSTANTIATION(MyClass);

MyClass.cpp

#include <MyClass.h>

MyClass::MyClass() { }

CMN_IMPLEMENT_SERVICES_TEMPLATED(MyClass);

You should also implement the ToStream and ToStreamRaw methods. If you plan to use this data type over the network, you must implement the SerializeRaw and DeSerializeRaw methods.

For more info on interfacing custom data types with CISST, see [wiki:cisstCommonFAQ].

8.1.2 Not inheriting from mtsGenericObject (i.e., using proxy wrapper)

This can be used to wrap any type for use with cisstMultiTask. In fact, the cisstMultiTask library already does this for common types such as int, double, vct3, etc.

For example, if you have a class MyClass that is not derived from mtsGenericObject, you can do the following:

MyClass.h

typedef mtsGenericObjectProxy<MyClass> mtsMyClassProxy;
CMN_DECLARE_SERVICES_INSTANTIATION(mtsMyClassProxy)

// Define "stream out" operator, if not already defined for your class
CISST_EXPORT std::ostream & operator << (std::ostream & output, const MyClass & object);

// overload cmnSerializeRaw and cmdDeSerializeRaw if you intend to send your data over the network
void CISST_EXPORT cmnSerializeRaw(std::ostream & outputStream, const MyClass & data);
void CISST_EXPORT cmnDeSerializeRaw(std::istream & inputStream, MyClass & data);

MyClass.cpp

CMN_IMPLEMENT_SERVICES_TEMPLATED(mtsMyClassProxy)

std::ostream & operator << (std::ostream & output, const MyClass & object)
{
    output << object.member1 << ", " << object.member2
           // ... repeat for other data members
           << std::endl;
    return output;
}

void CISST_EXPORT cmnSerializeRaw(std::ostream & outputStream, const MyClass & data)
{
    cmnSerializeRaw(outputStream, data.member1);
    cmnSerializeRaw(outputStream, data.member2);
    // ... repeat for other data members
}

void CISST_EXPORT cmnDeSerializeRaw(std::istream & inputStream, MyClass & data)
{
    cmnDeSerializeRaw(inputStream, data.member1);
    cmnDeSerializeRaw(inputStream, data.member2);
    // ... repeat for other data members (make sure order is the same as cmnSerializeRaw)
}

8.2 Data generator

There are a few issues with hand written data types:

  • this is a tedious task
  • it is fairly easy to introduce some bugs in the code by omitting one or more data members or base class in the serialize/de-serialize methods
  • each and every data type class has to be updated when a new feature is introduced

To avoid these issues, many libraries rely on a high level description (see for example Corba IDL, ICE, ROS messages, ...) and a code generator to produce the appropriate code in C, C++, ObjectiveC, Python, ... For the cisst libraries, we developed yet another data description format. One of the decisions made is to allow inline C/C++ code in the data description and therefore restrict the target language to C++. On the other hand, this allows us to:

  • create customized API for our data types while the code generator handles the common and repetitive part of the code.
  • use any C/C++ class within your data structures, i.e. message. The only requirement is that a few global functions need to be overloaded to handle non standard data types (e.g. a VTK mesh).

8.2.1 File format

The syntax is fairly simple and light:

  • the file contains a list of scopes, a scope is defined by a keyword followed by {, the scope's content and a closing };
  • each scope can contain other scopes or fields
  • each field is defined by a keyword followed by =, the field's content and a closing ;
  • one can use C++ style line comments, i.e. // The file format (supported scopes and fields) can be retrieved using the command line option -s or --syntax-only:
cisstDataGenerator --syntax-only
File syntax:
  class {
    base-class {
      is-data = <value must be one of 'true' 'false': default is 'true'>; // (optional) - indicates if the base class is a cisst data type itself
      type = <user defined string>; // (required) - C++ type for the base class, e.g. cmnGenericObject
      visibility = <value must be one of 'public' 'private' 'protected': default is 'public'>; // (optional) - determines if the base class should be public, ...
    }
    typedef {
      name = <user defined string>; // (required) - name of the new type defined
      type = <user defined string>; // (required) - C/C++ type used to define the new type
    }
    member {
      accessors = <value must be one of 'none' 'references' 'set-get' 'all': default is 'all'>; // (optional) - indicates which types of accessors should be generated for the data member
      default = <user defined string>; // (optional) - default value that should be assigned to the data member in the class constructor
      description = <user defined string>; // (optional) - user provided description of the data member
      is-data = <value must be one of 'true' 'false': default is 'true'>; // (optional) - indicates if the data member is a cisst data type itself
      is-size_t = <value must be one of 'true' 'false': default is 'false'>; // (optional) - indicates if the data member is a typedef of size_t or size_t
      name = <user defined string>; // (required) - name of the data member, will also be used to generate accessors
      type = <user defined string>; // (required) - C++ type of the data member (e.g. double, std::string, ...)
      visibility = <value must be one of 'public' 'protected' 'private': default is 'protected'>; // (optional) - indicates if the data member should be public, ...
    }
    inline-header {
      C++ code snippet - code that will be placed as-is in the generated header file
    }
    inline-code {
      C++ code snippet - code that will be placed as-is in the generated source file
    }
    attribute = <user defined string>; // (optional) - string place between 'class' and the class name (e.g. CISST_EXPORT)
    name = <user defined string>; // (required) - name of the generated C++ class
  }
  inline-header {
    C++ code snippet - code that will be placed as-is in the generated header file
  }
  inline-code {
    C++ code snippet - code that will be placed as-is in the generated source file
  }

It is important to note that the cisst data generator doesn't force any specific implementation related to cisstMultiTask, i.e. it is possible to use either inheritance from mtsGenericObject or the proxy approach.

See examples with inlined comments:

  • Classes not used with cisstMultiTask: source:/trunk/cisst/cisstCommon/examples/dataGenerator/demoData.txt
  • Classes used with cisstMultiTask:
  • source:/trunk/cisst/cisstParameterTypes/prmPositionCartesianGet.ccc
  • source:/trunk/cisst/cisstParameterTypes/prmPositionJointGet.ccc

8.2.2 CMake

We provide a CMake macro that simplifies the build process. This macro manages the dependencies as well as build rules between the description file, generated header and source files and object files. The macro is defined in the file cisstMacros.cmake which is automatically included when you include (${CISST_USE_FILE}). Here are two examples of use:

  # create data type using the data generator
  cisst_data_generator (cmnExDataGenerator    # prefix for the CMake variables that will contain the lists of headers/sources
                        ${CMAKE_CURRENT_BINARY_DIR}    # destination directory where do you want the generated files to go
                        ""    # subdirectory for the header file, see next example.  This will be appended to the destination directory.
                        demoData.txt)    # one or more cisst data description files

  # to compile cisst generated code, need to find header file
  include_directories (${CMAKE_CURRENT_BINARY_DIR})

  add_executable (cmnExDataGenerator
                  ${cmnExDataGenerator_CISST_DG_SRCS}   # variable automatically created and populated by cisst_data_generator macro using the provided prefix
                  main.cpp)

Another example using the include subdirectory and the list of header files generated:

# create data type using the data generator
cisst_data_generator (cisstParameterTypes
                      "${cisst_BINARY_DIR}/include" # where to save the files
                      "cisstParameterTypes/"           # sub directory for include, header files will in "include/cisstParameterTypes"
                                                                    # and can be included using #include <cisstParameterTypes/prmPositionCartesianGet.h>
                      prmPositionCartesianGet.ccc  # using multiple data description files
                      prmPositionJointGet.ccc)

# to compile cisst generated code, need to find header file
include_directories (${CMAKE_CURRENT_BINARY_DIR})

# ${cisstParameterTypes_CISST_DG_SRCS} contains the list of generated source files (absolute paths)
# ${cisstParameterTypes_CISST_DG_HDRS} contains the list of generated header files (absolute paths)
Clone this wiki locally