Skip to content

Commit e61b18d

Browse files
authored
sendScriptBlocking: Restart interface and resend script on readOnlyInterfaceException (#522)
Implements functionality to restart primary interface and retry execution of script code, if the interface was initially in read-only mode.
1 parent 6f52f02 commit e61b18d

4 files changed

Lines changed: 101 additions & 28 deletions

File tree

doc/architecture/primary_client.rst

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,18 +26,21 @@ Method signature:
2626
.. code-block:: c++
2727

2828
bool sendScriptBlocking(
29-
std::string program,
30-
std::string script_name = "",
31-
std::chrono::milliseconds timeout = std::chrono::seconds(1),
32-
bool fail_on_warnings = true
29+
const std::string& program,
30+
const std::string& script_name = "",
31+
const std::chrono::milliseconds start_timeout = std::chrono::seconds(1),
32+
const bool fail_on_warnings = true,
33+
const bool retry_on_readonly_interface = true
3334
);
3435

3536
| The ``sendScriptBlocking`` method will also accept valid URScript code, but blocks until the execution result of the given program is available.
3637
| 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.
3738
| 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.
38-
| If the program has not started within the given ``timeout``, the method throws an exception.
39+
| If the program has not started within the given ``start_timeout``, the method throws an exception.
3940
| If the robot encounters an error or runtime exception during program execution the method also throws an exception.
4041
| 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.
42+
| If ``retry_on_readonly_interface`` is true, the method will restart the primary interface and retry sending the program, if the primary interface is read-only. It will retry once, and if the interface is still read-only, an exception will be thrown. If false, the exception will be thrown, without restarting/retrying.
4143
| If no exceptions are thrown, the script has been executed successfully.
4244
| 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.
4345
| The exact exceptions that are thrown in various cases can be seen in the `primary client header file <https://github.com/UniversalRobots/Universal_Robots_Client_Library/blob/master/include/ur_client_library/primary/primary_client.h>`_.
46+
| Note: This method clears all stored error codes in the client during execution.

include/ur_client_library/primary/primary_client.h

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,10 @@ class PrimaryClient
9595
/*!
9696
* \brief Sends a custom script program to the robot.
9797
*
98-
* The given code must be valid according the UR Scripting Manual.
98+
* The given code must be valid according the UR Scripting Manual. This function doesn't give any
99+
* feedback whether the script was executed successfully or not, it only reports whether the
100+
* script was uploaded to the robot or not. For feedback on the execution of the script, use
101+
* sendScriptBlocking().
99102
*
100103
* \param program URScript code that shall be executed by the robot.
101104
*
@@ -123,6 +126,14 @@ class PrimaryClient
123126
* \param fail_on_warnings Whether or not the function should report a failure, if the robot reports a warning-level
124127
* error during execution. Default true
125128
*
129+
* \param retry_on_readonly_interface Whether to retry, if the primary interface is read-only. This will restart the
130+
* primary interface connection, and then try sending the script again. If the interface is still read-only a
131+
* ReadOnlyInterfaceException will be thrown. Default true
132+
* \note The primary interface connection is read-only when the robot is not in remote control
133+
* mode. If the robot switches from local control mode to remote control mode, the connection
134+
* remains read-only. In this case, reconnecting will result in a read-write primary interface connection, and the
135+
* script can be executed successfully.
136+
*
126137
* \throw urcl::ScriptCodeSyntaxException if the given script code has syntax errors, which are checked here.
127138
* \throw urcl::UrException if the stop command cannot be sent to the robot.
128139
* \throw urcl::TimeoutException if the robot doesn't stop the program within the given timeout.
@@ -136,9 +147,9 @@ class PrimaryClient
136147
* transferred. This can happen if the robot was recently switched from manual to remote control mode.
137148
* \throw urcl::RobotErrorCodeException if the robot encounters an error during script execution.
138149
*/
139-
void sendScriptBlocking(const std::string& program, std::string script_name = "",
140-
std::chrono::milliseconds start_timeout = std::chrono::seconds(1),
141-
bool fail_on_warnings = true);
150+
void sendScriptBlocking(const std::string& program, const std::string& script_name = "",
151+
const std::chrono::milliseconds start_timeout = std::chrono::seconds(1),
152+
const bool fail_on_warnings = true, const bool retry_on_readonly_interface = true);
142153

143154
bool checkCalibration(const std::string& checksum);
144155

@@ -338,7 +349,8 @@ class PrimaryClient
338349
*/
339350
RobotSeries getRobotSeries();
340351

341-
/* \brief Check if the current safety mode allows for script execution
352+
/*!
353+
* \brief Check if the current safety mode allows for script execution
342354
*
343355
* Safety modes allowing for execution are: NORMAL, REDUCED, RECOVERY, UNDEFINED_SAFETY_MODE
344356
*/
@@ -359,9 +371,11 @@ class PrimaryClient
359371
void keyMessageCallback(KeyMessage& msg);
360372
void runtimeExceptionCallback(RuntimeExceptionMessage& msg);
361373

362-
ScriptInfo prepare_script(std::string script, std::string script_name);
363-
std::vector<std::string> strip_comments_and_whitespace(std::vector<std::string> script_lines);
364-
std::string truncate_script_name(std::string candidate_name);
374+
ScriptInfo prepareScript(std::string script, std::string script_name);
375+
std::vector<std::string> stripCommentsAndWhitespace(std::vector<std::string> script_lines);
376+
std::string truncateScriptName(std::string candidate_name);
377+
void sendScriptMonitorExecution(const ScriptInfo& script_info, const std::chrono::milliseconds& timeout,
378+
const bool fail_on_warnings);
365379

366380
PrimaryParser parser_;
367381
std::shared_ptr<PrimaryConsumer> consumer_;

src/primary/primary_client.cpp

Lines changed: 38 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -137,18 +137,19 @@ bool PrimaryClient::safetyModeAllowsExecution()
137137
}
138138
}
139139

140-
void PrimaryClient::sendScriptBlocking(const std::string& program, std::string script_name,
141-
std::chrono::milliseconds timeout, bool fail_on_warnings)
140+
void PrimaryClient::sendScriptBlocking(const std::string& program, const std::string& script_name,
141+
const std::chrono::milliseconds start_timeout, const bool fail_on_warnings,
142+
const bool retry_on_readonly_interface)
142143
{
143-
ScriptInfo script_info = prepare_script(program, script_name);
144+
ScriptInfo script_info = prepareScript(program, script_name);
144145

145146
RobotMode robot_mode = getRobotMode();
146147
std::chrono::milliseconds robot_mode_timeout(1000);
147-
auto start = std::chrono::system_clock::now();
148+
auto start_time = std::chrono::system_clock::now();
148149
while (robot_mode == RobotMode::UNKNOWN)
149150
{
150151
auto now = std::chrono::system_clock::now();
151-
if (std::chrono::duration_cast<std::chrono::milliseconds>(now - start).count() > robot_mode_timeout.count())
152+
if (std::chrono::duration_cast<std::chrono::milliseconds>(now - start_time).count() > robot_mode_timeout.count())
152153
{
153154
throw TimeoutException("Robot mode not received within timeout. ", robot_mode_timeout);
154155
}
@@ -172,6 +173,31 @@ void PrimaryClient::sendScriptBlocking(const std::string& program, std::string s
172173

173174
throw SafetyModeException("Script execution via primary interface", allowed_modes, getSafetyMode());
174175
}
176+
177+
try
178+
{
179+
sendScriptMonitorExecution(script_info, start_timeout, fail_on_warnings);
180+
}
181+
catch ([[maybe_unused]] const ReadOnlyInterfaceException& exc)
182+
{
183+
if (retry_on_readonly_interface)
184+
{
185+
URCL_LOG_INFO("Script execution failed due to the primary interface being read-only. Restarting primary "
186+
"interface and retrying once.");
187+
stop();
188+
start();
189+
sendScriptMonitorExecution(script_info, start_timeout, fail_on_warnings);
190+
}
191+
else
192+
{
193+
throw;
194+
}
195+
}
196+
}
197+
198+
void PrimaryClient::sendScriptMonitorExecution(const ScriptInfo& script_info, const std::chrono::milliseconds& timeout,
199+
const bool fail_on_warnings)
200+
{
175201
// Clear runtime exception
176202
{
177203
std::scoped_lock lock(runtime_exception_mutex_);
@@ -191,6 +217,7 @@ void PrimaryClient::sendScriptBlocking(const std::string& program, std::string s
191217
throw StreamNotConnectedException("Script could not be sent to the robot. Ensure that the primary interface is "
192218
"connected.");
193219
}
220+
194221
// No feedback from secondary programs, so we assume success
195222
if (script_info.script_type == ScriptTypes::SEC)
196223
{
@@ -389,7 +416,7 @@ void PrimaryClient::sendScriptBlocking(const std::string& program, std::string s
389416
}
390417
}
391418

392-
std::vector<std::string> PrimaryClient::strip_comments_and_whitespace(std::vector<std::string> split_script)
419+
std::vector<std::string> PrimaryClient::stripCommentsAndWhitespace(std::vector<std::string> split_script)
393420
{
394421
std::vector<std::string> stripped_script;
395422
for (auto line : split_script)
@@ -413,7 +440,7 @@ std::vector<std::string> PrimaryClient::strip_comments_and_whitespace(std::vecto
413440
return stripped_script;
414441
}
415442

416-
std::string PrimaryClient::truncate_script_name(const std::string candidate_name)
443+
std::string PrimaryClient::truncateScriptName(const std::string candidate_name)
417444
{
418445
std::string final_name = candidate_name;
419446
// Limit script name length to 31, to ensure backwards compatibility
@@ -426,13 +453,13 @@ std::string PrimaryClient::truncate_script_name(const std::string candidate_name
426453
return final_name;
427454
}
428455

429-
ScriptInfo PrimaryClient::prepare_script(std::string script, std::string script_name)
456+
ScriptInfo PrimaryClient::prepareScript(std::string script, std::string script_name)
430457
{
431458
// Split the given script in to separate lines
432459
std::vector<std::string> split_script = splitString(script, "\n");
433460

434461
// Remove all comments and white-space-only lines
435-
std::vector<std::string> stripped_script = strip_comments_and_whitespace(split_script);
462+
std::vector<std::string> stripped_script = stripCommentsAndWhitespace(split_script);
436463

437464
if (stripped_script.size() == 0)
438465
{
@@ -452,7 +479,7 @@ ScriptInfo PrimaryClient::prepare_script(std::string script, std::string script_
452479
stripped_script[0].substr(0, 4).find("sec ") == script.npos)
453480
{
454481
// Check that the final name is not too long
455-
actual_script_name = truncate_script_name(actual_script_name);
482+
actual_script_name = truncateScriptName(actual_script_name);
456483
std::string definition = "def " + actual_script_name + "():";
457484
std::string end = "end";
458485
// Add indentation to the existing script code
@@ -483,7 +510,7 @@ ScriptInfo PrimaryClient::prepare_script(std::string script, std::string script_
483510
actual_script_type = ScriptTypes::SEC;
484511
}
485512
// Check that the script name is not too long, replace it, if it is
486-
actual_script_name = truncate_script_name(name_in_script);
513+
actual_script_name = truncateScriptName(name_in_script);
487514
if (actual_script_name.size() != name_in_script.size())
488515
{
489516
stripped_script[0].replace(stripped_script[0].find(name_in_script), name_in_script.size(), actual_script_name);

tests/test_primary_client.cpp

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -666,12 +666,41 @@ TEST_F(PrimaryClientFakeTest, test_send_script_to_read_only_server)
666666
// Make the fake server send an error code message with code 210 (read-only primary interface) when it receives a
667667
// script
668668
server_->setScriptCallback([this, script_code]([[maybe_unused]] const std::string& payload) {
669-
ASSERT_TRUE(
670-
server_->sendErrorCodeMessage(210, 0, ReportLevel::VIOLATION, "Simulated read-only primary interface error"));
669+
if (payload.find(script_code) != std::string::npos)
670+
{
671+
ASSERT_TRUE(
672+
server_->sendErrorCodeMessage(210, 0, ReportLevel::VIOLATION, "Simulated read-only primary interface error"));
673+
}
671674
});
672675

673-
EXPECT_THROW(client_->sendScriptBlocking(script_code, "test_fun", std::chrono::milliseconds(1000), false),
676+
// Fails even with retry
677+
EXPECT_THROW(client_->sendScriptBlocking(script_code, "test_fun", std::chrono::milliseconds(1000), false, true),
674678
ReadOnlyInterfaceException);
679+
680+
bool retry = false;
681+
// Send C210 on first try, then succeed
682+
server_->setScriptCallback([this, script_code, &retry]([[maybe_unused]] const std::string& payload) {
683+
if (!retry && payload.find(script_code) != std::string::npos)
684+
{
685+
ASSERT_TRUE(
686+
server_->sendErrorCodeMessage(210, 0, ReportLevel::VIOLATION, "Simulated read-only primary interface error"));
687+
retry = true;
688+
}
689+
else if (payload.find(script_code) != std::string::npos)
690+
{
691+
server_->sendKeyMessage("PROGRAM_XXX_STARTED", "test_fun");
692+
ASSERT_EQ(payload, "def test_fun():\n " + script_code + "\nend\n\n");
693+
server_->sendKeyMessage("PROGRAM_XXX_STOPPED", "test_fun");
694+
}
695+
});
696+
697+
// Fails with no retry
698+
EXPECT_THROW(client_->sendScriptBlocking(script_code, "test_fun", std::chrono::milliseconds(1000), false, false),
699+
ReadOnlyInterfaceException);
700+
701+
retry = false;
702+
// Succeeds on retry
703+
EXPECT_NO_THROW(client_->sendScriptBlocking(script_code, "test_fun", std::chrono::milliseconds(1000), false, true));
675704
}
676705

677706
TEST_F(PrimaryClientFakeTest, test_send_script_blocking_timeout_on_no_response)
@@ -681,7 +710,7 @@ TEST_F(PrimaryClientFakeTest, test_send_script_blocking_timeout_on_no_response)
681710
const std::string script_code = "textmsg(\"Still running\")";
682711

683712
// We do not set a script callback on the fake server, so it will not respond to the script being sent. This should
684-
// cause sendScriptBlocking to time out and return false.
713+
// cause sendScriptBlocking to time out and throw a TimeoutException.
685714
EXPECT_THROW(client_->sendScriptBlocking(script_code, "test_fun", std::chrono::milliseconds(100), false),
686715
TimeoutException);
687716
}

0 commit comments

Comments
 (0)