From 0ef331dc89ea5ea1a305d6a5f114b4dbe1a25435 Mon Sep 17 00:00:00 2001 From: Felix Exner Date: Tue, 6 May 2025 16:43:49 +0200 Subject: [PATCH 1/2] Allow an empty input recipe to the RTDE client --- include/ur_client_library/rtde/data_package.h | 17 +++++++++++++++++ include/ur_client_library/rtde/rtde_writer.h | 11 ++++++++++- src/rtde/rtde_client.cpp | 15 +++++++++++---- src/rtde/rtde_writer.cpp | 8 ++++++++ 4 files changed, 46 insertions(+), 5 deletions(-) diff --git a/include/ur_client_library/rtde/data_package.h b/include/ur_client_library/rtde/data_package.h index a7c90ecae..ce2b991d2 100644 --- a/include/ur_client_library/rtde/data_package.h +++ b/include/ur_client_library/rtde/data_package.h @@ -71,6 +71,22 @@ class DataPackage : public RTDEPackage this->protocol_version_ = other.protocol_version_; } + DataPackage& operator=(DataPackage& other) + { + this->data_ = other.data_; + this->recipe_ = other.recipe_; + this->protocol_version_ = other.protocol_version_; + return *this; + } + + DataPackage operator=(const DataPackage& other) + { + this->data_ = other.data_; + this->recipe_ = other.recipe_; + this->protocol_version_ = other.protocol_version_; + return *this; + } + /*! * \brief Creates a new DataPackage object, based on a given recipe. * @@ -82,6 +98,7 @@ class DataPackage : public RTDEPackage : RTDEPackage(PackageType::RTDE_DATA_PACKAGE), recipe_(recipe), protocol_version_(protocol_version) { } + virtual ~DataPackage() = default; /*! diff --git a/include/ur_client_library/rtde/rtde_writer.h b/include/ur_client_library/rtde/rtde_writer.h index 77a562d08..dc9f0537d 100644 --- a/include/ur_client_library/rtde/rtde_writer.h +++ b/include/ur_client_library/rtde/rtde_writer.h @@ -67,6 +67,15 @@ class RTDEWriter writer_thread_.join(); } } + + /*! + * \brief Sets a new input recipe. This can be used to change the input recipe on the fly, if + * needed. + * + * \param recipe The new recipe to use + */ + void setInputRecipe(const std::vector& recipe); + /*! * \brief Starts the writer thread, which periodically clears the queue to write packages to the * robot. @@ -162,7 +171,7 @@ class RTDEWriter uint8_t pinToMask(uint8_t pin); comm::URStream* stream_; std::vector recipe_; - uint8_t recipe_id_; + uint8_t recipe_id_ = 0; moodycamel::BlockingReaderWriterQueue> queue_; std::thread writer_thread_; bool running_; diff --git a/src/rtde/rtde_client.cpp b/src/rtde/rtde_client.cpp index 425dc981d..de92c64db 100644 --- a/src/rtde/rtde_client.cpp +++ b/src/rtde/rtde_client.cpp @@ -41,7 +41,6 @@ RTDEClient::RTDEClient(std::string robot_ip, comm::INotifier& notifier, const st : stream_(robot_ip, UR_RTDE_PORT) , output_recipe_(ensureTimestampIsPresent(readRecipe(output_recipe_file))) , ignore_unavailable_outputs_(ignore_unavailable_outputs) - , input_recipe_(readRecipe(input_recipe_file)) , parser_(output_recipe_) , prod_(std::make_unique>(stream_, parser_)) , notifier_(notifier) @@ -51,6 +50,11 @@ RTDEClient::RTDEClient(std::string robot_ip, comm::INotifier& notifier, const st , target_frequency_(target_frequency) , client_state_(ClientState::UNINITIALIZED) { + if (!input_recipe_file.empty()) + { + input_recipe_ = readRecipe(input_recipe_file); + writer_.setInputRecipe(input_recipe_); + } } RTDEClient::RTDEClient(std::string robot_ip, comm::INotifier& notifier, const std::vector& output_recipe, @@ -169,9 +173,12 @@ void RTDEClient::setupCommunication(const size_t max_num_tries, const std::chron return; } - setupInputs(); - if (client_state_ == ClientState::UNINITIALIZED) - return; + if (input_recipe_.size() > 0) + { + setupInputs(); + if (client_state_ == ClientState::UNINITIALIZED) + return; + } // We finished communication for now pipeline_->stop(); diff --git a/src/rtde/rtde_writer.cpp b/src/rtde/rtde_writer.cpp index ea543ab80..de0eae950 100644 --- a/src/rtde/rtde_writer.cpp +++ b/src/rtde/rtde_writer.cpp @@ -37,6 +37,14 @@ RTDEWriter::RTDEWriter(comm::URStream* stream, const std::vector& recipe) +{ + std::lock_guard guard(package_mutex_); + recipe_ = recipe; + package_ = DataPackage(recipe_); + package_.initEmpty(); +} + void RTDEWriter::init(uint8_t recipe_id) { recipe_id_ = recipe_id; From 412700654b40e4189007cb67c70e8c1d2427f9e7 Mon Sep 17 00:00:00 2001 From: Felix Exner Date: Mon, 6 Oct 2025 16:18:39 +0200 Subject: [PATCH 2/2] Update tests and documentation regarding empty input recipes --- doc/architecture/rtde_client.rst | 44 ++++++++++++++++++++++++++- tests/test_rtde_client.cpp | 51 ++++++++++++++++++++++++++++++-- 2 files changed, 91 insertions(+), 4 deletions(-) diff --git a/doc/architecture/rtde_client.rst b/doc/architecture/rtde_client.rst index c5ebce502..497906471 100644 --- a/doc/architecture/rtde_client.rst +++ b/doc/architecture/rtde_client.rst @@ -11,7 +11,7 @@ client. To use the RTDE-Client, you'll have to initialize and start it separatel .. code-block:: c++ - rtde_interface::RTDEClient my_client(ROBOT_IP, notifier, OUTPUT_RECIPE, INPUT_RECIPE); + rtde_interface::RTDEClient my_client(ROBOT_IP, notifier, OUTPUT_RECIPE_FILE, INPUT_RECIPE_FILE); my_client.init(); my_client.start(); while (true) @@ -28,6 +28,20 @@ outputs. Please refer to the `RTDE guide `_ on which elements are available. +.. note:: + + The recipes can be either passed as a filename or as a list of strings directly. E.g. the + following will work + + .. code-block:: c++ + + rtde_interface::RTDEClient my_client( + ROBOT_IP, + notifier, + {"timestamp", "actual_q"}, + {"speed_slider_mask", "speed_slider_fraction"} + ); + Inside the ``RTDEclient`` data is received in a separate thread, parsed by the ``RTDEParser`` and added to a pipeline queue. @@ -56,6 +70,29 @@ sure to frequency, please use the ``resetRTDEClient()`` method after the ``UrDriver`` object has been created. +Read-Only RTDEClient +-------------------- + +While RTDE allows multiple clients to connect to the same robot, only one client is allowed to +write data to the robot. To create a read-only RTDE client, the ``RTDEClient`` can be created with +an empty input recipe, like this: + +.. code-block:: c++ + + rtde_interface::RTDEClient my_client(ROBOT_IP, notifier, OUTPUT_RECIPE, {}); + // Alternatively, pass an empty filename when using recipe files + // rtde_interface::RTDEClient my_client(ROBOT_IP, notifier, OUTPUT_RECIPE_FILE, ""); + my_client.init(); + my_client.start(); + while (true) + { + std::unique_ptr data_pkg = my_client.getDataPackage(READ_TIMEOUT); + if (data_pkg) + { + std::cout << data_pkg->toString() << std::endl; + } + } + RTDEWriter ---------- @@ -66,3 +103,8 @@ The class offers specific methods for every RTDE input possible to write. Data is sent asynchronously to the RTDE interface. +.. 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``. diff --git a/tests/test_rtde_client.cpp b/tests/test_rtde_client.cpp index cd87c2355..865fa5722 100644 --- a/tests/test_rtde_client.cpp +++ b/tests/test_rtde_client.cpp @@ -120,9 +120,12 @@ TEST_F(RTDEClientTest, no_recipe) UrException); // Only input recipe is unconfigured - EXPECT_THROW( - client_.reset(new rtde_interface::RTDEClient(g_ROBOT_IP, notifier_, output_recipe_file_, input_recipe_file)), - UrException); + EXPECT_NO_THROW( + client_.reset(new rtde_interface::RTDEClient(g_ROBOT_IP, notifier_, output_recipe_file_, input_recipe_file))); + + EXPECT_THROW(client_.reset(new rtde_interface::RTDEClient(g_ROBOT_IP, notifier_, output_recipe_file_, + "/i/do/not/exist/urclrtdetest.txt")), + UrException); } TEST_F(RTDEClientTest, empty_recipe_file) @@ -413,6 +416,48 @@ TEST_F(RTDEClientTest, check_unknown_rtde_output_variable) EXPECT_THROW(client_->init(), UrException); } +TEST_F(RTDEClientTest, empty_input_recipe) +{ + std::vector empty_input_recipe = {}; + client_.reset(new rtde_interface::RTDEClient(g_ROBOT_IP, notifier_, resources_output_recipe_, empty_input_recipe)); + client_->init(); + client_->start(); + + // Test that we can receive and parse the timestamp from the received package to prove the setup was successful + const std::chrono::milliseconds read_timeout{ 100 }; + std::unique_ptr data_pkg = client_->getDataPackage(read_timeout); + + if (data_pkg == nullptr) + { + std::cout << "Failed to get data package from robot" << std::endl; + GTEST_FAIL(); + } + + double timestamp; + EXPECT_TRUE(data_pkg->getData("timestamp", timestamp)); + + EXPECT_FALSE(client_->getWriter().sendStandardDigitalOutput(1, false)); + + client_->pause(); + + client_.reset(new rtde_interface::RTDEClient(g_ROBOT_IP, notifier_, output_recipe_file_, "")); + client_->init(); + client_->start(); + + data_pkg = client_->getDataPackage(read_timeout); + + if (data_pkg == nullptr) + { + std::cout << "Failed to get data package from robot" << std::endl; + GTEST_FAIL(); + } + EXPECT_TRUE(data_pkg->getData("timestamp", timestamp)); + + EXPECT_FALSE(client_->getWriter().sendStandardDigitalOutput(1, false)); + + client_->pause(); +} + int main(int argc, char* argv[]) { ::testing::InitGoogleTest(&argc, argv);