Aron Type Reading
As already mention in the introduction, aron supports code generation in order to create a C++ (other languages also possible) class with plain C++ members which you can use for convenience. Further, the generated class offers methods to convert itself to an Aron object and to set the members from an Aron object. Code generation only makes sense for Aron data, however we need an Aron type specification in order to generate the class.
In the following we will describe how to specify Aron types in XML and how the generated code looks like.
XML type description and creation
In order to make Aron generate a C++ class for you, you first have to tell the program how the object should look like. Second you need to add the file to cmake in order to create the code generation target.
XML type description
Consider you want to use a data type called MyData
(in the namespace armarx::mydata
).
- Choose a library in an ArmarX package where your data type will be defined (or create a new one). Here, we will call it
MyDataLib
.
- In the directory
MyDataLib/
, create a directory aron/
(if necessary). Inside, add a file MyData.xml
. The directory structure should now look like this:
.../libraries/
- MyDataLib/
- CMakeLists.txt
- ...
- aron/
- MyData.xml
Then start defining your data in ARON. A hello world example could look like this:
<?xml version="1.0" encoding="UTF-8" ?>
<AronTypeDefinition>
<CodeIncludes>
</CodeIncludes>
<AronIncludes>
</AronIncludes>
<GenerateTypes>
<Object name="armarx::mydata::arondto::MyData">
<ObjectChild key="helloWorld">
<String />
</ObjectChild>
</Object>
</GenerateTypes>
</AronTypeDefinition>
Every type specification must have the top-level tag AronTypeDefinition
and must at least define one type in GenerateTypes
. In the GenerateTypes
tag you can add as many Object
or IntEnum
definitions as you want. <Object>
defines a new ARON object type (generating a C++ class) containing a number of children. Inside the Object
-tag you can add as many members as you want through the ObjectChild
-tag (generating a public member variable of the generated C++ class, with key
specifying the member's name). If a member should be e.g. optional you have to add the attribute optional="true"
to the member:
<ObjectChild key="helloWorld">
<String optional="true" />
</ObjectChild>
The same way you can define raw_ptr
, shared_ptr
or unique_ptr
members.
Conventions:
- Put your ARON object type into a
arondto::
namespace inside your usual namespace (e.g., armarx::mydata::arondto::MyData
), where "DTO" stands for "Data Transfer Object". This way, you can define or use a custom or existing C++ type with more methods/intelligence in the usual namespace
in your business logic code (e.g. armarx::mydata::MyData
).
If you want to use a definition of another Aron file, you can include the file using the AronIncludes
section. Simply add the files you want to include using a include
tag:
<AronIncludes>
<Include include="RobotAPI/libraries/aron/common/aron/PackagePath.xml" />
...
</AronIncludes>
After that, you can use the type definitions in your current xml file (you must specify the full namespace):
<ObjectChild key="referenceToMemoryID">
<armarx::arondto::PackagePath />
</ObjectChild>
Examples
In the following we define a class that uses all more complex types once:
<?xml version="1.0" encoding="UTF-8" ?>
<AronTypeDefinition>
<CodeIncludes>
<Include include="<Eigen/Core>" />
<Include include="<Eigen/Geometry>" />
<Include include="<pcl/point_cloud.h>" />
<Include include="<pcl/point_types.h>" />
<Include include="<opencv2/core/core.hpp>" />
</CodeIncludes>
<GenerateTypes>
<IntEnum name="TheIntEnum">
<EnumValue key="INT_ENUM_VALUE_0" value="0" />
<EnumValue key="INT_ENUM_VALUE_1" value="1" />
<EnumValue key="INT_ENUM_VALUE_2" value="2" />
<EnumValue key="INT_ENUM_VALUE_3" value="3" />
<EnumValue key="INT_ENUM_VALUE_4" value="4" />
<EnumValue key="INT_ENUM_VALUE_5" value="5" />
<EnumValue key="INT_ENUM_VALUE_6" value="6" />
<EnumValue key="INT_ENUM_VALUE_7" value="7" />
<EnumValue key="INT_ENUM_VALUE_8" value="8" />
</IntEnum>
<Object name="armarx::mydata::arondto::MyData">
<objectchild key="the_int_enum">
<TheIntEnum />
</objectchild>
<ObjectChild key="the_dict">
<Dict>
<Float />
</Dict>
</ObjectChild>
<ObjectChild key='the_list'>
<List>
<Float />
</List>
</ObjectChild>
<ObjectChild key='the_short_matrix'>
<Matrix rows="5" cols="7" type="int16" />
</ObjectChild>
<ObjectChild key='the_double_quaternion'>
<Quaternion type="float64" />
</ObjectChild>
<ObjectChild key='the_position'>
<Position />
</ObjectChild>
<ObjectChild key='the_orientation'>
<Orientation />
</ObjectChild>
<ObjectChild key='the_pose'>
<Pose />
</ObjectChild>
<ObjectChild key='the_rgb24_image'>
<Image type="rgb24" />
</ObjectChild>
<ObjectChild key='the_xyzrgb_pointcloud'>
<PointCloud type="PointXYZRGB" />
</ObjectChild>
</Object>
</GenerateTypes>
</AronTypeDefinition>
CMake Specification
In the CMakeLists.txt
, add or extend after the definition of the target (e.g. through add_library
or add_component
):
armarx_enable_aron_file_generation_for_target(
TARGET_NAME
${LIB_NAME}
ARON_FILES
aron/MyData.xml
)
Important changes
- I changed the xml type reader so that embedded classes are not allowed anymore! Before that you were able to define a new class inside an existing one, e.g.:
<?xml version="1.0" encoding="UTF-8" ?>
<AronTypeDefinition>
<GenerateTypes>
<Object name="armarx::mydata::arondto::MyData">
<ObjectChild key="helloWorld">
<Object name="armarx::mydata::arondto::MyOtherData">
...
</Object>
</ObjectChild>
</Object>
</GenerateTypes>
</AronTypeDefinition>
This is not supported anymore!
Full example
Say you have the following Aron XML type description:
<?xml version="1.0" encoding="UTF-8" ?>
<AronTypeDefinition>
<GenerateTypes>
<IntEnum name="armarx::NaturalIKControlMode">
<EnumValue key="CONTROL_MODE_0" value="0" />
<EnumValue key="CONTROL_MODE_1" value="1" />
<EnumValue key="CONTROL_MODE_2" value="2" />
</IntEnum>
<Object name='armarx::NaturalIKResult'>
<ObjectChild key='control_mode'>
<armarx::NaturalIKControlMode />
</ObjectChild>
<ObjectChild key='reached'>
<Bool />
</ObjectChild>
<ObjectChild key='jointValues'>
<List>
<Float />
</List>
</ObjectChild>
</Object>
</GenerateTypes>
</AronTypeDefinition>
The generated C++ file looks like:
#pragma once
#include <memory>
#include <string>
#include <vector>
#include <map>
#include <RobotAPI/interface/aron.h>
#include <RobotAPI/libraries/aron/core/codegenerator/codewriter/cpp/AronCppClass.h>
{
class NaturalIKControlMode
: public armarx::aron::codegenerator::cpp::AronCppClass
{
public:
enum class ImplEnum
{
CONTROL_MODE_0,
CONTROL_MODE_1,
CONTROL_MODE_2,
};
public:
using This = armarx::NaturalIKControlMode;
static constexpr ImplEnum CONTROL_MODE_0 = ImplEnum::CONTROL_MODE_0;
static constexpr ImplEnum CONTROL_MODE_1 = ImplEnum::CONTROL_MODE_1;
static constexpr ImplEnum CONTROL_MODE_2 = ImplEnum::CONTROL_MODE_2;
const std::map<ImplEnum, std::string> EnumToStringMap = {
{ImplEnum::CONTROL_MODE_0, "CONTROL_MODE_0"},
{ImplEnum::CONTROL_MODE_1, "CONTROL_MODE_1"},
{ImplEnum::CONTROL_MODE_2, "CONTROL_MODE_2"},
};
const std::map<std::string, ImplEnum> StringToEnumMap = {
{"CONTROL_MODE_0", ImplEnum::CONTROL_MODE_0},
{"CONTROL_MODE_1", ImplEnum::CONTROL_MODE_1},
{"CONTROL_MODE_2", ImplEnum::CONTROL_MODE_2},
};
const std::map<ImplEnum, int> EnumToValueMap = {
{ImplEnum::CONTROL_MODE_0, 0},
{ImplEnum::CONTROL_MODE_1, 1},
{ImplEnum::CONTROL_MODE_2, 2},
};
const std::map<int, ImplEnum> ValueToEnumMap = {
{0, ImplEnum::CONTROL_MODE_0},
{1, ImplEnum::CONTROL_MODE_1},
{2, ImplEnum::CONTROL_MODE_2},
};
public:
NaturalIKControlMode()
{
resetHard();
}
NaturalIKControlMode(const armarx::NaturalIKControlMode& i)
{
}
NaturalIKControlMode(const ImplEnum e)
{
}
public:
bool operator==(
const armarx::NaturalIKControlMode& i)
const
{
if (not (
value == i.value))
{
return false;
}
return true;
}
virtual void resetHard() override
{
}
virtual void resetSoft() override
{
}
template<class T>
{
std::map<std::string, int> aron_str2ValueMap;
return aron_w.
writeIntEnum(
"armarx::NaturalIKControlMode", aron_str2ValueMap, aron_maybeType,
aron_p);
}
template<class T>
{
}
template<class T>
{
using TNonConst = typename std::remove_const<T>::type;
{
const TNonConst* _suppressUnusedWarning;
(void) _suppressUnusedWarning;
}
this->resetSoft();
{
}
int aron_tmpValue;
value = ValueToEnumMap.at(aron_tmpValue);
}
{
return EnumToStringMap.at(
value);
}
void fromString(
const std::string&
str)
{
if (
auto it = StringToEnumMap.find(
str); it == StringToEnumMap.end())
{
throw armarx::LocalException(
"The input name is not valid. Could net set the enum to value '" +
str +
"'");
}
else
{
}
}
operator int() const
{
return EnumToValueMap.at(
value);
}
armarx::NaturalIKControlMode& operator=(ImplEnum
v)
{
return *this;
}
armarx::NaturalIKControlMode& operator=(
const armarx::NaturalIKControlMode&
c)
{
return *this;
}
armarx::NaturalIKControlMode& operator=(
int v)
{
if (
auto it = ValueToEnumMap.find(
v); it == ValueToEnumMap.end())
{
throw armarx::LocalException(
"The input int is not valid. Could net set the enum to value '" +
std::to_string(
v) +
"'");
}
else
{
}
return *this;
}
};
}
{
class NaturalIKResult
: public armarx::aron::codegenerator::cpp::AronCppClass
{
public:
using This = armarx::NaturalIKResult;
armarx::NaturalIKControlMode control_mode;
std::vector<float> jointValues;
bool reached;
public:
NaturalIKResult()
{
resetHard();
}
public:
bool operator==(
const armarx::NaturalIKResult& i)
const
{
if (not (control_mode == i.control_mode))
{
return false;
}
if (not (jointValues == i.jointValues))
{
return false;
}
if (not (reached == i.reached))
{
return false;
}
return true;
}
virtual void resetHard() override
{
control_mode.resetHard();
jointValues = std::vector<float>();
reached = bool();
}
virtual void resetSoft() override
{
control_mode.resetSoft();
jointValues.clear();
reached = bool();
}
template<class T>
{
std::map<std::string, T> aron_objectMembers;
auto aron_objectExtends = std::nullopt;
auto aron_variant_control_mode = armarx::NaturalIKControlMode::writeType(aron_w, armarx::aron::type::Maybe::eNone,
armarx::aron::Path(
aron_p, {
"control_mode"}));
aron_objectMembers.emplace("control_mode", aron_variant_control_mode);
auto aron_variant_jointValues = aron_w.
writeList(aron_variant_jointValues_dot_accepted_type, armarx::aron::type::Maybe::eNone,
armarx::aron::Path(
aron_p, {
"jointValues"}));
aron_objectMembers.emplace("jointValues", aron_variant_jointValues);
aron_objectMembers.emplace("reached", aron_variant_reached);
return aron_w.
writeObject(
"armarx::NaturalIKResult", aron_objectMembers, aron_objectExtends, aron_maybeType,
aron_p);
}
template<class T>
{
std::map<std::string, T> aron_objectMembers;
std::optional<T> aron_objectExtends;
auto aron_variant_control_mode = aron_w.
writeNull();
aron_objectMembers.emplace("control_mode", aron_variant_control_mode);
auto aron_variant_jointValues = aron_w.
writeNull();
std::vector<T> aron_variant_jointValues_listElements;
for(unsigned int aron_jointValues_it = 0; aron_jointValues_it < jointValues.size(); ++aron_jointValues_it)
{
auto aron_variant_jointValues_dot_at_lbrR_aron_jointValues_it_rbrR_ = aron_w.
writeNull();
aron_variant_jointValues_dot_at_lbrR_aron_jointValues_it_rbrR_ = aron_w.
writePrimitive(jointValues.at(aron_jointValues_it),
armarx::aron::Path(
aron_p, {
"jointValues", std::to_string(aron_jointValues_it)}));
aron_variant_jointValues_listElements.push_back(aron_variant_jointValues_dot_at_lbrR_aron_jointValues_it_rbrR_);
}
aron_objectMembers.emplace("jointValues", aron_variant_jointValues);
auto aron_variant_reached = aron_w.
writeNull();
aron_objectMembers.emplace("reached", aron_variant_reached);
}
template<class T>
{
using TNonConst = typename std::remove_const<T>::type;
{
const TNonConst* _suppressUnusedWarning;
(void) _suppressUnusedWarning;
}
this->resetSoft();
{
}
std::map<std::string, TNonConst> aron_objectMembers;
control_mode.read<
T>(aron_r, aron_objectMembers.at(
"control_mode"));
std::vector<TNonConst> aron_jointValues_listElements;
aron_r.readList(aron_objectMembers.at("jointValues"), aron_jointValues_listElements);
for (const auto& aron_jointValues_listValue : aron_jointValues_listElements)
{
float aron_jointValues_listTmp;
aron_r.readPrimitive(aron_jointValues_listValue, aron_jointValues_listTmp);
jointValues.push_back(aron_jointValues_listTmp);
}
aron_r.readPrimitive(aron_objectMembers.at("reached"), reached);
}
{
}
{
This t;
return t;
}
{
this->read<armarx::aron::data::reader::VariantReader::InputType>(reader, (
input));
}
{
}
};
}
As you see, the code generation creates a class for the NaturalIKControlMode
enum. This special class provides convenience methods compared to normal C++ enums. Further, it creates a class for the NaturalIKResult
object.
Both, the enum and the object provide methods for reading and writing. The read
method is used to parse a Aron object in an arbitrary representation (e.g. Aron variant or nlohmann::json) and to set the C++ members to the same values as the Aron object. To do so it uses a ReaderInterface implementation (see ARON Readers, Writers and Conversion). The write
method is used to create an Aron object in an arbitrary representation with the same values as the C++ object. This method uses a WriterInterface implementation.
For convenience, the toAron()
and fromAron()
methods, which internally use the read
and write
methods, are created. To get the structure of the C++ class as an Aron type object you can use the static toAronType()
method which internally uses the writeType
method.
Further, the code generation creates methods to compare two classes with each other (right now only operator==).
This means, when using Aron, you don't need to mess around with the Aron DTOs and Aron objects, you only have to set and use plain C++ members of a generated class!
(Optional) Add conversions to existing (potentially more intelligent) C++ types
Consider the case of simox::OrientedBoxf
and simox::arondto::OrientedBox
. The former is the business object (BO), used in daily business logic code (as it offers useful methods to construct and manipulate it). The latter is a mere data storage used to transfer the data, which is called a data transfer object (DTO).
Therefore, you usually want to convert a simox::arondto::OrientedBox
to a simox::OrientedBoxf
as soon as you want to do things other than passing it around over network (e.g., at the beginning of a component method or after reading it from the working memory). Likewise, you want to convert a simox::OrientedBoxf
to a simox::arondto::OrientedBox
when you send the information over network or store it in the memory.
To allow this conversion, follow these steps:
- In your library, add a file pair
aron_conversions.{h, cpp}
next to your aron/
directory and add them to the CMakeLists.txt
- In the header, declare functions following this form:
#pragma once
#include <SimoxUtility/shapes/OrientedBox.h>
#include <RobotAPI/libraries/aron/common/aron/OrientedBox.aron.generated.h>
{
}
Note:
- Both functions take the DTO first and the BO second.
fromAron()
takes a const
DTO and a non-const
BO.
toAron()
takes a non-const
DTO and a const
BO.
- All arguments are passed as reference (especially the
non-const
arguments).
- The functions are defined in the BO's namespace (
simox
in this example, armarx::mydata
in the running example).
- In the cpp file, define the functions like this:
#include "aron_conversions.h"
{
bo = OrientedBoxf(dto.center, dto.orientation, dto.extents);
}
{
dto.center =
bo.center();
dto.orientation =
bo.rotation();
dto.extents =
bo.dimensions();
}
- Note
- The implementation depends on your data. It might be as simple as copying all members, or require more complex conversions. Especially, you might use
fromAron()
for toAron()
for other ARON object types.
Lessons learned
- How to create a XML file with a valid Aron specification
- How to add this file to CMake
- How to use the generated class and how to convert it to an Aron object (e.g. for sending it to the memory)