Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/sdk_protos_map.csv
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
36 changes: 31 additions & 5 deletions .github/workflows/update_sdk_methods.py
Original file line number Diff line number Diff line change
Expand Up @@ -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('&lt;', '<').replace('&gt;', '>') 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]
Expand Down Expand Up @@ -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:
Expand Down
113 changes: 106 additions & 7 deletions docs/motion-planning/move-an-arm/move-by-joint-positions.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions static/include/components/apis/generated/arm-table.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. |
Expand Down
38 changes: 38 additions & 0 deletions static/include/components/apis/generated/arm.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion static/include/components/apis/generated/board.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:**
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
@@ -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.
Loading