diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 8051c4b8d..52a344bf8 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -46,6 +46,40 @@ jobs:
retention-days: 5
archive: false
+ unit_coverage:
+ name: unit_coverage
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v7
+ - name: Install build-tools
+ run: sudo apt-get update && sudo apt-get install -y build-essential cmake gcovr
+ - name: configure
+ run: >
+ mkdir build &&
+ cd build &&
+ cmake ..
+ -DBUILDING_TESTS=1
+ -DINTEGRATION_TESTS=0
+ -DCMAKE_COMPILE_WARNING_AS_ERROR=ON
+ env:
+ CXXFLAGS: -g -O2 -fprofile-arcs -ftest-coverage
+ CFLAGS: -g -O2 -fprofile-arcs -ftest-coverage
+ LDFLAGS: -fprofile-arcs -ftest-coverage
+ - name: build
+ run: cmake --build build --config Debug
+ - name: test
+ run: cd build && ctest --output-on-failure --output-junit junit.xml
+ - name: gcovr
+ run: cd build && gcovr -r .. --xml coverage.xml --gcov-ignore-parse-errors negative_hits.warn_once_per_file --exclude "../3rdparty"
+ - name: Upload coverage reports to Codecov with GitHub Action
+ uses: codecov/codecov-action@v7
+ with:
+ fail_ci_if_error: true
+ files: build/coverage.xml
+ flags: unit
+ env:
+ CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
+
run_tests:
timeout-minutes: 60
runs-on: ubuntu-latest
diff --git a/doc/architecture/rtde_client.rst b/doc/architecture/rtde_client.rst
index b45823974..a67a9ace3 100644
--- a/doc/architecture/rtde_client.rst
+++ b/doc/architecture/rtde_client.rst
@@ -36,10 +36,32 @@ 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::
+
+ **Recommended:** Construct a ``DataPackage`` from ``getOutputRecipe()`` after ``init()`` and
+ reuse it in your control loop. With a matching recipe, the normal data receive path of
+ ``getDataPackage()`` and ``getDataPackageBlocking()`` does not allocate.
+
+ **Still supported, but not recommended:** The older flow that lets the client allocate a
+ package remains available for compatibility. The deprecated ``getDataPackage(timeout)``
+ overload allocates a new package on each call, and passing a null unique pointer to either
+ read method also allocates a package. Passing a package with a foreign recipe is supported
+ through automatic repair, which may allocate. The null-pointer and foreign-recipe paths log
+ warnings; these warn about allocation, not unsupported usage. Prefer a reusable, matching-recipe
+ package for new code, especially in real-time loops.
+
+ The allocation-free guarantee applies only to the normal data receive path with that reused
+ package. It does not cover error handling, non-data messages or reconnection.
+
+ 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 read applies them to your ``DataPackage`` without
+ allocation. Until that has happened ``getData()`` throws ``std::bad_variant_access``. 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 +91,94 @@ 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 deliver their data into a ``DataPackage`` that the caller owns:
+``getDataPackage()`` copies the background reader's latest package into it, while
+``getDataPackageBlocking()`` parses the next package straight into it. Reusing a package with the
+negotiated recipe keeps the normal data receive path free of memory allocations. The
+older ``getDataPackage(timeout)`` overload, which returns a new package instead, is deprecated but
+still supported. It allocates on every call by design and is not recommended for new code or
+real-time use; prefer an overload that fills an existing, reusable package.
+
+Always check the return value before using received data. A background read returns ``false`` on
+timeout or when stopping or reconnecting cancels the pending read; restarting the reader does not
+make a cancelled read succeed with stale data. Both unique-pointer overloads retain caller
+ownership on failure and assign a previously null pointer only on success. A failed blocking read
+can still partially update an existing package's values if the incoming data is malformed.
+
+Pacing a loop with the robot
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Both read modes can pace an application loop at the robot's negotiated RTDE output frequency,
+without a separate fixed-period sleep. Choose the mode according to how directly the loop should
+follow incoming packages:
+
+- With ``start(false)``, ``getDataPackageBlocking()`` waits for the next package when no data is
+ already buffered. Calling it at the start of each iteration lets packet arrival pace the loop,
+ coupling it directly to the RTDE stream without a background-reader handoff. This is useful
+ when each iteration should read data and then compute and submit a response. See the
+ :ref:`rtde_roundtrip_example`.
+- With ``start(true)``, ``getDataPackage(package, timeout)`` can also pace the loop: after the
+ latest sample has been consumed, it waits for the background reader to publish another one,
+ up to the timeout. If a newer sample is already available, it returns immediately. This mode
+ decouples socket reading from application work and favors the latest sample; intermediate
+ samples can be overwritten when the application is slower than the stream. It suits loops
+ that need less direct synchronization and do not need to process every received sample.
+
+For background reads used as a loop clock, allow enough timeout for the expected RTDE period
+and scheduling jitter, and handle a ``false`` return instead of processing old data. In either
+mode, the loop must keep up with the negotiated frequency for steady pacing. Buffered data can
+make synchronous reads return immediately, and network or scheduling delays can make arrivals
+irregular. Neither mode guarantees phase synchronization with the robot's internal control cycle
+or receipt of a command in the next cycle. Use the output ``timestamp`` to track robot time and
+detect gaps between samples.
+
+Field data types
+~~~~~~~~~~~~~~~~
+
+``getData()`` has to be given a variable of the field's own type. A missing name returns
+``false``; a type mismatch throws ``std::bad_variant_access``.
+Rather than hardcoding which type a field has, ask the package: ``getDataType()`` reports the
+``DataType`` a field currently holds. A successful client read applies the robot's negotiated
+types to an output package; ``init()`` alone does not type application-owned packages.
+``createInputDataPackage()`` returns an input package with the negotiated types already applied.
+On a recipe-only package, ``setData()`` establishes an untyped field's type from the value written;
+subsequent writes must match that type. An untouched field has no type. 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 field has no type 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 +215,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;
}
@@ -121,12 +231,58 @@ RTDEWriter
The ``RTDEWriter`` class provides an interface to write data to the RTDE interface. Data fields that
should be written have to be defined inside the ``INPUT_RECIPE`` as noted above.
-The class offers specific methods for every RTDE input possible to write.
+.. important::
+
+ **Use RTDEClient to initialize and access RTDEWriter (recommended).** Create an ``RTDEClient``
+ with a non-empty input recipe, call ``init()``, then use its writer through ``getWriter()``.
+ The client handles the connection, protocol negotiation, input recipe setup and writer
+ initialization, including the field types and recipe ID reported by the robot.
+
+ Constructing and using ``RTDEWriter`` directly is still supported, but is a lower-level option
+ for applications that manage the RTDE connection and handshake themselves. It is not the
+ recommended approach for normal application code.
+
+The class offers convenience methods for common inputs and ``sendPackage()`` for a complete input
+recipe.
+
+Data is sent asynchronously to the RTDE interface. A successful ``sendPackage()`` or ``send...()``
+call updates the pending send buffer and notifies the writer thread; it does not confirm delivery
+or processing by the robot. This is not a FIFO queue of calls: multiple updates before the writer
+consumes the pending buffer can be coalesced, and a later ``sendPackage()`` can replace an earlier
+pending package. Separate helper calls may be transmitted separately or coalesced, depending on
+when the writer runs. Use ``sendPackage()`` to submit related fields together in one buffer update,
+not to guarantee a distinct transmission for every call.
+
+To write several fields at once, ask the client for a package that already carries the data types
+the robot reported for the input recipe. Call ``createInputDataPackage()`` after a successful
+``init()`` with a non-empty input recipe, fill the fields you care about and pass it to
+``sendPackage()``. The new package starts with zero values; when reusing it, fields retain their
+previous values unless explicitly changed or reset. Because the package is already typed,
+``setData()`` reports a value written with the wrong type immediately:
+
+.. code-block:: c++
+
+ rtde_interface::DataPackage input_pkg = my_client.createInputDataPackage();
+ input_pkg.setData("speed_slider_mask", uint32_t{ 1 });
+ input_pkg.setData("speed_slider_fraction", 0.5);
+ my_client.getWriter().sendPackage(input_pkg);
+
+A package constructed from ``getInputRecipe()`` still works. Its types are taken from the values
+written to it and are checked when the package is submitted to ``sendPackage()``. The field names
+and order must match the negotiated input recipe. Fields that remain untyped are sent as typed
+zeros, while incompatible types cause ``sendPackage()`` to return ``false``. See the
+:ref:`rtde_roundtrip_example` for a complete example.
-Data is sent asynchronously to the RTDE interface.
+If direct ``RTDEWriter`` use is required instead of the recommended ``RTDEClient`` flow, perform the RTDE handshake
+and configure the stopped writer with ``setProtocolVersion(negotiated_version)`` and
+``setRecipeTypes(acknowledged_types)`` before calling ``init(recipe_id)`` with the acknowledged
+input recipe ID. Constructing the writer or calling ``init(recipe_id)`` alone does not establish
+the field types. ``RTDEClient::init()`` handles these steps automatically.
.. note::
The ``RTDEWriter`` will return ``false`` on any writing attempts for fields that have not been
setup in the ``INPUT_RECIPE``. When no input recipe was provided, all write operations will
- return ``false``.
+ return ``false``. No writer thread is started in that case, and ``createInputDataPackage()``
+ throws ``UrException`` even after successful client initialization. The factory also throws
+ before input recipe negotiation or while the writer is stopped.
diff --git a/doc/examples.rst b/doc/examples.rst
index fae5a85e9..c401fbc4d 100644
--- a/doc/examples.rst
+++ b/doc/examples.rst
@@ -23,6 +23,7 @@ may be running forever until manually stopped.
examples/primary_pipeline
examples/primary_pipeline_calibration
examples/rtde_client
+ examples/rtde_roundtrip
examples/external_fts_through_rtde
examples/script_command_interface
examples/script_sender
diff --git a/doc/examples/rtde_client.rst b/doc/examples/rtde_client.rst
index 5339e7198..dd1586974 100644
--- a/doc/examples/rtde_client.rst
+++ b/doc/examples/rtde_client.rst
@@ -56,15 +56,20 @@ 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
+The loop reuses a package built from the negotiated output recipe, keeping the normal data receive
+path allocation-free. Null pointers allocate a package, and foreign-recipe repair may allocate;
+error handling, non-data messages and reconnection are outside this guarantee. The recipe only
+names the fields, so the first read applies their negotiated types in place without allocation.
+
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. If the type of the passed
+ variable doesn't match the type the robot reported for that field, it throws
+ ``std::bad_variant_access``.
Writing Data to the RTDE client
-------------------------------
@@ -91,7 +96,9 @@ initialize the RTDE client has to contain the keys necessary to send that specif
`_
for more information.
-.. note:: Every ``send...`` call to the RTDEWriter triggers a package sent to the robot. If you
- want to modify more than one input at a time, it is recommended to use the ``sendPackage()``
- method. That allows setting up the complete data package with its input recipe and sending that
- to the robot at once.
+.. note:: Every successful ``send...`` call updates the pending buffer and notifies the writer
+ thread. Calls may be coalesced before transmission; they are not queued as separate packages.
+ To submit several inputs together, use ``createInputDataPackage()`` after ``init()``, fill the
+ fields and pass the package to ``sendPackage()``. Separate helper calls can otherwise be
+ transmitted between updates. Neither API confirms delivery to the robot; see the
+ :ref:`rtde_roundtrip_example` for verification using robot outputs.
diff --git a/doc/examples/rtde_roundtrip.rst b/doc/examples/rtde_roundtrip.rst
new file mode 100644
index 000000000..e095a4622
--- /dev/null
+++ b/doc/examples/rtde_roundtrip.rst
@@ -0,0 +1,193 @@
+:github_url: https://github.com/UniversalRobots/Universal_Robots_Client_Library/blob/master/doc/examples/rtde_roundtrip.rst
+
+.. _rtde_roundtrip_example:
+
+RTDE register round-trip example
+================================
+
+This example shows how to write several `Real-Time Data Exchange (RTDE)
+`_
+inputs to the robot in a single package, at the robot's maximum frequency, and how to prove that
+the robot processed them.
+
+The ``send...()`` helpers on ``RTDEWriter`` update individual inputs and notify the asynchronous
+writer separately. When several general purpose registers have to change together,
+``sendPackage()`` submits them together in one pending-buffer update, avoiding a transmission
+between separate helper calls. It does not guarantee a separate transmission for every call.
+
+The example's source code can be found in `rtde_roundtrip.cpp
+`_.
+
+.. note:: The robot has to be powered on and, on an e-Series, in *remote control mode* for the
+ register-processing program to be accepted.
+
+Recipes as argument lists
+-------------------------
+
+``RTDEClient`` takes the input and output recipes as two lists of field names. Recipe files work
+as well; see :ref:`rtde_client_example`. ``timestamp`` is part of the output recipe either way,
+because the client adds it if it is missing.
+
+The general purpose register ranges reserved for external RTDE clients are bit registers
+``64..127`` and integer and double registers ``24..47``.
+
+.. literalinclude:: ../../examples/rtde_roundtrip.cpp
+ :language: c++
+ :caption: examples/rtde_roundtrip.cpp
+ :linenos:
+ :lineno-match:
+ :start-at: // We write the inputs, the robot program below writes the outputs.
+ :end-at: OUTPUT_DOUBLE_REGISTER };
+
+.. note:: Register fields, unlike the digital and analog outputs and the speed slider, need no
+ companion ``_mask`` key in the input recipe.
+
+Processing the registers on the robot
+-------------------------------------
+
+Input registers cannot be written from URScript, and output registers cannot be written through
+RTDE. Getting values back therefore requires a program on the robot.
+
+The program does not copy the values. RTDE also exposes the input registers as outputs, so a
+plain echo would be indistinguishable from that read-back. Instead the program inverts the bit,
+adds one to the integer and negates the double. A value that satisfies those relations can only
+have been produced by this program. ``sync()`` runs the loop once per control cycle.
+
+``sendScript()`` is used rather than ``sendScriptBlocking()``, because the latter would wait until
+the program stops, and this one loops forever.
+
+.. literalinclude:: ../../examples/rtde_roundtrip.cpp
+ :language: c++
+ :caption: examples/rtde_roundtrip.cpp
+ :linenos:
+ :lineno-match:
+ :start-at: const std::string MIRROR_PROGRAM
+ :end-at: end)";
+
+.. literalinclude:: ../../examples/rtde_roundtrip.cpp
+ :language: c++
+ :caption: examples/rtde_roundtrip.cpp
+ :linenos:
+ :lineno-match:
+ :start-at: // Start the robot program that processes the registers
+ :end-at: // The program keeps running until we stop it later.
+
+An input package with the robot's field types
+---------------------------------------------
+
+The data types of the input recipe belong to the robot and arrive with the handshake, so the
+package has to be created after ``init()``. ``createInputDataPackage()`` returns a zeroed package
+that already carries those types: ``setData()`` then rejects a wrong type immediately, and
+copying the package into the send buffer is a single memcpy.
+
+A package constructed from ``getInputRecipe()`` still works. Its types are taken from the values
+written to it and are only checked when the package is sent.
+
+.. literalinclude:: ../../examples/rtde_roundtrip.cpp
+ :language: c++
+ :caption: examples/rtde_roundtrip.cpp
+ :linenos:
+ :lineno-match:
+ :start-at: // RTDE client at the robot's maximum frequency
+ :end-at: my_client.start(false);
+
+``target_frequency = 0.0`` (the default) requests the robot's maximum: 125 Hz on CB3, 500 Hz on
+e-Series. See :ref:`real time setup` and :ref:`rtde_client`.
+
+Both ``DataPackage`` objects are allocated before the loop, so the normal RTDE data receive and
+submission paths reuse their storage without allocation. This does not extend to logging, error
+handling or reconnection. The output package is built from ``getOutputRecipe()`` and is therefore
+still untyped; the first read applies the robot's types to it in place, without allocation.
+
+Letting the robot pace the loop
+-------------------------------
+
+``start(false)`` leaves the background read thread off. ``getDataPackageBlocking()`` returns once
+per RTDE cycle and is this loop's time base. The input package is produced immediately after the
+read so it reaches the robot in time to be acted on in the next cycle. Printing is throttled to
+about once per second, so it stays out of the hot path.
+
+.. literalinclude:: ../../examples/rtde_roundtrip.cpp
+ :language: c++
+ :caption: examples/rtde_roundtrip.cpp
+ :linenos:
+ :lineno-match:
+ :start-at: // The blocking read is this loop's clock
+ :end-at: URCL_LOG_ERROR("Could not get a fresh data package from the robot.");
+
+Writing several inputs in one package
+-------------------------------------
+
+The newly created input package contains zeros. Reusing it preserves previously written values
+unless they are changed or reset. ``sendPackage()`` copies all fields into the pending buffer and
+notifies the writer thread without waiting for transmission. Multiple calls before the writer
+consumes that buffer can be coalesced, with a later package replacing an earlier pending one.
+Separate ``send...()`` calls may be transmitted separately or coalesced; they are not an atomic
+update of several fields. A successful return means the buffer update was accepted, not that the
+robot received or processed it.
+
+Without a run duration the loop runs until it is interrupted, so the counter wraps at one million
+rather than growing past what an integer register can hold. Every answer carries the counter value
+it belongs to, so verification is unaffected, and the lag is measured modulo the same period.
+
+.. literalinclude:: ../../examples/rtde_roundtrip.cpp
+ :language: c++
+ :caption: examples/rtde_roundtrip.cpp
+ :linenos:
+ :lineno-match:
+ :start-at: // Writing several general purpose inputs in one package
+ :end-at: URCL_LOG_ERROR("Sending RTDE data failed.");
+
+Verifying that the robot processed the data
+-------------------------------------------
+
+All three values sent in a cycle are derived from the cycle counter, so the integer the robot
+returns identifies which cycle an answer belongs to. ``echoed_int - 1`` is that counter. The
+expected bit is its inversion and the expected double is the negated sine. The robot's double
+register is a 64-bit value, so the negated sine comes back bit for bit and is compared exactly.
+Together with the inverted bit, that is what makes an answer attributable to this program rather
+than to RTDE's own read-back of the input registers.
+
+Against URSim the lag is one cycle: the values written after the read of cycle N are processed by
+the robot and observed in the read of cycle N+1. ``getData()`` needs a variable of the field's own
+type; ``getDataType()`` reports that type if the recipe is not known in advance.
+
+.. literalinclude:: ../../examples/rtde_roundtrip.cpp
+ :language: c++
+ :caption: examples/rtde_roundtrip.cpp
+ :linenos:
+ :lineno-match:
+ :start-at: // Reading what the robot made of the previous package
+ :end-at: ++mismatches;
+
+Cleanup
+-------
+
+The input registers are reset and the robot program is stopped. A failed stop is only logged,
+because CI runs the example for one second and still requires exit code 0.
+
+.. literalinclude:: ../../examples/rtde_roundtrip.cpp
+ :language: c++
+ :caption: examples/rtde_roundtrip.cpp
+ :linenos:
+ :lineno-match:
+ :start-at: // Reset the input registers before leaving
+ :end-at: return 0;
+
+Example output
+--------------
+
+The following shows a run against URSim 5.25.1 asking for 500 Hz. The echoed integer trails the
+sent integer by one cycle, and ``verified=1`` means the bit and the double match the
+transformations the robot program applies to that cycle.
+
+.. code::
+
+ [INFO] RTDE target frequency: 500.000000 Hz
+ sent: bit=1 int=484 double=-0.991869 | robot: bit=0 int=483 double=0.994216 | verified=1 lag_cycles=1 freq=483.063 Hz playing=1
+ sent: bit=0 int=967 double=-0.242772 | robot: bit=1 int=966 double=0.223323 | verified=1 lag_cycles=1 freq=482.826 Hz playing=1
+ sent: bit=1 int=1450 double=0.934895 | robot: bit=0 int=1449 double=-0.941806 | verified=1 lag_cycles=1 freq=482.669 Hz playing=1
+ [INFO] Cycles: 1931, average frequency: 482.628400 Hz, verified: 1929, mismatches: 0, last lag: 1 cycles
+
+A simulator shares the host's CPU, so the measured frequency stays somewhat below the requested
+one; on a real controller it tracks the target closely.
diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt
index 10007e75f..fe8bb0b2b 100644
--- a/examples/CMakeLists.txt
+++ b/examples/CMakeLists.txt
@@ -23,6 +23,10 @@ add_executable(rtde_client_example
rtde_client.cpp)
target_link_libraries(rtde_client_example ur_client_library::urcl)
+add_executable(rtde_roundtrip_example
+ rtde_roundtrip.cpp)
+target_link_libraries(rtde_roundtrip_example ur_client_library::urcl)
+
add_executable(dashboard_example
dashboard_example.cpp)
target_link_libraries(dashboard_example ur_client_library::urcl)
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/examples/rtde_roundtrip.cpp b/examples/rtde_roundtrip.cpp
new file mode 100644
index 000000000..d919122d8
--- /dev/null
+++ b/examples/rtde_roundtrip.cpp
@@ -0,0 +1,247 @@
+// -- 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 ------------------------------------------------
+
+#include
+#include
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+using namespace urcl;
+
+const std::string DEFAULT_ROBOT_IP = "192.168.56.101";
+
+// We write the inputs, the robot program below writes the outputs.
+const std::string INPUT_BIT_REGISTER = "input_bit_register_64";
+const std::string INPUT_INT_REGISTER = "input_int_register_24";
+const std::string INPUT_DOUBLE_REGISTER = "input_double_register_24";
+const std::string OUTPUT_BIT_REGISTER = "output_bit_register_64";
+const std::string OUTPUT_INT_REGISTER = "output_int_register_24";
+const std::string OUTPUT_DOUBLE_REGISTER = "output_double_register_24";
+
+// RTDE recipes as argument lists, so this example needs no recipe files. RTDEClient also takes two
+// filenames instead; see examples/rtde_client.cpp. The general purpose register ranges used here
+// are the ones the RTDE guide reserves for external clients: bit registers 64..127, integer and
+// double registers 24..47. Register fields need no companion "_mask" key.
+const std::vector INPUT_RECIPE = { INPUT_BIT_REGISTER, INPUT_INT_REGISTER, INPUT_DOUBLE_REGISTER };
+const std::vector OUTPUT_RECIPE = { "timestamp", "runtime_state", OUTPUT_BIT_REGISTER, OUTPUT_INT_REGISTER,
+ OUTPUT_DOUBLE_REGISTER };
+
+// All three values we send are derived from the cycle counter, so the integer the robot returns
+// identifies which cycle an answer belongs to.
+const double SINE_INCREMENT = 0.01; // rad per cycle
+
+// Without a run duration the loop below never ends, so the counter wraps instead of growing until
+// it overflows. One is the lowest value the "has the robot processed anything yet" check accepts,
+// and the robot program adds one to whatever it receives, so the sent value stays in
+// [1, COUNTER_WRAP] and the echoed one in [2, COUNTER_WRAP + 1].
+const int32_t COUNTER_WRAP = 1000000;
+
+// Robot program processing the general purpose inputs and writing the results to the outputs.
+// Input registers cannot be written from URScript and output registers cannot be written through
+// RTDE, so getting values back requires a program on the robot. The program does not copy the
+// values: it inverts the bit, adds one to the integer and negates the double. RTDE also offers the
+// input registers as outputs, so a copy would be indistinguishable from that read-back, while a
+// value satisfying this relation can only have been produced by this program. sync() runs the loop
+// once per control cycle.
+const std::string MIRROR_PROGRAM = R"(def rtde_register_mirror():
+ while (True):
+ write_output_boolean_register(64, not read_input_boolean_register(64))
+ write_output_integer_register(24, read_input_integer_register(24) + 1)
+ write_output_float_register(24, -1.0 * read_input_float_register(24))
+ sync()
+ end
+end)";
+
+int main(int argc, char* argv[])
+{
+ // Parse the ip arguments if given
+ std::string robot_ip = DEFAULT_ROBOT_IP;
+ if (argc > 1)
+ {
+ robot_ip = std::string(argv[1]);
+ }
+
+ // Parse how many seconds to run
+ int second_to_run = -1;
+ if (argc > 2)
+ {
+ second_to_run = std::stoi(argv[2]);
+ }
+
+ comm::INotifier notifier;
+
+ // Start the robot program that processes the registers
+ primary_interface::PrimaryClient primary_client(robot_ip, notifier);
+ primary_client.start();
+ try
+ {
+ primary_client.commandBrakeRelease();
+ }
+ catch (const UrException& e)
+ {
+ URCL_LOG_WARN("Could not release the brakes: %s", e.what());
+ }
+ if (!primary_client.sendScript(MIRROR_PROGRAM))
+ {
+ URCL_LOG_WARN("Could not upload the register-processing program. Output registers will stay at "
+ "zero until a matching program is running on the robot.");
+ }
+ // The program keeps running until we stop it later.
+
+ // RTDE client at the robot's maximum frequency
+ rtde_interface::RTDEClient my_client(robot_ip, notifier, OUTPUT_RECIPE, INPUT_RECIPE);
+ my_client.init();
+ URCL_LOG_INFO("RTDE target frequency: %f Hz", my_client.getTargetFrequency());
+
+ // An input package carrying the data types the robot reported for the input recipe. Those types
+ // are only known once the RTDE handshake has run, which is why this is created after init().
+ rtde_interface::DataPackage input_pkg = my_client.createInputDataPackage();
+ // The output package is still untyped; the first read applies the robot's types to it in place.
+ auto output_pkg = std::make_unique(my_client.getOutputRecipe());
+
+ my_client.start(false);
+
+ int32_t counter = 0;
+ // The counter wraps, so the frequency report needs its own count of the cycles that ran.
+ size_t cycles = 0;
+ size_t verified = 0;
+ size_t mismatches = 0;
+ int32_t last_lag_cycles = 0;
+ auto start_time = std::chrono::steady_clock::now();
+ auto last_print = start_time;
+
+ while (second_to_run <= 0 ||
+ std::chrono::duration_cast(std::chrono::steady_clock::now() - start_time).count() <
+ second_to_run)
+ {
+ // The blocking read is this loop's clock
+ if (!my_client.getDataPackageBlocking(output_pkg))
+ {
+ URCL_LOG_ERROR("Could not get a fresh data package from the robot.");
+ return 1;
+ }
+ const auto now = std::chrono::steady_clock::now();
+
+ // Reading what the robot made of the previous package
+ bool echoed_bit = false;
+ int32_t echoed_int = 0;
+ double echoed_double = 0.0;
+ uint32_t runtime_state = 0;
+ if (!output_pkg->getData(OUTPUT_BIT_REGISTER, echoed_bit) ||
+ !output_pkg->getData(OUTPUT_INT_REGISTER, echoed_int) ||
+ !output_pkg->getData(OUTPUT_DOUBLE_REGISTER, echoed_double) ||
+ !output_pkg->getData("runtime_state", runtime_state))
+ {
+ URCL_LOG_ERROR("Could not read the output registers from the received package.");
+ return 1;
+ }
+
+ bool verified_this_cycle = false;
+ if (echoed_int > 1)
+ {
+ const int32_t origin = echoed_int - 1; // the counter value the robot processed
+ const bool expected_bit = !((origin % 2) == 0);
+ const double expected_double = -std::sin(origin * SINE_INCREMENT);
+ // Taken modulo the wrap period, so a lag measured across a wrap is still a small number.
+ last_lag_cycles = (counter - origin + COUNTER_WRAP) % COUNTER_WRAP;
+ // The robot's double register is a 64-bit value, so the negated sine has to come back bit
+ // for bit. Together with the inverted bit that is the proof the robot processed this cycle.
+ if (echoed_bit == expected_bit && echoed_double == expected_double)
+ {
+ ++verified;
+ verified_this_cycle = true;
+ }
+ else
+ {
+ ++mismatches;
+ }
+ }
+
+ // Writing several general purpose inputs in one package
+ ++cycles;
+ counter = counter % COUNTER_WRAP + 1;
+ const bool sent_bit = (counter % 2) == 0;
+ const double sent_double = std::sin(counter * SINE_INCREMENT);
+ bool write_ok = input_pkg.setData(INPUT_BIT_REGISTER, sent_bit);
+ write_ok = write_ok && input_pkg.setData(INPUT_INT_REGISTER, counter);
+ write_ok = write_ok && input_pkg.setData(INPUT_DOUBLE_REGISTER, sent_double);
+ if (!write_ok || !my_client.getWriter().sendPackage(input_pkg))
+ {
+ URCL_LOG_ERROR("Sending RTDE data failed.");
+ return 1;
+ }
+
+ if (now - last_print >= std::chrono::seconds(1))
+ {
+ const double elapsed_s = std::chrono::duration(now - start_time).count();
+ const double measured_hz = elapsed_s > 0.0 ? static_cast(cycles) / elapsed_s : 0.0;
+ const bool program_playing =
+ static_cast(runtime_state) == rtde_interface::RUNTIME_STATE::PLAYING;
+ std::cout << "sent: bit=" << sent_bit << " int=" << counter << " double=" << sent_double
+ << " | robot: bit=" << echoed_bit << " int=" << echoed_int << " double=" << echoed_double
+ << " | verified=" << verified_this_cycle << " lag_cycles=" << last_lag_cycles << " freq=" << measured_hz
+ << " Hz playing=" << program_playing << std::endl;
+ if (echoed_int == 0)
+ {
+ std::cout << "No processed values yet. Is the register-processing program running on the robot?" << std::endl;
+ }
+ last_print = now;
+ }
+ }
+
+ const double elapsed_s = std::chrono::duration(std::chrono::steady_clock::now() - start_time).count();
+ const double average_hz = elapsed_s > 0.0 ? static_cast(cycles) / elapsed_s : 0.0;
+ URCL_LOG_INFO("Cycles: %zu, average frequency: %f Hz, verified: %zu, mismatches: %zu, last lag: %d cycles", cycles,
+ average_hz, verified, mismatches, last_lag_cycles);
+
+ // Reset the input registers before leaving
+ input_pkg.setData(INPUT_BIT_REGISTER, false);
+ input_pkg.setData(INPUT_INT_REGISTER, static_cast(0));
+ input_pkg.setData(INPUT_DOUBLE_REGISTER, 0.0);
+ my_client.getWriter().sendPackage(input_pkg);
+ std::this_thread::sleep_for(std::chrono::milliseconds(100));
+ try
+ {
+ primary_client.commandStop(false);
+ }
+ catch (const UrException& e)
+ {
+ URCL_LOG_WARN("Could not stop the robot program: %s", e.what());
+ }
+
+ return 0;
+}
diff --git a/include/ur_client_library/comm/producer.h b/include/ur_client_library/comm/producer.h
index 5843f57f7..644050579 100644
--- a/include/ur_client_library/comm/producer.h
+++ b/include/ur_client_library/comm/producer.h
@@ -47,8 +47,8 @@ class URProducer : public IProducer
bool running_;
- template
- bool tryGetImpl(ProductT& product)
+ template
+ bool tryGetImpl(ParseFrame&& parse_frame)
{
// TODO This function has become really ugly! That should be refactored!
@@ -63,11 +63,14 @@ class URProducer : public IProducer
// reset sleep amount
timeout_ = std::chrono::seconds(1);
BinParser bp(buf, read);
- return parser_.parse(bp, product);
+ return parse_frame(bp);
}
if (!running_)
+ {
+ URCL_LOG_DEBUG("Cannot receive a package: producer is stopped.");
return false;
+ }
const SocketState state = stream_.getState();
@@ -96,7 +99,10 @@ class URProducer : public IProducer
}
if (stream_.closed() || stream_.stopRequested())
+ {
+ URCL_LOG_DEBUG("Cannot receive a package: stream is closing or stopped.");
return false;
+ }
if (on_reconnect_cb_)
{
@@ -120,7 +126,10 @@ class URProducer : public IProducer
}
if (!running_ || stream_.closed() || stream_.stopRequested())
+ {
+ URCL_LOG_DEBUG("Package receive cancelled during reconnect backoff.");
return false;
+ }
if (stream_.reconnect())
continue;
@@ -129,8 +138,6 @@ class URProducer : public IProducer
if (next <= std::chrono::seconds(120))
timeout_ = next;
}
-
- return false;
}
public:
@@ -192,7 +199,7 @@ class URProducer : public IProducer
*/
bool tryGet(std::vector>& products) override
{
- return tryGetImpl(products);
+ return tryGetImpl([this, &products](BinParser& bp) { return parser_.parse(bp, products); });
}
/*!
@@ -207,7 +214,18 @@ class URProducer : public IProducer
*/
bool tryGet(std::unique_ptr& product) override
{
- return tryGetImpl(product);
+ return tryGetImpl([this, &product](BinParser& bp) { return parser_.parse(bp, product); });
+ }
+
+ /*!
+ * \brief Receives one frame using the existing read/reconnect loop and a custom parser.
+ * The callable is invoked synchronously and is never stored. It must return whether parsing
+ * succeeded, and diagnose failures. This allows parsing into borrowed, caller-owned storage.
+ */
+ template
+ bool tryGetWithParser(ParseFrame&& parse_frame)
+ {
+ return tryGetImpl(parse_frame);
}
/*!
diff --git a/include/ur_client_library/primary/package_header.h b/include/ur_client_library/primary/package_header.h
index 459d13580..bbca5cc6f 100644
--- a/include/ur_client_library/primary/package_header.h
+++ b/include/ur_client_library/primary/package_header.h
@@ -32,6 +32,7 @@
#include
#include
+#include
#include
#include "ur_client_library/types.h"
@@ -76,7 +77,11 @@ class PackageHeader
*/
static size_t getPackageLength(uint8_t* buf)
{
- return be32toh(*(reinterpret_cast<_package_size_type*>(buf)));
+ // Copy the bytes out instead of casting: buf can point anywhere inside a byte stream, and an
+ // unaligned _package_size_type access is undefined behavior.
+ _package_size_type package_size;
+ std::memcpy(&package_size, buf, sizeof(package_size));
+ return be32toh(package_size);
}
};
} // namespace primary_interface
diff --git a/include/ur_client_library/primary/primary_client.h b/include/ur_client_library/primary/primary_client.h
index 8440a9ff8..4d9d63098 100644
--- a/include/ur_client_library/primary/primary_client.h
+++ b/include/ur_client_library/primary/primary_client.h
@@ -66,7 +66,14 @@ class PrimaryClient
{
public:
PrimaryClient() = delete;
- PrimaryClient(const std::string& robot_ip, comm::INotifier& notifier);
+ /*!
+ * \brief Creates a new PrimaryClient object.
+ *
+ * \param robot_ip The IP of the robot
+ * \param notifier The notifier to notify of start and stop events
+ * \param port Optionally specify a different port
+ */
+ PrimaryClient(const std::string& robot_ip, comm::INotifier& notifier, const int port = UR_PRIMARY_PORT);
~PrimaryClient();
/*!
diff --git a/include/ur_client_library/rtde/data_package.h b/include/ur_client_library/rtde/data_package.h
index 3a5d69b1b..3046ec091 100644
--- a/include/ur_client_library/rtde/data_package.h
+++ b/include/ur_client_library/rtde/data_package.h
@@ -29,11 +29,18 @@
#ifndef UR_CLIENT_LIBRARY_DATA_PACKAGE_H_INCLUDED
#define UR_CLIENT_LIBRARY_DATA_PACKAGE_H_INCLUDED
-#include
+#include
+#include
+#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"
@@ -54,64 +61,179 @@ 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() throws std::bad_variant_access.
*/
class DataPackage : public RTDEPackage
{
public:
- using _rtde_type_variant = std::variant;
+ /*!
+ * \brief The type a data field can hold.
+ *
+ * The typed alternatives are exactly the members of DataType. 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.
+ */
+ using _rtde_type_variant = std::variant;
- DataPackage() = delete;
+ // A data package is created before the connection exists and then retyped in place from the
+ // robot's acknowledgement, so no alternative may own heap memory: retyping has to stay a
+ // write into the variant's inline storage.
+ static_assert(std::is_trivially_copyable_v<_rtde_type_variant>, "An RTDE data field must not own heap memory.");
- DataPackage(const DataPackage& other) : DataPackage(other.recipe_)
- {
- this->data_ = other.data_;
- this->protocol_version_ = other.protocol_version_;
- }
+ DataPackage() = delete;
- DataPackage& operator=(DataPackage& other)
+ DataPackage(const DataPackage& other)
+ : RTDEPackage(PackageType::RTDE_DATA_PACKAGE)
+ , recipe_id_(other.recipe_id_)
+ , recipe_(other.recipe_)
+ , values_(other.values_)
+ , zeros_(other.zeros_)
+ , protocol_version_(other.protocol_version_)
+ , recipe_hash_(other.recipe_hash_)
+ , layout_hash_(other.layout_hash_)
+ , fully_typed_(other.fully_typed_)
{
- this->data_ = other.data_;
- this->recipe_ = other.recipe_;
- this->protocol_version_ = other.protocol_version_;
- return *this;
+ rebuildFieldIndex();
}
- 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_;
+ // The name-to-index map holds string_views into recipe_. Replacing recipe_ would dangle those
+ // views and would allocate, so a same-recipe assignment (the receive path) leaves both alone.
+ if (recipe_hash_ != other.recipe_hash_ || recipe_.size() != other.recipe_.size())
+ {
+ this->recipe_ = other.recipe_;
+ this->recipe_hash_ = other.recipe_hash_;
+ rebuildFieldIndex();
+ }
+ this->values_ = other.values_;
+ this->zeros_ = other.zeros_;
this->protocol_version_ = other.protocol_version_;
+ this->layout_hash_ = other.layout_hash_;
+ this->fully_typed_ = other.fully_typed_;
return *this;
}
/*!
- * \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. Reusing it for normal client reads with the same recipe
+ * needs no further allocation; foreign-recipe assignment or repair may allocate. Construction
+ * needs no connection, but using getOutputRecipe() after init() accounts for unavailable fields
+ * removed during negotiation.
+ *
+ * \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;
+ virtual ~DataPackage();
/*!
- * \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 A package with this one's recipe, protocol version and data types, all values zero.
+ *
+ * Since it carries the same field names and types, it has this package's layout hash and can be
+ * copied into it with a single memcpy; see copyFrom().
+ *
+ * \returns A zeroed package with this package's layout
+ */
+ DataPackage emptyCopy() const;
+
+ /*!
+ * \brief Get the data type a field currently holds.
+ *
+ * After a client read or setTypes() applies the negotiated types, this is the type the robot
+ * reported. createInputDataPackage() also returns a package with those types already applied.
+ * The handshake alone does not type application-owned packages. On a recipe-only package,
+ * setData() can establish a field's type; this reports that stored type, which sendPackage()
+ * still checks against the robot. An untouched field has no type yet.
+ *
+ * \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.
*
+ * The payload is the bytes after the package header. Version 2 data packages start with a
+ * recipe-id byte; version 1 packages do not. That is the same layout serializePackage() writes
+ * after the header.
+ *
* \param bp A parser containing a serialized version of the package
*
* \returns True, if the package was parsed successfully, false otherwise
@@ -127,6 +249,9 @@ class DataPackage : public RTDEPackage
/*!
* \brief Serializes the package.
*
+ * Version 2 data packages start with a recipe-id byte; version 1 packages do not. The writer
+ * records the negotiated version with setProtocolVersion() before serializing.
+ *
* \param buffer Buffer to fill with the serialization
*
* \returns The total size of the serialized package
@@ -142,22 +267,19 @@ class DataPackage : public RTDEPackage
* \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.
+ *
+ * \throws std::bad_variant_access if the field is present but does not hold T, including when
+ * the field has not been typed yet.
*/
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())
- {
- val = std::get(it->second);
- }
- else
+ const std::optional index = fieldIndex(name);
+ if (!index.has_value())
{
return false;
}
+ val = std::get(values_[*index]);
return true;
}
@@ -170,24 +292,20 @@ class DataPackage : public RTDEPackage
* \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.
+ *
+ * \throws std::bad_variant_access if the field is present but the underlying type is not T,
+ * including when the field has not been typed yet.
*/
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");
-
- const auto it =
- std::find_if(data_.begin(), data_.end(), [&name](const std::pair& element) {
- return element.first == name;
- });
- if (it != data_.end())
- {
- val = std::bitset(std::get(it->second));
- }
- else
+ T recipe_type;
+ if (!getData(name, recipe_type))
{
return false;
}
+ val = std::bitset(recipe_type);
return true;
}
@@ -196,33 +314,40 @@ 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())
+ const std::optional index = fieldIndex(name);
+ if (!index.has_value())
{
- 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
+ _rtde_type_variant& field = values_[*index];
+ if (!std::holds_alternative(field) && !std::holds_alternative(field))
{
+ // 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;
}
+ const bool type_changed = std::holds_alternative(field);
+ field = val;
+ if (type_changed)
+ {
+ zeros_[*index] = T();
+ updateLayoutHash();
+ }
return true;
}
@@ -236,13 +361,125 @@ class DataPackage : public RTDEPackage
recipe_id_ = recipe_id;
}
+ /*!
+ * \brief Records the RTDE protocol version this package will serialize.
+ *
+ * Version 2 data packages start with a recipe-id byte; version 1 packages do not. The
+ * constructor defaults to version 2. The layout hash is always recomputed, including when the
+ * package is still untyped or only partially typed.
+ */
+ void setProtocolVersion(const uint16_t protocol_version)
+ {
+ protocol_version_ = protocol_version;
+ updateLayoutHash();
+ }
+
+ /*!
+ * \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.
+ * Every name is checked before any field is written, so a failure leaves the package unchanged.
+ */
+ void setTypes(const std::vector& types);
+
+ /*!
+ * \brief Copies same-recipe values without allocation; unset source fields become typed zeros.
+ * \param other Source package; its typed fields must match this typed destination.
+ * \returns False on recipe/type mismatch or an untyped destination, without changing values, recipe id or version.
+ */
+ bool copyFrom(const DataPackage& other);
+
+ /*!
+ * \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 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
+ * incompletely typed package cannot be parsed into or serialized. getData() throws
+ * std::bad_variant_access for an untyped field, but typed fields can already be read.
+ *
+ * \returns True if the package carries type information for all of its fields
+ */
+ bool isTyped() const
+ {
+ return fully_typed_;
+ }
+
+ /*!
+ * \brief FNV-1a identity of this package's field names, in order.
+ *
+ * Not sent on the wire. Used together with layoutHash() to identify a recipe without comparing
+ * field-name strings.
+ */
+ uint64_t recipeHash() const
+ {
+ return recipe_hash_;
+ }
+
+ /// Exact setup-time comparison of recipe field names and order; does not allocate.
+ bool hasRecipe(const std::vector& recipe) const
+ {
+ return recipe_ == recipe;
+ }
+
+ /*!
+ * \brief FNV-1a identity of this package's protocol version, field names and current variant indices.
+ *
+ * Not sent on the wire. Combined from the recipe hash, protocol version and type of every field,
+ * so it changes when the protocol version or a field's type changes, and does not change when a
+ * value is overwritten, reset or parsed.
+ */
+ uint64_t layoutHash() const
+ {
+ return layout_hash_;
+ }
+
private:
- // Const would be better here
- static std::unordered_map g_type_list;
- uint8_t recipe_id_;
- std::vector> data_;
+ /*!
+ * \brief Allocates one slot per recipe field, with the type left undecided.
+ */
+ void initStorage();
+
+ /*!
+ * \brief Recomputes layout_hash_ and fully_typed_ from the current values.
+ */
+ void updateLayoutHash();
+
+ /*!
+ * \brief Rebuilds the name-to-index map from recipe_.
+ *
+ * The keys are string_views into recipe_, so this must run after recipe_ is in its final place.
+ */
+ void rebuildFieldIndex();
+
+ /*!
+ * \brief The recipe index of \p name, or empty if the name is not in this package.
+ */
+ std::optional fieldIndex(const std::string_view name) const;
+
+ uint8_t recipe_id_ = 0;
std::vector recipe_;
- uint16_t protocol_version_;
+ std::unordered_map field_index_;
+ std::vector<_rtde_type_variant> values_;
+ std::vector<_rtde_type_variant> zeros_;
+ uint16_t protocol_version_ = 2;
+ uint64_t recipe_hash_ = 0;
+ uint64_t layout_hash_ = 0;
+ bool fully_typed_ = false;
};
} // namespace rtde_interface
diff --git a/include/ur_client_library/rtde/package_header.h b/include/ur_client_library/rtde/package_header.h
index e111eab30..7baed9028 100644
--- a/include/ur_client_library/rtde/package_header.h
+++ b/include/ur_client_library/rtde/package_header.h
@@ -31,6 +31,7 @@
#define UR_CLIENT_LIBRARY_RTDE__HEADER_H_INCLUDED
#include
+#include
#include
#include "ur_client_library/types.h"
#include "ur_client_library/comm/package_serializer.h"
@@ -73,7 +74,11 @@ class PackageHeader
*/
static size_t getPackageLength(uint8_t* buf)
{
- return be16toh(*(reinterpret_cast<_package_size_type*>(buf)));
+ // Copy the bytes out instead of casting: buf can point anywhere inside a byte stream, and an
+ // unaligned _package_size_type access is undefined behavior.
+ _package_size_type package_size;
+ std::memcpy(&package_size, buf, sizeof(package_size));
+ return be16toh(package_size);
}
/*!
diff --git a/include/ur_client_library/rtde/rtde_client.h b/include/ur_client_library/rtde/rtde_client.h
index 92d3693a2..7f0bebc21 100644
--- a/include/ur_client_library/rtde/rtde_client.h
+++ b/include/ur_client_library/rtde/rtde_client.h
@@ -29,6 +29,7 @@
#ifndef UR_CLIENT_LIBRARY_RTDE_CLIENT_H_INCLUDED
#define UR_CLIENT_LIBRARY_RTDE_CLIENT_H_INCLUDED
+#include
#include
#include "ur_client_library/comm/producer.h"
@@ -190,11 +191,20 @@ class RTDEClient
* received from the robot can be fetched with this. When no new data has been received since the last call to this
* function, it will wait for the time specified in the \p timeout parameter.
*
+ * This wait can pace an application loop at the negotiated RTDE output frequency. If a newer
+ * sample is already available, the call returns immediately. The background reader keeps only
+ * the latest sample, so intermediate samples may be skipped when the application is slower.
+ * Allow a timeout covering the expected period and scheduling jitter, and check the return value.
+ * For more direct packet-arrival pacing without a background-reader handoff, use start(false)
+ * and getDataPackageBlocking(). Neither mode guarantees phase synchronization with robot control.
+ *
* When packages are not read from the background thread, this function will return false and
* print an error message.
*
* \param data_package Reference to a DataPackage where the received data package will be stored
* if a package was fetched successfully.
+ * Foreign recipes are repaired with a warning; null pointers allocate a package with a warning.
+ * A null pointer is assigned only after a successful read. Stop/reconnect cancels pending reads.
* \param timeout Time to wait if no data package is currently in the queue
*
* \returns Whether a data package was received successfully
@@ -207,11 +217,22 @@ class RTDEClient
*
* This function will block until a new data package is received from the robot and return it.
*
+ * With start(false), calling this at the start of each loop iteration can pace application work
+ * at the negotiated RTDE output frequency: when no data is buffered, packet arrival releases
+ * the wait. Already buffered packages can return immediately, so the application must keep up
+ * with the stream to avoid lag. Network and scheduling jitter still apply; this does not guarantee
+ * phase synchronization with the robot's internal control cycle or next-cycle command delivery.
+ * getDataPackage() with background reading can also pace a loop, but favors the latest sample
+ * and decouples socket reading from application work.
+ *
* \param data_package Reference to a unique ptr where the received data package will be stored.
* For optimal performance, the data package pointer should contain a pre-allocated data package
- * that was initialized with the same output recipe as used in this RTDEClient. If it is not an
- * initialized data package, a new one will be allocated internally which will have a negative
- * performance impact and print a warning.
+ * that was built from the same output recipe as used in this RTDEClient. Such a package needs no
+ * data types of its own: the first read applies the ones the robot reported, which allocates
+ * nothing. Use getOutputRecipe() for the recipe; foreign recipes are repaired with a warning and may allocate.
+ * Null pointers allocate a package with a warning.
+ * The caller retains ownership on failure; null pointers are assigned only on success.
+ * Malformed data may partially update an existing package's values before failure.
*
* \returns Whether a data package was received successfully
*/
@@ -282,13 +303,29 @@ class RTDEClient
return input_recipe_;
}
+ /*!
+ * \brief Creates a data package for the input recipe, carrying the data types the robot reported
+ * during the RTDE handshake.
+ *
+ * Fill it with DataPackage::setData() and hand it to RTDEWriter::sendPackage() to write several
+ * inputs in a single package. Has to be called after successful init() with a non-empty input
+ * recipe. The new package contains zeros; reusing it retains previously written values.
+ *
+ * \throws UrException if the writer is stopped or input types have not been negotiated, including
+ * when the client has an empty input recipe.
+ */
+ DataPackage createInputDataPackage()
+ {
+ return writer_.createDataPackage();
+ }
+
/// Reads output or input recipe from a file and parses it into a vector of strings where each
/// string is a line from the file.
static std::vector readRecipe(const std::string& recipe_file);
ClientState getClientState() const
{
- return client_state_;
+ return client_state_.load();
}
/*! \brief Starts a background thread to read data packages from the robot.
@@ -335,10 +372,13 @@ class RTDEClient
std::atomic background_read_running_ = false;
std::thread background_read_thread_;
std::condition_variable background_read_cv_;
+ // Protected by read_mutex_; prevents a waiter from crossing a stop/restart or reconnect.
+ uint64_t background_read_session_id_ = 0;
DataPackage preallocated_data_pkg_;
- ClientState client_state_;
+ // Written by reconnect() on its own thread and read by getClientState() / start / pause.
+ std::atomic client_state_;
uint16_t protocol_version_;
@@ -380,6 +420,15 @@ class RTDEClient
bool sendStart();
bool sendPause();
+ /*!
+ * \brief Repairs foreign or untyped packages by assigning the negotiated output template.
+ */
+ void ensureOutputLayout(DataPackage& data_package, const DataPackage& output_template) const;
+ /*!
+ * \brief Allocates a package with the negotiated output layout if null, or delegates to reference repair.
+ */
+ void ensureOutputLayout(std::unique_ptr& data_package) const;
+
/*!
* \brief Reconnects to the RTDE interface and set the input and output recipes again.
*/
diff --git a/include/ur_client_library/rtde/rtde_parser.h b/include/ur_client_library/rtde/rtde_parser.h
index 9c97827b0..5ded0b103 100644
--- a/include/ur_client_library/rtde/rtde_parser.h
+++ b/include/ur_client_library/rtde/rtde_parser.h
@@ -19,6 +19,7 @@
*/
#pragma once
+#include
#include
#include "ur_client_library/comm/parser.h"
#include "ur_client_library/comm/bin_parser.h"
@@ -49,6 +50,8 @@ class RTDEParser : public comm::Parser
/*!
* \brief Creates a new RTDEParser object, registering the used recipe.
*
+ * Register robot-acknowledged types with setExpectedDataPackage() or setExpectedLayoutHash() before parsing data.
+ *
* \param recipe The recipe used in RTDE data communication
*/
RTDEParser(const std::vector& recipe) : recipe_(recipe), protocol_version_(1)
@@ -63,14 +66,22 @@ class RTDEParser : public comm::Parser
* \param bp A BinParser holding a serialized RTDE package
* \param result A pointer to the created RTDE package object. Ideally, the passed \p result is a pre-allocated
* package of the type expected to be read. For example, when RTDE communication has been setup it enters the data
- * communication phase, where the expected package is a DataPackage. If the package content inside the \p bp object
- * being doesn't match the result package's type or if the \p result is a nullptr, a new package will be allocated.
+ * communication phase, where the expected package is a DataPackage. A DataPackage passed for RTDE data must have
+ * the registered layout hash; a mismatch returns false. Null/non-data pointers require setExpectedDataPackage().
*
* \returns True, if the byte stream could successfully be parsed as an RTDE package, false
* otherwise
*/
bool parse(comm::BinParser& bp, std::unique_ptr& result) override;
+ /*!
+ * \brief Consumes one frame, borrowing the destination without replacing or deleting it.
+ * Returns false for non-data frames or parse failures. Malformed data may partially update
+ * field values; ownership is always retained by the caller. The destination must have the
+ * registered layout. Non-data frames use separate temporary storage.
+ */
+ bool parseDataPackage(comm::BinParser& bp, DataPackage& destination);
+
/*!
* \brief Uses the given BinParser to create package objects from the contained serialization.
*
@@ -84,9 +95,31 @@ class RTDEParser : public comm::Parser
"a pre-allocated package. This function will be removed in May 2027.")]]
bool parse(comm::BinParser& bp, std::vector>& results) override;
+ /*!
+ * \brief Records the RTDE protocol version used for parsing.
+ *
+ * A typed template registered with setExpectedDataPackage() is updated in place. A hash-only
+ * registration from setExpectedLayoutHash() is bound to the protocol version at registration
+ * time: changing the version clears it, and a new hash must be registered before data can be
+ * parsed. Setting the same version is a no-op.
+ */
void setProtocolVersion(uint16_t protocol_version)
{
+ if (protocol_version_ == protocol_version)
+ {
+ return;
+ }
protocol_version_ = protocol_version;
+ if (expected_data_package_.has_value())
+ {
+ expected_data_package_->setProtocolVersion(protocol_version);
+ layout_hash_ = expected_data_package_->layoutHash();
+ }
+ else if (expected_layout_known_)
+ {
+ expected_layout_known_ = false;
+ layout_hash_ = 0;
+ }
}
uint16_t getProtocolVersion() const
@@ -94,8 +127,50 @@ class RTDEParser : public comm::Parser
return protocol_version_;
}
+ /*!
+ * \brief Registers the expected data-package layout 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. The hash is bound to the parser's current protocol
+ * version; changing the version with setProtocolVersion() clears this registration.
+ *
+ * \param layout_hash The layout hash of the acknowledged output recipe
+ */
+ void setExpectedLayoutHash(uint64_t layout_hash)
+ {
+ // Clear any previous template when registering only a hash to enforce strict non-allocating mode.
+ expected_data_package_.reset();
+ layout_hash_ = layout_hash;
+ expected_layout_known_ = true;
+ }
+
+ /// Registers a typed template, enabling allocation for null/non-data pointers and the deprecated vector overload.
+ void setExpectedDataPackage(const DataPackage& data_package)
+ {
+ if (!data_package.isTyped())
+ {
+ throw UrException("The expected RTDE data package must be typed.");
+ }
+ if (!data_package.hasRecipe(recipe_))
+ {
+ throw UrException("The expected RTDE data package must use the parser's recipe.");
+ }
+ expected_data_package_.emplace(data_package);
+ expected_data_package_->setProtocolVersion(protocol_version_);
+ layout_hash_ = expected_data_package_->layoutHash();
+ expected_layout_known_ = true;
+ }
+
private:
+ bool parseDataPackagePayload(comm::BinParser& bp, DataPackage& package) const;
+
std::vector recipe_;
+ // Optional typed template restoring legacy allocation for null pointers and deprecated vector parse.
+ std::optional expected_data_package_;
+ uint64_t layout_hash_ = 0;
+ bool expected_layout_known_ = false;
+ 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..9bd0d615f 100644
--- a/include/ur_client_library/rtde/rtde_writer.h
+++ b/include/ur_client_library/rtde/rtde_writer.h
@@ -69,12 +69,17 @@ class RTDEWriter
* needed.
*
* \param recipe The new recipe to use
+ *
+ * \throws UrException if the writer is already running
*/
void setInputRecipe(const std::vector& recipe);
/*!
- * \brief Starts the writer thread, which periodically clears the queue to write packages to the
- * robot.
+ * \brief Starts the writer thread, which sends pending buffer updates to the robot.
+ *
+ * Apply the negotiated protocol version and input field types with setProtocolVersion() and
+ * setRecipeTypes() while stopped, before calling this method. This method does not negotiate
+ * or establish field types. RTDEClient::init() handles that setup for its writer.
*
* \param recipe_id The recipe id to use, so the robot correctly identifies the used recipe
*/
@@ -92,15 +97,37 @@ class RTDEWriter
/*!
* \brief Sends a complete RTDEPackage to the robot.
*
- * 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.
+ * Use this to submit multiple values together in one pending-buffer update. Separate helper
+ * calls may be transmitted separately or coalesced, depending on when the writer thread runs.
+ * Calls are not queued individually: a later update can replace an earlier pending package.
*
- * \param package The package to send
+ * Every field of \p package is copied into the pending buffer. Field names and order must match
+ * the input recipe the robot acknowledged. Typed fields must match the negotiated types;
+ * fields that are still untyped are copied as typed zeros. A mismatch returns false.
*
- * \returns Success of the package creation
+ * \param package The package to send, constructed from the client's input recipe
+ *
+ * \returns Whether the pending buffer update was accepted, not confirmation of transmission or
+ * processing by the robot.
*/
bool sendPackage(const DataPackage& package);
+ /*!
+ * \brief Creates a data package for the input recipe, carrying the data types the robot reported
+ * for it.
+ *
+ * The returned package has all values at zero and is ready to be filled with
+ * DataPackage::setData(). Since it already carries the robot's types, a value written with a wrong
+ * type is reported by setData() itself rather than only when the package is sent, and copying the
+ * package into the send buffer is a single memcpy.
+ *
+ * \returns A package built from the input recipe with the acknowledged data types applied
+ *
+ * \throws UrException if the writer is stopped or its input buffers are not typed. An RTDEClient
+ * with an empty input recipe does not start its writer, so this also throws for read-only clients.
+ */
+ DataPackage createDataPackage();
+
/*!
* \brief Creates a package to request setting a new value for the speed slider.
*
@@ -192,6 +219,29 @@ class RTDEWriter
*/
bool sendExternalForceTorque(const vector6d_t& external_force_torque);
+ /*!
+ * \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, if a type is unknown, or
+ * if the writer is already running
+ */
+ void setRecipeTypes(const std::vector& types);
+
+ /*!
+ * \brief Records the RTDE protocol version negotiated with the robot.
+ *
+ * Version 2 data packages start with a recipe-id byte; version 1 packages do not. Defaults to
+ * version 2. The client sets this after protocol negotiation.
+ *
+ * \throws UrException if the writer is already running
+ */
+ void setProtocolVersion(uint16_t protocol_version);
+
private:
void resetMasks(const std::shared_ptr& buffer);
void markStorageToBeSent();
@@ -200,6 +250,7 @@ class RTDEWriter
comm::URStream* stream_;
std::vector recipe_;
uint8_t recipe_id_;
+ uint16_t protocol_version_ = 2;
std::shared_ptr data_buffer0_;
std::shared_ptr data_buffer1_;
std::shared_ptr current_store_buffer_;
diff --git a/src/primary/primary_client.cpp b/src/primary/primary_client.cpp
index 68959ecd9..d2a163f28 100644
--- a/src/primary/primary_client.cpp
+++ b/src/primary/primary_client.cpp
@@ -42,8 +42,8 @@ namespace urcl
{
namespace primary_interface
{
-PrimaryClient::PrimaryClient(const std::string& robot_ip, [[maybe_unused]] comm::INotifier& notifier)
- : stream_(robot_ip, UR_PRIMARY_PORT)
+PrimaryClient::PrimaryClient(const std::string& robot_ip, [[maybe_unused]] comm::INotifier& notifier, const int port)
+ : stream_(robot_ip, port)
{
parser_.setStrictMode(COMPILE_OPTIONS.PRIMARY_CLIENT_STRICT_PARSING);
prod_.reset(new comm::URProducer(stream_, parser_));
diff --git a/src/rtde/data_package.cpp b/src/rtde/data_package.cpp
index b2abff24d..ea7025a2f 100644
--- a/src/rtde/data_package.cpp
+++ b/src/rtde/data_package.cpp
@@ -28,464 +28,387 @@
#include "ur_client_library/rtde/data_package.h"
-#include
+#include
+#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() },
-
- // 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() },
-
- // 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() },
-
- // NOT IN OFFICIAL DOCS
- { "tool_digital_output_mask", uint8_t() },
- { "tool_digital_output", uint8_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" },
};
+constexpr uint64_t g_FNV_OFFSET_BASIS = 14695981039346656037ULL;
+constexpr uint64_t g_FNV_PRIME = 1099511628211ULL;
+
+uint64_t fnv1a(uint64_t hash, const uint8_t* data, const size_t length)
+{
+ for (size_t i = 0; i < length; ++i)
+ {
+ hash ^= data[i];
+ hash *= g_FNV_PRIME;
+ }
+ return hash;
+}
+
+uint64_t fnv1aByte(uint64_t hash, const uint8_t value)
+{
+ hash ^= value;
+ hash *= g_FNV_PRIME;
+ return hash;
+}
+
+uint64_t hashRecipe(const std::vector& recipe)
+{
+ uint64_t hash = g_FNV_OFFSET_BASIS;
+ const uint64_t count = recipe.size();
+ hash = fnv1a(hash, reinterpret_cast(&count), sizeof(count));
+ for (const auto& name : recipe)
+ {
+ hash = fnv1a(hash, reinterpret_cast(name.data()), name.size());
+ // A separator so that "ab"+"c" and "a"+"bc" cannot produce the same digest.
+ hash = fnv1aByte(hash, 0);
+ }
+ return hash;
+}
+
+uint64_t hashLayout(const uint64_t recipe_hash, const uint16_t protocol_version,
+ const std::vector& values)
+{
+ uint64_t hash = recipe_hash;
+ hash = fnv1aByte(hash, static_cast(protocol_version));
+ hash = fnv1aByte(hash, static_cast(protocol_version >> 8));
+ for (const auto& value : values)
+ {
+ hash = fnv1aByte(hash, static_cast(value.index()));
+ }
+ return hash;
+}
+
+/*!
+ * \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 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.");
+}
+
+/*!
+ * \brief The protocol 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
+ */
+DataType typeFromName(const std::string_view type_name)
+{
+ for (const auto& entry : g_type_names)
+ {
+ if (entry.name == type_name)
+ {
+ return entry.type;
+ }
+ }
+
+ 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());
+}
+
+void copyValues(std::vector& destination,
+ const std::vector& source)
+{
+ if (destination.empty())
+ {
+ return;
+ }
+ std::memcpy(destination.data(), source.data(), destination.size() * sizeof(DataPackage::_rtde_type_variant));
+}
+} // namespace
+
+std::string toString(const DataType type)
+{
+ for (const auto& entry : g_type_names)
+ {
+ if (entry.type == type)
+ {
+ return std::string(entry.name);
+ }
+ }
+ throw UrException("Unhandled RTDE data type.");
+}
+
+void rtde_interface::DataPackage::rebuildFieldIndex()
+{
+ field_index_.clear();
+ field_index_.reserve(recipe_.size());
+ for (size_t i = 0; i < recipe_.size(); ++i)
+ {
+ field_index_.emplace(recipe_[i], i);
+ }
+}
+
+std::optional rtde_interface::DataPackage::fieldIndex(const std::string_view name) const
+{
+ const auto it = field_index_.find(name);
+ if (it == field_index_.end())
+ {
+ return std::nullopt;
+ }
+ return it->second;
+}
+
+std::optional rtde_interface::DataPackage::getDataType(const std::string_view name) const
+{
+ const std::optional index = fieldIndex(name);
+ if (!index.has_value())
+ {
+ return std::nullopt;
+ }
+ return typeOf(values_[*index]);
+}
+
+void rtde_interface::DataPackage::initStorage()
+{
+ values_.assign(recipe_.size(), std::monostate());
+ zeros_.assign(recipe_.size(), std::monostate());
+ rebuildFieldIndex();
+ recipe_hash_ = hashRecipe(recipe_);
+ updateLayoutHash();
+}
+
+void rtde_interface::DataPackage::updateLayoutHash()
+{
+ layout_hash_ = hashLayout(recipe_hash_, protocol_version_, values_);
+ fully_typed_ = std::none_of(values_.begin(), values_.end(), [](const _rtde_type_variant& field) {
+ return std::holds_alternative(field);
+ });
+}
+
+void rtde_interface::DataPackage::setTypes(const std::vector& types)
+{
+ if (types.size() != recipe_.size())
+ {
+ std::stringstream ss;
+ ss << "Cannot set the data types of an RTDE data package: got " << types.size() << " data types for a recipe with "
+ << recipe_.size() << " fields.";
+ throw UrException(ss.str());
+ }
+
+ // Confirm every name before writing any field. variantFor cannot fail once the name is known, so
+ // a later unknown type cannot leave earlier fields retyped while layout_hash_ still describes
+ // the old layout.
+ for (const auto& type_name : types)
+ {
+ typeFromName(type_name);
+ }
+
+ for (size_t i = 0; i < recipe_.size(); ++i)
+ {
+ values_[i] = variantFor(typeFromName(types[i]));
+ zeros_[i] = values_[i];
+ }
+ updateLayoutHash();
+}
+
void rtde_interface::DataPackage::initEmpty()
{
- data_.clear();
- data_.reserve(recipe_.size());
- for (auto& item : recipe_)
+ copyValues(values_, zeros_);
+}
+
+rtde_interface::DataPackage rtde_interface::DataPackage::emptyCopy() const
+{
+ // The delegated constructor allocates the storage, builds the name-to-index map and computes the
+ // recipe hash; the field types and their zero values are what this package contributes.
+ DataPackage package(recipe_, protocol_version_);
+ package.values_ = zeros_;
+ package.zeros_ = zeros_;
+ package.updateLayoutHash();
+ return package;
+}
+
+bool rtde_interface::DataPackage::copyFrom(const DataPackage& other)
+{
+ if (!isTyped())
+ {
+ URCL_LOG_ERROR("Cannot copy 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;
+ }
+
+ // copyValues() uses memcpy. Overlapping source and destination are undefined, so a package
+ // copied onto itself has to return before that path.
+ if (this == &other)
+ {
+ return true;
+ }
+
+ // Same field names and the same type on every field, so the whole value array can go across at
+ // once. This is the path a real-time loop takes.
+ if (layout_hash_ == other.layout_hash_ && values_.size() == other.values_.size())
+ {
+ copyValues(values_, other.values_);
+ return true;
+ }
+
+ // Backwards compatibility: accept partial input packages where unset fields default to typed zeros.
+ if (recipe_hash_ != other.recipe_hash_ || values_.size() != other.values_.size())
{
- if (g_type_list.find(item) == g_type_list.end())
+ return false;
+ }
+ // Validate every set field against destination types before writing to ensure atomic rejection.
+ for (size_t i = 0; i < values_.size(); ++i)
+ {
+ if (!std::holds_alternative(other.values_[i]) && values_[i].index() != other.values_[i].index())
{
- throw RTDEInvalidKeyException("Unknown item in recipe: " + item);
+ return false;
}
- _rtde_type_variant entry = g_type_list[item];
- data_.push_back({ item, entry });
}
+ for (size_t i = 0; i < values_.size(); ++i)
+ {
+ values_[i] = std::holds_alternative(other.values_[i]) ? zeros_[i] : other.values_[i];
+ }
+ return true;
}
+rtde_interface::DataPackage::~DataPackage() = default;
+
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;
+ }
+
+ // Same contract as serializePackage(): the bytes after the package header, so a version 2
+ // payload starts with the recipe-id byte.
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);
+ }
+ },
+ values_[i]);
}
return true;
}
@@ -493,16 +416,27 @@ bool rtde_interface::DataPackage::parseWith(comm::BinParser& bp)
std::string rtde_interface::DataPackage::toString() const
{
std::stringstream ss;
- for (auto& item : data_)
+ for (size_t i = 0; i < recipe_.size(); ++i)
{
- ss << item.first << ": ";
- if (std::holds_alternative(item.second))
+ ss << recipe_[i] << ": ";
+ if (std::holds_alternative(values_[i]))
{
- ss << int(std::get(item.second));
+ ss << int(std::get(values_[i]));
}
else
{
- std::visit([&ss](auto&& arg) { ss << arg; }, item.second);
+ std::visit(
+ [&ss](auto&& arg) {
+ if constexpr (is_untyped_v)
+ {
+ ss << "";
+ }
+ else
+ {
+ ss << arg;
+ }
+ },
+ values_[i]);
}
ss << std::endl;
}
@@ -511,23 +445,69 @@ std::string rtde_interface::DataPackage::toString() const
size_t rtde_interface::DataPackage::serializePackage(uint8_t* buffer)
{
- uint16_t payload_size = sizeof(recipe_id_);
+ 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;
+ }
- for (auto& item : data_)
+ uint16_t payload_size = 0;
+ if (protocol_version_ == 2)
{
- payload_size += std::visit([](auto&& arg) -> uint16_t { return sizeof(arg); }, item.second);
+ payload_size += sizeof(recipe_id_);
+ }
+
+ for (const auto& value : values_)
+ {
+ payload_size += std::visit(
+ [](auto&& arg) -> uint16_t {
+ if constexpr (is_untyped_v)
+ {
+ return 0;
+ }
+ else
+ {
+ return sizeof(arg);
+ }
+ },
+ value);
}
size_t size = 0;
size += PackageHeader::serializeHeader(buffer, PackageType::RTDE_DATA_PACKAGE, payload_size);
- size += comm::PackageSerializer::serialize(buffer + size, recipe_id_);
- for (size_t i = 0; i < data_.size(); ++i)
+ if (protocol_version_ == 2)
+ {
+ size += comm::PackageSerializer::serialize(buffer + size, recipe_id_);
+ }
+ for (size_t i = 0; i < values_.size(); ++i)
{
size += std::visit(
- [&buffer, &size](auto&& arg) -> size_t { return comm::PackageSerializer::serialize(buffer + size, arg); },
- data_[i].second);
+ [&buffer, &size](auto&& arg) -> size_t {
+ if constexpr (is_untyped_v)
+ {
+ return 0;
+ }
+ else
+ {
+ return comm::PackageSerializer::serialize(buffer + size, arg);
+ }
+ },
+ values_[i]);
}
return size;
}
+
+bool rtde_interface::DataPackage::resetData(const std::string_view name)
+{
+ const std::optional index = fieldIndex(name);
+ if (!index.has_value())
+ {
+ return false;
+ }
+ values_[*index] = zeros_[*index];
+ return true;
+}
+
} // namespace rtde_interface
} // namespace urcl
diff --git a/src/rtde/rtde_client.cpp b/src/rtde/rtde_client.cpp
index 3000db6ef..945f08265 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)
@@ -118,18 +120,29 @@ bool RTDEClient::init(const size_t max_connection_attempts, const std::chrono::m
unsigned int attempts = 0;
std::stringstream ss;
- while (!setupCommunication(max_connection_attempts, reconnection_timeout))
+ try
{
- if (++attempts >= max_initialization_attempts)
+ while (!setupCommunication(max_connection_attempts, reconnection_timeout))
{
+ if (++attempts >= max_initialization_attempts)
+ {
+ disconnect();
+ ss << "Failed to initialize RTDE client after " << max_initialization_attempts << " attempts";
+ throw UrException(ss.str());
+ }
+ // disconnect to start on a clean slate when trying to set up communication again
disconnect();
- ss << "Failed to initialize RTDE client after " << max_initialization_attempts << " attempts";
- throw UrException(ss.str());
+ URCL_LOG_ERROR("Failed to initialize RTDE client, retrying in %d seconds", initialization_timeout.count() / 1000);
+ std::this_thread::sleep_for(initialization_timeout);
}
- // disconnect to start on a clean slate when trying to set up communication again
+ }
+ catch (...)
+ {
+ // setupCommunication() can throw after setting INITIALIZING (invalid recipe, target frequency
+ // out of range). Leave the client disconnected and uninitialized so a later init() retries
+ // instead of returning true on a half-finished handshake.
disconnect();
- URCL_LOG_ERROR("Failed to initialize RTDE client, retrying in %d seconds", initialization_timeout.count() / 1000);
- std::this_thread::sleep_for(initialization_timeout);
+ throw;
}
client_state_ = ClientState::INITIALIZED;
// Set reconnection callback after we are initialized to ensure that a disconnect during initialization doesn't
@@ -220,6 +233,8 @@ uint16_t RTDEClient::negotiateProtocolVersion()
{
URCL_LOG_INFO("Negotiated RTDE protocol version to %hu.", protocol_version);
parser_.setProtocolVersion(protocol_version);
+ preallocated_data_pkg_.setProtocolVersion(protocol_version);
+ writer_.setProtocolVersion(protocol_version);
return protocol_version;
}
break;
@@ -296,10 +311,9 @@ bool RTDEClient::queryURControlVersion()
URCL_LOG_WARN("%s", ss.str().c_str());
}
}
- std::stringstream ss;
- ss << "Could not query urcontrol version after " << MAX_REQUEST_RETRIES
- << " tries. Please check the output of the "
- "negotiation attempts above to get a hint what could be wrong.";
+ URCL_LOG_ERROR("Could not query urcontrol version after %u tries. Please check the output of the negotiation "
+ "attempts above to get a hint what could be wrong.",
+ MAX_REQUEST_RETRIES);
return false;
}
@@ -328,9 +342,13 @@ void RTDEClient::resetOutputRecipe(const std::vector new_recipe)
disconnect();
output_recipe_.assign(new_recipe.begin(), new_recipe.end());
- preallocated_data_pkg_ = DataPackage(output_recipe_, protocol_version_);
+ // 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_);
+ preallocated_data_pkg_.setProtocolVersion(protocol_version_);
parser_ = RTDEParser(output_recipe_);
+ parser_.setProtocolVersion(protocol_version_);
prod_ = std::make_unique>(stream_, parser_);
}
@@ -380,7 +398,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 +448,9 @@ bool RTDEClient::setupOutputs()
}
else
{
- // All variables are accounted for in the RTDE package
+ preallocated_data_pkg_.setTypes(variable_types);
+ // Register typed template so parser can allocate for null pointers or deprecated vector calls.
+ parser_.setExpectedDataPackage(preallocated_data_pkg_);
return true;
}
}
@@ -471,7 +497,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 +518,7 @@ bool RTDEClient::setupInputs()
throw RTDEInputConflictException(input_recipe_[i]);
}
}
+ writer_.setRecipeTypes(variable_types);
writer_.init(tmp_input->input_recipe_id_);
return true;
@@ -524,7 +557,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 +653,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 +703,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))
@@ -743,14 +777,78 @@ std::unique_ptr RTDEClient::getDataPackage(std::chr
return std::unique_ptr(nullptr);
}
+void RTDEClient::ensureOutputLayout(DataPackage& data_package, const DataPackage& output_template) const
+{
+ if (data_package.layoutHash() == output_template.layoutHash())
+ {
+ return;
+ }
+ // Backwards compatibility: master repaired foreign recipes by assignment; warn because repair allocates.
+ if (data_package.recipeHash() != output_template.recipeHash())
+ {
+ URCL_LOG_WARN("Replacing a DataPackage with a different output recipe; this may allocate. "
+ "Construct it from RTDEClient::getOutputRecipe() to avoid this repair.");
+ }
+ data_package = output_template;
+}
+
+void RTDEClient::ensureOutputLayout(std::unique_ptr& data_package) const
+{
+ // Backwards compatibility: allocate a typed package if caller passed null.
+ if (data_package == nullptr)
+ {
+ URCL_LOG_WARN("No DataPackage supplied; allocating one with the negotiated output layout.");
+ data_package = std::make_unique(preallocated_data_pkg_);
+ return;
+ }
+ ensureOutputLayout(*data_package, preallocated_data_pkg_);
+}
+
bool RTDEClient::getDataPackage(std::unique_ptr& data_package,
std::chrono::milliseconds timeout)
{
- return getDataPackage(*data_package, timeout);
+ if (data_package)
+ {
+ return getDataPackage(*data_package, timeout);
+ }
+ std::unique_ptr candidate;
+ {
+ // Hold reconnect lock while allocating from preallocated_data_pkg_ to prevent race with reconnect.
+ std::unique_lock lock(reconnect_mutex_, std::try_to_lock);
+ if (!lock.owns_lock())
+ {
+ URCL_LOG_DEBUG("Cannot prepare RTDE output: communication setup is locked.");
+ return false;
+ }
+ if (reconnecting_)
+ {
+ URCL_LOG_DEBUG("Cannot prepare RTDE output while reconnecting.");
+ return false;
+ }
+ if (!background_read_running_)
+ {
+ URCL_LOG_ERROR("Cannot get RTDE output: background reading is not running.");
+ return false;
+ }
+ if (!preallocated_data_pkg_.isTyped())
+ {
+ URCL_LOG_ERROR("Cannot get RTDE output before recipe types are negotiated.");
+ return false;
+ }
+ ensureOutputLayout(candidate);
+ }
+ if (!getDataPackage(*candidate, timeout))
+ {
+ URCL_LOG_DEBUG("Failed to get RTDE data package within the specified timeout.");
+ return false; // The reference overload diagnoses the failure.
+ }
+ data_package = std::move(candidate);
+ return true;
}
bool RTDEClient::getDataPackage(DataPackage& data_package, std::chrono::milliseconds timeout)
{
+ std::unique_lock lock(read_mutex_);
if (reconnecting_)
{
URCL_LOG_WARN("Currently reconnecting to the RTDE interface, unable to get data package");
@@ -762,27 +860,39 @@ bool RTDEClient::getDataPackage(DataPackage& data_package, std::chrono::millisec
"reading or use getDataPackageBlocking(...).");
return false;
}
-
- if (new_data_.load())
+ const auto initial_session_id = background_read_session_id_;
+ if (!background_read_cv_.wait_for(lock, timeout, [this, initial_session_id] {
+ return new_data_.load() || !background_read_running_ || reconnecting_ ||
+ background_read_session_id_ != initial_session_id;
+ }))
{
- std::lock_guard guard(read_mutex_);
- data_package = *dynamic_cast(data_buffer0_.get());
- new_data_.store(false);
+ URCL_LOG_DEBUG("Timed out waiting for new RTDE data.");
+ return false;
}
- else
+ if (background_read_session_id_ != initial_session_id)
{
- std::unique_lock lock(read_mutex_);
- auto wait_result = background_read_cv_.wait_for(lock, timeout);
- if (wait_result == std::cv_status::timeout)
- {
- return false;
- }
- if (new_data_.load())
- {
- data_package = *dynamic_cast(data_buffer0_.get());
- new_data_.store(false);
- }
+ URCL_LOG_DEBUG("RTDE read cancelled by a reader lifecycle change.");
+ return false;
+ }
+ if (reconnecting_)
+ {
+ URCL_LOG_DEBUG("RTDE read cancelled by reconnect.");
+ return false;
}
+ if (!background_read_running_)
+ {
+ URCL_LOG_DEBUG("RTDE read cancelled because background reading stopped.");
+ return false;
+ }
+ auto* received = dynamic_cast(data_buffer0_.get());
+ if (!new_data_ || received == nullptr)
+ {
+ URCL_LOG_ERROR("RTDE reader signalled data without a received data package.");
+ return false;
+ }
+ ensureOutputLayout(data_package, *received);
+ data_package = *received;
+ new_data_ = false;
return true;
}
@@ -794,25 +904,38 @@ bool RTDEClient::getDataPackageBlocking(std::unique_ptr& data_packa
"background reading or use getDataPackage(...).");
return false;
}
-
// Cannot get data packages while reconnecting as we could end up getting some of the configuration packages
- std::unique_ptr base_package(data_package.release());
std::unique_lock lock(reconnect_mutex_, std::defer_lock);
if (lock.try_lock())
{
- if (prod_->tryGet(base_package))
+ if (!preallocated_data_pkg_.isTyped())
{
- lock.unlock();
- auto package_type = base_package->getType();
- if (package_type != PackageType::RTDE_DATA_PACKAGE)
- {
- URCL_LOG_ERROR("Received package from RTDE interface is not a data package, but of type %d", package_type);
- return false;
- }
- data_package.reset(dynamic_cast(base_package.release()));
- return true;
+ URCL_LOG_ERROR("Cannot read RTDE data before recipe types are negotiated.");
+ return false;
+ }
+ // Recheck after acquiring the setup lock; never compete with a background socket reader.
+ if (background_read_running_ || reconnecting_)
+ {
+ URCL_LOG_DEBUG("Blocking RTDE read cancelled: background reading or reconnect is active.");
+ return false;
+ }
+ const auto read_into = [this](DataPackage& destination) {
+ return prod_->tryGetWithParser(
+ [this, &destination](comm::BinParser& bp) { return parser_.parseDataPackage(bp, destination); });
+ };
+ if (data_package)
+ {
+ ensureOutputLayout(data_package);
+ return read_into(*data_package);
}
- lock.unlock();
+ std::unique_ptr candidate;
+ ensureOutputLayout(candidate);
+ if (!read_into(*candidate))
+ {
+ return false; // The producer/parser diagnoses the failure.
+ }
+ data_package = std::move(candidate);
+ return true;
}
else
{
@@ -821,7 +944,6 @@ bool RTDEClient::getDataPackageBlocking(std::unique_ptr& data_packa
std::this_thread::sleep_for(period);
}
- data_package.reset(dynamic_cast(base_package.release()));
return false;
}
@@ -932,7 +1054,12 @@ void RTDEClient::reconnectCallback()
{
reconnecting_thread_.join();
}
- reconnecting_ = true;
+ {
+ std::lock_guard lock(read_mutex_);
+ reconnecting_ = true;
+ ++background_read_session_id_;
+ }
+ background_read_cv_.notify_all();
reconnecting_thread_ = std::thread(&RTDEClient::reconnect, this);
}
@@ -943,17 +1070,45 @@ 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;
+ }
+ // 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();
+ std::lock_guard lock(read_mutex_);
+ data_buffer0_ = std::move(buffer0);
+ data_buffer1_ = std::move(buffer1);
+ new_data_ = false;
+ ++background_read_session_id_;
background_read_running_ = true;
- data_buffer0_ = std::make_unique(output_recipe_, protocol_version_);
- data_buffer1_ = std::make_unique(output_recipe_, protocol_version_);
-
- background_read_thread_ = std::thread(&RTDEClient::backgroundReadThreadFunc, this);
+ try
+ {
+ background_read_thread_ = std::thread(&RTDEClient::backgroundReadThreadFunc, this);
+ }
+ catch (...)
+ {
+ background_read_running_ = false;
+ background_read_cv_.notify_all();
+ throw;
+ }
}
void RTDEClient::stopBackgroundRead()
{
- background_read_running_ = false;
- background_read_cv_.notify_one();
+ {
+ std::lock_guard lock(read_mutex_);
+ background_read_running_ = false;
+ new_data_ = false;
+ ++background_read_session_id_;
+ }
+ background_read_cv_.notify_all();
if (background_read_thread_.joinable())
{
background_read_thread_.join();
@@ -979,15 +1134,19 @@ void RTDEClient::backgroundReadThreadFunc()
{
{
std::scoped_lock rw_lock(read_mutex_, write_mutex_);
+ if (!background_read_running_ || reconnecting_)
+ {
+ continue;
+ }
std::swap(data_buffer0_, data_buffer1_);
+ new_data_.store(true);
}
- new_data_.store(true);
background_read_cv_.notify_one();
}
else if (data_buffer1_->getType() == PackageType::RTDE_TEXT_MESSAGE)
{
- URCL_LOG_INFO(data_buffer1_->toString().c_str());
+ URCL_LOG_INFO("%s", data_buffer1_->toString().c_str());
}
}
else
@@ -1004,7 +1163,10 @@ void RTDEClient::backgroundReadThreadFunc()
std::this_thread::sleep_for(period);
}
}
- new_data_.store(false);
+ {
+ std::lock_guard lock(read_mutex_);
+ new_data_.store(false);
+ }
URCL_LOG_INFO("RTDE background read thread stopped");
}
diff --git a/src/rtde/rtde_parser.cpp b/src/rtde/rtde_parser.cpp
index 48bd60c25..c40eecb4c 100644
--- a/src/rtde/rtde_parser.cpp
+++ b/src/rtde/rtde_parser.cpp
@@ -27,6 +27,65 @@ namespace urcl
{
namespace rtde_interface
{
+bool RTDEParser::parseDataPackagePayload(comm::BinParser& bp, DataPackage& package) const
+{
+ return package.parseWith(bp);
+}
+
+bool RTDEParser::recipeTypesKnown() const
+{
+ if (expected_layout_known_)
+ {
+ 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::parseDataPackage(comm::BinParser& bp, DataPackage& destination)
+{
+ try
+ {
+ const auto type = getPackageTypeFromHeader(bp);
+ if (type != PackageType::RTDE_DATA_PACKAGE)
+ {
+ std::unique_ptr message(createNewPackageFromType(type));
+ if (!message->parseWith(bp) || !bp.empty())
+ {
+ URCL_LOG_ERROR("Malformed non-data RTDE frame, type %d.", static_cast(type));
+ return false;
+ }
+ URCL_LOG_WARN("Expected RTDE data but received type %d: %s", static_cast(type), message->toString().c_str());
+ return false;
+ }
+ if (!recipeTypesKnown())
+ {
+ return false;
+ }
+ if (destination.layoutHash() != layout_hash_)
+ {
+ destination.setProtocolVersion(protocol_version_);
+ }
+ if (destination.layoutHash() != layout_hash_)
+ {
+ URCL_LOG_DEBUG("Cannot parse RTDE data: destination layout does not match the registered layout.");
+ return false;
+ }
+ if (!parseDataPackagePayload(bp, destination) || !bp.empty())
+ {
+ URCL_LOG_ERROR("RTDE data payload was not parsed completely.");
+ return false;
+ }
+ return true;
+ }
+ catch (const UrException& error)
+ {
+ URCL_LOG_ERROR("RTDE data parsing failed: %s", error.what());
+ return false;
+ }
+}
bool RTDEParser::parse(comm::BinParser& bp, std::vector>& results)
{
@@ -54,14 +113,44 @@ bool RTDEParser::parse(comm::BinParser& bp, std::vector package(new DataPackage(recipe_, protocol_version_));
-
- if (!package->parseWith(bp))
+ if (!recipeTypesKnown())
+ {
+ return false;
+ }
+ if (expected_data_package_.has_value())
+ {
+ // Backwards compatibility: deprecated vector overload allocates a fresh package per cycle from template.
+ auto package = std::make_unique(*expected_data_package_);
+ if (!parseDataPackagePayload(bp, *package) || !bp.empty())
+ {
+ URCL_LOG_ERROR("RTDE data payload was not parsed completely.");
+ return false;
+ }
+ results.push_back(std::move(package));
+ break;
+ }
+ if (results.empty() || results.back() == nullptr)
+ {
+ URCL_LOG_ERROR("Cannot parse an RTDE data package without a pre-allocated DataPackage with the expected "
+ "layout.");
+ return false;
+ }
+ DataPackage* package = dynamic_cast(results.back().get());
+ if (package != nullptr && package->layoutHash() != layout_hash_)
+ {
+ // Re-sync negotiated protocol version in case parser version changed after package creation.
+ package->setProtocolVersion(protocol_version_);
+ }
+ if (package == nullptr || package->layoutHash() != layout_hash_)
+ {
+ URCL_LOG_DEBUG("Cannot parse RTDE data: destination layout does not match the registered layout.");
+ return false;
+ }
+ if (!parseDataPackagePayload(bp, *package))
{
URCL_LOG_ERROR("Package parsing of type %d failed!", static_cast(type));
return false;
}
- results.push_back(std::move(package));
break;
}
default:
@@ -104,25 +193,35 @@ 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)
- {
- URCL_LOG_WARN("The passed result pointer is empty. A new DataPackage will "
- "have to be allocated. Please pass a pre-allocated DataPackage if you expect a DataPackage "
- "would be sent.");
- }
- else
+ if (!expected_data_package_.has_value())
{
- URCL_LOG_WARN("Passed a pre-allocated RTDE package of type %u while a DataPackage was received. A new "
- "DataPackage will have to be allocated. Please pass a pre-allocated DataPackage if you expect "
- "a DataPackage would be sent.",
- result->getType());
+ URCL_LOG_DEBUG("Cannot allocate RTDE data: no typed template is registered.");
+ return false;
}
- result = std::make_unique(recipe_, protocol_version_);
+ // Backwards compatibility: allocate from template if caller supplied null or non-data package.
+ URCL_LOG_WARN("Allocating an RTDE DataPackage; pass a matching pre-allocated package to avoid allocation.");
+ result = std::make_unique(*expected_data_package_);
+ }
+
+ DataPackage* data_package = dynamic_cast(result.get());
+ if (data_package != nullptr && data_package->layoutHash() != layout_hash_)
+ {
+ // Re-sync negotiated protocol version in case parser version changed after package creation.
+ data_package->setProtocolVersion(protocol_version_);
+ }
+ if (data_package == nullptr || data_package->layoutHash() != layout_hash_)
+ {
+ URCL_LOG_DEBUG("Cannot parse RTDE data: destination layout does not match the registered layout.");
+ return false;
}
- if (!dynamic_cast(result.get())->parseWith(bp))
+ if (!parseDataPackagePayload(bp, *data_package))
{
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..bab4d03d9 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/exceptions.h"
#include "ur_client_library/log.h"
namespace urcl
@@ -59,28 +60,62 @@ RTDEWriter::RTDEWriter(comm::URStream* stream, const std::vector& recipe)
{
+ std::lock_guard lock_guard(store_mutex_);
if (running_)
{
throw UrException("Requesting to change the input recipe while the RTDEWriter is running. The writer has to be "
"stopped before setting the recipe.");
}
- std::lock_guard lock_guard(store_mutex_);
recipe_ = recipe;
used_masks_.clear();
for (const auto& field : recipe)
{
- if (field.size() >= 5 && field.substr(field.size() - 5) == "_mask")
+ if (field.size() >= 5 && field.compare(field.size() - 5, 5, "_mask") == 0)
{
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_);
+ data_buffer0_->setProtocolVersion(protocol_version_);
+ data_buffer1_->setProtocolVersion(protocol_version_);
current_store_buffer_ = data_buffer0_;
current_send_buffer_ = data_buffer1_;
}
+void RTDEWriter::setProtocolVersion(uint16_t protocol_version)
+{
+ std::lock_guard lock_guard(store_mutex_);
+ if (running_)
+ {
+ throw UrException("Cannot change the RTDE protocol version while the writer is running.");
+ }
+ protocol_version_ = protocol_version;
+ if (data_buffer0_ != nullptr)
+ {
+ data_buffer0_->setProtocolVersion(protocol_version);
+ }
+ if (data_buffer1_ != nullptr)
+ {
+ data_buffer1_->setProtocolVersion(protocol_version);
+ }
+}
+
+void RTDEWriter::setRecipeTypes(const std::vector& types)
+{
+ std::lock_guard lock_guard(store_mutex_);
+ if (running_)
+ {
+ throw UrException("Cannot apply RTDE recipe types while the writer is running.");
+ }
+ data_buffer0_->setTypes(types);
+ data_buffer1_->setTypes(types);
+}
+
void RTDEWriter::init(uint8_t recipe_id)
{
if (running_)
@@ -90,12 +125,19 @@ void RTDEWriter::init(uint8_t recipe_id)
}
{
std::lock_guard lock_guard(store_mutex_);
+ if (running_)
+ {
+ throw UrException("Requesting to init a RTDEWriter while it is running. The writer has to be "
+ "stopped before initializing it.");
+ }
data_buffer0_->setRecipeID(recipe_id);
data_buffer1_->setRecipeID(recipe_id);
+ current_store_buffer_ = data_buffer0_;
+ current_send_buffer_ = data_buffer1_;
+ running_ = true;
}
recipe_id_ = recipe_id;
new_data_available_ = false;
- running_ = true;
writer_thread_ = std::thread(&RTDEWriter::run, this);
}
@@ -143,11 +185,25 @@ void RTDEWriter::stop()
bool RTDEWriter::sendPackage(const DataPackage& package)
{
std::lock_guard guard(store_mutex_);
- *current_store_buffer_ = package;
+ if (!current_store_buffer_->copyFrom(package))
+ {
+ return false;
+ }
markStorageToBeSent();
return true;
}
+DataPackage RTDEWriter::createDataPackage()
+{
+ std::lock_guard guard(store_mutex_);
+ if (current_store_buffer_ == nullptr || !running_ || !current_store_buffer_->isTyped())
+ {
+ throw UrException("Cannot create an RTDE input data package before the robot has acknowledged the input recipe. "
+ "That happens during the RTDE handshake, so call this after RTDEClient::init().");
+ }
+ return current_store_buffer_->emptyCopy();
+}
+
bool RTDEWriter::sendSpeedSlider(double speed_slider_fraction)
{
if (speed_slider_fraction > 1.0 || speed_slider_fraction < 0.0)
@@ -404,19 +460,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 5479ca8b7..797831465 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -41,9 +41,6 @@ if (INTEGRATION_TESTS)
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
EXTRA_ARGS ${INTEGRATION_TESTS_ROBOT_IP_ARG}
)
- # Bound this teardown regression test so a hang fails CI fast instead of timing out the job.
- set_tests_properties(RTDEClientTest.destructor_not_blocked_by_stuck_reconnect_thread
- PROPERTIES TIMEOUT 60)
if (CHECK_RTDE_DOCS_RECIPE)
find_package(Python3 COMPONENTS Interpreter REQUIRED)
add_custom_target(generate_outputs ALL COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/resources/generate_rtde_outputs.py)
@@ -217,6 +214,53 @@ 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}
+)
+set_tests_properties(RTDEClientFakeServerTest.server_sender_start_and_stop_are_idempotent
+ RTDEClientFakeServerTest.blocking_failures_preserve_caller_ownership
+ RTDEBackgroundReadTest.notifications_without_data_do_not_succeed
+ RTDEBackgroundReadTest.stop_and_reconnect_cancel_pending_reads
+ RTDEClientFakeServerTest.server_sender_start_and_stop_are_serialized
+ RTDEClientFakeServerTest.too_few_output_types_fail_then_recover
+ RTDEClientFakeServerTest.too_many_output_types_fail_then_recover
+ RTDEClientFakeServerTest.too_few_input_types_fail_then_recover
+ RTDEClientFakeServerTest.too_many_input_types_fail_then_recover
+ RTDEClientFakeServerTest.unknown_output_type_fails_then_recovers
+ RTDEClientFakeServerTest.unknown_input_type_fails_then_recovers
+ RTDEClientFakeServerTest.disconnection_wait_tracks_each_connection
+ RTDEClientFakeServerTest.repeated_output_negotiation_failures_recover
+ RTDEClientFakeServerTest.input_in_use_exhausts_retries_then_recovers
+ PROPERTIES TIMEOUT 60)
+
+# Covers RTDEClient::reconnect() by taking the fake RTDE server away and giving it back, so it runs
+# without a robot. Kept apart from the tests above because these wait on retry timing and are
+# therefore slower.
+add_executable(rtde_client_reconnect_tests test_rtde_client_reconnect.cpp fake_rtde_server.cpp)
+target_link_libraries(rtde_client_reconnect_tests PRIVATE ur_client_library::urcl GTest::gtest_main)
+gtest_add_tests(TARGET rtde_client_reconnect_tests
+ WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
+)
+# Bound these so a hang fails CI fast instead of timing out the job.
+set_tests_properties(RTDEClientReconnectTest.destructor_not_blocked_by_stuck_reconnect_thread
+ RTDEClientReconnectTest.destroying_the_server_immediately_after_start
+ RTDEClientReconnectTest.reconnects_when_the_server_comes_back_during_background_read
+ RTDEClientReconnectTest.reconnects_when_the_server_comes_back_during_blocking_read
+ RTDEClientReconnectTest.destroying_the_client_while_the_server_is_silent
+ RTDEClientReconnectTest.reconnect_gives_up_when_the_handshake_keeps_failing
+ PROPERTIES TIMEOUT 60)
+
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..1be3b50b1 100644
--- a/tests/fake_rtde_server.cpp
+++ b/tests/fake_rtde_server.cpp
@@ -1,10 +1,496 @@
#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,
+ const uint16_t protocol_version = 2)
+{
+ auto package = std::make_unique(recipe);
+ package->setTypes(types);
+ package->setProtocolVersion(protocol_version);
+ return package;
+}
+} // namespace
RTDEServer::RTDEServer(const int port) : server_(port)
{
@@ -19,32 +505,197 @@ RTDEServer::RTDEServer(const int port) : server_(port)
RTDEServer::~RTDEServer()
{
+ // The TCP worker calls handlePackage() and the disconnect callback, both of which lock
+ // mutexes declared after server_. Join that thread here so those mutexes are still alive.
+ // ~TCPServer would otherwise do it too late, after the mutexes have already been destroyed.
+ // Finish callbacks before stopping the sender: an in-flight START may have acknowledged the
+ // request but not yet created send_thread_. Stopping it first would leave that new thread
+ // joinable when its destructor runs, causing std::terminate().
+ server_.shutdown();
stopSendingDataPackages();
}
+void RTDEServer::queueTextMessageBeforeVersionReply(const std::string& message)
+{
+ std::lock_guard lock(negotiation_mutex_);
+ pending_text_messages_.push_back(message);
+}
+
+void RTDEServer::setHighestAcceptedProtocolVersion(const uint16_t highest_accepted)
+{
+ std::lock_guard lock(negotiation_mutex_);
+ highest_accepted_protocol_version_ = highest_accepted;
+}
+
+std::vector RTDEServer::requestedProtocolVersions()
+{
+ std::lock_guard lock(negotiation_mutex_);
+ return requested_protocol_versions_;
+}
+
+void RTDEServer::setAcceptStart(const bool accept)
+{
+ std::lock_guard lock(negotiation_mutex_);
+ accept_start_ = accept;
+}
+
+void RTDEServer::setAcceptPause(const bool accept)
+{
+ std::lock_guard lock(negotiation_mutex_);
+ accept_pause_ = accept;
+}
+
+void RTDEServer::queueTextMessageBeforeSetupOutputs(const std::string& message)
+{
+ std::lock_guard lock(negotiation_mutex_);
+ pending_setup_outputs_text_messages_.push_back(message);
+}
+
+void RTDEServer::queueTextMessageBeforeSetupInputs(const std::string& message)
+{
+ std::lock_guard lock(negotiation_mutex_);
+ pending_setup_inputs_text_messages_.push_back(message);
+}
+
+void RTDEServer::setOutputTypeReply(const std::optional>& types)
+{
+ std::lock_guard lock(negotiation_mutex_);
+ output_type_reply_ = types;
+}
+
+void RTDEServer::setInputTypeReply(const std::optional>& types)
+{
+ std::lock_guard