From 5ce8100212c335d9657010ba4f23a082f1e35eec Mon Sep 17 00:00:00 2001 From: Jacob Larsen Date: Thu, 26 Mar 2026 14:49:20 +0000 Subject: [PATCH 01/74] Add preparation of script to be sent Adds function definition (if missing) and gives the function a name. --- .../primary/primary_client.h | 19 +++- src/primary/primary_client.cpp | 103 +++++++++++++++++- 2 files changed, 119 insertions(+), 3 deletions(-) diff --git a/include/ur_client_library/primary/primary_client.h b/include/ur_client_library/primary/primary_client.h index 27ed2f33c..90c292eee 100644 --- a/include/ur_client_library/primary/primary_client.h +++ b/include/ur_client_library/primary/primary_client.h @@ -47,6 +47,20 @@ namespace urcl { namespace primary_interface { + +struct ScriptInfo +{ + std::string script_name; + std::string script_code; + ScriptInfo(std::string name, std::string code) : script_name(name), script_code(code) {}; +}; + +enum ScriptTypes +{ + DEF = 0, + SEC = 1, +}; + class PrimaryClient { public: @@ -86,7 +100,7 @@ class PrimaryClient * * \returns true on successful upload, false otherwise. */ - bool sendScript(const std::string& program); + bool sendScript(const std::string& program, std::string script_name = "", ScriptTypes script_type = ScriptTypes::DEF); bool checkCalibration(const std::string& checksum); @@ -299,6 +313,9 @@ class PrimaryClient // The function is called whenever an error code message is received void errorMessageCallback(ErrorCode& code); + ScriptInfo prepare_script(std::string script, std::string script_name, ScriptTypes script_type); + std::vector strip_comments_and_whitespace(std::vector script_lines); + PrimaryParser parser_; std::shared_ptr consumer_; std::unique_ptr> multi_consumer_; diff --git a/src/primary/primary_client.cpp b/src/primary/primary_client.cpp index 9a2ac564e..30fc2594d 100644 --- a/src/primary/primary_client.cpp +++ b/src/primary/primary_client.cpp @@ -36,6 +36,7 @@ #include #include +#include namespace urcl { namespace primary_interface @@ -103,12 +104,15 @@ std::deque PrimaryClient::getErrorCodes() return error_codes; } -bool PrimaryClient::sendScript(const std::string& program) +bool PrimaryClient::sendScript(const std::string& program, std::string script_name, ScriptTypes script_type) { // urscripts (snippets) must end with a newline, or otherwise the controller's runtime will // not execute them. To avoid problems, we always just append a newline here, even if // there may already be one. - auto program_with_newline = program + '\n'; + + ScriptInfo script_with_name = prepare_script(program, script_name, script_type); + + auto program_with_newline = script_with_name.script_code; size_t len = program_with_newline.size(); const uint8_t* data = reinterpret_cast(program_with_newline.c_str()); @@ -139,6 +143,101 @@ bool PrimaryClient::sendScript(const std::string& program) return false; } +std::vector PrimaryClient::strip_comments_and_whitespace(std::vector split_script) +{ + std::vector stripped_script; + for (auto line : split_script) + { + for (auto c : line) + { + if (!isspace(c)) + { + if (c == '#') + { + break; + } + else + { + stripped_script.push_back(line); + break; + } + } + } + } + return stripped_script; +} + +ScriptInfo PrimaryClient::prepare_script(std::string script, std::string script_name, ScriptTypes script_type) +{ + // Validate script_name + static const std::regex valid_name(R"(^[A-Za-z_][A-Za-z0-9_]*$)"); + if (!std::regex_match(script_name, valid_name) && !script_name.empty()) + { + throw urcl::UrException("Invalid script name: '" + script_name + + "'. Can only contain letters, numbers and underscores. First character must be a letter or " + "underscore."); + } + // Split the given script in to separate lines + std::vector split_script = splitString(script, "\n"); + + // Remove all comments and white-space-only lines + std::vector stripped_script = strip_comments_and_whitespace(split_script); + + // Use given scipt name or create one + unsigned int current_time = + std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()) + .count(); + std::string actual_script_name = script_name.size() != 0 ? script_name : "script_" + std::to_string(current_time); + // Limit script name length to 31, to ensure backwards compatibility + if (actual_script_name.size() > 31) + { + actual_script_name = actual_script_name.substr(0, 31); + } + + // Is the script wrapped in a function definition? If not add one + if (stripped_script[0].find("def ") == script.npos && stripped_script[0].find("sec ") == script.npos) + { + // Assign appropriate type + std::string type; + switch (script_type) + { + case ScriptTypes::DEF: + type = "def"; + break; + case ScriptTypes::SEC: + type = "sec"; + break; + } + + std::string start = type + " " + actual_script_name + "():"; + std::string end = "end"; + // Add indentation to the existing script code + for (int i = 0; i < stripped_script.size(); i++) + { + stripped_script[i] = " " + stripped_script[i]; + } + // Add function definition and end statement to the stripped script lines vector + stripped_script.insert(stripped_script.begin(), start); + stripped_script.push_back(end); + } + + if (stripped_script.back().find("end") == script.npos) + { + throw urcl::UrException("Script contains either function definition or secondary process definition, but no 'end' " + "term. Script is invalid."); + } + + // Concatenate all the script lines in to the final script + std::string prepared_script = ""; + for (auto line : stripped_script) + { + prepared_script.append(line + "\n"); + } + + // Return final script code as well as the name of the script as it will be exectuted + return ScriptInfo(actual_script_name, prepared_script); +} + bool PrimaryClient::reconnectStream() { URCL_LOG_DEBUG("Closing primary stream..."); From 8f7efddb46ce69565f82f9979737e1079d973456 Mon Sep 17 00:00:00 2001 From: Jacob Larsen Date: Mon, 30 Mar 2026 09:22:29 +0000 Subject: [PATCH 02/74] Primary client example (WIP) --- examples/CMakeLists.txt | 4 ++++ examples/primary_client.cpp | 20 ++++++++++++++++++++ 2 files changed, 24 insertions(+) create mode 100644 examples/primary_client.cpp diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index b7da3567d..74e7d49b5 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -11,6 +11,10 @@ add_executable(primary_pipeline_example primary_pipeline.cpp) target_link_libraries(primary_pipeline_example ur_client_library::urcl) +add_executable(primary_client + primary_client.cpp) +target_link_libraries(primary_client ur_client_library::urcl) + add_executable(primary_pipeline_calibration_example primary_pipeline_calibration.cpp) target_link_libraries(primary_pipeline_calibration_example ur_client_library::urcl) diff --git a/examples/primary_client.cpp b/examples/primary_client.cpp new file mode 100644 index 000000000..0fb552225 --- /dev/null +++ b/examples/primary_client.cpp @@ -0,0 +1,20 @@ +#include +#include + +int main() +{ + auto notif = urcl::comm::INotifier(); + auto client = urcl::primary_interface::PrimaryClient("192.168.56.101", notif); + client.start(10); + // std::cout << "Robot mode: " << int(client.getRobotMode()) << std::endl; + // client.commandPowerOff(); + // std::cout << "Robot mode: " << int(client.getRobotMode()) << std::endl; + // client.commandBrakeRelease(); + // std::cout << "Robot mode: " << int(client.getRobotMode()) << std::endl; + auto s = "textmsg(\"hello\") \n #hhelllooe \n \n #helloe \n #jejfef"; + client.sendScript(s, "hello_world", urcl::primary_interface::ScriptTypes::SEC); + + client.sendScript(s, "", urcl::primary_interface::ScriptTypes::DEF); + using namespace std::chrono_literals; + std::this_thread::sleep_for(2000ms); +} \ No newline at end of file From 1e6e63139de017ed70deeeb26aa9ab093385663a Mon Sep 17 00:00:00 2001 From: Jacob Larsen Date: Mon, 30 Mar 2026 09:23:06 +0000 Subject: [PATCH 03/74] Add KeyMessage and RuntimeException to abstract consumer --- include/ur_client_library/primary/abstract_primary_consumer.h | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/include/ur_client_library/primary/abstract_primary_consumer.h b/include/ur_client_library/primary/abstract_primary_consumer.h index fdb545dd9..3abd7272a 100644 --- a/include/ur_client_library/primary/abstract_primary_consumer.h +++ b/include/ur_client_library/primary/abstract_primary_consumer.h @@ -38,6 +38,8 @@ #include "ur_client_library/primary/robot_state/configuration_data.h" #include "ur_client_library/primary/robot_state/masterboard_data.h" #include "ur_client_library/primary/robot_message/safety_mode_message.h" +#include "ur_client_library/primary/robot_message/key_message.h" +#include "ur_client_library/primary/robot_message/runtime_exception_message.h" namespace urcl { @@ -83,6 +85,8 @@ class AbstractPrimaryConsumer : public comm::IConsumer virtual bool consume(ConfigurationData& pkg) = 0; virtual bool consume(MasterboardData& pkg) = 0; virtual bool consume(SafetyModeMessage& pkg) = 0; + virtual bool consume(KeyMessage& pkg) = 0; + virtual bool consume(RuntimeExceptionMessage& pkg) = 0; private: /* data */ From 0dccfb09dc15d1100aef7372c5497a00eaa1e59e Mon Sep 17 00:00:00 2001 From: Jacob Larsen Date: Mon, 30 Mar 2026 09:24:34 +0000 Subject: [PATCH 04/74] Add KeyMessage and RuntimeException to primary consumer --- .../primary/primary_consumer.h | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/include/ur_client_library/primary/primary_consumer.h b/include/ur_client_library/primary/primary_consumer.h index 74b4d7dd8..a140ceded 100644 --- a/include/ur_client_library/primary/primary_consumer.h +++ b/include/ur_client_library/primary/primary_consumer.h @@ -33,6 +33,7 @@ #include "ur_client_library/primary/robot_state/masterboard_data.h" #include "ur_client_library/ur/datatypes.h" #include "ur_client_library/ur/version_information.h" +#include "ur_client_library/primary/robot_message/key_message.h" #include #include @@ -210,6 +211,34 @@ class PrimaryConsumer : public AbstractPrimaryConsumer error_code_message_callback_ = callback_function; } + virtual bool consume(KeyMessage& pkg) override + { + if (key_message_callback_ != nullptr) + { + key_message_callback_(pkg); + } + return true; + } + + void setKeyMessageCallback(std::function callback_function) + { + key_message_callback_ = callback_function; + } + + virtual bool consume(RuntimeExceptionMessage& pkg) override + { + if (runtime_exception_callback_ != nullptr) + { + runtime_exception_callback_(pkg); + } + return true; + } + + void setRuntimeExceptionCallback(std::function callback_function) + { + runtime_exception_callback_ = callback_function; + } + /*! * \brief Get the kinematics info * @@ -293,6 +322,8 @@ class PrimaryConsumer : public AbstractPrimaryConsumer private: std::function error_code_message_callback_; + std::function key_message_callback_; + std::function runtime_exception_callback_; std::mutex kinematics_info_mutex_; std::unique_ptr kinematics_info_; std::mutex robot_mode_mutex_; From 6a7e0d184f14adb33d35d8f1672f30226d9ad9ce Mon Sep 17 00:00:00 2001 From: Jacob Larsen Date: Mon, 30 Mar 2026 09:26:52 +0000 Subject: [PATCH 05/74] Add callbacks for KeyMessage and RuntimeException to primary client --- .../ur_client_library/primary/primary_client.h | 5 +++++ src/primary/primary_client.cpp | 15 +++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/include/ur_client_library/primary/primary_client.h b/include/ur_client_library/primary/primary_client.h index 90c292eee..6e150e479 100644 --- a/include/ur_client_library/primary/primary_client.h +++ b/include/ur_client_library/primary/primary_client.h @@ -312,6 +312,8 @@ class PrimaryClient // The function is called whenever an error code message is received void errorMessageCallback(ErrorCode& code); + void keyMessageCallback(KeyMessage& msg); + void runtimeExceptionCallback(RuntimeExceptionMessage& msg); ScriptInfo prepare_script(std::string script, std::string script_name, ScriptTypes script_type); std::vector strip_comments_and_whitespace(std::vector script_lines); @@ -328,6 +330,9 @@ class PrimaryClient std::mutex error_code_queue_mutex_; std::deque error_code_queue_; + + std::mutex key_meassage_queue_mutex_; + std::deque key_message_queue_; }; } // namespace primary_interface diff --git a/src/primary/primary_client.cpp b/src/primary/primary_client.cpp index 30fc2594d..ad639d095 100644 --- a/src/primary/primary_client.cpp +++ b/src/primary/primary_client.cpp @@ -49,6 +49,9 @@ PrimaryClient::PrimaryClient(const std::string& robot_ip, [[maybe_unused]] comm: consumer_.reset(new PrimaryConsumer()); consumer_->setErrorCodeMessageCallback(std::bind(&PrimaryClient::errorMessageCallback, this, std::placeholders::_1)); + consumer_->setKeyMessageCallback(std::bind(&PrimaryClient::keyMessageCallback, this, std::placeholders::_1)); + consumer_->setRuntimeExceptionCallback( + std::bind(&PrimaryClient::runtimeExceptionCallback, this, std::placeholders::_1)); // Configure multi consumer even though we only have one consumer as default, as this enables the user to add more // consumers after the object has been created @@ -95,6 +98,18 @@ void PrimaryClient::errorMessageCallback(ErrorCode& code) error_code_queue_.push_back(code); } +void PrimaryClient::keyMessageCallback(KeyMessage& msg) +{ + std::cout << "Key message callback: " << msg.toString() << std::endl; + std::lock_guard lock_guard(key_meassage_queue_mutex_); + key_message_queue_.push_back(msg); +} + +void PrimaryClient::runtimeExceptionCallback(RuntimeExceptionMessage& msg) +{ + std::cout << "Runtime exception: " << msg.toString() << std::endl; +} + std::deque PrimaryClient::getErrorCodes() { std::lock_guard lock_guard(error_code_queue_mutex_); From 39387ed9fd4001eaf169a76376966eaf51387935 Mon Sep 17 00:00:00 2001 From: Jacob Larsen Date: Mon, 30 Mar 2026 09:29:05 +0000 Subject: [PATCH 06/74] Add private sendScriptNoWrapping and use with member functions --- .../primary/primary_client.h | 2 + src/primary/primary_client.cpp | 47 +++++++++++++++++-- 2 files changed, 44 insertions(+), 5 deletions(-) diff --git a/include/ur_client_library/primary/primary_client.h b/include/ur_client_library/primary/primary_client.h index 6e150e479..9f9c3c77f 100644 --- a/include/ur_client_library/primary/primary_client.h +++ b/include/ur_client_library/primary/primary_client.h @@ -315,6 +315,8 @@ class PrimaryClient void keyMessageCallback(KeyMessage& msg); void runtimeExceptionCallback(RuntimeExceptionMessage& msg); + bool sendScriptNoWrapping(const std::string& program); + ScriptInfo prepare_script(std::string script, std::string script_name, ScriptTypes script_type); std::vector strip_comments_and_whitespace(std::vector script_lines); diff --git a/src/primary/primary_client.cpp b/src/primary/primary_client.cpp index ad639d095..e5b9534d9 100644 --- a/src/primary/primary_client.cpp +++ b/src/primary/primary_client.cpp @@ -253,6 +253,43 @@ ScriptInfo PrimaryClient::prepare_script(std::string script, std::string script_ return ScriptInfo(actual_script_name, prepared_script); } +bool PrimaryClient::sendScriptNoWrapping(const std::string& program) +{ + // urscripts (snippets) must end with a newline, or otherwise the controller's runtime will + // not execute them. To avoid problems, we always just append a newline here, even if + // there may already be one. + + auto program_with_newline = program + "\n"; + + size_t len = program_with_newline.size(); + const uint8_t* data = reinterpret_cast(program_with_newline.c_str()); + size_t written; + + const auto send_script_contents = [this, program_with_newline, data, len, + &written](const std::string&& description) -> bool { + if (stream_.write(data, len, written)) + { + URCL_LOG_DEBUG("Sent program to robot:\n%s", program_with_newline.c_str()); + return true; + } + const std::string error_message = "Could not send program to robot: " + description; + URCL_LOG_ERROR(error_message.c_str()); + return false; + }; + + if (send_script_contents("initial attempt")) + { + return true; + } + + if (reconnectStream()) + { + return send_script_contents("after reconnecting primary stream"); + } + + return false; +} + bool PrimaryClient::reconnectStream() { URCL_LOG_DEBUG("Closing primary stream..."); @@ -281,7 +318,7 @@ bool PrimaryClient::checkCalibration(const std::string& checksum) void PrimaryClient::commandPowerOn(const bool validate, const std::chrono::milliseconds timeout) { - if (!sendScript("power on")) + if (!sendScriptNoWrapping("power on")) { throw UrException("Failed to send power on command to robot"); } @@ -306,7 +343,7 @@ void PrimaryClient::commandPowerOn(const bool validate, const std::chrono::milli void PrimaryClient::commandPowerOff(const bool validate, const std::chrono::milliseconds timeout) { - if (!sendScript("power off")) + if (!sendScriptNoWrapping("power off")) { throw UrException("Failed to send power off command to robot"); } @@ -325,7 +362,7 @@ void PrimaryClient::commandPowerOff(const bool validate, const std::chrono::mill void PrimaryClient::commandBrakeRelease(const bool validate, const std::chrono::milliseconds timeout) { - if (!sendScript("set robotmode run")) + if (!sendScriptNoWrapping("set robotmode run")) { throw UrException("Failed to send brake release command to robot"); } @@ -344,7 +381,7 @@ void PrimaryClient::commandBrakeRelease(const bool validate, const std::chrono:: void PrimaryClient::commandUnlockProtectiveStop(const bool validate, const std::chrono::milliseconds timeout) { - if (!sendScript("set unlock protective stop")) + if (!sendScriptNoWrapping("set unlock protective stop")) { throw UrException("Failed to send unlock protective stop command to robot"); } @@ -369,7 +406,7 @@ void PrimaryClient::commandStop(const bool validate, const std::chrono::millisec throw UrException("Stopping a program while robot state is unknown. This should not happen"); } - if (!sendScript("stop program")) + if (!sendScriptNoWrapping("stop program")) { throw UrException("Failed to send the command `stop program` to robot"); } From 2adffdc96d0135e8c646a40e6d948cad6af28a4b Mon Sep 17 00:00:00 2001 From: Jacob Larsen Date: Mon, 30 Mar 2026 09:29:45 +0000 Subject: [PATCH 07/74] Check for robotmode in sendScript --- src/primary/primary_client.cpp | 41 +++++++++++++--------------------- 1 file changed, 15 insertions(+), 26 deletions(-) diff --git a/src/primary/primary_client.cpp b/src/primary/primary_client.cpp index e5b9534d9..44764f40a 100644 --- a/src/primary/primary_client.cpp +++ b/src/primary/primary_client.cpp @@ -127,35 +127,23 @@ bool PrimaryClient::sendScript(const std::string& program, std::string script_na ScriptInfo script_with_name = prepare_script(program, script_name, script_type); - auto program_with_newline = script_with_name.script_code; + std::cout << script_with_name.script_code << std::endl; - size_t len = program_with_newline.size(); - const uint8_t* data = reinterpret_cast(program_with_newline.c_str()); - size_t written; - - const auto send_script_contents = [this, program_with_newline, data, len, - &written](const std::string&& description) -> bool { - if (stream_.write(data, len, written)) - { - URCL_LOG_DEBUG("Sent program to robot:\n%s", program_with_newline.c_str()); - return true; - } - const std::string error_message = "Could not send program to robot: " + description; - URCL_LOG_ERROR(error_message.c_str()); - return false; - }; - - if (send_script_contents("initial attempt")) + RobotMode robot_mode = getRobotMode(); + while (robot_mode == RobotMode::UNKNOWN) { - return true; + URCL_LOG_INFO("Robot mode not received yet, waiting for it to be received."); + std::chrono::milliseconds update_period(100); + std::this_thread::sleep_for(update_period); + robot_mode = getRobotMode(); } - - if (reconnectStream()) + if (robot_mode != RobotMode::RUNNING) { - return send_script_contents("after reconnecting primary stream"); + URCL_LOG_ERROR("Robot is not in idle, cannot execute script."); + std::cout << "Robot is in mode: " << int(robot_mode) << std::endl; + return false; } - - return false; + return sendScriptNoWrapping(script_with_name.script_code); } std::vector PrimaryClient::strip_comments_and_whitespace(std::vector split_script) @@ -210,7 +198,8 @@ ScriptInfo PrimaryClient::prepare_script(std::string script, std::string script_ } // Is the script wrapped in a function definition? If not add one - if (stripped_script[0].find("def ") == script.npos && stripped_script[0].find("sec ") == script.npos) + if (stripped_script[0].substr(0, 4).find("def ") == script.npos && + stripped_script[0].substr(0, 4).find("sec ") == script.npos) { // Assign appropriate type std::string type; @@ -227,7 +216,7 @@ ScriptInfo PrimaryClient::prepare_script(std::string script, std::string script_ std::string start = type + " " + actual_script_name + "():"; std::string end = "end"; // Add indentation to the existing script code - for (int i = 0; i < stripped_script.size(); i++) + for (std::size_t i = 0; i < stripped_script.size(); i++) { stripped_script[i] = " " + stripped_script[i]; } From 6cc65f9917acf797d85f1ea14cd5a880ef151bec Mon Sep 17 00:00:00 2001 From: Jacob Larsen Date: Wed, 8 Apr 2026 14:00:43 +0000 Subject: [PATCH 08/74] fix name typo --- include/ur_client_library/primary/primary_client.h | 2 +- src/primary/primary_client.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/include/ur_client_library/primary/primary_client.h b/include/ur_client_library/primary/primary_client.h index 9f9c3c77f..17a33be0f 100644 --- a/include/ur_client_library/primary/primary_client.h +++ b/include/ur_client_library/primary/primary_client.h @@ -333,7 +333,7 @@ class PrimaryClient std::mutex error_code_queue_mutex_; std::deque error_code_queue_; - std::mutex key_meassage_queue_mutex_; + std::mutex key_message_queue_mutex_; std::deque key_message_queue_; }; diff --git a/src/primary/primary_client.cpp b/src/primary/primary_client.cpp index 44764f40a..af9aa7ba0 100644 --- a/src/primary/primary_client.cpp +++ b/src/primary/primary_client.cpp @@ -101,7 +101,7 @@ void PrimaryClient::errorMessageCallback(ErrorCode& code) void PrimaryClient::keyMessageCallback(KeyMessage& msg) { std::cout << "Key message callback: " << msg.toString() << std::endl; - std::lock_guard lock_guard(key_meassage_queue_mutex_); + std::lock_guard lock_guard(key_message_queue_mutex_); key_message_queue_.push_back(msg); } From 2d82abfa56c377a150205c9a4258d3f30f3f592e Mon Sep 17 00:00:00 2001 From: Jacob Larsen Date: Wed, 8 Apr 2026 14:02:13 +0000 Subject: [PATCH 09/74] Save the lates runtime exception --- include/ur_client_library/primary/primary_client.h | 3 +++ src/primary/primary_client.cpp | 3 ++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/include/ur_client_library/primary/primary_client.h b/include/ur_client_library/primary/primary_client.h index 17a33be0f..ad525016c 100644 --- a/include/ur_client_library/primary/primary_client.h +++ b/include/ur_client_library/primary/primary_client.h @@ -335,6 +335,9 @@ class PrimaryClient std::mutex key_message_queue_mutex_; std::deque key_message_queue_; + + std::mutex runtime_exception_mutex_; + std::shared_ptr latest_runtime_exception_; }; } // namespace primary_interface diff --git a/src/primary/primary_client.cpp b/src/primary/primary_client.cpp index af9aa7ba0..a991bf230 100644 --- a/src/primary/primary_client.cpp +++ b/src/primary/primary_client.cpp @@ -107,7 +107,8 @@ void PrimaryClient::keyMessageCallback(KeyMessage& msg) void PrimaryClient::runtimeExceptionCallback(RuntimeExceptionMessage& msg) { - std::cout << "Runtime exception: " << msg.toString() << std::endl; + std::scoped_lock lock(runtime_exception_mutex_); + latest_runtime_exception_ = std::make_shared(msg); } std::deque PrimaryClient::getErrorCodes() From 8697cdcd7706717f78522ecd680db5ab42dae426 Mon Sep 17 00:00:00 2001 From: Jacob Larsen Date: Wed, 8 Apr 2026 14:05:39 +0000 Subject: [PATCH 10/74] Add script execution feedback --- .../primary/primary_client.h | 3 +- src/primary/primary_client.cpp | 96 +++++++++++++++++-- 2 files changed, 90 insertions(+), 9 deletions(-) diff --git a/include/ur_client_library/primary/primary_client.h b/include/ur_client_library/primary/primary_client.h index ad525016c..a4e29e8d1 100644 --- a/include/ur_client_library/primary/primary_client.h +++ b/include/ur_client_library/primary/primary_client.h @@ -100,7 +100,8 @@ class PrimaryClient * * \returns true on successful upload, false otherwise. */ - bool sendScript(const std::string& program, std::string script_name = "", ScriptTypes script_type = ScriptTypes::DEF); + bool sendScript(const std::string& program, std::string script_name = "", ScriptTypes script_type = ScriptTypes::DEF, + int max_start_delay_ms = 1000); bool checkCalibration(const std::string& checksum); diff --git a/src/primary/primary_client.cpp b/src/primary/primary_client.cpp index a991bf230..2e1b92dbd 100644 --- a/src/primary/primary_client.cpp +++ b/src/primary/primary_client.cpp @@ -120,12 +120,9 @@ std::deque PrimaryClient::getErrorCodes() return error_codes; } -bool PrimaryClient::sendScript(const std::string& program, std::string script_name, ScriptTypes script_type) +bool PrimaryClient::sendScript(const std::string& program, std::string script_name, ScriptTypes script_type, + int max_start_delay_ms) { - // urscripts (snippets) must end with a newline, or otherwise the controller's runtime will - // not execute them. To avoid problems, we always just append a newline here, even if - // there may already be one. - ScriptInfo script_with_name = prepare_script(program, script_name, script_type); std::cout << script_with_name.script_code << std::endl; @@ -140,11 +137,94 @@ bool PrimaryClient::sendScript(const std::string& program, std::string script_na } if (robot_mode != RobotMode::RUNNING) { - URCL_LOG_ERROR("Robot is not in idle, cannot execute script."); - std::cout << "Robot is in mode: " << int(robot_mode) << std::endl; + URCL_LOG_ERROR("Robot is not running, cannot execute script."); + std::stringstream ss; + ss << "Robot is in mode: " << urcl::robotModeString(robot_mode) << " (" << int(robot_mode) << ")"; + URCL_LOG_ERROR(ss.str().c_str()); return false; } - return sendScriptNoWrapping(script_with_name.script_code); + SafetyMode safety_mode = getSafetyMode(); + if (safety_mode != SafetyMode::NORMAL && safety_mode != SafetyMode::UNDEFINED_SAFETY_MODE) + { + URCL_LOG_ERROR("Robot safety mode is not normal, cannot execute script."); + std::stringstream ss; + ss << "Robot safety mode is: " << safetyModeString(safety_mode) << " (" << unsigned(safety_mode) << ")"; + URCL_LOG_ERROR(ss.str().c_str()); + } + uint64_t exception_timestamp = 0; + { + std::scoped_lock lock(runtime_exception_mutex_); + if (latest_runtime_exception_ != nullptr) + { + exception_timestamp = latest_runtime_exception_->timestamp_; + } + } + + bool script_sent = sendScriptNoWrapping(script_with_name.script_code); + const auto script_start_time = std::chrono::system_clock::now(); + if (!script_sent) + { + URCL_LOG_ERROR("Script could not be sent."); + return false; + } + // No feedback from secondary programs, so we assume success + if (script_type == ScriptTypes::SEC) + { + return true; + } + bool script_finished = false; + bool script_started = false; + while (!script_finished) + { + { + std::scoped_lock lock(runtime_exception_mutex_); + if (latest_runtime_exception_ != nullptr && latest_runtime_exception_->timestamp_ > exception_timestamp) + { + URCL_LOG_ERROR("Runtime exception occured during script execution"); + std::stringstream ss; + ss << "Exception occured at line " << latest_runtime_exception_->line_number_ << ", column " + << latest_runtime_exception_->column_number_; + URCL_LOG_ERROR(ss.str().c_str()); + URCL_LOG_ERROR(latest_runtime_exception_->text_.c_str()); + return false; + } + } + auto errors = getErrorCodes(); + for (auto error : errors) + { + std::cout << error.to_string << std::endl; + } + { + std::scoped_lock lock(key_message_queue_mutex_); + if (key_message_queue_.size() > 0) + { + auto latest_message = key_message_queue_.back(); + if (latest_message.title_ == "PROGRAM_XXX_STOPPED" && latest_message.text_ == script_with_name.script_name) + { + URCL_LOG_INFO("Script with name %s executed successfully", script_with_name.script_name.c_str()); + return true; + } + if (!script_started && latest_message.title_ == "PROGRAM_XXX_STARTED" && + latest_message.text_ == script_with_name.script_name) + { + URCL_LOG_INFO("Script with name %s started", script_with_name.script_name.c_str()); + script_started = true; + } + } + } + auto current_time = std::chrono::system_clock::now(); + auto elapsed_time_ms = + std::chrono::duration_cast(current_time - script_start_time).count(); + + if (!script_started && elapsed_time_ms > max_start_delay_ms) + { + URCL_LOG_ERROR("Script not started within timeout"); + return false; + } + std::chrono::milliseconds wait_period(10); + std::this_thread::sleep_for(wait_period); + } + return false; } std::vector PrimaryClient::strip_comments_and_whitespace(std::vector split_script) From 7c7a03fa9e297958471b1a146f0ed499461ca2df Mon Sep 17 00:00:00 2001 From: Jacob Larsen Date: Wed, 8 Apr 2026 14:06:30 +0000 Subject: [PATCH 11/74] switch two checks in if statement --- src/primary/primary_client.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/primary/primary_client.cpp b/src/primary/primary_client.cpp index 2e1b92dbd..080b6f3ad 100644 --- a/src/primary/primary_client.cpp +++ b/src/primary/primary_client.cpp @@ -255,7 +255,7 @@ ScriptInfo PrimaryClient::prepare_script(std::string script, std::string script_ { // Validate script_name static const std::regex valid_name(R"(^[A-Za-z_][A-Za-z0-9_]*$)"); - if (!std::regex_match(script_name, valid_name) && !script_name.empty()) + if (!script_name.empty() && !std::regex_match(script_name, valid_name)) { throw urcl::UrException("Invalid script name: '" + script_name + "'. Can only contain letters, numbers and underscores. First character must be a letter or " From 7a743c8d3b29bf7aba5759e5e96219b7361edf99 Mon Sep 17 00:00:00 2001 From: Jacob Larsen Date: Wed, 8 Apr 2026 14:08:00 +0000 Subject: [PATCH 12/74] Example (very wip) --- examples/primary_client.cpp | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/examples/primary_client.cpp b/examples/primary_client.cpp index 0fb552225..2ba9ea6a8 100644 --- a/examples/primary_client.cpp +++ b/examples/primary_client.cpp @@ -3,18 +3,21 @@ int main() { + using namespace std::chrono_literals; auto notif = urcl::comm::INotifier(); auto client = urcl::primary_interface::PrimaryClient("192.168.56.101", notif); client.start(10); - // std::cout << "Robot mode: " << int(client.getRobotMode()) << std::endl; - // client.commandPowerOff(); - // std::cout << "Robot mode: " << int(client.getRobotMode()) << std::endl; - // client.commandBrakeRelease(); - // std::cout << "Robot mode: " << int(client.getRobotMode()) << std::endl; - auto s = "textmsg(\"hello\") \n #hhelllooe \n \n #helloe \n #jejfef"; - client.sendScript(s, "hello_world", urcl::primary_interface::ScriptTypes::SEC); + std::cout << "Client connected" << std::endl; + // std::this_thread::sleep_for(3000ms); + // std::cout << "Robot mode: " << int(client.getRobotMode()) << std::endl; + // client.commandPowerOff(); + // std::cout << "Robot mode: " << int(client.getRobotMode()) << std::endl; + // client.commandBrakeRelease(); + // std::cout << "Robot mode: " << int(client.getRobotMode()) << std::endl; + auto s = "movej([1,0,0,0,0,0], t=5)"; + client.sendScript(s, ""); - client.sendScript(s, "", urcl::primary_interface::ScriptTypes::DEF); - using namespace std::chrono_literals; - std::this_thread::sleep_for(2000ms); + // client.sendScript(s, "", urcl::primary_interface::ScriptTypes::DEF); + + // std::this_thread::sleep_for(2000ms); } \ No newline at end of file From 3b2538bbafac8d283637a7369ba338a04d7de5cd Mon Sep 17 00:00:00 2001 From: Jacob Larsen Date: Thu, 16 Apr 2026 09:23:44 +0000 Subject: [PATCH 13/74] Add ScriptCodeSyntaxException --- include/ur_client_library/exceptions.h | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/include/ur_client_library/exceptions.h b/include/ur_client_library/exceptions.h index 9554a80ce..c3c46e8fc 100644 --- a/include/ur_client_library/exceptions.h +++ b/include/ur_client_library/exceptions.h @@ -319,5 +319,22 @@ class RTDEInputConflictException : public UrException std::string key_; std::string message_; }; + +class ScriptCodeSyntaxException : public UrException +{ +public: + explicit ScriptCodeSyntaxException() = delete; + + explicit ScriptCodeSyntaxException(const std::string& text) : std::runtime_error(text) + { + } + + virtual ~ScriptCodeSyntaxException() = default; + + virtual const char* what() const noexcept override + { + return std::runtime_error::what(); + } +}; } // namespace urcl #endif // ifndef UR_CLIENT_LIBRARY_EXCEPTIONS_H_INCLUDED From d44d62c275396bf9af48f3d435e2c843dfc68eca Mon Sep 17 00:00:00 2001 From: Jacob Larsen Date: Thu, 16 Apr 2026 09:25:29 +0000 Subject: [PATCH 14/74] use ScriptCodeSyntaxException --- src/primary/primary_client.cpp | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/primary/primary_client.cpp b/src/primary/primary_client.cpp index 080b6f3ad..da315f57f 100644 --- a/src/primary/primary_client.cpp +++ b/src/primary/primary_client.cpp @@ -257,9 +257,10 @@ ScriptInfo PrimaryClient::prepare_script(std::string script, std::string script_ static const std::regex valid_name(R"(^[A-Za-z_][A-Za-z0-9_]*$)"); if (!script_name.empty() && !std::regex_match(script_name, valid_name)) { - throw urcl::UrException("Invalid script name: '" + script_name + - "'. Can only contain letters, numbers and underscores. First character must be a letter or " - "underscore."); + throw urcl::ScriptCodeSyntaxException("Invalid script name: '" + script_name + + "'. Can only contain letters, numbers and underscores. First character must " + "be a letter or " + "underscore."); } // Split the given script in to separate lines std::vector split_script = splitString(script, "\n"); @@ -308,8 +309,9 @@ ScriptInfo PrimaryClient::prepare_script(std::string script, std::string script_ if (stripped_script.back().find("end") == script.npos) { - throw urcl::UrException("Script contains either function definition or secondary process definition, but no 'end' " - "term. Script is invalid."); + throw urcl::ScriptCodeSyntaxException("Script contains either function definition or secondary process definition, " + "but no 'end' " + "term. Script is invalid."); } // Concatenate all the script lines in to the final script From 361a8e241b9969853dfb028a630a42990201bc4c Mon Sep 17 00:00:00 2001 From: Jacob Larsen Date: Thu, 16 Apr 2026 09:26:45 +0000 Subject: [PATCH 15/74] Add function safetyModeAllowsExecution --- .../primary/primary_client.h | 6 +++++ src/primary/primary_client.cpp | 27 +++++++++++++++++-- 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/include/ur_client_library/primary/primary_client.h b/include/ur_client_library/primary/primary_client.h index a4e29e8d1..ec8d9f2ca 100644 --- a/include/ur_client_library/primary/primary_client.h +++ b/include/ur_client_library/primary/primary_client.h @@ -300,6 +300,12 @@ class PrimaryClient * If no robot type data has been received yet, this will return UNDEFINED. */ RobotSeries getRobotSeries(); + + /* \brief Check if the current safety mode allows for script execution + * + * Safety modes allowing for execution are: NORMAL, REDUCED, RECOVERY, UNDEFINED_SAFETY_MODE + */ + bool safetyModeAllowsExecution(); private: /*! diff --git a/src/primary/primary_client.cpp b/src/primary/primary_client.cpp index da315f57f..dc32c8eac 100644 --- a/src/primary/primary_client.cpp +++ b/src/primary/primary_client.cpp @@ -120,6 +120,29 @@ std::deque PrimaryClient::getErrorCodes() return error_codes; } +bool PrimaryClient::safetyModeAllowsExecution() +{ + SafetyMode mode = getSafetyMode(); + switch (mode) + { + case SafetyMode::NORMAL: + return true; + + case SafetyMode::REDUCED: + return true; + + case SafetyMode::RECOVERY: + return true; + + // Safety mode might be unknown, as it is only updated on changes. + case SafetyMode::UNDEFINED_SAFETY_MODE: + return true; + + default: + return false; + } +} + bool PrimaryClient::sendScript(const std::string& program, std::string script_name, ScriptTypes script_type, int max_start_delay_ms) { @@ -143,8 +166,8 @@ bool PrimaryClient::sendScript(const std::string& program, std::string script_na URCL_LOG_ERROR(ss.str().c_str()); return false; } - SafetyMode safety_mode = getSafetyMode(); - if (safety_mode != SafetyMode::NORMAL && safety_mode != SafetyMode::UNDEFINED_SAFETY_MODE) + + if (!safetyModeAllowsExecution()) { URCL_LOG_ERROR("Robot safety mode is not normal, cannot execute script."); std::stringstream ss; From a0c36491fc96e05d00b684fc6e2f41d09c0d5be6 Mon Sep 17 00:00:00 2001 From: Jacob Larsen Date: Thu, 16 Apr 2026 09:27:28 +0000 Subject: [PATCH 16/74] Use std::chrono for timeout --- include/ur_client_library/primary/primary_client.h | 2 +- src/primary/primary_client.cpp | 7 +++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/include/ur_client_library/primary/primary_client.h b/include/ur_client_library/primary/primary_client.h index ec8d9f2ca..623e61ede 100644 --- a/include/ur_client_library/primary/primary_client.h +++ b/include/ur_client_library/primary/primary_client.h @@ -101,7 +101,7 @@ class PrimaryClient * \returns true on successful upload, false otherwise. */ bool sendScript(const std::string& program, std::string script_name = "", ScriptTypes script_type = ScriptTypes::DEF, - int max_start_delay_ms = 1000); + std::chrono::milliseconds timeout = std::chrono::seconds(1)); bool checkCalibration(const std::string& checksum); diff --git a/src/primary/primary_client.cpp b/src/primary/primary_client.cpp index dc32c8eac..d7f812471 100644 --- a/src/primary/primary_client.cpp +++ b/src/primary/primary_client.cpp @@ -144,7 +144,7 @@ bool PrimaryClient::safetyModeAllowsExecution() } bool PrimaryClient::sendScript(const std::string& program, std::string script_name, ScriptTypes script_type, - int max_start_delay_ms) + std::chrono::milliseconds timeout) { ScriptInfo script_with_name = prepare_script(program, script_name, script_type); @@ -236,10 +236,9 @@ bool PrimaryClient::sendScript(const std::string& program, std::string script_na } } auto current_time = std::chrono::system_clock::now(); - auto elapsed_time_ms = - std::chrono::duration_cast(current_time - script_start_time).count(); + auto elapsed_time = std::chrono::duration_cast(current_time - script_start_time); - if (!script_started && elapsed_time_ms > max_start_delay_ms) + if (!script_started && elapsed_time > timeout) { URCL_LOG_ERROR("Script not started within timeout"); return false; From 88aa3c9783692a8755e21e59cc19b2daab08df9d Mon Sep 17 00:00:00 2001 From: Jacob Larsen Date: Thu, 16 Apr 2026 09:28:41 +0000 Subject: [PATCH 17/74] more std::chrono --- src/primary/primary_client.cpp | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/primary/primary_client.cpp b/src/primary/primary_client.cpp index d7f812471..2e819c785 100644 --- a/src/primary/primary_client.cpp +++ b/src/primary/primary_client.cpp @@ -184,7 +184,6 @@ bool PrimaryClient::sendScript(const std::string& program, std::string script_na } bool script_sent = sendScriptNoWrapping(script_with_name.script_code); - const auto script_start_time = std::chrono::system_clock::now(); if (!script_sent) { URCL_LOG_ERROR("Script could not be sent."); @@ -195,9 +194,9 @@ bool PrimaryClient::sendScript(const std::string& program, std::string script_na { return true; } - bool script_finished = false; - bool script_started = false; - while (!script_finished) + const auto script_start_time = std::chrono::system_clock::now(); + // Ignore start delay if it is 0 + bool script_started = timeout == std::chrono::milliseconds(0) ? true : false; { { std::scoped_lock lock(runtime_exception_mutex_); From 391f11030d10bdda3f5f442eed05fc3d94215941 Mon Sep 17 00:00:00 2001 From: Jacob Larsen Date: Thu, 16 Apr 2026 09:30:45 +0000 Subject: [PATCH 18/74] Finish feedback loop --- src/primary/primary_client.cpp | 63 ++++++++++++++++++++++++++-------- 1 file changed, 48 insertions(+), 15 deletions(-) diff --git a/src/primary/primary_client.cpp b/src/primary/primary_client.cpp index 2e819c785..e2cfbd273 100644 --- a/src/primary/primary_client.cpp +++ b/src/primary/primary_client.cpp @@ -197,6 +197,7 @@ bool PrimaryClient::sendScript(const std::string& program, std::string script_na const auto script_start_time = std::chrono::system_clock::now(); // Ignore start delay if it is 0 bool script_started = timeout == std::chrono::milliseconds(0) ? true : false; + while (true) { { std::scoped_lock lock(runtime_exception_mutex_); @@ -205,32 +206,65 @@ bool PrimaryClient::sendScript(const std::string& program, std::string script_na URCL_LOG_ERROR("Runtime exception occured during script execution"); std::stringstream ss; ss << "Exception occured at line " << latest_runtime_exception_->line_number_ << ", column " - << latest_runtime_exception_->column_number_; + << latest_runtime_exception_->column_number_ << "\n"; + // Debug print for the user + auto script_lines = splitString(script_with_name.script_code, "\n"); + for (int i = 0; i < static_cast(script_lines.size()); i++) + { + if (!script_lines[i].empty()) + { + ss << script_lines[i] << "\n"; + } + if (i == latest_runtime_exception_->line_number_ - 1) + { + for (int j = 0; j < latest_runtime_exception_->column_number_ - 1; j++) + { + ss << " "; + } + ss << "^\n"; + } + } URCL_LOG_ERROR(ss.str().c_str()); - URCL_LOG_ERROR(latest_runtime_exception_->text_.c_str()); + URCL_LOG_ERROR("Runtime exception text: %s", latest_runtime_exception_->text_.c_str()); return false; } } + auto errors = getErrorCodes(); - for (auto error : errors) + if (errors.size() > 0) { - std::cout << error.to_string << std::endl; + URCL_LOG_ERROR("Robot encountered error(s) during script execution, stopping program"); + for (auto error : errors) + { + URCL_LOG_ERROR("Robot error code: %s", error.to_string.c_str()); + } + commandStop(); + return false; } + { std::scoped_lock lock(key_message_queue_mutex_); if (key_message_queue_.size() > 0) { - auto latest_message = key_message_queue_.back(); - if (latest_message.title_ == "PROGRAM_XXX_STOPPED" && latest_message.text_ == script_with_name.script_name) + auto key_messages = key_message_queue_; + key_message_queue_.clear(); + for (auto message : key_messages) { - URCL_LOG_INFO("Script with name %s executed successfully", script_with_name.script_name.c_str()); - return true; - } - if (!script_started && latest_message.title_ == "PROGRAM_XXX_STARTED" && - latest_message.text_ == script_with_name.script_name) - { - URCL_LOG_INFO("Script with name %s started", script_with_name.script_name.c_str()); - script_started = true; + if (message.title_ == "PROGRAM_XXX_STOPPED" && message.text_ == script_with_name.script_name) + { + URCL_LOG_INFO("Script with name %s executed successfully", script_with_name.script_name.c_str()); + return true; + } + else if (!script_started && message.title_ == "PROGRAM_XXX_STARTED" && + message.text_ == script_with_name.script_name) + { + URCL_LOG_INFO("Script with name %s started", script_with_name.script_name.c_str()); + script_started = true; + } + else // Put irrelevant messages back in the queue + { + key_message_queue_.push_back(message); + } } } } @@ -245,7 +279,6 @@ bool PrimaryClient::sendScript(const std::string& program, std::string script_na std::chrono::milliseconds wait_period(10); std::this_thread::sleep_for(wait_period); } - return false; } std::vector PrimaryClient::strip_comments_and_whitespace(std::vector split_script) From 1737b3ed6e57ba9ab52ed571855b7573a5cb61f8 Mon Sep 17 00:00:00 2001 From: Jacob Larsen Date: Thu, 16 Apr 2026 09:31:23 +0000 Subject: [PATCH 19/74] Fix safety mode error prints --- src/primary/primary_client.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/primary/primary_client.cpp b/src/primary/primary_client.cpp index e2cfbd273..fb2e647e9 100644 --- a/src/primary/primary_client.cpp +++ b/src/primary/primary_client.cpp @@ -158,6 +158,7 @@ bool PrimaryClient::sendScript(const std::string& program, std::string script_na std::this_thread::sleep_for(update_period); robot_mode = getRobotMode(); } + if (robot_mode != RobotMode::RUNNING) { URCL_LOG_ERROR("Robot is not running, cannot execute script."); @@ -169,11 +170,12 @@ bool PrimaryClient::sendScript(const std::string& program, std::string script_na if (!safetyModeAllowsExecution()) { - URCL_LOG_ERROR("Robot safety mode is not normal, cannot execute script."); + URCL_LOG_ERROR("Robot safety mode does not allow for script execution, cannot execute script."); std::stringstream ss; - ss << "Robot safety mode is: " << safetyModeString(safety_mode) << " (" << unsigned(safety_mode) << ")"; + ss << "Robot safety mode is: " << safetyModeString(getSafetyMode()) << " (" << unsigned(getSafetyMode()) << ")"; URCL_LOG_ERROR(ss.str().c_str()); } + uint64_t exception_timestamp = 0; { std::scoped_lock lock(runtime_exception_mutex_); From 64727557ed318ffb272f67211af6787c649224b5 Mon Sep 17 00:00:00 2001 From: Jacob Larsen Date: Thu, 16 Apr 2026 14:05:45 +0000 Subject: [PATCH 20/74] Refactor test_stop_command to use dashboard client, as the new implementation of sendScript is not compatible with the structure it has. --- tests/test_primary_client.cpp | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/tests/test_primary_client.cpp b/tests/test_primary_client.cpp index 1b99eafa0..72ea9ea3f 100644 --- a/tests/test_primary_client.cpp +++ b/tests/test_primary_client.cpp @@ -39,6 +39,7 @@ #include #include "ur_client_library/exceptions.h" #include "ur_client_library/helpers.h" +#include "ur_client_library/ur/dashboard_client.h" using namespace urcl; @@ -202,10 +203,21 @@ TEST_F(PrimaryClientTest, test_uninitialized_primary_client) TEST_F(PrimaryClientTest, test_stop_command) { + EXPECT_NO_THROW(client_->start()); + auto version = client_->getRobotVersion(); + urcl::DashboardClient::ClientPolicy policy = urcl::DashboardClient::ClientPolicy::G5; + std::string polyscope_prog_name = "wait_program.urp"; + if (version->major == 10) + { + policy = urcl::DashboardClient::ClientPolicy::POLYSCOPE_X; + polyscope_prog_name = "wait_program"; + } + auto dashboard_client_ = DashboardClient("192.168.56.101", policy); + + ASSERT_TRUE(dashboard_client_.connect()); // Without started communication the latest robot mode data is a nullptr EXPECT_THROW(client_->commandStop(), UrException); - EXPECT_NO_THROW(client_->start()); EXPECT_NO_THROW(client_->commandPowerOff()); EXPECT_NO_THROW(client_->commandBrakeRelease()); @@ -217,7 +229,8 @@ TEST_F(PrimaryClientTest, test_stop_command) " end\n" "end"; - EXPECT_TRUE(client_->sendScript(script_code)); + EXPECT_TRUE(dashboard_client_.commandLoadProgram(polyscope_prog_name)); + EXPECT_TRUE(dashboard_client_.commandPlay()); waitFor([this]() { return client_->getRobotModeData()->is_program_running_; }, std::chrono::seconds(5)); EXPECT_NO_THROW(client_->commandStop()); @@ -226,7 +239,7 @@ TEST_F(PrimaryClientTest, test_stop_command) // Without a program running it should not throw an exception EXPECT_NO_THROW(client_->commandStop()); - EXPECT_TRUE(client_->sendScript(script_code)); + EXPECT_TRUE(dashboard_client_.commandPlay()); waitFor([this]() { return client_->getRobotModeData()->is_program_running_; }, std::chrono::seconds(5)); EXPECT_THROW(client_->commandStop(true, std::chrono::milliseconds(1)), TimeoutException); EXPECT_NO_THROW(waitFor( @@ -236,7 +249,7 @@ TEST_F(PrimaryClientTest, test_stop_command) std::chrono::seconds(5))); // without validation - EXPECT_TRUE(client_->sendScript(script_code)); + EXPECT_TRUE(dashboard_client_.commandPlay()); waitFor([this]() { return client_->getRobotModeData()->is_program_running_; }, std::chrono::seconds(5)); EXPECT_NO_THROW(client_->commandStop(false)); EXPECT_NO_THROW(waitFor( From 60149c1fdd9f20158ee2de7566024771f22b48ca Mon Sep 17 00:00:00 2001 From: Jacob Larsen Date: Thu, 16 Apr 2026 14:06:39 +0000 Subject: [PATCH 21/74] Refactor test_program_execution_reports_exception sendScript now fails when runtime exception is thrown --- tests/test_primary_client.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_primary_client.cpp b/tests/test_primary_client.cpp index 72ea9ea3f..42b4bc172 100644 --- a/tests/test_primary_client.cpp +++ b/tests/test_primary_client.cpp @@ -403,7 +403,7 @@ TEST_F(PrimaryClientTest, test_program_execution_reports_exception) " calldoesntexist()\n" "end"; - EXPECT_TRUE(client_->sendScript(script_code)); + EXPECT_FALSE(client_->sendScript(script_code)); { // we get a RuntimeException message saying that out function doesn't exist bool answer_received = false; From bc52927215b7261a6437b0648c06930f1812399b Mon Sep 17 00:00:00 2001 From: Jacob Larsen Date: Thu, 16 Apr 2026 14:07:36 +0000 Subject: [PATCH 22/74] Add new sendScript tests --- tests/test_primary_client.cpp | 78 +++++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/tests/test_primary_client.cpp b/tests/test_primary_client.cpp index 42b4bc172..129829c2f 100644 --- a/tests/test_primary_client.cpp +++ b/tests/test_primary_client.cpp @@ -439,6 +439,84 @@ TEST_F(PrimaryClientTest, test_read_safety_mode) EXPECT_EQ(client_->getSafetyMode(), urcl::SafetyMode::NORMAL); } +TEST_F(PrimaryClientTest, test_send_script_happy_path) +{ + EXPECT_NO_THROW(client_->start()); + EXPECT_NO_THROW(client_->commandPowerOff()); + EXPECT_NO_THROW(client_->commandBrakeRelease()); + + const std::string fully_defined_script = "def test_fun():\n" + " textmsg(\"still running\")\n" + " sleep(0.1)\n" + " sync()\n" + "end"; + EXPECT_TRUE(client_->sendScript(fully_defined_script)); + + const std::string part_defined_script = "textmsg(\"still running\")\n" + "sleep(0.1)\n" + "sync()\n"; + EXPECT_TRUE(client_->sendScript(part_defined_script)); + EXPECT_TRUE(client_->sendScript(part_defined_script, "test_def", urcl::primary_interface::DEF)); + EXPECT_TRUE(client_->sendScript("textmsg(\"Still running\")", "test_sec", urcl::primary_interface::SEC)); +} + +TEST_F(PrimaryClientTest, test_send_script_fails_on_nonrunning_robot) +{ + EXPECT_NO_THROW(client_->start()); + EXPECT_NO_THROW(client_->commandPowerOff()); + EXPECT_FALSE(client_->sendScript("textmsg(\"Still running\")")); + EXPECT_NO_THROW(client_->commandPowerOn()); + EXPECT_FALSE(client_->sendScript("textmsg(\"Still running\")")); + EXPECT_NO_THROW(client_->commandBrakeRelease()); + EXPECT_TRUE(client_->sendScript("textmsg(\"Still running\")")); +} + +TEST_F(PrimaryClientTest, test_send_script_fails_on_bad_safety_mode) +{ + EXPECT_NO_THROW(client_->start()); + EXPECT_NO_THROW(client_->commandPowerOff()); + EXPECT_NO_THROW(client_->commandBrakeRelease()); + ASSERT_TRUE(client_->safetyModeAllowsExecution()); + EXPECT_FALSE(client_->sendScript("protective_stop()")); + EXPECT_FALSE(client_->sendScript("textmsg(\"Still running\")")); + EXPECT_NO_THROW(client_->commandUnlockProtectiveStop()); + EXPECT_TRUE(client_->sendScript("textmsg(\"Still running\")")); +} + +TEST_F(PrimaryClientTest, test_throw_on_malformed_scripts) +{ + EXPECT_NO_THROW(client_->start()); + const std::string script_no_end = "def test_fun():\n" + " textmsg(\"testing\")"; + EXPECT_THROW(client_->sendScript(script_no_end), urcl::ScriptCodeSyntaxException); + const std::string script_bad_name = "def 7_eight_9():\n" + " textmsg(\"testing\")" + "end"; + EXPECT_THROW(client_->sendScript(script_bad_name), urcl::ScriptCodeSyntaxException); + EXPECT_THROW(client_->sendScript("textmsg(\"testing\")", "0_errors"), urcl::ScriptCodeSyntaxException); +} + +TEST_F(PrimaryClientTest, test_fail_on_runtime_exception) +{ + EXPECT_NO_THROW(client_->start()); + EXPECT_NO_THROW(client_->commandPowerOff()); + EXPECT_NO_THROW(client_->commandBrakeRelease()); + // Non-invertible goal, should throw runtime exception + EXPECT_FALSE(client_->sendScript("movej(p[0,0,0,0,0,0])")); +} + +TEST_F(PrimaryClientTest, test_fail_on_robot_errors) +{ + EXPECT_NO_THROW(client_->start()); + EXPECT_NO_THROW(client_->commandPowerOff()); + EXPECT_NO_THROW(client_->commandBrakeRelease()); + // Impossible movement, will trigger an error and protective stop + EXPECT_FALSE(client_->sendScript("movel(p[0,0,0,0,0,0])")); + // reset the robot + client_->commandUnlockProtectiveStop(); + EXPECT_TRUE(client_->sendScript("movej([0,0,0,0,0,0])")); +} + int main(int argc, char* argv[]) { ::testing::InitGoogleTest(&argc, argv); From 6c1ed3d1b5858551c8c402fb1056a5c49f56f8c0 Mon Sep 17 00:00:00 2001 From: Jacob Larsen Date: Thu, 16 Apr 2026 14:11:02 +0000 Subject: [PATCH 23/74] Rename variable and remove debug prints --- src/primary/primary_client.cpp | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/src/primary/primary_client.cpp b/src/primary/primary_client.cpp index fb2e647e9..ca162feba 100644 --- a/src/primary/primary_client.cpp +++ b/src/primary/primary_client.cpp @@ -100,7 +100,6 @@ void PrimaryClient::errorMessageCallback(ErrorCode& code) void PrimaryClient::keyMessageCallback(KeyMessage& msg) { - std::cout << "Key message callback: " << msg.toString() << std::endl; std::lock_guard lock_guard(key_message_queue_mutex_); key_message_queue_.push_back(msg); } @@ -146,9 +145,7 @@ bool PrimaryClient::safetyModeAllowsExecution() bool PrimaryClient::sendScript(const std::string& program, std::string script_name, ScriptTypes script_type, std::chrono::milliseconds timeout) { - ScriptInfo script_with_name = prepare_script(program, script_name, script_type); - - std::cout << script_with_name.script_code << std::endl; + ScriptInfo script_info = prepare_script(program, script_name, script_type); RobotMode robot_mode = getRobotMode(); while (robot_mode == RobotMode::UNKNOWN) @@ -185,17 +182,18 @@ bool PrimaryClient::sendScript(const std::string& program, std::string script_na } } - bool script_sent = sendScriptNoWrapping(script_with_name.script_code); + bool script_sent = sendScriptNoWrapping(script_info.script_code); if (!script_sent) { URCL_LOG_ERROR("Script could not be sent."); return false; } // No feedback from secondary programs, so we assume success - if (script_type == ScriptTypes::SEC) + if (script_info.script_type == ScriptTypes::SEC) { return true; } + const auto script_start_time = std::chrono::system_clock::now(); // Ignore start delay if it is 0 bool script_started = timeout == std::chrono::milliseconds(0) ? true : false; @@ -205,12 +203,13 @@ bool PrimaryClient::sendScript(const std::string& program, std::string script_na std::scoped_lock lock(runtime_exception_mutex_); if (latest_runtime_exception_ != nullptr && latest_runtime_exception_->timestamp_ > exception_timestamp) { - URCL_LOG_ERROR("Runtime exception occured during script execution"); + URCL_LOG_ERROR("Runtime exception occured during script execution. Runtime exception type: %s", + latest_runtime_exception_->text_.c_str()); std::stringstream ss; ss << "Exception occured at line " << latest_runtime_exception_->line_number_ << ", column " << latest_runtime_exception_->column_number_ << "\n"; // Debug print for the user - auto script_lines = splitString(script_with_name.script_code, "\n"); + auto script_lines = splitString(script_info.script_code, "\n"); for (int i = 0; i < static_cast(script_lines.size()); i++) { if (!script_lines[i].empty()) @@ -227,7 +226,6 @@ bool PrimaryClient::sendScript(const std::string& program, std::string script_na } } URCL_LOG_ERROR(ss.str().c_str()); - URCL_LOG_ERROR("Runtime exception text: %s", latest_runtime_exception_->text_.c_str()); return false; } } @@ -252,15 +250,15 @@ bool PrimaryClient::sendScript(const std::string& program, std::string script_na key_message_queue_.clear(); for (auto message : key_messages) { - if (message.title_ == "PROGRAM_XXX_STOPPED" && message.text_ == script_with_name.script_name) + if (message.title_ == "PROGRAM_XXX_STOPPED" && message.text_ == script_info.script_name) { - URCL_LOG_INFO("Script with name %s executed successfully", script_with_name.script_name.c_str()); + URCL_LOG_INFO("Script with name %s executed successfully", script_info.script_name.c_str()); return true; } else if (!script_started && message.title_ == "PROGRAM_XXX_STARTED" && - message.text_ == script_with_name.script_name) + message.text_ == script_info.script_name) { - URCL_LOG_INFO("Script with name %s started", script_with_name.script_name.c_str()); + URCL_LOG_INFO("Script with name %s started", script_info.script_name.c_str()); script_started = true; } else // Put irrelevant messages back in the queue From 4dc6e75855f5318f676216b6fe2a85ada992419f Mon Sep 17 00:00:00 2001 From: Jacob Larsen Date: Thu, 16 Apr 2026 14:11:30 +0000 Subject: [PATCH 24/74] return false on bad safety mode --- src/primary/primary_client.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/primary/primary_client.cpp b/src/primary/primary_client.cpp index ca162feba..809e4e09e 100644 --- a/src/primary/primary_client.cpp +++ b/src/primary/primary_client.cpp @@ -171,6 +171,7 @@ bool PrimaryClient::sendScript(const std::string& program, std::string script_na std::stringstream ss; ss << "Robot safety mode is: " << safetyModeString(getSafetyMode()) << " (" << unsigned(getSafetyMode()) << ")"; URCL_LOG_ERROR(ss.str().c_str()); + return false; } uint64_t exception_timestamp = 0; From 637e3cb1e33debf5b2bc22f7d012d04ffac2c416 Mon Sep 17 00:00:00 2001 From: Jacob Larsen Date: Thu, 16 Apr 2026 14:11:59 +0000 Subject: [PATCH 25/74] comment with question --- src/primary/primary_client.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/primary/primary_client.cpp b/src/primary/primary_client.cpp index 809e4e09e..373ed4fec 100644 --- a/src/primary/primary_client.cpp +++ b/src/primary/primary_client.cpp @@ -274,6 +274,7 @@ bool PrimaryClient::sendScript(const std::string& program, std::string script_na if (!script_started && elapsed_time > timeout) { + // Should this stop the running program? URCL_LOG_ERROR("Script not started within timeout"); return false; } From 92e55e485bc66237f8722b06f80c8c0aa14959d8 Mon Sep 17 00:00:00 2001 From: Jacob Larsen Date: Thu, 16 Apr 2026 14:13:32 +0000 Subject: [PATCH 26/74] Refactor prepare_script and add script_type to ScriptInfo struct --- .../primary/primary_client.h | 15 ++--- src/primary/primary_client.cpp | 56 ++++++++++++------- 2 files changed, 44 insertions(+), 27 deletions(-) diff --git a/include/ur_client_library/primary/primary_client.h b/include/ur_client_library/primary/primary_client.h index 623e61ede..f26ec2fd5 100644 --- a/include/ur_client_library/primary/primary_client.h +++ b/include/ur_client_library/primary/primary_client.h @@ -48,19 +48,20 @@ namespace urcl namespace primary_interface { -struct ScriptInfo -{ - std::string script_name; - std::string script_code; - ScriptInfo(std::string name, std::string code) : script_name(name), script_code(code) {}; -}; - enum ScriptTypes { DEF = 0, SEC = 1, }; +struct ScriptInfo +{ + std::string script_name; + std::string script_code; + ScriptTypes script_type; + ScriptInfo(std::string name, std::string code, ScriptTypes type) + : script_name(name), script_code(code), script_type(type) {}; +}; class PrimaryClient { public: diff --git a/src/primary/primary_client.cpp b/src/primary/primary_client.cpp index 373ed4fec..07ade2cf6 100644 --- a/src/primary/primary_client.cpp +++ b/src/primary/primary_client.cpp @@ -309,15 +309,6 @@ std::vector PrimaryClient::strip_comments_and_whitespace(std::vecto ScriptInfo PrimaryClient::prepare_script(std::string script, std::string script_name, ScriptTypes script_type) { - // Validate script_name - static const std::regex valid_name(R"(^[A-Za-z_][A-Za-z0-9_]*$)"); - if (!script_name.empty() && !std::regex_match(script_name, valid_name)) - { - throw urcl::ScriptCodeSyntaxException("Invalid script name: '" + script_name + - "'. Can only contain letters, numbers and underscores. First character must " - "be a letter or " - "underscore."); - } // Split the given script in to separate lines std::vector split_script = splitString(script, "\n"); @@ -329,12 +320,7 @@ ScriptInfo PrimaryClient::prepare_script(std::string script, std::string script_ std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()) .count(); std::string actual_script_name = script_name.size() != 0 ? script_name : "script_" + std::to_string(current_time); - // Limit script name length to 31, to ensure backwards compatibility - if (actual_script_name.size() > 31) - { - actual_script_name = actual_script_name.substr(0, 31); - } - + ScriptTypes actual_script_type = script_type; // Is the script wrapped in a function definition? If not add one if (stripped_script[0].substr(0, 4).find("def ") == script.npos && stripped_script[0].substr(0, 4).find("sec ") == script.npos) @@ -351,7 +337,7 @@ ScriptInfo PrimaryClient::prepare_script(std::string script, std::string script_ break; } - std::string start = type + " " + actual_script_name + "():"; + std::string definition = type + " " + actual_script_name + "():"; std::string end = "end"; // Add indentation to the existing script code for (std::size_t i = 0; i < stripped_script.size(); i++) @@ -359,15 +345,45 @@ ScriptInfo PrimaryClient::prepare_script(std::string script, std::string script_ stripped_script[i] = " " + stripped_script[i]; } // Add function definition and end statement to the stripped script lines vector - stripped_script.insert(stripped_script.begin(), start); + stripped_script.insert(stripped_script.begin(), definition); stripped_script.push_back(end); } + // Otherwise extract script name and type from function + else + { + int name_end = stripped_script[0].find("("); + actual_script_name = stripped_script[0].substr(4, name_end - 4); + if (stripped_script[0].find("def") != stripped_script[0].npos) + { + actual_script_type = ScriptTypes::DEF; + } + else + { + actual_script_type = ScriptTypes::SEC; + } + } + // Validate script_name + static const std::regex valid_name(R"(^[A-Za-z_][A-Za-z0-9_]*$)"); + if (!std::regex_match(actual_script_name, valid_name)) + { + throw urcl::ScriptCodeSyntaxException("Invalid script name: '" + script_name + + "'. Can only contain letters, numbers and underscores. First character must " + "be a letter or " + "underscore."); + } + + // Limit script name length to 31, to ensure backwards compatibility + if (actual_script_name.size() > 31) + { + actual_script_name = actual_script_name.substr(0, 31); + URCL_LOG_WARN("Given script name was too long, and has been truncated. New script name is: %s", + actual_script_name.c_str()); + } if (stripped_script.back().find("end") == script.npos) { throw urcl::ScriptCodeSyntaxException("Script contains either function definition or secondary process definition, " - "but no 'end' " - "term. Script is invalid."); + "but no 'end' term. Script is invalid."); } // Concatenate all the script lines in to the final script @@ -378,7 +394,7 @@ ScriptInfo PrimaryClient::prepare_script(std::string script, std::string script_ } // Return final script code as well as the name of the script as it will be exectuted - return ScriptInfo(actual_script_name, prepared_script); + return ScriptInfo(actual_script_name, prepared_script, actual_script_type); } bool PrimaryClient::sendScriptNoWrapping(const std::string& program) From efbaa741611901d3fb16c4f377001ab3a71ba9b9 Mon Sep 17 00:00:00 2001 From: Jacob Larsen Date: Thu, 16 Apr 2026 14:34:53 +0000 Subject: [PATCH 27/74] Example, still wip --- examples/primary_client.cpp | 43 ++++++++++++++++++++++++++++--------- 1 file changed, 33 insertions(+), 10 deletions(-) diff --git a/examples/primary_client.cpp b/examples/primary_client.cpp index 2ba9ea6a8..c886bcfd0 100644 --- a/examples/primary_client.cpp +++ b/examples/primary_client.cpp @@ -8,16 +8,39 @@ int main() auto client = urcl::primary_interface::PrimaryClient("192.168.56.101", notif); client.start(10); std::cout << "Client connected" << std::endl; - // std::this_thread::sleep_for(3000ms); - // std::cout << "Robot mode: " << int(client.getRobotMode()) << std::endl; - // client.commandPowerOff(); - // std::cout << "Robot mode: " << int(client.getRobotMode()) << std::endl; - // client.commandBrakeRelease(); - // std::cout << "Robot mode: " << int(client.getRobotMode()) << std::endl; - auto s = "movej([1,0,0,0,0,0], t=5)"; - client.sendScript(s, ""); - // client.sendScript(s, "", urcl::primary_interface::ScriptTypes::DEF); + // Make sure the robot is running + client.commandBrakeRelease(); - // std::this_thread::sleep_for(2000ms); + if (!client.safetyModeAllowsExecution()) + { + std::cout << "Robot is not in a safety state where script execution is possible. Exiting." << std::endl; + return 0; + } + + const std::string fully_defined_script = R"""( +# This is a fully defined script function definition and all +# All comments in this script will be stripped before sending the script to the robot + +# Any whitespace-only lines will also be removed +def example_fun(): + movej([0,-0.75,0,0,0,0]) + sleep(0.1) + movel([0,0,-1.5,0,0,0], t=5) +end)"""; + + if (client.sendScript(fully_defined_script)) + { + // The function definition can also be omitted + // A function name will then be auto generated + client.sendScript(R"(textmsg("Successful program execution"))"); + } + // A script-function name can also be passed to the method as well as whether it should be a function or secondary + // program A timeout can also be given to limit the wait for the passed function to start. If timeout = 0, it will + // wait indefinitely. + client.sendScript(R"(textmsg("Named primary program"))", "cool_function_name", urcl::primary_interface::DEF, + std::chrono::milliseconds(2000)); + // There is however no feedback on secondary programs, so if will return successful as soon as the code is sent to the + // robot + client.sendScript(R"(textmsg("Named secondary program"))", "cool_secondary_name", urcl::primary_interface::SEC); } \ No newline at end of file From 2b1ee6ecd366a0b12529316ec26e33ab1a721a0a Mon Sep 17 00:00:00 2001 From: Jacob Larsen Date: Thu, 23 Apr 2026 07:43:13 +0000 Subject: [PATCH 28/74] Rename new sendScript to sendScriptBlocking Reinstate the old sendScript functionality --- .../ur_client_library/primary/primary_client.h | 9 +++++---- src/primary/primary_client.cpp | 18 +++++++++--------- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/include/ur_client_library/primary/primary_client.h b/include/ur_client_library/primary/primary_client.h index f26ec2fd5..81f11ffa5 100644 --- a/include/ur_client_library/primary/primary_client.h +++ b/include/ur_client_library/primary/primary_client.h @@ -101,8 +101,11 @@ class PrimaryClient * * \returns true on successful upload, false otherwise. */ - bool sendScript(const std::string& program, std::string script_name = "", ScriptTypes script_type = ScriptTypes::DEF, - std::chrono::milliseconds timeout = std::chrono::seconds(1)); + bool sendScript(const std::string& program); + + bool sendScriptBlocking(const std::string& program, std::string script_name = "", + ScriptTypes script_type = ScriptTypes::DEF, + std::chrono::milliseconds timeout = std::chrono::seconds(1)); bool checkCalibration(const std::string& checksum); @@ -323,8 +326,6 @@ class PrimaryClient void keyMessageCallback(KeyMessage& msg); void runtimeExceptionCallback(RuntimeExceptionMessage& msg); - bool sendScriptNoWrapping(const std::string& program); - ScriptInfo prepare_script(std::string script, std::string script_name, ScriptTypes script_type); std::vector strip_comments_and_whitespace(std::vector script_lines); diff --git a/src/primary/primary_client.cpp b/src/primary/primary_client.cpp index 07ade2cf6..a38f27f3c 100644 --- a/src/primary/primary_client.cpp +++ b/src/primary/primary_client.cpp @@ -142,8 +142,8 @@ bool PrimaryClient::safetyModeAllowsExecution() } } -bool PrimaryClient::sendScript(const std::string& program, std::string script_name, ScriptTypes script_type, - std::chrono::milliseconds timeout) +bool PrimaryClient::sendScriptBlocking(const std::string& program, std::string script_name, ScriptTypes script_type, + std::chrono::milliseconds timeout) { ScriptInfo script_info = prepare_script(program, script_name, script_type); @@ -183,7 +183,7 @@ bool PrimaryClient::sendScript(const std::string& program, std::string script_na } } - bool script_sent = sendScriptNoWrapping(script_info.script_code); + bool script_sent = sendScript(script_info.script_code); if (!script_sent) { URCL_LOG_ERROR("Script could not be sent."); @@ -397,7 +397,7 @@ ScriptInfo PrimaryClient::prepare_script(std::string script, std::string script_ return ScriptInfo(actual_script_name, prepared_script, actual_script_type); } -bool PrimaryClient::sendScriptNoWrapping(const std::string& program) +bool PrimaryClient::sendScript(const std::string& program) { // urscripts (snippets) must end with a newline, or otherwise the controller's runtime will // not execute them. To avoid problems, we always just append a newline here, even if @@ -462,7 +462,7 @@ bool PrimaryClient::checkCalibration(const std::string& checksum) void PrimaryClient::commandPowerOn(const bool validate, const std::chrono::milliseconds timeout) { - if (!sendScriptNoWrapping("power on")) + if (!sendScript("power on")) { throw UrException("Failed to send power on command to robot"); } @@ -487,7 +487,7 @@ void PrimaryClient::commandPowerOn(const bool validate, const std::chrono::milli void PrimaryClient::commandPowerOff(const bool validate, const std::chrono::milliseconds timeout) { - if (!sendScriptNoWrapping("power off")) + if (!sendScript("power off")) { throw UrException("Failed to send power off command to robot"); } @@ -506,7 +506,7 @@ void PrimaryClient::commandPowerOff(const bool validate, const std::chrono::mill void PrimaryClient::commandBrakeRelease(const bool validate, const std::chrono::milliseconds timeout) { - if (!sendScriptNoWrapping("set robotmode run")) + if (!sendScript("set robotmode run")) { throw UrException("Failed to send brake release command to robot"); } @@ -525,7 +525,7 @@ void PrimaryClient::commandBrakeRelease(const bool validate, const std::chrono:: void PrimaryClient::commandUnlockProtectiveStop(const bool validate, const std::chrono::milliseconds timeout) { - if (!sendScriptNoWrapping("set unlock protective stop")) + if (!sendScript("set unlock protective stop")) { throw UrException("Failed to send unlock protective stop command to robot"); } @@ -550,7 +550,7 @@ void PrimaryClient::commandStop(const bool validate, const std::chrono::millisec throw UrException("Stopping a program while robot state is unknown. This should not happen"); } - if (!sendScriptNoWrapping("stop program")) + if (!sendScript("stop program")) { throw UrException("Failed to send the command `stop program` to robot"); } From 6183bef88dbeb17fca7305f4f4b571af514ddf97 Mon Sep 17 00:00:00 2001 From: Jacob Larsen Date: Thu, 23 Apr 2026 08:02:08 +0000 Subject: [PATCH 29/74] Remove script_type parameter from sendScriptBlocking Secondary programs are still fully supported, but the input script has to be defined as one beforehand. Relatively few things can be called in a secondary program, so if one should be executed, the user should know enough to define it as such beforehand. --- examples/primary_client.cpp | 15 ++++++++----- .../primary/primary_client.h | 3 +-- src/primary/primary_client.cpp | 22 +++++-------------- 3 files changed, 16 insertions(+), 24 deletions(-) diff --git a/examples/primary_client.cpp b/examples/primary_client.cpp index c886bcfd0..77b1b1b84 100644 --- a/examples/primary_client.cpp +++ b/examples/primary_client.cpp @@ -29,18 +29,23 @@ def example_fun(): movel([0,0,-1.5,0,0,0], t=5) end)"""; - if (client.sendScript(fully_defined_script)) + if (client.sendScriptBlocking(fully_defined_script)) { // The function definition can also be omitted // A function name will then be auto generated - client.sendScript(R"(textmsg("Successful program execution"))"); + client.sendScriptBlocking(R"(textmsg("Successful program execution"))"); } // A script-function name can also be passed to the method as well as whether it should be a function or secondary // program A timeout can also be given to limit the wait for the passed function to start. If timeout = 0, it will // wait indefinitely. - client.sendScript(R"(textmsg("Named primary program"))", "cool_function_name", urcl::primary_interface::DEF, - std::chrono::milliseconds(2000)); + client.sendScriptBlocking(R"(textmsg("Named primary program"))", "cool_function_name", + std::chrono::milliseconds(2000)); // There is however no feedback on secondary programs, so if will return successful as soon as the code is sent to the // robot - client.sendScript(R"(textmsg("Named secondary program"))", "cool_secondary_name", urcl::primary_interface::SEC); + std::string secondary_script = R"(sec sec_script(): + (textmsg("Named secondary program") +end +)"; + + client.sendScriptBlocking(secondary_script, "cool_secondary_name"); } \ No newline at end of file diff --git a/include/ur_client_library/primary/primary_client.h b/include/ur_client_library/primary/primary_client.h index 81f11ffa5..71bfb5ad7 100644 --- a/include/ur_client_library/primary/primary_client.h +++ b/include/ur_client_library/primary/primary_client.h @@ -104,7 +104,6 @@ class PrimaryClient bool sendScript(const std::string& program); bool sendScriptBlocking(const std::string& program, std::string script_name = "", - ScriptTypes script_type = ScriptTypes::DEF, std::chrono::milliseconds timeout = std::chrono::seconds(1)); bool checkCalibration(const std::string& checksum); @@ -326,7 +325,7 @@ class PrimaryClient void keyMessageCallback(KeyMessage& msg); void runtimeExceptionCallback(RuntimeExceptionMessage& msg); - ScriptInfo prepare_script(std::string script, std::string script_name, ScriptTypes script_type); + ScriptInfo prepare_script(std::string script, std::string script_name); std::vector strip_comments_and_whitespace(std::vector script_lines); PrimaryParser parser_; diff --git a/src/primary/primary_client.cpp b/src/primary/primary_client.cpp index a38f27f3c..7f2ebfc1a 100644 --- a/src/primary/primary_client.cpp +++ b/src/primary/primary_client.cpp @@ -142,10 +142,10 @@ bool PrimaryClient::safetyModeAllowsExecution() } } -bool PrimaryClient::sendScriptBlocking(const std::string& program, std::string script_name, ScriptTypes script_type, +bool PrimaryClient::sendScriptBlocking(const std::string& program, std::string script_name, std::chrono::milliseconds timeout) { - ScriptInfo script_info = prepare_script(program, script_name, script_type); + ScriptInfo script_info = prepare_script(program, script_name); RobotMode robot_mode = getRobotMode(); while (robot_mode == RobotMode::UNKNOWN) @@ -307,7 +307,7 @@ std::vector PrimaryClient::strip_comments_and_whitespace(std::vecto return stripped_script; } -ScriptInfo PrimaryClient::prepare_script(std::string script, std::string script_name, ScriptTypes script_type) +ScriptInfo PrimaryClient::prepare_script(std::string script, std::string script_name) { // Split the given script in to separate lines std::vector split_script = splitString(script, "\n"); @@ -320,24 +320,12 @@ ScriptInfo PrimaryClient::prepare_script(std::string script, std::string script_ std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()) .count(); std::string actual_script_name = script_name.size() != 0 ? script_name : "script_" + std::to_string(current_time); - ScriptTypes actual_script_type = script_type; + ScriptTypes actual_script_type = urcl::primary_interface::ScriptTypes::DEF; // Is the script wrapped in a function definition? If not add one if (stripped_script[0].substr(0, 4).find("def ") == script.npos && stripped_script[0].substr(0, 4).find("sec ") == script.npos) { - // Assign appropriate type - std::string type; - switch (script_type) - { - case ScriptTypes::DEF: - type = "def"; - break; - case ScriptTypes::SEC: - type = "sec"; - break; - } - - std::string definition = type + " " + actual_script_name + "():"; + std::string definition = "def " + actual_script_name + "():"; std::string end = "end"; // Add indentation to the existing script code for (std::size_t i = 0; i < stripped_script.size(); i++) From 46f26584538e98d74f8bdfb62b2e7a4579ba0469 Mon Sep 17 00:00:00 2001 From: Jacob Larsen Date: Thu, 23 Apr 2026 08:06:10 +0000 Subject: [PATCH 30/74] Fix tests after changing around function names --- tests/test_primary_client.cpp | 56 ++++++++++++++--------------------- 1 file changed, 22 insertions(+), 34 deletions(-) diff --git a/tests/test_primary_client.cpp b/tests/test_primary_client.cpp index 129829c2f..d6dcbb3b0 100644 --- a/tests/test_primary_client.cpp +++ b/tests/test_primary_client.cpp @@ -39,7 +39,6 @@ #include #include "ur_client_library/exceptions.h" #include "ur_client_library/helpers.h" -#include "ur_client_library/ur/dashboard_client.h" using namespace urcl; @@ -203,21 +202,10 @@ TEST_F(PrimaryClientTest, test_uninitialized_primary_client) TEST_F(PrimaryClientTest, test_stop_command) { - EXPECT_NO_THROW(client_->start()); - auto version = client_->getRobotVersion(); - urcl::DashboardClient::ClientPolicy policy = urcl::DashboardClient::ClientPolicy::G5; - std::string polyscope_prog_name = "wait_program.urp"; - if (version->major == 10) - { - policy = urcl::DashboardClient::ClientPolicy::POLYSCOPE_X; - polyscope_prog_name = "wait_program"; - } - auto dashboard_client_ = DashboardClient("192.168.56.101", policy); - - ASSERT_TRUE(dashboard_client_.connect()); // Without started communication the latest robot mode data is a nullptr EXPECT_THROW(client_->commandStop(), UrException); + EXPECT_NO_THROW(client_->start()); EXPECT_NO_THROW(client_->commandPowerOff()); EXPECT_NO_THROW(client_->commandBrakeRelease()); @@ -229,8 +217,7 @@ TEST_F(PrimaryClientTest, test_stop_command) " end\n" "end"; - EXPECT_TRUE(dashboard_client_.commandLoadProgram(polyscope_prog_name)); - EXPECT_TRUE(dashboard_client_.commandPlay()); + EXPECT_TRUE(client_->sendScript(script_code)); waitFor([this]() { return client_->getRobotModeData()->is_program_running_; }, std::chrono::seconds(5)); EXPECT_NO_THROW(client_->commandStop()); @@ -239,7 +226,7 @@ TEST_F(PrimaryClientTest, test_stop_command) // Without a program running it should not throw an exception EXPECT_NO_THROW(client_->commandStop()); - EXPECT_TRUE(dashboard_client_.commandPlay()); + EXPECT_TRUE(client_->sendScript(script_code)); waitFor([this]() { return client_->getRobotModeData()->is_program_running_; }, std::chrono::seconds(5)); EXPECT_THROW(client_->commandStop(true, std::chrono::milliseconds(1)), TimeoutException); EXPECT_NO_THROW(waitFor( @@ -249,7 +236,7 @@ TEST_F(PrimaryClientTest, test_stop_command) std::chrono::seconds(5))); // without validation - EXPECT_TRUE(dashboard_client_.commandPlay()); + EXPECT_TRUE(client_->sendScript(script_code)); waitFor([this]() { return client_->getRobotModeData()->is_program_running_; }, std::chrono::seconds(5)); EXPECT_NO_THROW(client_->commandStop(false)); EXPECT_NO_THROW(waitFor( @@ -450,25 +437,26 @@ TEST_F(PrimaryClientTest, test_send_script_happy_path) " sleep(0.1)\n" " sync()\n" "end"; - EXPECT_TRUE(client_->sendScript(fully_defined_script)); + EXPECT_TRUE(client_->sendScriptBlocking(fully_defined_script)); const std::string part_defined_script = "textmsg(\"still running\")\n" "sleep(0.1)\n" "sync()\n"; - EXPECT_TRUE(client_->sendScript(part_defined_script)); - EXPECT_TRUE(client_->sendScript(part_defined_script, "test_def", urcl::primary_interface::DEF)); - EXPECT_TRUE(client_->sendScript("textmsg(\"Still running\")", "test_sec", urcl::primary_interface::SEC)); + EXPECT_TRUE(client_->sendScriptBlocking(part_defined_script)); + EXPECT_TRUE(client_->sendScriptBlocking(part_defined_script, "test_def")); + std::string sec_script = "sec test_sec():\n textmsg(\"Still running\")\nend"; + EXPECT_TRUE(client_->sendScriptBlocking(sec_script, "test_sec")); } TEST_F(PrimaryClientTest, test_send_script_fails_on_nonrunning_robot) { EXPECT_NO_THROW(client_->start()); EXPECT_NO_THROW(client_->commandPowerOff()); - EXPECT_FALSE(client_->sendScript("textmsg(\"Still running\")")); + EXPECT_FALSE(client_->sendScriptBlocking("textmsg(\"Still running\")")); EXPECT_NO_THROW(client_->commandPowerOn()); - EXPECT_FALSE(client_->sendScript("textmsg(\"Still running\")")); + EXPECT_FALSE(client_->sendScriptBlocking("textmsg(\"Still running\")")); EXPECT_NO_THROW(client_->commandBrakeRelease()); - EXPECT_TRUE(client_->sendScript("textmsg(\"Still running\")")); + EXPECT_TRUE(client_->sendScriptBlocking("textmsg(\"Still running\")")); } TEST_F(PrimaryClientTest, test_send_script_fails_on_bad_safety_mode) @@ -477,10 +465,10 @@ TEST_F(PrimaryClientTest, test_send_script_fails_on_bad_safety_mode) EXPECT_NO_THROW(client_->commandPowerOff()); EXPECT_NO_THROW(client_->commandBrakeRelease()); ASSERT_TRUE(client_->safetyModeAllowsExecution()); - EXPECT_FALSE(client_->sendScript("protective_stop()")); - EXPECT_FALSE(client_->sendScript("textmsg(\"Still running\")")); + EXPECT_FALSE(client_->sendScriptBlocking("protective_stop()")); + EXPECT_FALSE(client_->sendScriptBlocking("textmsg(\"Still running\")")); EXPECT_NO_THROW(client_->commandUnlockProtectiveStop()); - EXPECT_TRUE(client_->sendScript("textmsg(\"Still running\")")); + EXPECT_TRUE(client_->sendScriptBlocking("textmsg(\"Still running\")")); } TEST_F(PrimaryClientTest, test_throw_on_malformed_scripts) @@ -488,12 +476,12 @@ TEST_F(PrimaryClientTest, test_throw_on_malformed_scripts) EXPECT_NO_THROW(client_->start()); const std::string script_no_end = "def test_fun():\n" " textmsg(\"testing\")"; - EXPECT_THROW(client_->sendScript(script_no_end), urcl::ScriptCodeSyntaxException); + EXPECT_THROW(client_->sendScriptBlocking(script_no_end), urcl::ScriptCodeSyntaxException); const std::string script_bad_name = "def 7_eight_9():\n" - " textmsg(\"testing\")" + " textmsg(\"testing\")\n" "end"; - EXPECT_THROW(client_->sendScript(script_bad_name), urcl::ScriptCodeSyntaxException); - EXPECT_THROW(client_->sendScript("textmsg(\"testing\")", "0_errors"), urcl::ScriptCodeSyntaxException); + EXPECT_THROW(client_->sendScriptBlocking(script_bad_name), urcl::ScriptCodeSyntaxException); + EXPECT_THROW(client_->sendScriptBlocking("textmsg(\"testing\")", "0_errors"), urcl::ScriptCodeSyntaxException); } TEST_F(PrimaryClientTest, test_fail_on_runtime_exception) @@ -502,7 +490,7 @@ TEST_F(PrimaryClientTest, test_fail_on_runtime_exception) EXPECT_NO_THROW(client_->commandPowerOff()); EXPECT_NO_THROW(client_->commandBrakeRelease()); // Non-invertible goal, should throw runtime exception - EXPECT_FALSE(client_->sendScript("movej(p[0,0,0,0,0,0])")); + EXPECT_FALSE(client_->sendScriptBlocking("movej(p[0,0,0,0,0,0])")); } TEST_F(PrimaryClientTest, test_fail_on_robot_errors) @@ -511,10 +499,10 @@ TEST_F(PrimaryClientTest, test_fail_on_robot_errors) EXPECT_NO_THROW(client_->commandPowerOff()); EXPECT_NO_THROW(client_->commandBrakeRelease()); // Impossible movement, will trigger an error and protective stop - EXPECT_FALSE(client_->sendScript("movel(p[0,0,0,0,0,0])")); + EXPECT_FALSE(client_->sendScriptBlocking("movel(p[0,0,0,0,0,0])")); // reset the robot client_->commandUnlockProtectiveStop(); - EXPECT_TRUE(client_->sendScript("movej([0,0,0,0,0,0])")); + EXPECT_TRUE(client_->sendScriptBlocking("movej([0,0,0,0,0,0])")); } int main(int argc, char* argv[]) From 6cb44f70b1605784152244179119f28232c7d835 Mon Sep 17 00:00:00 2001 From: Jacob Larsen Date: Thu, 23 Apr 2026 08:53:38 +0000 Subject: [PATCH 31/74] Add docstring to sencScriptBlocking --- .../primary/primary_client.h | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/include/ur_client_library/primary/primary_client.h b/include/ur_client_library/primary/primary_client.h index 71bfb5ad7..41b5da5ef 100644 --- a/include/ur_client_library/primary/primary_client.h +++ b/include/ur_client_library/primary/primary_client.h @@ -103,6 +103,27 @@ class PrimaryClient */ bool sendScript(const std::string& program); + /*! + * \brief Send a custom script program to the robot, and wait for the execution result. + * + * The given code must be valid according the UR Scripting Manual. The given script code will be automatically wrapped + * in a function definition, if it is not already. Secondary programs can also be passed to this function, but must be + * fully defined as a secondary program when calling. Secondary programs create no feedback, so this function will + * return true as soon as the program is uploaded successfully to the robot (same as the sendScript function). + * + * \param program URScript code that shall be executed by the robot. + * + * \param script_name Name of the script to be executed. This will be ignored, if the given script already defines a + * function name. The script name will be used in log messages in both the client library and in the robot logs. If no + * name is defined in any way, the script will be given a generic, but unique, name. + * + * \param timeout Amount of time to allow before the robot must have confirmed that the script has been started. If + * timeout is 0, it will be ignored. Default value: 1 second + * + * \throw urcl::ScriptCodeSyntaxException if the given script code has syntax errors, which are checked here. + * + * \returns true on successful execution of the script, false otherwise + */ bool sendScriptBlocking(const std::string& program, std::string script_name = "", std::chrono::milliseconds timeout = std::chrono::seconds(1)); From 9477026372d4714adaf34af62de698abcf22abad Mon Sep 17 00:00:00 2001 From: Jacob Larsen Date: Thu, 23 Apr 2026 08:56:37 +0000 Subject: [PATCH 32/74] Disable some compiler warnings for primary_client.cpp --- CMakeLists.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index f882aa57f..0ebb6d344 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -87,6 +87,7 @@ else() src/ur/dashboard_client.cpp src/ur/dashboard_client_implementation_g5.cpp src/ur/dashboard_client_implementation_x.cpp + src/primary/primary_client.cpp PROPERTIES COMPILE_OPTIONS "-Wno-maybe-uninitialized") endif() From c4655c786021fe32fd1f191e365012d21fe52a3b Mon Sep 17 00:00:00 2001 From: Jacob Larsen Date: Thu, 23 Apr 2026 09:14:00 +0000 Subject: [PATCH 33/74] Improve error code handling --- src/primary/primary_client.cpp | 39 ++++++++++++++++++++++++++++------ 1 file changed, 33 insertions(+), 6 deletions(-) diff --git a/src/primary/primary_client.cpp b/src/primary/primary_client.cpp index 7f2ebfc1a..f53e9e8a7 100644 --- a/src/primary/primary_client.cpp +++ b/src/primary/primary_client.cpp @@ -234,13 +234,38 @@ bool PrimaryClient::sendScriptBlocking(const std::string& program, std::string s auto errors = getErrorCodes(); if (errors.size() > 0) { - URCL_LOG_ERROR("Robot encountered error(s) during script execution, stopping program"); + bool is_error = false; + bool is_read_only = false; for (auto error : errors) { - URCL_LOG_ERROR("Robot error code: %s", error.to_string.c_str()); + if (error.report_level == ReportLevel::VIOLATION || error.report_level == ReportLevel::FAULT) + { + URCL_LOG_ERROR("Robot error code with severity VIOLATION or FAULT received during script execution. Robot " + "error code: %s", + error.to_string.c_str()); + is_error = true; + } + else if (error.message_code == 210) + { + is_error = true; + is_read_only = true; + } + } + if (is_error) + { + if (!is_read_only) + { + commandStop(); + } + else + { + URCL_LOG_ERROR("Script cannot be executed since primary client is connected to a read-only primary " + "interface. If you have switched from local to remote mode recently, try reconnecting the " + "primary client and send the script code again."); + } + URCL_LOG_ERROR("Script execution failed due to error code(s) received from robot."); + return false; } - commandStop(); - return false; } { @@ -356,7 +381,8 @@ ScriptInfo PrimaryClient::prepare_script(std::string script, std::string script_ if (!std::regex_match(actual_script_name, valid_name)) { throw urcl::ScriptCodeSyntaxException("Invalid script name: '" + script_name + - "'. Can only contain letters, numbers and underscores. First character must " + "'. Can only contain letters, numbers and underscores. First character " + "must " "be a letter or " "underscore."); } @@ -370,7 +396,8 @@ ScriptInfo PrimaryClient::prepare_script(std::string script, std::string script_ } if (stripped_script.back().find("end") == script.npos) { - throw urcl::ScriptCodeSyntaxException("Script contains either function definition or secondary process definition, " + throw urcl::ScriptCodeSyntaxException("Script contains either function definition or secondary process " + "definition, " "but no 'end' term. Script is invalid."); } From 15d5ca1a1098136b4d1a8868e20df634132e2cd5 Mon Sep 17 00:00:00 2001 From: Jacob Larsen Date: Thu, 23 Apr 2026 10:19:57 +0000 Subject: [PATCH 34/74] Add fail_on_warning parameter --- include/ur_client_library/primary/primary_client.h | 5 ++++- src/primary/primary_client.cpp | 14 +++++++++++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/include/ur_client_library/primary/primary_client.h b/include/ur_client_library/primary/primary_client.h index 41b5da5ef..45cfab48b 100644 --- a/include/ur_client_library/primary/primary_client.h +++ b/include/ur_client_library/primary/primary_client.h @@ -120,12 +120,15 @@ class PrimaryClient * \param timeout Amount of time to allow before the robot must have confirmed that the script has been started. If * timeout is 0, it will be ignored. Default value: 1 second * + * \param fail_on_warnings Whether or not the function should report a failure, if the robot reports a warning-level + * error during execution. Default true + * * \throw urcl::ScriptCodeSyntaxException if the given script code has syntax errors, which are checked here. * * \returns true on successful execution of the script, false otherwise */ bool sendScriptBlocking(const std::string& program, std::string script_name = "", - std::chrono::milliseconds timeout = std::chrono::seconds(1)); + std::chrono::milliseconds timeout = std::chrono::seconds(1), bool fail_on_warnings = true); bool checkCalibration(const std::string& checksum); diff --git a/src/primary/primary_client.cpp b/src/primary/primary_client.cpp index f53e9e8a7..55438a2c6 100644 --- a/src/primary/primary_client.cpp +++ b/src/primary/primary_client.cpp @@ -143,7 +143,7 @@ bool PrimaryClient::safetyModeAllowsExecution() } bool PrimaryClient::sendScriptBlocking(const std::string& program, std::string script_name, - std::chrono::milliseconds timeout) + std::chrono::milliseconds timeout, bool fail_on_warnings) { ScriptInfo script_info = prepare_script(program, script_name); @@ -235,6 +235,7 @@ bool PrimaryClient::sendScriptBlocking(const std::string& program, std::string s if (errors.size() > 0) { bool is_error = false; + bool is_warning = false; bool is_read_only = false; for (auto error : errors) { @@ -245,6 +246,13 @@ bool PrimaryClient::sendScriptBlocking(const std::string& program, std::string s error.to_string.c_str()); is_error = true; } + if (error.report_level == ReportLevel::WARNING) + { + URCL_LOG_ERROR("Robot error code with severity WARNING received during script execution. Robot " + "error code: %s", + error.to_string.c_str()); + is_warning = true; + } else if (error.message_code == 210) { is_error = true; @@ -266,6 +274,10 @@ bool PrimaryClient::sendScriptBlocking(const std::string& program, std::string s URCL_LOG_ERROR("Script execution failed due to error code(s) received from robot."); return false; } + if (is_warning && fail_on_warnings) + { + return false; + } } { From 236950199d28bcdad573eba50a322cbd3da97605 Mon Sep 17 00:00:00 2001 From: Jacob Larsen Date: Thu, 23 Apr 2026 10:20:17 +0000 Subject: [PATCH 35/74] Rename some tests --- tests/test_primary_client.cpp | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/tests/test_primary_client.cpp b/tests/test_primary_client.cpp index d6dcbb3b0..b37d56d83 100644 --- a/tests/test_primary_client.cpp +++ b/tests/test_primary_client.cpp @@ -390,7 +390,7 @@ TEST_F(PrimaryClientTest, test_program_execution_reports_exception) " calldoesntexist()\n" "end"; - EXPECT_FALSE(client_->sendScript(script_code)); + EXPECT_TRUE(client_->sendScript(script_code)); { // we get a RuntimeException message saying that out function doesn't exist bool answer_received = false; @@ -426,7 +426,7 @@ TEST_F(PrimaryClientTest, test_read_safety_mode) EXPECT_EQ(client_->getSafetyMode(), urcl::SafetyMode::NORMAL); } -TEST_F(PrimaryClientTest, test_send_script_happy_path) +TEST_F(PrimaryClientTest, test_send_script_blocking_happy_path) { EXPECT_NO_THROW(client_->start()); EXPECT_NO_THROW(client_->commandPowerOff()); @@ -448,7 +448,7 @@ TEST_F(PrimaryClientTest, test_send_script_happy_path) EXPECT_TRUE(client_->sendScriptBlocking(sec_script, "test_sec")); } -TEST_F(PrimaryClientTest, test_send_script_fails_on_nonrunning_robot) +TEST_F(PrimaryClientTest, test_send_script_blocking_fails_on_nonrunning_robot) { EXPECT_NO_THROW(client_->start()); EXPECT_NO_THROW(client_->commandPowerOff()); @@ -459,19 +459,20 @@ TEST_F(PrimaryClientTest, test_send_script_fails_on_nonrunning_robot) EXPECT_TRUE(client_->sendScriptBlocking("textmsg(\"Still running\")")); } -TEST_F(PrimaryClientTest, test_send_script_fails_on_bad_safety_mode) +TEST_F(PrimaryClientTest, test_send_script_blocking_fails_on_bad_safety_mode) { EXPECT_NO_THROW(client_->start()); EXPECT_NO_THROW(client_->commandPowerOff()); EXPECT_NO_THROW(client_->commandBrakeRelease()); ASSERT_TRUE(client_->safetyModeAllowsExecution()); + EXPECT_FALSE(client_->sendScriptBlocking("protective_stop()")); EXPECT_FALSE(client_->sendScriptBlocking("textmsg(\"Still running\")")); EXPECT_NO_THROW(client_->commandUnlockProtectiveStop()); EXPECT_TRUE(client_->sendScriptBlocking("textmsg(\"Still running\")")); } -TEST_F(PrimaryClientTest, test_throw_on_malformed_scripts) +TEST_F(PrimaryClientTest, test_send_script_blocking_throw_on_malformed_scripts) { EXPECT_NO_THROW(client_->start()); const std::string script_no_end = "def test_fun():\n" @@ -484,7 +485,7 @@ TEST_F(PrimaryClientTest, test_throw_on_malformed_scripts) EXPECT_THROW(client_->sendScriptBlocking("textmsg(\"testing\")", "0_errors"), urcl::ScriptCodeSyntaxException); } -TEST_F(PrimaryClientTest, test_fail_on_runtime_exception) +TEST_F(PrimaryClientTest, test_send_script_blocking_fail_on_runtime_exception) { EXPECT_NO_THROW(client_->start()); EXPECT_NO_THROW(client_->commandPowerOff()); @@ -493,7 +494,7 @@ TEST_F(PrimaryClientTest, test_fail_on_runtime_exception) EXPECT_FALSE(client_->sendScriptBlocking("movej(p[0,0,0,0,0,0])")); } -TEST_F(PrimaryClientTest, test_fail_on_robot_errors) +TEST_F(PrimaryClientTest, test_send_script_blocking_fail_on_robot_errors) { EXPECT_NO_THROW(client_->start()); EXPECT_NO_THROW(client_->commandPowerOff()); From fd05d057bb58168cbdd0e422cc958ffecf96c3f4 Mon Sep 17 00:00:00 2001 From: Jacob Larsen Date: Thu, 30 Apr 2026 07:22:22 +0000 Subject: [PATCH 36/74] Refactor/clarify actual_script_name ternary --- src/primary/primary_client.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/primary/primary_client.cpp b/src/primary/primary_client.cpp index 55438a2c6..c7c00edcd 100644 --- a/src/primary/primary_client.cpp +++ b/src/primary/primary_client.cpp @@ -356,7 +356,7 @@ ScriptInfo PrimaryClient::prepare_script(std::string script, std::string script_ unsigned int current_time = std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()) .count(); - std::string actual_script_name = script_name.size() != 0 ? script_name : "script_" + std::to_string(current_time); + std::string actual_script_name = script_name.empty() ? "script_" + std::to_string(current_time) : script_name; ScriptTypes actual_script_type = urcl::primary_interface::ScriptTypes::DEF; // Is the script wrapped in a function definition? If not add one if (stripped_script[0].substr(0, 4).find("def ") == script.npos && From 3d7ad64be076bdc277cc26cc8f101d835ffe8a7c Mon Sep 17 00:00:00 2001 From: Jacob Larsen Date: Thu, 30 Apr 2026 07:24:22 +0000 Subject: [PATCH 37/74] formatting --- include/ur_client_library/primary/primary_client.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/ur_client_library/primary/primary_client.h b/include/ur_client_library/primary/primary_client.h index 45cfab48b..f3ff012b4 100644 --- a/include/ur_client_library/primary/primary_client.h +++ b/include/ur_client_library/primary/primary_client.h @@ -327,7 +327,7 @@ class PrimaryClient * If no robot type data has been received yet, this will return UNDEFINED. */ RobotSeries getRobotSeries(); - + /* \brief Check if the current safety mode allows for script execution * * Safety modes allowing for execution are: NORMAL, REDUCED, RECOVERY, UNDEFINED_SAFETY_MODE From 29c6753b3358072444b7b675cc60e1653c23808a Mon Sep 17 00:00:00 2001 From: Jacob Larsen Date: Thu, 30 Apr 2026 07:46:43 +0000 Subject: [PATCH 38/74] Fix data types to avoid implicit conversions --- src/primary/primary_client.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/primary/primary_client.cpp b/src/primary/primary_client.cpp index c7c00edcd..91b845c53 100644 --- a/src/primary/primary_client.cpp +++ b/src/primary/primary_client.cpp @@ -353,7 +353,7 @@ ScriptInfo PrimaryClient::prepare_script(std::string script, std::string script_ std::vector stripped_script = strip_comments_and_whitespace(split_script); // Use given scipt name or create one - unsigned int current_time = + int64_t current_time = std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()) .count(); std::string actual_script_name = script_name.empty() ? "script_" + std::to_string(current_time) : script_name; @@ -376,7 +376,7 @@ ScriptInfo PrimaryClient::prepare_script(std::string script, std::string script_ // Otherwise extract script name and type from function else { - int name_end = stripped_script[0].find("("); + size_t name_end = stripped_script[0].find("("); actual_script_name = stripped_script[0].substr(4, name_end - 4); if (stripped_script[0].find("def") != stripped_script[0].npos) { From f6808ecb2f2c0a44a7f265c5a4baf1e71e2add14 Mon Sep 17 00:00:00 2001 From: Jacob Larsen Date: Thu, 30 Apr 2026 09:48:29 +0000 Subject: [PATCH 39/74] Test for failure on bad script code --- tests/test_primary_client.cpp | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/tests/test_primary_client.cpp b/tests/test_primary_client.cpp index b37d56d83..35a7a9a96 100644 --- a/tests/test_primary_client.cpp +++ b/tests/test_primary_client.cpp @@ -506,6 +506,28 @@ TEST_F(PrimaryClientTest, test_send_script_blocking_fail_on_robot_errors) EXPECT_TRUE(client_->sendScriptBlocking("movej([0,0,0,0,0,0])")); } +TEST_F(PrimaryClientTest, test_send_script_blocking_fail_on_bad_script) +{ + EXPECT_NO_THROW(client_->start()); + EXPECT_NO_THROW(client_->commandPowerOff()); + EXPECT_NO_THROW(client_->commandBrakeRelease()); + + // auto consumer = std::make_shared(); + + // client_->addPrimaryConsumer(consumer); + + EXPECT_FALSE(client_->sendScriptBlocking("non_existing_func()")); + + // auto message = consumer->getOrWaitForMessage(); + // auto typed_msg = std::dynamic_pointer_cast(message); + // std::cout << typed_msg->toString() << std::endl; + const std::string script_code = "def illegal_fun():\n" + " calldoesntexist()\n" + "end"; + + EXPECT_FALSE(client_->sendScriptBlocking(script_code)); +} + int main(int argc, char* argv[]) { ::testing::InitGoogleTest(&argc, argv); From d80d973171b93a60cd7d6704462af5d248229bf3 Mon Sep 17 00:00:00 2001 From: Jacob Larsen Date: Thu, 30 Apr 2026 09:49:38 +0000 Subject: [PATCH 40/74] Ignore runtime exception timestamps --- src/primary/primary_client.cpp | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/primary/primary_client.cpp b/src/primary/primary_client.cpp index 91b845c53..ff35471a3 100644 --- a/src/primary/primary_client.cpp +++ b/src/primary/primary_client.cpp @@ -174,13 +174,9 @@ bool PrimaryClient::sendScriptBlocking(const std::string& program, std::string s return false; } - uint64_t exception_timestamp = 0; { std::scoped_lock lock(runtime_exception_mutex_); - if (latest_runtime_exception_ != nullptr) - { - exception_timestamp = latest_runtime_exception_->timestamp_; - } + latest_runtime_exception_ = nullptr; } bool script_sent = sendScript(script_info.script_code); @@ -202,7 +198,7 @@ bool PrimaryClient::sendScriptBlocking(const std::string& program, std::string s { { std::scoped_lock lock(runtime_exception_mutex_); - if (latest_runtime_exception_ != nullptr && latest_runtime_exception_->timestamp_ > exception_timestamp) + if (latest_runtime_exception_ != nullptr) { URCL_LOG_ERROR("Runtime exception occured during script execution. Runtime exception type: %s", latest_runtime_exception_->text_.c_str()); From db30adc17c7bdc2d57494bc381b070b52dbb886b Mon Sep 17 00:00:00 2001 From: Jacob Larsen Date: Thu, 30 Apr 2026 09:50:04 +0000 Subject: [PATCH 41/74] Separate check for 210 error code --- src/primary/primary_client.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/primary/primary_client.cpp b/src/primary/primary_client.cpp index ff35471a3..1b35d61de 100644 --- a/src/primary/primary_client.cpp +++ b/src/primary/primary_client.cpp @@ -249,7 +249,7 @@ bool PrimaryClient::sendScriptBlocking(const std::string& program, std::string s error.to_string.c_str()); is_warning = true; } - else if (error.message_code == 210) + if (error.message_code == 210) { is_error = true; is_read_only = true; From 5eacfa85739f4027089142fb3a6743c16398cecf Mon Sep 17 00:00:00 2001 From: Jacob Larsen Date: Thu, 30 Apr 2026 09:50:52 +0000 Subject: [PATCH 42/74] Copy out key message queue before processing --- src/primary/primary_client.cpp | 38 +++++++++++++++++----------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/src/primary/primary_client.cpp b/src/primary/primary_client.cpp index 1b35d61de..af52503eb 100644 --- a/src/primary/primary_client.cpp +++ b/src/primary/primary_client.cpp @@ -276,29 +276,29 @@ bool PrimaryClient::sendScriptBlocking(const std::string& program, std::string s } } + // Copy out key messages + std::deque key_messages; { std::scoped_lock lock(key_message_queue_mutex_); - if (key_message_queue_.size() > 0) + for (auto msg : key_message_queue_) { - auto key_messages = key_message_queue_; - key_message_queue_.clear(); - for (auto message : key_messages) + key_messages.push_back(msg); + } + key_message_queue_.clear(); + } + if (key_messages.size() > 0) + { + for (auto message : key_messages) + { + if (message.title_ == "PROGRAM_XXX_STOPPED" && message.text_ == script_info.script_name) { - if (message.title_ == "PROGRAM_XXX_STOPPED" && message.text_ == script_info.script_name) - { - URCL_LOG_INFO("Script with name %s executed successfully", script_info.script_name.c_str()); - return true; - } - else if (!script_started && message.title_ == "PROGRAM_XXX_STARTED" && - message.text_ == script_info.script_name) - { - URCL_LOG_INFO("Script with name %s started", script_info.script_name.c_str()); - script_started = true; - } - else // Put irrelevant messages back in the queue - { - key_message_queue_.push_back(message); - } + URCL_LOG_INFO("Script with name %s executed successfully", script_info.script_name.c_str()); + return true; + } + else if (!script_started && message.title_ == "PROGRAM_XXX_STARTED" && message.text_ == script_info.script_name) + { + URCL_LOG_INFO("Script with name %s started", script_info.script_name.c_str()); + script_started = true; } } } From d5427f9084a7adc4b807409964fa649aa2ff1b44 Mon Sep 17 00:00:00 2001 From: Jacob Larsen Date: Thu, 30 Apr 2026 09:51:22 +0000 Subject: [PATCH 43/74] add script name to timeout error log --- src/primary/primary_client.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/primary/primary_client.cpp b/src/primary/primary_client.cpp index af52503eb..6017cc953 100644 --- a/src/primary/primary_client.cpp +++ b/src/primary/primary_client.cpp @@ -308,7 +308,7 @@ bool PrimaryClient::sendScriptBlocking(const std::string& program, std::string s if (!script_started && elapsed_time > timeout) { // Should this stop the running program? - URCL_LOG_ERROR("Script not started within timeout"); + URCL_LOG_ERROR("Script %s not started within timeout", script_info.script_name.c_str()); return false; } std::chrono::milliseconds wait_period(10); From 943fb577d087a1d6460513bc313e6ba1e1635824 Mon Sep 17 00:00:00 2001 From: Jacob Larsen Date: Thu, 30 Apr 2026 10:55:55 +0000 Subject: [PATCH 44/74] Update example file name and use same setup as other examples --- examples/CMakeLists.txt | 6 +-- examples/primary_client.cpp | 51 ------------------------ examples/send_script_blocking.cpp | 66 +++++++++++++++++++++++++++++++ 3 files changed, 69 insertions(+), 54 deletions(-) delete mode 100644 examples/primary_client.cpp create mode 100644 examples/send_script_blocking.cpp diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index 74e7d49b5..8a12e14e0 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -11,9 +11,9 @@ add_executable(primary_pipeline_example primary_pipeline.cpp) target_link_libraries(primary_pipeline_example ur_client_library::urcl) -add_executable(primary_client - primary_client.cpp) -target_link_libraries(primary_client ur_client_library::urcl) +add_executable(send_script_blocking + send_script_blocking.cpp) +target_link_libraries(send_script_blocking ur_client_library::urcl) add_executable(primary_pipeline_calibration_example primary_pipeline_calibration.cpp) diff --git a/examples/primary_client.cpp b/examples/primary_client.cpp deleted file mode 100644 index 77b1b1b84..000000000 --- a/examples/primary_client.cpp +++ /dev/null @@ -1,51 +0,0 @@ -#include -#include - -int main() -{ - using namespace std::chrono_literals; - auto notif = urcl::comm::INotifier(); - auto client = urcl::primary_interface::PrimaryClient("192.168.56.101", notif); - client.start(10); - std::cout << "Client connected" << std::endl; - - // Make sure the robot is running - client.commandBrakeRelease(); - - if (!client.safetyModeAllowsExecution()) - { - std::cout << "Robot is not in a safety state where script execution is possible. Exiting." << std::endl; - return 0; - } - - const std::string fully_defined_script = R"""( -# This is a fully defined script function definition and all -# All comments in this script will be stripped before sending the script to the robot - -# Any whitespace-only lines will also be removed -def example_fun(): - movej([0,-0.75,0,0,0,0]) - sleep(0.1) - movel([0,0,-1.5,0,0,0], t=5) -end)"""; - - if (client.sendScriptBlocking(fully_defined_script)) - { - // The function definition can also be omitted - // A function name will then be auto generated - client.sendScriptBlocking(R"(textmsg("Successful program execution"))"); - } - // A script-function name can also be passed to the method as well as whether it should be a function or secondary - // program A timeout can also be given to limit the wait for the passed function to start. If timeout = 0, it will - // wait indefinitely. - client.sendScriptBlocking(R"(textmsg("Named primary program"))", "cool_function_name", - std::chrono::milliseconds(2000)); - // There is however no feedback on secondary programs, so if will return successful as soon as the code is sent to the - // robot - std::string secondary_script = R"(sec sec_script(): - (textmsg("Named secondary program") -end -)"; - - client.sendScriptBlocking(secondary_script, "cool_secondary_name"); -} \ No newline at end of file diff --git a/examples/send_script_blocking.cpp b/examples/send_script_blocking.cpp new file mode 100644 index 000000000..43f13403e --- /dev/null +++ b/examples/send_script_blocking.cpp @@ -0,0 +1,66 @@ +#include +#include +#include + +std::string DEFAULT_ROBOT_IP = "192.168.56.101"; + +int main(int argc, char* argv[]) +{ + // Set the loglevel to info to print info logs + urcl::setLogLevel(urcl::LogLevel::INFO); + + // Parse the ip arguments if given + std::string robot_ip = DEFAULT_ROBOT_IP; + if (argc > 1) + { + robot_ip = std::string(argv[1]); + } + auto notif = urcl::comm::INotifier(); + auto client = urcl::primary_interface::PrimaryClient(robot_ip, notif); + client.start(10); + std::cout << "Client connected" << std::endl; + + // --------------- INITIALIZATION END ------------------- + + // Make sure the robot is running + client.commandBrakeRelease(); + + if (!client.safetyModeAllowsExecution()) + { + std::cout << "Robot is not in a safety state where script execution is possible. Exiting." << std::endl; + return 0; + } + + // The sendScriptBlocking accepts script code, and will return true or false, + // depending on whether the script is successfully executed + const std::string fully_defined_script = R"""( +# This is a fully defined script, function definition and all +# All comments in this script will be stripped before sending the script to the robot + +# Any whitespace-only lines will also be removed +def example_fun(): + movej([0,-0.75,0,0,0,0]) + sleep(0.1) + movel([0,0,-1.5,0,0,0], t=5) +end)"""; + + if (client.sendScriptBlocking(fully_defined_script)) + { + // The function definition can also be omitted + // A function name will then be auto generated + client.sendScriptBlocking(R"(textmsg("Successful program execution"))"); + } + // A script-function name can also be passed to the method + // A timeout can also be given to limit the wait for the passed function to start. If timeout = 0, it will + // wait indefinitely. + client.sendScriptBlocking(R"(textmsg("hello"))", "cool_function_name", std::chrono::milliseconds(0)); + // There is no feedback on secondary programs, so it will return successful as soon as the script is sent to the + // robot (Behavior is the same the sendScript function) + // Note that secondary scripts have to be "fully defined" by the user. + std::string secondary_script = R"( +sec sec_script(): + textmsg("Named secondary program" +end +)"; + client.sendScriptBlocking(secondary_script); +} \ No newline at end of file From 77881a0ea8cfb708b0f149efffdad0f7e3e4eeaa Mon Sep 17 00:00:00 2001 From: Jacob Larsen Date: Thu, 30 Apr 2026 12:18:36 +0000 Subject: [PATCH 45/74] Use namespace urcl in example --- examples/send_script_blocking.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/examples/send_script_blocking.cpp b/examples/send_script_blocking.cpp index 43f13403e..545b18963 100644 --- a/examples/send_script_blocking.cpp +++ b/examples/send_script_blocking.cpp @@ -2,6 +2,8 @@ #include #include +using namespace urcl; + std::string DEFAULT_ROBOT_IP = "192.168.56.101"; int main(int argc, char* argv[]) @@ -15,8 +17,8 @@ int main(int argc, char* argv[]) { robot_ip = std::string(argv[1]); } - auto notif = urcl::comm::INotifier(); - auto client = urcl::primary_interface::PrimaryClient(robot_ip, notif); + auto notif = comm::INotifier(); + auto client = primary_interface::PrimaryClient(robot_ip, notif); client.start(10); std::cout << "Client connected" << std::endl; From 7e06117cb30db22228db5e88735ff826598e0232 Mon Sep 17 00:00:00 2001 From: Jacob Larsen Date: Thu, 30 Apr 2026 12:20:49 +0000 Subject: [PATCH 46/74] Add primary client to documentation --- doc/architecture.rst | 1 + doc/architecture/primary_client.rst | 42 +++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+) create mode 100644 doc/architecture/primary_client.rst diff --git a/doc/architecture.rst b/doc/architecture.rst index fe608dbfe..fced81da0 100644 --- a/doc/architecture.rst +++ b/doc/architecture.rst @@ -10,6 +10,7 @@ well as a couple of standalone modules to directly use subsets of the library's :maxdepth: 1 architecture/dashboard_client + architecture/primary_client architecture/reverse_interface architecture/rtde_client architecture/script_command_interface diff --git a/doc/architecture/primary_client.rst b/doc/architecture/primary_client.rst new file mode 100644 index 000000000..9bbbaf01d --- /dev/null +++ b/doc/architecture/primary_client.rst @@ -0,0 +1,42 @@ +:github_url: https://github.com/UniversalRobots/Universal_Robots_Client_Library/blob/master/doc/architecture/primary_client.rst + +.. _primary_client: + +PrimaryClient +============= + +The Primary Client serves as an interface to the robot's `primary interface `_, present on port 30001. +The ``PrimaryClient`` class supports, among other things, sending URScript code for execution on the robot through the primary interface. Currently it offers two methods of script execution: ``sendScript`` and ``sendScriptBlocking``. + +Script execution without feedback +--------------------------------- +Method signature: + +.. code-block:: c++ + + bool sendScript(std::string program); + +The ``sendScript`` method will accept valid URScript code, and send it to the robot through the primary interface. This is a non-blocking method, as it will return as soon as the program has been transferred to the robot. It returns true when the program is successfully transferred to the robot, and false otherwise. +There is no feedback on whether the program is actually executed on the robot. + +Script execution with feedback +------------------------------ +Method signature: + +.. code-block:: c++ + + bool sendScriptBlocking( + std::string program, + std::string script_name = "", + std::chrono::milliseconds timeout = std::chrono::seconds(1), + bool fail_on_warnings = true + ); + +| The ``sendScriptBlocking`` method will also accept valid URScript code, but blocks until the execution result of the given program is available. +| Prior to transferring the program it will first check that the robot is in a state where it can execute programs, if not it returns false. +| If the robot is ready, the program is then transferred, and the method will wait for the robot to report that the program has either started, finished or encountered an error. +| If the program has not started within the given ``timeout``, the method returns false. +| If the robot encounters an error or runtime exception during program execution the method also returns false. +| If ``fail_on_warnings`` is true, it will also return false, if the robot reports a warning during program execution. +| The method only returns true if the program is successfully executed on the robot. +| This method also accepts secondary programs, but no feedback is available for those, so it will behave similarly to the ``sendScript`` method in those cases, except for the pre-transfer checks. From 00436155c9d56eb94bb814afbc855a8fe0a3cbee Mon Sep 17 00:00:00 2001 From: Jacob Larsen Date: Thu, 30 Apr 2026 12:57:49 +0000 Subject: [PATCH 47/74] Remove debug stuff that should not have been committed --- tests/test_primary_client.cpp | 7 ------- 1 file changed, 7 deletions(-) diff --git a/tests/test_primary_client.cpp b/tests/test_primary_client.cpp index 35a7a9a96..21fff11e1 100644 --- a/tests/test_primary_client.cpp +++ b/tests/test_primary_client.cpp @@ -512,15 +512,8 @@ TEST_F(PrimaryClientTest, test_send_script_blocking_fail_on_bad_script) EXPECT_NO_THROW(client_->commandPowerOff()); EXPECT_NO_THROW(client_->commandBrakeRelease()); - // auto consumer = std::make_shared(); - - // client_->addPrimaryConsumer(consumer); - EXPECT_FALSE(client_->sendScriptBlocking("non_existing_func()")); - // auto message = consumer->getOrWaitForMessage(); - // auto typed_msg = std::dynamic_pointer_cast(message); - // std::cout << typed_msg->toString() << std::endl; const std::string script_code = "def illegal_fun():\n" " calldoesntexist()\n" "end"; From 2179c01201724f9441bab4ae4e3e66abb84b62f2 Mon Sep 17 00:00:00 2001 From: Jacob Larsen Date: Thu, 30 Apr 2026 12:58:31 +0000 Subject: [PATCH 48/74] Fix example --- examples/send_script_blocking.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/send_script_blocking.cpp b/examples/send_script_blocking.cpp index 545b18963..30f0bfa0f 100644 --- a/examples/send_script_blocking.cpp +++ b/examples/send_script_blocking.cpp @@ -57,11 +57,11 @@ end)"""; // wait indefinitely. client.sendScriptBlocking(R"(textmsg("hello"))", "cool_function_name", std::chrono::milliseconds(0)); // There is no feedback on secondary programs, so it will return successful as soon as the script is sent to the - // robot (Behavior is the same the sendScript function) + // robot (Behavior is the same the sendScript function, except that robot state is checked before script is sent) // Note that secondary scripts have to be "fully defined" by the user. std::string secondary_script = R"( sec sec_script(): - textmsg("Named secondary program" + textmsg("Named secondary program") end )"; client.sendScriptBlocking(secondary_script); From fd82a3c2cadf5c22e306b6f04701cc2abce5daa7 Mon Sep 17 00:00:00 2001 From: Jacob Larsen Date: Thu, 30 Apr 2026 13:28:31 +0000 Subject: [PATCH 49/74] add test for ignoring warnings try to fix the ones not working in CI --- tests/test_primary_client.cpp | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/tests/test_primary_client.cpp b/tests/test_primary_client.cpp index 21fff11e1..0c338b815 100644 --- a/tests/test_primary_client.cpp +++ b/tests/test_primary_client.cpp @@ -491,7 +491,7 @@ TEST_F(PrimaryClientTest, test_send_script_blocking_fail_on_runtime_exception) EXPECT_NO_THROW(client_->commandPowerOff()); EXPECT_NO_THROW(client_->commandBrakeRelease()); // Non-invertible goal, should throw runtime exception - EXPECT_FALSE(client_->sendScriptBlocking("movej(p[0,0,0,0,0,0])")); + EXPECT_FALSE(client_->sendScriptBlocking("movej(p[10,0,0,0,0,0])")); } TEST_F(PrimaryClientTest, test_send_script_blocking_fail_on_robot_errors) @@ -499,8 +499,8 @@ TEST_F(PrimaryClientTest, test_send_script_blocking_fail_on_robot_errors) EXPECT_NO_THROW(client_->start()); EXPECT_NO_THROW(client_->commandPowerOff()); EXPECT_NO_THROW(client_->commandBrakeRelease()); - // Impossible movement, will trigger an error and protective stop - EXPECT_FALSE(client_->sendScriptBlocking("movel(p[0,0,0,0,0,0])")); + // Impossible movement, will trigger a warning and protective stop + EXPECT_FALSE(client_->sendScriptBlocking("movel(p[10,0,0,0,0,0])")); // reset the robot client_->commandUnlockProtectiveStop(); EXPECT_TRUE(client_->sendScriptBlocking("movej([0,0,0,0,0,0])")); @@ -521,6 +521,15 @@ TEST_F(PrimaryClientTest, test_send_script_blocking_fail_on_bad_script) EXPECT_FALSE(client_->sendScriptBlocking(script_code)); } +TEST_F(PrimaryClientTest, test_send_script_blocking_ignore_warnings) +{ + EXPECT_NO_THROW(client_->start()); + EXPECT_NO_THROW(client_->commandPowerOff()); + EXPECT_NO_THROW(client_->commandBrakeRelease()); + // Impossible movement, will trigger an error and protective stop + EXPECT_TRUE(client_->sendScriptBlocking("movel(p[10,0,0,0,0,0])", "", std::chrono::seconds(1), false)); +} + int main(int argc, char* argv[]) { ::testing::InitGoogleTest(&argc, argv); From 35058e96626e8f70dde2d382f7978ed57d1dee0e Mon Sep 17 00:00:00 2001 From: Jacob Larsen Date: Thu, 30 Apr 2026 14:32:19 +0000 Subject: [PATCH 50/74] comment out "ignore_warnings" test as it is unstable in CI --- tests/test_primary_client.cpp | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/test_primary_client.cpp b/tests/test_primary_client.cpp index 0c338b815..9d5cce68b 100644 --- a/tests/test_primary_client.cpp +++ b/tests/test_primary_client.cpp @@ -521,14 +521,14 @@ TEST_F(PrimaryClientTest, test_send_script_blocking_fail_on_bad_script) EXPECT_FALSE(client_->sendScriptBlocking(script_code)); } -TEST_F(PrimaryClientTest, test_send_script_blocking_ignore_warnings) -{ - EXPECT_NO_THROW(client_->start()); - EXPECT_NO_THROW(client_->commandPowerOff()); - EXPECT_NO_THROW(client_->commandBrakeRelease()); - // Impossible movement, will trigger an error and protective stop - EXPECT_TRUE(client_->sendScriptBlocking("movel(p[10,0,0,0,0,0])", "", std::chrono::seconds(1), false)); -} +// TEST_F(PrimaryClientTest, test_send_script_blocking_ignore_warnings) +// { +// EXPECT_NO_THROW(client_->start()); +// EXPECT_NO_THROW(client_->commandPowerOff()); +// EXPECT_NO_THROW(client_->commandBrakeRelease()); +// // Impossible movement, will trigger an error and protective stop +// EXPECT_TRUE(client_->sendScriptBlocking("movel(p[10,0,0,0,0,0])", "", std::chrono::seconds(1), false)); +// } int main(int argc, char* argv[]) { From 6a8cbe8d7fe3a1cea4b2d3955d79edb4fd90b376 Mon Sep 17 00:00:00 2001 From: Jacob Larsen Date: Fri, 15 May 2026 10:40:49 +0000 Subject: [PATCH 51/74] assert that protective stop was cleared --- tests/test_primary_client.cpp | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/tests/test_primary_client.cpp b/tests/test_primary_client.cpp index 9d5cce68b..84119e287 100644 --- a/tests/test_primary_client.cpp +++ b/tests/test_primary_client.cpp @@ -502,8 +502,8 @@ TEST_F(PrimaryClientTest, test_send_script_blocking_fail_on_robot_errors) // Impossible movement, will trigger a warning and protective stop EXPECT_FALSE(client_->sendScriptBlocking("movel(p[10,0,0,0,0,0])")); // reset the robot - client_->commandUnlockProtectiveStop(); - EXPECT_TRUE(client_->sendScriptBlocking("movej([0,0,0,0,0,0])")); + ASSERT_NO_THROW(client_->commandUnlockProtectiveStop()); + EXPECT_TRUE(client_->sendScriptBlocking("movej([0.5,-0.5,0.5,0,0,0])")); } TEST_F(PrimaryClientTest, test_send_script_blocking_fail_on_bad_script) @@ -521,14 +521,14 @@ TEST_F(PrimaryClientTest, test_send_script_blocking_fail_on_bad_script) EXPECT_FALSE(client_->sendScriptBlocking(script_code)); } -// TEST_F(PrimaryClientTest, test_send_script_blocking_ignore_warnings) -// { -// EXPECT_NO_THROW(client_->start()); -// EXPECT_NO_THROW(client_->commandPowerOff()); -// EXPECT_NO_THROW(client_->commandBrakeRelease()); -// // Impossible movement, will trigger an error and protective stop -// EXPECT_TRUE(client_->sendScriptBlocking("movel(p[10,0,0,0,0,0])", "", std::chrono::seconds(1), false)); -// } +TEST_F(PrimaryClientTest, test_send_script_blocking_ignore_warnings) +{ + EXPECT_NO_THROW(client_->start()); + EXPECT_NO_THROW(client_->commandPowerOff()); + EXPECT_NO_THROW(client_->commandBrakeRelease()); + // Impossible movement, will trigger an error and protective stop + EXPECT_TRUE(client_->sendScriptBlocking("movel(p[10,0,0,0,0,0])", "", std::chrono::seconds(1), false)); +} int main(int argc, char* argv[]) { From 732df8f3e4acb022f19d006e30c31bcd79d3751f Mon Sep 17 00:00:00 2001 From: Jacob Larsen Date: Fri, 15 May 2026 10:43:00 +0000 Subject: [PATCH 52/74] Reset robot after protective stop --- tests/test_primary_client.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/test_primary_client.cpp b/tests/test_primary_client.cpp index 84119e287..58f47e809 100644 --- a/tests/test_primary_client.cpp +++ b/tests/test_primary_client.cpp @@ -528,6 +528,9 @@ TEST_F(PrimaryClientTest, test_send_script_blocking_ignore_warnings) EXPECT_NO_THROW(client_->commandBrakeRelease()); // Impossible movement, will trigger an error and protective stop EXPECT_TRUE(client_->sendScriptBlocking("movel(p[10,0,0,0,0,0])", "", std::chrono::seconds(1), false)); + // reset the robot + ASSERT_NO_THROW(client_->commandUnlockProtectiveStop()); + EXPECT_TRUE(client_->sendScriptBlocking("movej([0.5,-0.5,0.5,0,0,0])")); } int main(int argc, char* argv[]) From d38ece8a41faecbf8116a45f4525806433e9a614 Mon Sep 17 00:00:00 2001 From: Jacob Larsen Date: Fri, 15 May 2026 11:40:44 +0000 Subject: [PATCH 53/74] Check that stripped script is not empty cursor suggestion --- src/primary/primary_client.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/primary/primary_client.cpp b/src/primary/primary_client.cpp index 6017cc953..7cd305a85 100644 --- a/src/primary/primary_client.cpp +++ b/src/primary/primary_client.cpp @@ -348,6 +348,11 @@ ScriptInfo PrimaryClient::prepare_script(std::string script, std::string script_ // Remove all comments and white-space-only lines std::vector stripped_script = strip_comments_and_whitespace(split_script); + if (stripped_script.size() == 0) + { + throw urcl::ScriptCodeSyntaxException("Script is empty after stripping comments and whitespace."); + } + // Use given scipt name or create one int64_t current_time = std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()) From 89e5557e6162c0ade60692356873bb514692e505 Mon Sep 17 00:00:00 2001 From: Jacob Larsen Date: Fri, 15 May 2026 11:41:30 +0000 Subject: [PATCH 54/74] Robustify script name checking and reporting --- .../primary/primary_client.h | 1 + src/primary/primary_client.cpp | 51 ++++++++++++------- 2 files changed, 33 insertions(+), 19 deletions(-) diff --git a/include/ur_client_library/primary/primary_client.h b/include/ur_client_library/primary/primary_client.h index f3ff012b4..dbd76de9c 100644 --- a/include/ur_client_library/primary/primary_client.h +++ b/include/ur_client_library/primary/primary_client.h @@ -351,6 +351,7 @@ class PrimaryClient ScriptInfo prepare_script(std::string script, std::string script_name); std::vector strip_comments_and_whitespace(std::vector script_lines); + std::string truncate_and_check_script_name(std::string candidate_name); PrimaryParser parser_; std::shared_ptr consumer_; diff --git a/src/primary/primary_client.cpp b/src/primary/primary_client.cpp index 7cd305a85..e0680c44e 100644 --- a/src/primary/primary_client.cpp +++ b/src/primary/primary_client.cpp @@ -340,6 +340,26 @@ std::vector PrimaryClient::strip_comments_and_whitespace(std::vecto return stripped_script; } +std::string PrimaryClient::truncate_and_check_script_name(const std::string candidate_name) +{ + std::string final_name = candidate_name; + // Limit script name length to 31, to ensure backwards compatibility + if (final_name.size() > 31) + { + final_name = final_name.substr(0, 31); + URCL_LOG_WARN("Given script name was too long, and has been truncated. New script name is: %s", final_name.c_str()); + } + // Validate script_name + static const std::regex valid_name(R"(^[A-Za-z_][A-Za-z0-9_]*$)"); + if (!std::regex_match(final_name, valid_name)) + { + throw urcl::ScriptCodeSyntaxException("Invalid script name: '" + final_name + + "'. Can only contain letters, numbers and underscores. First character " + "must be a letter or underscore."); + } + return final_name; +} + ScriptInfo PrimaryClient::prepare_script(std::string script, std::string script_name) { // Split the given script in to separate lines @@ -357,7 +377,12 @@ ScriptInfo PrimaryClient::prepare_script(std::string script, std::string script_ int64_t current_time = std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()) .count(); + // Assign name according to inputs std::string actual_script_name = script_name.empty() ? "script_" + std::to_string(current_time) : script_name; + + // Check that the final name is valid + actual_script_name = truncate_and_check_script_name(actual_script_name); + ScriptTypes actual_script_type = urcl::primary_interface::ScriptTypes::DEF; // Is the script wrapped in a function definition? If not add one if (stripped_script[0].substr(0, 4).find("def ") == script.npos && @@ -378,7 +403,7 @@ ScriptInfo PrimaryClient::prepare_script(std::string script, std::string script_ else { size_t name_end = stripped_script[0].find("("); - actual_script_name = stripped_script[0].substr(4, name_end - 4); + std::string name_in_script = stripped_script[0].substr(4, name_end - 4); if (stripped_script[0].find("def") != stripped_script[0].npos) { actual_script_type = ScriptTypes::DEF; @@ -387,26 +412,14 @@ ScriptInfo PrimaryClient::prepare_script(std::string script, std::string script_ { actual_script_type = ScriptTypes::SEC; } + // Check that the script name is valid, replace it if it is not + actual_script_name = truncate_and_check_script_name(name_in_script); + if (actual_script_name.size() != name_in_script.size()) + { + stripped_script[0].replace(stripped_script[0].find(name_in_script), name_in_script.size(), actual_script_name); + } } - // Validate script_name - static const std::regex valid_name(R"(^[A-Za-z_][A-Za-z0-9_]*$)"); - if (!std::regex_match(actual_script_name, valid_name)) - { - throw urcl::ScriptCodeSyntaxException("Invalid script name: '" + script_name + - "'. Can only contain letters, numbers and underscores. First character " - "must " - "be a letter or " - "underscore."); - } - - // Limit script name length to 31, to ensure backwards compatibility - if (actual_script_name.size() > 31) - { - actual_script_name = actual_script_name.substr(0, 31); - URCL_LOG_WARN("Given script name was too long, and has been truncated. New script name is: %s", - actual_script_name.c_str()); - } if (stripped_script.back().find("end") == script.npos) { throw urcl::ScriptCodeSyntaxException("Script contains either function definition or secondary process " From 69e72b7a0318161ce66c8a53456562d9ce4ed750 Mon Sep 17 00:00:00 2001 From: Jacob Larsen Date: Fri, 15 May 2026 11:45:13 +0000 Subject: [PATCH 55/74] Clear existing messages of all types cursor suggestion --- src/primary/primary_client.cpp | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/primary/primary_client.cpp b/src/primary/primary_client.cpp index e0680c44e..626825667 100644 --- a/src/primary/primary_client.cpp +++ b/src/primary/primary_client.cpp @@ -173,11 +173,18 @@ bool PrimaryClient::sendScriptBlocking(const std::string& program, std::string s URCL_LOG_ERROR(ss.str().c_str()); return false; } - + // Clear runtime exception { std::scoped_lock lock(runtime_exception_mutex_); latest_runtime_exception_ = nullptr; } + // Clear existing error codes + getErrorCodes(); + // Clear key messages + { + std::scoped_lock lock(key_message_queue_mutex_); + key_message_queue_.clear(); + } bool script_sent = sendScript(script_info.script_code); if (!script_sent) From f7dacf44f44902bc55d63de87d138ad4ceeb74da Mon Sep 17 00:00:00 2001 From: Jacob Larsen Date: Fri, 15 May 2026 12:03:45 +0000 Subject: [PATCH 56/74] Implement tests for long names and empty scripts --- tests/test_primary_client.cpp | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/tests/test_primary_client.cpp b/tests/test_primary_client.cpp index 58f47e809..0ee1d9042 100644 --- a/tests/test_primary_client.cpp +++ b/tests/test_primary_client.cpp @@ -483,6 +483,8 @@ TEST_F(PrimaryClientTest, test_send_script_blocking_throw_on_malformed_scripts) "end"; EXPECT_THROW(client_->sendScriptBlocking(script_bad_name), urcl::ScriptCodeSyntaxException); EXPECT_THROW(client_->sendScriptBlocking("textmsg(\"testing\")", "0_errors"), urcl::ScriptCodeSyntaxException); + const std::string comments_only = "#only\n#comments\n\n\n#and\n#whitespace"; + EXPECT_THROW(client_->sendScriptBlocking(comments_only), urcl::ScriptCodeSyntaxException); } TEST_F(PrimaryClientTest, test_send_script_blocking_fail_on_runtime_exception) @@ -527,12 +529,28 @@ TEST_F(PrimaryClientTest, test_send_script_blocking_ignore_warnings) EXPECT_NO_THROW(client_->commandPowerOff()); EXPECT_NO_THROW(client_->commandBrakeRelease()); // Impossible movement, will trigger an error and protective stop - EXPECT_TRUE(client_->sendScriptBlocking("movel(p[10,0,0,0,0,0])", "", std::chrono::seconds(1), false)); + EXPECT_TRUE(client_->sendScriptBlocking("movel(p[10,0,0,0,0,0])", "", std::chrono::milliseconds(1000), false)); // reset the robot ASSERT_NO_THROW(client_->commandUnlockProtectiveStop()); EXPECT_TRUE(client_->sendScriptBlocking("movej([0.5,-0.5,0.5,0,0,0])")); } +TEST_F(PrimaryClientTest, test_send_script_blocking_replace_long_names) +{ + EXPECT_NO_THROW(client_->start()); + EXPECT_NO_THROW(client_->commandPowerOff()); + EXPECT_NO_THROW(client_->commandBrakeRelease()); + const std::string name = "this_is_a_very_long_script_name_that_should_be_truncated"; + EXPECT_TRUE(client_->sendScriptBlocking("textmsg(\"Still running\")", name)); + const std::string long_name_script = "def " + name + + "():\n" + " textmsg(\"still running\")\n" + " sleep(0.1)\n" + " sync()\n" + "end"; + EXPECT_TRUE(client_->sendScriptBlocking(long_name_script)); +} + int main(int argc, char* argv[]) { ::testing::InitGoogleTest(&argc, argv); From 69ca2be856754a349c41ce26e75d420354b2de8c Mon Sep 17 00:00:00 2001 From: Jacob Larsen Date: Fri, 15 May 2026 12:42:52 +0000 Subject: [PATCH 57/74] Change warning report from ERROR to WARN --- src/primary/primary_client.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/primary/primary_client.cpp b/src/primary/primary_client.cpp index 626825667..5ff3160fc 100644 --- a/src/primary/primary_client.cpp +++ b/src/primary/primary_client.cpp @@ -251,9 +251,9 @@ bool PrimaryClient::sendScriptBlocking(const std::string& program, std::string s } if (error.report_level == ReportLevel::WARNING) { - URCL_LOG_ERROR("Robot error code with severity WARNING received during script execution. Robot " - "error code: %s", - error.to_string.c_str()); + URCL_LOG_WARN("Robot error code with severity WARNING received during script execution. Robot " + "error code: %s", + error.to_string.c_str()); is_warning = true; } if (error.message_code == 210) From f87f95af0b364f701ac29c5e42df66ab4cf78639 Mon Sep 17 00:00:00 2001 From: Jacob Larsen Date: Fri, 15 May 2026 12:43:27 +0000 Subject: [PATCH 58/74] Rename function and move throw statement to end of prepare_script --- .../primary/primary_client.h | 2 +- src/primary/primary_client.cpp | 33 ++++++++++--------- 2 files changed, 18 insertions(+), 17 deletions(-) diff --git a/include/ur_client_library/primary/primary_client.h b/include/ur_client_library/primary/primary_client.h index dbd76de9c..0d3b237a6 100644 --- a/include/ur_client_library/primary/primary_client.h +++ b/include/ur_client_library/primary/primary_client.h @@ -351,7 +351,7 @@ class PrimaryClient ScriptInfo prepare_script(std::string script, std::string script_name); std::vector strip_comments_and_whitespace(std::vector script_lines); - std::string truncate_and_check_script_name(std::string candidate_name); + std::string truncate_script_name(std::string candidate_name); PrimaryParser parser_; std::shared_ptr consumer_; diff --git a/src/primary/primary_client.cpp b/src/primary/primary_client.cpp index 5ff3160fc..3332aea61 100644 --- a/src/primary/primary_client.cpp +++ b/src/primary/primary_client.cpp @@ -347,7 +347,7 @@ std::vector PrimaryClient::strip_comments_and_whitespace(std::vecto return stripped_script; } -std::string PrimaryClient::truncate_and_check_script_name(const std::string candidate_name) +std::string PrimaryClient::truncate_script_name(const std::string candidate_name) { std::string final_name = candidate_name; // Limit script name length to 31, to ensure backwards compatibility @@ -356,14 +356,7 @@ std::string PrimaryClient::truncate_and_check_script_name(const std::string cand final_name = final_name.substr(0, 31); URCL_LOG_WARN("Given script name was too long, and has been truncated. New script name is: %s", final_name.c_str()); } - // Validate script_name - static const std::regex valid_name(R"(^[A-Za-z_][A-Za-z0-9_]*$)"); - if (!std::regex_match(final_name, valid_name)) - { - throw urcl::ScriptCodeSyntaxException("Invalid script name: '" + final_name + - "'. Can only contain letters, numbers and underscores. First character " - "must be a letter or underscore."); - } + return final_name; } @@ -387,14 +380,13 @@ ScriptInfo PrimaryClient::prepare_script(std::string script, std::string script_ // Assign name according to inputs std::string actual_script_name = script_name.empty() ? "script_" + std::to_string(current_time) : script_name; - // Check that the final name is valid - actual_script_name = truncate_and_check_script_name(actual_script_name); - ScriptTypes actual_script_type = urcl::primary_interface::ScriptTypes::DEF; // Is the script wrapped in a function definition? If not add one if (stripped_script[0].substr(0, 4).find("def ") == script.npos && stripped_script[0].substr(0, 4).find("sec ") == script.npos) { + // Check that the final name is not too long + actual_script_name = truncate_script_name(actual_script_name); std::string definition = "def " + actual_script_name + "():"; std::string end = "end"; // Add indentation to the existing script code @@ -411,7 +403,7 @@ ScriptInfo PrimaryClient::prepare_script(std::string script, std::string script_ { size_t name_end = stripped_script[0].find("("); std::string name_in_script = stripped_script[0].substr(4, name_end - 4); - if (stripped_script[0].find("def") != stripped_script[0].npos) + if (stripped_script[0].substr(0, 4).find("def ") != stripped_script[0].npos) { actual_script_type = ScriptTypes::DEF; } @@ -419,15 +411,24 @@ ScriptInfo PrimaryClient::prepare_script(std::string script, std::string script_ { actual_script_type = ScriptTypes::SEC; } - // Check that the script name is valid, replace it if it is not - actual_script_name = truncate_and_check_script_name(name_in_script); + // Check that the script name is not too long, replace it, if it is + actual_script_name = truncate_script_name(name_in_script); if (actual_script_name.size() != name_in_script.size()) { stripped_script[0].replace(stripped_script[0].find(name_in_script), name_in_script.size(), actual_script_name); } } - if (stripped_script.back().find("end") == script.npos) + // Validate script_name + static const std::regex valid_name(R"(^[A-Za-z_][A-Za-z0-9_]*$)"); + if (!std::regex_match(actual_script_name, valid_name)) + { + throw urcl::ScriptCodeSyntaxException("Invalid script name: '" + actual_script_name + + "'. Can only contain letters, numbers and underscores. First character " + "must be a letter or underscore."); + } + + if (stripped_script.back().substr(0, 3).find("end") == script.npos) { throw urcl::ScriptCodeSyntaxException("Script contains either function definition or secondary process " "definition, " From b402463b1a8a1434b3fd480ae300a16097f8471a Mon Sep 17 00:00:00 2001 From: Jacob Larsen Date: Fri, 15 May 2026 13:14:10 +0000 Subject: [PATCH 59/74] Change troublesome test to use protective_stop() --- tests/test_primary_client.cpp | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/test_primary_client.cpp b/tests/test_primary_client.cpp index 0ee1d9042..c33459890 100644 --- a/tests/test_primary_client.cpp +++ b/tests/test_primary_client.cpp @@ -528,11 +528,10 @@ TEST_F(PrimaryClientTest, test_send_script_blocking_ignore_warnings) EXPECT_NO_THROW(client_->start()); EXPECT_NO_THROW(client_->commandPowerOff()); EXPECT_NO_THROW(client_->commandBrakeRelease()); - // Impossible movement, will trigger an error and protective stop - EXPECT_TRUE(client_->sendScriptBlocking("movel(p[10,0,0,0,0,0])", "", std::chrono::milliseconds(1000), false)); + // Trigger protective stop (warning level error code) + EXPECT_TRUE(client_->sendScriptBlocking("protective_stop()", "", std::chrono::milliseconds(1000), false)); // reset the robot ASSERT_NO_THROW(client_->commandUnlockProtectiveStop()); - EXPECT_TRUE(client_->sendScriptBlocking("movej([0.5,-0.5,0.5,0,0,0])")); } TEST_F(PrimaryClientTest, test_send_script_blocking_replace_long_names) From 58d75026cf5562dbf53df602680227f243c93d88 Mon Sep 17 00:00:00 2001 From: Jacob Larsen Date: Fri, 15 May 2026 13:14:44 +0000 Subject: [PATCH 60/74] Add timeout waiting for robot mode Cursor suggestion --- src/primary/primary_client.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/primary/primary_client.cpp b/src/primary/primary_client.cpp index 3332aea61..431d30477 100644 --- a/src/primary/primary_client.cpp +++ b/src/primary/primary_client.cpp @@ -148,8 +148,16 @@ bool PrimaryClient::sendScriptBlocking(const std::string& program, std::string s ScriptInfo script_info = prepare_script(program, script_name); RobotMode robot_mode = getRobotMode(); + std::chrono::milliseconds robot_mode_timeout(1000); + auto start = std::chrono::system_clock::now(); while (robot_mode == RobotMode::UNKNOWN) { + auto now = std::chrono::system_clock::now(); + if (std::chrono::duration_cast(now - start).count() > robot_mode_timeout.count()) + { + URCL_LOG_ERROR("Robot mode not received within %lld ms, exiting.", robot_mode_timeout.count()); + return false; + } URCL_LOG_INFO("Robot mode not received yet, waiting for it to be received."); std::chrono::milliseconds update_period(100); std::this_thread::sleep_for(update_period); From 811fc4d44105658b75d952e6552fb7b7005e3e72 Mon Sep 17 00:00:00 2001 From: Jacob Larsen Date: Fri, 15 May 2026 13:40:11 +0000 Subject: [PATCH 61/74] Update header docs to include throws triggered by commandStop Suggested by cursor --- include/ur_client_library/primary/primary_client.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/include/ur_client_library/primary/primary_client.h b/include/ur_client_library/primary/primary_client.h index 0d3b237a6..4436aac5b 100644 --- a/include/ur_client_library/primary/primary_client.h +++ b/include/ur_client_library/primary/primary_client.h @@ -124,6 +124,8 @@ class PrimaryClient * error during execution. Default true * * \throw urcl::ScriptCodeSyntaxException if the given script code has syntax errors, which are checked here. + * \throw urcl::UrException if the stop command cannot be sent to the robot. + * \throw urcl::TimeoutException if the robot doesn't stop the program within the given timeout. * * \returns true on successful execution of the script, false otherwise */ From 01cc0c7f4288e9159558fcb23cd1661c726297ae Mon Sep 17 00:00:00 2001 From: Jacob Larsen Date: Fri, 15 May 2026 13:40:26 +0000 Subject: [PATCH 62/74] Add note about protective stops to docs --- doc/architecture/primary_client.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/architecture/primary_client.rst b/doc/architecture/primary_client.rst index 9bbbaf01d..57510d265 100644 --- a/doc/architecture/primary_client.rst +++ b/doc/architecture/primary_client.rst @@ -37,6 +37,6 @@ Method signature: | If the robot is ready, the program is then transferred, and the method will wait for the robot to report that the program has either started, finished or encountered an error. | If the program has not started within the given ``timeout``, the method returns false. | If the robot encounters an error or runtime exception during program execution the method also returns false. -| If ``fail_on_warnings`` is true, it will also return false, if the robot reports a warning during program execution. +| If ``fail_on_warnings`` is true, it will also return false, if the robot reports a warning during program execution. Note: protective stops are reported as warnings by the robot. | The method only returns true if the program is successfully executed on the robot. | This method also accepts secondary programs, but no feedback is available for those, so it will behave similarly to the ``sendScript`` method in those cases, except for the pre-transfer checks. From 163f0ecbfac29aff08e35a1fb69fcc29d4f1480f Mon Sep 17 00:00:00 2001 From: Jacob Larsen Date: Fri, 15 May 2026 13:41:24 +0000 Subject: [PATCH 63/74] Check that function definition has '(' Cursor suggestion --- src/primary/primary_client.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/primary/primary_client.cpp b/src/primary/primary_client.cpp index 431d30477..6d6f9607c 100644 --- a/src/primary/primary_client.cpp +++ b/src/primary/primary_client.cpp @@ -410,6 +410,11 @@ ScriptInfo PrimaryClient::prepare_script(std::string script, std::string script_ else { size_t name_end = stripped_script[0].find("("); + if (name_end == stripped_script[0].npos) + { + throw urcl::ScriptCodeSyntaxException("Function definition detected in script, but a '(' could not be found. " + "Definition is invalid."); + } std::string name_in_script = stripped_script[0].substr(4, name_end - 4); if (stripped_script[0].substr(0, 4).find("def ") != stripped_script[0].npos) { From 60e334658f79af6df396057930baa632d2f6e28f Mon Sep 17 00:00:00 2001 From: Jacob Larsen Date: Fri, 15 May 2026 13:42:00 +0000 Subject: [PATCH 64/74] Test parentheses detection --- tests/test_primary_client.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/test_primary_client.cpp b/tests/test_primary_client.cpp index c33459890..b0dfcf52b 100644 --- a/tests/test_primary_client.cpp +++ b/tests/test_primary_client.cpp @@ -485,6 +485,10 @@ TEST_F(PrimaryClientTest, test_send_script_blocking_throw_on_malformed_scripts) EXPECT_THROW(client_->sendScriptBlocking("textmsg(\"testing\")", "0_errors"), urcl::ScriptCodeSyntaxException); const std::string comments_only = "#only\n#comments\n\n\n#and\n#whitespace"; EXPECT_THROW(client_->sendScriptBlocking(comments_only), urcl::ScriptCodeSyntaxException); + const std::string script_no_paren = "def test_fun:\n" + " textmsg(\"testing\")" + "end"; + EXPECT_THROW(client_->sendScriptBlocking(script_no_paren), urcl::ScriptCodeSyntaxException); } TEST_F(PrimaryClientTest, test_send_script_blocking_fail_on_runtime_exception) From f99c7503b8326e6fd2b5fa0c50f9fbe49c35b4f5 Mon Sep 17 00:00:00 2001 From: Felix Exner Date: Wed, 20 May 2026 10:47:57 +0200 Subject: [PATCH 65/74] Apply suggestions from code review Co-authored-by: Felix Exner --- include/ur_client_library/primary/primary_client.h | 6 +++--- src/primary/primary_client.cpp | 9 +-------- 2 files changed, 4 insertions(+), 11 deletions(-) diff --git a/include/ur_client_library/primary/primary_client.h b/include/ur_client_library/primary/primary_client.h index 4436aac5b..72ad08024 100644 --- a/include/ur_client_library/primary/primary_client.h +++ b/include/ur_client_library/primary/primary_client.h @@ -48,7 +48,7 @@ namespace urcl namespace primary_interface { -enum ScriptTypes +enum class ScriptTypes { DEF = 0, SEC = 1, @@ -117,7 +117,7 @@ class PrimaryClient * function name. The script name will be used in log messages in both the client library and in the robot logs. If no * name is defined in any way, the script will be given a generic, but unique, name. * - * \param timeout Amount of time to allow before the robot must have confirmed that the script has been started. If + * \param start_timeout Amount of time to allow before the robot must have confirmed that the script has been started. If * timeout is 0, it will be ignored. Default value: 1 second * * \param fail_on_warnings Whether or not the function should report a failure, if the robot reports a warning-level @@ -130,7 +130,7 @@ class PrimaryClient * \returns true on successful execution of the script, false otherwise */ bool sendScriptBlocking(const std::string& program, std::string script_name = "", - std::chrono::milliseconds timeout = std::chrono::seconds(1), bool fail_on_warnings = true); + std::chrono::milliseconds start_timeout = std::chrono::seconds(1), bool fail_on_warnings = true); bool checkCalibration(const std::string& checksum); diff --git a/src/primary/primary_client.cpp b/src/primary/primary_client.cpp index 6d6f9607c..a6a780871 100644 --- a/src/primary/primary_client.cpp +++ b/src/primary/primary_client.cpp @@ -125,14 +125,8 @@ bool PrimaryClient::safetyModeAllowsExecution() switch (mode) { case SafetyMode::NORMAL: - return true; - case SafetyMode::REDUCED: - return true; - case SafetyMode::RECOVERY: - return true; - // Safety mode might be unknown, as it is only updated on changes. case SafetyMode::UNDEFINED_SAFETY_MODE: return true; @@ -322,7 +316,6 @@ bool PrimaryClient::sendScriptBlocking(const std::string& program, std::string s if (!script_started && elapsed_time > timeout) { - // Should this stop the running program? URCL_LOG_ERROR("Script %s not started within timeout", script_info.script_name.c_str()); return false; } @@ -381,7 +374,7 @@ ScriptInfo PrimaryClient::prepare_script(std::string script, std::string script_ throw urcl::ScriptCodeSyntaxException("Script is empty after stripping comments and whitespace."); } - // Use given scipt name or create one + // Use given script name or create one int64_t current_time = std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()) .count(); From 8968da6b7e13c9207f36efc525771db6fb031f28 Mon Sep 17 00:00:00 2001 From: Felix Exner Date: Wed, 20 May 2026 11:25:48 +0200 Subject: [PATCH 66/74] Add line numbers to debug output --- src/primary/primary_client.cpp | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/primary/primary_client.cpp b/src/primary/primary_client.cpp index a6a780871..463c62e51 100644 --- a/src/primary/primary_client.cpp +++ b/src/primary/primary_client.cpp @@ -36,6 +36,7 @@ #include #include +#include #include namespace urcl { @@ -216,19 +217,22 @@ bool PrimaryClient::sendScriptBlocking(const std::string& program, std::string s << latest_runtime_exception_->column_number_ << "\n"; // Debug print for the user auto script_lines = splitString(script_info.script_code, "\n"); + int line_count = static_cast(script_lines.size()); + int line_number_width = std::to_string(line_count).size(); for (int i = 0; i < static_cast(script_lines.size()); i++) { if (!script_lines[i].empty()) { - ss << script_lines[i] << "\n"; + ss << std::setw(line_number_width) << (i + 1) << ": " << script_lines[i] << "\n"; } if (i == latest_runtime_exception_->line_number_ - 1) { - for (int j = 0; j < latest_runtime_exception_->column_number_ - 1; j++) + int output_column = latest_runtime_exception_->column_number_ - 1 + (line_number_width + 2); + for (int j = 0; j < output_column; j++) { ss << " "; } - ss << "^\n"; + ss << "^<--- here\n"; } } URCL_LOG_ERROR(ss.str().c_str()); From f6750b9aff73cc34c72638f40e2ac75b2a78c8e4 Mon Sep 17 00:00:00 2001 From: Felix Exner Date: Wed, 20 May 2026 11:27:35 +0200 Subject: [PATCH 67/74] Extend send_script example Add non-blocking send, add error case --- examples/CMakeLists.txt | 6 +-- ...nd_script_blocking.cpp => send_script.cpp} | 46 +++++++++++++++---- 2 files changed, 40 insertions(+), 12 deletions(-) rename examples/{send_script_blocking.cpp => send_script.cpp} (57%) diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index 8a12e14e0..b273c2998 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -11,9 +11,9 @@ add_executable(primary_pipeline_example primary_pipeline.cpp) target_link_libraries(primary_pipeline_example ur_client_library::urcl) -add_executable(send_script_blocking - send_script_blocking.cpp) -target_link_libraries(send_script_blocking ur_client_library::urcl) +add_executable(send_script + send_script.cpp) +target_link_libraries(send_script ur_client_library::urcl) add_executable(primary_pipeline_calibration_example primary_pipeline_calibration.cpp) diff --git a/examples/send_script_blocking.cpp b/examples/send_script.cpp similarity index 57% rename from examples/send_script_blocking.cpp rename to examples/send_script.cpp index 30f0bfa0f..bf5964315 100644 --- a/examples/send_script_blocking.cpp +++ b/examples/send_script.cpp @@ -1,10 +1,9 @@ #include -#include #include using namespace urcl; -std::string DEFAULT_ROBOT_IP = "192.168.56.101"; +std::string g_DEFAULT_ROBOT_IP = "192.168.56.101"; int main(int argc, char* argv[]) { @@ -12,7 +11,7 @@ int main(int argc, char* argv[]) urcl::setLogLevel(urcl::LogLevel::INFO); // Parse the ip arguments if given - std::string robot_ip = DEFAULT_ROBOT_IP; + std::string robot_ip = g_DEFAULT_ROBOT_IP; if (argc > 1) { robot_ip = std::string(argv[1]); @@ -20,7 +19,6 @@ int main(int argc, char* argv[]) auto notif = comm::INotifier(); auto client = primary_interface::PrimaryClient(robot_ip, notif); client.start(10); - std::cout << "Client connected" << std::endl; // --------------- INITIALIZATION END ------------------- @@ -29,8 +27,8 @@ int main(int argc, char* argv[]) if (!client.safetyModeAllowsExecution()) { - std::cout << "Robot is not in a safety state where script execution is possible. Exiting." << std::endl; - return 0; + URCL_LOG_ERROR("Robot is not in a safety state where script execution is possible. Exiting."); + return 1; } // The sendScriptBlocking accepts script code, and will return true or false, @@ -41,9 +39,11 @@ int main(int argc, char* argv[]) # Any whitespace-only lines will also be removed def example_fun(): - movej([0,-0.75,0,0,0,0]) + movej([0,-0.9,0.9,0,0,0]) sleep(0.1) - movel([0,0,-1.5,0,0,0], t=5) + current_pose = get_target_tcp_pose() + relative_move = p[0,0,-0.1,0,0,0] + movel(pose_trans(current_pose, relative_move), t=1) end)"""; if (client.sendScriptBlocking(fully_defined_script)) @@ -65,4 +65,32 @@ sec sec_script(): end )"; client.sendScriptBlocking(secondary_script); -} \ No newline at end of file + + // Sending wrong script code will result in a clear error + const std::string bad_script_code = R"""( +def bad_code(): + current_pose = get_target_tcp_pose() + movel(current_pos) # note pose vs pos +end)"""; + URCL_LOG_INFO("Sending bad script code..."); + bool success = client.sendScriptBlocking(bad_script_code); + { + std::stringstream ss; + ss << "Execution of bad code successful? " << std::boolalpha << success; + URCL_LOG_INFO("%s", ss.str().c_str()); + } + + // We can also send script code without any checks + URCL_LOG_INFO("Executing motion without feedback"); + client.sendScript("movej([0.1,-0.9,0.9,0,0,0])"); + // But we won't know when that is done or even if our code was correct. + // E.g. sending the bad script here will not give us any information + // The return value will only tell us that the script code has been sent to the robot. + URCL_LOG_INFO("Sending bad script code without feedback..."); + success = client.sendScript(bad_script_code); + { + std::stringstream ss; + ss << "Bad code sent to robot successfully? " << std::boolalpha << success; + URCL_LOG_INFO("%s", ss.str().c_str()); + } +} From 577f26c71c3efef29540058c4c59da59b5624995 Mon Sep 17 00:00:00 2001 From: Felix Exner Date: Wed, 20 May 2026 11:39:55 +0200 Subject: [PATCH 68/74] Add documentation for send_script example --- doc/examples.rst | 1 + doc/examples/send_script.rst | 99 ++++++++++++++++++++++++++++++++++++ 2 files changed, 100 insertions(+) create mode 100644 doc/examples/send_script.rst diff --git a/doc/examples.rst b/doc/examples.rst index dd90634c9..fcfa352a4 100644 --- a/doc/examples.rst +++ b/doc/examples.rst @@ -26,6 +26,7 @@ may be running forever until manually stopped. examples/external_fts_through_rtde examples/script_command_interface examples/script_sender + examples/send_script examples/spline_example examples/tool_contact_example examples/direct_torque_control diff --git a/doc/examples/send_script.rst b/doc/examples/send_script.rst new file mode 100644 index 000000000..a38948556 --- /dev/null +++ b/doc/examples/send_script.rst @@ -0,0 +1,99 @@ +:github_url: https://github.com/UniversalRobots/Universal_Robots_Client_Library/blob/master/doc/examples/send_script.rst + +.. _send_script_example: + +Send script example +=================== + +This example shows how to send arbitrary URScript code to the robot using the +:ref:`primary_client`. It demonstrates both the blocking variant (``sendScriptBlocking``), which +waits for execution feedback, and the non-blocking variant (``sendScript``), which only confirms +that the script has been forwarded to the robot. + +The full source code can be found in `send_script.cpp `_. + +Setting up the primary client +----------------------------- + +The example connects to the robot's primary interface by creating a ``PrimaryClient``. After +starting the client, the robot's brakes are released so that motion scripts can actually run, and +the safety state is checked before any script is sent: + +.. literalinclude:: ../../examples/send_script.cpp + :language: c++ + :caption: examples/send_script.cpp + :linenos: + :lineno-match: + :start-at: auto notif = comm::INotifier(); + :end-at: } + +Sending scripts with execution feedback +--------------------------------------- + +The ``sendScriptBlocking`` function uploads URScript code to the robot and waits until the robot +reports the result of the execution. The given code can be a fully defined script (with its own +``def ... end`` block) or a snippet that will automatically be wrapped into a function on the +client side. Comments and whitespace-only lines are stripped before the script is sent. + +.. literalinclude:: ../../examples/send_script.cpp + :language: c++ + :caption: examples/send_script.cpp + :linenos: + :lineno-match: + :start-at: const std::string fully_defined_script + :end-at: client.sendScriptBlocking(fully_defined_script) + +If you don't provide a function definition, the library wraps the snippet in one for you. You can +optionally pass a ``script_name`` (used in log messages on both the client and the robot) and a +``start_timeout`` that limits how long the call waits for the robot to confirm that the script has +started. A timeout of ``0`` means "wait indefinitely": + +.. literalinclude:: ../../examples/send_script.cpp + :language: c++ + :caption: examples/send_script.cpp + :linenos: + :lineno-match: + :start-at: client.sendScriptBlocking(R"(textmsg("Successful program execution"))"); + :end-at: client.sendScriptBlocking(R"(textmsg("hello"))", "cool_function_name", std::chrono::milliseconds(0)); + +Secondary programs can also be uploaded through ``sendScriptBlocking``. Since the robot does not +report execution feedback for secondary programs, the call returns as soon as the script has been +accepted. Note that secondary programs must be *fully defined* by the user (``sec ... end``): + +.. literalinclude:: ../../examples/send_script.cpp + :language: c++ + :caption: examples/send_script.cpp + :linenos: + :lineno-match: + :start-at: std::string secondary_script + :end-at: client.sendScriptBlocking(secondary_script); + +Reporting bad script code +------------------------- + +When a script contains errors (e.g. a typo or an undefined symbol), ``sendScriptBlocking`` will +report this back to the caller. The example sends a script that uses an undefined variable +``current_pos`` instead of ``current_pose``, and logs the result: + +.. literalinclude:: ../../examples/send_script.cpp + :language: c++ + :caption: examples/send_script.cpp + :linenos: + :lineno-match: + :start-at: const std::string bad_script_code + :end-before: // We can also send script code without any checks + +Sending scripts without feedback +-------------------------------- + +For situations where execution feedback is not needed, ``sendScript`` can be +used. It returns ``true`` as soon as the script has been transferred to the robot. The library +performs no further checks, so faulty script code will *not* be reported back here: + +.. literalinclude:: ../../examples/send_script.cpp + :language: c++ + :caption: examples/send_script.cpp + :linenos: + :lineno-match: + :start-at: // We can also send script code without any checks + :end-at: } From 918109147ed629aa8af18a1ec143a112fcd258fe Mon Sep 17 00:00:00 2001 From: Felix Exner Date: Wed, 20 May 2026 11:44:39 +0200 Subject: [PATCH 69/74] Small type improvements --- src/primary/primary_client.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/primary/primary_client.cpp b/src/primary/primary_client.cpp index 463c62e51..69a284df7 100644 --- a/src/primary/primary_client.cpp +++ b/src/primary/primary_client.cpp @@ -217,15 +217,15 @@ bool PrimaryClient::sendScriptBlocking(const std::string& program, std::string s << latest_runtime_exception_->column_number_ << "\n"; // Debug print for the user auto script_lines = splitString(script_info.script_code, "\n"); - int line_count = static_cast(script_lines.size()); + size_t line_count = script_lines.size(); int line_number_width = std::to_string(line_count).size(); - for (int i = 0; i < static_cast(script_lines.size()); i++) + for (size_t i = 0; i < line_count; i++) { if (!script_lines[i].empty()) { ss << std::setw(line_number_width) << (i + 1) << ": " << script_lines[i] << "\n"; } - if (i == latest_runtime_exception_->line_number_ - 1) + if (static_cast(i) == latest_runtime_exception_->line_number_ - 1) { int output_column = latest_runtime_exception_->column_number_ - 1 + (line_number_width + 2); for (int j = 0; j < output_column; j++) From ca1a0dfb3bea0e55522a007a903c51a9f68dedd3 Mon Sep 17 00:00:00 2001 From: Felix Exner Date: Wed, 20 May 2026 11:45:39 +0200 Subject: [PATCH 70/74] Add comment about C210 --- src/primary/primary_client.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/primary/primary_client.cpp b/src/primary/primary_client.cpp index 69a284df7..b647e8323 100644 --- a/src/primary/primary_client.cpp +++ b/src/primary/primary_client.cpp @@ -264,6 +264,9 @@ bool PrimaryClient::sendScriptBlocking(const std::string& program, std::string s } if (error.message_code == 210) { + // C210 means that the primary client is connected to a read-only primary interface, + // which means that scripts cannot be executed. We check for this error code to give the + // user a more specific error message in this case. is_error = true; is_read_only = true; } From 4e0fced7ba8cffb31b1d180ebde141fe2c49925b Mon Sep 17 00:00:00 2001 From: Felix Exner Date: Wed, 20 May 2026 11:48:34 +0200 Subject: [PATCH 71/74] Fix formatting errors introduced earlier --- include/ur_client_library/primary/primary_client.h | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/include/ur_client_library/primary/primary_client.h b/include/ur_client_library/primary/primary_client.h index 72ad08024..f4f1b110c 100644 --- a/include/ur_client_library/primary/primary_client.h +++ b/include/ur_client_library/primary/primary_client.h @@ -117,8 +117,8 @@ class PrimaryClient * function name. The script name will be used in log messages in both the client library and in the robot logs. If no * name is defined in any way, the script will be given a generic, but unique, name. * - * \param start_timeout Amount of time to allow before the robot must have confirmed that the script has been started. If - * timeout is 0, it will be ignored. Default value: 1 second + * \param start_timeout Amount of time to allow before the robot must have confirmed that the script has been started. + * If timeout is 0, it will be ignored. Default value: 1 second * * \param fail_on_warnings Whether or not the function should report a failure, if the robot reports a warning-level * error during execution. Default true @@ -130,7 +130,8 @@ class PrimaryClient * \returns true on successful execution of the script, false otherwise */ bool sendScriptBlocking(const std::string& program, std::string script_name = "", - std::chrono::milliseconds start_timeout = std::chrono::seconds(1), bool fail_on_warnings = true); + std::chrono::milliseconds start_timeout = std::chrono::seconds(1), + bool fail_on_warnings = true); bool checkCalibration(const std::string& checksum); From 77af0e9221749d1199f98de7698ffcea039d9d2a Mon Sep 17 00:00:00 2001 From: Felix Exner Date: Wed, 20 May 2026 12:49:10 +0200 Subject: [PATCH 72/74] Fix type --- src/primary/primary_client.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/primary/primary_client.cpp b/src/primary/primary_client.cpp index b647e8323..6f0f139c1 100644 --- a/src/primary/primary_client.cpp +++ b/src/primary/primary_client.cpp @@ -218,7 +218,7 @@ bool PrimaryClient::sendScriptBlocking(const std::string& program, std::string s // Debug print for the user auto script_lines = splitString(script_info.script_code, "\n"); size_t line_count = script_lines.size(); - int line_number_width = std::to_string(line_count).size(); + size_t line_number_width = std::to_string(line_count).size(); for (size_t i = 0; i < line_count; i++) { if (!script_lines[i].empty()) From fcd7448f86536bc3cd974d18e9e00a4a769a6a20 Mon Sep 17 00:00:00 2001 From: Felix Exner Date: Wed, 20 May 2026 13:00:33 +0200 Subject: [PATCH 73/74] Make line and col numbers uint32_t --- .../primary/robot_message/runtime_exception_message.h | 4 ++-- src/primary/primary_client.cpp | 7 ++++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/include/ur_client_library/primary/robot_message/runtime_exception_message.h b/include/ur_client_library/primary/robot_message/runtime_exception_message.h index 89c0b622b..b1d3b0616 100644 --- a/include/ur_client_library/primary/robot_message/runtime_exception_message.h +++ b/include/ur_client_library/primary/robot_message/runtime_exception_message.h @@ -99,8 +99,8 @@ class RuntimeExceptionMessage : public RobotMessage */ virtual std::string toString() const; - int32_t line_number_; - int32_t column_number_; + uint32_t line_number_; + uint32_t column_number_; std::string text_; }; } // namespace primary_interface diff --git a/src/primary/primary_client.cpp b/src/primary/primary_client.cpp index 6f0f139c1..f01a0130c 100644 --- a/src/primary/primary_client.cpp +++ b/src/primary/primary_client.cpp @@ -225,10 +225,11 @@ bool PrimaryClient::sendScriptBlocking(const std::string& program, std::string s { ss << std::setw(line_number_width) << (i + 1) << ": " << script_lines[i] << "\n"; } - if (static_cast(i) == latest_runtime_exception_->line_number_ - 1) + if (static_cast(i) == latest_runtime_exception_->line_number_ - 1) { - int output_column = latest_runtime_exception_->column_number_ - 1 + (line_number_width + 2); - for (int j = 0; j < output_column; j++) + uint32_t output_column = + latest_runtime_exception_->column_number_ - 1 + (static_cast(line_number_width) + 2); + for (uint32_t j = 0; j < output_column; j++) { ss << " "; } From 6ff46e0479fbdf3f371398d2af1f760b77ceb0f6 Mon Sep 17 00:00:00 2001 From: Felix Exner Date: Wed, 20 May 2026 14:03:25 +0200 Subject: [PATCH 74/74] Add grace period to look for errors once a program is stopped --- src/primary/primary_client.cpp | 50 +++++++++++++++++++++++++++++----- 1 file changed, 43 insertions(+), 7 deletions(-) diff --git a/src/primary/primary_client.cpp b/src/primary/primary_client.cpp index f01a0130c..a316c2da0 100644 --- a/src/primary/primary_client.cpp +++ b/src/primary/primary_client.cpp @@ -204,6 +204,15 @@ bool PrimaryClient::sendScriptBlocking(const std::string& program, std::string s const auto script_start_time = std::chrono::system_clock::now(); // Ignore start delay if it is 0 bool script_started = timeout == std::chrono::milliseconds(0) ? true : false; + // Error codes and key messages are produced by the same pipeline thread, but they live in two + // separate queues that are drained independently in this loop. A warning ErrorCode and the + // PROGRAM_XXX_STOPPED KeyMessage may therefore be visible to consecutive iterations rather than + // to the same one. To avoid returning success before such a "straggler" warning/error has been + // observed, we don't return immediately when STOPPED is seen. Instead we record that fact and + // keep draining the error / runtime exception queues for a short grace period. + bool program_stopped = false; + std::chrono::system_clock::time_point program_stopped_time; + const std::chrono::milliseconds post_stop_drain_period(100); while (true) { { @@ -309,8 +318,18 @@ bool PrimaryClient::sendScriptBlocking(const std::string& program, std::string s { if (message.title_ == "PROGRAM_XXX_STOPPED" && message.text_ == script_info.script_name) { - URCL_LOG_INFO("Script with name %s executed successfully", script_info.script_name.c_str()); - return true; + if (!program_stopped) + { + URCL_LOG_DEBUG("Script with name %s reported as stopped. Draining residual error / " + "runtime exception messages for up to %lld ms before reporting success.", + script_info.script_name.c_str(), static_cast(post_stop_drain_period.count())); + program_stopped = true; + program_stopped_time = std::chrono::system_clock::now(); + // STOPPED implies the script was started, otherwise the controller could not have + // stopped it. This avoids a spurious "not started within timeout" failure if the + // STARTED message was never observed in this loop. + script_started = true; + } } else if (!script_started && message.title_ == "PROGRAM_XXX_STARTED" && message.text_ == script_info.script_name) { @@ -319,13 +338,30 @@ bool PrimaryClient::sendScriptBlocking(const std::string& program, std::string s } } } - auto current_time = std::chrono::system_clock::now(); - auto elapsed_time = std::chrono::duration_cast(current_time - script_start_time); - if (!script_started && elapsed_time > timeout) + // After STOPPED has been observed, give the pipeline a short grace period to deliver any + // warning / fault / violation error codes that may have been parsed just before STOPPED but + // ended up in the error queue after this iteration's getErrorCodes() snapshot. Only declare + // success once that grace period elapsed without any reportable issue. + if (program_stopped) { - URCL_LOG_ERROR("Script %s not started within timeout", script_info.script_name.c_str()); - return false; + const auto now = std::chrono::system_clock::now(); + if (now - program_stopped_time >= post_stop_drain_period) + { + URCL_LOG_INFO("Script with name %s executed successfully", script_info.script_name.c_str()); + return true; + } + } + else + { + const auto current_time = std::chrono::system_clock::now(); + const auto elapsed_time = std::chrono::duration_cast(current_time - script_start_time); + + if (!script_started && elapsed_time > timeout) + { + URCL_LOG_ERROR("Script %s not started within timeout", script_info.script_name.c_str()); + return false; + } } std::chrono::milliseconds wait_period(10); std::this_thread::sleep_for(wait_period);