How to Implement a Custom Unit

Three components are required for implementing a custom unit in ArmarX:

  • A slice interface for the unit
  • A Component-derived class that implements this interface
  • An application that makes the unit executable

The Interface

The only capability of the demo unit that is developed in this howto is to periodically publish a certain value. The value is internally stored as a Variant and can be changed using the unit's interface. Every unit needs to define two interfaces:

module armarx
{
interface ExampleUnitInterface
{
void setPeriodicValue(VariantBase value);
};
interface ExampleUnitListener
{
void reportPeriodicValue(VariantBase value);
};
};

The ExampleUnit defines the two interfaces ExampleUnitInterface and ExampleUnitListener. The difference between these two interfaces is the component that is intended to implement it. The first interface, ExampleUnitInterface, will be implemented by the unit itself and can therefore define functions that manipulate the unit's behavior. In this case, we add a function that changes the value published by the unit. The other interface is to be implemented by any component that aims at receiving the unit's published values. This is primarily the matching observer. Therefore the functions defined in the listener-interface can be seen as callbacks for the observer, called whenever new values got published.

To satisfy the linker, the interface library of the unit-interface need to be linked. To do so, add the library name (here ExampleUnitInterfaces) to the COMPONENT_LIBS list in your CMakeLists.txt of your component.

The Component

A unit is essentially a component that implements the respective unit interface:

class ExampleUnit :
virtual public ExampleUnitInterface,
virtual public Component
{
public:
std::string getDefaultName() const override
{
return "ExampleUnit";
}
void onInitComponent() override;
void onConnectComponent() override;
void onDisconnectComponent() override;
void setPeriodicValue(const VariantBasePtr& value, const Ice::Current& c = Ice::emptyCurrent) override;
PropertyDefinitionsPtr createPropertyDefinitions() override;
protected:
void reportValue();
ExampleUnitListenerPrx listenerPrx;
PeriodicTask<ExampleUnit>::pointer_type periodicTask;
int period;
VariantPtr periodicValue;
};
using ExampleUnitPtr = IceInternal::Handle<ExampleUnit>;

The unit implementation ExampleUnit derives from the base class Component and from the interface ExampleUnitInterface. Both base classes define virtual methods that need to be implemented. In case of a Component, these methods are getDefaultName, onInitComponent, onConnectComponent and onExitComponent. The ExampleUnitInterface solely defines the method setPeriodicValue that is used by other components to configure ExampleUnit. Apart from that and some internal members for handling the emittance period and the value to publish, the unit needs a proxy to the ExampleUnitListener-interface. The type of this proxy is ExampleUnitListenerPrx.

As any other component, units may have properties, in this case the reporting period in milliseconds:

class ExampleUnitPropertyDefinitions:
public ComponentPropertyDefinitions
{
public:
ExampleUnitPropertyDefinitions(std::string prefix):
ComponentPropertyDefinitions(prefix)
{
defineOptionalProperty<int>("ReportPeriod", 1, "The unit's reporting period in ms");
}
};

The implementation of the ExampleUnit class looks as follows:

void ExampleUnit::onInitComponent()
{
period = getProperty<int>("ReportPeriod").getValue();
periodicValue = VariantPtr(new Variant(10));
offeringTopic("PeriodicExampleValue");
}
void ExampleUnit::onConnectComponent()
{
listenerPrx = getTopic<ExampleUnitListenerPrx>("PeriodicExampleValue");
if (periodicTask)
{
periodicTask->stop();
}
periodicTask = new PeriodicTask<ExampleUnit>(this, &ExampleUnit::reportValue, period, false, "ExampleValueProducer");
periodicTask->start();
}
void ExampleUnit::onDisconnectComponent()
{
periodicTask->stop();
}
void ExampleUnit::setPeriodicValue(const VariantBasePtr& value, const Ice::Current& c)
{
periodicValue = VariantPtr::dynamicCast(value);
}
PropertyDefinitionsPtr ExampleUnit::createPropertyDefinitions()
{
return PropertyDefinitionsPtr(new ExampleUnitPropertyDefinitions(getConfigIdentifier()));
}
void ExampleUnit::reportValue()
{
listenerPrx->reportPeriodicValue(periodicValue, IceUtil::Time::now().toMicroSecondsDouble());
}

By calling offeringTopic, the unit declares the name under which it is going to publish values. An observer can subscribe to this topic and will then receive the emitted values. The unit needs to fetch a proxy to the ExampleUnitListener-interface, as it is not implementing this interface itself, but using it to publish the periodic value. Therefore it needs to call getTopic to obtain the respective proxy object.

The Application

The last part necessary for building a custom unit is the application that makes the unit actually executable. Like for other executable ArmarX Components, we need a ExampleUnitApp class and a respective main-function that instantiates this class. Both parts should be located in the applications folder of an ArmarX project.

int main(int argc, char* argv[])
{
return armarx::runSimpleComponentApp<armarx::ExampleUnit>(argc, argv, "ExampleUnit");
}
armarx::VariantBasePtr
::IceInternal::Handle<::armarx::VariantBase > VariantBasePtr
Definition: ManagedIceObject.h:111
c
constexpr T c
Definition: UnscentedKalmanFilterTest.cpp:43
IceInternal::Handle
Definition: forward_declarations.h:8
armarx::VariantPtr
IceInternal::Handle< Variant > VariantPtr
Definition: Variant.h:42
cxxopts::value
std::shared_ptr< Value > value()
Definition: cxxopts.hpp:926
armarx::PropertyDefinitionsPtr
IceUtil::Handle< class PropertyDefinitionContainer > PropertyDefinitionsPtr
PropertyDefinitions smart pointer type.
Definition: forward_declarations.h:34
main
int main(int argc, char *argv[])
Definition: Admin.cpp:46
armarx
This file offers overloads of toIce() and fromIce() functions for STL container types.
Definition: ArmarXTimeserver.cpp:28