From 431c467193632a86d1803ed46fcc0ba0cb7a2134 Mon Sep 17 00:00:00 2001 From: Andrew Morrow Date: Tue, 15 Sep 2026 10:52:24 -0400 Subject: [PATCH] [DOCS-4801] Add documentation for MoveThroughJointPositionsStreamed --- .github/workflows/sdk_protos_map.csv | 1 + .github/workflows/update_sdk_methods.py | 36 +++++- .../move-an-arm/move-by-joint-positions.md | 113 ++++++++++++++++-- .../components/apis/generated/arm-table.md | 1 + .../include/components/apis/generated/arm.md | 38 ++++++ .../components/apis/generated/board.md | 2 +- ...MoveThroughJointPositionsStreamed.after.md | 11 ++ ...veThroughJointPositionsStreamed.batches.md | 1 + ...ThroughJointPositionsStreamed.responses.md | 1 + .../arm.MoveThroughJointPositionsStreamed.md | 4 + 10 files changed, 195 insertions(+), 13 deletions(-) create mode 100644 static/include/components/apis/overrides/methods/go.arm.MoveThroughJointPositionsStreamed.after.md create mode 100644 static/include/components/apis/overrides/methods/go.arm.MoveThroughJointPositionsStreamed.batches.md create mode 100644 static/include/components/apis/overrides/methods/go.arm.MoveThroughJointPositionsStreamed.responses.md create mode 100644 static/include/components/apis/overrides/protos/arm.MoveThroughJointPositionsStreamed.md diff --git a/.github/workflows/sdk_protos_map.csv b/.github/workflows/sdk_protos_map.csv index 138081ad7b..a3a10f30f5 100644 --- a/.github/workflows/sdk_protos_map.csv +++ b/.github/workflows/sdk_protos_map.csv @@ -5,6 +5,7 @@ arm,GetEndPosition,,get_end_position,EndPosition,endPosition,getEndPosition arm,MoveToPosition,,move_to_position,MoveToPosition,moveToPosition,moveToPosition arm,MoveToJointPositions,,move_to_joint_positions,MoveToJointPositions,moveToJointPositions,moveToJointPositions arm,MoveThroughJointPositions,,,MoveThroughJointPositions,, +arm,MoveThroughJointPositionsStreamed,,,MoveThroughJointPositionsStreamed,, arm,GetJointPositions,,get_joint_positions,JointPositions,jointPositions,getJointPositions arm,Get3DModels,,,Get3DModels,get3DModels,get3DModels ## Flutter-only client-side helper, sums link lengths from getKinematics() locally; no proto/RPC and no analog in other SDKs: diff --git a/.github/workflows/update_sdk_methods.py b/.github/workflows/update_sdk_methods.py index a5c77ed099..c341a0a41b 100755 --- a/.github/workflows/update_sdk_methods.py +++ b/.github/workflows/update_sdk_methods.py @@ -538,11 +538,29 @@ def parse_method_usage(usage_string): param_type_link = "https://pkg.go.dev/builtin#error" else: param_raw = regex.sub(r'<.*?>', '', param).removesuffix(')').split() - ## Handle channel data types (only used for Board > StreamTicks): - if len(param_raw) == 3 and param_raw[0] == 'ch': - type_name = 'ch chan' - param_type = 'Tick' - type_link = '#Tick' + + ## pkg.go.dev HTML-escapes the arrows in channel types, so put them back + ## before we match on the tokens: + param_raw = [token.replace('<', '<').replace('>', '>') for token in param_raw] + + ## Clear the per-parameter state. Python scopes these to the whole function, + ## so a parameter shape matching none of the cases below would otherwise + ## inherit the previous parameter's values and document itself as a copy of + ## its neighbor: + type_name = None + param_type = None + type_link = None + + ## Handle channel parameters, whose type spans two tokens: a direction + ## marker and the element type. All three directions occur in the SDK, + ## and the element type can itself be a slice: + if len(param_raw) == 3 and param_raw[1] in ('chan', '<-chan', 'chan<-'): + type_name = param_raw[0] + param_type = param_raw[1] + ' ' + param_raw[2] + try: + type_link = regex.findall(r'href="([^"]+)">', param)[-1] + except: + print("DEBUG: No type link found: {}, {}".format(usage_string, param)) ## Handle named parameters: elif len(param_raw) == 2: type_name = param_raw[0] @@ -589,6 +607,14 @@ def parse_method_usage(usage_string): except: print("DEBUG: No type link found: {}, {}, {}".format(usage_string, param, param_raw)) + ## Nothing above claimed this parameter. Fall back to the stripped source + ## text so the shape that got missed is visible in the output and in the + ## log, rather than quietly taking on its neighbor's identity: + if type_name is None and param_type is None: + print("DEBUG: Unhandled parameter shape: {}, {}".format(param, param_raw)) + type_name = '' + param_type = ' '.join(param_raw) + if type_link: param_type_link = type_link else: diff --git a/docs/motion-planning/move-an-arm/move-by-joint-positions.md b/docs/motion-planning/move-an-arm/move-by-joint-positions.md index 250a97d57a..3796d09e69 100644 --- a/docs/motion-planning/move-an-arm/move-by-joint-positions.md +++ b/docs/motion-planning/move-an-arm/move-by-joint-positions.md @@ -4,7 +4,7 @@ title: "Move an arm by setting joint positions" weight: 30 layout: "docs" type: "docs" -description: "Command an arm directly in joint space using MoveToJointPositions and MoveThroughJointPositions, bypassing the motion planner." +description: "Command an arm directly in joint space using MoveToJointPositions, MoveThroughJointPositions, and streamed trajectories, bypassing the motion planner." capabilities: ["motion-planning", "hw-arm"] aliases: - /motion-planning/motion-how-to/move-arm-joint-positions/ @@ -193,6 +193,94 @@ values in **radians**: `MaxVelRads`, `MaxAccRads`, `MaxVelRadsJoints`, `MaxAccRadsJoints`, `MaxTCPSpeedMPerSec`. The conversion happens at the wire boundary. +## MoveThroughJointPositionsStreamed + +`MoveThroughJointPositions` needs the whole trajectory before the arm starts +moving. That is fine for a handful of waypoints you already know. It stops +working when you are producing waypoints as you go: a teleoperation loop, a +trajectory arriving from another process, or a path still being optimized +while the arm executes the start of it. + +The streamed form takes waypoints in batches over an open stream. The arm +starts moving on the first batch, so generating the trajectory and executing +it overlap. + +### Waypoints carry time + +The two APIs describe motion differently. `MoveThroughJointPositions` takes +positions and a `MoveOptions` ceiling, then leaves the arm to work out the +timing. A streamed `TrajectoryPoint` names the time at which the arm should +arrive, and optionally the velocities and accelerations it should have when it +gets there. You hand the arm a time-parameterized trajectory instead of asking +it to build one. + +- `Time` is measured from the start of the motion. The first point must be + zero, and every point after it must be strictly later than the one before. +- `Constraints` is optional and set per point. If you set it on the first + point, the velocities there must be zero. +- Positions, velocities, and accelerations use radians and millimeters, the + same `referenceframe.Input` convention as `MoveToJointPositions`. The wire + format uses degrees. + +### Stream a trajectory + +You create both channels. Write batches to `batches` and close it to end the +motion. Read `responses` so a slow reader never stalls the client, and close +it after the call returns. + +```go +import ( + "time" + + "go.viam.com/rdk/components/arm" + "go.viam.com/rdk/referenceframe" +) + +batches := make(chan []arm.TrajectoryPoint) +responses := make(chan arm.Response) + +// The arm is free to acknowledge nothing at all, so this goroutine drains the +// channel rather than tracking progress. +go func() { + for range responses { + } +}() + +go func() { + defer close(batches) + + // Ten waypoints, 100ms apart, sent five at a time. nextWaypoint stands in + // for whatever is producing your trajectory. + batch := make([]arm.TrajectoryPoint, 0, 5) + for i := 0; i < 10; i++ { + batch = append(batch, arm.TrajectoryPoint{ + Time: time.Duration(i*100) * time.Millisecond, + Positions: nextWaypoint(i), + }) + if len(batch) == 5 { + batches <- batch + batch = make([]arm.TrajectoryPoint, 0, 5) + } + } +}() + +// Blocks until the arm finishes the trajectory, the stream fails, or another +// operation cancels it. +err := myArm.MoveThroughJointPositionsStreamed(ctx, batches, responses, nil) +close(responses) +if err != nil { + logger.Fatal(err) +} +``` + +Batches append to the motion in the order you send them. A waypoint cannot be +replaced or withdrawn once it is on the wire, so a trajectory you might still +revise is one to send late rather than early. + +Acknowledgments carry no payload, and an arm may send none, so they tell you +nothing about how far the motion has progressed. Read `GetJointPositions` if +you need to know where the arm actually is. + ## Reading current joint positions Use `GetJointPositions` to capture the arm's current configuration @@ -226,12 +314,13 @@ programmatically. ## Joint-space moves compared to motion.Move -| Motion path | Use when | -| ------------------------------------ | --------------------------------------------------------------------------------------------------- | -| `arm.MoveToJointPositions` | You know the joint angles you want. | -| `arm.MoveThroughJointPositions` (Go) | You have a sequence of joint targets and want per-call velocity or acceleration caps. | -| `arm.MoveToPosition` | You have a Cartesian target pose but don't need obstacle avoidance. | -| `motion.Move` | You have a Cartesian target and want obstacle avoidance, constraints, and IK picked by the planner. | +| Motion path | Use when | +| --------------------------------------- | --------------------------------------------------------------------------------------------------- | +| `arm.MoveToJointPositions` | You know the joint angles you want. | +| `arm.MoveThroughJointPositions` (Go) | You have a sequence of joint targets and want per-call velocity or acceleration caps. | +| `arm.MoveThroughJointPositionsStreamed` | You are producing waypoints as you go, or the trajectory is too long to send in one request. | +| `arm.MoveToPosition` | You have a Cartesian target pose but don't need obstacle avoidance. | +| `motion.Move` | You have a Cartesian target and want obstacle avoidance, constraints, and IK picked by the planner. | Joint-space moves are the right call when you need to control the posture of the arm precisely. They do not protect against collisions @@ -261,6 +350,16 @@ shorter `MoveToJointPositions` calls with sleeps between. {{< /expand >}} +{{< expand "Streamed trajectory rejected for point times" >}} + +A streamed trajectory is time-parameterized, so the arm rejects a batch whose +point times do not advance. The first point of the motion must be at time +zero, and every point after it must be strictly later than the one before, +across batch boundaries as well as within a batch. Check the time on the first +point of each batch against the last point of the batch before it. + +{{< /expand >}} + {{< expand "Wrong number of values error" >}} The `values` array must match the arm's degrees of freedom. A 6-DOF diff --git a/static/include/components/apis/generated/arm-table.md b/static/include/components/apis/generated/arm-table.md index 8975707249..f73d355aac 100644 --- a/static/include/components/apis/generated/arm-table.md +++ b/static/include/components/apis/generated/arm-table.md @@ -5,6 +5,7 @@ | [`MoveToPosition`](/reference/apis/components/arm/#movetoposition) | Move the end of the arm in a straight line to the desired pose, relative to the base of the arm. | | [`MoveToJointPositions`](/reference/apis/components/arm/#movetojointpositions) | Move each joint on the arm to the position specified in `positions`. | | [`MoveThroughJointPositions`](/reference/apis/components/arm/#movethroughjointpositions) | Move the arm's joints through the given positions in the order they are specified. | +| [`MoveThroughJointPositionsStreamed`](/reference/apis/components/arm/#movethroughjointpositionsstreamed) | Move the arm's joints through a trajectory delivered as a stream of timed waypoints. | | [`GetJointPositions`](/reference/apis/components/arm/#getjointpositions) | Get the current position of each joint on the arm. | | [`Get3DModels`](/reference/apis/components/arm/#get3dmodels) | Get the 3D models of the arm. | | [`CalculateMaxReach`](/reference/apis/components/arm/#calculatemaxreach) | Calculate the maximum reach of the arm by summing all link lengths from its kinematics data. | diff --git a/static/include/components/apis/generated/arm.md b/static/include/components/apis/generated/arm.md index 8f3d2eb425..911fcc5b53 100644 --- a/static/include/components/apis/generated/arm.md +++ b/static/include/components/apis/generated/arm.md @@ -376,6 +376,44 @@ For more information, see the [Go SDK Docs](https://pkg.go.dev/go.viam.com/rdk/c {{% /tab %}} {{< /tabs >}} +### MoveThroughJointPositionsStreamed + +Move the arm's joints through a trajectory delivered as a stream of timed waypoints. +`MoveThroughJointPositions` takes a whole trajectory in one request. +This method opens a stream instead and accepts batches of waypoints until the caller closes it, so a long or continuously generated trajectory does not have to be complete before the arm starts moving. +The call blocks until the arm finishes the trajectory, the stream fails, or a new operation cancels it. + +{{< tabs >}} +{{% tab name="Go" %}} + +**Parameters:** + +- `ctx` [(Context)](https://pkg.go.dev/context#Context): A Context carries a deadline, a cancellation signal, and other values across API boundaries. +- `batches` [(<-chan []TrajectoryPoint)](https://pkg.go.dev/go.viam.com/rdk/components/arm#TrajectoryPoint): The channel you write trajectory points to, one slice per batch. Batches append to the motion in the order you send them, and waypoints cannot be replaced or withdrawn once sent. Close this channel to signal that the trajectory is complete. +- `responses` [(chan<- Response)](https://pkg.go.dev/go.viam.com/rdk/components/arm#Response): The channel the arm writes acknowledgments to. An acknowledgment carries no payload, and an arm is free to send none at all, so read this channel to keep it drained rather than to confirm progress. Close it once the call returns. +- `extra` [(map[string]interface{})](https://go.dev/blog/maps): Extra options to pass to the underlying RPC call. + +**Returns:** + +- [(error)](https://pkg.go.dev/builtin#error): An error, if one occurred. + +You create and close both channels: `batches` when the trajectory is complete, `responses` only after the call has returned. + +Waypoint timing is part of the trajectory rather than a hint: + +- `Time` on the first point must be zero, and must strictly increase from one point to the next. +- If a point carries `Constraints`, the velocities on the first point must be zero. +- `Positions`, and the velocities and accelerations inside `Constraints`, follow the `referenceframe.Input` convention: radians and radians per second for revolute joints, millimeters and millimeters per second for prismatic ones. The wire format carries degrees, and the conversion happens at the boundary. + +When the arm's kinematics are available, each waypoint is checked against the joint limits before it goes on the wire. A waypoint outside the limits fails the call and tears the stream down, which can happen after earlier batches are already executing. + +Module authors implementing this method get the mirror image of this contract: the framework owns both channels, writes and closes `batches`, and closes `responses` after the implementation returns. See the [Go SDK Docs](https://pkg.go.dev/go.viam.com/rdk/components/arm#Arm) for that side of the interface. + +For more information, see the [Go SDK Docs](https://pkg.go.dev/go.viam.com/rdk/components/arm#Arm). + +{{% /tab %}} +{{< /tabs >}} + ### GetJointPositions Get the current position of each joint on the arm. diff --git a/static/include/components/apis/generated/board.md b/static/include/components/apis/generated/board.md index c552d25b2e..1fb083b1e6 100644 --- a/static/include/components/apis/generated/board.md +++ b/static/include/components/apis/generated/board.md @@ -1023,7 +1023,7 @@ For more information, see the [Python SDK Docs](https://python.viam.dev/autoapi/ - `ctx` [(Context)](https://pkg.go.dev/context#Context): A Context carries a deadline, a cancellation signal, and other values across API boundaries. - `interrupts` [([]DigitalInterrupt)](https://pkg.go.dev/go.viam.com/rdk/components/board#DigitalInterrupt): Slice of digital interrupts to receive ticks from. -- `ch chan` [(Tick)](https://pkg.go.dev/go.viam.com/rdk/components/board#Tick): The channel to stream Ticks, structs containing `Name`, `High`, and `TimestampNanosec` fields. +- `ch` [(chan Tick)](https://pkg.go.dev/go.viam.com/rdk/components/board#Tick): The channel to stream Ticks, structs containing `Name`, `High`, and `TimestampNanosec` fields. - `extra` [(map[string]interface{})](https://go.dev/blog/maps): Extra options to pass to the underlying RPC call. **Returns:** diff --git a/static/include/components/apis/overrides/methods/go.arm.MoveThroughJointPositionsStreamed.after.md b/static/include/components/apis/overrides/methods/go.arm.MoveThroughJointPositionsStreamed.after.md new file mode 100644 index 0000000000..1a9ba604aa --- /dev/null +++ b/static/include/components/apis/overrides/methods/go.arm.MoveThroughJointPositionsStreamed.after.md @@ -0,0 +1,11 @@ +You create and close both channels: `batches` when the trajectory is complete, `responses` only after the call has returned. + +Waypoint timing is part of the trajectory rather than a hint: + +- `Time` on the first point must be zero, and must strictly increase from one point to the next. +- If a point carries `Constraints`, the velocities on the first point must be zero. +- `Positions`, and the velocities and accelerations inside `Constraints`, follow the `referenceframe.Input` convention: radians and radians per second for revolute joints, millimeters and millimeters per second for prismatic ones. The wire format carries degrees, and the conversion happens at the boundary. + +When the arm's kinematics are available, each waypoint is checked against the joint limits before it goes on the wire. A waypoint outside the limits fails the call and tears the stream down, which can happen after earlier batches are already executing. + +Module authors implementing this method get the mirror image of this contract: the framework owns both channels, writes and closes `batches`, and closes `responses` after the implementation returns. See the [Go SDK Docs](https://pkg.go.dev/go.viam.com/rdk/components/arm#Arm) for that side of the interface. diff --git a/static/include/components/apis/overrides/methods/go.arm.MoveThroughJointPositionsStreamed.batches.md b/static/include/components/apis/overrides/methods/go.arm.MoveThroughJointPositionsStreamed.batches.md new file mode 100644 index 0000000000..f91946b6cf --- /dev/null +++ b/static/include/components/apis/overrides/methods/go.arm.MoveThroughJointPositionsStreamed.batches.md @@ -0,0 +1 @@ +The channel you write trajectory points to, one slice per batch. Batches append to the motion in the order you send them, and waypoints cannot be replaced or withdrawn once sent. Close this channel to signal that the trajectory is complete. diff --git a/static/include/components/apis/overrides/methods/go.arm.MoveThroughJointPositionsStreamed.responses.md b/static/include/components/apis/overrides/methods/go.arm.MoveThroughJointPositionsStreamed.responses.md new file mode 100644 index 0000000000..f4375a5567 --- /dev/null +++ b/static/include/components/apis/overrides/methods/go.arm.MoveThroughJointPositionsStreamed.responses.md @@ -0,0 +1 @@ +The channel the arm writes acknowledgments to. An acknowledgment carries no payload, and an arm is free to send none at all, so read this channel to keep it drained rather than to confirm progress. Close it once the call returns. diff --git a/static/include/components/apis/overrides/protos/arm.MoveThroughJointPositionsStreamed.md b/static/include/components/apis/overrides/protos/arm.MoveThroughJointPositionsStreamed.md new file mode 100644 index 0000000000..ba8b1707f2 --- /dev/null +++ b/static/include/components/apis/overrides/protos/arm.MoveThroughJointPositionsStreamed.md @@ -0,0 +1,4 @@ +Move the arm's joints through a trajectory delivered as a stream of timed waypoints. +`MoveThroughJointPositions` takes a whole trajectory in one request. +This method opens a stream instead and accepts batches of waypoints until the caller closes it, so a long or continuously generated trajectory does not have to be complete before the arm starts moving. +The call blocks until the arm finishes the trajectory, the stream fails, or a new operation cancels it.