The Orocos Component Builder's Manual

Open RObot COntrol Software

1.0.3

Orocos Version 1.0.3.

Permission is granted to copy, distribute and/or modify this document under the terms of the GNU Free Documentation License, Version 1.1 or any later version published by the Free Software Foundation, with no Invariant Sections, with no Front-Cover Texts, and with no Back-Cover Texts. A copy of this license can be found at http://www.fsf.org/copyleft/fdl.html.

Abstract

This document gives an introduction to building your own components for the Orocos ( Open RObot COntrol Software ) project.


Table of Contents

1. How to Read this Manual
1. Component Interfaces
2. Component Implementation
3. Orocos Real-Time Toolkit Overview
4. Orocos Real-Time Toolkit Software Structure
2. Setting up the Component Interface
1. Introduction
2. Hello World !
3. Setting Up a Basic Task
3.1. Task Application Code
3.2. Starting Task Execution
3.3. Interfacing the TaskContext
3.4. Introducing the TaskContext's Interface
3.5. The Data Flow Interface
3.6. The Method Interface
3.7. Method Argument and Return Types
3.8. The Attributes and Properties Interface
3.9. The Command Interface
3.10. The Event Interface
4. Connecting TaskContexts
4.1. Setting up the Execution Flow
4.2. Setting up the Data Flow
4.3. Disconnecting Tasks
5. Deploying Components
5.1. Overview
5.2. Embedded TaskCore Deployment
5.3. Embedded TaskContext Deployment: C++ Interface
5.4. Full TaskContext Deployment: Dynamic Interface
5.5. Putting it together
6. Using Tasks
6.1. Task Property Configuration
6.2. Task Scripts
7. Advanced Techniques
7.1. Using the TaskContext
7.2. Wrapping Methods in Functions
7.3. Waiting for Something : Synchronisation
7.4. Polymorphism : Task Interfaces
3. Orocos Scripting Reference
1. Introduction
2. General Scripting Concepts
2.1. Comments
2.2. Identifiers
2.3. Expressions
2.4. Parsing and Loading Programs
3. Orocos Program Scripts
3.1. Program Execution Semantics
3.2. Program Syntax
3.3. Starting and Stopping Programs from scripts
4. Orocos State Descriptions : The Real-Time State Machine
4.1. Introduction
4.2. StateMachine Workings
4.3. Parsing and Loading StateMachines
4.4. Defining StateMachines
4.5. Instantiating Machines: SubMachines and RootMachines
4.6. Starting and Stopping StateMachines from scripts
5. Program and State Example
4. Distributing Orocos Components with CORBA
1. Overview
1.1. Status
1.2. Requirements and Setup
1.3. Limitations
2. Code Examples
3. Usage
4. Orocos Corba Interfaces
5. Core Library Reference
1. Introduction
2. Periodic and non Periodic Activities
2.1. Creating a Periodic Activity
2.2. Periodic Activity Ordering
2.3. Example Periodic Activity Creation
2.4. Non Periodic Activities
2.5. Custom or Slave Activities
2.6. Accessing the Threads from Activities
2.7. Simulation
3. Commands
3.1. The Command Processor
4. Events
4.1. Event Basics
4.2. setup() and the Handle object
4.3. Choosing the Asynchronous Thread
4.4. Event Overrun Policy
4.5. The Completion Processor
5. Time Measurement and Conversion
5.1. The TimeService
5.2. Usage Example
6. Attributes
7. Properties
7.1. Introduction
7.2. Grouping Properties in a PropertyBag
7.3. Marshalling and Demarshalling Properties (Serialization)
8. The NameServer
8.1. Introduction
8.2. Using the NameServer
9. Buffers and DataObjects
9.1. Buffers
9.2. DataObjects
10. Logging
6. OS Abstraction Reference
1. Introduction
1.1. Realtime OS Abstraction
2. The Operating System Interface
2.1. Basics
3. OS Package Structure
3.1. The RTAI/LXRT OS target
3.2. Porting Orocos to other Architectures / OS'es
3.3. OS Header Files
4. Using Threads and Realtime Execution of Your Program
4.1. Writing the Program main()
4.2. The Orocos Thread System
4.3. Synchronisation Primitives
7. Hardware Device Interfaces
1. The Orocos Device Interface (DI)
2. The Device Interface Classes
2.1. Physical IO
2.2. Logical Device Interfaces
3. Porting Device Drivers to Device Interfaces
4. Interface Name Serving

Chapter 1. How to Read this Manual

This manual is for Software developers who wish to write their own software components using the Orocos Real-Time Toolkit. The possible communication primitives between components is defined and implemented in the Orocos Core Library (CoreLib). You can find the CoreLib Reference Chapter at the end to find out the precise semantics of our communication primitives. The Orocos hardware abstraction is included as well.

1. Component Interfaces

The most important Chapters to get started building a component are presented first. Orocos components are implemented using the 'TaskContext' class and the following Chapter explains step by step how to define the interface of your component, such that you can interact with your component from a user interface or other component.

2. Component Implementation

For implementing algorithms within your component, the Orocos Scripting Chapter details how to write programs and state machines. "Advanced Users" may benefit from this Chapter as well since the scripting language allows to 'program' components without recompiling the source.

3. Orocos Real-Time Toolkit Overview

The Real-Time Toolkit allows deployment, distribution and the building of real-time software components. It is sometimes refered to as 'middleware' because it sits between the application and the Operating System. It takes care of the real-time communication and execution of software components.

Figure 1.1.  Orocos as Middleware

Orocos as Middleware

4. Orocos Real-Time Toolkit Software Structure

The Real-Time Toolkit is structured in layers on top of the hardware target (CPU) and the devices (IO) and encompasses some libraries.

Figure 1.2.  Real-Time Toolkit Layers

Real-Time Toolkit Layers

All Orocos software communicates through Operating System abstractions with the underlying OS or through Device Interfaces with the IO hardware. On the left side on top of the OS Abstraction is the Core Library, which presents infrastructure for writing realtime (periodic) activities, including many communication primitives. On top of that is the Real-Time Toolkit, which allows to define browsable components which are capable of receiving (remote) commands, process programs and hierarchical state machines and share realtime data with other components. Your application may use that infrastructure to define its own components and set up communication between them.

On the right side, lives the Device Abstraction stack. The Operating System already provides a device driver model, Orocos device drivers map that model to machine or robot control specific Device Interfaces. An Orocos Device combines a number of these interfaces into an object representing a physical entity. It may represent a complete machine, or just a mere Sensor or Axis, such that your application can communicate with physical meaningful devices ( instead of IO ports).

Because of the Openness of Orocos, your application does not need to take-all-or-leave-all. You can pick out the level of abstraction which suits you best for a given application (indicated by the gradient).

Chapter 2. Setting up the Component Interface

Abstract

This document describes the Orocos Task Infrastructure, which allows to design Real-Time tasks which transparantly communicate with each other.

1. Introduction

This manual documents how multi-threaded components can be defined in Orocos such that they form a thread-safe robotics/machine control application. Each control component is defined as a "TaskContext", which defines the "context" in which the component task is executed. The context is built by the five Orocos primitives: Event, Property, Command, Method and Data Port. This document defines how a user can write his own task context and how it can be used in an application.

Figure 2.1.  Tasks Run in Threads

Tasks Run in Threads

Tasks run in (periodic) threads.

A task is a basic unit of functionality which executes one or more (real-time) programs in a single thread. The program can vary from a mere C function over a real-time program script to a real-time hierarchical state machine. The focus is completely on thread-safe time determinism. Meaning, that the system is free of priority-inversions, and all operations are lock-free (also data sharing and other forms of communication such as events and commands). Real-time tasks can communicate with non real-time tasks (and vice versa) transparantly.

The Orocos Task Infrastructure enables :

  • Lock free, thread-safe, inter-thread function calls.

  • Communication between hard Real-Time and non Real-Time threads.

  • Deterministic execution time during communication for the higher priority thread.

  • Synchronous and asynchronous communication between threads.

  • Interfaces for component distribution.

  • C++ class implementations for all the above.

This document relates to other manuals as such :

Core Library

provides the Event infrastructure, activity to thread mapping, Properties and lock-free data exchange implementations.

Execution Engine

provides a real-time program execution framework. It executes the real-time programs which interact with other tasks.

Program Parser

provides a scripting language which is convertible to a form which can be accepted by the Execution Engine.

The Scripting manual gives more details about script syntax for state machines and programs.

Figure 2.2.  Schematic Overview of a TaskContext

Schematic Overview of a TaskContext

The Execution Flow is formed by Programs and State Machines sending commands, events,... to Peer Tasks. The Data Flow is the propagation of data from one task to another, where one producer can have multiple consumers and vice versa.

A task's interface consists of : Attributes and Properties, Commands, Methods, Events and Data Flow ports which are all public. The class TaskContext groups all these interfaces and serves as the basic building block of applications. A component developer 'builds' these interfaces using the instructions found in this manual.

2. Hello World !

This section introduces tasks through the "hello world" application, which can be downloaded from Orocos.org. It contains one TaskContext component which has one instance of each communication primitive.

Figure 2.3.  Schematic Overview of the Hello Component.

Schematic Overview of the Hello Component.

Our hello world task.

The way we interact with TaskContexts during development of an Orocos application is through the Task Browser. The TaskBrowser is a powerful console tool which helps you to explore, execute and debug TaskContexts in running programs. All you have to do is to create a TaskBrowser and call its loop() method. When the program is started from a console, the TaskBrowser takes over user input and output.

[Note]Note

The OCL::TaskBrowser is a component of its own which is found in the Orocos Component Library (OCL).

  #include <ocl/TaskBrowser.hpp>
  #include <rtt/os/main.h>
  // ...

  using namespace Orocos;

  int ORO_main( int, char** )
  {
      // Create your tasks
      TaskContext* task = ...

      // when all is setup :
      TaskBrowser tbrowser( task );

      tbrowser.loop();
      return 0;
  }
    

The TaskBrowser uses the GNU readline library to easily enter commands to the tasks in your system. This means you can press TAB to complete your commands or press the up arrow to scroll through previous commands.

0.016 [ Info   ][main()] ./helloworld manually raises LogLevel to 'Info' (5). See also file 'orocos.log'.
0.017 [ Info   ][main()] **** Creating the 'Hello' component ****
0.018 [ Info   ][ConnectionC] Creating Asyn connection to the_event.
0.018 [ Info   ][ExecutionEngine::setActivity] Hello is periodic.
0.019 [ Info   ][main()] **** Starting the 'Hello' component ****
0.019 [ Info   ][main()] **** Using the 'Hello' component    ****
0.019 [ Info   ][main()] **** Reading a Property:            ****
0.019 [ Info   ][main()]      the_property = Hello World
0.019 [ Info   ][main()] **** Sending a Command:             ****
0.020 [ Info   ][main()]      Sending the_command : 1
0.020 [ Info   ][main()] **** Calling a Method:              ****
0.020 [ Info   ][main()]      Calling the_Method : Hello World
0.020 [ Info   ][main()] **** Emitting an Event:             ****
0.021 [ Info   ][main()] **** Starting the TaskBrowser       ****
0.021 [ Info   ][TaskBrowser] Creating a BufferConnection from the_buffer_port to the_buffer_port with size 13
0.021 [ Info   ][TaskBrowser] Connected Port the_buffer_port to peer Task Hello.
0.022 [ Info   ][Hello] Creating a DataConnection from the_data_port to the_data_port
0.022 [ Info   ][Hello] Connected Port the_data_port to peer Task TaskBrowser.
   Switched to : Hello
0.023 [ Info   ][main()] Entering Task Hello
0.023 [ Info   ][Hello] Hello Command: World
0.023 [ Info   ][Hello] Receiving Event: Hello World

  This console reader allows you to browse and manipulate TaskContexts.
  You can type in a command, event, method, expression or change variables.
  (type 'help' for instructions)
    TAB completion and HISTORY is available ('bash' like)

 In Task Hello. (Status of last Command : none )
 (type 'ls' for context info) :                                                     
    

The first [ Info ] lines are printed by the Orocos Logger, which has been configured to display informative messages to console. Normally, only warnings or worse are displayed by Orocos. You can always watch the log file 'orocos.log' in the same directory to see all messages. After the [Log Level], the [Origin] of the message is printed, and finally the message itself. These messages leave a trace of what was going on in the main() function before the prompt apeared.

Depending on what you type, the TaskBrowser will act differently. The built-in commands cd, help, quit and ls are seen as commands to the TaskBrowser itself, if you typed something else, it tries to evaluate your command to an expression and will print the result to the console. If you did not type an expression, it tries to parse it as a command to a (peer) task. If that also fails, it means you made a typo and it prints the syntax error to console.

 In Task Hello. (Status of last Command : none )
 (type 'ls' for context info) :1+1
      Got :1+1
   = 2
    

To display the contents of the current task, type ls, and switch to one of the listed peers with cd, while cd .. takes you one peer back in history. Since there are no peers other than the TaskBrowser itself, one can not cd anywhere in this example.

 In Task Hello. (Status of last Command : none )
 (type 'ls' for context info) :ls

 Listing Hello :

 Attributes   :
 (Attribute)      string the_attribute      = Hello World
 (Attribute)      string the_constant       = Hello World
 (Property )      string the_property       = Hello World          (Hello World Description)

 Interface    :
  Methods      : the_method isRunning start stop trigger update
  Commands     : the_command
  Events       : the_event

 Objects      :
            this (  )
   the_data_port ( A Task Object. )
 the_buffer_port ( A Task Object. )

 Ports        : the_data_port the_bufferPort
 Peers        : TaskBrowser
    
[Note]Note

To get a quick overview of the commands, type help.

First you get a list of the Properties and Attributes (alphabetical) of the current component. Properties are meant for configuration and can be written to disk. Attributes are solely for run-time values. Each of them can be changed (except constants.)

Next, the interface of this component is listed: One method is present the_method, one command the_command and one event the_event. They all print a 'Hello World' string when invoked.

In the example, the current task has only three objects: this, the_data_port and the_buffer_port. The this object serves as the public interface of the Hello component. These objects contain methods, commands or events. The next two objects are created to represent the data ports of the Hello component and contain the operations to send or receive data or query connection status.

Last, the peers are shown, that is, the components which are connected to this component. The HelloWorld component is a stand-alone component and has only the TaskBrowser as a peer.

To get a list of the Task's interface, you can always type an object name, for example this.

 In Task Hello. (Status of last Command : none )
 (type 'ls' for context info) : this
      Got :this

Printing Interface of 'Hello' :

  Command    : bool the_command( string the_arg )
   Hello Command Description
   the_arg : Use 'World' as argument to make the command succeed.
  Method     : string the_method( )
   Hello Method Description
  Event     : void the_Event( string the_data )
   Hello Event Description
   the_data : The data of this event.
    

Now we get more details about the commands, methods and events registered in the public interface. We see now that the the_command command takes one argument as a string, or that the the_method method returns a string. One can invoke each one of them:

 In Task Hello. (Status of last Command : none )
 (type 'ls' for context info) :the_method()
      Got :the_method()
 = Hello World
    

Methods are called directly and the TaskBrowser prints the result. The return value of the_method() was a string, which is "Hello World".

When a command is entered, it is sent to the Hello component, which will execute it on behalf of the sender. The different stages of its lifetime are displayed by the prompt. Hitting enter will refresh the status line:

 In Task Hello. (Status of last Command : none )
 (type 'ls' for context info) :the_command("World")
      Got :the_command("World")

 In Task Hello. (Status of last Command : queued)
1021.835 [ Info   ][Hello] Hello Command: World
 (type 'ls' for context info) :                   

1259.900 [ Info   ][main()] Checking Command: World
 In Task Hello. (Status of last Command : done )
 (type 'ls' for context info) :                   
    

A Command might be rejected (return false) in case it received invalid arguments:

 In Task Hello. (Status of last Command : done )
 (type 'ls' for context info) :the_command("Belgium")
      Got :the_command("Belgium")

 In Task Hello. (Status of last Command : queued )
 (type 'ls' for context info) :
1364.505 [ Info   ][Hello] Hello Command: Belgium


 In Task Hello. (Status of last Command : fail )
 (type 'ls' for context info) :         
    

Besides sending commands to tasks, you can alter the attributes of any task, program or state machine. The TaskBrowser will confirm validity of the assignment with 'true' or 'false' :

 In Task Hello. (Status of last Command : none )
 (type 'ls' for context info) :the_attribute
      Got :the_attribute
 = Hello World
 In Task Hello. (Status of last Command : none )
 (type 'ls' for context info) :the_attribute = "Veni Vidi Vici !"
      Got :the_attribute = "Veni Vidi Vici !"
 = true

 In Task Hello. (Status of last Command : none )
 (type 'ls' for context info) :the_attribute
      Got :the_attribute
 = Veni Vidi Vici !
    

Finally, let's emit an Event. The Hello World Event requires a payload. A callback handler was registered by the component, thus when we emit it, it can react to it:

 In Task Hello. (Status of last Command : none )
 (type 'ls' for context info) :the_event(the_attribute)
      Got :the_event(the_attribute)
 = true
 In Task Hello. (Status of last Command : none )
 (type 'ls' for context info) :
354.592 [ Info   ][Hello] Receiving Event: Veni Vidi Vici !
    

The example above passed the the_attribute object as an argument to the event, and it was received by our task correctly. Events are related to commands, but allow broadcasting of data, while a command has a designated receiver.

The Data Ports can be accessed through the the_data_port and the_buffer_port object interfaces. Once again, we can inspect the interface of an object by typing its name:

 In Task Hello. (Status of last Command : none )
 (type 'ls' for context info) :the_data_port
      Got :the_data_port

Printing Interface of 'the_data_port' :

  Method     : string Get( )
   Get the current value of this Data Port
  Method     : void Set( string const& Value )
   Set the current value of this Data Port
   Value : The new value.
    

The the_data_port object has two methods: Get() and Set(). Since data ports are used for sending unbuffered data packets between components, this makes sense. One can interact with the ports as such:

 In Task Hello. (Status of last Command : none )
 (type 'ls' for context info) :the_data_port.Set("World")
      Got :the_data_port.Set("World")
 = (void)

 In Task Hello. (Status of last Command : none )
 (type 'ls' for context info) :the_data_port.Get()
      Got :the_data_port.Get()
 = World
    

When a value is Set(), it is sent to whatever is connected to that port, when we read the port using Get(), we see that the previously set value is present. The advantage of using ports is that they are completely thread-safe for reading and writing, without requiring user code. The Hello component also contains a the_buffer_port for buffered data transfer. You are encouraged to play with that port as well.

Remember that the TaskBrowser was a component as well ? When a user enters ls, the interface of the visited component is listed. It is also possible to get an 'outside' view of the visited component, through the eyes of an external component. The leave allows a view from within the TaskBrowser itself:

 In Task Hello. (Status of last Command : none )
 (type 'ls' for context info) :leave
1001.607 [ Info   ][main()] Watching Task Hello

 Watching Task Hello. (Status of last Command : none )
 (type 'ls' for context info) :ls

 Listing TaskBrowser :

 Attributes   : (none)

 Interface    :
  Methods      : isRunning start stop trigger update
  Commands     : (none)
  Events       : (none)

 Objects      :
            this (  )
        the_data_port ( A Task Object. )
      the_buffer_port ( A Task Object. )

 Ports        : the_data_port the_buffer_port
 Hello Peers : TaskBrowser
    

The following things are noteworthy: 'ls' shows now the contents of the TaskBrowser itself and no longer of the Hello Component. The TaskBrowser has the same ports as the component it visist: the_data_port and the_buffer_port. These were created at run-time and allow to write or read ports from the visited component.

One can enter the 'inside' view again by entering enter:

 Watching Task Hello. (Status of last Command : none )
 (type 'ls' for context info) :enter
1322.653 [ Info   ][main()] Entering Task Hello

 In Task Hello. (Status of last Command : none )
 (type 'ls' for context info) :      
    

Last but not least, hitting TAB twice, will show you a list of possible completions, such as peers or commands :

 In Task Hello. (Status of last Command : none )
 (type 'ls' for context info) :
the_attribute     the_event         cd ..         quit
the_buffer_port.  the_method        help          this.
the_command       the_property      leave
the_constant      TaskBrowser.  list
the_data_port.     cd            ls
 (type 'ls' for context info) : 
    

TAB completion works even across peers, such that you can type a TAB completed command to another peer than the current peer.

In order to quit the TaskBrowser, enter quit:

 In Task Hello. (Status of last Command : none )
 (type 'ls' for context info) :quit

1575.720 [ Info   ][ExecutionEngine::setActivity] Hello is disconnected from its activity.
1575.741 [ Info   ][Logger] Orocos Logging Deactivated.      
    

The TaskBrowser Component is application independent, so that your enduser-application might need a more suitable interface. However, for testing and inspecting what is happening inside your real-time programs, it is a very useful tool. The next sections show how you can add properties, commands, methods etc to a TaskContext.

[Note]Note

If you want a more in-depth tutorial, see the 'task-intro' example for a TaskBrowser which visits a network of three TaskContexts.

3. Setting Up a Basic Task

Tasks are implemented by the TaskContext class. It is useful speaking of a context because it defines the context in which an activity (a program) operates. It defines the interface of the task, its properties, its peer tasks and uses its ExecutionEngine to handle its programs and to accept commands from other tasks.

This section walks you through the definition of an example task in order to show you how you could build your own task.

[Important]Important

The ready-to-execute code of this section can be found in the 'simple-task' package on the RTT Download section of the Orocos.org website.

A TaskContext is constructed as :

  #include <rtt/TaskContext.hpp>

  // we assume this is done in all the following code listings :
  using namespace RTT;

  TaskContext a_task("ATask");
      

The argument is the (unique) name of the task.

A task's interface consists of : Commands, Methods, Ports, Attributes and Properties and Events, which are all public. We will refer to them as members.

Figure 2.4.  Schematic Overview of a TaskContext

Schematic Overview of a TaskContext

The Execution Flow is formed by Programs and State Machines using the interface of Peer Tasks. The Data Flow is the propagation of data from one task to another, where one producer can have multiple consumers.

When a TaskContext is running, it accepts commands or events using its Execution Engine. The Execution Engine will check periodically for new commands in it's queue and execute programs which are running in the task. Thus to start using the task, one needs to start the Execution Engine. As long as it is not started, it will accept no commands, run no programs and react to no events.

3.1. Task Application Code

The user may insert his application code in the startup(), update() and shutdown() functions of a TaskContext by inheriting from that class.

  #include <rtt/TaskContext.hpp>
  class MyTask 
    : public TaskContext
  {
  public:
       MyTask(std::string name) 
         : TaskContext(name)
       {
          // see lateron what to put here.
       }
  
       /**
        * This function contains the application's startup code.
	* Return false to abort startup.
	*/
       bool startup() {
          // ...
	  return true;
       }

       /**
        * This function is periodically called.
	*/
       void update() {
          // Your algorithm for periodic execution goes inhere
       }

       /**
        * This function is called when the task is stopped.
	*/
       void shutdown() {
          // Your cleanup code
       }
  };

When a TaskContext is started, startup() is called once and may abort the startup sequence by returning false, for example, because of misconfiguration. When all went well, update() is called (a)periodically by the ExecutionEngine, see below. When the task is stopped, shutdown() is called after the last update().

3.2. Starting Task Execution

The functionality of a task, i.e. its algorithm, is executed by the Execution Engine. To run an ExecutionEngine, you need to use one of the ActivityInterface classes from the CoreLib, for example PeriodicActivity or NonPeriodicActivity. This relation is shown in Figure 2.5, “ Executing a TaskContext ”.

Figure 2.5.  Executing a TaskContext

Executing a TaskContext

You can make a TaskContext '(re-)active' by creating an Activity object which executes its Execution Engine. The Execution Engine delegates all work to specific 'Processors' and user code in update().

The Activity classes will allocate a thread which executes the Execution Engine. The choosen ActivityInterface object will invoke the Execution Engine, which will in turn invoke the application's methods above.

  #include <rtt/PeriodicActivity.hpp>

  using namespace RTT;

  TaskContext* a_task = new MyTask("the_task")
  // create a periodic activity with priority=5, period=1000Hz
  PeriodicActivity act(5, 0.001, a_task->engine() );
  // ... start the execution engine of a_task :
  act.start(); 
  // ...
  act.stop();

Which will run the Execution Engine of "ATask" with a timer frequency of 1kHz. This is the frequency at which state machines are evaluated, program steps taken, commands and events are accepted and executed and the application code is run. When the periodic activity is stopped again, all programs are stopped, state machines are brought into the final state and no more commands or events are accepted.

A TaskContext can also be run in a non periodic activity:

  #include <rtt/NonPeriodicActivity.hpp>

  using namespace RTT;

  TaskContext* a_task = new MyTask("the_task")
  // create a non periodic activity with priority=5
  NonPeriodicActivity nonpAct(5, a_task->engine() );
  // ... start the execution engine of a_task :
  nonpAct.start();
  // ...
  nonpAct.stop();

In case the Execution Engine waits for new Commands or Events to come in to be executed. Each time such an event happens, the user's application code (update()) is called as well.

[Warning]Warning

Non periodic activities should be used with care and with much thought. The ExecutionEngine will do absolutely nothing if no commands or asynchronous events come in. This may lead to surprising 'bugs' when program scripts or state machine scripts are executed, as they will only progress upon these events and seem to be stalled otherwise.

3.3. Interfacing the TaskContext

During development of your TaskContext, it is handy to connect the TaskBrowser to your task such that you can interactively manipulate it and it's properties:

  #include <ocl/TaskBrowser.hpp>
  // ... see above
  TaskBrowser browser(a_task);

  // Start the interactive console:
  browser.loop();

In which you can start/stop the task and manipulate every aspect of it's interface, as was seen in the previous section.

3.4. Introducing the TaskContext's Interface

A TaskContext exists of a number of accessor methods which expose a specific part of the interface. These methods are:

  a_task.ports();
  a_task.methods();
  a_task.attributes();
  a_task.properties();
  a_task.commands();
  a_task.events();
      

The meaning of these methods are explained in the following sections.

3.5. The Data Flow Interface

[Note]Purpose

The 'Data Flow' is a 'stream of data' between tasks. A classical control loop can be implemented using the Data Flow interface. The data is passed buffered or unbuffered from one task to another.

The Orocos Data Flow is implemented with the Port-Connector software pattern. Each task defines its data exchange ports and inter-task connectors transmit data from one port to another. A Port is defined by a name, unique within that task, the data type it wants to exchange and the buffered or un-buffered method of exchanging. Buffered exchange is done by "Buffer" Ports and un-buffered exchange is done by "Data" Ports.

A Data Port can offer read-only, write-only or read-write access to the unbuffered data. A Buffer Port can offer read-only, write-only and read-write access to the buffered data. The example below shows all these possiblities.

3.5.1. Setting up the Data Flow Interface

[Important]Important

The ready-to-execute code of this section can be found in the 'dataflow-task' package on the RTT Download page of the Orocos.org website.

Any kind of data can be exchanged (also user defined types) but for readability, only the 'double' C type is used here.

  #include <rtt/Ports.hpp>
  using namespace RTT;

  class MyTask
    : public TaskContext
  {
    // Read-only data port:
    ReadDataPort<double> indatPort;
    // Write-only data port:
    WriteDataPort<double> outdatPort;
    // Read-Write data port:
    DataPort<double> rwdatPort;

    // Read-only buffer port:
    ReadBufferPort<double> inbufPort;
    // Write-only buffer port:
    WriteBufferPort<double> outbufPort;
    // Read-Write buffer port:
    BufferPort<double> rwbufPort;
  public:
    // ...
    MyTask(std::string name)
      : TaskContext(name),
        indatPort("Data_R"),
	outdatPort("Data_W", 1.0),  // note: initial value
	rwdatPort("Data_RW", 1.0),

	inbufPort("SetPoint_X"),
	outbufPort("Packet_1", 15), // note: buffer size
	rwbufPort("Packet_2", 30)
    {
       this->ports()->addPort( &indatPort );
       this->ports()->addPort( &outdatPort );
       this->ports()->addPort( &rwdatPort );

       this->ports()->addPort( &inbufPort );
       this->ports()->addPort( &outbufPort );
       this->ports()->addPort( &rwbufPort );

       // more additions to follow, see below
     }

     // ...
  };

The example starts with declaring all the ports of MyTask. A template parameter specifies the type of data the task wants to exchange through that port. Logically, if two tasks are connected, they must agree on this type. The constructor of MyTask initialises each port with a name. This name is used to 'match' ports between connected tasks. Orocos will warn at run-time if names or types do not match.

Write and read-write Buffers take besides a name, the prefered buffer size as a parameter. In the example, these are the values 15 and 30. Before the task is connected to its peers, you can still change this value with the setBufferSize() function of the Port.

Finally, a 'write' port can take an initial value in the constructor as well. This value will be used when the connection between two ports is created in order to initialise the connection ( using 'connectPorts', see Section 4, “Connecting TaskContexts” ). If a 'write' port is connected to an existing connection, the initial value (and buffer size) are ignored and the settings of the existing connection are not touched. You can modify the initial value with the 'Set( value )' function in both Buffer and Data write ports.

[Note]Note

Alternatively, you can connect buffers in your main() program by writing:

  #include <rtt/ConnectionFactory.hpp>
  // ...
  int buf_size = 100;
  ConnectionFactory<double> cf;
  ConnectionInterface::shared_ptr con =
    cf.createBuffer(a_task->ports()->getPort("SetPoint_X"), 
                    b_task->ports()->getPort("SetPoint_X"), 
                    buf_size);
  if (con)
     con->connect();

before the tasks are connected. This connection will then take precedence.

3.5.2. Using the Data Flow Interface in C++

The Data Flow interface is used by your task from within the program scripts or its update() method. Logically the script or method reads the inbound data, calculates something and writes the outbound data.

  #include <rtt/Ports.hpp>
  using namespace RTT;

  class MyTask
    : public TaskContext
  {
     // ...Constructor sets up Ports, see above.

     bool startup() {
       // Check validity of (all) Ports:
       if ( !indatPort.connected() ) {
          // No connection was made !
	  return false;
       }
       if ( !outdatPort.connected() ) {
          // ...
       }
     }

     bool update() {
       // Read and write the Data Flow:
       // Unbuffered:
       double val = indatPort.Get();
       // calculate...
       outdatPort.Set( val );

       // Buffered:
       if ( inbufPort.Pop( val ) ) {
          // calculate...
       } else {
          // buffer empty.
       }

       if ( outbufPort.Push( val ) ) {
          // ok.
       } else {
          // buffer full.
       }
     }
     // ...
  };

It is wise to check in the startup() function if all necessary ports are connected() ( or ready() ). At this point, the task startup can still be aborted by returning false. Otherwise, a write to a port will be discarded, while a read returns the initial value or the default value. A Pop of a disconnected port will always return false.

3.5.3. Using Data Flow in Scripts

When a Port is connected, it becomes available to the Orocos scripting system such that (part of) the calculation can happen in a script. Also, the TaskBrowser can then be used to inspect the contents of the DataFlow online.

A small program script could be loaded into MyTask with the following contents:

  program MyControlProgram {
    double the_K  = K        // read task property, see later.
    double setp_d

    while ( true ) {
      if ( SetPoint_X.Pop( setp_d ) ) { // read Buffer Port
        double in_d = Data_R.Get()      // read Data Port
        double out_d = (setp_d - in_d) * K  // Calculate
        do Data_W.Set( out_d )          // write Data Port
      }
      do nothing       // this is a 'wait' point.
    }
  } 

The program "MyControlProgram" starts with declaring two variables and reading the task's Property 'K'. Then it goes into an endless loop, trying to Pop a setpoint value from the "SetPoint_X" Buffer Port. If that succeeds (buffer not empty) the "Data_R" Data Port is read and a simple calculation is done. The result is written to the "Data_W" Data Port and can now be read by the other end. Alternatively, the result may be directly used by the Task in order to write it to a device or any non-task object. You can use methods (below) to send data from scripts back to the C++ implementation.

Remark that the program is executed within the Execution Engine. In order to avoid the endless loop, a 'wait' point must be present. The "do nothing" command inserts such a wait point and is part of the Scripting syntax. If you plan to use Scripting state machines, such a while(true) loop and hence wait point is not necessary. See the Scripting Manual for a full overview of the syntax.

3.6. The Method Interface

[Note]Purpose

A task's methods are intended to be called 'synchronously' by the caller, i.e. are directly executed like a function. Use it to 'calculate' a result or change a parameter.

The easiest way to access a TaskContext's interface is through Methods. They resemble very much normal C or C++ functions, but they have the advantage to be usable in scripting or can be called over a network connection. They take arguments and return a value. The return value can in return be used as an argument for other Methods or stored in a variable. For all details, we refer to the Orocos Scripting Manual.

To add a TaskContext's method to the method interface, one proceeds similarly as when creating Data Ports. The data type is now replaced by a function signature, for example '

void(int, double)

' which is the signature of a function returning 'void' and having two arguments: an 'int' and a 'double'.

  #include <rtt/Method.hpp>
  using namespace RTT;

  class MyTask
    : public TaskContext
  {
    public:
    void reset() { ... }
    string getName() const { ... }
    double changeParameter(double f) { ... }
    // ...

    Method<void(void)> resetMethod;
    Method<string(void)> nameMethod;
    Method<double(double)> paramMethod;

    MyTask(std::string name)
      : TaskContext(name),
      resetMethod("reset", &MyTask::reset, this),
      nameMethod("name", &MyTask::getName, this),
      resetMethod("changeP", &MyTask::changeParameter, this)
    {
       // Add the method objects to the method interface:
       this->methods()->addMethod( &resetMethod, "Reset the system.");
       this->methods()->addMethod( &nameMethod, "Read out the name of the system.");
       this->methods()->addMethod( &changeP,
                                   "Change a parameter, return the old value.",
	                           "New Value", "The new value for the parameter."); 

       // more additions to follow, see below
     }
     // ...
  };

In the above example, we wish to add 3 class functions to the method interface: reset, getName and changeParameter. This can be done by constructing a Method object with the correct function signature for each such class function. Each Method object is initialised in the constructor with a name ("reset"), a pointer to the class function (&MyTask::reset) and a pointer to the class object (this). This setup allows the method objects resetMethod, nameMethod and paramMethod to be invoked just like one would call the functions directly.

After the method objects are constructed, we add methods to the method interface using the addMethod() function. The addMethod() function requires a a method object (&resetMethod), a description ("Reset the system.") and a name, description pair for each argument (such as in changeParameter).

Using this mechanism, any method of any class can be added to a task's method interface.

3.6.1. Invoking Methods in C++

In order to easily invoke a task's methods from a C++ program, only only needs a pointer to a TaskContext object, for example using the 'getPeer()' class function.

  // create a method:
  TaskContext* a_task_ptr;
  Method<void(void)> my_reset_meth 
       = a_task_ptr->methods()->getMethod<void(void)>("reset");

  // Call 'reset' of a_task:
  reset_meth();  

Methods can also be given arguments and collect return values. Both constant arguments and variable arguments are supported.

  // used to hold the return value:
  string name;
  Method<string(void)> name_meth = 
    a_task_ptr->methods()->getMethod<string(void)>("name");

  // Call 'name' of a_task:
  name = name_meth(); 

  cout << "Name was: " << name << endl; 

  // hold return value.
  double oldvalue;
  Method<double(double)> mychange_1 =
      a_task.methods()->create("changeP");

  // Call 'changeParameter' of a_task with argument '1.0'
  oldvalue = mychange_1( 1.0 );
  // oldvalue now contains previous value.

Up to 4 arguments can be given. If the signature was not correct, the method invocation will be ignored. One can check validity of a method object with the 'ready()' function:

  Method<double(double)> mychange_1 = ...;
  assert( mychange_1.ready() );

3.6.2. Invoking Methods in Scripts

To invoke methods from a script, one can then write :

  do ATask.changeP( 0.1 )
  // or :
  set result = ATask.changeP( 0.1 ) // store return value 

3.7. Method Argument and Return Types

The arguments can be of any class type and type qualifier (const, &, *,...). However, to be compatible with the Orocos Program Parser variables, it is best to follow the following guidelines :

Table 2.1. Method Return & Argument Types

C++ TypeIn C++ functions passed byMaps to Parser variable type
Primitive C types : double, int, bool, charvalue (no const, no reference )double, int, bool, char
C++ Container types : std::string, std::vector<double>const &string, array
Orocos Fixed Container types : RTT::Double6D, KDL::[Frame | Rotation | Twist | ... ]const &double6d, frame, rotation, twist, ...

Summarised, every non-class argument is best passed by value, and every class type is best passed by const reference. The parser does handle references (&) in the arguments or return type as well.

3.8. The Attributes and Properties Interface

[Note]Purpose

A task's attributes and properties are intended to configure and tune a task with certain values. Properties have the advantage of being writable to an XML format, hence can store 'persistent' state. For example, a control parameter. Attributes are lightweight values which can be read and written during runtime, for example, the current measured temperature.

A TaskContext may have any number of attributes or properties, of any type. They can be used by programs in the TaskContext to get (and set) configuration data. The task allows to store any C++ value type and also knows how to handle Property objects. Attributes are plain variables, while properties can be written to and updated from an XML file.

3.8.1. Adding Task Attributes or Properties

An attribute can be added in the task's interface (AttributeRepository) like this :

  #include <rtt/Property.hpp>
  #include <rtt/Attribute.hpp>

  class MyTask
    : public TaskContext
  {
     Attribute<bool> aflag;
     Attribute<int> max;

     Constant<double> pi;

     Property<std::string> param;
     Property<double> value;
  public:
    // ...
    MyTask(std::string name)
      : TaskContext(name),
        param("Param","Param Description","The String"),
	value("Value","Value Description", 1.23 ),
	aflag("aflag", false),
	max( "max", 5 ),
	pi( "pi", 3.14 )
    {
       // other code here...

       this->attributes()->addAttribute( &aflag );
       this->attributes()->addAttribute( &max );
      
       this->attributes()->addConstant( &pi );

       this->properties()->addProperty( &param );
       this->properties()->addProperty( &value );
     }
     // ...
  };

Which inserts an attribute of type bool and int, name 'aflag' and 'max' and initial value of false and 5 to the task's interface. A constant 'pi' is added as well. The methods return false if an attribute with that name already exists. Adding a Property is also straightforward. The property is added in a PropertyBag.

3.8.2. Accessing Task Attributes or Properties in C++

To get a value from the task, you can use the set() and get() methods :

  bool result = aflag.get();
  assert( result == false );

  param.set("New String");
  assert( param.get() == "New String" );

While another task can access it through the attributes() interface:

  Attribute<bool> the_flag = a_task->attributes()->getAttribute<bool>("aflag");
  assert( the_flag.ready() );

  bool result = the_flag.get();
  assert( result == false );

  Attribute<int> the_max = a_task->attributes()->getAttribute<int>("max");
  assert( the_max.ready() );
  the_max.set( 10 );
  assert( the_max.get() == 10 );

The attributes 'the_flag' and 'the_max' are called 'mirrors' of the original attributes of the task.

3.8.3. Accessing Task Attributes in Scripts

A program script can access the above attributes as in

  // a program in "ATask" does :
  var double pi2 = pi * 2.
  var int    myMax = 3
  set max = myMax

  set Param = "B Value"

  // an external (peer task) program does :
  var double pi2 = ATask.pi * 2.
  var int    myMax = 3
  set ATask.max = myMax	

When trying to assign a value to a constant, the script parser will throw an exception, thus before the program is run. You must always specify the task's name (or 'task') when accessing a task's attribute, this is different from methods and commands, which may omit the task's name if the program is running within the task.

[Important]Important

The same restrictions of Section 3.7, “Method Argument and Return Types” hold for the attribute types, when you want to access them from program scripts.

3.8.4. Storing and Loading Task Properties

See Section 6.1, “Task Property Configuration” for storing and loading the Properties to and from files, in order to store a TaskContext's state.

3.9. The Command Interface

[Note]Purpose

A task's commands are intended to be called 'asynchronously', thus sent by the caller to the receiver. Use it to 'reach a goal' in the receiver, typically this takes time to accomplish. Command functions are, in contrast with methods, executed by the receiver.

The command interface is very similar to the Method interface above.

3.9.1. Adding Commands to a TaskContext

To add a command to the Command Interface, one must create Command, objects :

  #include <rtt/Command.hpp>

  class MyTask
    : public TaskContext
  {
  public:
    /**
     * The first command starts a cycle.
     */
    bool startCycle() { ... }
    bool cycleDone() const { ... }

    Command<bool(void)> cycleCommand;

    /**
     * Another command cleans stuff up.
     */
    bool cleanupMess(double f) { ... }
    bool isMessCleaned() const { ... }

    Command<bool(double)> messCommand;

  public:
    MyTask(std::string name)
      : TaskContext(name),
        cycleCommand("startCycle",
	             &MyTask::startCycle,
                     &MyTask::cycleDone, this),
        messCommand( "cleanup",
                     &MyTask::cleanupMess,
                     &MyTask::isMessCleaned, this)
    {
      // ... other startup code here

      this->commands()->addCommand( &cycleCommand,
                                    "Start a new cycle.");
      this->commands()->addCommand( &messCommand,
                                    "Start cleanup operation.",
	                            "cfactor", "A cfactor denoting the thoroughness.");
    }
  };

Commands differ from Methods in that they take an extra function which is called the Completion Condition. It is a function which returns true when the command is done. The command itself also returns a boolean which indicates if it was accepted or not. Reasons to be rejected can be faulty arguments or that the system is not ready to accept a new command.

The Command object requires two member pointers instead of one, which must both return a 'bool'. The first one is the command function that does the actual work, and the completion condition is a function having :

  • exactly the same arguments as the command,

  • OR only the first argument of the command,

  • OR no arguments at all.

Analogous to addMethod(), addCommand adds the Command objects to the TaskContext interface and also requires a string describing the command, and two strings giving a name and description for every argument.

3.9.2. Invoking Commands in C++

Once a command is added to a TaskContext's interface, other tasks can make use of that command.

The Command class can be used to invoke commands as well. You can get such object from a task's interface:

  Command<bool(void)> mycom 
     = a_task.commands()->getCommand<bool(void)>("startCycle");
  // check if the command is ok:
  assert( mycom.ready() );

  // Send 'startCycle' to a_task (asynchronous).
  bool result = mycom();
  // next, check its status:
  bool accepted = mycom.accepted(); // accepted by execution engine?
  bool valid = mycom.valid();       // command was valid (well-formed)?
  bool done = mycom.done();         // command was done?
      

Such commands can also be given arguments. Both constant arguments and variable arguments are supported:

  // get a command:
  Command<bool(double)> mycleanup_1 =
      a_task.commands()->getCommand<bool(double)>("cleanup");

  // Send 'cleanup' to a_task with argument '1.0'
  bool result_1 = mycleanup_1( 1.0 );

  bool d_arg = 5.0;
  // Send 'cleanup' to a_task, reads contents of d_arg.
  bool result_2 = mycleanup_1(d_arg);

The current implementation supports up to 4 arguments. Since the use of 'structs' is allowed, this is enough for most applications.

3.9.3. Invoking Commands from Scripts

The above lets you write in a program script :

  do startCycle()
  do cleanup( 0.1 )	

when the program is loaded in a_task.

Commands returning false will propagate that error to the program or function calling that command, which will cause the program to enter an error state, ie it stops its execution.

[Important]Important

The same restrictions of Section 3.7, “Method Argument and Return Types” hold for the command and condition types, when you want to access them from program scripts.

3.10. The Event Interface

[Note]Purpose

A task's events are intended to be 'emitted', thus published by the task to subscribers. Use it to 'notify' interested parties of a change in the system.

A task may register its events in its interface in order to be used by its state machines and other tasks as well. Events are defined and explained in the Orocos CoreLib Reference Manual.

3.10.1. Adding Events

Events can be easily added to a task's interface, much like methods are:

  #include <rtt/Event.hpp>

  class MyTask
    : public TaskContext
  {
    // An event with a bool argument:
    Event< void(bool) > turnSwitch;
    // An event with three arguments:
    Event< bool(double, double, double) > moveAxis;
  public:
    MyTask(std::string name)
      : TaskContext(name),
        turnSwitch( "turnSwitch" ),
	moveAxis( "move" )
    {
      // ... other startup code here
  
      // add it to the task's interface:
      this->events()->addEvent( &turnSwitch, 
                                "Turn switch description", 
                                "d","Direction" ); 
      this->events()->addEvent( &moveAxis,
                                "x","X axis position",
				"y","Y axis position",
				"z","Z axis direction");
    }
  };

An Event object has the signature ('void(bool)') of the 'callback function' it will call when the event is 'emitted' (or 'fired'). The object is initialised with a name ("turnSwitch") and added to the interface ('addEvent').

3.10.2. Emitting Events in C++

Once events are added, they can be emitted using the Event object.

  Event< bool(double, double, double) > move_event 
    = a_task.events()->getEvent( "move" );
  assert( move_event.ready() );

  // emit the event 'move' with given args:
  move_event(1.0, 2.0, 3.0);

  // or with variable arguments:
  double a = 0.3, b = 0.2, c = 0.1;
  move_event(a, b, c); 

3.10.3. Reacting to Events in C++

Analogous to emitting an event, one can also react to an event in C++, using the Event interface. Event connections can be accessed through the Handle object. The first example shows how to setup a synchronous connection to the event of the previous examples:

   #include <boost/bind.hpp> 
  /** 
   * Example: Connect a class method to an Event.  
   */ 
  class Reactor 
  { 
  public: 
     bool react_callback(double a1, double a2, double a3) { 
        // use a1,a2, a3 
	return false; // return value is ignored.
     } 
  };

  /**
   * Example: Connect a 'C' function to an Event.
   */
  bool foo_callback( double a1, double a2, double a3 ) {
      // use a1, a2, a3
      return false; // ignored.
  }

  // Class callback:
  Reactor r;

  Handle h
    = a_task.events()->setupConnection("move")
                 .callback( &r, &Reactor::react_callback )
                 .handle();
  assert( h.ready() );
    
  h.connect(); // connect to event "move"

  move_event(1.0, 2.0, 3.0); // see previous example.

  // now Reactor::callback() was called.

  h.disconnect(); // disconnect again.

  // 'C' Function callback:
  h = a_task.events()->setupConnection("move")
                 .callback( &foo_callback )
                 .handle();

  h.connect();
  move_event(4.0, 5.0, 6.0)

  // now foo_callback is called with arguments.	
[Note]Note

Using the boost::bind function is not yet supported in this interface. You must provide the object and function separately in callback().

Analogous to the event example in the CoreLib Reference Manual, a class is made to react to the event. A connection is setup between the "move" event and the react_callback function of "r". The connection can be controlled using the handle to connect or disconnect the reaction to events. When connect() is called, every event invocation will call react_callback() with the given arguments. Using a 'C' function works analogous as shown above.

A second example continues, but uses an asynchronous connection. First a new task (b_task) is created which will handle the event asynchronously. During setup, the EventProcessor of b_task's Execution Engine is used to process the event.

  TaskContext b_task("BTask");
  PeriodicActivity ptask_b(5, 0.1); // priority, period
  ptask_b.run( &b_task );
  ptask_b.start();

  Handle h3 
    = a_task.events()->setupConnection("move")
                            .callback(&r, &react_callback,
                                      b_task.engine()->events() ).handle();
    
  assert( h3.ready() );

  h3.connect(); // connect asynchronously to event "move"

  move_event.emit(); // see previous example.

  // wait a bit...

  // now react_callback() was called from within b_task's execution engine.
	

Note that after passing the object and function, the EventProcessor of b_task is added in the callback method, such that the callback is executed in b_task's thread.

3.10.4. Using Events from Scripts

Events are as easy to use as methods (above) from within scripts, using the keyword do:

  do ATask.move( 1.0, 2.0, 3.0 )

It is also possible to react to events from within a state machine in order to change state. We refer to the Program Parser Manual for syntax and examples.

4. Connecting TaskContexts

A Real-Time system contains multiple concurrent tasks which must communicate to each other. TaskContext can be connected to each other such that they can communicate Real-Time data.

4.1. Setting up the Execution Flow

The addPeer and connectPeers functions are used to connect TaskContexts and allow them to use each other's interface. The connectPorts funtion sets up the data flow between tasks.

We call connected TaskContexts "Peers" as there is no fixed hierarchy. A connection from one TaskContext to its Peer can be uni- or bi-directional. In a uni-directional connection (addPeer ), only one peer can use the interface of the other, while in a bi-directional connection (connectPeers), both can use each others interface. This allows to build strictly hierarchical topological networks as well as complete flat or circular networks or any kind of mixed network.

Peers are connected as such (hasPeer takes a string argument ):

  // bi-directional :
  connectPeers( &a_task, &b_task );
  assert( a_task.hasPeer( &b_task.getName() ) 
          & b_task.hasPeer( &a_task.getName() ) );
  // uni-directional :
  a_task.addPeer( &c_task );
  assert( a_task.hasPeer( &c_task.getName() ) 
          & ! c_task.hasPeer( &a_task.getName() ) );

  // Access the interface of a Peer:
  Method<bool(void) m = a_task.getPeer( "CTask" )->methods()->getMethod<bool(void)>("aMethod");
  // etc. See interface usage in previous sections.

Both connectPeers and addPeer allow scripts or C++ code to use the interface of a connected Peer. connectPeers does this connection in both directions.

From within a program script, peers can be accessed by merely prefixing their name to the member you want to access. A program within "ATask" could access its peers as such :

  var bool result = CTask.aMethod() 

The peer connection graph can be traversed at arbitrary depth. Thus you can access your peer's peers.

4.2. Setting up the Data Flow

Data Flow between TaskContexts is setup by using connectPorts. The direction of the data flow is imposed by the read/write direction of the ports. The connectPorts(TaskContext* A, TaskContext* B) function creates a connection between TaskContext ports when both ports have the same name and type. It will never disconnect existing connections and only tries to add ports to existing connections or create new connections.

Before calling connectPorts, one may connect individual ports using a_port.connectTo(&b_port); when complexer data flow networks need to be formed.

Example 2.1. TaskContext Data Flow Topology Example

This diagram shows some possible topologies. Four tasks, "A", "B", "C" and "D" have each a port "MyData" and a port "MyData2". The example demonstrates that connections are always made from writer (sender) to reader (receiver).

Example data flow networks.

The first network has two writers and two readers for "MyData". It can be formed starting from "A" and adding "B","C" and "D" as peers of "A" respectively. Since the network started from "A" all peers share the same connection. The same network could have been formed starting from "B". The second diagram connects "A" to "C" and then "B" to "D". Two connections are now made and if the application tries to connect "B" to "C", this will fail since the "MyData" Port of "C" already participates in a connection.

The third network has one writer and three readers for "MyData2". It can now only be formed starting from "D" and adding "A","B" and "C" as peers to "D". Combining both network one and three is possible by merely invoking all the 'addPeer' methods in the correct order.

connectPorts tries to create connections in both directions. If a task's Port already has a connection, any task with compatible, unconnected ports will be added to that connection. For example, if "a_task" and "b_task" exchange a Data type "Data_X", and "c_task" reads "Data_X", the connectPorts will forward "Data_X" to "c_task" as well.

4.3. Disconnecting Tasks

Tasks can be disconnected from a network by invoking disconnect() on that task. It will inform all its peers that it has left the network and disconnect all its ports. This does not mean that all data flow connections are deleted. As long as one task's port still participates in a connection, the connections exist. When the last port disconnects, the data flow connection is cleaned up as well.

Example 2.2. TaskContext Peer Disconnection Example

(1) shows what would happen if tasks "A" and "B" are disconnected from the network. (2) shows what would happen if the connection itself is disconnected.

Disconnecting tasks: one can disconnect a whole task or disconnect only a port or connection of a task.

When A.disconnect() is called (1), it removes its participation from the "MyData" connection. The same happens for B.disconnect(). "C" and "D" read then from a connection which has no more writers. Adding "A" again to the network would make "A" again a writer of "MyData". If both "C" and "D" call disconnect as well, the "MyData" connection is cleaned up.

  a_task.disconnect();
  assert( !a_task.hasPeer( &b_task.getName() ) 
          && !b_task.hasPeer( &a_task.getName() );

  b_task.disconnect();
  assert( !c_task.hasPeer( &b_task.getName() ) 
          && ! d_task.hasPeer( &b_task.getName() );

Data Flow connections can be disconnected (2) as well, in which case all ports are disconnected.

  ConnectionInterface::shared_ptr con = a_task.ports()->getPort("MyData")->connection();
  if (con)
    con->disconnect();

  assert( !a_task.ports()->getPort("MyData").connected() );
  assert( !b_task.ports()->getPort("MyData").connected() );
  assert( !c_task.ports()->getPort("MyData").connected() );
  assert( !d_task.ports()->getPort("MyData").connected() ); 

5. Deploying Components

An Orocos component can be used in both embedded (<1MB RAM) or big systems (128MB RAM), depending on how it is created or used. This is called Component Deployment as the target receives one or more component implementations. The components must be adapted as such that they fit the target.

5.1. Overview

Figure 2.6, “ Component Deployment Levels ” shows the distinction between the three levels of Component Deployment.

Figure 2.6.  Component Deployment Levels

Component Deployment Levels

Three levels of using or creating Components can be accomplished in Orocos: Not distributed, embedded distributed and fully distributed.

If your application will not use distributed components and requires a very small footprint, the TaskCore can be used. The Orocos primitives apear directly in the interface and are called upon in a hard-coded way.

If you application requires a small footprint and distributed components, the C++ Interface of the TaskContext can be used in combination with a Distribution Library which does the network translation. It handles a predefined set of data types (mostly the 'C' types) and needs to be adapted if other data types need to be supported.

If footprint is of no concern to your application and you want to distribute any component completely transparantly, the TaskContext can be used in combination with a Remoting Library which does the network translation. A CORBA implementation of such a library is being developed on. It is a write-once, use-many implementation, which can pick up user defined types, without requiring modifications. It uses the Orocos Type System to manage user defined types.

5.2. Embedded TaskCore Deployment

A TaskCore is nothing more than a place holder for the Execution Engine and application code functions (startup(), update() and shutdown() ). The Component interface is built up by placing the Orocos primitives as public class members in a TaskCore subclass. Each component that wants to use this TaskCore must get a 'hard coded' pointer to it (or the interface it implements) and invoke the command, method etc. Since Orocos is by no means informed of the TaskCore's interface, it can not distribute a TaskCore.

5.3. Embedded TaskContext Deployment: C++ Interface

Instead of putting the Orocos primitives in the public interface of a subclass of TaskCore, one can subclass a TaskContext and register the primitives to the C++ Interface. This is a reduced interface of the TaskContext, which allows distribution by the use of a Distribution Library.

[Note]Note

The code presented is for commands, but can be equally applied for methds by using methods()->addMethod( & method ) for events and for each other Orocos primitive.

The process goes as such: A component inherits from TaskContext and has some Orocos primitives as class members. Instead of calling:

  commands()->addCommand(&com, "Description", "Arg1","Arg1 Description",...);

and providing a description for the primitive as well as each argument, one writes:

  commands()->addCommand( &com );

This is no more than a pointer registration, but already allows all C++ code to use the added primitive.

In order to access the interface of such a Component, the user code may use:

  taskA->commands()->getCommand("Name");

In order to distribute this component, an implementation of the Distribution Library is required. The specification of this library, and the application setup is in left to another design document.

5.4. Full TaskContext Deployment: Dynamic Interface

In case you are building your components as instructed in this manual, your component is ready for distribution as-is, given a Remoting library is used. The Orocos CORBA package implements such a Remoting library.

5.5. Putting it together

Using the three levels of deployment in one application is possible as well. To save space or execution efficiency, one can use TaskCores to implement local (hidden) functionality and export publicly visible interface using a Taskcontext. Figure 2.7, “ Example Component Deployment. ” is an small example of a TaskContext which uses two TaskCores to delegate work to. The Execution Engines may run in one or multiple threads.

Figure 2.7.  Example Component Deployment.

Example Component Deployment.

6. Using Tasks

This section elaborates on the interface all Task Contexts have from a 'Task user' perspective.

6.1. Task Property Configuration

As was seen in Section 3.8, “The Attributes and Properties Interface”, Property objects can be added to a task's interface. To read and write properties from or to files, you can use the TaskContext class' methods. It creates or reads files in the XML Component Property Format such that it is human readable and modifiable.

  // ...
  TaskContext* a_task = ...
  a_task->readProperties( "PropertyFile.cpf" );
  // ...
  a_task->writeProperties( "PropertyFile.cpf" ); 

Where readProperties() reads the file and updates the task's properties and writeProperties() updates the given file with the properties of the task. It is allowed to share a single file with multiple tasks or update the task's properties from multiple files.

6.2. Task Scripts

Orocos supports two types of scripts:

  • An Orocos Program Script (ops) contains a Real-Time functional program which calls methods and sends commands to tasks, depending on classical functional logic.

  • An Orocos State machine Description (osd) script contains a Real-Time (hierarchical) state machine which dictates which program script snippets are executed upon which event.

Both are loaded at runtime into a task. The scripts are parsed to an object tree, which can then be executed by the ExecutionEngine of a task.

6.2.1. Program Scripts

Program can be finely controlled once loaded in the