-
Notifications
You must be signed in to change notification settings - Fork 47
cisstMultiTask tutorial
In this tutorial we will create two simple components. The first component is a counter
. Its main function is to periodically increment an internal counter. When the counter overflows it should send an event. One should be able to query the current value and set the increment. If the user requests an incorrect value for the increment, the counter component will throw an event with an informative message.
The second component user
is designed to be connected to the counter
component. In this example, we're avoiding using a GUI toolkit to make the code as simple as possible. The user interface is text based.
The latest version of the code for this tutorial can be compiled along the cisst using the CMake options CISST_BUILD_EXAMPLES
and CISST_cisstMultiTask_EXAMPLES
. The code itself can be found in cisst/cisstMultiTask/examples/tutorial
For this component, we're using a the base class mtsTaskPeriodic
, i.e. the Run
method will be called periodically to perform all user defined computations and the library will attempt to maintain a constant frequency between calls to the Run
method. The amount of jitter depends on the services provided by the Operating System (the cisstOSAbstraction library provides an abstraction layer to the different OS features).
One of the parameters provided to the constructor is the desired periodicity provided as a double
representing the time in seconds. To make sure the code is readable, cisst has a set of constants used to indicate the units used, e.g. 5.0 * cmm_ms
indicates 5 milliseconds.
It is possible to use different types of components: continuous, event based, triggered by an external event, ... See all component and task types in cisstMultiTask concepts.
In the header file:
#include <cisstMultiTask/mtsTaskPeriodic.h>
class counter: public mtsTaskPeriodic {
...
};
In the implementation file:
counter::counter(const std::string & componentName, double periodInSeconds):
// base constructor, same component name and period. Third
// parameter is "false" because we don't need hard realtime. Last
// parameter, 500, is the size of the default state table
mtsTaskPeriodic(componentName, periodInSeconds, false, 500),
...
{
}
The Run
method of a task contains the user computations, in our example:
void counter::Run(void)
{
// process the commands received
ProcessQueuedCommands();
Counter += Increment;
if (Counter > 100.0) {
Counter = 0.0;
// it's good practice to check the returned value
mtsExecutionResult result = OverflowEvent();
if (!result) {
CMN_LOG_CLASS_RUN_ERROR << "SetIncrement: trigger \"OverflowEvent\" returned "
<< result << std::endl;
}
}
}
Since most components use queued commands to be thread safe, one must remember to empty the queues of commands using the method ProcessQueuedCommands
.
All cisstMultiTask components own at least one state table. A state table can be seen as a matrix where the columns represent a state data object and the rows the values of that data object over time. It is implemented as a circular buffer and is used to provide a thread safe mechanism to publish the component's data (one writer, multiple readers). For historical reasons, all components have a default state table member (StateTable
) but users can add as many state tables as they need (using AddStateTable
). One can add more state tables to handle:
- different refresh rates, i.e. some data doesn't change over time. In this example, the increment changes only when set by the user so we don't need to save the data at the same refresh rate as the counter value.
- similar groups of data, i.e. a component can maintain multiple similar interfaces corresponding to similar devices (e.g. left and right arms of a robot). In this case, one can use two tables containing the exact same data.
In the header file, declare data to be added to state table(s) and optionally user defined state tables:
// internal counter data
double Counter;
// increment used for the counter
mtsStateTable ConfigurationStateTable;
double Increment;
In implementation file, configuring the state tables:
// state table variables
StateTable.AddData(Counter, "Counter");
// user defined configuration state table
// first you need to add the state table to the component
AddStateTable(&ConfigurationStateTable);
// second, make sure we control when the table "advances"
ConfigurationStateTable.SetAutomaticAdvance(false);
// finally, add data to the state table
ConfigurationStateTable.AddData(Increment, "Increment");
For the user defined state table, since we turned off Automatic Advance, we need to Start and Advance manually:
// now we can add to the state table
ConfigurationStateTable.Start();
Increment = increment;
// make the circular buffer move one step
ConfigurationStateTable.Advance();
There is no need to Start and Advance the default state table StateTable
; this is performed automatically before and after the calls to the Run
method. This applies to all state tables with Automatic Advance turned on (default behavior).
cisstMultiTask uses the command pattern, i.e. the user doesn't directly call the C++ methods of a component. All methods are encapsulated in command objects (e.g. mtsCommandVoid
). These command objects are grouped in interfaces and can be retrieved by name at runtime (see also cisstMultiTask concepts). In practice, the first step is to create an interface that will contain some of the provided features of the component (i.e. commands and events). To add an mtsInterfaceProvided
:
// add a provided interface
mtsInterfaceProvided * interfaceProvided = AddInterfaceProvided("User");
// for applications dynamically creating interfaces, the user
// should make sure the interface has been added properly.
// AddInterfaceProvided could fail if there is already an
// interface with the same name.
if (!interfaceProvided) {
CMN_LOG_CLASS_INIT_ERROR << "failed to add \"User\" to component \""
<< this->GetName() << "\"" << std::endl;
return;
}
The next step is to add some commands to the newly created provided interface. For the counter component, we're going to use 3 different types of commands:
-
Void: command that doesn't require any payload, usually encapsulating a method with the signature
void method(void)
. Void commands usually change the state of the component and therefore encapsulate non const methods. If the component owns its own thread, void commands are queued. -
Write: command that requires a payload, usually encapsulating a method with the signature
void method(const payloadType & payload)
. Write commands usually change the state of the component of the state and therefore encapsulate non const methods. If the component owns its own thread, write commands are queued. -
Read: command used to get some information from the component, usually encapsulating a method with the signature
void method(payloadType & placeHolder) const
. Read commands shouldn't change the state of the component and therefore encapsulate const methods. Read commands are never queued, so make sure the encapsulated method is thread safe. For this example, we're using a built-in mechanism to read from the state table which happens to be thread safe.
As for the AddInterfaceProvided
, one should test if the command has been added properly. This is unlikely to fail for a hard-coded list of commands.
In the header file, declaration of encapsulated methods:
protected:
// internal methods used for the provided commands
void SetIncrement(const double & increment);
void Reset(void);
In your implementation file, add the provided interface and commands:
// add a provided interface
mtsInterfaceProvided * interfaceProvided = AddInterfaceProvided("User");
// for applications dynamically creating interfaces, the user
// should make sure the interface has been added properly.
// AddInterfaceProvided could fail if there is already an
// interface with the same name.
if (!interfaceProvided) {
CMN_LOG_CLASS_INIT_ERROR << "failed to add \"User\" to component \""
<< this->GetName() << "\"" << std::endl;
return;
}
// add a void command. The signature of the method used should be
// "void method(void)". As for the interface, it is possible to
// check if a command has been added properly using the returned
// value.
if (!interfaceProvided->AddCommandVoid(&counter::Reset, this, "Reset")) {
CMN_LOG_CLASS_INIT_ERROR << "failed to add command to interface \""
<< interfaceProvided->GetFullName()
<< "\"" << std::endl;
}
// in this example, all the commands have a different name so
// there is really no need to check the returned value for
// AddCommand ...
// add a write command. The signature of the method used should
// be "void method(const type & payload)". We also need to
// provide the default value expected by this command.
interfaceProvided->AddCommandWrite(&counter::SetIncrement, this,
"SetIncrement", 1.0);
// add a command to read the latest value from a state table
interfaceProvided->AddCommandReadState(StateTable, Counter,
"GetValue");
interfaceProvided->AddCommandReadState(ConfigurationStateTable, Increment,
"GetIncrement");
Commands are always triggered by the other component, i.e. the component connected to the provided interface (in our example, counter component has the provided interface User). It is also possible to add events to a provided interface. Events are triggered by the component that owns the provided interface. As for the commands, it is possible to declare an event with or without payload (write event and void event).
In the header file:
// overflow event
mtsFunctionVoid OverflowEvent;
// event thrown if the increment value is invalid, sends current increment
mtsFunctionWrite InvalidIncrementEvent;
In the implementation file, to add the events to the provided interface:
// add a void event. We need to provide the function
// (mtsFunction) that will be used to trigger the event.
interfaceProvided->AddEventVoid(OverflowEvent, "Overflow");
// add a write event, i.e. an event with a payload. We need to
// provide the function (mtsFunction) that will be used to trigger
// the event as well as the default value/type of the payload.
interfaceProvided->AddEventWrite(InvalidIncrementEvent, "InvalidIncrement",
std::string());
To trigger an event:
// it's good practice to check the returned value
mtsExecutionResult result = OverflowEvent();
if (!result) {
CMN_LOG_CLASS_RUN_ERROR << "SetIncrement: trigger \"OverflowEvent\" returned "
<< result << std::endl;
}
For events, the two possible errors are:
-
mtsFunction
is not bound, i.e. the user forgot to use this function as an event trigger, i.e. it was not used withAddEvent
- Invalid data type for a write event, the payload doesn't correspond to the type declared with
AddEventWrite
.
Our user component uses the terminal to display its state and key hits to trigger the different commands. The default commands are:
Press ...
[g] to get current counter value
[r] to reset counter
[i] to set a new counter increment
[q] to quit
Since we want to continuously observe the user's key hits, the user component is derived from mtsTaskContinuous
. In the Run
method, we use the cisstCommon functions cmnKbHit
and cmnGetChar
to capture the key hits in a non-blocking manner. We need to make sure listening to keyboard hits is not blocking because the Run
method also calls ProcessQueuedEvents
. In general, programmers should make sure computations in the Run
methods are non-blocking.
The user component needs a required interface to group all the features it requires. Each feature is declared as a function (object of type mtsFunction...
). Overall, when the two components are connected, the user's required interface is connected to the counter's provided interface. Each function declared in the user's required interface is connected to the corresponding command in the counter's provided interface. The two sets of features (commands and events of a provided interface vs. functions and event handlers of a required interface) can be subsets of each other (see also cisstMultiTask concepts).
In the header file:
// functions used in the interface required to send commands to counter
mtsFunctionVoid Reset;
mtsFunctionRead GetValue;
mtsFunctionWrite SetIncrement;
In the implementation file:
// add an interface required.
mtsInterfaceRequired * interfaceRequired = this->AddInterfaceRequired("Counter");
if (!interfaceRequired) {
CMN_LOG_CLASS_INIT_ERROR << "failed to add \"Counter\" to component \"" << this->GetName() << "\"" << std::endl;
return;
}
// add a void function, i.e. send a request without payload
interfaceRequired->AddFunction("Reset", this->Reset);
// add a read function, i.e. function to retrieve data
interfaceRequired->AddFunction("GetValue", this->GetValue);
// add a write function, i.e. send a request with a payload
interfaceRequired->AddFunction("SetIncrement", this->SetIncrement);
Event handlers are similar to commands, i.e. they also encapsulate C++ methods and come in two flavors, void and write. One must first declare the methods used to handle the events, most likely private
or protected
.
In the header file:
// methods used as event handlers for events coming from counter
void OverflowHandler(void);
void InvalidIncrementHandler(const std::string & message);
Event handlers must be added to an existing provided interface when the component is being constructed or configured:
// add a void event handler, i.e. handle an event without a
// payload. The method used should have the signature "void
// method(void)"
interfaceRequired->AddEventHandlerVoid(&user::OverflowHandler, this, "Overflow");
// add a write event handler, i.e. handle an event with a payload.
// The method used should have the signature "void method(const
// type & payload)"
interfaceRequired->AddEventHandlerWrite(&user::InvalidIncrementHandler, this, "InvalidIncrement");
If the component owns its thread, events will most likely be queued on the required interface. To make sure the queues don't get filled, one has to process all the queued events. In the Run
method:
void user::Run(void)
{
// process the events received
ProcessQueuedEvents();
// other things to do
...
}
Once the two components have been implemented, they need to be connected together using their interfaces. A required interface can be connected to one and only one provided interface. On the other hand, it is possible to connect multiple required interfaces to a single provided interface.
When connecting a required interface to a provided interface:
- The interface names don't have to match
- Name and types of commands and events must match
- The provided interface can have more commands and events than the required interface (unused features)
- Functions and event handlers of a required interface can be tagged as optional. If so, the required interface can still be connected to a provided interface that doesn't provide said features
To manage all the components, use the mtsComponentManager
object. The manager is implemented as a singleton so we need to call the static mtsComponentManager::GetInstance()
method. The following steps are:
- Add the components to the manager
- Connect the components
- Create the components; create the component threads and call their
Startup
method - Start the components; the
Run
method will be called - Kill the components; stop the threads and call the
Cleanup
method
In the main file:
// component manager is a singleton
mtsComponentManager * componentManager = mtsComponentManager::GetInstance();
// create counter and user components
counter * counterPointer = new counter("counter", 1.0 * cmn_s);
user * userPointer = new user("user");
// add the components to the component manager
componentManager->AddComponent(counterPointer);
componentManager->AddComponent(userPointer);
// connect the components, task.RequiresInterface -> task.ProvidesInterface
componentManager->Connect("user", "Counter", "counter", "User");
// create the components
componentManager->CreateAll();
componentManager->WaitForStateAll(mtsComponentState::READY, 2.0 * cmn_s);
// start the periodic Run
componentManager->StartAll();
componentManager->WaitForStateAll(mtsComponentState::ACTIVE, 2.0 * cmn_s);
// loop until the user tells us to quit
while (!userPointer->Quit) {
osaSleep(100.0 * cmn_ms);
}
// cleanup
componentManager->KillAll();
componentManager->WaitForStateAll(mtsComponentState::FINISHED, 2.0 * cmn_s);
componentManager->Cleanup();
- Home
- Libraries & components
- Download
- Compile (FAQ)
- Reference manual
- cisstCommon
- cisstVector
- cisstNumerical
- cisstOSAbstraction
- TBD
- cisstMultiTask
- cisstRobot
- cisstStereoVision
- Developers