Write a Memory Server and Client (C++)

Objective: Learn how to integrate a C++ data type into the memory system, create a server with a segment for that data type, and create a client reading from and writing to the server.

Previous Tutorials: Write a Server and Client Communicating via RPC (C++), Write a Publisher and Subscriber Communicating via Topics (C++)

Next Tutorials: none

Reference Code: memory_tutorial

More information in the RobotAPI Wiki.

Todo:
Move information to RobotAPI doc

Create a new ArmarX Package

Create a new package:

# Create a new ArmarX package:
armarx-package init memory_tutorial
# Prepare:
cd memory_tutorial/build
CC="gcc-8" CXX="g++-8" cmake ..
# or with ccache:
CC="ccache gcc-8" CXX="ccache g++-8" cmake ..
# Build (should do nothing at this point):
make

Create a Library

Add a library for the new modality:

cd .. # Move to the root directory of memory_tutorial.
armarx-package add library object_instance

You should now have a library generated in source/memory_tutorial/object_instance:

ls source/memory_tutorial/object_instance/

Test whether your package builds:

cd build/
cmake ..
make

Add the C++ Business Object (BO) Class

Use QtCreator or your file browser to create a new .cpp/.h pair for the class ObjectInstance.

The CMakeLists.txt should look like this:

armarx_add_library(object_instance
SOURCES
ObjectInstance.cpp
HEADERS
ObjectInstance.h
DEPENDENCIES
ArmarXCoreInterfaces
ArmarXCore
RobotAPICore # Required by FramedPose
)

The ObjectInstance class could look like this (ObjectInstance.h):

#pragma once
#include <Eigen/Core>
#include <SimoxUtility/shapes/OrientedBox.h>
namespace memory_tutorial::object_instance
{
/**
* @brief A pose in a semantic frame.
*/
class FramedPose
{
public:
std::string frame = armarx::GlobalFrame;
std::string agent = "";
};
/**
* @brief Business Object (BO) class of object instances.
*/
class ObjectInstance
{
public:
std::string name;
FramedPose pose;
armarx::PackagePath fileLocation = armarx::PackagePath("PriorKnowledgeData", "");
simox::OrientedBoxf localOobb;
};
}

The file ObjectInstance.cpp can stay relatively empty.

Add RobotAPI to your package's dependencies in the top-level CMakeLists.txt:

# Required ArmarX dependencies.
armarx_find_package(PUBLIC RobotAPI REQUIRED)

Add the ARON Data-Transfer-Object (DTO) Class

Add this at the top of the CMakeLists.txt of the object_instance library:

armarx_add_aron_library(object_instance_aron
ARON_FILES
aron/FramedPose.xml
aron/ObjectInstance.xml
)

The file content could like this:

FramedPose.xml:

<?xml version="1.0" encoding="UTF-8" ?>
<!--
Core segment type of Object/Instance.
-->
<AronTypeDefinition>
<GenerateTypes>
<Object name="memory_tutorial::object_instance::arondto::FramedPose">
<ObjectChild key="pose">
<Pose />
</ObjectChild>
<ObjectChild key="frame">
<String />
</ObjectChild>
<ObjectChild key="agent">
<String />
</ObjectChild>
</Object>
</GenerateTypes>
</AronTypeDefinition>

ObjectInstance.xml:

<?xml version="1.0" encoding="UTF-8" ?>
<!--
Core segment type of Object/Instance.
-->
<AronTypeDefinition>
<AronIncludes>
<Include include="<RobotAPI/libraries/aron/common/aron/OrientedBox.xml>" autoinclude="true" />
<Include include="<RobotAPI/libraries/aron/common/aron/PackagePath.xml>" autoinclude="true" />
<Include include="<memory_tutorial/object_instance/aron/FramedPose.xml>" autoinclude="true" />
</AronIncludes>
<GenerateTypes>
<Object name="memory_tutorial::object_instance::arondto::ObjectInstance">
<ObjectChild key="name">
<String />
</ObjectChild>
<ObjectChild key="pose">
<memory_tutorial::object_instance::arondto::FramedPose />
</ObjectChild>
<ObjectChild key="fileLocation">
<armarx::arondto::PackagePath />
</ObjectChild>
<ObjectChild key="oobb">
<simox::arondto::OrientedBox />
</ObjectChild>
</Object>
</GenerateTypes>
</AronTypeDefinition>

Add Forward Declarations (Optional)

A forward declaration is a declaration of a class without definition of its implementation:

class ObjectInstance;

This tells the compiler that the symbol ObjectInstance is a class, but not how it is comprised (i.e. which member variables and methods it has). Even without the definition, the class name can already be used as reference, pointer, function argument and function return type. Using forward declarations can heavily reduce the amount of includes the users of a class or header file pull into their code, and thus can heavily speed up compilation.

To relieve library users from writing their own forward declarations when they would like to use them for your types, it is customary to provide a forward_declarations.h header file maintained by the library maintainer. Let's add it:

armarx_add_library(object_instance
HEADERS
...
forward_declarations.h
...
)

The forward_declarations.h file can look like this:

#pragma once
namespace memory_tutorial::object_instance
{
class ObjectInstance;
class FramedPose;
}
namespace memory_tutorial::object_instance::arondto
{
class ObjectInstance;
class FramedPose;
}

As you can see, this file does not pull any other includes, but allows users to use your types where they do not require your classes to be fully defined (e.g. in their own headers).

Add Converter Functions

armarx_add_library(object_instance
SOURCES
...
aron_conversions.cpp
HEADERS
...
aron_conversions.h
DEPENDENCIES
...
aroncommon # For common converters.
object_instance_aron # To generate aron classes
)

The aron_conversions.h can look like this:

#pragma once
#include <memory_tutorial/object_instance/forward_declarations.h>
namespace memory_tutorial::object_instance
{
void toAron(arondto::ObjectInstance& dto, const ObjectInstance& bo);
void fromAron(const arondto::ObjectInstance& dto, ObjectInstance& bo);
}

As you can see, here we don't require the class definitions as they are only used as function arguments. The definitions are only included in the source file.

The functions are implemented in aron_conversions.cpp (note the explicit namespace specifications of the free functions to detect signature mismatch):

#include "aron_conversions.h"
#include <memory_tutorial/object_instance/ObjectInstance.h>
#include <memory_tutorial/object_instance/aron/ObjectInstance.aron.generated.h>
namespace memory_tutorial
{
{
dto.pose = bo.pose;
dto.frame = bo.frame;
dto.agent = bo.agent;
}
{
bo.pose = dto.pose;
bo.frame = dto.frame;
bo.agent = dto.agent;
}
void object_instance::toAron(arondto::ObjectInstance& dto, const ObjectInstance& bo)
{
dto.name = bo.name;
toAron(dto.pose, bo.pose);
toAron(dto.fileLocation, bo.fileLocation);
toAron(dto.oobb, bo.localOobb);
}
void object_instance::fromAron(const arondto::ObjectInstance& dto, ObjectInstance& bo)
{
bo.name = dto.name;
fromAron(dto.pose, bo.pose);
fromAron(dto.fileLocation, bo.fileLocation);
fromAron(dto.oobb, bo.localOobb);
}
}

Produce and Consume Data

Your application will usually involve

  • a data source (where your data comes from, e.g. algorithms results or other components),
  • a data sink (where your data goes to, e.g. algorithm inputs or other components),
  • or both.

For the sake of this tutorial, we will stub the data source and sink with a ProducerConsumer class. The ProducerConsumer has

  • a method produce(), which generates a an ObjectInstance (the data source)
  • a method consume(), which takes an ObjectInstance and prints and visualizes it.

Add the files ProducerConsumer.{h, cpp} and change the CMakeLists.txt to:

armarx_add_library(object_instance
...
SOURCES
...
ProducerConsumer.cpp
...
HEADERS
...
ProducerConsumer.h
...
DEPENDENCIES
...
ArViz # from RobotAPI
...
...
)

The file contents could look like this:

ProducerConsumer.h:

#pragma once
#include <optional>
#include <memory_tutorial/object_instance/forward_declarations.h>
namespace memory_tutorial::object_instance
{
class ProducerConsumer
{
public:
object_instance::ObjectInstance produce();
void consume(const object_instance::ObjectInstance& instance);
public:
std::optional<armarx::viz::Client> arviz;
};
}

ProducerConsumer.cpp:

#include "ProducerConsumer.h"
#include <chrono>
#include <Eigen/Core>
#include <SimoxUtility/math/pose/pose.h>
#include <memory_tutorial/object_instance/ObjectInstance.h>
namespace memory_tutorial::object_instance
{
object_instance::ObjectInstance ProducerConsumer::produce()
{
const int time_ms = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::system_clock::now().time_since_epoch()).count();
float speed = 0.25;
const float time = speed * static_cast<float>(time_ms) / 1000.f;
object_instance::ObjectInstance instance;
instance.name = "an amicelli object instance";
simox::math::position(instance.pose.pose) = 1000 * Eigen::Vector3f(std::sin(time), std::cos(time), 1.f);
instance.pose.frame = armarx::GlobalFrame;
instance.pose.agent = "";
instance.fileLocation = armarx::PackagePath("PriorKnowledgeData", "PriorKnowledgeData/objects/KIT/Amicelli/Amicelli.xml");
instance.localOobb = simox::OrientedBoxf(
Eigen::Vector3f::Zero(), Eigen::Quaternionf::Identity(),
100 * Eigen::Vector3f(1, 2, 3));
return instance;
}
void ProducerConsumer::consume(const object_instance::ObjectInstance& instance)
{
ARMARX_INFO << "Consuming instance '" << instance.name << "' "
<< "\n- pose in '" << instance.pose.frame << "': \n" << instance.pose.pose
;
if (arviz.has_value())
{
armarx::viz::Layer layer = arviz->layer("consume");
armarx::data::PackagePath path = instance.fileLocation.serialize();
layer.add(armarx::viz::Object(instance.name)
.file(path.package, path.path)
.pose(instance.pose.pose)
);
layer.add(armarx::viz::Pose(instance.name + " pose")
.pose(instance.pose.pose)
.scale(2)
);
arviz->commit(layer);
}
}
}

Create a Memory Server

Create the Component

Add a component that will serve the new memory:

cd .. # Move to the root directory of memory_tutorial.
armarx-package add component object_memory
Todo:
Incorporate content of RobotAPI Wiki

Turn the Component into a Memory Server

Add the following entries to the CMakeLists.txt of the new component (memory_tutorial/source/memory_tutorial/components/object_memory/CMakeLists.txt):

armarx_add_component(object_memory
...
ICE_DEPENDENCIES
...
RobotAPIInterfaces
...
DEPENDENCIES
PUBLIC
...
armem_server
...
...
)

Extend the generated ComponentInterface.ice by the following lines (you can skip this if your component does not yet implement an ice interface):

#pragma once
#include <RobotAPI/interface/armem/server/MemoryInterface.ice>
module memory_tutorial { module components { module object_memory
{
interface ComponentInterface extends
armarx::armem::server::MemoryInterface
{
...
};
};};};

Extend the generated Component.h by the following lines:

...
#include <RobotAPI/libraries/armem/server/plugins/ReadWritePluginUser.h>
...
namespace memory_tutorial::components::object_memory
{
class Component:
...
...
{
...
};
}

Extend the generated Component.cpp by the following lines:

#include "Component.h"
...
#include <RobotAPI/libraries/armem/server/wm/memory_definitions.h>
...
namespace memory_tutorial::components::object_memory
{
Component::createPropertyDefinitions()
{
...
workingMemory().name() = "Object";
...
return def;
}
...
}

Add a Core Segment for Object Instances

Extend the generated Component.cpp by the following lines:

#include "Component.h"
...
#include <memory_tutorial/object_instance/aron/ObjectInstance.aron.generated.h>
...
namespace memory_tutorial::components::object_memory
{
...
void
Component::onInitComponent()
{
...
workingMemory().addCoreSegment("Instance", object_instance::arondto::ObjectInstance::ToAronType())
...
}
...
}

To access the object_instance library, add it to the CMakeLists.txt:

armarx_add_component(object_memory
...
DEPENDENCIES
PUBLIC
...
memory_tutorial::object_instance
...
...
)

That's it! The memory server is ready.

Test the Memory Server

Open the ArmarX Gui.

armarx gui

Open the Scenario Manager.

  • Click Open empty GUI
  • Search for Meta.ScenarioManager
  • Open and start the scenario ArMemCore. This scenario contains the MemoryNameSystem (MNS) component and its dependencies.
  • Create a new scenario called MemoryTutorial in your package (memory_tutorial).
  • Add the component object_memory from your package by drag and drop from Application Database.
  • Start the scenario MemoryTutorial.
  • Now, open the gui plugin MemoryViewer.

In the MemoryViewer, you should see a two-column layout. The left side shows the different memories and their contents. There should be a single top-level entry named Object, which is the name you gave your memory server. It should have a child entry Instance with the type ObjectInstance and level CoreSegment. Below it, there should not be other entries yet.

When you see this, your memory server should be up and running and ready to receive data and handle queries by memory clients.

Create a Memory Client

Create the Component

Add a component that will write to and read from the memory server.

cd .. # Move to the root directory of memory_tutorial.
armarx-package add component object_memory_client

Turn the Component into a Memory Client

Add the following entries to the CMakeLists.txt of the new component (memory_tutorial/source/memory_tutorial/components/object_memory_client/CMakeLists.txt):

armarx_add_component(object_memory_client
...
ICE_DEPENDENCIES
...
RobotAPIInterfaces
...
DEPENDENCIES
PUBLIC
...
armem
...
...
)

Extend the generated ComponentInterface.ice by the following lines (you can skip this if your component does not yet implement an ice interface):

#pragma once
#include <RobotAPI/interface/armem/client/MemoryListenerInterface.ice>
module memory_tutorial { module components { module object_memory_client
{
interface ComponentInterface extends
armarx::armem::client::MemoryListenerInterface
{
...
};
};};};

Extend the generated Component.h by the following lines:

...
#include <RobotAPI/libraries/armem/client/plugins/ListeningPluginUser.h>
...
namespace memory_tutorial::components::object_memory_client
{
class Component :
...
...
{
...
};
}
Note
If you do not need to subscribe memory updates, you can use #include <RobotAPI/libraries/armem/client/plugins/PluginUser.h> and virtual public armarx::armem::ClientPluginUser instead. In this case, you also don't have to change your component's ice interface.

By using the client plugin, your component inherits a [memoryNameSystem()](\ref armarx::armem::client::plugins::PluginUser#memoryNameSystem) method that you can use to access the Memory Name System (MNS). The MNS is your main entry point to the memory system.

Add a Running Task

When your component should do something after starting up, this should be done in a separate thread that is started at the end of your onConnectComponent(). In ArmarX, you can use a SimpleRunningTask for that. (This is a feature of ArmarX(Core), not of the memory system.)

Extend your component like this:

Component.h:

...
#include <ArmarXCore/core/services/tasks/TaskUtil.h>
...
namespace memory_tutorial::components::object_memory_client
{
class Component :
...
{
...
private:
...
void run();
...
private:
...
...
};
}

Component.cpp:

...
namespace memory_tutorial::components::object_memory_client
{
...
void
Component::onConnectComponent()
{
...
this->task = new armarx::SimpleRunningTask<>([this]()
{
this->run();
});
this->task->start();
}
...
void
Component::run()
{
}
}

Now we can add code to the run() method that is executed after the component is fully connected.

Write Data to the Memory Server

Produce the Data

Add a ProducerConsumer to your component and enable the ArVizComponentPlugin to allow 3D visualization:

Component.h:

...
...
#include <memory_tutorial/object_instance/ProducerConsumer.h>
...
namespace memory_tutorial::components::object_memory_client
{
class Component :
...,
{
...
private:
object_instance::ProducerConsumer producerConsumer;
};
}

This requires us to add the libraries object_instance (for the PrducerConsumer class) and RobotAPIComponentPlugins (for the ArViz component plugin) to our client's dependencies:

armarx_add_component(object_memory_client
...
DEPENDENCIES
PUBLIC
...
RobotAPIComponentPlugins
object_instance
...
...
)

Pass the ArViz client to the producerConsumer in your onConnectComponent() (not in the run() method, as this is still part of the "startup process"):

Component.cpp:

...
namespace memory_tutorial::components::object_memory_client
{
...
void
Component::onConnectComponent()
{
producerConsumer.arviz = arviz;
...
}
...
}

Now we can use producerConsumer.produce() to get a new object instance (and later producerConsumer.consume() to log and visualize it):

Component.cpp:

...
namespace memory_tutorial::components::object_memory_client
{
...
void
Component::run()
{
// Write data to the memory (commit).
{
// Obtain new data as business object (BO).
object_instance::ObjectInstance bo = producerConsumer.produce();
}
}
...
}

Get a Writer

To write data to a memory, you need an armarx::armem::client::Writer. To create a Writer, you need:

  • The memory name system (MNS) (client)
  • The Memory ID of the part of the memory you want to write to.

You can get the MNS client via the memoryNameSystem() method in your component. As always in ArmarX, remote operations are ready when onConnectComponent() is called:

Component.cpp:

...
namespace memory_tutorial::components::object_memory_client
{
void
Component::run()
{
namespace armem = armarx::armem;
const armem::MemoryID coreSegmentID("Object", "Instance");
// Write data to the memory (commit).
{
...
// Create a writer to the memory segment.
armem::client::Writer writer = memoryNameSystem().getWriter(coreSegmentID);
}
...
}
}
Note
By default, the classes armarx::armem::client::MemoryNameSystem and armarx::armem::client::Writer are just forward declared. To use them, add the includes shown above.

Commit the Data

The Writer provides a method called [commit()](\ref armarx::armem::client::Writer#commit) that allows you to send updates to a memory server. Each armarx::armem::EntityUpdate creates a single entity snapshot (i.e. the state of something at a speciifc point in time). Multiple updates can be bundled in a armarx::armem::Commit and send together in a single network call.

Committing data to a memory often involves the follow steps:

  1. Create a writer to the memory segment.
  2. Obtain new data as business object (BO).
  3. Convert the data to a data transfer object (DTO), i.e. the C++ class generated from the ARON XMl.
  4. Build an EntityUpdate with this data.
  5. Send the EntityUpdate in a commit to the memory, using the Writer.

In full, this can be done e.g. like this:

Component.cpp:

...
#include <RobotAPI/libraries/aron/common/aron_conversions/core.h>
#include <memory_tutorial/object_instance/aron_conversions.h>
#include <memory_tutorial/object_instance/ObjectInstance.h>
#include <memory_tutorial/object_instance/aron/ObjectInstance.aron.generated.h>
...
namespace memory_tutorial::components::object_memory_client
{
void
Component::run()
{
namespace armem = armarx::armem;
const armem::MemoryID coreSegmentID("Object", "Instance");
// Write data to the memory (commit).
{
// 1. Create a writer to the memory segment.
armem::client::Writer writer = memoryNameSystem().getWriter(coreSegmentID);
// 2. Obtain new data as business object (BO).
object_instance::ObjectInstance bo = producerConsumer.produce();
// 3. Convert the data to a data transfer object (DTO).
object_instance::arondto::ObjectInstance dto;
toAron(dto, bo);
// 4. Build an `EntityUpdate` with this data.
update.entityID = coreSegmentID
.withProviderSegmentName(this->getName())
update.referencedTime = armem::Time::Now();
update.instancesData = { dto.toAron() };
// 5. Send the `EntityUpdate` in a commit to the memory, using the `Writer` ...
const bool singleUpdate = true;
if (singleUpdate)
{
// ... by sending the update itself (if there is only one update) ...
writer.commit(update);
}
else
{
// ... or bundle it as a commit first to commit multiple updates.
armem::Commit commit;
commit.add(update);
writer.commit(commit);
}
}
...
}
}

Test the Memory Client (Writing)

Add the object_memory_client to your scenario and start it (with ArMemCore and the object_memory still running). After the object_memory_client started (and executed its onConnectComponent()), the structure in the MemoryViewer should look like this:

- Object (Memory)
- Instance (Core Segment)
- object_memory_client (Provider Segment)
- an amicelli object instance (Entity)
- <timestamp> (Entity Snapshot)
- 0 (Entity Instance)

When you click on the entity (an amicelli object instance), the entity snapshot (<timestamp>) or the entity instance (0), the contained data should be shown on the right hand side of the MemoryViewer.

Read Data from the Memory Server

To get data from a memory, you need an armarx::armem::client::Reader. Just as Writers allow you to commit data to any memory server, Readers allow you to query (i.e. request) data from any memory server. And again, you get a Reader from the MNS client by specifing the corresponding memory ID. You can get a Reader as early as your onConnectComponent() is called:

...
#include <RobotAPI/libraries/armem/client/Reader.h>
...
void
Component::run()
{
namespace armem = armarx::armem;
const armem::MemoryID coreSegmentID("Object", "Instance");
...
// Read data from the memory (query).
{
// 1. Create reader to the memory segment.
armem::client::Reader reader = memoryNameSystem().getReader(coreSegmentID);
}
...
}

While a Writer allows you to write data via a commit, a Reader allows you to read data via a query. A query is a data request that defines which part of the memory you are interested in. For example, you could say something like:

  • "Give me the latest snapshot * of all entities whose names contain "amicelli"
  • of all provider segments
  • in this specific core segment."

As you can see, on each level of the hierarchical memory structure (i.e. memory, core segment, provider segment, entity), you can specify what slice of the data at this level you want to be part of the response.

You can build queries via the armarx::armem::client::QueryBuilder. For example, to build the query above, you could write:

...
#include <RobotAPI/libraries/armem/client/query.h>
...
void
Component::run()
{
namespace armem = armarx::armem;
const armem::MemoryID coreSegmentID("Object", "Instance");
...
// Read data from the memory (query).
{
...
// 2. Build a query.
armem::client::QueryBuilder builder;
const bool withQueryFns = true;
if (withQueryFns)
{
namespace qfs = armem::client::query_fns;
builder.coreSegments(qfs::withName(coreSegmentID.coreSegmentName))
.providerSegments(qfs::all())
.entities(qfs::withNamesContaining("amicelli"))
.snapshots(qfs::latest())
;
}
else
{
builder.coreSegments().withName(coreSegmentID.coreSegmentName)
.providerSegments().all()
.entities().withNamesContaining("amicelli")
.snapshots().latest()
;
}
}
...
}
Note
Queries start at core segments (i.e. which core segments of a memory?) and end with snapshots (i.e. which snapshots of an entity?). At the moment, you cannot pick a specific instance in the entity snapshot, but you can select any instance once you have the data.

Once told the query builder the object of your desire, you can have it build the query input (i.e. the request to the memory server) and pass it to the Reader's query() method. This provides you with a query result (i.e. the response by the memory server).

...
#include <RobotAPI/libraries/armem/client/query.h>
...
void
Component::run()
{
namespace armem = armarx::armem;
...
// Read data from the memory (query).
{
// 1. Create reader to the memory segment.
armem::client::Reader reader = memoryNameSystem().getReader(coreSegmentID);
// 2. Build a query.
armem::client::QueryBuilder builder;
... // Define the query.
// 3. Perform the query.
armem::client::QueryResult result = reader.query(builder.buildQueryInput());
}
...
}

The armarx::armem::client::QueryResult contains mainly these things:

  • A success flag which tells you whether an error occurred during the query (e.g. the memory server was unavailable)
  • an errorMessage which is set when success is false
  • a memory of type armarx::armem::wm::Memory containing the queried data.

Always check the success flag and handle the error appropriately. (Note that if your component/library gracefully handles an error, i.e. an event does not affect its well-defined execution, the event is not worth an ARMARX_ERROR, and maybe not even an ARMARX_WARNING).

In case of success, you can consult memory to obtain your data. armem::wm::Memory is basically a multi-level slice through the memory server's hierarchical data structure, i.e. it has the same levels (core segment > provider segment > entity > snapshot > instance > data), but contains only the elements that matched your query.

You can process the query result by iterating through the memory by

  1. looking up specific keys, e.g. from a specific memory ID, or
  2. iterating through the memory levels using its forEach...() functions.

(1) is often useful when you want to obtain specific entity snapshots or instances, e.g. when another component referred you to a memory element via its memory ID or when you got notified that a new snapshot arrived (see Subscribing Memory Updates below).

(2) is useful when you want to process all elements that match a specific criterion or are part of a specific segment.

For the moment, we will opt for (2) and process the data of all instances in the query result. We will see an example for (1) later.

The data itself is held by entity instances (multiple of which can build an entity snapshot). Thus, we will:

  • Iterate through all entity instances,
  • convert the data to our business object (BO) class, and
  • consume the data.

We can do it like this:

...
#include <RobotAPI/libraries/aron/common/aron_conversions/core.h>
...
void
Component::run()
{
namespace armem = armarx::armem;
...
// Read data from the memory (query).
{
...
// 3. Perform the query.
armem::client::QueryResult result = reader.query(builder.buildQueryInput());
// 4. Process the query result.
if (result.success)
{
armem::wm::Memory& memory = result.memory;
memory.forEachInstance([this](const armem::wm::EntityInstance& instance)
{
object_instance::ObjectInstance objectInstance =
armarx::aron::fromAron<object_instance::ObjectInstance>(
object_instance::arondto::ObjectInstance::FromAron(instance.data()));
this->producerConsumer.consume(objectInstance);
});
}
else
{
// Handle the error.
ARMARX_INFO << "Query failed: " << result.errorMessage;
}
}
}

Note that the conversion back to the BO class involves these steps:

  • From a generic ARON container (instance.data()) to the ARON DTO class generated from the ARON XML (object_instance::arondto::ObjectInstance::FromAron())
  • From the ARON DTO class to the BO (object_instance::ObjectInstance) via armarx::aron::fromAron<object_instance::ObjectInstance>().

The function template armarx::aron::fromAron<BO>(const DTO&) relies on the existence of a free function void fromAron(const DTO& dto, BO& bo), which we defined earlier (see Add converter functions above).

Note
The Reader also has shorthand methods for some commonly used queries (such as the latest snapshots of a whole segment). When you look inside these methods, you see that they are just boiling with water, as they say, i.e. they are using a QueryBuilder to define the queries and use the query() function to perform the query.

Test the Memory Client (Reading)

When you run the memory client now, it should write a new snapshot first, then read from the memory. You should be able to observe the following things:

  • The Memory Viewer shows the new snapshot.
  • The new snapshot is printed in the log.
  • The new snapshot is visualized in ArViz (open the ArViz gui plugin to see the 3D visualization).

Because we are requesting the latest snapshot only, and there is only one entity containing "amicelli" yet, the consume function should print only one entry in the log. This entry should match the latest snapshot in the memory viewer, which is the snapshot the client committed just before sending the query.

Keep restarting the client component and observe the results. You can see the entity's whole timeline by checking (from) "Begin" and (to) "End" in the query control panel below the memory view (it may be collapsed) when "Index Range" is selected or by switching to "All" in the drop-down menu.

Subscribe Memory Updates

You might agree that the Reader is great for getting data on-demand, but what if you want to process new data that arrives in the memory server? Do you have to constantly "poll" the memory server for new data? Luckily, no, you do not have to poll. Instead, you can subcribe memory updates.

Each time a memory server receives data in a commit, it stores the data in its working memory and broadcasts a notification in an update topic. The notification contains a list of the memory IDs of all entity snapshots that have been added or updated in this commit.

A (listening) memory client can subscribe to these notifications. When subscribing, the client specifies a memory ID it wants to be notified about as well as a callback function. The callback function is called when a notification contains memory IDs that are "contained by" the subscribed ID (for example, Object contains Object/Instance and Object/Instance contains Object/Instance/client/my_object).

Similar to creating a writer, you can subscribe to updates of a memory ID using the Memory Name System (MNS) client. For example:

void
Component::run()
{
namespace armem = armarx::armem;
const armem::MemoryID coreSegmentID("Object", "Instance");
...
// Subscribe to memory updates.
{
auto callback = [this](const std::vector<armem::MemoryID>& updatedSnapshotIDs)
{
ARMARX_INFO << "Updated IDs: " << updatedSnapshotIDs;
};
memoryNameSystem().subscribe(coreSegmentID, callback);
}
}

This will log any snapshot IDs committed to the Object/Instance core segment. You can also just subscribe a memory (e.g. Object) or a specific entity (e.g. Object/Instance/client/my_object).

Note
Subscribing can also be done in the onConnectComponent() (and, in fact, in the onInitComponent()), but updates will only be received after the onConnectComponent() finished.

To try this, move the subscribing code above in front of the writing code. Then restart the memory client. The commit should trigger a notification that you receive via the subscription.

Note
The callbacks are called from ice threads (triggered by an ice topic), so they may run in parallel to any other threads in your component. Make sure to prevent race conditions by using appropriate synchronisation techniques when accessing data shared between threads.

The update notification does not provide you the data yet. The idea is that you are notified about the update happening, but the data is not pro-actively (and thus potentially unnecessarily) to you along the update. This allows you to do any custom decision making you need in your code, such as only reading the most recent update or ignoring the update altogether.

Luckily, you already know how to get the data: You use your good old friend, the Reader. (I got told it already missed you.) And look at that, it even has a "Welcome back old friend" present for you: the method queryMemoryIDs().

... yay?

Isn't it great? It's just what you need! It takes a list of memory IDs (especially snapshot IDs) and queries each and every single one of them!

Yay!

Yeah, right? So let's put it into action. Surely, you know can guess how this goes now.

void
Component::run()
{
namespace armem = armarx::armem;
const armem::MemoryID coreSegmentID("Object", "Instance");
// Subscribe to memory updates.
{
armem::client::Reader reader = memoryNameSystem().getReader(coreSegmentID);
auto callback = [this, reader](const std::vector<armem::MemoryID>& updatedSnapshotIDs)
{
ARMARX_INFO << "Updated IDs: " << updatedSnapshotIDs;
armem::client::QueryResult result = reader.queryMemoryIDs(updatedSnapshotIDs);
if (result.success)
{
result.memory.forEachInstance([this](const armem::wm::EntityInstance& instance)
{
object_instance::ObjectInstance objectInstance =
armarx::aron::fromAron<object_instance::ObjectInstance>(
object_instance::arondto::ObjectInstance::FromAron(instance.data()));
this->producerConsumer.consume(objectInstance);
});
}
};
memoryNameSystem().subscribe(coreSegmentID, callback);
}
...
}
...

You can just use the same exact code to process the query result.

Test the Memory Client (Subscription)

When you run the example client again, you should now the see log printed by the consume() function twice (once by the regular query and one by the query triggered by the update).

If you only see one output entry in the LogViewer, this can be due to the built-in spam reduction (which suppresses repeated exact same log entries). In this case, you can also look in the console / terminal window you used to run armarx gui. There, you should see the two log entries.

Client.h
armarx::SimpleRunningTask
Usage:
Definition: TaskUtil.h:70
ArVizComponentPlugin.h
armarx::navigation::graph::coreSegmentID
const armem::MemoryID coreSegmentID
Definition: constants.h:30
Writer.h
armarx::armem::Commit
A bundle of updates to be sent to the memory.
Definition: Commit.h:89
RobotAPI
This file is part of ArmarX.
armarx::VariantType::FramedPose
const VariantTypeId FramedPose
Definition: FramedPose.h:37
armarx::core::time::DateTime::Now
static DateTime Now()
Definition: DateTime.cpp:55
armarx::viz::Object::file
Object & file(std::string const &project, std::string const &filename)
Definition: Elements.h:328
armarx::GlobalFrame
const std::string GlobalFrame
Definition: FramedPose.h:62
armarx::armem
Definition: LegacyRobotStateMemoryAdapter.cpp:31
armarx::viz::Layer::add
void add(ElementT const &element)
Definition: Layer.h:29
armarx::control::common
This file is part of ArmarX.
Definition: aron_conversions.cpp:25
plugins
armarx::armem::client::ListeningPluginUser
plugins::ListeningPluginUser ListeningPluginUser
Definition: ListeningPluginUser.h:48
GfxTL::Identity
void Identity(MatrixXX< N, N, T > *a)
Definition: MatrixXX.h:523
armarx::ArVizComponentPluginUser
Provides a ready-to-use ArViz client arviz as member variable.
Definition: ArVizComponentPlugin.h:36
armarx::armem::MemoryID::withProviderSegmentName
MemoryID withProviderSegmentName(const std::string &name) const
Definition: MemoryID.cpp:412
armarx::viz::Object
Definition: Elements.h:321
armarx::armem::MemoryID
A memory ID.
Definition: MemoryID.h:47
FramedPose.h
armarx::armem::server::plugins::ReadWritePluginUser
Base class of memory server components.
Definition: ReadWritePluginUser.h:20
armarx::armem::EntityUpdate
An update of an entity for a specific point in time.
Definition: Commit.h:27
armarx::viz::Pose
Definition: Elements.h:179
simox.h
armarx.h
armarx::armem::server::ltm::util::mongodb::detail::update
bool update(mongocxx::collection &coll, const nlohmann::json &query, const nlohmann::json &update)
Definition: mongodb.cpp:67
aron_conversions.h
GfxTL::Matrix4f
MatrixXX< 4, 4, float > Matrix4f
Definition: MatrixXX.h:601
armarx::armem::MemoryID::withEntityName
MemoryID withEntityName(const std::string &name) const
Definition: MemoryID.cpp:420
armarx::armem::Commit::add
EntityUpdate & add()
Definition: Commit.cpp:81
armarx::ComponentPropertyDefinitions
Default component property definition container.
Definition: Component.h:70
ARMARX_INFO
#define ARMARX_INFO
Definition: Logging.h:174
armarx::viz::ElementOps::pose
DerivedT & pose(Eigen::Matrix4f const &pose)
Definition: ElementOps.h:159
armarx::viz::ElementOps::scale
DerivedT & scale(Eigen::Vector3f scale)
Definition: ElementOps.h:227
IceUtil::Handle< class PropertyDefinitionContainer >
armarx::fromAron
void fromAron(const arondto::PackagePath &dto, PackageFileLocation &bo)
MemoryNameSystem.h
Logging.h
armarx::toAron
void toAron(arondto::PackagePath &dto, const PackageFileLocation &bo)
armarx::PackagePath
Definition: PackagePath.h:55
armarx::viz::Layer
Definition: Layer.h:12
armarx::armem::client::plugins::ListeningPluginUser
A memory name system client which listens to the memory updates topic (MemoryListenerInterface).
Definition: ListeningPluginUser.h:23
armarx::aron::bo
const std::optional< BoT > & bo
Definition: aron_conversions.h:166
PackagePath.h