orca-robotics


INTRODUCTION
Overview
Download and Install
Quick Start
Documentation
Publications

REPOSITORY
Interfaces
Components
Libraries
Utilities
Software Map

DEVELOPER
Tutorials
Examples
Dev Guide
Dashboard
Wiki
login/pass: orca/orca

PEOPLE
Contributors
Users

SourceForge.net Logo
Project
Download
Mailing lists

 

         

Writing a super-simple component

Note:
Reviewed for release 2.10.0.
This page describes the basic structure of a typical Orca component. We use Brick component as an example. When you are done, go to the next tutorial: Running a super-simple component

Source files

Go to the top directory of the Orca distribution and type the following:

$ ls src/examples/brick
brick.def  doc.dox  CMakeLists.txt  mainthread.cpp  mainthread.h

What are all these files? Here we'll just list their purpose, click on the links for the line-by-line explanation of the source.

  • CMakeLists.txt -- A special text file used by the CMake build system. Based on the CMakeLists.txt in the current directory and the similar files in all directories above, CMake will generate Makefiles (or equivalents for non make-based systems, e.g. MS Visual Studio)

  • brick.def -- The component definition file. It contains information about the component's provided and required interfaces, configuration parameters, etc. During compilation a special utility Def2Cfg is automatically called to convert brick.def file into a sample brick.cfg file which is later installed into [ORCA_INSTALL]/cfg directory. The format of .def file is described in the documentation for the Def2Cfg utility.

  • doc.dox -- Component documentation in Doxygen format. This documentation is generated from doc.dox. For the details of the format, see Doxygen documentation.

CMakeLists.txt

ORCA_SET_COMPONENT_NAME( Brick )

SET( build TRUE )
# Check for human input
GBX_REQUIRE_OPTION( build EXE ${APP_NAME} ON )

# libs on which our code depends would go here
# SET( dep_libs ??? )
# GBX_REQUIRE_TARGETS( build EXE ${APP_NAME} ${dep_libs} )

IF( build)

    INCLUDE( ${ORCA_CMAKE_DIR}/UseComponentRules.cmake )

    FILE( GLOB srcs *.cpp )
    
    # build stand-alone component  
    ORCA_WRAP_MAINTHREAD_INTO_APP( ${APP_NAME} ${APP_CTRLC_HANDLER} ${COMP_NAMESPACE} ${COMP_NAME} ${srcs} )
#     TARGET_LINK_LIBRARIES( ${APP_NAME} ${dep_libs} )
    
    # build IceBox service
    IF( ORCA_BUILD_SERVICES )
        ORCA_WRAP_MAINTHREAD_INTO_SERVICE( ${SERVICE_NAME} ${COMP_NAMESPACE} ${COMP_NAME} ${srcs} )
#         TARGET_LINK_LIBRARIES( ${SERVICE_NAME} ${dep_libs} )    
    ENDIF( ORCA_BUILD_SERVICES )

    ORCA_GENERATE_CONFIG_FILE( ${APP_NAME}.def )

ENDIF( build )

Line by Line Walkthrough
Each directory has a CMakeLists.txt which describes how to compile sources in that directory. CMake uses its own language to describe what needs to be done. Orca project uses standard CMake keywords and a few custom macros which we borrow from the GearBox project. Here's how you can tell where the macros are defined:
  • All GearBox macros start with "GBX_"
  • All Orca macros start with "ORCA_"
  • The rest are standard CMake commands and macros.

ORCA_SET_COMPONENT_NAME ( Brick )
Sets component name and 3 other related CMake variables which we'll use later in this file.
  • APP_NAME application executable name (defaults to the component name converted to lower case)
  • SERVICE_NAME service library name (defaults to the component name prepended with project name and appended with "Service")
  • COMP_NAMESPACE component namespace (defaults to the component name converted to lower case)

Also sets APP_CTRLC_HANDLER=1 (instructs application to install Ctrl-C Handler)

For custom values, call this function first and then override some or all of the variables manually. For example:

      ORCA_SET_COMPONENT_NAME( MyComponent )
      SET( COMP_NAMESPACE mycomp )

SET( build TRUE )
This and the following few lines determine whether we are going to build this component or not. We start with the assumption that we will and then check a few requirements sequentially. We define a variable called BUILD and set it to TRUE, i.e. "this library will be built". Failure to meet any one of the requirements will assign FALSE to this "cumulative variable".

# Check for human input
GBX_REQUIRE_OPTION( build EXE ${APP_NAME} ON )
This line calls a CMake macro which defines a user-configurable compilation option. It will be called BUILD_BRICK (BUILD + the name of our component in upper case). This option can be used as a variable to implement conditional compilation. The default value of this variable is ON, equivalent to TRUE. CMake comes with a configuration tool called ccmake. In Linux, when you type "ccmake ." in the top directory of the Orca distribution, you will see a list of all compilation options. Scroll down to BUILD_BRICK and press ENTER, the value of the option will change to OFF. Now configure (press c) and generate (press g). The build options have now been adjusted such that the Brick example will not be compiled. Repeat the process to turn the compilation back on or delete CMakeCache.txt to go back to defaults.

# libs on which our code depends would go here
# SET( dep_libs ??? )
# GBX_REQUIRE_TARGETS( build EXE ${APP_NAME} ${dep_libs} )
Pound sign marks comments. Here we comment out a line which would normally be used to specify library dependencies on one of the Orca targets (typically libraries but could be executables as well). This example does not need to be linked to any library besides the typical ones specified by UseComponentRules.cmake script.

IF( build)
Do not compile this example if any of requirements are not satisfied. In this case we only have one requirement: the wish of the user. Typically, there will be several, e.g. the OS, required libraries, etc.

    INCLUDE( ${ORCA_CMAKE_DIR}/UseComponentRules.cmake )
Most components are compiled with the same compiler options, include the same type of header files and are linked to the same libraries. These common settings are defined in a CMake file called UseComponentRules.cmake and located in [ORCA-SRC]/cmake directory. The CMake INCLUDE command imports all the commands from that file as if we typed them in here by hand.

Because the UseComponentRules.cmake script is installed with the rest of the Orca files, satellite projects which use Orca can use this script file as well. The CMake variable ORCA_CMAKE_DIR works out the correct location of Orca CMake scripts.

    FILE( GLOB srcs *.cpp )
Search for file in the current directory which fit the specified pattern and assign the list to the variables COMP_SRCS.
  • Instead of searching you can just list the files you need.
    SET( srcs mainthread.cpp )
    

    ORCA_WRAP_MAINTHREAD_INTO_APP( ${APP_NAME} ${APP_CTRLC_HANDLER} ${COMP_NAMESPACE} ${COMP_NAME} ${srcs} )
This macro wraps an Orca component around a single thread and places that component into an Orca application. It expects to find
  • files named mainthread.h/cpp
  • which define a class ${COMP_NAMESPACE}MainThread
  • which derives from gbxiceutilacfr::Thread or its derivatives

If your component is non-standard (e.g. has several threads or needs to do something special in the constructor) you can create a custom Orca component. There's a macro for this case as well called WRAP_COMPONENT_INTO_APP which will place the custom component into an Orca application. If your main.cpp file needs to be nonstandard, then you have to write it by hand without the help of the macros. When writing custom component.h/cpp and main.cpp files it may be useful to take a look at the automatically generated versions. Look at the contents of brick/autogen directory (it is generated during the cmake process).

#     TARGET_LINK_LIBRARIES( ${APP_NAME} ${dep_libs} )
This would be uncommented in case we had dependencies.

    IF( ORCA_BUILD_SERVICES )
        ORCA_WRAP_MAINTHREAD_INTO_SERVICE( ${SERVICE_NAME} ${COMP_NAMESPACE} ${COMP_NAME} ${srcs} )
#         TARGET_LINK_LIBRARIES( ${SERVICE_NAME} ${dep_libs} )    
    ENDIF( ORCA_BUILD_SERVICES )
If the global option called BUILD_SERVICES is ON, create a shared library called libOrcaBrick.so (default value for ${SERVICE_NAME}). The macro wraps an Orca service around a single thread. The BUILD_SERVICES option is defined globally but can be user-configured with ccmake tool. The default setting is OFF. The macro will also schedule this service library to be installed into [ORCA-INSTALL]/lib. We could also link this library to dependencies.

    ORCA_GENERATE_CONFIG_FILE( ${APP_NAME}.def )
We call a macro to automatically generate a configuration file (.cfg) from the component definition file (.def). For more information on config files, see Orca Configuration Files.

ENDIF( build )
End of the IF block. Most of the CMake syntax you'll need you can work out from the existing files. For more details, consult the CMake documentation.

There are additional sources of information on CMake. Take a look at the CMake documentation and a GearBox build system tutorial.

mainthread.h

#ifndef MAIN_THREAD_H
#define MAIN_THREAD_H

#include <gbxsickacfr/gbxiceutilacfr/subsystemthread.h>
#include <orcaice/context.h>

namespace brick
{

class MainThread: public gbxsickacfr::gbxiceutilacfr::SubsystemThread
{       
public:
    MainThread( const orcaice::Context& context );

    // from SubsystemThread
    virtual void walk();

private:
    orcaice::Context context_;
};

} // namespace

#endif

Line by Line Walkthrough
#include <gbxsickacfr/gbxiceutilacfr/subsystemthread.h>
#include <orcaice/context.h>
Definitions of the thread class from GearBox and orcaice::Context classes.

class MainThread: public gbxsickacfr::gbxiceutilacfr::SubsystemThread
The one and only thread used by our component is derived from hydroutil::SubsystemThread class. This thread class derives from a basic hydroutil::Thread class and adds a bit of functionality to it.

Firstly, it catches all common and uncommon exception which may originate in our code. This does not mean that we should not worry about exceptions because when SubsystemThread catches an exception it does not know what to do, so it just prints out a statement and waits for the component to shut down. This behavior is useful because, without this precaution, the program would simply seg fault and you wouldn't event know what happened.

Secondly, this class simplifies usage of hydroutil::Status class by integrating functionality of hydroutil::SubStatus. Status can keep track of multiple subsystems which can be defined arbitrarily and are destinguished by a name. Typically, each thread is a subsystem. It can be in certain state and this state can be reported through the hydroutil::Status facility.

If something about the way hydroutil::SubsystemThread behaves doesn't suit your needs, you can always use the more general hydroutil::Thread.

public:
    MainThread( const orcaice::Context& context );
The context described above is passed down to our thread by the component.

    virtual void walk();
We implement the pure virtual function walk() defined in hydroutil::SubsystemThread. Function walk is the entry point of the thread, the thread execution happens in it. (The equivalent function in the more general hydroutil::Thread is called run(), i.e. walking is safer than running. :)

private:
    orcaice::Context context_;
};
We will keep a copy of the context so we can use it throughout the life of the thread.

mainthread.cpp

#include <iostream>
#include <orcaice/orcaice.h>

#include "mainthread.h"

using namespace std;
using namespace brick;

MainThread::MainThread( const orcaice::Context& context ) :
    SubsystemThread( context.tracer(), context.status(), "MainThread" ),
    context_(context)
{
    subStatus().setMaxHeartbeatInterval( 20.0 );
}

void
MainThread::walk()
{
    // multi-try function
    orcaice::activate( context_, this, subsysName() );

    //
    // Read configuration settings
    //
    std::string prefix = context_.tag() + ".Config.";

    int sleepIntervalMs = orcaice::getPropertyAsIntWithDefault( context_.properties(),
            prefix+"SleepIntervalMs", 1000 );

    subStatus().ok( "Initialized" );

    //
    // Main loop
    //   
    subStatus().setMaxHeartbeatInterval( sleepIntervalMs * 3.0/1000.0 );

    while( !isStopping() )
    {
        context_.tracer().debug( "Running main loop", 5 );
        subStatus().heartbeat();

        // here we can do something useful

        IceUtil::ThreadControl::sleep(IceUtil::Time::milliSeconds(sleepIntervalMs));
    }
}

Line by Line Walkthrough
MainThread::MainThread( const orcaice::Context& context ) :
    SubsystemThread( context.tracer(), context.status(), "MainThread" ),
    context_(context)
{
A copy of the context is stored for future use. Note that the constructor is executed in the calling thread, in this case in the thread which created the component. We initialize SubsystemThread with the tracer and status objects contained in the context. We also name the subsystem corresponding to this thread. This name will be used for all status reporting.

    subStatus().setMaxHeartbeatInterval( 20.0 );
The Status facility implements the heartbeat pattern. In this line we make a promiss to Status that we will update status information at least as often as every 20 seconds. This promiss will allow Status to raise an alarm if there's no update from this subsystem (thread) for longer than the specified interval.

Note that this line is equivalento to calling.

    context_.status().setMaxHeartbeatInterval( subsysName(), 20.0 );
The simplified syntax is provided by SubsystemThread.

void
MainThread::walk()
{
This is the entry point of the thread. Now we are running in our own thread. We get here after Thread::start() is called by the component.

Executing code in a separate thread usually means that we are responsible for catching all possible exceptions. We don't do it here because hydroutil::SubsystemThread does it for us.

    orcaice::activate( context_, this, subsysName() );
We call a libOrcaIce function to intialize the server capability of the component. (Technically, we are initializing the Ice::ObjectAdapter).

The component does not perform this step automatically and leave it up to the designer because this is an important step and you may want to choose when to do it. For example you may want to setup all your required interfaces (i.e. clients) first and then enable the provided interfaces.

This operation may fail for various reasons. Common ones are: the network cable is unplugged, the Registry is unreachable. This standard function will try to activate indefinitely after sleeping for a few seconds. But we want to be able to get out of this infinite loop (e.g. with a Ctrl-C). For this reason, we supply this function with a pointer to a Thread (i.e. this = the pointer to this thread) so it can find out that it's time to stop. By the way, this is also the reason why orcaice::activate() should be called from a thread and not from Component::start().

    std::string prefix = context_.tag() + ".Config.";
    int sleepIntervalMs = orcaice::getPropertyAsIntWithDefault( context_.properties(),
            prefix+"SleepIntervalMs", 1000 );
Reading in configuration parameters from the config file. All configuration properties start with the component tag, followed by a period and "Config" and another period. We form a string prefix by following this recipe. See the complete guide to Orca Configuration Files for more details.

This component has a single configuration parameter specifying the sleep interval within the main loop. It must be specified as an integer in milliseconds. See Brick documentation for the description of this config parameter.

We use a helper function from libOrcaIce which returns an integer with default. The value is looked up in the property set available from the component context using the key "Brick.Config.SleepIntervalMs". If this property is absent in the config file, the default value of 1000ms = 1s is used.

Note that we should probably do more checks on the specified value, e.g. make sure that it is not negative.

    subStatus().ok( "Initialized" );
When the SubsystemThread is created it automatically enters Initializing state. Now that initialization is complete we inform Status, the status is now Ok with a diagnostic string "Initialized". The other 2 possible status states are Warning and Fault.

    subStatus().setMaxHeartbeatInterval( sleepIntervalMs * 3.0/1000.0 );
Initialization is complete and thread behavior becomes more predictable, we can now promiss to update Status more frequently.

    while( !isStopping() )
    {
We are entering the main loop. It will execute until the thread is told to stop. After that the value returned by isStopping() function will become TRUE and we will exit from the loop.

        context_.tracer().debug( "Running main loop", 5 );
We use the tracer to let the user know what's going on. The hydroutil::Tracer defines several types of trace messages: info, warning, error, and debug. Each one can be forwarded to any number of destinations: display, network, log, or file. The default configuration is shown in the documentation. E.g. the default debug-to-display setting is 0, therefore this debug statement of level 5 will not be shown on the screen. In the next tutorial we will show how to change this setting.

        subStatus().heartbeat();
At every iteration of the loop, we call the heartbeat() function of the status object letting it know that this subsystem is not stuck.

        // here we can do something useful

        IceUtil::ThreadControl::sleep(IceUtil::Time::milliSeconds(sleepIntervalMs));
Here we would actually perform some useful operations, e.g. read data from harware, perform computations, send messages over the network. Instead, this example component just sleeps for the amount of times specified in the config file. The sleep function provided by libIceUtil is cross-platform.

    }
}
When the component is told to stop (by calling Component::stop() function), it will tell all of its threads to stop (by calling the Thread::stop() function). After this, the return value of Thread::isStopping() will change to TRUE, we will drop out from the main loop and exit from walk(). The thread will self-destruct and at this point the the main thread "joins" this one.

brick.def

DefaultComponentTag=Brick

# Provided Interfaces
# none

# Required Interfaces
#none

# Component Configuration
Config.SleepIntervalMs.Default=1000

Line by Line Walkthrough
DefaultComponentTag=Brick
This component tag will be pre-pended to all component properties in the brick.cfg file.

# Provided Interfaces
# none

# Required Interfaces
#none
This is a super-simple example, it has no (non-standard) provided or required interfaces. The following standard interfaces are automatically provided by libOrcaIce : Home, Status, Tracer. These do not need to be specified here.

# Component Configuration
Config.SleepIntervalMs.Default=1000
This will be translated into the following line of brick.cfg file "Brick.Config.SleepIntervalMs=1000"

What's Next?

When you are done, go to the next tutorial: Running a super-simple component.
 

Webmaster: Tobias Kaupp (tobasco at users.sourceforge.net)


Generated for Orca Robotics by  doxygen 1.4.5