Skip to content

Commit f783f1c

Browse files
authored
If no new commands are received, halt (#440)
Currently, we only exit the main loop when no new command is received anymore (already taking any configured receive timeout into account). However, if we don't receive a new command anymore, that should always be considered an unintentional interruption of the control flow. In that case, we should stop program execution, as interrupting the communication was an unexpected error event. If interrupting the communication in order to continue with the rest of the program was intentional, users will send a stop command. This will end the main control loop at another code branch leading to a clean shutdown of the external_control part in order to be able to continue with the rest of the program. This should be closing #438 but in order to make sense with the ROS drivers, we would need UniversalRobots/Universal_Robots_ROS2_Driver#1678 and potentially UniversalRobots/Universal_Robots_ROS_Driver#767 before merging this.
1 parent a131a32 commit f783f1c

9 files changed

Lines changed: 395 additions & 384 deletions

resources/external_control.urscript

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1003,7 +1003,8 @@ while control_mode > MODE_STOPPED:
10031003
end
10041004
else:
10051005
textmsg("Socket timed out waiting for command on reverse_socket. The script will exit now.")
1006-
control_mode = MODE_STOPPED
1006+
stopj(STOPJ_ACCELERATION)
1007+
halt
10071008
end
10081009
exit_critical
10091010
end

tests/CMakeLists.txt

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,22 @@ if (INTEGRATION_TESTS)
8282
TEST_SUFFIX _headless
8383
)
8484

85+
# ExternalControlProgram tests
86+
add_executable(external_control_program_tests_urcap test_external_control_program.cpp)
87+
target_link_libraries(external_control_program_tests_urcap PRIVATE ur_client_library::urcl GTest::gtest_main)
88+
gtest_add_tests(TARGET external_control_program_tests_urcap
89+
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
90+
EXTRA_ARGS --headless false
91+
TEST_SUFFIX _urcap
92+
)
93+
add_executable(external_control_program_tests_headless test_external_control_program.cpp)
94+
target_link_libraries(external_control_program_tests_headless PRIVATE ur_client_library::urcl GTest::gtest_main)
95+
gtest_add_tests(TARGET external_control_program_tests_headless
96+
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
97+
EXTRA_ARGS --headless true
98+
TEST_SUFFIX _headless
99+
)
100+
85101
# InstructionExecutor tests
86102
add_executable(instruction_executor_test_urcap test_instruction_executor.cpp)
87103
target_link_libraries(instruction_executor_test_urcap PRIVATE ur_client_library::urcl GTest::gtest_main)
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
// -- BEGIN LICENSE BLOCK ----------------------------------------------
2+
// Copyright 2026 Universal Robots A/S
3+
//
4+
// Redistribution and use in source and binary forms, with or without
5+
// modification, are permitted provided that the following conditions are met:
6+
//
7+
// * Redistributions of source code must retain the above copyright
8+
// notice, this list of conditions and the following disclaimer.
9+
//
10+
// * Redistributions in binary form must reproduce the above copyright
11+
// notice, this list of conditions and the following disclaimer in the
12+
// documentation and/or other materials provided with the distribution.
13+
//
14+
// * Neither the name of the {copyright_holder} nor the names of its
15+
// contributors may be used to endorse or promote products derived from
16+
// this software without specific prior written permission.
17+
//
18+
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
19+
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
20+
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
21+
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
22+
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
23+
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
24+
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
25+
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
26+
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
27+
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
28+
// POSSIBILITY OF SUCH DAMAGE.
29+
// -- END LICENSE BLOCK ------------------------------------------------
30+
31+
#include <gtest/gtest.h>
32+
33+
#include "test_utils.h"
34+
#include "ur_client_library/example_robot_wrapper.h"
35+
36+
using namespace urcl;
37+
const std::string SCRIPT_FILE = "../resources/external_control.urscript";
38+
const std::string OUTPUT_RECIPE = "resources/rtde_output_recipe.txt";
39+
const std::string INPUT_RECIPE = "resources/rtde_input_recipe.txt";
40+
std::string g_ROBOT_IP = "192.168.56.101";
41+
bool g_HEADLESS = true;
42+
43+
std::unique_ptr<ExampleRobotWrapper> g_my_robot;
44+
45+
class ExternalControlProgramTest : public ::testing::Test
46+
{
47+
protected:
48+
static void SetUpTestSuite()
49+
{
50+
if (!(robotVersionLessThan(g_ROBOT_IP, "10.0.0") || g_HEADLESS))
51+
{
52+
GTEST_SKIP_("Running URCap tests for PolyScope X is currently not supported.");
53+
}
54+
}
55+
void SetUp() override
56+
{
57+
std::string modified_script_path = extendScript(SCRIPT_FILE);
58+
59+
g_my_robot = std::make_unique<ExampleRobotWrapper>(g_ROBOT_IP, OUTPUT_RECIPE, INPUT_RECIPE, g_HEADLESS,
60+
"external_control.urp", modified_script_path);
61+
if (!g_my_robot->isHealthy())
62+
{
63+
ASSERT_TRUE(g_my_robot->resendRobotProgram());
64+
ASSERT_TRUE(g_my_robot->waitForProgramRunning(500));
65+
}
66+
server_.reset(new TestableTcpServer(60005));
67+
server_->start();
68+
}
69+
70+
void TearDown() override
71+
{
72+
server_.reset();
73+
}
74+
75+
std::string extendScript(const std::string& script_path)
76+
{
77+
char modified_script_path[] = "urscript.XXXXXX";
78+
#ifdef _WIN32
79+
# define mkstemp _mktemp_s
80+
#endif
81+
std::ignore = mkstemp(modified_script_path);
82+
83+
std::ofstream ofs(modified_script_path);
84+
if (ofs.bad())
85+
{
86+
std::cout << "Failed to create temporary files" << std::endl;
87+
throw std::runtime_error("Failed to create temporary files");
88+
}
89+
std::ifstream in_file(script_path);
90+
std::string prog((std::istreambuf_iterator<char>(in_file)), (std::istreambuf_iterator<char>()));
91+
prog += "\nsocket_open(\"{{SERVER_IP_REPLACE}}\", 60005, \"test_socket\")\n";
92+
prog += "\nsleep(0.6)\n";
93+
prog += "\ntextmsg(\"sleeping done.\")\n";
94+
ofs << prog;
95+
ofs.close();
96+
97+
return modified_script_path;
98+
}
99+
100+
std::unique_ptr<TestableTcpServer> server_;
101+
};
102+
103+
TEST_F(ExternalControlProgramTest, program_halts_on_timeout)
104+
{
105+
vector6d_t zeros = { 0, 0, 0, 0, 0, 0 };
106+
g_my_robot->getUrDriver()->writeJointCommand(zeros, comm::ControlMode::MODE_IDLE, RobotReceiveTimeout::millisec(200));
107+
EXPECT_FALSE(server_->waitForConnectionCallback(1000));
108+
}
109+
110+
TEST_F(ExternalControlProgramTest, stop_control_does_not_halt_program)
111+
{
112+
vector6d_t zeros = { 0, 0, 0, 0, 0, 0 };
113+
g_my_robot->getUrDriver()->writeJointCommand(zeros, comm::ControlMode::MODE_IDLE, RobotReceiveTimeout::off());
114+
115+
// Make sure that we can stop the robot control, when robot receive timeout has been set off
116+
g_my_robot->getUrDriver()->stopControl();
117+
EXPECT_TRUE(server_->waitForConnectionCallback(1000));
118+
}
119+
120+
int main(int argc, char* argv[])
121+
{
122+
::testing::InitGoogleTest(&argc, argv);
123+
124+
for (int i = 0; i < argc; i++)
125+
{
126+
if (std::string(argv[i]) == "--robot_ip" && i + 1 < argc)
127+
{
128+
g_ROBOT_IP = argv[i + 1];
129+
++i;
130+
}
131+
if (std::string(argv[i]) == "--headless" && i + 1 < argc)
132+
{
133+
std::string headless = argv[i + 1];
134+
g_HEADLESS = headless == "true" || headless == "1" || headless == "True" || headless == "TRUE";
135+
++i;
136+
}
137+
}
138+
139+
return RUN_ALL_TESTS();
140+
}

tests/test_pipeline.cpp

Lines changed: 11 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@
3131
#include <gtest/gtest.h>
3232
#include <condition_variable>
3333

34+
#include "test_utils.h"
35+
3436
#include <ur_client_library/comm/pipeline.h>
3537
#include <ur_client_library/comm/tcp_server.h>
3638
#include <ur_client_library/comm/stream.h>
@@ -45,8 +47,7 @@ class PipelineTest : public ::testing::Test
4547
protected:
4648
void SetUp()
4749
{
48-
server_.reset(new comm::TCPServer(60002));
49-
server_->setConnectCallback(std::bind(&PipelineTest::connectionCallback, this, std::placeholders::_1));
50+
server_.reset(new TestableTcpServer(60002));
5051
server_->start();
5152

5253
// Setup pipeline
@@ -68,28 +69,7 @@ class PipelineTest : public ::testing::Test
6869
server_.reset();
6970
}
7071

71-
void connectionCallback(const socket_t filedescriptor)
72-
{
73-
std::lock_guard<std::mutex> lk(connect_mutex_);
74-
client_fd_ = filedescriptor;
75-
connect_cv_.notify_one();
76-
connection_callback_ = true;
77-
}
78-
79-
bool waitForConnectionCallback(int milliseconds = 100)
80-
{
81-
std::unique_lock<std::mutex> lk(connect_mutex_);
82-
if (connect_cv_.wait_for(lk, std::chrono::milliseconds(milliseconds)) == std::cv_status::no_timeout ||
83-
connection_callback_ == true)
84-
{
85-
connection_callback_ = false;
86-
return true;
87-
}
88-
return false;
89-
}
90-
91-
std::unique_ptr<comm::TCPServer> server_;
92-
socket_t client_fd_;
72+
std::unique_ptr<TestableTcpServer> server_;
9373

9474
std::unique_ptr<comm::URStream<rtde_interface::RTDEPackage>> stream_;
9575
std::unique_ptr<rtde_interface::RTDEParser> parser_;
@@ -138,8 +118,6 @@ class PipelineTest : public ::testing::Test
138118
private:
139119
std::condition_variable connect_cv_;
140120
std::mutex connect_mutex_;
141-
142-
bool connection_callback_ = false;
143121
};
144122

145123
TEST_F(PipelineTest, get_product_from_stopped_pipeline)
@@ -151,13 +129,13 @@ TEST_F(PipelineTest, get_product_from_stopped_pipeline)
151129

152130
TEST_F(PipelineTest, get_product_from_running_pipeline)
153131
{
154-
waitForConnectionCallback();
132+
server_->waitForConnectionCallback();
155133
pipeline_->run();
156134

157135
// RTDE package with timestamp
158136
uint8_t data_package[] = { 0x00, 0x0c, 0x55, 0x01, 0x40, 0xbb, 0xbf, 0xdb, 0xa5, 0xe3, 0x53, 0xf7 };
159137
size_t written;
160-
server_->write(client_fd_, data_package, sizeof(data_package), written);
138+
server_->write(data_package, sizeof(data_package), written);
161139

162140
std::unique_ptr<rtde_interface::RTDEPackage> urpackage;
163141
std::chrono::milliseconds timeout{ 500 };
@@ -178,13 +156,13 @@ TEST_F(PipelineTest, get_product_from_running_pipeline)
178156

179157
TEST_F(PipelineTest, stop_pipeline)
180158
{
181-
waitForConnectionCallback();
159+
server_->waitForConnectionCallback();
182160
pipeline_->run();
183161

184162
// RTDE package with timestamp
185163
uint8_t data_package[] = { 0x00, 0x0c, 0x55, 0x01, 0x40, 0xbb, 0xbf, 0xdb, 0xa5, 0xe3, 0x53, 0xf7 };
186164
size_t written;
187-
server_->write(client_fd_, data_package, sizeof(data_package), written);
165+
server_->write(data_package, sizeof(data_package), written);
188166

189167
std::unique_ptr<rtde_interface::RTDEPackage> urpackage;
190168
std::chrono::milliseconds timeout{ 500 };
@@ -206,13 +184,13 @@ TEST_F(PipelineTest, consumer_pipeline)
206184
pipeline_.reset(
207185
new comm::Pipeline<rtde_interface::RTDEPackage>(*producer_.get(), &consumer, "RTDE_PIPELINE", notifier_));
208186
pipeline_->init();
209-
waitForConnectionCallback();
187+
server_->waitForConnectionCallback();
210188
pipeline_->run();
211189

212190
// RTDE package with timestamp
213191
uint8_t data_package[] = { 0x00, 0x0c, 0x55, 0x01, 0x40, 0xbb, 0xbf, 0xdb, 0xa5, 0xe3, 0x53, 0xf7 };
214192
size_t written;
215-
server_->write(client_fd_, data_package, sizeof(data_package), written);
193+
server_->write(data_package, sizeof(data_package), written);
216194

217195
// Wait for data to be consumed
218196
int max_retries = 3;
@@ -223,7 +201,7 @@ TEST_F(PipelineTest, consumer_pipeline)
223201
{
224202
break;
225203
}
226-
server_->write(client_fd_, data_package, sizeof(data_package), written);
204+
server_->write(data_package, sizeof(data_package), written);
227205
count++;
228206
}
229207
EXPECT_LT(count, max_retries);

tests/test_producer.cpp

Lines changed: 5 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
#include <gtest/gtest.h>
3232
#include <chrono>
3333
#include <condition_variable>
34+
#include "test_utils.h"
3435

3536
#include <ur_client_library/comm/producer.h>
3637
#include <ur_client_library/comm/stream.h>
@@ -44,8 +45,7 @@ class ProducerTest : public ::testing::Test
4445
protected:
4546
void SetUp()
4647
{
47-
server_.reset(new comm::TCPServer(60002));
48-
server_->setConnectCallback(std::bind(&ProducerTest::connectionCallback, this, std::placeholders::_1));
48+
server_.reset(new TestableTcpServer(60002));
4949
server_->start();
5050
}
5151

@@ -55,34 +55,7 @@ class ProducerTest : public ::testing::Test
5555
server_.reset();
5656
}
5757

58-
void connectionCallback(const socket_t filedescriptor)
59-
{
60-
std::lock_guard<std::mutex> lk(connect_mutex_);
61-
client_fd_ = filedescriptor;
62-
connect_cv_.notify_one();
63-
connection_callback_ = true;
64-
}
65-
66-
bool waitForConnectionCallback(int milliseconds = 100)
67-
{
68-
std::unique_lock<std::mutex> lk(connect_mutex_);
69-
if (connect_cv_.wait_for(lk, std::chrono::milliseconds(milliseconds)) == std::cv_status::no_timeout ||
70-
connection_callback_ == true)
71-
{
72-
connection_callback_ = false;
73-
return true;
74-
}
75-
return false;
76-
}
77-
78-
std::unique_ptr<comm::TCPServer> server_;
79-
socket_t client_fd_;
80-
81-
private:
82-
std::condition_variable connect_cv_;
83-
std::mutex connect_mutex_;
84-
85-
bool connection_callback_ = false;
58+
std::unique_ptr<TestableTcpServer> server_;
8659
};
8760

8861
TEST_F(ProducerTest, get_data_package)
@@ -94,13 +67,13 @@ TEST_F(ProducerTest, get_data_package)
9467
comm::URProducer<rtde_interface::RTDEPackage> producer(stream, parser);
9568

9669
producer.setupProducer();
97-
waitForConnectionCallback();
70+
server_->waitForConnectionCallback();
9871
producer.startProducer();
9972

10073
// RTDE package with timestamp
10174
uint8_t data_package[] = { 0x00, 0x0c, 0x55, 0x01, 0x40, 0xbb, 0xbf, 0xdb, 0xa5, 0xe3, 0x53, 0xf7 };
10275
size_t written;
103-
server_->write(client_fd_, data_package, sizeof(data_package), written);
76+
server_->write(data_package, sizeof(data_package), written);
10477

10578
std::vector<std::unique_ptr<rtde_interface::RTDEPackage>> products;
10679
EXPECT_EQ(producer.tryGet(products), true);

0 commit comments

Comments
 (0)