From 6836fabcbcc91187df2f98260416bb80d69e4d23 Mon Sep 17 00:00:00 2001
From: urrsk <41109954+urrsk@users.noreply.github.com>
Date: Fri, 28 Aug 2026 11:45:08 +0200
Subject: [PATCH 01/18] Take RTDE data types from the robot's setup
acknowledgement
DataPackage carried a hand-maintained table of every subscribable field
name and its data type. Every field the controller gained had to be added
to it, and a field missing from it could not be used even though the robot
would happily serve it. The robot already reports the data type of every
field it acknowledges, so the table was a second source of truth for
information we were being told anyway.
The library now keeps only the list of data types the protocol defines and
takes the per-field types from the setup acknowledgement.
Applications do not have to change. A DataPackage is still constructed from
a recipe, that is still the only place it allocates, and the robot's answer
is applied to it afterwards in place. Because every RTDE type is trivially
copyable and lives inline in the variant, changing which type a field holds
touches no memory, so a package can be created before the connection exists
and still be used in a real-time loop. tests/test_rtde_allocations.cpp pins
this down by counting allocations across the receive and send paths.
- Add DataPackage::getDataType(), reporting the type the robot gave a field
so it does not have to be hardcoded, as an rtde_interface::DataType with
toString() for the protocol's name. This is the only entry point the
change adds; applying types, relaying them to the parser and writer, and
asking whether a package is ready are all internal.
- Remove DataPackage::g_type_list.
- A field name the robot does not know is now reported by RTDEClient::init()
instead of the constructor, still as an RTDEInvalidKeyException, since it
is the robot that decides which names exist.
- RTDEWriter::sendPackage() validates the package it is given against the
robot's types, naming the field and both types on a mismatch, and sends
unset fields as zeros. setData() can no longer catch a wrong type on its
own because it is what establishes an input field's type.
- getData() returns false rather than throwing std::bad_variant_access on a
type mismatch, and takes the field name as a string_view so passing a
literal does not allocate.
Also fixes comm::TCPServer::writeUnchecked() reading its written
out-parameter before assigning it, which made the fake RTDE server silently
send nothing and is why the new robot-free tests could not run before.
See doc/migration_notes.rst for the three behavioural differences.
---
doc/architecture/rtde_client.rst | 75 +-
doc/examples/rtde_client.rst | 9 +-
doc/migration_notes.rst | 28 +
examples/rtde_client.cpp | 2 -
include/ur_client_library/rtde/data_package.h | 254 +++++-
include/ur_client_library/rtde/rtde_parser.h | 27 +
include/ur_client_library/rtde/rtde_writer.h | 23 +-
src/comm/tcp_server.cpp | 1 +
src/rtde/data_package.cpp | 763 ++++++++----------
src/rtde/rtde_client.cpp | 51 +-
src/rtde/rtde_parser.cpp | 56 +-
src/rtde/rtde_writer.cpp | 42 +-
tests/CMakeLists.txt | 15 +
tests/fake_rtde_server.cpp | 537 +++++++++++-
tests/resources/generate_rtde_outputs.py | 5 +-
tests/rtde_test_helpers.h | 88 ++
tests/test_pipeline.cpp | 7 +-
tests/test_producer.cpp | 8 +-
tests/test_rtde_allocations.cpp | 294 +++++++
tests/test_rtde_client.cpp | 59 +-
tests/test_rtde_client_fake_server.cpp | 378 +++++++++
tests/test_rtde_data_package.cpp | 260 +++++-
tests/test_rtde_parser.cpp | 66 +-
tests/test_rtde_writer.cpp | 79 +-
24 files changed, 2537 insertions(+), 590 deletions(-)
create mode 100644 tests/rtde_test_helpers.h
create mode 100644 tests/test_rtde_allocations.cpp
create mode 100644 tests/test_rtde_client_fake_server.cpp
diff --git a/doc/architecture/rtde_client.rst b/doc/architecture/rtde_client.rst
index b45823974..31022ca61 100644
--- a/doc/architecture/rtde_client.rst
+++ b/doc/architecture/rtde_client.rst
@@ -36,10 +36,20 @@ the :ref:`rtde_client_example` for an example of the blocking read method.
{
if (my_client.getDataPackage(data_pkg, READ_TIMEOUT))
{
- std::cout << data_pkg->toString() << std::endl;
+ std::cout << data_pkg.toString() << std::endl;
}
}
+.. note::
+ Constructing the ``DataPackage`` is where its memory is allocated, so create it before entering
+ your control loop and reuse it: ``getDataPackage()`` and ``getDataPackageBlocking()`` don't
+ allocate.
+
+ A recipe only lists field names. The data types belonging to them are reported by the robot when
+ it acknowledges the recipe, and the first package received applies them to your ``DataPackage``,
+ which costs no memory. Until that has happened ``getData()`` on the package fails. See `Field
+ data types`_ for how to ask a package what type it gave a field.
+
Upon construction, two recipe files have to be given, one for the RTDE inputs, one for the RTDE
outputs. Please refer to the `RTDE
guide `_
@@ -69,6 +79,53 @@ After calling ``my_client.start()``, data can be read from the
Remember that, when not using a background thread, data has to be polled regularly, as the robot
will shutdown RTDE communication if the receiving side doesn't empty its buffer.
+Both methods parse into a ``DataPackage`` that the caller owns, which is what keeps the read path
+free of memory allocations. The deprecated ``getDataPackage(timeout)`` overload, which returns a new
+package instead, allocates on every call by design and is therefore not suited for real-time use.
+
+Field data types
+~~~~~~~~~~~~~~~~
+
+``getData()`` has to be given a variable of the field's own type, and returns ``false`` if it isn't.
+Rather than hardcoding which type a field has, ask the package: ``getDataType()`` reports the
+``DataType`` the robot gave a field, or nothing at all if the recipe hasn't been acknowledged yet.
+This is useful for code that has to handle whatever recipe it is configured with, such as a bridge
+to another middleware:
+
+.. code-block:: c++
+
+ const std::optional type = data_pkg.getDataType(field_name);
+ if (!type)
+ {
+ // Not part of the recipe, or the recipe hasn't been acknowledged yet
+ return;
+ }
+
+ // For "actual_q" this prints "VECTOR6D", the same spelling the RTDE guide uses
+ std::cout << field_name << " is a " << rtde_interface::toString(*type) << std::endl;
+
+ switch (*type)
+ {
+ case rtde_interface::DataType::DOUBLE:
+ {
+ double value;
+ data_pkg.getData(field_name, value);
+ break;
+ }
+ case rtde_interface::DataType::VECTOR6D:
+ {
+ vector6d_t value;
+ data_pkg.getData(field_name, value);
+ break;
+ }
+ // ... remaining types
+ }
+
+``DataType`` covers the complete set the protocol defines: ``BOOL``, ``UINT8``, ``UINT32``,
+``UINT64``, ``INT32``, ``DOUBLE``, ``VECTOR3D``, ``VECTOR6D``, ``VECTOR6INT32`` and
+``VECTOR6UINT32``. Switching over it exhaustively means the compiler will point out any case a
+future protocol addition leaves unhandled.
+
Writing data
------------
@@ -105,11 +162,11 @@ an empty input recipe, like this:
// Alternatively, pass an empty filename when using recipe files
// rtde_interface::RTDEClient my_client(ROBOT_IP, notifier, OUTPUT_RECIPE_FILE, "");
my_client.init();
- auto data_pkg = std::make_unique(my_client->getOutputRecipe());
+ auto data_pkg = std::make_unique(my_client.getOutputRecipe());
my_client.start();
while (true)
{
- if (my_client.getDataPackage(data_package, READ_TIMEOUT))
+ if (my_client.getDataPackage(data_pkg, READ_TIMEOUT))
{
std::cout << data_pkg->toString() << std::endl;
}
@@ -125,6 +182,18 @@ The class offers specific methods for every RTDE input possible to write.
Data is sent asynchronously to the RTDE interface.
+To write several fields at once, construct a ``DataPackage`` from ``RTDEClient::getInputRecipe()``,
+fill the fields you care about and pass it to ``sendPackage()``. Fields you leave alone are sent as
+zeros. Since the robot decides what type each field has, ``sendPackage()`` is where a value written
+with the wrong type is reported:
+
+.. code-block:: c++
+
+ rtde_interface::DataPackage input_pkg(my_client.getInputRecipe());
+ input_pkg.setData("speed_slider_mask", 1);
+ input_pkg.setData("speed_slider_fraction", 0.5);
+ my_client.getWriter().sendPackage(input_pkg);
+
.. note::
The ``RTDEWriter`` will return ``false`` on any writing attempts for fields that have not been
diff --git a/doc/examples/rtde_client.rst b/doc/examples/rtde_client.rst
index 5339e7198..4e9779b28 100644
--- a/doc/examples/rtde_client.rst
+++ b/doc/examples/rtde_client.rst
@@ -56,15 +56,18 @@ fetch data synchronously. Hence, we pass ``false`` to the ``start()`` method.
:start-at: auto data_pkg = std::make_unique(my_client.getOutputRecipe());
:end-before: // Change the speed slider
+Creating the package we read into is the last allocation the read path makes; the loop below reuses
+the same package. The recipe only names the fields, so the first package received is also what tells
+this one what type each of its fields has, which needs no further memory.
+
In our main loop, we wait for a new data package to arrive using the blocking read method. Once
received, data from the received package can be accessed using the ``getData()`` method of the
``DataPackage`` object. This method takes the key of the data to be accessed as a parameter and
returns the corresponding value.
.. note:: The key used to access data has to be part of the output recipe used to initialize the RTDE
- client. Passing a string literal, e.g. ``"actual_q"``, is possible but not recommended as it is
- converted to an ``std::string`` automatically, causing heap allocations which should be avoided
- in Real-Time contexts.
+ client. ``getData()`` returns ``false`` for an unknown key, and also if the type of the passed
+ variable doesn't match the type the robot reported for that field.
Writing Data to the RTDE client
-------------------------------
diff --git a/doc/migration_notes.rst b/doc/migration_notes.rst
index bb271b5f0..a98a9c599 100644
--- a/doc/migration_notes.rst
+++ b/doc/migration_notes.rst
@@ -5,6 +5,34 @@ This document contains notes on the migration of the ur_client_library between m
It contains only breaking changes.
+RTDE field types come from the robot
+------------------------------------
+
+The data types of an RTDE recipe's fields are now taken from the robot's answer to the recipe setup,
+instead of from a table of field names maintained inside the library. No application code has to
+change for this: ``DataPackage`` is still constructed from a recipe, still allocates all of its
+storage there, and is typed by the robot's answer afterwards, which costs no memory.
+
+Three consequences are worth knowing about:
+
+- **A field name the robot doesn't know is reported later.** Since the library no longer has its own
+ list of field names, a typo is caught when the robot rejects the recipe during
+ ``RTDEClient::init()`` rather than while constructing the ``RTDEClient``. It is still an
+ ``RTDEInvalidKeyException``, and ``ignore_unavailable_outputs`` still strips such fields instead.
+- **A wrongly typed input field is reported when the package is sent.** ``DataPackage::setData()``
+ decides a field's type from the value passed to it, so it can no longer tell on its own that the
+ robot expects something else. ``RTDEWriter::sendPackage()`` checks the package against the robot's
+ answer and names the field and both types if they disagree.
+- **Reading a field as the wrong type no longer throws.** ``DataPackage::getData()`` used to let a
+ ``std::bad_variant_access`` escape when the passed variable didn't match the field's type. It now
+ returns ``false`` and logs which type the robot reported for that field, matching what its
+ documentation always promised. Code that caught that exception should check the return value
+ instead.
+
+On a ``DataPackage`` that hasn't been typed yet, meaning it has neither received data nor been
+written to, ``getData()`` fails with an explanatory message instead of returning stale values, and
+``getDataType()`` reports that the field has no type yet.
+
Migrating from 1.x.x to 2.x.x
-----------------------------
diff --git a/examples/rtde_client.cpp b/examples/rtde_client.cpp
index d92605c5a..995f359ab 100644
--- a/examples/rtde_client.cpp
+++ b/examples/rtde_client.cpp
@@ -40,7 +40,6 @@ const std::string DEFAULT_ROBOT_IP = "192.168.56.101";
const std::string OUTPUT_RECIPE = "examples/resources/rtde_output_recipe.txt";
const std::string INPUT_RECIPE = "examples/resources/rtde_input_recipe.txt";
-// Preallocation of string to avoid allocation in main loop
const std::string TARGET_SPEED_FRACTION = "target_speed_fraction";
void printFraction(const double fraction, const std::string& label, const size_t width = 20)
@@ -101,7 +100,6 @@ int main(int argc, char* argv[])
{
// Data fields in the data package are accessed by their name. Only names present in the
// output recipe can be accessed. Otherwise this function will return false.
- // We preallocated the string TARGET_SPEED_FRACTION to avoid allocations in the main loop.
data_pkg->getData(TARGET_SPEED_FRACTION, target_speed_fraction);
printFraction(target_speed_fraction, TARGET_SPEED_FRACTION);
}
diff --git a/include/ur_client_library/rtde/data_package.h b/include/ur_client_library/rtde/data_package.h
index 3a5d69b1b..fdb62577e 100644
--- a/include/ur_client_library/rtde/data_package.h
+++ b/include/ur_client_library/rtde/data_package.h
@@ -30,10 +30,14 @@
#define UR_CLIENT_LIBRARY_DATA_PACKAGE_H_INCLUDED
#include
-#include
+#include
+#include
+#include
+#include
#include
#include
+#include "ur_client_library/log.h"
#include "ur_client_library/types.h"
#include "ur_client_library/rtde/rtde_package.h"
@@ -41,6 +45,8 @@ namespace urcl
{
namespace rtde_interface
{
+class RTDEWriter;
+
/*!
* \brief Possible values for the runtime state
*/
@@ -54,33 +60,87 @@ enum class RUNTIME_STATE : uint32_t
RESUMING = 5
};
+/*!
+ * \brief The data types an RTDE field can have.
+ *
+ * This is the complete set the protocol defines. Which one a given field has is decided by the
+ * robot when it acknowledges a recipe, so this list is all the type knowledge the library needs to
+ * carry; see DataPackage::getDataType().
+ */
+enum class DataType : uint8_t
+{
+ BOOL,
+ UINT8,
+ UINT32,
+ UINT64,
+ INT32,
+ DOUBLE,
+ VECTOR3D,
+ VECTOR6D,
+ VECTOR6INT32,
+ VECTOR6UINT32
+};
+
+/*!
+ * \brief The name the RTDE protocol uses for a data type, e.g. "VECTOR6D".
+ *
+ * This is the spelling the robot uses on the wire and the RTDE guide uses in its field tables.
+ */
+std::string toString(const DataType type);
+
/*!
* \brief The DataPackage class handles communication in the form of RTDE data packages both to and
* from the robot. It contains functionality to parse and serialize packages for arbitrary recipes.
+ *
+ * A recipe only names the fields to exchange; their data types are reported by the robot in the
+ * RTDE setup acknowledgement. Constructing a package from a recipe therefore allocates all of its
+ * storage but leaves it *untyped*, and the acknowledgement later decides which type each field
+ * holds. Since every RTDE data type is trivially copyable with inline storage, that second step
+ * costs no memory, which is why a package can be created before a connection exists and still be
+ * used in a real-time loop:
+ *
+ * \code
+ * rtde_interface::DataPackage data_pkg(my_client.getOutputRecipe()); // allocates here
+ * while (true)
+ * {
+ * my_client.getDataPackage(data_pkg, timeout); // types it once, then never allocates
+ * }
+ * \endcode
+ *
+ * Until a package has been typed, either by receiving into it or by writing to it with setData(),
+ * it cannot be parsed into or serialized and getData() will fail.
*/
class DataPackage : public RTDEPackage
{
public:
- using _rtde_type_variant = std::variant;
+ /*!
+ * \brief The type a data field can hold.
+ *
+ * std::monostate is the state of a field whose type isn't decided yet, which is how a package
+ * constructed from a recipe alone starts out. It is also what distinguishes the fields an
+ * application has written from the ones it left alone.
+ */
+ using _rtde_type_variant = std::variant;
DataPackage() = delete;
- DataPackage(const DataPackage& other) : DataPackage(other.recipe_)
+ DataPackage(const DataPackage& other)
+ : RTDEPackage(PackageType::RTDE_DATA_PACKAGE)
+ , recipe_id_(other.recipe_id_)
+ , data_(other.data_)
+ , recipe_(other.recipe_)
+ , protocol_version_(other.protocol_version_)
{
- this->data_ = other.data_;
- this->protocol_version_ = other.protocol_version_;
}
- DataPackage& operator=(DataPackage& other)
- {
- this->data_ = other.data_;
- this->recipe_ = other.recipe_;
- this->protocol_version_ = other.protocol_version_;
- return *this;
- }
-
- DataPackage operator=(const DataPackage& other)
+ /*!
+ * \brief Copies recipe, type information and values from another package.
+ *
+ * The recipe id is deliberately left untouched: an RTDEWriter's send buffers own the id that was
+ * negotiated during the input setup, while packages passed in by an application have none.
+ */
+ DataPackage& operator=(const DataPackage& other)
{
this->data_ = other.data_;
this->recipe_ = other.recipe_;
@@ -89,25 +149,48 @@ class DataPackage : public RTDEPackage
}
/*!
- * \brief Creates a new DataPackage object, based on a given recipe.
+ * \brief Creates a new DataPackage object based on a given recipe, allocating all of its storage.
*
- * \param recipe The used recipe
+ * The data types of the recipe's fields are only known once the robot has acknowledged the
+ * recipe, so the package starts out *untyped*: it cannot be parsed into or serialized, and
+ * getData() fails, until it has been typed. That happens either by receiving into it (see
+ * RTDEClient::getDataPackage()) or, for input recipes, by writing to it with setData().
+ *
+ * Typing a package does not allocate, so this constructor is the only point at which the package
+ * touches the heap. Call it wherever suits your application; it needs no connection.
*
+ * \param recipe The used recipe
* \param protocol_version Protocol version used for the RTDE communication
*/
DataPackage(const std::vector& recipe, const uint16_t& protocol_version = 2)
: RTDEPackage(PackageType::RTDE_DATA_PACKAGE), recipe_(recipe), protocol_version_(protocol_version)
{
- initEmpty();
+ initStorage();
}
virtual ~DataPackage() = default;
/*!
- * \brief Initializes to contained list with empty values based on the recipe.
+ * \brief Resets every data field to a default-constructed value of its own type.
+ *
+ * The types are left alone, so a typed package stays typed.
*/
void initEmpty();
+ /*!
+ * \brief Get the data type the robot reported for a field.
+ *
+ * Which type a field holds is decided by the robot when it acknowledges the recipe, so this is
+ * the way to find out what to pass to getData() without hardcoding it. A package that hasn't
+ * been acknowledged yet has no answer to give.
+ *
+ * \param name The string identifier for the data field as used in the documentation.
+ *
+ * \returns The field's data type, or an empty optional if the field cannot be found inside the
+ * package or if its type isn't known yet.
+ */
+ std::optional getDataType(const std::string_view name) const;
+
/*!
* \brief Sets the attributes of the package by parsing a serialized representation of the
* package.
@@ -141,23 +224,27 @@ class DataPackage : public RTDEPackage
* \param name The string identifier for the data field as used in the documentation.
* \param val Target variable. Make sure, it's the correct type.
*
- * \returns True on success, false if the field cannot be found inside the package.
+ * \returns True on success, false if the field cannot be found inside the package or if its type
+ * doesn't match the requested one.
*/
template
- bool getData(const std::string& name, T& val) const
+ bool getData(const std::string_view name, T& val) const
{
const auto it =
std::find_if(data_.begin(), data_.end(), [&name](const std::pair& element) {
return element.first == name;
});
- if (it != data_.end())
+ if (it == data_.end())
{
- val = std::get(it->second);
+ return false;
}
- else
+ const T* value = std::get_if(&it->second);
+ if (value == nullptr)
{
+ reportReadFailure(name, it->second);
return false;
}
+ val = *value;
return true;
}
@@ -169,10 +256,11 @@ class DataPackage : public RTDEPackage
* \param name The string identifier for the data field as used in the documentation.
* \param val Target variable. Make sure, it's the correct type.
*
- * \returns True on success, false if the field cannot be found inside the package.
+ * \returns True on success, false if the field cannot be found inside the package or if its type
+ * doesn't match the requested one.
*/
template
- bool getData(const std::string& name, std::bitset& val) const
+ bool getData(const std::string_view name, std::bitset& val) const
{
static_assert(sizeof(T) * 8 >= N, "Bitset is too large for underlying variable");
@@ -180,14 +268,17 @@ class DataPackage : public RTDEPackage
std::find_if(data_.begin(), data_.end(), [&name](const std::pair& element) {
return element.first == name;
});
- if (it != data_.end())
+ if (it == data_.end())
{
- val = std::bitset(std::get(it->second));
+ return false;
}
- else
+ const T* value = std::get_if(&it->second);
+ if (value == nullptr)
{
+ reportReadFailure(name, it->second);
return false;
}
+ val = std::bitset(*value);
return true;
}
@@ -196,33 +287,36 @@ class DataPackage : public RTDEPackage
*
* The data package contains a lot of different data fields, depending on the recipe.
*
+ * On a field whose type isn't decided yet this establishes the type from \p val. Whether that
+ * matches what the robot expects is checked when the package is sent, since only then is the
+ * robot's acknowledgement available. On a field that already has a type, \p val has to match it.
+ *
* \param name The string identifier for the data field as used in the documentation.
* \param val Value to set. Make sure, it's the correct type.
*
- * \returns True on success, false if the field cannot be found inside the package.
+ * \returns True on success, false if the field cannot be found inside the package or if its type
+ * doesn't match the passed one.
*/
template
- bool setData(const std::string& name, const T& val)
+ bool setData(const std::string_view name, const T& val)
{
const auto it =
std::find_if(data_.begin(), data_.end(), [&name](const std::pair& element) {
return element.first == name;
});
- if (it != data_.end())
+ if (it == data_.end())
{
- if (!std::holds_alternative(it->second))
- {
- // TODO: It might be better to replace the return type by void and use exceptions for the
- // error case.
- URCL_LOG_ERROR("Type of passed data doesn't match type of existing field for index '%s'", name.c_str());
- return false;
- }
- it->second = val;
+ return false;
}
- else
+ if (!std::holds_alternative(it->second) && !std::holds_alternative(it->second))
{
+ // TODO: It might be better to replace the return type by void and use exceptions for the
+ // error case.
+ URCL_LOG_ERROR("Type of passed data doesn't match type of existing field for index '%.*s'",
+ static_cast(name.size()), name.data());
return false;
}
+ it->second = val;
return true;
}
@@ -236,10 +330,82 @@ class DataPackage : public RTDEPackage
recipe_id_ = recipe_id;
}
+protected:
+ // Applying the robot's setup acknowledgement to a package is the library's job: the parser does
+ // it on the way in, the writer when the input recipe is acknowledged, and the client for the
+ // package it reads into. An application never has the types to pass here.
+ friend class RTDEWriter;
+ friend class RTDEClient;
+ friend class RTDEParser;
+
+ /*!
+ * \brief Applies the data types reported by the robot, resetting all values to zero.
+ *
+ * The storage was already allocated by the constructor, so this only decides which type each
+ * field holds and therefore performs no memory allocation. That is what allows a package to be
+ * created before the recipe has been acknowledged and still be used in a real-time loop.
+ *
+ * \param types The data types of the recipe's fields, in the same order as the recipe
+ *
+ * \throws UrException if the number of types doesn't match the recipe or if a type is unknown
+ */
+ void initEmpty(const std::vector& types);
+
private:
- // Const would be better here
- static std::unordered_map g_type_list;
- uint8_t recipe_id_;
+ /*!
+ * \brief Whether every field of this package has a data type.
+ *
+ * A package constructed from a recipe alone is untyped until either the robot's setup
+ * acknowledgement has been applied to it or setData() has been used to write to every field. An
+ * untyped package cannot be parsed into or serialized, and getData() fails on it.
+ *
+ * There is no separate flag for this: a field whose type is undecided holds a std::monostate, so
+ * the fields themselves are the answer. The scan is over recipe-many variant tags and costs far
+ * less than the parse it guards.
+ *
+ * \returns True if the package carries type information for all of its fields
+ */
+ bool isTyped() const
+ {
+ return std::none_of(data_.begin(), data_.end(), [](const std::pair& field) {
+ return std::holds_alternative(field.second);
+ });
+ }
+
+ /*!
+ * \brief Resets a data field to a default-constructed value of its own type.
+ *
+ * \param name The string identifier for the data field as used in the documentation.
+ *
+ * \returns True on success, false if the field cannot be found inside the package.
+ */
+ bool resetData(const std::string_view name);
+
+ /*!
+ * \brief Copies the fields that \p other has values for into this package.
+ *
+ * Fields \p other hasn't written are left untouched, which is what lets an application send an
+ * input package covering only part of the recipe. This package keeps its own types, so it is
+ * where a type disagreement between the application and the robot surfaces.
+ *
+ * \param other The package to copy values from
+ *
+ * \returns True if every value could be copied, false if \p other names a field this package
+ * doesn't have or holds a value of a different type than the robot reported for it
+ */
+ bool copySetFieldsFrom(const DataPackage& other);
+
+ /*!
+ * \brief Allocates one slot per recipe field, with the type left undecided.
+ */
+ void initStorage();
+
+ /*!
+ * \brief Logs why reading \p field didn't produce the requested type.
+ */
+ static void reportReadFailure(const std::string_view name, const _rtde_type_variant& field);
+
+ uint8_t recipe_id_ = 0;
std::vector> data_;
std::vector recipe_;
uint16_t protocol_version_;
diff --git a/include/ur_client_library/rtde/rtde_parser.h b/include/ur_client_library/rtde/rtde_parser.h
index 9c97827b0..d4326f731 100644
--- a/include/ur_client_library/rtde/rtde_parser.h
+++ b/include/ur_client_library/rtde/rtde_parser.h
@@ -49,6 +49,9 @@ class RTDEParser : public comm::Parser
/*!
* \brief Creates a new RTDEParser object, registering the used recipe.
*
+ * The data types belonging to the recipe are only known once the robot has acknowledged it, so
+ * setRecipeTypes() has to be called before data packages can be parsed.
+ *
* \param recipe The recipe used in RTDE data communication
*/
RTDEParser(const std::vector& recipe) : recipe_(recipe), protocol_version_(1)
@@ -94,8 +97,32 @@ class RTDEParser : public comm::Parser
return protocol_version_;
}
+protected:
+ // Relays the data types from the robot's setup acknowledgement, which only the client receives.
+ friend class RTDEClient;
+
+ /*!
+ * \brief Registers the data types belonging to the recipe, as reported by the robot in the RTDE
+ * setup acknowledgement.
+ *
+ * This has to be called before the robot starts sending data packages, i.e. before the
+ * RTDE_CONTROL_PACKAGE_START request is sent.
+ *
+ * \param types The data types, in the same order as the recipe
+ */
+ void setRecipeTypes(const std::vector& types)
+ {
+ recipe_types_ = types;
+ }
+
private:
+ static std::unique_ptr makeTypedDataPackage(const std::vector& recipe,
+ const std::vector& types,
+ const uint16_t protocol_version);
+
std::vector recipe_;
+ std::vector recipe_types_;
+ bool recipeTypesKnown() const;
PackageType getPackageTypeFromHeader(comm::BinParser& bp) const;
RTDEPackage* createNewPackageFromType(PackageType type) const;
diff --git a/include/ur_client_library/rtde/rtde_writer.h b/include/ur_client_library/rtde/rtde_writer.h
index 8b482454f..807b37183 100644
--- a/include/ur_client_library/rtde/rtde_writer.h
+++ b/include/ur_client_library/rtde/rtde_writer.h
@@ -95,7 +95,12 @@ class RTDEWriter
* Use this if multiple values need to be sent at once. When using the other provided functions,
* an RTDE data package will be sent each time.
*
- * \param package The package to send
+ * Only the fields \p package has values for are taken over; the rest of the input recipe is sent
+ * as zeros. The values are checked against the data types the robot reported for the input
+ * recipe, so a field written with the wrong type is reported here rather than silently corrupting
+ * the package.
+ *
+ * \param package The package to send, constructed from the client's input recipe
*
* \returns Success of the package creation
*/
@@ -192,6 +197,22 @@ class RTDEWriter
*/
bool sendExternalForceTorque(const vector6d_t& external_force_torque);
+protected:
+ // Relays the data types from the robot's setup acknowledgement, which only the client receives.
+ friend class RTDEClient;
+
+ /*!
+ * \brief Applies the data types the robot reported for the input recipe.
+ *
+ * This is what makes the send buffers usable, and it is also the reference against which values
+ * passed to sendPackage() are checked.
+ *
+ * \param types The data types of the input recipe's fields, in the same order as the recipe
+ *
+ * \throws UrException if the number of types doesn't match the recipe or if a type is unknown
+ */
+ void setRecipeTypes(const std::vector& types);
+
private:
void resetMasks(const std::shared_ptr& buffer);
void markStorageToBeSent();
diff --git a/src/comm/tcp_server.cpp b/src/comm/tcp_server.cpp
index b0d3646c2..9760d9964 100644
--- a/src/comm/tcp_server.cpp
+++ b/src/comm/tcp_server.cpp
@@ -456,6 +456,7 @@ bool TCPServer::write(const socket_t fd, const uint8_t* buf, const size_t buf_le
bool TCPServer::writeUnchecked(const socket_t fd, const uint8_t* buf, const size_t buf_len, size_t& written)
{
+ written = 0;
size_t remaining = buf_len;
// handle partial sends
diff --git a/src/rtde/data_package.cpp b/src/rtde/data_package.cpp
index b2abff24d..bb0b2e75d 100644
--- a/src/rtde/data_package.cpp
+++ b/src/rtde/data_package.cpp
@@ -29,463 +29,268 @@
#include "ur_client_library/rtde/data_package.h"
#include
+
+#include "ur_client_library/exceptions.h"
+
namespace urcl
{
namespace rtde_interface
{
-std::unordered_map DataPackage::g_type_list{
- // INPUTS
- { "speed_slider_mask", uint32_t() },
- { "speed_slider_fraction", double() },
- { "standard_digital_output_mask", uint8_t() },
- { "standard_digital_output", uint8_t() },
- { "configurable_digital_output_mask", uint8_t() },
- { "configurable_digital_output", uint8_t() },
- { "standard_analog_output_mask", uint8_t() },
- { "standard_analog_output_type", uint8_t() },
- { "standard_analog_output_0", double() },
- { "standard_analog_output_1", double() },
- { "external_force_torque", vector6d_t() },
+namespace
+{
+/*!
+ * \brief Whether the alternative a visitor was handed is the "type not decided yet" one.
+ *
+ * The visitors below are only reached on typed packages, but they still have to compile for every
+ * alternative of the variant.
+ */
+template
+constexpr bool is_untyped_v = std::is_same_v, std::monostate>;
+
+/*!
+ * \brief The RTDE protocol's name for each data type.
+ *
+ * The single place the spellings live. Both directions of the name conversion read from it, so a
+ * name can never disagree with itself.
+ */
+constexpr struct
+{
+ DataType type;
+ std::string_view name;
+} g_type_names[] = {
+ { DataType::BOOL, "BOOL" },
+ { DataType::UINT8, "UINT8" },
+ { DataType::UINT32, "UINT32" },
+ { DataType::UINT64, "UINT64" },
+ { DataType::INT32, "INT32" },
+ { DataType::DOUBLE, "DOUBLE" },
+ { DataType::VECTOR3D, "VECTOR3D" },
+ { DataType::VECTOR6D, "VECTOR6D" },
+ { DataType::VECTOR6INT32, "VECTOR6INT32" },
+ { DataType::VECTOR6UINT32, "VECTOR6UINT32" },
+};
+
+/*!
+ * \brief The data type a field holds, or an empty optional if it has none yet.
+ */
+std::optional typeOf(const DataPackage::_rtde_type_variant& field)
+{
+ if (std::holds_alternative(field))
+ {
+ return DataType::BOOL;
+ }
+ if (std::holds_alternative(field))
+ {
+ return DataType::UINT8;
+ }
+ if (std::holds_alternative(field))
+ {
+ return DataType::UINT32;
+ }
+ if (std::holds_alternative(field))
+ {
+ return DataType::UINT64;
+ }
+ if (std::holds_alternative(field))
+ {
+ return DataType::INT32;
+ }
+ if (std::holds_alternative(field))
+ {
+ return DataType::DOUBLE;
+ }
+ if (std::holds_alternative(field))
+ {
+ return DataType::VECTOR3D;
+ }
+ if (std::holds_alternative(field))
+ {
+ return DataType::VECTOR6D;
+ }
+ if (std::holds_alternative(field))
+ {
+ return DataType::VECTOR6INT32;
+ }
+ if (std::holds_alternative(field))
+ {
+ return DataType::VECTOR6UINT32;
+ }
+ return std::nullopt;
+}
+
+/*!
+ * \brief Names the type a field holds for an error message, even if it has none.
+ */
+std::string typeNameOf(const DataPackage::_rtde_type_variant& field)
+{
+ const std::optional type = typeOf(field);
+ return type.has_value() ? toString(*type) : "unknown";
+}
- // INPUT / OUTPUT
- { "input_bit_registers0_to_31", uint32_t() },
- { "input_bit_registers32_to_63", uint32_t() },
- { "input_bit_register_64", bool() },
- { "input_bit_register_65", bool() },
- { "input_bit_register_66", bool() },
- { "input_bit_register_67", bool() },
- { "input_bit_register_68", bool() },
- { "input_bit_register_69", bool() },
- { "input_bit_register_70", bool() },
- { "input_bit_register_71", bool() },
- { "input_bit_register_72", bool() },
- { "input_bit_register_73", bool() },
- { "input_bit_register_74", bool() },
- { "input_bit_register_75", bool() },
- { "input_bit_register_76", bool() },
- { "input_bit_register_77", bool() },
- { "input_bit_register_78", bool() },
- { "input_bit_register_79", bool() },
- { "input_bit_register_80", bool() },
- { "input_bit_register_81", bool() },
- { "input_bit_register_82", bool() },
- { "input_bit_register_83", bool() },
- { "input_bit_register_84", bool() },
- { "input_bit_register_85", bool() },
- { "input_bit_register_86", bool() },
- { "input_bit_register_87", bool() },
- { "input_bit_register_88", bool() },
- { "input_bit_register_89", bool() },
- { "input_bit_register_90", bool() },
- { "input_bit_register_91", bool() },
- { "input_bit_register_92", bool() },
- { "input_bit_register_93", bool() },
- { "input_bit_register_94", bool() },
- { "input_bit_register_95", bool() },
- { "input_bit_register_96", bool() },
- { "input_bit_register_97", bool() },
- { "input_bit_register_98", bool() },
- { "input_bit_register_99", bool() },
- { "input_bit_register_100", bool() },
- { "input_bit_register_101", bool() },
- { "input_bit_register_102", bool() },
- { "input_bit_register_103", bool() },
- { "input_bit_register_104", bool() },
- { "input_bit_register_105", bool() },
- { "input_bit_register_106", bool() },
- { "input_bit_register_107", bool() },
- { "input_bit_register_108", bool() },
- { "input_bit_register_109", bool() },
- { "input_bit_register_110", bool() },
- { "input_bit_register_111", bool() },
- { "input_bit_register_112", bool() },
- { "input_bit_register_113", bool() },
- { "input_bit_register_114", bool() },
- { "input_bit_register_115", bool() },
- { "input_bit_register_116", bool() },
- { "input_bit_register_117", bool() },
- { "input_bit_register_118", bool() },
- { "input_bit_register_119", bool() },
- { "input_bit_register_120", bool() },
- { "input_bit_register_121", bool() },
- { "input_bit_register_122", bool() },
- { "input_bit_register_123", bool() },
- { "input_bit_register_124", bool() },
- { "input_bit_register_125", bool() },
- { "input_bit_register_126", bool() },
- { "input_bit_register_127", bool() },
- { "input_int_register_0", int32_t() },
- { "input_int_register_1", int32_t() },
- { "input_int_register_2", int32_t() },
- { "input_int_register_3", int32_t() },
- { "input_int_register_4", int32_t() },
- { "input_int_register_5", int32_t() },
- { "input_int_register_6", int32_t() },
- { "input_int_register_7", int32_t() },
- { "input_int_register_8", int32_t() },
- { "input_int_register_9", int32_t() },
- { "input_int_register_10", int32_t() },
- { "input_int_register_11", int32_t() },
- { "input_int_register_12", int32_t() },
- { "input_int_register_13", int32_t() },
- { "input_int_register_14", int32_t() },
- { "input_int_register_15", int32_t() },
- { "input_int_register_16", int32_t() },
- { "input_int_register_17", int32_t() },
- { "input_int_register_18", int32_t() },
- { "input_int_register_19", int32_t() },
- { "input_int_register_20", int32_t() },
- { "input_int_register_21", int32_t() },
- { "input_int_register_22", int32_t() },
- { "input_int_register_23", int32_t() },
- { "input_int_register_24", int32_t() },
- { "input_int_register_25", int32_t() },
- { "input_int_register_26", int32_t() },
- { "input_int_register_27", int32_t() },
- { "input_int_register_28", int32_t() },
- { "input_int_register_29", int32_t() },
- { "input_int_register_30", int32_t() },
- { "input_int_register_31", int32_t() },
- { "input_int_register_32", int32_t() },
- { "input_int_register_33", int32_t() },
- { "input_int_register_34", int32_t() },
- { "input_int_register_35", int32_t() },
- { "input_int_register_36", int32_t() },
- { "input_int_register_37", int32_t() },
- { "input_int_register_38", int32_t() },
- { "input_int_register_39", int32_t() },
- { "input_int_register_40", int32_t() },
- { "input_int_register_41", int32_t() },
- { "input_int_register_42", int32_t() },
- { "input_int_register_43", int32_t() },
- { "input_int_register_44", int32_t() },
- { "input_int_register_45", int32_t() },
- { "input_int_register_46", int32_t() },
- { "input_int_register_47", int32_t() },
- { "input_double_register_0", double() },
- { "input_double_register_1", double() },
- { "input_double_register_2", double() },
- { "input_double_register_3", double() },
- { "input_double_register_4", double() },
- { "input_double_register_5", double() },
- { "input_double_register_6", double() },
- { "input_double_register_7", double() },
- { "input_double_register_8", double() },
- { "input_double_register_9", double() },
- { "input_double_register_10", double() },
- { "input_double_register_11", double() },
- { "input_double_register_12", double() },
- { "input_double_register_13", double() },
- { "input_double_register_14", double() },
- { "input_double_register_15", double() },
- { "input_double_register_16", double() },
- { "input_double_register_17", double() },
- { "input_double_register_18", double() },
- { "input_double_register_19", double() },
- { "input_double_register_20", double() },
- { "input_double_register_21", double() },
- { "input_double_register_22", double() },
- { "input_double_register_23", double() },
- { "input_double_register_24", double() },
- { "input_double_register_25", double() },
- { "input_double_register_26", double() },
- { "input_double_register_27", double() },
- { "input_double_register_28", double() },
- { "input_double_register_29", double() },
- { "input_double_register_30", double() },
- { "input_double_register_31", double() },
- { "input_double_register_32", double() },
- { "input_double_register_33", double() },
- { "input_double_register_34", double() },
- { "input_double_register_35", double() },
- { "input_double_register_36", double() },
- { "input_double_register_37", double() },
- { "input_double_register_38", double() },
- { "input_double_register_39", double() },
- { "input_double_register_40", double() },
- { "input_double_register_41", double() },
- { "input_double_register_42", double() },
- { "input_double_register_43", double() },
- { "input_double_register_44", double() },
- { "input_double_register_45", double() },
- { "input_double_register_46", double() },
- { "input_double_register_47", double() },
+/*!
+ * \brief Creates an empty value of the given data type.
+ *
+ * Switching over the enum rather than testing names in sequence means the compiler points at this
+ * function if a data type is ever added to the protocol.
+ */
+DataPackage::_rtde_type_variant variantFor(const DataType type)
+{
+ switch (type)
+ {
+ case DataType::BOOL:
+ return bool();
+ case DataType::UINT8:
+ return uint8_t();
+ case DataType::UINT32:
+ return uint32_t();
+ case DataType::UINT64:
+ return uint64_t();
+ case DataType::INT32:
+ return int32_t();
+ case DataType::DOUBLE:
+ return double();
+ case DataType::VECTOR3D:
+ return vector3d_t();
+ case DataType::VECTOR6D:
+ return vector6d_t();
+ case DataType::VECTOR6INT32:
+ return vector6int32_t();
+ case DataType::VECTOR6UINT32:
+ return vector6uint32_t();
+ }
+ throw UrException("Unhandled RTDE data type.");
+}
- // OUTPUTS
- { "timestamp", double() },
- { "target_q", vector6d_t() },
- { "target_qd", vector6d_t() },
- { "target_qdd", vector6d_t() },
- { "target_current", vector6d_t() },
- { "target_moment", vector6d_t() },
- { "actual_q", vector6d_t() },
- { "actual_qd", vector6d_t() },
- { "actual_current", vector6d_t() },
- { "actual_current_window", vector6d_t() },
- { "actual_current_as_torque", vector6d_t() },
- { "joint_control_output", vector6d_t() },
- { "actual_TCP_pose", vector6d_t() },
- { "actual_TCP_speed", vector6d_t() },
- { "actual_TCP_force", vector6d_t() },
- { "target_TCP_pose", vector6d_t() },
- { "target_TCP_speed", vector6d_t() },
- { "tcp_offset", vector6d_t() },
- { "actual_TCP_acceleration", vector6d_t() },
- { "target_TCP_acceleration", vector6d_t() },
- { "actual_digital_input_bits", uint64_t() },
- { "actual_configurable_digital_input_bits", uint64_t() },
- { "joint_temperatures", vector6d_t() },
- { "actual_execution_time", double() },
- { "target_execution_time", double() },
- { "robot_mode", int32_t() },
- { "joint_mode", vector6int32_t() },
- { "safety_mode", int32_t() },
- { "safety_status", int32_t() },
- { "actual_tool_accelerometer", vector3d_t() },
- { "speed_scaling", double() },
- { "target_speed_fraction", double() },
- { "actual_momentum", double() },
- { "actual_main_voltage", double() },
- { "actual_robot_voltage", double() },
- { "actual_robot_current", double() },
- { "actual_joint_voltage", vector6d_t() },
- { "actual_digital_output_bits", uint64_t() },
- { "actual_configurable_digital_output_bits", uint64_t() },
- { "runtime_state", uint32_t() },
- { "elbow_position", vector3d_t() },
- { "elbow_velocity", vector3d_t() },
- { "robot_status_bits", uint32_t() },
- { "safety_status_bits", uint32_t() },
- { "analog_io_types", uint32_t() },
- { "standard_analog_input0", double() },
- { "standard_analog_input1", double() },
- { "standard_analog_output0", double() },
- { "standard_analog_output1", double() },
- { "io_current", double() },
- { "output_bit_registers0_to_31", uint32_t() },
- { "output_bit_registers32_to_63", uint32_t() },
- { "output_bit_register_64", bool() },
- { "output_bit_register_65", bool() },
- { "output_bit_register_66", bool() },
- { "output_bit_register_67", bool() },
- { "output_bit_register_68", bool() },
- { "output_bit_register_69", bool() },
- { "output_bit_register_70", bool() },
- { "output_bit_register_71", bool() },
- { "output_bit_register_72", bool() },
- { "output_bit_register_73", bool() },
- { "output_bit_register_74", bool() },
- { "output_bit_register_75", bool() },
- { "output_bit_register_76", bool() },
- { "output_bit_register_77", bool() },
- { "output_bit_register_78", bool() },
- { "output_bit_register_79", bool() },
- { "output_bit_register_80", bool() },
- { "output_bit_register_81", bool() },
- { "output_bit_register_82", bool() },
- { "output_bit_register_83", bool() },
- { "output_bit_register_84", bool() },
- { "output_bit_register_85", bool() },
- { "output_bit_register_86", bool() },
- { "output_bit_register_87", bool() },
- { "output_bit_register_88", bool() },
- { "output_bit_register_89", bool() },
- { "output_bit_register_90", bool() },
- { "output_bit_register_91", bool() },
- { "output_bit_register_92", bool() },
- { "output_bit_register_93", bool() },
- { "output_bit_register_94", bool() },
- { "output_bit_register_95", bool() },
- { "output_bit_register_96", bool() },
- { "output_bit_register_97", bool() },
- { "output_bit_register_98", bool() },
- { "output_bit_register_99", bool() },
- { "output_bit_register_100", bool() },
- { "output_bit_register_101", bool() },
- { "output_bit_register_102", bool() },
- { "output_bit_register_103", bool() },
- { "output_bit_register_104", bool() },
- { "output_bit_register_105", bool() },
- { "output_bit_register_106", bool() },
- { "output_bit_register_107", bool() },
- { "output_bit_register_108", bool() },
- { "output_bit_register_109", bool() },
- { "output_bit_register_110", bool() },
- { "output_bit_register_111", bool() },
- { "output_bit_register_112", bool() },
- { "output_bit_register_113", bool() },
- { "output_bit_register_114", bool() },
- { "output_bit_register_115", bool() },
- { "output_bit_register_116", bool() },
- { "output_bit_register_117", bool() },
- { "output_bit_register_118", bool() },
- { "output_bit_register_119", bool() },
- { "output_bit_register_120", bool() },
- { "output_bit_register_121", bool() },
- { "output_bit_register_122", bool() },
- { "output_bit_register_123", bool() },
- { "output_bit_register_124", bool() },
- { "output_bit_register_125", bool() },
- { "output_bit_register_126", bool() },
- { "output_bit_register_127", bool() },
- { "output_int_register_0", int32_t() },
- { "output_int_register_1", int32_t() },
- { "output_int_register_2", int32_t() },
- { "output_int_register_3", int32_t() },
- { "output_int_register_4", int32_t() },
- { "output_int_register_5", int32_t() },
- { "output_int_register_6", int32_t() },
- { "output_int_register_7", int32_t() },
- { "output_int_register_8", int32_t() },
- { "output_int_register_9", int32_t() },
- { "output_int_register_10", int32_t() },
- { "output_int_register_11", int32_t() },
- { "output_int_register_12", int32_t() },
- { "output_int_register_13", int32_t() },
- { "output_int_register_14", int32_t() },
- { "output_int_register_15", int32_t() },
- { "output_int_register_16", int32_t() },
- { "output_int_register_17", int32_t() },
- { "output_int_register_18", int32_t() },
- { "output_int_register_19", int32_t() },
- { "output_int_register_20", int32_t() },
- { "output_int_register_21", int32_t() },
- { "output_int_register_22", int32_t() },
- { "output_int_register_23", int32_t() },
- { "output_int_register_24", int32_t() },
- { "output_int_register_25", int32_t() },
- { "output_int_register_26", int32_t() },
- { "output_int_register_27", int32_t() },
- { "output_int_register_28", int32_t() },
- { "output_int_register_29", int32_t() },
- { "output_int_register_30", int32_t() },
- { "output_int_register_31", int32_t() },
- { "output_int_register_32", int32_t() },
- { "output_int_register_33", int32_t() },
- { "output_int_register_34", int32_t() },
- { "output_int_register_35", int32_t() },
- { "output_int_register_36", int32_t() },
- { "output_int_register_37", int32_t() },
- { "output_int_register_38", int32_t() },
- { "output_int_register_39", int32_t() },
- { "output_int_register_40", int32_t() },
- { "output_int_register_41", int32_t() },
- { "output_int_register_42", int32_t() },
- { "output_int_register_43", int32_t() },
- { "output_int_register_44", int32_t() },
- { "output_int_register_45", int32_t() },
- { "output_int_register_46", int32_t() },
- { "output_int_register_47", int32_t() },
- { "output_double_register_0", double() },
- { "output_double_register_1", double() },
- { "output_double_register_2", double() },
- { "output_double_register_3", double() },
- { "output_double_register_4", double() },
- { "output_double_register_5", double() },
- { "output_double_register_6", double() },
- { "output_double_register_7", double() },
- { "output_double_register_8", double() },
- { "output_double_register_9", double() },
- { "output_double_register_10", double() },
- { "output_double_register_11", double() },
- { "output_double_register_12", double() },
- { "output_double_register_13", double() },
- { "output_double_register_14", double() },
- { "output_double_register_15", double() },
- { "output_double_register_16", double() },
- { "output_double_register_17", double() },
- { "output_double_register_18", double() },
- { "output_double_register_19", double() },
- { "output_double_register_20", double() },
- { "output_double_register_21", double() },
- { "output_double_register_22", double() },
- { "output_double_register_23", double() },
- { "output_double_register_24", double() },
- { "output_double_register_25", double() },
- { "output_double_register_26", double() },
- { "output_double_register_27", double() },
- { "output_double_register_28", double() },
- { "output_double_register_29", double() },
- { "output_double_register_30", double() },
- { "output_double_register_31", double() },
- { "output_double_register_32", double() },
- { "output_double_register_33", double() },
- { "output_double_register_34", double() },
- { "output_double_register_35", double() },
- { "output_double_register_36", double() },
- { "output_double_register_37", double() },
- { "output_double_register_38", double() },
- { "output_double_register_39", double() },
- { "output_double_register_40", double() },
- { "output_double_register_41", double() },
- { "output_double_register_42", double() },
- { "output_double_register_43", double() },
- { "output_double_register_44", double() },
- { "output_double_register_45", double() },
- { "output_double_register_46", double() },
- { "output_double_register_47", double() },
- { "actual_robot_energy_consumed", double() },
- { "actual_robot_braking_energy_dissipated", double() },
- { "encoder0_raw", int32_t() },
- { "encoder1_raw", int32_t() },
- { "euromap67_input_bits", uint32_t() },
- { "euromap67_output_bits", uint32_t() },
- { "euromap67_24V_voltage", double() },
- { "euromap67_24V_current", double() },
- { "tool_mode", uint32_t() },
- { "tool_analog_input_types", uint32_t() },
- { "tool_analog_input0", double() },
- { "tool_analog_input1", double() },
- { "tool_output_voltage", int32_t() },
- { "tool_output_current", double() },
- { "tool_temperature", double() },
- { "tool_output_mode", uint8_t() },
- { "tool_digital_output0_mode", uint8_t() },
- { "tool_digital_output1_mode", uint8_t() },
- { "tcp_force_scalar", double() },
- { "joint_position_deviation_ratio", double() },
- { "collision_detection_ratio", double() },
- { "ft_raw_wrench", vector6d_t() },
- { "wrench_calc_from_currents", vector6d_t() },
- { "payload", double() },
- { "payload_cog", vector3d_t() },
- { "payload_inertia", vector6d_t() },
- { "script_control_line", uint32_t() },
- { "time_scale_source", int32_t() },
- { "target_gravity", vector3d_t() },
- { "target_base_acceleration", vector6d_t() },
- { "control_step", uint64_t() },
- { "target_base_wrench", vector6d_t() },
+/*!
+ * \brief Creates an empty value of the RTDE data type with the given name.
+ *
+ * \param type_name One of the RTDE data type names as reported by the robot in a setup
+ * acknowledgement
+ *
+ * \throws UrException if the name is not a known RTDE data type
+ */
+DataPackage::_rtde_type_variant variantFromTypeName(const std::string_view type_name)
+{
+ for (const auto& entry : g_type_names)
+ {
+ if (entry.name == type_name)
+ {
+ return variantFor(entry.type);
+ }
+ }
- // NOT IN OFFICIAL DOCS
- { "tool_digital_output_mask", uint8_t() },
- { "tool_digital_output", uint8_t() },
-};
+ std::stringstream ss;
+ ss << "'" << type_name
+ << "' is not a known RTDE data type. Expected one of BOOL, UINT8, UINT32, UINT64, INT32, "
+ "DOUBLE, VECTOR3D, VECTOR6D, VECTOR6INT32 or VECTOR6UINT32.";
+ throw UrException(ss.str());
+}
+} // namespace
-void rtde_interface::DataPackage::initEmpty()
+std::string toString(const DataType type)
{
- data_.clear();
- data_.reserve(recipe_.size());
- for (auto& item : recipe_)
+ for (const auto& entry : g_type_names)
{
- if (g_type_list.find(item) == g_type_list.end())
+ if (entry.type == type)
{
- throw RTDEInvalidKeyException("Unknown item in recipe: " + item);
+ return std::string(entry.name);
}
- _rtde_type_variant entry = g_type_list[item];
- data_.push_back({ item, entry });
+ }
+ throw UrException("Unhandled RTDE data type.");
+}
+
+std::optional rtde_interface::DataPackage::getDataType(const std::string_view name) const
+{
+ const auto it =
+ std::find_if(data_.begin(), data_.end(), [&name](const std::pair& element) {
+ return element.first == name;
+ });
+ if (it == data_.end())
+ {
+ return std::nullopt;
+ }
+ return typeOf(it->second);
+}
+
+void rtde_interface::DataPackage::reportReadFailure(const std::string_view name, const _rtde_type_variant& field)
+{
+ if (std::holds_alternative(field))
+ {
+ URCL_LOG_ERROR("Cannot read the data field '%.*s', as its data type isn't known yet. The data types of a recipe "
+ "are reported by the robot during the RTDE handshake, so a data package can only be read from "
+ "after it has received data at least once.",
+ static_cast(name.size()), name.data());
+ return;
+ }
+ URCL_LOG_ERROR("Type of requested data doesn't match type of existing field for index '%.*s'. The robot reports "
+ "that field as %s.",
+ static_cast(name.size()), name.data(), typeNameOf(field).c_str());
+}
+
+void rtde_interface::DataPackage::initStorage()
+{
+ data_.resize(recipe_.size());
+ for (size_t i = 0; i < recipe_.size(); ++i)
+ {
+ data_[i].first = recipe_[i];
+ data_[i].second = std::monostate();
+ }
+}
+
+void rtde_interface::DataPackage::initEmpty(const std::vector& types)
+{
+ if (types.size() != recipe_.size())
+ {
+ std::stringstream ss;
+ ss << "Cannot initialize an RTDE data package: got " << types.size() << " data types for a recipe with "
+ << recipe_.size() << " fields.";
+ throw UrException(ss.str());
+ }
+
+ // The storage was allocated by the constructor and every RTDE type lives inline in the variant,
+ // so deciding the types here cannot allocate. That is what makes it safe to type a package that
+ // an application is already holding, in the middle of a real-time loop.
+ if (data_.size() != recipe_.size())
+ {
+ initStorage();
+ }
+ for (size_t i = 0; i < recipe_.size(); ++i)
+ {
+ data_[i].second = variantFromTypeName(types[i]);
+ }
+}
+
+void rtde_interface::DataPackage::initEmpty()
+{
+ for (auto& item : data_)
+ {
+ std::visit([](auto&& arg) { arg = std::decay_t(); }, item.second);
}
}
bool rtde_interface::DataPackage::parseWith(comm::BinParser& bp)
{
+ if (!isTyped())
+ {
+ URCL_LOG_ERROR("Cannot parse into an RTDE data package before the data types of its recipe are known. Those are "
+ "reported by the robot during the RTDE handshake.");
+ return false;
+ }
+
if (protocol_version_ == 2)
{
bp.parse(recipe_id_);
}
for (size_t i = 0; i < recipe_.size(); ++i)
{
- std::visit([&bp](auto&& arg) { bp.parse(arg); }, data_[i].second);
+ std::visit(
+ [&bp](auto&& arg) {
+ if constexpr (!is_untyped_v)
+ {
+ bp.parse(arg);
+ }
+ },
+ data_[i].second);
}
return true;
}
@@ -502,7 +307,18 @@ std::string rtde_interface::DataPackage::toString() const
}
else
{
- std::visit([&ss](auto&& arg) { ss << arg; }, item.second);
+ std::visit(
+ [&ss](auto&& arg) {
+ if constexpr (is_untyped_v)
+ {
+ ss << "";
+ }
+ else
+ {
+ ss << arg;
+ }
+ },
+ item.second);
}
ss << std::endl;
}
@@ -511,11 +327,29 @@ std::string rtde_interface::DataPackage::toString() const
size_t rtde_interface::DataPackage::serializePackage(uint8_t* buffer)
{
+ if (!isTyped())
+ {
+ URCL_LOG_ERROR("Cannot serialize an RTDE data package before the data types of its recipe are known. Those are "
+ "reported by the robot during the RTDE handshake.");
+ return 0;
+ }
+
uint16_t payload_size = sizeof(recipe_id_);
for (auto& item : data_)
{
- payload_size += std::visit([](auto&& arg) -> uint16_t { return sizeof(arg); }, item.second);
+ payload_size += std::visit(
+ [](auto&& arg) -> uint16_t {
+ if constexpr (is_untyped_v)
+ {
+ return 0;
+ }
+ else
+ {
+ return sizeof(arg);
+ }
+ },
+ item.second);
}
size_t size = 0;
size += PackageHeader::serializeHeader(buffer, PackageType::RTDE_DATA_PACKAGE, payload_size);
@@ -523,11 +357,66 @@ size_t rtde_interface::DataPackage::serializePackage(uint8_t* buffer)
for (size_t i = 0; i < data_.size(); ++i)
{
size += std::visit(
- [&buffer, &size](auto&& arg) -> size_t { return comm::PackageSerializer::serialize(buffer + size, arg); },
+ [&buffer, &size](auto&& arg) -> size_t {
+ if constexpr (is_untyped_v)
+ {
+ return 0;
+ }
+ else
+ {
+ return comm::PackageSerializer::serialize(buffer + size, arg);
+ }
+ },
data_[i].second);
}
return size;
}
+
+bool rtde_interface::DataPackage::resetData(const std::string_view name)
+{
+ const auto it =
+ std::find_if(data_.begin(), data_.end(), [&name](const std::pair& element) {
+ return element.first == name;
+ });
+ if (it == data_.end())
+ {
+ return false;
+ }
+ std::visit([](auto&& arg) { arg = std::decay_t(); }, it->second);
+ return true;
+}
+
+bool rtde_interface::DataPackage::copySetFieldsFrom(const DataPackage& other)
+{
+ bool all_copied = true;
+ for (const auto& source : other.data_)
+ {
+ if (std::holds_alternative(source.second))
+ {
+ continue;
+ }
+
+ const auto destination =
+ std::find_if(data_.begin(), data_.end(), [&source](const std::pair& element) {
+ return element.first == source.first;
+ });
+ if (destination == data_.end())
+ {
+ URCL_LOG_ERROR("The data field '%s' is not part of the recipe the robot acknowledged.", source.first.c_str());
+ all_copied = false;
+ continue;
+ }
+ if (source.second.index() != destination->second.index())
+ {
+ URCL_LOG_ERROR("The value passed for the data field '%s' is of type %s, but the robot reports that field as %s.",
+ source.first.c_str(), typeNameOf(source.second).c_str(), typeNameOf(destination->second).c_str());
+ all_copied = false;
+ continue;
+ }
+ destination->second = source.second;
+ }
+ return all_copied;
+}
} // namespace rtde_interface
} // namespace urcl
diff --git a/src/rtde/rtde_client.cpp b/src/rtde/rtde_client.cpp
index 3000db6ef..3c5f5d1df 100644
--- a/src/rtde/rtde_client.cpp
+++ b/src/rtde/rtde_client.cpp
@@ -40,6 +40,8 @@ namespace urcl
{
namespace rtde_interface
{
+// The pre-allocated package gets its storage here, but the field types are only known once the
+// robot has acknowledged the output recipe, which is when setupOutputs() applies them.
RTDEClient::RTDEClient(std::string robot_ip, comm::INotifier& notifier, const std::string& output_recipe_file,
const std::string& input_recipe_file, double target_frequency, bool ignore_unavailable_outputs,
const uint32_t port)
@@ -328,6 +330,8 @@ void RTDEClient::resetOutputRecipe(const std::vector new_recipe)
disconnect();
output_recipe_.assign(new_recipe.begin(), new_recipe.end());
+ // The data types of the new recipe are unknown until the robot acknowledges it again, at which
+ // point setupOutputs() applies them to this package without allocating.
preallocated_data_pkg_ = DataPackage(output_recipe_, protocol_version_);
parser_ = RTDEParser(output_recipe_);
@@ -380,7 +384,13 @@ bool RTDEClient::setupOutputs()
std::vector variable_types = splitString(tmp_output->variable_types_, ",");
std::vector available_variables;
std::vector unavailable_variables;
- assert(output_recipe_.size() == variable_types.size());
+ if (output_recipe_.size() != variable_types.size())
+ {
+ URCL_LOG_ERROR("The robot acknowledged the output recipe with %zu data types while the recipe contains %zu "
+ "fields. Cannot set up the RTDE outputs.",
+ variable_types.size(), output_recipe_.size());
+ return false;
+ }
for (std::size_t i = 0; i < variable_types.size(); ++i)
{
const std::string variable_name = output_recipe_[i];
@@ -424,7 +434,12 @@ bool RTDEClient::setupOutputs()
}
else
{
- // All variables are accounted for in the RTDE package
+ // All variables are accounted for in the RTDE package. The robot told us their data types,
+ // so this is the point where everything holding received data learns what it holds. The
+ // storage itself already exists, so this doesn't allocate and neither does the receive path
+ // from here on.
+ parser_.setRecipeTypes(variable_types);
+ preallocated_data_pkg_.initEmpty(variable_types);
return true;
}
}
@@ -471,7 +486,13 @@ bool RTDEClient::setupInputs()
{
std::vector variable_types = splitString(tmp_input->variable_types_, ",");
- assert(input_recipe_.size() == variable_types.size());
+ if (input_recipe_.size() != variable_types.size())
+ {
+ URCL_LOG_ERROR("The robot acknowledged the input recipe with %zu data types while the recipe contains %zu "
+ "fields. Cannot set up the RTDE inputs.",
+ variable_types.size(), input_recipe_.size());
+ return false;
+ }
for (std::size_t i = 0; i < variable_types.size(); ++i)
{
URCL_LOG_DEBUG("%s confirmed as datatype: %s", input_recipe_[i].c_str(), variable_types[i].c_str());
@@ -486,6 +507,7 @@ bool RTDEClient::setupInputs()
throw RTDEInputConflictException(input_recipe_[i]);
}
}
+ writer_.setRecipeTypes(variable_types);
writer_.init(tmp_input->input_recipe_id_);
return true;
@@ -524,7 +546,8 @@ bool RTDEClient::isRobotBooted()
if (!sendStart())
return false;
- std::unique_ptr package = std::make_unique(output_recipe_, protocol_version_);
+ // Shaped like the packages we are about to receive, so the parser doesn't have to allocate one
+ std::unique_ptr package = std::make_unique(preallocated_data_pkg_);
double timestamp = 0;
int reading_count = 0;
@@ -619,7 +642,7 @@ bool RTDEClient::sendStart()
// Worst case we get a data package as part of a race condition in the communication. If we
// didn't preallocate that, it might print a warning.
- std::unique_ptr package = std::make_unique(output_recipe_, protocol_version_);
+ std::unique_ptr package = std::make_unique(preallocated_data_pkg_);
unsigned int num_retries = 0;
while (num_retries < MAX_REQUEST_RETRIES)
{
@@ -669,7 +692,7 @@ bool RTDEClient::sendPause()
}
// Worst case we get a data package as part of a race condition in the communication. If we
// didn't preallocate that, it might print a warning.
- std::unique_ptr package = std::make_unique(output_recipe_, protocol_version_);
+ std::unique_ptr package = std::make_unique(preallocated_data_pkg_);
std::chrono::time_point start = std::chrono::steady_clock::now();
int seconds = 5;
while (std::chrono::steady_clock::now() - start < std::chrono::seconds(seconds))
@@ -943,9 +966,21 @@ void RTDEClient::startBackgroundRead()
URCL_LOG_WARN("Requested to start RTDEClient's background read, while it is already running. Doing nothing.");
return;
}
+ if (!preallocated_data_pkg_.isTyped())
+ {
+ URCL_LOG_ERROR("Cannot start RTDEClient's background read before the RTDE communication has been set up, as the "
+ "data types of the output recipe are reported by the robot. Please call init() first.");
+ return;
+ }
background_read_running_ = true;
- data_buffer0_ = std::make_unique(output_recipe_, protocol_version_);
- data_buffer1_ = std::make_unique(output_recipe_, protocol_version_);
+ // Copying the package the blocking read uses gives these the same recipe and data types without
+ // needing to know what those are. Its values could be from an earlier read, so drop them.
+ auto buffer0 = std::make_unique(preallocated_data_pkg_);
+ auto buffer1 = std::make_unique(preallocated_data_pkg_);
+ buffer0->initEmpty();
+ buffer1->initEmpty();
+ data_buffer0_ = std::move(buffer0);
+ data_buffer1_ = std::move(buffer1);
background_read_thread_ = std::thread(&RTDEClient::backgroundReadThreadFunc, this);
}
diff --git a/src/rtde/rtde_parser.cpp b/src/rtde/rtde_parser.cpp
index 48bd60c25..2d9e62d81 100644
--- a/src/rtde/rtde_parser.cpp
+++ b/src/rtde/rtde_parser.cpp
@@ -27,6 +27,28 @@ namespace urcl
{
namespace rtde_interface
{
+// A package allocates its storage from the recipe and learns its field types from the robot's
+// acknowledgement afterwards, which costs no memory.
+std::unique_ptr RTDEParser::makeTypedDataPackage(const std::vector& recipe,
+ const std::vector& types,
+ const uint16_t protocol_version)
+{
+ auto package = std::make_unique(recipe, protocol_version);
+ package->initEmpty(types);
+ return package;
+}
+
+bool RTDEParser::recipeTypesKnown() const
+{
+ if (recipe_types_.size() == recipe_.size())
+ {
+ return true;
+ }
+ URCL_LOG_ERROR("Received an RTDE data package while the data types of the output recipe are unknown. Those are "
+ "reported by the robot when it acknowledges the recipe, so this means a data package arrived before "
+ "the RTDE handshake was completed.");
+ return false;
+}
bool RTDEParser::parse(comm::BinParser& bp, std::vector>& results)
{
@@ -54,7 +76,11 @@ bool RTDEParser::parse(comm::BinParser& bp, std::vector package(new DataPackage(recipe_, protocol_version_));
+ if (!recipeTypesKnown())
+ {
+ return false;
+ }
+ std::unique_ptr package = makeTypedDataPackage(recipe_, recipe_types_, protocol_version_);
if (!package->parseWith(bp))
{
@@ -104,6 +130,10 @@ bool RTDEParser::parse(comm::BinParser& bp, std::unique_ptr& result
{
case PackageType::RTDE_DATA_PACKAGE:
{
+ if (!recipeTypesKnown())
+ {
+ return false;
+ }
if (result == nullptr || result->getType() != PackageType::RTDE_DATA_PACKAGE)
{
if (result == nullptr)
@@ -119,10 +149,30 @@ bool RTDEParser::parse(comm::BinParser& bp, std::unique_ptr& result
"a DataPackage would be sent.",
result->getType());
}
- result = std::make_unique(recipe_, protocol_version_);
+ result = makeTypedDataPackage(recipe_, recipe_types_, protocol_version_);
+ }
+
+ DataPackage* data_package = dynamic_cast(result.get());
+ if (!data_package->isTyped())
+ {
+ // A package built from a recipe alone doesn't know its field types yet. Applying the ones
+ // the robot reported doesn't allocate, so this happens right here rather than by handing
+ // the caller a replacement package.
+ try
+ {
+ data_package->initEmpty(recipe_types_);
+ }
+ catch (const UrException& e)
+ {
+ URCL_LOG_ERROR("The passed pre-allocated DataPackage does not fit the negotiated output recipe (%s). A new "
+ "DataPackage will have to be allocated.",
+ e.what());
+ result = makeTypedDataPackage(recipe_, recipe_types_, protocol_version_);
+ data_package = dynamic_cast(result.get());
+ }
}
- if (!dynamic_cast(result.get())->parseWith(bp))
+ if (!data_package->parseWith(bp))
{
URCL_LOG_ERROR("Package parsing of type %d failed!", static_cast(type));
return false;
diff --git a/src/rtde/rtde_writer.cpp b/src/rtde/rtde_writer.cpp
index c0c48ccdb..989f3cfbb 100644
--- a/src/rtde/rtde_writer.cpp
+++ b/src/rtde/rtde_writer.cpp
@@ -28,6 +28,7 @@
#include "ur_client_library/rtde/rtde_writer.h"
#include
+#include "ur_client_library/helpers.h"
#include "ur_client_library/log.h"
namespace urcl
@@ -74,6 +75,9 @@ void RTDEWriter::setInputRecipe(const std::vector& recipe)
used_masks_.push_back(field);
}
}
+ // All storage the send path needs is allocated here. The buffers stay unusable until the robot
+ // has reported the data types of the recipe's fields, which setRecipeTypes() then applies without
+ // allocating again.
data_buffer0_ = std::make_shared(recipe_);
data_buffer1_ = std::make_shared(recipe_);
@@ -81,6 +85,13 @@ void RTDEWriter::setInputRecipe(const std::vector& recipe)
current_send_buffer_ = data_buffer1_;
}
+void RTDEWriter::setRecipeTypes(const std::vector& types)
+{
+ std::lock_guard lock_guard(store_mutex_);
+ data_buffer0_->initEmpty(types);
+ data_buffer1_->initEmpty(types);
+}
+
void RTDEWriter::init(uint8_t recipe_id)
{
if (running_)
@@ -92,6 +103,8 @@ void RTDEWriter::init(uint8_t recipe_id)
std::lock_guard lock_guard(store_mutex_);
data_buffer0_->setRecipeID(recipe_id);
data_buffer1_->setRecipeID(recipe_id);
+ current_store_buffer_ = data_buffer0_;
+ current_send_buffer_ = data_buffer1_;
}
recipe_id_ = recipe_id;
new_data_available_ = false;
@@ -143,7 +156,20 @@ void RTDEWriter::stop()
bool RTDEWriter::sendPackage(const DataPackage& package)
{
std::lock_guard guard(store_mutex_);
- *current_store_buffer_ = package;
+ if (!current_store_buffer_->isTyped())
+ {
+ URCL_LOG_ERROR("Cannot send RTDE input data before the RTDE communication has been set up, as the data types of "
+ "the input recipe are reported by the robot.");
+ return false;
+ }
+
+ // Fields the caller didn't write are sent as zeros rather than as whatever the previous package
+ // left in the buffer, so that a package means the same thing no matter what was sent before it.
+ current_store_buffer_->initEmpty();
+ if (!current_store_buffer_->copySetFieldsFrom(package))
+ {
+ return false;
+ }
markStorageToBeSent();
return true;
}
@@ -404,19 +430,7 @@ void RTDEWriter::resetMasks(const std::shared_ptr& buffer)
{
for (const auto& mask_name : used_masks_)
{
- // "speed_slider_mask" is uint32_t, all others are uint8_t
- // If we reset it to the wrong type, serialization will be wrong
- if (mask_name == "speed_slider_mask")
-
- {
- uint32_t mask = 0;
- buffer->setData(mask_name, mask);
- }
- else
- {
- uint8_t mask = 0;
- buffer->setData(mask_name, mask);
- }
+ buffer->resetData(mask_name);
}
}
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index 313489a16..513ab0ff1 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -212,6 +212,21 @@ target_link_libraries(rtde_parser_tests PRIVATE ur_client_library::urcl GTest::g
gtest_add_tests(TARGET rtde_parser_tests
)
+# Checks that exchanging RTDE data doesn't allocate once the recipes have been set up. Uses the
+# in-process fake RTDE server, so it runs without a robot.
+add_executable(rtde_allocation_tests test_rtde_allocations.cpp fake_rtde_server.cpp)
+target_link_libraries(rtde_allocation_tests PRIVATE ur_client_library::urcl GTest::gtest_main)
+gtest_add_tests(TARGET rtde_allocation_tests
+)
+
+# Covers RTDEClient's public interface against the in-process fake RTDE server. The tests in
+# test_rtde_client.cpp go further but need a reachable robot, so they only run with INTEGRATION_TESTS.
+add_executable(rtde_client_fake_server_tests test_rtde_client_fake_server.cpp fake_rtde_server.cpp)
+target_link_libraries(rtde_client_fake_server_tests PRIVATE ur_client_library::urcl GTest::gtest_main)
+gtest_add_tests(TARGET rtde_client_fake_server_tests
+ WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
+)
+
add_executable(tcp_server_tests test_tcp_server.cpp)
if (MSVC)
target_compile_options(tcp_server_tests PRIVATE /Zc:lambda)
diff --git a/tests/fake_rtde_server.cpp b/tests/fake_rtde_server.cpp
index c0219029c..4dac1144e 100644
--- a/tests/fake_rtde_server.cpp
+++ b/tests/fake_rtde_server.cpp
@@ -1,10 +1,494 @@
#include "fake_rtde_server.h"
+#include "rtde_test_helpers.h"
#include
+#include
#include "ur_client_library/comm/package_serializer.h"
#include "ur_client_library/log.h"
namespace urcl
{
+namespace
+{
+// The RTDE data type of every field a robot knows about. On a real robot this information is part
+// of the answer to a recipe setup request, which is where the client library takes it from, so the
+// test double has to be able to answer the same way.
+//
+// tests/resources/generate_rtde_outputs.py reads the output fields out of this table, so keep the
+// section comments below intact.
+// clang-format off
+const std::unordered_map g_variable_types{
+ // INPUTS
+ { "speed_slider_mask", "UINT32" },
+ { "speed_slider_fraction", "DOUBLE" },
+ { "standard_digital_output_mask", "UINT8" },
+ { "standard_digital_output", "UINT8" },
+ { "configurable_digital_output_mask", "UINT8" },
+ { "configurable_digital_output", "UINT8" },
+ { "standard_analog_output_mask", "UINT8" },
+ { "standard_analog_output_type", "UINT8" },
+ { "standard_analog_output_0", "DOUBLE" },
+ { "standard_analog_output_1", "DOUBLE" },
+ { "external_force_torque", "VECTOR6D" },
+
+ // INPUT / OUTPUT
+ { "input_bit_registers0_to_31", "UINT32" },
+ { "input_bit_registers32_to_63", "UINT32" },
+ { "input_bit_register_64", "BOOL" },
+ { "input_bit_register_65", "BOOL" },
+ { "input_bit_register_66", "BOOL" },
+ { "input_bit_register_67", "BOOL" },
+ { "input_bit_register_68", "BOOL" },
+ { "input_bit_register_69", "BOOL" },
+ { "input_bit_register_70", "BOOL" },
+ { "input_bit_register_71", "BOOL" },
+ { "input_bit_register_72", "BOOL" },
+ { "input_bit_register_73", "BOOL" },
+ { "input_bit_register_74", "BOOL" },
+ { "input_bit_register_75", "BOOL" },
+ { "input_bit_register_76", "BOOL" },
+ { "input_bit_register_77", "BOOL" },
+ { "input_bit_register_78", "BOOL" },
+ { "input_bit_register_79", "BOOL" },
+ { "input_bit_register_80", "BOOL" },
+ { "input_bit_register_81", "BOOL" },
+ { "input_bit_register_82", "BOOL" },
+ { "input_bit_register_83", "BOOL" },
+ { "input_bit_register_84", "BOOL" },
+ { "input_bit_register_85", "BOOL" },
+ { "input_bit_register_86", "BOOL" },
+ { "input_bit_register_87", "BOOL" },
+ { "input_bit_register_88", "BOOL" },
+ { "input_bit_register_89", "BOOL" },
+ { "input_bit_register_90", "BOOL" },
+ { "input_bit_register_91", "BOOL" },
+ { "input_bit_register_92", "BOOL" },
+ { "input_bit_register_93", "BOOL" },
+ { "input_bit_register_94", "BOOL" },
+ { "input_bit_register_95", "BOOL" },
+ { "input_bit_register_96", "BOOL" },
+ { "input_bit_register_97", "BOOL" },
+ { "input_bit_register_98", "BOOL" },
+ { "input_bit_register_99", "BOOL" },
+ { "input_bit_register_100", "BOOL" },
+ { "input_bit_register_101", "BOOL" },
+ { "input_bit_register_102", "BOOL" },
+ { "input_bit_register_103", "BOOL" },
+ { "input_bit_register_104", "BOOL" },
+ { "input_bit_register_105", "BOOL" },
+ { "input_bit_register_106", "BOOL" },
+ { "input_bit_register_107", "BOOL" },
+ { "input_bit_register_108", "BOOL" },
+ { "input_bit_register_109", "BOOL" },
+ { "input_bit_register_110", "BOOL" },
+ { "input_bit_register_111", "BOOL" },
+ { "input_bit_register_112", "BOOL" },
+ { "input_bit_register_113", "BOOL" },
+ { "input_bit_register_114", "BOOL" },
+ { "input_bit_register_115", "BOOL" },
+ { "input_bit_register_116", "BOOL" },
+ { "input_bit_register_117", "BOOL" },
+ { "input_bit_register_118", "BOOL" },
+ { "input_bit_register_119", "BOOL" },
+ { "input_bit_register_120", "BOOL" },
+ { "input_bit_register_121", "BOOL" },
+ { "input_bit_register_122", "BOOL" },
+ { "input_bit_register_123", "BOOL" },
+ { "input_bit_register_124", "BOOL" },
+ { "input_bit_register_125", "BOOL" },
+ { "input_bit_register_126", "BOOL" },
+ { "input_bit_register_127", "BOOL" },
+ { "input_int_register_0", "INT32" },
+ { "input_int_register_1", "INT32" },
+ { "input_int_register_2", "INT32" },
+ { "input_int_register_3", "INT32" },
+ { "input_int_register_4", "INT32" },
+ { "input_int_register_5", "INT32" },
+ { "input_int_register_6", "INT32" },
+ { "input_int_register_7", "INT32" },
+ { "input_int_register_8", "INT32" },
+ { "input_int_register_9", "INT32" },
+ { "input_int_register_10", "INT32" },
+ { "input_int_register_11", "INT32" },
+ { "input_int_register_12", "INT32" },
+ { "input_int_register_13", "INT32" },
+ { "input_int_register_14", "INT32" },
+ { "input_int_register_15", "INT32" },
+ { "input_int_register_16", "INT32" },
+ { "input_int_register_17", "INT32" },
+ { "input_int_register_18", "INT32" },
+ { "input_int_register_19", "INT32" },
+ { "input_int_register_20", "INT32" },
+ { "input_int_register_21", "INT32" },
+ { "input_int_register_22", "INT32" },
+ { "input_int_register_23", "INT32" },
+ { "input_int_register_24", "INT32" },
+ { "input_int_register_25", "INT32" },
+ { "input_int_register_26", "INT32" },
+ { "input_int_register_27", "INT32" },
+ { "input_int_register_28", "INT32" },
+ { "input_int_register_29", "INT32" },
+ { "input_int_register_30", "INT32" },
+ { "input_int_register_31", "INT32" },
+ { "input_int_register_32", "INT32" },
+ { "input_int_register_33", "INT32" },
+ { "input_int_register_34", "INT32" },
+ { "input_int_register_35", "INT32" },
+ { "input_int_register_36", "INT32" },
+ { "input_int_register_37", "INT32" },
+ { "input_int_register_38", "INT32" },
+ { "input_int_register_39", "INT32" },
+ { "input_int_register_40", "INT32" },
+ { "input_int_register_41", "INT32" },
+ { "input_int_register_42", "INT32" },
+ { "input_int_register_43", "INT32" },
+ { "input_int_register_44", "INT32" },
+ { "input_int_register_45", "INT32" },
+ { "input_int_register_46", "INT32" },
+ { "input_int_register_47", "INT32" },
+ { "input_double_register_0", "DOUBLE" },
+ { "input_double_register_1", "DOUBLE" },
+ { "input_double_register_2", "DOUBLE" },
+ { "input_double_register_3", "DOUBLE" },
+ { "input_double_register_4", "DOUBLE" },
+ { "input_double_register_5", "DOUBLE" },
+ { "input_double_register_6", "DOUBLE" },
+ { "input_double_register_7", "DOUBLE" },
+ { "input_double_register_8", "DOUBLE" },
+ { "input_double_register_9", "DOUBLE" },
+ { "input_double_register_10", "DOUBLE" },
+ { "input_double_register_11", "DOUBLE" },
+ { "input_double_register_12", "DOUBLE" },
+ { "input_double_register_13", "DOUBLE" },
+ { "input_double_register_14", "DOUBLE" },
+ { "input_double_register_15", "DOUBLE" },
+ { "input_double_register_16", "DOUBLE" },
+ { "input_double_register_17", "DOUBLE" },
+ { "input_double_register_18", "DOUBLE" },
+ { "input_double_register_19", "DOUBLE" },
+ { "input_double_register_20", "DOUBLE" },
+ { "input_double_register_21", "DOUBLE" },
+ { "input_double_register_22", "DOUBLE" },
+ { "input_double_register_23", "DOUBLE" },
+ { "input_double_register_24", "DOUBLE" },
+ { "input_double_register_25", "DOUBLE" },
+ { "input_double_register_26", "DOUBLE" },
+ { "input_double_register_27", "DOUBLE" },
+ { "input_double_register_28", "DOUBLE" },
+ { "input_double_register_29", "DOUBLE" },
+ { "input_double_register_30", "DOUBLE" },
+ { "input_double_register_31", "DOUBLE" },
+ { "input_double_register_32", "DOUBLE" },
+ { "input_double_register_33", "DOUBLE" },
+ { "input_double_register_34", "DOUBLE" },
+ { "input_double_register_35", "DOUBLE" },
+ { "input_double_register_36", "DOUBLE" },
+ { "input_double_register_37", "DOUBLE" },
+ { "input_double_register_38", "DOUBLE" },
+ { "input_double_register_39", "DOUBLE" },
+ { "input_double_register_40", "DOUBLE" },
+ { "input_double_register_41", "DOUBLE" },
+ { "input_double_register_42", "DOUBLE" },
+ { "input_double_register_43", "DOUBLE" },
+ { "input_double_register_44", "DOUBLE" },
+ { "input_double_register_45", "DOUBLE" },
+ { "input_double_register_46", "DOUBLE" },
+ { "input_double_register_47", "DOUBLE" },
+
+ // OUTPUTS
+ { "timestamp", "DOUBLE" },
+ { "target_q", "VECTOR6D" },
+ { "target_qd", "VECTOR6D" },
+ { "target_qdd", "VECTOR6D" },
+ { "target_current", "VECTOR6D" },
+ { "target_moment", "VECTOR6D" },
+ { "actual_q", "VECTOR6D" },
+ { "actual_qd", "VECTOR6D" },
+ { "actual_current", "VECTOR6D" },
+ { "actual_current_window", "VECTOR6D" },
+ { "actual_current_as_torque", "VECTOR6D" },
+ { "joint_control_output", "VECTOR6D" },
+ { "actual_TCP_pose", "VECTOR6D" },
+ { "actual_TCP_speed", "VECTOR6D" },
+ { "actual_TCP_force", "VECTOR6D" },
+ { "target_TCP_pose", "VECTOR6D" },
+ { "target_TCP_speed", "VECTOR6D" },
+ { "tcp_offset", "VECTOR6D" },
+ { "actual_TCP_acceleration", "VECTOR6D" },
+ { "target_TCP_acceleration", "VECTOR6D" },
+ { "actual_digital_input_bits", "UINT64" },
+ { "actual_configurable_digital_input_bits", "UINT64" },
+ { "joint_temperatures", "VECTOR6D" },
+ { "actual_execution_time", "DOUBLE" },
+ { "target_execution_time", "DOUBLE" },
+ { "robot_mode", "INT32" },
+ { "joint_mode", "VECTOR6INT32" },
+ { "safety_mode", "INT32" },
+ { "safety_status", "INT32" },
+ { "actual_tool_accelerometer", "VECTOR3D" },
+ { "speed_scaling", "DOUBLE" },
+ { "target_speed_fraction", "DOUBLE" },
+ { "actual_momentum", "DOUBLE" },
+ { "actual_main_voltage", "DOUBLE" },
+ { "actual_robot_voltage", "DOUBLE" },
+ { "actual_robot_current", "DOUBLE" },
+ { "actual_joint_voltage", "VECTOR6D" },
+ { "actual_digital_output_bits", "UINT64" },
+ { "actual_configurable_digital_output_bits", "UINT64" },
+ { "runtime_state", "UINT32" },
+ { "elbow_position", "VECTOR3D" },
+ { "elbow_velocity", "VECTOR3D" },
+ { "robot_status_bits", "UINT32" },
+ { "safety_status_bits", "UINT32" },
+ { "analog_io_types", "UINT32" },
+ { "standard_analog_input0", "DOUBLE" },
+ { "standard_analog_input1", "DOUBLE" },
+ { "standard_analog_output0", "DOUBLE" },
+ { "standard_analog_output1", "DOUBLE" },
+ { "io_current", "DOUBLE" },
+ { "output_bit_registers0_to_31", "UINT32" },
+ { "output_bit_registers32_to_63", "UINT32" },
+ { "output_bit_register_64", "BOOL" },
+ { "output_bit_register_65", "BOOL" },
+ { "output_bit_register_66", "BOOL" },
+ { "output_bit_register_67", "BOOL" },
+ { "output_bit_register_68", "BOOL" },
+ { "output_bit_register_69", "BOOL" },
+ { "output_bit_register_70", "BOOL" },
+ { "output_bit_register_71", "BOOL" },
+ { "output_bit_register_72", "BOOL" },
+ { "output_bit_register_73", "BOOL" },
+ { "output_bit_register_74", "BOOL" },
+ { "output_bit_register_75", "BOOL" },
+ { "output_bit_register_76", "BOOL" },
+ { "output_bit_register_77", "BOOL" },
+ { "output_bit_register_78", "BOOL" },
+ { "output_bit_register_79", "BOOL" },
+ { "output_bit_register_80", "BOOL" },
+ { "output_bit_register_81", "BOOL" },
+ { "output_bit_register_82", "BOOL" },
+ { "output_bit_register_83", "BOOL" },
+ { "output_bit_register_84", "BOOL" },
+ { "output_bit_register_85", "BOOL" },
+ { "output_bit_register_86", "BOOL" },
+ { "output_bit_register_87", "BOOL" },
+ { "output_bit_register_88", "BOOL" },
+ { "output_bit_register_89", "BOOL" },
+ { "output_bit_register_90", "BOOL" },
+ { "output_bit_register_91", "BOOL" },
+ { "output_bit_register_92", "BOOL" },
+ { "output_bit_register_93", "BOOL" },
+ { "output_bit_register_94", "BOOL" },
+ { "output_bit_register_95", "BOOL" },
+ { "output_bit_register_96", "BOOL" },
+ { "output_bit_register_97", "BOOL" },
+ { "output_bit_register_98", "BOOL" },
+ { "output_bit_register_99", "BOOL" },
+ { "output_bit_register_100", "BOOL" },
+ { "output_bit_register_101", "BOOL" },
+ { "output_bit_register_102", "BOOL" },
+ { "output_bit_register_103", "BOOL" },
+ { "output_bit_register_104", "BOOL" },
+ { "output_bit_register_105", "BOOL" },
+ { "output_bit_register_106", "BOOL" },
+ { "output_bit_register_107", "BOOL" },
+ { "output_bit_register_108", "BOOL" },
+ { "output_bit_register_109", "BOOL" },
+ { "output_bit_register_110", "BOOL" },
+ { "output_bit_register_111", "BOOL" },
+ { "output_bit_register_112", "BOOL" },
+ { "output_bit_register_113", "BOOL" },
+ { "output_bit_register_114", "BOOL" },
+ { "output_bit_register_115", "BOOL" },
+ { "output_bit_register_116", "BOOL" },
+ { "output_bit_register_117", "BOOL" },
+ { "output_bit_register_118", "BOOL" },
+ { "output_bit_register_119", "BOOL" },
+ { "output_bit_register_120", "BOOL" },
+ { "output_bit_register_121", "BOOL" },
+ { "output_bit_register_122", "BOOL" },
+ { "output_bit_register_123", "BOOL" },
+ { "output_bit_register_124", "BOOL" },
+ { "output_bit_register_125", "BOOL" },
+ { "output_bit_register_126", "BOOL" },
+ { "output_bit_register_127", "BOOL" },
+ { "output_int_register_0", "INT32" },
+ { "output_int_register_1", "INT32" },
+ { "output_int_register_2", "INT32" },
+ { "output_int_register_3", "INT32" },
+ { "output_int_register_4", "INT32" },
+ { "output_int_register_5", "INT32" },
+ { "output_int_register_6", "INT32" },
+ { "output_int_register_7", "INT32" },
+ { "output_int_register_8", "INT32" },
+ { "output_int_register_9", "INT32" },
+ { "output_int_register_10", "INT32" },
+ { "output_int_register_11", "INT32" },
+ { "output_int_register_12", "INT32" },
+ { "output_int_register_13", "INT32" },
+ { "output_int_register_14", "INT32" },
+ { "output_int_register_15", "INT32" },
+ { "output_int_register_16", "INT32" },
+ { "output_int_register_17", "INT32" },
+ { "output_int_register_18", "INT32" },
+ { "output_int_register_19", "INT32" },
+ { "output_int_register_20", "INT32" },
+ { "output_int_register_21", "INT32" },
+ { "output_int_register_22", "INT32" },
+ { "output_int_register_23", "INT32" },
+ { "output_int_register_24", "INT32" },
+ { "output_int_register_25", "INT32" },
+ { "output_int_register_26", "INT32" },
+ { "output_int_register_27", "INT32" },
+ { "output_int_register_28", "INT32" },
+ { "output_int_register_29", "INT32" },
+ { "output_int_register_30", "INT32" },
+ { "output_int_register_31", "INT32" },
+ { "output_int_register_32", "INT32" },
+ { "output_int_register_33", "INT32" },
+ { "output_int_register_34", "INT32" },
+ { "output_int_register_35", "INT32" },
+ { "output_int_register_36", "INT32" },
+ { "output_int_register_37", "INT32" },
+ { "output_int_register_38", "INT32" },
+ { "output_int_register_39", "INT32" },
+ { "output_int_register_40", "INT32" },
+ { "output_int_register_41", "INT32" },
+ { "output_int_register_42", "INT32" },
+ { "output_int_register_43", "INT32" },
+ { "output_int_register_44", "INT32" },
+ { "output_int_register_45", "INT32" },
+ { "output_int_register_46", "INT32" },
+ { "output_int_register_47", "INT32" },
+ { "output_double_register_0", "DOUBLE" },
+ { "output_double_register_1", "DOUBLE" },
+ { "output_double_register_2", "DOUBLE" },
+ { "output_double_register_3", "DOUBLE" },
+ { "output_double_register_4", "DOUBLE" },
+ { "output_double_register_5", "DOUBLE" },
+ { "output_double_register_6", "DOUBLE" },
+ { "output_double_register_7", "DOUBLE" },
+ { "output_double_register_8", "DOUBLE" },
+ { "output_double_register_9", "DOUBLE" },
+ { "output_double_register_10", "DOUBLE" },
+ { "output_double_register_11", "DOUBLE" },
+ { "output_double_register_12", "DOUBLE" },
+ { "output_double_register_13", "DOUBLE" },
+ { "output_double_register_14", "DOUBLE" },
+ { "output_double_register_15", "DOUBLE" },
+ { "output_double_register_16", "DOUBLE" },
+ { "output_double_register_17", "DOUBLE" },
+ { "output_double_register_18", "DOUBLE" },
+ { "output_double_register_19", "DOUBLE" },
+ { "output_double_register_20", "DOUBLE" },
+ { "output_double_register_21", "DOUBLE" },
+ { "output_double_register_22", "DOUBLE" },
+ { "output_double_register_23", "DOUBLE" },
+ { "output_double_register_24", "DOUBLE" },
+ { "output_double_register_25", "DOUBLE" },
+ { "output_double_register_26", "DOUBLE" },
+ { "output_double_register_27", "DOUBLE" },
+ { "output_double_register_28", "DOUBLE" },
+ { "output_double_register_29", "DOUBLE" },
+ { "output_double_register_30", "DOUBLE" },
+ { "output_double_register_31", "DOUBLE" },
+ { "output_double_register_32", "DOUBLE" },
+ { "output_double_register_33", "DOUBLE" },
+ { "output_double_register_34", "DOUBLE" },
+ { "output_double_register_35", "DOUBLE" },
+ { "output_double_register_36", "DOUBLE" },
+ { "output_double_register_37", "DOUBLE" },
+ { "output_double_register_38", "DOUBLE" },
+ { "output_double_register_39", "DOUBLE" },
+ { "output_double_register_40", "DOUBLE" },
+ { "output_double_register_41", "DOUBLE" },
+ { "output_double_register_42", "DOUBLE" },
+ { "output_double_register_43", "DOUBLE" },
+ { "output_double_register_44", "DOUBLE" },
+ { "output_double_register_45", "DOUBLE" },
+ { "output_double_register_46", "DOUBLE" },
+ { "output_double_register_47", "DOUBLE" },
+ { "actual_robot_energy_consumed", "DOUBLE" },
+ { "actual_robot_braking_energy_dissipated", "DOUBLE" },
+ { "encoder0_raw", "INT32" },
+ { "encoder1_raw", "INT32" },
+ { "euromap67_input_bits", "UINT32" },
+ { "euromap67_output_bits", "UINT32" },
+ { "euromap67_24V_voltage", "DOUBLE" },
+ { "euromap67_24V_current", "DOUBLE" },
+ { "tool_mode", "UINT32" },
+ { "tool_analog_input_types", "UINT32" },
+ { "tool_analog_input0", "DOUBLE" },
+ { "tool_analog_input1", "DOUBLE" },
+ { "tool_output_voltage", "INT32" },
+ { "tool_output_current", "DOUBLE" },
+ { "tool_temperature", "DOUBLE" },
+ { "tool_output_mode", "UINT8" },
+ { "tool_digital_output0_mode", "UINT8" },
+ { "tool_digital_output1_mode", "UINT8" },
+ { "tcp_force_scalar", "DOUBLE" },
+ { "joint_position_deviation_ratio", "DOUBLE" },
+ { "collision_detection_ratio", "DOUBLE" },
+ { "ft_raw_wrench", "VECTOR6D" },
+ { "wrench_calc_from_currents", "VECTOR6D" },
+ { "payload", "DOUBLE" },
+ { "payload_cog", "VECTOR3D" },
+ { "payload_inertia", "VECTOR6D" },
+ { "script_control_line", "UINT32" },
+ { "time_scale_source", "INT32" },
+ { "target_gravity", "VECTOR3D" },
+ { "target_base_acceleration", "VECTOR6D" },
+ { "control_step", "UINT64" },
+ { "target_base_wrench", "VECTOR6D" },
+
+ // NOT IN OFFICIAL DOCS
+ { "tool_digital_output_mask", "UINT8" },
+ { "tool_digital_output", "UINT8" },
+};
+// clang-format on
+
+// Mimics a robot's answer to a recipe setup request: the data type of every requested field, or
+// "NOT_FOUND" for fields the robot doesn't know.
+std::vector variableTypesFor(const std::vector& recipe)
+{
+ std::vector types;
+ types.reserve(recipe.size());
+ for (const auto& name : recipe)
+ {
+ const auto it = g_variable_types.find(name);
+ types.push_back(it == g_variable_types.end() ? "NOT_FOUND" : it->second);
+ }
+ return types;
+}
+
+std::string joinStrings(const std::vector& strings, const std::string& delimiter = ",")
+{
+ std::string result;
+ for (const auto& string : strings)
+ {
+ if (!result.empty())
+ {
+ result += delimiter;
+ }
+ result += string;
+ }
+ return result;
+}
+
+bool allVariablesFound(const std::vector& types)
+{
+ return std::find(types.begin(), types.end(), "NOT_FOUND") == types.end();
+}
+
+// Unlike a client, the server side knows the data types up front, so it applies them itself right
+// after allocating the package.
+std::unique_ptr makeTypedDataPackage(const std::vector& recipe,
+ const std::vector& types)
+{
+ auto package = std::make_unique(recipe);
+ package->initEmpty(types);
+ return package;
+}
+} // namespace
RTDEServer::RTDEServer(const int port) : server_(port)
{
@@ -52,7 +536,7 @@ void RTDEServer::messageCallback([[maybe_unused]] const socket_t filedescriptor,
send_buffer, rtde_interface::PackageType::RTDE_REQUEST_PROTOCOL_VERSION, sizeof(uint8_t));
send_size += serializer.serialize(send_buffer + send_size, accepted);
- size_t written;
+ size_t written = 0;
server_.writeUnchecked(filedescriptor, send_buffer, send_size, written);
break;
}
@@ -69,7 +553,7 @@ void RTDEServer::messageCallback([[maybe_unused]] const socket_t filedescriptor,
send_size += serializer.serialize(send_buffer + send_size, version); // bugfix
send_size += serializer.serialize(send_buffer + send_size, version); // build
- size_t written;
+ size_t written = 0;
server_.writeUnchecked(filedescriptor, send_buffer, send_size, written);
break;
}
@@ -80,24 +564,29 @@ void RTDEServer::messageCallback([[maybe_unused]] const socket_t filedescriptor,
std::string variable_names_str;
bp.parseRemainder(variable_names_str);
output_recipe_ = splitString(variable_names_str);
+ const std::vector variable_types = variableTypesFor(output_recipe_);
+ const std::string variable_types_str = joinStrings(variable_types);
- output_data_package_ = std::make_unique(output_recipe_);
- output_data_package_->initEmpty();
+ {
+ std::lock_guard data_lock(output_data_mutex_);
+ output_data_package_.reset();
+ if (allVariablesFound(variable_types))
+ {
+ output_data_package_ = makeTypedDataPackage(output_recipe_, variable_types);
+ }
+ }
comm::PackageSerializer serializer;
uint8_t send_buffer[4096];
size_t send_size = 0;
send_size += rtde_interface::PackageHeader::serializeHeader(
send_buffer, rtde_interface::PackageType::RTDE_CONTROL_PACKAGE_SETUP_OUTPUTS,
- static_cast(variable_names_str.length() + sizeof(uint8_t)));
+ static_cast(variable_types_str.length() + sizeof(uint8_t)));
uint8_t recipe_id = 1;
send_size += serializer.serialize(send_buffer + send_size, recipe_id);
- send_size += serializer.serialize(send_buffer + send_size,
- variable_names_str); // We return the variable
- // names list directly. For the initialization process, it
- // is only important, that no field is "NOT_FOUND".
+ send_size += serializer.serialize(send_buffer + send_size, variable_types_str);
- size_t written;
+ size_t written = 0;
server_.writeUnchecked(filedescriptor, send_buffer, send_size, written);
URCL_LOG_INFO("Output recipe set");
break;
@@ -107,23 +596,26 @@ void RTDEServer::messageCallback([[maybe_unused]] const socket_t filedescriptor,
std::string variable_names_str;
bp.parseRemainder(variable_names_str);
input_recipe_ = splitString(variable_names_str);
+ const std::vector variable_types = variableTypesFor(input_recipe_);
+ const std::string variable_types_str = joinStrings(variable_types);
- input_data_package_ = std::make_unique(input_recipe_);
+ input_data_package_.reset();
+ if (allVariablesFound(variable_types))
+ {
+ input_data_package_ = makeTypedDataPackage(input_recipe_, variable_types);
+ }
comm::PackageSerializer serializer;
uint8_t send_buffer[4096];
size_t send_size = 0;
send_size += rtde_interface::PackageHeader::serializeHeader(
send_buffer, rtde_interface::PackageType::RTDE_CONTROL_PACKAGE_SETUP_INPUTS,
- static_cast(variable_names_str.length() + sizeof(uint8_t)));
+ static_cast(variable_types_str.length() + sizeof(uint8_t)));
uint8_t recipe_id = 1;
send_size += serializer.serialize(send_buffer + send_size, recipe_id);
- send_size += serializer.serialize(send_buffer + send_size,
- variable_names_str); // We return the variable
- // names list directly. For the initialization process, it
- // is only important, that no field is "NOT_FOUND".
+ send_size += serializer.serialize(send_buffer + send_size, variable_types_str);
- size_t written;
+ size_t written = 0;
server_.writeUnchecked(filedescriptor, send_buffer, send_size, written);
URCL_LOG_INFO("Input recipe set with %zu variables.", input_recipe_.size());
@@ -139,7 +631,7 @@ void RTDEServer::messageCallback([[maybe_unused]] const socket_t filedescriptor,
bool accepted = true;
send_size += serializer.serialize(send_buffer + send_size, accepted);
- size_t written;
+ size_t written = 0;
server_.writeUnchecked(filedescriptor, send_buffer, send_size, written);
startSendingDataPackages();
break;
@@ -154,7 +646,7 @@ void RTDEServer::messageCallback([[maybe_unused]] const socket_t filedescriptor,
bool accepted = true;
send_size += serializer.serialize(send_buffer + send_size, accepted);
- size_t written;
+ size_t written = 0;
server_.writeUnchecked(filedescriptor, send_buffer, send_size, written);
stopSendingDataPackages();
break;
@@ -212,7 +704,7 @@ void RTDEServer::sendDataLoop()
output_data_package_->setData("timestamp", timestamp);
uint8_t buffer[65536];
size_t size = output_data_package_->serializePackage(buffer);
- size_t written;
+ size_t written = 0;
server_.write(client_socket_, buffer, size, written);
}
std::this_thread::sleep_for(std::chrono::duration(1.0 / output_frequency_));
@@ -227,7 +719,10 @@ void RTDEServer::actOnInput()
double speed_slider_fraction = 0.0;
input_data_package_->getData("speed_slider_fraction", speed_slider_fraction);
std::lock_guard data_lock(output_data_mutex_);
- output_data_package_->setData("target_speed_fraction", speed_slider_fraction);
+ if (output_data_package_ != nullptr)
+ {
+ output_data_package_->setData("target_speed_fraction", speed_slider_fraction);
+ }
}
}
diff --git a/tests/resources/generate_rtde_outputs.py b/tests/resources/generate_rtde_outputs.py
index cf4f527b9..7d9382299 100644
--- a/tests/resources/generate_rtde_outputs.py
+++ b/tests/resources/generate_rtde_outputs.py
@@ -33,7 +33,10 @@
import re
URCL_PATH = pathlib.Path(__file__).parent.parent.parent.resolve()
-PKG_PATH = [i for i in pathlib.Path(URCL_PATH.as_posix()).glob("**/data_package.cpp")]
+# The client library takes the data types of the RTDE fields from the robot's answer to the recipe
+# setup, so the list of all known fields only exists in the test double that has to emulate that
+# answer.
+PKG_PATH = [i for i in pathlib.Path(URCL_PATH.as_posix()).glob("**/fake_rtde_server.cpp")]
assert len(PKG_PATH) == 1
diff --git a/tests/rtde_test_helpers.h b/tests/rtde_test_helpers.h
new file mode 100644
index 000000000..f85be8126
--- /dev/null
+++ b/tests/rtde_test_helpers.h
@@ -0,0 +1,88 @@
+// -- BEGIN LICENSE BLOCK ----------------------------------------------
+// Copyright 2026 Universal Robots A/S
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+//
+// * Redistributions in binary form must reproduce the above copyright
+// notice, this list of conditions and the following disclaimer in the
+// documentation and/or other materials provided with the distribution.
+//
+// * Neither the name of the {copyright_holder} nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+// POSSIBILITY OF SUCH DAMAGE.
+// -- END LICENSE BLOCK ------------------------------------------------
+
+#pragma once
+
+// Applying the data types from an RTDE setup acknowledgement is the library's own job, so the data
+// package, the parser and the writer all keep those entry points out of their public interface.
+// Tests, and the fake server standing in for the robot, reach them through these subclasses.
+
+#include
+#include
+
+#include
+#include
+#include
+
+namespace urcl
+{
+namespace test
+{
+class TestableDataPackage : public rtde_interface::DataPackage
+{
+public:
+ using rtde_interface::DataPackage::DataPackage;
+ using rtde_interface::DataPackage::initEmpty;
+};
+
+class TestableRTDEParser : public rtde_interface::RTDEParser
+{
+public:
+ explicit TestableRTDEParser(const std::vector& recipe) : rtde_interface::RTDEParser(recipe)
+ {
+ }
+
+ using rtde_interface::RTDEParser::setRecipeTypes;
+};
+
+class TestableRTDEWriter : public rtde_interface::RTDEWriter
+{
+public:
+ TestableRTDEWriter(comm::URStream* stream, const std::vector& recipe)
+ : rtde_interface::RTDEWriter(stream, recipe)
+ {
+ }
+
+ using rtde_interface::RTDEWriter::setRecipeTypes;
+};
+
+/*!
+ * \brief Builds a data package the way the library does: allocate from the recipe, then apply the
+ * data types the robot reported for it.
+ */
+inline TestableDataPackage typedPackage(const std::vector& recipe, const std::vector& types,
+ const uint16_t protocol_version = 2)
+{
+ TestableDataPackage package(recipe, protocol_version);
+ package.initEmpty(types);
+ return package;
+}
+} // namespace test
+} // namespace urcl
diff --git a/tests/test_pipeline.cpp b/tests/test_pipeline.cpp
index c7f692234..b24de2cc7 100644
--- a/tests/test_pipeline.cpp
+++ b/tests/test_pipeline.cpp
@@ -40,6 +40,8 @@
#include
#include
+#include "rtde_test_helpers.h"
+
using namespace urcl;
class PipelineTest : public ::testing::Test
@@ -53,7 +55,8 @@ class PipelineTest : public ::testing::Test
// Setup pipeline
stream_.reset(new comm::URStream("127.0.0.1", 60002));
std::vector recipe = { "timestamp" };
- parser_.reset(new rtde_interface::RTDEParser(recipe));
+ parser_.reset(new test::TestableRTDEParser(recipe));
+ parser_->setRecipeTypes({ "DOUBLE" });
parser_->setProtocolVersion(2);
producer_.reset(new comm::URProducer(*stream_.get(), *parser_.get()));
@@ -72,7 +75,7 @@ class PipelineTest : public ::testing::Test
std::unique_ptr server_;
std::unique_ptr> stream_;
- std::unique_ptr parser_;
+ std::unique_ptr parser_;
std::unique_ptr> producer_;
std::unique_ptr> pipeline_;
comm::INotifier notifier_;
diff --git a/tests/test_producer.cpp b/tests/test_producer.cpp
index 32069be32..0770ab3f4 100644
--- a/tests/test_producer.cpp
+++ b/tests/test_producer.cpp
@@ -38,6 +38,8 @@
#include
#include
+#include "rtde_test_helpers.h"
+
using namespace urcl;
class ProducerTest : public ::testing::Test
@@ -62,7 +64,8 @@ TEST_F(ProducerTest, get_data_package)
{
comm::URStream stream("127.0.0.1", 60002);
std::vector recipe = { "timestamp" };
- rtde_interface::RTDEParser parser(recipe);
+ test::TestableRTDEParser parser(recipe);
+ parser.setRecipeTypes({ "DOUBLE" });
parser.setProtocolVersion(2);
comm::URProducer producer(stream, parser);
@@ -97,7 +100,8 @@ TEST_F(ProducerTest, connect_non_connected_robot)
{
comm::URStream stream("127.0.0.1", 12321);
std::vector recipe = { "timestamp" };
- rtde_interface::RTDEParser parser(recipe);
+ test::TestableRTDEParser parser(recipe);
+ parser.setRecipeTypes({ "DOUBLE" });
parser.setProtocolVersion(2);
comm::URProducer producer(stream, parser);
diff --git a/tests/test_rtde_allocations.cpp b/tests/test_rtde_allocations.cpp
new file mode 100644
index 000000000..ab24bbfbf
--- /dev/null
+++ b/tests/test_rtde_allocations.cpp
@@ -0,0 +1,294 @@
+// -- BEGIN LICENSE BLOCK ----------------------------------------------
+// Copyright 2026 Universal Robots A/S
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+//
+// * Redistributions in binary form must reproduce the above copyright
+// notice, this list of conditions and the following disclaimer in the
+// documentation and/or other materials provided with the distribution.
+//
+// * Neither the name of the {copyright_holder} nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+// POSSIBILITY OF SUCH DAMAGE.
+// -- END LICENSE BLOCK ------------------------------------------------
+
+// All memory an RTDE connection needs for exchanging data is allocated while the recipes are set
+// up, using the data types the robot reports in its acknowledgements. These tests pin that down by
+// counting allocations around a few hundred receive cycles against the fake RTDE server.
+
+#include
+
+#include
+#include
+
+#include
+#include
+
+#include "fake_rtde_server.h"
+
+using namespace urcl;
+
+namespace
+{
+// Counting is per-thread: the fake server and, in the background-read case, the client's read
+// thread run in the same process, and their allocations are none of this test's business.
+thread_local std::size_t g_allocation_count = 0;
+thread_local bool g_count_allocations = false;
+
+constexpr int g_FAKE_RTDE_PORT = 60005;
+constexpr double g_RTDE_FREQUENCY = 125.0;
+constexpr int g_WARMUP_CYCLES = 10;
+constexpr int g_MEASURED_CYCLES = 50;
+
+/*!
+ * \brief Counts the allocations made on the current thread for as long as it is alive.
+ */
+class AllocationCounter
+{
+public:
+ AllocationCounter()
+ {
+ g_allocation_count = 0;
+ g_count_allocations = true;
+ }
+
+ ~AllocationCounter()
+ {
+ g_count_allocations = false;
+ }
+
+ std::size_t count() const
+ {
+ return g_allocation_count;
+ }
+};
+} // namespace
+
+// These replace the global allocation functions, so pairing malloc with free is correct here even
+// though GCC cannot see across the replacement and flags it.
+#if defined(__GNUC__) && !defined(__clang__)
+# pragma GCC diagnostic push
+# pragma GCC diagnostic ignored "-Wmismatched-new-delete"
+#endif
+
+void* operator new(std::size_t size)
+{
+ if (g_count_allocations)
+ {
+ ++g_allocation_count;
+ }
+ void* memory = std::malloc(size == 0 ? 1 : size);
+ if (memory == nullptr)
+ {
+ throw std::bad_alloc();
+ }
+ return memory;
+}
+
+void* operator new[](std::size_t size)
+{
+ return operator new(size);
+}
+
+void operator delete(void* memory) noexcept
+{
+ std::free(memory);
+}
+
+void operator delete[](void* memory) noexcept
+{
+ std::free(memory);
+}
+
+void operator delete(void* memory, std::size_t) noexcept
+{
+ std::free(memory);
+}
+
+void operator delete[](void* memory, std::size_t) noexcept
+{
+ std::free(memory);
+}
+
+#if defined(__GNUC__) && !defined(__clang__)
+# pragma GCC diagnostic pop
+#endif
+
+// Guards the tests below: if the counter stopped seeing allocations, they would pass vacuously.
+TEST(AllocationCounterTest, counts_allocations)
+{
+ std::size_t allocations = 0;
+ std::vector values;
+ {
+ AllocationCounter counter;
+ values.resize(1024);
+ allocations = counter.count();
+ }
+ EXPECT_GT(allocations, 0);
+}
+
+class RTDEAllocationTest : public ::testing::Test
+{
+protected:
+ void SetUp() override
+ {
+ server_ = std::make_unique(g_FAKE_RTDE_PORT);
+ // Skip the client's bootup check, which would otherwise read data for a second
+ server_->setStartTime(std::chrono::steady_clock::now() - std::chrono::seconds(42));
+
+ client_ = std::make_unique("localhost", notifier_, output_recipe_, input_recipe_,
+ g_RTDE_FREQUENCY, false, g_FAKE_RTDE_PORT);
+ ASSERT_TRUE(client_->init());
+ }
+
+ void TearDown() override
+ {
+ client_.reset();
+ server_.reset();
+ }
+
+ // A recipe covering all data types that appear on the receiving side
+ std::vector output_recipe_{ "timestamp", "actual_q",
+ "actual_TCP_force", "runtime_state",
+ "robot_status_bits", "actual_digital_input_bits",
+ "joint_mode", "payload_cog",
+ "tool_mode", "output_int_register_24" };
+ std::vector input_recipe_{ "speed_slider_mask", "speed_slider_fraction" };
+
+ comm::INotifier notifier_;
+ std::unique_ptr server_;
+ std::unique_ptr client_;
+};
+
+TEST_F(RTDEAllocationTest, blocking_receive_does_not_allocate)
+{
+ ASSERT_TRUE(client_->start(false));
+ auto data_pkg = std::make_unique(client_->getOutputRecipe());
+
+ // The first cycles let every buffer along the way reach its final capacity
+ for (int i = 0; i < g_WARMUP_CYCLES; ++i)
+ {
+ ASSERT_TRUE(client_->getDataPackageBlocking(data_pkg));
+ }
+
+ // Deliberately no gtest macros inside the measured section, as those allocate themselves
+ int received = 0;
+ bool all_data_read = true;
+ double timestamp = 0.0;
+ vector6d_t actual_q{};
+ std::bitset<18> digital_input_bits;
+ std::size_t allocations = 0;
+ {
+ AllocationCounter counter;
+ for (int i = 0; i < g_MEASURED_CYCLES; ++i)
+ {
+ if (!client_->getDataPackageBlocking(data_pkg))
+ {
+ continue;
+ }
+ ++received;
+ all_data_read &= data_pkg->getData("timestamp", timestamp);
+ all_data_read &= data_pkg->getData("actual_q", actual_q);
+ all_data_read &= data_pkg->getData("actual_digital_input_bits", digital_input_bits);
+ }
+ allocations = counter.count();
+ }
+
+ EXPECT_EQ(allocations, 0);
+ EXPECT_TRUE(all_data_read);
+ EXPECT_GT(received, 0);
+ EXPECT_GT(timestamp, 0.0);
+}
+
+TEST_F(RTDEAllocationTest, background_receive_does_not_allocate)
+{
+ ASSERT_TRUE(client_->start(true));
+ rtde_interface::DataPackage data_pkg(client_->getOutputRecipe());
+ const std::chrono::milliseconds read_timeout{ 100 };
+
+ for (int i = 0; i < g_WARMUP_CYCLES; ++i)
+ {
+ ASSERT_TRUE(client_->getDataPackage(data_pkg, read_timeout));
+ }
+
+ int received = 0;
+ bool all_data_read = true;
+ double timestamp = 0.0;
+ std::size_t allocations = 0;
+ {
+ AllocationCounter counter;
+ for (int i = 0; i < g_MEASURED_CYCLES; ++i)
+ {
+ if (!client_->getDataPackage(data_pkg, read_timeout))
+ {
+ continue;
+ }
+ ++received;
+ all_data_read &= data_pkg.getData("timestamp", timestamp);
+ }
+ allocations = counter.count();
+ }
+
+ EXPECT_EQ(allocations, 0);
+ EXPECT_TRUE(all_data_read);
+ EXPECT_GT(received, 0);
+ EXPECT_GT(timestamp, 0.0);
+
+ client_->pause();
+}
+
+TEST_F(RTDEAllocationTest, sending_input_data_does_not_allocate)
+{
+ ASSERT_TRUE(client_->start(true));
+ rtde_interface::DataPackage input_pkg(client_->getInputRecipe());
+ ASSERT_TRUE(input_pkg.setData("speed_slider_mask", static_cast(1)));
+
+ for (int i = 0; i < g_WARMUP_CYCLES; ++i)
+ {
+ ASSERT_TRUE(client_->getWriter().sendSpeedSlider(0.5));
+ }
+
+ bool all_sent = true;
+ std::size_t allocations = 0;
+ {
+ AllocationCounter counter;
+ for (int i = 0; i < g_MEASURED_CYCLES; ++i)
+ {
+ all_sent &= input_pkg.setData("speed_slider_fraction", 0.5);
+ all_sent &= client_->getWriter().sendPackage(input_pkg);
+ }
+ allocations = counter.count();
+ }
+
+ EXPECT_EQ(allocations, 0);
+ EXPECT_TRUE(all_sent);
+
+ client_->pause();
+}
+
+int main(int argc, char* argv[])
+{
+ ::testing::InitGoogleTest(&argc, argv);
+
+ // Logging allocates, and a log statement inside a measured section would rightfully be counted.
+ // Keep the routine chatter out of the way so the tests measure the data exchange itself.
+ setLogLevel(LogLevel::ERROR);
+
+ return RUN_ALL_TESTS();
+}
diff --git a/tests/test_rtde_client.cpp b/tests/test_rtde_client.cpp
index 1fcff8b8f..f9938527d 100644
--- a/tests/test_rtde_client.cpp
+++ b/tests/test_rtde_client.cpp
@@ -290,17 +290,17 @@ TEST_F(RTDEClientTest, output_recipe_file)
}
}
+// The robot is the authority on which fields exist and what type they have, so a typo in a recipe
+// is reported when the robot rejects it during init(), not already at construction time.
TEST_F(RTDEClientTest, input_recipe_with_invalid_key)
{
std::vector actual_input_recipe = resources_input_recipe_;
actual_input_recipe.push_back("i_do_not_exist");
- EXPECT_THAT(
- [&]() {
- client_.reset(
- new rtde_interface::RTDEClient(g_ROBOT_IP, notifier_, resources_output_recipe_, actual_input_recipe));
- },
- testing::ThrowsMessage(testing::HasSubstr("i_do_not_exist")));
+ client_.reset(new rtde_interface::RTDEClient(g_ROBOT_IP, notifier_, resources_output_recipe_, actual_input_recipe));
+
+ EXPECT_THAT([&]() { client_->init(); }, testing::ThrowsMessage(testing::HasSubstr("i_do_not_"
+ "exist")));
}
TEST_F(RTDEClientTest, output_recipe_with_invalid_key)
@@ -308,12 +308,10 @@ TEST_F(RTDEClientTest, output_recipe_with_invalid_key)
std::vector actual_output_recipe = resources_output_recipe_;
actual_output_recipe.push_back("i_do_not_exist");
- EXPECT_THAT(
- [&]() {
- client_.reset(
- new rtde_interface::RTDEClient(g_ROBOT_IP, notifier_, actual_output_recipe, resources_input_recipe_));
- },
- testing::ThrowsMessage(testing::HasSubstr("i_do_not_exist")));
+ client_.reset(new rtde_interface::RTDEClient(g_ROBOT_IP, notifier_, actual_output_recipe, resources_input_recipe_));
+
+ EXPECT_THAT([&]() { client_->init(); }, testing::ThrowsMessage(testing::HasSubstr("i_do_not_"
+ "exist")));
TestableRTDEClient client(g_ROBOT_IP, notifier_, resources_output_recipe_, resources_input_recipe_);
client.injectOutputRecipe(actual_output_recipe);
@@ -445,7 +443,7 @@ TEST_F(RTDEClientTest, get_data_package_fake_server)
// Test that we can receive a package and extract data from the received package
const std::chrono::milliseconds read_timeout{ 100 };
- auto data_pkg = rtde_interface::DataPackage(client_->getOutputRecipe());
+ rtde_interface::DataPackage data_pkg(client_->getOutputRecipe());
if (!client_->getDataPackage(data_pkg, read_timeout))
{
std::cout << "Failed to get data package from robot" << std::endl;
@@ -471,7 +469,7 @@ TEST_F(RTDEClientTest, destroy_client_after_server_stops_sending)
URCL_LOG_INFO("Receiving data package from fake server to verify that connection is working.");
const std::chrono::milliseconds read_timeout{ 100 };
- auto data_pkg = rtde_interface::DataPackage(client_->getOutputRecipe());
+ rtde_interface::DataPackage data_pkg(client_->getOutputRecipe());
ASSERT_TRUE(client_->getDataPackage(data_pkg, read_timeout));
double timestamp = 0.0;
@@ -771,30 +769,35 @@ TEST_F(RTDEClientTest, check_unknown_rtde_output_variable)
{
client_->init();
- std::vector incorrect_output_recipe = client_->getOutputRecipe();
+ const VersionInformation robot_version = client_->getVersion();
+ const std::vector output_recipe = client_->getOutputRecipe();
+ std::vector incorrect_output_recipe = output_recipe;
incorrect_output_recipe.push_back("unknown_rtde_variable");
+ // Only one client can hold the RTDE input recipe at a time, so disconnect before setting up the
+ // next one.
+ client_.reset();
+
// If unknown variables are not ignored, initialization should fail
- EXPECT_THROW(client_.reset(new rtde_interface::RTDEClient(g_ROBOT_IP, notifier_, incorrect_output_recipe,
- resources_input_recipe_, 0.0, false)),
- RTDEInvalidKeyException);
+ auto client = std::make_unique(g_ROBOT_IP, notifier_, incorrect_output_recipe,
+ resources_input_recipe_, 0.0, false);
+ EXPECT_THROW(client->init(), RTDEInvalidKeyException);
// Unknown variables (by the control box) can be ignored, so initialization should succeed
- if ((client_->getVersion().major == 5 && client_->getVersion().minor < 23) ||
- (client_->getVersion().major == 10 && client_->getVersion().minor < 11))
+ if ((robot_version.major == 5 && robot_version.minor < 23) || (robot_version.major == 10 && robot_version.minor < 11))
{
- std::vector output_recipe = client_->getOutputRecipe();
- output_recipe.push_back("actual_robot_energy_consumed"); // That has been added in 5.23.0 / 10.11.0
- client_.reset(
- new rtde_interface::RTDEClient(g_ROBOT_IP, notifier_, output_recipe, resources_input_recipe_, 0.0, true));
- EXPECT_TRUE(client_->init());
+ std::vector newer_output_recipe = output_recipe;
+ newer_output_recipe.push_back("actual_robot_energy_consumed"); // That has been added in 5.23.0 / 10.11.0
+ client = std::make_unique(g_ROBOT_IP, notifier_, newer_output_recipe,
+ resources_input_recipe_, 0.0, true);
+ EXPECT_TRUE(client->init());
}
// Passing a completely unknown variable should still lead to an exception, even if unknown
// variables are ignored.
- EXPECT_THROW(client_.reset(new rtde_interface::RTDEClient(g_ROBOT_IP, notifier_, incorrect_output_recipe,
- resources_input_recipe_, 0.0, true)),
- RTDEInvalidKeyException);
+ client = std::make_unique(g_ROBOT_IP, notifier_, incorrect_output_recipe,
+ resources_input_recipe_, 0.0, true);
+ EXPECT_THROW(client->init(), RTDEInvalidKeyException);
}
TEST_F(RTDEClientTest, empty_input_recipe)
diff --git a/tests/test_rtde_client_fake_server.cpp b/tests/test_rtde_client_fake_server.cpp
new file mode 100644
index 000000000..656aa7284
--- /dev/null
+++ b/tests/test_rtde_client_fake_server.cpp
@@ -0,0 +1,378 @@
+// -- BEGIN LICENSE BLOCK ----------------------------------------------
+// Copyright 2026 Universal Robots A/S
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+//
+// * Redistributions in binary form must reproduce the above copyright
+// notice, this list of conditions and the following disclaimer in the
+// documentation and/or other materials provided with the distribution.
+//
+// * Neither the name of the {copyright_holder} nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+// POSSIBILITY OF SUCH DAMAGE.
+// -- END LICENSE BLOCK ------------------------------------------------
+
+// Covers the public surface of RTDEClient against the fake RTDE server. The tests in
+// test_rtde_client.cpp are more thorough but need a reachable robot, so they only run when
+// INTEGRATION_TESTS is enabled; these run everywhere.
+
+#include
+
+#include
+#include
+#include
+
+#include "fake_rtde_server.h"
+
+using namespace urcl;
+
+namespace
+{
+constexpr int g_FAKE_RTDE_PORT = 60006;
+constexpr double g_RTDE_FREQUENCY = 125.0;
+// The fake server answers the version query with 10.10.10.10, so the client takes the e-Series limit
+constexpr double g_MAX_FREQUENCY = 500.0;
+constexpr std::chrono::milliseconds g_READ_TIMEOUT{ 200 };
+
+const std::vector g_OUTPUT_RECIPE{ "timestamp", "actual_q", "target_speed_fraction", "runtime_state" };
+const std::vector g_INPUT_RECIPE{ "speed_slider_mask", "speed_slider_fraction" };
+} // namespace
+
+class RTDEClientFakeServerTest : public ::testing::Test
+{
+protected:
+ void SetUp() override
+ {
+ server_ = std::make_unique(g_FAKE_RTDE_PORT);
+ // Skip the client's bootup check, which would otherwise read data for a second
+ server_->setStartTime(std::chrono::steady_clock::now() - std::chrono::seconds(42));
+ client_ = makeClient(g_OUTPUT_RECIPE, g_INPUT_RECIPE, g_RTDE_FREQUENCY);
+ }
+
+ void TearDown() override
+ {
+ client_.reset();
+ server_.reset();
+ }
+
+ std::unique_ptr makeClient(const std::vector& output_recipe,
+ const std::vector& input_recipe,
+ double target_frequency,
+ bool ignore_unavailable_outputs = false)
+ {
+ return std::make_unique("localhost", notifier_, output_recipe, input_recipe,
+ target_frequency, ignore_unavailable_outputs, g_FAKE_RTDE_PORT);
+ }
+
+ comm::INotifier notifier_;
+ std::unique_ptr server_;
+ std::unique_ptr client_;
+};
+
+// The address is the one the socket resolved to, so it only exists once the socket is connected.
+TEST_F(RTDEClientFakeServerTest, get_ip)
+{
+ EXPECT_TRUE(client_->getIP().empty());
+
+ ASSERT_TRUE(client_->init());
+
+ EXPECT_EQ(client_->getIP(), "127.0.0.1");
+}
+
+TEST_F(RTDEClientFakeServerTest, recipes_are_reported_as_given)
+{
+ EXPECT_EQ(client_->getOutputRecipe(), g_OUTPUT_RECIPE);
+ EXPECT_EQ(client_->getInputRecipe(), g_INPUT_RECIPE);
+}
+
+// The client needs the timestamp to tell whether the robot has finished booting, so it adds the
+// field to recipes that don't ask for it.
+TEST_F(RTDEClientFakeServerTest, timestamp_is_added_to_the_output_recipe)
+{
+ auto client = makeClient({ "actual_q" }, g_INPUT_RECIPE, g_RTDE_FREQUENCY);
+
+ const std::vector expected_recipe{ "actual_q", "timestamp" };
+ EXPECT_EQ(client->getOutputRecipe(), expected_recipe);
+}
+
+TEST_F(RTDEClientFakeServerTest, read_recipe_from_file)
+{
+ const std::vector recipe = rtde_interface::RTDEClient::readRecipe("resources/rtde_input_recipe.txt");
+
+ EXPECT_FALSE(recipe.empty());
+ EXPECT_EQ(recipe.front(), "speed_slider_mask");
+
+ EXPECT_THROW(rtde_interface::RTDEClient::readRecipe("resources/there_is_no_such_recipe.txt"), UrException);
+}
+
+TEST_F(RTDEClientFakeServerTest, client_state_follows_the_communication)
+{
+ EXPECT_EQ(client_->getClientState(), rtde_interface::ClientState::UNINITIALIZED);
+
+ ASSERT_TRUE(client_->init());
+ EXPECT_EQ(client_->getClientState(), rtde_interface::ClientState::INITIALIZED);
+
+ ASSERT_TRUE(client_->start());
+ EXPECT_EQ(client_->getClientState(), rtde_interface::ClientState::RUNNING);
+
+ ASSERT_TRUE(client_->pause());
+ EXPECT_EQ(client_->getClientState(), rtde_interface::ClientState::PAUSED);
+}
+
+TEST_F(RTDEClientFakeServerTest, init_is_idempotent)
+{
+ ASSERT_TRUE(client_->init());
+ EXPECT_TRUE(client_->init());
+ EXPECT_EQ(client_->getClientState(), rtde_interface::ClientState::INITIALIZED);
+}
+
+TEST_F(RTDEClientFakeServerTest, start_and_pause_out_of_order)
+{
+ EXPECT_FALSE(client_->start());
+ EXPECT_FALSE(client_->pause());
+
+ ASSERT_TRUE(client_->init());
+ EXPECT_FALSE(client_->pause());
+
+ ASSERT_TRUE(client_->start());
+ ASSERT_TRUE(client_->pause());
+ // A paused client can be started again
+ EXPECT_TRUE(client_->start());
+ EXPECT_TRUE(client_->pause());
+}
+
+TEST_F(RTDEClientFakeServerTest, version_is_taken_from_the_robot)
+{
+ ASSERT_TRUE(client_->init());
+
+ const VersionInformation version = client_->getVersion();
+ EXPECT_EQ(version.major, 10);
+ EXPECT_EQ(version.minor, 10);
+}
+
+TEST_F(RTDEClientFakeServerTest, target_frequency_defaults_to_the_maximum)
+{
+ auto client = makeClient(g_OUTPUT_RECIPE, g_INPUT_RECIPE, 0.0);
+ EXPECT_EQ(client->getTargetFrequency(), 0.0);
+
+ ASSERT_TRUE(client->init());
+
+ EXPECT_EQ(client->getMaxFrequency(), g_MAX_FREQUENCY);
+ EXPECT_EQ(client->getTargetFrequency(), client->getMaxFrequency());
+}
+
+TEST_F(RTDEClientFakeServerTest, configured_target_frequency_is_kept)
+{
+ ASSERT_TRUE(client_->init());
+
+ EXPECT_EQ(client_->getMaxFrequency(), g_MAX_FREQUENCY);
+ EXPECT_EQ(client_->getTargetFrequency(), g_RTDE_FREQUENCY);
+}
+
+TEST_F(RTDEClientFakeServerTest, target_frequency_outside_the_robots_range_throws)
+{
+ auto too_low = makeClient(g_OUTPUT_RECIPE, g_INPUT_RECIPE, -1.0);
+ EXPECT_THROW(too_low->init(), UrException);
+
+ auto too_high = makeClient(g_OUTPUT_RECIPE, g_INPUT_RECIPE, g_MAX_FREQUENCY + 1.0);
+ EXPECT_THROW(too_high->init(), UrException);
+}
+
+TEST_F(RTDEClientFakeServerTest, receive_with_background_read)
+{
+ ASSERT_TRUE(client_->init());
+ ASSERT_TRUE(client_->start(true));
+
+ rtde_interface::DataPackage data_pkg(client_->getOutputRecipe());
+ ASSERT_TRUE(client_->getDataPackage(data_pkg, g_READ_TIMEOUT));
+
+ double timestamp = 0.0;
+ ASSERT_TRUE(data_pkg.getData("timestamp", timestamp));
+ EXPECT_GT(timestamp, 0.0);
+
+ // Blocking reads would compete with the background thread for the same packages
+ auto blocking_pkg = std::make_unique(client_->getOutputRecipe());
+ EXPECT_FALSE(client_->getDataPackageBlocking(blocking_pkg));
+
+ client_->pause();
+}
+
+TEST_F(RTDEClientFakeServerTest, receive_with_background_read_into_a_unique_ptr)
+{
+ ASSERT_TRUE(client_->init());
+ ASSERT_TRUE(client_->start(true));
+
+ auto data_pkg = std::make_unique(client_->getOutputRecipe());
+ ASSERT_TRUE(client_->getDataPackage(data_pkg, g_READ_TIMEOUT));
+
+ double timestamp = 0.0;
+ ASSERT_TRUE(data_pkg->getData("timestamp", timestamp));
+ EXPECT_GT(timestamp, 0.0);
+
+ client_->pause();
+}
+
+TEST_F(RTDEClientFakeServerTest, receive_without_background_read)
+{
+ ASSERT_TRUE(client_->init());
+ ASSERT_TRUE(client_->start(false));
+
+ auto data_pkg = std::make_unique(client_->getOutputRecipe());
+ ASSERT_TRUE(client_->getDataPackageBlocking(data_pkg));
+
+ double timestamp = 0.0;
+ ASSERT_TRUE(data_pkg->getData("timestamp", timestamp));
+ EXPECT_GT(timestamp, 0.0);
+
+ // Without the background thread there is nothing for the non-blocking overload to read from
+ rtde_interface::DataPackage other_pkg(client_->getOutputRecipe());
+ EXPECT_FALSE(client_->getDataPackage(other_pkg, g_READ_TIMEOUT));
+
+ client_->pause();
+}
+
+TEST_F(RTDEClientFakeServerTest, background_read_can_be_stopped_and_started)
+{
+ ASSERT_TRUE(client_->init());
+ ASSERT_TRUE(client_->start(true));
+
+ rtde_interface::DataPackage data_pkg(client_->getOutputRecipe());
+ ASSERT_TRUE(client_->getDataPackage(data_pkg, g_READ_TIMEOUT));
+
+ client_->stopBackgroundRead();
+ EXPECT_FALSE(client_->getDataPackage(data_pkg, g_READ_TIMEOUT));
+
+ client_->startBackgroundRead();
+ EXPECT_TRUE(client_->getDataPackage(data_pkg, g_READ_TIMEOUT));
+
+ client_->pause();
+}
+
+TEST_F(RTDEClientFakeServerTest, background_read_before_init_is_refused)
+{
+ client_->startBackgroundRead();
+
+ rtde_interface::DataPackage data_pkg(client_->getOutputRecipe());
+ EXPECT_FALSE(client_->getDataPackage(data_pkg, g_READ_TIMEOUT));
+}
+
+TEST_F(RTDEClientFakeServerTest, deprecated_get_data_package_returns_a_usable_package)
+{
+ ASSERT_TRUE(client_->init());
+ ASSERT_TRUE(client_->start(true));
+
+ URCL_SILENCE_DEPRECATED_BEGIN
+ std::unique_ptr data_pkg = client_->getDataPackage(g_READ_TIMEOUT);
+ URCL_SILENCE_DEPRECATED_END
+
+ ASSERT_NE(data_pkg, nullptr);
+ double timestamp = 0.0;
+ ASSERT_TRUE(data_pkg->getData("timestamp", timestamp));
+ EXPECT_GT(timestamp, 0.0);
+
+ client_->pause();
+}
+
+// The fake server echoes the speed slider fraction back as target_speed_fraction, which is enough to
+// see a value travel all the way through the writer and back.
+TEST_F(RTDEClientFakeServerTest, write_and_read_back_input_data)
+{
+ ASSERT_TRUE(client_->init());
+ ASSERT_TRUE(client_->start(true));
+
+ rtde_interface::DataPackage input_pkg(client_->getInputRecipe());
+ ASSERT_TRUE(input_pkg.setData("speed_slider_mask", 1));
+ ASSERT_TRUE(input_pkg.setData("speed_slider_fraction", 0.25));
+ ASSERT_TRUE(client_->getWriter().sendPackage(input_pkg));
+
+ rtde_interface::DataPackage data_pkg(client_->getOutputRecipe());
+ double target_speed_fraction = 0.0;
+ for (int i = 0; i < 20 && target_speed_fraction == 0.0; ++i)
+ {
+ ASSERT_TRUE(client_->getDataPackage(data_pkg, g_READ_TIMEOUT));
+ ASSERT_TRUE(data_pkg.getData("target_speed_fraction", target_speed_fraction));
+ }
+ EXPECT_DOUBLE_EQ(target_speed_fraction, 0.25);
+
+ client_->pause();
+}
+
+// The robot is the authority on which fields exist, so a typo in a recipe is caught from the
+// acknowledgement rather than from a table inside the library.
+TEST_F(RTDEClientFakeServerTest, unknown_output_field_throws)
+{
+ auto client = makeClient({ "timestamp", "not_a_field_the_robot_knows" }, g_INPUT_RECIPE, g_RTDE_FREQUENCY);
+
+ EXPECT_THROW(client->init(), RTDEInvalidKeyException);
+}
+
+TEST_F(RTDEClientFakeServerTest, unknown_output_field_can_be_ignored)
+{
+ auto client =
+ makeClient({ "timestamp", "actual_q", "not_a_field_the_robot_knows" }, g_INPUT_RECIPE, g_RTDE_FREQUENCY, true);
+
+ ASSERT_TRUE(client->init());
+
+ const std::vector expected_recipe{ "timestamp", "actual_q" };
+ EXPECT_EQ(client->getOutputRecipe(), expected_recipe);
+
+ ASSERT_TRUE(client->start(true));
+ rtde_interface::DataPackage data_pkg(client->getOutputRecipe());
+ EXPECT_TRUE(client->getDataPackage(data_pkg, g_READ_TIMEOUT));
+
+ client->pause();
+}
+
+TEST_F(RTDEClientFakeServerTest, unknown_input_field_throws)
+{
+ auto client = makeClient(g_OUTPUT_RECIPE, { "not_a_field_the_robot_knows" }, g_RTDE_FREQUENCY);
+
+ EXPECT_THROW(client->init(), RTDEInvalidKeyException);
+}
+
+// The other constructor takes recipe files, and a missing or empty output recipe is rejected right
+// away rather than at handshake time.
+TEST_F(RTDEClientFakeServerTest, recipe_files)
+{
+ EXPECT_NO_THROW(rtde_interface::RTDEClient("localhost", notifier_, "resources/rtde_output_recipe.txt",
+ "resources/rtde_input_recipe.txt", g_RTDE_FREQUENCY, false,
+ g_FAKE_RTDE_PORT));
+
+ EXPECT_THROW(rtde_interface::RTDEClient("localhost", notifier_, "", "resources/rtde_input_recipe.txt",
+ g_RTDE_FREQUENCY, false, g_FAKE_RTDE_PORT),
+ UrException);
+
+ EXPECT_THROW(rtde_interface::RTDEClient("localhost", notifier_, "resources/empty.txt",
+ "resources/rtde_input_recipe.txt", g_RTDE_FREQUENCY, false, g_FAKE_RTDE_PORT),
+ UrException);
+
+ EXPECT_THROW(rtde_interface::RTDEClient("localhost", notifier_, "resources/rtde_output_recipe.txt",
+ "/i/do/not/exist/urclrtdetest.txt", g_RTDE_FREQUENCY, false,
+ g_FAKE_RTDE_PORT),
+ UrException);
+}
+
+int main(int argc, char* argv[])
+{
+ ::testing::InitGoogleTest(&argc, argv);
+
+ setLogLevel(LogLevel::ERROR);
+
+ return RUN_ALL_TESTS();
+}
diff --git a/tests/test_rtde_data_package.cpp b/tests/test_rtde_data_package.cpp
index f641fcf21..20a184ef7 100644
--- a/tests/test_rtde_data_package.cpp
+++ b/tests/test_rtde_data_package.cpp
@@ -28,14 +28,19 @@
#include
+#include
#include
+#include "rtde_test_helpers.h"
+
using namespace urcl;
+using urcl::test::typedPackage;
TEST(rtde_data_package, serialize_pkg)
{
std::vector recipe{ "speed_slider_mask" };
- rtde_interface::DataPackage package(recipe);
+ std::vector types{ "UINT32" };
+ auto package = typedPackage(recipe, types);
uint32_t value = 1;
package.setData("speed_slider_mask", value);
@@ -57,7 +62,8 @@ TEST(rtde_data_package, serialize_pkg)
TEST(rtde_data_package, parse_pkg_protocolv2)
{
std::vector recipe{ "timestamp", "actual_q" };
- rtde_interface::DataPackage package(recipe);
+ std::vector types{ "DOUBLE", "VECTOR6D" };
+ auto package = typedPackage(recipe, types);
uint8_t data_package[] = { 0x01, 0x40, 0xd0, 0x75, 0x8c, 0x49, 0xba, 0x5e, 0x35, 0xbf, 0xf9, 0x9c, 0x77, 0xd1, 0x10,
0xb4, 0x60, 0xbf, 0xfb, 0xa2, 0x33, 0xd1, 0x10, 0xb4, 0x60, 0xc0, 0x01, 0x9f, 0xbe, 0x68,
@@ -90,7 +96,8 @@ TEST(rtde_data_package, parse_pkg_protocolv2)
TEST(rtde_data_package, parse_pkg_protocolv1)
{
std::vector recipe{ "timestamp", "actual_q" };
- rtde_interface::DataPackage package(recipe, 1);
+ std::vector types{ "DOUBLE", "VECTOR6D" };
+ auto package = typedPackage(recipe, types, 1);
uint8_t data_package[] = { 0x40, 0xd0, 0x75, 0x8c, 0x49, 0xba, 0x5e, 0x35, 0xbf, 0xf9, 0x9c, 0x77, 0xd1, 0x10,
0xb4, 0x60, 0xbf, 0xfb, 0xa2, 0x33, 0xd1, 0x10, 0xb4, 0x60, 0xc0, 0x01, 0x9f, 0xbe,
@@ -122,7 +129,8 @@ TEST(rtde_data_package, parse_pkg_protocolv1)
TEST(rtde_data_package, get_data_not_part_of_recipe)
{
std::vector recipe{ "timestamp", "actual_q" };
- rtde_interface::DataPackage package(recipe);
+ std::vector types{ "DOUBLE", "VECTOR6D" };
+ auto package = typedPackage(recipe, types);
uint32_t speed_slider_mask;
EXPECT_FALSE(package.getData("speed_slider_mask", speed_slider_mask));
@@ -131,7 +139,8 @@ TEST(rtde_data_package, get_data_not_part_of_recipe)
TEST(rtde_data_package, set_data_not_part_of_recipe)
{
std::vector recipe{ "timestamp", "actual_q" };
- rtde_interface::DataPackage package(recipe);
+ std::vector types{ "DOUBLE", "VECTOR6D" };
+ auto package = typedPackage(recipe, types);
uint32_t speed_slider_mask = 1;
EXPECT_FALSE(package.setData("speed_slider_mask", speed_slider_mask));
@@ -140,7 +149,8 @@ TEST(rtde_data_package, set_data_not_part_of_recipe)
TEST(rtde_data_package, parse_and_get_bitset_data)
{
std::vector recipe{ "robot_status_bits" };
- rtde_interface::DataPackage package(recipe);
+ std::vector types{ "UINT32" };
+ auto package = typedPackage(recipe, types);
uint8_t data_package[] = { 0x01, 0x00, 0x00, 0x00, 0x00, 0x40, 0xb2, 0x3d, 0xa9, 0xfb, 0xe7, 0x6c, 0x8b };
comm::BinParser bp(data_package, sizeof(data_package));
@@ -157,7 +167,8 @@ TEST(rtde_data_package, parse_and_get_bitset_data)
TEST(rtde_data_package, parse_incorrect_data_size)
{
std::vector recipe{ "timestamp", "actual_q" };
- rtde_interface::DataPackage package(recipe);
+ std::vector types{ "DOUBLE", "VECTOR6D" };
+ auto package = typedPackage(recipe, types);
// Data package with incorrect size (should be 56 bytes for the given recipe)
uint8_t data_package[] = { 0x01, 0x40, 0xd0, 0x75, 0x8c, 0x49, 0xba, 0x5e, 0x35, 0xbf };
@@ -171,7 +182,8 @@ TEST(rtde_data_package, data_package_to_string)
{
std::vector recipe{ "speed_slider_mask", "speed_slider_fraction", "external_force_torque",
"standard_digital_output_mask", "actual_digital_output_bits" };
- rtde_interface::DataPackage package(recipe);
+ std::vector types{ "UINT32", "DOUBLE", "VECTOR6D", "UINT8", "UINT64" };
+ auto package = typedPackage(recipe, types);
ASSERT_TRUE(package.setData("speed_slider_mask", 1));
ASSERT_TRUE(package.setData("speed_slider_fraction", 0.5));
ASSERT_TRUE(package.setData("external_force_torque", vector6d_t{ -1.6007, -1.7271, -2.203, -0.808, 1.5951, -0.031 }));
@@ -189,6 +201,238 @@ TEST(rtde_data_package, data_package_to_string)
EXPECT_EQ(expected_str, pkg_str);
}
+TEST(rtde_data_package, every_rtde_data_type_can_be_applied)
+{
+ // The set of type names the robot may report is the only type knowledge the library still
+ // carries, so check that each one maps onto the C++ type an application expects to read.
+ std::vector