Skip to content

Commit b437116

Browse files
authored
Streaming API for trajectory forwarding (#528)
This enables streaming trajectory segments using the FORWARD communication protocol without knowing the number of segments ahead of time. It includes: - The addition of the `STREAM_START` and `STREAM_END` control messages to the `TrajectoryControlMesssage` enumeration. - An example using the new API - Documentation to cover all the changes
1 parent 7c65b2f commit b437116

10 files changed

Lines changed: 957 additions & 23 deletions

File tree

doc/architecture/reverse_interface.rst

Lines changed: 61 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,8 @@ meaning:
4949
- joint velocities (SPEEDJ)
5050
- trajectory instructions (FORWARD)
5151

52-
- field 1: Trajectory control mode(1: TRAJECTORY_MODE_RECEIVE, -1: TRAJECTORY_MODE_CANCEL)
53-
- field 2: Number of trajectory points left to transfer
52+
- field 1: Trajectory control mode (1: TRAJECTORY_MODE_RECEIVE, 2: TRAJECTORY_MODE_STREAM_START, 3: TRAJECTORY_MODE_STREAM_END, -1: TRAJECTORY_MODE_CANCEL). See :ref:`streaming_trajectories` for the streaming modes.
53+
- field 2: Trajectory point count. Its interpretation depends on the control mode in field 1.
5454

5555
- Cartesian velocities (SPEEDL)
5656
- Cartesian pose (POSE)
@@ -88,6 +88,65 @@ meaning:
8888

8989
Depending on the control mode one can use the ``write()`` (SERVOJ, SPEEDJ, SPEEDL, POSE, TORQUE), ``writeTrajectoryControlMessage()`` (FORWARD) or ``writeFreedriveControlMessage()`` (FREEDRIVE) function to write a message to the "reverse_socket".
9090

91+
.. _streaming_trajectories:
92+
93+
Streaming trajectories
94+
~~~~~~~~~~~~~~~~~~~~~~~~
95+
96+
A ``FORWARD``-mode trajectory can be executed in one of two ways.
97+
98+
A *finite* trajectory declares its total length up front:
99+
``writeTrajectoryControlMessage(TRAJECTORY_START, n)`` promises exactly ``n`` points, and execution
100+
completes once they have been consumed. This is the mode used by
101+
:ref:`trajecotry_joint_interface_example`.
102+
103+
A *streaming* trajectory is open-ended. The producer does not commit to a point count in advance; it
104+
opens the stream, writes points for as long as it likes, and signals end-of-stream explicitly. This
105+
suits points produced on the fly -- from a planner or a teleoperation source -- where the total length
106+
is unknown when motion begins. The message sequence is:
107+
108+
.. code-block:: c++
109+
110+
writeTrajectoryControlMessage(TRAJECTORY_STREAM_START) // open the stream
111+
// write a motion primitive, any number of times:
112+
// writeTrajectoryPoint(...) / writeTrajectorySplinePoint(...) / writeMotionPrimitive(...)
113+
writeTrajectoryControlMessage(TRAJECTORY_STREAM_END, n) // close the stream
114+
115+
``TRAJECTORY_STREAM_START`` opens the trajectory; its point-count argument is unused. The producer then
116+
writes motion primitives -- via ``writeTrajectoryPoint()``, ``writeTrajectorySplinePoint()`` or
117+
``writeMotionPrimitive()`` -- for as long as it likes. ``TRAJECTORY_STREAM_END`` closes it.
118+
119+
.. important::
120+
The ``n`` passed to ``TRAJECTORY_STREAM_END`` must equal the total number of motion primitives the
121+
producer wrote since ``TRAJECTORY_STREAM_START``. The controller consumes primitives asynchronously
122+
as it executes them and cannot otherwise tell how many are still outstanding; it uses ``n`` to
123+
account for those still in flight before completing. Aborting a stream with ``TRAJECTORY_CANCEL``
124+
requires the same count, for the same reason. (For a finite ``TRAJECTORY_START`` trajectory the
125+
cancel argument is unused.)
126+
127+
Two obligations fall on the producer:
128+
129+
- **Do not starve the controller.** The robot consumes the stream as it executes it, so the producer
130+
must keep work available: the next primitive must arrive before the robot finishes the ones it
131+
already holds. The budget for delivering it is simply the duration of the work in hand -- a primitive
132+
that runs for five minutes gives five minutes to produce its successor, while a stream of
133+
eight-millisecond primitives demands one every eight milliseconds. If the robot runs out of submitted
134+
work before the next primitive arrives and before ``TRAJECTORY_STREAM_END`` is sent, the trajectory
135+
ends with ``TRAJECTORY_RESULT_FAILURE``. Delivering primitives as they are produced, without holding
136+
them back, is the natural way to satisfy this.
137+
138+
- **End at rest.** Streaming adds no wind-down deceleration of its own. Make the final point a
139+
controlled stop, with zero velocity and acceleration, so the spline interpolates smoothly to rest at
140+
the goal. The trajectory thread issues ``stopj`` on exit regardless, so a non-zero terminal point
141+
still stops the robot, but the transition will be less smooth.
142+
143+
On completion the callback registered with ``setTrajectoryEndCallback()`` reports
144+
``TRAJECTORY_RESULT_SUCCESS`` for a clean end (``TRAJECTORY_STREAM_END`` processed, all points
145+
consumed), ``TRAJECTORY_RESULT_FAILURE`` for a producer underrun, or ``TRAJECTORY_RESULT_CANCELED`` if
146+
the stream was aborted with ``TRAJECTORY_CANCEL``.
147+
148+
See :ref:`trajectory_streaming_example` for a complete, working example.
149+
91150
.. _direct_torque_control_mode:
92151

93152
Direct torque control mode

doc/examples.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,5 @@ may be running forever until manually stopped.
3131
examples/tool_contact_example
3232
examples/direct_torque_control
3333
examples/trajectory_point_interface
34+
examples/trajectory_streaming
3435
examples/ur_driver
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
:github_url: https://github.com/UniversalRobots/Universal_Robots_Client_Library/blob/master/doc/examples/trajectory_streaming.rst
2+
3+
.. _trajectory_streaming_example:
4+
5+
Trajectory streaming example
6+
============================
7+
8+
This example demonstrates open-ended *trajectory streaming*: a producer sends points to the
9+
controller without declaring a total point count up front, then signals end-of-stream when it is
10+
finished. Contrast this with the :ref:`trajecotry_joint_interface_example`, which uses the finite
11+
trajectory path where the point count is fixed in the initial ``TRAJECTORY_START`` message.
12+
13+
The streaming communication contract -- the ``STREAM_START`` / ``STREAM_END`` handshake, the meaning
14+
of the end-of-stream point count, the producer's obligation not to starve the controller, and the
15+
trajectory result codes -- is described in :ref:`streaming_trajectories`. This page walks through the
16+
example code and refers back to that section for the authoritative contract.
17+
18+
Setup
19+
-----
20+
21+
As with the other examples, we create a ``UrDriver`` (here through ``ExampleRobotWrapper``) and
22+
register a callback that fires when a trajectory finishes:
23+
24+
.. literalinclude:: ../../examples/trajectory_streaming.cpp
25+
:language: c++
26+
:caption: examples/trajectory_streaming.cpp
27+
:linenos:
28+
:lineno-match:
29+
:start-at: const bool headless_mode = true;
30+
:end-before: // --------------- PRE-POSITION VIA FINITE TRAJECTORY
31+
32+
Trajectory execution is asynchronous, so completion is reported through a callback. The result
33+
distinguishes a successful run from a cancellation or a failure:
34+
35+
.. literalinclude:: ../../examples/trajectory_streaming.cpp
36+
:language: c++
37+
:caption: examples/trajectory_streaming.cpp
38+
:linenos:
39+
:lineno-match:
40+
:start-at: void trajDoneCallback
41+
:end-at: }
42+
43+
While a trajectory runs, the control PC must keep telling the robot program that the connection is
44+
still alive. We do this by sending ``TRAJECTORY_NOOP`` messages while we wait; without them the robot
45+
program stops waiting for input and the ``external_control`` program exits:
46+
47+
.. literalinclude:: ../../examples/trajectory_streaming.cpp
48+
:language: c++
49+
:caption: examples/trajectory_streaming.cpp
50+
:linenos:
51+
:lineno-match:
52+
:start-at: // Pump TRAJECTORY_NOOP
53+
:end-before: // Sample a quintic-Hermite
54+
55+
Generating a motion
56+
-------------------
57+
58+
To have something to stream, we sample a quintic-Hermite blend between two joint configurations. The
59+
blend has zero velocity and acceleration at both endpoints, which -- as the streaming contract
60+
recommends -- gives the final point the controlled-stop property needed to bring the robot cleanly to
61+
rest:
62+
63+
.. literalinclude:: ../../examples/trajectory_streaming.cpp
64+
:language: c++
65+
:caption: examples/trajectory_streaming.cpp
66+
:linenos:
67+
:lineno-match:
68+
:start-at: // Sample a quintic-Hermite
69+
:end-before: int main(
70+
71+
Pre-positioning
72+
---------------
73+
74+
Before streaming, we park the robot at a known start pose using an ordinary finite trajectory. This
75+
doubles as a demonstration of the ``TRAJECTORY_START`` path for comparison:
76+
77+
.. literalinclude:: ../../examples/trajectory_streaming.cpp
78+
:language: c++
79+
:caption: examples/trajectory_streaming.cpp
80+
:linenos:
81+
:lineno-match:
82+
:start-after: // --------------- PRE-POSITION VIA FINITE TRAJECTORY
83+
:end-before: // ----------------- STREAMING TRAJECTORY DEMO
84+
85+
Streaming the trajectory
86+
------------------------
87+
88+
We precompute all of the points up front. The final point inherits the ``qd = 0``, ``qdd = 0``
89+
boundary condition of the quintic blend, so it is a compliant controlled-stop terminal:
90+
91+
.. literalinclude:: ../../examples/trajectory_streaming.cpp
92+
:language: c++
93+
:caption: examples/trajectory_streaming.cpp
94+
:linenos:
95+
:lineno-match:
96+
:start-after: // ----------------- STREAMING TRAJECTORY DEMO
97+
:end-before: URCL_LOG_INFO("Streaming %d points
98+
99+
The stream itself is the ``STREAM_START`` -> points -> ``STREAM_END`` handshake. We open the stream,
100+
write every point back-to-back, then close it, passing the total number of points written so the
101+
controller knows how many are still outstanding before it reports completion:
102+
103+
.. literalinclude:: ../../examples/trajectory_streaming.cpp
104+
:language: c++
105+
:caption: examples/trajectory_streaming.cpp
106+
:linenos:
107+
:lineno-match:
108+
:start-at: URCL_LOG_INFO("Streaming %d points
109+
:end-before: g_my_robot->getUrDriver()->stopControl();
110+
111+
Because this example delivers the whole motion up front, it can never starve the controller: all the
112+
work is in hand before execution finishes, so the robot simply plays through it. The example also
113+
measures the time from sending ``STREAM_END`` to the trajectory-done callback. That time is large
114+
here, but not because ending a stream is costly: the producer sends every point and then
115+
``STREAM_END`` within milliseconds, long before the robot has finished moving, so the callback cannot
116+
fire until the robot has worked through the entire backlog. A producer that instead fed points in step
117+
with the robot's motion would send ``STREAM_END`` just as the robot reached the final point, and the
118+
callback would follow almost immediately.
119+
120+
For the general case, where points are produced on the fly and pacing does matter, see the producer
121+
obligations in :ref:`streaming_trajectories`.

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)

0 commit comments

Comments
 (0)