The Orocos Open Realtime Control Services Developer'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 delves deeply into the Orocos ( Open RObot COntrol Software ) infrastructure. It targets developers which wish to extend or understand Orocos. It is complementary to the user's manuals.


Chapter 1. How to Read this Manual

This manual is for Software developers who wish to extend the Orocos framework.

The first Chapter details some aspects of the build system, for those who want full controll over footprint.

The second Chapter details the classes and concepts of the OS abstraction layer. It is useful to understand the Orocos threading mechanism and how Orocos can be ported to other platforms.

The third and fourth Chapters document the Core Library and Task Infrastructure and are the same as the one found in the Component Builder's manual.

The fifth Chapter shows some mechanisms on how to use CoreLib primitives behind the scenes in the Task Infrastructure.

The sixth Chapter details the workings of the Execution Engine and how it uses the various 'Processor' types to handle program execution and communication.

The last Chapter details on how to extend the Orocos Parser framework with user defined types.

1. Code directory structure: the Packages

The functionality available in Orocos is structured in a number of sub-projects; each of these is called a (Orocos) Package. A Package contains a coherent and related set of functionality, in the form of documentation and class libraries.

The packages directory is found under the orocos-rtt directory. The orocos-rtt directory holds some general documentation in the doc directory (like this manual) and scripts for setting up a working packages directory.

Chapter 2. Inside the Build System

Abstract

This document explains how the packages of Orocos, the Open RObot COntrol Software project must be installed and configured.

1. Advanced Build Options

[Warning]Warning

This section is not intended for classical users. Read the 'Getting Started' manual first.

This section groups some technical installation instructions for Orocos developers or persons who wish to control tightly what is built.

1.1. Using the Graphical configtool program

When the target is selected with configure, you can run make configure_packages to pop up the GUI configtool. You need to install this program on your system. Some things to do are :

  • Add/Remove a Package to/from your configuration : click on Build->Packages. Only packages in the repository can be added or removed. This operation does not remove a package from the repository.

  • Install a new Package in the repository : click on Tools->Administration, this allows you to select an .epk file from your hard disk, which will be added to your packages repository.

1.2. Upgrading Your Configuration

In between Orocos versions, configuration options may be added or removed to reflect feature additions or removal. In that case, your current orocos.ecc file will not be properly parsed by the tools. To solve this, you need to run

  make upgrade_packages

such that your old configuration is imported in the new one. After an upgrade, your selected template, added packages and so on will remain as before.

[Note]Note

You can manually save your configuration using ecosconfig export /tmp/savefile.ecc in your old build tree, followed by make new_packages and ecosconfig import /tmp/savefile.ecc in your new build tree. ( manually invoking ecosconfig requires setting ECOS_REPOSITORY ) to your packages directory).

1.3. Introduction to the eCos tool chain

This section goes into more detail how package management is done behind the scenes and is optional literature for the average user.

The above make commands are optional. Take a look at the Makefile to see what they really do. A standard eCos configuration goes as follows :

  1. Set the environment variable :

    export ECOS_REPOSITORY=/path/to/orocos-1.0.3/packages/

    All The ecos tools need this variable to be able to work on the packages. The make targets did this automatically, but if you want to invoke the tools yourself, you need to set it once.

  2. Install any new .epk packages using

    ecosadmin.tcl add filename.epk

    in the packages directory.

    This only adds a package to the repository, it does not mean it is automatically compiled ! Adding a package to a configuration in the next steps will assure that it is compiled.

  3. Go to the eCos build directory. In Orocos this is :

    orocos-1.0.3/build/packages

    But technically, it can be any directory where you create an orocos.ecc configuration file.

  4. Start a new build using the

    ecosconfig new gnulinux  corelib-os

    command. Where gnulinux or lxrt is the target name and corelib-os is the template name. A template selects a number of packages which belong together. You are allowed to put your own template in orocos-1.0.3/packages/templates. All this information (with default options) is written in the orocos.ecc file

    [Warning]Warning

    This will overwrite any previous orocos.ecc configuration file in that directory !

  5. Add all detected support packages. The make target new_packages does this by running :

      for i in $(find $ECOS_REPOSITORY/support -name "*.cdl"); do \
      ecosconfig add support_$(basename $i .cdl); done 

    They will all be added to the orocos.ecc file. Fortunately, you can also add the support (or any other) packages in the graphical configtool menu->Build->Packages.

  6. Select additional packages for your build with the graphical configtool or with

    ecosconfig add package_name

    Configtool will give you a list of all available packages in the repository if you go to menu->Build->Packages and

    ecosconfig list

    does the same. This will add the package and its options to the orocos.ecc file (which contains configuration info of each selected package). Now the packages will show up when you run configtool or run ecosconfig check.

  7. Configure the selected packages with the graphical program

    configtool orocos.ecc

    Save and exit.

  8. Create the makefiles using

    ecosconfig tree

    The build tree is setup now. You need to run this command each time you change the configuration in the above step !

  9. Compile using

    make

  10. The results (headers and libtarget.a) are in the install directory.

1.4. Installing Orocos from Subversion (similar to CVS)

This section will do its best to help you through the Subversion ( svn ) installation process. Installing Orocos from SVN is optional and requires some familiarity with the tools we use. It is only advised for Orocos developers.

The commands below show how to obtain the orocos-trunk Package Tree from the Subversion tree (read-only)

  svn co http://svn.mech.kuleuven.be/repos/orocos/orocos-trunk 
  cd orocos-trunk 

So, make sure you have the Subversion program installed, i.e., the command "svn" works on your system. The repository is web-browsable through a ViewCVS frontend .

The next step requires that you use automake version 1.9 and autconf version 2.54 or later. You can check your versions with the --version option:

  $ automake --version
  automake (GNU automake) 1.9.5
   ...
  $ autoconf --version
  autoconf (GNU Autoconf) 2.54	

[Note]Note

You need these programs if you intend to modify the makefiles

When you get something similar, proceed with calling the ./autogen.sh command in the base directory , in order to initialize the autoconf and automake files:

  ./autogen.sh

The configure script will detect the installed software on your system and generate the appropriate scripts.

The next steps are exactly the same as when you install the system starting from a so-called “epk” package, as explained in the next section. Of course, you don't have to apply the ecosadmin.tcl command when installing from SVN.

Chapter 3. Inside The Core Library

Abstract

This document explains the design and implementation of the Core Library of Orocos, the Open RObot COntrol Software project. The CoreLib provides infrastructural support for the functional and application components of the Orocos framework.

1. Introduction

This document contains some bits about the design of the CoreLib.

2. Multi-Threading

2.1. Priority Inversions

A Priority inversion is the term used to indicate a scheduling situation in which a high priority thread is blocked on a resource which is held by a low priority thread, while a medium priority thread is running, preventing the low priority thread to free the resource for the high priority thread.

The result is an inverted priority because a medium priority thread is running while the high priority thread should be runnen, hence, the medium priority thread has, in practice, a higher priority than the high priority thread.

There are roughly said two solution to this problem. 1. Do not block on resources from high priority threads. 2. Use priority inheritance, where a thread gets the priority of the highest priority thread being blocked on a resource it holds. Once it releases the resource, its priority goes back to normal and the high priority thread can resume.

In essence, Orocos does not know of priority inversions and does not know if the underlying Operating System properly solves this common situation. Furthermore, it can be prooven that there are situations where priority inheritance does not work. Therefore, we try to provide as much as possible lock-free implementations of inter-thread messaging. Table 3.1, “Classes Possibly Subject to Priority Inversion” lists the know uses of Orocos which might lead to priority inversion.

Table 3.1. Classes Possibly Subject to Priority Inversion

Class/methodRationale
DataObjectLocked

Uses Mutex for serialising concurrent access. Alternative Lock-free implementations are possible.

PeriodicActivity::start(), PeriodicActivity::stop()

Uses Mutex for serialising concurrent access. Alternative implementation are incorrect, since stop() guarantees that finalize() will be called after the last step(), hence a mutex/semaphore is used such that it will block until the step() returns and then call finalize().

Logger

Uses Mutex for serialising concurrent access. No Alternative implementation is possible, Std C++ IO must be serialised.

Table 3.2, “Classes Not Subject to Priority Inversion” shows communication infrastructure in Orocos which is especially designed to be lock-free and which is thus not subject to priority inversions. It is our aim to shrink the former table and grow the latter in Orocos' development lifetime.

Table 3.2. Classes Not Subject to Priority Inversion

Class/methodRationale
DataObjectLockFree

Uses a single writer, multiple reader Lock-free implementation. A read only returns the last written value. Used by the ControlKernel application to communicate data between Components.

AtomicQueue

Uses Compare And Swap (CAS) to store object pointers in an atomic queue. Used by the Processor class to queue incoming Commands.

BufferLockFree

Uses a many writers, multiple readers Lock-free CAS implementation. A read returns the oldest written value in a FIFO way.

ListLockFree

Uses a many writers, multiple readers Lock-free CAS implementation of a single linked list. A special member function 'apply' must be used to manipulate the objects within the list.

Event::emit()

Uses the ListLockFree above for keeping track of subscribers. Concurrent invocations of emit() will lead to concurrent execution of the subscriber's callback functions.

Handle::connect() and Handle::disconnect()

(Dis)Connection of an event handle is hard real-time and lock-free because of the lock-free event implementation.

3. DataSources

A DataSource is the fundamental data exchange entity within Orocos. It's interface is that of an 'expression': it can be evaluate()'d, its result can be get() and an AssignableDataSource can even be set(). The DataObject types are implementations of DataSources, but many more kinds exists which are used by the Scripting engine and the task infrastructure.

The DataSourceBase interface is the most basic interface for exchanging data ( value types ) between objects. They are reference counted ('smart pointers'), such that ownership across many objects must not be managed. The DataObjectInterface implements the DataSource interface, and thus all Orocos DataObject types are DataSources.

The DataObjectInterface has multiple implementations, depending on the specific data access locking needs:

  • DataObject. This is the most simple DataObject implementation. The Get() and Set() methods directly map onto the contents and can always be inlined by the compiler. It offers no thread safety, but maximum efficiency for copying data.

  • DataObjectLocked. This is a thread safe DataObject whose Set() and Get() methods are guarded by a single mutex. The second thread accessing this object will always block, which is not always appropriate in a realtime system.

  • DataObjectPrioritySet. This is a more complex DataObject which gives always priority to the thread calling Set(), which will never block. The thread accessing Get() will block if the Set() thread is accessing the contents. It is mainly used for sharing data between two kernels, running at different priorities.

    [Note]Note

    This DataObject will only work if the Set() thread has the highest priority. When the inverse is true, data corruption will occur. It is obvious that this DataObject can only be used if both threads have static priorities (which is the case for all threads in the Orocos framework).

  • DataObjectPriorityGet. The inverse of DataObjectPrioritySet. The thread accessing Get() will never block.

  • DataObjectLockFree. This DataObject implements a non blocking reader/writer buffer which always returns the last written value to the reader. If the reader is preempted with a write and a read, the last read will return a newer value, while the first read continues to read the uncorrupted old value. The depth of this buffer must be readers+3, for the algorithm to succeed in doing every write. Apart from memory consumption, it is one of the best thread-safe DataObject implementations.

Chapter 4. Inside The Task Infrastructure

Abstract

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

1. Introduction

This manual documents how low-level RTT classes can be used.

2. ProgramLoader and PropertyLoader

The TaskContext uses these two classes to manage properties and programs scripts.

2.1. Task Property Configuration

As was seen in ???, CoreLib Properties can be added to a task's AttributeRepository. To read and write properties from or to files, you can use the PropertyLoader class. It uses the XML Component Property Format such that it is human readable.

  #include <rtt/PropertyLoader.hpp>
  // ...
  TaskContext* a_task = ...
  PropertyLoader ploader;
  ploader.configure("PropertyFile.cpf", a_task );
  // ...
  ploader.save("PropertyFile.cpf", a_task ); 

Where 'configure' reads the file and configures updates the task's properties and 'save' 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. Fortunately, the TaskContext has implemented this functionality also as script methods, thus you do not need to do this manually.

2.2. Task Program Scripts

2.2.1. Functions

A function is formed by a group of commands and methods, which can be executed by a task. The scripting language defines functions as :

  export function myFun( int arg1, double arg2 )
  {
    // Group commands and methods
    var ...
    do ...

  } 
  // repeat... 

where the optional export keyword makes the function available as a task's command ( which will fail if one of its contained commands fail ) :

  do ATask.myFun( 1, 2. )

If you omit the export keyword, then the function will not become available as a command. To use such a function, you need to execute it in the Execution Engine's Program Processor ( see below ), or call it in a program, which was parsed in the same file (see ??? ).

Functions must be parsed by the Parser, before they can be executed by the Execution Engine. It executes the Function until it finishes or it goes into error. In both cases, the Function is removed from the queue and can then safely be deleted (or re-run).

[Note]Note

The Parser and ProgramLoader are located in the Orocos Program Parser package and not in the Task Context package.

To directly execute any number of not exported functions in a file, or add an exported function in a TaskContext's Command API, do :

  #include <rtt/ProgramLoader.hpp>

  TaskContext* a_task = ...
  ProgramLoader loader;
  ProgramLoader::Functions funcs;

  funcs = loader.loadFunction( "Functions.ops", a_task );

funcs is an STL container wich contains all functions being executed.

[Warning]Warning

Using loadFunction with functions that require arguments will execute the functions with default initialisation of the arguments. It is easier to export the function and invoke it as a Command. Otherwise, use programs to call such functions.

2.2.2. Programs

Programs are special functions in that they can be finely controlled once loaded in the ProgramProcessor. A program can be paused, it's variables inspected and reset while it is loaded in the Processor. It provides thus more funcitonality than a mere function. A program script calling the previous function would look like :

 [ ... myFun() function definition ... ]

  program myBar
  {
    var int i = 1
    var double j = 2.0
    do myFun(i,j)
  }

As with functions, any number of programs may be listed in a file.

Orocos Programs are loaded a bit different into a TaskContext :

  #include <rtt/ProgramLoader.hpp>

  TaskContext* a_task = ...
  ProgramLoader parser;

  loader.loadProgram( "ProgramBar.ops", a_task ); 

Take a look the ProgramInterface class reference for more program related functions.

2.2.3. State Machines

Hierarchical state machines are modelled in Orocos with the StateMachine class. They are like programs in that they can call a peer task's members, but the calls are grouped in a state and only executed when the state machine is in that state. This section limits to showing how an Orocos State Description (osd) script can be loaded in a Task Context.

  #include <rtt/ProgramLoader.hpp>

  TaskContext* a_task = ...
  ProgramLoader loader;

  loader.loadStateMachine( "StateMachineBar.osd", a_task ); 

Again, take a look at the StateMachine class reference for more details about state context related functions.

Chapter 5. Inside The Execution Engine

Abstract

The 'execution engine' executes Programs, State Machines, commands and events. It can execute any number of Programs and State Machines in parallel in a single thread.

1. Introduction to the Execution Engine

Orocos is meant for building realtime systems. You will find many useful classes in the Orocos CoreLib to build them, but they would only act as a noninteractive whole. The Execution package allows a user to configure a system, execute user-defined programs on that system and interactively accept commands. An ExecutionEngine contains a series of Processors which loads state machine and program definitions and executes them. Execution of the programs and state machines will happen in real-time. In addition, it processes incoming commands and events in real-time as well. The execution engine thus exists of four processors : ProgramProcessor, StateMachineProcessor, CommandProcessor and the EventProcessor. The Execution Engine needs to run in a periodic (preferably) or non periodic corelib task in order to execute these processors.

This document bundles the execution semantics of the Execution Engine and its processors and is no mandatory literature for understanding the Orocos task execution infrastructure.

The Parser will generate Programs and StateMachines from user defined text files, which are then executed in realtime.

2. The Execution Engine

A TaskContext's ExecutionEngine Executes Programs, Hierarchical StateMachines and processes Commands and events.

2.1. Purpose

The ExecutionEngine is the core class of this package. It executes the decision logic of the TaskContext by executing programs and state machines. Any number of these may be loaded and can be controlled separately. It allows to start, pause, step,... stop programs and state machines. No recompilation is needed in order to use new programs or state machines. Its task is to serialise (do one after the other) the execution of programs, state machines, commands and events such that they are not concurrently executing, and can thus interact thread-safely. A TaskContext is running when it's ExecutionEngine is running. The Task Context provides the interface of the control task, the Execution Engine executes the implementation.

2.2. Running the Execution Engine

In order to start a task context, its execution engine must be run in a periodic corelib task.

  #include <rtt/TaskContext.hpp>
  #include <rtt/PeriodicActivity.hpp>

  RTT::TaskContext mytaskC;
  RTT::PeriodicActivity ptask(5, 0.01 ); // Periodic task 100Hz.

  ptask.run( mytaskC.engine() ); // run the execution engine.

  ptask.start();
      

After it is started, it will execute all the processors in each periodic step. First the Program Processor, then the State Machine Processor, then the Command Processor and finally the Event Processor.

2.3. Executing Programs

The FunctionGraph is a tree composed of command nodes ( See : ActionInterface class ), where each node contains one or more statements. A FunctionGraph keeps track of the start node and the node to be executed next. As such a program is executed one node at a time and the transition to the next node is done on a given boolean condition ( See : ConditionInterface class). The FunctionGraph is built by the program parser and almost never by the programmer by hand.

In order to access (start, stop, pause,...) a loaded program, use :

  ProgramInterface* prog = mytaskC.engine()->programs()->getProgram("ProgName");
  if (prog == 0 ) {
      // program not found.
  }
  int line = prog->getLineNumber();
  prog->start();
  // ...
  prog->pause();
  // .. etc. 	  
	

All commands are also accessible from within the interactive 'TaskBrowser'.

Figure 5.1. Executing Program Statements

Executing Program Statements

Programs are generated from a script. The Parser is able to convert Orocos Program Scripts to a Program which can be loaded in the Program Processor.

Figure 5.2. Using a Program

Using a Program

Loading, unloading and deleting a Program may throw an exception. The program_load_exception and program_unload_exception should be try'ed and catch'ed on load and unload or deletion of a Program in the Program Processor.

[Tip]Tip

Alternatively, you can use the ProgramLoader class as a user-friendly frontend for loading Programs and Functions in tasks.

2.4. State Machine and States

The StateMachine is a collection of states, linked to each other through transition definitions. It represents a state machine of the realtime system's logic and may access internal and external data for deciding on its state transitions. Every 'device' has some states or configurations in which a specific action must be taken (on entry, during or on exit) and transitions between states are defined by boundary conditions (guards) or triggered by events. Every such state is defined by the StateInterface. A state itself is defined by four programs : entry, run, handle and exit. They are called by the State Machine Processor when this state is entered, run or left. When no state transition took place, handle is called in a periodic execution step of the StateMachineProcessor. Otherwise, first the exit program of the current state is called, then the entry program and then the run program of the new state is called. The next periodic step will continue the run program, or when finished, evaluate transition conditions, and if none succeeds, handle will be called, and so on. Orocos Events may be processed during the run program. The exit program can check for interuption and clean up if necessary. After the exit program (if any), the transition program is executed (again, if any).

As transitions define when a state will be left, preconditions are used to define when a state should not be entered, and function as an extra condition which is checked before the transition is tried.

Figure 5.3. Executing a StateMachine

Executing a StateMachine

The Parser is able to convert Orocos State Descriptions to a State Machine wich can be loaded in the Processor.

In order to access (activate, start, stop, pause,...) a loaded state machine, use :

  StateMachine* smach = mytaskC.engine()->states()->getStateMachine("MachineName");
  if (smach == 0 ) {
      // state machine not found.
  }
  smach->activate();
  string state = smach->getCurrentStateName();
  smach->start();
  // ...
  smach->stop();
  // .. etc. 	  
	

All commands are also accessible from within the interactive 'TaskBrowser'.

Figure 5.4. Using a StateMachine

Using a StateMachine

Loading, unloading and deleting a State Machine may throw an exception. The program_load_exception and program_unload_exception should be try'ed and catch'ed on load and unload or deletion of a State Machine in the Processor.

[Tip]Tip

Alternatively, you can use the ProgramLoader class as a user-friendly frontend for loading State Machines.

2.5. The Command Processor

The CommandProcessor is responsible for accepting command requests from other (realtime) tasks. It uses a non-blocking atomic queue to store incoming requests and fetch-and-execute them in its periodic step. It offers thus a thread-safe realtime means of message passing between threads, without intervention of the Operating System.

  #include <rtt/TaskContext.hpp>

  ActionInterface* com = ... // Create a command, see the next Section.

  int ticket = mytaskC.engine()->commands()->process( com ); 

If ticket is zero, the command was not accepted by the processor, possibly the queue was full or it was not running. You can query if a command has been fetched and executed by calling :

  if ( mytaskC.engine()->commands()->isProcessed( ticket ) )
     // done !
  else
     // still busy ! 

Figure 5.5. Tasks Sending Commands

Tasks Sending Commands

Tasks of different threads communicate by sending commands to each other's Command Processors. When Task T1, running in Thread (a), requests that T2, running in Thread (b) processes a command, the command is stored in a command queue of that task's Command Processor. When T2 runs its Command Processor, the queue is checked and the command is executed. T1 can optionally check if the command was accepted and executed, using a Completion Condition ( see TaskContext and Program Parser manuals. )

3. Creating Commands

[Note]Note

Since Orocos Version 0.22.0, it is far easier to create commands using the command factories in the TaskContext. Please see the Task Context manual for examples. This section is still valid, but harder to apply in practice.

Apart from the Program and State Machine Commands (which are generated by the Parsers), the user can convert C/C++ functions in commands for the Processor to execute. This is useful if a function must be called at a later moment.

3.1. Generic Functors

Orocos uses the 'Generic Functor' paradigm to encapsulate Commands. This means that an object (the functor) is created which holds a pointer to the function to be executed. If this function needs arguments, these are also stored in the functor. The object can then be passed around until another object decides to execute the functor. Execution of a functor leads to the original function to be called, together with the arguments.

Figure 5.6. A Generic Functor

A Generic Functor

3.2. Creating a CommandFunctor

The CommandFunctor is the object used to store the function pointer in. It implements the ActionInterface such that it can be execute()'ed by the Processor :

  #include <rtt/CommandFunctor.hpp>
  void foo();

  ActionInterface* command = newCommandFunctor( &foo );

  command->execute(); // calls foo()

  delete command;
	

notice that we use a factory-function newCommandFunctor in order to avoid providing a template parameter.

It is possible to wrap more complex functions in a CommandFunctor, if the boost::bind library is used :

  #include <rtt/CommandFunctor.hpp>
  #include <boost/bind.hpp>
  
  void foo( int x, int y );
  int a1 = 1, a2 = 2;

  ActionInterface* command = newCommandFunctor( boost::bind( &foo, a1, a2 ) );

  command->execute(); // calls foo(a1,a2)

  delete command;
	

Argument 'binding' is a very powerful feature of C++. It allows to provide the arguments of a function in advance and execute the function lateron.

It is also possible to call the memberfunction of an object. In that case, the first parameter of the function becomes the pointer to the object, followed by the arguments of the function ( which must all be variables ) :

  #include <rtt/CommandFunctor.hpp>
  #include <boost/bind.hpp>
  
  class X {
  public:
    void foo( int x, int y );
  };

  X x_object;
  int a1 = 1, a2 = 2;
  ActionInterface* command = newCommandFunctor( boost::bind( &X::foo, x_object, a1, a2 ) );

  command->execute(); // calls x_object.foo(a1,a2)

  delete command;
	

notice that the foo function is now prefixed by the class scope 'X::'.

The CommandFunctor allows us to bind a function to a ActionInterface. Since the Program Processor can execute ActionInterface objects, it is a powerful way to delay calling of a function to a later moment.

3.3. Processing a Command

Using the CommandFunctor from the previous section, we can pass the command to the processor :

  ActionInterface* command = ...
  Processor* proc = ...

  int nr = proc->process( command );
	

If nr is non-zero, the command was accepted, if zero, the command fifo is full and a new process attempt must be made.

Another thread instructs the processor to execute all queued commands (and programs) synchronically calling the

  proc->step();
	

function. The easiest way to do this is to create a Task which runs the Processor.

3.4. Common Usage Examples

The CommandFunctor can be used when a separate thread of execution wants to execute a function in the Processor thread. In Orocos, this happens when an external commando must be processed by the realtime control kernel. In one thread, the CommandFunctor is created (which contains the function to be called) and is passed to the Processor of the ExecutionExtension, which is part of the control kernel. The control kernel thread executes all queued commands in the Processor after the control calculations are done. In this way, safe data access can be guaranteed.

4. StateMachine and Program Implementation Details

[Note]Note

This section gives a short description of the inside of Programs and StateMachines, and can be skipped by most users.

4.1. ( State ) Transitions : Condition Edge

The ConditionEdge defines an edge of the state diagram. It contains a condition on which a next state is entered. The ConditionInterface encapsulates that logic. Conditions can be ordered by priority, so that it is defined in which order they are checked. A multiple of conditions can lead to the same state.

4.2. Statements : Command Node

The CommandNode contains a Command and is connected by edges of the type ConditionEdge, these edges connect one node with another and allow the transition if the contained condition evaluates to true. When a program is executed, it executes the command and runs through the list of edges of that node. When a Condition is found valid, the next program node to be executed is found. If no condition is fulfilled, the same command node will be executed again. Also a line number can be associated with each command node, as a reference to the input file formatted by the user.

4.3. The Command class

The Command is the abstraction of a user directive that has to be executed. A Command can be execute()'ed and reset()'ed. For each action exists one Command, but a Command can be composed of other Commands. The basic interface, ActionInterface, is provided by the Orocos CoreLib.

4.4. The Condition class

The Condition is the abstraction of a user expression that has to be evaluated. A Condition can be evaluated()'ed and reset()'ed. Many primitive expressions can be evaluated and a Condition can be composed of other Conditions. The basic interface, ConditionInterface, is provided by the Orocos CoreLib

Chapter 6. Inside The Program Script Parser

Abstract

This document describes the insides Orocos Parser system, in the different ways it can be used and extended.

1. Introduction

The Orocos Parser allows users of the Orocos system to write programs and state machines controlling the system in a user-friendly realtime script language. The advantage of scripting is that it is easily extendible and does not need recompilation of the main program. It is implemented using the Boost.Spirit parser library, and should be fairly easy to work with.

This document is about extending the Orocos parser to support extra types, overload existing operators, and/or add new operators.

2. Extending the parser

[Note]Note

This Section is only relevant to persons willing to extend the internals of the Orocos Parser framework.

2.1. Parser Limitations

For various reasons, during the development of the Orocos parser, it has proven necessary to hard-code various things, mostly relating to the defined types, and the operations supported on them. The parser supports using different types of objects than the predefined ones, but the major limitations are:

  • For some types like vectors, rotations and frames, special syntax was added. Currently, this is limited to the so-called constructors, that allow you to construct e.g. a vector from three doubles.

2.2. Alleviating the Limitations

We will address the ways to address the various limitations.

2.2.1. Adding special syntax

This section will explain how to add a custom constructor, or a custom operator, that you will then be able to use in expressions.. The operator can take one to three arguments of any type, and can return any type..

You need to do two things in order to do this:

  • make the parser know about the new syntax

  • tell the parser what the new syntax means

You should make the parser aware of the new syntax in the file rtt/program-parser/src/ExpressionParser.cxx. There, in the ExpressionParser constructor, the syntax of an expression is defined. There, you should add the new syntax. I'm afraid I can't explain you other than either copying from an existing syntax or reading the Boost.spirit documentation. You need to couple your new syntax with a semantic action like "bind( &ExpressionParser::seen_binary, this, "%" ) for a binary action that you want to give the name "%". The name "%" is just an identifier that should be unique to your new operator, it can be any string you want.

Next, you need to define the operator in Operators.cpp, in much the same way as you should do for overloading an existing operator. However, instead of then using an existing string like "+", you should use the string you chose while defining your new syntax above.