Create a "Hello World!" Component in ArmarX (C++)

Objective: Learn how to create an ArmarX package, write a component that writes "Hello World!" to the log, and start it in the ArmarX GUI.

Previous Tutorials: none

Next Tutorials: Understand Distributed Systems

Reference Code: component_tutorials

Developing in ArmarX

So, you want to write code that does stuff in ArmarX? Okay, then let's get you started. First, we'll have to teach you the basics:

  • How to create your own ArmarX package, and
  • how to create and run a component in that package.

Create an ArmarX Package

If you want to write code that does stuff in ArmarX, you need an ArmarX package. An ArmarX package is a CMake project with a special kind of structure. CMake is a tool that manages C++ projects and tells the compiler (e.g. GCC, Clang) and build system (e.g. Makefiles, Ninja) how to build the project. A CMake project is a directory with CMakeLists.txt files in it. These files tell CMake how the project is structured, e.g. which C++ files should be bundled into a library and which C++ files should be turned into executables.

CMakeLists.txt files are written and maintained by hand by the developers. For example, when you add a new class Awesome in a pair of Awesome.h and Awesome.cpp files, you need to add these files in the respective CMakeLists.txt files. More on that later, though.

The ArmarX Package Tool

To create and interact with ArmarX packages, ArmarX provides the ArmarX Package Tool armarx-package. Let's see what this tool can offer:

armarx-package --help
Note
This is another command than armarx: It is armarx-package, with a hyphen, not armarx package.
  • armarx manages the ArmarX background communication service.
  • armarx-package interacts with the files and directories making up an ArmarX package.

Let's see whether there is something about creating ArmarX packages ...

... _(This is the part where you run the command above and read the output!)_

Aha! The init command looks like a good candidate:

...
init Create a new ArmarX package
...

Ask the command how it is used:

$ armarx-package init --help

Create a Package for This Tutorial

Okay, something like armarx-package init our_project_name should get us going. But before we create the package, we need to find a good place for it (although you can also move it later). For the sake of this series of tutorials, let's create a directory in your ArmarX workspace:

cd $ARMARX_WORKSPACE # e.g., ~/code/
mkdir tutorials
cd tutorials

In that directory, you will create your first ArmarX package. (Nervous? No need to! It is like going to the dentist, just ... well ok, it is nothing like going to the dentist. Anyway...) Wisely choose a package name, e.g. component_tutorials (we will continue to use the package in later tutorials).

Then, when you are ready, take deep breath, and run:

armarx-package init component_tutorials

This command should output something along these lines:

> Creating directory ...... /.../code/tutorials/component_tutorials ...
> Creating directory ...... /.../code/tutorials/component_tutorials/source ...
> Creating directory ...... /.../code/tutorials/component_tutorials/source/component_tutorials ...
> Generating .............. /.../code/tutorials/component_tutorials/source/component_tutorials/CMakeLists.txt ...
> Creating directory ...... /.../code/tutorials/component_tutorials/scenarios ...
> Creating directory ...... /.../code/tutorials/component_tutorials/data ...
> Creating directory ...... /.../code/tutorials/component_tutorials/data/component_tutorials ...
> Generating .............. /.../code/tutorials/component_tutorials/data/component_tutorials/VariantInfo-component_tutorials.xml ...
> Creating directory ...... /.../code/tutorials/component_tutorials/etc ...
> Creating directory ...... /.../code/tutorials/component_tutorials/etc/cmake ...
> Generating .............. /.../code/tutorials/component_tutorials/etc/cmake/Usecomponent_tutorials.cmake ...
> Generating .............. /.../code/tutorials/component_tutorials/etc/cmake/ArmarXPackageVersion.cmake ...
> Creating directory ...... /.../code/tutorials/component_tutorials/etc/doxygen ...
> Creating directory ...... /.../code/tutorials/component_tutorials/etc/doxygen/pages ...
> Generating .............. /.../code/tutorials/component_tutorials/etc/doxygen/pages/Overview.dox ...
> Generating .............. /.../code/tutorials/component_tutorials/etc/doxygen/pages/Tutorials.dox ...
> Generating .............. /.../code/tutorials/component_tutorials/etc/doxygen/pages/HowTos.dox ...
> Generating .............. /.../code/tutorials/component_tutorials/etc/doxygen/pages/FAQ.dox ...
> Generating .............. /.../code/tutorials/component_tutorials/etc/doxygen/pages/GuiPlugins.dox ...
> Generating .............. /.../code/tutorials/component_tutorials/etc/doxygen/pages/Components.dox ...
> Generating .............. /.../code/tutorials/component_tutorials/etc/doxygen/mainpage.dox ...
> Generating .............. /.../code/tutorials/component_tutorials/etc/CMakeLists.txt ...
> Generating .............. /.../code/tutorials/component_tutorials/README.md ...
> Generating .............. /.../code/tutorials/component_tutorials/LICENSE.md ...
> Generating .............. /.../code/tutorials/component_tutorials/CMakeLists.txt ...
> Generating .............. /.../code/tutorials/component_tutorials/.gitignore ...
> component_tutorials package created.

Examine the Created Package

Run ls or open a file explorer and explore what the command created:

$ ls
component_tutorials

It created a directory with the name of our package (component_tutorials) in our current working directory (tutorials). Let's see what's inside:

$ ls component_tutorials/
CMakeLists.txt data etc LICENSE.md README.md scenarios source

We will briefly go through the created files and directories:

  • README.md: A README file. This file is shown on GitLab when you put this project in a repository on that platform.
  • LICENSE.md: The License file. ArmarX packages typically use the GPLv2 license.
  • CMakeLists.txt: The top-level CMakeLists.txt. As said above, all ArmarX packages are Cmake projects, and this file is the main entry point for CMake.
  • data/: Here you can place a bit of data, e.g. object models. Files and directories in data/ directories of ArmarX packages can be found in code.
  • etc/: Miscellaneous files, e.g. additional doxygen documentation.
  • scenarios/: Configuration files of scenarios. If you did the tutorials in the Getting Started level, you already got to know one scenario from the user perspective.
  • source/: The actual C++ source code (i.e. .cpp and .h files, among others).
  • python/ (not generated by default): Like source/, but for Python code. This directory is only created when you add a first Python package in your ArmarX package. More on that in a later tutorial, though.

Optional: Create a Package in a Git Repository

For this tutorial, it is not necessary to put the package under version control. However, for your own project (e.g. your thesis), you should definitely set up a Git repository and add the package to it right away. See How to Create an ArmarX Package in a Git Repository for how to do that. If you are just doing the tutorial, you can skip this part and check it later for your real project.

Configure and Build the Empty Project

An empty package should also be a valid one. So let's try to configure and build the empty project.

By convention, build artifacts from CMake projects are often stored in a directory called build. There, we will configure and build the project. If we build the project manually, we first have to create it. Go the top-level directory of the created package, and create the build directory. Then, go to the created build directory, and invoke CMake like this:

cd component_tutorials/
mkdir build
cd build
cmake ..

This is the typical way of running CMake (not just for ArmarX packages). To summarize, it typically involves these steps:

  1. Create a build directory next to the project's top-level CMakeLists.txt.
  2. Go into that directory.
  3. Run cmake .., where the .. points to the parent directory, which contains the top-level CMakeLists.txt.

On Ubuntu 18, you might get the following error:

[...]
== Setting up ArmarX project ...
-- ArmarX next generation package.
-- Configuring ArmarX project `component_tutorials`.
CMake Error at /.../code/armarx/ArmarXCore/etc/cmake/latest/setup.cmake:9 (message):
Minimum compiler version of 8.2 (for gcc) is required for ArmarX! You are
using: 7.5.0
Call Stack (most recent call first):
/.../code/armarx/ArmarXCore/etc/cmake/ArmarXProject.cmake:146 (include)
CMakeLists.txt:10 (armarx_project)
-- Configuring incomplete, errors occurred!
See also "/.../code/tutorials/component_tutorials/build/CMakeFiles/CMakeOutput.log".
See also "/.../code/tutorials/component_tutorials/build/CMakeFiles/CMakeError.log".

This is because the standard compiler on Ubuntu 18 is GCC 7. However, ArmarX requires at least GCC 8. We need to help CMake choose the correct compiler. To this end, run this command:

cmake -DCMAKE_C_COMPILER=$ARMARX_C_COMPILER -DCMAKE_CXX_COMPILER=$ARMARX_CXX_COMPILER ..

You only need to do that once. If CMake runs successfully, it will remember this setting.

The warnings No project() command being present, 'armarx_enable_modern_cmake_projekt' will be removed, and Legacy mode: Including Use-file are expected at the moment and can be ignored.

The last lines printed by cmake .. should look something like this:

...
-- Configuring done
-- Generating done
-- Build files have been written to: /.../code/tutorials/component_tutorials/build

As you can see, CMake actually performs two steps:

  • Configuring: CMake goes through all CMakeLists.txt files, gathers information and understands your project.
  • Generating: CMake generates files for the build system (e.g. Makefiles or Ninja).

Note that CMake is not building your project: CMake is a build system generator, not a build system. For example, when you are using Makefiles as a build system (which is the default), you should find a file called Makefile in the build directory. This is generated by CMake, which in turns allows you to run make to build the project.

So, in order to actually build the project, we need to invoke the build system. In the build directory:

cmake --build .

where the . points to the directory containing the generated files (i.e. build). (This is different from plain cmake .., which needs the directory where the CMakeLists.txt is.)

The output should look like this:

In other words ... nothing. Isn't this strange? Shouldn't this do something?

Well, no. Your project is empty, remember? So there is actually nothing to do. But we did not encounter any error, so that is good.

By the way, you can also invoke the build system directly, which is sometimes less of a hassle than using CMake to build the project. For example, when using Makefiles (which is the default), you can just run

make

in the build directory. At the moment, this should do nothing as well.

Okay, let's summarize: We have got an empty ArmarX package that we can configure and build, although it is not very interesting yet. So next, we will add something to build to the package!

A few more words about the build directory: Configuring and building will generate files in this directory. These files should not be committed to version control (i.e. Git), as they would create unneeded conflicts and unnecessarily bloat up the repository. For this reason, the build directory is ignored by default.

Add a Hello World Component

In a hello world example, you might expect a file main.cpp with a main function like int main(int argc, char* argv[]). In ArmarX, these technical details are hidden from you. Instead, we want you to think in components.

In simple terms, a component is a process that communicates with ArmarX. We talk more about what "communicate" actually means here in the next tutorial. For now, think of a component as a program that you can start (e.g. as part of a scenario) and that has entry points for your code. So, not that different from the main.cpp after all.

Note one additional detail though: To be completely precise, the executable that you start is an application. An application has a main() function, and then loads one or multiple components in its process. So technically, the relationship between applications and components is 1 to N (as in 1 application can have N components, with N >= 1). However, in most cases, the relationship is 1 to 1, as in you start 1 application which loads 1 component.

Just keep that in mind:

An application is a process (in operating systems terms), and it consists of one or multiple components, although one is the usual case.

Create an Empty Component

Now, back to our hello world component. Just as we used the armarx-package tool to create the ArmarX package, we can use it to create a component in a package. The only difference is that we need to run the command from the root directory of the ArmarX package to which it shall add the component. So, go to that directory, and call armarx-pacakge:

cd component_tutorials/
armarx-package add component hello_world

The arguments add component tell the pacakge tool to add a component, and hello_world is the component's name. The command should print something like this:

> Creating directory ...... /.../code/tutorials/component_tutorials/source/component_tutorials/components ...
> Generating .............. /.../code/tutorials/component_tutorials/source/component_tutorials/components/CMakeLists.txt ...
> Creating directory ...... /.../code/tutorials/component_tutorials/source/component_tutorials/components/hello_world ...
> Generating .............. /.../code/tutorials/component_tutorials/source/component_tutorials/components/hello_world/./Component.cpp ...
> Generating .............. /.../code/tutorials/component_tutorials/source/component_tutorials/components/hello_world/./ComponentInterface.ice ...
> Generating .............. /.../code/tutorials/component_tutorials/source/component_tutorials/components/hello_world/./Component.h ...
> Updating................. /.../code/tutorials/component_tutorials/source/component_tutorials/components/hello_world/./Component.cpp ...
> Generating .............. /.../code/tutorials/component_tutorials/source/component_tutorials/components/hello_world/CMakeLists.txt ...
> Updating cmake .......... /.../code/tutorials/component_tutorials/source/component_tutorials/components/CMakeLists.txt ...
> Updating cmake .......... /.../code/tutorials/component_tutorials/source/component_tutorials/CMakeLists.txt ...
> hello_world component element created.

As you can see, the command created a new subdirectory components/hello_world/ in the source/component_tutorials/ directory. The structure of the source directory is now:

CMakeLists.txt # Top-level CMakeLists.txt of component_tutorials.
source/
component_tutorials/
CMakeLists.txt
components/
CMakeLists.txt
hello_world/
CMakeLists.txt
Component.h
Component.cpp
ComponentInterface.ice

We can notice a few things here:

  • The first directory in source is component_tutorials, i.e. a directory with the project's name. This is a C++ best practice: It makes sure that all includes start with the project name. For example:
    #include <component_tutorials/components/hello_world/Component.h>
    // Instead of:
    #include <components/hello_world/Component.h>
  • The next directory is called components. This is an ArmarX convention: All components are placed in the components directory by default, although this is not strictly necessary. It makes the project cleaner, however.
  • The next directory is hello_world: This is where your new component is located.
  • Inside the hello_world directory, there are a CMakeLists.txt, a header file Component.h, a source file Component.cpp, and a strange file ComponentInterface.ice. The .h and .cpp file are the C++ source code of your component. The CMakeLists.txt defines the component in CMake. Do not worry about that frosty strange file for now, we will come to that in the next tutorial.
  • Finally, note that almost all directories in this hierarchy have their own CMakeLists.txt. This is to make them more "local" and "explicit". For example, your component is defined in its CMakeLists.txt (i.e. the one in hello_world). The CMakeLists.txt in components only has a single line add_subdirectory(hello_world), which tells CMake to recurse into the hello_world directory and read the CMakeLists.txt there. A take-away here is that CMake only looks at files and directories that we explicitly tell it about: It does not automatically recurse into all subdirectories of the project and find all .cpp and .h files, for example. And that is a good thing.

Build the Empty Component

To check whether everything is working and to get you used to it, we will build the project again and see whether anything changed now. So, just as before, go to the build directory, then configure (cmake ..), then build (make) the project:

cd build/
cmake ..
make

Actually, you could have skipped the cmake ... When building an ArmarX package (make), an ArmarX package will automatically CMake again if it detects modified files. However, in normal (non-ArmarX) projects, you usually have to re-run CMake explicitly.

Now the output of make should look like this:

[ 16%] Generating `/.../code/tutorials/component_tutorials/build/source/component_tutorials/components/hello_world/ComponentInterface.{h|cpp}` from `/.../code/tutorials/component_tutorials/source/component_tutorials/components/hello_world/ComponentInterface.ice`.
[ 33%] Building CXX object source/component_tutorials/components/hello_world/CMakeFiles/hello_world_ice.dir/ComponentInterface.cpp.o
[ 50%] Linking CXX shared library ../../../../lib/libcomponent_tutorials_hello_world_ice.so
[ 50%] Built target hello_world_ice
[ 66%] Building CXX object source/component_tutorials/components/hello_world/CMakeFiles/hello_world_cmp.dir/Component.cpp.o
[ 66%] Built target hello_world_cmp
[ 83%] Building CXX object source/component_tutorials/components/hello_world/CMakeFiles/hello_world_run.dir/.../code/armarx/ArmarXCore/etc/templates/decoupled_main.cpp.o
[100%] Linking CXX executable ../../../../bin/hello_world_run
[100%] Built target hello_world_run

You may get warnings about clock-skews on H²T lab PCs: You can ignore them (they are caused by minor delays due to network-based file access).

Look at that a bit. Note especially the lines starting with Built the target .... We can find these lines:

Built target hello_world_ice
Built target hello_world_cmp
Built target hello_world_run

Here we see that the component we generated actually defines three "targets" (a target is something that can be built), with different suffixes (_ice, _cmp, and _run).

  • The _ice target has to do with the strange .ice file from before - it can wait for later.
  • The _cmp target is the main target of the component. Technically (in C++ terms), it is a library, i.e. a collection of code (e.g. classes and functions) without a main() function. It is not executable on its own.
    Note
    We will use the term "library" in a narrower, more semantic sense in ArmarX: Libraries are a place for code that can be used to solve your concrete use case. A component is already a use case, and is not meant for re-use on a code-level. So in this semantic sense, a component should not be seen as a library.
  • The _run target is the application (we mentioned it above). This one is an executable, i.e. a program with a main() function that can be executed.

Start the Empty Component

Why don't we start the application/component pair right now and see what happens? After all, an empty component is also a valid one (it is just pretty boring). The executable (also sometimes called a binary, although compiled libraries are binary as well, of course) is in the build/bin/ directory, and has the same name as the application target. So, from the build directory, we can run it this way:

./bin/hello_world_run

If you get an error like this:

$ ./bin/hello_world_run
[315165][14:44:13.557][][std::cerr]: Could not contact default locator at 'IceGrid/Locator:tcp -p 16927 -h localhost'
Did you start armarx?
To start armarx: armarx start
To kill a hanging armarx: armarx killIce

do as said and start ArmarX (armarx start), then try again.

The application will probably print something like

[315364][14:45:40.396][][IceManager]: Topic Log created

and will then keep running (like the ArmarX GUI before). You can stop it by pressing Ctrl+C in the terminal window:

^C[315364][14:48:29.601][hello_world][Application::interruptCallback(int)]: Interrupt received: 2
[315386][14:48:29.610][hello_world][ObjectScheduler]: disconnecting object hello_world
[315382][14:48:29.617][hello_world][ObjectScheduler]: disconnecting object hello_worldThreadList
[315566][14:48:29.643][hello_world][ArmarXManager]: Shutdown of ArmarXManager finished!

Hooray! We created, built, started and stopped our first ArmarX component! However, it is not very interesting at the moment. So, let us fill it with life.

Make the Component Do Stuff

... where "do stuff" means printing "Hello World!", as is customary for a first program in a new world. For that, we have to edit the component's source code. And for that, we need an IDE.

Excursion: Set up a C++ IDE

There are different C++ IDEs out there: QtCreator by Qt, Visual Studio by Microsoft, CLion by JetBrains, and of course hundreds of editors with C++ extensions that fight for their right of being called an IDE. They all have their benefits and shortcomings. Here, we will focus on QtCreator. One reason is that the ArmarX GUI is built on the well-established GUI framework Qt, and the QtCreator is equipped with tools for that purpose. However, it is also a pretty solid C++ IDE in general. Of course, you are free to use whatever IDE or vim-like editor you want, but you have to map what we show here to that yourself.

To set up QtCreator and configure it properly for ArmarX, follow How To Set up QtCreator for ArmarX.

Open the ArmarX Package in QtCreator

Start QtCreator from a terminal where your ArmarX workspace is active:

qtcreator

Starting it from the terminal that QtCreator uses the well-defined environment of your ArmarX workspace.

In QtCreator:

  1. From the menu in the top, choose File > Open File or Project ...
  2. Navigate to your package ($ARMARX_WORKSPACE/tutorials/component_tutorials if you followed this tutorial). Choose the **CMakeLists.txt** and press Open.
  3. Now QtCreator asks you to configure the project's Kit. This step is only necessary when you open a project in QtCreator for the first time. A Kit is a suite of settings that QtCreator uses to configure projects (and their CMake). If you have set up QtCreator according to the instructions above, you should have an "ArmarX" Kit. This what you want to use. Also, you usually want to build in "Release with Debug Information". This means that you want the compiler to somewhat optimize the code, but leave enough information that you can debug an application. You achieve that this way:
    1. Uncheck the Imported Kit
    2. Check the ArmarX Kit
    3. Expand the ArmarX Kit and uncheck all entries except "Release with Debug Information".
    4. Make sure that the build directory is correct, i.e. the same we used so far (.../component_tutorials/build).

The Kit configuration should look like this:

Use the ArmarX Kit for the opened project.

Finally, press Configure Project (button on the lower right). This should lead you to the Edit mode. (Look at the dark grey sidebar on the left side. You were at the Projects mode just now.) Expand your project.

If the project tree contains an entry <File System> like here,

QtCreator failed to fully parse the project.

it means that QtCreator was not able to fully parse the project. To fix it, we have to change back to the Projects mode. You will probably find something like this:

The project view shows a warning with the current project.

In this case, press Re-configure with Initial Parameters and confirm with Yes. This should resolve the warning.

If you now go back to the Edit mode and expand your project, it should look like this:

The project tree in QtCreator.

What a beauty! It is a really nice package, if I may daresay.

You can see the different directories and files that we found after creating the component. The lines with the small hammer icon are the CMake targets: (As said before, a target is something that can be built. Modern CMake has a strong focus on targets.) Looking closely, you can see that there are the same three targets we noticed before: hello_world_cmp, hello_world_ice and hello_world_run. In addition, you see how the files are associated with them:

  • Component.h and Component.cpp belong to hello_world_cmp.
  • ComponentInterface.ice belongs to hello_world_ice.
  • The hello_world_run looks a bit strange: It is actually associated with a file decoupled_main.cpp from ArmarXCore. If you open that file, you can find a small main() function. This is a bit of a trick to save some build time and disk space. For now, you do not have to think about it too much.

At this point, take a moment to open your component's CMakeLists.txt (the one in hello_world). It should look something like this:

armarx_add_component(hello_world
ICE_FILES
ComponentInterface.ice
ICE_DEPENDENCIES
ArmarXCoreInterfaces
# RobotAPIInterfaces
# ARON_FILES
# aron/my_type.xml
SOURCES
Component.cpp
HEADERS
Component.h
DEPENDENCIES
# ArmarXCore
ArmarXCore
## ArmarXCoreComponentPlugins # For DebugObserver plugin.
# ArmarXGui
## ArmarXGuiComponentPlugins # For RemoteGui plugin.
# RobotAPI
## RobotAPICore
## RobotAPIInterfaces
## RobotAPIComponentPlugins # For ArViz and other plugins.
# DEPENDENCIES_LEGACY
## Add libraries that do not provide any targets but ${FOO_*} variables.
# FOO
# If you need a separate shared component library you can enable it with the following flag.
# SHARED_COMPONENT_LIBRARY
)

This is CMake code: Yes, CMake is a bit of its own programming language. The lines starting with a # are comments. Note the following things:

  • There is a single command, armarx_add_component(), which takes several arguments.
  • The first argument (written on the same line) is hello_world: The component's name.
  • The next arguments are written in a key-values pattern: First the KEY in capital letters, then (indented) one or more values.
  • We can discover our files:
    • ComponentInterface.ice is part of the ICE_FILES argument
    • Component.cpp is part of the SOURCES
    • Component.h is part of the HEADERS
  • Also, there are DEPENDENCIES and ICE_DEPENDENCIES, which contain ArmarXCore and ArmarXCoreInterfaces at the moment.
  • There are a lot of out-commented lines. These are common lines that you often want to add, so they are added as opt-in directly, so you can easily enable or delete them.

See CMake for more details about ArmarX CMake commands.

Okay, so far so good. Let's open some actual code!

Examine the Component's Source Code

Finally, let's have a look at the C++ code. Let's start with the header file, Component.h. Open it using your mouse-enabled double click skill.

The Header File (<tt>Component.h</tt>)

As you can see, the component's code also has a lot of opt-in code which is out-commented by default. Let's remove all that for the moment. When remove even more, so we get the absolute minimum, the component header looks like this:

#pragma once
namespace component_tutorials::components::hello_world
{
class Component :
virtual public armarx::Component
{
public:
/// @see armarx::ManagedIceObject::getDefaultName()
std::string getDefaultName() const override;
/// Get the component's default name.
static std::string GetDefaultName();
protected:
/// @see PropertyUser::createPropertyDefinitions()
armarx::PropertyDefinitionsPtr createPropertyDefinitions() override;
/// @see armarx::ManagedIceObject::onInitComponent()
void onInitComponent() override;
/// @see armarx::ManagedIceObject::onConnectComponent()
void onConnectComponent() override;
/// @see armarx::ManagedIceObject::onDisconnectComponent()
void onDisconnectComponent() override;
/// @see armarx::ManagedIceObject::onExitComponent()
void onExitComponent() override;
private:
static const std::string defaultName;
};
} // namespace component_tutorials::components::hello_world

Let's quickly go through the most important parts:

#pragma once

This is a standard C++ include guard. Every header file should have it. In older files, you will find a #ifndef XY_H / #define XY_H / #endif combination. #pragma once is like a modern shorthand for that.

Here, we include the base class of all components: armarx::Component. It is located in the ArmarX package ArmarXCore.

namespace component_tutorials::components::hello_world
{

The armarx-package tool generates your component in a respective namespace, which is made to match the directory structure. Therefore, the first part is the package name (component_tutorials), the second part is components (analogous to the directory), and the last part is hello_world for your component. It is a good idea to try to match directories with namespaces (to the extent where it makes sense).

class Component :
virtual public armarx::Component
{

The component is a class which derives / inherits (at least) from the armarx::Component base class.

public:
/// @see armarx::ManagedIceObject::getDefaultName()
std::string getDefaultName() const override;
/// Get the component's default name.
static std::string GetDefaultName();

The armarx::Component class has a few abstract methods ("pure virtual" in C++ terms) that our component class needs to implement. One of them is getDefaultName(), which should return the component's default name ("hello_world" in our example). In addition, we have a static method with the same purpose. Why do we have both?

  • The static function can be called without creating an instance of the class.
  • Static functions cannot be inherited in C++. Therefore, the non-static function is necessary to allowing inheriting it. In turn, this enables the base class (armarx::Component) to call it using polymorphism.
protected:
/// @see PropertyUser::createPropertyDefinitions()
armarx::PropertyDefinitionsPtr createPropertyDefinitions() override;
/// @see armarx::ManagedIceObject::onInitComponent()
void onInitComponent() override;
/// @see armarx::ManagedIceObject::onConnectComponent()
void onConnectComponent() override;
/// @see armarx::ManagedIceObject::onDisconnectComponent()
void onDisconnectComponent() override;
/// @see armarx::ManagedIceObject::onExitComponent()
void onExitComponent() override;

These are the main functions we use to customize the component. The first (createPropertyDefinitions()) has to do with properties. Properties are the ArmarX solution for making components parameterizable. Think of a "property" like a "parameter" of your component has, e.g. a name or a threshold.

The other methods are used to implement the component lifecycle. There are different states a component can be in:

  • Initializing itself (onInitComponent()),
  • being connected to other components (onConnectComponent()),
  • having lost connection to a required component (onDisconnectComponent()),
  • and being shut down (onExitComponent())

See the documentation of ManagedIceObject for details about the lifecycle.

private:
static const std::string defaultName;
};

This static member variable is used to implement the static function GetDefaultName(). Also, we close the class Component here.

Note
In C++ terms, a "member" is an element of a class. For example, a method would be a "member function." An attribute would be called a "member variable." So a "member" can be an attribute or a method. Its opposite are "free" variables and functions, which are not part of a class.
} // namespace component_tutorials::components::hello_world

Finally, we close the namespace.

Okay, let's go to the source (.cpp) file:

The Source File (<tt>Component.cpp</tt>)

The minimal source file of our component looks like this:

#include "Component.h"
namespace component_tutorials::components::hello_world
{
const std::string
Component::defaultName = "hello_world";
Component::createPropertyDefinitions()
{
return def;
}
void
Component::onInitComponent()
{
}
void
Component::onConnectComponent()
{
}
void
Component::onDisconnectComponent()
{
}
void
Component::onExitComponent()
{
}
std::string
Component::getDefaultName() const
{
return Component::defaultName;
}
std::string
Component::GetDefaultName()
{
return Component::defaultName;
}
ARMARX_REGISTER_COMPONENT_EXECUTABLE(Component, Component::GetDefaultName());
} // namespace component_tutorials::components::hello_world

Let's go through it.

First, we include the header file Component.h - this is standard C++. In addition, we need a header DecoupledSingleComponent/Decoupled.h from ArmarXCore. Remember the file decoupled_main.cpp from ArmarX that was associated with the application target (hello_world_run)? This has to do with that.

namespace component_tutorials::components::hello_world
{

Next, we open the same namespace as in the header. Again, standard C++.

const std::string
Component::defaultName = "hello_world";

This is the definition of the static const variable defaultName that was declared in the header. In C++, static variables need to be defined in source (.cpp) files (with a few exceptions such as int).

In C++, many things such as functions and variables are declared in header files

and defined in source files.

A declaration makes a symbol (e.g. a function or variable name) known to the compiler and tells it a few important things, such as the function signature or type. The definition actually defines the content of the symbol, i.e. the function code or variable value.

Component::createPropertyDefinitions()
{
return def;
}
void
Component::onInitComponent()
{
}
void
Component::onConnectComponent()
{
}
void
Component::onDisconnectComponent()
{
}
void
Component::onExitComponent()
{
}

Next, we have (almost) empty definitions (i.e. implementations) of the property and lifecycle methods declared in the header. This is where will soon hook ourselves into.

std::string
Component::getDefaultName() const
{
return Component::defaultName;
}
std::string
Component::GetDefaultName()
{
return Component::defaultName;
}

These are the implementations of the default name-related functions. They both just return the static constant defaultName. You usually do not have to change anything here.

ARMARX_REGISTER_COMPONENT_EXECUTABLE(Component, Component::GetDefaultName());

This special line again has to do the "decoupled" main thing. Just leave this line as is and do not think too much about it.

} // namespace component_tutorials::components::hello_world

Finally, we close the opened namespace.

Edit the Component's Source Code

So far so good. Now, where were we ...? Oh yes, we wanted to print "Hello World!" somewhere. Where would we add that now?

When a component is being started, a few things are happening in order:

  1. The component is constructed, i.e. an instance of our Component class is created.
  2. The component is initialized. Here, we can initialize the component internally.
  3. The component is connected. Here, we can hook up with other components.

We usually do not create or edit the component's constructor. So, the first point where we can do stuff, is during the initialization. Therefore, we need to edit the method onInitComponent(), which is called during initialization to allow the component to initialize itself.

We are finally going to write code! Take the method onInitComponent(), and add the following line to it:

void
Component::onInitComponent()
{
std::cout << "Hello World!" << std::endl;
}

This is the standard way of printing things to the standard output in C++.

You could now build the project in the terminal like we did before (make or cmake --build . in the build directory). However, you can also do it in QtCreator directly.

  • Either press the hammer icon in the lower left corner
  • or just use the shortcut Ctrl+B.

QtCreator should open the Compile Output pane showing you the log of the build process. Wait until the build is finished.

Let's run the new build. As with building, we could start the application from the terminal (./bin/hello_world_run). But QtCreator can help us here, as well: You can start run the application by pressing the green play button in the lower left corner, just above the hammer icon (the one without the bug). When you press that button, QtCreator should open the Application Output pane and display the output of the running application. There, you should see the following lines:

12:00:48: Starting /.../code/tutorials/component_tutorials/build/bin/hello_world_run ...
[13579][12:00:58.364][hello_world][std::cout]: Hello World!

There it is! We did it! We created a component and convinced it to announce our very entering of the new world!

But wait, there is one more thing before we can pat ourselves on the back. The std::cout << ... << std::endl way of printing is very simple, but also quite limited. As many other frameworks, ArmarX has its own logging infrastructure. Using that, the line above would look like this:

void
Component::onInitComponent()
{
ARMARX_INFO << "Hello World!";
}

No, you do not need the << std::endl at the end when using ARAMRX_INFO.

Change the code to this version now, then compile and run the application again.

If you get an error starting like this:

[23259][12:08:18.200][hello_world][void armarx::handleExceptions()]: Caught IceUtil::Exception:
/.../code/armarx/ArmarXCore/source/ArmarXCore/core/ArmarXManager.cpp:278: ::Ice::AlreadyRegisteredException:
::component_tutorials::components::hello_world::ComponentInterface with id `hello_world' is already registered

It means that there is already an application running that registered the component hello_world. In ArmarX, there can only be one component with a specific name (it is like an ID). Probably, you did not stop the application you started earler. Check your terminals and the Application Output pane of QtCreator. Stop any applications that are still running, and try again.

Now, you should see this line:

[23650][12:11:34.390][hello_world][hello_world]: Hello World!

Compare that to the line from before:

[13579][12:00:58.364][hello_world][std::cout]: Hello World!

They are not that much different from each other.

  • The first parts are the process IDs (23650 and 13579) and the time stamps, which naturally differ because we ran the component multiple times.
  • The next part, [hello_world], is the same in both: This is the name of the running application, which is the same as the component's name.
  • The next part differs: [hello_world] vs. [std::cout]. This part the log tag. It depends on the context of the logging code. In a component, the tag is just the component's name. In a plain C++ class, it can be defined by the programmer. So the log tag is a useful way to indicate where the log message came from.

However, when we use just std::cout << ... << std::endl, ArmarX has no way of knowing where the log came from, because std::cout does not track such information. Instead, the macro ARMARX_INFO is aware of its context and passes that information on.

In addition, ArmarX has several ordered log levels. They are, from most to least detailed:

  • ARMARX_DEBUG
  • ARMARX_VERBOSE
  • ARMARX_INFO
  • ARMARX_WARNING
  • ARMARX_ERROR
  • ARMARX_FATAL

ArmarX provides tools that allow you to filter log messages according to their log level. So in summary, in ArmarX, do not use

std::cout << ... << std::endl;

but instead one of these logging macros, e.g.

ARMARX_INFO << ...;

(without the << std::endl at the end).

See Logging of output and data for more details about the ArmarX logging framework.

Todo:
Add an explaination on how to add the component in the scenario manager

Conclusion and TL;DR

You did it! Now you can start patting you on the back. Yes, right there. Good job.

Now, that probably felt like a lot. And yes, we touched on a lot of points. Let's look back at what you actually did:

  1. You created a new ArmarX package using armarx-package init component_tutorials in a directory of your choice.
  2. You added a component hello_world to that package by running armarx-package add component hello_world in the root directory of the created package component_tutorials.
  3. You added the line ARMARX_INFO << "Hello World!"; to the method onInitComponent() of the generated component class.
  4. You configured the project by running cmake .. and built it by running cmake --build . or make in the build directory of the package.
  5. You ran the component by starting the executable via ./bin/hello_world_run in the build directory of the package.

In addition, you learned something about the background of each of these steps. We could go on, e.g. about how to create a scenario with your component and how to add parameters to it. But we will leave those topics for the next tutorials. For now, this tutorial is finished.

Next Up

If you are still up for more, you can continue with the next tutorial: Understand Distributed Systems

ARMARX_REGISTER_COMPONENT_EXECUTABLE
#define ARMARX_REGISTER_COMPONENT_EXECUTABLE(ComponentT, applicationName)
Definition: Decoupled.h:31
Component.h
armarx::Component
Baseclass for all ArmarX ManagedIceObjects requiring properties.
Definition: Component.h:95
Decoupled.h
armarx::ComponentPropertyDefinitions
Default component property definition container.
Definition: Component.h:70
ARMARX_INFO
#define ARMARX_INFO
Definition: Logging.h:174
IceUtil::Handle< class PropertyDefinitionContainer >