Skip to content

Commit 730e6e7

Browse files
committed
poc of streaming interface and urscript implementation
1 parent 7b14796 commit 730e6e7

4 files changed

Lines changed: 281 additions & 9 deletions

File tree

include/ur_client_library/control/reverse_interface.h

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,13 @@ namespace control
4848
*/
4949
enum class TrajectoryControlMessage : int32_t
5050
{
51-
TRAJECTORY_CANCEL = -1, ///< Represents command to cancel currently active trajectory.
52-
TRAJECTORY_NOOP = 0, ///< Represents no new control command.
53-
TRAJECTORY_START = 1, ///< Represents command to start a new trajectory.
51+
TRAJECTORY_CANCEL = -1, ///< Represents command to cancel currently active trajectory.
52+
TRAJECTORY_NOOP = 0, ///< Represents no new control command.
53+
TRAJECTORY_START = 1, ///< Represents command to start a new finite trajectory of declared length.
54+
TRAJECTORY_STREAM_START = 2, ///< Represents command to start an open-ended streaming trajectory.
55+
TRAJECTORY_STREAM_END = 3, ///< Represents command to terminate an open-ended streaming trajectory.
56+
///< The point_number argument must equal the total number of spline points
57+
///< the caller wrote on the trajectory socket since TRAJECTORY_STREAM_START.
5458
};
5559

5660
/*!

resources/external_control.urscript

Lines changed: 44 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,16 @@ REVERSE_INTERFACE_DATA_DIMENSION = 8
3333

3434
TRAJECTORY_MODE_RECEIVE = 1
3535
TRAJECTORY_MODE_CANCEL = -1
36+
TRAJECTORY_MODE_STREAM_START = 2
37+
TRAJECTORY_MODE_STREAM_END = 3
38+
39+
# Sentinel value used for trajectory_points_left while an open-ended
40+
# streaming trajectory is in progress. The trajectoryThread predicate
41+
# "trajectory_points_left > 0" stays as written; this large starting
42+
# value keeps the loop running until either STREAM_END converts the
43+
# remaining unread points to a finite count, or the producer underruns.
44+
# 2^31 - 1 is exactly representable in URScript (double precision).
45+
STREAMING_SENTINEL = 2147483647
3646

3747
MOTION_TYPE_MOVEJ = 0
3848
MOTION_TYPE_MOVEL = 1
@@ -102,6 +112,7 @@ global extrapolate_count = 0
102112
global extrapolate_max_count = 0
103113
global control_mode = MODE_UNINITIALIZED
104114
global trajectory_points_left = 0
115+
global trajectory_streaming = False
105116
global spline_qdd = [0, 0, 0, 0, 0, 0]
106117
global spline_qd = [0, 0, 0, 0, 0, 0]
107118
global tool_contact_running = False
@@ -538,6 +549,10 @@ thread trajectoryThread():
538549
textmsg("Executing trajectory. Number of points: ", trajectory_points_left)
539550
local is_first_point = True
540551
local is_robot_moving = False
552+
# Snapshot streaming-ness at thread spawn so the underrun else-branch
553+
# can distinguish a clean post-STREAM_END drain (flag has since been
554+
# cleared) from a mid-stream underrun (flag still set).
555+
local was_streaming_at_start = trajectory_streaming
541556
local INDEX_TIME = TRAJECTORY_DATA_DIMENSION
542557
local INDEX_BLEND = INDEX_TIME + 1
543558
# same index as blend parameter, depending on point type
@@ -754,8 +769,15 @@ thread trajectoryThread():
754769
spline_qd = [0, 0, 0, 0, 0, 0]
755770
end
756771
else:
757-
textmsg("Receiving trajectory point failed!")
758-
trajectory_result = TRAJECTORY_RESULT_FAILURE
772+
if was_streaming_at_start and not trajectory_streaming:
773+
# Producer signalled STREAM_END between the predicate check and
774+
# the socket read. Drop any pending unread points and exit cleanly
775+
# rather than spin one timeout per missing point.
776+
trajectory_points_left = 0
777+
else:
778+
textmsg("Receiving trajectory point failed!")
779+
trajectory_result = TRAJECTORY_RESULT_FAILURE
780+
end
759781
end
760782
is_first_point = False
761783
end
@@ -939,11 +961,11 @@ thread script_commands():
939961
friction_compensation_mode = FRICTION_COMP_MODE_FRICTION_SCALES
940962
viscous_scale = [raw_command[2] / MULT_jointstate, raw_command[3] / MULT_jointstate, raw_command[4] / MULT_jointstate, raw_command[5] / MULT_jointstate, raw_command[6] / MULT_jointstate, raw_command[7] / MULT_jointstate]
941963
coulomb_scale = [raw_command[8] / MULT_jointstate, raw_command[9] / MULT_jointstate, raw_command[10] / MULT_jointstate, raw_command[11] / MULT_jointstate, raw_command[12] / MULT_jointstate, raw_command[13] / MULT_jointstate]
942-
elif command == SET_TARGET_PAYLOAD:
943-
mass = raw_command[2] / MULT_jointstate
944-
cog = [raw_command[3] / MULT_jointstate, raw_command[4] / MULT_jointstate, raw_command[5] / MULT_jointstate]
964+
elif command == SET_TARGET_PAYLOAD:
965+
mass = raw_command[2] / MULT_jointstate
966+
cog = [raw_command[3] / MULT_jointstate, raw_command[4] / MULT_jointstate, raw_command[5] / MULT_jointstate]
945967
inertia = [raw_command[6] / MULT_jointstate, raw_command[7] / MULT_jointstate, raw_command[8] / MULT_jointstate, raw_command[9] / MULT_jointstate, raw_command[10] / MULT_jointstate, raw_command[11] / MULT_jointstate]
946-
transition_time = raw_command[12] / MULT_time
968+
transition_time = raw_command[12] / MULT_time
947969
{% if ROBOT_SOFTWARE_VERSION >= v5.10.0 %}
948970
set_target_payload(mass, cog, inertia, transition_time)
949971
{% else %}
@@ -1137,6 +1159,22 @@ while control_mode > MODE_STOPPED:
11371159
wait_for_trajectory_cleanup()
11381160
trajectory_points_left = params_mult[3]
11391161
thread_trajectory = run trajectoryThread()
1162+
elif params_mult[2] == TRAJECTORY_MODE_STREAM_START:
1163+
kill thread_trajectory
1164+
request_trajectory_cleanup()
1165+
wait_for_trajectory_cleanup()
1166+
trajectory_streaming = True
1167+
trajectory_points_left = STREAMING_SENTINEL
1168+
thread_trajectory = run trajectoryThread()
1169+
elif params_mult[2] == TRAJECTORY_MODE_STREAM_END:
1170+
# params_mult[3] is the total count of spline points the producer
1171+
# wrote to the trajectory socket since STREAM_START. Convert the
1172+
# in-flight sentinel into the remaining unread count:
1173+
# M_consumed = STREAMING_SENTINEL - trajectory_points_left
1174+
# remaining = params_mult[3] - M_consumed
1175+
# = trajectory_points_left + params_mult[3] - STREAMING_SENTINEL
1176+
trajectory_points_left = trajectory_points_left + params_mult[3] - STREAMING_SENTINEL
1177+
trajectory_streaming = False
11401178
elif params_mult[2] == TRAJECTORY_MODE_CANCEL:
11411179
textmsg("cancel received")
11421180
kill thread_trajectory

tests/CMakeLists.txt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,16 @@ if (INTEGRATION_TESTS)
114114
TEST_SUFFIX _headless
115115
)
116116

117+
# Streaming trajectory tests. Headless-only; the URCap path does not
118+
# exercise anything different at the URScript level for streaming.
119+
add_executable(trajectory_streaming_tests_headless test_trajectory_streaming.cpp)
120+
target_link_libraries(trajectory_streaming_tests_headless PRIVATE ur_client_library::urcl GTest::gtest_main)
121+
gtest_add_tests(TARGET trajectory_streaming_tests_headless
122+
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
123+
EXTRA_ARGS --headless true ${INTEGRATION_TESTS_ROBOT_IP_ARG}
124+
TEST_SUFFIX _headless
125+
)
126+
117127
# InstructionExecutor tests
118128
add_executable(instruction_executor_test_urcap test_instruction_executor.cpp)
119129
target_link_libraries(instruction_executor_test_urcap PRIVATE ur_client_library::urcl GTest::gtest_main)
Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
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 <chrono>
32+
#include <condition_variable>
33+
#include <memory>
34+
#include <mutex>
35+
#include <string>
36+
#include <thread>
37+
38+
#include <gtest/gtest.h>
39+
40+
#include "ur_client_library/control/trajectory_point_interface.h"
41+
#include "ur_client_library/example_robot_wrapper.h"
42+
#include "ur_client_library/log.h"
43+
#include "ur_client_library/types.h"
44+
45+
using namespace urcl;
46+
47+
const std::string SCRIPT_FILE = "../resources/external_control.urscript";
48+
const std::string OUTPUT_RECIPE = "resources/rtde_output_recipe.txt";
49+
const std::string INPUT_RECIPE = "resources/rtde_input_recipe.txt";
50+
std::string g_ROBOT_IP = "192.168.56.101";
51+
bool g_HEADLESS = true;
52+
53+
std::unique_ptr<ExampleRobotWrapper> g_my_robot;
54+
55+
std::condition_variable g_trajectory_result_cv;
56+
std::mutex g_trajectory_result_mutex;
57+
control::TrajectoryResult g_trajectory_result = control::TrajectoryResult::TRAJECTORY_RESULT_UNKNOWN;
58+
bool g_trajectory_result_received = false;
59+
60+
void handleTrajectoryState(control::TrajectoryResult state)
61+
{
62+
std::lock_guard<std::mutex> lk(g_trajectory_result_mutex);
63+
g_trajectory_result = state;
64+
g_trajectory_result_received = true;
65+
g_trajectory_result_cv.notify_one();
66+
URCL_LOG_INFO("Received trajectory result %s", control::trajectoryResultToString(state).c_str());
67+
}
68+
69+
class TrajectoryStreamingTest : public ::testing::Test
70+
{
71+
protected:
72+
static void SetUpTestSuite()
73+
{
74+
// Streaming tests are headless only. The URCap path doesn't exercise
75+
// anything different at the URScript level for streaming, so there is
76+
// no value in maintaining a separate _urcap variant.
77+
ASSERT_TRUE(g_HEADLESS) << "trajectory streaming tests require headless mode";
78+
79+
g_my_robot = std::make_unique<ExampleRobotWrapper>(g_ROBOT_IP, OUTPUT_RECIPE, INPUT_RECIPE, g_HEADLESS,
80+
"external_control.urp", SCRIPT_FILE);
81+
ASSERT_TRUE(g_my_robot->isHealthy());
82+
g_my_robot->getUrDriver()->registerTrajectoryDoneCallback(&handleTrajectoryState);
83+
}
84+
85+
void SetUp() override
86+
{
87+
if (!g_my_robot->isHealthy())
88+
{
89+
ASSERT_TRUE(g_my_robot->resendRobotProgram());
90+
ASSERT_TRUE(g_my_robot->waitForProgramRunning(500));
91+
}
92+
resetTrajectoryResultState();
93+
}
94+
95+
static void resetTrajectoryResultState()
96+
{
97+
std::lock_guard<std::mutex> lk(g_trajectory_result_mutex);
98+
g_trajectory_result = control::TrajectoryResult::TRAJECTORY_RESULT_UNKNOWN;
99+
g_trajectory_result_received = false;
100+
}
101+
102+
// Pump TRAJECTORY_NOOP on the reverse socket so the urscript's main
103+
// dispatcher read does not time out while we wait for the trajectory
104+
// result callback. Returns the captured result, or
105+
// TRAJECTORY_RESULT_UNKNOWN if the deadline elapsed first.
106+
control::TrajectoryResult waitForTrajectoryResultPumpingNoops(std::chrono::milliseconds total_timeout)
107+
{
108+
const auto deadline = std::chrono::steady_clock::now() + total_timeout;
109+
while (std::chrono::steady_clock::now() < deadline)
110+
{
111+
std::unique_lock<std::mutex> lk(g_trajectory_result_mutex);
112+
if (g_trajectory_result_cv.wait_for(lk, std::chrono::milliseconds(100),
113+
[] { return g_trajectory_result_received; }))
114+
{
115+
return g_trajectory_result;
116+
}
117+
lk.unlock();
118+
g_my_robot->getUrDriver()->writeTrajectoryControlMessage(
119+
control::TrajectoryControlMessage::TRAJECTORY_NOOP);
120+
}
121+
return control::TrajectoryResult::TRAJECTORY_RESULT_UNKNOWN;
122+
}
123+
};
124+
125+
// Clean end-to-end stream: STREAM_START, N spline points at a held pose,
126+
// STREAM_END(N). Expect TRAJECTORY_RESULT_SUCCESS via the end callback.
127+
// All points sit at the same pose so the test exercises the streaming
128+
// protocol without exercising motion dynamics.
129+
TEST_F(TrajectoryStreamingTest, stream_end_yields_success)
130+
{
131+
const vector6d_t held_pose = { 0.0, -1.57, 0.0, -1.57, 0.0, 0.0 };
132+
const vector6d_t zero = { 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 };
133+
134+
// Position the robot at held_pose with a one-shot finite trajectory.
135+
ASSERT_TRUE(g_my_robot->getUrDriver()->writeTrajectoryControlMessage(
136+
control::TrajectoryControlMessage::TRAJECTORY_START, 1));
137+
ASSERT_TRUE(g_my_robot->getUrDriver()->writeTrajectorySplinePoint(held_pose, zero, zero, 2.0f));
138+
ASSERT_EQ(control::TrajectoryResult::TRAJECTORY_RESULT_SUCCESS,
139+
waitForTrajectoryResultPumpingNoops(std::chrono::seconds(5)));
140+
resetTrajectoryResultState();
141+
142+
// Stream: STREAM_START, N points, STREAM_END(N).
143+
const int k_num_points = 50;
144+
const float k_step_time = 0.01f;
145+
ASSERT_TRUE(g_my_robot->getUrDriver()->writeTrajectoryControlMessage(
146+
control::TrajectoryControlMessage::TRAJECTORY_STREAM_START));
147+
for (int i = 0; i < k_num_points; ++i)
148+
{
149+
ASSERT_TRUE(g_my_robot->getUrDriver()->writeTrajectorySplinePoint(held_pose, zero, zero, k_step_time));
150+
}
151+
ASSERT_TRUE(g_my_robot->getUrDriver()->writeTrajectoryControlMessage(
152+
control::TrajectoryControlMessage::TRAJECTORY_STREAM_END, k_num_points));
153+
154+
EXPECT_EQ(control::TrajectoryResult::TRAJECTORY_RESULT_SUCCESS,
155+
waitForTrajectoryResultPumpingNoops(std::chrono::seconds(5)));
156+
}
157+
158+
// STREAM_START with no spline points and no STREAM_END. The trajectoryThread's
159+
// first socket_read on the trajectory socket times out (0.5s before any motion),
160+
// trips the underrun else-branch with both was_streaming_at_start and
161+
// trajectory_streaming still True, and emits TRAJECTORY_RESULT_FAILURE.
162+
TEST_F(TrajectoryStreamingTest, stream_underrun_yields_failure)
163+
{
164+
ASSERT_TRUE(g_my_robot->getUrDriver()->writeTrajectoryControlMessage(
165+
control::TrajectoryControlMessage::TRAJECTORY_STREAM_START));
166+
167+
EXPECT_EQ(control::TrajectoryResult::TRAJECTORY_RESULT_FAILURE,
168+
waitForTrajectoryResultPumpingNoops(std::chrono::seconds(3)));
169+
}
170+
171+
// STREAM_START followed by points, then TRAJECTORY_CANCEL before STREAM_END.
172+
// The existing cancel pathway tears down the trajectory thread, drains
173+
// remaining points via clearTrajectoryPointsThread (whose loop terminates
174+
// when the trajectory socket goes silent on first timeout), and emits
175+
// TRAJECTORY_RESULT_CANCELED.
176+
TEST_F(TrajectoryStreamingTest, stream_cancel_yields_canceled)
177+
{
178+
const vector6d_t held_pose = { 0.0, -1.57, 0.0, -1.57, 0.0, 0.0 };
179+
const vector6d_t zero = { 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 };
180+
181+
ASSERT_TRUE(g_my_robot->getUrDriver()->writeTrajectoryControlMessage(
182+
control::TrajectoryControlMessage::TRAJECTORY_STREAM_START));
183+
// Pile a batch of points into the OS socket buffer so the cleanup path
184+
// has something to drain.
185+
for (int i = 0; i < 100; ++i)
186+
{
187+
ASSERT_TRUE(g_my_robot->getUrDriver()->writeTrajectorySplinePoint(held_pose, zero, zero, 0.01f));
188+
}
189+
// Give the trajectoryThread a moment to consume a handful of points so
190+
// the cancel hits mid-stream rather than racing the spawn.
191+
std::this_thread::sleep_for(std::chrono::milliseconds(50));
192+
193+
ASSERT_TRUE(g_my_robot->getUrDriver()->writeTrajectoryControlMessage(
194+
control::TrajectoryControlMessage::TRAJECTORY_CANCEL));
195+
196+
EXPECT_EQ(control::TrajectoryResult::TRAJECTORY_RESULT_CANCELED,
197+
waitForTrajectoryResultPumpingNoops(std::chrono::seconds(5)));
198+
}
199+
200+
int main(int argc, char* argv[])
201+
{
202+
::testing::InitGoogleTest(&argc, argv);
203+
204+
for (int i = 0; i < argc; i++)
205+
{
206+
if (std::string(argv[i]) == "--robot_ip" && i + 1 < argc)
207+
{
208+
g_ROBOT_IP = argv[i + 1];
209+
++i;
210+
}
211+
if (std::string(argv[i]) == "--headless" && i + 1 < argc)
212+
{
213+
std::string headless = argv[i + 1];
214+
g_HEADLESS = headless == "true" || headless == "1" || headless == "True" || headless == "TRUE";
215+
++i;
216+
}
217+
}
218+
219+
return RUN_ALL_TESTS();
220+
}

0 commit comments

Comments
 (0)