From 1782b532a65c1ab4c9bd3ff5d422ccc86b0f2ab6 Mon Sep 17 00:00:00 2001 From: Jacob Larsen Date: Fri, 12 Jun 2026 14:03:21 +0000 Subject: [PATCH 01/16] Move ReportLevel enum to datatypes.h To avoid circular dependencies --- .../ur_client_library/primary/primary_consumer.h | 16 ++++++++-------- .../primary/robot_message/error_code_message.h | 14 +------------- include/ur_client_library/ur/datatypes.h | 14 ++++++++++++++ 3 files changed, 23 insertions(+), 21 deletions(-) diff --git a/include/ur_client_library/primary/primary_consumer.h b/include/ur_client_library/primary/primary_consumer.h index a140ceded..6c25f3f1c 100644 --- a/include/ur_client_library/primary/primary_consumer.h +++ b/include/ur_client_library/primary/primary_consumer.h @@ -136,18 +136,18 @@ class PrimaryConsumer : public AbstractPrimaryConsumer switch (code.report_level) { - case urcl::primary_interface::ReportLevel::DEBUG: - case urcl::primary_interface::ReportLevel::DEVL_DEBUG: - case urcl::primary_interface::ReportLevel::DEVL_INFO: - case urcl::primary_interface::ReportLevel::DEVL_WARNING: - case urcl::primary_interface::ReportLevel::DEVL_VIOLATION: - case urcl::primary_interface::ReportLevel::DEVL_FAULT: + case ReportLevel::DEBUG: + case ReportLevel::DEVL_DEBUG: + case ReportLevel::DEVL_INFO: + case ReportLevel::DEVL_WARNING: + case ReportLevel::DEVL_VIOLATION: + case ReportLevel::DEVL_FAULT: URCL_LOG_DEBUG(log_contents.c_str()); break; - case urcl::primary_interface::ReportLevel::INFO: + case ReportLevel::INFO: URCL_LOG_INFO(log_contents.c_str()); break; - case urcl::primary_interface::ReportLevel::WARNING: + case ReportLevel::WARNING: URCL_LOG_WARN(log_contents.c_str()); break; default: diff --git a/include/ur_client_library/primary/robot_message/error_code_message.h b/include/ur_client_library/primary/robot_message/error_code_message.h index a4fc63bb3..9e3a69c8e 100644 --- a/include/ur_client_library/primary/robot_message/error_code_message.h +++ b/include/ur_client_library/primary/robot_message/error_code_message.h @@ -29,24 +29,12 @@ #define UR_CLIENT_LIBRARY_PRIMARY_ERROR_CODE_MESSAGE_H_INCLUDED #include "ur_client_library/primary/robot_message.h" +#include "ur_client_library/ur/datatypes.h" namespace urcl { namespace primary_interface { -enum class ReportLevel : int32_t -{ - DEBUG = 0, - INFO = 1, - WARNING = 2, - VIOLATION = 3, - FAULT = 4, - DEVL_DEBUG = 128, - DEVL_INFO = 129, - DEVL_WARNING = 130, - DEVL_VIOLATION = 131, - DEVL_FAULT = 132 -}; struct ErrorCode { diff --git a/include/ur_client_library/ur/datatypes.h b/include/ur_client_library/ur/datatypes.h index 3af4ebb7f..8157257b9 100644 --- a/include/ur_client_library/ur/datatypes.h +++ b/include/ur_client_library/ur/datatypes.h @@ -120,6 +120,20 @@ enum class RobotSeries UR_SERIES = 3 }; +enum class ReportLevel : int32_t +{ + DEBUG = 0, + INFO = 1, + WARNING = 2, + VIOLATION = 3, + FAULT = 4, + DEVL_DEBUG = 128, + DEVL_INFO = 129, + DEVL_WARNING = 130, + DEVL_VIOLATION = 131, + DEVL_FAULT = 132 +}; + inline std::string robotModeString(const RobotMode& mode) { switch (mode) From 6ada6f7ec8b595838585cf7ae7a24517d22dfbd9 Mon Sep 17 00:00:00 2001 From: Jacob Larsen Date: Fri, 12 Jun 2026 14:04:57 +0000 Subject: [PATCH 02/16] Add reportLevelString --- include/ur_client_library/ur/datatypes.h | 28 ++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/include/ur_client_library/ur/datatypes.h b/include/ur_client_library/ur/datatypes.h index 8157257b9..b7b6eb1a1 100644 --- a/include/ur_client_library/ur/datatypes.h +++ b/include/ur_client_library/ur/datatypes.h @@ -134,6 +134,34 @@ enum class ReportLevel : int32_t DEVL_FAULT = 132 }; +inline std::string reportLevelString(const ReportLevel& code) +{ + switch (code) + { + case ReportLevel::DEBUG: + return "DEBUG"; + case ReportLevel::INFO: + return "INFO"; + case ReportLevel::WARNING: + return "WARNING"; + case ReportLevel::VIOLATION: + return "VIOLATION"; + case ReportLevel::FAULT: + return "FAULT"; + case ReportLevel::DEVL_DEBUG: + return "DEVL_DEBUG"; + case ReportLevel::DEVL_INFO: + return "DEVL_INFO"; + case ReportLevel::DEVL_WARNING: + return "DEVL_WARNING"; + case ReportLevel::DEVL_VIOLATION: + return "DEVL_VIOLATION"; + case ReportLevel::DEVL_FAULT: + return "DEVL_FAULT"; + } + throw std::invalid_argument("Unknown report level: " + std::to_string(static_cast(code))); +} + inline std::string robotModeString(const RobotMode& mode) { switch (mode) From adbb0a7cd98f2147458740964a6269fac217e0f3 Mon Sep 17 00:00:00 2001 From: Jacob Larsen Date: Fri, 12 Jun 2026 14:05:48 +0000 Subject: [PATCH 03/16] Implement exceptions --- include/ur_client_library/exceptions.h | 130 +++++++++++++++++++++++++ 1 file changed, 130 insertions(+) diff --git a/include/ur_client_library/exceptions.h b/include/ur_client_library/exceptions.h index c3c46e8fc..aa3f3d6c9 100644 --- a/include/ur_client_library/exceptions.h +++ b/include/ur_client_library/exceptions.h @@ -33,7 +33,9 @@ #include #include #include +#include #include "ur/version_information.h" +#include "ur_client_library/ur/datatypes.h" #ifdef _WIN32 # define NOMINMAX @@ -336,5 +338,133 @@ class ScriptCodeSyntaxException : public UrException return std::runtime_error::what(); } }; + +class RobotModeException : public UrException +{ +public: + explicit RobotModeException() = delete; + + explicit RobotModeException(const std::string& operation, const RobotMode& required, const RobotMode& actual) + : std::runtime_error("Incorrect robot mode: " + robotModeString(actual)) + { + std::stringstream ss; + ss << "Robot is in incorrect mode for the requested operation: " << operation << "\n" + << "Required robot mode: " << urcl::robotModeString(required) << " (" << int(required) << ") \n" + << "Actual robot mode: " << urcl::robotModeString(actual) << " (" << int(actual) << ")"; + text_ = ss.str(); + } + + virtual ~RobotModeException() = default; + + virtual const char* what() const noexcept override + { + return text_.c_str(); + } + +private: + std::string text_; +}; + +class SafetyModeException : public UrException +{ +public: + explicit SafetyModeException() = delete; + + explicit SafetyModeException(const std::string& operation, const std::vector& options, + const urcl::SafetyMode& actual) + : std::runtime_error("Incorrect safety mode: " + safetyModeString(actual)) + { + std::stringstream ss; + ss << "Robot is in incorrect safety mode for the requested operation: " << operation << "\n" + << "Safety mode should be one of: \n"; + + for (auto mode : options) + { + ss << urcl::safetyModeString(mode) << " (" << int(mode) << ")\n"; + } + ss << "\n" + << "Actual safety mode: " << urcl::safetyModeString(actual) << " (" << int(actual) << ")"; + text_ = ss.str(); + } + + virtual ~SafetyModeException() = default; + + virtual const char* what() const noexcept override + { + return text_.c_str(); + } + +private: + std::string text_; +}; + +class StreamNotConnectedException : public UrException +{ +public: + explicit StreamNotConnectedException() = delete; + + explicit StreamNotConnectedException(const std::string& text) : std::runtime_error(text) + { + } + + virtual ~StreamNotConnectedException() = default; + + virtual const char* what() const noexcept override + { + return std::runtime_error::what(); + } +}; + +class RobotRuntimeException : public UrException +{ +public: + explicit RobotRuntimeException() = delete; + + explicit RobotRuntimeException(const std::string& text) : std::runtime_error(text) + { + } + + virtual ~RobotRuntimeException() = default; + + virtual const char* what() const noexcept override + { + return std::runtime_error::what(); + } +}; + +class ReadOnlyInterfaceException : public UrException +{ +public: + explicit ReadOnlyInterfaceException() = delete; + + explicit ReadOnlyInterfaceException(const std::string& text) : std::runtime_error(text) + { + } + + virtual ~ReadOnlyInterfaceException() = default; + + virtual const char* what() const noexcept override + { + return std::runtime_error::what(); + } +}; + +class RobotErrorCodeException : public UrException +{ +public: + explicit RobotErrorCodeException() = delete; + + explicit RobotErrorCodeException(const std::string& text) : std::runtime_error(text) + { + } + + virtual ~RobotErrorCodeException() = default; + + virtual const char* what() const noexcept override + { + return std::runtime_error::what(); + } +}; + } // namespace urcl #endif // ifndef UR_CLIENT_LIBRARY_EXCEPTIONS_H_INCLUDED From c306ef4e530bb89687deea49f7a095281f7cf20c Mon Sep 17 00:00:00 2001 From: Jacob Larsen Date: Fri, 12 Jun 2026 14:06:06 +0000 Subject: [PATCH 04/16] Use exceptions --- src/primary/primary_client.cpp | 84 ++++++++++++++++++---------------- 1 file changed, 44 insertions(+), 40 deletions(-) diff --git a/src/primary/primary_client.cpp b/src/primary/primary_client.cpp index a413c648a..eb3e9bd0c 100644 --- a/src/primary/primary_client.cpp +++ b/src/primary/primary_client.cpp @@ -150,8 +150,7 @@ bool PrimaryClient::sendScriptBlocking(const std::string& program, std::string s 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; + throw TimeoutException("Robot mode not received within timeout. ", robot_mode_timeout); } URCL_LOG_INFO("Robot mode not received yet, waiting for it to be received."); std::chrono::milliseconds update_period(100); @@ -161,20 +160,17 @@ bool PrimaryClient::sendScriptBlocking(const std::string& program, std::string s if (robot_mode != RobotMode::RUNNING) { - 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; + throw RobotModeException("Script execution via primary interface", urcl::RobotMode::RUNNING, robot_mode); } if (!safetyModeAllowsExecution()) { - URCL_LOG_ERROR("Robot safety mode does not allow for script execution, cannot execute script."); - std::stringstream ss; - ss << "Robot safety mode is: " << safetyModeString(getSafetyMode()) << " (" << unsigned(getSafetyMode()) << ")"; - URCL_LOG_ERROR(ss.str().c_str()); - return false; + std::vector allowed_modes = { urcl::SafetyMode::NORMAL, urcl::SafetyMode::REDUCED, + urcl::SafetyMode::RECOVERY }; + allowed_modes.push_back(urcl::SafetyMode::UNDEFINED_SAFETY_MODE); // Remove when safety mode gets updated + // continuously + + throw SafetyModeException("Script execution via primary interface", allowed_modes, getSafetyMode()); } // Clear runtime exception { @@ -192,12 +188,15 @@ bool PrimaryClient::sendScriptBlocking(const std::string& program, std::string s bool script_sent = sendScript(script_info.script_code); if (!script_sent) { - URCL_LOG_ERROR("Script could not be sent."); - return false; + throw StreamNotConnectedException("Script could not be sent to the robot. Ensure that the primary interface is " + "connected."); } // No feedback from secondary programs, so we assume success if (script_info.script_type == ScriptTypes::SEC) { + URCL_LOG_INFO("Script %s was determined to be a secondary program. Script was transferred successfully, but no " + "further feedback will be provided.", + script_info.script_name.c_str()); return true; } @@ -219,10 +218,10 @@ bool PrimaryClient::sendScriptBlocking(const std::string& program, std::string s std::scoped_lock lock(runtime_exception_mutex_); if (latest_runtime_exception_ != nullptr) { - 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 " + ss << "Runtime exception occured during script execution." + << "Runtime exception type: " << latest_runtime_exception_->text_ << "\n" + << "Exception occured at line " << latest_runtime_exception_->line_number_ << ", column " << latest_runtime_exception_->column_number_ << "\n"; // Line and column numbers should always be 1-based, but we check that they are greater // than 0 just to be sure before using them for indexing in the debug print below @@ -249,9 +248,8 @@ bool PrimaryClient::sendScriptBlocking(const std::string& program, std::string s ss << "^<--- here\n"; } } - URCL_LOG_ERROR(ss.str().c_str()); } - return false; + throw RobotRuntimeException(ss.str()); } } @@ -261,20 +259,25 @@ bool PrimaryClient::sendScriptBlocking(const std::string& program, std::string s bool is_error = false; bool is_warning = false; bool is_read_only = false; + + std::stringstream error_stream; + error_stream << "Robot error codes received during script execution: \n"; + for (auto error : errors) { 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()); + error_stream << "Code: " << error.to_string << ", severity: " << reportLevelString(error.report_level) + << "\n"; is_error = true; } if (error.report_level == ReportLevel::WARNING) { - URCL_LOG_WARN("Robot error code with severity WARNING received during script execution. Robot " - "error code: %s", - error.to_string.c_str()); + if (fail_on_warnings) + { + error_stream << "Code: " << error.to_string << ", severity: " << reportLevelString(error.report_level) + << "\n"; + } is_warning = true; } if (error.message_code == 210) @@ -294,16 +297,17 @@ bool PrimaryClient::sendScriptBlocking(const std::string& program, std::string s } 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."); + throw ReadOnlyInterfaceException("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; + throw RobotErrorCodeException(error_stream.str()); } if (is_warning && fail_on_warnings) { - return false; + throw RobotErrorCodeException(error_stream.str()); } } @@ -364,8 +368,8 @@ bool PrimaryClient::sendScriptBlocking(const std::string& program, std::string s if (!script_started && elapsed_time > timeout) { - URCL_LOG_ERROR("Script %s not started within timeout", script_info.script_name.c_str()); - return false; + throw urcl::TimeoutException("Script with name " + script_info.script_name + " not started within timeout. ", + timeout); } } std::chrono::milliseconds wait_period(10); @@ -420,7 +424,7 @@ ScriptInfo PrimaryClient::prepare_script(std::string script, std::string script_ if (stripped_script.size() == 0) { - throw urcl::ScriptCodeSyntaxException("Script is empty after stripping comments and whitespace."); + throw ScriptCodeSyntaxException("Script is empty after stripping comments and whitespace."); } // Use given script name or create one @@ -478,16 +482,16 @@ 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 (!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."); + throw 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, " - "but no 'end' term. Script is invalid."); + throw 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 98c517a3f8c1724d5475e687f56de18fe750abbf Mon Sep 17 00:00:00 2001 From: Jacob Larsen Date: Fri, 12 Jun 2026 14:15:29 +0000 Subject: [PATCH 05/16] Refactor nonrunning robot test --- tests/test_primary_client.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_primary_client.cpp b/tests/test_primary_client.cpp index bd4beca44..d1d02f10c 100644 --- a/tests/test_primary_client.cpp +++ b/tests/test_primary_client.cpp @@ -501,9 +501,9 @@ TEST_F(PrimaryClientTest, test_send_script_blocking_fails_on_nonrunning_robot) { EXPECT_NO_THROW(client_->start()); EXPECT_NO_THROW(client_->commandPowerOff()); - EXPECT_FALSE(client_->sendScriptBlocking("textmsg(\"Still running\")")); + EXPECT_THROW(client_->sendScriptBlocking("textmsg(\"Still running\")"), RobotModeException); EXPECT_NO_THROW(client_->commandPowerOn()); - EXPECT_FALSE(client_->sendScriptBlocking("textmsg(\"Still running\")")); + EXPECT_THROW(client_->sendScriptBlocking("textmsg(\"Still running\")"), RobotModeException); EXPECT_NO_THROW(client_->commandBrakeRelease()); EXPECT_TRUE(client_->sendScriptBlocking("textmsg(\"Still running\")")); } From b7a52e61d82b0b7faf50973c9ff258243c8bfbdc Mon Sep 17 00:00:00 2001 From: Jacob Larsen Date: Fri, 12 Jun 2026 14:29:33 +0000 Subject: [PATCH 06/16] Refactor fake primary server code to use new ReportLevel --- tests/fake_primary_server.cpp | 5 ++--- tests/fake_primary_server.h | 2 +- tests/test_fake_primary_server.cpp | 4 ++-- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/tests/fake_primary_server.cpp b/tests/fake_primary_server.cpp index 76c8981ea..4edd903d3 100644 --- a/tests/fake_primary_server.cpp +++ b/tests/fake_primary_server.cpp @@ -244,9 +244,8 @@ bool FakePrimaryServer::sendSafetyModeMessage(SafetyMode safety_mode, int32_t me return sendRobotMessage(primary_interface::RobotMessagePackageType::ROBOT_MESSAGE_SAFETY_MODE, payload); } -bool FakePrimaryServer::sendErrorCodeMessage(int32_t message_code, int32_t message_argument, - primary_interface::ReportLevel report_level, const std::string& text, - uint32_t data_type, uint32_t data) +bool FakePrimaryServer::sendErrorCodeMessage(int32_t message_code, int32_t message_argument, ReportLevel report_level, + const std::string& text, uint32_t data_type, uint32_t data) { std::vector payload(sizeof(int32_t) + sizeof(int32_t) + sizeof(int32_t) + sizeof(uint32_t) + sizeof(uint32_t) + text.size()); diff --git a/tests/fake_primary_server.h b/tests/fake_primary_server.h index e4425bc57..5071718e2 100644 --- a/tests/fake_primary_server.h +++ b/tests/fake_primary_server.h @@ -187,7 +187,7 @@ class FakePrimaryServer /*! * \brief Send an ErrorCodeMessage. */ - bool sendErrorCodeMessage(int32_t message_code, int32_t message_argument, primary_interface::ReportLevel report_level, + bool sendErrorCodeMessage(int32_t message_code, int32_t message_argument, ReportLevel report_level, const std::string& text, uint32_t data_type = 0, uint32_t data = 0); // ===================== Concrete RobotState helpers ===================== diff --git a/tests/test_fake_primary_server.cpp b/tests/test_fake_primary_server.cpp index 658880289..642f69faa 100644 --- a/tests/test_fake_primary_server.cpp +++ b/tests/test_fake_primary_server.cpp @@ -233,11 +233,11 @@ TEST_F(FakePrimaryServerTest, send_safety_mode_message_roundtrip) TEST_F(FakePrimaryServerTest, send_error_code_message_roundtrip) { - ASSERT_TRUE(server_->sendErrorCodeMessage(210, 0, primary_interface::ReportLevel::VIOLATION, "read-only PI")); + ASSERT_TRUE(server_->sendErrorCodeMessage(210, 0, ReportLevel::VIOLATION, "read-only PI")); auto msg = client_->consumer().waitFor(); ASSERT_NE(msg, nullptr); EXPECT_EQ(msg->message_code_, 210); - EXPECT_EQ(msg->report_level_, primary_interface::ReportLevel::VIOLATION); + EXPECT_EQ(msg->report_level_, ReportLevel::VIOLATION); EXPECT_EQ(msg->text_, "read-only PI"); } From 3b244580d77a998dc143896df776d36175de9949 Mon Sep 17 00:00:00 2001 From: Jacob Larsen Date: Fri, 12 Jun 2026 14:51:52 +0000 Subject: [PATCH 07/16] Refactor rest of tests --- tests/test_primary_client.cpp | 32 ++++++++++++++++++-------------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/tests/test_primary_client.cpp b/tests/test_primary_client.cpp index d1d02f10c..15cd27912 100644 --- a/tests/test_primary_client.cpp +++ b/tests/test_primary_client.cpp @@ -515,8 +515,8 @@ TEST_F(PrimaryClientTest, test_send_script_blocking_fails_on_bad_safety_mode) EXPECT_NO_THROW(client_->commandBrakeRelease()); ASSERT_TRUE(client_->safetyModeAllowsExecution()); - EXPECT_FALSE(client_->sendScriptBlocking("protective_stop()")); - EXPECT_FALSE(client_->sendScriptBlocking("textmsg(\"Still running\")")); + EXPECT_THROW(client_->sendScriptBlocking("protective_stop()"), RobotErrorCodeException); + EXPECT_THROW(client_->sendScriptBlocking("textmsg(\"Still running\")"), SafetyModeException); EXPECT_NO_THROW(client_->commandUnlockProtectiveStop()); EXPECT_TRUE(client_->sendScriptBlocking("textmsg(\"Still running\")")); } @@ -546,7 +546,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[10,0,0,0,0,0])")); + EXPECT_THROW(client_->sendScriptBlocking("movej(p[10,0,0,0,0,0])"), RobotRuntimeException); } TEST_F(PrimaryClientTest, test_send_script_blocking_fail_on_robot_errors) @@ -555,7 +555,7 @@ TEST_F(PrimaryClientTest, test_send_script_blocking_fail_on_robot_errors) EXPECT_NO_THROW(client_->commandPowerOff()); EXPECT_NO_THROW(client_->commandBrakeRelease()); // Impossible movement, will trigger a warning and protective stop - EXPECT_FALSE(client_->sendScriptBlocking("movel(p[10,0,0,0,0,0])")); + EXPECT_THROW(client_->sendScriptBlocking("movel(p[10,0,0,0,0,0])"), RobotErrorCodeException); // reset the robot ASSERT_NO_THROW(client_->commandUnlockProtectiveStop()); EXPECT_TRUE(client_->sendScriptBlocking("movej([0.5,-0.5,0.5,0,0,0])")); @@ -567,13 +567,13 @@ TEST_F(PrimaryClientTest, test_send_script_blocking_fail_on_bad_script) EXPECT_NO_THROW(client_->commandPowerOff()); EXPECT_NO_THROW(client_->commandBrakeRelease()); - EXPECT_FALSE(client_->sendScriptBlocking("non_existing_func()")); + EXPECT_THROW(client_->sendScriptBlocking("non_existing_func()"), RobotRuntimeException); const std::string script_code = "def illegal_fun():\n" " calldoesntexist()\n" "end"; - EXPECT_FALSE(client_->sendScriptBlocking(script_code)); + EXPECT_THROW(client_->sendScriptBlocking(script_code), RobotRuntimeException); } TEST_F(PrimaryClientTest, test_send_script_blocking_ignore_warnings) @@ -607,8 +607,9 @@ TEST_F(PrimaryClientFakeTest, test_send_script_blocking_fail_on_missing_robot_mo { // We did NOT send a robot mode, yet. - EXPECT_FALSE( - client_->sendScriptBlocking("textmsg(\"Still running\")", "test_fun", std::chrono::milliseconds(1000), false)); + EXPECT_THROW( + client_->sendScriptBlocking("textmsg(\"Still running\")", "test_fun", std::chrono::milliseconds(1000), false), + TimeoutException); server_->setScriptCallback([this]([[maybe_unused]] const std::string& payload) { server_->sendKeyMessage("PROGRAM_XXX_STARTED", "test_fun"); @@ -642,7 +643,7 @@ TEST_F(PrimaryClientFakeTest, test_send_script_blocking_fail_on_fault) { server_->sendKeyMessage("PROGRAM_XXX_STARTED", "test_fun"); ASSERT_EQ(payload, "def test_fun():\n " + script_code + "\nend\n\n"); - ASSERT_TRUE(server_->sendErrorCodeMessage(999, 0, primary_interface::ReportLevel::FAULT, "Simulated fault")); + ASSERT_TRUE(server_->sendErrorCodeMessage(999, 0, ReportLevel::FAULT, "Simulated fault")); } else { @@ -652,7 +653,8 @@ TEST_F(PrimaryClientFakeTest, test_send_script_blocking_fail_on_fault) }); URCL_LOG_INFO("Sending script that will trigger a fault on the fake server"); - EXPECT_FALSE(client_->sendScriptBlocking(script_code, "test_fun", std::chrono::milliseconds(1000), false)); + EXPECT_THROW(client_->sendScriptBlocking(script_code, "test_fun", std::chrono::milliseconds(1000), false), + RobotErrorCodeException); } TEST_F(PrimaryClientFakeTest, test_send_script_to_read_only_server) @@ -664,11 +666,12 @@ TEST_F(PrimaryClientFakeTest, test_send_script_to_read_only_server) // Make the fake server send an error code message with code 210 (read-only primary interface) when it receives a // script server_->setScriptCallback([this, script_code]([[maybe_unused]] const std::string& payload) { - ASSERT_TRUE(server_->sendErrorCodeMessage(210, 0, primary_interface::ReportLevel::VIOLATION, - "Simulated read-only primary interface error")); + ASSERT_TRUE( + server_->sendErrorCodeMessage(210, 0, ReportLevel::VIOLATION, "Simulated read-only primary interface error")); }); - EXPECT_FALSE(client_->sendScriptBlocking(script_code, "test_fun", std::chrono::milliseconds(1000), false)); + EXPECT_THROW(client_->sendScriptBlocking(script_code, "test_fun", std::chrono::milliseconds(1000), false), + ReadOnlyInterfaceException); } TEST_F(PrimaryClientFakeTest, test_send_script_blocking_timeout_on_no_response) @@ -679,7 +682,8 @@ TEST_F(PrimaryClientFakeTest, test_send_script_blocking_timeout_on_no_response) // We do not set a script callback on the fake server, so it will not respond to the script being sent. This should // cause sendScriptBlocking to time out and return false. - EXPECT_FALSE(client_->sendScriptBlocking(script_code, "test_fun", std::chrono::milliseconds(100), false)); + EXPECT_THROW(client_->sendScriptBlocking(script_code, "test_fun", std::chrono::milliseconds(100), false), + TimeoutException); } int main(int argc, char* argv[]) From c1613b70f19e5f7f33c21771aaccd5ab1ec49bdc Mon Sep 17 00:00:00 2001 From: Jacob Larsen Date: Fri, 12 Jun 2026 15:02:58 +0000 Subject: [PATCH 08/16] Change docstring --- include/ur_client_library/primary/primary_client.h | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/include/ur_client_library/primary/primary_client.h b/include/ur_client_library/primary/primary_client.h index f4f1b110c..cc1db9b4d 100644 --- a/include/ur_client_library/primary/primary_client.h +++ b/include/ur_client_library/primary/primary_client.h @@ -126,6 +126,15 @@ class PrimaryClient * \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. + * \throw urcl::TimeoutException if the robot mode is not received within 1 second. + * \throw urcl::TimeoutException if the program does not start within the given timeout. + * \throw urcl::RobotModeException if the robot is in an incorrect mode for script execution. + * \throw urcl::SafetyModeException if the robot is in an incorrect safety mode for script execution + * \throw urcl::StreamNotConnectedException if the script cannot be transferred to the robot. + * \throw urcl::RobotRuntimeException if the given script causes a runtime exception on the robot. + * \throw urcl::ReadOnlyInterfaceException if the primary interface is in read-only mode when the script is + * transferred. This can happen if the robot was recently switched from manual to remote control mode. + * \throw urcl::RobotErrorCodeException if the robot encounters an error during script execution. * * \returns true on successful execution of the script, false otherwise */ From a37bf149fe7ff86bda0bfc611209d7386344fd03 Mon Sep 17 00:00:00 2001 From: Jacob Larsen Date: Fri, 12 Jun 2026 15:08:44 +0000 Subject: [PATCH 09/16] Edit docs --- doc/architecture/primary_client.rst | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/doc/architecture/primary_client.rst b/doc/architecture/primary_client.rst index 57510d265..9a3ed67f4 100644 --- a/doc/architecture/primary_client.rst +++ b/doc/architecture/primary_client.rst @@ -33,10 +33,11 @@ Method signature: ); | 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. +| Prior to transferring the program it will first check that the robot is in a state where it can execute programs, otherwise an exception is thrown. | 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. Note: protective stops are reported as warnings by the robot. +| If the program has not started within the given ``timeout``, the method throws an exception. +| If the robot encounters an error or runtime exception during program execution the method also throws an exception. +| If ``fail_on_warnings`` is true, it will also throw an exception, 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. +| The exact exceptions that are thrown in various cases can be seen in the `primary client header file `_. From be8cb8234d235493d7f13a4143c7884fba421bf5 Mon Sep 17 00:00:00 2001 From: Jacob Larsen Date: Mon, 15 Jun 2026 06:53:57 +0000 Subject: [PATCH 10/16] Update method to not return anything on success --- doc/architecture/primary_client.rst | 2 +- examples/send_script.cpp | 31 +++++++++++-------- .../primary/primary_client.h | 6 ++-- src/primary/primary_client.cpp | 4 +-- tests/test_primary_client.cpp | 22 ++++++------- 5 files changed, 33 insertions(+), 32 deletions(-) diff --git a/doc/architecture/primary_client.rst b/doc/architecture/primary_client.rst index 9a3ed67f4..1fc044f2d 100644 --- a/doc/architecture/primary_client.rst +++ b/doc/architecture/primary_client.rst @@ -38,6 +38,6 @@ Method signature: | If the program has not started within the given ``timeout``, the method throws an exception. | If the robot encounters an error or runtime exception during program execution the method also throws an exception. | If ``fail_on_warnings`` is true, it will also throw an exception, 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. +| If no exceptions are thrown, the script has been executed successfully. | 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. | The exact exceptions that are thrown in various cases can be seen in the `primary client header file `_. diff --git a/examples/send_script.cpp b/examples/send_script.cpp index a832fcfa1..dddcff467 100644 --- a/examples/send_script.cpp +++ b/examples/send_script.cpp @@ -45,13 +45,10 @@ def example_fun(): relative_move = p[0,-0.1,0,0,0,0] movel(pose_trans(current_pose, relative_move), t=1) 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"))"); - } + 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. @@ -66,18 +63,26 @@ end )"; client.sendScriptBlocking(secondary_script); - // Sending wrong script code will result in a clear error + // Sending wrong script code will result in an exception with a clear explanation 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); + try { - std::stringstream ss; - ss << "Execution of bad code successful? " << std::boolalpha << success; - URCL_LOG_INFO("%s", ss.str().c_str()); + client.sendScriptBlocking(bad_script_code); + } + catch (const RobotRuntimeException& exc) + { + URCL_LOG_INFO("Caught expected runtime exception from sendScriptBlocking"); + URCL_LOG_INFO(exc.what()); + } + catch (const UrException& exc) + { + URCL_LOG_INFO("Caught unexpected exception from sendScriptBlocking"); + URCL_LOG_INFO(exc.what()); } // We can also send script code without any checks @@ -87,7 +92,7 @@ end)"""; // 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); + bool success = client.sendScript(bad_script_code); { std::stringstream ss; ss << "Bad code sent to robot successfully? " << std::boolalpha << success; diff --git a/include/ur_client_library/primary/primary_client.h b/include/ur_client_library/primary/primary_client.h index cc1db9b4d..c4f57c924 100644 --- a/include/ur_client_library/primary/primary_client.h +++ b/include/ur_client_library/primary/primary_client.h @@ -109,7 +109,7 @@ class PrimaryClient * 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). + * return 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. * @@ -135,10 +135,8 @@ class PrimaryClient * \throw urcl::ReadOnlyInterfaceException if the primary interface is in read-only mode when the script is * transferred. This can happen if the robot was recently switched from manual to remote control mode. * \throw urcl::RobotErrorCodeException if the robot encounters an error during script execution. - * - * \returns true on successful execution of the script, false otherwise */ - bool sendScriptBlocking(const std::string& program, std::string script_name = "", + void sendScriptBlocking(const std::string& program, std::string script_name = "", std::chrono::milliseconds start_timeout = std::chrono::seconds(1), bool fail_on_warnings = true); diff --git a/src/primary/primary_client.cpp b/src/primary/primary_client.cpp index eb3e9bd0c..94828890a 100644 --- a/src/primary/primary_client.cpp +++ b/src/primary/primary_client.cpp @@ -137,7 +137,7 @@ bool PrimaryClient::safetyModeAllowsExecution() } } -bool PrimaryClient::sendScriptBlocking(const std::string& program, std::string script_name, +void PrimaryClient::sendScriptBlocking(const std::string& program, std::string script_name, std::chrono::milliseconds timeout, bool fail_on_warnings) { ScriptInfo script_info = prepare_script(program, script_name); @@ -197,7 +197,6 @@ bool PrimaryClient::sendScriptBlocking(const std::string& program, std::string s URCL_LOG_INFO("Script %s was determined to be a secondary program. Script was transferred successfully, but no " "further feedback will be provided.", script_info.script_name.c_str()); - return true; } const auto script_start_time = std::chrono::system_clock::now(); @@ -358,7 +357,6 @@ bool PrimaryClient::sendScriptBlocking(const std::string& program, std::string s 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 diff --git a/tests/test_primary_client.cpp b/tests/test_primary_client.cpp index 15cd27912..60d22b225 100644 --- a/tests/test_primary_client.cpp +++ b/tests/test_primary_client.cpp @@ -486,15 +486,15 @@ TEST_F(PrimaryClientTest, test_send_script_blocking_happy_path) " sleep(0.1)\n" " sync()\n" "end"; - EXPECT_TRUE(client_->sendScriptBlocking(fully_defined_script)); + EXPECT_NO_THROW(client_->sendScriptBlocking(fully_defined_script)); const std::string part_defined_script = "textmsg(\"still running\")\n" "sleep(0.1)\n" "sync()\n"; - EXPECT_TRUE(client_->sendScriptBlocking(part_defined_script)); - EXPECT_TRUE(client_->sendScriptBlocking(part_defined_script, "test_def")); + EXPECT_NO_THROW(client_->sendScriptBlocking(part_defined_script)); + EXPECT_NO_THROW(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")); + EXPECT_NO_THROW(client_->sendScriptBlocking(sec_script, "test_sec")); } TEST_F(PrimaryClientTest, test_send_script_blocking_fails_on_nonrunning_robot) @@ -505,7 +505,7 @@ TEST_F(PrimaryClientTest, test_send_script_blocking_fails_on_nonrunning_robot) EXPECT_NO_THROW(client_->commandPowerOn()); EXPECT_THROW(client_->sendScriptBlocking("textmsg(\"Still running\")"), RobotModeException); EXPECT_NO_THROW(client_->commandBrakeRelease()); - EXPECT_TRUE(client_->sendScriptBlocking("textmsg(\"Still running\")")); + EXPECT_NO_THROW(client_->sendScriptBlocking("textmsg(\"Still running\")")); } TEST_F(PrimaryClientTest, test_send_script_blocking_fails_on_bad_safety_mode) @@ -518,7 +518,7 @@ TEST_F(PrimaryClientTest, test_send_script_blocking_fails_on_bad_safety_mode) EXPECT_THROW(client_->sendScriptBlocking("protective_stop()"), RobotErrorCodeException); EXPECT_THROW(client_->sendScriptBlocking("textmsg(\"Still running\")"), SafetyModeException); EXPECT_NO_THROW(client_->commandUnlockProtectiveStop()); - EXPECT_TRUE(client_->sendScriptBlocking("textmsg(\"Still running\")")); + EXPECT_NO_THROW(client_->sendScriptBlocking("textmsg(\"Still running\")")); } TEST_F(PrimaryClientTest, test_send_script_blocking_throw_on_malformed_scripts) @@ -558,7 +558,7 @@ TEST_F(PrimaryClientTest, test_send_script_blocking_fail_on_robot_errors) EXPECT_THROW(client_->sendScriptBlocking("movel(p[10,0,0,0,0,0])"), RobotErrorCodeException); // reset the robot ASSERT_NO_THROW(client_->commandUnlockProtectiveStop()); - EXPECT_TRUE(client_->sendScriptBlocking("movej([0.5,-0.5,0.5,0,0,0])")); + EXPECT_NO_THROW(client_->sendScriptBlocking("movej([0.5,-0.5,0.5,0,0,0])")); } TEST_F(PrimaryClientTest, test_send_script_blocking_fail_on_bad_script) @@ -582,7 +582,7 @@ TEST_F(PrimaryClientTest, test_send_script_blocking_ignore_warnings) EXPECT_NO_THROW(client_->commandPowerOff()); EXPECT_NO_THROW(client_->commandBrakeRelease()); // Trigger protective stop (warning level error code) - EXPECT_TRUE(client_->sendScriptBlocking("protective_stop()", "", std::chrono::milliseconds(1000), false)); + EXPECT_NO_THROW(client_->sendScriptBlocking("protective_stop()", "", std::chrono::milliseconds(1000), false)); // reset the robot ASSERT_NO_THROW(client_->commandUnlockProtectiveStop()); } @@ -593,14 +593,14 @@ TEST_F(PrimaryClientTest, test_send_script_blocking_replace_long_names) 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)); + EXPECT_NO_THROW(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)); + EXPECT_NO_THROW(client_->sendScriptBlocking(long_name_script)); } TEST_F(PrimaryClientFakeTest, test_send_script_blocking_fail_on_missing_robot_mode) @@ -622,7 +622,7 @@ TEST_F(PrimaryClientFakeTest, test_send_script_blocking_fail_on_missing_robot_mo // std::this_thread::sleep_for(std::chrono::milliseconds(200)); server_->sendRobotModeData(RobotMode::RUNNING, true, true, true, false, false, false, false); }); - EXPECT_TRUE( + EXPECT_NO_THROW( client_->sendScriptBlocking("textmsg(\"Still running\")", "test_fun", std::chrono::milliseconds(1000), false)); if (delayed_robot_mode_thread.joinable()) From 5c6b90e81f5f9b805e3d1565c8e48ba823742942 Mon Sep 17 00:00:00 2001 From: Jacob Larsen Date: Mon, 15 Jun 2026 06:57:30 +0000 Subject: [PATCH 11/16] Removed a little too much return --- src/primary/primary_client.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/primary/primary_client.cpp b/src/primary/primary_client.cpp index 94828890a..7c8fb2644 100644 --- a/src/primary/primary_client.cpp +++ b/src/primary/primary_client.cpp @@ -197,6 +197,7 @@ void PrimaryClient::sendScriptBlocking(const std::string& program, std::string s URCL_LOG_INFO("Script %s was determined to be a secondary program. Script was transferred successfully, but no " "further feedback will be provided.", script_info.script_name.c_str()); + return; } const auto script_start_time = std::chrono::system_clock::now(); @@ -357,6 +358,7 @@ void PrimaryClient::sendScriptBlocking(const std::string& program, std::string s 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; } } else From 35b646892ecd531212af92ae01838c0ee31ddd50 Mon Sep 17 00:00:00 2001 From: Felix Exner Date: Mon, 15 Jun 2026 10:23:40 +0200 Subject: [PATCH 12/16] Update examples/send_script.cpp --- examples/send_script.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/send_script.cpp b/examples/send_script.cpp index dddcff467..7b4d02221 100644 --- a/examples/send_script.cpp +++ b/examples/send_script.cpp @@ -81,8 +81,8 @@ end)"""; } catch (const UrException& exc) { - URCL_LOG_INFO("Caught unexpected exception from sendScriptBlocking"); - URCL_LOG_INFO(exc.what()); + URCL_LOG_ERROR("Caught unexpected exception from sendScriptBlocking"); + URCL_LOG_ERROR(exc.what()); } // We can also send script code without any checks From 9920a4bd312f63ff64171052d6de385555c0d12c Mon Sep 17 00:00:00 2001 From: Jacob Larsen Date: Mon, 15 Jun 2026 13:46:35 +0000 Subject: [PATCH 13/16] Add missing report levels --- include/ur_client_library/ur/datatypes.h | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/include/ur_client_library/ur/datatypes.h b/include/ur_client_library/ur/datatypes.h index b7b6eb1a1..701a02c7a 100644 --- a/include/ur_client_library/ur/datatypes.h +++ b/include/ur_client_library/ur/datatypes.h @@ -127,11 +127,13 @@ enum class ReportLevel : int32_t WARNING = 2, VIOLATION = 3, FAULT = 4, + CRITICAL_FAULT = 5, DEVL_DEBUG = 128, DEVL_INFO = 129, DEVL_WARNING = 130, DEVL_VIOLATION = 131, - DEVL_FAULT = 132 + DEVL_FAULT = 132, + DEVL_CRITICAL_FAULT = 133 }; inline std::string reportLevelString(const ReportLevel& code) @@ -148,6 +150,8 @@ inline std::string reportLevelString(const ReportLevel& code) return "VIOLATION"; case ReportLevel::FAULT: return "FAULT"; + case ReportLevel::CRITICAL_FAULT: + return "CRITICAL_FAULT"; case ReportLevel::DEVL_DEBUG: return "DEVL_DEBUG"; case ReportLevel::DEVL_INFO: @@ -158,6 +162,8 @@ inline std::string reportLevelString(const ReportLevel& code) return "DEVL_VIOLATION"; case ReportLevel::DEVL_FAULT: return "DEVL_FAULT"; + case ReportLevel::DEVL_CRITICAL_FAULT: + return "DEVL_CRITICAL_FAULT"; } throw std::invalid_argument("Unknown report level: " + std::to_string(static_cast(code))); } From ef68c40dcb7eabd747f83e9394651c75448a23ea Mon Sep 17 00:00:00 2001 From: Jacob Larsen Date: Mon, 15 Jun 2026 13:47:02 +0000 Subject: [PATCH 14/16] Add test of parsing of report levels, as well as their string representation --- tests/test_primary_parser.cpp | 53 +++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/tests/test_primary_parser.cpp b/tests/test_primary_parser.cpp index 114612a32..ac2549944 100644 --- a/tests/test_primary_parser.cpp +++ b/tests/test_primary_parser.cpp @@ -341,6 +341,33 @@ const unsigned char SAFETY_MODE_MESSAGE_STRING[] = { 0x80, 0x07, 0x06, 0x05 }; +const unsigned char REPORT_LEVELS_TEMPLATE[] = { + // message size + 0x00, 0x00, 0x00, 0x20, + // message type robot message + 0x14, + // timestamp + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // source + 0xfd, + // robot_message_type ERROR_CODE + 0x06, + // Robot message code + 0x00, 0x00, 0x00, 0x00, + // Robot message argument + 0x00, 0x00, 0x00, 0x00, + // Robot message report level + 0x00, 0x00, 0x00, 0x00, + // data type + 0x00, 0x00, 0x00, 0x00, + // data + 0x80, 0x07, 0x06, 0x05, + // text length + 0x00, 0x01, + // message + 0x00 +}; + class PrimaryParserTest : public ::testing::Test { protected: @@ -826,6 +853,32 @@ TEST_F(PrimaryParserTest, parse_masterboard_data_with_immi) EXPECT_NE(msg_str.find("IMMI current: 0.75"), std::string::npos); } +TEST_F(PrimaryParserTest, parse_error_code_report_levels) +{ + std::vector levels = { 0, 1, 2, 3, 4, 5, 128, 129, 130, 131, 132, 133 }; + std::vector level_strings = { "DEBUG", "INFO", "WARNING", "VIOLATION", + "FAULT", "CRITICAL_FAULT", "DEVL_DEBUG", "DEVL_INFO", + "DEVL_WARNING", "DEVL_VIOLATION", "DEVL_FAULT", "DEVL_CRITICAL_FAULT" }; + + unsigned char raw_data[sizeof(REPORT_LEVELS_TEMPLATE)]; + for (size_t i = 0; i < levels.size(); i++) + { + memcpy(raw_data, REPORT_LEVELS_TEMPLATE, sizeof(REPORT_LEVELS_TEMPLATE)); + raw_data[26] = levels[i] & 0xFF; + comm::BinParser bp(raw_data, sizeof(raw_data)); + std::vector> products; + ASSERT_TRUE(parser_.parse(bp, products)); + ASSERT_EQ(products.size(), 1); + + if (primary_interface::ErrorCodeMessage* data = + dynamic_cast(products[0].get())) + { + EXPECT_EQ(data->report_level_, ReportLevel(levels[i])); + EXPECT_EQ(reportLevelString(data->report_level_), level_strings[i]); + } + } +} + int main(int argc, char* argv[]) { ::testing::InitGoogleTest(&argc, argv); From 96e68049d068e80616c2408b155ea339c92df2e9 Mon Sep 17 00:00:00 2001 From: Felix Exner Date: Mon, 15 Jun 2026 16:47:23 +0200 Subject: [PATCH 15/16] Handle all report levels in primaryconsumer --- .../primary/primary_consumer.h | 7 ++-- tests/test_primary_client.cpp | 40 +++++++++++++++++++ 2 files changed, 44 insertions(+), 3 deletions(-) diff --git a/include/ur_client_library/primary/primary_consumer.h b/include/ur_client_library/primary/primary_consumer.h index 6c25f3f1c..a46756627 100644 --- a/include/ur_client_library/primary/primary_consumer.h +++ b/include/ur_client_library/primary/primary_consumer.h @@ -142,6 +142,7 @@ class PrimaryConsumer : public AbstractPrimaryConsumer case ReportLevel::DEVL_WARNING: case ReportLevel::DEVL_VIOLATION: case ReportLevel::DEVL_FAULT: + case ReportLevel::DEVL_CRITICAL_FAULT: URCL_LOG_DEBUG(log_contents.c_str()); break; case ReportLevel::INFO: @@ -150,9 +151,9 @@ class PrimaryConsumer : public AbstractPrimaryConsumer case ReportLevel::WARNING: URCL_LOG_WARN(log_contents.c_str()); break; - default: - // urcl::primary_interface::ReportLevel::VIOLATION: - // urcl::primary_interface::ReportLevel::FAULT: + case ReportLevel::VIOLATION: + case ReportLevel::FAULT: + case ReportLevel::CRITICAL_FAULT: URCL_LOG_ERROR(log_contents.c_str()); break; } diff --git a/tests/test_primary_client.cpp b/tests/test_primary_client.cpp index 60d22b225..2fe840c72 100644 --- a/tests/test_primary_client.cpp +++ b/tests/test_primary_client.cpp @@ -686,6 +686,46 @@ TEST_F(PrimaryClientFakeTest, test_send_script_blocking_timeout_on_no_response) TimeoutException); } +TEST_F(PrimaryClientFakeTest, test_error_code_report_levels_get_handled_correctly) +{ + ASSERT_TRUE(server_->sendErrorCodeMessage(999, 0, ReportLevel::DEBUG, "Simulated Debug")); + ASSERT_TRUE(server_->sendErrorCodeMessage(999, 0, ReportLevel::INFO, "Simulated Info")); + ASSERT_TRUE(server_->sendErrorCodeMessage(999, 0, ReportLevel::WARNING, "Simulated Warning")); + ASSERT_TRUE(server_->sendErrorCodeMessage(999, 0, ReportLevel::VIOLATION, "Simulated Violation")); + ASSERT_TRUE(server_->sendErrorCodeMessage(999, 0, ReportLevel::FAULT, "Simulated Fault")); + ASSERT_TRUE(server_->sendErrorCodeMessage(999, 0, ReportLevel::CRITICAL_FAULT, "Simulated Critical Fault")); + ASSERT_TRUE(server_->sendErrorCodeMessage(999, 0, ReportLevel::DEVL_DEBUG, "Simulated DEVL Debug")); + ASSERT_TRUE(server_->sendErrorCodeMessage(999, 0, ReportLevel::DEVL_INFO, "Simulated DEVL Info")); + ASSERT_TRUE(server_->sendErrorCodeMessage(999, 0, ReportLevel::DEVL_WARNING, "Simulated DEVL Warning")); + ASSERT_TRUE(server_->sendErrorCodeMessage(999, 0, ReportLevel::DEVL_VIOLATION, "Simulated DEVL Violation")); + ASSERT_TRUE(server_->sendErrorCodeMessage(999, 0, ReportLevel::DEVL_FAULT, "Simulated DEVL Fault")); + ASSERT_TRUE(server_->sendErrorCodeMessage(999, 0, ReportLevel::DEVL_CRITICAL_FAULT, "Simulated DEVL Critical Fault")); + + auto errors = client_->getErrorCodes(); + + // Wait until we asynchronously receive all the error codes we sent. + waitFor( + [&errors, this]() { + auto new_errors = client_->getErrorCodes(); + errors.insert(errors.end(), new_errors.begin(), new_errors.end()); + return errors.size() >= 12; + }, + std::chrono::seconds(1)); + + EXPECT_EQ(errors[0].report_level, ReportLevel::DEBUG); + EXPECT_EQ(errors[1].report_level, ReportLevel::INFO); + EXPECT_EQ(errors[2].report_level, ReportLevel::WARNING); + EXPECT_EQ(errors[3].report_level, ReportLevel::VIOLATION); + EXPECT_EQ(errors[4].report_level, ReportLevel::FAULT); + EXPECT_EQ(errors[5].report_level, ReportLevel::CRITICAL_FAULT); + EXPECT_EQ(errors[6].report_level, ReportLevel::DEVL_DEBUG); + EXPECT_EQ(errors[7].report_level, ReportLevel::DEVL_INFO); + EXPECT_EQ(errors[8].report_level, ReportLevel::DEVL_WARNING); + EXPECT_EQ(errors[9].report_level, ReportLevel::DEVL_VIOLATION); + EXPECT_EQ(errors[10].report_level, ReportLevel::DEVL_FAULT); + EXPECT_EQ(errors[11].report_level, ReportLevel::DEVL_CRITICAL_FAULT); +} + int main(int argc, char* argv[]) { ::testing::InitGoogleTest(&argc, argv); From db63a4f650dedf2596d0cd776552683f8a63738b Mon Sep 17 00:00:00 2001 From: Felix Exner Date: Mon, 15 Jun 2026 16:57:00 +0200 Subject: [PATCH 16/16] Use switch case for report level treatment This way, every possible level has to be explicitly categorized. --- src/primary/primary_client.cpp | 34 +++++++++++++++++++++++----------- 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/src/primary/primary_client.cpp b/src/primary/primary_client.cpp index 7c8fb2644..c53af8540 100644 --- a/src/primary/primary_client.cpp +++ b/src/primary/primary_client.cpp @@ -265,20 +265,32 @@ void PrimaryClient::sendScriptBlocking(const std::string& program, std::string s for (auto error : errors) { - if (error.report_level == ReportLevel::VIOLATION || error.report_level == ReportLevel::FAULT) + switch (error.report_level) { - error_stream << "Code: " << error.to_string << ", severity: " << reportLevelString(error.report_level) - << "\n"; - is_error = true; - } - if (error.report_level == ReportLevel::WARNING) - { - if (fail_on_warnings) - { + case ReportLevel::VIOLATION: + case ReportLevel::FAULT: + case ReportLevel::CRITICAL_FAULT: error_stream << "Code: " << error.to_string << ", severity: " << reportLevelString(error.report_level) << "\n"; - } - is_warning = true; + is_error = true; + break; + case ReportLevel::WARNING: + if (fail_on_warnings) + { + error_stream << "Code: " << error.to_string << ", severity: " << reportLevelString(error.report_level) + << "\n"; + } + is_warning = true; + break; + case ReportLevel::DEBUG: + case ReportLevel::INFO: + case ReportLevel::DEVL_DEBUG: + case ReportLevel::DEVL_INFO: + case ReportLevel::DEVL_WARNING: + case ReportLevel::DEVL_VIOLATION: + case ReportLevel::DEVL_FAULT: + case ReportLevel::DEVL_CRITICAL_FAULT: + break; } if (error.message_code == 210) {