|
| 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