Skip to content

PoC of streaming API for external_control#528

Open
acmorrow wants to merge 6 commits into
UniversalRobots:masterfrom
acmorrow:streaming
Open

PoC of streaming API for external_control#528
acmorrow wants to merge 6 commits into
UniversalRobots:masterfrom
acmorrow:streaming

Conversation

@acmorrow

@acmorrow acmorrow commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

@urfeex - Here is what I had Claude put together to demonstrate the possibility of keeping open trajectories, per #524.

It includes:

  • The addition of the STREAM_START and STREAM_END control messages to the TrajectoryControlMesssage enumeration.
  • An integration test for streaming and an example program.
  • Support for configuring the cmake build with a target IP for integration testing. Not perfect since it gets baked into the build, but not terrible either.
  • Argument parsing fixes such that setting the robot IP on the command line works for various integration tests.
  • Updates to the external_contro.urscript to support stream start and stream end.

I'm using this right now and it seems to work for some basic cases. I haven't stress tested it or thought deeply about the various edge cases or error scenarios that this may need to cover.

The goal here is really just to put some code behind the ideas over in #524 so we can discuss further. I'm happy to rework any of this as necessary.

I will also leave some comments directly on the review for a few things I want to call out.


Note

High Risk
Changes real-time trajectory execution on the robot via external_control URScript (sentinel counts, STREAM_END math, failure vs success paths); incorrect behavior could truncate motion or report wrong results until hardened beyond this PoC.

Overview
Adds open-ended trajectory streaming so producers can send spline points without declaring the total count up front, then finish with TRAJECTORY_STREAM_END and the total points written since TRAJECTORY_STREAM_START.

The C++ API extends TrajectoryControlMessage with TRAJECTORY_STREAM_START / TRAJECTORY_STREAM_END and documents per-mode semantics on ReverseInterface and UrDriver. external_control.urscript implements streaming via a trajectory_streaming flag, a STREAMING_SENTINEL point budget, STREAM_END math to convert remaining buffered points into a finite drain count, guards so stray or duplicate STREAM_END cannot corrupt finite trajectories, and refined read-timeout handling to distinguish clean post-STREAM_END races from producer underruns or count mismatches.

Ships examples/trajectory_streaming.cpp and headless integration coverage in test_trajectory_streaming.cpp (success, underrun, cancel, and regression cases for stray/double STREAM_END and over-count STREAM_END). Integration test CMake gains INTEGRATION_TESTS_ROBOT_IP passed as --robot_ip to ctest; several test main() parsers no longer stop after the first flag so --robot_ip and --headless can both apply.

Reviewed by Cursor Bugbot for commit 97e9452. Bugbot is set up for automated code reviews on this repo. Configure here.

Comment thread resources/external_control.urscript Outdated
Comment thread resources/external_control.urscript
Comment thread resources/external_control.urscript Outdated
}
const auto stream_end_send_wall = std::chrono::steady_clock::now();
g_my_robot->getUrDriver()->writeTrajectoryControlMessage(
urcl::control::TrajectoryControlMessage::TRAJECTORY_STREAM_END, k_num_points);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an important note: the client is obligated to provide the total number of points streamed when calling TRAJECTORY_STREAM_END, so that we can properly reset the counter so it counts down to zero and the trajectory ends naturally.

Comment thread tests/CMakeLists.txt
@acmorrow

Copy link
Copy Markdown
Contributor Author

@urfeex - I see the bot has flagged a few things, I'll take a look at those and post an update.

@acmorrow

Copy link
Copy Markdown
Contributor Author

@urfeex - Also, an update: today we got this running on a physical UR5e and we were able to stream to the arm. So, there is at least some evidence that this is feasible in the real world.

@acmorrow

Copy link
Copy Markdown
Contributor Author

@urfeex - Pushed an update to address the deficiencies that the cursor bot tagged. I had claude right some tests, we confirmed they failed, made fixes, confirmed they passed.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

Reviewed by Cursor Bugbot for commit 97e9452. Configure here.

# unread remainder still in the OS socket buffer is therefore
# the producer's total minus the consumer's consumed count.
trajectory_points_left = params_mult[3] - (STREAMING_SENTINEL - trajectory_points_left)
trajectory_streaming = False

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Low stream end count succeeds early

Medium Severity

When TRAJECTORY_STREAM_END carries a total point count below the number of spline points already written, the dispatcher sets trajectory_points_left to zero while extra points remain in the trajectory socket buffer. trajectoryThread then exits with TRAJECTORY_RESULT_SUCCESS instead of failing, unlike a short finite TRAJECTORY_START trajectory.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 97e9452. Configure here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is that actually true? I suspect that this behavior is more or less the same as the non-streaming case.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would say, it's the same. If the number of sent trajectory points is higher than the communicated number, trajectory points remain in the buffer after clearing. This is the case for both, a finite trajectory and a streaming trajectory.

@urfeex urfeex left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be nice if we could move the IP-address refactoring to a separate PR, so this one gets a bit smaller in scope.

From a first glance, this looks quite clean. We would need some documentation updates, both on the trajectory point interface and one for the example (which could contain a lot of the text written in the example itself right now).

* \param trajectory_action One of the values of TrajectoryControlMessage. The value selects which
* trajectory-control action the URScript dispatcher takes, and dictates how \p point_number is
* interpreted:
* - TRAJECTORY_CANCEL (-1): Cancels the currently executing trajectory. \p point_number is unused.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens to the remaining points if a trajectory is canceled? If I understand things correctly, the buffer cleanup will try to clean a very high number of points and then hit the socket read timeout as defined in clearTrajectoryPointsThread(). We could make this more effective (avoid the timeout) by passing the total number of points along as done with TRAJECTORY_STREAM_END. It should still work passing along 0 to not break behavior.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just tested this and adding trajectory_points_left = params_mult[3] - (STREAMING_SENTINEL - trajectory_points_left) in the script code section for elif params_mult[2] == TRAJECTORY_MODE_CANCEL: made the cleanup function clean the remaining number of points not timing out on reading from the socket.

* calls the producer will issue on the trajectory socket. URScript reads exactly that many points
* and then completes.
* - TRAJECTORY_STREAM_START (2): Begins an open-ended (streaming) trajectory. The producer streams
* spline points and signals end-of-stream with TRAJECTORY_STREAM_END. \p point_number is unused.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this restricted to splines only? From what I can see the same mechanism also works for sending movej, movel, etc.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just tested this with the motion sequence from the instruction_executor example. This worked like a charm.

# unread remainder still in the OS socket buffer is therefore
# the producer's total minus the consumer's consumed count.
trajectory_points_left = params_mult[3] - (STREAMING_SENTINEL - trajectory_points_left)
trajectory_streaming = False

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would say, it's the same. If the number of sent trajectory points is higher than the communicated number, trajectory points remain in the buffer after clearing. This is the case for both, a finite trajectory and a streaming trajectory.

@urfeex

urfeex commented Jun 26, 2026

Copy link
Copy Markdown
Member

I've added the two issues that this PR will close when being merged.

@acmorrow

Copy link
Copy Markdown
Contributor Author

@urfeex - No problem about splitting out the testing changes, they were in their own commit anyway. I've made a new issue (#530) and a new PR (#531). I just made a new branch and cherry-picked out the relevant commit from this one.

We will need to sort out how to drop out that commit from this review since it is the base commit. Probably, we just review what is here until satisfied, and then I'll do a final rebase above master and force-push to drop out the now vacuous testing changes commit, but lmk if you want to handle it differently.

@urfeex

urfeex commented Jun 26, 2026

Copy link
Copy Markdown
Member

Yes, a rebase on master once the testing changes are merged should be the way to go.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants