Skip to content

Commit 7ab6a55

Browse files
committed
add an example
1 parent 730e6e7 commit 7ab6a55

2 files changed

Lines changed: 284 additions & 0 deletions

File tree

examples/CMakeLists.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,10 @@ add_executable(trajectory_point_interface_example
5959
trajectory_point_interface.cpp)
6060
target_link_libraries(trajectory_point_interface_example ur_client_library::urcl)
6161

62+
add_executable(trajectory_streaming_example
63+
trajectory_streaming.cpp)
64+
target_link_libraries(trajectory_streaming_example ur_client_library::urcl)
65+
6266
add_executable(instruction_executor
6367
instruction_executor.cpp)
6468
target_link_libraries(instruction_executor ur_client_library::urcl)

examples/trajectory_streaming.cpp

Lines changed: 280 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,280 @@
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+
// ----------------------------------------------------------------------------
32+
// Trajectory streaming example
33+
//
34+
// Demonstrates the open-ended (streaming) trajectory feature: a producer
35+
// streams individual spline points to the controller without committing to a
36+
// total point count up front, then signals end-of-stream when finished.
37+
//
38+
// Compare with examples/trajectory_point_interface.cpp, which uses the legacy
39+
// finite-trajectory path where the total point count is declared in the
40+
// initial TRAJECTORY_START message.
41+
//
42+
// API shape:
43+
//
44+
// writeTrajectoryControlMessage(TRAJECTORY_STREAM_START)
45+
// writeTrajectorySplinePoint(...) // repeat N times
46+
// writeTrajectoryControlMessage(TRAJECTORY_STREAM_END, N)
47+
//
48+
// The N passed to STREAM_END must equal the total number of spline points
49+
// the producer wrote on the trajectory socket since STREAM_START. The
50+
// controller-side dispatcher uses this count to compute how many points
51+
// remain unread in the OS socket buffer and finishes draining them before
52+
// the trajectory-done callback fires.
53+
//
54+
// Caller contract:
55+
//
56+
// 1. Do not starve the controller. Once trajectory execution begins, the
57+
// URScript-side reader times out reads after one step time (~8 ms once
58+
// moving). A timeout while streaming is treated as a producer fault and
59+
// yields TRAJECTORY_RESULT_FAILURE via the trajectory-done callback.
60+
// The producer must keep at least one point in flight on the trajectory
61+
// socket at all times until STREAM_END has been sent.
62+
//
63+
// 2. Send STREAM_END while at least one streamed point is still
64+
// unconsumed. This is implied by rule 1 but worth stating: if the
65+
// producer pauses long enough after the last spline point that the
66+
// consumer drains the buffer before STREAM_END arrives, rule 1 will
67+
// have already failed.
68+
//
69+
// 3. Make the last streamed point a controlled-stop terminal:
70+
// qd = (0, ..., 0), qdd = (0, ..., 0). URScript's spline runner applies
71+
// the same is_last_point time-axis scaling that the legacy finite path
72+
// uses, which preserves the positional trajectory while bringing
73+
// velocity to zero. A compliant terminal point (zeros) lets the spline
74+
// naturally end at rest. A non-compliant terminal point still results
75+
// in a controlled stop because the trajectory thread unconditionally
76+
// issues stopj(STOPJ_ACCELERATION) on exit, but the spline-to-stopj
77+
// transition may be less smooth.
78+
//
79+
// Failure modes via TrajectoryEndCallback:
80+
//
81+
// TRAJECTORY_RESULT_SUCCESS - clean stream end (STREAM_END processed,
82+
// all streamed points consumed).
83+
// TRAJECTORY_RESULT_FAILURE - producer underrun (timeout while
84+
// trajectory_streaming was still True).
85+
// TRAJECTORY_RESULT_CANCELED - the legacy TRAJECTORY_CANCEL message
86+
// was sent mid-stream. The existing
87+
// cleanup-then-respond path applies.
88+
//
89+
// ----------------------------------------------------------------------------
90+
91+
#include <atomic>
92+
#include <chrono>
93+
#include <memory>
94+
#include <string>
95+
#include <thread>
96+
#include <vector>
97+
98+
#include <ur_client_library/example_robot_wrapper.h>
99+
#include "ur_client_library/control/trajectory_point_interface.h"
100+
#include "ur_client_library/log.h"
101+
#include "ur_client_library/types.h"
102+
103+
const std::string DEFAULT_ROBOT_IP = "192.168.56.101";
104+
const std::string OUTPUT_RECIPE = "examples/resources/rtde_output_recipe.txt";
105+
const std::string INPUT_RECIPE = "examples/resources/rtde_input_recipe.txt";
106+
107+
std::unique_ptr<urcl::ExampleRobotWrapper> g_my_robot;
108+
std::atomic<bool> g_trajectory_done{ false };
109+
std::atomic<urcl::control::TrajectoryResult> g_trajectory_result{ urcl::control::TrajectoryResult::TRAJECTORY_RESULT_UNKNOWN };
110+
111+
void trajDoneCallback(const urcl::control::TrajectoryResult& result)
112+
{
113+
g_trajectory_result = result;
114+
g_trajectory_done = true;
115+
URCL_LOG_INFO("Trajectory done with result %s", urcl::control::trajectoryResultToString(result).c_str());
116+
}
117+
118+
// Pump TRAJECTORY_NOOP on the reverse socket while waiting for the
119+
// trajectory-done callback. Without this the URScript dispatcher's
120+
// reverse_socket read times out, which terminates the external_control
121+
// program entirely.
122+
void waitForTrajectoryDoneWithKeepalives()
123+
{
124+
while (!g_trajectory_done)
125+
{
126+
std::this_thread::sleep_for(std::chrono::milliseconds(100));
127+
g_my_robot->getUrDriver()->writeTrajectoryControlMessage(
128+
urcl::control::TrajectoryControlMessage::TRAJECTORY_NOOP);
129+
}
130+
}
131+
132+
// Sample a quintic-Hermite interpolation between q_start and q_end with
133+
// zero boundary velocity and acceleration. Returns position, velocity, and
134+
// acceleration at fractional time s in [0, 1] given total duration t_total.
135+
//
136+
// q(s) = q_start + (q_end - q_start) * (10s^3 - 15s^4 + 6s^5)
137+
// qd(t) = (q_end - q_start) * (30s^2 - 60s^3 + 30s^4) / t_total
138+
// qdd(t) = (q_end - q_start) * (60s - 180s^2 + 120s^3) / t_total^2
139+
//
140+
// The boundary conditions q(0)=q_start, q(1)=q_end, qd(0)=qd(1)=0,
141+
// qdd(0)=qdd(1)=0 are exactly what URScript's quintic spline runner needs
142+
// for a clean start-to-rest motion.
143+
struct SampledPoint
144+
{
145+
urcl::vector6d_t q;
146+
urcl::vector6d_t qd;
147+
urcl::vector6d_t qdd;
148+
};
149+
150+
SampledPoint sampleQuinticHermite(const urcl::vector6d_t& q_start, const urcl::vector6d_t& q_end, const double s,
151+
const double t_total)
152+
{
153+
const double s2 = s * s;
154+
const double s3 = s2 * s;
155+
const double s4 = s3 * s;
156+
const double s5 = s4 * s;
157+
const double pos_blend = 10.0 * s3 - 15.0 * s4 + 6.0 * s5;
158+
const double vel_blend = (30.0 * s2 - 60.0 * s3 + 30.0 * s4) / t_total;
159+
const double acc_blend = (60.0 * s - 180.0 * s2 + 120.0 * s3) / (t_total * t_total);
160+
161+
SampledPoint out{};
162+
for (size_t i = 0; i < q_start.size(); ++i)
163+
{
164+
const double delta = q_end[i] - q_start[i];
165+
out.q[i] = q_start[i] + delta * pos_blend;
166+
out.qd[i] = delta * vel_blend;
167+
out.qdd[i] = delta * acc_blend;
168+
}
169+
return out;
170+
}
171+
172+
int main(int argc, char* argv[])
173+
{
174+
urcl::setLogLevel(urcl::LogLevel::INFO);
175+
176+
std::string robot_ip = DEFAULT_ROBOT_IP;
177+
if (argc > 1)
178+
{
179+
robot_ip = argv[1];
180+
}
181+
182+
const bool headless_mode = true;
183+
g_my_robot = std::make_unique<urcl::ExampleRobotWrapper>(robot_ip, OUTPUT_RECIPE, INPUT_RECIPE, headless_mode,
184+
"external_control.urp");
185+
if (!g_my_robot->isHealthy())
186+
{
187+
URCL_LOG_ERROR("Robot initialization failed. See preceding output for details.");
188+
return 1;
189+
}
190+
191+
g_my_robot->getUrDriver()->registerTrajectoryDoneCallback(&trajDoneCallback);
192+
193+
// --------------- PRE-POSITION VIA LEGACY FINITE TRAJECTORY ----------------
194+
// Park the robot at a known starting pose before the streaming demo. This
195+
// also exercises the legacy TRAJECTORY_START path for comparison.
196+
197+
const urcl::vector6d_t pose_a = { -1.57, -1.57, 0.0, -1.57, 0.0, 0.0 };
198+
const urcl::vector6d_t pose_b = { -1.57, -1.20, 0.5, -1.57, 0.0, 0.0 };
199+
const urcl::vector6d_t zero = { 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 };
200+
201+
URCL_LOG_INFO("Pre-positioning to pose A via finite trajectory");
202+
g_trajectory_done = false;
203+
g_my_robot->getUrDriver()->writeTrajectoryControlMessage(
204+
urcl::control::TrajectoryControlMessage::TRAJECTORY_START, 1);
205+
g_my_robot->getUrDriver()->writeTrajectorySplinePoint(pose_a, zero, zero, 3.0f);
206+
waitForTrajectoryDoneWithKeepalives();
207+
if (g_trajectory_result != urcl::control::TrajectoryResult::TRAJECTORY_RESULT_SUCCESS)
208+
{
209+
URCL_LOG_ERROR("Pre-positioning failed");
210+
return 1;
211+
}
212+
213+
// ----------------- STREAMING TRAJECTORY DEMO -----------------------------
214+
// Stream a quintic-Hermite motion from pose A to pose B. We pre-compute
215+
// all points up front and send them back-to-back; the OS socket buffer
216+
// absorbs them and the URScript-side reader consumes them at its own pace
217+
// (~one per controller step time, ~8 ms on Polyscope 5). For dense
218+
// streaming workloads where points are produced on the fly, the producer
219+
// is responsible for pacing fast enough to keep the buffer non-empty.
220+
221+
const int k_num_points = 200;
222+
const double k_motion_duration_s = 2.0;
223+
const double dt = k_motion_duration_s / k_num_points;
224+
225+
std::vector<SampledPoint> points;
226+
points.reserve(k_num_points);
227+
for (int i = 1; i <= k_num_points; ++i)
228+
{
229+
const double s = static_cast<double>(i) / k_num_points;
230+
points.push_back(sampleQuinticHermite(pose_a, pose_b, s, k_motion_duration_s));
231+
}
232+
// The boundary conditions on the quintic Hermite guarantee qd=0, qdd=0 at
233+
// the final point. That is exactly the "controlled-stop terminal" the
234+
// caller contract recommends.
235+
236+
URCL_LOG_INFO("Streaming %d points over %.2f s nominal motion duration", k_num_points, k_motion_duration_s);
237+
g_trajectory_done = false;
238+
g_trajectory_result = urcl::control::TrajectoryResult::TRAJECTORY_RESULT_UNKNOWN;
239+
240+
const auto stream_start_wall = std::chrono::steady_clock::now();
241+
g_my_robot->getUrDriver()->writeTrajectoryControlMessage(
242+
urcl::control::TrajectoryControlMessage::TRAJECTORY_STREAM_START);
243+
for (const auto& p : points)
244+
{
245+
if (!g_my_robot->getUrDriver()->writeTrajectorySplinePoint(p.q, p.qd, p.qdd, static_cast<float>(dt)))
246+
{
247+
URCL_LOG_ERROR("writeTrajectorySplinePoint failed mid-stream");
248+
return 1;
249+
}
250+
}
251+
const auto stream_end_send_wall = std::chrono::steady_clock::now();
252+
g_my_robot->getUrDriver()->writeTrajectoryControlMessage(
253+
urcl::control::TrajectoryControlMessage::TRAJECTORY_STREAM_END, k_num_points);
254+
255+
waitForTrajectoryDoneWithKeepalives();
256+
const auto callback_wall = std::chrono::steady_clock::now();
257+
258+
const auto send_duration = std::chrono::duration_cast<std::chrono::milliseconds>(stream_end_send_wall - stream_start_wall);
259+
const auto end_to_callback = std::chrono::duration_cast<std::chrono::milliseconds>(callback_wall - stream_end_send_wall);
260+
URCL_LOG_INFO("All %d points written in %lld ms wall-clock", k_num_points, static_cast<long long>(send_duration.count()));
261+
// Time from sending STREAM_END to the trajectory-done callback. Since the
262+
// producer batched all points into the OS socket buffer faster than the
263+
// consumer could process them, this duration is dominated by the consumer
264+
// working through the queued points, not by STREAM_END processing itself.
265+
// For a true steady-state producer pacing one point per controller step,
266+
// this duration would be approximately one step time plus the unconditional
267+
// stopj at trajectory thread exit.
268+
URCL_LOG_INFO("Time from sending STREAM_END to trajectory-done callback: %lld ms", static_cast<long long>(end_to_callback.count()));
269+
URCL_LOG_INFO("Final result: %s",
270+
urcl::control::trajectoryResultToString(g_trajectory_result).c_str());
271+
272+
if (g_trajectory_result != urcl::control::TrajectoryResult::TRAJECTORY_RESULT_SUCCESS)
273+
{
274+
URCL_LOG_ERROR("Streaming trajectory did not complete successfully");
275+
return 1;
276+
}
277+
278+
g_my_robot->getUrDriver()->stopControl();
279+
return 0;
280+
}

0 commit comments

Comments
 (0)