INTRODUCTION Overview Download and Install Quick Start Documentation Publications NONFRAMEWORK CODE Driver Interfaces Drivers Libraries Utilities FRAMEWORK CODE Interfaces Components Libraries Utilities Full Software Listings DEVELOPER Tutorials Examples Dev Guide Dashboard PEOPLE Contributors Users Project Download Mailing lists
|
Writing a super-simple component
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 filesGo 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.txtORCA_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( ${srcs} ) # TARGET_LINK_LIBRARIES( ${APP_NAME} ${dep_libs} ) # build IceBox service IF( ORCA_BUILD_SERVICES ) ORCA_WRAP_MAINTHREAD_INTO_SERVICE( ${srcs} ) # TARGET_LINK_LIBRARIES( ${SERVICE_NAME} ${dep_libs} ) ENDIF( ORCA_BUILD_SERVICES ) ORCA_GENERATE_CONFIG_FILE( ${APP_NAME}.def ) ENDIF( build )
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:
ORCA_SET_COMPONENT_NAME ( Brick ) Sets component name and 3 other related CMake variables which we'll use later in this file.
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 # 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 # 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 Because the FILE( GLOB srcs *.cpp ) Search for file in the current directory which fit the specified pattern and assign the list to the variables
ORCA_WRAP_MAINTHREAD_INTO_APP( ${srcs} ) This macro wraps an Orca component around a single thread and places that component into an Orca application. It expects to find
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 # 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( ${srcs} ) # TARGET_LINK_LIBRARIES( ${SERVICE_NAME} ${dep_libs} ) ENDIF( ORCA_BUILD_SERVICES ) If the global option called 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 gbxiceutilacfr::SubsystemThread { public: MainThread( const orcaice::Context& context ); private: // from SubsystemThread virtual void initialise(); virtual void work(); orcaice::Context context_; }; } // namespace #endif
#include <gbxsickacfr/gbxiceutilacfr/subsystemthread.h> #include <orcaice/context.h> Definitions of the thread class from GearBox and orcaice::Context classes. class MainThread: public 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 initialise(); virtual void work(); We implement the virtual functions 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) { } void MainThread::initialise() { setMaxHeartbeatInterval( 20.0 ); // do some initialization here, e.g. create a hardware driver. } void MainThread::work() { // // Read configuration settings // std::string prefix = context_.tag() + ".Config."; int sleepIntervalMs = orcaice::getPropertyAsIntWithDefault( context_.properties(), prefix+"SleepIntervalMs", 1000 ); setMaxHeartbeatInterval( sleepIntervalMs * 3.0/1000.0 ); while( !isStopping() ) { context_.tracer().debug( "Running main loop", 5 ); health().heartbeat(); // here we can do something useful IceUtil::ThreadControl::sleep(IceUtil::Time::milliSeconds(sleepIntervalMs)); } }
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. void MainThread::initialise() { 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. While this function is running, the Subsystem is in setMaxHeartbeatInterval( 20.0 ); The Status facility implements the heartbeat pattern. In this line we make a promise to Status that we will update status information at least as often as every 20 seconds. This promise 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. This example doesn't actually have anything to initialize. Normally we would create and initialize hardware and algorithm drivers here. } void MainThread::work() { We finished initializing and are now working, i.e. the Subsystem is in 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. setMaxHeartbeatInterval( sleepIntervalMs * 3.0/1000.0 ); Initialization is complete and thread behavior becomes more predictable, we can now promise 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. health().heartbeat(); At every iteration of the loop, we call the // 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 hardware, 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.defDefaultComponentTag=Brick # Provided Interfaces # none # Required Interfaces #none # Component Configuration Config.SleepIntervalMs.Default=1000
DefaultComponentTag=Brick This component tag will be pre-pended to all component properties in the # 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 : orca_interface_home, orca_interface_status, orca_interface_tracer. These do not need to be specified here. # Component Configuration Config.SleepIntervalMs.Default=1000 This will be translated into the following line of 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)